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

Add Ability

@@ -583,12 +590,12 @@ def repository_detail(
-

Add Evidence

- - - +

Add Capability Support

+ + + - +
@@ -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

+ +

Evidence supporting this capability

+ """ ) @@ -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.