|
|
|
|
@@ -428,8 +428,12 @@ leaves the row `disabled`, which the reset never touches.
|
|
|
|
|
|
|
|
|
|
### 2.5 Install, uninstall, purge
|
|
|
|
|
|
|
|
|
|
Modules live on a **mounted volume**, not in the image — the same treatment `uploads` already gets in
|
|
|
|
|
`docker-compose.yml`. That is what makes the WordPress model work against a pull-only image.
|
|
|
|
|
Modules live on a **mounted volume**, not in the image. That is what makes the WordPress model work
|
|
|
|
|
against a pull-only image. *(Settled in Phase 2 PR 9: it is a **bind mount** of `./modules`, not the
|
|
|
|
|
named volume this sentence originally reached for by analogy with `uploads` — hand-placing a module
|
|
|
|
|
directory is a supported install below, and a named volume would route it through `docker cp`. The
|
|
|
|
|
image's copy is excluded by `.dockerignore`, so a module in a builder's working tree can never ship
|
|
|
|
|
inside an image; see `modules/README.md` in the website repo.)*
|
|
|
|
|
|
|
|
|
|
**Install:** admin selects the module → bundle downloaded from the module repo's release and verified
|
|
|
|
|
against its `sha256` → unpacked into `modules/<id>/` on the volume → `installed_modules` row written →
|
|
|
|
|
@@ -511,14 +515,14 @@ too (API §7.2).
|
|
|
|
|
7. Client `src/modules/registry.js`, the `window.__rg` shared-dependency global, the chunk's static
|
|
|
|
|
mount and the `htmlShell` script injection — empty registry, no visible change.
|
|
|
|
|
8. `MOD_PATHS` → `roles`-derived (§1.4); the generic feature-provider seam (§1.5).
|
|
|
|
|
9. `docker-compose.yml` gains the `modules` volume.
|
|
|
|
|
9. `docker-compose.yml` gains the `modules` mount.
|
|
|
|
|
|
|
|
|
|
Exit criterion: no **existing** URL moves and every existing test passes. If Phase 2 changes one URL,
|
|
|
|
|
it is wrong. PR 6 is the single deliberate exception in the phase and it *adds*: `routes.manifest.json`
|
|
|
|
|
gains exactly one line, `GET /api/v1/public/modules`, and nothing else in the file moves. Every other
|
|
|
|
|
PR in Phase 2 produces a zero-line diff.
|
|
|
|
|
|
|
|
|
|
**Progress: PRs 1-7 done.**
|
|
|
|
|
**Progress: complete — all nine PRs landed.**
|
|
|
|
|
|
|
|
|
|
- **PR 1** — `installed_modules` and the state machine, with the stored shape and the boot rules
|
|
|
|
|
settled in §2.4 above.
|
|
|
|
|
@@ -678,6 +682,54 @@ fixed enum because the leg set is whatever has been registered.
|
|
|
|
|
admin can relabel a module row in Admin → Navigation and have it persist and apply — the whole
|
|
|
|
|
point of merging before the override layer. Zero CSP reports, zero console errors.
|
|
|
|
|
|
|
|
|
|
- **PR 9** — the mount itself, which closes the phase: `docker-compose.yml` gains `./modules` at
|
|
|
|
|
`/app/modules`, the `Dockerfile` creates that directory node-owned, `.dockerignore` keeps any local
|
|
|
|
|
one out of the image and `modules/README.md` documents the directory for whoever opens it.
|
|
|
|
|
|
|
|
|
|
It is a **bind mount, not the named volume** §2.5 assumed by analogy with `uploads`. Placing a
|
|
|
|
|
module directory by hand is a supported install, and a named volume makes that a `docker cp` into a
|
|
|
|
|
running container — the one install path an operator without the admin panel has, routed through
|
|
|
|
|
the least discoverable mechanism Docker offers. A bind mount makes it `tar -xf … -C ./modules`, and
|
|
|
|
|
makes the installed set something an operator can *see*. §2.5 is amended above; nothing else about
|
|
|
|
|
install, uninstall or purge changes.
|
|
|
|
|
|
|
|
|
|
Two consequences worth stating, because both are silent failures rather than errors. The directory
|
|
|
|
|
is **tracked** — via its README, the same shape `brand/` already uses — because Docker recreates a
|
|
|
|
|
missing bind-mount source as `root:root`, and the container runs as uid 1000: delete `modules/` from
|
|
|
|
|
a checkout and the next install fails on a permission error that names no cause. And the mount is
|
|
|
|
|
**read-write**, since §2.5's install unpacks into it from inside the container; deferring that to
|
|
|
|
|
Phase 4 would have bought nothing, as a mount-mode change is a redeploy either way.
|
|
|
|
|
|
|
|
|
|
`.dockerignore` matters more than it looks. `COPY . .` would otherwise bake whatever module the
|
|
|
|
|
builder had checked out into every image — and because Docker seeds a *fresh* named volume from
|
|
|
|
|
the image's contents, that module could have appeared on a production deployment that never
|
|
|
|
|
installed it. The exclusion is what makes "modules live on a mount, never in the image" true rather
|
|
|
|
|
than merely intended.
|
|
|
|
|
|
|
|
|
|
Verified against a **running container**, which is the only thing that can check any of the above —
|
|
|
|
|
a compose file that parses proves nothing about ownership, and nothing about what the image
|
|
|
|
|
contains. The image carries an empty, node-owned `/app/modules` despite a module sitting in the
|
|
|
|
|
build context. A module on the mount loads, mounts, runs `onBoot` and reaches `started`;
|
|
|
|
|
`/api/v1/public/modules` lists it; its chunk serves from the entry's directory alone, with the
|
|
|
|
|
module's own server source and `module.json` both `404`. The [§7.7](MODULE_API.md#77-the-client-half-has-to-be-verified-in-a-browser--the-timing-bug-no-test-could-see)
|
|
|
|
|
browser smoke was re-run against the containerised stack rather than a working tree: in Chrome, the
|
|
|
|
|
page renders on first paint inside core's `PublicLayout`, drawing React and the UI kit off
|
|
|
|
|
`window.__rg`, with its nav row interleaved into core's public nav — under the enforced
|
|
|
|
|
`script-src 'self'`, with zero CSP reports and no console errors. Removing the directory by hand and
|
|
|
|
|
restarting reconciles the row to `startup_failed`/`require` exactly as §2.4 says, and leaves core
|
|
|
|
|
healthy with no script injected and `{"modules":[]}` published.
|
|
|
|
|
|
|
|
|
|
**933 server tests** and **160 client tests** pass, both unchanged — this PR ships no application
|
|
|
|
|
code. `routes.manifest.json` stays at 230 routes and the OpenAPI spec regenerates byte-identical.
|
|
|
|
|
The one source change is a comment: `scripts/routeManifest.js` enumerated the filesystem-conditional
|
|
|
|
|
mounts it excludes and had never been told about `/modules`. Its filter is an allowlist, so the
|
|
|
|
|
behaviour was always right and only the explanation was stale.
|
|
|
|
|
|
|
|
|
|
**Phase 2 is complete.** Core can discover, validate, mount, migrate, boot, publish, serve and
|
|
|
|
|
navigate a module it does not contain, on a deployment that builds nothing — and it does all of that
|
|
|
|
|
while no existing URL has moved. The exit criterion held: `routes.manifest.json` went 229 → 230 across
|
|
|
|
|
the whole phase, and the one added line is PR 6's deliberate `GET /api/v1/public/modules`.
|
|
|
|
|
|
|
|
|
|
**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;
|
|
|
|
|
`scripts/importSpawnAtlas.js` and `db/spawnAtlas.art.json`; `usersShard.controller.js` **minus
|
|
|
|
|
@@ -694,7 +746,193 @@ Acceptance, all four required:
|
|
|
|
|
ownership move, which changes no URL. After extraction the core manifest no longer contains UO
|
|
|
|
|
routes — `module-uo` generates and freezes its own in its own repo.
|
|
|
|
|
4. **A written `module-rust` dry run** — manifest, mounts, nav entries, one notification stream — not
|
|
|
|
|
implemented, to prove the contract generalises before more is built on it.
|
|
|
|
|
implemented, to prove the contract generalises before more is built on it. It lands as
|
|
|
|
|
`docs/modules/rust-dryrun.md`, where §2.10 already aggregates module documentation; Phase 5's
|
|
|
|
|
Integration Kit links to it rather than copying it, per the kit's own never-re-specify rule
|
|
|
|
|
(§2.11).
|
|
|
|
|
|
|
|
|
|
#### 2.7.1 Phase 3's shape — settled 2026-08-11
|
|
|
|
|
|
|
|
|
|
Measured against `edge` at the close of Phase 2, the surface is **72 server files / ~9,700 lines**,
|
|
|
|
|
**51 client files / ~3,700 lines**, and **32 of core's 82 server test files**. The counts in the
|
|
|
|
|
paragraph above were written in Phase 0 against a smaller tree and are superseded by the slice table
|
|
|
|
|
below.
|
|
|
|
|
|
|
|
|
|
**The finding that sets the order: the two halves are independent.** Because §1.2 preserves API URLs
|
|
|
|
|
exactly, core's client keeps calling `/api/v1/public/shard/status` after that route is served by the
|
|
|
|
|
module, and a module page calls the same URL while core still serves it. Nothing forces a feature's
|
|
|
|
|
server and client halves to move together, so the extraction is **server-first, then client**, sliced
|
|
|
|
|
by feature — which keeps each PR inside one layer and one review's worth of context.
|
|
|
|
|
|
|
|
|
|
**Merge order across the two repos: `module-uo` first, then `website`.** The loader's `ownedByCore`
|
|
|
|
|
probe means a module cannot *load* while core still owns its prefix — but `module-uo`'s own CI never
|
|
|
|
|
loads it into core, so its PR merges perfectly well beforehand. Taking that order means `edge` serves
|
|
|
|
|
the feature from core right up to the moment core drops it, and there is never a window where the
|
|
|
|
|
branch is missing a feature outright. The reverse order would break `edge` at every slice boundary
|
|
|
|
|
for the length of a review. Verification is unaffected either way: a slice is proved by running the
|
|
|
|
|
*pair* together locally — the module branch checked out into `website/modules/uo`, the deletion
|
|
|
|
|
branch checked out in `website/` — before either merges.
|
|
|
|
|
|
|
|
|
|
Each slice is one `module-uo` PR (adds), one `website` PR (deletes), and one `docs` PR:
|
|
|
|
|
|
|
|
|
|
| # | Slice | Moves |
|
|
|
|
|
| --- | --- | --- |
|
|
|
|
|
| 0 | **The bundle skeleton** | `module.json`, both `package.json`s, `server/index.js` registering nothing, the Vite library build + the four shared-dep shims, CI armed, the §5.1 zero-internal-imports check. `website` untouched. |
|
|
|
|
|
| 1 | **The whole server half** | 40 files / ~9,674 lines, 25 of core's 82 test files, 27 of its 68 tables — every UO model, util, router and controller, `config/shardStreams.js`, `scripts/importSpawnAtlas.js` and the art JSON. **One merge, five commits** (below). |
|
|
|
|
|
| 2 | **Public pages** | `Shard`, `ShardActivity`, `Rules`, `Atlas`, `AtlasCreature`, `ChampSpawns`, `Market`, `MarketVendor`, `Governors`, `Guilds`, `Houses`, `Leaderboards`, `PlayersOnline`, `VendorSales`, `data/cityCrests.js`, `lib/shardEvents.js`, `lib/useShardFeed.js`, and their public nav rows — under `/uo/*` per §2.8 |
|
|
|
|
|
| 3 | **Admin + player pages** | `ShardAdmin`, `ShardOps`, `ShardVisibility`, `SpawnAtlas`, `HousesAdmin`, `AdminCharacter(s)`, `PlayerCharacter(s)`, `GameAccounts`, `CharacterSheet`, `CharacterStats`, `ShardAccountActions`, `CreateGameAccountForm`, `useShardFeatures` and the `useShardFlags` feature provider — under `/admin/uo/*` and `/player/uo/*` |
|
|
|
|
|
| 4 | **De-UO core's copy** | `About`, `Screenshots`, `Website`, `SiteFooter`, `heroLayout`'s defaults, `api/client.js`'s `shard`/`atlas` namespaces, and the comments in `navOverrides.js` — plus the §5.2 CI grep that keeps them out |
|
|
|
|
|
| 5 | **Close the phase** | `module-uo`'s frozen route manifest and release workflow; `docs/modules/uo/` and `docs/modules/rust-dryrun.md` |
|
|
|
|
|
|
|
|
|
|
##### Why the server half cannot be sliced — found 2026-08-11, before writing any of it
|
|
|
|
|
|
|
|
|
|
The table above used to run to ten slices, with the server half split five ways by feature. It does
|
|
|
|
|
not divide, and the reason is that two contract rules compose:
|
|
|
|
|
|
|
|
|
|
- **A mount prefix is claimed whole.** `ownedByCore` probes the live tier router and `registerRoutes`
|
|
|
|
|
validates single-segment prefixes, so `/admin/shard` moves as one unit — and it is a single
|
|
|
|
|
386-line router carrying 25 routes that span atlas, clilocs, shard-ops, visibility, market *and*
|
|
|
|
|
account links.
|
|
|
|
|
- **A model cannot be shared across the boundary** (§5.1), so a model moves with the *last* route
|
|
|
|
|
that consumes it.
|
|
|
|
|
|
|
|
|
|
Take the closure and every prefix is in it:
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
/public/atlas ──shardAtlas── /admin/shard ──shardState,shardEvents,shardMarket── /public/shard
|
|
|
|
|
│ │
|
|
|
|
|
shardClilocs,shardLinks uoLinkConfig
|
|
|
|
|
│ │
|
|
|
|
|
/player/shard /admin/uo-link
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Landing any one of the old slices alone would either strand core importing `modules/uo/` — which is
|
|
|
|
|
precisely 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.** `/admin/atlas` and
|
|
|
|
|
`/admin/clilocs` alongside a slimmer `/admin/shard` would make the closure fall apart. It also
|
|
|
|
|
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
|
|
|
|
|
(`data/api/AdminApi.kt`). A prefix rename is a client break, and the API surface is frozen for
|
|
|
|
|
exactly this reason.
|
|
|
|
|
|
|
|
|
|
**So slice 1 is one PR per repo, structured as five commits** along the old slice lines, reviewable
|
|
|
|
|
one at a time while landing atomically: atlas + clilocs · the live shard · market · account links and
|
|
|
|
|
the `admin.users.detail` slot · the town-crier leg. The alternative considered was stacked PRs into a
|
|
|
|
|
per-repo integration branch; it buys PR-level granularity for ten extra PRs and two long-lived
|
|
|
|
|
branches, and commits give most of the same reading order for none of it.
|
|
|
|
|
|
|
|
|
|
**The client half is unaffected** and still slices cleanly: the client registry takes routes per
|
|
|
|
|
*area*, with no prefix atomicity and no shared models — which is the same asymmetry that let the two
|
|
|
|
|
halves be separated in the first place.
|
|
|
|
|
|
|
|
|
|
**Criterion 1 is a grep over code, not over prose** — see [API §5.2](MODULE_API.md#52-zero-uo-identifiers-in-core-ci-website-repo)
|
|
|
|
|
for what that means precisely. Core's marketing copy says "shard" in a dozen places, and a literal
|
|
|
|
|
word grep would have made every one of them a CI failure while proving nothing about the boundary.
|
|
|
|
|
Slice 4 rewrites that copy anyway, because a core that still reads as a UO site is not the
|
|
|
|
|
game-agnostic platform this workstream is for — but it is a deliberate piece of work with its own
|
|
|
|
|
review, not an exemption hidden in a grep pattern.
|
|
|
|
|
|
|
|
|
|
**One small gap in the kit, deliberately not closed.** The UO client views import almost exactly the
|
|
|
|
|
seven §3.4 members — plus `lib/format.js`, a pure leaf formatter. The module **vendors a copy**
|
|
|
|
|
rather than core adding an eighth member: the kit is closed on purpose, and a function with no
|
|
|
|
|
props and no layout cannot drift the way a component can. The same is not true of `PublicLayout`,
|
|
|
|
|
which is why that one is in the kit.
|
|
|
|
|
|
|
|
|
|
#### Slice 0 — the bundle skeleton (Module-uo#2, 2026-08-11)
|
|
|
|
|
|
|
|
|
|
`module.json`, an entry point taking `(ctx, api)`, the Vite library build, four shims, and both
|
|
|
|
|
boundary checks. It **registers nothing**, and core is untouched — what it proves is the delivery
|
|
|
|
|
path itself, before a single UO file moves into it. 29 server tests and 9 client tests, both new.
|
|
|
|
|
|
|
|
|
|
Verified against a real core rather than asserted: the module loads, mounts its zero routes, reaches
|
|
|
|
|
`started` and is published by `/api/v1/public/modules`; its chunk serves from the entry's directory
|
|
|
|
|
with `Cache-Control: no-cache` while its server source, `module.json` and `package.json` all 404;
|
|
|
|
|
and in Chrome, under the enforced `script-src 'self'`, the chunk reports every shared dependency
|
|
|
|
|
**identity-equal** to core's, with zero CSP reports.
|
|
|
|
|
|
|
|
|
|
**Three findings, each of which had produced a green build that was wrong.** The first amends the
|
|
|
|
|
contract and is written up at [API §3.6](MODULE_API.md#36-vite-library-mode-build): `external` and
|
|
|
|
|
the aliases do not compose, so `external` is now empty and a resolution-time build guard replaces
|
|
|
|
|
it. The second is that guard's own two failures — hooking `load` (first-wins, so it never ran) and
|
|
|
|
|
deriving its forbidden list from the alias list (so deleting an alias deleted the guard). Both were
|
|
|
|
|
found by breaking an alias on purpose and checking the build actually went red, which is the only
|
|
|
|
|
way a guard's absence is visible.
|
|
|
|
|
|
|
|
|
|
The third is about the boundary check itself and generalises past this repo. **`checkImports.js`
|
|
|
|
|
failed on its own documentation** — the comment naming `require("../../etc/passwd")` as an example
|
|
|
|
|
of what to catch, and the entry point's comment explaining why a module must never
|
|
|
|
|
`require('express')`. A check that cannot survive being described is one people stop writing
|
|
|
|
|
comments around, so it strips comments and template literals with a character walk rather than a
|
|
|
|
|
regexp (a URL in a string contains a comment opener; a comment contains quotes) and carries its own
|
|
|
|
|
test suite. The same applies to slice 4's §5.2 grep, which will be read by a codebase that discusses
|
|
|
|
|
modules constantly.
|
|
|
|
|
|
|
|
|
|
#### Slice 1 — the whole server half (Module-uo#3 + website#137, 2026-08-11)
|
|
|
|
|
|
|
|
|
|
40 files, ~9,674 lines, 27 of 68 tables, 25 of 82 test files. Core no longer contains anything that
|
|
|
|
|
knows what a shard is. Three commits per repo, readable in order.
|
|
|
|
|
|
|
|
|
|
**The acceptance criterion held exactly.** Core's `routes.manifest.json` goes 228 → 158 public
|
|
|
|
|
routes, and the 70 that left reappear byte-identical once the module is loaded — proved by generating
|
|
|
|
|
the manifest against core+module and diffing it against the pre-extraction file: zero missing, zero
|
|
|
|
|
added, and `routes.guards.json` identical across all 228, so no auth gate moved either.
|
|
|
|
|
|
|
|
|
|
**`server/core.js` is the port mechanism and the shape is the finding.** Ported code requires its
|
|
|
|
|
dependencies at file scope, which runs before `register()` and therefore before any `ctx` exists — so
|
|
|
|
|
every member of that file is a stable function resolving `ctx` when *called*, and nothing may be
|
|
|
|
|
destructured off `ctx` at init either, because core is free to hand over a getter. That kept the port
|
|
|
|
|
to a one-line import change per file instead of a signature change per function. Its consequence:
|
|
|
|
|
**require order is load-bearing.** A router does `const express = core.express` at its own file
|
|
|
|
|
scope, so `core.init(ctx)` must run before the first `require` under `router/`, and the module's
|
|
|
|
|
entry point requires its routers inside `register()` for exactly that reason.
|
|
|
|
|
|
|
|
|
|
**The contract grew to 1.1.0**, four members, none of which could be avoided:
|
|
|
|
|
`ctx.activity.log` (an admin action a module performs belongs in core's *one* audit log — a module
|
|
|
|
|
with its own is a second place to look, which means a place nobody looks), `ctx.users.getById`,
|
|
|
|
|
`ctx.site.baseUrl`, and `ctx.middleware.rateLimit` + `accountChangeLimiter`. The rate-limit split is
|
|
|
|
|
worth restating: a module states its own window and cap because it knows what its endpoints cost, and
|
|
|
|
|
takes the plumbing from core so there is one `express-rate-limit` in the process and one place a
|
|
|
|
|
breach is logged.
|
|
|
|
|
|
|
|
|
|
**`registerPostHook` is the fourth registry and the last coupling removed** — see
|
|
|
|
|
[API §2.4](MODULE_API.md#24-api--what-the-module-registers).
|
|
|
|
|
|
|
|
|
|
**What was vendored, and what deliberately was not.** `deriveExcerpt` came across as nine lines of
|
|
|
|
|
pure text handling; core's **sanitiser** sitting beside it did not, because a second copy of a
|
|
|
|
|
security control diverges silently the moment either is fixed. That is the line: pure leaf helpers
|
|
|
|
|
may be copied, controls may not.
|
|
|
|
|
|
|
|
|
|
**Two defects the extraction exposed, both in core.** The loader matched `CREATE TABLE` against the
|
|
|
|
|
**raw** fragment, so a schema file whose header says "every CREATE TABLE carries IF NOT EXISTS" was
|
|
|
|
|
rejected for a prefix violation on a table called `carries` — the same class as slice 0's boundary
|
|
|
|
|
check failing on its own documentation, and now fixed on both scans by reading split statements. And
|
|
|
|
|
the atlas art map resolved `../../../db/data`, correct in core and pointing outside `server/` in the
|
|
|
|
|
module: a path that happens to resolve is exactly what survives a green suite, because the
|
|
|
|
|
absent-file branch returns `{}` and looks like the normal case. It was caught by the integration run,
|
|
|
|
|
not by tests.
|
|
|
|
|
|
|
|
|
|
**One deliberate behaviour change.** `uoLinkSocket.start()` and the sidecar health probe used to run
|
|
|
|
|
*after* the listener bound and now run before it, because `onBoot` does. `start()` returns as soon as
|
|
|
|
|
the reconnecting client is armed, but the probe is a real HTTP call, so it is fired and **not**
|
|
|
|
|
awaited — an unreachable sidecar must not hold the site closed. Reporting that the bridge is down is
|
|
|
|
|
diagnostics; being up is not a precondition for serving a page.
|
|
|
|
|
|
|
|
|
|
**One test stayed that looked like it should move.** `playerRouteAccess.test.js` guards a real past
|
|
|
|
|
bug — an admin 403'd off their own characters — through a now-module-owned URL, but the *guarantee*
|
|
|
|
|
is core's: `/player/*` is role-agnostic self-service. It stays and asserts that through
|
|
|
|
|
`/player/appeals`. Moving it would have left core with no test of its own tier rule, which is
|
|
|
|
|
precisely what regressed once before.
|
|
|
|
|
|
|
|
|
|
**A note for anyone running core's suite locally: remove `modules/uo` first.** With a module
|
|
|
|
|
installed the manifest tests fail correctly — core's committed manifest is core-only, and the live
|
|
|
|
|
stack has the module's routes on it.
|
|
|
|
|
|
|
|
|
|
**One thing to know before running a module locally: the loader skips a *symlinked* module directory
|
|
|
|
|
silently.** `readdirSync(…, { withFileTypes: true }).filter(e => e.isDirectory())` reports a Windows
|
|
|
|
|
junction as a symlink, so a module linked rather than copied into `modules/` is simply not there,
|
|
|
|
|
with nothing logged. Not a defect for a real install — `modules/` is a bind mount of real
|
|
|
|
|
directories (§2.5) — but it is the first thing to check when a module fails to appear.
|
|
|
|
|
|
|
|
|
|
**Phase 4 — Delivery.** The admin-panel Modules screen (install, enable, disable, retry, purge,
|
|
|
|
|
`startup_failed` with its recorded reason) and the Docker-environment path from §2.5. Deliberately
|
|
|
|
|
@@ -844,3 +1082,7 @@ row for it — when it has content, not while it is an empty repo.
|
|
|
|
|
| 11 | Website work lands on `edge` and reaches `main` as one cutover at the end | §2.9 |
|
|
|
|
|
| 12 | The module repo is `RunicGateway/Module-uo`; the module id is `uo` | §2.3 |
|
|
|
|
|
| 13 | `RunicGateway/Integration-kit` is the module-builder's instruction book — module + sidecar + game plugin, teaching only, never re-specifying a contract | §2.11 |
|
|
|
|
|
| 14 | Phase 3 extracts **server-first, then client**, sliced by feature; `module-uo` merges before `website` in each pair | §2.7.1 |
|
|
|
|
|
| 15 | Criterion 1's grep reads **code, not prose**; core's UO copy is rewritten in its own slice instead | API §5.2, §2.7.1 |
|
|
|
|
|
| 16 | `module-uo`'s CI checks core out at a **pinned ref** to generate its frozen route manifest | API §5.3 |
|
|
|
|
|
| 17 | The `module-rust` dry run lands as `docs/modules/rust-dryrun.md`; the Integration Kit links to it | §2.7.1, §2.11 |
|
|
|
|
|
|