Evidence with supportive metadata

This commit is contained in:
2026-04-29 15:52:37 +02:00
parent 6c0a7db5e4
commit eb1513e463
10 changed files with 194 additions and 9 deletions

View File

@@ -102,8 +102,12 @@ CREATE TABLE IF NOT EXISTS candidate_evidence (
repository_id INTEGER NOT NULL REFERENCES repositories(id) ON DELETE CASCADE, repository_id INTEGER NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,
analysis_run_id INTEGER NOT NULL REFERENCES analysis_runs(id) ON DELETE CASCADE, analysis_run_id INTEGER NOT NULL REFERENCES analysis_runs(id) ON DELETE CASCADE,
capability_id INTEGER NOT NULL REFERENCES candidate_capabilities(id) ON DELETE CASCADE, capability_id INTEGER NOT NULL REFERENCES candidate_capabilities(id) ON DELETE CASCADE,
target_kind TEXT NOT NULL DEFAULT 'capability',
target_id INTEGER,
type TEXT NOT NULL, type TEXT NOT NULL,
reference TEXT NOT NULL, reference TEXT NOT NULL,
reference_kind TEXT NOT NULL DEFAULT 'source',
reference_id INTEGER,
strength TEXT NOT NULL DEFAULT 'medium', strength TEXT NOT NULL DEFAULT 'medium',
status TEXT NOT NULL DEFAULT 'candidate', status TEXT NOT NULL DEFAULT 'candidate',
source_refs TEXT NOT NULL DEFAULT '[]', source_refs TEXT NOT NULL DEFAULT '[]',
@@ -156,8 +160,12 @@ CREATE TABLE IF NOT EXISTS approved_evidence (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
repository_id INTEGER NOT NULL REFERENCES repositories(id) ON DELETE CASCADE, repository_id INTEGER NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,
capability_id INTEGER NOT NULL REFERENCES approved_capabilities(id) ON DELETE CASCADE, capability_id INTEGER NOT NULL REFERENCES approved_capabilities(id) ON DELETE CASCADE,
target_kind TEXT NOT NULL DEFAULT 'capability',
target_id INTEGER,
type TEXT NOT NULL, type TEXT NOT NULL,
reference TEXT NOT NULL, reference TEXT NOT NULL,
reference_kind TEXT NOT NULL DEFAULT 'source',
reference_id INTEGER,
strength TEXT NOT NULL DEFAULT 'medium', strength TEXT NOT NULL DEFAULT 'medium',
source_refs TEXT NOT NULL DEFAULT '[]', source_refs TEXT NOT NULL DEFAULT '[]',
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP

View File

@@ -145,6 +145,10 @@ class CandidateEvidence:
strength: str strength: str
status: str status: str
source_refs: list[SourceReference] source_refs: list[SourceReference]
target_kind: str = "capability"
target_id: int | None = None
reference_kind: str = "source"
reference_id: int | None = None
@dataclass(frozen=True) @dataclass(frozen=True)
@@ -200,6 +204,10 @@ class Evidence:
reference: str reference: str
strength: str strength: str
source_refs: list[SourceReference] = field(default_factory=list) source_refs: list[SourceReference] = field(default_factory=list)
target_kind: str = "capability"
target_id: int | None = None
reference_kind: str = "source"
reference_id: int | None = None
@dataclass(frozen=True) @dataclass(frozen=True)

View File

@@ -1162,6 +1162,10 @@ class RegistryService:
type: str, type: str,
reference: str, reference: str,
strength: str = "medium", strength: str = "medium",
target_kind: str = "capability",
target_id: int | None = None,
reference_kind: str = "source",
reference_id: int | None = None,
) -> int: ) -> int:
self.store.ensure_capability(repository_id, capability_id) self.store.ensure_capability(repository_id, capability_id)
return self.store.create_evidence( return self.store.create_evidence(
@@ -1170,6 +1174,10 @@ class RegistryService:
type=type, type=type,
reference=reference, reference=reference,
strength=strength, strength=strength,
target_kind=target_kind,
target_id=target_id,
reference_kind=reference_kind,
reference_id=reference_id,
) )
def update_evidence( def update_evidence(
@@ -1180,6 +1188,10 @@ class RegistryService:
type: str | None = None, type: str | None = None,
reference: str | None = None, reference: str | None = None,
strength: str | None = None, strength: str | None = None,
target_kind: str | None = None,
target_id: int | None = None,
reference_kind: str | None = None,
reference_id: int | None = None,
) -> RepositoryAbilityMap: ) -> RepositoryAbilityMap:
self.store.update_evidence( self.store.update_evidence(
repository_id, repository_id,
@@ -1187,6 +1199,10 @@ class RegistryService:
type=type, type=type,
reference=reference, reference=reference,
strength=strength, strength=strength,
target_kind=target_kind,
target_id=target_id,
reference_kind=reference_kind,
reference_id=reference_id,
) )
return self.store.get_ability_map(repository_id) return self.store.get_ability_map(repository_id)

