docs: define repository rename identity contract
All checks were successful
CI Smoke / host-smoke (push) Successful in 1s
CI Smoke / container-smoke (push) Successful in 1s

Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a049a4-ee9f-78e1-9d66-2cb0f9bea3e3
This commit is contained in:
tegwick 2026-08-28 21:24:50 +02:00
parent 80daf659bb
commit 8754983a0d
3 changed files with 277 additions and 4 deletions

View file

@ -55,7 +55,7 @@
| workplan | STATE-WP-0082 | finished | — | workplans/STATE-WP-0082-multi-owner-review-contracts-and-receipts.md |
| workplan | STATE-WP-0083 | active | — | workplans/STATE-WP-0083-forge-derived-projection-reset.md |
| workplan | STATE-WP-0084 | active | — | workplans/STATE-WP-0084-forge-read-for-private-repositories.md |
| workplan | STATE-WP-0085 | ready | — | workplans/STATE-WP-0085-repository-lineage-preserving-rename.md |
| workplan | STATE-WP-0085 | active | — | workplans/STATE-WP-0085-repository-lineage-preserving-rename.md |
| task | STATE-WP-ADHOC-2026-06-04-T01 | done | — | workplans/ADHOC-2026-06-04.md |
| task | STATE-WP-ADHOC-2026-07-01-T01 | done | — | workplans/ADHOC-2026-07-01.md |
| task | STATE-WP-ADHOC-2026-07-01-T02 | done | — | workplans/ADHOC-2026-07-01.md |
@ -321,7 +321,7 @@
| task | STATE-WP-0084-T02 | progress | — | workplans/STATE-WP-0084-forge-read-for-private-repositories.md |
| task | STATE-WP-0084-T03 | progress | — | workplans/STATE-WP-0084-forge-read-for-private-repositories.md |
| task | STATE-WP-0084-T04 | wait | — | workplans/STATE-WP-0084-forge-read-for-private-repositories.md |
| task | STATE-WP-0085-T01 | todo | — | workplans/STATE-WP-0085-repository-lineage-preserving-rename.md |
| task | STATE-WP-0085-T01 | done | — | workplans/STATE-WP-0085-repository-lineage-preserving-rename.md |
| task | STATE-WP-0085-T02 | todo | — | workplans/STATE-WP-0085-repository-lineage-preserving-rename.md |
| task | STATE-WP-0085-T03 | todo | — | workplans/STATE-WP-0085-repository-lineage-preserving-rename.md |
| task | STATE-WP-0085-T04 | todo | — | workplans/STATE-WP-0085-repository-lineage-preserving-rename.md |

View file

