generated from coulomb/repo-seed
profile drill-down/source-link preservation
This commit is contained in:
@@ -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
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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)
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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"):
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user