View File

@@ -48,6 +48,7 @@ class RegistryStore:
connection.executescript(migration_path.read_text(encoding="utf-8")) connection.executescript(migration_path.read_text(encoding="utf-8"))
self._ensure_content_chunks_table(connection) self._ensure_content_chunks_table(connection)
self._ensure_approved_source_ref_columns(connection) self._ensure_approved_source_ref_columns(connection)
self._ensure_evidence_relationship_columns(connection)
self._ensure_expectation_gaps_table(connection) self._ensure_expectation_gaps_table(connection)
def connect(self) -> sqlite3.Connection: def connect(self) -> sqlite3.Connection:
@@ -70,6 +71,41 @@ class RegistryStore:
f"ALTER TABLE {table} ADD COLUMN source_refs TEXT NOT NULL DEFAULT '[]'" f"ALTER TABLE {table} ADD COLUMN source_refs TEXT NOT NULL DEFAULT '[]'"
) )
def _ensure_evidence_relationship_columns(
self,
connection: sqlite3.Connection,
) -> None:
for table in ("candidate_evidence", "approved_evidence"):
columns = {
row["name"]
for row in connection.execute(f"PRAGMA table_info({table})").fetchall()
}
if "target_kind" not in columns:
connection.execute(
f"ALTER TABLE {table} ADD COLUMN target_kind TEXT NOT NULL DEFAULT 'capability'"
)
if "target_id" not in columns:
connection.execute(f"ALTER TABLE {table} ADD COLUMN target_id INTEGER")
if "reference_kind" not in columns:
connection.execute(
f"ALTER TABLE {table} ADD COLUMN reference_kind TEXT NOT NULL DEFAULT 'source'"
)
if "reference_id" not in columns:
connection.execute(
f"ALTER TABLE {table} ADD COLUMN reference_id INTEGER"
)
connection.execute(
f"""
UPDATE {table}
SET target_kind = COALESCE(NULLIF(target_kind, ''), 'capability'),
target_id = COALESCE(target_id, capability_id),
reference_kind = COALESCE(NULLIF(reference_kind, ''), 'source')
WHERE target_id IS NULL
OR target_kind = ''
OR reference_kind = ''
"""
)
def _ensure_content_chunks_table(self, connection: sqlite3.Connection) -> None: def _ensure_content_chunks_table(self, connection: sqlite3.Connection) -> None:
connection.execute( connection.execute(
""" """
@@ -355,16 +391,21 @@ class RegistryStore:
connection.execute( connection.execute(
""" """
INSERT INTO candidate_evidence INSERT INTO candidate_evidence
(repository_id, analysis_run_id, capability_id, type, (repository_id, analysis_run_id, capability_id,
reference, strength, source_refs) target_kind, target_id, type, reference,
VALUES (?, ?, ?, ?, ?, ?, ?) reference_kind, reference_id, strength, source_refs)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", """,
( (
repository_id, repository_id,
analysis_run_id, analysis_run_id,
capability_id, capability_id,
"capability",
capability_id,
evidence.type, evidence.type,
evidence.reference, evidence.reference,
"source",
None,
evidence.strength, evidence.strength,
self._source_refs_to_json(evidence.source_refs), self._source_refs_to_json(evidence.source_refs),
), ),
@@ -409,7 +450,8 @@ class RegistryStore:
).fetchall() ).fetchall()
evidence_rows = connection.execute( evidence_rows = connection.execute(
""" """
SELECT id, capability_id, type, reference, strength, status, source_refs SELECT id, capability_id, target_kind, target_id, type, reference,
reference_kind, reference_id, strength, status, source_refs
FROM candidate_evidence FROM candidate_evidence
WHERE repository_id = ? AND analysis_run_id = ? WHERE repository_id = ? AND analysis_run_id = ?
ORDER BY id ORDER BY id
@@ -442,6 +484,10 @@ class RegistryStore:
strength=row["strength"], strength=row["strength"],
status=row["status"], status=row["status"],
source_refs=self._source_refs_from_json(row["source_refs"]), source_refs=self._source_refs_from_json(row["source_refs"]),
target_kind=row["target_kind"],
target_id=row["target_id"],
reference_kind=row["reference_kind"],
reference_id=row["reference_id"],
) )
) )
@@ -1709,20 +1755,30 @@ class RegistryStore:
type: str, type: str,
reference: str, reference: str,
strength: str, strength: str,
target_kind: str = "capability",
target_id: int | None = None,
reference_kind: str = "source",
reference_id: int | None = None,
source_refs: list[SourceReference] | None = None, source_refs: list[SourceReference] | None = None,
) -> int: ) -> int:
target_id = capability_id if target_id is None else target_id
with self.connect() as connection: with self.connect() as connection:
cursor = connection.execute( cursor = connection.execute(
""" """
INSERT INTO approved_evidence INSERT INTO approved_evidence
(repository_id, capability_id, type, reference, strength, source_refs) (repository_id, capability_id, target_kind, target_id, type,
VALUES (?, ?, ?, ?, ?, ?) reference, reference_kind, reference_id, strength, source_refs)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", """,
( (
repository_id, repository_id,
capability_id, capability_id,
target_kind,
target_id,
type, type,
reference, reference,
reference_kind,
reference_id,
strength, strength,
self._source_refs_to_json(source_refs or []), self._source_refs_to_json(source_refs or []),
), ),
@@ -1737,6 +1793,10 @@ class RegistryStore:
type: str | None = None, type: str | None = None,
reference: str | None = None, reference: str | None = None,
strength: str | None = None, strength: str | None = None,
target_kind: str | None = None,
target_id: int | None = None,
reference_kind: str | None = None,
reference_id: int | None = None,
) -> None: ) -> None:
self._update_approved_row( self._update_approved_row(
table="approved_evidence", table="approved_evidence",
@@ -1747,6 +1807,10 @@ class RegistryStore:
"type": type, "type": type,
"reference": reference, "reference": reference,
"strength": strength, "strength": strength,
"target_kind": target_kind,
"target_id": target_id,
"reference_kind": reference_kind,
"reference_id": reference_id,
}, },
) )
@@ -1837,15 +1901,20 @@ class RegistryStore:
connection.execute( connection.execute(
""" """
INSERT INTO approved_evidence INSERT INTO approved_evidence
(repository_id, capability_id, type, reference, strength, (repository_id, capability_id, target_kind, target_id,
type, reference, reference_kind, reference_id, strength,
source_refs) source_refs)
VALUES (?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", """,
( (
repository_id, repository_id,
approved_capability_id, approved_capability_id,
evidence.target_kind,
evidence.target_id or approved_capability_id,
evidence.type, evidence.type,
evidence.reference, evidence.reference,
evidence.reference_kind,
evidence.reference_id,
evidence.strength, evidence.strength,
self._source_refs_to_json(evidence.source_refs), self._source_refs_to_json(evidence.source_refs),
), ),
@@ -1883,7 +1952,8 @@ class RegistryStore:
).fetchall() ).fetchall()
evidence_rows = connection.execute( evidence_rows = connection.execute(
""" """
SELECT id, capability_id, type, reference, strength, source_refs SELECT id, capability_id, target_kind, target_id, type, reference,
reference_kind, reference_id, strength, source_refs
FROM approved_evidence FROM approved_evidence
WHERE repository_id = ? WHERE repository_id = ?
ORDER BY id ORDER BY id
@@ -1914,6 +1984,10 @@ class RegistryStore:
reference=row["reference"], reference=row["reference"],
strength=row["strength"], strength=row["strength"],
source_refs=self._source_refs_from_json(row["source_refs"]), source_refs=self._source_refs_from_json(row["source_refs"]),
target_kind=row["target_kind"],
target_id=row["target_id"],
reference_kind=row["reference_kind"],
reference_id=row["reference_id"],
) )
) )

