Add trusted auto-approval migration inventory

This commit is contained in:
2026-05-15 16:56:42 +02:00
parent a2c8ba9442
commit de8d184a4b
11 changed files with 381 additions and 3 deletions

View File

@@ -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
that deterministic approval is allowed.
The migration inventory and rebuild procedure are documented in
`docs/migrations/trusted-auto-approval.md`.
## Quality Criteria Relationship
The quality criteria registry in `docs/quality-criteria/` defines the formal

View 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.

View File

@@ -1,6 +1,8 @@
from __future__ import annotations
import argparse
import json
from dataclasses import asdict
from pathlib import Path
from typing import Sequence
@@ -171,6 +173,19 @@ def build_parser() -> argparse.ArgumentParser:
default="markdown",
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
@@ -187,6 +202,8 @@ def main(argv: Sequence[str] | None = None) -> int:
return self_assess_command(args, parser)
if args.command == "list-quality-criteria":
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}")
return 2
@@ -254,6 +271,39 @@ def list_quality_criteria_command(args: argparse.Namespace) -> int:
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(
args: argparse.Namespace,
parser: argparse.ArgumentParser,

View File

@@ -63,6 +63,22 @@ class ReviewDecision:
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:
fields = review_decision_audit_fields(decision.action, decision.notes)
return replace_review_decision(decision, **fields)

View File

@@ -40,6 +40,7 @@ from repo_registry.core.models import (
ReviewDecision,
ScanSummary,
SearchResult,
TrustedAutoApprovalMigrationRecord,
enrich_review_decision,
)
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_scanning.scanner import DeterministicScanner
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:
@@ -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(
self,
repository_id: int,
@@ -626,7 +675,15 @@ class RegistryService:
analysis_run_id: int,
*,
notes: str = "",
allow_deprecated_migration_mode: bool = False,
) -> 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)
approved_count = 0
skipped_count = 0

View File

@@ -75,6 +75,7 @@ from repo_registry.web_api.schemas import (
ReviewDecisionResponse,
ScanSummaryResponse,
SearchResultResponse,
TrustedAutoApprovalMigrationRecordResponse,
)
@@ -208,6 +209,20 @@ def list_quality_criteria() -> dict[str, object]:
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(
"/repos",
status_code=201,

View File

@@ -524,6 +524,21 @@ class ReviewDecisionResponse(BaseModel):
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):
id: str
title: str

View File

@@ -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):
source = write_repo(tmp_path)
golden_path = tmp_path / "golden.json"

View 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

View File

@@ -3,6 +3,9 @@ import sqlite3
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.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()
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():
client = TestClient(app)
@@ -246,6 +291,12 @@ def test_openapi_contract_snapshot_for_stable_agent_paths():
"success_schema": "QualityCriteriaRegistryResponse",
}
},
"/review/migrations/trusted-auto-approvals": {
"get": {
"tags": ["review"],
"success_schema": "list[TrustedAutoApprovalMigrationRecordResponse]",
}
},
"/repos": {
"get": {"tags": ["repositories"], "success_schema": "list[RepositoryResponse]"},
"post": {"tags": ["repositories"], "success_schema": "RepositoryResponse"},

View File

@@ -4,7 +4,7 @@ type: workplan
title: "Agentic Characteristic Acceptance"
domain: capabilities
repo: repo-scoping
status: active
status: done
owner: codex
topic_slug: foerster-capabilities
created: "2026-05-15"
@@ -258,7 +258,7 @@ metadata, and manual approval still produces human review decisions.
```task
id: RREG-WP-0014-T08
status: todo
status: done
priority: medium
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
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
- Deterministic rules no longer approve candidate characteristics.