Friction log (creator and player)
Triage (design, 2026-09-23, after Crate Works and Crossword)
| Theme | Entries | Resolution |
|---|---|---|
| Authoring format | F-CW1, F-XW1, F-CW14 | Engine ask W1: an ASCII board() helper with a legend and stable ids in @miau/ |
| 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 |
| Solver speed and intent | F-CW4, F-CW5, F-XW8, F-XW10, F2 | --forbid and --replay added to tools/. Engine ask W3: a fast stepState path and CI adoption |
| Sokoban goal | F-CW10 | Engine ask W4: core GOAL property (spec in docs/) |
| Gates and switches | F-CW17 | Engine ask W5: semantics plus 3 drafts in crate-works.md |
| 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 |
| Validation and metadata | F-CW8, F-CW9, F-XW11, F-XW7, F-CW16 | Engine ask W7 |
| 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) |
| 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) |
| 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 |
| 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) |
| 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) |
| 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) |
Append-only. Design triages each entry into the worlds inbox (engine asks) or design.md (design changes).
### F<n>: <short title>
- from: <world or person>
- side: creator | player
- severity: blocker | high | medium | low
- owner (suggested): engine | design | content
- status: open
What happened, with the exact file, level, or command.
**Proposed fix:** …
F1: Existing bundled packs are bypassable
- from: design (solver pass)
- side: player
- severity: high
- owner (suggested): content
- status: open
node tools/ on the shipped mods: every level in garden-basics,
word-lab, and moon-cat is winnable without moving a word (BYPASS), and
shared-control and moon-cat-intro are TRIVIAL (3 and 5 steps). Core
two-things-can-win is also a BYPASS (6 steps, RRRRRR). The packs don't yet
teach the rule system they're named after.
Proposed fix: rework these packs with the solver (content), and add the
solver to CI as an authoring check (engine): BYPASS becomes a warning unless
the level declares allowBypass: true.
F2: No solver or bypass check for authors
- from: design
- side: creator
- severity: high
- owner (suggested): engine
- status: open (a stopgap is in
tools/)solve.mjs
A creator can't tell whether a level is solvable, or skippable, without
playing it exhaustively. Proposed fix: adopt tools/, or an
equivalent, as a supported npm run solve -- <pack>, and surface its result
in the Workshop as "verified" once the level editor exists.
F-XW1: Coordinates by hand; no grid-based level helper
- from: crossword (world-builder)
- side: creator
- severity: medium
- owner (suggested): engine
- status: open
@miau/ only offers word(text, x, y) and object(noun, x, y), so
a 12 by 8 board means counting 30 or more coordinates by hand. It's easy to
put a word one cell off and silently break a crossing. For
mods/ I wrote a 20-line grid(id, rows, meta) helper
inside the pack. Rows are whitespace-separated cells: . for empty, m r f w
for things, uppercase for words, and A+B for stacks. It also rejects ragged
rows. Every other world will write its own version.
Proposed fix: add grid(rows, { legend, id }) to @miau/, using
the same notation (stacks with +, ragged rows rejected, ids derived from
<level>-<token>-<x>-<y> so they stay stable).
F-XW2: Sentences can only be pinned by geometry, and players can't see it
- from: crossword
- side: creator and player
- severity: high
- owner (suggested): design (presentation), engine (optional primitive)
- status: open
Every "given" sentence in Crossword (for example MIAU IS YOU in
company, ROCK IS DEFEAT in shove-the-danger, FLAG IS WIN in
pass-it-on) is made unbreakable by placing it in the top or bottom row
flush against a side edge. That's the only way to get an unbreakable rule
without custom rules. It has two costs. (1) Creators must know the trick,
and it also traps words: an IS pushed under a pinned word in an edge column
can never come out, which silently softlocked my first draft of
spend-it-twice. (2) Players can't tell a pinned sentence from a loose one
until they try to push it. My company hint has to say "stuck to the edge".
Proposed fix: design: draw an edge-pinned or immovable word with a
distinct cue (for example, a flat lip on the side it's pinned against) and
show a short shake plus the blocked sound when a push fails at a board edge.
Engine: an optional fixed: true on word records, rendered with the same
cue, so authors don't have to rely on corners.
F-XW3: Stacked words are hard to read and draw two bars
- from: crossword (
carry-the-pair) - side: player
- severity: high
- owner (suggested): design (with engine for spans)
- status: open
carry-the-pair hinges on noticing that WIN and DEFEAT share one
cell and can't be separated. Design's stack treatment (a ±4° fan and 9%
offset, with a ×N badge only for 3+ items) is subtle at the 22px minimum
cell. The board never says "these two move together". Also,
state.sentences returns two same-axis spans of equal length for
FLAG IS WIN+DEFEAT (FLAG IS WIN and FLAG IS DEFEAT). Neither contains
the other, so the renderer draws two bars over the same cells.
Proposed fix: design: always badge word stacks (×2) and fan them wider
when a stack sits in a sentence. The ledger should show a stacked predicate
as a single WIN+DEFEAT chip. Engine: merge spans that cover the same cells
into one span with stackedIds.
F-XW4: AND is greedy and never backtracks, so stacked connectives read inconsistently
- from: crossword (parser probing)
- side: creator
- severity: medium
- owner (suggested): engine
- status: open
src/ parseRules consumes AND noun pairs before it looks for
IS, and it never retries. Repro (text grid): ROCK AND+IS FLAG gives no
rule, but ROCK AND+IS WIN gives ROCK IS WIN. MIAU AND+ROCK IS YOU
gives only ROCK IS YOU, so MIAU is dropped. I avoided building on this
because it looks accidental.
Proposed fix: decide the rule. Either the parser tries every reading of
a stacked cell (backtracking), or authoring validation warns when a stack
mixes a connective with a noun or with IS. Document the choice in
docs/.
F-XW5: Rule precedence differs from the reference game and isn't documented
- from: crossword
- side: creator and player
- severity: medium
- owner (suggested): engine (document), design (decide)
- status: open
Found while building pure-grammar levels:
(a) ROCK IS STOP AND PUSH is a wall, because planMove checks STOP before
PUSH. In the reference game, PUSH wins.
(b) MIAU IS YOU AND DEFEAT doesn't destroy the cat, and MIAU IS YOU AND WIN+DEFEAT wins, because contacts only resolve in cells with 2 or more
occupants. In the reference game, the carrier is destroyed.
(c) ROCK IS ROCK doesn't stop ROCK IS FLAG. In the reference game, X IS X blocks transformation.
shove-the-danger (the STOP decoy) and carry-the-pair (the self-win with
DEFEAT) depend on (a) and (b). If the engine changes, those two levels change.
Proposed fix: publish a precedence table in docs/ (STOP vs
PUSH, self-contact, X IS X), and mark each behaviour intentional or a bug.
Worlds can then cite it.
F-XW6: Transformation conflicts and chains are invisible and order-dependent
- from: crossword
- side: creator and player
- severity: medium
- owner (suggested): engine
- status: open
(a) With ROCK IS FLAG and ROCK IS WALL both active, the winner is
whichever word comes first in the level's objects array. Swapping two rows
of an otherwise identical level swaps the result. (b) With ROCK IS FLAG and
FLAG IS WALL, a rock is a FLAG after reset and becomes a WALL on the next
accepted move, one link per turn. The level loads in an intermediate state.
(c) MIAU IS YOU AND ROCK turns the cat into a rock at reset, so the level
starts with nothing as YOU even though the board says MIAU IS YOU.
Proposed fix: resolve transformations to a fixed point each turn, with
cycle detection. Pick conflicts deterministically in reading order (top to
bottom, left to right), not object order. Have validation warn when a level
starts with no YOU.
F-XW7: The default noun glyphs are hidden in src/main.js
- from: crossword
- side: creator
- severity: medium
- owner (suggested): engine
- status: open
The brief for this world was "use only nouns with default emoji". The only
list is a glyphs object literal on line 67 of src/ (MIAU, CAT,
BABA, FLAG, ROCK, WALL, WATER, GRASS, STAR, KEY, DOOR, KEKE, THORN, GEM). It
isn't exported, documented, or reflected in vocabulary, and nothing tells
an author that a noun without a palette entry falls back to a text label.
Proposed fix: move the table to a shared module (for example
src/), export it, list it in docs/, and have
loadMod warn (not fail) when a noun has neither a palette entry nor a
default glyph.
F-XW8: Solver capacity limits challenge design
- from: crossword (
spend-it-twice) - side: creator
- severity: high
- owner (suggested): engine
- status: open
tools/ builds a fresh createGame (with structured clones) for
every expansion. A 12 by 9 draft of spend-it-twice with three loose words
hit LIMIT after 2000001 states in 2m39s. I had to shrink it to 9 by 7 to
verify it (35 steps, 41s). A full pass over this 7-level pack at --max 2000000 takes about 2m45s. So the solver, not the design, decided the size of
the world's hardest level.
Proposed fix: step a single mutable state with an undo stack instead of
createGame per node, use a compact integer state key, and add an optional
A* mode (for example, distance of loose words to target cells) or a
push-level macro search for long levels.
F-XW9: Standing on a thing and then writing its rule wins
- from: crossword (
carry-the-pairfirst draft) - side: creator
- severity: medium
- owner (suggested): design (decide), engine (solver flag)
- status: open
The first draft had FLAG IS WIN+STOP. The solver broke the sentence, walked
onto the flag, and rebuilt FLAG IS WIN+STOP vertically under the cat. That
won in 16 steps, because STOP never ejects an occupant. It's a legitimate
reference-game trick, but it defeats any "the goal is STOP" framing, and
BYPASS doesn't catch it (words did move). I fixed it by using DEFEAT
instead.
Proposed fix: have the solver print the rules active at the winning step
and flag WON-WHILE-OVERLAPPING when the win came from a rule formed while
YOU already shared the cell, so authors can decide.
F-XW10: The solver shows one route and can't check the intended one
- from: crossword (
down-to-miau) - side: creator
- severity: medium
- owner (suggested): engine
- status: open
down-to-miau was meant to be solved with ROCK IS MIAU (a
transformation). The shortest route was ROCK / down onto the
shared YOU, and I only found that by writing a replay script
(/) to print the rules added on each step. I then added a
wall (a black square) to block that column.
Proposed fix: add solver flags --explain (print rules added or removed
along the path, and the board at the end), --require "ROCK IS MIAU" (the
route must form this rule), and --forbid "ROCK IS YOU" (report whether a
route avoiding it exists).
F-XW11: Validation ignores world metadata and the quality-bar constraints
- from: crossword
- side: creator
- severity: low
- owner (suggested): engine
- status: open
loadMod accepts manifest.world (any shape), category (any value),
introduces and reuses (anything), and boards up to 128 by 128. The worlds
README requires boards of at most 14 by 10 and category in required | optional | challenge, but a typo such as category: 'challange' or a
15-wide board passes silently.
Proposed fix: validate manifest.world (glyph of at most 16 chars,
tagline of at most 80 chars, accent in the six property tokens) and
category. Emit non-fatal warnings for boards over 14 by 10 and for
introduces that isn't a string array.
F-XW12: With crossing words, the ledger doesn't show which sentence a word is in
- from: crossword (
across-and-down,pass-it-on) - side: player
- severity: medium
- owner (suggested): design
- status: open
In a crossing, the shared IS is fully opaque as long as either sentence is
active. If the player breaks the vertical sentence, the IS still looks
active, and only the dimmed FLAG shows that anything changed. In a chain
like ROCK IS FLAG IS WIN, the middle FLAG belongs to two rules. The
ledger lists rules but doesn't point back to their tiles or axis.
Proposed fix: hovering or focusing a ledger rule highlights its source
tiles (rule.sourceIds already exists) and draws its bar brighter. Show a
small axis mark (across or down) on ledger rules that came from a vertical
span.
F-XW13: SINK silently deletes words
- from: crossword (parser probing)
- side: player
- severity: low
- owner (suggested): design
- status: open
A word pushed into a cell with a SINK thing is destroyed along with the
thing (MIAU IS YOU /, step right: the STOP word
and the rock are gone). In a pure-grammar level, that can remove a word the
solution needs, and the only sign is the missing tile.
Proposed fix: show a destroyed word with its own effect (the letters
scattering) and a ledger note like STOP IS GONE, so the player knows to
undo.
F-CW1: No map format; every level is hand-counted coordinates
- from: Crate Works
- side: creator
- severity: high
- owner (suggested): engine
- status: open
Seven boards of 40 to 110 entities each would have meant writing hundreds of
word('IS', 8, 3, 'x-is') calls and counting columns by hand. Crate Works
defines its own board(rows, meta) helper at the top of
mods/: one character per cell, a legend (# WALL,
$ CRATE, ~ GAP, uppercase letters for words), and row-width checks with
readable errors ("row 2 is 11 wide, expected 13"). That's about 25 lines
every pack author will rewrite.
Proposed fix: add map(rows, legend, meta) to @miau/. Legend
values are 'WORD', { noun }, or an array for stacks. Include row-width and
unknown-character errors that name the level, column, and row, and a
documented default legend so packs and bug reports read the same way.
F-CW2: Rule words can't be fixed, so every level spends rows sealing an "office"
- from: Crate Works
- side: creator
- severity: high
- owner (suggested): engine
- status: open
Every rule a level depends on (WALL IS STOP, GAP IS SINK, TRUCK IS WIN)
can be broken by the player unless it's physically sealed away. The only
tools are a full wall row, which needs WALL IS STOP and so is circular, or
packing row 0 so the board edge stops upward pushes. Two of my drafts fell to
this. load-order v1 was solved in 15 by pushing STOP up out of
WALL IS STOP from the room below, which made the walls passable. That's a
word move, so the solver didn't flag BYPASS. spend-it-last v1 was solved by
sinking a word into a row-0 gap and then pushing GAP IS SINK sideways.
Proposed fix: add a per-word fixed: true flag. The engine treats it as
immovable and the renderer draws it bolted: no lip, a flat tile, a small rivet
in the gutter. Also add a documented "sealed rules" slot outside the board for
rules that should never be editable (a ledger-only rule).
F-CW3: Solid terrain requires a breakable rule
- from: Crate Works
- side: creator
- severity: medium
- owner (suggested): engine
- status: open
A WALL does nothing unless WALL IS STOP is on the board, so every level
pays three tiles plus sealing (F-CW2) just to have walls. Players also have to
read that sentence in every level even though it is never the puzzle.
Proposed fix: allow level-declared terrain nouns that are STOP
implicitly: terrain: ['WALL']. They're shown in the ledger as a greyed
built-in rule and can still be overridden by a written rule if a level wants
that.
F-CW4: tools/solve.mjs is too slow and runs out of memory on normal board sizes
- from: Crate Works
- side: creator
- severity: high
- owner (suggested): engine
- status: open
The solver builds a new createGame and runs structuredClone for every
node, about 25k states/s. spend-it-last (14×5) needed --max 2000000 and
70 s. A 13×9 room with four loose words hit the limit, and my faster
re-implementation ran out of memory at 4 GB storing full states in the BFS
frontier. I had to write a compact simulator (/, about 30×
faster, differentially tested against src/ with random walks) to
iterate at all.
Proposed fix: give the engine a stepState(compactState, dir) fast path
with no events and no clone. Store parent pointers instead of path strings.
Hash with typed arrays. Add --replay <path> to print frames, because I also
had to write that.
F-CW5: The solver can't say whether the intended rule is needed
- from: Crate Works
- side: creator
- severity: medium
- owner (suggested): engine
- status: open
BYPASS only catches "no word moved". Unintended rule edits pass silently. The
first crates-move was won with CRATE IS WIN (WIN pushed onto
CRATE IS PUSH) instead of TRUCK IS WIN, and load-order v1 was won by
breaking WALL IS STOP. I added --forbid "RULE" to my own solver to prove
that GAP IS CRATE and CRATE IS YOU are necessary.
Proposed fix: add --forbid "X IS Y" and --require "X IS Y" to
tools/, and optional level metadata intended: { rules: [...] }
that the checker verifies (unsolvable without them). Also print the rule set
at the moment of winning, which exposes most accidental shortcuts at a glance.
F-CW6: Mods can't declare their own terrain slab; the WALL palette glyph is silently ignored
- from: Crate Works
- side: creator
- severity: medium
- owner (suggested): engine
- status: open
src/ draws a slab only for nouns in options.wallNouns,
which defaults to ['WALL'], and nothing passes that option. My palette entry
WALL: '🧱' is ignored without a warning, and a themed noun like SHELF or
RACK would render as a small emoji instead of terrain.
Proposed fix: a palette flag { glyph, terrain: true }, or
manifest.terrain: ['SHELF'], passed through to wallNouns. The loader
should warn when a palette entry will never be drawn.
F-CW7: A wall that isn't STOP still looks like solid terrain
- from: Crate Works
- side: player
- severity: medium
- owner (suggested): design
- status: open
The slab rendering depends on the noun, not the rules. If WALL IS STOP is
broken, or a level makes walls PUSH, the board still shows solid racking,
and the player learns that walls are passable only by bumping into them.
Crate Works avoids this by always sealing WALL IS STOP (F-CW2), but other
worlds will hit it.
Proposed fix: draw terrain slabs only while the noun is STOP. Otherwise
draw a hatched outline (passable) or a slab with a lip (pushable).
F-CW8: manifest.world is accepted with no validation
- from: Crate Works
- side: creator
- severity: low
- owner (suggested): engine
- status: open
I added world: { glyph, tagline, accent } per D20. loadMod accepts any
shape there (a typo such as accent: 'pushh' or an 200-character tagline
passes), so mistakes would only show up at render time, if at all.
Proposed fix: validate world.glyph (1 to 16 characters), tagline
(80 characters or fewer, no U+00B7), and accent (one of
you|push|stop|win|sink|defeat) in validateMod, with the same readable
error lines as other fields.
F-CW9: Level metadata (category, introduces, reuses) has no schema
- from: Crate Works
- side: creator
- severity: low
- owner (suggested): design
- status: open
The README requires category and introduces, but nothing validates them,
and there's no convention for lessons that aren't a single token. I used
strings like 'sinking words', 'shared YOU', and 'noun transform'
alongside tokens like 'GAP'. A tool that builds a curriculum view can't
tell these apart.
Proposed fix: validate category against required|optional|challenge.
Split introduces into words: [...] (tokens, checked against the level)
and ideas: [...] (free text). Document reuses.
F-CW10: No way to express a real Sokoban goal ("every crate on a dock")
- from: Crate Works
- side: player
- severity: high
- owner (suggested): engine
- status: open
WIN only fires when a YOU shares a cell with a WIN thing, so multi-box levels
can't have a goal that depends on where the boxes are. Crate Works works
around it (holes that eat boxes, pushing a PUSH AND WIN truck onto a parked
YOU, crate-as-YOU), but the genre's core satisfaction, the last box clicking
home, isn't available.
Proposed fix: a core property GOAL (DOCK IS GOAL). The level is won at
end of turn when a YOU exists, at least one GOAL thing exists, and every GOAL
thing shares its cell with another PUSH entity (words count). Full spec, and
two prototype-verified draft levels, are in docs/
under "Goal feature spec".
F-CW11: A sunk word takes its rule away with no announcement
- from: Crate Works
- side: player
- severity: medium
- owner (suggested): design
- status: open
In spend-it-last, load-order, and night-shift, the key event is a word
entering a gap: the word, the gap, and possibly a rule vanish in one turn. The
ledger just loses a line. A player who sank PUSH too early doesn't learn why
the crate stopped moving.
Proposed fix: when a destroyed entity is a word that was in an active
sentence, show a short float of the lost rule, struck through
(CRATE IS PUSH with a line through it), and play a distinct sink sound for
words. The destroyed event already carries text.
F-CW12: Lockstep YOUs give no feedback about who was blocked
- from: Crate Works
- side: player
- severity: medium
- owner (suggested): design
- status: open
in-step and back-it-up depend on one YOU being held still by a wall while
the other moves. Right now the blocked YOU just doesn't move. That's the most
important information in the level, and it's shown by nothing happening.
Proposed fix: a per-entity bump for every YOU whose move was refused, even
when another YOU moved and the turn was accepted. The engine returns only the
first blocked, so settled() should return blocked: [] for every refused
YOU.
F-CW13: Crates without PUSH look exactly like pushable crates
- from: Crate Works
- side: player
- severity: medium
- owner (suggested): design
- status: open
In night-shift, GAP IS CRATE turns 13 gaps into crates at once. Because
CRATE IS PUSH was just dismantled, they're walk-through decoration, but they
look identical to the pushable crate from level 1, so the solution looks
impossible until the player walks into one. The mass transform also needs a
strong cue.
Proposed fix: give things with no active properties a subtle "inert"
treatment (for example 70% opacity or no ground shadow). When a transform
changes more than about 4 entities at once, use a staggered pop.
F-CW14: Position-derived object ids change when the map is edited
- from: Crate Works
- side: creator
- severity: low
- owner (suggested): engine
- status: open
To avoid id boilerplate, my board() helper derives ids from the level id
and starting cell (night-shift-3-4). Moving one tile renames it. That's
harmless today, but anything keyed by entity id (replays, the facing map,
future gate state) would silently break across pack versions.
Proposed fix: document that replays are move lists only, and state which
features, if any, persist entity ids. If some do, have the map helper assign
ids by reading order per noun (crate-1, crate-2), which survives most
edits.
F-CW15: 14×10, sealed rule rows, and blank-row spacing compete for space
- from: Crate Works
- side: creator
- severity: low
- owner (suggested): design
- status: open
A sealed rule office costs 2 rows (words and walls), or 4 with the "blank row
between stacked sentences" rule. On a 10-row board that leaves 6 rows for the
puzzle. load-order ends up with two rule rows separated only by a wall row,
and spend-it-last packs four sentences into one row with single-wall
separators.
Proposed fix: a sealed-rules strip outside the board (see F-CW2), or
count sealed rows separately from the 14×10 limit.
F-CW16: The engine changed under a live authoring session
- from: Crate Works
- side: creator
- severity: low
- owner (suggested): engine
- status: open
Partway through, src/ gained uncommitted automata support and the
solver was updated. Core semantics didn't change, but I only found that out by
diffing and re-running a differential test.
Proposed fix: keep a short ENGINE-CHANGES.md or version note that says
whether core turn resolution changed. Have the solver print the engine
version it ran against.
F-CW17: The engine snapshot has no per-entity mechanism state (gates and switches)
- from: Crate Works
- side: creator
- severity: medium
- owner (suggested): engine
- status: open
The gates-and-switches proposal needs a gate to remember open (a jam depends
on history), and object records need a channel. Today reset() rebuilds
objects from a fixed set of fields, so extra fields are dropped, and
object() has no options argument. Proposed semantics, and three
prototype-verified scenarios, are in docs/ under
"Gates and switches (proposal)".
Proposed fix: let object records carry validated extra fields
(channel), keep open in state.objects so undo and share just work, and
include it in the solver hash.
F-CK1: An undeclared property word silently becomes a noun and transforms things
- from: Clockwork
- side: creator
- severity: blocker
- owner (suggested): engine
- status: open
The parser treats any word that isn't IS, AND, or a known property as a
noun. On the shipped engine, the draft level key-and-door
(/) contains DOOR IS SHUT. It loads
without a warning, and at reset every door turns into a SHUT thing with no
glyph. The door stops being STOP, so the lock is gone. My first scratch test
did the same with GEAR IS MOVE: every gear became a MOVE thing. A creator
who misspells a property, or uses one from a newer build, gets a level that
quietly plays wrong. validateLevel also accepts { noun: 'TICK' } in a level
that declares TICK as an automaton property.
Proposed fix: at load time, warn about any predicate word that isn't a
known or declared property and isn't a noun used by the pack's objects,
palette, or vocabulary. The warning should say something like "SHUT isn't a
thing in this pack. Did you mean a property?" Also reject an object whose noun
is a declared property word. See ask 1 in docs/.
F-CK2: New level features are silently ignored by builds that don't know them
- from: Clockwork
- side: creator
- severity: high
- owner (suggested): engine
- status: open
level.automata shipped without a schema bump, and mods.js passes unknown
level keys through. Take a pack that uses automata, or the proposed
pairs, or even a typo like automaton. It loads on a build that doesn't
know the field, and its property words turn into nouns (F-CK1). The only
version gate, manifest.engine, is still 1.
Proposed fix: add manifest.features: ['automata', 'pairs']. The loader
rejects any feature it doesn't know with the player line "This pack needs a
newer Miau." Validation also warns about unknown level keys. See ask 8 in
clockwork-api.md.
F-CK3: A player has no way to learn what a custom property does
- from: Clockwork
- side: player
- severity: high
- owner (suggested): design
- status: open
TICK is a word no player has seen before. The stage draws its tile and a
next-step arrow on each actor, but nothing says "this thing repeats its
steps, one for each of your turns". Clockwork puts a one-sentence
explanation in each level hint, but hints nudge. They shouldn't have to
define vocabulary.
Proposed fix (design): the first time a level shows an unfamiliar property
tile, show a small card: the tile, its one-line describe, and a short demo
of the pattern. After that, the line shows under the ledger entry.
Proposed fix (engine): accept a pack-level glossary,
properties: { TICK: { accent, describe } }, where accent is a property
colour token and describe is at most 80 characters. Clockwork already
declares it, and today's loader ignores it.
F-CK4: The renderer hard-codes the custom property list
- from: Clockwork
- side: creator
- severity: medium
- owner (suggested): engine
- status: open
src/ hard-codes its own PROPERTIES set, which now includes
TICK, and colours cues with colors.TICK. The engine takes property words
from level.automata[].property, but the renderer doesn't. A pack that names
its property WIND or SPIN gets a noun-shaped tile with no chamfer and no
cue colour, so it reads as a noun.
Proposed fix: derive the property set and cue colour from the level's
declared properties and the glossary accent (F-CK3), and remove the
hard-coded word.
F-CK5: Players see only the next beat, and nothing at all for a paused clock
- from: Clockwork
- side: player
- severity: medium
- owner (suggested): design
- status: open
drawAutomataIndicators draws one arrow for the next step, and only while the
rule is active. In wind-it-up the player can't preview what the mouse will
do before winding it. In escapement (29 steps) the plan depends on where the
mouse will be 4 to 6 beats from now. Two facts are also invisible: a paused
clock keeps its phase, and handing TICK to another noun keeps that phase
too.
Proposed fix: add a pattern strip for each automaton (in the ledger or a
HUD row) that shows every step as a small arrow or pause mark, highlights
the current beat, and stays visible, dimmed, while paused. Keep the on-sprite
arrow for the next beat only.
F-CK6: A moving hazard can't guard anything, because contacts only run at end of turn
- from: Clockwork
- side: creator
- severity: medium
- owner (suggested): design
- status: open
Contacts resolve once, after the automata beat. A cat can step into a DEFEAT
cog's cell on the turn the cog moves away, swap cells with it head-on, or let a
STOP patroller walk onto it and then leave. Every patrol-guard level I drafted
(stop-the-clock in three layouts) had a timing bypass or blew up the solver.
The same rule is a feature elsewhere: a thing that is both YOU and TICK
moves twice and skips the cell in between, which is what double-time uses to
cross a cog line.
Proposed fix: document the "end-of-turn contact" rule and its two
consequences (swap-through, and double moves skipping cells) in
docs/ next to the automata schema. If guards matter later, the
engine could add an opt-in contact: 'each-move' per automaton. That needs a
spec, so don't add it silently.
F-CK7: The automata spec, the loader, and the engine disagree
- from: Clockwork
- side: creator
- severity: medium
- owner (suggested): engine
- status: open
Three mismatches:
openspec/describes one automaton with step IDs and property additions and removals. The code implements many automata withchanges/ add-rule-driven-automata/ specs/ .../ spec.md moveandwaitsteps.- The engine accepts
'U'/'W'shorthand steps, butmods.jsrejects them. - The engine's
MAX_AUTOMATAis 32, and so is the loader's, but only the loader rejects aphasebeyond the pattern.
I authored against the code, and I only knew which one was true by reading both files. Proposed fix: update the spec to the shipped model, and have the loader and engine share one validator, or at least one table of limits and accepted forms.
F-CK8: A function anywhere in a pack gives an unhelpful load error
- from: Clockwork
- side: creator
- severity: low
- owner (suggested): engine
- status: open
My first behaviour-API draft exported
properties: { MOVE: { run(view, self) { ... } } }. loadMod fails with
run(){} could not be cloned., because normalizeMod runs structuredClone
over the export. Packs are data-only by design (URL links and the pack library
store no code), but the message doesn't say so.
Proposed fix: before cloning, walk the export for functions and report
"levels[2].automata[0].pattern: packs are data only; functions aren't
supported" with a path.
F-CK9: BYPASS counts automaton pushes as the player's work
- from: Clockwork
- side: creator
- severity: medium
- owner (suggested): engine
- status: open
tools/ flags BYPASS only when no word moves at all. On a level where
a mouse pushes the WIN word, a player who just walks and waits is never
flagged. Yet "the player never touched a word" is exactly the skip an author
wants to know about. mouse-keeps-time is hands-off on purpose, and nothing in
the tool output says so. My prototype verifier
(/) adds a HANDS-OFF flag: won without the
player's own push chain moving a word (moves tagged cause: 'automaton' are
ignored). Speed is the same problem as F-CW4. mouse-hole and escapement
return LIMIT at the default --max 300000 and need 3 to 4 million expansions,
about 3 minutes each.
Proposed fix: add HANDS-OFF to tools/ for levels with automata,
and let a level declare allowHandsOff: true. For speed, see F-CW4.
F-TW1: A sunk word's rule stays active for the next move (ghost rule)
- from: Tidewater
- side: creator and player
- severity: high
- owner (suggested): engine
- status: open
step() parses rules, transforms, and then resolves contacts, but it never
re-parses rules after contacts remove things. If a sentence is still whole on
the turn one of its words sinks (for example, pushing ROCK IS STOP right
until STOP enters water), state.rules still contains ROCK IS STOP. The
next move into a rock is rejected with reason stop. A rejected move doesn't
refresh rules either, so the ghost lasts until some other move is accepted.
The ledger shows a rule whose word is gone. Repro: /, also
in docs/, "Surprising engine behaviour" 1. No Tidewater
level depends on this. My search port had to copy the stale behaviour to
match the engine.
Proposed fix: call refreshRules() (and transform()) again after
resolveContacts() when anything was removed. Add a regression test with the
repro. Decide whether the post-removal transform also applies in the same
turn. I suggest yes, to reach a fixed point.
F-TW2: "Become a boat to cross the water" can't be expressed
- from: Tidewater
- side: creator
- severity: medium
- owner (suggested): design (decide), engine
- status: open
The world brief asked for "becoming a CRAB or BOAT to cross what MIAU
cannot". In the core engine, SINK removes every occupant of the cell whatever
its noun, so a BOAT sinks exactly like a cat. DEFEAT is the only hazard that
depends on identity (it only removes YOU things), and nothing grants immunity.
Identity puzzles therefore reduce to "become whoever is already on the other
side" (sideways), or to changing the hazard itself (shell-game,
last-boat-out).
Proposed fix: add FLOAT as the next core property: a FLOAT thing and a
SINK thing in one cell don't interact, as in the reference game. It's the
smallest addition that makes vehicles, bridges, and "moored" objects
authorable. Until then, document in docs/ that SINK ignores
identity.
F-TW3: Anything placed on water vanishes at reset, silently
- from: Tidewater
- side: creator
- severity: medium
- owner (suggested): engine
- status: open
reset() resolves contacts, so a boat moored on a WATER IS SINK cell, or a
jelly floating in the sea, is destroyed before the first frame. The level
loads as if the author forgot the object. Nothing reports it: not loadMod,
not the solver. This also blocks design's crumbling-tile default ("place WATER
under it"). See "Crumbling tiles (proposal)" in docs/.
Proposed fix: have validation (or the solver) warn "N things are
destroyed at reset (BOAT at column 4, row 2 sinks)". Longer term, FLOAT
(F-TW2) makes these stacks legal.
F-TW4: A palette entry for MIAU silently turns off the cat's facing
- from: Tidewater
- side: creator and player
- severity: medium
- owner (suggested): engine
- status: fixed in the working tree (
src/now inherits built-in facing when the glyph matches); kept for the recordrender/ facing.js
src/ only applies the built-in DIRECTIONAL_NOUNS facing
when a pack has no palette entry for the noun. Every example pack
(word-lab, garden-basics) writes MIAU: '🐈' as a plain string, so in
those packs the cat never turns around, and nothing warns about it. I only
found this by reading the renderer.
Proposed fix: when a pack's plain-string glyph is identical to the
built-in glyph for that noun, inherit the built-in faces. Update the
example packs to { glyph: '🐈', faces: 'left' }, and say so in
docs/.
F-TW5: A syntax error in a pack gives no line number
- from: Tidewater
- side: creator
- severity: low
- owner (suggested): engine
- status: open
An apostrophe in a single-quoted hint ('You can't reach...') produced
Could not load tidewater.cjs: Unexpected identifier 't'. There was no line
or column, because the source runs through new Function, and the
sourceURL comment doesn't surface in the message. With a pack made of text
maps, finding it meant bisecting by hand. An apostrophe in a hint is also the
most likely mistake a writer will make.
Proposed fix: before new Function, parse the source with a cheap
syntax check that reports line and column, or catch the SyntaxError and
read the line from error.stack, adjusting for the 2 prepended lines. In the
Workshop error note, show the offending line.
F-TW6: Newer emoji (🪼, 🪸) have no fallback glyph
- from: Tidewater
- side: player
- severity: low
- owner (suggested): design
- status: open
🪼 (Unicode 15, 2022) and 🪸 (Unicode 14) render well on current Apple fonts,
but show tofu on older Android and Windows 10. The jelly is the hazard in 5
of 7 levels, and an unreadable hazard is a fairness problem. The palette
takes exactly one glyph per noun, so a creator has no way to say "or 🎐 if
that's missing".
Proposed fix: allow palette[NOUN] = { glyph: '🪼', fallback: '🎐' },
with the renderer checking glyph support once (canvas measureText width
against the tofu width). Or document a "safe emoji" floor (Unicode 13) for
bundled packs.
F-TW7: Walking into water is instant and untelegraphed
- from: Tidewater
- side: player
- severity: medium
- owner (suggested): design
- status: open
SINK resolves in the same step as the move. The first time a player learns
that water is deadly is when the cat disappears, and in one-for-the-tide
the sacrifice of a crab reads exactly like a mistake. The world depends on
the player choosing what to feed the sea, so the difference between "fed
the sea" and "fell in" matters. Undo recovers it, but the lesson arrives as
a punishment.
Proposed fix: (a) a one-frame "about to sink" wobble on a YOU thing whose
next step in the held direction enters a SINK or DEFEAT cell. It's
presentation-only and computed from properties(). (b) A distinct sink
effect when a YOU is lost while other YOUs remain ("one for the tide"), not
the same effect as a crate or word sinking.
F-TW8: Transformation beats contact, and the player can't see why they survived
- from: Tidewater
- side: player
- severity: low
- owner (suggested): design
- status: open
In one step you can complete JELLY IS SHELL and walk onto a jelly. The
jelly becomes a shell before DEFEAT is checked, so you survive (repro in
docs/, behaviour 2). It's a good moment, but the
transform tween and the move tween play together, so it looks like the cat
walked into a jellyfish and nothing happened.
Proposed fix: play the transform pop before the move settles (a
40 to 60 ms lead) whenever a moved YOU ends on an entity that transformed
this step, so the order of events is visible.
F-TW9: A crumbling tile can't have water "under" it in the current contact model
- from: Tidewater (crumbling tiles proposal)
- side: creator
- severity: medium
- owner (suggested): design (decide), engine
- status: open
Design's default for crumbling tiles says "a level can place WATER under it
to make the hole deadly". A cell with DRIFT and WATER under
WATER IS SINK is destroyed at reset (F-TW3), so that authoring pattern
can't exist without a floor layer. Full semantics and two draft levels are in
docs/, "Crumbling tiles (proposal)".
Proposed fix: a crumble becomes a leftover noun
(leaves: 'WATER', keeping the entity id) instead of revealing what's
underneath. Express counts above 1 as a leftover chain (SAND, then SHOAL,
then WATER) rather than a counter, so the state stays in noun and undo,
sharing, and the solver hash need no change.
F-TW10: Solver capacity decided the size of the finale (+1 to F-CW4 and F-XW8)
- from: Tidewater
- side: creator
- severity: medium
- owner (suggested): engine
- status: open
The first 12×9 draft of last-boat-out ran tools/
out of memory (a 4 GB heap crash, not LIMIT). A compact port reached 13
million states without a solution. The shipped 10×7 version needs 261k states
and about 90 s, with a peak RSS of 1.7 GB. A mutable-state port with compact
string keys (/, /, differentially tested with
0 mismatches over 57k random steps) is about 20× faster and supports
"solve without rule X" and "solve without sinking a word". Those constraint
modes are how I proved each level's key edit is necessary.
Proposed fix: adopt a compact-key, mutable-state solver and add
--without-rule "<RULE>" and --no-word-loss constraint flags. They answer
"is my intended idea required?" (F-CW5, F-XW10) directly.
F-DS1: shell-game's key rule can't be machine-proven
- from: design (CI setup)
- side: creator
- severity: low
- owner (suggested): content
- status: resolved (Tidewater reworked
shell-gameto 5×7 with pinned sentences;--forbid 'JELLY IS SHELL'is UNSOLVED after 623,612 states, and the level declaresmustForm)
tidewater/ hits LIMIT at 3M states. Per
the engine audit, the authored words over-approximate alternative rules, and a
lexical shortcut would be unsound. Proposed fix: rework the proof with
fewer loose words and a smaller open area, so the forbid search terminates.
Then add mustForm: ['JELLY IS SHELL'].
F-XW14: Proving mustForm cost Crossword its decoy words
- from: crossword (W6 rework)
- side: creator
- severity: medium
- owner (suggested): engine
- status: open
--forbid has to prove UNSOLVED exhaustively. Each loose word multiplies
the state space, and words can drift through non-STOP things (a column of
DEFEAT rocks) into the whole board. In shove-the-danger, the three loose
words (AND, PUSH, and a decoy STOP) hit LIMIT at the 2M default and
needed about 5M to prove. pass-it-on needed 9.5M at 10 by 7. To make the
proofs fit, I removed the decoy and narrowed both boards (7 by 7 and 8 by 7).
The decoy was good design: it made the player choose PUSH over STOP.
spend-it-twice proves at 1,905,020 states, 95% of the default, so it now
declares solverBudget: 4000000 for margin. A full pack run takes about 6
minutes. This is a sibling of F-XW8, F-TW10, and F-DS1.
Proposed fix: make the forbid search prune by word-motion symmetry, or
by a static dead-word analysis (a word in an edge row or column that can't
reach any target cell). Also consider a per-level CI budget report that shows
how close each proof is to its cap.
F-XW15: mustForm can't express "some crossing rule"
- from: crossword (
across-and-down) - side: creator
- severity: low
- owner (suggested): engine
- status: open
across-and-down teaches "one IS, read across and down". --forbid 'FLAG IS WIN' solves in 26 steps through a different crossing (FLAG IS YOU down,
then MIAU IS WIN across), so the level's real invariant is "some vertical
sentence shares the IS". mustForm only takes exact rule texts, so I declared
nothing, and the level can't be regression-checked.
Proposed fix: allow structured requirements such as mustForm: [{ axis: 'v' }], { predicate: 'WIN' }, or { sharesWordWith: 'MIAU IS YOU' },
checked against rule.axis and rule.sourceIds, with the same forbid-based
proof.
F-XW16: A precedence change silently broke shipped levels
- from: crossword (W6 rework)
- side: creator and player
- severity: high
- owner (suggested): engine
- status: open
W6 (D23) changed STOP versus PUSH, self-DEFEAT, X IS X, transform order, and
stacked parsing. Packs still declare engine: 1, and nothing in load or
solve told me my levels changed. I found out only when the design lead
mentioned that carry-the-pair now hit LIMIT (its old route killed the cat)
and that shove-the-danger's decoy had changed meaning. A player who had
installed the old pack would find a proof unwinnable with no explanation.
Proposed fix: bump the manifest engine (or add a rules semantic
version) whenever precedence changes. Warn on load when a pack targets older
semantics. Run every bundled and world pack through tools/ with its
mustForm in CI, so a semantics change fails loudly.
F-TW11: A per-id string leaves can't express a crumble chain
- from: Tidewater (FRAIL fixtures)
- side: creator
- severity: medium
- owner (suggested): engine (schema), design (confirm)
- status: resolved (D25 revised: leftovers are a per-level noun map in
mechanics[frail].leaves, looked up by current noun; fixtures updated)
D25 says leaves stays with the entity id and that multi-step crumbling is a
noun chain. Those two decisions conflict. SAND with leaves: 'SHOAL' crumbles
into a SHOAL that still carries leaves: 'SHOAL', so the second crossing turns
SHOAL into SHOAL, and the sandbar never runs out. Draft B ("Two tides") can't
be authored with a string. docs/ (two-tides)
uses a proposed map form instead.
Resolution (accepted, D25 revised): leftovers are a per-level noun
map only: mechanics: [{ id: 'frail', type: 'frail', version: 1, state: 'derived', leaves: { SAND: 'SHOAL', SHOAL: 'WATER' } }]. There's no
per-object leaves field and no string form. A crumbling thing becomes
leaves[currentNoun] and keeps its id, and a noun missing from the map is
removed. Per-object variety (one plank leaves WATER, another nothing) is given
up in exchange for rules players can read ("sand turns to shoal"); a level
that needs two behaviours uses two nouns.
F-TW12: A loose FRAIL sentence lets NOUN IS WIN skip the crossing
- from: Tidewater (FRAIL fixtures)
- side: creator
- severity: low
- owner (suggested): content
- status: open
The first "One plank each" fixture left DRIFT IS FRAIL loose, following
the draft's "break the rule to keep the bridge" alternative. The solver
immediately swapped FRAIL for the loose WIN and won by touching a plank
(DRIFT IS WIN, 14 steps). Any FRAIL level with a loose property word and
walkable frail things has this shortcut. The fixture now pins the sentence in
a corner.
Proposed fix: a guideline for FRAIL levels: pin the FRAIL sentence, or
make sure no loose property word can take its slot. Add a solver lint that
warns when the winning rule's subject is a noun whose only role was FRAIL.