docs(website): settle the three de-entanglement registries and what they cost #129

Merged
whitlocktech merged 1 commits from docs/module-registries into main 2026-08-10 23:16:48 +00:00
Member

Phase 2 PR 4 of MODULE_SYSTEM.md §2.7 — the docs half of website#131. Records what §1.8's three entangled files and §1.9's extension slot actually became, and the four decisions taken with them.

MODULE_API.md §2.4 — the registration surface

  • registerNotificationStreams takes the catalog alone. mapEvent was a leftover from before §1.8's push inversion was settled: a module owns fromShardEvent outright and calls publish() with a stream id it already resolved, so core never needs a second route to the same place. It follows that the public-safety filter (a sensitive kind can never produce a public push) is module-internal — which is the right home, because the kinds, the streams and the filter then become one file that moves together rather than a rule in core about data only the module defines.
  • Two booleans, not one scope. The entry shape is the response body of GET /auth/me/notifications/streams, and a shipped Android client reads both fields. scope was never the wire shape.
  • registerAnnounceLeg gains label, so a module's leg renders in the admin panel with no client change — the panel used to hold a client-side { towncrier, discord } label table.
  • Legs are rows, not columns. announce_jobs carried leg-prefixed column groups; a module cannot ALTER a core table, so a registered leg had nowhere to live. Hence announce_job_legs, and a rollup over all of a job's legs.
  • Every call stages. Shape is checked at the call; whether a name is taken can only be answered once the batch is complete. A module that throws halfway through register() leaves nothing behind — the registry-side twin of §4.3's second-pass mount rule.

New §6.5 — grandfathered names

Eight names predate the module system and cannot take a module prefix: seven stream ids (stored in notification_subs, read by a shipped Android client) and towncrier (a stored value in announce_job_legs.leg). Allowed to uo alone by explicit allowlist — the same shape and reasoning as the loader's LEGACY_TABLE_PREFIXES. Grandfathering rather than dropping the rule is what keeps it real for every module written after this one.

New §6.6 — a slot is invisible to static analysis

§6.1 settled the fragment merge for modules. PR 4 found core needs the identical machinery for its own slot fills, one phase earlier than the plan expected: a slot's router is created by declareSlot() and filled later, so there is no literal mount for swagger-autogen to follow. Moving the six users-detail routes behind the slot deleted 407 lines from swagger-output.json — with Swagger-autogen: Success and no warning. Same failure as §7.4, different cause, and it would have shipped six undocumented core routes against CLAUDE.md's standing rule.

Which slots, and where each hangs, are both derived — from registries.filledSlots() and from the live express stack via routeManifest's own mountPath — because a written-down copy of either is a copy that drifts.

Also

  • MODULE_SYSTEM.md §1.8 / §1.9 record the outcome per file; §2.7 the progress.
  • BACKEND_DESIGN.md and website-README.md follow the moved files. The PUBLIC_KINDS reference in the visibility section named notificationStreams.js; it has always lived in utils/shardBroadcast.js, and is corrected.

Pairs with website#131. AI-assisted (Claude Code).

🤖 Generated with Claude Code

https://claude.ai/code/session_018ocYxQWk3EhZe5gWRJXFU8

