feat: advance conformance and deterministic ID migration

Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a023c0-a0a3-7c03-b395-5a0d2757214d
This commit is contained in:
tegwick 2026-08-21 22:43:37 +02:00
parent 3791411591
commit ad621d6c0d
10 changed files with 513 additions and 20 deletions

View file

@ -0,0 +1,64 @@
schema: repo-manager.hub-record-authority.v1
updated: "2026-08-21"
workplan_task: RMGR-WP-0005-T08
classes:
file-derived:
authority: repository
offline: write-and-commit-source-file
hub-native-append:
authority: hub-core
offline: append-to-idempotent-outbox
hub-native-control:
authority: owning-central-hub
offline: no-generic-replay
derived-cache:
authority: upstream-source
offline: rebuild-or-refresh
retired:
authority: archived-provenance
offline: reject-writes
records:
agent_messages: {class: hub-native-control, owner: hub-core}
capability_catalog: {class: file-derived, owner: repo-manager, transition: retire-direct-db-writes}
capability_requests: {class: file-derived, owner: repo-manager, transition: register-entry-files}
contributions: {class: file-derived, owner: repo-manager}
decisions: {class: file-derived, owner: repo-manager, transition: retire-direct-db-writes}
doi_cache: {class: derived-cache, owner: repo-manager}
domain_goals: {class: hub-native-control, owner: hub-core}
domains: {class: hub-native-control, owner: hub-core}
extension_points: {class: file-derived, owner: repo-manager}
fabric_graph_edges: {class: derived-cache, owner: fin-hub}
fabric_graph_imports: {class: derived-cache, owner: fin-hub}
fabric_graph_nodes: {class: derived-cache, owner: fin-hub}
intake_notes: {class: file-derived, owner: repo-manager, transition: retire-direct-db-writes}
intakes: {class: file-derived, owner: repo-manager, transition: retire-direct-db-writes}
interface_changes: {class: hub-native-control, owner: hub-core}
legacy_interface_usage_buckets: {class: derived-cache, owner: hub-core}
legacy_interfaces: {class: derived-cache, owner: hub-core}
managed_repos: {class: file-derived, owner: repo-manager, note: composite-repository-projection}
progress_events: {class: hub-native-append, owner: hub-core}
repo_goals: {class: file-derived, owner: repo-manager}
sbom_entries: {class: derived-cache, owner: repo-manager}
sbom_snapshots: {class: derived-cache, owner: repo-manager}
service_catalog: {class: hub-native-control, owner: ops-hub}
service_cloud: {class: hub-native-control, owner: ops-hub}
service_first_party: {class: hub-native-control, owner: ops-hub}
service_self_hosted: {class: hub-native-control, owner: ops-hub}
service_third_party: {class: hub-native-control, owner: ops-hub}
suggestion_notes: {class: retired, owner: archive}
suggestion_relevance_bumps: {class: retired, owner: archive}
suggestions: {class: retired, owner: archive}
tasks: {class: file-derived, owner: repo-manager}
td_notes: {class: file-derived, owner: repo-manager}
technical_debt: {class: file-derived, owner: repo-manager}
token_events: {class: hub-native-append, owner: hub-core}
topics: {class: hub-native-control, owner: hub-core, transition: retire-legacy-topic-spine}
tpsc_catalog: {class: hub-native-control, owner: ops-hub}
tpsc_entries: {class: derived-cache, owner: repo-manager}
tpsc_snapshots: {class: derived-cache, owner: repo-manager}
workplan_dependencies: {class: file-derived, owner: repo-manager}
workplan_launch_requests: {class: hub-native-control, owner: activity-core}
workplans: {class: file-derived, owner: repo-manager}
write_idempotency_keys: {class: derived-cache, owner: hub-core}

View file

@ -0,0 +1,46 @@
---
id: RMGR-EVIDENCE-0005-ORPHANS-20260821
type: evidence
workplan_task: RMGR-WP-0005-T09
created: "2026-08-21"
source: local State Hub at http://127.0.0.1:8000
---
# Local-cache orphan disposition rebaseline
The 2026-08-17 baseline of 28 workplans without `backing_filename` is stale.
On 2026-08-21 the local State Hub returned 224 unbound rows:
| Status | Count | Disposition class |
| --- | ---: | --- |
| `active` | 1 | live, broken link |
| `blocked` | 1 | live, broken link |
| `proposed` | 3 | live, broken link |
| `ready` | 2 | live, broken link |
| `finished` | 201 | closed provenance |
| `archived` | 16 | closed provenance |
Every one of the seven live rows has a matching repository file. There are no
confirmed live hub-first workplans in this snapshot.
| Hub slug | Repository file |
| --- | --- |
| `nk-wp-0027` | `net-kingdom/workplans/NK-WP-0027-reef-placement-reconciliation.md` |
| `state-wp-0081` | `state-hub/workplans/STATE-WP-0081-cluster-self-sufficiency-and-registrar.md` |
| `railiance-wp-0022` | `railiance-platform/workplans/RAILIANCE-WP-0022-agent-high-risk-boundary-coverage.md` |
| `railiance-wp-0023` | `railiance-platform/workplans/RAILIANCE-WP-0023-hub-core-candidate-credential-lanes.md` |
| `rail-en-wp-0001` | `railiance-enablement/workplans/RAIL-EN-WP-0001-private-by-default-templates.md` |
| `rail-ho-wp-0010` | `railiance-infra/workplans/RAIL-HO-WP-0010-new-reef-ports-need-a-grant.md` |
| `ops-wp-0003` | `ops-hub/workplans/OPS-WP-0003-extension-contract-alignment.md` |
The 217 closed rows remain cheap historical provenance. They need not be
reconstructed as files, but the cache-rebuild procedure must export or retain
them before replacing the local database. Repairing the seven live bindings is
the remaining prerequisite for T07.
## Reproduction
The counts came from `GET /workplans/`, selecting rows whose
`backing_filename` is null and grouping by status. Repository UUIDs were joined
to `GET /repos/`; canonical ids and exact titles were then searched in each
reported checkout. This evidence records no secret or credential value.

View file

@ -0,0 +1,55 @@
---
id: RMGR-CONTRACT-AUTHORITY-0001
type: contract
title: "State Hub record authority and offline behaviour"
version: "1"
status: draft-reviewable
created: "2026-08-21"
updated: "2026-08-21"
workplan_task: RMGR-WP-0005-T08
---
# State Hub record authority and offline behaviour v1
`config/hub-record-authority.yaml` classifies every table imported by the State
Hub model package on 2026-08-21. It is the machine-readable routing contract;
this document defines the meaning of its classes.
| Class | Truth source | Offline behaviour | Central write rule |
| --- | --- | --- | --- |
| `file-derived` | Repository file and Git history | Change the source file and commit. The commit is the write. | Re-index after Git settles; reject direct state pushes. |
| `hub-native-append` | The named central hub | Append to an idempotent local outbox and replay. | Accept append-only events with a stable source identity. |
| `hub-native-control` | The named central hub | Do not apply generic append-only replay. Stay read-only unless the record type has an ordered, idempotent command contract. | Accept commands at the one authoritative owner. |
| `derived-cache` | A named upstream source, repository file, log, or runtime observation | Do not buffer. Rebuild or refresh. | Replaceable projection only. |
| `retired` | Archived provenance | Reject new writes. | Read-only until physical deletion. |
The first class is ADR-010's file-derived side. The next two are subdivisions of
its hub-native side. This subdivision is necessary because the current schema
contains mutable control records: message read/archive markers, launch-request
status, service catalog changes, and domain/topic administration are not
append-only events and cannot safely be reconciled by unordered union. A generic
offline buffer is therefore valid only for `hub-native-append`.
Several current State Hub REST endpoints still mutate records classified as
file-derived. Those endpoints are compatibility surfaces during extraction;
their existence does not confer authority. Decisions and intakes are
file-derived under the ratified work-record standard and Repo Manager's governed
commands, superseding ADR-010's older examples.
`managed_repos` is a composite projection. Repository identity, purpose, and
classification derive from repository declarations and Forge; host paths and
observation timestamps are replaceable observations. No field in that row makes
the database a source of repository truth.
## Consequences for the offline edge
- Buffer only progress and token event appends under the generic replay path.
- Route file-derived commands to Repo Manager; when central is absent, commit
the file and let later indexing observe it.
- Keep control-plane commands online-only until their owning hub publishes
ordering and idempotency semantics. Queuing them in the append-only stream
would silently invent a conflict policy.
- Rebuild caches and reject writes to retired suggestion tables.
This narrows the offline-buffer part of `STATE-WP-0068`; it does not authorize
changes in repositories owned by hub-core, activity-core, ops-hub, or fin-hub.

View file

@ -39,8 +39,17 @@ silently disambiguated with a repository slug.
```bash
rmgr identifier derive --namespace <fleet-namespace> --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
```
Activation and bulk migration remain separate governed steps. Publishing this
function does not retroactively rewrite existing identifiers.
`migration-plan` is non-mutating. Its versioned JSON output preserves every
current-to-derived UUID mapping and marks each repository as one atomic apply
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.
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.

View file

@ -286,6 +286,14 @@ def main(argv: list[str] | None = None) -> int:
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=".")
p_id_plan = identifier_sub.add_parser(
"migration-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("--namespace", required=True)
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")
args = parser.parse_args(argv)
@ -569,7 +577,11 @@ def main(argv: list[str] | None = None) -> int:
if not args.identifier_command:
p_identifier.print_help()
return 2
from repo_manager.identifiers import derive_work_record_uuid, scan_live_identifier_collisions
from repo_manager.identifiers import (
derive_work_record_uuid,
plan_identifier_migration,
scan_live_identifier_collisions,
)
if args.identifier_command == "derive":
try:
@ -583,8 +595,26 @@ def main(argv: list[str] | None = None) -> int:
"record_id": args.record_id,
"uuid": str(derived),
}
else:
elif args.identifier_command == "preflight":
result = scan_live_identifier_collisions(Path(args.root))
else:
try:
result = plan_identifier_migration(Path(args.root), args.namespace)
except ValueError as exc:
print(json.dumps({"ok": False, "error": str(exc)}, indent=2))
return 1
if args.output:
output = Path(args.output)
if output.exists() and not args.force:
print(
json.dumps(
{"ok": False, "error": f"output exists: {output}; use --force to replace"},
indent=2,
)
)
return 1
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(json.dumps(result, indent=2) + "\n", encoding="utf-8")
print(json.dumps(result, indent=2))
return 0 if result.get("ok") else 1

View file

@ -33,6 +33,33 @@ def derive_work_record_uuid(namespace: str, identifier: str) -> uuid.UUID:
return uuid.uuid5(DERIVATION_NAMESPACE_UUID, derivation_name(namespace, identifier))
def _migration_mapping(
*,
namespace: str,
repo: Path,
path: str,
kind: str,
identifier: str,
current_uuid: str | None,
) -> dict[str, str | None]:
derived_uuid = str(derive_work_record_uuid(namespace, identifier))
if not current_uuid:
action = "assign"
elif current_uuid == derived_uuid:
action = "unchanged"
else:
action = "replace"
return {
"repo": repo.name,
"path": path,
"kind": kind,
"record_id": identifier,
"current_uuid": current_uuid,
"derived_uuid": derived_uuid,
"action": action,
}
def scan_live_identifier_collisions(root: Path) -> dict[str, Any]:
"""Report live workplan/task identifiers that cannot safely be derived."""
by_id: dict[str, list[dict[str, str]]] = defaultdict(list)
@ -66,3 +93,99 @@ def scan_live_identifier_collisions(root: Path) -> dict[str, Any]:
"safe_to_derive": not collisions,
}
def plan_identifier_migration(root: Path, namespace: str) -> dict[str, Any]:
"""Plan a live-record UUID migration without changing files or databases.
The report is the durable old-to-derived mapping required by RMGR-WP-0005.
Eligibility is deliberately computed per repository: a repository with one
colliding or malformed live identifier is skipped as a whole.
"""
# Validate even for an empty fleet.
derivation_name(namespace, "RMGR-WP-0000")
root = root.resolve()
collision_report = scan_live_identifier_collisions(root)
collision_ids = set(collision_report["collisions"])
repositories: list[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):
mappings: list[dict[str, str | None]] = []
blockers: list[dict[str, str]] = []
for path in iter_workplan_files(repo):
parsed = parse_workplan_file(path, repo_root=repo)
if parsed.status not in LIVE_WORKPLAN_STATUSES:
continue
if not parsed.id:
blockers.append({"path": parsed.path, "reason": "live workplan has no canonical id"})
continue
if parsed.id in collision_ids:
blockers.append({"path": parsed.path, "record_id": parsed.id, "reason": "live identifier collision"})
try:
mappings.append(
_migration_mapping(
namespace=namespace,
repo=repo,
path=parsed.path,
kind="workplan",
identifier=parsed.id,
current_uuid=parsed.state_hub_workstream_id,
)
)
except ValueError as exc:
blockers.append({"path": parsed.path, "record_id": parsed.id, "reason": str(exc)})
for task in parsed.tasks:
if task.status in {"done", "cancel"}:
continue
if not task.id:
blockers.append({"path": parsed.path, "reason": "unfinished task has no canonical id"})
continue
if task.id in collision_ids:
blockers.append({"path": parsed.path, "record_id": task.id, "reason": "live identifier collision"})
try:
mappings.append(
_migration_mapping(
namespace=namespace,
repo=repo,
path=parsed.path,
kind="task",
identifier=task.id,
current_uuid=task.state_hub_task_id,
)
)
except ValueError as exc:
blockers.append({"path": parsed.path, "record_id": task.id, "reason": str(exc)})
if not mappings and not blockers:
continue
eligible = not blockers
repositories.append(
{
"repo": repo.name,
"path": str(repo),
"eligible": eligible,
"atomic_unit": True,
"blockers": blockers,
"mappings": mappings,
}
)
totals["repositories"] += 1
totals["eligible" if eligible else "skipped"] += 1
totals["records"] += len(mappings)
for mapping in mappings:
totals[str(mapping["action"])] += 1
return {
"schema": "repo-manager.identifier-migration-plan.v1",
"ok": totals["skipped"] == 0,
"ready_to_apply": totals["skipped"] == 0,
"root": str(root),
"namespace": namespace.strip(),
"derivation_version": DERIVATION_VERSION,
"namespace_uuid": str(DERIVATION_NAMESPACE_UUID),
"scope": "live workplans and unfinished tasks",
"apply_policy": "all-or-nothing per repository",
"totals": totals,
"repositories": repositories,
}

