generated from coulomb/repo-seed
Evidence with supportive metadata
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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"],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user