Commit Graph

371 Commits

Author SHA1 Message Date
d4beba1a5d Merge pull request 'docs(tree): sync website/PROJECT_TREE.md' (#150) from chore/sync-website-tree into main
Reviewed-on: #150
2026-08-12 23:17:29 +00:00
runic-docs-bot
fc3dd30feb docs(tree): sync website/PROJECT_TREE.md from RunicGateway/website@e0c961c [skip ci] 2026-08-12 23:10:47 +00:00
02fd9f4c2d Merge pull request 'docs(modules): record the cutover, and retire what it makes untrue' (#149) from docs/module-system-cutover into main
Reviewed-on: #149
2026-08-12 23:05:56 +00:00
cfd1800347 docs(modules): record the cutover, and retire what it makes untrue
The module system reached `main` on 2026-08-12 (website#150) and `edge` was
deleted. Several present-tense claims across two plans became false at that
merge; this is the pass that closes them.

MODULE_SYSTEM.md's status banner was the loudest: it still announced "in
implementation - Phase 2's core scaffolding is landing, PRs 1-7 of 9 done",
three phases stale. It now says SHIPPED and gives the shape of what landed.

The substantive addition is 2.9's "as landed" section: the four decisions that
settled the cutover (none of them a code question), and what the merge does to a
LIVE deployment - no DROP TABLE anywhere, one guarded and already-verified
column drop in 228 changed files, no newly-required env var, and a declared
module starting on the same first boot rather than waiting for an operator
click. That paragraph is the one a future cutover will want, and none of it was
written down anywhere before.

Phase 5 is marked COMPLETE in the phase list (2.11.1 already recorded slice 3
closing it), decision 11 carries its landing date, and 2.11.1's "the pin is one
of the things the cutover has to revisit" is resolved in place - the sha did not
move, only the branch label, so the coreApi equality assertion stays armed.

The Android note is the one that changes meaning rather than tense. It said the
`gameAccountSignup` fix "must land before that cutover"; the org lead released
that gate instead, so the note now records a LIVE regression - the shipped app
hides game-account creation from every user - rather than a scheduling item. An
accepted break and a forgotten one differ only by whether someone wrote it down.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-12 18:03:34 -05:00
33838bdf10 Merge pull request 'docs(modules): correct F3 in the acceptance record' (#148) from docs/kit-acceptance-f3-correction into main
Reviewed-on: #148
2026-08-12 22:42:15 +00:00
c33a8a5509 docs(modules): correct F3 in the acceptance record
Only the Gitea release workflow carries the publishing placeholders. The report
said both flavours; the GitHub one uses GITHUB_REPOSITORY and friends and has
nothing to rename. Found while fixing it in the kit (Integration-kit#4), which is
where the single checklist row landed.

Also records what that implies about a run like this: every finding was
re-derived before being acted on, and it was not ceremony — this one was wrong in
a detail and F5 understated its problem in a way that changed the fix. A cold
agent's report is evidence, not a verdict, which is the standard the rest of the
plan already applies to a passing suite.

Separate PR because #147 was already merged; a push to a merged branch is silent.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-12 17:41:37 -05:00
4eec823741 Merge pull request 'docs(modules): close Phase 5 — the acceptance run, and the page shell it found' (#147) from docs/module-phase5-slice3 into main
Reviewed-on: #147
2026-08-12 19:34:02 +00:00
3116e7bbf6 docs(modules): close Phase 5 — the acceptance run, and the page shell it found
Slice 3 of Phase 5 (MODULE_SYSTEM.md 2.11.1), and the phase's last slice.

docs/modules/kit-acceptance.md is decision 5's deliverable: a cold agent given the
Integration Kit and the documents it links to — never core's source, never
module-uo — built a working module for a second game, which was then installed
into a real core and taken through MODULE_API.md 7.7's browser smoke. Verdict
recorded whichever way it went, and it went **yes, with caveats**: one pass, no
core source, and three of the four normative documents never opened.

The finding that justifies the two-stage shape is the one the agent structurally
could not reach, because it had no core to render against. A module page built
exactly as the kit teaches renders OUTSIDE the site: PublicLayout supplies the
chrome and not the body, and the `shell-... page-body` wrapper every core public
page writes for itself is two class names that appear in no contract. That is
3.4's own stated failure — "a module page that does not look like the site it is
installed in" — reached by following 3.4.

Fixed in core rather than documented at the reader, so the class names stay
core's private business and the theming workstream keeps its freedom to rename
them: PublicLayout takes an opt-in `shell` width, MODULE_API_VERSION 1.5.0
(website#148, merges first).

- MODULE_API.md 1.1: 1.5.0's entry, and a new bump-table row — adding an
  OPTIONAL prop or argument is minor. "A member's signature changes" is major
  because a call already written changes meaning, and an optional prop changes
  none; the table now says what it means rather than leaving it to be argued.
- MODULE_API.md 3.4: the shell prop, why a module names a width and never a
  class, and the eight-vs-seven miscount the run also turned up — the kit had
  faithfully carried it out of the contract into the template, which is the
  never-re-specify rule working exactly as designed on a wrong input.
- rust-dryrun.md: coreApi ^1.3.0 -> ^1.5.0, as a dated correction per decision 33.
  It is the only complete module.json in the kit's reading path and nothing
  checks a JSON block inside a Markdown file, which is the reusable half.
- MODULE_SYSTEM.md 2.11.1: slice 3 recorded, plus the third finding worth
  generalising — a check whose failure message asserts a diagnosis has to be
  right about it. `check:swagger` failed on a pristine template on Windows
  (CRLF) while blaming the routes, green on the Linux runner forever.
- Decision 34: core owns the page body as well as the chrome.

The banner does not come off. Decision 32 makes that a person's to remove, this
run exercised the website-module half only (the module has no sidecar, so
chapters 3 and 4 were never tested), and an agent does not skim or give up.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-12 14:28:26 -05:00
c6f4c2478f Merge pull request 'docs(modules): record the kit's extra checks and slice 2' (#146) from docs/module-phase5-slice2 into main
Reviewed-on: #146
2026-08-12 18:36:41 +00:00
abd04f04fd docs(modules): record the kit's extra checks and slice 2
Decision 2 of §2.11.1 still said the kit's CI "checks three things"; it runs five.
Two were added as the things they guard came into existence, and the pattern is
worth naming rather than leaving as CI archaeology: each exists because a class of
claim in that repo had nothing looking at it. checkRenameSites (slice 0) holds the
template's rename checklist against the tree in both directions.
checkChapterPaths (slice 2) requires every path a chapter names in backticks to
exist — not a markdown link, so the link check never saw those, and not code, so
nothing else did.

Its anchor list is STATED, not derived from the tree, for the reason API §3.6 gives
about the template's own build guard: a derived list cannot fail when what it
derives from changes, so a renamed `template/` would stop being checked at the
moment every mention of it became wrong. An anchor matching nothing fails the
build, the same rule §5.2's exemptions follow.

Also records Phase 5 progress (slices 0-2 landed; only the acceptance run is
outstanding) and slice 2's verification, which is the shape decision 5 scales up:
chapter 1's walkthrough was RUN against a real core on `edge`, including the three
failures the chapter tells a reader to cause on purpose, and the chapter now quotes
core's own messages for them.

Integration-kit half: Integration-kit#3.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-12 13:21:46 -05:00
5badb91a96 Merge pull request 'docs(modules): settle Phase 5 — the Integration Kit, and the sidecar rule' (#145) from docs/module-phase5-plan into main
Reviewed-on: #145
2026-08-12 14:40:54 +00:00
89aff5c26b docs(modules): settle Phase 5 — the Integration Kit, and the sidecar rule
Phase 4 closed; this is the plan for Phase 5, the Integration Kit, settled with
the org lead on six decisions.

MODULE_SYSTEM.md
- §2.11.1 (new): Phase 5's shape — measured starting state, the six decisions,
  the four slices, the acceptance mechanism.
- §2.11: layer 2 rewritten. The sidecar is the default and only path to a game,
  and it is a non-blocking dumb forwarder that persists before it forwards. A
  game that already ships a remote-control surface gets a thin sidecar, not none.
- §2.10: a MODULE_API_VERSION bump now carries a pass over the kit.
- Part 3: decisions 28–33.
- The Phase 5 line in Part 2's phase list: STARTED 2026-08-12.

MODULE_API.md
- §2.7 gains one prohibition: a module does not open a connection to a game
  server from the website process — a game socket, RCON, a query port, an
  engine admin API — with the three reasons it exists (the website is the
  internet-facing process, the sidecar owns the durable copy, neither side can
  stall the other). Normative prose with no CI behind it, stated so that every
  second module does not re-decide it.
- §1.1: the 1.4.0 entry. No member changed; minor deliberately.

modules/rust-dryrun.md
- §2 "Talking to the game" rewritten from "No sidecar" to a thin RCON sidecar,
  with a dated Correction note recording what the exercise originally concluded
  and why it was overruled. Corrected in view rather than silently rewritten.

Pairs with website#… (the 1.4.0 bump, into edge); either order.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-12 09:38:07 -05:00
91ac28e11d Merge pull request 'docs(modules): close Phase 4 — de-UO the backend reference, record the acceptance run' (#144) from docs/module-phase4-slice4 into main
Reviewed-on: #144
2026-08-12 14:19:16 +00:00
42867d7ef8 docs(modules): close Phase 4 — de-UO the backend reference, record the acceptance run
Phase 4 slice 4. BACKEND_DESIGN.md had never been de-UO'd: Phase 3 rewrote core's
code and README, but §5.2's identifier check reads code, not prose, so nothing
ever looked at the design document. It still described 27 shard_* tables, 20 UO
route rows and the shard visibility ladder as core's, a phase after core stopped
being able to serve any of them.

Moved, text unchanged:

  BACKEND_DESIGN §3   six shard_* schema sections (226 lines) → modules/uo/SCHEMA.md
  BACKEND_DESIGN §4   13 public + 7 admin UO route rows       → modules/uo/API.md
  BACKEND_DESIGN §6.5 the audience ladder (70 lines)          → modules/uo/API.md §4

Core keeps the seam and gains the eight /admin/modules routes it had never
documented. §6.5 becomes "Module-owned audience boundaries": core's security
boundary ends at authentication, roles and the session, and a module that serves
game data brings its own.

Also fixed on the way: users.router.js was still listed as 15 routes (it is 9 —
six went to the extension slot), and the push section still promised
config/shardStreams.js "moves out with it" four slices after it left.

The acceptance table now carries results. Criterion 2 was proved for real against
module-uo v0.3.0 on an empty database, which turned up the uninstall ordering
defect fixed in website#146.

AI disclosure: this contribution was AI-assisted (Claude Code).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-12 09:16:29 -05:00
49ab5a1266 Merge pull request 'docs(website): record phase 4 slice 3, and the boot-order defect it created' (#143) from docs/module-phase4-slice3 into main
Reviewed-on: #143
2026-08-12 13:02:00 +00:00
102ea6db19 docs(website): record slice 3, and the boot-order defect it created
MODULE_SYSTEM.md §2.7.2 gains the slice 3 write-up: why the id and version are
written out in MODULES (the offline no-op has to be a file read, not a fetch),
the four decisions, and the table of what the smoke proved against the real
v0.3.0 release on a fresh database.

Two sections are corrected rather than extended. §2.5's "declarative module set"
promise did not anticipate that the two install surfaces need a rule about who
wins — the declaration owns the volume, the row owns whether a module runs — and
decision 4's "resolution runs before the server starts" is true of the SCAN, not
of the process: it runs inside start(), which is what buys it the database.

MODULE_API.md §2.6 no longer says the fragments are replayed by ensureSchema().
They are, for every caller except the server, which scans the volume later than
it ensures the schema and so replays them itself. That was a live defect for an
afternoon: it announced itself only as the "no module scan in this process" skip
line, which is correct output for `npm run seed` and means the opposite in a
booting server, and on a database that already had the tables the module started
perfectly. Nothing in the contract moves, so MODULE_API_VERSION is unchanged.

BACKEND_DESIGN.md: the boot sequence in the tree, and MODULES beside the
module_source_hosts setting it installs through.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-12 07:57:21 -05:00
245be5dc91 Merge pull request 'docs(website): phase 4 decisions 5-6, slices 1-2, and a rule main currently gets wrong' (#142) from docs/module-phase4-slices into main
Reviewed-on: #142
2026-08-12 09:11:27 +00:00
f753dfcff8 docs(website): record slice 2, and the three defects a browser found
The Modules screen as built (website#143), and the three things standing it
up against a live server exposed. Two of them are older than this phase and
neither was reachable from a test.

- A fresh install over a row the previous boot left `startup_failed` rendered
  the old failure - "module directory not present on the volume", one second
  after the files were written there - and, because that branch is not
  pending, suppressed the restart banner the install had just told the
  operator to use. Fixed by a derivation rather than a special case: the
  loader scans once at require time, so a module on the volume with no live
  record arrived after that scan and anything the row says predates it.

- The boot refresh had been nulling every install's provenance. source and
  sha256 exist for this screen and never survived a restart, because
  lifecycle.boot() re-records with neither and the upsert assigned both
  unconditionally. It could not have been found before Phase 4 wrote the first
  non-null value those columns ever had - and the model's test fake
  reproduced the defect faithfully, assigning exactly like the SQL.

- The restart killed the server outright on Windows. process.kill(pid,
  'SIGTERM') reaches the graceful handler on Linux and is unconditional
  termination where POSIX signals do not exist, so a Windows host got no
  module onShutdown, no pool close and no log flush. process.emit('SIGTERM')
  reaches the same listener everywhere. The test had stubbed process.kill and
  asserted the call - precisely the call whose meaning differs by platform.

That last one generalises: deployment is Linux containers and would never
have shown it. A smoke that only runs where the code ships cannot find a class
of defect that only bites the people developing it.

Also documents the COALESCE in BACKEND_DESIGN's installed_modules section.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-12 03:50:46 -05:00
88b6257333 docs(website): record slice 1, and correct two rules it disproved
The install service and admin API as built (website#142), plus the two
things §2.7.2 got wrong before there was any code to check them against.

The top-level directory rule was checking against nothing real. module-uo's
release workflow packs module-uo-<version>/, not uo/, so "reject a top-level
directory whose name is not the manifest's id" would have refused every
bundle that exists. That level is stripped instead - its name belongs to
whoever published the bundle, the directory it lands in has to be the id the
loader scans for - and what replaces the check is stronger: the UNPACKED
module.json must agree with the install manifest about id and version.

And tar has to be pinned forward rather than merely depended on. Installing
it gets 6.x, which npm audit reports as critical, and the advisory list reads
as this feature's own threat model: hardlink traversal via drive-relative
linkpath, symlink poisoning, hardlink escape through a symlink chain, PAX
size override on GNU long-name headers, decompression DoS. Refusing symlink
and hardlink entry types outright is what takes the extractor off most of
that list rather than depending on the library to contain them.

Also records the measurement behind the two-pass unpack, which was assumed in
the plan and is now known: node-tar DOES reject an escaping member, but late.
An archive whose fourth member escapes throws and leaves the first three on
disk - and the loader only asks whether module.json is present, so a
half-unpacked bundle is a module as far as the next boot is concerned.

Refreshes api-route-inventory.json (158 -> 166 public) and the route count in
BACKEND_DESIGN, and documents module_source_hosts beside the other seeded
settings keys - bootstrapped from the environment, owned by the database.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-12 03:12:25 -05:00
6bca9adf49 docs(website): two more phase 4 decisions, one of them a correction
Both came out of writing slice 1 against §2.5 rather than from planning, and
one of them means §2.5 was wrong rather than merely vague.

5. Purge is offered inside the uninstall flow, because it cannot be offered
   after it. §2.5 promised a disabled row an operator could purge later — but
   purge.sql is a file INSIDE the module directory that uninstall deletes, so
   "later" would have left a Purge button with nothing to run. The destructive
   choice now sits at the one moment the file is still there: an opt-in
   checkbox in the uninstall dialog, beside the standalone Purge action on a
   module that is still installed. "Never bundled into uninstall" becomes
   "never implied by uninstall". The cost is stated rather than hidden — an
   operator who does not tick it keeps their tables, and reinstalling is how
   they get the ability to drop them back.

6. The host allowlist bootstraps from MODULE_SOURCE_HOSTS and then lives in
   the database, admin-editable, with the variable no longer overriding it —
   the shape core already uses for seeded settings. The argument for keeping
   it in the environment does not survive contact with what this screen is: an
   admin who can install a module already has code execution in the process,
   so an admin who can name a host has gained nothing they did not have.

Also renames the subsection to six decisions and updates acceptance criterion
2 to name the checkbox.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-12 02:43:26 -05:00
976c1b5839 Merge pull request 'docs(website): settle phase 4's shape — delivery' (#141) from docs/module-phase4-plan into main
Reviewed-on: #141
2026-08-12 07:37:14 +00:00
4c9b2e7e9c docs(website): settle phase 4's shape — delivery
Phase 3 closed with core at 158 routes and module-uo releasable; phase 4 is
the first phase that is not about the boundary. It answers how a module gets
onto a box an operator owns, and how it comes off again.

Adds MODULE_SYSTEM.md §2.7.2 recording the measured starting state (the
producer side is finished — module-uo's release.yml already publishes the
tarball, the install manifest and SHA256SUMS, and module.json declares
purge.sql — while core has the installed_modules provenance columns and
nothing that fetches, verifies, unpacks or purges), the four org-lead
decisions, the five slices, the unpack threat model and the acceptance table.

The four decisions:

1. Restart is a button. Install, uninstall and re-enable only take effect at
   boot; recording a pending change and telling the operator to restart the
   container contradicts §2.4's "no shell access to the box", so an admin
   action runs the shutdown and exits, and the supervisor the shipped compose
   file already declares brings it back.
2. The install source is a pasted manifest URL, not a catalog — a catalog
   would make core's release cadence decide which modules exist. Safety is the
   declared sha256 plus an https host allowlist, which also stops the endpoint
   being an SSRF primitive.
3. Disable dispatches that one module's onShutdown before flipping the guard.
   As built, disable makes a module invisible rather than stopped — module-uo
   keeps its sidecar socket open and keeps ingesting into shard_* tables —
   which is wrong for the case the button exists for. Enable is not the mirror:
   there is no onBoot re-dispatch and the hooks were never promised re-entrant,
   so enable flips the row and offers the restart.
4. The declarative Docker set is an environment variable, resolved before the
   server starts, idempotent and offline-safe: an already-unpacked module at
   the declared version is a no-op.

Amends §2.4 (disable is no longer a pure guard flip) and §2.5 (what "admin
selects the module" and "restart" concretely mean). Records re-entrant
lifecycle hooks as a second candidate for a future MODULE_API major bump,
beside the identity-provider gap the rust dry run found.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-12 02:31:20 -05:00
d171253715 Merge pull request 'docs(website): close phase 3 — slice 5, the fragment obligation, and the rust dry run' (#140) from docs/module-close-phase3 into main
Reviewed-on: #140
2026-08-12 04:11:11 +00:00
872352389d docs(website): close phase 3 — slice 5, the fragment obligation, and the rust dry run
Records the slice that closes the extraction, and adds the two documents phase 3
owed: docs/modules/uo/ and the module-rust dry run.

**The slice found an obligation neither repo had built.** MODULE_API.md §2.8 and
§6.1a settle the OpenAPI fragment in detail — a module ships one, core merges the
fragments of started modules into /api/docs.json. Neither half existed, so the 72
URLs module-uo serves were in no spec at all. §2.8 and §6.1a now record what was
built, including the four things settled while building it: the filename is fixed
rather than declared, a module namespaces what it DEFINES and references core's
shared schemas by core's name, the generator derives its prefixes from the
module's own register() call, and swagger-autogen's diagnostics have to be
captured because it reports a broken annotation and then prints Success.

**§5.3 gains the design decision the frozen manifest actually made:** it is a
SUBTRACTION, not a prefix filter. Generating the manifest without the module and
then with it answers "what does the module serve" AND "did core lose anything",
and the second is the one §1.2 promises to the shipped Android app. A module that
shadowed a core route cannot appear as an addition anywhere.

**BACKEND_DESIGN.md §4.0.1** is new: /api/docs.json is assembled per request, the
two generated artifacts are core's alone, and the route count was still 228.

**docs/modules/** is new, per §2.10 (module documentation aggregates here, not in
module repos): docs/modules/uo/README.md orients a reader on what module-uo
serves, owns and needs from an operator, and links out to the feature docs that
already existed rather than restating them.

**docs/modules/rust-dryrun.md** is phase 3's fourth acceptance criterion. A
written, deliberately unimplemented module for Rust — chosen because it wipes
monthly, runs several servers rather than one shard, identifies by Steam, and
ships RCON so there is no sidecar to write. The contract generalises: same
manifest, same seven registration calls, same schema rules, and six of the UI
kit's seven members wanted by a game with nothing in common with the one the kit
was curated from.

It found one real gap — **a module cannot register an identity provider**, and
"Sign in with Steam" is what a Rust community expects. Recorded as the first
candidate for a future MODULE_API_VERSION bump rather than bolted on: an identity
provider participates in session creation, and §2.7's link-only SSO policy has to
survive it.

Also: website-README.md refreshed from the repo (it was several changes stale),
and three settled decisions added (18-20).

Pairs with Module-uo#6 and website#141.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-11 23:05:56 -05:00
404158b10f Merge pull request 'docs(website): record slice 4, and the §5.2 check as built' (#139) from docs/module-de-uo-core into main
Reviewed-on: #139
2026-08-12 03:00:36 +00:00
472e510f6f docs(website): record slice 4, and the §5.2 check as built
MODULE_SYSTEM.md §2.7.1 gains the slice 4 record: the 190 dead lines of UO API
bindings slice 3 left behind (with zero consumers, which is why nothing caught
them), the copy rewrite and why the words stay in code rather than becoming a
config surface, the two settings rows core had no business seeding, and the
check that keeps all of it out. The slice table row is corrected to what
actually moved, and README.md is recorded as deliberately deferred to slice 5
rather than quietly skipped.

The defect gets its own subsection with the SQL, because it is a shape that will
recur: a guard in core and the statement it guards in a fragment run in the wrong
order, since core's schema is replayed in full before any fragment. Verified
against a real MariaDB in all three states, including the pinned-instance case
that makes it a one-shot rather than a re-bump.

MODULE_API.md §5.2 stops describing a grep and describes what was built — four
details promoted to contract because each was wrong first: whole-word matching
(a substring pass flags `defaultImage`, which contains "ultIma"), the
comments-then-strings character walk, grandfathering exemptions that fail the
build when they stop matching, and `git ls-files` over a directory walk.

BACKEND_DESIGN.md's settings table gains the keys a MODULE seeds, and the
ordering rule behind them.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-11 21:55:03 -05:00
1b8c2e4806 Merge pull request 'docs(website): record slice 3, and MODULE_API 1.3.0' (#138) from docs/module-extract-client into main
Reviewed-on: #138
Reviewed-by: Colby Whitlock <whitlocktech@gmail.com>
2026-08-12 00:35:15 +00:00
931f77ef49 docs(website): record slice 3, and MODULE_API 1.3.0
The client half's move: 35 files / 5,332 lines (the table said 51 / ~3,700 and
is corrected, not re-derived to match). The seven-kit-members-plus-format claim
held exactly, so the kit needed no additions.

Contract, 1.2.0 -> 1.3.0, all additive: `icon` on a nav item, a third slot
`player.invite.accepted`, and `api.BASE` — which §3.5 specified from the first
draft and shared.js had never published, because nothing needed it until a
module had to build an EventSource URL.

§2.3's narrowing note said game-signup policy was core's business. It was wrong
in both directions: the setting's help text names Bridge.cfg, and slice 1 had
shipped a controller calling a function the narrowing does not expose, so
POST /player/shard/account answered 500 for every caller until this slice.

§3.7 gains the rule the invite slot needed: core may ask whether a slot is
filled when the answer changes CONTROL FLOW, never when it changes decoration.
Decoration goes inside `<Slot wrap>` — that is the footer-separator bug.

§7.7 gains what running it against the real module found, rather than a
throwaway: the unguarded portal icon, the relative-MODULES_DIR trap, and the two
operational notes (a module needs its own npm ci; copy the directory, never
symlink it).

Also recorded: the nav-override cost is worse than "a hidden row may reappear" —
the review instance had the nine UO rows gathered into a dropdown section, and
the whole section is lost. And `api-route-inventory.json` was a slice stale at
228 routes; refreshed to the 158 core actually serves.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-11 19:00:43 -05:00
fc05412070 Merge pull request 'docs(website): client extension slots, and the client half is one slice (phase 3, slice 2)' (#137) from docs/module-client-slots into main
Reviewed-on: #137
2026-08-11 21:54:07 +00:00
f47e2d3c6e Merge pull request 'docs(website): the server half does not slice, and slice 1's record' (#136) from docs/module-extract-server into main
Reviewed-on: #136
2026-08-11 21:53:20 +00:00
96b364b0bc docs(website): record slice 2, and what the browser changed about it
The contract as written had core branch on hasExtension to decide about its own
decoration around a slot. That is right when nothing is installed and wrong when
something is installed and fails: the slot IS filled, so the separator renders,
and the component then throws into the boundary and leaves the separator behind
on its own. Core decorates through <Slot wrap> now, inside the boundary, and
hasExtension is gone rather than kept as a trap for the next caller. Every unit
test passed both before and after -- the 7.7 browser smoke is what saw it.

Also recorded, neither a defect: core's own fill occupies a slot, so a module
cannot fill either one until the client half deletes core's (worth stating,
because a module written against 1.2.0 today cannot use them); and core's UO
sections on the user-detail page now fail to load, which is slice 1 removing the
routes rather than anything this slice did.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-11 16:46:11 -05:00
262c86ad4f docs(website): client extension slots, and the client half is one slice
Two findings from surveying the client half, and one org-lead decision that
turns them into the same piece of work.

The client half does not divide into public and admin after all. Routes slice by
area, but useShardFeed and shardEvents are imported by eight public pages AND
three admin views, and a shared leaf moves with its last consumer -- the same
rule the server half taught. Of the three ways out, two trade a real cost for a
boundary that lasts one review, so the client half is one slice. The nav rows
keep their feature provider as a consequence (rows without it fail open, so
disabled surfaces would advertise themselves again), and VendorSales turns out
to have no public consumer at all.

Its three consumers include core's own UserDetail, which renders six UO sections
with nowhere to put them: the server got declareSlot/registerExtension in phase
2 PR 4 and the client never got the twin. SiteFooter's link to a URL the
extraction deletes is the same gap one component over.

So slice 2 is core-only and adds rather than moves: client extension slots
(API 3.7), the site.footer.status and admin.users.detail slots, core filling
both itself. MODULE_API_VERSION goes to 1.2.0.

A slot is named for a PLACE, never for a meaning -- core supplies the position
and the styling, the module owns the label, the target, the data and whether it
renders at all. Typing a slot by its content would put game semantics back into
core, which is what this phase removes.

This is also the one slice where core merges before module-uo, because a chunk
cannot call registerExtension before the function exists. Harmless here: the
slice only adds, and core fills both slots with its own components under owner
id `core`, so the page is unchanged and the mechanism is proved before anything
moves.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-11 16:32:08 -05:00
f3a6231084 docs(website): record slice 1, and MODULE_API 1.1.0
The whole server half is out: 40 files, ~9,674 lines, 27 of 68 tables. The
acceptance criterion held exactly -- core's manifest goes 228 to 158 public
routes and the 70 that left reappear byte-identical once the module loads, with
routes.guards identical across all 228.

The contract grew to 1.1.0: ctx.activity.log, ctx.users.getById,
ctx.site.baseUrl, ctx.middleware.rateLimit + accountChangeLimiter, and a fourth
registry, registerPostHook. Each is documented with why it could not be
vendored, because that reasoning is the useful part -- an admin action a module
performs belongs in core's ONE audit log, a second rate-limit store is a limit
enforced by two counters, and core's CMS was calling a UO file directly.

§2.7.1 gains the slice record: the core.js port mechanism and its consequence
(require order is load-bearing), the vendoring line (pure leaf helpers may be
copied, security controls may not), the two core defects the extraction exposed,
the one deliberate behaviour change, and the one test that looked like it should
move and should not.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-11 12:11:09 -05:00
f402395fa5 docs(website): the server half does not slice, and why
Found before writing any of it. §2.7.1 split the server extraction five ways by
feature; it does not divide, because two contract rules compose.

A mount prefix is claimed whole -- ownedByCore probes the live tier router and
registerRoutes validates single-segment prefixes -- and /admin/shard is one
386-line router carrying 25 routes across atlas, clilocs, shard-ops, visibility,
market and account links. Meanwhile a model cannot be shared across the boundary
(§5.1), so it moves with the last route that consumes it. Take the closure and
every prefix is in it: /public/atlas holds shardAtlas with /admin/shard, which
holds shardState/shardEvents/shardMarket with /public/shard, which holds
uoLinkConfig with /admin/uo-link, and shardClilocs/shardLinks with /player/shard.

Landing any one of the old slices alone would either strand core importing
modules/uo/ -- what acceptance criterion 2 forbids -- or delete routes core is
still serving.

Giving the admin routes their own prefixes would divide it and is rejected: it
changes API URLs, which §1.2 promises not to do, and not hypothetically. The
shipped Android app calls POST /api/v1/admin/shard/kick, /ban, /unban,
/broadcast and the three /pages routes.

So the table is now six slices: 0 the skeleton (done), 1 the whole server half
as ONE PR per repo structured as five commits along the old slice lines, 2-4 the
client halves, 5 close the phase. The client half is unaffected and still slices
cleanly -- the registry takes routes per area, with no prefix atomicity and no
shared models, the same asymmetry that let the two halves be separated at all.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-11 01:50:37 -05:00
9930b375f2 Merge pull request 'docs(website): settle Phase 3's shape, correct the library build, record slice 0' (#135) from docs/module-phase3-plan into main
Reviewed-on: #135
2026-08-11 06:43:13 +00:00
7548c20820 docs(website): correct the library build, and record slice 0
Slice 0 built the module bundle skeleton against the contract and found that
§3.6 does not work as written. It shows Rollup's `external` alongside the
resolve aliases, and the two do not compose: Rollup asks `external` BEFORE
Vite's alias resolver runs, so a specifier in both is marked external and never
aliased. The chunk then emits bare `import "react"`, which no browser can
resolve without an import map, and CSP forbids the inline script an import map
has to be. It built cleanly and emitted exactly that.

§3.6 is corrected: alias only, `external` empty, with the alias table shown in
full because the anchoring is what stops `react` also capturing
`react/jsx-runtime`. What `external` was guarding -- a missed alias welding a
second React into the chunk -- moves to a resolution-time build plugin, and two
properties of that plugin are now contract because both were wrong first: it
hooks `transform` rather than `load` (first-wins, so it never ran), and its
forbidden-package list is stated rather than derived from the alias list
(deriving it means deleting an alias also deletes the guard).

Also records slice 0's outcome in §2.7.1, including the finding that generalises
past this repo: the boundary check failed on its own documentation, because the
comments describing what it catches are written in the syntax it catches. Slice
8's §5.2 grep has the same problem waiting for it. And the loader skips a
SYMLINKED module directory silently, which is the first thing to check when a
module fails to appear locally.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-11 01:33:38 -05:00
749233d378 docs(website): settle Phase 3's shape, slices and merge order
Phase 2 is closed, so Phase 3 needs a plan before any of it is extracted.
Four decisions, and one finding that set the first of them.

The finding: because API URLs are preserved (§1.2), a feature's server and
client halves are independent. Core's client keeps calling
/api/v1/public/shard/status after the module serves it, and a module page
calls the same URL while core still does. Nothing forces vertical slices, so
the extraction is server-first then client, sliced by feature, ten slices.

Merge order within a slice is module-uo first, then website. The loader's
ownedByCore probe stops a module LOADING while core owns its prefix, but the
module's own CI never loads it into core, so its PR merges fine beforehand --
and edge then serves the feature from core right up to the moment core drops
it, with no window where the branch is missing it outright.

Criterion 1's grep reads code, not prose: filenames, import specifiers, route
path literals and declared identifiers. Core's marketing copy legitimately
says "shard" in a dozen places and a literal word grep would have failed CI on
each while proving nothing about the boundary. That copy is rewritten in its
own slice instead, which is real work with a real review rather than an
exemption hidden in a pattern.

module-uo's CI clones core at a pinned ref to freeze its route manifest --
nothing else proves the URLs it claims are the URLs it serves -- and the
module-rust dry run lands in docs/modules/ where §2.10 already aggregates
module documentation.

Also records the measured surface (72 server files, 51 client, 32 test files),
which supersedes the Phase 0 estimate, and the one kit gap: lib/format.js is
vendored by the module rather than becoming an eighth §3.4 member.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-11 01:10:13 -05:00
12e4eaad10 Merge pull request 'docs(website): Phase 2 PR 9 — the modules mount, and Phase 2 complete' (#134) from docs/module-compose-volume into main
Reviewed-on: #134
2026-08-11 05:57:58 +00:00
8a7b099c2d docs(website): Phase 2 PR 9 — the modules mount, and Phase 2 complete
Records the last Phase 2 PR and closes the phase.

Amends §2.5: the mount is a bind mount of ./modules, not the named volume the
section reached for by analogy with uploads. Hand-placing a module directory is
a supported install in that same section, and a named volume routes it through
`docker cp` — the least discoverable mechanism Docker offers, for the one
install path an operator without the admin panel has.

Two things the build settled that the plan had not considered, both silent
failures rather than errors: the directory has to be tracked, because Docker
recreates a missing bind-mount source as root-owned and the container is uid
1000; and .dockerignore has to exclude it, because COPY . . would otherwise bake
a builder's checked-out module into every image — and Docker seeds a fresh named
volume from image contents, so it could have surfaced on a deployment that never
installed it.

MODULE_API §4.1 gains the concrete Compose values and states outright that a
missing modules directory is not an error, which the loader has always done and
the contract never said.

Website side: RunicGateway/website#136.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-11 00:50:17 -05:00
d66ee832c5 Merge pull request 'docs(website): Phase 2 PR 8 — the nav interleave and the feature-provider seam' (#133) from docs/module-nav-interleave into main
Reviewed-on: #133
2026-08-11 05:29:18 +00:00
f740530eb8 docs(website): settle the nav interleave and the feature-provider seam
Records Phase 2 PR 8 of the module system: MODULE_SYSTEM.md gains the PR
entry in 2.7 and the built-it notes on 1.4 and 1.5; MODULE_API.md 3.3 is
amended where building it settled something the draft left open or got wrong.

The amendments to 3.3:

- The pipeline arrow had role/feature filtering BEFORE admin overrides. The
  code has always been the other way round, deliberately - the filter runs
  last so it stays a boundary an override cannot cross (THEMING_AND_NAV 7).
- `feature` was documented as public-area-only. It applies in all three
  areas: core's admin and player navs still carry no flags, but a module row
  that declares a gate and has it silently ignored is a trap.
- How a provider is found: by the module that registered the row, not by a
  prefix parsed out of the flag name. Core's own rows resolve against the
  owner id `core`, which core registers useShardFlags under.
- What a provider hook returns, and that every unknown fails OPEN.
- Why calling one hook per provider in a loop is legal, and why the
  enumerator is a module export rather than a member of registry.
- Six details the interleave settled: unordered rows append rather than
  defaulting to 0; an ungrouped admin row gets its own trailing group rather
  than joining core's; a module-created group is a legal override
  destination; a colliding `to` is dropped with a warning; and why the
  interleave must precede the override merge.

MOD_PATHS' replacement is recorded in both files, including the defect the
derivation fixed: the moderator redirect was a third hardcoded list that
disagreed with MOD_PATHS about /admin/houses. THEMING_AND_NAV 7's
"moderator confinement" note is amended to match.

Code: website PR 8 (client-only; 160 client tests, manifest and OpenAPI
unchanged), verified with the 7.7 browser smoke.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-10 23:42:21 -05:00
b43b40e71b Merge pull request 'docs(website): Phase 2 PR 7 — the client chunk's delivery contract' (#132) from docs/module-client-registry into main
Reviewed-on: #132
2026-08-11 04:01:28 +00:00
17d8608a7f docs(website): record Phase 2 PR 7 — the client chunk's delivery contract
MODULE_API.md
  - 3.1 gains the four normative constraints PR 7 settled: the static root is
    the entry's directory (an entry in the module root is rejected), the mount
    sits behind the module's state guard with no-cache, the script tag is
    injected before </body> so core's bundle runs first, and core renders on
    DOMContentLoaded with a readyState === 'complete' check
  - 2.1: client.entry must be in a subdirectory; present-but-empty is rejected
  - 3.4: AdminPage struck from the UI kit — core has no such component
  - 7.7 (new): the browser smoke, and the timing bug no test in this repo can
    see. Every test passed against a build that did not work in a browser

MODULE_SYSTEM.md
  - 2.6 step 3 amended to the </body> injection point
  - 2.7 records PR 7's four decisions and what verified them; 1-7 of 9 done

BACKEND_DESIGN.md
  - /modules is the fourth filesystem-conditional static mount outside the
    route manifest, with its root, guard and cache policy stated

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-10 22:54:39 -05:00
fdede79909 Merge pull request 'docs(website): settle what core publishes about an installed module' (#131) from docs/module-public-endpoint into main
Reviewed-on: #131
2026-08-11 03:16:23 +00:00
6a39ef63c6 docs(website): settle what core publishes about an installed module
Phase 2, PR 6 of MODULE_SYSTEM.md 2.7 — GET /api/v1/public/modules.

MODULE_API.md gains 2.9, the normative shape: four fields (id, name, version,
capabilities) in scan order, and what is deliberately absent. Only started
modules appear, so a disabled or failed one is absent exactly as 4.4 already
leaves its routes and nav; no state, failure_stage or failure_reason reaches an
anonymous caller; no client chunk URL, because htmlShell hands the browser the
tag; an empty array is a real answer, but the 7.6 guard is a 500 and never [].
Also records why it owns the /modules prefix rather than sitting in the
root-mounted site router, where the loader's collision probe could not see it.

6.7 amends MODULE_SYSTEM 2.6 step 4, which said the SPA reads the endpoint "to
learn what to load" — step 3 of the same list had already answered loading a
different way, and 3.1.3 is the normative version. The endpoint feature-detects.
2.6 step 4 now says so, and Part 6's intro no longer claims a fixed count of
amendments.

MODULE_SYSTEM.md 2.7 records PR 6 and restates the phase exit criterion
honestly: no EXISTING URL moves, and PR 6 is the single deliberate addition in
Phase 2. Status line updated — the design is in implementation, not unimplemented.

BACKEND_DESIGN.md documents the route in the /public table and the router in the
folder map, and the published api-route-inventory.json mirror is refreshed to
228 public routes (the prose count was stale at 226).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-10 22:03:35 -05:00
e733ac0c9c Merge pull request 'docs(website): settle boot/shutdown dispatch and what a boot writes down' (#130) from docs/module-lifecycle into main
Reviewed-on: #130
2026-08-11 02:32:48 +00:00
004e217806 docs(website): settle boot/shutdown dispatch and what a boot writes down
Records phase 2 PR 5 of the module system: the lifecycle hooks a module
registers, how they are dispatched, and what a boot does to installed_modules.

MODULE_API.md 2.5 gains the reconcile's four steps in order, the rules that fall
out of them (the operator's `disabled` wins over any outcome; a bookkeeping
failure is not a boot failure; a module with no onBoot still reaches `started`;
a module whose onBoot threw gets no onShutdown), and why onBoot has no timeout
while onShutdown has a five-second budget -- shutdown races the process being
killed and boot does not.

4.4 gains the failure_stage table: every failure is recorded against the 4.3
step that produced it, so the admin panel can say where a module broke and not
only what the message was.

MODULE_SYSTEM.md 2.4 records the new rule for a row whose directory is gone, and
2.7 the PR 5 progress entry with its four decisions. BACKEND_DESIGN.md's
installed_modules section gains the write path now that one exists.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-10 20:32:10 -05:00
30589f1fa5 Merge pull request 'docs(website): settle the three de-entanglement registries and what they cost' (#129) from docs/module-registries into main
Reviewed-on: #129
2026-08-10 23:16:46 +00:00
58b435be70 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>
2026-08-10 17:51:02 -05:00
bcd3a750e7 Merge pull request 'docs(website): settle how module schema fragments are validated and replayed' (#128) from docs/module-schema-replay into main
Reviewed-on: #128
2026-08-10 22:05:57 +00:00