Phase 2 PR 4 of `MODULE_SYSTEM.md` §2.7 — the docs half of website#131. Records what §1.8's three entangled files and §1.9's extension slot actually became, and the four decisions taken with them. ## `MODULE_API.md` §2.4 — the registration surface - **`registerNotificationStreams` takes the catalog alone.** `mapEvent` was a leftover from before §1.8's push inversion was settled: a module owns `fromShardEvent` outright and calls `publish()` with a stream id it already resolved, so core never needs a second route to the same place. It follows that the public-safety filter (a sensitive kind can never produce a *public* push) is module-internal — which is the right home, because the kinds, the streams and the filter then become one file that moves together rather than a rule in core about data only the module defines. - **Two booleans, not one `scope`.** The entry shape is the response body of `GET /auth/me/notifications/streams`, and a shipped Android client reads both fields. `scope` was never the wire shape. - **`registerAnnounceLeg` gains `label`**, so a module's leg renders in the admin panel with no client change — the panel used to hold a client-side `{ towncrier, discord }` label table. - **Legs are rows, not columns.** `announce_jobs` carried leg-prefixed column groups; a module cannot `ALTER` a core table, so a registered leg had nowhere to live. Hence `announce_job_legs`, and a rollup over *all* of a job's legs. - **Every call stages.** Shape is checked at the call; whether a name is taken can only be answered once the batch is complete. A module that throws halfway through `register()` leaves nothing behind — the registry-side twin of §4.3's second-pass mount rule. ## New §6.5 — grandfathered names Eight names predate the module system and cannot take a module prefix: seven stream ids (stored in `notification_subs`, read by a shipped Android client) and `towncrier` (a stored value in `announce_job_legs.leg`). Allowed to **`uo` alone** by explicit allowlist — the same shape and reasoning as the loader's `LEGACY_TABLE_PREFIXES`. Grandfathering rather than dropping the rule is what keeps it real for every module written after this one. ## New §6.6 — a slot is invisible to static analysis §6.1 settled the fragment merge for *modules*. PR 4 found core needs the identical machinery for its **own** slot fills, one phase earlier than the plan expected: a slot's router is created by `declareSlot()` and filled later, so there is no literal mount for `swagger-autogen` to follow. Moving the six users-detail routes behind the slot deleted **407 lines** from `swagger-output.json` — with `Swagger-autogen: Success` and no warning. Same failure as §7.4, different cause, and it would have shipped six undocumented core routes against CLAUDE.md's standing rule. Which slots, and where each hangs, are both **derived** — from `registries.filledSlots()` and from the live express stack via `routeManifest`'s own `mountPath` — because a written-down copy of either is a copy that drifts. ## Also - `MODULE_SYSTEM.md` §1.8 / §1.9 record the outcome per file; §2.7 the progress. - `BACKEND_DESIGN.md` and `website-README.md` follow the moved files. The `PUBLIC_KINDS` reference in the visibility section named `notificationStreams.js`; it has always lived in `utils/shardBroadcast.js`, and is corrected. Pairs with **website#131**. AI-assisted (Claude Code). 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_018ocYxQWk3EhZe5gWRJXFU8
wtclaude added 1 commit 2026-08-10 22:51:37 +00:00
Phase 2 PR 4 of MODULE_SYSTEM.md §2.7. Records what §1.8's three entangled files
and §1.9's extension slot actually became, and four decisions taken with them.

MODULE_API.md §2.4:
- registerNotificationStreams takes the catalog ALONE. `mapEvent` was a leftover
  from before §1.8's push inversion was settled — a module owns fromShardEvent
  and calls publish() with an id it resolved, so core never needs a second route
  to the same place. It follows that the public-safety filter is module-internal,
  which is the right home: the kinds, the streams and the filter become one file
  that moves together.
- the entry shape is two booleans, not a single `scope` — that object is the body
  of GET /auth/me/notifications/streams and a shipped Android client reads both.
- registerAnnounceLeg gains `label`, so a module's leg renders in the admin panel
  with no client change; and legs became `announce_job_legs` ROWS, because a
  module cannot ALTER a core table and a registered leg had nowhere to live.
- every call STAGES; nothing commits until the module as a whole is known good.

New §6.5 — the eight grandfathered names (seven stream ids, one leg id), allowed
to `uo` alone by explicit allowlist, the same shape as the loader's legacy table
prefixes. Grandfathering rather than dropping the rule is what keeps it real for
every module written after this one.

New §6.6 — an extension slot is invisible to static analysis, so core needs §6.1's
fragment merge for its OWN slot fills, a phase earlier than the plan expected.
Moving the six users-detail routes behind the slot deleted 407 lines from
swagger-output.json while printing `Success`. Which slots and where each hangs are
both derived — from the registry and from the live express stack — because a
written-down copy drifts.

MODULE_SYSTEM.md §1.8 / §1.9 record the outcome per file, and §2.7 the progress.
BACKEND_DESIGN.md and website-README.md follow the moved files; the PUBLIC_KINDS
reference at §"visibility" named the wrong file and is corrected to
utils/shardBroadcast.js.

Co-Authored-By: Claude <noreply@anthropic.com>
whitlocktech merged commit 30589f1fa5 into main 2026-08-10 23:16:48 +00:00
whitlocktech deleted branch docs/module-registries 2026-08-10 23:16:49 +00:00
Sign in to join this conversation.
No description provided.