docs(website): settle the three de-entanglement registries and what they cost
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>
This commit is contained in:
@@ -153,16 +153,36 @@ Everything else is a folder move. These are not:
|
||||
1. **`src/config/notificationStreams.js`** — the push stream catalog. `mapShardEvent()` and most of
|
||||
`STREAMS` are shard-derived, and it imports `PUBLIC_KINDS` from `utils/shardBroadcast`. Push
|
||||
*infrastructure* is core; this *catalog* is module content.
|
||||
→ `registerNotificationStreams({ streams, mapEvent })`.
|
||||
→ `registerNotificationStreams(streams)`.
|
||||
2. **`src/utils/pushDispatch.js`** — core infrastructure, but `fromShardEvent()` (line 112) requires
|
||||
the `shardLinks` model (line 21) and `mapShardEvent` (line 23).
|
||||
→ invert: `publish()` stays core, `fromShardEvent` moves into the module and calls it.
|
||||
3. **`src/utils/announceWorker.js`** — the news dispatcher, with two delivery legs: Discord (core)
|
||||
and town crier (module, via `uoLinkClient.postTownCrier`, line 36).
|
||||
→ `registerAnnounceLeg({ leg, dispatch, classify })`.
|
||||
→ `registerAnnounceLeg({ leg, label, dispatch, classify })`.
|
||||
|
||||
`src/utils/newsGump.js` is module-side (news → in-game gump) and moves whole.
|
||||
|
||||
**Done in Phase 2 PR 4**, with core still the only registrant — the registries are
|
||||
`src/modules/registries.js` and core goes through them by the same door a module will
|
||||
(`registerCore()`, called explicitly from `app.js` before `modules.load()`). What each of the three
|
||||
became:
|
||||
|
||||
1. Split in two. `config/coreStreams.js` is core's one stream (`news.post`, produced by the website's
|
||||
own posts path); `config/shardStreams.js` is the other seven plus `mapShardEvent` and the
|
||||
public-safety filter, and moves to module-uo whole. `registerNotificationStreams` lost its
|
||||
`mapEvent` half — see [`MODULE_API.md`](MODULE_API.md) §2.4 for why that was a leftover, and what
|
||||
follows for the public/personal split.
|
||||
2. Inverted. `pushDispatch.js` is `publish` + `isAllowedEndpoint` and nothing else;
|
||||
`utils/shardPush.js` holds `fromShardEvent` and is what `shardIngest` now calls.
|
||||
3. Legs became registrations, and per-leg **rows**. The `towncrier_*` / `discord_*` column groups on
|
||||
`announce_jobs` could never have held a module's leg — a module cannot `ALTER` a core table — so
|
||||
they became `announce_job_legs`, backfilled and dropped in the same idempotent replay. The worker
|
||||
no longer contains the word "towncrier": it iterates whatever is registered.
|
||||
|
||||
The residue in core is a one-time backfill block in `schema.sql`, deletable once every deployment has
|
||||
booted it, and the two lines of `registerCore()` that Phase 3 turns into module-uo's `register()`.
|
||||
|
||||
### 1.9 A fourth mount shape: module routes under a core resource
|
||||
|
||||
`router/v1/admin/users.router.js` mounts `usersShard.controller.js` at six UO sub-paths of a **core**
|
||||
@@ -175,6 +195,17 @@ narrow **extension slot** on `/admin/users/:id` that the module mounts into, so
|
||||
what "shard" means and all six URLs are preserved. Only core may declare an extension slot; a module
|
||||
may not invent one.
|
||||
|
||||
**Both done in Phase 2 PR 4**, with core filling its own slot: the six paths are
|
||||
`router/v1/admin/usersShard.router.js`, registered into `admin.users.detail` by `registerCore()`, and
|
||||
Phase 3 changes the registrant rather than the routes. The slot's router is created at declare time
|
||||
and filled later, because `users.router.js` is required while `app.js` is still being built. It is
|
||||
mounted **last** on the resource, so core wins any path conflict by first-match.
|
||||
|
||||
One consequence was not foreseen and is worth the warning: **a slot is invisible to static analysis.**
|
||||
There is no literal mount for `swagger-autogen` to follow, so the move silently deleted all six paths
|
||||
from `swagger-output.json` while printing `Success`. The OpenAPI build now merges a generated
|
||||
fragment per filled slot — [`MODULE_API.md`](MODULE_API.md) §6.6.
|
||||
|
||||
### 1.10 The Discord bot has no UO logic
|
||||
|
||||
The draft listed the bot's "UO-specific event/moderation logic" as an extraction candidate. Grepping
|
||||
@@ -453,7 +484,7 @@ too (API §7.2).
|
||||
Exit criterion: `routes.manifest.json` diff is zero lines and every existing test passes. If Phase 2
|
||||
changes one URL, it is wrong.
|
||||
|
||||
**Progress: PRs 1-3 done.**
|
||||
**Progress: PRs 1-4 done.**
|
||||
|
||||
- **PR 1** — `installed_modules` and the state machine, with the stored shape and the boot rules
|
||||
settled in §2.4 above.
|
||||
@@ -476,8 +507,31 @@ changes one URL, it is wrong.
|
||||
as, because the file is replayed on **every boot**. Found while wiring it: `npm run seed` calls
|
||||
`ensureSchema()` without ever requiring `app.js`, so the replay has to tolerate an unscanned loader.
|
||||
|
||||
- **PR 4** — the three de-entanglement registries, `src/modules/registries.js`. Core's own streams,
|
||||
its Discord announce leg and its users-detail routes all go through them, so the seams are
|
||||
exercised on every boot before a module depends on them; §1.8 and §1.9 above record what each
|
||||
became. Four decisions landed with it, all recorded in [`MODULE_API.md`](MODULE_API.md): announce
|
||||
legs became a **child table** rather than waiting for Phase 3 (§2.4 — a module cannot alter a core
|
||||
table, so a registered leg had nowhere to live); **`mapEvent` dropped** from the stream registry
|
||||
(§2.4 — a leftover from before the push inversion was settled); **core registers through the same
|
||||
staging area a module uses**; and core's six shard sub-paths **moved behind the slot now** rather
|
||||
than in Phase 3.
|
||||
|
||||
Registering is **validate-then-commit**: the loader stages a module's claims and the second pass
|
||||
commits them, so a module that throws halfway through `register()` — or fails a later validation
|
||||
step — leaves nothing behind. That is the registry-side twin of PR 2's second-pass mount rule.
|
||||
|
||||
Two build tools needed teaching, both because a mechanism this PR introduced is one they had never
|
||||
seen. `scripts/routeManifest.js` could not decode a **parameterised mount**: its unwinder expected
|
||||
a group shape express does not emit, and the branch had never run. It threw rather than guessing,
|
||||
which is exactly what it is for. And `swagger-autogen` could not follow a route into an extension
|
||||
**slot**, deleting 407 lines while reporting success; the fix is the fragment merge core owed
|
||||
anyway ([`MODULE_API.md`](MODULE_API.md) §6.6).
|
||||
|
||||
There is still no module on the volume and no boot wiring, so this changes nothing an operator or a
|
||||
client can see: 856 tests pass and `routes.manifest.json` is unchanged at 229 routes.
|
||||
client can see: **884 tests pass** and `routes.manifest.json` is unchanged at 229 routes. The two
|
||||
lines of OpenAPI that do move are the retry endpoint's summary and its `leg`, which is no longer a
|
||||
fixed enum because the leg set is whatever has been registered.
|
||||
|
||||
**Phase 3 — Extract `module-uo`.** Moves out of `website/`: the 8 model directories and their 25
|
||||
tables; the nine UO `utils/` files plus `newsGump.js`; the 13 router/controller files;
|
||||
|
||||
Reference in New Issue
Block a user