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:
tegwick 2026-08-21 23:57:03 +02:00
parent 3c6887a3e0
commit 956efbb7ae
9 changed files with 2915 additions and 17 deletions

View file

@ -43,7 +43,7 @@
| 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-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-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 |

View 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

View file

@ -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

View file

@ -25,10 +25,12 @@ fixed value above rather than recomputing it. Fleet namespace names are
lowercase DNS-label style. Canonical identifiers are
`PREFIX-WP-NNNN` or `PREFIX-WP-NNNN-TNN`.
The repository is not a namespace. Under the current N1 posture, the fleet must
declare one shared namespace name before activation. A future fork uses its own
namespace name and therefore derives different UUIDs for the same unqualified
identifier, as required by ADR-011.
The repository is not a namespace. The fleet owner declared the current shared
namespace as **`helixforge`** on 2026-08-21; the versioned declaration is
`config/fleet-namespace.yaml`. This raises the namespace posture from implied
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
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.
```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 migration-plan --root /path/to/fleet \
--namespace <fleet-namespace> --output uuid-migration.json
rmgr identifier migration-plan --root /path/to/fleet --output uuid-migration.json
rmgr identifier migration-verify --plan uuid-migration.json
```
`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
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.
Publishing or planning this function does not retroactively rewrite existing
identifiers. The caller must supply the namespace explicitly until the current
fleet namespace is declared by the namespace owner under ADR-011.
identifiers. `--namespace` remains an explicit override for a foreign/forked
fleet; omitting it uses the checked-in `helixforge` declaration.

View file

@ -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

View file

@ -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",
}

View file

@ -1,16 +1,23 @@
from __future__ import annotations
import subprocess
from pathlib import Path
import pytest
from repo_manager.identifiers import (
derive_work_record_uuid,
load_fleet_namespace,
plan_identifier_migration,
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:
path.parent.mkdir(parents=True, exist_ok=True)
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",
}
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:
@ -97,3 +106,33 @@ def test_migration_plan_skips_entire_repo_affected_by_collision(tmp_path: Path)
assert one_plan["eligible"] is False
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"])
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"]
)

View file

@ -277,7 +277,7 @@ before then.
```task
id: RMGR-WP-0005-T03
status: progress
status: done
priority: high
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.
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
```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
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
```task