View file

@ -0,0 +1,59 @@
from pathlib import Path
import yaml
EXPECTED_STATE_HUB_TABLES = {
"agent_messages",
"capability_catalog",
"capability_requests",
"contributions",
"decisions",
"doi_cache",
"domain_goals",
"domains",
"extension_points",
"fabric_graph_edges",
"fabric_graph_imports",
"fabric_graph_nodes",
"intake_notes",
"intakes",
"interface_changes",
"legacy_interface_usage_buckets",
"legacy_interfaces",
"managed_repos",
"progress_events",
"repo_goals",
"sbom_entries",
"sbom_snapshots",
"service_catalog",
"service_cloud",
"service_first_party",
"service_self_hosted",
"service_third_party",
"suggestion_notes",
"suggestion_relevance_bumps",
"suggestions",
"tasks",
"td_notes",
"technical_debt",
"token_events",
"topics",
"tpsc_catalog",
"tpsc_entries",
"tpsc_snapshots",
"workplan_dependencies",
"workplan_launch_requests",
"workplans",
"write_idempotency_keys",
}
def test_every_state_hub_table_has_one_authority_class() -> None:
path = Path(__file__).parents[1] / "config" / "hub-record-authority.yaml"
document = yaml.safe_load(path.read_text(encoding="utf-8"))
classes = set(document["classes"])
records = document["records"]
assert set(records) == EXPECTED_STATE_HUB_TABLES
assert {record["class"] for record in records.values()} <= classes
assert all(record.get("owner") for record in records.values())

View file

