Skip to content

Rulings

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

Rulings

Authority correction — 2026-08-31: This page preserves historical owner questions and answers; it is not decision authority. Only current in-repo specs can settle judgement. An authoritative source is a committed specification inside facebook/astryx declaring authority: current. Decision-window expiries, auto-ratified recommendations, fork-wiki rulings, merged code, issues, component docs, and prior reviews are context/evidence only. Unanswered questions never auto-resolve.

Taste questions the loop cannot settle, and what was historically proposed or answered. A reusable answer must be recorded in a current in-repo spec before a reviewer applies it.

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

Runs may ask and track; they never self-decide.

  1. Put open owner questions in the final report block so they remain visible.
  2. Cindy may reply in that thread. Preserve her words as decision evidence and route them to the repo-spec owner.
  3. The answer becomes reusable only after a maintainer records the boundary in a committed authority: current spec inside facebook/astryx. Until then the PR remains novel-human, AUTHOR CAN PROCEED: no, with no public REVIEW, post, or merge.

The queue never blocks unrelated work. An unanswered question holds that PR, not the run. Age changes visibility, never authority: mark an old question stale, but never take the loop's recommendation or close the question on its behalf.

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

Every blocked PR below re-checked against GitHub 2026-08-24 05:00 UTC — all still open, all still blocked for the reason stated.


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."

#5288 is still open with that request-changes standing (2026-08-23 23:08 UTC). 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.


Historical decisions and proposals — not review authority

11. PR #4022 should drop the unreviewable scripts and keep the base-theme contrast fix — DECIDED 2026-08-27 by Cindy.

For #4022, do not approve running the two added executable files. Ask the author to remove them and keep the emphasized-border contrast update in the base theme as the focused change.

Cindy: "hmm scripts added are not in english, I think we can suggest skipping the scripts and just taking the contrast update for the emphasized border in the base theme"

Historical recorded outcome (not review authority): when a fork PR’s executable additions cannot be confidently reviewed and are not required for its focused source change, request their removal and review the source change separately; do not seek permission to run them.

Still needs asking: an executable addition that is essential to the stated change and cannot be separated, after a static safety read explains exactly what it does.

Ratified from Cindy’s reply in the review-action thread. Routed to the reviewer session for application to #4022.


10. ChatComposerDrawer keeps the collapsed count chip — HISTORICAL AUTO-RATIFICATION — NON-AUTHORITATIVE.

#5399 does not replace the collapsed Badge with {count} {label}. The decision window expired without an owner answer, so the loop applied option a. The plain-text spelling cannot pluralize a free-form consumer label, drops CJK measure words and number formatting, and removes the count's only theme handle. If the chip feels too heavy, its paint is a theme decision on the existing Badge rather than a second count grammar.

Historical recorded outcome (not review authority): free-form labels and counts are not composed in JSX as a sentence. This collapsed count stays a chip; a visual-weight adjustment uses the existing theme surface.

Still needs asking: replacing the free-form label with a catalog or ICU-shaped API, or changing count-indicator treatment system-wide.

Historical auto-ratification; non-authoritative as of 2026-08-31. The existing request-changes remains; no close was performed.


9. The remaining dismissal-layer migrations ship as six tracked PRs — HISTORICAL AUTO-RATIFICATION — NON-AUTHORITATIVE.

The shared dismissal stack already shipped, so there is no integration branch left to integrate. The six remaining owners migrate independently, ordered by measured risk: BottomSheet, BottomSheetSwitcher, CommandPalette, ContextMenu, DropdownMenuSubMenu, and PowerSearchEditPopover. #3343 remains the umbrella and #5168 the live cross-layer bug.

Historical recorded outcome (not review authority): one reviewable PR per layer, each proving its nested-layer and IME behavior against the shared stack. This page tracks all six as unstarted until a PR exists.

Still needs asking: a migration that changes public behavior beyond adopting the landed stack, or evidence that two layers cannot be separated safely.

Historical auto-ratification; non-authoritative as of 2026-08-31. No implementation PR was created by this ledger-only transition.


8. Menu-divider paint is themable; layout belongs to the menu — HISTORICAL AUTO-RATIFICATION — NON-AUTHORITATIVE.

