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>
|
<tbody>{run_rows or '<tr><td colspan="5" class="muted">No runs yet.</td></tr>'}</tbody>
|
||||||
</table>
|
</table>
|
||||||
</section>
|
</section>
|
||||||
<section class="panel">
|
<section class="stack">
|
||||||
<h2>Approved Ability Map</h2>
|
<div class="panel">
|
||||||
|
<h2>Approved Characteristics</h2>
|
||||||
{render_graph_counts(
|
{render_graph_counts(
|
||||||
asdict(ability_map),
|
asdict(ability_map),
|
||||||
facts_count=None,
|
facts_count=None,
|
||||||
@@ -548,13 +549,19 @@ def repository_detail(
|
|||||||
),
|
),
|
||||||
)}
|
)}
|
||||||
{render_approved_registry_actions(repository_id, asdict(ability_map))}
|
{render_approved_registry_actions(repository_id, asdict(ability_map))}
|
||||||
|
</div>
|
||||||
|
<div class="panel">
|
||||||
<h2>Latest Candidate Graph</h2>
|
<h2>Latest Candidate Graph</h2>
|
||||||
{render_latest_candidate_counts(repository_id, latest_candidate, service)}
|
{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)}
|
{render_ability_map(asdict(ability_map), repository_id)}
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
<section class="panel" style="margin-top:18px">
|
<section class="panel" style="margin-top:18px">
|
||||||
<h2>Manual Registry Entry</h2>
|
<h2>Manual Characteristic Tuning</h2>
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
<form class="stack" method="post" action="/ui/repos/{repository_id}/abilities">
|
<form class="stack" method="post" action="/ui/repos/{repository_id}/abilities">
|
||||||
<h3>Add Ability</h3>
|
<h3>Add Ability</h3>
|
||||||
@@ -583,12 +590,12 @@ def repository_detail(
|
|||||||
<button type="submit">Add Feature</button>
|
<button type="submit">Add Feature</button>
|
||||||
</form>
|
</form>
|
||||||
<form class="stack" method="post" action="/ui/repos/{repository_id}/evidence">
|
<form class="stack" method="post" action="/ui/repos/{repository_id}/evidence">
|
||||||
<h3>Add Evidence</h3>
|
<h3>Add Capability Support</h3>
|
||||||
<label>Capability ID <input name="capability_id" type="number" min="1" required></label>
|
<label>Supported capability ID <input name="capability_id" type="number" min="1" required></label>
|
||||||
<label>Type <input name="type" required></label>
|
<label>Support type <input name="type" placeholder="fact, documentation, test, example, feature" required></label>
|
||||||
<label>Reference <input name="reference" 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>
|
<label>Strength <input name="strength" value="medium" required></label>
|
||||||
<button type="submit">Add Evidence</button>
|
<button type="submit">Add Support</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -1850,7 +1857,7 @@ def render_latest_candidate_counts(
|
|||||||
analysis_run_id, candidate_graph = latest_candidate
|
analysis_run_id, candidate_graph = latest_candidate
|
||||||
facts_count = len(service.list_observed_facts(repository_id, analysis_run_id))
|
facts_count = len(service.list_observed_facts(repository_id, analysis_run_id))
|
||||||
return render_graph_counts(
|
return render_graph_counts(
|
||||||
asdict(candidate_graph),
|
active_candidate_graph(asdict(candidate_graph)),
|
||||||
facts_count=facts_count,
|
facts_count=facts_count,
|
||||||
label_prefix="candidate",
|
label_prefix="candidate",
|
||||||
base_href=(
|
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(
|
def render_graph_counts(
|
||||||
graph: dict,
|
graph: dict,
|
||||||
facts_count: int | None = None,
|
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", [])
|
abilities = ability_map.get("abilities", [])
|
||||||
if not abilities:
|
if not abilities:
|
||||||
return '<p class="muted">No approved entries yet.</p>'
|
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 = []
|
items = []
|
||||||
for ability in abilities:
|
for ability in abilities:
|
||||||
capabilities = []
|
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>
|
<span class="pill">{capability['confidence']:.2f} {escape(capability['confidence_label'])}</span>
|
||||||
<p class="muted">{escape(capability['description'])}</p>
|
<p class="muted">{escape(capability['description'])}</p>
|
||||||
{render_approved_capability_forms(capability, repository_id)}
|
{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>
|
</li>
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
@@ -2675,7 +2722,18 @@ def render_ability_map(ability_map: dict, repository_id: int) -> str:
|
|||||||
</li>
|
</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:
|
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:
|
def render_approved_evidence(evidence: dict, repository_id: int) -> str:
|
||||||
return f"""
|
return f"""
|
||||||
<li>
|
<li>
|
||||||
{escape(evidence["type"])}
|
<strong>{escape(evidence["type"])}</strong>
|
||||||
<span class="pill">{escape(evidence["strength"])}</span>
|
<span class="pill">{escape(evidence["strength"])}</span>
|
||||||
<span class="source">{escape(evidence["reference"])}</span>
|
<span class="source">{escape(evidence["reference"])}</span>
|
||||||
{render_sources(evidence.get("source_refs", []))}
|
{render_sources(evidence.get("source_refs", []))}
|
||||||
<form class="stack" method="post" action="/ui/repos/{repository_id}/evidence/{evidence['id']}/edit">
|
<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>Reference <input name="reference" value="{escape(evidence['reference'])}" required></label>
|
||||||
<label>Strength <input name="strength" value="{escape(evidence['strength'])}" 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>
|
||||||
<form method="post" action="/ui/repos/{repository_id}/evidence/{evidence['id']}/delete">
|
<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>
|
</form>
|
||||||
</li>
|
</li>
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1213,7 +1213,10 @@ def test_ui_register_analyze_and_approve_loop(tmp_path):
|
|||||||
|
|
||||||
approved_detail = client.get(approve_response.headers["location"])
|
approved_detail = client.get(approve_response.headers["location"])
|
||||||
assert approved_detail.status_code == 200
|
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 "1 abilities" in approved_detail.text
|
||||||
assert "2 capabilities" in approved_detail.text
|
assert "2 capabilities" in approved_detail.text
|
||||||
assert "2 features" 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,
|
follow_redirects=False,
|
||||||
)
|
)
|
||||||
assert reject_response.status_code == 303
|
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(
|
default_listing = client.get(
|
||||||
f"/ui/repos/{repository_id}/elements",
|
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)
|
detail_response = client.get(repository_path)
|
||||||
assert detail_response.status_code == 200
|
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(
|
ability_response = client.post(
|
||||||
f"{repository_path}/abilities",
|
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
|
storage so duplicate or overlapping LLM/deterministic claims merge into one
|
||||||
review item while preserving stronger descriptions, confidence, source refs, and
|
review item while preserving stronger descriptions, confidence, source refs, and
|
||||||
nested capabilities/features/evidence.
|
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