Skip to content

Rulings

Cindy Zhang edited this page Aug 24, 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.

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.

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.

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? — WITHDRAWN, 2026-08-23

Asked, then dissolved by asking a better question of the same PR. Kept because the dissolution is the useful part.

The question assumed #5288 posed one problem. It posed two, and separating them removed the ruling:

  • Rendering. cellAlignStyles (Markdown.tsx:238) has only center and end — no start — so | :--- | renders identically to | --- |. Fine in LTR by accident; wrong in RTL, where an explicitly left-aligned column goes right. A real bug, no consumer needed, and the PR does not fix it. → R3j
  • Introspection. Reading the source back out of the DOM. Real need, but not a guarantee to bury in Markdown's contract undisclosed — and components already hands renderers typed values at render time (heading receives a generated id for exactly this). It has no table/tableCell/list entry; that gap is the actual finding.

Cindy: "The former is something to fix, the latter is not a guarantee in our contract a test should rely on."

Posted as request-changes asking which problem is being solved. What may still need a ruling later, and only if lexs says components does not serve him: do we owe a round-trip path at all? Much smaller than the original question.


8. Is a menu divider's vertical rhythm a theme value, or spacing the menu owns? Cost #4667 and #4743 their subject — asked in round one of #4743, never answered, and both PRs deleted the contested part rather than wait. No divider fix has shipped from either.

Cindy, on #4743: "I'm working through a rule I haven't written into the wiki yet, so treat this as direction rather than settled policy: layout properties encode a component's structure, so they shouldn't become theme seams. … I said on #4667 that a themable divider seemed okay to me, and I still lean that way for its paint. Margin is the part I want to be deliberate about."

  • a paint is themable, layout is not — a divider's colour and thickness are theme values, its margin is the menu's ← recommended, and it is her own lean written as a rule
  • b margin too, as a public var — what #4743 originally proposed
  • c neither; the menu owns the whole divider

Raised 2026-08-23 out of the #4743 retraction. Not attached to that PR — it is merged. Answering it also settles the general rule, which is what the two dead attempts were really waiting on.


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