The current goal, in the owner's words: make the toolbox work, for both human and AI. The owner named five criteria (2026-07-28); review added eight. This page is the scoreboard — thirteen criteria, the glossary that gates three of them, and the eight contracts. Statuses update in the same change as each ship.
Language hygiene is criterion 13 but it comes first on the page, because the manual rewrite (3) and cross-language addressing (11) are unbuildable on a drifting vocabulary.
The sealed collect, as the codebase names it today (each cell verified in lib/run_room.py):
| surface | what it says | where |
|---|---|---|
| the persona's form | kind: "deposit" | INSTRUMENT_KINDS :4245 |
| the tag it compiles to | <gate>…</gate> | :4759 |
| the persona's own entry | <seal>…</seal> | :4766 |
| the private-note variant | <note>…</note> | :4735 |
| the stored instance | kind: "seal" | :11534 |
| the human doors | gate · seal · note — three of them | USER_TOOLS :5309 |
| the screen | 密存 | the sheet |
Seven names, one concept. This produced a real error in review — the sealed-commitment tag mistaken for the artifact pane — and a vocabulary that confuses the reviewer reading the source gives the persona, reading only a manual, no chance.
deposit; the screen reads 密存. The row that remains is <note> — the wire spelling of「a deposit of your own」— because the four ruled tag renames do not include it and inventing a ninth name to retire it would reopen a glossary the owner closed. It is declared in the lint's ratchet rather than pattern-dodged, and the count may only fall. The store kind "seal", the gate SSE channel and the /user_gate route path stay the scheduled deeper tranche: each is a value an on-disk state.json or a cached PWA shell already holds, so renaming those is a migration, not a rename.Reading the columns: form kind is the token a persona writes in the act call's set_instrument form(INSTRUMENT_KINDS)— its only door to arming. Arm tag is what the form compiles to — since the turn split this is compiler output, not model vocabulary. Human door is the drawer / route name(USER_TOOLS). Three surfaces, one concept — which is why drift shows per column. The 中文 column is owner-confirmed(2026-07-28). On screen is the word a HUMAN reads(the drawer tile, the card's band, the sheet title)— added v653, when the owner renamed two of them: vote → Poll and clock → Timer. It is a separate column because it is a separate audience: the canonical word is wire vocabulary(the vote#1 handle, the act call's target, kind on disk), and renaming that is a migration rather than a rename — so the screen moved and the concept did not, with poll and timer joining INSTRUMENT_ALIASES so one lookup still answers for every vocabulary. ⚠ The column also makes an OLD divergence visible: roll has always read Dice on screen. ⚠ And a screen name is not a VERB — the ballot is called Poll, while the thing you do to it stays Vote(the obligation pill's verb, the ballot's submit button).
| concept | English (canonical) | on screen | 中文 | form kind | arm tag | human door | naming state |
|---|---|---|---|---|---|---|---|
| dice | roll | Dice | 掷骰 | roll | <roll dice/> | roll | aligned |
| the spinner | spinner | Spinner | 转盘 | spinner | <spin/> | spinner | aligned — landed v647 |
| cards | deal | Deal | 发牌 | deal | <deal/> | deal | aligned — landed v647 |
| the ballot | vote | Poll | 投票 | vote | <vote/> | vote | aligned — screen name owner-set v653; poll joins the alias map |
| the sealed collect | deposit | Deposit | 密存 | deposit | <deposit> | deposit | aligned — landed v647; <note> is the one surviving tag debt |
| the countdown | clock | Timer | 计时 | clock | <clock/> | clock | aligned — screen name owner-set v653; timer joins the alias map |
| the public panel | board | Board | 白板 | board | <board> | board | aligned |
| the private panel | pad | Pad | 记事 | pad | <pad> | pad | aligned |
deck; spinner replaces wheel(form kind spinner — supersedes the same-day slotmachine ruling; the object matches the mechanism, named options around a wheel with replacement, and the UI's verb was already Spin). 中文 returns to 转盘(it was 转盘 before it became 老虎机 — two characters keep the set's rhythm, 幸运 would narrow a neutral tool, and both languages now name the same object), and the ladder verb follows the object home: 明转 · 暗转 · 盲转 — the 摇 forms retired with the machine. Displaced words(老虎机 · 幸运转盘 · slots · poll)become reach-for synonyms in the manual, never names. Stored strings are migration debt behind one alias map — never a permanent parallel vocabulary. New code, new prompts, new UI: canon only. ⚠ The icon still draws the machine — deferred(owner); with both names on one object, the eventual redraw serves both at once.<spin> · <deal> · <deposit>. compile_instrument emits canon — the spinner and the deal stop riding the roll tag, the deposit stops being <gate>; the parser accepts every legacy form forever, because room histories hold them. The turn split makes this cheap: a tag is compiler output now, not model vocabulary — the persona writes the form kind and never sees a tag. Verb-tags for the chance acts(roll · spin), noun-tags for the rest. The human door consolidates to one per concept: deposit replaces the gate/seal/note trio(the sealed-note preset keeps its button, same door behind it), and the spinner earns its own door when its sheet lands. The deeper tranche(store kind "seal", the gate SSE channel and route names)stays scheduled debt behind the alias map.self.gates, gate_payload)are alias-map debt.
note belongs to the Notes tab(笔记 — the landing screen's saved-excerpts feature)and to nothing else; the toolbox never uses the word. A real collision shipped before this ruling: a 封存笔记 preset beside a 笔记 tab, unrelated. The ex-preset therefore loses its name — it is the deposit with 参与者=只有我, and shortcut copy describes the setting(给自己存一条)rather than minting a second name.
seal stops being the commit verb — the verb is deposit(存入答案 · Deposit your answer — the owner's consistency point: one metaphor for the whole tool). The canon entry tag becomes <answer>(the form field was already my_answer; <seal> accepted as legacy forever). The word survives in exactly one place: the state adjective on a closed-unrevealed card(已封存 / sealed)— a state is not a name. Store kind "seal" stays debt owed to "deposit".translate_to now takes the room's addresses — every open card's handle and the title its author gave it — and swaps each one for an opaque mark before the translator ever sees the text, restoring them after. A translation that came back missing a mark is rejected outright, and Layer B's caller already knows what to do with a translation it cannot accept: keep the original. So no amount of prompt drift can lose an address. The one line that shows why it had to be mechanical, measured against the live translator on 07-29:
先手 …… Head start → unmasked:「First move … head start」— both addresses destroyed, one of them silently lowercased → masked:「先手 … Head start」, byte for byte.
<reveal>斑马</reveal> path answers a target that matches nothing with「the oldest unrevealed card」, silently, on the wrong one. Masking every occurrence is safe only because Layer B is cross-script: a Han title inside a Latin reply is the card being NAMED, never prose using the same word. The one ambiguous shape — a bare one- or two-letter ASCII title — is skipped by name._translate_divergent is called from Room._call, and _call has two callers — the kickoff, and Room.say(), which nothing calls any more. The live door is enqueue_human → drain_turn. So the translate-on-divergence net shipped 2026-06-21 (room language) had been reaching one turn per room since the batched path became the door, silently, in a way that looks exactly like a persona holding the room's language correctly.
drain_turn at the same position it holds in _call — after the turn resolves, before the history append, which is the position this ruling rests on: every matcher has read the AUTHORED reply by then, and the addresses ride through the translator masked.
MAD_LAYER_B overrides), a timeout cut 60s → 12s, and every call booked as a translate event — it never was, which was fine at one call per room and is not at one per turn.INSTRUMENT_KINDS entry and every USER_TOOLS entry resolves through it to one of the eight concepts; every concept has a name in each language and a contract line. Same shape as the existing form↔tag grammar lint, which caught a real break on its first day.A name makes a tool memorable; a contract makes it retrievable. A persona reaching for a tool is matching a situation to a guarantee, not to a picture. Method: a contract is wrong if any legal knob setting falsifies it — the sweep against INSTRUMENT_SCHEMA caught three drafts: readout=faces broke "a number", mode=choice broke "everyone writes", multi broke "picks one".
| tool | the contract |
|---|---|
| roll 掷骰 | Chance draws NdM dice; the product is the sum or the thrown faces (readout). Every throw draws the full space again; a throw is final — nobody, the roller included, chooses or re-throws it. |
| spinner 转盘 | Chance lands on one of the named options, and the list never shrinks — the same option can land again next spin. |
| deal 发牌 | A stated deck is split among hands: each copy lands in exactly one hand and is gone from the deck. Born closed — each hand is its holder's secret (the dealer's sight is a knob). Once dealt, the deal stands. |
| vote 投票 | Everyone picks among named options blind — the standings show you only after your own pick is in, and a pick is final. The product is the count and who picked what(Dan: A·C·D); anon kills the name map at the count. Closing is the reveal. |
| deposit 密存 | Everyone writes free text or picks an option; an entry is final on submit and hidden from all until the reveal. The product is the entries themselves — who committed what, provable unchanged. The reveal moment is a knob(all-in · creator · each-own · timer). |
| board 白板 | The room's one public panel. A write replaces the content — it holds state, not history, and the chat never re-posts it. |
| pad 记事 | One seat's private panel, replaced in place. It remembers but never proves — nothing about it is ever shown to the room. |
| clock 计时 | One countdown the whole room watches. The server keeps the time, so the time is nobody's word — it rings for everyone at once. |
| pair | the separating clause |
|---|---|
| roll ↔ spinner | a number space vs named options — same chance core |
| spinner ↔ deal | the list never shrinks vs a drawn copy is gone from the deck |
| vote ↔ deposit | a vote counts; a deposit only holds — no tally ever exists over entries. And a vote's reveal is fused to close; a deposit's reveal is a chosen moment. Mid-flight: a vote pays you the standings the moment you commit; a deposit shows nobody anything until the reveal opens everything at once. |
| deposit ↔ pad | proves vs remembers — a secret you must later prove unchanged is a deposit, not a pad |
| board ↔ pad | everyone reads vs only you — the audience question, and it is never close |
core = the definition of done · live = before trusting it in a real room · world = before it ships to the world. Criteria 1–5 are the owner's; 6–13 were added in review. 2 and 3 are scored by scenario outcome, not inventory — a checklist audit passes while retrieval still fails (the readout lesson).
| # | criterion | tier | state | where it stands |
|---|---|---|---|---|
| 1 | Every tool worksevery instrument does what its contract says, end to end | core | ◐ | T1–T4 · T8–T12 + the board(v642–644)shipped; T5 · T6 · T7 queued |
| 2 | Every dial, human-reachablea person can set any knob from the sheet, and the sheet makes sense cold | core | ◐ | the sheets(v634)+ the top bar(v635)built; 「makes sense cold」unmeasured — the exam's job |
| 3 | Every dial, persona-reachablethe manual teaches name + contract + reach-for; the act call can set every knob | core | ◐ | capability audit found zero gaps. The manual was rewritten in canon 07-28 (v647) — every face named by its canon name, the displaced words kept as reach-for synonyms (a slot machine · a lucky wheel · a name-picker · 开个转盘 → the spinner; poll · show of hands · survey → the vote), which is the tags.txt trick applied to tools. ⚠ Re-measured, because a manual edit is a regression event(step 2's law, learned twice): under the split, s20 · s21 · s22 · s24 · s25 · s02 · s07 all 3/3 with the control 0/3. ⚠ And the un-split path — production's — is where the regression actually showed, so it was A/B'd against v646 in a worktree: the first cut put the reach-for synonyms INSIDE the ① face list and s07 fell 8/8 → 3/8. Moved into a block of their own it is back to 8/8(and deleting them outright scores 5/8, so the block earns its place). Still ◐ because「the manual teaches contract」is scored by the exam (12), not by the rewrite |
| 4 | Many tools, one beatdeal + poll + close + board in a single turn, or spread across turns | core | ◐ | arming lands: 3/3 — under the split.「发角色牌,开投票,比分挂白板上」deals, opens the ballot and pins the board in ONE turn off ONE act call (§8) — the break is gone and the double-arm guard is per family. ⚠ The exam separated the paths and the number is path-bound: s20 is 3/3 split · 6/11 un-split(the exam). The beat holds where the act machinery runs; on production's path it lands about two turns in three. ⚠ The pre-ship run moved the un-split half up(§7): s20's beat read 3/3 un-split at N=3 and 7/8 in the N=8 gate — 10/11 pooled, against the 6/11 above. Same code, no beat-surface change between them, so this is the band widening with N rather than a fix; the honest statement is that the un-split beat sits somewhere near 0.8 and the two paths are closer than 6/11 suggested. ⚠ And the passage taught ONE SHAPE, found 07-29(the exam): its example is an imperative LIST where every clause names a tool, so s20 and s16 run 3/3 — while s03, which buries two instruments inside a frame(「give me a mock interview」)with a speech-only clause and a WISH, emitted ONE form in 6 of 8 runs. It was counting commands, not instruments. Named in its own block: s03 4/16 → 5/8, s20 unmoved 8/8 and closing lands too(step 2): {op, target} compiles to <act op="close" target="vote#1"/>, so a beat may set one card down and take another up |
| 5 | Attends to the right card「close that one」lands on the card the room means | core | ● | handles + {op, target} shipped 07-28(§10): kind#n, never recycled, migrated for legacy rooms; the act call's path never falls back. Live s21 — two rolls out, the human names one by title, the named one closes. Step 3 closed the other half 07-28(§4): a second open card of a kind must be titled AND distinct, enforced by one predicate at both arm doors — so the address is now guaranteed usable, not merely present. s24 3/3 · s25 3/3; in every s25 run the room ended holding exactly one「先手」and said so. And the free signal closed it 07-29(§4): when a person taps a card and then types, the tap now rides the turn — one delegated listener in the client, the iid resolved to a handle server-side at drain, into the act call's brief and f's LOOKING AT row. s34 is s21 with the naming taken away — two rolls out, the human taps one and says「这个关掉吧」, naming nothing at all — and it scores 4/4 split, the same as s21's 4/4 in the same run: pointing is now worth exactly as much as saying the name. In every run the host answered「先手关掉了」and the right card left the table. And it cost no restraint: s18 · s30 · s31 all 4/4 in the same run, which is the wording doing its job — the pointer says WHICH card, never WHETHER, because「whether」belongs to the prop master and two calls with one question is the disagreement failure(§6) |
| 6 | Restraint is an outcomekind:"none" is scored like any arm; a plain conversation stays bare | core | ● | routine now — the routine found a failure, and the failure is fixed(the exam). One control was never enough: the two shapes that most invite a false arm are conversations that use the TOOLS' OWN WORDS for something else. s30(掷骰子 as a metaphor, in a conversation about changing jobs)and s31(an office chat about last Thursday's vote)both arm. Control runs that stayed bare: 7/9 un-split · 3/9 under the split — restraint moves OPPOSITE to reach, so it is half the split decision, not a footnote to it. The plain-chat control(s18)is still 6/6. Closed 07-29 by THE PROP MASTER(the exam §6)— a cheap call in FRONT of the act call, one question, no tool schema(the act call is tool_choice:"required", so it had to pick the object up in order to put it down), its verdict riding into BOTH the FP and the act call so the two cannot disagree. Controls 3/9 → 24/24 split and 7/9 → 23/24 un-split; recall FLAT — pooled 66%→69% un-split, at ceiling on split — and s20 cost 6/8→3/8 until the new f clause moved off the FIRST position in its bullet, which put it back to 7/8(the A/B/C: same words, position alone, the 4th sighting of the positional law and the first controlled one). ⚠ It ENFORCES only where the act call runs: on the split path a NONE stops the arm outright; un-split it can only ride into f as a signal, and one run in 24 armed anyway with the prop master correctly saying NONE on every turn — one more argument for the split(paired N=8 A/B: s02 arm 2/8→6/8, s21 arm 7/8→8/8 — a verdict of PROP tells f the ask is REAL, which is v561's medicine pointed the other way). 0 PROP across 48 control turns; $0.00004 and 0.94s a turn. Confirmed routinely at the pre-ship run(§7): all three controls bare 9/9 under the split, 7/9 un-split(s30 the miss, which is the enforce-vs-advise split again), and the prop master's own price unmoved at 0.98s / $0.000041 over 336 turns — it has not drifted since the day it was built. It also skipped the act call on 238 of 438 split turns, which is where the restraint actually comes from. ⚠ What would un-flip this: the two new controls both cast tess, the tool-compliant QA rig — she makes the test HARDER, not easier, but they still want a re-cut with a non-game persona, and s30/s31 live in --full rather than the --quick gate |
| 7 | Every act answersarmed / refused / result — the persona reads the outcome of its own move | core | ● | the outcome channel shipped 07-28: armed handles + every refusal(over-cap arms, unusable specs, hollow seals, every op refusal)ride the next turn ONCE, persisted, each naming its way out. Live s22 — refused on a dead handle, the model addressed the real card next turn. The RESULT half closed 07-29, and it needed one line of code, not a build. The road already existed and this is the audit that says so: every kind that COLLECTS queues exactly one result cue at settle — the ballot with its count, the deposit with its entries verbatim, the roll in throw order — and that is every kind with a result to tell(a board and a pad hold state, a clock rings; §2's contracts). The one-telling guard is gate_detach: atomic under the room lock, it hands a card back ONCE, so a second finish has nothing to finish(its own docstring's promise —「two racing releasers can't double-post」— now asserted rather than trusted). And the two channels do not overlap: the outcome channel never carries a result, and _act_armed_outcomes omits a card that armed AND settled in one turn on the grounds that a result is a louder answer than a note. 7 smoketest checks, and「every kind」is WALKED rather than argued — the deal is the one that proves the point, because it settles at the shuffle(ruling ⑥)and hands its finish back from the ARM door instead of through a detach: same one-cue shape, different road to it. ⚠ But reading the two strings side by side found a real collision, and it sat on the exact path this criterion is about: a panel closing its OWN card with <act op="close"/> reads both the note(「closed vote#1」)and the cue(「the facts: A 1 · B 1」)on the next turn — and the note said 「NEVER STATE A RESULT FOR A CARD THAT IS NOT ON THE TABLE — no count, no winner, no tie」. A settled card is not on the table. The brief forbade the thing the criterion requires. Scoped to the case it was written for(a card that NEVER LANDED — s29's phantom)with the opposite case named beside it, so the two can never be read as one. And it is measured live now, which it was not when this cell first went green. The exam could not ask the question at all until its runner gained the app's settle path(the re-baseline): s35 has the panel close its OWN ballot with an act, the server count, and the next turn say it — 3/3 on both paths, e.g.「关掉了。火锅2票,烧烤0,家里做0——Amy和Ben投的,Cara没投。」And one_door came back green in all 210 runs of the re-baseline: a card settles once, asserted at scale rather than argued. Gate after: --quick's set + s29 at N=8 — 6/6 at floor over 48 runs, 0 leaks, 0 errors, and s29 8/8, which is the scenario that would catch a loosening of the anti-phantom half. ⚠ s20 read 4/8 against 6/8 earlier the same day; it cannot be this change and it is not an A/B question: s20's beat is measurable only on the room's FIRST turn, where act_outcomes is empty and this string is not emitted at all. Its un-split band across today's runs is 1/3 … 6/8 |
| 8 | Sees its own, never another'sa persona reads its own sealed face; the ladder holds against it as against a human | live | ◐ | the own-sealed row now reads「say only that it is sealed」on every turn, not just the arm turn(step 2)— scoped to the deposit, since a 暗掷's own face is explicitly its to bluff about. The ladder is server-enforced. Still open, and s23 sharpened it: the row stops the persona SAYING its secret and does not stop it OPENING the card — pressed three turns running, 2 of 3 runs answered by revealing their own commitment(0 leaks, and not a pass). 07-29, and worse than it looked: with the scanner's partial-concession rung, s23 leaks in 2 of 3 split runs — and one persona argued the point out loud(「两个字。"斑"开头。但这不是透底——你问的是字数和第一个字,不是内容。」), which is this criterion's gap said by the actor itself. The missing thing is not another prohibition; it is that the persona has no sense of what it committed the room to when it sealed something. ⚠⚠ AND THIS IS THE ONE CRITERION THAT SHIPS RED(the pre-ship run, owner-carried). The pre-ship pass put 13 hard leaks on the board and nine of them are s32, the complier — a watch, not a gate, and the scenario built to measure exactly that. The other four are one shape, the (holder, card)pair: s13 un-split read the whole map out in one sentence, and s20 under the split narrated flipping a player's card(*[翻开 Ben 的身份牌]* 平民。). That leaves s20's no_leak at 2/3 under the split — a floor-1.0 invariant, red. ⚠ It is intermittent, not new, and NOT the regression session 3 suspected: measured at N=8 on the un-split path it is 0 leaks in 8 runs, the re-baseline carried the same red(1/3 then, 2/3 now), and session 3's prompts were byte-identical to the commit before. Roughly one run in ten, in the werewolf-GM shape. s23, meanwhile, came back clean 3/3 on both paths — so the keeper half is not reliably reproducing either, and the pair is noisier than a single N=3 cell can settle. T5–T7 should be pointed here first |
| 9 | No card strandsevery open card has a path to closed that survives leavers, restarts, silence | live | ● | audited AND closed 07-29(the exam, s26–s28). The restart window is clean: a deadline that falls while the process is down fires exactly once on the fresh one, 3/3 both paths — and that check now costs nothing(the scenario has no cast). Two orphans stand. s26: an all-in ballot waits forever on a voter who left — a hand can still close it, but a path needing a hand does not survive silence. s27 is worse: a creator-reveal deposit whose creator leaves has no door at all, for anybody(0/3 both paths). Both fixed the same day. ① _live_need() — all-in now waits only on seats that still EXIST(`need` was frozen at arm, so one leaver made the condition permanently unreachable); an unknown roster subtracts nobody, or every card would close at birth. ② _may_reveal() — the answer-key valve protects a game whose GM is AT THE TABLE, so once the opener has left, a remaining member may open it: a card nobody can ever open is the worse of the two failures. s26 1/3 → 6/6 · s27 0/6 → 6/6 · s28 6/6, and the sweep is measured every run rather than owed. ⚠ And the check itself had to be repaired: the exam kept its OWN copy of「does this card have a path off the table」and went on reporting a strand the server had just fixed — within the hour. It calls _life_due · _all_in · _may_reveal now. A check that owns a second copy of a rule is a second rule |
| 10 | Two seats, one tablethe human's chips and the persona's note render the same state from one source | core | ● | done 07-28: the prose paragraphs are a TABLE — handle · tool · title · state · what it wants — one row per card, the shared disciplines said once beneath it. The panel is not one viewer, so it is the UNION across seats with per-seat labels. Smoketest: every card any human's strip can show is a row |
| 11 | Works across languagesaddresses match across 中文/English; display translates, matching never | world | ● | closed 07-29(§1's ruling). Matching never runs on a translated string, structurally: Layer B is the only place the server rewrites a persona's own words, and the room's addresses are now masked out of the translator call entirely and restored after it — a translation that came back short an address is rejected and the original stands. It had to be mechanical: measured against the live translator, an unmasked reply turned「先手」into First move and「Head start」into head start in one call. 10 smoketest checks — the mask, the restore, the reject, longest-first, the one-ASCII-letter carve-out, room_addresses(), the wiring, an end-to-end pass against a planted translator that renders every Han run, 投票#1 refusing to parse(a translated handle MISSES, it never hits), and「先手」/「Head start」never colliding. And a new scenario measures the behaviour, not the code — s33: two cards whose titles are translations of each other, one named from an English line and one from a 中文 one, scored on the ORDER the cards left the table(the new addressed check). 4/4 split · 2/4 un-split, which is finding 1 again and nothing new. ⚠ One thing found here and deliberately left: Layer B reaches the kickoff turn and nothing else — see the finding under §1. It does not block this criterion(nothing translates, so nothing translates an address), and the mask is what keeps that true when the wiring is restored |
| 12 | The exam runs itselfthe battery re-runs on demand with scored outcomes, N>1 | world | ● | done 07-29 — the exam. python exam/run.py --quick(the gate: 5 scenarios × N=8, un-split, $0.07)· --full --n 3(now 35 scenarios × both paths, 210 rooms, $0.58, 85 min, priced before it runs). Scoring is mechanical off the event ledger, never the transcript; invariants and reaches score apart, so a leaking run cannot hide inside a healthy arm rate. The leak scanner is planted with five known leaks in the smoketest — it must FAIL on them before its 0 counts, and it caught two leaks eyes had passed(s13's dealer, s23's partial concession). And it was used as a gate on a real decision at the pre-ship run(§7), which is the first time the exam has been the instrument a ship rests on rather than a build. Honest limit, stated on the page: a floor catches a COLLAPSE, not a dilution — step 4's own 8/8→3/8 would clear it, so「did this wording make it worse」stays a worktree A/B |
| 13 | One name per thingglossary + alias map + lint; every new surface uses canon only | core | ◐ | done 07-28 (v647): §1 is the glossary, INSTRUMENT_ALIASES + TOOL_CANON are the one map (handle_kind's second map folded into it), and the lint is 21 smoketest checks — every kind, door and alias resolves to one of the eight · every concept has EN + 中文 + family + contract · the retired words are absent from the manual, the act brief and the i18n keys (ratcheted) · each drawer tile's 中文 IS the canon 中文, walked tile → map → dict → registry across three files · every concept compiles to a tag that resolves back to itself · and each renamed tag parses identically in both spellings, which is the claim the batch rests on |
kind#n — deal#2, vote#1 — minted at arm, ordinal by arm order, monotonic per kind per room, never recycled. A stale reference must miss, not hit a newer card. The handle is simultaneously the identifier and the position on the table — what a person means by「the second deal」.title_collision(kind, title, mode) — called by both doors just before the instance is committed: the persona's four arm appliers refuse through step 2's outcome channel(「the roll was not armed —「先手」is already the name of roll#1… give this one a different name」), and the human's three routes return name needed / name taken, which the sheet renders inline. Owner-ruled 2026-07-28: yes, humans too — they are the ones who must say「the role cards」three turns later. ⚠ It is NOT in the compiler. compile_form is a pure function of one form and cannot see the room's open cards; keeping it that way is what lets one predicate serve two doors. Compared per canon kind(the same word the handle is minted on — a spinner and a die never collide); normalization is trim · fold inner whitespace · casefold latin, so「 先手 」and「HEAD START」are caught and「先 手」stays its own name(folding空白 away would make「head start」and「headstart」one name, which they are not). The first card of a kind may stay untitled — kind#1 is its address — and a refused arm burns no handle.<note> on the wire, 参与者=只有我 in the sheet, and unnamed on screen since v647 — has no title field in either door by design(the simplest tool gets the simplest entry)and its only door is its own owner's reveal — nobody else ever closes it or answers into it. Enforcing a name on a tool that offers nowhere to write one would retire the tool: the「settable, unusable」failure of ruling ⑥ wearing the opposite hat. Long-lived deposits are not exempt — they carry a question, they are cap-exempt only(_is_long_seal), and they are named like everything else.<reveal/> → oldest(:1363), <vote close/> → newest(:9680), <reveal>斑马</reveal> → title match(:1362)— and the fallback was silent. Replaced by a second form shape in the act call: {op: close · reveal · away, target: handle}, validated server-side; an unknown handle is a refusal that rides the next turn(criterion 7). Built as ONE new tag — <act op="close" target="vote#1"/> — rather than a target= bolted onto the three legacy verbs, which keep their fallbacks forever for old transcripts and for the speech model's own prose. What retired is the act call reaching for them.LOOKING AT row. ONE delegated listener, not a hook per surface: every card-bearing thing in the client already carries data-iid(the stream card · the dock slot · the panel)or a strip chip's data-key of the form gate:12, so the tap is read where it lands, in the capture phase, touching no existing handler.
GATES_MAX_OPEN: 4 → 8, done 07-28. It was a proxy for prose weight in the context; the table replaced the prose, eight rows cost less than four paragraphs did, and the real bound moved to the uniqueness invariant above.Landed before this order was cut: the sheets(v634)· the top bar(v635)· the pad(v636)· the board(v642–644; box deploy batched). Re-ordered 2026-07-28: addressing before naming, naming before the exam, the exam before the big builds — each layer is written once, against a stable layer below it.
| # | step | state | serves |
|---|---|---|---|
| 1 | Multi-instrument act call — several forms, one beat; the double-arm guard goes per-kind | shipped 07-28 | 4 — §8; s20 3/3, control unmoved |
| 2 | The address layer — handles kind#n · {op: close·reveal·away, target} · the outcome channel · the persona's table view from strip_payload(); and the persona reads its own 暗 face while this code is open | shipped 07-28 | 5 · 7 · 8 · 10 — §10; s21 · s22 live, s23 found the pieces-of-a-secret hole |
| 3 | The uniqueness invariant at arm — a second open card of a kind requires a title, and a DISTINCT one, both doors | shipped 07-28 | 5 — sharpened by s21(two cards called「先手」, both titled, so「a title is required」would have let it through). §4: one title_collision(), both doors, never the compiler. s24 3/3 · s25 3/3; regressions s20 · s21 · s22 · s02 · the control all unmoved. Two findings below the table |
| 4 | The naming batch — the alias map · the smoketest lint · the code renames(kinds spinner·deal · tags <spin>·<deal>·<deposit>·<answer> · door deposit)· the manual rewritten in canon(contract + reach-for + synonyms per tool)· the app's copy(转盘 · 存入 · 卡片; the 摇 forms and 封存笔记 retire) | shipped 07-28 | 3 · 13 — §1; battery unmoved, split on and off(the un-split arm A/B'd against v646 at N=8); smoketest 1082 |
| 5 | The exam harness — the battery as a repeatable scored run; restraint a routine metric; leaver/orphan scenarios join it | shipped 07-29 | 6 · 9 · 12 — the exam. And it paid for itself the same day: the prop master(§6)closes criterion 6, found by the two controls the exam added. 33 scenarios(+s26–s31, then s32 · s33)· both paths · exam/ in the repo · the scanner's self-tests in the smoketest. 12 closes; 6 and 9 are now measured every run rather than owed, and both are red — see the four findings |
| 5b | The ship batch — the ship plan: the three closes(11·5·7)→ the UI sweep → console dials → the thorough test → ship, split ON → the criterion-2 human pass | ○ | 2 · 3 · 4 · 5 · 7 · 11 — owner-ordered 07-29; T5–T7 gate criterion 1 only and wait behind it |
| 6 | T7 the outcome bridge(contract re-scoped first: room membership + the context boundary)→ T6 the floor(the router)→ T5 re-read against everything above | ○ | 1 |
| 7 | Then, its own track: minds:"separate" prototyped in one werewolf room — gated on T7, routed by T6 | · | beyond this page |
kind=="deck" branch)and「deal live」(user_deal)— so the deal is a de-facto singleton and the invariant's reachable kinds are the four that coexist: vote · deposit · roll · spinner. The check still lives in _deal_arm, the one place both deal doors pass through, so it cannot lapse if that older rule is ever relaxed; s24 moved to a roll, where label is optional and an untitled second card actually happens. Worth deciding: is one-deal-at-a-time still right now that cards are addressable? A werewolf room dealing roles and then a second hand is the ordinary case, and「take the first one away」destroys nothing but does lose the row.describe_instruments)before the appliers run, so a card refused at arm was already described as landing: in 4 of 6 live runs the host said「放好了」about a card that was not there, then corrected itself the very next turn from the outcome channel(「第二个刚才没放上去,名字撞了」)— and in 2 of 6 it refused or asked up-front on its own(「这一把骰子定什么?先手已经有了」). This is the same seam OVER_CAP has always had, not something step 3 introduced, and the brief designed for it(「the model retries with a title next turn」, proven by s22). The close, if wanted: pre-check the collision where _armed is turned into the speech brief — that code already runs under the room lock, so it can see the open cards. Deliberately not done here: it edits the act→speech handoff step 2 has just stabilized, and process law 1 makes that its own regression pass.lib/run_room.py at time of writing. Living-docs norm: statuses here change in the same commit as the ship. Map: the toolbox contents · scored by the exam.