generated from coulomb/repo-seed
Towards systematic evidence
This commit is contained in:
64
docs/characteristic-evidence-model.md
Normal file
64
docs/characteristic-evidence-model.md
Normal 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.
|
||||
@@ -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>
|
||||
"""
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user