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
|
|
@ -43,7 +43,7 @@
|
||||||
| task | RMGR-WP-0004-T09 | done | — | workplans/RMGR-WP-0004-repository-standards-conformance.md |
|
| task | RMGR-WP-0004-T09 | done | — | workplans/RMGR-WP-0004-repository-standards-conformance.md |
|
||||||
| task | RMGR-WP-0005-T01 | done | — | workplans/RMGR-WP-0005-registrar-consolidation-deterministic-ids.md |
|
| task | RMGR-WP-0005-T01 | done | — | workplans/RMGR-WP-0005-registrar-consolidation-deterministic-ids.md |
|
||||||
| task | RMGR-WP-0005-T02 | wait | — | workplans/RMGR-WP-0005-registrar-consolidation-deterministic-ids.md |
|
| task | RMGR-WP-0005-T02 | wait | — | workplans/RMGR-WP-0005-registrar-consolidation-deterministic-ids.md |
|
||||||
| task | RMGR-WP-0005-T03 | progress | — | workplans/RMGR-WP-0005-registrar-consolidation-deterministic-ids.md |
|
| task | RMGR-WP-0005-T03 | done | — | workplans/RMGR-WP-0005-registrar-consolidation-deterministic-ids.md |
|
||||||
| task | RMGR-WP-0005-T04 | progress | — | workplans/RMGR-WP-0005-registrar-consolidation-deterministic-ids.md |
|
| task | RMGR-WP-0005-T04 | progress | — | workplans/RMGR-WP-0005-registrar-consolidation-deterministic-ids.md |
|
||||||
| task | RMGR-WP-0005-T05 | wait | — | workplans/RMGR-WP-0005-registrar-consolidation-deterministic-ids.md |
|
| task | RMGR-WP-0005-T05 | wait | — | workplans/RMGR-WP-0005-registrar-consolidation-deterministic-ids.md |
|
||||||
| task | RMGR-WP-0005-T06 | done | — | workplans/RMGR-WP-0005-registrar-consolidation-deterministic-ids.md |
|
| task | RMGR-WP-0005-T06 | done | — | workplans/RMGR-WP-0005-registrar-consolidation-deterministic-ids.md |
|
||||||
|
|
|
||||||
12
config/fleet-namespace.yaml
Normal file
12
config/fleet-namespace.yaml
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
schema: repo-manager.fleet-namespace.v1
|
||||||
|
namespace: helixforge
|
||||||
|
scope: HelixForge fleet
|
||||||
|
lineage:
|
||||||
|
kind: root
|
||||||
|
parent: null
|
||||||
|
posture:
|
||||||
|
current: {N: 2, U: 1, R: 1, S: 1}
|
||||||
|
target: {N: 3, U: 2, R: 2, S: 1}
|
||||||
|
declared_by: Bernd Worsch
|
||||||
|
declared: "2026-08-21"
|
||||||
|
decision_ref: RMGR-WP-0005-T03
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
---
|
||||||
|
id: RMGR-EVIDENCE-0005-HELIXFORGE-MIGRATION-20260821
|
||||||
|
type: evidence
|
||||||
|
workplan_task: RMGR-WP-0005-T04
|
||||||
|
created: "2026-08-21"
|
||||||
|
---
|
||||||
|
|
||||||
|
# HelixForge deterministic-identifier migration readiness
|
||||||
|
|
||||||
|
Fleet owner Bernd Worsch declared `helixforge` as the authoritative current
|
||||||
|
namespace. Repo Manager records it in `config/fleet-namespace.yaml`; omitting
|
||||||
|
`--namespace` now resolves to that checked-in declaration.
|
||||||
|
|
||||||
|
## Source preflight
|
||||||
|
|
||||||
|
- 114 repository workplan roots scanned under `/home/worsch`;
|
||||||
|
- 242 live workplan/task identifiers;
|
||||||
|
- zero live identifier collisions;
|
||||||
|
- one initial ineligible repository (`net-kingdom`) due to 14 legacy task ids
|
||||||
|
written as `-T1`…`-T8`;
|
||||||
|
- those ids and their four documentation references were normalized to
|
||||||
|
`-T01`…`-T08` in NetKingdom commit `25ed928`;
|
||||||
|
- regenerated plan: 42/42 repositories eligible, 242 records, 209 replacements,
|
||||||
|
33 assignments, zero skipped;
|
||||||
|
- plan SHA-256:
|
||||||
|
`98a7d876d49fbec9c062f970c18e25750da4cd4b5c89ab0d4ed24d50183abbda`.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Central-projection gate
|
||||||
|
|
||||||
|
No UUID was rewritten. State Hub uses these UUIDs as primary keys, with
|
||||||
|
references from tasks, progress and token events, decisions, dependencies,
|
||||||
|
launch requests, intakes, capability requests, contributions, technical debt,
|
||||||
|
extension points, and legacy suggestions. The checked-in foreign keys specify
|
||||||
|
delete behavior but no `ON UPDATE CASCADE` behavior.
|
||||||
|
|
||||||
|
Consequently, changing the repository fields first would detach the central
|
||||||
|
projection, while directly updating database primary keys would fail or leave
|
||||||
|
references split. Apply remains unauthorized until State Hub has:
|
||||||
|
|
||||||
|
1. a transactional old→derived projection migration covering every referencing
|
||||||
|
table;
|
||||||
|
2. durable alias/provenance for old ids used by saved links and events;
|
||||||
|
3. per-repository prepare/apply/rollback behavior coordinated with the Git
|
||||||
|
commit and push seal; and
|
||||||
|
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.
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -25,10 +25,12 @@ fixed value above rather than recomputing it. Fleet namespace names are
|
||||||
lowercase DNS-label style. Canonical identifiers are
|
lowercase DNS-label style. Canonical identifiers are
|
||||||
`PREFIX-WP-NNNN` or `PREFIX-WP-NNNN-TNN`.
|
`PREFIX-WP-NNNN` or `PREFIX-WP-NNNN-TNN`.
|
||||||
|
|
||||||
The repository is not a namespace. Under the current N1 posture, the fleet must
|
The repository is not a namespace. The fleet owner declared the current shared
|
||||||
declare one shared namespace name before activation. A future fork uses its own
|
namespace as **`helixforge`** on 2026-08-21; the versioned declaration is
|
||||||
namespace name and therefore derives different UUIDs for the same unqualified
|
`config/fleet-namespace.yaml`. This raises the namespace posture from implied
|
||||||
identifier, as required by ADR-011.
|
N1 to declared N2. A future fork uses its own namespace name and therefore
|
||||||
|
derives different UUIDs for the same unqualified identifier, as required by
|
||||||
|
ADR-011.
|
||||||
|
|
||||||
Only proposed, ready, active, blocked, or backlog workplans and their unfinished
|
Only proposed, ready, active, blocked, or backlog workplans and their unfinished
|
||||||
tasks enter the live derivation set. Finished and archived history retains its
|
tasks enter the live derivation set. Finished and archived history retains its
|
||||||
|
|
@ -37,10 +39,10 @@ the live-collision preflight. Any duplicate is a hard refusal; it is never
|
||||||
silently disambiguated with a repository slug.
|
silently disambiguated with a repository slug.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
rmgr identifier derive --namespace <fleet-namespace> --record-id RMGR-WP-0005
|
rmgr identifier derive --record-id RMGR-WP-0005
|
||||||
rmgr identifier preflight --root /path/to/fleet
|
rmgr identifier preflight --root /path/to/fleet
|
||||||
rmgr identifier migration-plan --root /path/to/fleet \
|
rmgr identifier migration-plan --root /path/to/fleet --output uuid-migration.json
|
||||||
--namespace <fleet-namespace> --output uuid-migration.json
|
rmgr identifier migration-verify --plan uuid-migration.json
|
||||||
```
|
```
|
||||||
|
|
||||||
`migration-plan` is non-mutating. Its versioned JSON output preserves every
|
`migration-plan` is non-mutating. Its versioned JSON output preserves every
|
||||||
|
|
@ -49,7 +51,12 @@ unit. A collision or malformed live identifier makes that whole repository
|
||||||
ineligible while leaving unaffected repositories visible in the plan. Existing
|
ineligible while leaving unaffected repositories visible in the plan. Existing
|
||||||
output files are not replaced unless `--force` is explicit.
|
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.
|
||||||
|
|
||||||
Activation and applying a bulk migration remain separate governed steps.
|
Activation and applying a bulk migration remain separate governed steps.
|
||||||
Publishing or planning this function does not retroactively rewrite existing
|
Publishing or planning this function does not retroactively rewrite existing
|
||||||
identifiers. The caller must supply the namespace explicitly until the current
|
identifiers. `--namespace` remains an explicit override for a foreign/forked
|
||||||
fleet namespace is declared by the namespace owner under ADR-011.
|
fleet; omitting it uses the checked-in `helixforge` declaration.
|
||||||
|
|
|
||||||
|
|
@ -282,7 +282,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||||
p_identifier = sub.add_parser("identifier", help="Deterministic work-record identifiers")
|
p_identifier = sub.add_parser("identifier", help="Deterministic work-record identifiers")
|
||||||
identifier_sub = p_identifier.add_subparsers(dest="identifier_command")
|
identifier_sub = p_identifier.add_subparsers(dest="identifier_command")
|
||||||
p_id_derive = identifier_sub.add_parser("derive", help="Derive one UUIDv5")
|
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_derive.add_argument("--record-id", required=True)
|
||||||
p_id_preflight = identifier_sub.add_parser("preflight", help="Scan live identifier collisions")
|
p_id_preflight = identifier_sub.add_parser("preflight", help="Scan live identifier collisions")
|
||||||
p_id_preflight.add_argument("--root", default=".")
|
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",
|
help="Emit a non-mutating, per-repository old-to-derived UUID plan",
|
||||||
)
|
)
|
||||||
p_id_plan.add_argument("--root", default=".")
|
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("--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_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")
|
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")
|
sbom_sub = p_sbom.add_subparsers(dest="sbom_command")
|
||||||
|
|
@ -616,27 +621,40 @@ def main(argv: list[str] | None = None) -> int:
|
||||||
return 2
|
return 2
|
||||||
from repo_manager.identifiers import (
|
from repo_manager.identifiers import (
|
||||||
derive_work_record_uuid,
|
derive_work_record_uuid,
|
||||||
|
load_fleet_namespace,
|
||||||
plan_identifier_migration,
|
plan_identifier_migration,
|
||||||
scan_live_identifier_collisions,
|
scan_live_identifier_collisions,
|
||||||
|
verify_identifier_migration_plan,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
namespace = getattr(args, "namespace", None) or load_fleet_namespace()
|
||||||
|
|
||||||
if args.identifier_command == "derive":
|
if args.identifier_command == "derive":
|
||||||
try:
|
try:
|
||||||
derived = derive_work_record_uuid(args.namespace, args.record_id)
|
derived = derive_work_record_uuid(namespace, args.record_id)
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
print(json.dumps({"ok": False, "error": str(exc)}, indent=2))
|
print(json.dumps({"ok": False, "error": str(exc)}, indent=2))
|
||||||
return 1
|
return 1
|
||||||
result = {
|
result = {
|
||||||
"ok": True,
|
"ok": True,
|
||||||
"namespace": args.namespace,
|
"namespace": namespace,
|
||||||
"record_id": args.record_id,
|
"record_id": args.record_id,
|
||||||
"uuid": str(derived),
|
"uuid": str(derived),
|
||||||
}
|
}
|
||||||
elif args.identifier_command == "preflight":
|
elif args.identifier_command == "preflight":
|
||||||
result = scan_live_identifier_collisions(Path(args.root))
|
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:
|
else:
|
||||||
try:
|
try:
|
||||||
result = plan_identifier_migration(Path(args.root), args.namespace)
|
result = plan_identifier_migration(Path(args.root), namespace)
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
print(json.dumps({"ok": False, "error": str(exc)}, indent=2))
|
print(json.dumps({"ok": False, "error": str(exc)}, indent=2))
|
||||||
return 1
|
return 1
|
||||||
|
|
|
||||||
|
|
@ -2,22 +2,41 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
import re
|
import re
|
||||||
import uuid
|
import uuid
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
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.parse.workplan import iter_workplan_files, parse_workplan_file
|
||||||
from repo_manager.prefix_registry import iter_repo_roots
|
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_NAMESPACE_UUID = uuid.UUID("a4058507-5c4a-5a00-ab06-fffa4fb46009")
|
||||||
DERIVATION_VERSION = "repo-manager.work-record-uuid.v1"
|
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"})
|
LIVE_WORKPLAN_STATUSES = frozenset({"proposed", "ready", "active", "blocked", "backlog"})
|
||||||
_NAMESPACE_RE = re.compile(r"^[a-z0-9][a-z0-9.-]{0,62}$")
|
_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,})?$")
|
_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:
|
def derivation_name(namespace: str, identifier: str) -> str:
|
||||||
"""Return the exact UTF-8 UUIDv5 name input defined by contract v1."""
|
"""Return the exact UTF-8 UUIDv5 name input defined by contract v1."""
|
||||||
namespace = namespace.strip()
|
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}
|
totals = {"repositories": 0, "eligible": 0, "skipped": 0, "records": 0, "replace": 0, "assign": 0, "unchanged": 0}
|
||||||
|
|
||||||
for repo in iter_repo_roots(root):
|
for repo in iter_repo_roots(root):
|
||||||
|
fingerprint, _source_files = source_fingerprint(repo)
|
||||||
mappings: list[dict[str, str | None]] = []
|
mappings: list[dict[str, str | None]] = []
|
||||||
blockers: list[dict[str, str]] = []
|
blockers: list[dict[str, str]] = []
|
||||||
for path in iter_workplan_files(repo):
|
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,
|
"repo": repo.name,
|
||||||
"path": str(repo),
|
"path": str(repo),
|
||||||
|
"planned_head_sha": head_sha(repo),
|
||||||
|
"source_fingerprint": fingerprint,
|
||||||
"eligible": eligible,
|
"eligible": eligible,
|
||||||
"atomic_unit": True,
|
"atomic_unit": True,
|
||||||
"blockers": blockers,
|
"blockers": blockers,
|
||||||
|
|
@ -176,7 +198,7 @@ def plan_identifier_migration(root: Path, namespace: str) -> dict[str, Any]:
|
||||||
for mapping in mappings:
|
for mapping in mappings:
|
||||||
totals[str(mapping["action"])] += 1
|
totals[str(mapping["action"])] += 1
|
||||||
|
|
||||||
return {
|
report = {
|
||||||
"schema": "repo-manager.identifier-migration-plan.v1",
|
"schema": "repo-manager.identifier-migration-plan.v1",
|
||||||
"ok": totals["skipped"] == 0,
|
"ok": totals["skipped"] == 0,
|
||||||
"ready_to_apply": 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),
|
"namespace_uuid": str(DERIVATION_NAMESPACE_UUID),
|
||||||
"scope": "live workplans and unfinished tasks",
|
"scope": "live workplans and unfinished tasks",
|
||||||
"apply_policy": "all-or-nothing per repository",
|
"apply_policy": "all-or-nothing per repository",
|
||||||
|
"generated_at": utc_now_text(),
|
||||||
"totals": totals,
|
"totals": totals,
|
||||||
"repositories": repositories,
|
"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",
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,23 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import subprocess
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from repo_manager.identifiers import (
|
from repo_manager.identifiers import (
|
||||||
derive_work_record_uuid,
|
derive_work_record_uuid,
|
||||||
|
load_fleet_namespace,
|
||||||
plan_identifier_migration,
|
plan_identifier_migration,
|
||||||
scan_live_identifier_collisions,
|
scan_live_identifier_collisions,
|
||||||
|
verify_identifier_migration_plan,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_declared_fleet_namespace_is_helixforge() -> None:
|
||||||
|
assert load_fleet_namespace() == "helixforge"
|
||||||
|
|
||||||
|
|
||||||
def _workplan(path: Path, identifier: str, status: str, task_status: str = "todo") -> None:
|
def _workplan(path: Path, identifier: str, status: str, task_status: str = "todo") -> None:
|
||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
path.write_text(
|
path.write_text(
|
||||||
|
|
@ -80,6 +87,8 @@ def test_migration_plan_preserves_mapping_and_is_atomic_per_repo(tmp_path: Path)
|
||||||
"ONE-WP-0001-T01",
|
"ONE-WP-0001-T01",
|
||||||
}
|
}
|
||||||
assert all(item["current_uuid"] is None for item in one_plan["mappings"])
|
assert all(item["current_uuid"] is None for item in one_plan["mappings"])
|
||||||
|
assert len(report["plan_sha256"]) == 64
|
||||||
|
assert report["generated_at"].endswith("Z")
|
||||||
|
|
||||||
|
|
||||||
def test_migration_plan_skips_entire_repo_affected_by_collision(tmp_path: Path) -> None:
|
def test_migration_plan_skips_entire_repo_affected_by_collision(tmp_path: Path) -> None:
|
||||||
|
|
@ -97,3 +106,33 @@ def test_migration_plan_skips_entire_repo_affected_by_collision(tmp_path: Path)
|
||||||
assert one_plan["eligible"] is False
|
assert one_plan["eligible"] is False
|
||||||
assert any(item["record_id"] == "ONE-WP-0002" for item in one_plan["mappings"])
|
assert any(item["record_id"] == "ONE-WP-0002" for item in one_plan["mappings"])
|
||||||
assert any(item["reason"] == "live identifier collision" for item in one_plan["blockers"])
|
assert any(item["reason"] == "live identifier collision" for item in one_plan["blockers"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_migration_verification_detects_tampering_and_source_drift(tmp_path: Path) -> None:
|
||||||
|
repo = tmp_path / "one"
|
||||||
|
path = repo / "workplans" / "one.md"
|
||||||
|
_workplan(path, "ONE-WP-0001", "active")
|
||||||
|
subprocess.run(["git", "init"], cwd=repo, check=True, capture_output=True)
|
||||||
|
subprocess.run(["git", "add", "."], cwd=repo, check=True, capture_output=True)
|
||||||
|
subprocess.run(
|
||||||
|
["git", "-c", "user.name=Test", "-c", "user.email=test@example.com", "commit", "-m", "seed"],
|
||||||
|
cwd=repo,
|
||||||
|
check=True,
|
||||||
|
capture_output=True,
|
||||||
|
)
|
||||||
|
plan = plan_identifier_migration(tmp_path, "helixforge")
|
||||||
|
assert verify_identifier_migration_plan(plan)["ok"] is True
|
||||||
|
|
||||||
|
path.write_text(path.read_text(encoding="utf-8") + "\nchanged\n", encoding="utf-8")
|
||||||
|
verification = verify_identifier_migration_plan(plan)
|
||||||
|
assert verification["ok"] is False
|
||||||
|
assert any(
|
||||||
|
error["reason"] == "authoritative source changed after planning"
|
||||||
|
for error in verification["errors"]
|
||||||
|
)
|
||||||
|
|
||||||
|
plan["namespace"] = "tampered"
|
||||||
|
assert any(
|
||||||
|
error["reason"] == "plan SHA-256 mismatch"
|
||||||
|
for error in verify_identifier_migration_plan(plan)["errors"]
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -277,7 +277,7 @@ before then.
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: RMGR-WP-0005-T03
|
id: RMGR-WP-0005-T03
|
||||||
status: progress
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "28067729-498d-4f47-89bd-5b9718e999c7"
|
state_hub_task_id: "28067729-498d-4f47-89bd-5b9718e999c7"
|
||||||
```
|
```
|
||||||
|
|
@ -330,6 +330,14 @@ current fleet namespace boundary/name: ADR-011 intentionally leaves that an
|
||||||
open governance question, so Repo Manager does not infer it from a hostname.
|
open governance question, so Repo Manager does not infer it from a hostname.
|
||||||
No existing minted identifier was rewritten implicitly.
|
No existing minted identifier was rewritten implicitly.
|
||||||
|
|
||||||
|
**Result (2026-08-21):** fleet owner Bernd Worsch declared `helixforge` as the
|
||||||
|
authoritative namespace. `config/fleet-namespace.yaml` records the decision,
|
||||||
|
root lineage, and accurate `N2 U1 R1 S1` posture with `N3 U2 R2 S1` target.
|
||||||
|
Identifier commands now use that declaration by default while retaining an
|
||||||
|
explicit override for foreign namespaces. The derivation contract, fixed UUID,
|
||||||
|
collision/unarchive guard, and independent reproduction test are all active;
|
||||||
|
bulk replacement remains the separately governed T04 operation.
|
||||||
|
|
||||||
## Migrate the fleet
|
## Migrate the fleet
|
||||||
|
|
||||||
```task
|
```task
|
||||||
|
|
@ -358,6 +366,20 @@ malformed live identifier. Applying those plans remains gated on the namespace
|
||||||
declaration and a compatibility write path that updates file and central
|
declaration and a compatibility write path that updates file and central
|
||||||
projection together.
|
projection together.
|
||||||
|
|
||||||
|
**Progress (2026-08-21, namespace declared):** the `helixforge` fleet preflight
|
||||||
|
scanned 114 repository roots and found 242 live identifiers with zero
|
||||||
|
collisions. Fourteen non-canonical NetKingdom task ids were the sole initial
|
||||||
|
repository blocker and were normalized in commit `25ed928`. The regenerated,
|
||||||
|
SHA-256-sealed mapping is source-ready across 42/42 affected repositories: 209
|
||||||
|
UUID replacements and 33 assignments, with per-repository HEAD/source
|
||||||
|
fingerprints verified by `rmgr identifier migration-verify`.
|
||||||
|
|
||||||
|
Apply is still deliberately unauthorized. State Hub's workplan/task primary
|
||||||
|
keys are referenced by numerous foreign keys without `ON UPDATE CASCADE`; a
|
||||||
|
file-first or direct-PK rewrite would split the projection. Evidence and the
|
||||||
|
required transactional/alias/rollback contract are recorded in
|
||||||
|
`docs/evidence/RMGR-WP-0005-helixforge-migration-readiness-2026-08-21.md`.
|
||||||
|
|
||||||
## Retire the interim rule
|
## Retire the interim rule
|
||||||
|
|
||||||
```task
|
```task
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue