clone

a browser grid puzzle game where you push words around to rewrite the rules of each level

main
miau-is-true / docs / worlds / friction.md
friction.md 48.4K rendered
1# Friction log (creator and player)23## Triage (design, 2026-09-23, after Crate Works and Crossword)45| Theme | Entries | Resolution |6| --- | --- | --- |7| Authoring format | F-CW1, F-XW1, F-CW14 | Engine ask W1: an ASCII `board()` helper with a legend and stable ids in `@miau/engine` |8| Fixed rules and terrain | F-CW2, F-CW3, F-CW6, F-XW2, F-CW15 | Design D23: `fixed` words and `terrain` cells (visuals specified). Engine ask W2 |9| Solver speed and intent | F-CW4, F-CW5, F-XW8, F-XW10, F2 | `--forbid` and `--replay` added to `tools/solve.mjs`. Engine ask W3: a fast `stepState` path and CI adoption |10| Sokoban goal | F-CW10 | Engine ask W4: core `GOAL` property (spec in `docs/worlds/crate-works.md`) |11| Gates and switches | F-CW17 | Engine ask W5: semantics plus 3 drafts in `crate-works.md` |12| Precedence and transforms | F-XW5, F-XW6, F-XW4, F-XW9 | Design D23: Baba precedence. Engine ask W6: implement it, fix AND silently dropping a noun, make transforms deterministic |13| Validation and metadata | F-CW8, F-CW9, F-XW11, F-XW7, F-CW16 | Engine ask W7 |14| Presentation | F-CW7, F-CW11, F-CW12, F-CW13, F-XW3, F-XW12, F-XW13 | Design D23 (all except F-CW13, which inspect mode covers, and F-XW12, which ledger hover covers) |15| Tidewater engine | F-TW1, F-TW2, F-TW3, F-TW5, F-TW10 | Engine asks W8 (rules not re-parsed after SINK: **fixed in `c3537ca`, and the design re-ran the repro and saw no phantom rule**), W9 (FLOAT), W10 (loader warnings), W3 (solver) |16| Tidewater design | F-TW4, F-TW6, F-TW7, F-TW8, F-TW9 | D23 updates: crumble to a leftover noun, glyph `fallback`, no hazard pre-warning (by design). F-TW4 is fixed by the engine's facing change. F-TW8 is covered by transform-before-contact sequencing in the juice |17| Resolved by W6 (Baba precedence and stacked grammar) | F-XW4, F-XW5 (a to c), F-XW6 (a) | **Resolved.** Engine W6 landed, and crossword was reworked against it (all 7 solve, 6 with a proven `mustForm`) |18| Crossword W6 rework | F-XW14, F-XW15, F-XW16 | F-XW16 → engine ask W14 (a rules-semantics version plus a load warning; CI already solves packs with `mustForm`). F-XW14 and F-XW15 → W3 follow-ups (structured `mustForm` patterns; solver capacity shaping level size) |19| Existing packs bypassable | F1 | **Resolved in plan.** The engine bundles the four worlds in their place, and `garden-basics` and `word-lab` leave the default INDEX (they stay importable) |202122Append-only. Design triages each entry into the worlds inbox (engine asks) or23design.md (design changes).2425```md26### F<n>: <short title>27- from: <world or person>28- side: creator | player29- severity: blocker | high | medium | low30- owner (suggested): engine | design | content31- status: open3233What happened, with the exact file, level, or command.34**Proposed fix:** …35```3637---3839### F1: Existing bundled packs are bypassable40- from: design (solver pass)41- side: player42- severity: high43- owner (suggested): content44- status: open4546`node tools/solve.mjs` on the shipped mods: every level in `garden-basics`,47`word-lab`, and `moon-cat` is winnable without moving a word (BYPASS), and48`shared-control` and `moon-cat-intro` are TRIVIAL (3 and 5 steps). Core49`two-things-can-win` is also a BYPASS (6 steps, `RRRRRR`). The packs don't yet50teach the rule system they're named after.51**Proposed fix:** rework these packs with the solver (content), and add the52solver to CI as an authoring check (engine): BYPASS becomes a warning unless53the level declares `allowBypass: true`.5455### F2: No solver or bypass check for authors56- from: design57- side: creator58- severity: high59- owner (suggested): engine60- status: open (a stopgap is in `tools/solve.mjs`)6162A creator can't tell whether a level is solvable, or skippable, without63playing it exhaustively. **Proposed fix:** adopt `tools/solve.mjs`, or an64equivalent, as a supported `npm run solve -- <pack>`, and surface its result65in the Workshop as "verified" once the level editor exists.6667### F-XW1: Coordinates by hand; no grid-based level helper68- from: crossword (world-builder)69- side: creator70- severity: medium71- owner (suggested): engine72- status: open7374`@miau/engine` only offers `word(text, x, y)` and `object(noun, x, y)`, so75a 12 by 8 board means counting 30 or more coordinates by hand. It's easy to76put a word one cell off and silently break a crossing. For77`mods/worlds/crossword.cjs` I wrote a 20-line `grid(id, rows, meta)` helper78inside the pack. Rows are whitespace-separated cells: `.` for empty, `m r f w`79for things, uppercase for words, and `A+B` for stacks. It also rejects ragged80rows. Every other world will write its own version.81**Proposed fix:** add `grid(rows, { legend, id })` to `@miau/engine`, using82the same notation (stacks with `+`, ragged rows rejected, ids derived from83`<level>-<token>-<x>-<y>` so they stay stable).8485### F-XW2: Sentences can only be pinned by geometry, and players can't see it86- from: crossword87- side: creator and player88- severity: high89- owner (suggested): design (presentation), engine (optional primitive)90- status: open9192Every "given" sentence in Crossword (for example `MIAU IS YOU` in93`company`, `ROCK IS DEFEAT` in `shove-the-danger`, `FLAG IS WIN` in94`pass-it-on`) is made unbreakable by placing it in the top or bottom row95flush against a side edge. That's the only way to get an unbreakable rule96without custom rules. It has two costs. (1) Creators must know the trick,97and it also traps words: an `IS` pushed under a pinned word in an edge column98can never come out, which silently softlocked my first draft of99`spend-it-twice`. (2) Players can't tell a pinned sentence from a loose one100until they try to push it. My `company` hint has to say "stuck to the edge".101**Proposed fix:** design: draw an edge-pinned or immovable word with a102distinct cue (for example, a flat lip on the side it's pinned against) and103show a short shake plus the blocked sound when a push fails at a board edge.104Engine: an optional `fixed: true` on word records, rendered with the same105cue, so authors don't have to rely on corners.106107### F-XW3: Stacked words are hard to read and draw two bars108- from: crossword (`carry-the-pair`)109- side: player110- severity: high111- owner (suggested): design (with engine for spans)112- status: open113114`carry-the-pair` hinges on noticing that `WIN` and `DEFEAT` share one115cell and can't be separated. Design's stack treatment (a ±4° fan and 9%116offset, with a `×N` badge only for 3+ items) is subtle at the 22px minimum117cell. The board never says "these two move together". Also,118`state.sentences` returns two same-axis spans of equal length for119`FLAG IS WIN+DEFEAT` (`FLAG IS WIN` and `FLAG IS DEFEAT`). Neither contains120the other, so the renderer draws two bars over the same cells.121**Proposed fix:** design: always badge word stacks (`×2`) and fan them wider122when a stack sits in a sentence. The ledger should show a stacked predicate123as a single `WIN+DEFEAT` chip. Engine: merge spans that cover the same cells124into one span with `stackedIds`.125126### F-XW4: AND is greedy and never backtracks, so stacked connectives read inconsistently127- from: crossword (parser probing)128- side: creator129- severity: medium130- owner (suggested): engine131- status: open132133`src/engine.js` `parseRules` consumes `AND noun` pairs before it looks for134`IS`, and it never retries. Repro (text grid): `ROCK AND+IS FLAG` gives **no135rule**, but `ROCK AND+IS WIN` gives `ROCK IS WIN`. `MIAU AND+ROCK IS YOU`136gives only `ROCK IS YOU`, so `MIAU` is dropped. I avoided building on this137because it looks accidental.138**Proposed fix:** decide the rule. Either the parser tries every reading of139a stacked cell (backtracking), or authoring validation warns when a stack140mixes a connective with a noun or with `IS`. Document the choice in141`docs/modding.md`.142143### F-XW5: Rule precedence differs from the reference game and isn't documented144- from: crossword145- side: creator and player146- severity: medium147- owner (suggested): engine (document), design (decide)148- status: open149150Found while building pure-grammar levels:151(a) `ROCK IS STOP AND PUSH` is a wall, because `planMove` checks STOP before152PUSH. In the reference game, PUSH wins.153(b) `MIAU IS YOU AND DEFEAT` doesn't destroy the cat, and `MIAU IS YOU AND154WIN+DEFEAT` wins, because contacts only resolve in cells with 2 or more155occupants. In the reference game, the carrier is destroyed.156(c) `ROCK IS ROCK` doesn't stop `ROCK IS FLAG`. In the reference game, `X IS157X` blocks transformation.158`shove-the-danger` (the STOP decoy) and `carry-the-pair` (the self-win with159DEFEAT) depend on (a) and (b). If the engine changes, those two levels change.160**Proposed fix:** publish a precedence table in `docs/modding.md` (STOP vs161PUSH, self-contact, `X IS X`), and mark each behaviour intentional or a bug.162Worlds can then cite it.163164### F-XW6: Transformation conflicts and chains are invisible and order-dependent165- from: crossword166- side: creator and player167- severity: medium168- owner (suggested): engine169- status: open170171(a) With `ROCK IS FLAG` and `ROCK IS WALL` both active, the winner is172whichever word comes first in the level's `objects` array. Swapping two rows173of an otherwise identical level swaps the result. (b) With `ROCK IS FLAG` and174`FLAG IS WALL`, a rock is a FLAG after reset and becomes a WALL on the next175accepted move, one link per turn. The level loads in an intermediate state.176(c) `MIAU IS YOU AND ROCK` turns the cat into a rock at reset, so the level177starts with nothing as YOU even though the board says `MIAU IS YOU`.178**Proposed fix:** resolve transformations to a fixed point each turn, with179cycle detection. Pick conflicts deterministically in reading order (top to180bottom, left to right), not object order. Have validation warn when a level181starts with no YOU.182183### F-XW7: The default noun glyphs are hidden in src/main.js184- from: crossword185- side: creator186- severity: medium187- owner (suggested): engine188- status: open189190The brief for this world was "use only nouns with default emoji". The only191list is a `glyphs` object literal on line 67 of `src/main.js` (MIAU, CAT,192BABA, FLAG, ROCK, WALL, WATER, GRASS, STAR, KEY, DOOR, KEKE, THORN, GEM). It193isn't exported, documented, or reflected in `vocabulary`, and nothing tells194an author that a noun without a palette entry falls back to a text label.195**Proposed fix:** move the table to a shared module (for example196`src/glyphs.js`), export it, list it in `docs/modding.md`, and have197`loadMod` warn (not fail) when a noun has neither a palette entry nor a198default glyph.199200### F-XW8: Solver capacity limits challenge design201- from: crossword (`spend-it-twice`)202- side: creator203- severity: high204- owner (suggested): engine205- status: open206207`tools/solve.mjs` builds a fresh `createGame` (with structured clones) for208every expansion. A 12 by 9 draft of `spend-it-twice` with three loose words209hit `LIMIT after 2000001 states` in 2m39s. I had to shrink it to 9 by 7 to210verify it (35 steps, 41s). A full pass over this 7-level pack at `--max2112000000` takes about 2m45s. So the solver, not the design, decided the size of212the world's hardest level.213**Proposed fix:** step a single mutable state with an undo stack instead of214`createGame` per node, use a compact integer state key, and add an optional215A* mode (for example, distance of loose words to target cells) or a216push-level macro search for long levels.217218### F-XW9: Standing on a thing and then writing its rule wins219- from: crossword (`carry-the-pair` first draft)220- side: creator221- severity: medium222- owner (suggested): design (decide), engine (solver flag)223- status: open224225The first draft had `FLAG IS WIN+STOP`. The solver broke the sentence, walked226onto the flag, and rebuilt `FLAG IS WIN+STOP` vertically under the cat. That227won in 16 steps, because STOP never ejects an occupant. It's a legitimate228reference-game trick, but it defeats any "the goal is STOP" framing, and229BYPASS doesn't catch it (words did move). I fixed it by using DEFEAT230instead.231**Proposed fix:** have the solver print the rules active at the winning step232and flag `WON-WHILE-OVERLAPPING` when the win came from a rule formed while233YOU already shared the cell, so authors can decide.234235### F-XW10: The solver shows one route and can't check the intended one236- from: crossword (`down-to-miau`)237- side: creator238- severity: medium239- owner (suggested): engine240- status: open241242`down-to-miau` was meant to be solved with `ROCK IS MIAU` (a243transformation). The shortest route was `ROCK / IS / YOU` down onto the244shared YOU, and I only found that by writing a replay script245(`/tmp/xw/replay.mjs`) to print the rules added on each step. I then added a246wall (a black square) to block that column.247**Proposed fix:** add solver flags `--explain` (print rules added or removed248along the path, and the board at the end), `--require "ROCK IS MIAU"` (the249route must form this rule), and `--forbid "ROCK IS YOU"` (report whether a250route avoiding it exists).251252### F-XW11: Validation ignores world metadata and the quality-bar constraints253- from: crossword254- side: creator255- severity: low256- owner (suggested): engine257- status: open258259`loadMod` accepts `manifest.world` (any shape), `category` (any value),260`introduces` and `reuses` (anything), and boards up to 128 by 128. The worlds261README requires boards of at most 14 by 10 and `category` in `required |262optional | challenge`, but a typo such as `category: 'challange'` or a26315-wide board passes silently.264**Proposed fix:** validate `manifest.world` (`glyph` of at most 16 chars,265`tagline` of at most 80 chars, `accent` in the six property tokens) and266`category`. Emit non-fatal warnings for boards over 14 by 10 and for267`introduces` that isn't a string array.268269### F-XW12: With crossing words, the ledger doesn't show which sentence a word is in270- from: crossword (`across-and-down`, `pass-it-on`)271- side: player272- severity: medium273- owner (suggested): design274- status: open275276In a crossing, the shared `IS` is fully opaque as long as either sentence is277active. If the player breaks the vertical sentence, the `IS` still looks278active, and only the dimmed `FLAG` shows that anything changed. In a chain279like `ROCK IS FLAG IS WIN`, the middle `FLAG` belongs to two rules. The280ledger lists rules but doesn't point back to their tiles or axis.281**Proposed fix:** hovering or focusing a ledger rule highlights its source282tiles (`rule.sourceIds` already exists) and draws its bar brighter. Show a283small axis mark (across or down) on ledger rules that came from a vertical284span.285286### F-XW13: SINK silently deletes words287- from: crossword (parser probing)288- side: player289- severity: low290- owner (suggested): design291- status: open292293A word pushed into a cell with a SINK thing is destroyed along with the294thing (`MIAU IS YOU / ROCK IS SINK / m STOP r`, step right: the `STOP` word295and the rock are gone). In a pure-grammar level, that can remove a word the296solution needs, and the only sign is the missing tile.297**Proposed fix:** show a destroyed word with its own effect (the letters298scattering) and a ledger note like `STOP IS GONE`, so the player knows to299undo.300301### F-CW1: No map format; every level is hand-counted coordinates302- from: Crate Works303- side: creator304- severity: high305- owner (suggested): engine306- status: open307308Seven boards of 40 to 110 entities each would have meant writing hundreds of309`word('IS', 8, 3, 'x-is')` calls and counting columns by hand. Crate Works310defines its own `board(rows, meta)` helper at the top of311`mods/worlds/crate-works.cjs`: one character per cell, a legend (`#` WALL,312`$` CRATE, `~` GAP, uppercase letters for words), and row-width checks with313readable errors ("row 2 is 11 wide, expected 13"). That's about 25 lines314every pack author will rewrite.315**Proposed fix:** add `map(rows, legend, meta)` to `@miau/engine`. Legend316values are `'WORD'`, `{ noun }`, or an array for stacks. Include row-width and317unknown-character errors that name the level, column, and row, and a318documented default legend so packs and bug reports read the same way.319320### F-CW2: Rule words can't be fixed, so every level spends rows sealing an "office"321- from: Crate Works322- side: creator323- severity: high324- owner (suggested): engine325- status: open326327Every rule a level depends on (`WALL IS STOP`, `GAP IS SINK`, `TRUCK IS WIN`)328can be broken by the player unless it's physically sealed away. The only329tools are a full wall row, which needs `WALL IS STOP` and so is circular, or330packing row 0 so the board edge stops upward pushes. Two of my drafts fell to331this. `load-order` v1 was solved in 15 by pushing `STOP` up out of332`WALL IS STOP` from the room below, which made the walls passable. That's a333word move, so the solver didn't flag BYPASS. `spend-it-last` v1 was solved by334sinking a word into a row-0 gap and then pushing `GAP IS SINK` sideways.335**Proposed fix:** add a per-word `fixed: true` flag. The engine treats it as336immovable and the renderer draws it bolted: no lip, a flat tile, a small rivet337in the gutter. Also add a documented "sealed rules" slot outside the board for338rules that should never be editable (a ledger-only rule).339340### F-CW3: Solid terrain requires a breakable rule341- from: Crate Works342- side: creator343- severity: medium344- owner (suggested): engine345- status: open346347A `WALL` does nothing unless `WALL IS STOP` is on the board, so every level348pays three tiles plus sealing (F-CW2) just to have walls. Players also have to349read that sentence in every level even though it is never the puzzle.350**Proposed fix:** allow level-declared terrain nouns that are `STOP`351implicitly: `terrain: ['WALL']`. They're shown in the ledger as a greyed352built-in rule and can still be overridden by a written rule if a level wants353that.354355### F-CW4: tools/solve.mjs is too slow and runs out of memory on normal board sizes356- from: Crate Works357- side: creator358- severity: high359- owner (suggested): engine360- status: open361362The solver builds a new `createGame` and runs `structuredClone` for every363node, about 25k states/s. `spend-it-last` (14×5) needed `--max 2000000` and36470 s. A 13×9 room with four loose words hit the limit, and my faster365re-implementation ran out of memory at 4 GB storing full states in the BFS366frontier. I had to write a compact simulator (`/tmp/cw/sim.mjs`, about 30×367faster, differentially tested against `src/engine.js` with random walks) to368iterate at all.369**Proposed fix:** give the engine a `stepState(compactState, dir)` fast path370with no events and no clone. Store parent pointers instead of path strings.371Hash with typed arrays. Add `--replay <path>` to print frames, because I also372had to write that.373374### F-CW5: The solver can't say whether the *intended* rule is needed375- from: Crate Works376- side: creator377- severity: medium378- owner (suggested): engine379- status: open380381BYPASS only catches "no word moved". Unintended rule edits pass silently. The382first `crates-move` was won with `CRATE IS WIN` (WIN pushed onto383`CRATE IS PUSH`) instead of `TRUCK IS WIN`, and `load-order` v1 was won by384breaking `WALL IS STOP`. I added `--forbid "RULE"` to my own solver to prove385that `GAP IS CRATE` and `CRATE IS YOU` are necessary.386**Proposed fix:** add `--forbid "X IS Y"` and `--require "X IS Y"` to387`tools/solve.mjs`, and optional level metadata `intended: { rules: [...] }`388that the checker verifies (unsolvable without them). Also print the rule set389at the moment of winning, which exposes most accidental shortcuts at a glance.390391### F-CW6: Mods can't declare their own terrain slab; the WALL palette glyph is silently ignored392- from: Crate Works393- side: creator394- severity: medium395- owner (suggested): engine396- status: open397398`src/render/board.js` draws a slab only for nouns in `options.wallNouns`,399which defaults to `['WALL']`, and nothing passes that option. My palette entry400`WALL: '🧱'` is ignored without a warning, and a themed noun like `SHELF` or401`RACK` would render as a small emoji instead of terrain.402**Proposed fix:** a palette flag `{ glyph, terrain: true }`, or403`manifest.terrain: ['SHELF']`, passed through to `wallNouns`. The loader404should warn when a palette entry will never be drawn.405406### F-CW7: A wall that isn't STOP still looks like solid terrain407- from: Crate Works408- side: player409- severity: medium410- owner (suggested): design411- status: open412413The slab rendering depends on the noun, not the rules. If `WALL IS STOP` is414broken, or a level makes walls `PUSH`, the board still shows solid racking,415and the player learns that walls are passable only by bumping into them.416Crate Works avoids this by always sealing `WALL IS STOP` (F-CW2), but other417worlds will hit it.418**Proposed fix:** draw terrain slabs only while the noun is `STOP`. Otherwise419draw a hatched outline (passable) or a slab with a lip (pushable).420421### F-CW8: manifest.world is accepted with no validation422- from: Crate Works423- side: creator424- severity: low425- owner (suggested): engine426- status: open427428I added `world: { glyph, tagline, accent }` per D20. `loadMod` accepts any429shape there (a typo such as `accent: 'pushh'` or an 200-character tagline430passes), so mistakes would only show up at render time, if at all.431**Proposed fix:** validate `world.glyph` (1 to 16 characters), `tagline`432(80 characters or fewer, no U+00B7), and `accent` (one of433`you|push|stop|win|sink|defeat`) in `validateMod`, with the same readable434error lines as other fields.435436### F-CW9: Level metadata (category, introduces, reuses) has no schema437- from: Crate Works438- side: creator439- severity: low440- owner (suggested): design441- status: open442443The README requires `category` and `introduces`, but nothing validates them,444and there's no convention for lessons that aren't a single token. I used445strings like `'sinking words'`, `'shared YOU'`, and `'noun transform'`446alongside tokens like `'GAP'`. A tool that builds a curriculum view can't447tell these apart.448**Proposed fix:** validate `category` against `required|optional|challenge`.449Split `introduces` into `words: [...]` (tokens, checked against the level)450and `ideas: [...]` (free text). Document `reuses`.451452### F-CW10: No way to express a real Sokoban goal ("every crate on a dock")453- from: Crate Works454- side: player455- severity: high456- owner (suggested): engine457- status: open458459WIN only fires when a YOU shares a cell with a WIN thing, so multi-box levels460can't have a goal that depends on where the boxes are. Crate Works works461around it (holes that eat boxes, pushing a `PUSH AND WIN` truck onto a parked462YOU, crate-as-YOU), but the genre's core satisfaction, the last box clicking463home, isn't available.464**Proposed fix:** a core property `GOAL` (`DOCK IS GOAL`). The level is won at465end of turn when a YOU exists, at least one GOAL thing exists, and every GOAL466thing shares its cell with another `PUSH` entity (words count). Full spec, and467two prototype-verified draft levels, are in `docs/worlds/crate-works.md`468under "Goal feature spec".469470### F-CW11: A sunk word takes its rule away with no announcement471- from: Crate Works472- side: player473- severity: medium474- owner (suggested): design475- status: open476477In `spend-it-last`, `load-order`, and `night-shift`, the key event is a word478entering a gap: the word, the gap, and possibly a rule vanish in one turn. The479ledger just loses a line. A player who sank `PUSH` too early doesn't learn why480the crate stopped moving.481**Proposed fix:** when a destroyed entity is a word that was in an active482sentence, show a short float of the lost rule, struck through483(`CRATE IS PUSH` with a line through it), and play a distinct sink sound for484words. The `destroyed` event already carries `text`.485486### F-CW12: Lockstep YOUs give no feedback about who was blocked487- from: Crate Works488- side: player489- severity: medium490- owner (suggested): design491- status: open492493`in-step` and `back-it-up` depend on one YOU being held still by a wall while494the other moves. Right now the blocked YOU just doesn't move. That's the most495important information in the level, and it's shown by nothing happening.496**Proposed fix:** a per-entity bump for every YOU whose move was refused, even497when another YOU moved and the turn was accepted. The engine returns only the498first `blocked`, so `settled()` should return `blocked: []` for every refused499YOU.500501### F-CW13: Crates without PUSH look exactly like pushable crates502- from: Crate Works503- side: player504- severity: medium505- owner (suggested): design506- status: open507508In `night-shift`, `GAP IS CRATE` turns 13 gaps into crates at once. Because509`CRATE IS PUSH` was just dismantled, they're walk-through decoration, but they510look identical to the pushable crate from level 1, so the solution looks511impossible until the player walks into one. The mass transform also needs a512strong cue.513**Proposed fix:** give things with no active properties a subtle "inert"514treatment (for example 70% opacity or no ground shadow). When a transform515changes more than about 4 entities at once, use a staggered pop.516517### F-CW14: Position-derived object ids change when the map is edited518- from: Crate Works519- side: creator520- severity: low521- owner (suggested): engine522- status: open523524To avoid id boilerplate, my `board()` helper derives ids from the level id525and starting cell (`night-shift-3-4`). Moving one tile renames it. That's526harmless today, but anything keyed by entity id (replays, the facing map,527future gate state) would silently break across pack versions.528**Proposed fix:** document that replays are move lists only, and state which529features, if any, persist entity ids. If some do, have the map helper assign530ids by reading order per noun (`crate-1`, `crate-2`), which survives most531edits.532533### F-CW15: 14×10, sealed rule rows, and blank-row spacing compete for space534- from: Crate Works535- side: creator536- severity: low537- owner (suggested): design538- status: open539540A sealed rule office costs 2 rows (words and walls), or 4 with the "blank row541between stacked sentences" rule. On a 10-row board that leaves 6 rows for the542puzzle. `load-order` ends up with two rule rows separated only by a wall row,543and `spend-it-last` packs four sentences into one row with single-wall544separators.545**Proposed fix:** a sealed-rules strip outside the board (see F-CW2), or546count sealed rows separately from the 14×10 limit.547548### F-CW16: The engine changed under a live authoring session549- from: Crate Works550- side: creator551- severity: low552- owner (suggested): engine553- status: open554555Partway through, `src/engine.js` gained uncommitted automata support and the556solver was updated. Core semantics didn't change, but I only found that out by557diffing and re-running a differential test.558**Proposed fix:** keep a short `ENGINE-CHANGES.md` or version note that says559whether core turn resolution changed. Have the solver print the engine560version it ran against.561562### F-CW17: The engine snapshot has no per-entity mechanism state (gates and switches)563- from: Crate Works564- side: creator565- severity: medium566- owner (suggested): engine567- status: open568569The gates-and-switches proposal needs a gate to remember `open` (a jam depends570on history), and object records need a `channel`. Today `reset()` rebuilds571objects from a fixed set of fields, so extra fields are dropped, and572`object()` has no options argument. Proposed semantics, and three573prototype-verified scenarios, are in `docs/worlds/crate-works.md` under574"Gates and switches (proposal)".575**Proposed fix:** let object records carry validated extra fields576(`channel`), keep `open` in `state.objects` so undo and share just work, and577include it in the solver hash.578579### F-CK1: An undeclared property word silently becomes a noun and transforms things580- from: Clockwork581- side: creator582- severity: blocker583- owner (suggested): engine584- status: open585586The parser treats any word that isn't `IS`, `AND`, or a known property as a587noun. On the shipped engine, the draft level `key-and-door`588(`/tmp/clockwork-proto/clockwork-pairs.cjs`) contains `DOOR IS SHUT`. It loads589without a warning, and at reset every door turns into a `SHUT` thing with no590glyph. The door stops being `STOP`, so the lock is gone. My first scratch test591did the same with `GEAR IS MOVE`: every gear became a `MOVE` thing. A creator592who misspells a property, or uses one from a newer build, gets a level that593quietly plays wrong. `validateLevel` also accepts `{ noun: 'TICK' }` in a level594that declares `TICK` as an automaton property.595**Proposed fix:** at load time, warn about any predicate word that isn't a596known or declared property and isn't a noun used by the pack's objects,597palette, or vocabulary. The warning should say something like "SHUT isn't a598thing in this pack. Did you mean a property?" Also reject an object whose noun599is a declared property word. See ask 1 in `docs/worlds/clockwork-api.md`.600601### F-CK2: New level features are silently ignored by builds that don't know them602- from: Clockwork603- side: creator604- severity: high605- owner (suggested): engine606- status: open607608`level.automata` shipped without a schema bump, and `mods.js` passes unknown609level keys through. Take a pack that uses `automata`, or the proposed610`pairs`, or even a typo like `automaton`. It loads on a build that doesn't611know the field, and its property words turn into nouns (F-CK1). The only612version gate, `manifest.engine`, is still `1`.613**Proposed fix:** add `manifest.features: ['automata', 'pairs']`. The loader614rejects any feature it doesn't know with the player line "This pack needs a615newer Miau." Validation also warns about unknown level keys. See ask 8 in616clockwork-api.md.617618### F-CK3: A player has no way to learn what a custom property does619- from: Clockwork620- side: player621- severity: high622- owner (suggested): design623- status: open624625`TICK` is a word no player has seen before. The stage draws its tile and a626next-step arrow on each actor, but nothing says "this thing repeats its627steps, one for each of your turns". Clockwork puts a one-sentence628explanation in each level hint, but hints nudge. They shouldn't have to629define vocabulary.630**Proposed fix (design):** the first time a level shows an unfamiliar property631tile, show a small card: the tile, its one-line `describe`, and a short demo632of the pattern. After that, the line shows under the ledger entry.633**Proposed fix (engine):** accept a pack-level glossary,634`properties: { TICK: { accent, describe } }`, where `accent` is a property635colour token and `describe` is at most 80 characters. Clockwork already636declares it, and today's loader ignores it.637638### F-CK4: The renderer hard-codes the custom property list639- from: Clockwork640- side: creator641- severity: medium642- owner (suggested): engine643- status: open644645`src/render/board.js` hard-codes its own `PROPERTIES` set, which now includes646`TICK`, and colours cues with `colors.TICK`. The engine takes property words647from `level.automata[].property`, but the renderer doesn't. A pack that names648its property `WIND` or `SPIN` gets a noun-shaped tile with no chamfer and no649cue colour, so it reads as a noun.650**Proposed fix:** derive the property set and cue colour from the level's651declared properties and the glossary `accent` (F-CK3), and remove the652hard-coded word.653654### F-CK5: Players see only the next beat, and nothing at all for a paused clock655- from: Clockwork656- side: player657- severity: medium658- owner (suggested): design659- status: open660661`drawAutomataIndicators` draws one arrow for the next step, and only while the662rule is active. In `wind-it-up` the player can't preview what the mouse will663do before winding it. In `escapement` (29 steps) the plan depends on where the664mouse will be 4 to 6 beats from now. Two facts are also invisible: a paused665clock keeps its phase, and handing `TICK` to another noun keeps that phase666too.667**Proposed fix:** add a pattern strip for each automaton (in the ledger or a668HUD row) that shows every step as a small arrow or pause mark, highlights669the current beat, and stays visible, dimmed, while paused. Keep the on-sprite670arrow for the next beat only.671672### F-CK6: A moving hazard can't guard anything, because contacts only run at end of turn673- from: Clockwork674- side: creator675- severity: medium676- owner (suggested): design677- status: open678679Contacts resolve once, after the automata beat. A cat can step into a `DEFEAT`680cog's cell on the turn the cog moves away, swap cells with it head-on, or let a681`STOP` patroller walk onto it and then leave. Every patrol-guard level I drafted682(`stop-the-clock` in three layouts) had a timing bypass or blew up the solver.683The same rule is a feature elsewhere: a thing that is both `YOU` and `TICK`684moves twice and skips the cell in between, which is what `double-time` uses to685cross a cog line.686**Proposed fix:** document the "end-of-turn contact" rule and its two687consequences (swap-through, and double moves skipping cells) in688`docs/modding.md` next to the automata schema. If guards matter later, the689engine could add an opt-in `contact: 'each-move'` per automaton. That needs a690spec, so don't add it silently.691692### F-CK7: The automata spec, the loader, and the engine disagree693- from: Clockwork694- side: creator695- severity: medium696- owner (suggested): engine697- status: open698699Three mismatches:700- `openspec/changes/add-rule-driven-automata/specs/.../spec.md` describes one701 automaton with step IDs and property additions and removals. The code702 implements many automata with `move` and `wait` steps.703- The engine accepts `'U'`/`'W'` shorthand steps, but `mods.js` rejects them.704- The engine's `MAX_AUTOMATA` is 32, and so is the loader's, but only the705 loader rejects a `phase` beyond the pattern.706707I authored against the code, and I only knew which one was true by reading708both files.709**Proposed fix:** update the spec to the shipped model, and have the loader710and engine share one validator, or at least one table of limits and accepted711forms.712713### F-CK8: A function anywhere in a pack gives an unhelpful load error714- from: Clockwork715- side: creator716- severity: low717- owner (suggested): engine718- status: open719720My first behaviour-API draft exported721`properties: { MOVE: { run(view, self) { ... } } }`. `loadMod` fails with722`run(){} could not be cloned.`, because `normalizeMod` runs `structuredClone`723over the export. Packs are data-only by design (URL links and the pack library724store no code), but the message doesn't say so.725**Proposed fix:** before cloning, walk the export for functions and report726"levels[2].automata[0].pattern: packs are data only; functions aren't727supported" with a path.728729### F-CK9: BYPASS counts automaton pushes as the player's work730- from: Clockwork731- side: creator732- severity: medium733- owner (suggested): engine734- status: open735736`tools/solve.mjs` flags BYPASS only when no word moves at all. On a level where737a mouse pushes the `WIN` word, a player who just walks and waits is never738flagged. Yet "the player never touched a word" is exactly the skip an author739wants to know about. `mouse-keeps-time` is hands-off on purpose, and nothing in740the tool output says so. My prototype verifier741(`/tmp/clockwork-proto/verify.mjs`) adds a **HANDS-OFF** flag: won without the742player's own push chain moving a word (moves tagged `cause: 'automaton'` are743ignored). Speed is the same problem as F-CW4. `mouse-hole` and `escapement`744return LIMIT at the default `--max 300000` and need 3 to 4 million expansions,745about 3 minutes each.746**Proposed fix:** add HANDS-OFF to `tools/solve.mjs` for levels with automata,747and let a level declare `allowHandsOff: true`. For speed, see F-CW4.748749### F-TW1: A sunk word's rule stays active for the next move (ghost rule)750- from: Tidewater751- side: creator and player752- severity: high753- owner (suggested): engine754- status: open755756`step()` parses rules, transforms, and then resolves contacts, but it never757re-parses rules after contacts remove things. If a sentence is still whole on758the turn one of its words sinks (for example, pushing `ROCK IS STOP` right759until `STOP` enters water), `state.rules` still contains `ROCK IS STOP`. The760next move into a rock is rejected with reason `stop`. A rejected move doesn't761refresh rules either, so the ghost lasts until some other move is accepted.762The ledger shows a rule whose word is gone. Repro: `/tmp/tw/repro.mjs`, also763in `docs/worlds/tidewater.md`, "Surprising engine behaviour" 1. No Tidewater764level depends on this. My search port had to copy the stale behaviour to765match the engine.766**Proposed fix:** call `refreshRules()` (and `transform()`) again after767`resolveContacts()` when anything was removed. Add a regression test with the768repro. Decide whether the post-removal transform also applies in the same769turn. I suggest yes, to reach a fixed point.770771### F-TW2: "Become a boat to cross the water" can't be expressed772- from: Tidewater773- side: creator774- severity: medium775- owner (suggested): design (decide), engine776- status: open777778The world brief asked for "becoming a CRAB or BOAT to cross what MIAU779cannot". In the core engine, SINK removes every occupant of the cell whatever780its noun, so a BOAT sinks exactly like a cat. DEFEAT is the only hazard that781depends on identity (it only removes YOU things), and nothing grants immunity.782Identity puzzles therefore reduce to "become whoever is already on the other783side" (`sideways`), or to changing the hazard itself (`shell-game`,784`last-boat-out`).785**Proposed fix:** add `FLOAT` as the next core property: a FLOAT thing and a786SINK thing in one cell don't interact, as in the reference game. It's the787smallest addition that makes vehicles, bridges, and "moored" objects788authorable. Until then, document in `docs/modding.md` that SINK ignores789identity.790791### F-TW3: Anything placed on water vanishes at reset, silently792- from: Tidewater793- side: creator794- severity: medium795- owner (suggested): engine796- status: open797798`reset()` resolves contacts, so a boat moored on a `WATER IS SINK` cell, or a799jelly floating in the sea, is destroyed before the first frame. The level800loads as if the author forgot the object. Nothing reports it: not `loadMod`,801not the solver. This also blocks design's crumbling-tile default ("place WATER802under it"). See "Crumbling tiles (proposal)" in `docs/worlds/tidewater.md`.803**Proposed fix:** have validation (or the solver) warn "N things are804destroyed at reset (BOAT at column 4, row 2 sinks)". Longer term, FLOAT805(F-TW2) makes these stacks legal.806807### F-TW4: A palette entry for MIAU silently turns off the cat's facing808- from: Tidewater809- side: creator and player810- severity: medium811- owner (suggested): engine812- status: fixed in the working tree (`src/render/facing.js` now inherits built-in facing when the glyph matches); kept for the record813814`src/render/board.js` only applies the built-in `DIRECTIONAL_NOUNS` facing815when a pack has **no** palette entry for the noun. Every example pack816(`word-lab`, `garden-basics`) writes `MIAU: '🐈'` as a plain string, so in817those packs the cat never turns around, and nothing warns about it. I only818found this by reading the renderer.819**Proposed fix:** when a pack's plain-string glyph is identical to the820built-in glyph for that noun, inherit the built-in `faces`. Update the821example packs to `{ glyph: '🐈', faces: 'left' }`, and say so in822`docs/modding.md`.823824### F-TW5: A syntax error in a pack gives no line number825- from: Tidewater826- side: creator827- severity: low828- owner (suggested): engine829- status: open830831An apostrophe in a single-quoted hint (`'You can't reach...'`) produced832`Could not load tidewater.cjs: Unexpected identifier 't'`. There was no line833or column, because the source runs through `new Function`, and the834`sourceURL` comment doesn't surface in the message. With a pack made of text835maps, finding it meant bisecting by hand. An apostrophe in a hint is also the836most likely mistake a writer will make.837**Proposed fix:** before `new Function`, parse the source with a cheap838syntax check that reports line and column, or catch the `SyntaxError` and839read the line from `error.stack`, adjusting for the 2 prepended lines. In the840Workshop error note, show the offending line.841842### F-TW6: Newer emoji (🪼, 🪸) have no fallback glyph843- from: Tidewater844- side: player845- severity: low846- owner (suggested): design847- status: open848849🪼 (Unicode 15, 2022) and 🪸 (Unicode 14) render well on current Apple fonts,850but show tofu on older Android and Windows 10. The jelly is the hazard in 5851of 7 levels, and an unreadable hazard is a fairness problem. The palette852takes exactly one glyph per noun, so a creator has no way to say "or 🎐 if853that's missing".854**Proposed fix:** allow `palette[NOUN] = { glyph: '🪼', fallback: '🎐' }`,855with the renderer checking glyph support once (canvas `measureText` width856against the tofu width). Or document a "safe emoji" floor (Unicode 13) for857bundled packs.858859### F-TW7: Walking into water is instant and untelegraphed860- from: Tidewater861- side: player862- severity: medium863- owner (suggested): design864- status: open865866SINK resolves in the same step as the move. The first time a player learns867that water is deadly is when the cat disappears, and in `one-for-the-tide`868the sacrifice of a crab reads exactly like a mistake. The world depends on869the player *choosing* what to feed the sea, so the difference between "fed870the sea" and "fell in" matters. Undo recovers it, but the lesson arrives as871a punishment.872**Proposed fix:** (a) a one-frame "about to sink" wobble on a YOU thing whose873next step in the held direction enters a SINK or DEFEAT cell. It's874presentation-only and computed from `properties()`. (b) A distinct sink875effect when a YOU is lost while other YOUs remain ("one for the tide"), not876the same effect as a crate or word sinking.877878### F-TW8: Transformation beats contact, and the player can't see why they survived879- from: Tidewater880- side: player881- severity: low882- owner (suggested): design883- status: open884885In one step you can complete `JELLY IS SHELL` and walk onto a jelly. The886jelly becomes a shell before DEFEAT is checked, so you survive (repro in887`docs/worlds/tidewater.md`, behaviour 2). It's a good moment, but the888transform tween and the move tween play together, so it looks like the cat889walked into a jellyfish and nothing happened.890**Proposed fix:** play the transform pop *before* the move settles (a89140 to 60 ms lead) whenever a moved YOU ends on an entity that transformed892this step, so the order of events is visible.893894### F-TW9: A crumbling tile can't have water "under" it in the current contact model895- from: Tidewater (crumbling tiles proposal)896- side: creator897- severity: medium898- owner (suggested): design (decide), engine899- status: open900901Design's default for crumbling tiles says "a level can place WATER under it902to make the hole deadly". A cell with `DRIFT` and `WATER` under903`WATER IS SINK` is destroyed at reset (F-TW3), so that authoring pattern904can't exist without a floor layer. Full semantics and two draft levels are in905`docs/worlds/tidewater.md`, "Crumbling tiles (proposal)".906**Proposed fix:** a crumble *becomes* a leftover noun907(`leaves: 'WATER'`, keeping the entity id) instead of revealing what's908underneath. Express counts above 1 as a leftover chain (SAND, then SHOAL,909then WATER) rather than a counter, so the state stays in `noun` and undo,910sharing, and the solver hash need no change.911912### F-TW10: Solver capacity decided the size of the finale (+1 to F-CW4 and F-XW8)913- from: Tidewater914- side: creator915- severity: medium916- owner (suggested): engine917- status: open918919The first 12×9 draft of `last-boat-out` ran `tools/solve.mjs --max 2000000`920out of memory (a 4 GB heap crash, not LIMIT). A compact port reached 13921million states without a solution. The shipped 10×7 version needs 261k states922and about 90 s, with a peak RSS of 1.7 GB. A mutable-state port with compact923string keys (`/tmp/tw/eng.mjs`, `/tmp/tw/fs.mjs`, differentially tested with9240 mismatches over 57k random steps) is about 20× faster and supports925"solve without rule X" and "solve without sinking a word". Those constraint926modes are how I proved each level's key edit is necessary.927**Proposed fix:** adopt a compact-key, mutable-state solver and add928`--without-rule "<RULE>"` and `--no-word-loss` constraint flags. They answer929"is my intended idea required?" (F-CW5, F-XW10) directly.930931### F-DS1: shell-game's key rule can't be machine-proven932- from: design (CI setup)933- side: creator934- severity: low935- owner (suggested): content936- status: resolved (Tidewater reworked `shell-game` to 5×7 with pinned sentences; `--forbid 'JELLY IS SHELL'` is UNSOLVED after 623,612 states, and the level declares `mustForm`)937938`tidewater/shell-game --forbid 'JELLY IS SHELL'` hits LIMIT at 3M states. Per939the engine audit, the authored words over-approximate alternative rules, and a940lexical shortcut would be unsound. **Proposed fix:** rework the proof with941fewer loose words and a smaller open area, so the forbid search terminates.942Then add `mustForm: ['JELLY IS SHELL']`.943944### F-XW14: Proving mustForm cost Crossword its decoy words945- from: crossword (W6 rework)946- side: creator947- severity: medium948- owner (suggested): engine949- status: open950951`--forbid` has to prove UNSOLVED exhaustively. Each loose word multiplies952the state space, and words can drift through non-STOP things (a column of953DEFEAT rocks) into the whole board. In `shove-the-danger`, the three loose954words (`AND`, `PUSH`, and a decoy `STOP`) hit LIMIT at the 2M default and955needed about 5M to prove. `pass-it-on` needed 9.5M at 10 by 7. To make the956proofs fit, I removed the decoy and narrowed both boards (7 by 7 and 8 by 7).957The decoy was good design: it made the player choose `PUSH` over `STOP`.958`spend-it-twice` proves at 1,905,020 states, 95% of the default, so it now959declares `solverBudget: 4000000` for margin. A full pack run takes about 6960minutes. This is a sibling of F-XW8, F-TW10, and F-DS1.961**Proposed fix:** make the forbid search prune by word-motion symmetry, or962by a static dead-word analysis (a word in an edge row or column that can't963reach any target cell). Also consider a per-level CI budget report that shows964how close each proof is to its cap.965966### F-XW15: mustForm can't express "some crossing rule"967- from: crossword (`across-and-down`)968- side: creator969- severity: low970- owner (suggested): engine971- status: open972973`across-and-down` teaches "one IS, read across and down". `--forbid 'FLAG IS974WIN'` solves in 26 steps through a different crossing (`FLAG IS YOU` down,975then `MIAU IS WIN` across), so the level's real invariant is "some vertical976sentence shares the IS". `mustForm` only takes exact rule texts, so I declared977nothing, and the level can't be regression-checked.978**Proposed fix:** allow structured requirements such as `mustForm: [{ axis:979'v' }]`, `{ predicate: 'WIN' }`, or `{ sharesWordWith: 'MIAU IS YOU' }`,980checked against `rule.axis` and `rule.sourceIds`, with the same forbid-based981proof.982983### F-XW16: A precedence change silently broke shipped levels984- from: crossword (W6 rework)985- side: creator and player986- severity: high987- owner (suggested): engine988- status: open989990W6 (D23) changed STOP versus PUSH, self-DEFEAT, `X IS X`, transform order, and991stacked parsing. Packs still declare `engine: 1`, and nothing in load or992solve told me my levels changed. I found out only when the design lead993mentioned that `carry-the-pair` now hit LIMIT (its old route killed the cat)994and that `shove-the-danger`'s decoy had changed meaning. A player who had995installed the old pack would find a proof unwinnable with no explanation.996**Proposed fix:** bump the manifest `engine` (or add a `rules` semantic997version) whenever precedence changes. Warn on load when a pack targets older998semantics. Run every bundled and world pack through `tools/solve.mjs` with its999`mustForm` in CI, so a semantics change fails loudly.10001001### F-TW11: A per-id string `leaves` can't express a crumble chain1002- from: Tidewater (FRAIL fixtures)1003- side: creator1004- severity: medium1005- owner (suggested): engine (schema), design (confirm)1006- status: resolved (D25 revised: leftovers are a per-level noun map in `mechanics[frail].leaves`, looked up by current noun; fixtures updated)10071008D25 says `leaves` stays with the entity id and that multi-step crumbling is a1009noun chain. Those two decisions conflict. SAND with `leaves: 'SHOAL'` crumbles1010into a SHOAL that still carries `leaves: 'SHOAL'`, so the second crossing turns1011SHOAL into SHOAL, and the sandbar never runs out. Draft B ("Two tides") can't1012be authored with a string. `docs/worlds/fixtures/frail.json` (`two-tides`)1013uses a proposed map form instead.1014**Resolution (accepted, D25 revised):** leftovers are a **per-level noun1015map only**: `mechanics: [{ id: 'frail', type: 'frail', version: 1, state:1016'derived', leaves: { SAND: 'SHOAL', SHOAL: 'WATER' } }]`. There's no1017per-object `leaves` field and no string form. A crumbling thing becomes1018`leaves[currentNoun]` and keeps its id, and a noun missing from the map is1019removed. Per-object variety (one plank leaves WATER, another nothing) is given1020up in exchange for rules players can read ("sand turns to shoal"); a level1021that needs two behaviours uses two nouns.10221023### F-TW12: A loose FRAIL sentence lets NOUN IS WIN skip the crossing1024- from: Tidewater (FRAIL fixtures)1025- side: creator1026- severity: low1027- owner (suggested): content1028- status: open10291030The first "One plank each" fixture left `DRIFT IS FRAIL` loose, following1031the draft's "break the rule to keep the bridge" alternative. The solver1032immediately swapped `FRAIL` for the loose `WIN` and won by touching a plank1033(`DRIFT IS WIN`, 14 steps). Any FRAIL level with a loose property word and1034walkable frail things has this shortcut. The fixture now pins the sentence in1035a corner.1036**Proposed fix:** a guideline for FRAIL levels: pin the FRAIL sentence, or1037make sure no loose property word can take its slot. Add a solver lint that1038warns when the winning rule's subject is a noun whose only role was FRAIL.