generated from coulomb/repo-seed
Status drift warnings
This commit is contained in:
@@ -129,6 +129,18 @@ class ScanSummary:
|
|||||||
facts: list[ObservedFact]
|
facts: list[ObservedFact]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class CharacteristicRebuildResult:
|
||||||
|
repository: Repository
|
||||||
|
analysis_run: AnalysisRun
|
||||||
|
dry_run: bool
|
||||||
|
confirmed: bool
|
||||||
|
cleared_approved: bool
|
||||||
|
previous_counts: dict[str, int]
|
||||||
|
previous_ids: dict[str, list[int]]
|
||||||
|
candidate_counts: dict[str, int]
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class SourceReference:
|
class SourceReference:
|
||||||
fact_id: int | None
|
fact_id: int | None
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ from repo_registry.core.models import (
|
|||||||
CandidateEvidence,
|
CandidateEvidence,
|
||||||
CandidateFeature,
|
CandidateFeature,
|
||||||
CandidateGraph,
|
CandidateGraph,
|
||||||
|
CharacteristicRebuildResult,
|
||||||
ContentChunk,
|
ContentChunk,
|
||||||
ExpectationGap,
|
ExpectationGap,
|
||||||
ObservedFact,
|
ObservedFact,
|
||||||
@@ -228,14 +229,13 @@ class RegistryService:
|
|||||||
notes=f"Generated {len(candidates)} candidate ability draft(s).",
|
notes=f"Generated {len(candidates)} candidate ability draft(s).",
|
||||||
)
|
)
|
||||||
if trusted_auto_approve:
|
if trusted_auto_approve:
|
||||||
self.approve_candidate_graph(
|
self.trusted_auto_approve_candidate_graph(
|
||||||
repository_id,
|
repository_id,
|
||||||
completed_run.id,
|
completed_run.id,
|
||||||
notes=(
|
notes=(
|
||||||
"Trusted auto-populate mode approved candidate graph "
|
"Trusted auto-populate mode reviewed candidate graph "
|
||||||
f"after {candidate_source} candidate generation."
|
f"after {candidate_source} candidate generation."
|
||||||
),
|
),
|
||||||
action="trusted_auto_approve_candidate_graph",
|
|
||||||
)
|
)
|
||||||
log_operation(
|
log_operation(
|
||||||
"analysis_completed",
|
"analysis_completed",
|
||||||
@@ -335,6 +335,79 @@ 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 rebuild_characteristics_from_scratch(
|
||||||
|
self,
|
||||||
|
repository_id: int,
|
||||||
|
*,
|
||||||
|
dry_run: bool = True,
|
||||||
|
confirm: bool = False,
|
||||||
|
source_path: str | None = None,
|
||||||
|
use_cached_checkout: bool = False,
|
||||||
|
use_llm_assistance: bool = True,
|
||||||
|
access_username: str | None = None,
|
||||||
|
access_password: str | None = None,
|
||||||
|
) -> CharacteristicRebuildResult:
|
||||||
|
if not dry_run and not confirm:
|
||||||
|
raise ValueError("confirmed rebuild requires confirm=True")
|
||||||
|
|
||||||
|
repository = self.store.get_repository(repository_id)
|
||||||
|
previous_counts = self._approved_counts(repository_id)
|
||||||
|
previous_ids = self._approved_ids(repository_id)
|
||||||
|
summary = self.analyze_repository(
|
||||||
|
repository_id,
|
||||||
|
source_path=source_path,
|
||||||
|
use_cached_checkout=use_cached_checkout,
|
||||||
|
use_llm_assistance=use_llm_assistance,
|
||||||
|
trusted_auto_approve=False,
|
||||||
|
access_username=access_username,
|
||||||
|
access_password=access_password,
|
||||||
|
)
|
||||||
|
if summary.analysis_run.status != "completed":
|
||||||
|
return CharacteristicRebuildResult(
|
||||||
|
repository=repository,
|
||||||
|
analysis_run=summary.analysis_run,
|
||||||
|
dry_run=dry_run,
|
||||||
|
confirmed=confirm,
|
||||||
|
cleared_approved=False,
|
||||||
|
previous_counts=previous_counts,
|
||||||
|
previous_ids=previous_ids,
|
||||||
|
candidate_counts={},
|
||||||
|
)
|
||||||
|
|
||||||
|
graph = self.store.get_candidate_graph(repository_id, summary.analysis_run.id)
|
||||||
|
candidate_counts = self._candidate_counts(graph)
|
||||||
|
cleared = False
|
||||||
|
if not dry_run:
|
||||||
|
self.store.clear_approved_characteristics(repository_id)
|
||||||
|
self.store.update_repository_status(repository_id, "analyzed")
|
||||||
|
cleared = True
|
||||||
|
|
||||||
|
action = (
|
||||||
|
"rebuild_characteristics_from_scratch"
|
||||||
|
if cleared
|
||||||
|
else "dry_run_rebuild_characteristics_from_scratch"
|
||||||
|
)
|
||||||
|
self.store.create_review_decision(
|
||||||
|
repository_id,
|
||||||
|
summary.analysis_run.id,
|
||||||
|
action=action,
|
||||||
|
notes=(
|
||||||
|
f"Previous approved counts: {previous_counts}. "
|
||||||
|
f"Previous approved IDs: {previous_ids}. "
|
||||||
|
f"New candidate counts: {candidate_counts}."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return CharacteristicRebuildResult(
|
||||||
|
repository=repository,
|
||||||
|
analysis_run=summary.analysis_run,
|
||||||
|
dry_run=dry_run,
|
||||||
|
confirmed=confirm,
|
||||||
|
cleared_approved=cleared,
|
||||||
|
previous_counts=previous_counts,
|
||||||
|
previous_ids=previous_ids,
|
||||||
|
candidate_counts=candidate_counts,
|
||||||
|
)
|
||||||
|
|
||||||
def approve_candidate_graph(
|
def approve_candidate_graph(
|
||||||
self,
|
self,
|
||||||
repository_id: int,
|
repository_id: int,
|
||||||
@@ -406,6 +479,160 @@ class RegistryService:
|
|||||||
self.store.update_repository_status(repository_id, "indexed")
|
self.store.update_repository_status(repository_id, "indexed")
|
||||||
return self.store.get_ability_map(repository_id)
|
return self.store.get_ability_map(repository_id)
|
||||||
|
|
||||||
|
def trusted_auto_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)
|
||||||
|
approved_count = 0
|
||||||
|
skipped_count = 0
|
||||||
|
for ability in graph.abilities:
|
||||||
|
if ability.status != "candidate":
|
||||||
|
continue
|
||||||
|
candidate_capabilities = [
|
||||||
|
capability
|
||||||
|
for capability in ability.capabilities
|
||||||
|
if capability.status == "candidate"
|
||||||
|
]
|
||||||
|
safe_capabilities = [
|
||||||
|
capability
|
||||||
|
for capability in candidate_capabilities
|
||||||
|
if self._trusted_auto_approve_capability_safe(capability)
|
||||||
|
]
|
||||||
|
skipped_count += len(candidate_capabilities) - len(safe_capabilities)
|
||||||
|
if not safe_capabilities:
|
||||||
|
continue
|
||||||
|
approved_ability_id = self._ensure_approved_ability(repository_id, ability)
|
||||||
|
for capability in safe_capabilities:
|
||||||
|
self._create_approved_capability_subtree(
|
||||||
|
repository_id,
|
||||||
|
approved_ability_id,
|
||||||
|
capability,
|
||||||
|
)
|
||||||
|
self.store.mark_candidate_capability_status(
|
||||||
|
repository_id,
|
||||||
|
analysis_run_id,
|
||||||
|
capability.id,
|
||||||
|
"approved",
|
||||||
|
)
|
||||||
|
approved_count += 1
|
||||||
|
if len(safe_capabilities) == len(candidate_capabilities):
|
||||||
|
self.store.mark_candidate_ability_status(
|
||||||
|
repository_id,
|
||||||
|
analysis_run_id,
|
||||||
|
ability.id,
|
||||||
|
"approved",
|
||||||
|
)
|
||||||
|
|
||||||
|
if approved_count:
|
||||||
|
self.store.update_repository_status(repository_id, "indexed")
|
||||||
|
self.store.create_review_decision(
|
||||||
|
repository_id,
|
||||||
|
analysis_run_id,
|
||||||
|
action="trusted_auto_approve_candidate_graph",
|
||||||
|
notes=(
|
||||||
|
f"{notes} Auto-approved {approved_count} safe candidate "
|
||||||
|
f"capability(s); left {skipped_count} for review."
|
||||||
|
).strip(),
|
||||||
|
)
|
||||||
|
return self.store.get_ability_map(repository_id)
|
||||||
|
|
||||||
|
def _trusted_auto_approve_capability_safe(
|
||||||
|
self,
|
||||||
|
capability: CandidateCapability,
|
||||||
|
) -> bool:
|
||||||
|
has_source_refs = bool(capability.source_refs) or any(
|
||||||
|
feature.source_refs for feature in capability.features
|
||||||
|
)
|
||||||
|
if not has_source_refs:
|
||||||
|
return False
|
||||||
|
if capability.primary_class == "repository-structure":
|
||||||
|
return False
|
||||||
|
if capability.primary_class == "llm-integration":
|
||||||
|
return bool(
|
||||||
|
{"utility-owned", "utility-facade", "utility-adapter"}
|
||||||
|
& set(capability.attributes)
|
||||||
|
)
|
||||||
|
if capability.primary_class in {"interface", "API", "CLI", "callable", "api", "cli"}:
|
||||||
|
return capability.confidence >= 0.55
|
||||||
|
if capability.features:
|
||||||
|
return capability.confidence >= 0.55
|
||||||
|
return capability.confidence >= 0.75
|
||||||
|
|
||||||
|
def _approved_counts(self, repository_id: int) -> dict[str, int]:
|
||||||
|
ability_map = self.store.get_ability_map(repository_id)
|
||||||
|
capabilities = [
|
||||||
|
capability
|
||||||
|
for ability in ability_map.abilities
|
||||||
|
for capability in ability.capabilities
|
||||||
|
]
|
||||||
|
features = [
|
||||||
|
feature
|
||||||
|
for capability in capabilities
|
||||||
|
for feature in capability.features
|
||||||
|
]
|
||||||
|
evidence = [
|
||||||
|
item
|
||||||
|
for capability in capabilities
|
||||||
|
for item in capability.evidence
|
||||||
|
]
|
||||||
|
return {
|
||||||
|
"abilities": len(ability_map.abilities),
|
||||||
|
"capabilities": len(capabilities),
|
||||||
|
"features": len(features),
|
||||||
|
"evidence": len(evidence),
|
||||||
|
}
|
||||||
|
|
||||||
|
def _approved_ids(self, repository_id: int) -> dict[str, list[int]]:
|
||||||
|
ability_map = self.store.get_ability_map(repository_id)
|
||||||
|
capabilities = [
|
||||||
|
capability
|
||||||
|
for ability in ability_map.abilities
|
||||||
|
for capability in ability.capabilities
|
||||||
|
]
|
||||||
|
features = [
|
||||||
|
feature
|
||||||
|
for capability in capabilities
|
||||||
|
for feature in capability.features
|
||||||
|
]
|
||||||
|
evidence = [
|
||||||
|
item
|
||||||
|
for capability in capabilities
|
||||||
|
for item in capability.evidence
|
||||||
|
]
|
||||||
|
return {
|
||||||
|
"abilities": [ability.id for ability in ability_map.abilities],
|
||||||
|
"capabilities": [capability.id for capability in capabilities],
|
||||||
|
"features": [feature.id for feature in features],
|
||||||
|
"evidence": [item.id for item in evidence],
|
||||||
|
}
|
||||||
|
|
||||||
|
def _candidate_counts(self, graph: CandidateGraph) -> dict[str, int]:
|
||||||
|
capabilities = [
|
||||||
|
capability
|
||||||
|
for ability in graph.abilities
|
||||||
|
for capability in ability.capabilities
|
||||||
|
]
|
||||||
|
features = [
|
||||||
|
feature
|
||||||
|
for capability in capabilities
|
||||||
|
for feature in capability.features
|
||||||
|
]
|
||||||
|
evidence = [
|
||||||
|
item
|
||||||
|
for capability in capabilities
|
||||||
|
for item in capability.evidence
|
||||||
|
]
|
||||||
|
return {
|
||||||
|
"abilities": len(graph.abilities),
|
||||||
|
"capabilities": len(capabilities),
|
||||||
|
"features": len(features),
|
||||||
|
"evidence": len(evidence),
|
||||||
|
}
|
||||||
|
|
||||||
def accept_candidate_ability(
|
def accept_candidate_ability(
|
||||||
self,
|
self,
|
||||||
repository_id: int,
|
repository_id: int,
|
||||||
|
|||||||
@@ -2226,6 +2226,14 @@ class RegistryStore:
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def clear_approved_characteristics(self, repository_id: int) -> None:
|
||||||
|
self.get_repository(repository_id)
|
||||||
|
with self.connect() as connection:
|
||||||
|
connection.execute(
|
||||||
|
"DELETE FROM approved_abilities WHERE repository_id = ?",
|
||||||
|
(repository_id,),
|
||||||
|
)
|
||||||
|
|
||||||
def get_ability_map(self, repository_id: int) -> RepositoryAbilityMap:
|
def get_ability_map(self, repository_id: int) -> RepositoryAbilityMap:
|
||||||
repository = self.get_repository(repository_id)
|
repository = self.get_repository(repository_id)
|
||||||
scope = self._ensure_scope(repository_id)
|
scope = self._ensure_scope(repository_id)
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ from repo_registry.web_api.schemas import (
|
|||||||
CandidateFeatureMerge,
|
CandidateFeatureMerge,
|
||||||
CandidateGraphApproval,
|
CandidateGraphApproval,
|
||||||
CandidateGraphResponse,
|
CandidateGraphResponse,
|
||||||
|
CharacteristicRebuildRequest,
|
||||||
|
CharacteristicRebuildResponse,
|
||||||
CandidateLeafRelink,
|
CandidateLeafRelink,
|
||||||
CandidateRejection,
|
CandidateRejection,
|
||||||
CapabilityGapRequest,
|
CapabilityGapRequest,
|
||||||
@@ -271,6 +273,33 @@ def create_analysis_run(
|
|||||||
return asdict(summary)
|
return asdict(summary)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post(
|
||||||
|
"/repos/{repository_id}/characteristics/rebuild",
|
||||||
|
tags=["analysis"],
|
||||||
|
response_model=CharacteristicRebuildResponse,
|
||||||
|
)
|
||||||
|
def rebuild_characteristics_from_scratch(
|
||||||
|
repository_id: int,
|
||||||
|
payload: CharacteristicRebuildRequest,
|
||||||
|
service: RegistryService = Depends(get_service),
|
||||||
|
) -> dict[str, object]:
|
||||||
|
try:
|
||||||
|
return asdict(
|
||||||
|
service.rebuild_characteristics_from_scratch(
|
||||||
|
repository_id,
|
||||||
|
dry_run=payload.dry_run,
|
||||||
|
confirm=payload.confirm,
|
||||||
|
source_path=payload.source_path,
|
||||||
|
use_cached_checkout=payload.use_cached_checkout,
|
||||||
|
use_llm_assistance=payload.use_llm_assistance,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except NotFoundError as exc:
|
||||||
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||||
|
except ValueError as exc:
|
||||||
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||||
|
|
||||||
|
|
||||||
@app.get(
|
@app.get(
|
||||||
"/repos/{repository_id}/analysis-runs",
|
"/repos/{repository_id}/analysis-runs",
|
||||||
tags=["analysis"],
|
tags=["analysis"],
|
||||||
|
|||||||
@@ -289,6 +289,25 @@ class AnalysisRunChangeApproval(BaseModel):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class CharacteristicRebuildRequest(BaseModel):
|
||||||
|
dry_run: bool = True
|
||||||
|
confirm: bool = False
|
||||||
|
source_path: str | None = None
|
||||||
|
use_cached_checkout: bool = False
|
||||||
|
use_llm_assistance: bool = True
|
||||||
|
|
||||||
|
|
||||||
|
class CharacteristicRebuildResponse(BaseModel):
|
||||||
|
repository: RepositoryResponse
|
||||||
|
analysis_run: AnalysisRunResponse
|
||||||
|
dry_run: bool
|
||||||
|
confirmed: bool
|
||||||
|
cleared_approved: bool
|
||||||
|
previous_counts: dict[str, int]
|
||||||
|
previous_ids: dict[str, list[int]]
|
||||||
|
candidate_counts: dict[str, int]
|
||||||
|
|
||||||
|
|
||||||
class CandidateRejection(BaseModel):
|
class CandidateRejection(BaseModel):
|
||||||
notes: str = ""
|
notes: str = ""
|
||||||
|
|
||||||
|
|||||||
@@ -721,6 +721,18 @@ def repository_detail(
|
|||||||
<thead><tr><th>Run</th><th>Status</th><th>Started</th><th>Compare</th><th>Error</th></tr></thead>
|
<thead><tr><th>Run</th><th>Status</th><th>Started</th><th>Compare</th><th>Error</th></tr></thead>
|
||||||
<tbody>{run_rows or '<tr><td colspan="5" class="muted">No runs yet.</td></tr>'}</tbody>
|
<tbody>{run_rows or '<tr><td colspan="5" class="muted">No runs yet.</td></tr>'}</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
<h2>Rebuild Characteristics</h2>
|
||||||
|
<form class="stack" method="post" action="/ui/repos/{repository_id}/characteristics/rebuild">
|
||||||
|
<label>Override source path <input name="source_path" placeholder="Optional local path"></label>
|
||||||
|
<label class="checkbox"><input type="checkbox" name="use_cached_checkout" value="1"> Use cached checkout</label>
|
||||||
|
<label class="checkbox"><input type="checkbox" name="use_llm_assistance" value="1" checked> Use LLM assistance if configured</label>
|
||||||
|
<label class="checkbox"><input type="checkbox" name="dry_run" value="1" checked> Dry run only</label>
|
||||||
|
<label class="checkbox"><input type="checkbox" name="confirm" value="1"> Confirm clearing approved characteristics when dry run is off</label>
|
||||||
|
<div class="actions">
|
||||||
|
<button class="secondary" type="submit">Rebuild</button>
|
||||||
|
<span data-pending>Rebuilding...</span>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</section>
|
</section>
|
||||||
<section class="stack">
|
<section class="stack">
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
@@ -1146,6 +1158,30 @@ def create_analysis_run_from_form(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/ui/repos/{repository_id}/characteristics/rebuild")
|
||||||
|
def rebuild_characteristics_from_form(
|
||||||
|
repository_id: int,
|
||||||
|
source_path: str = Form(""),
|
||||||
|
use_cached_checkout: str | None = Form(None),
|
||||||
|
use_llm_assistance: str | None = Form(None),
|
||||||
|
dry_run: str | None = Form(None),
|
||||||
|
confirm: str | None = Form(None),
|
||||||
|
service: RegistryService = Depends(get_service),
|
||||||
|
) -> RedirectResponse:
|
||||||
|
result = service.rebuild_characteristics_from_scratch(
|
||||||
|
repository_id,
|
||||||
|
dry_run=bool(dry_run),
|
||||||
|
confirm=bool(confirm),
|
||||||
|
source_path=source_path or None,
|
||||||
|
use_cached_checkout=bool(use_cached_checkout),
|
||||||
|
use_llm_assistance=bool(use_llm_assistance),
|
||||||
|
)
|
||||||
|
return RedirectResponse(
|
||||||
|
f"/ui/repos/{repository_id}/analysis-runs/{result.analysis_run.id}",
|
||||||
|
status_code=303,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/ui/repos/{repository_id}/analysis-runs/{analysis_run_id}")
|
@router.get("/ui/repos/{repository_id}/analysis-runs/{analysis_run_id}")
|
||||||
def analysis_run_detail(
|
def analysis_run_detail(
|
||||||
repository_id: int,
|
repository_id: int,
|
||||||
|
|||||||
@@ -372,6 +372,7 @@ def test_candidate_generator_maps_llm_provider_facts_to_capability():
|
|||||||
feature for feature in capability.features if feature.name == "Use OpenRouter Models"
|
feature for feature in capability.features if feature.name == "Use OpenRouter Models"
|
||||||
)
|
)
|
||||||
assert openrouter_feature.primary_class == "integration"
|
assert openrouter_feature.primary_class == "integration"
|
||||||
|
assert {"llm-provider", "openrouter"} <= set(openrouter_feature.attributes)
|
||||||
|
|
||||||
|
|
||||||
def test_candidate_generator_does_not_promote_llm_provider_mentions_to_capability():
|
def test_candidate_generator_does_not_promote_llm_provider_mentions_to_capability():
|
||||||
@@ -402,4 +403,3 @@ def test_candidate_generator_does_not_promote_llm_provider_mentions_to_capabilit
|
|||||||
for capability in graph[0].capabilities
|
for capability in graph[0].capabilities
|
||||||
if capability.name == "Route LLM Requests Across Providers"
|
if capability.name == "Route LLM Requests Across Providers"
|
||||||
] == []
|
] == []
|
||||||
assert {"llm-provider", "openrouter"} <= set(openrouter_feature.attributes)
|
|
||||||
|
|||||||
@@ -746,10 +746,92 @@ def test_analyze_repository_can_trusted_auto_approve_candidates(tmp_path):
|
|||||||
decisions = service.list_review_decisions(repository.id, summary.analysis_run.id)
|
decisions = service.list_review_decisions(repository.id, summary.analysis_run.id)
|
||||||
|
|
||||||
assert service.get_repository(repository.id).status == "indexed"
|
assert service.get_repository(repository.id).status == "indexed"
|
||||||
assert graph.abilities[0].status == "approved"
|
statuses_by_capability = {
|
||||||
|
capability.name: capability.status
|
||||||
|
for capability in graph.abilities[0].capabilities
|
||||||
|
}
|
||||||
|
assert statuses_by_capability["Expose Repository Interface"] == "approved"
|
||||||
|
assert statuses_by_capability["Describe Repository Structure"] == "candidate"
|
||||||
assert ability_map.abilities[0].name == "Report Health Over HTTP"
|
assert ability_map.abilities[0].name == "Report Health Over HTTP"
|
||||||
assert decisions[0].action == "trusted_auto_approve_candidate_graph"
|
assert decisions[0].action == "trusted_auto_approve_candidate_graph"
|
||||||
assert "deterministic candidate generation" in decisions[0].notes
|
assert "deterministic candidate generation" in decisions[0].notes
|
||||||
|
assert "Auto-approved 1 safe candidate capability(s); left 1 for review." in decisions[0].notes
|
||||||
|
|
||||||
|
|
||||||
|
def test_rebuild_characteristics_dry_run_preserves_approved_map(tmp_path):
|
||||||
|
source = tmp_path / "repo"
|
||||||
|
source.mkdir()
|
||||||
|
(source / "README.md").write_text("# Rebuild\nReports health over HTTP.\n", encoding="utf-8")
|
||||||
|
(source / "app.py").write_text('@app.get("/health")\ndef health():\n return {}\n', encoding="utf-8")
|
||||||
|
service = make_service(tmp_path)
|
||||||
|
repository = service.register_repository(name="Rebuild", url=str(source))
|
||||||
|
summary = service.analyze_repository(repository.id, use_llm_assistance=False)
|
||||||
|
service.approve_candidate_graph(repository.id, summary.analysis_run.id)
|
||||||
|
|
||||||
|
result = service.rebuild_characteristics_from_scratch(
|
||||||
|
repository.id,
|
||||||
|
dry_run=True,
|
||||||
|
source_path=str(source),
|
||||||
|
use_llm_assistance=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.dry_run is True
|
||||||
|
assert result.cleared_approved is False
|
||||||
|
assert result.previous_counts["abilities"] == 1
|
||||||
|
assert result.previous_ids["abilities"]
|
||||||
|
assert result.candidate_counts["abilities"] == 1
|
||||||
|
assert service.ability_map(repository.id).abilities
|
||||||
|
decisions = service.list_review_decisions(repository.id, result.analysis_run.id)
|
||||||
|
assert decisions[-1].action == "dry_run_rebuild_characteristics_from_scratch"
|
||||||
|
|
||||||
|
|
||||||
|
def test_rebuild_characteristics_requires_confirmation_before_clearing(tmp_path):
|
||||||
|
service = make_service(tmp_path)
|
||||||
|
source = tmp_path / "repo"
|
||||||
|
source.mkdir()
|
||||||
|
(source / "README.md").write_text("# Rebuild\n", encoding="utf-8")
|
||||||
|
repository = service.register_repository(name="Rebuild", url=str(source))
|
||||||
|
|
||||||
|
try:
|
||||||
|
service.rebuild_characteristics_from_scratch(
|
||||||
|
repository.id,
|
||||||
|
dry_run=False,
|
||||||
|
confirm=False,
|
||||||
|
source_path=str(source),
|
||||||
|
use_llm_assistance=False,
|
||||||
|
)
|
||||||
|
except ValueError as exc:
|
||||||
|
assert "confirm=True" in str(exc)
|
||||||
|
else:
|
||||||
|
raise AssertionError("expected confirmed rebuild to require confirm=True")
|
||||||
|
|
||||||
|
|
||||||
|
def test_rebuild_characteristics_confirmed_clears_approved_map(tmp_path):
|
||||||
|
source = tmp_path / "repo"
|
||||||
|
source.mkdir()
|
||||||
|
(source / "README.md").write_text("# Rebuild\nReports health over HTTP.\n", encoding="utf-8")
|
||||||
|
(source / "app.py").write_text('@app.get("/health")\ndef health():\n return {}\n', encoding="utf-8")
|
||||||
|
service = make_service(tmp_path)
|
||||||
|
repository = service.register_repository(name="Rebuild", url=str(source))
|
||||||
|
summary = service.analyze_repository(repository.id, use_llm_assistance=False)
|
||||||
|
service.approve_candidate_graph(repository.id, summary.analysis_run.id)
|
||||||
|
|
||||||
|
result = service.rebuild_characteristics_from_scratch(
|
||||||
|
repository.id,
|
||||||
|
dry_run=False,
|
||||||
|
confirm=True,
|
||||||
|
source_path=str(source),
|
||||||
|
use_llm_assistance=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.cleared_approved is True
|
||||||
|
assert result.previous_counts["abilities"] == 1
|
||||||
|
assert result.previous_ids["abilities"]
|
||||||
|
assert service.ability_map(repository.id).abilities == []
|
||||||
|
assert service.get_repository(repository.id).status == "analyzed"
|
||||||
|
decisions = service.list_review_decisions(repository.id, result.analysis_run.id)
|
||||||
|
assert decisions[-1].action == "rebuild_characteristics_from_scratch"
|
||||||
|
assert "Previous approved IDs" in decisions[-1].notes
|
||||||
|
|
||||||
|
|
||||||
def test_analyze_repository_records_llm_failure_and_falls_back(tmp_path):
|
def test_analyze_repository_records_llm_failure_and_falls_back(tmp_path):
|
||||||
|
|||||||
@@ -182,6 +182,12 @@ def test_openapi_contract_snapshot_for_stable_agent_paths():
|
|||||||
"/repos/{repository_id}/analysis-runs/{base_analysis_run_id}/diff/{target_analysis_run_id}": {
|
"/repos/{repository_id}/analysis-runs/{base_analysis_run_id}/diff/{target_analysis_run_id}": {
|
||||||
"get": {"tags": ["review"], "success_schema": "AnalysisRunDiffResponse"}
|
"get": {"tags": ["review"], "success_schema": "AnalysisRunDiffResponse"}
|
||||||
},
|
},
|
||||||
|
"/repos/{repository_id}/characteristics/rebuild": {
|
||||||
|
"post": {
|
||||||
|
"tags": ["analysis"],
|
||||||
|
"success_schema": "CharacteristicRebuildResponse",
|
||||||
|
}
|
||||||
|
},
|
||||||
"/repos/{repository_id}/analysis-runs/{analysis_run_id}/candidate-abilities/{candidate_ability_id}": {
|
"/repos/{repository_id}/analysis-runs/{analysis_run_id}/candidate-abilities/{candidate_ability_id}": {
|
||||||
"patch": {"tags": ["review"], "success_schema": "CandidateGraphResponse"}
|
"patch": {"tags": ["review"], "success_schema": "CandidateGraphResponse"}
|
||||||
},
|
},
|
||||||
@@ -1767,7 +1773,118 @@ def test_ui_register_and_explore_lands_on_analysis_result(tmp_path):
|
|||||||
|
|
||||||
repository_detail = client.get("/ui/repos/1")
|
repository_detail = client.get("/ui/repos/1")
|
||||||
assert repository_detail.status_code == 200
|
assert repository_detail.status_code == 200
|
||||||
assert "Use Approved Registry" in repository_detail.text
|
assert "Latest Candidate Graph" in repository_detail.text
|
||||||
|
assert "Rebuild Characteristics" in repository_detail.text
|
||||||
|
assert "Use Approved Registry" not in repository_detail.text
|
||||||
|
finally:
|
||||||
|
app.dependency_overrides.clear()
|
||||||
|
|
||||||
|
|
||||||
|
def test_rebuild_characteristics_endpoint_dry_run_and_confirm(tmp_path):
|
||||||
|
source = tmp_path / "rebuild-api"
|
||||||
|
source.mkdir()
|
||||||
|
(source / "README.md").write_text("# Rebuild API\nReports status.\n", encoding="utf-8")
|
||||||
|
(source / "app.py").write_text('@app.get("/status")\ndef status():\n return {}\n', encoding="utf-8")
|
||||||
|
|
||||||
|
def override_settings():
|
||||||
|
return Settings(
|
||||||
|
database_path=str(tmp_path / "rebuild-api.sqlite3"),
|
||||||
|
checkout_root=str(tmp_path / "rebuild-api-checkouts"),
|
||||||
|
)
|
||||||
|
|
||||||
|
app.dependency_overrides[get_settings] = override_settings
|
||||||
|
client = TestClient(app)
|
||||||
|
try:
|
||||||
|
repository_id = client.post(
|
||||||
|
"/repos",
|
||||||
|
json={"name": "Rebuild API", "url": str(source)},
|
||||||
|
).json()["id"]
|
||||||
|
run_id = client.post(
|
||||||
|
f"/repos/{repository_id}/analysis-runs",
|
||||||
|
json={"source_path": str(source), "use_llm_assistance": False},
|
||||||
|
).json()["analysis_run"]["id"]
|
||||||
|
approve_response = client.post(
|
||||||
|
f"/repos/{repository_id}/analysis-runs/{run_id}/candidate-graph/approve",
|
||||||
|
json={"notes": "approve before rebuild"},
|
||||||
|
)
|
||||||
|
assert approve_response.status_code == 200
|
||||||
|
|
||||||
|
dry_run_response = client.post(
|
||||||
|
f"/repos/{repository_id}/characteristics/rebuild",
|
||||||
|
json={
|
||||||
|
"dry_run": True,
|
||||||
|
"source_path": str(source),
|
||||||
|
"use_llm_assistance": False,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert dry_run_response.status_code == 200
|
||||||
|
dry_run = dry_run_response.json()
|
||||||
|
assert dry_run["cleared_approved"] is False
|
||||||
|
assert dry_run["previous_counts"]["abilities"] == 1
|
||||||
|
assert dry_run["previous_ids"]["abilities"]
|
||||||
|
|
||||||
|
rejected_response = client.post(
|
||||||
|
f"/repos/{repository_id}/characteristics/rebuild",
|
||||||
|
json={
|
||||||
|
"dry_run": False,
|
||||||
|
"confirm": False,
|
||||||
|
"source_path": str(source),
|
||||||
|
"use_llm_assistance": False,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert rejected_response.status_code == 400
|
||||||
|
|
||||||
|
confirmed_response = client.post(
|
||||||
|
f"/repos/{repository_id}/characteristics/rebuild",
|
||||||
|
json={
|
||||||
|
"dry_run": False,
|
||||||
|
"confirm": True,
|
||||||
|
"source_path": str(source),
|
||||||
|
"use_llm_assistance": False,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert confirmed_response.status_code == 200
|
||||||
|
confirmed = confirmed_response.json()
|
||||||
|
assert confirmed["cleared_approved"] is True
|
||||||
|
assert client.get(f"/repos/{repository_id}/ability-map").json()["abilities"] == []
|
||||||
|
finally:
|
||||||
|
app.dependency_overrides.clear()
|
||||||
|
|
||||||
|
|
||||||
|
def test_ui_rebuild_characteristics_form_runs_dry_run(tmp_path):
|
||||||
|
source = tmp_path / "ui-rebuild"
|
||||||
|
source.mkdir()
|
||||||
|
(source / "README.md").write_text("# UI Rebuild\nReports status.\n", encoding="utf-8")
|
||||||
|
|
||||||
|
def override_settings():
|
||||||
|
return Settings(
|
||||||
|
database_path=str(tmp_path / "ui-rebuild.sqlite3"),
|
||||||
|
checkout_root=str(tmp_path / "ui-rebuild-checkouts"),
|
||||||
|
)
|
||||||
|
|
||||||
|
app.dependency_overrides[get_settings] = override_settings
|
||||||
|
client = TestClient(app)
|
||||||
|
try:
|
||||||
|
repository_id = client.post(
|
||||||
|
"/repos",
|
||||||
|
json={"name": "UI Rebuild", "url": str(source)},
|
||||||
|
).json()["id"]
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
f"/ui/repos/{repository_id}/characteristics/rebuild",
|
||||||
|
data={
|
||||||
|
"source_path": str(source),
|
||||||
|
"dry_run": "1",
|
||||||
|
"use_llm_assistance": "",
|
||||||
|
"use_cached_checkout": "",
|
||||||
|
},
|
||||||
|
follow_redirects=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 303
|
||||||
|
assert f"/ui/repos/{repository_id}/analysis-runs/" in response.headers["location"]
|
||||||
|
detail = client.get(response.headers["location"])
|
||||||
|
assert "dry_run_rebuild_characteristics_from_scratch" in detail.text
|
||||||
finally:
|
finally:
|
||||||
app.dependency_overrides.clear()
|
app.dependency_overrides.clear()
|
||||||
|
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ Acceptance criteria:
|
|||||||
|
|
||||||
```task
|
```task
|
||||||
id: RREG-WP-0009-T05
|
id: RREG-WP-0009-T05
|
||||||
status: todo
|
status: in_progress
|
||||||
priority: medium
|
priority: medium
|
||||||
state_hub_task_id: "d10d4bd7-4e5e-4efc-a724-b072fc53b8d2"
|
state_hub_task_id: "d10d4bd7-4e5e-4efc-a724-b072fc53b8d2"
|
||||||
```
|
```
|
||||||
@@ -168,7 +168,7 @@ Acceptance criteria:
|
|||||||
|
|
||||||
```task
|
```task
|
||||||
id: RREG-WP-0009-T06
|
id: RREG-WP-0009-T06
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "490b7926-9d03-4663-9d4d-9de9bbb9c755"
|
state_hub_task_id: "490b7926-9d03-4663-9d4d-9de9bbb9c755"
|
||||||
```
|
```
|
||||||
|
|||||||
Reference in New Issue
Block a user