@ -4,7 +4,11 @@ from pathlib import Path
import pytest
from repo_manager.identifiers import derive_work_record_uuid, scan_live_identifier_collisions
from repo_manager.identifiers import (
derive_work_record_uuid,
plan_identifier_migration,
scan_live_identifier_collisions,
)
def _workplan(path: Path, identifier: str, status: str, task_status: str = "todo") -> None:
@ -48,3 +52,48 @@ def test_preflight_blocks_live_collision_but_ignores_archived_history(tmp_path:
_workplan(two / "workplans" / "two.md", "SHARED-WP-0001", "archived")
report = scan_live_identifier_collisions(tmp_path)
assert report["ok"] is True
def test_migration_plan_preserves_mapping_and_is_atomic_per_repo(tmp_path: Path) -> None:
one = tmp_path / "one"
two = tmp_path / "two"
_workplan(one / "workplans" / "one.md", "ONE-WP-0001", "active")
_workplan(two / "workplans" / "two.md", "TWO-WP-0001", "ready", task_status="done")
report = plan_identifier_migration(tmp_path, "fleet-a")
assert report["ok"] is True
assert report["apply_policy"] == "all-or-nothing per repository"
assert report["totals"] == {
"repositories": 2,
"eligible": 2,
"skipped": 0,
"records": 3,
"replace": 0,
"assign": 3,
"unchanged": 0,
}
one_plan = next(item for item in report["repositories"] if item["repo"] == "one")
assert one_plan["atomic_unit"] is True
assert {item["record_id"] for item in one_plan["mappings"]} == {
"ONE-WP-0001",
"ONE-WP-0001-T01",
}
assert all(item["current_uuid"] is None for item in one_plan["mappings"])
def test_migration_plan_skips_entire_repo_affected_by_collision(tmp_path: Path) -> None:
one = tmp_path / "one"
two = tmp_path / "two"
_workplan(one / "workplans" / "one.md", "SHARED-WP-0001", "active")
_workplan(one / "workplans" / "safe.md", "ONE-WP-0002", "active")
_workplan(two / "workplans" / "two.md", "SHARED-WP-0001", "ready")
report = plan_identifier_migration(tmp_path, "fleet-a")
assert report["ok"] is False
assert report["totals"]["skipped"] == 2
one_plan = next(item for item in report["repositories"] if item["repo"] == "one")
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"])

View file

@ -4,7 +4,7 @@ type: workplan
title: "Repository standards conformance and governed scaffolding"
domain: infotech
repo: repo-manager
status: active
status: finished
owner: codex
topic_slug: infotech
created: "2026-08-16"
@ -86,7 +86,7 @@ anti-patterns (`INTENT.md`+`GOAL.md`, `PRJ-WP-`). Index:
```task
id: RMGR-WP-0004-T02
status: progress
status: done
priority: high
state_hub_task_id: "2c9b0cb1-f0f0-40f7-83ef-1828298338d3"
```
@ -106,11 +106,14 @@ Known first findings, both live today:
- every `prj-` repo would collide on workplan prefix `PRJ-WP-`, because the
prefix is derived from the flavor marker rather than from the project.
**Progress (2026-08-21):** `rmgr conform` reports `missing` vs
`contradictory` and flags both known findings. Remaining: call from the
consistency lane / STATE-WP-0080 guard. The earlier C-15 gate is gone:
**Result (2026-08-21):** `rmgr conform` reports `missing` vs
`contradictory` and flags both known findings. The earlier C-15 gate is gone:
`RMGR-WP-0005-T06` is complete, so this task is no longer `todo` in substance.
The compatibility call is now live as State Hub consistency check C-35
(`state-hub` commit `b9d9ffe`). It executes the Repo Manager command and
projects its findings without copying flavor rules into the retiring hub.
## Own governed scaffolding
```task
@ -151,7 +154,7 @@ Durable flavors get `INTENT.md` and an optional foundation workplan.
```task
id: RMGR-WP-0004-T04
status: wait
status: done
priority: medium
state_hub_task_id: "8cd3ba11-b16b-4910-8bf6-651a24d47b8c"
```
@ -166,6 +169,13 @@ running it on a `prj-` repo produces the standard's session order —
This matters beyond first registration: regeneration runs fleet-wide, so a
flavor-blind template silently re-breaks conformant repos.
Result (2026-08-21): current generator and fleet output were re-audited rather
than changed from the stale task premise. Templates are brief-first and contain
no `INTENT.md` assumption; the four `prj-` repositories have zero incorrect
purpose-document references. The two literal `INTENT.md` mentions explicitly
say project repositories use `GOAL.md`, which is the desired safeguard.
Repo Manager conformance is now the regeneration guard via C-35.
## Accept the handoff from state-hub
```task
@ -293,7 +303,7 @@ ADR-007 § Migration.
```task
id: RMGR-WP-0004-T09
status: progress
status: done
priority: high
state_hub_task_id: "3a6ba954-4e35-4b2d-8dff-d3db84ed832c"
```
@ -459,6 +469,14 @@ to the next free `KONT-WP-` number.
every clash is against an archived file. Assign them before either repo
allocates its next number, or they become live collisions.
Result (2026-08-21): all 11 records named by the live-collision section are now
terminal (`finished` or legacy `completed`). `rmgr identifier preflight --root
/home/worsch` sees 263 live identifiers across 113 repositories with zero
collisions, so T03's gate is cleared without rewriting frozen history. Dormant
future ownership for `WHYNOT-WP`, `OPS-WP`, and `REPO-WP` is handed off as live
residual `RMGR-IN-0002`; it must be resolved before the next allocation under
any of those prefixes.
## Confirm the canon boundary
```task
@ -482,3 +500,10 @@ Result (2026-08-21): the seam is explicit in
standards and Repo Manager implements conformance and governed mutation. The
canon ADR for this workplan links the enforcement placement; no competing
semantic standard was introduced here.
## Closure
Finished 2026-08-21. Standards modeling, conformance, governed scaffolding,
compatibility coverage, identifier uniqueness, prefix ownership, and the canon
boundary are live. Dormant prefix ownership is preserved as registered residual
`RMGR-IN-0002` rather than keeping this implementation workplan open.

View file

@ -313,8 +313,9 @@ derivation scope this task needs:
checked against the live namespace and renumbered if it clashes. Build this
check alongside derivation, not after.
Remaining prerequisite is the live-collision remediation below, tracked on
`RMGR-WP-0004-T09`. 11 files, 5 identifiers.
The live-collision remediation tracked on `RMGR-WP-0004-T09` completed
2026-08-21. All 11 affected files are terminal history, and a fresh fleet
preflight found 263 live identifiers with zero collisions.
Progress (2026-08-21): the versioned derivation function and collision guard are
implemented in Repo Manager and published as
@ -322,15 +323,18 @@ implemented in Repo Manager and published as
`a4058507-5c4a-5a00-ab06-fffa4fb46009` and exact name bytes
`<fleet-namespace>\n<canonical-id>`. `rmgr identifier derive|preflight` provides
independent reproduction and a hard live-collision refusal, including the
unarchive hazard. Activation remains correctly gated on T09's 11-file
remediation and declaration of the current N1 fleet namespace name; no existing
minted identifier was rewritten implicitly.
unarchive hazard. `rmgr identifier migration-plan` now emits the versioned,
non-mutating old-to-derived mapping needed by T04 and refuses a whole repository
when any live record is unsafe. Activation is gated only on declaration of the
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.
## Migrate the fleet
```task
id: RMGR-WP-0005-T04
status: wait
status: progress
priority: high
state_hub_task_id: "503a23a9-ede1-4cf1-bd32-e9669b84ce58"
```
@ -346,6 +350,14 @@ value, update the database to match, and write the file.
- Repositories with unresolved identifier collisions cannot migrate until
`ADR-007` § Migration is ruled on; skip and report them rather than guessing.
**Progress (2026-08-21):** `rmgr identifier migration-plan --root ...
--namespace ... --output ...` implements the non-mutating first phase. Its
versioned JSON retains every current-to-derived UUID, labels the repository as
the atomic unit, and skips an entire affected repository on a collision or
malformed live identifier. Applying those plans remains gated on the namespace
declaration and a compatibility write path that updates file and central
projection together.
## Retire the interim rule
```task
@ -388,7 +400,7 @@ portion of the divergence is redundant and needs no merge — only a rebuild.
```task
id: RMGR-WP-0005-T08
status: wait
status: done
priority: high
state_hub_task_id: "241cf058-2f3e-4d49-8cc9-5c714be4a1cf"
```
@ -411,11 +423,22 @@ this split most of what it buffers does not need buffering, and only the
append-only stream does. Re-examine before building further on it — this likely
reduces its scope.
**Result (2026-08-21):** `config/hub-record-authority.yaml` classifies all 42
tables imported by the current State Hub model package; a regression test keeps
the inventory explicit. `docs/hub-record-authority_v1.md` defines truth source,
offline behaviour, write rule, and target owner. The audit adapted the original
binary wording: hub-native origin contains both append-only events and mutable
control records. Only `progress_events` and `token_events` enter the generic
append/replay path; mutable message, launch, domain/topic, interface, and catalog
state requires an owner-specific ordered command or stays online-only. Decisions
and intakes are file-derived under the ratified work-record standard, superseding
ADR-010's older example list.
## Disposition the orphaned hub-first records
```task
id: RMGR-WP-0005-T09
status: wait
status: progress
priority: high
state_hub_task_id: "d40cc4a8-4280-4940-ac1d-dc1049f1b678"
```
@ -442,6 +465,16 @@ Note: one of these records is already labelled `SPURIOUS bootstrap (statehub
register collision)` in `repo-manager`, independent corroboration of the
`STATE-WP-0080` defect.
**Rebaselined 2026-08-21:** the local cache now has 224 rows without a backing
filename, not 28: 217 are closed (`finished`/`archived`) provenance and seven are
live. All seven live rows have matching files across `net-kingdom`, `state-hub`,
`railiance-platform`, `railiance-enablement`, `railiance-infra`, and `ops-hub`;
there are zero confirmed live hub-first rows in this snapshot. Exact ids and
paths are captured in
`docs/evidence/RMGR-WP-0005-orphan-disposition-2026-08-21.md`. Remaining work is
to repair those seven bindings and retain/export the 217 closed rows before T07
replaces the cache.
## Assign one authoritative hub per record
```task