The re-architecture ruling of 2026-07-21, reached after a week of live play (steps 1–7 built and tested) put stress on the six-move basis — and after the werewolf trace (§5) broke it in an honest place. The six moves remain the product's face; underneath them sits a smaller orthogonal engine. Owner-designed; this page is the record.
Every tool splits into a mechanism (what the server guarantees, identically in every game forever) and a policy (what the room's agreed contract decides). A week of live failures traced to exactly this line being blurred — see §3.
| tool | the mechanism — what the server guarantees | the contract decides |
|---|---|---|
| 🎲 The randomizer | a fair draw from a distribution — dice(2d6, 5d7)· pick(with replacement — the spinner)· deck(without replacement — the deal, ruling ⑦)— witnessed, timestamped, on the record; delivery: onto the table(public)or into the safe(face-down, §5b) | who may draw, when, and how many times (default = 1); where the result lands; what it means |
| 📋 The state recorder | one shared, persistent, always-visible state strip; consistent structure: Currently(比分、回合)+ Next step(该干什么) — the game's program counter | the schema; what advances the counter |
| ✉ The safe | anyone — persona or human — deposits sealed content; the deposit is visible, the content is server-held; opens at the agreed moment, timestamp-proven unchanged | who deposits; when it opens (a manual reveal, a checklist firing, the clock) |
| ☑ The checklist | a condition over room events that triggers the next step when satisfied: all committed a secret · all drew · all spoke · at least N spoke("spoke" = any user message — ruling ②); any member can release a stuck one | which condition; who counts; what firing does (wake the host, open the safe, post the facts) |
| 🕐 The clock | standalone furniture — the meeting room's wall clock: set it, everyone watches it count, it rings on the record. Never folded into the checklist(ruling ④: facilitators don't record time on the flipboard) | what the ring means; who acts on it (usually: the ring cues the host) |
| 🎙 The floor | new — found by the werewolf trace. Server-enforced speech permission: mute / unmute, visible and attributable(「你已出局 · 旁观中」). Session-scoped ONLY — muting exists solely inside a consented game contract | who holds the floor, when; what restores it |
And one structural scope, not furniture: the room itself. The werewolf den is not a whisper channel — it is a temp room whose members are the wolves. Visibility by membership, on machinery that already exists and is battle-tested (rooms, DMs, history floors), replaces visibility by message-routing. The study's aside (step 10) is superseded: a whisper is a game-scoped DM — the same sub-room mechanism, pair-sized.
@tool(「帮我开个匿名投票,5分钟后自动截止」). The law that makes tier 3 trustworthy: NL fills the SAME form a human would see and shows it as a one-tap-confirm preview — it compiles into the form, never past it. This keeps the two-layer law(the model translates, never constructs), hands the human a receipt of what their words became, and quietly teaches the form over time. @Assistant dispatch is the precedent; @tool sits beside it.The mechanism states facts; the contract owns meaning. Every violation of this line produced a live bug within days:
Evidence the basis is right: everything built in steps 3–7 is a composition of it — including two machines that were built separately and turn out to be the same shape with a different payload.
| what we built / deferred | its decomposition |
|---|---|
| the circle(sealed collect, 7a/7c) | safe × checklist(all-committed)— deposits visible as sealed bubbles, firing opens the safe + cues the host once |
| the collect-roll(7b) | randomizer × checklist(all-drew)— same composition, different payload |
| a vote | safe × checklist — everyone puts a name in; reveal together(already works; never needed naming) |
| the open go-around(deferred in 7a as "open collect") | checklist alone(all-spoke)— nothing sealed, just the barrier |
| the wake(step 9) | the clock — its ring is the host's cue; consent + rate caps unchanged |
| the aside / whisper(step 10) | room scope — a game-scoped DM; the per-viewer message-routing design is superseded |
| a timed phase("5 minutes, then vote") | clock → safe × checklist — checklists chain, each firing advances the board's Next step |
| the role deal(werewolf, face-down — §5b) | randomizer × safe — a deck dealt into the safe: fair by shuffle-at-arm, private by delivery |
| a dot-vote / rating(the tally — §8 candidate) | safe × server count — structured deposits; the COUNT posts as a fact, arithmetic leaves the model's hands |
The chain is the session runtime. A card(step 8)is the program text · the board is the program counter · checklists are the barriers · the randomizer, safe, clock, and floor are the instructions. Step 8 doesn't need to invent a runtime — it composes this one. The owner's two game traces make the point visually — every round game is the same loop; only the payload changes:
The test case(owner, 2026-07-21): the wolves' night discussion and kill vote, persona as GM. Decomposed:
The game's opening move: each player learns only their own role, the GM knows all, the room knows nothing. Randomizer × safe — the deck dealt into the safe:
The deck can be simulated by many rolls — so why mode it? Because reducibility can't be the criterion: everything reduces(a deck is many rolls, a pick is a labeled die, NdM is N×1dM), and by that logic the randomizer collapses to a coin. Three tests draw the real line, each from a principle already on this page:
The result is a randomizer whose whole surface is a cross product of short closed lists — never one feature per game:
| axis | values | closed by |
|---|---|---|
| distribution | dice(NdM)· pick(w/ replacement)· deck(w/o replacement) | the physical objects on real tables(ruling ⑦) |
| budget | default 1 · or the contract's grant | ruling ①("otherwise it's a cast-until-satisfied") |
| delivery | the table(public)· the safe(face-down) | §5b — does showing it early change what people do? |
| access | the contract's grants(a card session)· default-open(a free room) | ruling ⑧ |
Is delivery-to-safe something the LLM assembles at runtime? No — and that is a load-bearing choice. The one-brain law: a model-improvised delivery route cannot referee the model — if it「simulated」face-down by DMing roles in prose, it could misdeal, leak, or remember the deal differently in round five. The trust lives precisely in the fact that the model cannot touch the route.
Are these all the essential tools for productive human interaction? For structure, nearly — with three named candidates; for content, that was never the toolbox's job. The test is the boundary law itself: a tool earns mechanism status only where conversation breaks without a server guarantee — chance, secrecy, simultaneity, arithmetic, time, permission, scope. Everything else productive humans do together(asking good questions, synthesizing, deciding)is orchestration. Walking the full range — interview, book club, dating, brainstorm, retro, negotiation, classroom, games — three compositions don't close cleanly:
| candidate | what breaks without it | verdict |
|---|---|---|
| The tally — structured ballots(choose · distribute N dots · rate 1–5)with server-counted results | counting is a FACT, and arithmetic is precisely where the model wobbles(Lee's meltdown was score arithmetic); dot-voting a brainstorm, ranking, any retro | strongest — cheap: the safe with structured deposits + aggregation at reveal; the count joins「who rolled what」as furniture-stated fact |
| Anonymous reveal — the safe opening content without authors | psychological safety(the study's social-safety leg): honest retro feedback, blind review, 匿名提问 — people won't deposit what will be attributed | one policy knob: safe reveal ∈ {attributed · anonymous}. Completes a symmetry — the deal is author-visible-content-hidden; this is the inverse |
| The ledger — conserved tokens(chips · budgets · bids: can't spend what you don't hold) | auctions, poker, budget exercises, staked negotiation — conservation is a guarantee the board can't give(board numbers are host-written arithmetic) | parked — waits for the scenario that demands it |
anon); the ledger is still parked.Looks like a gap, isn't: the speaking queue(floor granted in sequence + the board's Next step)· breakout pairing(deck deal × room scope)· minutes and artifacts(the transcript is native memory; wildcard-pane artifacts exist)· cross-session continuity(the persistent-memory track, not table furniture)· documents-in(a chat-app capability, not toolbox).
And the deeper answer: completeness is not provable by enumeration, and doesn't need to be. Physical meeting rooms converged on roughly this same short list — whiteboard, flipchart + dots, timer, talking piece, dice, breakout rooms — over a century of nobody designing it. Completeness comes from convergent evolution, and the evolution mechanism is already running: every probe(RPS · 比大小 · the deal)either decomposed cleanly or surfaced exactly one honest knob, and the bruises get smaller and more peripheral each time. That is what a converging basis feels like. The library of games and scenarios is the measurement instrument; the basis is complete when scenarios stop bruising.
Shipped as the poll pilot(v577, owner combo pass v578) — the poll now carries all three dials with the WhatsApp defaults; the store became a LIST so polls coexist. ⚠ The owner's combo review renamed the middle axis: for the poll it is place ∈ {inline, pinned}, not sticky {none·follow·pin} — follow confused users and pin covers its use, so it survives only as the text circle's legacy value. And blocking=all(the composer lock)is now in scope for the poll. The combo law that fell out: placement must match blocking(host⟹pinned, all⟹unique). The pattern is left reusable for a per-tool rollout on owner approval. Build notes: T8 in the build log.
Rulings ⑨–⑪ came out of the owner perfecting the poll. The load-bearing one is ⑪: the tools were born from games and quietly defaulted to sticky and unique(a fixed dock strip, one at a time)— but the product's daily life is chat, so the right default is the WhatsApp poll: a message you may answer or ignore. Three dials capture the whole space, and the default of each is the relaxed value.
| axis | values(default first) | what it controls |
|---|---|---|
| blocking | none · host · all | whose actions wait for the tool to finish before they are unlocked |
| sticky | none · follow · pin | where the tool bubble lives — and whether other bubbles push it away |
| unique | no · yes | whether a second same-tool can open before the first is closed |
Each axis is a small ladder, not a switch — and the middle rungs are what our games actually use.
| value | humans may chat? | the panel(the AI)speaks? | where it fits |
|---|---|---|---|
| none | yes | yes — may even comment mid-vote | a casual poll, a cast die |
| host | yes | no — the AI waits, then answers ONCE reading the whole round | the sealed round · the collect-roll(the one-turn law) |
| all | no — composer locked, with a visible reason | no | a werewolf final vote · the deal's pickup — the Among Us meeting; the floor(T6)applied room-wide; cards only |
The circle today is host-blocking, not all — people were never silenced; the AI was. That distinction was implicit until now; a card will want to choose it deliberately.
| value | behaviour | natural home |
|---|---|---|
| none | scrolls away like any message; you answer it or you don't | the poll's default · a cast die |
| follow | re-floats to the stream's bottom as new messages land — never lost, still in-stream(the DiscordDiceBot trick; the liveTool host, already built) | the active instrument of a turn-based game |
| pin | a status chip in the strip — you pin the number, not the card; tap the chip to expand or jump to it(WhatsApp's pinned-message banner) | the board · the clock — whose essence already is a one-line number |
This is why the field study's dock verdict was about defaults, not mechanism: a poll pinned as a HUD strip was a none-sticky tool forced to pin. The strip holds numbers(board, clock); furniture with buttons(a poll)belongs in the stream.
The cost: the server holds ONE gate slot today, so unique=no(plural polls)means instruments become a list with instance ids, and chips/cards render per instance — honest but bounded work. The prize: under non-blocking, non-unique defaults, a sealed round can no longer capture「your next message」(chat keeps flowing)— which forces the sealed answer to become an input on the card itself. That is strictly better than what we shipped: the gate-mode strip, the message-capture surprise, and the「why did my message disappear」confusion all evaporate. The properties framework didn't just classify the tools — it found the circle's UX bug.
follow)while humans keep chatting and the panel stays silent(host-blocking). Takeover(right): a status chip pins the count, the card fills the screen, and the composer locks for everyone(all-blocking · card sessions only).<gate options="石头, 剪刀, 布"> makes RPS one tap, revealed by person at close).kind="seal"). An answer is an input ON the card(a free-text sheet or a tapped option), never a captured message: the message-capturing circle — the gate-mode strip, held bubbles, sealed placeholders — is RETIRED(a pre-T10 open circle releases-with-what's-in on load). It withholds content from EVERYONE until close(no reveal-on-answer — there is no tally to anchor, only your own answer shown to you), reveals BY PERSON not by option, and keeps the 🔒(v585 in reverse — the most sealed tool in the room). Two doors(the drawer's「Sealed collect」dialog + the persona's <gate> + <seal>); FINAL on submit; the cue always on a close that mattered. Build notes: T10 in the build log.none · none · no)and still hide its running tally.sticky is renamed place ∈ {inline, pinned} —「follow confuses; pin covers its use」; follow survives only as the text circle's legacy value. ② blocking=all is built, not cards-only: the composer locks for everyone until close(voting stays reachable — it is on the card), server-refused too. ③ Two coercions, enforced in _clean_props and mirrored as locked buttons in the form: blocking=host ⟹ place=pinned(an inline card is lost the moment chat keeps flowing past a silent AI)· blocking=all ⟹ unique=yes(a locked composer cannot open a second one). The load-bearing insight: a tool's placement must match its blocking. This is the as-shipped ruleset every tool in the rollout copies.host and not for all. Pinning exists because an inline card is lost when chat keeps flowing — and under all nothing flows, so an inline card holds the tail by construction. The poll's form had been forcing pinned for both(never the server's law: _clean_props only ever did host⟹pinned); inline + all is now open to people, and for a run of polls it reads better than a sheet floating over the closed ones. ⚠ The same host||all ⟹ pinned line still sits in the roll(T9) and sealed-collect(T10) dialogs — left untouched pending the owner's call on whether the same reasoning applies there.| capability | status / where it lands |
|---|---|
| persona creates a room + seats people(the den; the game-scoped DM) | the door(step 11)widened: sub-rooms born from a parent, humans seatable — behind the §7 consent design |
| the outcome bridge(a sub-room's checklist result posts to its parent) | new, small — the same by-construction leak control as the gate's held answers: only the firing's outcome crosses |
| the floor(mute / unmute) | new, small — a policy on the say route + a visible composer state; session-scoped only |
| the clock | new furniture(standalone — ruling ④); its ring absorbs step 9's timing; consent + rate caps stay |
| the checklist as a first-class visual | generalize the 7a gate card: one dock card rendering any condition(votes · rolls · spoke · N-of-M) |
| persona persistent memory | its own major track — needed for relationship continuity across unrelated chats; not a blocker for the den or the game-scoped whisper (the sub-room's brief carries the game context) |
| the deck mode + into-the-safe delivery + the private flip(§5b) | new randomizer knobs(ruling ⑦ + the delivery axis)— unlocks the face-down deal, secret pairings, hidden prompts |
| the humans' touch drawer(ruling ⑧) | the composer's second door: every mechanism the panel can arm — die · wheel · deck · sealed round · vote · note · board · clock, and the away that takes each down. Same mechanisms, same events, same capsules, attributed to a person. Owner ruling v565 — the parity law: whatever a persona can do, a human can do, and its acceptance is an ALL-HUMAN room running a whole game with no persona seated. Free rooms default-open; sessions defer to the card's grants(room_tool_access()) |
| the tally · anonymous reveal(§8) | ruling material — the strongest candidates; the ledger stays parked |
Persona-initiated rooms and seat-dragging are the first tools whose misuse reaches outside a room(notification spam, unwanted inclusion). Principles, to be designed before the door ships:
The internals are already close: the gate object is a checklist instance(a need-list, a have-set, a trigger); its subs are safe deposits; the collect-roll is the same barrier over draws. The re-architecture extracts the checklist's condition types, unifies the safe under one store, and adds the three small mechanisms(clock · floor · bridge)— while the user-facing faces do not change: people play with 骰子, 白板, 信封, and「waiting on Dan」, never with a "condition-trigger primitive." The furniture principle survives by keeping familiar faces on an orthogonal engine. Cards(step 8)compose against the engine in plain language: a state schema, a safe policy, checklist conditions, randomizer grants, clock settings, floor rules.
Shipped v636 as the eighth tool. It is the only instrument that puts nothing in front of the room.
<note>, which was correctly merged INTO the deposit: who answers never changed what a deposit was.
| board | pad | |
|---|---|---|
| audience | the room | you alone |
| lifecycle · 参与者 · 可见性 | none · none · public | none · none · private by definition |
| length rule | screen budget — glanceable | need budget — prune what stopped mattering(capped at 2 000 chars) |
| sheet | title · content · 谁能改 · 传笔(v642) | content. That is the whole sheet. |
| the field | plain textarea, markdown — rendered for readers | plain textarea, plain text — there are no readers |
| result bubble | yes | none, ever |
<pad title="…">, and that is not an inconsistency: several seats' pads ride ONE panel prompt, so each has to say whose it is. Different problem, different answer. Titles a person had already stored are folded into the first line of their text at load rather than orphaned.visibility, who, place, blocking, reveal and close on a board and on a pad, and fails unless both compile to a bare body tag. ⚠ v642 amends the second half of that sentence, not the first. The board's sheet is no longer two fields — it grew 谁能改 and 传笔(§14)— and the guard was re-cut, not deleted: it now probes the parser against a DECLARED attribute set(board = title · edit · handover, pad = title)and still fails on any word nobody listed. The visibility half is untouched and load-bearing: 谁能改 says who holds the marker, never who may look. A board is always public; that is what makes it a board.The pad is the only tool whose entire value is that nobody else sees it: a leak does not degrade it, it destroys it. So the design question was not「where do we filter it out」but「where can it be put such that no filter is needed」. Three guarantees, in descending order of how much work they save:
self.pads lives in state.json only(the deal's idiom, taken further). replay(), transcript.md, the export and the Seen digest all walk events or the transcript — so a pad cannot reach any of them, and nobody has to remember to exclude it.hub.publish(). A viewer-less SSE fan-out is the precise failure gate_payload(g, viewer_uid) exists to prevent, and the pad has no use for one: only the owner changes their own pad, so their own client already knows.pad_payload(viewer) can only return that viewer's own and returns None for a viewer-less caller; user_pad() takes no owner argument, so the route has no parameter through which one person could name another. There is deliberately no method that returns「the room's pads」, because a caller that could ask for that would eventually ship one.The leak test was written before the feature(the brief's instruction, and it earned itself immediately — it caught a real exposure on the first run). Two humans and two personas, each holding a pad with a unique string; every other party's payload, strip, replay, riding note and on-disk record is asserted to contain none of it. If that test is hard to write, the storage shape is wrong.
self.system holds every seated profile — so there is no per-persona prompt to scope to. Each seat's pad therefore rides labelled with whose it is, and the block instructs a seat to read and write only its own: exactly the status quo of [Your sealed notes], which has always been cast-wide. The human wall is a different thing entirely and is structural — a u: key is unreachable from _pad_note, and the test asserts it. Making the seat wall structural too means splitting the panel call per persona; that is a turn-architecture change, not a pad change.And one honest note about the record. A persona's <pad> was written by the model, so the verbatim reply survives server-side in the panel's own history and in the say event's forensic raw — the same two places a sealed <note>'s secret already lives(the v539 law: a persona keeps its own grammar). Neither is served to any participant. A human's pad reaches no model call and no reply, so it is in neither, and the test proves that separately.
The pad rides every turn, which is what makes it useful and also makes it a compounding per-turn tax invisible to the person paying it. Hence a hard cap(2 000 chars ≈ 500–700 tokens)with a warning from 1 600, and a rule worth keeping: a write over the cap is REFUSED, never truncated. Trimming would delete the owner's words while reporting success.
| question | ruling |
|---|---|
| one pad per owner, per room? | yes — per-room matches every other instrument and keeps the leak test's assertions room-local. A second pad is a heading in the first. |
| more than one each? | no. |
| the per-turn cost | a hard cap(2 000), warn at 1 600, refuse over — rather than trusting the manual's「prune」line alone. |
| does a human's pad survive them leaving? | yes — it is their memory OF the room, not the room's furniture, so a rejoin finds it as they left it. It matches the room_members.status grain, where leaving destroys no content. Only deleting the room takes it. |
Shipped v642, then v643 — ten owner reads on the built thing, two of which reversed a v642 ruling(§⑤). The board was the oldest tool in the box and the least finished: it worked, and three things settled in design had never been built.
user_board and _apply_panel_board)— which is ruling ⑫ holding by construction rather than by care.
It renders through the app's one pipeline — renderRich(mdToHtml → sanitize → mentions → house emoji), the same call a chat bubble makes. Bold, italic, strike, inline code, bullet and numbered lists, headings and a GFM pipe table all work; the grammar guard the study set(never raw HTML, never an iframe)is the sanitizer that pipeline already ends with, so it costs nothing to keep. A board-local renderer was the wrong answer: a second markdown implementation is how a table comes to work in the chat and not on the board, so the two things the board needed and the shared layer lacked — GFM tables, GFM strike — were added there, and every bubble in the app gained them too.
**Dan** 3」with its stars showing is worse than either the markup working or the markup being absent, and a table row's pipes are pure noise at that width. One reader still decides what a board says(_md_gist, server-side, beside the rest of the strip)— it is just that this reader says it flat.BOARD_MAX = 1200 is one module constant read by all three, the grammar deliberately reads wider than the cap so an over-long tag is SEEN, and past the cap a write is refused rather than trimmed — with the reason, since「ask for the pen」and「write less」want opposite next moves.| dial | default | what it means |
|---|---|---|
| 谁能改 | 仅创建者(was: anyone) | who holds the marker — never who may look. A board is always public; that is what makes it a board. |
| 传笔 | 关 | may others ASK you for the pen? Only meaningful under 仅创建者 — with 所有人 there is no marker to lend, so the row is not drawn(ruling ⑥'s treatment)rather than greyed. |
board_set carrying no mode field at all projects as 所有人. Every board pinned before v642 was pinned under「anybody writes」, and silently locking one to whoever happened to touch it last would take the marker off a room mid-game. The test is the KEY's absence, not a falsy value — those are two different facts.board_pen {to, by} and board_pen_ask {by} are their own stream, deliberately. Folding a hand-over onto a board_set would mean every 给笔 re-pins the board — a fresh timestamp, a fresh author, a fresh everything — and the projection could no longer tell「the creator handed the marker over」from「the creator wrote something」.
asks —「u:8 raised a hand」— and each client decides whether that is theirs to answer. A broadcast saying「you have a request」has no viewer to be「you」; that is a failure this codebase has already paid for twice. The per-viewer half lives in strip_payload(viewer_uid), which is scoped by construction, and the request surfaces as a 待办 pill in the top bar.<board pen="u3"/>(hand it over; a roster id or the person's name)· <board pen="back"/>(take it back)· <board pen="ask"/>(raise your own hand at somebody else's board)— carrying no body, split off in the applier so it never competes with「the last write wins」, and running the write FIRST so a reply that updates the board and then lends the pen did both with the pen it still held. It goes through the same valve as the human route(one lock-free core, two wrappers), so a persona cannot hand over what it does not hold. This is ruling ⑫ read as「same dials」, not merely「same access」, and the riding note now names the act in its own verb once(the v635 fix, applied here rather than rediscovered):「Ben asked for the pen. Hand it over with <board pen="u8"/> —「Ben, it's yours」with no tag hands over nothing」. An unreadable target moves NOTHING and is counted(the T1 units law: a word we cannot read must never become a hand-over to the wrong person).A non-creator edit is not reversible — the board IS the state, so there is no undo and no revert-to-previous; the history is in the event log and the UI does not offer it.
The expected author of a board is often a persona. A room that wants a whiteboard kept will seat a secretary to keep it, and a persona can hold the pen — that is the point rather than a side effect. So board_note_sp lost its「short plain lines only; it is a scoreboard, not a document」half(wrong now)and kept the other(a panel, not a document); the tag grew edit="all" and the bare word handover.
_board_note() carries the PERMISSION every turn, per seat: 「YOU HOLD THE PEN」or「⚠ ONLY X holds the pen — a <board> from you will be REFUSED」. And a refused write is never silent(board_bad + stderr + a persisted board_refused the next turn's note reports, with the reason): a model that is not told simply writes the same tag forever.<board away/>. And it needs no exception for the open case: with 谁能改 = 所有人 everybody may write, so everybody may clear, by construction.verb and wears the 待办 look in place: still exactly one chip, in one position, showing its verb instead of its value. The priority rule survives: THE FIT exempts an owed entry from the merge, so 状态 still yields width first — just never the one entry in it that is actually an obligation.| the read | what shipped |
|---|---|
| the sheet, not yours | The WHOLE sheet greys(.bk-ro)with a banner that says why —「Ben has the pen — you cannot write on this board.」A dead commit over normal-looking fields makes a person type a paragraph before finding out, and a reason turns「it won't let me」into「Ben has the pen」. The banner itself stays at full strength: it is the one thing there you are meant to read. |
| 传笔 under 所有人 | Greyed and struck, not removed — real but unavailable, the 全部 convention(v642 hid it per ruling ⑥; the owner's call is that a vanishing control reads as「this board has no such setting」). The hint swaps to say why. ⚠ And this fixed a real bug the owner photographed — see below. |
| an Edit door | On the CARD, for whoever may actually write. The card is where you are looking when you decide to change the board, so the way in belongs there and not only in the ➕ drawer — and its presence IS the pen made visible. |
| the owner never asks | 要笔 is gone from the creator's card. Asking for your own board reads as though it had got away from you; 收回 is the creator's move and is always there. |
| who edited it | Shown in every mode and now said in words —「Amy · edited 10m ago」. A board is state somebody is accountable for, so「who touched this last」is never the part that drops out. |
| who holds the pen | Shown whenever there is a pen to hold, never inferred from the presence of a button. |
| the field | Eight line-boxes(it was ~4½), derived from ONE --lh exactly as the pad's is, so the box and the text cannot disagree about what a line is. ⚠ Measured, not assumed: the first cut wrote the padding wrong and the field showed 7.4 lines. |
| lines | See below — the last one, and the most interesting. |
hidden never worked on the segment at all. [hidden]'s display:none is a UA-stylesheet rule, and .pk-seg{display:flex} — an author rule — outranks it. So the 传笔 buttons stayed on screen while their LABEL and hint(plain <div>s with no display rule)vanished: exactly the owner's「the off/on's gap to 谁能改 disappeared」, and it also let an impossible 所有人 + 开 pair sit there looking settable. The general form is worth keeping: setting hidden on a node your own CSS gives a display to does nothing. Grey it, or write display:none yourself./api/rooms/{id}/replay never populated name_map — and it is the one path a plain PAGE LOAD takes while rebuilding the whole room payload, every projection in it included. The fix is at the DOOR, one line, not per-projection. ② _actor_name degraded to the RAW KEY, which is worse than empty: a fallback that looks like data leaves the caller unable to tell it failed. It returns "" now, board_state() reads the name stamped on the event at write time, and the client re-resolves from its own roster as a last resort. Three layers, none depending on a per-request RAM field.board_may_clear). A borrower still cannot delete somebody else’s board. And the live wash stopped reporting the pen:「Something is pinned right now — Dan has the pen」beside a button that takes it down is two unrelated facts in one sentence..pk-grp.off). ⚠ And wrapping the field STOLE its gap — .pk-lab:not(:first-child) is an ADJACENCY rule, so a label moved inside a wrapper becomes :first-child and its 22px silently goes to zero: the very symptom v643 fixed, re-created by the fix’s own wrapper. Two sightings of one shape in two versions: v643’s hidden lost to our own display rule, and this lost to our own new wrapper. Wrap a field, carry its spacing.mdToHtml/renderRich take {breaks:true} and the board passes it. An OPTION rather than a second renderer(a board-local markdown is how a table comes to work in the chat and not on the board)and rather than a global flip(a host bubble's wrapped prose should still reflow). The board declares its own paragraph semantics, which is the honest shape: prose reflows, a panel does not.| out of scope | why, and what mitigates it |
|---|---|
row-level edits(<board set="Dan" to="13"/>) | The drift risk from a persona rewriting the whole board every turn is real, but the tag grammar for incremental edits is its own build. The manual's「keep it short」is the mitigation for now. |
| the board's own history / undo UI | The board is the state(above). The event log holds the history; the UI does not offer it. |