A divider's color and thickness may be theme values because they describe its paint. Its margin and surrounding rhythm stay with the menu because they encode structure. This preserves the direction already recorded on #4743 without reopening the two withdrawn implementations.

Historical recorded outcome (not review authority): component theme seams may expose divider paint, not structural spacing. A menu owns the gap around its divider.

Still needs asking: a divider whose thickness changes layout rather than paint, or a shared layout primitive that owns spacing independently of the menu.

Historical auto-ratification; non-authoritative as of 2026-08-31. #4667 is closed and #4743 is merged; no PR action was needed.


5. Treegrid v1 uses row focus and documents that scope — HISTORICAL AUTO-RATIFICATION — NON-AUTHORITATIVE.

#4690 may proceed with row focus for v1, composing the existing tree and grid focus primitives and documenting that full cell navigation is outside the first version. Existing interactive descendants must remain reachable; combinations that require cell navigation are not silently claimed as supported by the v1 contract.

Historical recorded outcome (not review authority): a first treegrid may use row focus when its documented supported rows remain operable, and its docs must name the v1 limitation.

Still needs asking: full cell navigation, an entry mode for interactive descendants, or support claims that make either necessary.

Historical auto-ratification; non-authoritative as of 2026-08-31. The author can proceed against this scope.


15. Drawer pane scope / modality proposal — HISTORICAL, NOT AUTHORITATIVE.

This entry was previously marked decided by decision-window expiry. Cindy withdrew that authority on 2026-08-31. No applicable authority: current spec in facebook/astryx settles whether Drawer should add independent modality and hasScrim axes. Current architecture records leave component modality to the dialog family, and current main has no Drawer.spec.md. The choice is novel-human; the proposal below is preserved only as history/evidence.

The contract. containerRef selects scope only: viewport when absent, the referenced element when present. Modality is a separate explicit public axis, modality?: 'modal' | 'nonModal', defaulting to 'modal'. hasScrim controls whether scrim paint is present; dismissal and underlying operability follow modality. Neither containerRef nor hasScrim may silently change focus containment, inertness, pointer blocking, or aria-modal.

A modal Drawer makes content behind it in the chosen scope unavailable to both pointer and sequential keyboard interaction and restores focus on close. A non-modal Drawer leaves that content operable by both. aria-modal is emitted only when its global semantic promise is actually true. The implementation may choose the shared mechanism, but it may not ship a scope where the scrim blocks one input modality and not another.

Impact. A builder can move the same inspector between a page and a split pane without changing what hasScrim means or discovering a hidden focus policy. A keyboard user cannot activate a dimmed control that a pointer user is blocked from. Existing viewport and new pane call sites state modality in the same vocabulary, so a later host change does not silently rewrite interaction.

Why this owner. Drawer already owns the side-panel concept, inline edge, width/reveal, entry/exit, close affordances, focus restoration, and sibling LIFO contract. The demonstrated class is still a Drawer—split-view, dashboard, canvas, and chart inspectors—not the card/media scrim owned by Overlay and not a new component. Scope belongs on Drawer; modality does not belong to scope.

Current unresolved boundary: the entire proposed Drawer split—pane scope, modality, and scrim semantics—still needs a human decision because no applicable current in-repo spec settles it. Lab reversibility changes urgency, not authority.

Current state: AUTHOR CAN PROCEED: no; ask one private owner question; no contributor-facing REVIEW, post, or merge until a current in-repo spec records the decision.

Historically auto-ratified from Review 5550 round 2; explicitly withdrawn as decision authority on 2026-08-31. No public PR action was taken in that round.

14. Spinner paint-variable proposal — HISTORICAL, NOT AUTHORITATIVE.

This entry was previously marked decided by decision-window expiry. That auto-ratification is withdrawn as authority. The proposal below remains useful history, but the loop may apply it only if an applicable current in-repo spec with authority: current settles the same boundary.

#5214 proposed keeping --spinner-color and --spinner-track-color alongside its geometry pair.

