feat(identifier): declare helixforge fleet namespace
Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a023c0-a0a3-7c03-b395-5a0d2757214d
This commit is contained in:
parent
3c6887a3e0
commit
956efbb7ae
9 changed files with 2915 additions and 17 deletions
|
|
@ -282,7 +282,7 @@ def main(argv: list[str] | None = None) -> int:
|
|||
p_identifier = sub.add_parser("identifier", help="Deterministic work-record identifiers")
|
||||
identifier_sub = p_identifier.add_subparsers(dest="identifier_command")
|
||||
p_id_derive = identifier_sub.add_parser("derive", help="Derive one UUIDv5")
|
||||
p_id_derive.add_argument("--namespace", required=True)
|
||||
p_id_derive.add_argument("--namespace", default=None, help="Override declared fleet namespace")
|
||||
p_id_derive.add_argument("--record-id", required=True)
|
||||
p_id_preflight = identifier_sub.add_parser("preflight", help="Scan live identifier collisions")
|
||||
p_id_preflight.add_argument("--root", default=".")
|
||||
|
|
@ -291,9 +291,14 @@ def main(argv: list[str] | None = None) -> int:
|
|||
help="Emit a non-mutating, per-repository old-to-derived UUID plan",
|
||||
)
|
||||
p_id_plan.add_argument("--root", default=".")
|
||||
p_id_plan.add_argument("--namespace", required=True)
|
||||
p_id_plan.add_argument("--namespace", default=None, help="Override declared fleet namespace")
|
||||
p_id_plan.add_argument("--output", default=None, help="Write the provenance mapping as JSON")
|
||||
p_id_plan.add_argument("--force", action="store_true", help="Replace an existing --output file")
|
||||
p_id_verify = identifier_sub.add_parser(
|
||||
"migration-verify",
|
||||
help="Verify a sealed migration plan against current repository sources",
|
||||
)
|
||||
p_id_verify.add_argument("--plan", required=True)
|
||||
|
||||
p_sbom = sub.add_parser("sbom", help="Derive SBOM snapshots and licence reports from repository files")
|
||||
sbom_sub = p_sbom.add_subparsers(dest="sbom_command")
|
||||
|
|
@ -616,27 +621,40 @@ def main(argv: list[str] | None = None) -> int:
|
|||
return 2
|
||||
from repo_manager.identifiers import (
|
||||
derive_work_record_uuid,
|
||||
load_fleet_namespace,
|
||||
plan_identifier_migration,
|
||||
scan_live_identifier_collisions,
|
||||
verify_identifier_migration_plan,
|
||||
)
|
||||
|
||||
namespace = getattr(args, "namespace", None) or load_fleet_namespace()
|
||||
|
||||
if args.identifier_command == "derive":
|
||||
try:
|
||||
derived = derive_work_record_uuid(args.namespace, args.record_id)
|
||||
derived = derive_work_record_uuid(namespace, args.record_id)
|
||||
except ValueError as exc:
|
||||
print(json.dumps({"ok": False, "error": str(exc)}, indent=2))
|
||||
return 1
|
||||
result = {
|
||||
"ok": True,
|
||||
"namespace": args.namespace,
|
||||
"namespace": namespace,
|
||||
"record_id": args.record_id,
|
||||
"uuid": str(derived),
|
||||
}
|
||||
elif args.identifier_command == "preflight":
|
||||
result = scan_live_identifier_collisions(Path(args.root))
|
||||
elif args.identifier_command == "migration-verify":
|
||||
try:
|
||||
plan = json.loads(Path(args.plan).read_text(encoding="utf-8"))
|
||||
if not isinstance(plan, dict):
|
||||
raise TypeError("migration plan must be a JSON object")
|
||||
result = verify_identifier_migration_plan(plan)
|
||||
except (OSError, TypeError, ValueError, json.JSONDecodeError) as exc:
|
||||
print(json.dumps({"ok": False, "error": str(exc)}, indent=2))
|
||||
return 1
|
||||
else:
|
||||
try:
|
||||
result = plan_identifier_migration(Path(args.root), args.namespace)
|
||||
result = plan_identifier_migration(Path(args.root), namespace)
|
||||
except ValueError as exc:
|
||||
print(json.dumps({"ok": False, "error": str(exc)}, indent=2))
|
||||
return 1
|
||||
|
|
|
|||
|
|
@ -2,22 +2,41 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import uuid
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
|
||||
from repo_manager.cache import source_fingerprint
|
||||
from repo_manager.gitops import head_sha
|
||||
from repo_manager.parse.workplan import iter_workplan_files, parse_workplan_file
|
||||
from repo_manager.prefix_registry import iter_repo_roots
|
||||
from repo_manager.time import utc_now_text
|
||||
|
||||
DERIVATION_NAMESPACE_UUID = uuid.UUID("a4058507-5c4a-5a00-ab06-fffa4fb46009")
|
||||
DERIVATION_VERSION = "repo-manager.work-record-uuid.v1"
|
||||
FLEET_NAMESPACE_SCHEMA = "repo-manager.fleet-namespace.v1"
|
||||
DEFAULT_NAMESPACE_CONTRACT = Path(__file__).resolve().parents[2] / "config" / "fleet-namespace.yaml"
|
||||
LIVE_WORKPLAN_STATUSES = frozenset({"proposed", "ready", "active", "blocked", "backlog"})
|
||||
_NAMESPACE_RE = re.compile(r"^[a-z0-9][a-z0-9.-]{0,62}$")
|
||||
_RECORD_ID_RE = re.compile(r"^[A-Z][A-Z0-9-]*-WP-[0-9]{4}(?:-T[0-9]{2,})?$")
|
||||
|
||||
|
||||
def load_fleet_namespace(path: Path = DEFAULT_NAMESPACE_CONTRACT) -> str:
|
||||
data = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
|
||||
if not isinstance(data, dict) or data.get("schema") != FLEET_NAMESPACE_SCHEMA:
|
||||
raise ValueError(f"unsupported fleet namespace contract: {path}")
|
||||
namespace = str(data.get("namespace") or "").strip()
|
||||
if not _NAMESPACE_RE.fullmatch(namespace):
|
||||
raise ValueError(f"invalid fleet namespace in contract: {path}")
|
||||
return namespace
|
||||
|
||||
|
||||
def derivation_name(namespace: str, identifier: str) -> str:
|
||||
"""Return the exact UTF-8 UUIDv5 name input defined by contract v1."""
|
||||
namespace = namespace.strip()
|
||||
|
|
@ -110,6 +129,7 @@ def plan_identifier_migration(root: Path, namespace: str) -> dict[str, Any]:
|
|||
totals = {"repositories": 0, "eligible": 0, "skipped": 0, "records": 0, "replace": 0, "assign": 0, "unchanged": 0}
|
||||
|
||||
for repo in iter_repo_roots(root):
|
||||
fingerprint, _source_files = source_fingerprint(repo)
|
||||
mappings: list[dict[str, str | None]] = []
|
||||
blockers: list[dict[str, str]] = []
|
||||
for path in iter_workplan_files(repo):
|
||||
|
|
@ -164,6 +184,8 @@ def plan_identifier_migration(root: Path, namespace: str) -> dict[str, Any]:
|
|||
{
|
||||
"repo": repo.name,
|
||||
"path": str(repo),
|
||||
"planned_head_sha": head_sha(repo),
|
||||
"source_fingerprint": fingerprint,
|
||||
"eligible": eligible,
|
||||
"atomic_unit": True,
|
||||
"blockers": blockers,
|
||||
|
|
@ -176,7 +198,7 @@ def plan_identifier_migration(root: Path, namespace: str) -> dict[str, Any]:
|
|||
for mapping in mappings:
|
||||
totals[str(mapping["action"])] += 1
|
||||
|
||||
return {
|
||||
report = {
|
||||
"schema": "repo-manager.identifier-migration-plan.v1",
|
||||
"ok": totals["skipped"] == 0,
|
||||
"ready_to_apply": totals["skipped"] == 0,
|
||||
|
|
@ -186,6 +208,66 @@ def plan_identifier_migration(root: Path, namespace: str) -> dict[str, Any]:
|
|||
"namespace_uuid": str(DERIVATION_NAMESPACE_UUID),
|
||||
"scope": "live workplans and unfinished tasks",
|
||||
"apply_policy": "all-or-nothing per repository",
|
||||
"generated_at": utc_now_text(),
|
||||
"totals": totals,
|
||||
"repositories": repositories,
|
||||
}
|
||||
canonical = json.dumps(report, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
||||
report["plan_sha256"] = hashlib.sha256(canonical).hexdigest()
|
||||
return report
|
||||
|
||||
|
||||
def verify_identifier_migration_plan(plan: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Verify a saved plan against its seal and current repository sources."""
|
||||
errors: list[dict[str, str]] = []
|
||||
if plan.get("schema") != "repo-manager.identifier-migration-plan.v1":
|
||||
errors.append({"scope": "plan", "reason": "unsupported schema"})
|
||||
expected_seal = plan.get("plan_sha256")
|
||||
unsealed = {key: value for key, value in plan.items() if key != "plan_sha256"}
|
||||
canonical = json.dumps(unsealed, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
||||
actual_seal = hashlib.sha256(canonical).hexdigest()
|
||||
if expected_seal != actual_seal:
|
||||
errors.append({"scope": "plan", "reason": "plan SHA-256 mismatch"})
|
||||
try:
|
||||
declared_namespace = load_fleet_namespace()
|
||||
except (OSError, ValueError) as exc:
|
||||
errors.append({"scope": "namespace", "reason": str(exc)})
|
||||
declared_namespace = None
|
||||
if plan.get("namespace") != declared_namespace:
|
||||
errors.append(
|
||||
{
|
||||
"scope": "namespace",
|
||||
"reason": f"plan={plan.get('namespace')!r}, declared={declared_namespace!r}",
|
||||
}
|
||||
)
|
||||
if not plan.get("ready_to_apply"):
|
||||
errors.append({"scope": "plan", "reason": "plan is not ready_to_apply"})
|
||||
|
||||
checked_repositories = 0
|
||||
for repository in plan.get("repositories") or []:
|
||||
repo = Path(str(repository.get("path") or ""))
|
||||
scope = str(repository.get("repo") or repo.name or "repository")
|
||||
if not repository.get("eligible"):
|
||||
errors.append({"scope": scope, "reason": "repository is ineligible"})
|
||||
continue
|
||||
if not repo.is_dir():
|
||||
errors.append({"scope": scope, "reason": f"repository path is missing: {repo}"})
|
||||
continue
|
||||
checked_repositories += 1
|
||||
if head_sha(repo) != repository.get("planned_head_sha"):
|
||||
errors.append({"scope": scope, "reason": "Git HEAD changed after planning"})
|
||||
current_fingerprint, _source_files = source_fingerprint(repo)
|
||||
if current_fingerprint != repository.get("source_fingerprint"):
|
||||
errors.append({"scope": scope, "reason": "authoritative source changed after planning"})
|
||||
|
||||
return {
|
||||
"schema": "repo-manager.identifier-migration-verification.v1",
|
||||
"ok": not errors,
|
||||
"namespace": plan.get("namespace"),
|
||||
"plan_sha256": expected_seal,
|
||||
"checked_repositories": checked_repositories,
|
||||
"errors": errors,
|
||||
"source_preconditions_satisfied": not errors,
|
||||
"apply_authorized": False,
|
||||
"remaining_gate": "transactional central-projection migration and rollback",
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue