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

View File

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

View File

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

View File

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

View File

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

View File

@@ -927,11 +927,11 @@ def render_ability_map(ability_map: dict) -> str:
capabilities = [] capabilities = []
for capability in ability["capabilities"]: for capability in ability["capabilities"]:
features = "".join( 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"] for feature in capability["features"]
) )
evidence = "".join( 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"] for item in capability["evidence"]
) )
capabilities.append( capabilities.append(
@@ -955,6 +955,28 @@ def render_ability_map(ability_map: dict) -> str:
return f'<div class="tree"><ul>{"".join(items)}</ul></div>' 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: def search_result_href(result: dict) -> str:
href = f"/ui/repos/{result['repository_id']}" href = f"/ui/repos/{result['repository_id']}"
if result.get("capability_id"): if result.get("capability_id"):
@@ -968,7 +990,7 @@ def render_sources(source_refs: list[dict]) -> str:
if not source_refs: if not source_refs:
return "" return ""
sources = ", ".join( 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] for ref in source_refs[:5]
) )
if len(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>" 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: def render_search_context(result: dict) -> str:
details = [] details = []
if result.get("ability_name"): 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 len(second_approval.abilities) == 1
assert ability_map.abilities[0].name == "Review Example Repository Usefulness" 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].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) candidate_graph = service.candidate_graph(repository.id, summary.analysis_run.id)
assert candidate_graph.abilities[0].status == "approved" 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 "Review UI Repo Repository Usefulness" in approved_detail.text
assert "Language: Python" in approved_detail.text assert "Language: Python" in approved_detail.text
assert "Framework: FastAPI" 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"}) search_response = client.get("/ui/search", params={"q": "repository"})
assert search_response.status_code == 200 assert search_response.status_code == 200