-
Notifications
You must be signed in to change notification settings - Fork 0
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.
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."
- 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.
- 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. - 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.
Every blocked PR below re-checked against GitHub 2026-08-24 05:00 UTC — all still open, all still blocked for the reason stated.
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. #4590 still open, request-changes standing, last touched 2026-08-23 20:58 UTC.
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
#3938 still open, request-changes standing, last touched 2026-08-24 01:08 UTC.
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. #4690 still open at 2026-08-24 05:00 UTC — the clock is still running.
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 onlycenterandend— nostart— 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
componentsalready hands renderers typed values at render time (headingreceives a generatedidfor exactly this). It has notable/tableCell/listentry; 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.
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.
9. Six layers still own their own Escape — one integration branch, or six tracked PRs? Blocks nothing today · ruling 6 answered its parent, not this half
#4881 merged whole on 2026-08-24 without them, so "does it land as one piece" was answered incrementally — by events, not by choice. Six 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, still not measured), and #5322 put a hand-rolled IME guard into BottomSheet, one of the six.
- a six PRs, one per layer, ordered by risk and tracked on this page ← recommended — the stack has already shipped, so an integration branch has nothing left to integrate against
- b one integration branch (R26b) carrying all six — one migration for consumers, one large review
- c migrate a layer when it breaks — the double-dismiss stays live and gets found by a user
Raised 2026-08-24 out of ruling 6's unanswered second half. Nothing on GitHub tracks the six today — #3343 is the umbrella program, #5168 the nearest live bug. It blocks nothing today, so it is not a release block — this page is its record until it is answered.
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 |
Reference. Moved here 2026-08-24 from the pre-release debt register, which is now astryx/release_blocks. 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.
| 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 as kt-eo47
|
Markdown range → any other spelling |
None owed — ruling 1 settled as landed. range publishes as {start, end} offsets; there is no second spelling to migrate to |
Banner defaultIsExpanded → collapsible
|
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 |
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:
- 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.
- 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.
- 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.
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 |