feat(modules): the three de-entanglement registries, with core as the registrant #131
Reference in New Issue
Block a user
No description provided.
Delete Branch "feature/module-registries"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Phase 2 PR 4 of
docs/website/MODULE_SYSTEM.md§2.7. Addsserver/src/modules/registries.jsand moves core's own notification streams, announce leg and users-detail routes behind it — so the three seams §1.8 and §1.9 named are exercised on every boot, long before a module depends on them. Docs half: docs#129.Nothing an operator or a client can see changes. 884 tests pass (856 before),
routes.manifest.jsonis unchanged at 229 routes, and the OpenAPI diff is two lines of intent.The four decisions
announce_job_legs (job_id, leg, status, attempts, last_error, next_attempt_at)replaces thetowncrier_*/discord_*column groups. A module cannotALTERa core table, so a registered leg had nowhere to live — the registry's signature was a promise the schema could not keep. Doing it now rather than in Phase 3 means doing it without also moving forty files.mapEventdropped fromregisterNotificationStreamsfromShardEventand calls core'spublish()with a stream id it resolved. A second mapping mechanism was a leftover. The public-safety filter, the kinds it reads and the streams it protects now live in one file and move together.registries.registerCore()inapp.jsbeforemodules.load(). A registry only core's hardcoded base bypasses is one whose first real exercise is a module./admin/users/:id/shard/*paths moved behind the slot nowadmin.users.detailslot;getUsermoved back toadmin.controller.jsper §1.9. Phase 3 changes the registrant, not the routes.Validate-then-commit
The loader stages what a module claims and the second pass commits it. A module that throws halfway through
register()— or failscheckDeclaredafter it — leaves nothing behind. A half-registered stream catalog would be worse than a missing one: it is a subscribable stream nothing will ever publish to. This is the registry-side twin of PR 2's second-pass mount rule, and two new loader tests pin it.Two build tools needed teaching
Both because a mechanism this PR introduces is one they had never seen.
scripts/routeManifest.jscould not decode a parameterised mount. Its unwinder expected(?:([^\/]+?)); express 4.22 emits(?:\/([^/]+?)), with the separator inside the group. The branch had never run, because until now every mount in the tree was literal. It threw rather than guessing, which is exactly what it is for.swagger-autogencould not follow a route into an extension slot. The slot's router is created bydeclareSlot()and filled later, so there is no literal mount for a static parse. Regenerating deleted 407 lines and printedSwagger-autogen: Success— the spike's exact failure (MODULE_API.md§7.4), and it would have shipped six undocumented core routes.swagger/slotSpecs.jsgenerates a fragment per filled slot and re-roots it at the prefix the router actually hangs at in the live app — read from the express stack viarouteManifest's ownmountPath, so the manifest and the spec cannot disagree about what a mount decodes to. Which slots and where each hangs are both derived, never written down. An empty fragment is a hard build failure, because an empty fragment is what the silent drop looks like.swagger/mergeSpec.jsis the ~40-line merge helper §6.1a already owed core for module fragments — written here and proved against core's own slot first.Verification
moduleRegistries.test.js, 9 inannounceLegs.test.js, 3 inadminUsers.test.js, 2 more loader tests, plus retargets.routes.manifest.json: zero-line diff, 229 routes.routes.guards.jsonmoves by one line — the DELETE unlink route's handler count 5→4, from dropping an inlineadminOnlythe router-leveluse(adminOnly)already applies. Its gate list is unchanged, as are all six slot routes'.legno longer being a fixed["towncrier","discord"]enum.information_schemabecause aSELECTof a dropped column is a parse error, not a runtime one; the columns go withDROP COLUMN IF EXISTS, MariaDB-native and replay-safe.npm run seedstill works (PR 3's second-entry-point trap).What is left of
towncrierin coreThe structural mentions are gone — no columns, no
LEGSarray, no branch in the worker. What remains is prose, theLEGACY_LEGSallowlist, the tworegisterCore()lines Phase 3 turns into module-uo'sregister(), and the one-time backfill block inschema.sql, which is documented as deletable once every deployment has booted it. Phase 3's acceptance grep should expect those.AI-assisted (Claude Code).
🤖 Generated with Claude Code
https://claude.ai/code/session_018ocYxQWk3EhZe5gWRJXFU8