View File

@@ -179,6 +179,10 @@ class EvidenceCreate(BaseModel):
type: str type: str
reference: str reference: str
strength: str = "medium" strength: str = "medium"
target_kind: str = "capability"
target_id: int | None = None
reference_kind: str = "source"
reference_id: int | None = None
model_config = { model_config = {
"json_schema_extra": { "json_schema_extra": {
@@ -188,6 +192,8 @@ class EvidenceCreate(BaseModel):
"type": "unit_test", "type": "unit_test",
"reference": "tests/test_email_classification.py", "reference": "tests/test_email_classification.py",
"strength": "strong", "strength": "strong",
"target_kind": "capability",
"reference_kind": "source",
} }
] ]
} }
@@ -198,6 +204,10 @@ class EvidenceUpdate(BaseModel):
type: str | None = None type: str | None = None
reference: str | None = None reference: str | None = None
strength: str | None = None strength: str | None = None
target_kind: str | None = None
target_id: int | None = None
reference_kind: str | None = None
reference_id: int | None = None
class AnalysisRunCreate(BaseModel): class AnalysisRunCreate(BaseModel):
@@ -475,6 +485,10 @@ class CandidateEvidenceResponse(BaseModel):
strength: str strength: str
status: str status: str
source_refs: list[SourceReferenceResponse] source_refs: list[SourceReferenceResponse]
target_kind: str = "capability"
target_id: int | None = None
reference_kind: str = "source"
reference_id: int | None = None
class CandidateFeatureResponse(BaseModel): class CandidateFeatureResponse(BaseModel):
@@ -665,6 +679,10 @@ class EvidenceResponse(BaseModel):
reference: str reference: str
strength: str strength: str
source_refs: list[SourceReferenceResponse] source_refs: list[SourceReferenceResponse]
target_kind: str = "capability"
target_id: int | None = None
reference_kind: str = "source"
reference_id: int | None = None
class FeatureResponse(BaseModel): class FeatureResponse(BaseModel):

View File

@@ -592,8 +592,12 @@ def repository_detail(
<form class="stack" method="post" action="/ui/repos/{repository_id}/evidence"> <form class="stack" method="post" action="/ui/repos/{repository_id}/evidence">
<h3>Add Capability Support</h3> <h3>Add Capability Support</h3>
<label>Supported 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>Supported characteristic kind <input name="target_kind" value="capability" required></label>
<label>Supported characteristic ID <input name="target_id" type="number" min="1" placeholder="Defaults to supported capability ID"></label>
<label>Support type <input name="type" placeholder="fact, documentation, test, example, feature" 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>Reference <input name="reference" placeholder="Observed fact, file, or lower-level characteristic" required></label>
<label>Reference kind <input name="reference_kind" value="source" placeholder="source, fact, feature, capability"></label>
<label>Reference ID <input name="reference_id" type="number" min="1" placeholder="Optional fact or characteristic ID"></label>
<label>Strength <input name="strength" value="medium" required></label> <label>Strength <input name="strength" value="medium" required></label>
<button type="submit">Add Support</button> <button type="submit">Add Support</button>
</form> </form>
@@ -724,6 +728,10 @@ def create_evidence_from_form(
type: str = Form(...), type: str = Form(...),
reference: str = Form(...), reference: str = Form(...),
strength: str = Form("medium"), strength: str = Form("medium"),
target_kind: str = Form("capability"),
target_id: int | None = Form(default=None),
reference_kind: str = Form("source"),
reference_id: int | None = Form(default=None),
service: RegistryService = Depends(get_service), service: RegistryService = Depends(get_service),
) -> RedirectResponse: ) -> RedirectResponse:
service.add_evidence( service.add_evidence(
@@ -732,6 +740,10 @@ def create_evidence_from_form(
type=type, type=type,
reference=reference, reference=reference,
strength=strength, strength=strength,
target_kind=target_kind,
target_id=target_id,
reference_kind=reference_kind,
reference_id=reference_id,
) )
return RedirectResponse(f"/ui/repos/{repository_id}", status_code=303) return RedirectResponse(f"/ui/repos/{repository_id}", status_code=303)
@@ -836,6 +848,10 @@ def edit_evidence_from_form(
type: str = Form(...), type: str = Form(...),
reference: str = Form(...), reference: str = Form(...),
strength: str = Form("medium"), strength: str = Form("medium"),
target_kind: str = Form("capability"),
target_id: int | None = Form(default=None),
reference_kind: str = Form("source"),
reference_id: int | None = Form(default=None),
service: RegistryService = Depends(get_service), service: RegistryService = Depends(get_service),
) -> RedirectResponse: ) -> RedirectResponse:
service.update_evidence( service.update_evidence(
@@ -844,6 +860,10 @@ def edit_evidence_from_form(
type=type, type=type,
reference=reference, reference=reference,
strength=strength, strength=strength,
target_kind=target_kind,
target_id=target_id,
reference_kind=reference_kind,
reference_id=reference_id,
) )
return RedirectResponse(f"/ui/repos/{repository_id}", status_code=303) return RedirectResponse(f"/ui/repos/{repository_id}", status_code=303)
@@ -2795,15 +2815,25 @@ 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:
target_kind = escape(str(evidence.get("target_kind") or "capability"))
target_id = evidence.get("target_id")
reference_kind = escape(str(evidence.get("reference_kind") or "source"))
reference_id = evidence.get("reference_id")
return f""" return f"""
<li> <li>
<strong>{escape(evidence["type"])}</strong> <strong>{escape(evidence["type"])}</strong>
<span class="pill">{escape(evidence["strength"])}</span> <span class="pill">{escape(evidence["strength"])}</span>
<span class="pill">supports {target_kind}{f' #{target_id}' if target_id else ''}</span>
<span class="pill">references {reference_kind}{f' #{reference_id}' if reference_id else ''}</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>Supported characteristic kind <input name="target_kind" value="{target_kind}" required></label>
<label>Supported characteristic ID <input name="target_id" type="number" min="1" value="{target_id or ''}"></label>
<label>Support 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>Reference kind <input name="reference_kind" value="{reference_kind}" required></label>
<label>Reference ID <input name="reference_id" type="number" min="1" value="{reference_id or ''}"></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 Support</button> <button class="secondary" type="submit">Save Support</button>
</form> </form>

View File

@@ -75,6 +75,8 @@ def test_manual_registry_builds_ability_map(tmp_path):
type="unit_test", type="unit_test",
reference="tests/test_email_classification.py", reference="tests/test_email_classification.py",
strength="strong", strength="strong",
reference_kind="fact",
reference_id=42,
) )
ability_map = service.ability_map(repository.id) ability_map = service.ability_map(repository.id)
@@ -86,6 +88,10 @@ def test_manual_registry_builds_ability_map(tmp_path):
assert capability.inputs == ["subject", "body"] assert capability.inputs == ["subject", "body"]
assert capability.features[0].location == "src/routes/classify_email.py" assert capability.features[0].location == "src/routes/classify_email.py"
assert capability.evidence[0].strength == "strong" assert capability.evidence[0].strength == "strong"
assert capability.evidence[0].target_kind == "capability"
assert capability.evidence[0].target_id == capability_id
assert capability.evidence[0].reference_kind == "fact"
assert capability.evidence[0].reference_id == 42
def test_manual_registry_updates_and_deletes_approved_entries(tmp_path): def test_manual_registry_updates_and_deletes_approved_entries(tmp_path):
@@ -127,6 +133,8 @@ def test_manual_registry_updates_and_deletes_approved_entries(tmp_path):
repository.id, repository.id,
evidence_id, evidence_id,
strength="strong", strength="strong",
reference_kind="feature",
reference_id=feature_id,
) )
ability = ability_map.abilities[0] ability = ability_map.abilities[0]
@@ -137,6 +145,8 @@ def test_manual_registry_updates_and_deletes_approved_entries(tmp_path):
assert capability.outputs == ["response"] assert capability.outputs == ["response"]
assert capability.features[0].location == "src/api.py" assert capability.features[0].location == "src/api.py"
assert capability.evidence[0].strength == "strong" assert capability.evidence[0].strength == "strong"
assert capability.evidence[0].reference_kind == "feature"
assert capability.evidence[0].reference_id == feature_id
service.delete_feature(repository.id, feature_id) service.delete_feature(repository.id, feature_id)
service.delete_evidence(repository.id, evidence_id) service.delete_evidence(repository.id, evidence_id)

View File

@@ -34,6 +34,12 @@ def test_initialize_is_idempotent_and_applies_expected_columns(tmp_path):
assert "source_refs" in feature_columns assert "source_refs" in feature_columns
assert "source_refs" in evidence_columns assert "source_refs" in evidence_columns
assert {
"target_kind",
"target_id",
"reference_kind",
"reference_id",
} <= evidence_columns
assert "content_chunks" in tables assert "content_chunks" in tables
assert "expectation_gaps" in tables assert "expectation_gaps" in tables

View File

@@ -1609,6 +1609,8 @@ def test_ui_manual_registry_entry_loop(tmp_path):
assert detail_response.status_code == 200 assert detail_response.status_code == 200
assert "Manual Characteristic Tuning" in detail_response.text assert "Manual Characteristic Tuning" in detail_response.text
assert "Add Capability Support" in detail_response.text assert "Add Capability Support" in detail_response.text
assert "Supported characteristic kind" in detail_response.text
assert "Reference kind" in detail_response.text
ability_response = client.post( ability_response = client.post(
f"{repository_path}/abilities", f"{repository_path}/abilities",
@@ -1660,6 +1662,7 @@ def test_ui_manual_registry_entry_loop(tmp_path):
"capability_id": str(capability_id), "capability_id": str(capability_id),
"type": "documentation", "type": "documentation",
"reference": "README.md", "reference": "README.md",
"reference_kind": "source",
"strength": "medium", "strength": "medium",
}, },
follow_redirects=False, follow_redirects=False,
@@ -1671,6 +1674,8 @@ def test_ui_manual_registry_entry_loop(tmp_path):
assert "Manual Capability" in detail_response.text assert "Manual Capability" in detail_response.text
assert "Manual API" in detail_response.text assert "Manual API" in detail_response.text
assert "README.md" in detail_response.text assert "README.md" in detail_response.text
assert "supports capability" in detail_response.text
assert "references source" in detail_response.text
assert "ID " in detail_response.text assert "ID " in detail_response.text
assert "Save Ability" in detail_response.text assert "Save Ability" in detail_response.text
@@ -1719,6 +1724,10 @@ def test_ui_manual_registry_entry_loop(tmp_path):
data={ data={
"type": "test", "type": "test",
"reference": "tests/test_manual.py", "reference": "tests/test_manual.py",
"target_kind": "capability",
"target_id": str(capability_id),
"reference_kind": "feature",
"reference_id": str(feature_id),
"strength": "strong", "strength": "strong",
}, },
follow_redirects=False, follow_redirects=False,
@@ -1730,6 +1739,7 @@ def test_ui_manual_registry_entry_loop(tmp_path):
assert "Edited Manual Capability" in detail_response.text assert "Edited Manual Capability" in detail_response.text
assert "Edited Manual API" in detail_response.text assert "Edited Manual API" in detail_response.text
assert "tests/test_manual.py" in detail_response.text assert "tests/test_manual.py" in detail_response.text
assert f"references feature #{feature_id}" in detail_response.text
delete_feature_response = client.post( delete_feature_response = client.post(
f"{repository_path}/features/{feature_id}/delete", f"{repository_path}/features/{feature_id}/delete",

View File

@@ -226,3 +226,8 @@ 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 relationship clear. The data model has an additive path toward characteristic
references so existing approved/candidate workflows continue to work while references so existing approved/candidate workflows continue to work while
future iterations can link evidence to facts or deeper characteristics. future iterations can link evidence to facts or deeper characteristics.
Implementation note 2026-04-29: approved and candidate evidence now carry
additive support metadata: `target_kind`, `target_id`, `reference_kind`, and
`reference_id`. Existing capability-bound evidence remains compatible, while the
UI exposes these fields as supported-characteristic and reference metadata.