Impact. A theme author can control the moving arc and stationary track independently on the spinner target. They do not have to know whether each host established a usable currentColor; on current main, only Button and CheckboxInput explicitly choose shade="inherit" among 23 production Spinner call sites. Equivalent loading indicators therefore keep one predictable theme contract across hosts.

Why this tier. The track has its own visual role (--color-track, plus the existing reduced-alpha treatment for on-media/inherit shades), so it is not merely a fixed ratio of the arc. Two permanent names buy a capability the host cascade cannot guarantee. The current head pays the public-surface cost with one existing target, both locales, doc-driven theme-build coverage, exact-head reachability, and browser evidence across default, themed, hairline, sizing, and labelled states.

Historical proposal (not authority): a component may expose an explicit paint var when a theme must control paint independently of host currentColor, the visual part has its own semantic role, and the variable is documented and driven through the component's existing target.

Current state: apply this proposal only if an applicable current in-repo spec records it; otherwise the public theme-contract choice is novel-human.

Historically auto-ratified from Review 5214 round 8; explicitly withdrawn as decision authority on 2026-08-31. No public PR action was taken.

13. PR 5204 disposition — INSTRUCTED 2026-08-27.

"review loop on 5204 approve and merge if good"

"coordinate this with the epic for richtextarea" — Cindy

Run the independent review loop on #5204, reconcile its scope and disposition with the RichTextArea epic #981, and approve and merge only if the loop is clean. This is a PR-specific instruction, not a general review rule.

Outcome, 2026-08-27: the loop requested changes at head 8f470c9e; review posted, so nothing was merged. The canary-vs-stable scope was recorded on #981. Full record: Review 5204.


12. Which tier owns a header's block-end dock? — ANSWERED 2026-08-26. LayoutHeader owns it.

"using the tablist, the underline is in the right place so it's an improvement over the current one." — Cindy

The ownership split is ratified:

  • TabList owns inline full bleed. Its existing isFullBleed implementation is accepted; it reaches the inline edge and compensates labels from the container variables. There is no remaining choice about the prop or whether the behavior is an improvement.
  • LayoutHeader owns the block-end dock. Add paddingBlockEnd?: SpacingStep in the per-edge padding family already published by Section, then use paddingBlockEnd={0} on detail-page. The whole tabsRow object and style={tabsRow} disappear; no replacement constant remains.

This is the smallest general owner because LayoutHeader publishes the block padding and paints the divider. The driven arm preserves all five targets at both tested header paddings; the exact-head marginBottom: -16 misses the divider by 7.00px at padding={6}, while the LayoutHeader arm remains docked at −1.00px. The arm also passes the 0/8/16/24px, RTL, hug/fill, trailing TabMenu, and prop-false matrix.

The alternatives are closed: a bottom slot adds a new concept and gap rule; a TabList-owned block margin cannot see the header geometry and moves the ghost; the existing Section seam restates the magic number; keeping template CSS contradicts ruling 4.

The remaining work on #3938 is implementation, not design: land the LayoutHeader prop and zero-CSS template usage, and move the geometry assertion into a required Chromium CI path. Full measurements, frames, and exact arm diff are in Review 3938 round 3b.

Ratified 2026-08-26 by Cindy after reviewing the current-head and LayoutHeader-arm redline. LayoutFooter.paddingBlockStart remains outside this PR's scope.

3. isEdgeAligned on List — ANSWERED 2026-08-25. It is isFullBleed, and it needs a clamp.

"Full bleed as a component prop is okay. It cancels out container padding. List items are weird though. We technically cancel list item padding but only up to container padding.""I dislike adding another prop naming convention.""Maybe we need to rename the whole thing but I don't want to do breaking changes like that yet." — Cindy

One prop name across the family: isFullBleed. Read at the CONTENT level, not the box level, it means the same thing everywhere — your content reaches the container's content edge:

component what moves amount
Divider the line the container's full padding (no inset of its own)
List the row label its own 8px inset, clamped
TabList the labels, and the underline to the divider its own inset, clamped

The amount differs per component; the promise does not. Additive, no rename of anything shipped, no breaking change — which is the constraint.

THE CLAMP, and it is the part #4590 is missing:

marginInline: calc(-1 * min(var(--_item-inset-inline), var(--container-padding-inline-start, 0px)))

