fix(identifier): verify migration per repository unit
Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a023c0-a0a3-7c03-b395-5a0d2757214d
This commit is contained in:
parent
956efbb7ae
commit
887943108e
6 changed files with 49 additions and 19 deletions
|
|
@ -23,13 +23,18 @@ namespace. Repo Manager records it in `config/fleet-namespace.yaml`; omitting
|
|||
- regenerated plan: 42/42 repositories eligible, 242 records, 209 replacements,
|
||||
33 assignments, zero skipped;
|
||||
- plan SHA-256:
|
||||
`98a7d876d49fbec9c062f970c18e25750da4cd4b5c89ab0d4ed24d50183abbda`.
|
||||
`122ce72b27c2df50fb5604f3b324b68fbf7d2ec392c041370bfba7e0850a0e26`.
|
||||
|
||||
The full old-to-derived mapping is
|
||||
`docs/evidence/RMGR-WP-0005-helixforge-uuid-migration-plan-2026-08-21.json`.
|
||||
It carries canonical UTC generation time plus per-repository Git HEAD and
|
||||
authoritative-source fingerprints. `rmgr identifier migration-verify` validates
|
||||
the seal and all 42 source preconditions.
|
||||
authoritative-source fingerprints. Source drift is a hard failure; HEAD drift
|
||||
with identical authoritative bytes is reported but remains eligible, avoiding
|
||||
self-invalidation when the plan itself is committed. `rmgr identifier migration-verify` validates
|
||||
the seal and source preconditions, either fleet-wide or for one repository
|
||||
atomic unit. Concurrent changes observed after the first seal were correctly
|
||||
rejected; `--repo` prevents unrelated fleet churn from invalidating a stable
|
||||
unit while still refusing the changed repository.
|
||||
|
||||
## Central-projection gate
|
||||
|
||||
|
|
@ -51,4 +56,5 @@ references split. Apply remains unauthorized until State Hub has:
|
|||
4. an isolated PostgreSQL rehearsal proving forward and reverse mappings.
|
||||
|
||||
This is a discovered compatibility requirement, not a source-data blocker. The
|
||||
sealed plan remains evidence; any source or HEAD drift requires regeneration.
|
||||
sealed plan remains evidence; source or HEAD drift requires regeneration for
|
||||
the affected repository before its apply.
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
"namespace_uuid": "a4058507-5c4a-5a00-ab06-fffa4fb46009",
|
||||
"scope": "live workplans and unfinished tasks",
|
||||
"apply_policy": "all-or-nothing per repository",
|
||||
"generated_at": "2026-08-21T21:55:58.740255Z",
|
||||
"generated_at": "2026-08-21T22:02:06.141659Z",
|
||||
"totals": {
|
||||
"repositories": 42,
|
||||
"eligible": 42,
|
||||
|
|
@ -206,8 +206,8 @@
|
|||
{
|
||||
"repo": "audit-core",
|
||||
"path": "/home/worsch/audit-core",
|
||||
"planned_head_sha": "d0790524e289acdd56e85a70d7a4ece7255421eb",
|
||||
"source_fingerprint": "85bd1ad69aab0c5802add729393f6c9724ea17d1d763b8779eee3cf9ff4f326f",
|
||||
"planned_head_sha": "5c5e98c8b550e0bb7c012fd5739e4db6c2f9734f",
|
||||
"source_fingerprint": "0304242794b0ad6b2cc166d3e54de01dee6aebd56ff8a666e65dfc78bef31ae9",
|
||||
"eligible": true,
|
||||
"atomic_unit": true,
|
||||
"blockers": [],
|
||||
|
|
@ -1976,7 +1976,7 @@
|
|||
{
|
||||
"repo": "repo-manager",
|
||||
"path": "/home/worsch/repo-manager",
|
||||
"planned_head_sha": "5dc2f3cdf66318e3814d1bb667cc9f8f108ab4d7",
|
||||
"planned_head_sha": "956efbb7aef00b63e344a5a5005d1e10307e17bf",
|
||||
"source_fingerprint": "d64ebac83d52c784ca174db71cab24998070bee93f16ede572e1cc61d6698b58",
|
||||
"eligible": true,
|
||||
"atomic_unit": true,
|
||||
|
|
@ -2164,8 +2164,8 @@
|
|||
{
|
||||
"repo": "state-hub",
|
||||
"path": "/home/worsch/state-hub",
|
||||
"planned_head_sha": "81861d816bec95212edd843341f663a75c6aa867",
|
||||
"source_fingerprint": "14ba1e8ff89ad8f1e659565254909958c41068c7a240dd57029a35cb5cd1f41d",
|
||||
"planned_head_sha": "ce52e9d1e21616051305224914962bd89047de59",
|
||||
"source_fingerprint": "ea755727b529c0129f985e798170b8e85a059bf35071fca5f5cffac5ef787c36",
|
||||
"eligible": true,
|
||||
"atomic_unit": true,
|
||||
"blockers": [],
|
||||
|
|
@ -2503,7 +2503,7 @@
|
|||
{
|
||||
"repo": "whitehat-security",
|
||||
"path": "/home/worsch/whitehat-security",
|
||||
"planned_head_sha": "beab2a04d1835b8ad9836a3db72a6743da6653db",
|
||||
"planned_head_sha": "3770b41b74e46639db03b3c27da195bf343a1dc8",
|
||||
"source_fingerprint": "040460e8c099a904bcf6ab72985d670359e3c893e618e28ec7c2a525143bd0c5",
|
||||
"eligible": true,
|
||||
"atomic_unit": true,
|
||||
|
|
@ -2660,5 +2660,5 @@
|
|||
]
|
||||
}
|
||||
],
|
||||
"plan_sha256": "98a7d876d49fbec9c062f970c18e25750da4cd4b5c89ab0d4ed24d50183abbda"
|
||||
"plan_sha256": "122ce72b27c2df50fb5604f3b324b68fbf7d2ec392c041370bfba7e0850a0e26"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ rmgr identifier derive --record-id RMGR-WP-0005
|
|||
rmgr identifier preflight --root /path/to/fleet
|
||||
rmgr identifier migration-plan --root /path/to/fleet --output uuid-migration.json
|
||||
rmgr identifier migration-verify --plan uuid-migration.json
|
||||
rmgr identifier migration-verify --plan uuid-migration.json --repo repo-manager
|
||||
```
|
||||
|
||||
`migration-plan` is non-mutating. Its versioned JSON output preserves every
|
||||
|
|
@ -53,8 +54,12 @@ output files are not replaced unless `--force` is explicit.
|
|||
|
||||
Plans carry a canonical UTC creation time, SHA-256 integrity seal, and per-repo
|
||||
Git HEAD/source fingerprints. `migration-verify` rejects a modified plan, a
|
||||
namespace mismatch, an ineligible repository, or any source/HEAD change after
|
||||
planning. A new plan is required after any such drift.
|
||||
namespace mismatch, an ineligible repository, or authoritative-source drift.
|
||||
HEAD movement is reported but is not a hard failure when the source fingerprint
|
||||
is unchanged; otherwise committing the plan itself would invalidate its own
|
||||
repository. `--repo` verifies one atomic apply unit, so unrelated fleet churn
|
||||
does not invalidate a stable repository. A new plan is required for each unit
|
||||
whose authoritative source drifts.
|
||||
|
||||
Activation and applying a bulk migration remain separate governed steps.
|
||||
Publishing or planning this function does not retroactively rewrite existing
|
||||
|
|
|
|||
|
|
@ -299,6 +299,7 @@ def main(argv: list[str] | None = None) -> int:
|
|||
help="Verify a sealed migration plan against current repository sources",
|
||||
)
|
||||
p_id_verify.add_argument("--plan", required=True)
|
||||
p_id_verify.add_argument("--repo", default=None, help="Verify one repository atomic unit")
|
||||
|
||||
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")
|
||||
|
|
@ -648,7 +649,7 @@ def main(argv: list[str] | None = None) -> int:
|
|||
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)
|
||||
result = verify_identifier_migration_plan(plan, repo_slug=args.repo)
|
||||
except (OSError, TypeError, ValueError, json.JSONDecodeError) as exc:
|
||||
print(json.dumps({"ok": False, "error": str(exc)}, indent=2))
|
||||
return 1
|
||||
|
|
|
|||
|
|
@ -217,7 +217,9 @@ def plan_identifier_migration(root: Path, namespace: str) -> dict[str, Any]:
|
|||
return report
|
||||
|
||||
|
||||
def verify_identifier_migration_plan(plan: dict[str, Any]) -> dict[str, Any]:
|
||||
def verify_identifier_migration_plan(
|
||||
plan: dict[str, Any], *, repo_slug: str | None = None
|
||||
) -> 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":
|
||||
|
|
@ -243,29 +245,43 @@ def verify_identifier_migration_plan(plan: dict[str, Any]) -> dict[str, Any]:
|
|||
if not plan.get("ready_to_apply"):
|
||||
errors.append({"scope": "plan", "reason": "plan is not ready_to_apply"})
|
||||
|
||||
repositories = list(plan.get("repositories") or [])
|
||||
if repo_slug is not None:
|
||||
repositories = [item for item in repositories if item.get("repo") == repo_slug]
|
||||
if not repositories:
|
||||
errors.append({"scope": repo_slug, "reason": "repository is absent from plan"})
|
||||
|
||||
checked_repositories = 0
|
||||
for repository in plan.get("repositories") or []:
|
||||
repository_results: list[dict[str, Any]] = []
|
||||
for repository in repositories:
|
||||
repo = Path(str(repository.get("path") or ""))
|
||||
scope = str(repository.get("repo") or repo.name or "repository")
|
||||
before = len(errors)
|
||||
if not repository.get("eligible"):
|
||||
errors.append({"scope": scope, "reason": "repository is ineligible"})
|
||||
repository_results.append({"repo": scope, "ok": False})
|
||||
continue
|
||||
if not repo.is_dir():
|
||||
errors.append({"scope": scope, "reason": f"repository path is missing: {repo}"})
|
||||
repository_results.append({"repo": scope, "ok": False})
|
||||
continue
|
||||
checked_repositories += 1
|
||||
if head_sha(repo) != repository.get("planned_head_sha"):
|
||||
errors.append({"scope": scope, "reason": "Git HEAD changed after planning"})
|
||||
head_changed = head_sha(repo) != repository.get("planned_head_sha")
|
||||
current_fingerprint, _source_files = source_fingerprint(repo)
|
||||
if current_fingerprint != repository.get("source_fingerprint"):
|
||||
errors.append({"scope": scope, "reason": "authoritative source changed after planning"})
|
||||
repository_results.append(
|
||||
{"repo": scope, "ok": len(errors) == before, "head_changed": head_changed}
|
||||
)
|
||||
|
||||
return {
|
||||
"schema": "repo-manager.identifier-migration-verification.v1",
|
||||
"ok": not errors,
|
||||
"namespace": plan.get("namespace"),
|
||||
"plan_sha256": expected_seal,
|
||||
"repo_filter": repo_slug,
|
||||
"checked_repositories": checked_repositories,
|
||||
"repository_results": repository_results,
|
||||
"errors": errors,
|
||||
"source_preconditions_satisfied": not errors,
|
||||
"apply_authorized": False,
|
||||
|
|
|
|||
|
|
@ -122,6 +122,8 @@ def test_migration_verification_detects_tampering_and_source_drift(tmp_path: Pat
|
|||
)
|
||||
plan = plan_identifier_migration(tmp_path, "helixforge")
|
||||
assert verify_identifier_migration_plan(plan)["ok"] is True
|
||||
assert verify_identifier_migration_plan(plan, repo_slug="one")["ok"] is True
|
||||
assert verify_identifier_migration_plan(plan, repo_slug="missing")["ok"] is False
|
||||
|
||||
path.write_text(path.read_text(encoding="utf-8") + "\nchanged\n", encoding="utf-8")
|
||||
verification = verify_identifier_migration_plan(plan)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue