Towards systematic evidence

This commit is contained in:
2026-04-29 14:41:39 +02:00
parent 93640976b0
commit 6c0a7db5e4
4 changed files with 171 additions and 17 deletions

View File

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

View File

@@ -537,8 +537,9 @@ def repository_detail(
<tbody>{run_rows or '<tr><td colspan="5" class="muted">No runs yet.</td></tr>'}</tbody>
</table>
</section>
<section class="panel">
<h2>Approved Ability Map</h2>
<section class="stack">
<div class="panel">
<h2>Approved Characteristics</h2>
{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))}
</div>
<div class="panel">
<h2>Latest Candidate Graph</h2>
{render_latest_candidate_counts(repository_id, latest_candidate, service)}
</div>
<div class="panel">
<h2>Approved Characteristic Tree</h2>
{render_ability_map(asdict(ability_map), repository_id)}
</div>
</section>
</div>
<section class="panel" style="margin-top:18px">
<h2>Manual Registry Entry</h2>
<h2>Manual Characteristic Tuning</h2>
<div class="grid">
<form class="stack" method="post" action="/ui/repos/{repository_id}/abilities">
<h3>Add Ability</h3>
@@ -583,12 +590,12 @@ def repository_detail(
<button type="submit">Add Feature</button>
</form>
<form class="stack" method="post" action="/ui/repos/{repository_id}/evidence">
<h3>Add Evidence</h3>
<label>Capability ID <input name="capability_id" type="number" min="1" required></label>
<label>Type <input name="type" required></label>
<label>Reference <input name="reference" required></label>
<h3>Add Capability Support</h3>
<label>Supported capability ID <input name="capability_id" type="number" min="1" required></label>
<label>Support type <input name="type" placeholder="fact, documentation, test, example, feature" required></label>
<label>Reference <input name="reference" placeholder="Observed fact, file, or lower-level characteristic" required></label>
<label>Strength <input name="strength" value="medium" required></label>
<button type="submit">Add Evidence</button>
<button type="submit">Add Support</button>
</form>
</div>
</section>
@@ -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 '<p class="muted">No approved entries yet.</p>'
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:
<span class="pill">{capability['confidence']:.2f} {escape(capability['confidence_label'])}</span>
<p class="muted">{escape(capability['description'])}</p>
{render_approved_capability_forms(capability, repository_id)}
<ul>{features}{evidence}</ul>
<h3>Features</h3>
<ul>{features or '<li class="muted">No approved features.</li>'}</ul>
<h3>Evidence supporting this capability</h3>
<ul>{evidence or '<li class="muted">No support evidence yet.</li>'}</ul>
</li>
"""
)
@@ -2675,7 +2722,18 @@ def render_ability_map(ability_map: dict, repository_id: int) -> str:
</li>
"""
)
return f'<div class="tree"><ul>{"".join(items)}</ul></div>'
return f"""
<div class="tree">
<ul>
<li id="scope-{repository['id']}">
<strong>{escape(repository['name'])}</strong>
<span class="pill">scope</span>
<p class="muted">{escape(scope_description)}</p>
<ul>{"".join(items)}</ul>
</li>
</ul>
</div>
"""
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"""
<li>
{escape(evidence["type"])}
<strong>{escape(evidence["type"])}</strong>
<span class="pill">{escape(evidence["strength"])}</span>
<span class="source">{escape(evidence["reference"])}</span>
{render_sources(evidence.get("source_refs", []))}
<form class="stack" method="post" action="/ui/repos/{repository_id}/evidence/{evidence['id']}/edit">
<label>Type <input name="type" value="{escape(evidence['type'])}" required></label>
<label>Support type <input name="type" value="{escape(evidence['type'])}" required></label>
<label>Reference <input name="reference" value="{escape(evidence['reference'])}" required></label>
<label>Strength <input name="strength" value="{escape(evidence['strength'])}" required></label>
<button class="secondary" type="submit">Save Evidence</button>
<button class="secondary" type="submit">Save Support</button>
</form>
<form method="post" action="/ui/repos/{repository_id}/evidence/{evidence['id']}/delete">
<button class="secondary" type="submit">Delete Evidence</button>
<button class="secondary" type="submit">Delete Support</button>
</form>
</li>
"""

View File

@@ -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",

View File

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