Cancel the component's own inset only up to the container's padding. #4590 today cancels the inset outright. In a Card that is 8 against 12, so the clamp never bites and it looks right — but in a popover (12px) or a 0px full-bleed area it over-pulls and the rows hang outside the content edge.

The clamp also makes the design doc's special case disappear: "Except for 0px full bleed, then just render it normally" falls straight out of min(inset, 0) = 0. Nothing to special-case.

Source of truth: the internal “Layouts and Spacing” design, "Content spacing compensation". Container padding is 12px for popovers, 16px otherwise, 0px full bleed. Per-component rules: buttons and inputs 8px from the edge in headers/footers/toolbars/corners; lists always −8px matching their inner padding; tables and scroll areas flush.

Noted, not acted on: the doc says lists do this always, where #4590 makes it opt-in. Left opt-in for now — making it default is a behaviour change on shipped surface and this ruling is deliberately additive. Worth revisiting with the rename Cindy says she does not want yet.


4. isFullBleed on TabList — ANSWERED 2026-08-25.

On a component with content of its own, isFullBleed does BOTH halves.

"I don't want breaking the current default behavior.""The template shouldn't need extra css if we're adding the prop — that's the whole point of adding it.""Yeah that seems okay to do both actually." — Cindy

The two halves, and why one is not enough:

what it does computed from
bleed pulls the box out through the container's padding so the strip spans edge to edge — this is what makes the active-tab underline meet the header divider calc(-1 * var(--container-padding-inline-start/end))
compensate pads the first and last tab back in by that same amount, so the label lands on the container's content inset max(var(--container-padding-inline-start, <fallback>), --spacing-2) — Table's containerEdgeStyles, table.stylex.ts:73

They cancel at the box level and do not at the content level. That gap is the measured 4px: the box moves 16, the label was already inset 12, so it lands 4px past the body text below it.

Divider only ever needs the first half — it is a line, it has no content to align. Copying Divider's prop onto a component that has labels gets you half a feature, which is exactly what #3938 shipped.

The acceptance test is the template. detail-page's tabsRow must lose ALL its hand-written CSS — not shrink, disappear. A template still carrying a style={{marginInline: -12}} after the prop lands is the prop failing its purpose. The -12 is also a magic number: the container publishes 16, and Section.tsx:66 already shows the right spelling (calc(-1 * var(--container-padding-inline-start, 0px))).

Everything is computed from the container system vars. No hardcoded amount anywhere — not in the prop, not in the template. That is what makes it correct at any container padding and any density, and it is why the 4px cannot come back.

Not --container-padding-*-blind: a standalone TabList, where those vars are unset, keeps its current geometry via the fallback. Same shape Table uses so a standalone table is unchanged.

Ruling 3 stays unpaired from this. Different families: bleed cancels the container's padding; edge compensation cancels a component's own inset. #4590 is the second, and its measurement (12px vs 8px in a Card) is what proved the two are not one question.

Open, for whoever implements it: Toolbar is already a landed member of the edge-compensation family (edgeCompSlot.inset(...), four call sites) and its own docs show a TabList inside it. The template hand-rolls <HStack style={tabsRow}> where a Toolbar may already do this. Worth measuring before building the prop — if Toolbar solves it, #3938 closes as unnecessary and the template gets simpler.


Answered by Cindy, or answered by what landed. Three moved here on 2026-08-24.

