diff --git a/docs/characteristic-evidence-model.md b/docs/characteristic-evidence-model.md
new file mode 100644
index 0000000..0c1b25b
--- /dev/null
+++ b/docs/characteristic-evidence-model.md
@@ -0,0 +1,64 @@
+# Characteristic And Evidence Model
+
+The registry should treat a repository profile as a characteristic tree.
+
+## Characteristics
+
+A characteristic is an interpreted claim about a repository. The current concrete
+levels are:
+
+- Scope: the single root characteristic for the repository.
+- Ability: a high-level thing the repository is meant to enable.
+- Capability: a more specific capacity that contributes to an ability.
+- Feature: a concrete user-facing, operational, interface, or implementation
+ feature that contributes to a capability.
+
+The regular target shape is:
+
+```text
+Scope -> Ability -> Capability -> Feature -> Observed Fact
+```
+
+This regular tree is an orientation tool, not a claim that every real repository
+is perfectly tree-shaped. Cross references and same-level references can be
+useful during review, but they are also quality signals: frequent same-level
+feature references may indicate that features are too coarse, too fine, or
+organized under the wrong capability.
+
+## Facts, Source References, And Evidence
+
+Observed facts are deterministic scanner output. They describe what was seen in
+the repository: files, languages, frameworks, routes, tests, documentation,
+provider names, configuration variables, and similar source-linked observations.
+
+Source references point from interpreted claims back to files or facts.
+
+Evidence is support for a characteristic. It is not the same thing as an observed
+fact. Evidence may reference:
+
+- Observed facts.
+- Source files or content chunks.
+- Lower-level characteristics, such as a capability using features as evidence.
+
+Evidence should usually point downward in abstraction. An ability can use
+capabilities or features as support. A capability can use features or facts as
+support. A feature should usually use facts or source references as support, not
+abilities or capabilities.
+
+Same-level evidence references are allowed as review material, but should be
+treated as a possible organization smell.
+
+## Implementation Direction
+
+The current schema still stores evidence on capabilities, with textual
+references and source refs. The next additive schema step should generalize this
+without breaking existing data:
+
+- Add a scope root per repository.
+- Add typed evidence targets: supported characteristic kind/id.
+- Add typed evidence references: fact, source ref, content chunk, or
+ characteristic kind/id.
+- Keep legacy evidence fields until migration/export/search have been updated.
+
+The UI should make this relationship clear by presenting evidence as support
+under the characteristic it supports, not as a peer of features.
diff --git a/src/repo_registry/web_ui/views.py b/src/repo_registry/web_ui/views.py
index 034597b..e4c1898 100644
--- a/src/repo_registry/web_ui/views.py
+++ b/src/repo_registry/web_ui/views.py
@@ -537,8 +537,9 @@ def repository_detail(
{run_rows or '| No runs yet. |
'}
-
- Approved Ability Map
+
+
+
Approved Characteristics
{render_graph_counts(
asdict(ability_map),
facts_count=None,
@@ -548,13 +549,19 @@ def repository_detail(
),
)}
{render_approved_registry_actions(repository_id, asdict(ability_map))}
+
+
Latest Candidate Graph
{render_latest_candidate_counts(repository_id, latest_candidate, service)}
+
+
+
Approved Characteristic Tree
{render_ability_map(asdict(ability_map), repository_id)}
+
- Manual Registry Entry
+ Manual Characteristic Tuning
@@ -1850,7 +1857,7 @@ def render_latest_candidate_counts(
analysis_run_id, candidate_graph = latest_candidate
facts_count = len(service.list_observed_facts(repository_id, analysis_run_id))
return render_graph_counts(
- asdict(candidate_graph),
+ active_candidate_graph(asdict(candidate_graph)),
facts_count=facts_count,
label_prefix="candidate",
base_href=(
@@ -1865,6 +1872,39 @@ def render_latest_candidate_counts(
)
+def active_candidate_graph(graph: dict) -> dict:
+ active_abilities = []
+ for ability in graph.get("abilities", []):
+ active_capabilities = []
+ for capability in ability.get("capabilities", []):
+ active_features = [
+ feature
+ for feature in capability.get("features", [])
+ if feature.get("status", "candidate") != "rejected"
+ ]
+ active_evidence = [
+ evidence
+ for evidence in capability.get("evidence", [])
+ if evidence.get("status", "candidate") != "rejected"
+ ]
+ capability_active = (
+ capability.get("status", "candidate") != "rejected"
+ and (active_features or active_evidence)
+ )
+ if capability_active:
+ active_capability = {**capability}
+ active_capability["features"] = active_features
+ active_capability["evidence"] = active_evidence
+ active_capabilities.append(active_capability)
+ if ability.get("status", "candidate") != "rejected" and active_capabilities:
+ active_ability = {**ability}
+ active_ability["capabilities"] = active_capabilities
+ active_abilities.append(active_ability)
+ active_graph = {**graph}
+ active_graph["abilities"] = active_abilities
+ return active_graph
+
+
def render_graph_counts(
graph: dict,
facts_count: int | None = None,
@@ -2639,6 +2679,10 @@ def render_ability_map(ability_map: dict, repository_id: int) -> str:
abilities = ability_map.get("abilities", [])
if not abilities:
return 'No approved entries yet.
'
+ repository = ability_map["repository"]
+ scope_description = repository.get("description") or (
+ f"Scope root for the approved characteristics of {repository['name']}."
+ )
items = []
for ability in abilities:
capabilities = []
@@ -2659,7 +2703,10 @@ def render_ability_map(ability_map: dict, repository_id: int) -> str:
{capability['confidence']:.2f} {escape(capability['confidence_label'])}
{escape(capability['description'])}
{render_approved_capability_forms(capability, repository_id)}
-
+ Features
+ {features or '- No approved features.
'}
+ Evidence supporting this capability
+ {evidence or '- No support evidence yet.
'}
"""
)
@@ -2675,7 +2722,18 @@ def render_ability_map(ability_map: dict, repository_id: int) -> str:
"""
)
- return f''
+ return f"""
+
+ """
def render_approved_ability_forms(ability: dict, repository_id: int) -> str:
@@ -2739,18 +2797,18 @@ def render_approved_feature(feature: dict, repository_id: int) -> str:
def render_approved_evidence(evidence: dict, repository_id: int) -> str:
return f"""
- {escape(evidence["type"])}
+ {escape(evidence["type"])}
{escape(evidence["strength"])}
{escape(evidence["reference"])}
{render_sources(evidence.get("source_refs", []))}
"""
diff --git a/tests/test_web_api.py b/tests/test_web_api.py
index 7e2e7c5..0cf0e5a 100644
--- a/tests/test_web_api.py
+++ b/tests/test_web_api.py
@@ -1213,7 +1213,10 @@ def test_ui_register_analyze_and_approve_loop(tmp_path):
approved_detail = client.get(approve_response.headers["location"])
assert approved_detail.status_code == 200
- assert "Approved Ability Map" in approved_detail.text
+ assert "Approved Characteristics" in approved_detail.text
+ assert "Approved Characteristic Tree" in approved_detail.text
+ assert "scope" in approved_detail.text
+ assert "Evidence supporting this capability" in approved_detail.text
assert "1 abilities" in approved_detail.text
assert "2 capabilities" in approved_detail.text
assert "2 features" in approved_detail.text
@@ -1471,6 +1474,9 @@ def test_ui_element_listing_hides_rejected_candidates_by_default(tmp_path):
follow_redirects=False,
)
assert reject_response.status_code == 303
+ repository_detail = client.get(f"/ui/repos/{repository_id}")
+ assert repository_detail.status_code == 200
+ assert "0 candidate features" in repository_detail.text
default_listing = client.get(
f"/ui/repos/{repository_id}/elements",
@@ -1601,7 +1607,8 @@ def test_ui_manual_registry_entry_loop(tmp_path):
detail_response = client.get(repository_path)
assert detail_response.status_code == 200
- assert "Manual Registry Entry" in detail_response.text
+ assert "Manual Characteristic Tuning" in detail_response.text
+ assert "Add Capability Support" in detail_response.text
ability_response = client.post(
f"{repository_path}/abilities",
diff --git a/workplans/RREG-WP-0003-automatic-repository-exploration.md b/workplans/RREG-WP-0003-automatic-repository-exploration.md
index 4b1f637..44eb4b5 100644
--- a/workplans/RREG-WP-0003-automatic-repository-exploration.md
+++ b/workplans/RREG-WP-0003-automatic-repository-exploration.md
@@ -201,3 +201,28 @@ Follow-up hardening completed 2026-04-29: candidate graphs are normalized before
storage so duplicate or overlapping LLM/deterministic claims merge into one
review item while preserving stronger descriptions, confidence, source refs, and
nested capabilities/features/evidence.
+
+## P1: Characteristic Scope And Evidence Model
+
+```task
+id: RREG-WP-0003-T10
+status: in_progress
+priority: medium
+state_hub_task_id: "0d3fa9e0-bb3e-4bf2-bf8d-4681c5b7bdf5"
+```
+
+Evolve the registry model from a fixed `Ability → Capability → Feature →
+Evidence` presentation into a repository characteristic model with a single
+`Scope` root above abilities. Treat abilities, capabilities, and features as
+characteristics at different abstraction levels. Evidence should be modeled as
+support for a characteristic and may reference observed facts or lower-level
+characteristics; upward references from features to capabilities/abilities should
+be avoided, and same-layer references should be allowed but treated as a review
+smell that can indicate suboptimal granularity or organization.
+
+Acceptance: the UI explains and presents scope as the root of the approved
+characteristic tree, separates features from evidence/support under each
+capability, and allows manual tuning of evidence in a way that makes the support
+relationship clear. The data model has an additive path toward characteristic
+references so existing approved/candidate workflows continue to work while
+future iterations can link evidence to facts or deeper characteristics.