feat(events): the authoring UI proper (Phase 13) #195

Merged
whitlocktech merged 1 commits from feature/events-p13-authoring-ui into edge 2026-09-07 22:12:47 +00:00
Member

Events Phase 13. One of two: website, docs (docs#223), both onto edge. Spec of record: docs/website/EVENTS.md §I.

MODULE_API_VERSION is untouched. Every schema this form renders was already in the catalog — a param's name, type, whether it is required, its description, its example, and the option source behind it. The module contract needed nothing.

The two placeholders, replaced

Phase 3 shipped a step's params and a phase's where as raw JSON boxes, each captioned in the source as a placeholder for this phase. The interesting half is not that they are forms now — it is when they stop being one.

A value the editor cannot round-trip is shown rather than silently rewritten, and that is the condition builder's rule ported rather than a new idea: flattening A and (B or C) into A and B and C and dropping a param the action does not declare are the same mistake, a save that looks clean and means something else. Three things force a step into JSON and the screen names which — the action is dormant, the step carries an undeclared param (which the save refuses by name, and which is what the author needs to read), or a value no single control can hold.

The gate builder is conditionRowsFrom/conditionsFromRows/coerceLiteral, imported, not reimplemented. The grammar behind a phase gate IS the engagement grammar: the server validates it with engagement/conditions.js and renders the diagnosis panel's sentence from the same labels. The coercion is the half worth reviewing — every value in an HTML input is a string, and { cmp: 'gt', value: "5" } against an int variable is rightly refused by a server whose error the author would then read as a complaint about JSON rather than about what they typed.

One route, and why it is a route

POST /admin/events/price, admin, editor — the gate verify has, for a stronger version of its reason.

A module's cost() is a server function of params (§F), so a meter has nothing to add up until something asks. The dry run is the wrong thing to ask on a debounce twice over: it dispatches every step through the module — and through it to a sidecar and a game tick — and a pass against a published version is RECORDED, which is the stamp §K's unattended-start gate reads. This dispatches nothing and records nothing, and takes the spec in the body because the plan being priced is unsaved between keystrokes.

A step it cannot price is reported in unpriced, never counted as free. All three ways that happens make the totals an under-count, and an author trusting a number smaller than what will happen is worse off than one with no number. An undeclared dimension is still counted: the action really will try to spend it — the step is refused at dispatch for exactly that — so the amount is true and it is the enforcement that is missing.

Two defects in already-merged code

  • Creating an event has been impossible since Phase 6. events/new was added beside events/:id and binds no param, and React Router ranks a static segment above a dynamic one whatever the order — so isNew was false, the editor fetched /admin/events/undefined, and the 400 was invisible: !form is true for every failed load, so the error state sat behind a spinner that never stopped. One route now, new a value of :id, and the error is checked before the form.
  • 12b's searchable sources had no consumer. The server half shipped — q on the options route, searchable on the answer — and the only UI that reads a source never sent a term. The spawner list is 6,707 entries against MAX_OPTIONS' 2,000, so the one screen that picks a spawner was picking from a truncation of two thirds of the world with nothing saying so. Which control is drawn follows the source's own searchable, never the length of the answer.

Smaller decisions worth a line

  • A boolean param is a three-value select, because a checkbox cannot say "not set" — a real third state for an optional boolean, and a checkbox would have posted false for every param nobody touched.
  • An empty field removes the key rather than posting "": checkParams reads undefined, null and '' alike, so a blank required field is answered "is required" instead of with a type complaint.
  • A half-typed number is kept as typedcoerceLiteral's rule, so - does not become NaN mid-keystroke.
  • The meter debounces AND counts generations. Requests 400ms apart need not answer in order, and an older answer landing last would describe a plan already changed. A failure leaves the last answer standing, dimmed.

Checks

Server npm test: 2113 tests, 2024 pass, 0 fail (89 DB-skipped) — the new eventPrice.test.js (13) plus the 403 walk's new row. Client npm test: 380 pass, 0 fail, 18 new. The client builds. routes:manifest and swagger regenerated — one route added, none moved.

Walked in a browser against the local stack, with a throwaway module registering a costed action, a budget dimension, a 6,707-entry searchable source and a trigger with typed variables — core alone declares no budget and no searchable source, so there would otherwise have been nothing to meter or search. The walk authored a multi-phase event with no raw JSON at any point, and ended with three things agreeing: the run came back keyed walk:Yew (params reaching the concurrency-key template), the runner refused the step with "asks for 45 of rig.creatures; 0 of 30" exactly as the meter had warned, and the diagnosis panel rendered the builder's predicate as "region is "Yew" and tier is at least 3". The stored tree came back as {"cmp":"gte","value":3} — an int, not "3".

🤖 Generated with Claude Code

https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4

Events Phase 13. One of **two**: `website`, `docs` (docs#223), both onto `edge`. Spec of record: [`docs/website/EVENTS.md`](https://gitea.whitlocktech.com/RunicGateway/docs/pulls/223) §I. **`MODULE_API_VERSION` is untouched.** Every schema this form renders was already in the catalog — a param's name, type, whether it is required, its description, its example, and the option source behind it. The module contract needed nothing. ## The two placeholders, replaced Phase 3 shipped a step's params and a phase's `where` as raw JSON boxes, each captioned in the source as a placeholder for this phase. The interesting half is not that they are forms now — it is **when they stop being one.** A value the editor cannot round-trip is **shown rather than silently rewritten**, and that is the condition builder's rule ported rather than a new idea: flattening `A and (B or C)` into `A and B and C` and dropping a param the action does not declare are the same mistake, a save that looks clean and means something else. Three things force a step into JSON and the screen names which — the action is dormant, the step carries an undeclared param (which the save refuses **by name**, and which is what the author needs to read), or a value no single control can hold. **The gate builder is `conditionRowsFrom`/`conditionsFromRows`/`coerceLiteral`, imported, not reimplemented.** The grammar behind a phase gate IS the engagement grammar: the server validates it with `engagement/conditions.js` and renders the diagnosis panel's sentence from the same labels. The coercion is the half worth reviewing — every value in an HTML input is a string, and `{ cmp: 'gt', value: "5" }` against an `int` variable is rightly refused by a server whose error the author would then read as a complaint about JSON rather than about what they typed. ## One route, and why it is a route `POST /admin/events/price`, `admin, editor` — the gate `verify` has, for a stronger version of its reason. A module's `cost()` is a server function of params (§F), so a meter has nothing to add up until something asks. **The dry run is the wrong thing to ask on a debounce twice over**: it dispatches every step through the module — and through it to a sidecar and a game tick — and a pass against a published version is RECORDED, which is the stamp §K's unattended-start gate reads. This dispatches nothing and records nothing, and takes the spec in the **body** because the plan being priced is unsaved between keystrokes. **A step it cannot price is reported in `unpriced`, never counted as free.** All three ways that happens make the totals an under-count, and an author trusting a number smaller than what will happen is worse off than one with no number. An **undeclared dimension is still counted**: the action really will try to spend it — the step is refused at dispatch for exactly that — so the amount is true and it is the enforcement that is missing. ## Two defects in already-merged code - **Creating an event has been impossible since Phase 6.** `events/new` was added beside `events/:id` and binds no param, and React Router ranks a static segment above a dynamic one whatever the order — so `isNew` was false, the editor fetched `/admin/events/undefined`, and the 400 was **invisible**: `!form` is true for every failed load, so the error state sat behind a spinner that never stopped. One route now, `new` a value of `:id`, and the error is checked before the form. - **12b's searchable sources had no consumer.** The server half shipped — `q` on the options route, `searchable` on the answer — and the only UI that reads a source never sent a term. The spawner list is 6,707 entries against `MAX_OPTIONS`' 2,000, so the one screen that picks a spawner was picking from a truncation of two thirds of the world with nothing saying so. Which control is drawn follows the source's own `searchable`, never the length of the answer. ## Smaller decisions worth a line - **A boolean param is a three-value select**, because a checkbox cannot say *"not set"* — a real third state for an optional boolean, and a checkbox would have posted `false` for every param nobody touched. - **An empty field removes the key** rather than posting `""`: `checkParams` reads `undefined`, `null` and `''` alike, so a blank required field is answered *"is required"* instead of with a type complaint. - **A half-typed number is kept as typed** — `coerceLiteral`'s rule, so `-` does not become `NaN` mid-keystroke. - **The meter debounces AND counts generations.** Requests 400ms apart need not answer in order, and an older answer landing last would describe a plan already changed. A failure leaves the last answer standing, dimmed. ## Checks Server `npm test`: **2113 tests, 2024 pass, 0 fail** (89 DB-skipped) — the new `eventPrice.test.js` (13) plus the 403 walk's new row. Client `npm test`: **380 pass, 0 fail**, 18 new. The client builds. `routes:manifest` and `swagger` regenerated — **one route added, none moved**. **Walked in a browser against the local stack**, with a throwaway module registering a costed action, a budget dimension, a 6,707-entry searchable source and a trigger with typed variables — core alone declares no budget and no searchable source, so there would otherwise have been nothing to meter or search. The walk authored a multi-phase event with **no raw JSON at any point**, and ended with three things agreeing: the run came back keyed `walk:Yew` (params reaching the concurrency-key template), the runner refused the step with *"asks for 45 of rig.creatures; 0 of 30"* exactly as the meter had warned, and the diagnosis panel rendered the builder's predicate as *"region is "Yew" and tier is at least 3"*. The stored tree came back as `{"cmp":"gte","value":3}` — an int, not `"3"`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
wtclaude added 1 commit 2026-09-07 21:36:13 +00:00
feat(events): the authoring UI proper (Phase 13)
Some checks failed
PR Checks / client-build (pull_request) Successful in 31s
PR Checks / bot-tests (pull_request) Successful in 31s
PR Checks / server-tests (pull_request) Failing after 9m1s
8453762e3b
Replaces the two raw JSON boxes Phase 3 shipped as explicit placeholders: a
step's params are a form rendered from the action's own declaration, and a
phase's advance condition is the engagement condition builder. Adds the live cap
meter, the searchable option source's first consumer, and a start dialog
carrying the three fields the route has taken since Phase 10.

One route: POST /admin/events/price, admin+editor. A module's cost() runs on the
server and only there, so a meter has nothing to add up until something asks --
and the dry run is the wrong thing to ask on a debounce twice over: it dispatches
every step through the module and a pass against a version is RECORDED, which is
the stamp K's unattended-start gate reads. This dispatches nothing and records
nothing, and takes the spec in the body because the plan being priced is unsaved
between keystrokes.

A form gives way to JSON on the condition builder's own rule: a value the editor
cannot round-trip is SHOWN rather than silently rewritten. Dropping a param the
action does not declare and flattening `A and (B or C)` are the same mistake.

Two defects fixed in already-merged code:

  * Creating an event has been impossible since Phase 6. `events/new` was added
    beside `events/:id` and binds no param, and React Router ranks a static
    segment above a dynamic one whatever the order -- so the editor was handed no
    id and fetched /admin/events/undefined. Worse, the failure was invisible:
    `!form` is true for every failed load, so the error state sat behind a
    spinner that never stopped.
  * 12b's searchable sources had no consumer. The server half shipped and the
    only UI that reads a source never sent a term, so the 6,707-entry spawner
    list was picked from a 2,000-entry truncation with nothing saying so.

Server: 2113 tests, 2024 pass, 0 fail (89 DB-skipped). Client: 380 pass, 0 fail.
routes:manifest and swagger regenerated -- one route added, none moved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
whitlocktech merged commit 6e6c24065c into edge 2026-09-07 22:12:47 +00:00
whitlocktech deleted branch feature/events-p13-authoring-ui 2026-09-07 22:12:49 +00:00
Sign in to join this conversation.
No description provided.