docs/adr/0002-registry-single-writer.md
Rama main · réplica de lectura del repositorio original.
# The Registry is the sole writer of the workspaces table
The `workspaces` table is the Registry from ADR 0001, and its invariants (untrack-never-delete,
tombstone/revert symmetry, observation columns owned by sync, annotation columns owned by features)
were re-implemented by ten independent writers across six slices. We decided a single Registry module
owns every write through a verb vocabulary — `register` (emdash-created, with Provenance), `adopt`
(host-discovered, without), `refresh` (observations only), `untrack`/`revertUntrack`, `resurrect`,
`annotate`, and `purge` (hard delete, valid only on already-untracked rows) — with verbs accepting an
optional transaction handle so callers keep atomicity across tables. Raw drizzle access to the table
outside the module is banned by lint.
## Considered options
- **Full read/write monopoly** — rejected: read needs are join-shaped and consumer-specific
(search wants branch names per task, sync wants live roots per host), so the interface would
inflate into one-caller getters. Instead the Registry exports only `getLive`/`findLiveByKey`
and a named `liveWorkspaces()` predicate that external joins must build on.
- **Writes only, reads raw** — rejected: the liveness rule ("tracked" means `untrackedAt IS NULL`)
appeared ~26 times; when its meaning changes (e.g. excluding removal-pending rows), that must be
a one-site edit.
## Consequences
- The positive-assertion invariant is structural, not remembered: the snapshot type consumed by
convergence is only constructible from a successful scan, and snapshot application returns a report.
- `purge` legitimizes retention deletion of dead rows; it asserts rows are already untracked and
throws otherwise. Untracking remains the only way a tracked row leaves the Registry.
- The `isAnnotated` predicate (task link or Provenance present) is a Registry rule, not a sync detail.