Compare commits
1 Commits
132620e8b3
...
docs/event
| Author | SHA1 | Date | |
|---|---|---|---|
| c26d06a782 |
@@ -79,8 +79,6 @@ android-app/
|
||||
│ │ │ │ │ │ └── PushTickle.kt
|
||||
│ │ │ │ │ ├── result/
|
||||
│ │ │ │ │ │ └── ApiResult.kt
|
||||
│ │ │ │ │ ├── time/
|
||||
│ │ │ │ │ │ └── Instants.kt
|
||||
│ │ │ │ │ ├── web/
|
||||
│ │ │ │ │ │ ├── WebHandoff.kt
|
||||
│ │ │ │ │ │ └── WebsiteUrls.kt
|
||||
@@ -92,7 +90,6 @@ android-app/
|
||||
│ │ │ │ │ │ │ ├── AdminDto.kt
|
||||
│ │ │ │ │ │ │ ├── AuthDto.kt
|
||||
│ │ │ │ │ │ │ ├── ContactDto.kt
|
||||
│ │ │ │ │ │ │ ├── EventsDto.kt
|
||||
│ │ │ │ │ │ │ ├── NotificationsDto.kt
|
||||
│ │ │ │ │ │ │ ├── PageDto.kt
|
||||
│ │ │ │ │ │ │ ├── PlayerShardDto.kt
|
||||
@@ -105,7 +102,6 @@ android-app/
|
||||
│ │ │ │ │ │ ├── AdminApi.kt
|
||||
│ │ │ │ │ │ ├── AuthApi.kt
|
||||
│ │ │ │ │ │ ├── AuthRefreshApi.kt
|
||||
│ │ │ │ │ │ ├── EventsApi.kt
|
||||
│ │ │ │ │ │ ├── MeApi.kt
|
||||
│ │ │ │ │ │ ├── NotificationsApi.kt
|
||||
│ │ │ │ │ │ ├── PlayerShardApi.kt
|
||||
@@ -121,13 +117,11 @@ android-app/
|
||||
│ │ │ │ │ ├── ConnectionRepository.kt
|
||||
│ │ │ │ │ ├── ContactRepository.kt
|
||||
│ │ │ │ │ ├── ContentRepository.kt
|
||||
│ │ │ │ │ ├── EventsRepository.kt
|
||||
│ │ │ │ │ ├── NotificationsRepository.kt
|
||||
│ │ │ │ │ ├── PlayerShardRepository.kt
|
||||
│ │ │ │ │ ├── SettingsRepository.kt
|
||||
│ │ │ │ │ ├── ShardFeaturesRepository.kt
|
||||
│ │ │ │ │ ├── ShardRepository.kt
|
||||
│ │ │ │ │ ├── SiteCapabilitiesRepository.kt
|
||||
│ │ │ │ │ └── WikiRepository.kt
|
||||
│ │ │ │ ├── di/
|
||||
│ │ │ │ │ ├── AppModule.kt
|
||||
@@ -163,16 +157,6 @@ android-app/
|
||||
│ │ │ │ │ ├── contact/
|
||||
│ │ │ │ │ │ ├── ContactScreen.kt
|
||||
│ │ │ │ │ │ └── ContactViewModel.kt
|
||||
│ │ │ │ │ ├── events/
|
||||
│ │ │ │ │ │ ├── EventScreen.kt
|
||||
│ │ │ │ │ │ ├── EventSeriesScreen.kt
|
||||
│ │ │ │ │ │ ├── EventSeriesViewModel.kt
|
||||
│ │ │ │ │ │ ├── EventsScreen.kt
|
||||
│ │ │ │ │ │ ├── EventsViewModel.kt
|
||||
│ │ │ │ │ │ ├── EventTimes.kt
|
||||
│ │ │ │ │ │ ├── EventViewModel.kt
|
||||
│ │ │ │ │ │ ├── MyEventsScreen.kt
|
||||
│ │ │ │ │ │ └── MyEventsViewModel.kt
|
||||
│ │ │ │ │ ├── home/
|
||||
│ │ │ │ │ │ ├── HomeScreen.kt
|
||||
│ │ │ │ │ │ └── HomeViewModel.kt
|
||||
@@ -351,7 +335,6 @@ android-app/
|
||||
│ │ │ │ │ └── WikiDtoTest.kt
|
||||
│ │ │ │ └── fake/
|
||||
│ │ │ │ ├── FakeAdminApi.kt
|
||||
│ │ │ │ ├── FakeEventsApi.kt
|
||||
│ │ │ │ ├── FakeNotificationsApi.kt
|
||||
│ │ │ │ ├── FakePlayerShardApi.kt
|
||||
│ │ │ │ ├── FakePublicApi.kt
|
||||
@@ -362,8 +345,7 @@ android-app/
|
||||
│ │ │ └── repository/
|
||||
│ │ │ ├── AccountTrustedDevicesTest.kt
|
||||
│ │ │ ├── ConnectionVersionGuardTest.kt
|
||||
│ │ │ ├── ShardFeaturesRepositoryTest.kt
|
||||
│ │ │ └── SiteCapabilitiesRepositoryTest.kt
|
||||
│ │ │ └── ShardFeaturesRepositoryTest.kt
|
||||
│ │ ├── ui/
|
||||
│ │ │ ├── admin/
|
||||
│ │ │ │ ├── AdminContentViewModelTest.kt
|
||||
@@ -374,12 +356,8 @@ android-app/
|
||||
│ │ │ │ └── BrandAssetsTest.kt
|
||||
│ │ │ ├── contact/
|
||||
│ │ │ │ └── ContactViewModelTest.kt
|
||||
│ │ │ ├── events/
|
||||
│ │ │ │ ├── EventsViewModelsTest.kt
|
||||
│ │ │ │ └── EventTimesTest.kt
|
||||
│ │ │ ├── navigation/
|
||||
│ │ │ │ ├── MenuAccessTest.kt
|
||||
│ │ │ │ ├── MenuCapabilityGatingTest.kt
|
||||
│ │ │ │ ├── MenuFeatureGatingTest.kt
|
||||
│ │ │ │ ├── NavOverridesTest.kt
|
||||
│ │ │ │ ├── NavPathsTest.kt
|
||||
|
||||
31
link/v6.md
31
link/v6.md
@@ -80,31 +80,10 @@ A repeat of a key still in flight is answered **`bridge.busy`**: nothing runs, a
|
||||
told to come back. It is deliberately not spelled `bridge.busy.error` — nothing is wrong, the work
|
||||
is happening.
|
||||
|
||||
**2. A key that has begun is never released — except on a refusal.** Not when the handler throws.
|
||||
Releasing it would let a retry re-run a command that may have applied half of itself, which is the
|
||||
exact failure this file exists to prevent. A handler that throws stores a `bridge.error` reply
|
||||
instead, so the retry gets a definite answer and the step fails once rather than looping.
|
||||
|
||||
**A REFUSAL is the third case**, added by the Phase 16 acceptance walk and amending protocol 7 in
|
||||
place. A handler that ran to completion and answered `*.error` did not do anything — every refusal
|
||||
on this plane is a guard: a missing `runId`, an unknown item, a cap, a rate limit, a write that
|
||||
failed and left the value alone. Remembering it froze the answer for ever, so a refusal that
|
||||
*waiting fixes* could never be retried past. `uo.world.save` is the case that found it: the shard
|
||||
saves at most every 300 seconds, the module documents that as "the one refusal on this plane that
|
||||
waiting fixes", and six attempts over four minutes all replayed one frozen sentence — "the last save
|
||||
was 227 seconds ago" — because the number was the first reply's, not the clock's. A step's key is
|
||||
one value for the life of the step, so the operator's retry control could not escape it either.
|
||||
|
||||
So a refusal releases the key: nothing happened, and the caller may ask again. The refusal is still
|
||||
**emitted** to the caller, which is what ends that attempt; it is simply not remembered as the key's
|
||||
answer. A refusal is recognised by its `kind` ending in `.error`, matched on the suffix so a handler
|
||||
family added later is covered without extending a list. `bridge.error` is excluded deliberately —
|
||||
that is the reply the shard writes when a handler THREW, which is the case whose key must be kept.
|
||||
|
||||
**This puts a rule on handlers, and it is the rule the release rests on: do not answer `*.error`
|
||||
after changing the world.** Report a partial change in an `ok` reply, as `item.grant` does with
|
||||
`granted`/`missed` and `world.despawn` with `removed`/`gone`/`refused`. The shard cannot verify
|
||||
"nothing happened"; it takes the `.error` kind as the claim.
|
||||
**2. A key that has begun is never released.** Not even when the handler throws. Releasing it would
|
||||
let a retry re-run a command that may have applied half of itself, which is the exact failure this
|
||||
file exists to prevent. A handler that throws stores a `bridge.error` reply instead, so the retry
|
||||
gets a definite answer and the step fails once rather than looping.
|
||||
|
||||
**3. A replay is stamped with the REPEAT's correlation id.** The sidecar's `reqId` is a fresh
|
||||
per-process counter, so a retry is waiting on an id the first attempt never used. Replaying the
|
||||
@@ -123,7 +102,7 @@ evicted key's repeat *would* be applied a second time — so an eviction that dr
|
||||
its TTL prints a console warning naming the count. If the promise is ever actually breached, an
|
||||
operator reads it here rather than discovering a doubled spawn in the world.
|
||||
|
||||
`[bridge status` reports `idem(keys= seen= replayed= busy= evicted= uncorrelated= refused=)`.
|
||||
`[bridge status` reports `idem(keys= seen= replayed= busy= evicted= uncorrelated=)`.
|
||||
|
||||
#### 2.1.1 How the reply is captured
|
||||
|
||||
|
||||
@@ -1847,7 +1847,7 @@ no URL moved.
|
||||
| `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 |
|
||||
|
||||
@@ -15,14 +15,6 @@ UO actions, the integrations, the authoring UI, the public surface — needed no
|
||||
Those fourteen phases reach the game only to *announce*, over verbs the write plane already carries; nothing in them creates or
|
||||
changes a thing in the world.
|
||||
|
||||
**COMPLETE as of 2026-09-10.** All seventeen phases are built and on `main` in every repository they
|
||||
touch. P16 ran as three legs — **16a** the acceptance walk from `edge`, **16b** the six-step cutover
|
||||
and the re-verify against released artefacts, **16c** `runicgateway.com` and `.profile` — and 16c
|
||||
opened by closing the seventh cutover step 16b had left standing (`docs#232`). The platform the
|
||||
workstream leaves behind is sidecar **v2.2.0**, overlay **v1.2.0**, bundle **2026.09.10** on
|
||||
**protocol 7**, `Module-uo` **v1.2.2**, and `MODULE_API_VERSION` **1.10.0**. Each phase's record is
|
||||
in its own section below; `edge` stays standing, unused, in every repository.
|
||||
|
||||
---
|
||||
|
||||
## Before anything: three facts about the ground
|
||||
@@ -1828,280 +1820,12 @@ website, emulator — running a real multi-phase event, including three delibera
|
||||
Then `edge` → `main`, in the order every previous cutover used: the protocol side first, the module,
|
||||
core, docs, then the kit's re-pin and `runicgateway.com`.
|
||||
|
||||
> **Split into 16a (the walk) and 16b (the cutover)** (org lead, 2026-09-09), on the same argument
|
||||
> 12a/12b and 14a/14b were split on. The two sentences above cannot both hold: `link`,
|
||||
> `servuo-plugins` and `Module-uo` all release on push to **`main`**, so no released artefact
|
||||
> carrying events can exist until after the cutover. Engagement Phase 13 met the same wall and
|
||||
> resolved it the other way, cutting over first and walking from `main`. Here the walk goes first
|
||||
> against artefacts built from `edge` exactly the way a release builds them, because every walk in
|
||||
> this workstream has found defects and a defect found on `edge` is a reviewed PR rather than a
|
||||
> hotfix to `main`. **16b re-verifies against the real released bundle** — install, boot, run one
|
||||
> event — so the delivery path is still proved, just second. A third leg, **16c**, carries
|
||||
> `runicgateway.com` and `.profile`.
|
||||
>
|
||||
> **16a WALKED, and it is four repos** — `Module-uo`, `website`, `servuo-plugins`, `docs`. The whole
|
||||
> rig: real ServUO 57.4 (208k items, 42k mobiles) → a `cargo --release` sidecar on protocol 7 → core
|
||||
> with the module installed from a release-shaped bundle → the Android app on an emulator. The
|
||||
> overlay was deployed from a tarball built the way CI builds one, into a tree with `Scripts/Custom/
|
||||
> Bridge` and `Saves/Bridge` deleted first, so it was a first install rather than an upgrade.
|
||||
>
|
||||
> **All three deliberate failures pass.** (1) A mid-run process kill landed mid-TEARDOWN — sharper
|
||||
> than mid-step, since a phase executes in about a second — with the run `completed`, cleanup
|
||||
> `pending`, a lease half-returned and 21 world objects up: teardown resumed on restart and all 15
|
||||
> steps still read `attempts = 1`, so nothing re-executed. (2) The sidecar killed during a phase gate
|
||||
> left the run `degraded` rather than failed, `core.lease` retrying with a reason, and the four world
|
||||
> writes behind it **parked at `attempts = 0`**; the shard reconnected on its own. (3) A cap of 5
|
||||
> against a step asking for 12 answered `refused` — its own status — with `code: "cap"` and *"asks
|
||||
> for 12 of `uo.creatures`; 0 of 5 is already spent this run"*, and the dry run had already refused
|
||||
> it in the author's own words.
|
||||
>
|
||||
> **Six defects, all in code already merged to `edge`, and the suites were green on either side of
|
||||
> every one.** Two were blocking or worse. **The spawn atlas could not import on a stock ServUO
|
||||
> tree** — a case-sensitive JS dedupe against an `..._ai_ci` PRIMARY KEY, four colliding decoration
|
||||
> spellings in ServUO's own files, and the whole transaction lost; with no atlas every option source
|
||||
> answers empty and no world verb can be authored at all. **Teardown of all five world verbs was a
|
||||
> no-op that reported success** — `revertOwned` sent the despawn under the step's key, which is the
|
||||
> key the SPAWN used, so the shard replayed the spawn's reply and `OnDespawn` never ran; the ledger
|
||||
> read `reverted` while the shard held all 21 objects, and the same despawn under a fresh key removed
|
||||
> every one. Then: **the public calendar served neither live nor recent runs** though §I promises all
|
||||
> three, so the site said `live` on one page and showed nothing on the other; **a resource left
|
||||
> `reverting` by a crash was never reclaimed**, and the manual cleanup route answered 200 while doing
|
||||
> nothing, which stranded a lease and blocked the NEXT run of the same event; **a transient refusal
|
||||
> under an idempotency key was permanent**, because the shard's store had no case for a handler that
|
||||
> ran and deliberately did nothing; and **three facts every announcement computes were declared by no
|
||||
> trigger** and silently dropped.
|
||||
>
|
||||
> One reported defect was **withdrawn**: `skip` refusing a `failed` step is not a dead end, because
|
||||
> `resume` carries a run past any settled step — the route's own docs say so and the rig confirmed
|
||||
> it. The runner claims only `pending` steps, so `failed` and `refused` are both settled.
|
||||
>
|
||||
> Every fix is verified against the rig, not only against tests: the atlas imports 309 decor types
|
||||
> and 6,455 points; a full four-phase run's teardown leaves the shard owning **0**; a lease stranded
|
||||
> by a real crash is reclaimed in one sweep and `cleanup_status` reaches `complete`; the same save
|
||||
> key 25 seconds apart answers "15 seconds ago" then "40 seconds ago"; and `/site/events` shows a
|
||||
> live run as **Happening now** beside recent ones, in the browser and in the app. Each new test was
|
||||
> confirmed to FAIL without its fix.
|
||||
>
|
||||
> **`Module-uo`'s `revert` no longer forwards core's key at all** — `MODULE_API.md` now says why that
|
||||
> key identifies a lost dispatch rather than addressing the undo. **Protocol 7 is amended in place**:
|
||||
> a refusal releases its key, with the rule that pays for it written down — *do not answer `*.error`
|
||||
> after changing the world*.
|
||||
|
||||
> **16b CUT OVER (2026-09-09/10) — six steps, and core before the module.** The protocol pair
|
||||
> (`link#40` + `servuo-plugins#26`) is ONE step, not two: `bundle.yml`'s Gate 1 reads the protocol
|
||||
> number out of both released artefacts and refuses a pair that disagrees, so whichever lands first
|
||||
> leaves a compose that cannot run. Then core (`website#199`), the module carrying its own re-pin
|
||||
> (`Module-uo#34`), the app (`Android-app#46`), the kit's re-pin (`Integration-kit#11`), and docs.
|
||||
>
|
||||
> **Core lands before the module**, which departs from the sentence above and matches what the
|
||||
> engagement cutover actually did: `Module-uo`'s `ci/core-ref.json` has to name a website `main` sha
|
||||
> carrying MODULE_API 1.10.0, and that sha does not exist until core has landed. Four decisions, all
|
||||
> as recommended (org lead, 2026-09-09): that order; the app merges with **no `v*` tag**, so no APK
|
||||
> was cut; the re-verify walks the whole delivery path; and **`edge` stays standing** in every repo
|
||||
> rather than being deleted as the module-system cutover deleted its own.
|
||||
>
|
||||
> Releases cut: sidecar **v2.2.0**, overlay **v1.2.0**, bundle **2026.09.10 (protocol 7)**. `website`
|
||||
> never releases. `MODULE_API_VERSION` and `EVENTS.md` are untouched by this leg — the cutover moves
|
||||
> no contract.
|
||||
>
|
||||
> **`Integration-kit#10` had been merged early**, on 2026-09-08, though it was written to be held —
|
||||
> so the kit's `main` was red on `checkCoreApi` for two days. That is what step 5 closes, and it is
|
||||
> the reason the re-pin is a repair rather than only a date.
|
||||
>
|
||||
> #### The outage, and what it did not break
|
||||
>
|
||||
> Gitea was unreachable for about ten minutes in the middle of the window (Cloudflare 1033/530) and
|
||||
> killed **both** release runs. `link`'s built every binary and wrote `SHA256SUMS`, then died pushing
|
||||
> the tag: `fatal: unable to access … The requested URL returned error: 530`. `servuo-plugins`' died
|
||||
> inside `Set up job` after 11m52s with no step ever executing — which is why that job's log route
|
||||
> answers 500 while its predecessor's serves fine: **there is no log blob, and that absence is
|
||||
> evidence.** No tag was pushed either time, so the orphaned-tag failure mode did not occur, and
|
||||
> re-running both by `workflow_dispatch` published them. The first to land left the pair mismatched
|
||||
> and compose run 102 failed exactly as the PRs predicted; the second dispatched it again and 103
|
||||
> composed. `link`'s `rust-gates` reds on three earlier PRs were `curl: (6) Could not resolve host:
|
||||
> sh.rustup.rs` inside the runner — infrastructure, not code, on all four counts.
|
||||
>
|
||||
> **A job's log IS readable on this instance, through the web route rather than the API:**
|
||||
> `/{owner}/{repo}/actions/runs/<n>/jobs/<j>/logs` with an API token, served as `text/plain`; step
|
||||
> statuses come from the UI's own POST endpoint with a `_csrf` cookie. Every earlier phase diagnosed
|
||||
> CI by reproducing jobs locally, on the belief that logs were unreachable. They are not, and reading
|
||||
> one is what turned four red X's into four known causes in about ten minutes.
|
||||
>
|
||||
> #### A seventh defect, red on every events PR since Phase 10
|
||||
>
|
||||
> `website`'s `server-tests` job had been failing since `#192` — eight PRs, every one reporting
|
||||
> `# fail 1`, always **the same single test**, so nothing else was ever hiding behind it. The
|
||||
> workstream merged over it eight times.
|
||||
>
|
||||
> `events/announce.js` asked `Intl.DateTimeFormat('en-GB', { …, hour12: true })`, and **that is not
|
||||
> the same request as a 12-hour clock.** For a locale whose default cycle is h23 — `en-GB` is one —
|
||||
> Node 20 resolves `hour12: true` to **`h11`**, whose hours run 0–11, so midnight renders `0:00 am`;
|
||||
> Node 22 and later resolve it to `h12` and it renders `12:00 am`. **Same ICU (78.2) on both sides**,
|
||||
> so this is V8's ECMA-402 behaviour and not locale data — no amount of matching the runner's locale
|
||||
> would have found it.
|
||||
>
|
||||
> The image ships `node:20-alpine` and CI runs Node 20, while a dev machine is newer. So the mail
|
||||
> every real recipient got said **"0:00 am"** beside a schedule editor saying "12:00 AM" — one
|
||||
> instant, two spellings, the exact contradiction that option was added to prevent — and it rendered
|
||||
> correctly in front of everyone who reviewed it. Fixed to `hourCycle: 'h12'` (`website#200`), which
|
||||
> is the form `recurrence.js` had already adopted for the mirror-image case (`h23` **rather than**
|
||||
> `hour12: false`); `announce.js` was the last `hour12` in either repo.
|
||||
>
|
||||
> **The rule: `hour12` is a request about a locale's preference, `hourCycle` is a request about the
|
||||
> clock. Ask for the clock.** And the test now says so out loud, because it can only fail on Node 20:
|
||||
> a green run on a dev machine is not evidence, and CI is what holds that line.
|
||||
>
|
||||
> #### The re-verify, from artefacts an operator would download
|
||||
>
|
||||
> This is the leg 16a could not do — a locally built bundle cannot go through core's module installer,
|
||||
> which is https-only with a host allowlist.
|
||||
>
|
||||
> | | |
|
||||
> |---|---|
|
||||
> | installer | released `v0.1.1` binary, checksum matched against the release's own `SHA256SUMS` |
|
||||
> | bundle | resolved **2026.09.10, protocol 7**; both component checksums verified by the installer |
|
||||
> | overlay sync | a **first install** into a stock 57.4 tree — `add=30 change=1 unchanged=0` |
|
||||
> | script build | `0 Warning(s) 0 Error(s)` — the released overlay compiles on a stock tree, which no release had ever been asked to prove |
|
||||
> | shard boot | `[Bridge] enabled=True … adminWrite=True … events=True`, then `connected to 127.0.0.1:7788` |
|
||||
> | sidecar | `server.hello` for **208,568 items / 42,871 mobiles**; `x-uolink-version: 7` |
|
||||
> | 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
|
||||
subsystem is.
|
||||
|
||||
> **16c BUILT (2026-09-09/10) — and it began by closing the cutover's missing seventh step.**
|
||||
> `runicgateway.com#30` and `.profile#6`, both onto `main`; the leg's first act was `docs#232`.
|
||||
>
|
||||
> #### The step 16b left on `edge`
|
||||
>
|
||||
> Six repositories were cut over and every one of them showed **0 commits on `edge` that are not on
|
||||
> `main`**. This one showed **46**. Step 6 (`#231`) landed the record *on* `edge` rather than cutting
|
||||
> `edge` over — an easy thing to miss, because the step's own PR merged green and closed. The
|
||||
> consequence was quiet and total: `docs` `main` opened `EVENTS.md` with *"revision 5. **No code
|
||||
> written.** Read against … `MODULE_API_VERSION` 1.9.0 · sidecar protocol 5"* while six repositories
|
||||
> shipped the engine on protocol 7, and `link/v6.md` and `v7.md` — the specs of record for two
|
||||
> protocol versions — existed on no default branch anywhere.
|
||||
>
|
||||
> **Nothing in this workstream could have caught it.** Every check that guards a contract lives in
|
||||
> the repository that *depends* on the contract, and a documentation repository has no dependants.
|
||||
> What found it was the one check that reads `docs` from outside: `runicgateway.com`'s
|
||||
> `checkReference.mjs` asserts every canonical document it names still exists on `main`, and adding
|
||||
> the current protocol spec failed with `✗ canonical doc link/v7.md`. **The site is the docs
|
||||
> repository's only dependant, and 16c is the only phase that would ever have run that check.**
|
||||
>
|
||||
> The merge was clean, and both `PROJECT_TREE.md` files stayed on `main`'s newer automated syncs —
|
||||
> `edge` never edited them, so git kept `main`'s side. `edge` stays standing, per 16b's decision,
|
||||
> now four generated commits behind.
|
||||
>
|
||||
> #### The site
|
||||
>
|
||||
> The checks were red before the phase started and named their own answers, which is the whole
|
||||
> bargain §12 of that repository's plan struck: nine `checkFacts` values (protocol 5 → **7** in all
|
||||
> three declaration sites, `moduleApi` → **1.10.0**, the bundle triple, `link` **v2.2.0**, `Module-uo`
|
||||
> **v1.2.2** — a third module release, one past the v1.2.1 the cutover cut), and **twenty-seven
|
||||
> `Bridge.cfg` keys** the site listed nowhere. Those became five groups rather than an appendix,
|
||||
> because `EventsEnabled` is a *second consent switch* and belongs beside the ceilings it governs
|
||||
> rather than filed under `AdminWriteEnabled`.
|
||||
>
|
||||
> Two pages, the treatment Teams has: **Scheduled events** under Administration and **Events
|
||||
> architecture**. Two capability entries, so the homepage, `/features/` and `/modules/` stop omitting
|
||||
> the subsystem — and the calendar one is deliberately **not** `needsModule`, because a bare core can
|
||||
> author and run an event and that marker means "present, correct and permanently empty".
|
||||
>
|
||||
> **`/privacy` owed a row and had none.** `event_run_participants` is personal data — scores and
|
||||
> ranks against a module-opaque member key, linked to an account where one is linked, feeding a
|
||||
> participant's own history. The new `deploy-events` row states the retention exactly, including the
|
||||
> asymmetry that matters: the diagnostic log is swept after 90 days **and only on terminal runs**,
|
||||
> while the run, its steps and its participants are never swept, because they are the record of what
|
||||
> was done to a shared world.
|
||||
>
|
||||
> **A naming collision worth fixing while it was cheap.** `reference/event-catalog` is about what a
|
||||
> shard *emits*; with a scheduled-event system shipped, two things in the documentation were called
|
||||
> an event catalog. Retitled **"Shard event catalog"**, with the route left alone so nothing outside
|
||||
> that repository breaks — and the page now opens by saying which of the two it is, since the kinds
|
||||
> it lists are exactly what a phase can wait for.
|
||||
>
|
||||
> **No screenshots**, and stated as a choice: capturing the events surfaces means standing the whole
|
||||
> rig back up for images no check requires, and the engagement workstream's site leg added none
|
||||
> either.
|
||||
>
|
||||
> #### `.profile`
|
||||
>
|
||||
> One bullet, and two stale numbers. The bullet says the posture rather than the feature list — off
|
||||
> by default, caps in the database, cleanup generated from a ledger, and *an event does not edit the
|
||||
> world, it holds a lease the game restores on its own deadline*. The numbers are protocol **5 → 7**
|
||||
> in the four values the installer prints (the block a reader copies into Admin → Shard, where a
|
||||
> wrong number is a pairing failure with no obvious cause) and **module-uo v1.1.0 → v1.2.2**.
|
||||
>
|
||||
> #### What 16c did not need
|
||||
>
|
||||
> No `EVENTS.md` change, no `MODULE_API_VERSION` change, no protocol change, no release. The phase
|
||||
> moves no contract — it makes the ones already moved legible from outside the organisation.
|
||||
|
||||
---
|
||||
|
||||
## What this plan does not do
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -169,8 +169,6 @@ website/
|
||||
│ │ ├── lib/
|
||||
│ │ │ ├── adminNav.js
|
||||
│ │ │ ├── engagementRules.js
|
||||
│ │ │ ├── eventAuthoring.js
|
||||
│ │ │ ├── eventCalendar.js
|
||||
│ │ │ ├── format.js
|
||||
│ │ │ ├── heroLayout.js
|
||||
│ │ │ ├── moduleAdmin.js
|
||||
@@ -218,11 +216,6 @@ website/
|
||||
│ │ │ │ │ ├── EngagementSuppressions.jsx
|
||||
│ │ │ │ │ ├── EngagementTemplates.jsx
|
||||
│ │ │ │ │ ├── EngagementTriggers.jsx
|
||||
│ │ │ │ │ ├── EventActions.jsx
|
||||
│ │ │ │ │ ├── EventEditor.jsx
|
||||
│ │ │ │ │ ├── EventRun.jsx
|
||||
│ │ │ │ │ ├── EventsAdmin.jsx
|
||||
│ │ │ │ │ ├── EventsCalendar.jsx
|
||||
│ │ │ │ │ ├── HeroEditor.jsx
|
||||
│ │ │ │ │ ├── InvitesAdmin.jsx
|
||||
│ │ │ │ │ ├── Moderation.jsx
|
||||
@@ -253,7 +246,6 @@ website/
|
||||
│ │ │ │ ├── ForgotPassword.jsx
|
||||
│ │ │ │ ├── PlayerAccount.jsx
|
||||
│ │ │ │ ├── PlayerAppeals.jsx
|
||||
│ │ │ │ ├── PlayerEvents.jsx
|
||||
│ │ │ │ ├── PlayerInbox.jsx
|
||||
│ │ │ │ ├── PlayerLogin.jsx
|
||||
│ │ │ │ ├── PlayerNotifications.jsx
|
||||
@@ -266,9 +258,6 @@ website/
|
||||
│ │ │ ├── public/
|
||||
│ │ │ │ ├── About.jsx
|
||||
│ │ │ │ ├── CmsPage.jsx
|
||||
│ │ │ │ ├── EventPage.jsx
|
||||
│ │ │ │ ├── Events.jsx
|
||||
│ │ │ │ ├── EventSeries.jsx
|
||||
│ │ │ │ ├── FiveOnFriday.jsx
|
||||
│ │ │ │ ├── Maintenance.jsx
|
||||
│ │ │ │ ├── News.jsx
|
||||
@@ -290,8 +279,6 @@ website/
|
||||
│ │ ├── apiClient.test.js
|
||||
│ │ ├── emailTemplates.test.js
|
||||
│ │ ├── engagementRules.test.js
|
||||
│ │ ├── eventAuthoring.test.js
|
||||
│ │ ├── eventCalendar.test.js
|
||||
│ │ ├── featureGate.test.js
|
||||
│ │ ├── format.test.js
|
||||
│ │ ├── heroLayout.test.js
|
||||
@@ -365,7 +352,6 @@ website/
|
||||
│ │ │ └── validateBlocks.js
|
||||
│ │ ├── config/
|
||||
│ │ │ ├── brand.js
|
||||
│ │ │ ├── coreEventActions.js
|
||||
│ │ │ ├── coreStreams.js
|
||||
│ │ │ ├── coreTriggers.js
|
||||
│ │ │ ├── csp.js
|
||||
@@ -407,18 +393,6 @@ website/
|
||||
│ │ │ ├── suppressions.js
|
||||
│ │ │ ├── templates.js
|
||||
│ │ │ └── templateSeeds.js
|
||||
│ │ ├── events/
|
||||
│ │ │ ├── announce.js
|
||||
│ │ │ ├── authorize.js
|
||||
│ │ │ ├── cleanup.js
|
||||
│ │ │ ├── dispatch.js
|
||||
│ │ │ ├── gates.js
|
||||
│ │ │ ├── ledger.js
|
||||
│ │ │ ├── participants.js
|
||||
│ │ │ ├── price.js
|
||||
│ │ │ ├── recurrence.js
|
||||
│ │ │ ├── spec.js
|
||||
│ │ │ └── verify.js
|
||||
│ │ ├── middleware/
|
||||
│ │ │ ├── botScore.js
|
||||
│ │ │ ├── loginProtection.js
|
||||
@@ -468,25 +442,6 @@ website/
|
||||
│ │ │ │ ├── engagementSuppressions.db.js
|
||||
│ │ │ │ ├── engagementTemplates.db.js
|
||||
│ │ │ │ └── engagementTemplates.model.js
|
||||
│ │ │ ├── events/
|
||||
│ │ │ │ ├── eventActionSettings.db.js
|
||||
│ │ │ │ ├── eventCalendar.model.js
|
||||
│ │ │ │ ├── eventDefinitions.db.js
|
||||
│ │ │ │ ├── eventDefinitions.model.js
|
||||
│ │ │ │ ├── eventJson.js
|
||||
│ │ │ │ ├── eventPhaseGates.db.js
|
||||
│ │ │ │ ├── eventPublic.model.js
|
||||
│ │ │ │ ├── eventRunBudget.db.js
|
||||
│ │ │ │ ├── eventRunControls.model.js
|
||||
│ │ │ │ ├── eventRunLog.db.js
|
||||
│ │ │ │ ├── eventRunParticipants.db.js
|
||||
│ │ │ │ ├── eventRunResources.db.js
|
||||
│ │ │ │ ├── eventRuns.db.js
|
||||
│ │ │ │ ├── eventRuns.model.js
|
||||
│ │ │ │ ├── eventRunSteps.db.js
|
||||
│ │ │ │ ├── eventSeries.db.js
|
||||
│ │ │ │ ├── eventSeries.model.js
|
||||
│ │ │ │ └── eventVersions.db.js
|
||||
│ │ │ ├── invites/
|
||||
│ │ │ │ ├── invites.db.js
|
||||
│ │ │ │ └── invites.model.js
|
||||
@@ -604,8 +559,6 @@ website/
|
||||
│ │ │ │ │ ├── emailConfig.controller.js
|
||||
│ │ │ │ │ ├── engagement.controller.js
|
||||
│ │ │ │ │ ├── engagement.router.js
|
||||
│ │ │ │ │ ├── events.controller.js
|
||||
│ │ │ │ │ ├── events.router.js
|
||||
│ │ │ │ │ ├── imageUpload.js
|
||||
│ │ │ │ │ ├── index.js
|
||||
│ │ │ │ │ ├── invites.controller.js
|
||||
@@ -654,8 +607,6 @@ website/
|
||||
│ │ │ │ ├── player/
|
||||
│ │ │ │ │ ├── appeals.controller.js
|
||||
│ │ │ │ │ ├── appeals.router.js
|
||||
│ │ │ │ │ ├── events.controller.js
|
||||
│ │ │ │ │ ├── events.router.js
|
||||
│ │ │ │ │ ├── index.js
|
||||
│ │ │ │ │ ├── teamForum.controller.js
|
||||
│ │ │ │ │ ├── teamForum.router.js
|
||||
@@ -664,8 +615,6 @@ website/
|
||||
│ │ │ │ ├── public/
|
||||
│ │ │ │ │ ├── engagement.controller.js
|
||||
│ │ │ │ │ ├── engagement.router.js
|
||||
│ │ │ │ │ ├── events.controller.js
|
||||
│ │ │ │ │ ├── events.router.js
|
||||
│ │ │ │ │ ├── index.js
|
||||
│ │ │ │ │ ├── modules.controller.js
|
||||
│ │ │ │ │ ├── modules.router.js
|
||||
@@ -697,7 +646,6 @@ website/
|
||||
│ │ │ ├── engagementEmit.js
|
||||
│ │ │ ├── engagementRetentionPrune.js
|
||||
│ │ │ ├── engagementWorker.js
|
||||
│ │ │ ├── eventRunner.js
|
||||
│ │ │ ├── forumHtml.js
|
||||
│ │ │ ├── htmlShell.js
|
||||
│ │ │ ├── logger.js
|
||||
@@ -770,27 +718,6 @@ website/
|
||||
│ │ ├── engagementRetentionSql.test.js
|
||||
│ │ ├── engagementTemplatesAdmin.test.js
|
||||
│ │ ├── engagementTriggers.test.js
|
||||
│ │ ├── eventActionRegistry.test.js
|
||||
│ │ ├── eventAnnounce.test.js
|
||||
│ │ ├── eventAuthorize.test.js
|
||||
│ │ ├── eventCleanup.test.js
|
||||
│ │ ├── eventGates.test.js
|
||||
│ │ ├── eventIntegrations.test.js
|
||||
│ │ ├── eventLedger.test.js
|
||||
│ │ ├── eventModuleContract.test.js
|
||||
│ │ ├── eventParticipants.test.js
|
||||
│ │ ├── eventPrice.test.js
|
||||
│ │ ├── eventPublic.test.js
|
||||
│ │ ├── eventRecurrence.test.js
|
||||
│ │ ├── eventRunControls.test.js
|
||||
│ │ ├── eventRunner.test.js
|
||||
│ │ ├── eventRunnerSql.test.js
|
||||
│ │ ├── eventsAdmin.test.js
|
||||
│ │ ├── eventSchedule.test.js
|
||||
│ │ ├── eventSeries.test.js
|
||||
│ │ ├── eventSpec.test.js
|
||||
│ │ ├── eventsRoles.test.js
|
||||
│ │ ├── eventVerify.test.js
|
||||
│ │ ├── honeypot.test.js
|
||||
│ │ ├── htmlShell.test.js
|
||||
│ │ ├── inviteController.test.js
|
||||
|
||||
Reference in New Issue
Block a user