generated from coulomb/repo-seed
Add trusted auto-approval migration inventory
This commit is contained in:
@@ -91,6 +91,9 @@ legacy path must be identifiable in review decisions and self-scoping
|
|||||||
assessment artifacts. They should be treated as review debt, not as evidence
|
assessment artifacts. They should be treated as review debt, not as evidence
|
||||||
that deterministic approval is allowed.
|
that deterministic approval is allowed.
|
||||||
|
|
||||||
|
The migration inventory and rebuild procedure are documented in
|
||||||
|
`docs/migrations/trusted-auto-approval.md`.
|
||||||
|
|
||||||
## Quality Criteria Relationship
|
## Quality Criteria Relationship
|
||||||
|
|
||||||
The quality criteria registry in `docs/quality-criteria/` defines the formal
|
The quality criteria registry in `docs/quality-criteria/` defines the formal
|
||||||
|
|||||||
62
docs/migrations/trusted-auto-approval.md
Normal file
62
docs/migrations/trusted-auto-approval.md
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
# Trusted Auto-Approval Migration
|
||||||
|
|
||||||
|
`trusted_auto_approve_candidate_graph` is historical migration behavior, not an
|
||||||
|
allowed acceptance path. Deterministic analysis may generate facts and
|
||||||
|
candidates, and deterministic quality gates may block or require review, but
|
||||||
|
approval now requires human judgement or configured agentic review.
|
||||||
|
|
||||||
|
## Identify Historical Runs
|
||||||
|
|
||||||
|
Use the inventory surfaces before rebuilding a repository with approved maps:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
repo-scoping list-legacy-auto-approvals --format json
|
||||||
|
```
|
||||||
|
|
||||||
|
The API exposes the same inventory at:
|
||||||
|
|
||||||
|
```text
|
||||||
|
GET /review/migrations/trusted-auto-approvals
|
||||||
|
```
|
||||||
|
|
||||||
|
Each record identifies the repository, analysis run, review decision, current
|
||||||
|
approved ability count, scanner version when available, and the recommended next
|
||||||
|
step. These records are derived from review decisions whose action is
|
||||||
|
`trusted_auto_approve_candidate_graph`.
|
||||||
|
|
||||||
|
## Rebuild Without Losing Audit History
|
||||||
|
|
||||||
|
Historical review decisions are retained. Rebuilding characteristics creates a
|
||||||
|
new analysis run and can clear the currently approved characteristic tree, but it
|
||||||
|
does not delete the old review-decision audit trail.
|
||||||
|
|
||||||
|
1. Run a dry run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
repo-scoping rebuild-characteristics --repo <repo-id> --dry-run --no-llm
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Inspect candidate output, quality-gate outcomes, and existing review
|
||||||
|
decisions.
|
||||||
|
|
||||||
|
3. Confirm the rebuild only when ready:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
repo-scoping rebuild-characteristics --repo <repo-id> --confirm --agentic-review
|
||||||
|
```
|
||||||
|
|
||||||
|
4. If no agentic reviewer is configured, complete human review through the
|
||||||
|
candidate graph approval/edit/reject flow.
|
||||||
|
|
||||||
|
## Compatibility Notes
|
||||||
|
|
||||||
|
- `AnalysisRunCreate.trusted_auto_approve` remains as a deprecated API input
|
||||||
|
for older callers, but requests are routed to agentic review and do not
|
||||||
|
deterministically approve candidates.
|
||||||
|
- The CLI does not expose deterministic trusted auto-approval. Use
|
||||||
|
`--agentic-review` during rebuild or approve after human review.
|
||||||
|
- The service method `trusted_auto_approve_candidate_graph()` is guarded by
|
||||||
|
`allow_deprecated_migration_mode=True` and should only be used to replay or
|
||||||
|
inspect historical migration behavior in controlled tests or migration tools.
|
||||||
|
- Self-scoping assessment artifacts continue to flag
|
||||||
|
`trusted_auto_approve_candidate_graph` as review debt.
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import json
|
||||||
|
from dataclasses import asdict
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Sequence
|
from typing import Sequence
|
||||||
|
|
||||||
@@ -171,6 +173,19 @@ def build_parser() -> argparse.ArgumentParser:
|
|||||||
default="markdown",
|
default="markdown",
|
||||||
help="Criteria output format.",
|
help="Criteria output format.",
|
||||||
)
|
)
|
||||||
|
legacy = subparsers.add_parser(
|
||||||
|
"list-legacy-auto-approvals",
|
||||||
|
help="List historical trusted deterministic auto-approval records.",
|
||||||
|
)
|
||||||
|
legacy.add_argument("--database-path", help="Override REPO_REGISTRY_DATABASE_PATH.")
|
||||||
|
legacy.add_argument("--checkout-root", help="Override REPO_REGISTRY_CHECKOUT_ROOT.")
|
||||||
|
legacy.add_argument("--output", help="Write inventory output to this path instead of stdout.")
|
||||||
|
legacy.add_argument(
|
||||||
|
"--format",
|
||||||
|
choices=["json", "markdown"],
|
||||||
|
default="markdown",
|
||||||
|
help="Inventory output format.",
|
||||||
|
)
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
|
|
||||||
@@ -187,6 +202,8 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|||||||
return self_assess_command(args, parser)
|
return self_assess_command(args, parser)
|
||||||
if args.command == "list-quality-criteria":
|
if args.command == "list-quality-criteria":
|
||||||
return list_quality_criteria_command(args)
|
return list_quality_criteria_command(args)
|
||||||
|
if args.command == "list-legacy-auto-approvals":
|
||||||
|
return list_legacy_auto_approvals_command(args)
|
||||||
parser.error(f"unknown command: {args.command}")
|
parser.error(f"unknown command: {args.command}")
|
||||||
return 2
|
return 2
|
||||||
|
|
||||||
@@ -254,6 +271,39 @@ def list_quality_criteria_command(args: argparse.Namespace) -> int:
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def list_legacy_auto_approvals_command(args: argparse.Namespace) -> int:
|
||||||
|
service = service_from_args(args)
|
||||||
|
records = service.list_trusted_auto_approval_migration_records()
|
||||||
|
if args.format == "json":
|
||||||
|
content = json.dumps([asdict(record) for record in records], indent=2) + "\n"
|
||||||
|
else:
|
||||||
|
content = legacy_auto_approval_records_markdown(records)
|
||||||
|
if args.output:
|
||||||
|
write_text(args.output, content)
|
||||||
|
else:
|
||||||
|
print(content, end="" if content.endswith("\n") else "\n")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def legacy_auto_approval_records_markdown(records) -> str:
|
||||||
|
if not records:
|
||||||
|
return "No legacy trusted auto-approval records found.\n"
|
||||||
|
lines = ["# Legacy Trusted Auto-Approval Records", ""]
|
||||||
|
for record in records:
|
||||||
|
lines.extend(
|
||||||
|
[
|
||||||
|
(
|
||||||
|
f"- repo={record.repository_id}:{record.repository_name} "
|
||||||
|
f"run={record.analysis_run_id} decision={record.review_decision_id}"
|
||||||
|
),
|
||||||
|
f" status={record.analysis_run_status} scanner={record.scanner_version or 'unknown'}",
|
||||||
|
f" approved_abilities={record.current_approved_ability_count}",
|
||||||
|
f" next={record.recommended_next_step}",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
return "\n".join(lines) + "\n"
|
||||||
|
|
||||||
|
|
||||||
def self_assess_command(
|
def self_assess_command(
|
||||||
args: argparse.Namespace,
|
args: argparse.Namespace,
|
||||||
parser: argparse.ArgumentParser,
|
parser: argparse.ArgumentParser,
|
||||||
|
|||||||
@@ -63,6 +63,22 @@ class ReviewDecision:
|
|||||||
decision_kind: str = "other"
|
decision_kind: str = "other"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class TrustedAutoApprovalMigrationRecord:
|
||||||
|
repository_id: int
|
||||||
|
repository_name: str
|
||||||
|
repository_url: str
|
||||||
|
repository_status: str
|
||||||
|
analysis_run_id: int | None
|
||||||
|
analysis_run_status: str
|
||||||
|
scanner_version: str
|
||||||
|
review_decision_id: int
|
||||||
|
decision_created_at: str
|
||||||
|
notes: str
|
||||||
|
current_approved_ability_count: int
|
||||||
|
recommended_next_step: str
|
||||||
|
|
||||||
|
|
||||||
def enrich_review_decision(decision: ReviewDecision) -> ReviewDecision:
|
def enrich_review_decision(decision: ReviewDecision) -> ReviewDecision:
|
||||||
fields = review_decision_audit_fields(decision.action, decision.notes)
|
fields = review_decision_audit_fields(decision.action, decision.notes)
|
||||||
return replace_review_decision(decision, **fields)
|
return replace_review_decision(decision, **fields)
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ from repo_registry.core.models import (
|
|||||||
ReviewDecision,
|
ReviewDecision,
|
||||||
ScanSummary,
|
ScanSummary,
|
||||||
SearchResult,
|
SearchResult,
|
||||||
|
TrustedAutoApprovalMigrationRecord,
|
||||||
enrich_review_decision,
|
enrich_review_decision,
|
||||||
)
|
)
|
||||||
from repo_registry.candidate_graph.generator import CandidateGraphGenerator
|
from repo_registry.candidate_graph.generator import CandidateGraphGenerator
|
||||||
@@ -52,7 +53,7 @@ from repo_registry.repo_ingestion.git import GitIngestionService
|
|||||||
from repo_registry.repo_ingestion.metadata import RepositoryMetadataExtractor
|
from repo_registry.repo_ingestion.metadata import RepositoryMetadataExtractor
|
||||||
from repo_registry.repo_scanning.scanner import DeterministicScanner
|
from repo_registry.repo_scanning.scanner import DeterministicScanner
|
||||||
from repo_registry.semantic import EmbeddingProvider, cosine_similarity
|
from repo_registry.semantic import EmbeddingProvider, cosine_similarity
|
||||||
from repo_registry.storage.sqlite import RegistryStore
|
from repo_registry.storage.sqlite import NotFoundError, RegistryStore
|
||||||
|
|
||||||
|
|
||||||
class RegistryService:
|
class RegistryService:
|
||||||
@@ -426,6 +427,54 @@ class RegistryService:
|
|||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def list_trusted_auto_approval_migration_records(
|
||||||
|
self,
|
||||||
|
) -> list[TrustedAutoApprovalMigrationRecord]:
|
||||||
|
records: list[TrustedAutoApprovalMigrationRecord] = []
|
||||||
|
for repository in self.list_repositories():
|
||||||
|
approved_ability_count = len(self.ability_map(repository.id).abilities)
|
||||||
|
for decision in self.list_review_decisions(repository.id):
|
||||||
|
if decision.action != "trusted_auto_approve_candidate_graph":
|
||||||
|
continue
|
||||||
|
run_status = "unknown"
|
||||||
|
scanner_version = ""
|
||||||
|
if decision.analysis_run_id is not None:
|
||||||
|
try:
|
||||||
|
run = self.get_analysis_run(
|
||||||
|
repository.id,
|
||||||
|
decision.analysis_run_id,
|
||||||
|
)
|
||||||
|
except NotFoundError:
|
||||||
|
run_status = "missing"
|
||||||
|
else:
|
||||||
|
run_status = run.status
|
||||||
|
scanner_version = run.scanner_version
|
||||||
|
records.append(
|
||||||
|
TrustedAutoApprovalMigrationRecord(
|
||||||
|
repository_id=repository.id,
|
||||||
|
repository_name=repository.name,
|
||||||
|
repository_url=repository.url,
|
||||||
|
repository_status=repository.status,
|
||||||
|
analysis_run_id=decision.analysis_run_id,
|
||||||
|
analysis_run_status=run_status,
|
||||||
|
scanner_version=scanner_version,
|
||||||
|
review_decision_id=decision.id,
|
||||||
|
decision_created_at=decision.created_at,
|
||||||
|
notes=decision.notes,
|
||||||
|
current_approved_ability_count=approved_ability_count,
|
||||||
|
recommended_next_step=(
|
||||||
|
"Dry-run rebuild-characteristics, then confirm a "
|
||||||
|
"rebuild with manual or agentic review before "
|
||||||
|
"publishing replacement registry truth."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return sorted(
|
||||||
|
records,
|
||||||
|
key=lambda record: (record.decision_created_at, record.review_decision_id),
|
||||||
|
reverse=True,
|
||||||
|
)
|
||||||
|
|
||||||
def record_expectation_gap(
|
def record_expectation_gap(
|
||||||
self,
|
self,
|
||||||
repository_id: int,
|
repository_id: int,
|
||||||
@@ -626,7 +675,15 @@ class RegistryService:
|
|||||||
analysis_run_id: int,
|
analysis_run_id: int,
|
||||||
*,
|
*,
|
||||||
notes: str = "",
|
notes: str = "",
|
||||||
|
allow_deprecated_migration_mode: bool = False,
|
||||||
) -> RepositoryAbilityMap:
|
) -> RepositoryAbilityMap:
|
||||||
|
if not allow_deprecated_migration_mode:
|
||||||
|
raise ValueError(
|
||||||
|
"trusted deterministic auto-approval is deprecated. Use "
|
||||||
|
"request_agentic_review() or approve_candidate_graph() after "
|
||||||
|
"human review; pass allow_deprecated_migration_mode=True only "
|
||||||
|
"when replaying historical migration behavior."
|
||||||
|
)
|
||||||
graph = self.store.get_candidate_graph(repository_id, analysis_run_id)
|
graph = self.store.get_candidate_graph(repository_id, analysis_run_id)
|
||||||
approved_count = 0
|
approved_count = 0
|
||||||
skipped_count = 0
|
skipped_count = 0
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ from repo_registry.web_api.schemas import (
|
|||||||
ReviewDecisionResponse,
|
ReviewDecisionResponse,
|
||||||
ScanSummaryResponse,
|
ScanSummaryResponse,
|
||||||
SearchResultResponse,
|
SearchResultResponse,
|
||||||
|
TrustedAutoApprovalMigrationRecordResponse,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -208,6 +209,20 @@ def list_quality_criteria() -> dict[str, object]:
|
|||||||
return criteria_registry_dict(load_quality_criteria())
|
return criteria_registry_dict(load_quality_criteria())
|
||||||
|
|
||||||
|
|
||||||
|
@app.get(
|
||||||
|
"/review/migrations/trusted-auto-approvals",
|
||||||
|
tags=["review"],
|
||||||
|
response_model=list[TrustedAutoApprovalMigrationRecordResponse],
|
||||||
|
)
|
||||||
|
def list_trusted_auto_approval_migration_records(
|
||||||
|
service: RegistryService = Depends(get_service),
|
||||||
|
) -> list[dict[str, object]]:
|
||||||
|
return [
|
||||||
|
asdict(record)
|
||||||
|
for record in service.list_trusted_auto_approval_migration_records()
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
@app.post(
|
@app.post(
|
||||||
"/repos",
|
"/repos",
|
||||||
status_code=201,
|
status_code=201,
|
||||||
|
|||||||
@@ -524,6 +524,21 @@ class ReviewDecisionResponse(BaseModel):
|
|||||||
decision_kind: str = "other"
|
decision_kind: str = "other"
|
||||||
|
|
||||||
|
|
||||||
|
class TrustedAutoApprovalMigrationRecordResponse(BaseModel):
|
||||||
|
repository_id: int
|
||||||
|
repository_name: str
|
||||||
|
repository_url: str
|
||||||
|
repository_status: str
|
||||||
|
analysis_run_id: int | None
|
||||||
|
analysis_run_status: str
|
||||||
|
scanner_version: str
|
||||||
|
review_decision_id: int
|
||||||
|
decision_created_at: str
|
||||||
|
notes: str
|
||||||
|
current_approved_ability_count: int
|
||||||
|
recommended_next_step: str
|
||||||
|
|
||||||
|
|
||||||
class QualityCriterionResponse(BaseModel):
|
class QualityCriterionResponse(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
title: str
|
title: str
|
||||||
|
|||||||
@@ -182,6 +182,40 @@ def test_list_quality_criteria_cli_writes_json(tmp_path):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_legacy_auto_approvals_cli_writes_json_inventory(tmp_path):
|
||||||
|
service = make_service(tmp_path)
|
||||||
|
source = write_repo(tmp_path)
|
||||||
|
repository = service.register_repository(name="Legacy CLI", url=str(source))
|
||||||
|
summary = service.analyze_repository(repository.id, use_llm_assistance=False)
|
||||||
|
service.trusted_auto_approve_candidate_graph(
|
||||||
|
repository.id,
|
||||||
|
summary.analysis_run.id,
|
||||||
|
allow_deprecated_migration_mode=True,
|
||||||
|
)
|
||||||
|
output_path = tmp_path / "legacy-auto-approvals.json"
|
||||||
|
|
||||||
|
exit_code = main(
|
||||||
|
[
|
||||||
|
"list-legacy-auto-approvals",
|
||||||
|
"--format",
|
||||||
|
"json",
|
||||||
|
"--output",
|
||||||
|
str(output_path),
|
||||||
|
"--database-path",
|
||||||
|
str(tmp_path / "registry.sqlite3"),
|
||||||
|
"--checkout-root",
|
||||||
|
str(tmp_path / "checkouts"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
records = json.loads(output_path.read_text(encoding="utf-8"))
|
||||||
|
assert exit_code == 0
|
||||||
|
assert records[0]["repository_id"] == repository.id
|
||||||
|
assert records[0]["repository_name"] == "Legacy CLI"
|
||||||
|
assert records[0]["analysis_run_id"] == summary.analysis_run.id
|
||||||
|
assert records[0]["current_approved_ability_count"] == 1
|
||||||
|
|
||||||
|
|
||||||
def test_self_assess_cli_exports_challenger_and_comparison(tmp_path):
|
def test_self_assess_cli_exports_challenger_and_comparison(tmp_path):
|
||||||
source = write_repo(tmp_path)
|
source = write_repo(tmp_path)
|
||||||
golden_path = tmp_path / "golden.json"
|
golden_path = tmp_path / "golden.json"
|
||||||
|
|||||||
65
tests/test_trusted_auto_approval_migration.py
Normal file
65
tests/test_trusted_auto_approval_migration.py
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
from repo_registry.core.service import RegistryService
|
||||||
|
from repo_registry.repo_ingestion.git import GitIngestionService
|
||||||
|
from repo_registry.storage.sqlite import RegistryStore
|
||||||
|
|
||||||
|
|
||||||
|
def make_service(tmp_path):
|
||||||
|
store = RegistryStore(tmp_path / "registry.sqlite3")
|
||||||
|
store.initialize()
|
||||||
|
return RegistryService(store, ingestion=GitIngestionService(tmp_path / "checkouts"))
|
||||||
|
|
||||||
|
|
||||||
|
def write_api_repo(tmp_path):
|
||||||
|
source = tmp_path / "api-repo"
|
||||||
|
source.mkdir()
|
||||||
|
(source / "README.md").write_text("# API Repo\nReports health.\n", encoding="utf-8")
|
||||||
|
(source / "app.py").write_text('@app.get("/health")\ndef health():\n return {}\n', encoding="utf-8")
|
||||||
|
return source
|
||||||
|
|
||||||
|
|
||||||
|
def test_trusted_auto_approval_requires_explicit_migration_mode(tmp_path):
|
||||||
|
service = make_service(tmp_path)
|
||||||
|
repository = service.register_repository(
|
||||||
|
name="Legacy Guard Repo",
|
||||||
|
url=str(write_api_repo(tmp_path)),
|
||||||
|
)
|
||||||
|
summary = service.analyze_repository(repository.id, use_llm_assistance=False)
|
||||||
|
|
||||||
|
try:
|
||||||
|
service.trusted_auto_approve_candidate_graph(
|
||||||
|
repository.id,
|
||||||
|
summary.analysis_run.id,
|
||||||
|
)
|
||||||
|
except ValueError as exc:
|
||||||
|
assert "deprecated" in str(exc)
|
||||||
|
else:
|
||||||
|
raise AssertionError("trusted auto-approval should be guarded by default")
|
||||||
|
|
||||||
|
assert service.ability_map(repository.id).abilities == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_legacy_auto_approval_inventory_identifies_review_debt(tmp_path):
|
||||||
|
service = make_service(tmp_path)
|
||||||
|
repository = service.register_repository(
|
||||||
|
name="Legacy Inventory Repo",
|
||||||
|
url=str(write_api_repo(tmp_path)),
|
||||||
|
)
|
||||||
|
summary = service.analyze_repository(repository.id, use_llm_assistance=False)
|
||||||
|
|
||||||
|
service.trusted_auto_approve_candidate_graph(
|
||||||
|
repository.id,
|
||||||
|
summary.analysis_run.id,
|
||||||
|
notes="Historical migration replay.",
|
||||||
|
allow_deprecated_migration_mode=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
records = service.list_trusted_auto_approval_migration_records()
|
||||||
|
assert len(records) == 1
|
||||||
|
record = records[0]
|
||||||
|
assert record.repository_id == repository.id
|
||||||
|
assert record.repository_name == "Legacy Inventory Repo"
|
||||||
|
assert record.analysis_run_id == summary.analysis_run.id
|
||||||
|
assert record.review_decision_id > 0
|
||||||
|
assert record.analysis_run_status == "completed"
|
||||||
|
assert record.current_approved_ability_count == 1
|
||||||
|
assert "rebuild" in record.recommended_next_step
|
||||||
@@ -3,6 +3,9 @@ import sqlite3
|
|||||||
|
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from repo_registry.core.service import RegistryService
|
||||||
|
from repo_registry.repo_ingestion.git import GitIngestionService
|
||||||
|
from repo_registry.storage.sqlite import RegistryStore
|
||||||
from repo_registry.web_api import app as app_module
|
from repo_registry.web_api import app as app_module
|
||||||
from repo_registry.web_api.app import Settings, app, get_service, get_settings
|
from repo_registry.web_api.app import Settings, app, get_service, get_settings
|
||||||
|
|
||||||
@@ -191,6 +194,48 @@ def test_quality_gate_override_api_records_auditable_override(tmp_path):
|
|||||||
app.dependency_overrides.clear()
|
app.dependency_overrides.clear()
|
||||||
|
|
||||||
|
|
||||||
|
def test_trusted_auto_approval_migration_inventory_api(tmp_path):
|
||||||
|
source = tmp_path / "legacy-api-repo"
|
||||||
|
source.mkdir()
|
||||||
|
(source / "README.md").write_text("# Legacy API Repo\nReports health.\n", encoding="utf-8")
|
||||||
|
(source / "app.py").write_text('@app.get("/health")\ndef health():\n return {}\n', encoding="utf-8")
|
||||||
|
database_path = str(tmp_path / "legacy-auto-approval.sqlite3")
|
||||||
|
store = RegistryStore(database_path)
|
||||||
|
store.initialize()
|
||||||
|
service = RegistryService(
|
||||||
|
store,
|
||||||
|
ingestion=GitIngestionService(tmp_path / "checkouts"),
|
||||||
|
)
|
||||||
|
repository = service.register_repository(name="Legacy API Repo", url=str(source))
|
||||||
|
summary = service.analyze_repository(repository.id, use_llm_assistance=False)
|
||||||
|
service.trusted_auto_approve_candidate_graph(
|
||||||
|
repository.id,
|
||||||
|
summary.analysis_run.id,
|
||||||
|
allow_deprecated_migration_mode=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def override_settings():
|
||||||
|
return Settings(
|
||||||
|
database_path=database_path,
|
||||||
|
checkout_root=str(tmp_path / "checkouts"),
|
||||||
|
)
|
||||||
|
|
||||||
|
app.dependency_overrides[get_settings] = override_settings
|
||||||
|
client = TestClient(app)
|
||||||
|
try:
|
||||||
|
response = client.get("/review/migrations/trusted-auto-approvals")
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
records = response.json()
|
||||||
|
assert records[0]["repository_id"] == repository.id
|
||||||
|
assert records[0]["repository_name"] == "Legacy API Repo"
|
||||||
|
assert records[0]["analysis_run_id"] == summary.analysis_run.id
|
||||||
|
assert records[0]["current_approved_ability_count"] == 1
|
||||||
|
assert "rebuild" in records[0]["recommended_next_step"]
|
||||||
|
finally:
|
||||||
|
app.dependency_overrides.clear()
|
||||||
|
|
||||||
|
|
||||||
def test_openapi_contract_snapshot_for_stable_agent_paths():
|
def test_openapi_contract_snapshot_for_stable_agent_paths():
|
||||||
client = TestClient(app)
|
client = TestClient(app)
|
||||||
|
|
||||||
@@ -246,6 +291,12 @@ def test_openapi_contract_snapshot_for_stable_agent_paths():
|
|||||||
"success_schema": "QualityCriteriaRegistryResponse",
|
"success_schema": "QualityCriteriaRegistryResponse",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/review/migrations/trusted-auto-approvals": {
|
||||||
|
"get": {
|
||||||
|
"tags": ["review"],
|
||||||
|
"success_schema": "list[TrustedAutoApprovalMigrationRecordResponse]",
|
||||||
|
}
|
||||||
|
},
|
||||||
"/repos": {
|
"/repos": {
|
||||||
"get": {"tags": ["repositories"], "success_schema": "list[RepositoryResponse]"},
|
"get": {"tags": ["repositories"], "success_schema": "list[RepositoryResponse]"},
|
||||||
"post": {"tags": ["repositories"], "success_schema": "RepositoryResponse"},
|
"post": {"tags": ["repositories"], "success_schema": "RepositoryResponse"},
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ type: workplan
|
|||||||
title: "Agentic Characteristic Acceptance"
|
title: "Agentic Characteristic Acceptance"
|
||||||
domain: capabilities
|
domain: capabilities
|
||||||
repo: repo-scoping
|
repo: repo-scoping
|
||||||
status: active
|
status: done
|
||||||
owner: codex
|
owner: codex
|
||||||
topic_slug: foerster-capabilities
|
topic_slug: foerster-capabilities
|
||||||
created: "2026-05-15"
|
created: "2026-05-15"
|
||||||
@@ -258,7 +258,7 @@ metadata, and manual approval still produces human review decisions.
|
|||||||
|
|
||||||
```task
|
```task
|
||||||
id: RREG-WP-0014-T08
|
id: RREG-WP-0014-T08
|
||||||
status: todo
|
status: done
|
||||||
priority: medium
|
priority: medium
|
||||||
state_hub_task_id: "3d5475f6-71a7-4ca7-aa69-573e91d1fe1e"
|
state_hub_task_id: "3d5475f6-71a7-4ca7-aa69-573e91d1fe1e"
|
||||||
```
|
```
|
||||||
@@ -272,6 +272,16 @@ Acceptance criteria:
|
|||||||
- The old behavior is either removed or guarded behind an explicit deprecated
|
- The old behavior is either removed or guarded behind an explicit deprecated
|
||||||
migration mode that cannot run by default.
|
migration mode that cannot run by default.
|
||||||
|
|
||||||
|
Implementation note 2026-05-15: added a guarded migration compatibility layer
|
||||||
|
for historical `trusted_auto_approve_candidate_graph` records. The service now
|
||||||
|
requires `allow_deprecated_migration_mode=True` before replaying the legacy
|
||||||
|
auto-approval method, exposes an inventory of legacy auto-approval review debt,
|
||||||
|
and documents the dry-run/rebuild/re-review path in
|
||||||
|
`docs/migrations/trusted-auto-approval.md`. Added
|
||||||
|
`repo-scoping list-legacy-auto-approvals` and
|
||||||
|
`GET /review/migrations/trusted-auto-approvals` so existing maps produced by the
|
||||||
|
legacy path can be identified before rebuilds.
|
||||||
|
|
||||||
## Completion Criteria
|
## Completion Criteria
|
||||||
|
|
||||||
- Deterministic rules no longer approve candidate characteristics.
|
- Deterministic rules no longer approve candidate characteristics.
|
||||||
|
|||||||
Reference in New Issue
Block a user