Follows the finding recorded a commit ago: §H told a module the revert contract
accepts a `detail`, `classify()` had never read one, and `module-uo` had been
answering one since Phase 12b — so `uo.item.grant`'s report of which recipients
missed out was written into nothing.
Fixed in `website#197` by making the member real rather than by deleting the
reporting, because §H's sentence was right and only its example was wrong.
* **§1.1, 1.10.0** gains `detail` as a third envelope member beside Phase
10's two: optional, on both SUCCESS shapes, carried and never interpreted,
objects only, 4KB, dropped-and-logged rather than failing the step.
* **§2.4** gains the contract rule — core reads no key out of it, because a
switch on known keys anywhere in core would be core learning one module's
vocabulary.
* **EVENTS.md §F** records the fix, including the half that is easy to miss:
the run console's `describeLogLine` default returns a kind WORD, so the new
line would have rendered as the literal string "step.detail" — the channel
existing and showing nothing.
* **§H's wipes row** no longer claims `detail` is unread.
MODULE_API stays 1.10.0, amended in place — still on `edge`. The failure
channel is unchanged and is still `error` alone.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
2026-09-08 18:50:04 -05:00
4 changed files with 7 additions and 237 deletions
| `DELETE /admin/events/series/:seriesId` | admin, editor | delete it, detaching its definitions; answers with how many |
| `GET /admin/events/calendar` | staff | the calendar for a window: materialised runs and projected occurrences (Phase 4) |
| `GET/PUT /admin/events/actions` | admin | which actions are enabled on this deployment, and their per-run caps (Phase 6). `admin` on the read as well as the write; the PUT takes one action at a time |
| `GET /public/events` | — | **the calendar** (Phase 14a): upcoming, live and recent, by series. Runs and projections interleaved and each saying which it is, ascending by instant. Instants are UTC and every entry carries the EVENT's own zone; the reader's zone places them. Rehearsals and unlisted events are absent. A run is an INTERVAL, not an instant: an entry is in the window when the run OVERLAPS it, so one that began before the window and has not ended is still "what is on" (Phase 16a — reading the start instant alone made this route serve only the first of its three words, while the event's own page said `live`). Defaults to seven days back through 31 days out — the tail is where "recent" lives — and the window may span at most 92; the anonymous surface is the one with no login in front of it. Projections are forecast from NOW, never into the tail, since a slot the runner has already passed did not happen |
| `GET /public/events` | — | **the calendar** (Phase 14a): upcoming, live and recent, by series. Runs and projections interleaved and each saying which it is, ascending by instant. Instants are UTC and every entry carries the EVENT's own zone; the reader's zone places them. Rehearsals and unlisted events are absent. Defaults to now through 31 days out and the window may span at most 92 — the anonymous surface is the one with no login in front of it |
| `GET /public/events/:slug` | — | **one event** (Phase 14a): storyline, arc, what is live, what is next, what happened recently, and a results table once one is published. Takes an optional `?run=`, which is what an announcement's link carries, so a mail about last Friday's occurrence does not open next Friday's; a run belonging to some other event is **ignored rather than refused**, because a stale link in a months-old mail should land on the event it was about. A draft, an archived definition and an unlisted one all answer 404 |
| `GET /public/events/series/:slug` | — | **the arc** (Phase 14a). A series with no listed events is a 404, not an empty page: the arc is a label on its definitions, so a page for an empty one would publish the fact that an operator has named something they have not announced |
| `GET /player/events/history` | auth | **this account's participation** (Phase 14a) — the run, when it was, the score a module reported, and the rank once results were published (null until then, which is a real state rather than an error). Self-scoped on the session with **no id parameter**, deliberately: a route that took one would be a middleware mistake away from publishing who attended what. Keyset-paged on the participation row's id. It obeys the calendar's two exclusions, so attending an unannounced event does not disclose that it exists |
> | event plane | `lease.list.ok` (config **and** targeted property leases), `item.catalog.ok` with its bounds, `GET /world/<run>` an empty list rather than a 404 |
> | core | released `main` on a throwaway database, `capabilities: ["events"]` on `/public/version` |
> | one event | published, run, **`completed` / `health: ok`**, results published |
> | the page | `/site/events` reads *"Everything scheduled, live and recently finished"* and lists a run that finished two minutes earlier |
>
> The last row is 16a's calendar fix holding on `main`: before it, a run that had already started or
> finished was absent and the page rendered `entries: []`.
>
> **A fresh `Bridge.cfg` still ships `EventsEnabled=false` and `AdminWriteEnabled=false`** — the
> operator's real first-boot state, and the released config confirms it rather than a working tree's.
>
> One thing checked and deliberately **not** reported as a defect: a **cancelled** run appears on the
> public calendar. It is meant to. The entry carries its own `status`, and the page renders a past
> cancelled run as **"Did not happen"** — the honest label, not a silent omission.
>
> **The module's own install was walked too**, once step 3 cut `Module-uo` **v1.2.1**. The module
> arrived the way an operator's would: `POST /admin/modules` naming the release's **manifest** (not
> its tarball — core answers a tarball with *"the install manifest is larger than 262144 bytes"*,
> which is the size guard doing its job), core fetched the artifact over https from the allowlisted
> host, verified its `sha256`, and mounted it on the next boot with 12 event actions and 27 triggers.
> Then `PUT /admin/uo-link/config` with the four values the installer printed answered
> **`status: connected`, `pluginConnected: true`, `protocol: 7`** — released core, released module,
> released sidecar, released overlay, all four talking.
>
> On that rig the two Phase 16a fixes were confirmed in the shipped artefacts rather than in a working
> tree: the atlas **imported off a stock tree** (309 decor types, 6,455 points, 800 creatures, 558
> landmarks, 387 regions, 25 champions — the import that used to die at 313), and a world verb ran and
> **tore down for real** — three orcs spawned, ledger `reverted` ×3, `cleanup: complete`, and the shard
> itself answering `world.owned → owned: [], pruned: 0`. That last check is the one 16a's no-op
> teardown hid behind. The enablement gate and the cap behaved as specified on the way past: the dry
> run refused the action before it was enabled, then priced it `uo.creatures 3 of 10`.
>
> **And the leg found two more defects, both in the released bundle and neither visible to any test**
> (`Module-uo#35`).
>
> **The aggregator discarded the `UniqueId`, so no Phase 12b property lease was authorable at all.**
> All 6,455 spawn points imported with `unique_id` NULL; `listSpawners` filters
> `unique_id IS NOT NULL`, so `uo.options.spawners` — the only source those leases have — was an empty
> dropdown with nothing to explain itself. Every part of the path was right except one line: the files
> carry `<UniqueId>`, `parsePoints` returns it, the column exists, the insert passes it. `buildAtlas`
> rebuilds each point from an explicit field list and the field was not on it. **`PARSER_VERSION = 4`'s
> own note says a point keeps its `UniqueId` and names Phase 12b as the reason** — that bump exists to
> re-read trees for this field, and the field was dropped one function later. The intent shipped as a
> comment. Fixing it needs `PARSER_VERSION` 5 as well, because the tree's hashes have not changed —
> only what is kept from them — so nothing would re-read an existing install.
>
> **A landmark option value named 23 places at once.** 558 landmarks, 320 distinct `facet/name`:
> `Trammel/Entrance` is Blighted Grove, Covetous, Deceit, Despise, Destard and 18 more, and
> `landmarkPoint` resolves with `.find()`. So 22 of the 23 were unreachable and an author who picked
> "Entrance — Destard" got Blighted Grove, with a successful run and no warning. **The group was
> already the disambiguator** — shown in the dropdown, left out of the value. Now `facet/group/name`,
> distinct across all 558, with the two-part read kept as a fallback because a published version is
> immutable and those stored values are the authored record. A three-part value whose group is gone
> refuses rather than falling back: it asked for one place.
>
> Both are the same failure shape as 16a's blocking defect and worth naming as a class: **an option
> source that answers empty, or answers with a value that does not identify one thing, disables a
> feature silently.** Nothing errors, the form simply cannot express the thing — and a test that
> checks the parser, or the query, or the column in isolation passes throughout. The atlas fixture had
> no `<UniqueId>` in it at all until this phase, which is why a green suite said nothing for two.
**Two documents that are cutover-window work by construction.**
- **`runicgateway.com`** — `checkFacts` reads `main`, so any claim about events is unverifiable until
the cutover lands. Same 12a/12b split the engagement workstream needed.**16b landed it**, so both
of these are now unblocked: `main` carries the engine, the module and the app, and the bundle triple
the site quotes is sidecar **v2.2.0** / overlay **v1.2.0** / bundle **2026.09.10**.
the cutover lands. Same 12a/12b split the engagement workstream needed.
- **`.profile`** — the org landing page is updated when the *shape* of the project changes, which a new
@@ -1193,16 +1193,6 @@ rather than implementation and belong here:
with the key and an EMPTY list, meaning *"a command went out under this key and core never learned
what it did"*. Answering that honestly is what makes an unattended world write recoverable; a
module that cannot answer it says so, and the row stays visible to an operator.
- **That key IDENTIFIES a dispatch; it is not a key to send on the undo.** It names the command core
lost the answer to, so the module can ask the game about it. Forwarding it as the outgoing key of
the reverting command is a different thing entirely, and on a game whose at-most-once store keys on
the key alone — as the uo-link shard's does — the undo is then recognised as a repeat of the DO and
answered with the original reply. `module-uo` made exactly this mistake: teardown of all five world
verbs was a no-op that reported success, because every despawn carried the key its spawn had gone
out under. Found by the Phase 16 acceptance walk, with the ledger reading `reverted` and the shard
still holding every object. A command that undoes needs a key of its own or none at all; a repeated
undo is usually harmless by construction ("already gone" is a success), which is what makes *none*
the right answer more often than not.
- **Core owns cleanup, and it is derived rather than authored.** There is no `on_teardown` on an
action and no cleanup phase in a spec: an operator cannot be relied on to write the undo, and an
aborted run never reaches the phase they wrote it in. Cleanup is one sweep over the ledger and it
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.