profile drill-down/source-link preservation

This commit is contained in:
2026-04-26 00:13:45 +02:00
parent e8fdfe6e17
commit 5aa76af78c
8 changed files with 88 additions and 11 deletions

View File

@@ -135,6 +135,7 @@ CREATE TABLE IF NOT EXISTS approved_features (
type TEXT NOT NULL,
location TEXT NOT NULL DEFAULT '',
confidence REAL NOT NULL DEFAULT 1.0,
source_refs TEXT NOT NULL DEFAULT '[]',
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
@@ -145,6 +146,7 @@ CREATE TABLE IF NOT EXISTS approved_evidence (
type TEXT NOT NULL,
reference TEXT NOT NULL,
strength TEXT NOT NULL DEFAULT 'medium',
source_refs TEXT NOT NULL DEFAULT '[]',
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);

View File

@@ -189,6 +189,7 @@ class CandidateGraphGenerator:
path=fact.path,
kind=fact.kind,
name=fact.name,
line=fact.metadata.get("line"),
)
for fact in facts
]

View File

@@ -62,6 +62,7 @@ class SourceReference:
path: str
kind: str
name: str
line: int | None = None
@dataclass(frozen=True)
@@ -123,6 +124,7 @@ class Evidence:
type: str
reference: str
strength: str
source_refs: list[SourceReference] = field(default_factory=list)
@dataclass(frozen=True)
@@ -132,6 +134,7 @@ class Feature:
type: str
location: str
confidence: float
source_refs: list[SourceReference] = field(default_factory=list)
@dataclass(frozen=True)

View File

@@ -162,6 +162,7 @@ class RegistryService:
type=feature.type,
location=feature.location,
confidence=feature.confidence,
source_refs=feature.source_refs,
)
for evidence in capability.evidence:
if evidence.status != "candidate":
@@ -172,6 +173,7 @@ class RegistryService:
type=evidence.type,
reference=evidence.reference,
strength=evidence.strength,
source_refs=evidence.source_refs,
)
if pending_abilities:

View File

@@ -40,6 +40,7 @@ class RegistryStore:
migration_path = Path(__file__).parents[3] / "migrations" / "0001_initial.sql"
with self.connect() as connection:
connection.executescript(migration_path.read_text(encoding="utf-8"))
self._ensure_approved_source_ref_columns(connection)
def connect(self) -> sqlite3.Connection:
connection = sqlite3.connect(self.database_path)
@@ -47,6 +48,20 @@ class RegistryStore:
connection.execute("PRAGMA foreign_keys = ON")
return connection
def _ensure_approved_source_ref_columns(
self,
connection: sqlite3.Connection,
) -> None:
for table in ("approved_features", "approved_evidence"):
columns = {
row["name"]
for row in connection.execute(f"PRAGMA table_info({table})").fetchall()
}
if "source_refs" not in columns:
connection.execute(
f"ALTER TABLE {table} ADD COLUMN source_refs TEXT NOT NULL DEFAULT '[]'"
)
def create_repository(
self,
*,
@@ -1153,15 +1168,24 @@ class RegistryStore:
type: str,
location: str,
confidence: float,
source_refs: list[SourceReference] | None = None,
) -> int:
with self.connect() as connection:
cursor = connection.execute(
"""
INSERT INTO approved_features
(repository_id, capability_id, name, type, location, confidence)
VALUES (?, ?, ?, ?, ?, ?)
(repository_id, capability_id, name, type, location, confidence, source_refs)
VALUES (?, ?, ?, ?, ?, ?, ?)
""",
(repository_id, capability_id, name, type, location, confidence),
(
repository_id,
capability_id,
name,
type,
location,
confidence,
self._source_refs_to_json(source_refs or []),
),
)
return int(cursor.lastrowid)
@@ -1173,15 +1197,23 @@ class RegistryStore:
type: str,
reference: str,
strength: str,
source_refs: list[SourceReference] | None = None,
) -> int:
with self.connect() as connection:
cursor = connection.execute(
"""
INSERT INTO approved_evidence
(repository_id, capability_id, type, reference, strength)
VALUES (?, ?, ?, ?, ?)
(repository_id, capability_id, type, reference, strength, source_refs)
VALUES (?, ?, ?, ?, ?, ?)
""",
(repository_id, capability_id, type, reference, strength),
(
repository_id,
capability_id,
type,
reference,
strength,
self._source_refs_to_json(source_refs or []),
),
)
return int(cursor.lastrowid)
@@ -1208,7 +1240,7 @@ class RegistryStore:
).fetchall()
feature_rows = connection.execute(
"""
SELECT id, capability_id, name, type, location, confidence
SELECT id, capability_id, name, type, location, confidence, source_refs
FROM approved_features
WHERE repository_id = ?
ORDER BY id
@@ -1217,7 +1249,7 @@ class RegistryStore:
).fetchall()
evidence_rows = connection.execute(
"""
SELECT id, capability_id, type, reference, strength
SELECT id, capability_id, type, reference, strength, source_refs
FROM approved_evidence
WHERE repository_id = ?
ORDER BY id
@@ -1234,6 +1266,7 @@ class RegistryStore:
type=row["type"],
location=row["location"],
confidence=row["confidence"],
source_refs=self._source_refs_from_json(row["source_refs"]),
)
)
@@ -1245,6 +1278,7 @@ class RegistryStore:
type=row["type"],
reference=row["reference"],
strength=row["strength"],
source_refs=self._source_refs_from_json(row["source_refs"]),
)
)
@@ -1608,6 +1642,7 @@ class RegistryStore:
"path": source_ref.path,
"kind": source_ref.kind,
"name": source_ref.name,
"line": source_ref.line,
}
for source_ref in source_refs
]
@@ -1620,6 +1655,7 @@ class RegistryStore:
path=item.get("path", ""),
kind=item.get("kind", ""),
name=item.get("name", ""),
line=item.get("line"),
)
for item in json.loads(value)
]

View File

@@ -927,11 +927,11 @@ def render_ability_map(ability_map: dict) -> str:
capabilities = []
for capability in ability["capabilities"]:
features = "".join(
f'<li>{escape(feature["name"])} <span class="pill">{escape(feature["type"])}</span> <span class="source">{escape(feature["location"])}</span></li>'
render_approved_feature(feature)
for feature in capability["features"]
)
evidence = "".join(
f'<li>{escape(item["type"])} <span class="pill">{escape(item["strength"])}</span> <span class="source">{escape(item["reference"])}</span></li>'
render_approved_evidence(item)
for item in capability["evidence"]
)
capabilities.append(
@@ -955,6 +955,28 @@ def render_ability_map(ability_map: dict) -> str:
return f'<div class="tree"><ul>{"".join(items)}</ul></div>'
def render_approved_feature(feature: dict) -> str:
return f"""
<li>
{escape(feature["name"])}
<span class="pill">{escape(feature["type"])}</span>
<span class="source">{escape(feature["location"])}</span>
{render_sources(feature.get("source_refs", []))}
</li>
"""
def render_approved_evidence(evidence: dict) -> str:
return f"""
<li>
{escape(evidence["type"])}
<span class="pill">{escape(evidence["strength"])}</span>
<span class="source">{escape(evidence["reference"])}</span>
{render_sources(evidence.get("source_refs", []))}
</li>
"""
def search_result_href(result: dict) -> str:
href = f"/ui/repos/{result['repository_id']}"
if result.get("capability_id"):
@@ -968,7 +990,7 @@ def render_sources(source_refs: list[dict]) -> str:
if not source_refs:
return ""
sources = ", ".join(
f'<span class="source">{escape(ref["kind"])}:{escape(ref["path"] or ref["name"])}</span>'
f'<span class="source">{escape(ref["kind"])}:{escape(source_ref_label(ref))}</span>'
for ref in source_refs[:5]
)
if len(source_refs) > 5:
@@ -976,6 +998,13 @@ def render_sources(source_refs: list[dict]) -> str:
return f"<p>{sources}</p>"
def source_ref_label(ref: dict) -> str:
label = ref["path"] or ref["name"]
if ref.get("line"):
label = f"{label}:{ref['line']}"
return label
def render_search_context(result: dict) -> str:
details = []
if result.get("ability_name"):

View File

@@ -297,6 +297,9 @@ def test_approve_candidate_graph_publishes_ability_map_once(tmp_path):
assert len(second_approval.abilities) == 1
assert ability_map.abilities[0].name == "Review Example Repository Usefulness"
assert ability_map.abilities[0].capabilities[0].features[0].location == "app.py"
assert ability_map.abilities[0].capabilities[0].features[0].source_refs
assert ability_map.abilities[0].capabilities[0].features[0].source_refs[0].line == 3
assert ability_map.abilities[0].capabilities[0].evidence[0].source_refs
candidate_graph = service.candidate_graph(repository.id, summary.analysis_run.id)
assert candidate_graph.abilities[0].status == "approved"

View File

@@ -315,6 +315,7 @@ def test_ui_register_analyze_and_approve_loop(tmp_path):
assert "Review UI Repo Repository Usefulness" in approved_detail.text
assert "Language: Python" in approved_detail.text
assert "Framework: FastAPI" in approved_detail.text
assert "interface:app.py:3" in approved_detail.text
search_response = client.get("/ui/search", params={"q": "repository"})
assert search_response.status_code == 200