chore(consistency): sync task status from DB [auto]

Updated by fix-consistency on 2026-05-15:
  - update .custodian-brief.md for repo-scoping
This commit is contained in:
2026-05-15 21:14:21 +02:00
parent f38ed6847c
commit 084159e51c
42 changed files with 5 additions and 5 deletions

473
src/repo_scoping/cli.py Normal file
View File

@@ -0,0 +1,473 @@
from __future__ import annotations
import argparse
import json
from dataclasses import asdict
from pathlib import Path
from typing import Sequence
from repo_registry.acceptance import (
criteria_registry_json,
criteria_registry_markdown,
load_quality_criteria,
)
from repo_registry.core.models import CharacteristicRebuildResult, Repository
from repo_registry.core.service import RegistryService
from repo_registry.llm_extraction import LLMCandidateExtractor, create_llm_connect_adapter
from repo_registry.repo_ingestion.git import GitIngestionService
from repo_registry.self_scoping.assessment import artifact_json, export_assessment_artifact
from repo_registry.self_scoping.comparison import (
compare_assessment_to_golden,
comparison_json,
comparison_markdown,
load_json,
)
from repo_registry.storage.sqlite import NotFoundError, RegistryStore
from repo_registry.web_api.app import Settings
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog="repo-scoping",
description="Repository Scoping maintenance commands.",
)
subparsers = parser.add_subparsers(dest="command", required=True)
rebuild = subparsers.add_parser(
"rebuild-characteristics",
help="Rebuild candidate characteristics for one or more repositories.",
)
target = rebuild.add_mutually_exclusive_group(required=True)
target.add_argument("--repo", help="Repository id or exact repository name.")
target.add_argument("--all", action="store_true", help="Rebuild every repository.")
rebuild.add_argument("--dry-run", action="store_true", help="Preview without clearing approved characteristics.")
rebuild.add_argument("--no-llm", action="store_true", help="Disable configured LLM assistance.")
rebuild.add_argument(
"--agentic-review",
action="store_true",
help="Request configured agentic review after a confirmed rebuild.",
)
rebuild.add_argument(
"--confirm",
action="store_true",
help="Confirm a destructive rebuild for selected repositories.",
)
rebuild.add_argument(
"--confirm-all",
action="store_true",
help="Confirm a destructive all-repository rebuild.",
)
rebuild.add_argument("--database-path", help="Override REPO_REGISTRY_DATABASE_PATH.")
rebuild.add_argument("--checkout-root", help="Override REPO_REGISTRY_CHECKOUT_ROOT.")
export = subparsers.add_parser(
"export-assessment",
help="Export a completed analysis run as a self-scoping assessment artifact.",
)
export.add_argument("--repo", required=True, help="Repository id or exact repository name.")
export.add_argument("--analysis-run", type=int, required=True, help="Completed analysis run id.")
export.add_argument("--output", help="Write artifact JSON to this path instead of stdout.")
export.add_argument(
"--role",
choices=["baseline", "challenger", "negative_regression_seed"],
default="challenger",
help="Assessment artifact role.",
)
export.add_argument(
"--outcome",
choices=[
"baseline",
"challenger",
"preferred",
"tied",
"rejected",
"superseded",
"needs-human",
],
default="challenger",
help="Initial assessment outcome.",
)
export.add_argument("--reviewer", default="codex", help="Reviewer name recorded in the artifact.")
export.add_argument("--summary", help="Assessment summary override.")
export.add_argument("--database-path", help="Override REPO_REGISTRY_DATABASE_PATH.")
export.add_argument("--checkout-root", help="Override REPO_REGISTRY_CHECKOUT_ROOT.")
compare = subparsers.add_parser(
"compare-assessment",
help="Compare a self-scoping assessment artifact against a golden profile.",
)
compare.add_argument("--golden", required=True, help="Golden profile JSON path.")
compare.add_argument(
"--assessment",
required=True,
help="Assessment artifact JSON path.",
)
compare.add_argument("--output", help="Write comparison report to this path instead of stdout.")
compare.add_argument(
"--format",
choices=["json", "markdown"],
default="markdown",
help="Comparison report format.",
)
self_assess = subparsers.add_parser(
"self-assess",
help="Run repo-scoping against a source tree and compare the result to a golden profile.",
)
self_assess.add_argument(
"--repo",
default="repo-scoping",
help="Repository id or exact repository name to reuse; created by name when absent.",
)
self_assess.add_argument(
"--source-path",
default=".",
help="Source tree to analyze; defaults to the current working directory.",
)
self_assess.add_argument(
"--golden",
default="docs/self-scoping/golden/repo-scoping-golden-profile.v1.json",
help="Golden profile JSON path.",
)
self_assess.add_argument(
"--assessment-output",
help="Write challenger assessment artifact JSON to this path.",
)
self_assess.add_argument(
"--comparison-output",
help="Write comparison report to this path instead of stdout.",
)
self_assess.add_argument(
"--format",
choices=["json", "markdown"],
default="markdown",
help="Comparison report format.",
)
self_assess.add_argument(
"--with-llm",
action="store_false",
dest="no_llm",
help="Use configured LLM assistance during the self-assessment run.",
)
self_assess.add_argument(
"--agentic-review",
action="store_true",
help="Request configured agentic review; leaves candidates pending when none is configured.",
)
self_assess.add_argument(
"--fail-on-regression",
action="store_true",
help="Return exit code 1 only when comparison status is regression.",
)
self_assess.add_argument("--database-path", help="Override REPO_REGISTRY_DATABASE_PATH.")
self_assess.add_argument("--checkout-root", help="Override REPO_REGISTRY_CHECKOUT_ROOT.")
self_assess.set_defaults(no_llm=True)
criteria = subparsers.add_parser(
"list-quality-criteria",
help="List the active characteristic quality criteria registry.",
)
criteria.add_argument(
"--criteria-path",
help="Override the default quality criteria registry JSON path.",
)
criteria.add_argument("--output", help="Write criteria output to this path instead of stdout.")
criteria.add_argument(
"--format",
choices=["json", "markdown"],
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
def main(argv: Sequence[str] | None = None) -> int:
parser = build_parser()
args = parser.parse_args(argv)
if args.command == "rebuild-characteristics":
return rebuild_characteristics_command(args, parser)
if args.command == "export-assessment":
return export_assessment_command(args, parser)
if args.command == "compare-assessment":
return compare_assessment_command(args)
if args.command == "self-assess":
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
def rebuild_characteristics_command(
args: argparse.Namespace,
parser: argparse.ArgumentParser,
) -> int:
dry_run = bool(args.dry_run)
if not dry_run and args.all and not args.confirm_all:
parser.error("--all destructive rebuilds require --confirm-all")
if not dry_run and not (args.confirm or args.confirm_all):
parser.error("destructive rebuilds require --confirm or --confirm-all")
service = service_from_args(args)
repositories = selected_repositories(service, args)
if not repositories:
parser.error("no repositories matched the requested target")
for repository in repositories:
result = service.rebuild_characteristics_from_scratch(
repository.id,
dry_run=dry_run,
confirm=not dry_run,
use_llm_assistance=not args.no_llm,
)
if args.agentic_review and not dry_run and result.analysis_run.status == "completed":
service.request_agentic_review(
repository.id,
result.analysis_run.id,
notes="CLI agentic review request after rebuild.",
)
print(rebuild_summary_line(service, result, args))
return 0
def compare_assessment_command(args: argparse.Namespace) -> int:
comparison = compare_assessment_to_golden(
load_json(args.golden),
load_json(args.assessment),
)
content = (
comparison_json(comparison)
if args.format == "json"
else comparison_markdown(comparison)
)
if args.output:
write_text(args.output, content)
else:
print(content, end="" if content.endswith("\n") else "\n")
return 0
def list_quality_criteria_command(args: argparse.Namespace) -> int:
registry = load_quality_criteria(args.criteria_path)
content = (
criteria_registry_json(registry)
if args.format == "json"
else criteria_registry_markdown(registry)
)
if args.output:
write_text(args.output, content)
else:
print(content, end="" if content.endswith("\n") else "\n")
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,
) -> int:
service = service_from_args(args)
source_path = Path(args.source_path).expanduser().resolve()
if not source_path.is_dir():
parser.error(f"source path does not exist or is not a directory: {source_path}")
repository = self_assessment_repository(service, args.repo, source_path)
summary = service.analyze_repository(
repository.id,
source_path=str(source_path),
use_llm_assistance=not args.no_llm,
agentic_review=args.agentic_review,
trusted_auto_approve=False,
)
if summary.analysis_run.status != "completed":
parser.error(summary.analysis_run.error_message or "analysis failed")
artifact = export_assessment_artifact(
service,
repository.id,
summary.analysis_run.id,
role="challenger",
outcome="challenger",
reviewer="self-assess",
)
comparison = compare_assessment_to_golden(load_json(args.golden), artifact)
if args.assessment_output:
write_text(args.assessment_output, artifact_json(artifact))
report = (
comparison_json(comparison)
if args.format == "json"
else comparison_markdown(comparison)
)
if args.comparison_output:
write_text(args.comparison_output, report)
else:
print(report, end="" if report.endswith("\n") else "\n")
if args.fail_on_regression and comparison["status"] == "regression":
return 1
return 0
def export_assessment_command(
args: argparse.Namespace,
parser: argparse.ArgumentParser,
) -> int:
service = service_from_args(args)
repositories = selected_repositories(service, args)
if not repositories:
parser.error("no repositories matched the requested target")
if len(repositories) > 1:
parser.error("assessment export requires exactly one repository")
repository = repositories[0]
try:
artifact = export_assessment_artifact(
service,
repository.id,
args.analysis_run,
role=args.role,
outcome=args.outcome,
reviewer=args.reviewer,
summary=args.summary,
)
except (NotFoundError, ValueError) as exc:
parser.error(str(exc))
content = artifact_json(artifact)
if args.output:
write_text(args.output, content)
else:
print(content, end="")
return 0
def service_from_args(args: argparse.Namespace) -> RegistryService:
settings = Settings()
database_path = Path(args.database_path or settings.database_path)
checkout_root = args.checkout_root or settings.checkout_root
database_path.parent.mkdir(parents=True, exist_ok=True)
store = RegistryStore(database_path)
store.initialize()
llm_extractor = None
no_llm = getattr(args, "no_llm", True)
if not no_llm and settings.llm_enabled and settings.llm_provider:
adapter = create_llm_connect_adapter(settings.llm_provider, model=settings.llm_model)
llm_extractor = LLMCandidateExtractor(adapter)
return RegistryService(
store,
ingestion=GitIngestionService(checkout_root),
llm_extractor=llm_extractor,
)
def selected_repositories(
service: RegistryService,
args: argparse.Namespace,
) -> list[Repository]:
repositories = service.list_repositories()
if getattr(args, "all", False):
return repositories
repo = str(args.repo)
if repo.isdigit():
try:
return [service.get_repository(int(repo))]
except NotFoundError:
return []
return [repository for repository in repositories if repository.name == repo]
def self_assessment_repository(
service: RegistryService,
repo: str,
source_path: Path,
) -> Repository:
selected = selected_repositories(service, argparse.Namespace(repo=repo, all=False))
if selected:
return selected[0]
if repo.isdigit():
raise NotFoundError(f"repository {repo} was not found")
return service.register_repository(
name=repo,
url=str(source_path),
description="Self-scoping assessment target.",
)
def write_text(path: str | Path, content: str) -> None:
target = Path(path)
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text(content, encoding="utf-8")
def rebuild_summary_line(
service: RegistryService,
result: CharacteristicRebuildResult,
args: argparse.Namespace,
) -> str:
graph = (
service.candidate_graph(result.repository.id, result.analysis_run.id)
if result.analysis_run.status == "completed"
else None
)
remaining_review = 0
if graph is not None:
remaining_review = sum(
1
for ability in graph.abilities
for capability in ability.capabilities
if capability.status == "candidate"
)
candidate_source = "deterministic" if args.no_llm else "configured"
return (
f"repo={result.repository.id}:{result.repository.name} "
f"latest_analysis_run={result.analysis_run.id} "
f"candidate_source={candidate_source} "
f"dry_run={result.dry_run} "
f"cleared_approved={result.cleared_approved} "
f"approved_superseded={result.previous_counts} "
f"candidates={result.candidate_counts} "
f"remaining_review_queue={remaining_review}"
)
if __name__ == "__main__":
raise SystemExit(main())