Skip to content

Rulings

Cindy Zhang edited this page Aug 23, 2026 · 41 revisions

Rulings

Taste questions the loop cannot settle, and what was decided.

A ruling is a question only Cindy can answer — tier (prop vs hook vs provider vs primitive), naming, whether surface earns its permanence, whether we want a capability at all. Everything verifiable is the loop's own job (R22); a question that reaches this page must already have the browser work done.

How it runs

Both runs ask, both runs apply. Cindy, 2026-08-23: "You can ask me twice a day but keep rulings running in case I can't get to them."

  1. Every run puts open rulings in the last block of its report — chat scrolls up, so the final block is the one that gets read.
  2. Cindy replies in that thread: 1c 2a, partial fine, any order. Replying is never expected. A run that asks and gets no answer is normal, not a stall.
  3. Every run's first job: read this page, match any replies since the last run, apply and ratify — the answer becomes a rule in Critic Rules or a line on the relevant page, so the question is asked once, ever. Then unblock what was waiting and say so.

The queue never blocks the work. An unanswered ruling holds that PR, not the run — everything else proceeds. Twelve unanswered questions means twelve parked PRs and a normal night otherwise.

Ask at most 3 new per run and carry the rest. Two runs a day is two chances to be seen, not double the questions. The cap is per run because it is a limit on what a person can absorb in one sitting, and the escalation clock below is in days, not runs, for the same reason.

Escalation is by age in DAYS, not by how many times it was shown. 2 days → mark stale · 3 days → the loop takes its own recommendation, applies it, and records that it did. Being shown six times in three days is not six chances ignored; it is one question that aged out, and the loop resolves it rather than asking a seventh time.

One entry per QUESTION, not per PR. "Which tier does this belong in" arriving from a review, the gap loop and the nightly audit is one entry with three blocked things — that is what earns it her attention.

Escalation is by age, not repetition. 2 nights → stale · 3 nights → the loop takes its own recommendation, applies it, and records that it did. The same question is never asked a fourth time.

Every entry names a recommendation. An entry without one is not ready to ask.

Open

1. Markdown source position — which spelling does the parser take? Blocks #5290 (lexs, contributor, open) · settles #5154 (Cindy's draft) · settles whether <Markdown> can ever emit positions

Two PRs add "where did this block come from" to the same parser: range as character offsets, vs position as {startLine, endLine}. They conflict in parser.ts — whichever lands second is a rewrite.

R6b applies: the second is the maintainer's own in-flight design, so this is not arbitration between peers. The real question is does the contributor's case change the design, and when does it land — not pick a spelling.

  • a offsets win — take range, drop position. Costs the nested case: a blockquote child's text is not contiguous, so no [start,end) addresses it
  • b lines win — take position. Costs streaming: the draft kills the incremental cache, 71.8ms vs 34.9 on a 14k doc, 2× and growing
  • c one option, both units, offsets derived from the line span — four numbers, nested blocks get lines only. Measured free: 36.7ms vs 36.2 for offsets alone. Build on #5290's incremental path ← recommended, one concept one spelling
  • d land both — BlockNode carries range and position forever

Whichever way this goes, the PR owes a perf test (Cindy, 2026-08-23: "because this has perf impact we should install perf tests"). The risk here is a lost incremental cache, not slowness — the draft's shape is 2× and growing on a 14k document because it invalidates every chunk. parser.perf.test.ts today asserts wall-clock milliseconds, which our own R18c forbids and which would have passed all four variants. Assert the invariant instead: blocks re-parsed per chunk. See R18c-cache.

2. The #4163 doc-contract PRs each ship their own hand-written test — keep, or one derived gate? Blocks 5 PRs: #4315 · #4316 · #4317 · #4319 · #4320

Each adds a contract test asserting the prop names that same PR just added — a tautology at merge time, five copies of getProps, and no drift detection once the docs and the source diverge later. #4163's own plan is a derived gate, like check-theming-targets.mjs. The docs are worth landing either way; the test mechanism is the question.

  • a land all five as-is — docs now, five tautological tests and five copies of the helper to unpick later
  • b land the docs, drop the tests, and file the derived gate — nothing guards drift in the gap ← recommended, the tests do not do the job they look like they do
  • c hold all five until the gate exists — docs wait on infrastructure nobody has scheduled
  • d land the docs and write the gate ourselves in the same pass — costs us the gate now

Raised 2026-08-23 by the batch-2 aged sweep.


3. Does the edge-adjustment family take a second mechanism, and what is it called? Blocks #4590 (jiunshinn, isEdgeAligned on List)

Your #2626 comment said follow Divider/Table and that this "depends on container awareness". The PR argues for a different mechanism and argues it well: measured, the Cancel margin mirrors Item's inset — x=16 aligned by default, x=20 with item on --spacing-3, and butter already does that on eight targets.

  • a container awareness as originally called, one mechanism — the PR is redone against it
  • b take the PR's mechanism, name it consistently with Divider/Table's spelling ← recommended if the measurements hold, it is already the shape in use
  • c both, explicitly scoped — a second mechanism on a public prop, permanently
  • d hold until a second consumer needs edge alignment

Raised 2026-08-23 by the batch-3 aged sweep.


4. isFullBleed on TabList — component prop, or the container mechanism we already have? Blocks #3938 (jiunshinn, 40 days old)

Same family as ruling 3 and probably the same answer: a container-driven bleed mechanism already exists, and this adds a per-component prop instead.

  • a the existing container mechanism — the PR is redone against it, no new surface
  • b take the prop, name it consistently with the family ← recommended only if the container mechanism genuinely cannot reach a tab bar
  • c both, scoped — a second spelling on public surface, permanently
  • d close it; not enough demand to settle the mechanism yet

Rule 3 and rule 4 are the same question twice — a per-component prop vs an existing container-level mechanism, for edge/bleed alignment. Answer them together and the answer is a standing default that closes the category (see What each ruling bought).


5. treegrid v1: row focus, or full cell navigation? Blocks #4690 (AKnassa)

This one has a clock on it — the author was told it is queued, after already waiting 10 days.

  • a row focus for v1, cells later — ships now, a second migration later
  • b full cell navigation now — correct against the ARIA pattern, larger, and the author has not signed up for it
  • c row focus, and say plainly in the docs that it is v1 ← recommended, ships the accessible thing without promising the rest

Raised 2026-08-23 by the batch-1 aged sweep.


6. Does the dismissal stack ship with the hover-layer machinery, or without? Blocks #4881 (ours, +3173/−346, 18 files) · decides whether it makes 0.5.0

Your sizing question from 16 Aug, now answerable. The review pass came back clean at round 2 — 13 scenarios measured two-sided in Chromium, 6 improve, 0 regress. On main today a modal-in-modal loses both, a required dialog lets the host behind it close, and a Lightbox or MobileNav over a required dialog is keyboard-unclosable. All fixed.

Tooltip and HoverCard are the only two needing the presence-at-press-time machinery. Everything else is green without it. So:

  • a ship whole — the four real bugs fixed together, one migration, largest diff
  • b ship without the hover layers, add them after — smaller, but Tooltip-in-Dialog stays broken and the stack has an exception in it from day one
  • c ship whole into 0.5.0 ← recommended, the unclosable-modal bugs are live today
  • d hold it out of 0.5.0 entirely — nothing regresses, everything stays broken another cycle

Note the release is currently cutting 0.4.8, not 0.5.0 — every changeset on main is patch and #5255's minor is unmerged. That is a separate decision but it lands on the same Monday.

Second half of the same question: does it land as ONE piece? Six components adopt the stack; six more layers still own their own Escape — BottomSheet, BottomSheetSwitcher, CommandPalette, ContextMenu, DropdownMenuSubMenu, PowerSearchEditPopover. A stacked layer inside an unstacked one is the double-dismiss the stack exists to fix (mechanism named, not yet measured). And #5322 merged today put a hand-rolled IME guard in BottomSheet, which is one of the six.

If the answer is "one piece", #4881 wants an integration branch (R26b) and the six migrations go into it. If it is "incrementally", the six need to be tracked now rather than discovered later.

Raised 2026-08-23 after the #4881 review-until-clean pass.


7. Does Markdown's rendered DOM carry what the SOURCE said, and behind what? Blocks the second half of #5288 (lexs) · settles the four remaining members #5290's changeset enumerates: escapes, emphasis characters, fence characters, heading depth

#5288 adds data-align on cells and data-delimiter on <ol> — inert to render, byte-identical output, but a read contract with no type, no declaration and no drift guard. BaseProps lets consumers pass any data-*, so component and caller share one unpartitioned namespace. #5290 raised this same question and deliberately left it to you.