@ -0,0 +1,268 @@
---
id: STATE-ADR-001
type: architecture-decision-record
title: "Preserve repository identity across canonical-name changes"
status: accepted
owner: state-hub
date: "2026-08-28"
deciders:
- operator
- state-hub
related:
- STATE-WP-0085
- CUST-ADR-007
- CUST-ADR-012
---
# Preserve repository identity across canonical-name changes
## Status and scope
Accepted for the implementation governed by `STATE-WP-0085`. This decision
defines the State Hub compatibility contract. It does not authorize a live
Forgejo rename.
The contract covers a repository name change within one Forgejo instance and
owner. A Forgejo owner transfer is a different identity and authorization
problem and is out of scope. A product or runtime rename is also out of scope:
module paths, binaries, packages, image names, namespaces, releases, DNS names,
environment variables, policy vocabulary, and user-facing names change only
when the target repository's migration workplan says so.
## Context
State Hub already anchors most durable relationships to `managed_repos.id`, but
it also exposes the current repository slug through URLs, commands, generated
files, routing keys, cached projections, and historical evidence. Treating the
slug as identity would split history when a repository is renamed. Rewriting
every occurrence would instead erase provenance and make interrupted migration
recovery unsafe.
Forgejo is authoritative for the Git repository. State Hub is authoritative
for its repository UUID, canonical slug, protected aliases, and rename-operation
evidence. Repository files remain authoritative for work records; State Hub
continues to be their read model in accordance with `CUST-ADR-012`.
## Decision
A repository is identified by an immutable State Hub UUID and a verified
immutable Forge identity. Its slug, name, URL, and checkout paths are mutable
coordinates. A rename changes coordinates on the existing repository row,
retains the former slug as a protected alias, and records an idempotent phased
operation. It never registers a replacement repository.
### Identity model
| Element | Authority | Mutability | Rename rule |
| --- | --- | --- | --- |
| State Hub repository UUID | State Hub | Immutable | The operation is addressed by UUID; it must be identical before and after every phase. |
| Forge provider/instance | Estate configuration | Immutable in this contract | A move between instances is not a rename. |
| Forge owner | Forgejo | Immutable in this contract | Owner transfer requires a separate contract. |
| Forge repository numeric ID | Forgejo | Immutable | Preflight must verify it; any mismatch fails closed. |
| Canonical repository slug/name | Forgejo, mirrored by State Hub | Mutable | Exactly one canonical slug names the repository at a time. |
| Prior slugs | State Hub | Append-only | Retained as protected aliases; they resolve to the same UUID and cannot be claimed by another live repository. |
| Clone/web URL | Forgejo | Mutable projection | Recomputed from verified forge coordinates; never used as durable identity. |
| Local and host checkout paths | Operator/registration | Mutable projection | Re-registered after a new clone; old paths remain evidence until explicit cleanup. |
| Default branch | Forgejo/Git | Mutable outside the rename | Captured by preflight and compared before mutation; rename does not change it. |
| Expected source commit | Git | Immutable preflight assertion | Apply fails if the selected branch head has moved; a fresh preflight is required. |
| Workplan/task identifiers and Hub UUIDs | Repository files/registrar | Immutable | Preserved without re-keying. Existing prefix values remain valid. |
| Historical event/message/provenance values | Producing system | Immutable evidence | Retain the value observed at event time; resolve aliases only when a live action is requested. |
An implementation may normalize Forge identity into dedicated tables, but the
uniqueness boundary is `(provider instance, owner, forge repository ID)`. It
must not infer or backfill the numeric repository ID from a slug, URL, checkout,
or initial Git commit.
### Invariants
| ID | Invariant | Required proof |
| --- | --- | --- |
| I1 | A rename uses one `managed_repos` row. | Repository-row count and UUID are unchanged. |
| I2 | Forge repository identity is unchanged. | Provider, owner, and numeric ID match the accepted preflight. |
| I3 | The selected source is unchanged at cutover. | Default branch and expected head commit match the accepted preflight. |
| I4 | Work history and telemetry retain identity. | Workplan, task, progress, decision, token, SBOM, service, DoI, and TPSC relationships still reference the same repository UUID and their baseline counts/checksums match. |
| I5 | Existing work-record identifiers are not renamed. | No identifier or filename rewrite is caused solely by the slug change. |
| I6 | The former slug remains usable as an alias. | Old and new slug resolve to the same UUID; canonical responses identify the new slug. |
| I7 | Historical evidence is not rewritten. | Stored event, message, review, contribution, raw metadata, and source-provenance values remain byte-for-byte intact. |
| I8 | Each phase is idempotent. | Repeating a completed phase with the same operation ID is a no-op returning the achieved state. |
| I9 | Every incomplete state has one safe next action. | The operation exposes its achieved phase and recovery instruction. |
| I10 | Cleanup follows verification. | No alias, checkout, route, or rollback input is removed by this operation. |
### Slug treatment vocabulary
Every slug-bearing surface has exactly one primary treatment:
| Treatment | Meaning |
| --- | --- |
| `foreign-key anchored` | Durable relationship already uses the repository UUID. Preserve the row; render the current canonical slug when needed. |
| `alias-resolved` | The value requests a live lookup, route, or mutation. Accept canonical or protected alias, resolve to one UUID, then emit the canonical slug. |
| `reprojected` | Derived data is rebuilt from repository files or current coordinates after cutover. Do not hand-edit cached rows. |
| `historically retained` | The string records what a producer observed. Never rewrite it; a separate repository UUID may be added for future records. |
| `target-repo handoff` | State Hub does not own the source. Generate an explicit task for its owner and verify the result. |
## Slug-bearing surface inventory
This inventory is the baseline for `STATE-WP-0085`. New slug-bearing fields
must declare one of the treatments above before they are added.
### Persistence and domain models
| Surface | Treatment | Owner and required behavior |
| --- | --- | --- |
| `managed_repos.slug`, `name`, `remote_url`, `local_path`, `host_paths`, `git_fingerprint` | alias-resolved | State Hub updates canonical coordinates on the existing UUID. Previous slug becomes a protected alias; previous paths/URL remain operation evidence. |
| `workplans.repo_id`, `repo_goals.repo_id`, `workplan_launch_requests.repo_id` | foreign-key anchored | State Hub preserves relationships and renders the canonical slug. Repository-file bindings are reprojected. |
| `token_events.repo_id`, `sbom_snapshots.repo_id`, `sbom_entries.repo_id`, `tpsc_snapshots.repo_id`, `service_first_party.repo_id`, `doi_cache.repo_id`, `capability_catalog.repo_id` | foreign-key anchored | State Hub preserves rows and history. Cached/materialized responses use the canonical slug after invalidation. |
| `intakes.repo_id` | foreign-key anchored | State Hub preserves the relationship. `source_repo_path` remains historical provenance. |
| `contributions.repo_id` | foreign-key anchored | State Hub preserves a known local target relationship. |
| `contributions.target_repo` | historically retained | Contribution producer owns the observed string; do not rewrite past records. New live inputs resolve aliases when a matching managed repository exists. |
| `agent_messages.from_agent`, `agent_messages.to_agent` | historically retained | State Hub retains delivered envelope values. Message creation and inbox lookup are alias-resolved, so the old repository agent name continues to reach the canonical inbox. Message bodies are never rewritten. |
| `interface_changes.repo_id` | foreign-key anchored | State Hub preserves the origin repository relationship. |
| `interface_changes.affected_repo_slugs` | historically retained | State Hub retains the audience recorded at publication. New publication, pending-recipient queries, and acknowledgements resolve aliases to UUID-equivalent recipients. |
| `work_record_identifier_aliases.repo_slug` | historically retained | Registrar owns identifier provenance. A repository-name alias must not rewrite or repurpose a work-record identifier alias. |
| `review_contracts.source_repo`, `review_receipts.source_repo`, and their `source_path` values | historically retained | Review producer owns source provenance. Future records may additionally bind a repository UUID. |
| `fabric_graph_imports.source_repo_slug`, `fabric_graph_nodes.source_repo_slug`, `fabric_graph_nodes.repo_slug`, `fabric_graph_edges.source_repo_slug` | target-repo handoff | Fabric source owns these projections. Re-import from the updated source and verify lineage; State Hub does not silently rewrite them. |
| Any `source_path`, `raw_metadata`, progress payload, decision text, or event payload containing the old slug | historically retained | The producing component owns the evidence. Search results may enrich it with current canonical identity, but storage is unchanged. |
### REST API and serialization
| Surface | Treatment | Owner and required behavior |
| --- | --- | --- |
| `/repos/{slug}` and `/repos/{slug}/paths`; repo lookup, dispatch, DoI, sync, remote-URL and fingerprint lookup responses | alias-resolved | Repositories router resolves aliases to UUID. A response includes canonical slug and whether the request used an alias; canonical links use the new slug. Mutations bind to UUID. |
| `/repo-goals?repo_slug`, `/sbom/{repo_slug}`, SBOM ingest/list filters, `/tpsc/snapshots?repo_slug` | alias-resolved | Owning routers resolve once through the repository resolver and query by UUID. |
| Capability-catalog, service-first-party, contribution, and workplan-file-binding request `repo_slug` fields | alias-resolved | Request handlers resolve old/new slug to the same UUID. Generated and response values use the canonical slug unless explicitly historical. |
| Message `from_agent`/`to_agent` creation and inbox query parameters | alias-resolved | Message router canonicalizes repository-agent routing for matching while retaining the original envelope. Non-repository agent names are unaffected. |
| Interface-change `repo_slug`, `affected_repo_slugs`, pending `origin_repo_slug`, and affected-repo filters | alias-resolved | Interface-change router resolves local repository recipients by UUID-equivalence and retains original publication evidence. |
| Fabric graph `source_repo_slug` and `repo` filters and payloads | target-repo handoff | Fabric importer accepts old evidence but refreshed canonical data comes from its source projection. |
| Review contract/receipt `source_repo` and contribution `target_repo` representations | historically retained | APIs return the recorded value and may add canonical-resolution metadata; they do not mutate history on read. |
| Repo token summaries, DoI, SBOM, TPSC, service, capability, consistency, and dispatch response slugs derived from `repo_id` | foreign-key anchored | Serializers render the canonical slug from the repository relationship. |
### CLI, MCP, consistency, and generated files
| Surface | Treatment | Owner and required behavior |
| --- | --- | --- |
| `statehub register`, repo update/paths/dispatch/DoI/sync commands, and repo-slug options | alias-resolved | State Hub CLI resolves existing aliases, refuses duplicate registration, and prints UUID plus canonical slug. A new checkout updates the same UUID. |
| MCP tools accepting `repo_slug` for repository updates, ADR validation, consistency, SBOM, TPSC, repo goals, dispatch, interface changes, and ad-hoc tasks | alias-resolved | MCP adapters use the same REST resolver and return canonical identity. They do not implement a second rename path. |
| `scripts/statehub_register.py` slug/path/remote/fingerprint discovery and scaffold output | alias-resolved | Registration script recognizes the Forge identity/alias before POST or PATCH and generates only canonical new material. |
| `scripts/consistency_check.py` repository lookup, host paths, file bindings, inbox queries, prefixes, and briefs | reprojected | Consistency tooling resolves aliases, reads the canonical repository files, and regenerates bindings/briefs without changing stable work-record IDs. |
| `.custodian-brief.md`, `WORK-RECORDS.md`, workplan-file bindings, repository indexes, and other generated repository-name views | reprojected | State Hub consistency projection regenerates current coordinates from canonical source and records its source revision. |
| Repository-native workplans, `AGENTS.md`, catalog/manifest files, and other source files containing the repository slug | target-repo handoff | The target repository migration workplan updates intentional live references and retains historical ones. State Hub verifies the committed source revision. |
### Dashboard, events, edge, and external systems
| Surface | Treatment | Owner and required behavior |
| --- | --- | --- |
| Dashboard repository list and `/repos/[slug]` route | alias-resolved | Dashboard accepts an old route, identifies alias use, and links/redirects to the canonical route without losing the UUID. |
| Dashboard domain, inbox, interface-change, TPSC, SBOM, service, and capability queries | alias-resolved | Dashboard passes either slug to the API and renders returned canonical identity. |
| Existing `org.statehub.repo.registered` events | historically retained | Event Router retains payloads. Registration is not emitted for rename. |
| New `org.statehub.repo.renamed` and rename-phase events | historically retained | State Hub emits operation ID, repository UUID, Forge identity, old/new slug, phase, actor, expected commit, and evidence reference. Events contain no credential. |
| Edge outbox `repo_slug` metadata and literal request path/body | historically retained | Edge relay preserves queued envelopes. Central alias-aware handlers replay old-slug requests against the same UUID. Rename phase mutations are never edge-queueable and require central availability. |
| Edge read-cache keys containing old/new slug | reprojected | Edge relay treats them as separate keys, invalidates repository-scoped keys on phase events, and returns canonical identity. Stale old-slug cache content must not authorize a mutation. |
| Forgejo repository name, web/clone URL, redirects, Actions variables, hooks, branch protection, packages, releases, and deploy keys | target-repo handoff | Forgejo/HelixForge operator performs and verifies the rename. Redirects are compatibility evidence, not canonical configuration. Owner transfer remains out of scope. |
| Downstream Git remotes, submodules, module/dependency references, CI consumers, image/package references, deployments, policies, credentials, fabric declarations, and documentation | target-repo handoff | Each owning repository or platform component receives a target-workplan task. Product/runtime-name changes require explicit decisions rather than following the repository slug automatically. |
| Local working copies and automation checkout paths | target-repo handoff | Operator clones the canonical URL, registers the new path against the existing UUID, verifies it, and only then removes an old checkout under a separately confirmed cleanup step. |
## Operation and phase contract
Every rename has an immutable operation UUID and captures:
- State Hub repository UUID;
- Forge provider, instance, owner, and numeric repository ID;
- old and proposed canonical slug, name, URLs, and registered paths;
- default branch and expected head commit;
- preflight expiry and evidence checksum;
- actor, confirmation evidence, achieved phase, timestamps, and errors.
Each phase is a compare-and-set transition. A caller requests one phase, not an
opaque multi-system transaction. Before changing anything, the phase handler
observes actual State Hub and Forgejo state. If the requested result is already
true for this operation, it returns success without repeating the side effect.
If reality corresponds to a later or conflicting state, it stops and reports
the observed facts.
Live rename phase transitions are Red-lane operations. They require central
State Hub availability, an unexpired accepted preflight, explicit operator
confirmation, and the operation UUID. They are excluded from edge queued-write
allowlists.
### Phase and interruption recovery
| Phase | Durable truth | Allowed next phase | Safe action after interruption |
| --- | --- | --- | --- |
| `draft` | Intent exists; no identity assertion has been accepted. | `preflighted` or cancel draft | Run non-mutating preflight. It is always safe to abandon a draft. |
| `preflighted` | Forge identity, old/new availability, branch/head, baselines, effects, and expiry are recorded; no rename has occurred. | `forge-renamed` | Re-read Forgejo and State Hub. If any compare value changed, expire the preflight and run it again. Otherwise request only the Forge phase. |
| `forge-renamed` | Forgejo numeric ID now has the new name; State Hub may still expose the old canonical slug. | `statehub-rebound` or `rollback-preflight` | Verify Forge ID and commit. Prefer completing the State Hub rebind. If continuation is unsafe, first prove the old Forge slug is free, then enter rollback preflight. Do not register a new Hub repository. |
| `statehub-rebound` | Existing Hub UUID has the new canonical slug and protected old alias; Forge identity matches. | `source-synced` or `rollback-preflight` | Verify both slugs resolve to one UUID. Regenerate State Hub-owned projections and sync repository source. Alias compatibility makes pausing here safe. |
| `source-synced` | Target repository source and State Hub projections are committed at the recorded revision. | `consumers-verified` or `rollback-preflight` | Resume external handoffs from the recorded checklist. Do not assume source changes rolled out to consumers. |
| `consumers-verified` | Required consumers, routes, artifacts, deployment references, and telemetry continuity checks have evidence. | `completed` or `rollback-preflight` | Re-run final invariant checks. Complete only if no required handoff remains open. |
| `completed` | Invariants and required handoffs passed; old alias remains protected. | none in this operation | No automatic action. Optional checkout or compatibility cleanup is a separately approved target-repository task. |
| `rollback-preflight` | Desired rollback point, old-slug availability, current IDs/commits, and irreversible handoffs are recorded. | `rolled-back` or resume forward | If the old slug is unavailable or an external change is unsafe to reverse, stop and resume forward. Otherwise reverse one phase at a time, Forgejo first when its name must change. |
| `rolled-back` | Forgejo and State Hub canonical coordinates are restored and both observed slugs remain protected aliases; history is intact. | new operation only | Verify the original canonical route and consumers. Keep evidence and aliases; investigate before starting a new operation. |
### Failure states
| Failure code | Detection | Mutation rule | Recovery |
| --- | --- | --- | --- |
| `forge-unreachable` | Forge identity or repository cannot be read. | None. | Restore connectivity and repeat the same phase. Never infer success from a redirect or local clone. |
| `forge-identity-mismatch` | Provider, owner, or numeric ID differs from preflight. | None. | Stop for operator investigation; a new preflight cannot waive an identity mismatch without a separately reviewed migration. |
| `source-moved` | Default branch or expected head commit differs. | None. | Expire preflight and generate a new one against the intended commit. |
| `target-slug-conflict` | New slug is owned by another Forgejo or State Hub identity/alias. | None. | Choose another slug or resolve the conflicting ownership outside this operation. |
| `old-slug-unavailable` | Rollback target is no longer free. | No rollback mutation. | Continue forward or resolve ownership explicitly; do not steal or delete an alias. |
| `statehub-conflict` | Old/new slug, alias, or Forge identity resolves to multiple/different UUIDs. | None. | Repair identity data under a separate reviewed incident; preserve all evidence. |
| `forge-renamed-hub-pending` | Forge ID has new name but State Hub remains canonical on old slug. | Only `statehub-rebound` or rollback after preflight. | Verify ID/commit, then resume rebind with the same operation ID. |
| `hub-rebound-source-pending` | Hub canonical slug changed but repository projections/source are stale. | No duplicate rebind. | Resolve through either alias, then run source/projection sync for the recorded revision. |
| `consumer-verification-failed` | One or more external handoffs lack evidence or health checks fail. | Do not complete or clean up. | Keep aliases and both checkout facts; repair the named consumer or enter rollback preflight. |
| `edge-writes-pending` | Relevant outbox envelopes or unknown queued writes exist. | Preflight fails closed. | Replay/quarantine the outbox, verify idempotency against aliases, then preflight again. |
| `operation-conflict` | Another active operation targets the same UUID, Forge ID, old slug, or new slug. | None. | Finish/cancel the owning operation; only one active rename may hold the identity lock. |
| `evidence-drift` | Counts, checksums, paths, or source revision no longer match accepted evidence. | None for the requested phase. | Explain the drift and renew preflight; never silently update the accepted baseline. |
### Retry and rollback limits
- An idempotency key is the operation UUID plus requested phase. Reuse of the
key with different expectations is a conflict.
- A completed phase cannot be overwritten by a retry. Reconciliation may
append observed evidence and an error, but not erase prior evidence.
- Rollback is a new sequence on the same operation record and must itself pass
compare-and-set checks. It never deletes aliases, operation history, events,
or historical values.
- Before `forge-renamed`, rollback means abandoning an unexpired intent and has
no external side effect.
- After `forge-renamed`, automatic rollback is possible only while the old
Forge slug is provably available and external effects can be reversed.
- After source or consumer changes, rollback generates explicit handoffs for
irreversible or independently owned effects. It must not claim that a Forge
and Hub coordinate reversal also reversed packages, deployments, policies,
caches, or product/runtime names.
## Consequences
- T02 must add verified Forge identity, protected repository aliases, and an
operation journal without changing existing repository UUIDs.
- T03 must expose UUID-addressed preflight and one-phase mutation APIs with the
failure codes above. Slug convenience routes are read/resolution helpers only.
- T04 and T05 must centralize alias resolution and prove that history,
telemetry, work records, and queued-write behavior remain continuous.
- T06 must generate target-repository workplans that separate repository,
product, runtime, consumer, and cleanup decisions.
- T07 and T08 may orchestrate and rehearse the contract, but the live
`flex-auth` to `access-engine` operation remains gated by its adopted target
workplan.
- Protected aliases may accumulate. A future alias-retirement policy needs its
own evidence and decision; this rename workflow does not delete them.
## Rejected alternatives
| Alternative | Reason rejected |
| --- | --- |
| Delete and re-register under the new slug | Creates a second repository identity and strands foreign-key history. |
| Update only `managed_repos.slug` | Leaves routing, files, external projections, retries, and recovery undefined. |
| Mass-rewrite every old-slug string | Destroys historical provenance and cannot safely update independently owned systems. |
| Use Forgejo redirects indefinitely | Redirects do not update canonical remotes, routing, packages, policies, or deployment consumers. |
| Make repository and product/runtime rename one operation | Couples independently risky decisions and makes rollback boundaries unknowable. |
| Queue live rename phases through the edge relay | A disconnected multi-system identity mutation cannot prove current Forge identity, conflicts, or ordering. |
## References
- `STATE-WP-0085` — lineage-preserving repository rename workflow
- `CUST-ADR-007` — stable workplan identity and registrar authority
- `CUST-ADR-012` — Forge projection, source revision, and rebuildability
- `docs/architecture/state-hub_v0.1.md` — State Hub system context

View file

@ -116,8 +116,9 @@ N/A for this stub.
## 9. Architecture Decisions
This repo has no `docs/adr/` corpus. Binding decisions live in
the-custodian and are listed on the estate map:
Estate-wide binding decisions live in the-custodian and are listed on the
estate map. Service-local implementation decisions live in `docs/adr/` and do
not supersede the estate decisions:
| Estate ADR | Status | Why it binds this system |
| --- | --- | --- |
@ -126,6 +127,10 @@ the-custodian and are listed on the estate map:
| `CUST-ADR-007` | accepted | One writer of workplan UUIDs. |
| `CUST-ADR-010` | proposed | Two kinds of hub data. |
| Local ADR | Status | Scope |
| --- | --- | --- |
| `STATE-ADR-001` | accepted | Repository identity and recovery contract for canonical-name changes. |
Do not treat a State Hub `/decisions` row as the published ADR.
---