generated from coulomb/repo-seed
first review workflow slice
This commit is contained in:
10
README.md
10
README.md
@@ -81,3 +81,13 @@ curl http://127.0.0.1:8000/repos/1/analysis-runs/1/candidate-graph
|
|||||||
```
|
```
|
||||||
|
|
||||||
Candidate entries are source-linked review seeds. They are not canonical registry truth until a review workflow approves them.
|
Candidate entries are source-linked review seeds. They are not canonical registry truth until a review workflow approves them.
|
||||||
|
|
||||||
|
Approve a candidate graph into the canonical registry:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://127.0.0.1:8000/repos/1/analysis-runs/1/candidate-graph/approve \
|
||||||
|
-H 'content-type: application/json' \
|
||||||
|
-d '{"notes":"Approved first review package"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Approval copies candidate abilities, capabilities, features, and evidence into the approved registry tables, marks candidates approved, and moves the repository status to `indexed`.
|
||||||
|
|||||||
@@ -97,6 +97,15 @@ CREATE TABLE IF NOT EXISTS candidate_evidence (
|
|||||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS review_decisions (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
repository_id INTEGER NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,
|
||||||
|
analysis_run_id INTEGER REFERENCES analysis_runs(id) ON DELETE SET NULL,
|
||||||
|
action TEXT NOT NULL,
|
||||||
|
notes TEXT NOT NULL DEFAULT '',
|
||||||
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS approved_abilities (
|
CREATE TABLE IF NOT EXISTS approved_abilities (
|
||||||
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,
|
||||||
@@ -148,6 +157,7 @@ CREATE INDEX IF NOT EXISTS idx_candidate_abilities_repository ON candidate_abili
|
|||||||
CREATE INDEX IF NOT EXISTS idx_candidate_capabilities_repository ON candidate_capabilities(repository_id);
|
CREATE INDEX IF NOT EXISTS idx_candidate_capabilities_repository ON candidate_capabilities(repository_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_candidate_features_repository ON candidate_features(repository_id);
|
CREATE INDEX IF NOT EXISTS idx_candidate_features_repository ON candidate_features(repository_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_candidate_evidence_repository ON candidate_evidence(repository_id);
|
CREATE INDEX IF NOT EXISTS idx_candidate_evidence_repository ON candidate_evidence(repository_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_review_decisions_repository ON review_decisions(repository_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_abilities_repository ON approved_abilities(repository_id);
|
CREATE INDEX IF NOT EXISTS idx_abilities_repository ON approved_abilities(repository_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_capabilities_repository ON approved_capabilities(repository_id);
|
CREATE INDEX IF NOT EXISTS idx_capabilities_repository ON approved_capabilities(repository_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_features_repository ON approved_features(repository_id);
|
CREATE INDEX IF NOT EXISTS idx_features_repository ON approved_features(repository_id);
|
||||||
|
|||||||
@@ -103,6 +103,67 @@ class RegistryService:
|
|||||||
def candidate_graph(self, repository_id: int, analysis_run_id: int) -> CandidateGraph:
|
def candidate_graph(self, repository_id: int, analysis_run_id: int) -> CandidateGraph:
|
||||||
return self.store.get_candidate_graph(repository_id, analysis_run_id)
|
return self.store.get_candidate_graph(repository_id, analysis_run_id)
|
||||||
|
|
||||||
|
def approve_candidate_graph(
|
||||||
|
self,
|
||||||
|
repository_id: int,
|
||||||
|
analysis_run_id: int,
|
||||||
|
*,
|
||||||
|
notes: str = "",
|
||||||
|
) -> RepositoryAbilityMap:
|
||||||
|
graph = self.store.get_candidate_graph(repository_id, analysis_run_id)
|
||||||
|
pending_abilities = [
|
||||||
|
ability for ability in graph.abilities if ability.status == "candidate"
|
||||||
|
]
|
||||||
|
for ability in pending_abilities:
|
||||||
|
approved_ability_id = self.store.create_ability(
|
||||||
|
repository_id,
|
||||||
|
name=ability.name,
|
||||||
|
description=ability.description,
|
||||||
|
confidence=ability.confidence,
|
||||||
|
)
|
||||||
|
for capability in ability.capabilities:
|
||||||
|
approved_capability_id = self.store.create_capability(
|
||||||
|
repository_id,
|
||||||
|
approved_ability_id,
|
||||||
|
name=capability.name,
|
||||||
|
description=capability.description,
|
||||||
|
inputs=capability.inputs,
|
||||||
|
outputs=capability.outputs,
|
||||||
|
confidence=capability.confidence,
|
||||||
|
)
|
||||||
|
for feature in capability.features:
|
||||||
|
self.store.create_feature(
|
||||||
|
repository_id,
|
||||||
|
approved_capability_id,
|
||||||
|
name=feature.name,
|
||||||
|
type=feature.type,
|
||||||
|
location=feature.location,
|
||||||
|
confidence=feature.confidence,
|
||||||
|
)
|
||||||
|
for evidence in capability.evidence:
|
||||||
|
self.store.create_evidence(
|
||||||
|
repository_id,
|
||||||
|
approved_capability_id,
|
||||||
|
type=evidence.type,
|
||||||
|
reference=evidence.reference,
|
||||||
|
strength=evidence.strength,
|
||||||
|
)
|
||||||
|
|
||||||
|
if pending_abilities:
|
||||||
|
self.store.mark_candidate_graph_status(
|
||||||
|
repository_id,
|
||||||
|
analysis_run_id,
|
||||||
|
"approved",
|
||||||
|
)
|
||||||
|
self.store.create_review_decision(
|
||||||
|
repository_id,
|
||||||
|
analysis_run_id,
|
||||||
|
action="approve_candidate_graph",
|
||||||
|
notes=notes,
|
||||||
|
)
|
||||||
|
self.store.update_repository_status(repository_id, "indexed")
|
||||||
|
return self.store.get_ability_map(repository_id)
|
||||||
|
|
||||||
def add_ability(
|
def add_ability(
|
||||||
self,
|
self,
|
||||||
repository_id: int,
|
repository_id: int,
|
||||||
|
|||||||
@@ -361,6 +361,47 @@ class RegistryStore:
|
|||||||
abilities=abilities,
|
abilities=abilities,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def mark_candidate_graph_status(
|
||||||
|
self,
|
||||||
|
repository_id: int,
|
||||||
|
analysis_run_id: int,
|
||||||
|
status: str,
|
||||||
|
) -> None:
|
||||||
|
with self.connect() as connection:
|
||||||
|
for table in (
|
||||||
|
"candidate_abilities",
|
||||||
|
"candidate_capabilities",
|
||||||
|
"candidate_features",
|
||||||
|
"candidate_evidence",
|
||||||
|
):
|
||||||
|
connection.execute(
|
||||||
|
f"""
|
||||||
|
UPDATE {table}
|
||||||
|
SET status = ?
|
||||||
|
WHERE repository_id = ? AND analysis_run_id = ?
|
||||||
|
""",
|
||||||
|
(status, repository_id, analysis_run_id),
|
||||||
|
)
|
||||||
|
|
||||||
|
def create_review_decision(
|
||||||
|
self,
|
||||||
|
repository_id: int,
|
||||||
|
analysis_run_id: int,
|
||||||
|
*,
|
||||||
|
action: str,
|
||||||
|
notes: str = "",
|
||||||
|
) -> int:
|
||||||
|
with self.connect() as connection:
|
||||||
|
cursor = connection.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO review_decisions
|
||||||
|
(repository_id, analysis_run_id, action, notes)
|
||||||
|
VALUES (?, ?, ?, ?)
|
||||||
|
""",
|
||||||
|
(repository_id, analysis_run_id, action, notes),
|
||||||
|
)
|
||||||
|
return int(cursor.lastrowid)
|
||||||
|
|
||||||
def fail_analysis_run(
|
def fail_analysis_run(
|
||||||
self,
|
self,
|
||||||
repository_id: int,
|
repository_id: int,
|
||||||
|
|||||||
@@ -69,6 +69,10 @@ class AnalysisRunCreate(BaseModel):
|
|||||||
source_path: str | None = None
|
source_path: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class CandidateGraphApproval(BaseModel):
|
||||||
|
notes: str = ""
|
||||||
|
|
||||||
|
|
||||||
app = FastAPI(title="Repository Ability Registry", version="0.1.0")
|
app = FastAPI(title="Repository Ability Registry", version="0.1.0")
|
||||||
|
|
||||||
|
|
||||||
@@ -161,6 +165,25 @@ def get_candidate_graph(
|
|||||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/repos/{repository_id}/analysis-runs/{analysis_run_id}/candidate-graph/approve")
|
||||||
|
def approve_candidate_graph(
|
||||||
|
repository_id: int,
|
||||||
|
analysis_run_id: int,
|
||||||
|
payload: CandidateGraphApproval,
|
||||||
|
service: RegistryService = Depends(get_service),
|
||||||
|
) -> dict[str, object]:
|
||||||
|
try:
|
||||||
|
return asdict(
|
||||||
|
service.approve_candidate_graph(
|
||||||
|
repository_id,
|
||||||
|
analysis_run_id,
|
||||||
|
notes=payload.notes,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except NotFoundError as exc:
|
||||||
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||||
|
|
||||||
|
|
||||||
@app.post("/repos/{repository_id}/abilities", status_code=201)
|
@app.post("/repos/{repository_id}/abilities", status_code=201)
|
||||||
def create_ability(
|
def create_ability(
|
||||||
repository_id: int,
|
repository_id: int,
|
||||||
|
|||||||
@@ -144,6 +144,43 @@ def test_analyze_repository_records_snapshot_and_observed_facts(tmp_path):
|
|||||||
assert "Expose Repository Interface" in capability_names
|
assert "Expose Repository Interface" in capability_names
|
||||||
|
|
||||||
|
|
||||||
|
def test_approve_candidate_graph_publishes_ability_map_once(tmp_path):
|
||||||
|
source = tmp_path / "repo"
|
||||||
|
source.mkdir()
|
||||||
|
(source / "README.md").write_text("# Example\n", encoding="utf-8")
|
||||||
|
(source / "app.py").write_text(
|
||||||
|
"from fastapi import FastAPI\n"
|
||||||
|
"app = FastAPI()\n"
|
||||||
|
'@app.get("/health")\n'
|
||||||
|
"def health():\n"
|
||||||
|
" return {}\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
service = make_service(tmp_path)
|
||||||
|
repository = service.register_repository(name="Example", url=str(source))
|
||||||
|
summary = service.analyze_repository(repository.id)
|
||||||
|
|
||||||
|
ability_map = service.approve_candidate_graph(
|
||||||
|
repository.id,
|
||||||
|
summary.analysis_run.id,
|
||||||
|
notes="Looks good for the first pass.",
|
||||||
|
)
|
||||||
|
second_approval = service.approve_candidate_graph(
|
||||||
|
repository.id,
|
||||||
|
summary.analysis_run.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert service.get_repository(repository.id).status == "indexed"
|
||||||
|
assert len(ability_map.abilities) == 1
|
||||||
|
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"
|
||||||
|
|
||||||
|
candidate_graph = service.candidate_graph(repository.id, summary.analysis_run.id)
|
||||||
|
assert candidate_graph.abilities[0].status == "approved"
|
||||||
|
|
||||||
|
|
||||||
def test_analyze_repository_failure_is_recorded(tmp_path):
|
def test_analyze_repository_failure_is_recorded(tmp_path):
|
||||||
service = make_service(tmp_path)
|
service = make_service(tmp_path)
|
||||||
repository = service.register_repository(
|
repository = service.register_repository(
|
||||||
|
|||||||
@@ -112,6 +112,22 @@ def test_api_analysis_run_loop(tmp_path):
|
|||||||
"Review Frontend Repository Usefulness"
|
"Review Frontend Repository Usefulness"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
approve_response = client.post(
|
||||||
|
f"/repos/{repository_id}/analysis-runs/"
|
||||||
|
f"{run['analysis_run']['id']}/candidate-graph/approve",
|
||||||
|
json={"notes": "Approved in API test"},
|
||||||
|
)
|
||||||
|
assert approve_response.status_code == 200
|
||||||
|
ability_map = approve_response.json()
|
||||||
|
assert ability_map["repository"]["status"] == "indexed"
|
||||||
|
assert ability_map["abilities"][0]["name"] == (
|
||||||
|
"Review Frontend Repository Usefulness"
|
||||||
|
)
|
||||||
|
|
||||||
|
search_response = client.get("/search", params={"q": "structure"})
|
||||||
|
assert search_response.status_code == 200
|
||||||
|
assert search_response.json()
|
||||||
|
|
||||||
facts_response = client.get(f"/repos/{repository_id}/observed-facts")
|
facts_response = client.get(f"/repos/{repository_id}/observed-facts")
|
||||||
assert facts_response.status_code == 200
|
assert facts_response.status_code == 200
|
||||||
fact_names = {
|
fact_names = {
|
||||||
|
|||||||
Reference in New Issue
Block a user