# question ruling the general rule behind it now decided without asking still needs asking
1 Markdown source position — which spelling does the parser take? Offsets, opt-in — option a, not the recommended c. #5290 merged 2026-08-23 20:51: range behind {sourceRanges: true}, top-level blocks only, nested children carry none. #5366 reshaped it to {start, end} an hour later. #5154 closed When two spellings of one concept compete, the one that keeps the existing invariant wins. Coverage bought with a lost cache is not free coverage. A second unit waits for a consumer the first is shown to fail, and new introspection ships opt-in and absent by default Any "add a second unit for what we already emit" PR — lines beside offsets, ids beside paths: no, until the first spelling provably fails someone. And any new introspection surface: opt-in, or it is not additive The nested case — a blockquote or list child carrying a range means threading a base offset through two recursive parses, a design change and not an addition. And whether <Markdown> emits ranges into the DOM at all (#5290 deliberately left data-md-range out): new public surface, R1g-surface, a fresh ruling
2 The #4163 doc-contract PRs each ship their own hand-written test — keep, or one derived gate? Option b, and she had already answered it hours earlier: "Docs only good to merge as long as they're correct", then "Docs don't need tests". #4315 · #4317 · #4320 merged 01:24, #4316 at 04:14; #5382 deleted all five tautological tests (+0/−263). The gate was never filed Docs are graded on correctness, not on coverage. A test written in the same PR as the thing it asserts proves only that the author typed both. Drift is caught by a gate derived from the source, or it is not caught Every "should this docs PR carry a test" question: land it if it is correct. And a hand-written mirror of the source in any PR is a finding, not a merit When the doc file is runtime input — consumed by the CLI or codegen — it is code and takes real tests. And who schedules the derived gate: filed 2026-08-24 as #5421
6 Does the dismissal stack ship with the hover-layer machinery, or without? Option c — shipped whole. #4881 merged 2026-08-24 03:50 as 47d5160d, +3706/−394 across 22 files (the ask said +3173/−346, 18 files). Its changeset is [breaking]/minor, so the cut is 0.5.0, not 0.4.8. Cindy: "just classify it correctly don't try to game the versioning" The changeset category describes what the change does to public meaning; the version follows the category, never the reverse. A published prop whose name is unchanged and whose meaning is not is breaking — no codemod can carry that, so the changeset is the only warning a user gets Any "can we call this a patch to stay in the line" question: no. Any meaning change to published surface: [breaking], and it lands in the release breaking changes land in Surface that is merged but unreleased is not covered — renaming it before the cut is free, and that is a different call. Release timing — which Monday a breaking stack cuts — stays hers

| 10 | Is the checkbox's label target astryx-checkbox-label or astryx-checkbox-input-label? | astryx-checkbox-label — keep it. Cindy, 2026-08-24: "It's fine." #5183 merged as 75b5af29 with the short spelling. The convention argument was real — the root target is astryx-checkbox-input, <root-target>-<part> holds across all 238 targets on main, and the same doc file deprecates astryx-checkbox three lines above — and it lost anyway | A convention derived by grep is evidence, not a verdict. 238-for-238 establishes what the system does; it does not establish that a shorter name is wrong. Where a target reads unambiguously to a theme author, brevity beats derivational consistency, and the person who owns the system decides which one this is | Any "this name breaks the <root>-<part> pattern" finding on an unreleased target: state it once as a note, never as a hold, and never as the reason a PR does not merge. A name that reads right is right | The twelve remaining #5417 rows are not settled by this — each is its own read, and this ruling says the pattern does not decide them either way. And a target that a theme has already reached is frozen regardless: this only covers naming before first use |

| 11 | A component-scoped themeable glyph — new member of the core IconName union, or a namespaced extension key? | Namespaced, to start. Cindy, 2026-08-24: "Probably namespaced to start""for component icons". Raised by #5466 (numberInputStepperDown); the enabling change is #5475, which makes <Icon icon="numberInput:stepperDown">, useIcon and defineTheme({icons}) take the key | The union is the expensive tier, and expense is measured on the people who did not ask for the change. IconRegistry is Record<IconName, ReactNode>, so a union member adds a required key to every downstream registry and a scaffolded theme stops compiling on a patch. A key one component owns goes in the tier that costs nobody else | Any "give component X its own semantic icon name" PR: namespaced key, no IconName member, Core default in defaultIcons under the same key. The 13 other components sharing chevronDown are covered by this without asking again | Whether a glyph is component-scoped at all — a name several components would resolve is a system concept and belongs in the union. And whether the namespaced tier stays the answer once several components use it: "to start" is explicitly provisional |

