generated from coulomb/repo-seed
Add canon reset and reingest guardrails
This commit is contained in:
@@ -20,6 +20,7 @@ from .graph import FabricGraph, build_graph
|
||||
from .graph_explorer import fabric_graph_explorer_payload
|
||||
from .llm_extraction import LLMExtractionConfig
|
||||
from .reconciliation import reconcile_discovery_snapshots
|
||||
from .registry import RESET_CONFIRMATION_TOKEN
|
||||
from .scanner import EXTRACTOR_VERSION, ScanOptions, scan_repo
|
||||
from .validation import validate_roots
|
||||
|
||||
@@ -240,6 +241,26 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
accept_discovery.add_argument("--accept-review-state", action="append", default=None)
|
||||
accept_discovery.add_argument("--commit", default=None)
|
||||
accept_discovery.add_argument("--json", action="store_true", help="Print the raw accept response.")
|
||||
|
||||
export_archive = registry_sub.add_parser(
|
||||
"export-reset-archive",
|
||||
help="Export registry graph/discovery data before a guarded reset.",
|
||||
)
|
||||
export_archive.add_argument("output", type=Path)
|
||||
export_archive.add_argument("--registry-url", default="http://127.0.0.1:8765")
|
||||
export_archive.add_argument("--overwrite", action="store_true", help="Overwrite an existing archive file.")
|
||||
export_archive.add_argument("--json", action="store_true", help="Print archive metadata.")
|
||||
|
||||
reset_graph = registry_sub.add_parser(
|
||||
"reset-graph-data",
|
||||
help="Export an archive, then reset registry graph/discovery data with an explicit confirmation token.",
|
||||
)
|
||||
reset_graph.add_argument("--registry-url", default="http://127.0.0.1:8765")
|
||||
reset_graph.add_argument("--archive", type=Path, required=True, help="Archive JSON file to write before reset.")
|
||||
reset_graph.add_argument("--overwrite-archive", action="store_true", help="Overwrite an existing archive file.")
|
||||
reset_graph.add_argument("--confirm", required=True, help=f"Must equal {RESET_CONFIRMATION_TOKEN}.")
|
||||
reset_graph.add_argument("--reason", required=True)
|
||||
reset_graph.add_argument("--json", action="store_true", help="Print the raw reset response.")
|
||||
return parser
|
||||
|
||||
|
||||
@@ -311,6 +332,10 @@ def main(argv: list[str] | None = None) -> int:
|
||||
return _registry_ingest_discovery(args)
|
||||
if args.registry_command == "accept-discovery":
|
||||
return _registry_accept_discovery(args)
|
||||
if args.registry_command == "export-reset-archive":
|
||||
return _registry_export_reset_archive(args)
|
||||
if args.registry_command == "reset-graph-data":
|
||||
return _registry_reset_graph_data(args)
|
||||
|
||||
parser.error(f"unknown command {args.command!r}")
|
||||
return 2
|
||||
@@ -1011,7 +1036,12 @@ def _scan_manifest_exit_code(summary: dict[str, Any], args: argparse.Namespace)
|
||||
|
||||
|
||||
def _manifest_discovery_snapshot_path(base_dir: Path, slug: str, profile: str) -> Path:
|
||||
return base_dir.resolve() / f"{_slugify(slug)}-{_slugify(profile)}.discovery.json"
|
||||
slug_part = _slugify(slug)
|
||||
raw_slug_part = slug.strip().lower()
|
||||
if slug_part != raw_slug_part:
|
||||
fingerprint = hashlib.sha256(slug.encode("utf-8")).hexdigest()[:8]
|
||||
slug_part = f"{slug_part}-{fingerprint}"
|
||||
return base_dir.resolve() / f"{slug_part}-{_slugify(profile)}.discovery.json"
|
||||
|
||||
|
||||
def _candidate_counts(snapshot: dict[str, Any]) -> dict[str, int]:
|
||||
@@ -1285,6 +1315,57 @@ def _registry_accept_discovery(args: argparse.Namespace) -> int:
|
||||
return 0
|
||||
|
||||
|
||||
def _registry_export_reset_archive(args: argparse.Namespace) -> int:
|
||||
try:
|
||||
archive = _registry_get_checked(args.registry_url, "/exports/reset-archive")
|
||||
archive_sha256 = _write_json_archive(args.output, archive, overwrite=args.overwrite)
|
||||
except (RegistryRequestError, OSError) as exc:
|
||||
print(f"ERROR {exc}", file=sys.stderr)
|
||||
return 1
|
||||
metadata = {
|
||||
"archive": str(args.output),
|
||||
"archive_sha256": archive_sha256,
|
||||
"counts": archive.get("counts", {}),
|
||||
}
|
||||
if args.json:
|
||||
print(json.dumps(metadata, indent=2, sort_keys=True))
|
||||
else:
|
||||
print(f"wrote reset archive {args.output}")
|
||||
print(f"archive sha256 {archive_sha256}")
|
||||
return 0
|
||||
|
||||
|
||||
def _registry_reset_graph_data(args: argparse.Namespace) -> int:
|
||||
if args.confirm != RESET_CONFIRMATION_TOKEN:
|
||||
print(f"ERROR --confirm must equal {RESET_CONFIRMATION_TOKEN}", file=sys.stderr)
|
||||
return 1
|
||||
try:
|
||||
archive = _registry_get_checked(args.registry_url, "/exports/reset-archive")
|
||||
archive_sha256 = _write_json_archive(args.archive, archive, overwrite=args.overwrite_archive)
|
||||
result = _registry_post_checked(
|
||||
args.registry_url,
|
||||
"/admin/reset-graph-data",
|
||||
{
|
||||
"confirm": args.confirm,
|
||||
"reason": args.reason,
|
||||
"archive_path": str(args.archive),
|
||||
"archive_sha256": archive_sha256,
|
||||
},
|
||||
)
|
||||
except (RegistryRequestError, OSError) as exc:
|
||||
print(f"ERROR {exc}", file=sys.stderr)
|
||||
return 1
|
||||
if args.json:
|
||||
print(json.dumps(result, indent=2, sort_keys=True))
|
||||
else:
|
||||
print(f"wrote reset archive {args.archive}")
|
||||
print(f"archive sha256 {archive_sha256}")
|
||||
print(f"reset event {result['id']} recorded")
|
||||
print(f"dropped {json.dumps(result.get('dropped_counts', {}), sort_keys=True)}")
|
||||
print(f"preserved {result.get('repositories_preserved', 0)} repository registration(s)")
|
||||
return 0
|
||||
|
||||
|
||||
def _scan_repo(args: argparse.Namespace) -> int:
|
||||
snapshot = scan_repo(
|
||||
ScanOptions(
|
||||
@@ -1369,6 +1450,15 @@ class RegistryRequestError(Exception):
|
||||
self.status_code = status_code
|
||||
|
||||
|
||||
def _write_json_archive(path: Path, archive: dict[str, object], *, overwrite: bool) -> str:
|
||||
if path.exists() and not overwrite:
|
||||
raise OSError(f"archive already exists: {path} (use --overwrite or --overwrite-archive)")
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
data = json.dumps(archive, indent=2, sort_keys=True).encode("utf-8")
|
||||
path.write_bytes(data)
|
||||
return hashlib.sha256(data).hexdigest()
|
||||
|
||||
|
||||
def _registry_post(registry_url: str, path: str, payload: dict[str, object]) -> dict[str, object]:
|
||||
try:
|
||||
return _registry_post_checked(registry_url, path, payload)
|
||||
|
||||
Reference in New Issue
Block a user