diff --git a/docs/repository-standards_v0.1.md b/docs/repository-standards_v0.1.md index c6a1f7d..1b47fc0 100644 --- a/docs/repository-standards_v0.1.md +++ b/docs/repository-standards_v0.1.md @@ -33,3 +33,24 @@ rmgr prefix-uniqueness --root .. Registry: [`config/workplan-prefix-registry.yaml`](../config/workplan-prefix-registry.yaml). Work: `RMGR-WP-0004-T01`, `RMGR-WP-0004-T08`. + +## Identifier registrar (interim) + +Until UUIDv5 derivation lands (`RMGR-WP-0005-T03`), only the registrar +instance may mint `state_hub_workstream_id` / `state_hub_task_id` into +files (`ADR-007` decision 2). Other hubs may read and project; they must +not write new hub primary keys into git. + +| Signal | Registrar? | +| --- | --- | +| `STATEHUB_REGISTRAR=1` / `true` / `yes` / `on` | yes | +| `STATEHUB_REGISTRAR=0` / `false` / `no` / `off` | no | +| env unset, hostname starts with `railiance` | yes | +| env unset, any other hostname | no | + +Accepted cost: new workplan/task registration requires connectivity to +the registrar. Disconnected work cannot register until T03. Implementation: +`repo_manager.registrar.is_identifier_registrar`; consumed by +`statehub fix-consistency` C-06 / C-11 / C-32. + +Work: `RMGR-WP-0005-T01`. diff --git a/src/repo_manager/registrar.py b/src/repo_manager/registrar.py new file mode 100644 index 0000000..0b21f77 --- /dev/null +++ b/src/repo_manager/registrar.py @@ -0,0 +1,30 @@ +"""Interim single-writer for hub identifiers (ADR-007 decision 2). + +Until deterministic derivation (RMGR-WP-0005-T03) ships, exactly one +instance may mint ``state_hub_workstream_id`` / ``state_hub_task_id`` +into repository files. Other instances may read and project; they must +not write new hub primary keys into git-tracked files. + +The registrar is the automated production instance (hostname prefix +``railiance``). Override with ``STATEHUB_REGISTRAR=1`` or ``=0``. +""" + +from __future__ import annotations + +import os +import socket + +_TRUE = frozenset({"1", "true", "yes", "on"}) +_FALSE = frozenset({"0", "false", "no", "off"}) +REGISTRAR_HOST_PREFIX = "railiance" + + +def is_identifier_registrar(*, hostname: str | None = None) -> bool: + """Return True if this process may mint hub IDs into repository files.""" + raw = os.environ.get("STATEHUB_REGISTRAR", "").strip().lower() + if raw in _TRUE: + return True + if raw in _FALSE: + return False + host = (hostname if hostname is not None else socket.gethostname()).strip().lower() + return host.startswith(REGISTRAR_HOST_PREFIX) diff --git a/tests/test_registrar.py b/tests/test_registrar.py new file mode 100644 index 0000000..57c9c16 --- /dev/null +++ b/tests/test_registrar.py @@ -0,0 +1,25 @@ +from repo_manager.registrar import is_identifier_registrar + + +def test_env_true_wins_over_hostname(monkeypatch): + monkeypatch.setenv("STATEHUB_REGISTRAR", "1") + assert is_identifier_registrar(hostname="bnt-lap001") is True + + +def test_env_false_wins_over_production_hostname(monkeypatch): + monkeypatch.setenv("STATEHUB_REGISTRAR", "0") + assert is_identifier_registrar(hostname="railiance") is False + + +def test_hostname_prefix_when_env_unset(monkeypatch): + monkeypatch.delenv("STATEHUB_REGISTRAR", raising=False) + assert is_identifier_registrar(hostname="railiance") is True + assert is_identifier_registrar(hostname="railiance-prod") is True + assert is_identifier_registrar(hostname="bnt-lap001") is False + + +def test_env_aliases(monkeypatch): + monkeypatch.setenv("STATEHUB_REGISTRAR", "yes") + assert is_identifier_registrar(hostname="other") is True + monkeypatch.setenv("STATEHUB_REGISTRAR", "off") + assert is_identifier_registrar(hostname="railiance") is False diff --git a/workplans/RMGR-WP-0004-repository-standards-conformance.md b/workplans/RMGR-WP-0004-repository-standards-conformance.md index 8d49cbe..bd5cf4c 100644 --- a/workplans/RMGR-WP-0004-repository-standards-conformance.md +++ b/workplans/RMGR-WP-0004-repository-standards-conformance.md @@ -170,7 +170,7 @@ flavor-blind template silently re-breaks conformant repos. ```task id: RMGR-WP-0004-T05 -status: todo +status: done priority: medium state_hub_task_id: "8402b8b6-2a1e-4236-b27d-d50b6161a176" ``` @@ -192,6 +192,12 @@ Follow the dual-run strangler pattern already proven in `RMGR-WP-0002` and points at `rmgr scaffold`. T03 is done. Cutover / inventory move can start alongside `STATE-WP-0080-T02`. +**Result (2026-08-18):** Cutover is live via `STATE-WP-0080-T02`: project +flavor delegates to `rmgr scaffold` (or registers only when `GOAL.md` +exists); durable repos keep hub templating until `STATE-WP-0080-T04` +deletes it. Inventory `caller:custodian-cli` is already `move` → +`repo-manager`. Coordination remains `SHR-WP-0001`. + ## Add compatibility and regression tests ```task diff --git a/workplans/RMGR-WP-0005-registrar-consolidation-deterministic-ids.md b/workplans/RMGR-WP-0005-registrar-consolidation-deterministic-ids.md index 7f2804e..e2f18fd 100644 --- a/workplans/RMGR-WP-0005-registrar-consolidation-deterministic-ids.md +++ b/workplans/RMGR-WP-0005-registrar-consolidation-deterministic-ids.md @@ -50,7 +50,7 @@ model; a file carrying a hub's private key is the file holding hub state. ```task id: RMGR-WP-0005-T01 -status: todo +status: done priority: high state_hub_task_id: "b57a6882-280d-4f0a-9c73-899843dfc3d3" ``` @@ -69,6 +69,12 @@ workstation hubs are rebuildable caches (`ADR-010` decision 2). Interim, and deliberately so — it trades availability for correctness, and T03 removes the need for the trade. +**Result (2026-08-18):** `repo_manager.registrar.is_identifier_registrar` +(`STATEHUB_REGISTRAR` env, else hostname prefix `railiance`). +`statehub fix-consistency` skips C-06 / C-11 / C-32 mint+writeback when +this instance is not the registrar. Read/project checks are unchanged. +Cost documented in `docs/repository-standards_v0.1.md`. + ## Re-register identifiers minted outside the registrar ```task