| 12 | What must happen after a feature/system ruling, and are old template-gap issues implementation-ready? | The ruling must become the public disposition. Cindy, 2026-08-27: “These should all have at least been needs human,” and “you didn't post request changes on some of these PRs where you had that ruling.” #3934 and #3935 are new feature/system work whose issues stay needs-scoping; #4053 and #4158 are old implementations whose template-derived issues must be re-examined before replacement work; WCAG color-token attempts including #4022 are owner-led rather than parallel contributor pickups; #4228 stays human-blocked even though 7eb427ea fixed the prior 16px/20px indicator alignment defect—the fix settled correctness, not the system decision | Issue detail is not approval, and a private verdict is not delivery. A new feature/system gets human review. After the human decides, request-changes is posted as a GitHub review; closure gets a closure comment. Template-derived gaps are revalidated against current direction before implementation | Future new feature/system PRs are automatically Needs human; needs-scoping blocks implementation; template grading evidence cannot by itself authorize a component/API; a settled request-changes verdict must be posted | Which owner-approved system shape replaces a gap, and whether a currently scoped feature remains worth building, still need the responsible owner |

| 13 | Can a contributor-authored vibe-test feature be accepted when the implementation is sound but the PR does not prove why the project needs it? | No—human block, and close when the owner has not chosen the capability. Cindy on #4545, “feat(vibe-tests): add MiniMax design judge provider”: “I dont' think we're ready to accept any PRs for vibe tests from contrib.” New feature additions need clear problem and impact statements; #4545 should have been routed as a human block, not accepted | Implementation quality cannot supply product need. A new feature must name the person, current failed task, demand evidence, experienced impact, and why existing mechanisms do not reach. Maintainer-owned test/vibe infrastructure also needs an owner willing to absorb the new surface | Reviews can reject mechanism-only PROBLEM/IMPACT statements; contributor vibe-test features route to human before acceptance | Whether a specific owner-backed vibe-test capability is wanted, and its boundary, still belongs to that owner |

How the version is decided

Reference. Moved here 2026-08-24 from the pre-release debt register, which is now the internal release-block register. Ruling 6 is the taste half of this; below is the mechanics.

The changeset category picks the version, never the reverse. scripts/check-changesets.mjs couples them: a [breaking] changeset must be minor (0.x.y → 0.(x+1).0), every other category must be patch, and major is rejected while we are pre-1.0.

Cindy, on exactly that temptation: "just classify it correctly don't try to game the versioning." Stated intent, 2026-08-22: "we're cutting 0.5.0 Monday and that's the release we're landing breaking changes in."

The 0.5.0 cut, as it stood on 2026-08-24. @astryxdesign/core published at 0.4.7 with 29 pending changesets, all patch but two: #4881 (the dismissal stack) and #5255 (Banner collapsible), both [breaking]/minor, both merged that morning. One honest category on one merged PR is what moved the release — not the six feature-shaped changesets sitting beside it.

Codemods owed

item codemod owed?
MultiSelector.formatTriggerCount rename Not if it happens before the cut — the prop is unreleased, so there is nobody to migrate. After the cut: yes, plus a [breaking] changeset. Tracked in the internal release-block register
Markdown range → any other spelling None owedruling 1 settled as landed. range publishes as {start, end} offsets; there is no second spelling to migrate to
Banner defaultIsExpandedcollapsible Shipped. banner-collapsible-content is staged in transforms/next/ and promotes at the cut (verified end to end by simulating the promotion). It skips props-object sites deliberately; the changeset names the spread hole and says to grep — see Review 5255
hasActiveFocusTrapEscape meaning change (#4881) No codemod can carry it — the name is unchanged and the meaning is not. The [breaking]/minor changeset is the only warning the user gets

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%) — 12 blocked PRs across the 64 reviewed.

2026-08-24: 0.5 per 10. Nine of those twelve came unblocked in one night — ruling 1 (2 PRs), ruling 2 (5), ruling 6 (1), ruling 7 dissolved (1) — leaving three blocked (#4590 · #3938 · #4690) against the same 64.

What moved it was answers landing, not questions going unasked. One new question was raised (ruling 9), and it came out of an answered ruling's unanswered half — which is the honest way for the number to grow. The test of the three Decided rows is not that the number fell tonight; it is whether it stays low because those rows stopped the next questions from being asked. If it climbs back with questions the rows already answer, 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