Cindy's steer, 2026-08-23: "I don't know if we should have any guarantees for markdown to have these attributes by default. If it's a pass through I'm more okay with a clear pass through contract." — which reframes the question. The objection is not the attributes, it is Markdown guaranteeing them by default. A passthrough is a different thing: the caller asks, the caller owns the name, and the component promises nothing about it beyond forwarding.

  • a unconditional attributes as #5288 has them — Markdown guarantees them by default, permanent, unpartitioned, precedent set by silence
  • b a reserved prefix (data-md-*), still unconditional — partitioned, still a default guarantee
  • c the opt-in prop #5290 offered, reserved prefix, all members at once — one surface, but still ours to guarantee once switched on
  • d passthrough, not emission — the component forwards what a caller asks for and guarantees nothing by default. But the existing {...props} passthrough only reaches the ROOT, and a caller wanting data-align on every cell is asking for computation, not forwarding — so d either becomes f or collapses into c
  • e no DOM round-trip at all; consumers use parseMarkdown and the source they already hold
  • f extend components — no DOM contract at allrecommended

Why f. Cindy: "These features shouldn't be needing DOM introspection." MarkdownProps.components?: Partial<MarkdownComponents> is already public and shipped — a typed render-override map for code, inlineCode, citation, link, heading, paragraph, image, blockquote, hr. A caller who needs to know something about a node overrides its renderer and receives it at render time, in React, typed. No attribute, no querySelector, nothing to guarantee.

The precedent is exact. heading already receives a generated id slug — specifically so the caller can render it as the element's id and keep Outline navigation working. The system's existing answer to "the caller needs a derived value on a node" is hand it to the renderer, not stamp it on the DOM.

components has no table, tableCell or list entry — that gap is the actual finding, and filling it is extending a landed typed override, which is repair-shaped rather than new surface (R16f).

What still needs an answer if f is taken: selection genuinely lives in the DOM — getSelection() returns nodes, and the copy-out case has to map a node back to a block. Under f the caller builds that mapping themselves at render time (a ref, an index, their own attribute on their own element), and their name in their namespace is not ours to guarantee. Confirm that is workable for lexs before closing the question. Worth pinning before it is written.

The other half of #5288 is settled and needs no ruling — restoring BaseProps forwarding on List/Markdown is repair of a declared contract (R16f), and a real a11y fix: aria-label was silently dropped. Posted asking lexs to split them.

Raised 2026-08-23.


Raised 2026-08-23. Cindy's lean is c; the archaeology changed its shape from "both units side by side" to "offsets derived from lines". Note the two halves of c have different justifications — nested coverage is his own user's gap (copying one bullet out of a list gets the whole list), while line numbers serve only the consumer we would be closing.

Decided

# question ruling the general rule behind it now decided without asking still needs asking
(none yet — this page starts 2026-08-23)

What each ruling bought

A ruling that only settles one PR was half wasted. Cindy: "We should strive to keep a ledger of those decisions to inform and harden rules so the loop can handle more automatically over time."

So every ratified ruling records what it now lets the loop decide alone. That is the number that matters: not how many questions were answered, but how many future questions stopped existing.

When ratifying, ask three things and write the answers into the row:

  1. What is the general rule behind this answer? Not "5290 uses offsets" but "one concept gets one spelling; a second spelling needs the first to have landed." A ruling phrased about a PR teaches nothing.
  2. What class of question does it now close? Name it, so the next run can match against it instead of asking. This is the hardening.
  3. What would still need asking? The edge the rule does not reach — so the loop knows the boundary and does not over-apply a ruling into ground it never covered. Over-application is the failure mode here: a rule stretched past its evidence is worse than the question it replaced.

The health metric: rulings asked per 10 PRs should fall over time. It was measured at ~1.9 per 10 on 2026-08-23 (feat 62%, fix 0%) with 4 distinct questions behind 5 instances. If that number is flat after a month of rulings, the answers are being recorded but not generalised — the ledger has rows and the critic has no new rules.

Rulings already made in-session

These predate the page and are recorded so they are not re-litigated. All are in Critic Rules.

question ruling rule
Does finding another open PR adding the same surface establish the class? No. A class is made of LANDED members; a draft is weaker than an open PR, and a closed one is evidence of rejection. Mechanical test: are the other members merged? R22
A team member's in-flight design overlaps a contributor's PR — arbitrate? No. R6b: the team's system-level solution takes precedence; redirect rather than compare. Authorship changes what an overlap means R22 × R6b
Is a direct el.style ref write a defect to be replaced with StyleX? No — it may be deliberate for render count. Ask why before proposing the layer. R7c
May styling key off an ARIA attribute? Never. ARIA is an accessibility contract. Key off theme-exposed state; if it is not exposed, expose it. R7c
Does a new prop always lose to a mechanism change? No — R1g-surface is a high bar, not a ban. hasRowHighlight was fine. R7c
Where do competing PRs get resolved? One review for the set; hold all judgements until the last is reviewed, then post together, winner first. R6d
What does the loop owe a feature PR? Settle everything verifiable, hand over the shapes and one question. R22

Clone this wiki locally