--- status: historical created: 2026-09-05 archived: 2026-09-05 reason: "Record of the clone rehearsal of S6 item 4 — the Topics member-namespace backfill and its operator link-bind: the gap reproduced, the link-bind closed it, and the deploy that has to precede both was refused by a check the plan's gates do not perform." --- # Rehearsing the Topics member-namespace backfill S6 item 4 of [`../../plans/collection-naming-topics.md`](../../plans/collection-naming-topics.md) is the production backfill that names the Topics filed before the board had a namespace, plus the one-time operator link-bind that lets each of them read the name. This is what a clone rehearsal on 2026-09-05 measured. Estuary was not contacted, read-only calls included. Repository at `63de1a1e8c`. The graft is `2a116f1d73` (#6937); "pre-graft source" below means `topics/main.tsx` and `topics/topic.tsx` at `5fd6cce7c7`, its parent. Every command quoted below was run. Message text is verbatim; JSON envelopes are trimmed to the fields that carry the finding, and long single-line messages are shown on one line inside their fence. One convention about mechanisms, and its limits. This rehearsal measured behavior; it did not instrument causes. Where a passage below says WHY something happened, the reason is read from the named source file and cited there, and no run attributed an observation to it — so an uncited reason is absent rather than a finding. Two rounds of review found five uncited reasons in this document, three of them after that convention was written, which says what the convention is worth: it tells a reader how to read a sentence, and it does not find the sentences. A reason can be stated without any connective to search for, so what found the last three was a person reading each claim and asking whether the runs reached it. ## The headline The gap is real and the link-bind closes it. What the rehearsal did not expect to find is that the deploy which has to happen first is refused on both legs, and that the reason is general rather than particular to `shortName`. Plan item 4 claimed the graft "needs no schema flag"; that claim was wrong, and the gates it rested on do not examine the thing that refuses. ## Setup Two local stores, because the first could not be carried past the refusal. **Store 1 — a genuine pre-graft clone.** A board deployed from the pre-graft `main.tsx`, three Topics filed through *its* `addTopic` (so they predate the namespace rather than being unwired by hand), snapshotted with `VACUUM INTO` and cloned: ``` $ cf space clone did:key:z6MkvL9cg… --from …/pre-graft-snapshot.sqlite --to …/clone cloned did:key:z6MkvL9cgMNDMvoScKXn7WWThANXpnLvtGpRKnoyymXmTCaM snapshot 6.4 MB 2fRzzNiLzKJ4SoPqwkDxnBfCZcYMoHdX9fes3-TcXWU counts 170 commits, 758 revisions, 578 entities content fid1:jQ_m7NFJfywAns9M_T3Yf0PdTOR5bCOL1stWD4eYLWs 480 entities fingerprinted, 123 generated cells excluded ``` Served with `HOST=127.0.0.1 MEMORY_DIR=… ./scripts/start-local-dev.sh --port-offset 70`; the `SERVING A REHEARSAL CLONE` banner was checked in the toolshed log. Baseline `cf space verify` before anything: `baseline intact · removed 0 · changed 0 · added 0 · content unchanged · commits 170 → 170`. **Store 2 — the post-deploy state, assembled directly.** The grafted board from current `main.tsx`, holding Topics that `addTopic` never wired: pieces deployed straight from `topic.tsx` with `cf piece new` and put on the board with `cf piece link /topics/ --allow-non-existing`. It reproduces the SHAPE a completed board-and-children deploy would leave — a grafted board holding grafted Topics with no `boardNames` wiring — by assembling it directly, because the deploy that would reach it was refused on the first store. No forced update ran anywhere in this rehearsal, so nothing here says what a real transition leaves behind; this store supports claims about the shape only. ## Finding 1 — the deploy is refused on both legs `setsrc --check` over the pre-graft clone, board leg: ``` piece source is incompatible with retained input: input link at topics.0 schema is not compatible: input link at topics.0.shortName: an unconstrained schema is no longer accepted ``` and topic leg: ``` piece source is incompatible with retained input: input link at mentionable schema is not compatible: input link at mentionable[].shortName: an unconstrained schema is no longer accepted ``` The checker was verified against a known answer first: re-checking the board with the source it already runs is accepted, so the checker is not refusing everything. **The refusal is not about `shortName`.** Two probes, each the pre-graft `main.tsx` with one property added to `TopicDemand` and nothing else changed: | probe | added to `TopicDemand` | result | | --- | --- | --- | | A | `probeField?: string` | `input link at topics.0.probeField: an unconstrained schema is no longer accepted` | | B | `probeField?: unknown` | identical message | So **any** new property on a per-member demand is refused over a board holding members that do not publish it. The schema recorded on the retained link to each member is unconstrained at that path, and narrowing an unconstrained schema is what `packages/piece/src/schema-compatibility.ts` refuses. A third probe with a defaulted `probeField: string | Default<"">` did not compile and was abandoned; the `Default<>` spelling's own incompatibility is recorded in #6937's message and was not re-measured here. **Why the gates said otherwise.** #6937 concluded "`pattern-compat` is clean against every recorded baseline, so this needs no accepted break and no schema flag to deploy." `tasks/pattern-vintage.ts` states what each of the two tiers proves: `deno task pattern-compat` "proves the contract a pattern declares is still compatible with every contract it has declared before", and pattern-vintage the stronger thing that a real document written by an older version is still materializable. Both are about the pattern's own contract and its own stored documents; neither reaches the schema recorded on a link into a *sibling* piece, which is the check that fires here. Both being green is compatible with the deploy being refused, and was. The forced deploy was **not** rehearsed. `--dangerously-allow-incompatible-schema` is held behind explicit team authorization by `skills/topics/SKILL.md`, and the rehearsal stopped at the refusal rather than waive a proof it had no authorization to waive. What the flag leaves behind on a populated board is therefore still unmeasured. ## Finding 2 — the topic leg has an older blocker, independent of the graft Moving the board first clears the topic leg's `mentionable[].shortName` refusal: with the grafted board in place, `setsrc --check` of the grafted `topic.tsx` over a pre-graft Topic reports something else instead. ``` piece source is incompatible with retained input: input link at mentionable schema is not compatible: input link at mentionable[].piece: newly required argument field has no default ``` That one predates the graft. `cf piece getsrc` of a board-created Topic returns `topic.tsx` and nothing else, and `diff` against the pre-graft file says IDENTICAL — yet `setsrc --check` of that byte-identical source over its own piece is refused with the same message. It reproduced in both stores, including on a control piece deployed clean and never otherwise touched. **What a Topic loses by never taking this step.** Measured on the second store: a Topic still running pre-graft source on a grafted board is named by `backfillNames` like any other member, answers to `cf cell get /top/4 title` with `"Legacy topic C (pre-graft source)"`, and reads back as an ordinary `index` row carrying its title and no `shortName` without emptying the array around it. `cf cell get --cell shortName` reports the property does not exist rather than failing to materialize. So naming, `/top/` addressing and index membership survive the step being skipped, and `shortName` is what is absent. That bounds the loss from BELOW only: no run here forced a Topic source update, so what else a completed one would change is not established by this rehearsal. The mechanism: `mentionable` is declared `Writable` on the Topic (`packages/patterns/topics/topic.tsx`), and `provePreservedContracts` in `packages/piece/src/ops/piece-controller.ts` proves both directions when the destination handle can write. The board's row publishes `piece`; the Topic's `TopicMentionable` projection does not name it; the write-back direction fails. Declaring `mentionable` read-only, as `boardCrossrefs` and `boardNames` already are, would drop that leg of the proof — not attempted here, and it needs checking that nothing writes through the binding first. ## Finding 3 — the gap reproduces The instrument was verified against a known answer before any empty read was trusted: a Topic filed through the grafted `addTopic` in the same board read `"shortName": "1"` in its own result and in the board's `index` row. An empty read from the same commands therefore means the system, not the reader. `backfillNames`, once, through the board: ``` { "invocation": "backfill-1", "status": "settled", "receipt": "/of:fid1:jN3t1AzX5Uq_R2Tj-j2zlchglDmyIak4PTytJ9YBGNw", "result": { "assigned": [ "2", "3" ] } } ``` The board's map and table hold the names: ``` $ cf cell get --cell "$BOARD" names --step { "1": {}, "2": {}, "3": {} } $ cf cell get --cell "$BOARD" namesTable --step --select name [ { "name": "1" }, { "name": "2" }, { "name": "3" } ] ``` The Topics do not. Their `index` rows carry no `shortName` beside the control row that does, and their own read fails to materialize: ``` $ cf cell get --cell "$TOPIC" shortName --step Cannot read piece result at "shortName": stored data is present, but its schema could not resolve all required values. The piece was stepped, but the required value still did not materialize. ``` ## Finding 4 — the link-bind closes it One Topic was bound and a second left alone as a control inside the same board: ``` $ cf piece link "$BOARD/namesTable" "$TOPIC/boardNames" Linked fid1:PsEKqxGfhAaFgHbDMukzSr7_8qF3wISOsKPL0BSsoXs/namesTable to fid1:0SpKr2JWj939XCwSEX4H6uyXA8fzx5HILsNBCPLiiFQ/boardNames ``` Afterwards the bound Topic reads `"2"` from its own `shortName` and from its board `index` row, and `cf cell get /top/2 title` returns its title. The unbound one kept reporting the materialization message above until it was bound later, at which point it read `"3"`. Member addressing was exercised after binding the board's map as a slug with `cf piece set-slug top "$BOARD/names"`: | address | result | | --- | --- | | `cf cell get /top/2 title` | `"Legacy topic A"` | | `cf cell get /@graft-rehearsal/top/2 title` | `"Legacy topic A"` | | `cf cell get //graft-rehearsal/top/2 title` | `Target must include a piece handle, e.g. "/of:fid1:abc123/path".` | | `cf cell get //did:key:z6MkiHoo…/top/2 title` | same refusal | | `cf cell get /top/999 title` | `no member 999 in top` | | `cf cell get /top title` | `no member title in top` | The `///…` spelling from the cell reference grammar is not accepted by this build for either a space name or a DID. ## Finding 5 — cost, idempotence, ordering, partial failure - **Idempotent on both halves.** A second `backfillNames` returns `{"assigned": []}` and writes no key. A second `cf piece link` with the same two endpoints prints `Linked …` and commits nothing: `cf inspect churn --bucket 5` over the bracketing window reported `no timed commits in window`. `wrote to space` on stdout is not evidence of a commit. - **One commit and one revision per Topic** for a bind that changes something, measured the same way on a freshly deployed piece (`1 commits / 1 revisions`). Wall time 1.1 s and 8.6 s on the two binds that were timed. The cost that matters at board scale is one CLI process per Topic: ~125 sequential invocations for the Estuary board, which is the shape [`../topics-board-migration-2026-08-28.md`](../topics-board-migration-2026-08-28.md) found dying 4-6 minutes in from a laptop. - **Binding before the backfill works.** A Topic bound before it had a name read the materialization failure immediately after `backfillNames` returned `["4","5"]`, and read `"5"` on the next identical command with no write between them. The first read after a backfill can report no name for a correctly wired Topic: **read twice before concluding a bind failed.** - **`/top/` resolves without the bind.** A backfilled but unbound Topic answered `cf cell get /top/3 title` with its title while its own `shortName` was still absent. So the bind is what makes the name visible ON THE MEMBER, not what makes it addressable. Why resolution is unaffected was not investigated here; the design has it follow the slug into the map and then the link (`../../plans/collection-naming-topics.md`, decision 3). - **Partial failure is benign and resumable.** A board with some Topics bound and some not served all of them, named rows beside unnamed ones; the repair is to bind the rest, and nothing has to be undone. The audit is on the durable argument, not the derived value: `cf cell get --cell "$TOPIC" boardNames --input --select name` returns the whole names table for a bound Topic and `[]` for an unbound one. The `keys` of the input document say nothing either way; the input declares `boardNames?: ReadonlyCell>` (`packages/patterns/topics/topic.tsx`), which is where the empty value an unbound Topic reads comes from, and no run here separated a defaulted key from a written one. - **`addTopic` keeps wiring its own children afterwards.** A Topic filed after the operator step returned `"name": "6"`, carried the table, and read `"6"`. The step is one-time. - **A Topic on the grafted board that publishes no `shortName` does not empty the array.** A pre-graft-source Topic sitting among grafted ones read back as an ordinary `index` row with a title and no `shortName`, with all four rows present — the claim the property's optional spelling exists for, confirmed on a live board. - **Binding a piece the board does not hold** succeeds and produces no name. The lookup is by identity — `nameOf` finds the row whose `member` `equals` the one asked about (`packages/patterns/collection-naming/naming.ts`) — so a non-member has no row to find; the run observed the absent name, not the lookup. ## Finding 6 — two tool behaviors that mislead **`setsrc --check` writes to the store.** Isolated on the clone after a `cf space reset`, one step at a time. Serving it and stopping again, with nothing run against it, leaves it pristine — `removed 0 · changed 0 · added 0 · content unchanged · commits 170 → 170` — so the server is not what writes. Then exactly one `setsrc --check`, the refused board leg above, which by its own report replaced no source: ``` baseline intact removed 0 changed 0 added 18 content CHANGED commits 170 → 172 revisions 758 → 776 ``` Nothing authored moved. What the run establishes is the size and nothing about where the writes came from: one refused check, 18 entities and two commits. The run did not attribute them. The mechanism is documented at the implementation, and it is not the piece being started. `PieceController.checkPattern()` in `packages/piece/src/ops/piece-controller.ts` says so in its own doc comment: compiling the candidate goes through `compileAndSavePattern`, "which writes the compiled module set and its source docs into the space's content-addressed store (CT-1623) — the same write the apply would do, idempotent, and attached to nothing", while what it does NOT do is touch the piece — "no pointer move, no argument re-stage, no source transition, no revision". So the behavior is intended and known where it is implemented; what is missing is any of it reaching the operator. The clone is no longer pristine, `verify` prints "After a schema migration this is the EXPECTED result" over a clone that had no migration, and a flag documented in its CLI help as reporting "without updating the piece" is a trap for exactly the operator who is being careful. **`--allow-non-existing` on `piece link` is a silent success that poisons the piece.** Binding a Topic whose pattern has no `boardNames` input — one whose source has not been migrated yet — is refused, and that refusal is the guard keeping the bind behind its source migration: ``` Target path "boardNames" does not exist on piece fid1:6hFfedgkyexpLrT0aHBqQIOj-UeETNB9IwKBevb4ke8 Use --allow-non-existing to link anyway. ``` Taking that suggestion prints `Linked …` and changes nothing the pattern can see: the Topic's input map does not gain the key — neither `cf cell get --cell --input` nor `cf piece inspect`'s Source (Inputs) lists it — and no name appears. The link was written into the *argument document* instead, which the pattern's input map does not reach. Two reads do reach it, and both mislead: the argument document's path directly (`/of:fid1:XEqNlJJHF_…/boardNames`) and, more dangerously, `cf cell get --cell boardNames --input`, which is the audit query for "is this Topic bound?" and answers yes. The damage is on the next migration of that Topic. Measured within one piece, same command before and after a single forced bind, with nothing else changed: ``` # before piece source is incompatible with retained input: input link at mentionable schema is not compatible: input link at mentionable[].piece: newly required argument field has no default # after updated arguments do not match the candidate schema: boardNames: value does not match type array piece source is incompatible with retained input: input link at mentionable schema is not compatible: input link at mentionable[].piece: newly required argument field has no default ``` ## What this rehearsal did not cover - The forced deploy under `--dangerously-allow-incompatible-schema`, and therefore anything about what a forced board or Topic update leaves behind. - Whether the Estuary deployment's own vintage refuses the same way. It runs an older commit than either source here, and the retained-link schemas it holds were written by that commit. `cf piece setsrc --check` against the deployment is the one-command answer, and it belongs to whoever is authorized to touch Estuary. - The full source package. The clone's pieces were deployed without attached tests, and the `--check` runs carried none, so nothing here measures the packaging or type-checking of the six `topics/*.test.tsx` entries a real `setsrc` would attach. - Anything about the deployment rather than the store: shell bundle, CDN, and concurrent human traffic are all absent from a clone. ## Where the procedure went The operator procedure — the order, the two commands, the audit query, the measurements above in short form, and the two traps — is the "Naming the Topics that predate the namespace" section of `skills/topics/SKILL.md`.