Skip to content

fix(plugin-kanban): announce "No cards" by card count, not by lane count - #9169

Merged
os-tesla merged 8 commits into
mainfrom
claude/issue-9045-kanban-empty-state-lane-count
Sep 12, 2026
Merged

fix(plugin-kanban): announce "No cards" by card count, not by lane count#9169
os-tesla merged 8 commits into
mainfrom
claude/issue-9045-kanban-empty-state-lane-count

Conversation

@os-tesla

@os-tesla os-tesla commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator

Fixes #9045
Fixes #9170

KanbanImpl derived its board-level empty state from
totalCardCount === 0 && boardColumns.length > 1. The second conjunct is a
lane count, and it made the announcement unreachable on two shapes:

  • a zero-lane board — no lanes at all, so nothing on screen said anything;
  • a one-lane board — the board-level live region never painted, and the only
    "No cards" string was the lane's own dashed placeholder: a plain span with
    no role and no aria-live.

DataEmptyState is the board's only role="status" aria-live="polite" region,
so on both shapes a screen-reader user was told nothing at all.

The predicate now asks whether there are any CARDS: totalCardCount === 0.

⚠️ This PR has TWO landing rounds and one withdrawn one. Round one is
everything down to "Acceptance notes". Round two is objectui#9170 — the half that
round one's own predicate change made necessary, since widening the region to
paint at one lane made "1 columns" reachable and the repository's own tripwire
caught it. An earlier attempt at round two took a different route and was backed
out in full; the history below says why, and the commits are left in place rather
than rewritten.

⭐ The premise is live, and that is asserted rather than inherited

A lane-less object-kanban document could not pass validation until
objectui#9021 made ObjectKanbanSchema.groupBy optional, as the protocol
declares it. Re-checked on this tree by a leg named LIVE PREMISE, which parses
{ type: 'object-kanban', objectName: 'deal' } green through both published
faces (ObjectKanbanSchema.safeParse and safeValidateSchema). Without that
widening the zero-lane rows below would be measuring an unauthorable shape.

The predicate is older than the widening. ⛔ This is not a defect
objectui#9021 introduced — it is what made it reachable.

⚠️ The lane count was NOT guarding the loading state — measured, not assumed

The plausible reading is that > 1 separated "still loading" from "genuinely
empty", since a board mid-flight can look lane-less. It did not. That
distinction is carried by a separate conjunct,
showEmptyState = isBoardEmpty && recordsSettled (objectui#8827), which this PR
does not touch.

Established two ways, not by argument:

  1. Two STILL LOADING legs drive a zero-lane and a one-lane board with the
    query held on a deferred promise, assert nothing is announced, then release
    it and assert the announcement arrives. Both halves are load-bearing — the
    first alone stays green if the empty state is deleted outright.
  2. Ablation M3 below removes the recordsSettled conjunct and only those
    two legs go red, on their in-flight half, with the message "nobody may claim
    the board is empty before the answer arrives"
    . The lane count is not what
    holds the claim back; that conjunct is.

What a one-lane board now renders

Exactly what a multi-lane empty board has always rendered: the board-level live
region, and no per-lane placeholder. suppressEmptyPlaceholder is unchanged
— its own stated reason is that the board-level state is already saying it, so a
per-lane copy would be a duplicate, and on a one-lane empty board that reason is
now true where it used to be vacuous. The board-level region is a live
region and the placeholder never was, so the visible affordance moves up one
level while the announcement is gained. The ONE LANE leg pins that the string
appears exactly once.

Acceptance

row board announces? leg
subject zero-lane, settled, no cards yes (was: no) ZERO LANES
subject one-lane, settled, no cards yes (was: no), exactly once ONE LANE
lit control one-lane with a card no LIT CONTROL
loading zero-lane, query in flight no, then yes on settle STILL LOADING
loading one-lane, query in flight no, then yes on settle STILL LOADING
⛔ NON-REGRESSION multi-lane with cards no NON-REGRESSION
⛔ NON-REGRESSION multi-lane, no cards yes, as it always did NON-REGRESSION

The two NON-REGRESSION rows were already correct on the parent commit:
in the baseline run below they passed while the two subject rows failed. ⛔ They
are not evidence of this fix and are labelled so in the file itself.

Baseline — the pins measured against the parent commit, before any edit

pnpm exec vitest run packages/plugin-kanban/src/__tests__/emptyStateLaneCountBlind-9045.test.tsx
BASELINE_EXIT=1        Tests  4 failed | 4 passed (8)

AssertionError: a zero-lane board holds no cards and must say so: expected false to be true
AssertionError: withholding it forever is the regression this must not trade for: expected false to be true
AssertionError: one lane is still a board that holds no cards: expected false to be true

Both STILL LOADING legs failed on their second half only — nothing was
announced while in flight even before the fix, which is what makes them a pin on
recordsSettled rather than on this change.

Ablation — round one

Each leg mutates KanbanImpl.tsx on disk, proves it both directions
(occurrences of the removed text and of the injected text, plus the blob hash
moving), runs the pins, then restores with git checkout HEAD -- ABSOLUTE_PATH
under a trap and proves the restore by state: git hash-object back to the
HEAD blob and git diff HEAD empty. ⛔ Never by an exit code. No build is
involved — vitest resolves this package from src, so the mutated bytes are the
bytes under test.

HEAD blob for the file at the time: a349d334.

leg mutation blob after result
M1 restore && boardColumns.length > 1 7b8d7c52 4 failed / 4 passed — both subject rows red, both non-regression controls green
M2 && boardColumns.length > 0 (the half-fix) 651b44c5 2 failed / 6 passed — only the two ZERO LANES legs red; both ONE LANE legs pass
M3 drop && recordsSettled dc575ac6 2 failed / 6 passed — only the two STILL LOADING legs red, on their in-flight half

M2 is the trap detector. It is exactly the "fix the obvious half" shape,
and the zero-lane rows fail alone under it while the one-lane rows pass — which
is how the two rows are shown to be able to fail independently, not as one
shared cause.

Prose round one falsified, corrected in place

Three present-tense statements described the removed conjunct as live; all three
are now correct. ⭐ No assertion moved — the diff of
recordsSettledEmptyState-8827.test.tsx is comment-only, so the legs that file
owns remain untouched controls.

One fixture repair was genuinely needed: laneLessBoard-8990.test.tsx's
laneTitles helper scooped any h3, and the empty state renders its title
as one. That only became reachable on a lane-less board here, and the leg
asserting "no lane key and no declared lanes: no lanes" went red with
expected [ 'No cards' ] to deeply equal [] — measured before it was touched.
The helper now excludes the live region, restoring what it says it returns.
⛔ Not a loosening: the legs that assert on lane titles compare against lane
values and picklist labels, neither of which that filter can remove.

Verification — round one, at a8b1cdfcb

what command result
package + kanban-rendering consumers pnpm exec vitest run packages/plugin-kanban/ examples/schema-catalog/test/kanban-column-cards-6939.test.tsx examples/schema-catalog/test/catalog-gallery-render.test.tsx packages/app-shell/src/views/ObjectView.kanbanGroupByRetired-8213.test.tsx Test Files 54 passed (54) · Tests 923 passed (923), VERDICT command-exit 0
typecheck pnpm --filter @object-ui/plugin-kanban run type-check exit 0
typecheck really reads the new pins tsc -p tsconfig.test.json --listFiles the new file is in the list — asserted, not assumed
changeset presence node scripts/check-changeset-presence.mjs exit 0
gates check:new-line-citations, check:control-bytes, check:i18n-keys, check:unreferenced-sources, check-changeset-no-major.mjs all exit 0

Everything else is CI's run, not this seat's.

Acceptance notes

The empty state's description read "1 columns" on a one-lane board — filed as
objectui#9170 and REPAIRED HERE, in round two below.
DataEmptyState's
description was composed as the lane count plus the pack's kanban.columns unit
word, a bare plural with no singular form. Measured on this branch, the live
region's text was "No cards0 columns", "No cards1 columns" and
"No cards2 columns" at zero, one and two lanes. Only the one-lane form is
ungrammatical, and it became reachable here, since the region previously
never painted below two lanes.

⚠️ This paragraph used to end by declining the repair, on the grounds that it
needs a change to the published @object-ui/i18n catalogue and round one was
claimed Clause-②: no. That deferral did not hold: the repository's own
source-text tripwire in
packages/i18n/src/__tests__/residue-namespaces-3546.test.tsx pinned the > 1
guard because it was what made the bare plural safe, so removing the guard
turned this PR red and it could not land without the repair. ⇒ round two.


Round two — objectui#9170: the announcement drops the lane count

What the tripwire caught, and why it was right

Test (shard 1/4) was red on exactly one assertion, at
packages/i18n/src/__tests__/residue-namespaces-3546.test.tsx:692:

AssertionError: the >1 guard moved — a plural family would now be required:
  expected  …  to contain 'const isBoardEmpty = totalCardCount === 0 && boardColumns.length > 1;'

That pin was neither stale nor broken. Its own comment stated the rule it
guarded: en is plural-only and that is safe because the empty state only
renders above one lane, so the count can never be 1. Round one removed that
conjunct, so the premise was gone and the pin fired exactly as designed.

Reproduced locally on the round-one head before anything was edited:
Test Files 1 failed (1) · Tests 1 failed | 42 passed (43), on that line.

⚠️ An earlier attempt took the wrong route and was backed out in full

objectui#9170 offered three routes and the maintainer chose route 3 — drop
the lane count from the description — recorded verbatim on the card at
2026-09-12T02:14:55Z, with route 1 (a plural family across ten locales) named
explicitly as a fallback that is not needed. A later ruling at 07:24Z
ordered route 1 instead; it was issued without reading the card's comments and
has since been retracted in full by the seat that issued it.

Route 1 was implemented on this branch and is reverted by 6e8856066, which
restores the tree byte-for-byte (git diff a8b1cdfcb5 names zero files). ⛔ Not
a force-push and ⛔ not a history rewrite: the attempt and its withdrawal both
stay readable. What lands is route 3, not a hybrid of two rulings.

The repair

The board-level DataEmptyState declares no description. The live region
announces its title only, at every lane count.

⭐ That makes "no plural family is needed" true by construction rather than
by a predicate: there is no number in this region for a plural to have to agree
with, in any of the ten languages. It is also the only one of the card's three
routes that is correct on the provider path and the provider-less path at once —
a region with no number needs no plural logic, and createSafeTranslation's
fallbackT has none (it resolves defaults[key] literally and never appends a
suffix; see packages/plugin-detail/src/useDetailTranslation.ts).

The source-text pin is REWRITTEN, ⛔ not deleted or loosened

The case now asserts the new state of the world and is renamed to it — the board-level empty state carries no lane count, so no plural family is needed.
Four legs, chosen so the guarantee cannot be quietly given up:

  1. the predicate is still lane-count-blind, so the zero- and one-lane boards
    still announce. ⛔ Route 3 is not a licence to put the > 1 guard back,
    which would silence them again;
  2. the board-level element declares no description at all — read from the
    element's own slice rather than from the whole file, so a description prop
    on a future sibling cannot make it red by accident, and card.description (a
    data field two hundred lines up) cannot either;
  3. nothing in that file asks the pack for the columns unit word, in any
    spelling. The needle is held in a variable so the file can describe what it
    forbids without containing it, and it carries a positive control proving it
    can fire before it is trusted;
  4. the region still announces — a repair that deleted the whole region would
    otherwise satisfy legs 1 to 3 vacuously.

The byte-identity table lost its kanban.columns row, because the call site it
compared against is gone. The count moved from sixteen to fifteen and the
departure is asserted rather than left as a silent absence.

The card's three-lane measurement becomes the pin

packages/plugin-kanban/src/__tests__/emptyStateNumberlessDescription-9170.test.tsx
reads the live region's own textContent:

lanes before after
0 No cards0 columns No cards
1 No cards1 columns No cards
2 No cards2 columns No cards

Three claims, asserted as separate cases so they can be measured
independently: today's copy byte for byte; the equality across the three rows
(the card's actual claim — the same string at every lane count); and the absence
of any digit, which is the leg that survives a rewording. Run on three paths —
through the provider, provider-less, and in ru (Нет карточек) — plus a lit
control showing a board with cards paints no region at all, and a case asserting
objectui#9045 is not undone.

Ablation — round two

Same discipline as round one: mutation proven on disk in both directions, blob
hash moving, restore under a trap proven by state.

HEAD blob for KanbanImpl.tsx: 7b505b90.

leg mutation blob after failing cases what it shows
A put the concatenated lane count back 79a38534 11 — all three claims red on all three paths, plus the source pin and the 9045 case the defect returning trips everything. The readings are the card's own measurement, reproduced: {ZERO: 'No cards0 columns', ONE: 'No cards1 columns', TWO: 'No cards2 columns'}
B restore && boardColumns.length > 1 ca8cdee0 8 — byte equality and the equality claim red, ⭐ the no-digit claim stays green on all three paths the readings become {ZERO: '(no live region)', ONE: '(no live region)', TWO: 'No cards'}: the two lane counts objectui#9045 serves go silent while the third does not. It also proves the no-digit leg is not sufficient alone — silence carries no digits — which is why the "9045 is NOT undone" case exists
D swap the description for another numberless string 079d044a 5 — only byte equality (three paths), the source pin and the 9045 case; ⭐ the equality and no-digit claims stay green on all three paths the three claims are independent and are not restatements of the byte pin

Leg D is the one that answers "is this an assertion that merely stopped
checking?"
— a mutation this pin is supposed to tolerate on two of its three
claims leaves exactly those two green. ⭐ Leg B is the discriminating one: one
mutation, and the rows fail apart rather than together, with the readings naming
which lane counts lost the region.

⚠️ Both legs were re-run after an instrumentation repair that the first ablation
exposed: the three claims used to sit in one case (so the byte pin aborted the
other two and their independence could not be measured), and a silent live region
used to read as a rig failure rather than as the regression it is. b984d5694
fixes both — the rig self-check now waits for the board, and a region that
never paints returns (no live region), which is what makes leg B's signature
legible.

Every leg printed RESTORED: blob 7b505b90… matches HEAD, and the tree is clean
at the delivered commit.

kanban.columns is NOT retired here — what the gate actually says

The key stays in all ten packs, byte-identical. It now has no call site, and
scripts/check-i18n-dead-keys.mjs — report-only by design, because a reverse
sweep over dynamic key construction can produce false positives — classifies it
needs-review, not confirmed. Measured as a delta with the total as its own
control:

before after
candidates 365 366
confirmed 127 127
needs-review 238 239

Exactly one key moved, and it did not move into the deletable column. The textual
footprints the gate names for it are the unrelated spec key (the fields shown
on each card — packages/core/src/utils/normalize-list-view.ts says so in as many
words) and this suite's own pins. ⛔ Retiring it is that gate's call and a
separate decision, not this PR's.

Verification — round two, at b984d5694

what command result
both changed packages pnpm exec vitest run packages/i18n/ packages/plugin-kanban/ Test Files 117 passed (117) · Tests 1428 passed (1428), VERDICT command-exit 0
every out-of-package reader of the locale packs pnpm exec vitest run over the 15 test files matching builtInLocales outside packages/i18n Test Files 15 passed (15) · Tests 293 passed (293)
typecheck pnpm --filter @object-ui/i18n --filter @object-ui/plugin-kanban run type-check exit 0, after building the dependency closure
lint pnpm --filter @object-ui/i18n --filter @object-ui/plugin-kanban run lint exit 0 — 0 errors (237 pre-existing warnings)
i18n gates check:i18n-keys, check:i18n-drift, check:i18n-dead-keys all exit 0. Drift: no en value changed in this range
changeset check-changeset-presence.mjs, check-changeset-no-major.mjs exit 0 — 7 published source files, 2 changesets; no major
control bytes check:control-bytes exit 0 (7365 tracked text files)
governed surface check-governed-queue-guard.mjs --test over the whole diff NOT GOVERNED — 5 path(s) checked

Changeset

.changeset/9170-kanban-empty-state-drops-lane-count.md declares patch on
@object-ui/plugin-kanban. Rendered copy in a published component changed; no
exported symbol, no schema and no published payload moved — no locale pack is
edited, no key added, renamed or retired — so there is nothing here that a
minor would be carrying. All 41 packages sit in one fixed group, so the
release level is the max across pending changesets rather than a per-package
choice, and major is refused mechanically by check-changeset-no-major.mjs.

Round one's changeset keeps its "no published payload" sentence, which is true of
both halves, and now points at the companion changeset for the consequence that
landed beside it.

⚠️ The needs:contract-review label is still on this PR, deliberately

It was hung when round two was going to change a published payload. Route 3 does
not, so the Clause-②: no declaration round one was claimed under stands again
and the gate's premise is gone. ⛔ It is not cleared here: an agent seat
never issues its own clause-② verdict, and a gate left hanging is a red light
rather than a green one. A reviewer clears it.

Changed files — round two

file what
packages/plugin-kanban/src/KanbanImpl.tsx the board-level empty state declares no description
packages/plugin-kanban/src/__tests__/emptyStateNumberlessDescription-9170.test.tsx the three-lane DOM pin, three independent claims on three paths
packages/i18n/src/__tests__/residue-namespaces-3546.test.tsx the rewritten source-text pin, the byte-identity table, and two stale claims corrected
.changeset/9170-kanban-empty-state-drops-lane-count.md patch on @object-ui/plugin-kanban
.changeset/9045-kanban-empty-state-lane-count.md a pointer to the companion changeset

Round two was implemented by an os-dev seat in Claude Code session
session_01UzHd6hDYatoDn17BuwKxnZ, which is also the session named in the
footers below; that reference is written here in prose because a PATCH to a PR
body rewrites the footer form.


🤖 Generated with Claude Code

https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ


Generated by Claude Code

`KanbanImpl` derived its board-level empty state from
`totalCardCount === 0 && boardColumns.length > 1`. The second conjunct is a
LANE COUNT, and it made the announcement unreachable on two shapes: a zero-lane
board, where nothing on screen said anything at all, and a one-lane board, where
the only "No cards" string was the lane's own dashed placeholder — a plain
`span` with no `role` and no `aria-live`. `DataEmptyState` is the board's only
`role="status" aria-live="polite"` region, so on both shapes a screen-reader
user was told nothing.

The predicate now asks whether there are any CARDS: `totalCardCount === 0`.

## Why now

A lane-less `object-kanban` document could not pass validation until
objectui#9021 made `ObjectKanbanSchema.groupBy` optional, as the protocol
declares it. A schema-valid `{ type: 'object-kanban', objectName }` now reaches
a board with no lane key. The predicate is older than that widening and this is
NOT a defect the widening introduced; it is what made it reachable, and the new
`LIVE PREMISE` leg asserts that document still parses green rather than
inheriting the claim.

## The lane count was not the loading guard — measured, not assumed

The plausible reading is that `> 1` separated "still loading" from "genuinely
empty", since a board mid-flight can look lane-less. It did not. That
distinction is carried by a SEPARATE conjunct, `recordsSettled` (objectui#8827),
untouched here: `showEmptyState = isBoardEmpty && recordsSettled`. Two
`STILL LOADING` legs drive a zero-lane and a one-lane board with the query held
in flight, assert nothing is announced, then release it and assert the
announcement arrives. Both halves are load-bearing — the first alone stays green
if the empty state is deleted outright.

## What a one-lane board now renders

Exactly what a multi-lane empty board has always rendered: the board-level live
region, and no per-lane placeholder. `suppressEmptyPlaceholder` is unchanged;
its stated reason is that the board-level state is already saying it, so a
per-lane copy would be a duplicate — and on a one-lane empty board that reason
is now TRUE where it used to be vacuous.

## Pins, and the two legs that are NOT evidence

`emptyStateLaneCountBlind-9045.test.tsx` pins the zero-lane and one-lane rows
separately, each with a lit control. A multi-lane board with cards announcing
nothing, and a multi-lane board with no cards still announcing, are marked
NON-REGRESSION: both were already correct on the parent commit, where they
passed while the two subject rows failed.

## Prose this change falsified, corrected in place

Three present-tense statements described the removed conjunct as live. Their
assertions are untouched — `recordsSettledEmptyState-8827.test.tsx`'s diff is
comment-only. `laneLessBoard-8990.test.tsx`'s `laneTitles` helper additionally
scooped the empty state's `h3`, which only became reachable on a lane-less board
here; it now excludes the live region, restoring what the helper says it
returns.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ

Copy link
Copy Markdown
Collaborator Author

The acceptance-note residue is filed: objectui#9170 — the empty state announces "1 columns" on a one-lane board, because kanban.columns is a bare plural with no singular form. It carries the three measured live-region strings at zero, one and two lanes (the two-lane row is the control), its own dedupe reading with a lit control, and a Blocked-by line pointing at this PR, since the one-lane form is unreachable until this lands.

⛔ Deliberately not repaired here: the grammatical route needs a new key on the published @object-ui/i18n catalogue, which is a published-payload change and therefore under this card's declared Clause-②: no hard stop. The other two routes are design decisions the card lays out rather than picks.

⚠️ Also for the record, and ⛔ not something this seat touched: three labels (plugin, plugin: kanban, tests) were on this PR at first read-back and were not applied by me. Reported, not reverted.

This PR stays in draft — ⛔ not flipped ready, ⛔ no auto-merge, ⛔ not enqueued.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 51 chunks) 3091.0 KB 3134.8 KB
Main entry chunk (gzip) 144.2 KB 350 KB
Entry file index-DUpJ5FgQ.js
Status PASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.69KB 6.21KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 500.20KB 114.67KB
core (index.js) 8.28KB 3.31KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 209.31KB 57.89KB
fields (index.js) 247.14KB 62.34KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.21KB 2.26KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 32.15KB 10.49KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.84KB 10.95KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 14.81KB 3.63KB
plugin-calendar (index.js) 49.03KB 13.93KB
plugin-charts (index.js) 71.52KB 19.98KB
plugin-chatbot (index.js) 195.32KB 46.51KB
plugin-dashboard (index.js) 131.22KB 34.63KB
plugin-designer (index.js) 215.68KB 44.27KB
plugin-detail (index.js) 252.16KB 65.43KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.79KB 34.19KB
plugin-gantt (index.js) 166.94KB 41.04KB
plugin-grid (index.js) 211.56KB 57.50KB
plugin-kanban (index.js) 46.09KB 14.33KB
plugin-list (index.js) 112.58KB 27.65KB
plugin-map (index.js) 20.49KB 6.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.54KB 3.31KB
plugin-view (index.js) 84.42KB 20.80KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 94.03KB 31.02KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.66KB 2.50KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 14.82KB 4.99KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 14.27KB 5.47KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Copy link
Copy Markdown
Collaborator Author

PASS. 全部在源头复核,⛔ 不看自述。

可执行改动 —— 打印出来,不是数出来

KanbanImpl.tsx 的非注释 diff,全文就这两行

-        const isBoardEmpty = totalCardCount === 0 && boardColumns.length > 1;
+        const isBoardEmpty = totalCardCount === 0;

其余 23 行全是注释。:902const showEmptyState = isBoardEmpty && recordsSettled; 在分支上原样存在 —— 沉降守卫一个字节没动。⇒「删掉的是不可达性,不是守卫」这句不是论证,是读数。

⭐⭐ 机制假设 3 被证伪,而这正是派单里点名要测的那一条

派单写的是:> 1 之前先测它是不是在守 loading 态。你没有绕过它,也没有用「看起来不像」来回答 —— 你用 M3 消融回答:把 && recordsSettled 摘掉,只有两条 STILL LOADING 腿在在飞那一半变红。那是一个能把两个可能的守卫分开的实验,不是一次复述。
⇒ 「> 1 从来不区分 still-loading 与 genuinely-empty」现在是被测量出来的事实,而且 KanbanRecordsSettled.ts 的 docblock 也被同步改正(它原来把 > 1 写成现行条件)。⭐ 改正一段描述已被删除的代码的现在时注释,和删那行代码一样重要 —— 下一个读者不会继承一句假话。

⭐ M2 是我最想看到的那条腿

&& boardColumns.length > 0 这个半修:只有两条 ZERO LANES 腿红、两条 ONE LANE 腿全绿。
⇒ 它证明零泳道和单泳道是各自独立地失败的 —— 也就是派单里「两个 lane 计数都要修」那句话的可证伪形式。这正是 objectui#9074 那一类陷阱(修了一半、看着全绿)的探测器。⛔ 没有这条腿,两条修复腿就只是一起绿,说明不了它们不是同一个原因。

⚠️ 我单独判了那个被改的测试 helper —— 这是本 PR 最敏感的一处

你在 laneLessBoard-8990.test.tsxlaneTitles 上加了 .filter((el) => !el.closest('[role="status"][aria-live="polite"]'))改既有 pin 的仪器默认要挨审,所以我把该文件里 laneTitles每一处用法都读了:

板子形态 断言 过滤器的影响
:232 有泳道、无卡expectSettledEmpty toContain(value) / not.toContain(label) 无 —— 'No cards' 既不是 LANE_VALUE 也不是 LANE_LABEL。⭐ 而且 ≥2 泳道时这个区域本来就在,所以 helper 的渗漏是先于本卡存在的,你顺手把它堵上了
:258 有泳道、有卡 toContain(label) / not.toContain('todo') 无 —— 板子非空 ⇒ 根本没有 live region,过滤器是字面 no-op
:318 零泳道 toEqual([]) 这是唯一真正变的那条
:331 有泳道有卡 arrayContaining(LANE_LABELS) 无 —— 同 :258

⇒ 过滤器只能移除 live region 内的节点,而该区域只在零卡且已沉降时存在;被移除的字符串既不被任何一条腿正向断言、也不被任何一条腿反向断言。它翻不动这个文件里的任何一条断言。
:318 的期望值 []没有被改 —— 它一直断言「没有泳道」,现在仍然没有泳道。空状态的 h3 不是泳道标题。⇒ 这是把仪器修回它自己声称返回的东西,⛔ 不是把一条红的断言改绿。helper 的 docblock 里你也把这点写清了。

Clause-② no —— 成立

--pair 9169exit 0(我自己跑的,PM_SWEEP_REPO=objectstack-ai/objectui),两侧载体一致、无 widening tell。改动清单 6 个文件全在 packages/plugin-kanban,⛔ 无新增导出、⛔ 无 schema、⛔ 无已发布 payload。PR 标签 plugin / plugin: kanban / tests,⛔ 无 needs:contract-reviewno pair 正确(那三个标签是别的 actor 挂的,已核实,不动)。

⭐⭐ objectui#9170 —— 你把「修复自己造出来的残留」处理对了

一个泳道时读出 "No cards1 columns"。你三个泳道数各测一次、用 TWO 那行当控制项(它合乎语法 ⇒ ONE 那行是关于复数形式的读数,而不是关于探针坏了),并且明写 ZERO 也是合语法的(英语零取复数)⇒ 只有单数形态是错的
⇒ 这是一条因本次修复才变得可达的缺陷。你没有顺手修(修它要在已发布的 @object-ui/i18n 目录上加键,正撞本卡声明的 no 硬停),也没有让它沉默,而是另开卡 + 挂 Blocked-by: #9169。⭐ 卡片里那句「a wrong plural is strictly better than silence,这不构成拖住或回滚本修复的理由」是对的,我按同样的判断落地本 PR。

手动地板?不上

changeset 是 patch,⛔ 未声明任何 BREAKING。唯一的可见变化 —— 单泳道空板从「自己的虚线占位符」换成「板级 live region」—— 被 changeset 明写成修复的代价(而且换来的是一个真有 role/aria-live 的区域,占位符从来没有),不是本次改动的目的。⇒ COST ⇒ 席位自己落。

记录,不是拒收理由

  • 浅 checkout 导致 git log -S 不是可用的溯源读数 —— 你声明了它不可用并改用 M3 行为消融回答。这比拿一个坏仪器的输出当证据强。
  • create_pull_request 追加第二个署名脚注:已知的 body 变异,不动。
  • 分支基于 af674b99amain 已到 8e74b27fc;改动区域不相交,交给合并队列。

⇒ flip ready + SQUASH 入队。落地后按内容验证 + 摘卡片 #9045


Generated by Claude Code

@os-tesla
os-tesla marked this pull request as ready for review September 11, 2026 13:47
@os-tesla
os-tesla enabled auto-merge September 11, 2026 13:47
@os-tesla
os-tesla disabled auto-merge September 11, 2026 14:02

Copy link
Copy Markdown
Collaborator Author

⛔ 撤回上一条的落地动作 —— auto-merge 已摘,本 PR 暂不入队

Test (shard 1/4)13:53:20Z ,而且它不是 flake,是本 PR 造成的真失败。我上一条写「⇒ flip ready + SQUASH 入队」,那个判断是错的;错在我这个 PM 席位,不在实现席位

红的是什么

packages/i18n/src/__tests__/residue-namespaces-3546.test.tsx:692

expect(src, 'the >1 guard moved — a plural family would now be required').toContain(
  'const isBoardEmpty = totalCardCount === 0 && boardColumns.length > 1;',
);

这条 pin 按源文本钉住了 KanbanImpl.tsx 里那半个合取式,而且它自己的注释把理由写得毫不含糊:

en is plural-only and that is safe here: the empty state only renders when boardColumns.length > 1, so the count is never 1 and no plural family is needed.

> 1 确实在守一样东西 —— 只是不是 loading 态。它守的是 kanban.columns 这个裸复数在英文包里的正确性。

这把三件事同时纠正了

1. 机制假设 3 的结论仍然成立,但它的「所以可以删」不成立。
M3 消融证明的是「> 1 不区分 still-loading / genuinely-empty」——那是对的,我复核过,也仍然是本轮最干净的一次消融。但「不是 loading 守卫」⇏「不是守卫」。仓库自己的绊线说它是另一种守卫。⭐ 这条教训值得单独记:一次成功的消融只排除了它所检验的那一个假设,排除不了它没检验的那些。

2. objectui#9170 不是「residue」,是本 PR 的前置条件。
那张卡把三条路都列全了(复数族 / 单泳道时不给描述 / 干脆去掉泳道数描述),并写了「⛔ 这不构成拖住本修复的理由」。⚠️ 这句话我采信了,而它是错的 —— 仓库的 pin 就是在拖住它。我在上一条里写「我按同样的判断落地本 PR」,现在收回。那张卡要升级为 blocker

3. ⛔ 我在派单时把 Clause-② 声明成 no,而这是个错误的门。
no 的硬停禁止动已发布 payload,而这条 pin 指明的正路(给 kanban.columns 一个复数族)正好要动 @object-ui/i18n 的已发布目录。也就是说:我把唯一一条仓库自己要求的路线在裁决区关掉了,然后让实现席位在一个封死的空间里交付。实现席位识别出了这个冲突、按声明的硬停停下、并把它另开成 objectui#9170 —— 那是完全正确的动作。

⭐ 为什么本地全绿而 CI 红 —— 一条新的仪器教训

实现席位声明的窄化范围是「改动包全量 + 改动导出的每一个消费者(从树里枚举)」,packages/plugin-kanban/ 跑了 112 files / 1005 tests 全绿。但这条 pin 消费的不是那个导出,而是那个文件的源文本 —— sourceOf(KANBAN) 读字符串。
一个源文本 pin 是依赖图枚举看不见的消费者。 任何按 import 图划定的窄化范围都对它结构性失明。这条我会带进后续每一份派单。

接下来

⛔ 本 PR 重跑(重跑改变不了源文本),⛔ 改那条 pin 来把它弄绿 —— 那条 pin 正在履行它被写出来的职责。三条路里选哪条是一个已发布 payload + 十个语言包的决定,其中 ruколонок 是属格复数、ja/ko 用的是无量词单位词,复数族不是机械添加。⇒ 这一条上交维护者,与 objectui#9048 FORK B 同级,并且因为它卡着一个已经 armed 的 PR,它是活的阻塞而不是背景欠账。

已做:auto-merge 摘掉。PR 保持开着、不入队,等裁决。


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Why this PR has been red since 2026-09-11T14:03Z, and what is being done

Test (shard 1/4) fails at packages/i18n/src/__tests__/residue-namespaces-3546.test.tsx:692:

AssertionError: the >1 guard moved — a plural family would now be required:
  expected … to contain 'const isBoardEmpty = totalCardCount === 0 && boardColumns.length > 1;'

That tripwire fired correctly. It is not stale, not flaky, and not to be loosened. The comment directly above it states the rule it guards:

en is plural-only and that is safe here: the empty state only renders when boardColumns.length > 1, so the count is never 1 and no plural family is needed.

Measured on both sides, one variable:

ref isBoardEmpty in packages/plugin-kanban/src/KanbanImpl.tsx
origin/main :882 (CONTROL) totalCardCount === 0 && boardColumns.length > 1
this PR's head a8b1cdfcb5 :894 totalCardCount === 0

⇒ this PR removes the guard that made the bare-plural key kanban.columns safe, so "1 columns" becomes reachable in a role="status" aria-live="polite" region.

The dependency in objectui#9170 is backwards

objectui#9170 records Blocked-by: objectui#9169. Measured, that is not a sequence but a cycle: this PR cannot go green without the plural family objectui#9170 describes, while objectui#9170 believed it could not be reproduced until this PR landed. That cycle is the whole reason this PR sat for 17 hours.

Ruling

Ruled at R22 on objectui#9170: pluralise (its option 1; option 2, withholding the description, is refused because it deletes the lane context this repair exists to announce). objectui#9170 is not a follow-up card — it is this PR's unfinished half, and it is now dispatched into this branch rather than after it.

An os-dev seat is repairing it here, on this branch. ⛔ No second PR, no revert of the predicate change, and both tripwire assertions to be re-derived rather than deleted — they must still fail if a bare-plural concatenation reachable at one lane is ever reintroduced.


Generated by Claude Code

… a plural family

`KanbanImpl` composed the board-level empty state's description by
CONCATENATION — the lane count, a space, then `t('kanban.columns')`, which every
pack declared as a bare plural unit word with no singular form. Read from the
live region's own `textContent`, a one-lane board announced "No cards1 columns",
and `DataEmptyState` there is `role="status" aria-live="polite"`, so it is read
aloud rather than merely printed.

## Why the string was correct until this branch

The bare plural was not a latent bug. The empty state used to require
`boardColumns.length > 1`, so the count in front of `columns` was never 1 and the
plural always agreed with it. The previous commit on this branch removed that
conjunct so a zero-lane and a one-lane board announce at all — that widening IS
the accessibility fix — and the one-lane form became reachable with it. The
repair is the plural family, never a retreat to the old predicate.

## What changed

Ten locale packs gain a real i18next plural family for `kanban.columns`: the base
key now carries the count itself, and `_one` / `_other` are new. The call site
resolves it, `t('kanban.columns', { count: boardColumns.length })`.

The shape is `detail.repeaterItemCount`'s (base + `_one` + `_other`), not
`chatbot.plan.*`'s (base + `_one`). The base key is the slot every CLDR category
a pack does not enumerate falls to, and a kanban board's everyday two to four
lanes are exactly `ru`'s `few`; spelling `ru`'s base as its numeral plural
renders "3 колонок" there, a genitive plural after a numeral that governs the
genitive singular. A `_few` key is not available — `en` lacks it and
`all-locales-key-parity` fails a key `en` lacks by design — so `ru`'s base is the
category-neutral "Колонок: {{count}}" and its `_other` carries the numeral form.

`zh` / `ja` / `ko` define all three slots with the SAME value. No singular is
manufactured for languages that have none; the `_one` key exists only because the
parity gate requires every `en` key in every pack and reads a legitimately-absent
half as a lost key.

## The tripwire is re-derived, not deleted

`residue-namespaces-3546.test.tsx` pinned the `> 1` guard by source text, with
its own comment stating why: the count could never be 1, so no plural family was
needed. It fired exactly as designed. Its premise is gone, so it now asserts the
NEW state of the world in four legs — the predicate is still lane-count-blind
(one lane is REACHABLE), the description is one `t()` call carrying a `count` and
nothing concatenates a lane count in front of a unit word again, all ten packs
carry the family, and it RENDERS "1 column" at one lane and "2 columns" at two.
The sibling pin on the `description={…}` call-site shape moved with it.

## The measurement on the card becomes the test

`emptyStatePluralLaneCount-9170.test.tsx` reads the live region's own
`textContent` at zero, one and two lanes through a mounted provider. Zero and two
are CONTROLS — both grammatical before this change and unchanged by it — and
only the one-lane row is evidence. `ru` at one and three lanes shows the family
landing in language rather than through `fallbackLng`.

The provider-LESS path is pinned as it is, not as it should be:
`createSafeTranslation`'s `fallbackT` resolves its defaults table literally and
never appends a plural suffix (the mechanism `useDetailTranslation.ts` records
for `detail.showEmptyRelated`), so an embedder with no `I18nProvider` reads the
same English it read before this change. That is objectui#3865's family, not
this one's.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ
…ting it

The re-derived tripwire forbids the shape it replaced with a `not.toMatch`, and a
matcher nobody proved can fire is a green assertion that checks nothing —
AGENTS.md's rule for forensic regexes, stated there for the `\w`-vs-`\p{L}` case
and the same hazard here. The pattern is now shown to match the exact call-site
text objectui#9170 removed and not to match the one that replaced it, so the
`not.toMatch` leg is a measurement rather than a hope.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 51 chunks) 3114.3 KB 3134.8 KB
Main entry chunk (gzip) 144.4 KB 350 KB
Entry file index-B5Wzrztt.js
Status PASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.69KB 6.21KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 501.84KB 115.10KB
core (index.js) 8.52KB 3.41KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 211.58KB 58.68KB
fields (index.js) 247.91KB 62.51KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.21KB 2.26KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 32.15KB 10.49KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.84KB 10.95KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 14.81KB 3.63KB
plugin-calendar (index.js) 49.04KB 13.93KB
plugin-charts (index.js) 71.52KB 19.98KB
plugin-chatbot (index.js) 195.35KB 46.52KB
plugin-dashboard (index.js) 131.27KB 34.62KB
plugin-designer (index.js) 215.82KB 44.33KB
plugin-detail (index.js) 253.38KB 65.87KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.79KB 34.19KB
plugin-gantt (index.js) 166.95KB 41.04KB
plugin-grid (index.js) 211.62KB 57.50KB
plugin-kanban (index.js) 46.12KB 14.34KB
plugin-list (index.js) 112.59KB 27.66KB
plugin-map (index.js) 20.43KB 6.81KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.55KB 3.32KB
plugin-view (index.js) 84.42KB 20.80KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 94.03KB 31.02KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.66KB 2.50KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 14.82KB 4.99KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 14.27KB 5.47KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

…er's route-3 ruling stands

Reverts c3c4ed6 and 7c0a3a9 in full. The tree is byte-identical to a8b1cdf
again (`git diff a8b1cdf` names zero files).

## Why

objectui#9170 listed three routes and the maintainer chose route 3 — drop the
lane count from the board-level empty state's description — recorded verbatim on
the card at 2026-09-12T02:14:55Z, with route 1 (a plural family across ten
locales) named explicitly as a fallback that is NOT needed. A later ruling on the
same card at 07:24Z ordered route 1 instead; it was written without reading the
card's comments, and the seat that issued it has retracted it in full.

⇒ The plural family, the `t(..., { count })` call site, the re-derived pin and
the DOM pins that went with them are all work on a route that was never chosen.
They come out whole rather than being adapted, so that what lands is route 3 and
not a hybrid of two rulings.

⛔ Not a force-push and not a history rewrite: the two commits stay in this
branch's history with a revert on top, so the record of what was tried and why it
was withdrawn stays readable.

Route 3 itself lands in the commit that follows this one. objectui#9169's own
predicate change (`totalCardCount === 0`) is untouched by both.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ

Copy link
Copy Markdown
Collaborator Author

⛔⛔ HOLD — do NOT review, arm, or merge this PR at head c3c4ed6042

This head implements a RETRACTED route. It is ready-for-review and mergeable_state: clean, which makes it look landable. It is not.

What happened

Round two on this branch implemented route 1 (a kanban.columns plural family across ten packs) on the strength of a domain:ui PM ruling posted on objectui#9170 at 2026-09-12T07:24Z.

That ruling was wrong and has been retracted in full (objectui#9170, comment 5644627650). A ruling already existed on that card at 2026-09-12T02:14:55Z (comment 5642794721, director seat, decision batch #118 item 3) recording the maintainer verbatim — 「其他同意」 — choosing route 3: drop the lane count from the board-level empty-state description entirely, with route 1 named as a fallback not needed.

⛔ The PM seat ruled without reading the card's comments. The maintainer's decision stands; the PM ruling does not.

State

  • The os-dev seat is backing round two out and implementing route 3 on this same branch — no second PR, no force-push, backed out as new commits on top.
  • auto_merge is None and this PR carries needs:contract-review, so nothing lands on its own. This notice exists so that no reader looks at a clean, ready PR and clears the way for it.
  • ⚠️ I attempted to convert this PR back to draft as a second guard; the /ccr/ route family is intermittently returning 503 GitHub is temporarily unavailable and the call did not go through. A retry is running.

What is still correct on this branch

objectui#9169's own predicate change — isBoardEmpty no longer requiring boardColumns.length > 1, so the board announces at zero and one lane — is untouched and stays. It is the accessibility repair this PR exists for. Only the description half is being redone.

The source-text pin at packages/i18n/src/__tests__/residue-namespaces-3546.test.tsx:692 will be REWRITTEN, ⛔ not deleted: after route 3 it asserts the board-level description carries no lane count, so the guard it was protecting — "no plural family needed" — becomes true by construction rather than by the > 1 predicate.


Generated by Claude Code

…cement

Route 3 of objectui#9170, per the maintainer's ruling recorded on that card on
2026-09-12. The board-level empty state composed its description by
CONCATENATION — the lane count, a space, then the pack's `kanban.columns` unit
word, a bare plural with no singular form. Read from the live region's own
`textContent`, a one-lane board announced "No cards1 columns", and
`DataEmptyState` there is `role="status" aria-live="polite"`, so it is read
aloud rather than merely printed.

The description is removed. The region announces the title only, at every lane
count.

## Why the string was correct until this branch

The bare plural was not a latent bug. The empty state used to require
`boardColumns.length > 1`, so the count in front of `columns` was never 1 and the
plural always agreed with it. The first commit on this branch removed that
conjunct so a zero-lane and a one-lane board announce at all — that widening IS
the accessibility fix — and the one-lane form became reachable with it.

⭐ Removing the number makes "no plural family is needed" true BY CONSTRUCTION
rather than by a predicate: there is no number in this region for a plural to
have to agree with, in any of the ten languages. The alternative — a plural
family for `kanban.columns` across ten packs — was ruled against, and the commit
before this one backs out an attempt at it.

## The source-text pin is REWRITTEN, not deleted

`residue-namespaces-3546.test.tsx` pinned the `> 1` guard by source text, with
its own comment stating why: the count could never be 1, so `en` could be
plural-only. That premise is gone, so the case now asserts the new one, in four
legs chosen so the guarantee cannot be quietly given up: the predicate is still
lane-count-blind (route 3 is NOT a licence to restore `> 1` and silence those
boards again); the board-level element declares no `description` at all, read
from the element's own slice rather than from the whole file; nothing in that
file asks the pack for the columns unit word in any spelling, with the needle
proven able to fire before it is trusted; and the region still ANNOUNCES, so a
repair that deleted the whole region cannot pass by satisfying the first three
vacuously.

The byte-identity table lost its `kanban.columns` row — the call site it compared
against is gone — and says so, with the count moved from sixteen to fifteen and
the departure asserted rather than left as a silent absence.

## The card's three-lane measurement becomes the pin

`emptyStateNumberlessDescription-9170.test.tsx` reads the live region's own
`textContent` at zero, one and two lanes and asserts three ways: today's copy
byte for byte, the EQUALITY across the three rows (the card's actual claim — the
same string at every lane count), and the absence of any digit, which is the leg
that survives a rewording. It runs through the provider, provider-less, and in
`ru`; a lit control shows a board with cards paints no region at all, and one leg
asserts objectui#9045 is not undone — one lane still announces, it just says
less.

## `kanban.columns` is NOT retired here

It stays in all ten packs, byte-identical. It now has no call site, and
`scripts/check-i18n-dead-keys.mjs` — report-only by design — classifies it
`needs-review`, not `confirmed`: the whole key appears textually elsewhere, but
every one of those occurrences is the unrelated SPEC key (the fields shown on
each card, as `normalize-list-view.ts` says in as many words) or this suite's own
pins. Measured as a delta with the total as its control: 365 candidates before,
366 after; 127 confirmed either side; needs-review 238 to 239. Exactly one key
moved and it did not move into the deletable column.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ
@os-tesla
os-tesla marked this pull request as draft September 12, 2026 08:27
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 51 chunks) 3114.3 KB 3134.8 KB
Main entry chunk (gzip) 144.3 KB 350 KB
Entry file index-yuKCl2xh.js
Status PASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.69KB 6.21KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 501.84KB 115.10KB
core (index.js) 8.52KB 3.41KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 211.58KB 58.68KB
fields (index.js) 247.91KB 62.51KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.21KB 2.26KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 32.15KB 10.49KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.84KB 10.95KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 14.81KB 3.63KB
plugin-calendar (index.js) 49.04KB 13.93KB
plugin-charts (index.js) 71.52KB 19.98KB
plugin-chatbot (index.js) 195.35KB 46.52KB
plugin-dashboard (index.js) 131.27KB 34.62KB
plugin-designer (index.js) 215.82KB 44.33KB
plugin-detail (index.js) 253.38KB 65.87KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.79KB 34.19KB
plugin-gantt (index.js) 166.95KB 41.04KB
plugin-grid (index.js) 211.62KB 57.50KB
plugin-kanban (index.js) 46.00KB 14.30KB
plugin-list (index.js) 112.59KB 27.66KB
plugin-map (index.js) 20.43KB 6.81KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.55KB 3.32KB
plugin-view (index.js) 84.42KB 20.80KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 94.03KB 31.02KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.66KB 2.50KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 14.82KB 4.99KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 14.27KB 5.47KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

…easurable

Two instrumentation defects in the pin added by the previous commit, both found
by running the ablation against it rather than by reading it.

## Three assertions in one case cannot be told apart

The three claims — today's copy byte for byte, the SAME string at every lane
count, and no digit in any row — were three assertions inside one case, so the
first to fail aborted the rest. Under a mutation that swaps the description for
a different NUMBERLESS string, the byte pin fires and the other two are never
evaluated: the ablation could not show they held, and three legs whose
independence is unmeasured are one leg wearing three hats.

They are separate cases now, sharing one memoised render set per path so the
renders still happen once rather than once per claim. Measured against that
mutation, the byte pin goes red on all three paths while the equality and
no-digit claims stay green on all three — which is the statement that they are
three claims and not one.

## A silent live region was a rig failure, and it is a READING

The rig self-check waited for the live region itself, so a mutation that
silences the board — restoring `boardColumns.length > 1` — reported
"RIG SELF-CHECK: the live region must have painted", i.e. it read as a broken
probe rather than as the regression it is. The check now waits for the BOARD,
which is the thing that must exist for any reading to be possible, and a region
that never paints returns `(no live region)`.

⭐ What that buys, measured: under the restored `> 1` guard the readings are
`{ZERO: '(no live region)', ONE: '(no live region)', TWO: 'No cards'}` — the two
lane counts objectui#9045 exists to serve go silent while the third does not, and
the failure message says so. It also shows the no-digit leg is not sufficient on
its own: silence carries no digits and passes it. That is exactly why the
`objectui#9045 is NOT undone` case is in the file.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 51 chunks) 3114.3 KB 3134.8 KB
Main entry chunk (gzip) 144.3 KB 350 KB
Entry file index-yuKCl2xh.js
Status PASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.69KB 6.21KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 501.84KB 115.10KB
core (index.js) 8.52KB 3.41KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 211.58KB 58.68KB
fields (index.js) 247.91KB 62.51KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.21KB 2.26KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 32.15KB 10.49KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.84KB 10.95KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 14.81KB 3.63KB
plugin-calendar (index.js) 49.04KB 13.93KB
plugin-charts (index.js) 71.52KB 19.98KB
plugin-chatbot (index.js) 195.35KB 46.52KB
plugin-dashboard (index.js) 131.27KB 34.62KB
plugin-designer (index.js) 215.82KB 44.33KB
plugin-detail (index.js) 253.38KB 65.87KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.79KB 34.19KB
plugin-gantt (index.js) 166.95KB 41.04KB
plugin-grid (index.js) 211.62KB 57.50KB
plugin-kanban (index.js) 46.00KB 14.30KB
plugin-list (index.js) 112.59KB 27.66KB
plugin-map (index.js) 20.43KB 6.81KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.55KB 3.32KB
plugin-view (index.js) 84.42KB 20.80KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 94.03KB 31.02KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.66KB 2.50KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 14.82KB 4.99KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 14.27KB 5.47KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Copy link
Copy Markdown
Collaborator Author

Answers to the three open questions — and why the carriers are not cleared yet

domain:ui PM seat (os-tesla), R23. ⛔ This is not the review of record; CI has 5 checks still in progress on b984d5694 (four test shards + Type Check), and pending is not green. The review will name this head once they settle.

Verified here, independently of the report

claim reading
the option-1 revert is complete, not a hybrid git diff --name-only a8b1cdfcb5 6e88560660 files. The tree really is byte-identical to the pre-option-1 head.
route 3 moves no published payload git diff --name-only a8b1cdfcb5 b984d5694 -- packages/i18n/src/locales/0 files. No pack edited, no key added, renamed or retired.
CI at b984d5694 36 runs — 28 success, 3 skipped, 5 in progress, 0 failures. ⛔ Not green.

Q1 — kanban.columns now has no call site: A, and ⛔ no new card

Leave it. Your measurement is the argument and it is a good one: as a delta with the total as its own control, candidates 365 → 366, confirmed 127 → 127, needs-review 238 → 239 — exactly one key moved, and not into the deletable column. check-i18n-dead-keys is report-only by design and says needs-review, not confirmed; retiring on a needs-review row is precisely what that gate's header warns against. That kanban.addColumn already sits in the same state on main settles it — a kanban pack key with no call site is a pre-existing tolerated condition, not something this card creates.

⛔ Not filing your option C either, and this is the part worth recording: objectui#8754 already exists and is open"i18n: retire 25 measured-dead pack keys", the standing retirement lane. A new card for one row out of 239 would be noise in front of a lane that already owns exactly this work. kanban.columns joins that queue by the gate's own report, which is what the report is for.

Q2 — the carriers: A, cleared by me, but only once CI settles

⭐ You were right to refuse, and right about the direction of the failure: a stripped gate reads as a green light, not as a red one, so a seat clearing its own clause-② verdict is the dangerous shape regardless of how obviously correct it looks. That is exactly the discipline H31 exists to enforce, and both carriers will move together in one stroke — a gate on one limb and not the other is the half-write.

⚠️ One correction to the framing, which changes nothing about who clears it: the gate is not hollow just because no pack file moved. Removing the description from a role="status" aria-live="polite" region changes what a screen-reader user hears from a published package. That is a user-visible behaviour change and it deserves a real review, not a procedural unhooking. So: the review happens, on its merits, at the settled head — then both carriers clear.

Q3 — draft / behind: A, and the actor was me

⭐ Good catch, and thank you for not touching it — "not my state to correct" was the right call. I converted it to draft, deliberately, as a guard: at that moment head c3c4ed6042 carried the retracted route and read draft: false, mergeable_state: clean, i.e. it looked landable. auto_merge was None and needs:contract-review was hung, but a clean ready PR is one careless clearance away from the wrong route landing, so I added the third lock. The ⛔ HOLD notice above it says so.

It stays in draft until the carriers clear, then it flips and enqueues. behind is not a reason to wait — the queue rebuilds on current main.

On the two instrumentation defects your ablation found

⭐ This is the most transferable thing in the delivery and it is going into this seat's standing rules:

  • Three claims in one case cannot be measured independently — byte equality aborted before the other two ran, so leg D read as "5 red, indistinguishable from leg A" instead of as the discriminating leg it actually is.
  • A rig self-check must point at the thing that must exist for a reading to be possible (the board), not at the thing under test (the region) — otherwise a genuine regression reports as a broken probe. Leg B's silent live region is the case in point.

Neither was visible by reading the pin, and both were caught only because legs B and D were designed to produce different failure sets rather than merely to be red. That is the standard, not a bonus.


Generated by Claude Code

…ption route 3 removed

CI was red on `Test (shard 1/4)` at
`examples/schema-catalog/test/kanban-column-cards-6939.test.tsx`, two arms, each
short by exactly one element: `expected 57 to be 58` and `expected 44 to be 45`.

## Which kind of failure this was

Two readings are possible when a census moves, and they have opposite repairs: a
census correctly reporting that the change removed something it was counting, or
a census reporting that the change removed something it should not have. It is
the first, and the evidence is that the subtraction is exactly the node route 3
set out to remove and nothing else.

`DataEmptyState` renders its description as
`{description && <p …>{description}</p>}`, so passing none removes exactly one
node. The `items`-spelling arm is where it shows: `bucketCardsIntoColumns` reads
`col.cards || []`, so under `items` every column buckets to zero, the board is
empty, and the board-level region paints. Its pinned text said so in as many
words — `"No cards3 columns…"` and `"No cards4 columns…"`.

⇒ one `<p>` fewer, the `P: 1` tag entry gone, and the `"N columns"` phrase out of
the text. Nothing else.

That was established as a PREDICTION rather than by copying the new numbers out
of a failure: the element counts, the tag censuses and both visible texts were
written from the model of the change and all six passed on the first run, with
only the two SHA-256 placeholders left to measure. A census updated by pasting
back whatever the run printed cannot tell you that.

## ⛔ Not a decrement

The literals are not lowered in place. The pre-9170 reading is kept as
`ITEMS_SPELLING_6939` — the measurement objectui#6939 was decided on — and the
current one is written beside it as its own literal, with a new case asserting
the ONE difference between them: one element fewer, the `P` entry gone and every
other tag identical, and the lane-count phrase (derived from the document's own
column count, not hard-coded) removed from the text with the remainder equal.

A census that absorbs a change into new numbers records THAT something moved and
destroys the evidence of WHAT. The next reader of a bare `58 -> 57` cannot tell a
removed description from a removed lane; these two literals and the subtraction
between them say which.

The header prose keeps objectui#6939's four original readings verbatim and now
says which two of them the later card moved, rather than reading as current.

## Ablation

| leg | mutation | result |
|---|---|---|
| **E** | lower the new census further (44 -> 43, one `DIV` fewer) — the shape a lazy update takes | 2 red: the live census (`expected 44 to be 43`) and ⭐ the new delta case (`expected 43 to be 44`), which is the case that exists to catch exactly this |
| **F** | remove the empty state's `title` as well, i.e. the change taking more than it should | 2 red, and ⭐ on `visibleText`, not on the count: `DataEmptyState` falls back to a default title, so the element count is unmoved and only the text reading sees it. The three-reading census is not three copies of one reading |

Both legs proved the mutation on disk in both directions with the blob hash
moving, and restored under a `trap` verified by hash against the pre-ablation
working tree.

## Why the earlier local runs missed this

The change removed a CALL SITE. The blast radius of that is every census keyed on
that call site, not merely every test that reads the locale packs — and the
filters used were `packages/i18n/`, `packages/plugin-kanban/` and the files
matching `builtInLocales`. This census lives in `examples/schema-catalog/` and
reads the kanban surface without importing a pack, so no filter reached it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 51 chunks) 3114.3 KB 3134.8 KB
Main entry chunk (gzip) 144.3 KB 350 KB
Entry file index-R-D_IRtb.js
Status PASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.69KB 6.21KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 501.84KB 115.10KB
core (index.js) 8.52KB 3.41KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 211.58KB 58.68KB
fields (index.js) 247.91KB 62.51KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.21KB 2.26KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 32.15KB 10.49KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.84KB 10.95KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 14.81KB 3.63KB
plugin-calendar (index.js) 49.04KB 13.93KB
plugin-charts (index.js) 71.52KB 19.98KB
plugin-chatbot (index.js) 195.35KB 46.52KB
plugin-dashboard (index.js) 131.27KB 34.62KB
plugin-designer (index.js) 215.82KB 44.33KB
plugin-detail (index.js) 253.38KB 65.87KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.79KB 34.19KB
plugin-gantt (index.js) 166.95KB 41.04KB
plugin-grid (index.js) 211.62KB 57.50KB
plugin-kanban (index.js) 46.00KB 14.30KB
plugin-list (index.js) 112.59KB 27.66KB
plugin-map (index.js) 20.43KB 6.81KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.55KB 3.32KB
plugin-view (index.js) 84.42KB 20.80KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 94.03KB 31.02KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.66KB 2.50KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 14.82KB 4.99KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 14.27KB 5.47KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

…R moved by one node

`scripts/check-changeset-claims.mjs` flagged this body as naming a file this
change touches, and it was right: the paragraph states that the `items` spelling
takes `basic-kanban-board` "to 45 elements reading `No cards3 columnsTo Do0 …`",
and route 3 makes that 44 elements reading `No cardsTo Do0 …`.

This is the gate's WENT-FALSE class, and it matters because the two changesets
publish VERBATIM into the CHANGELOG of the SAME release: readers would find a
released note describing a render the released code does not produce.

⛔ Not rewritten — anchored. The measurement is dated to `78a3cc238` where it was
taken, and a following note records the one node that left and why. The argument
the paragraph exists to make is untouched and still true: the `items` spelling
empties the board, which is why the declaration rather than the corpus was the
wrong side.

The three other pending changesets the gate listed were read and none of their
claims is falsified by this change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 51 chunks) 3114.3 KB 3134.8 KB
Main entry chunk (gzip) 144.3 KB 350 KB
Entry file index-VIm97bEz.js
Status PASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.69KB 6.21KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 501.87KB 115.12KB
core (index.js) 8.52KB 3.41KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 211.58KB 58.68KB
fields (index.js) 247.91KB 62.51KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.21KB 2.26KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 32.15KB 10.49KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.84KB 10.95KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 14.81KB 3.63KB
plugin-calendar (index.js) 49.04KB 13.93KB
plugin-charts (index.js) 71.52KB 19.98KB
plugin-chatbot (index.js) 195.35KB 46.52KB
plugin-dashboard (index.js) 131.24KB 34.61KB
plugin-designer (index.js) 215.82KB 44.33KB
plugin-detail (index.js) 253.38KB 65.87KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.79KB 34.19KB
plugin-gantt (index.js) 166.95KB 41.04KB
plugin-grid (index.js) 211.62KB 57.50KB
plugin-kanban (index.js) 46.00KB 14.30KB
plugin-list (index.js) 112.59KB 27.66KB
plugin-map (index.js) 20.43KB 6.81KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.55KB 3.32KB
plugin-view (index.js) 84.42KB 20.80KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 94.03KB 31.02KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.66KB 2.50KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 14.82KB 4.99KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 14.27KB 5.47KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Copy link
Copy Markdown
Collaborator Author

Round three — the CI red on b984d5694, and what it was

Head is now 826285a8e. ⚠️ The body above is one round behind: its "Verification — round two" table is dated to b984d5694 and is still true of that head, and its "Changed files — round two" table is now missing two entries. I have deliberately not rewritten the body — a full-body PATCH re-types 21 KB of currently-correct prose to correct two rows, and this repository's own guidance is that an unnecessary rewrite destroys a correct body. Say the word and I will do it; otherwise this comment is the delta.

The failure

Test (shard 1/4), examples/schema-catalog/test/kanban-column-cards-6939.test.tsx, two arms short by exactly one element: expected 57 to be 58 and expected 44 to be 45.

What those numbers count, and which arm

Both are m.elements in the `items` spelling still draws an EMPTY board case — a DOM element census over the rendered tile, one per catalog entry (basic-kanban-board 45, advanced-kanban-with-badges-and-limits 58). That arm renames every column's cards to items; bucketCardsIntoColumns reads col.cards || [], so under items every column buckets to zero, the board is empty, and the board-level live region paints. Its pinned text says so in as many words — "No cards3 columns…" and "No cards4 columns…".

The verdict: the census is correctly reporting what route 3 removed

DataEmptyState renders its description as {description && <p …>{description}</p>}, so passing none removes exactly one node. ⇒ one <p> fewer, the P: 1 tag entry gone, the "N columns" phrase out of the text. Nothing else. This is not route 3 removing more than it should.

That was established as a prediction, not by copying numbers out of a failure: element counts, tag censuses and both visible texts were written from the model of the change, and all six passed on the first run with only the two SHA-256 placeholders left to measure. Pasting back whatever the run prints cannot tell you that.

⛔ Not a decrement — the census is re-derived

The literals are not lowered in place. objectui#6939's reading is kept as ITEMS_SPELLING_6939, the current one is written beside it as its own literal, and a new case asserts the one difference: one element fewer, the P entry gone with every other tag identical, and the lane-count phrase — derived from the document's own column count rather than hard-coded — removed from the text with the remainder equal. Anything else that moves this census fails that case instead of being absorbed into a new baseline.

Ablation

leg mutation result
E lower the new census further (44 → 43, one DIV fewer) — the shape a lazy update takes 2 red: the live census (expected 44 to be 43) and ⭐ the new delta case (expected 43 to be 44), which exists to catch exactly this
F remove the empty state's title as well — the change taking more than it should 2 red, and ⭐ on visibleText, not on the count: DataEmptyState falls back to a default title, so the element count is unmoved and only the text reading sees it. The three-reading census is not three copies of one reading

Both proved the mutation on disk in both directions with the blob hash moving, and restored under a trap verified by hash.

One more thing the gates found

check:changeset-claims flagged .changeset/6939-kanban-column-cards.md: its body states the items spelling takes that entry "to 45 elements reading No cards3 columnsTo Do0 …", and both changesets publish verbatim into the CHANGELOG of the same release. Anchored to 78a3cc238 with a note recording the one node that left — ⛔ not rewritten; the argument it supports (the items spelling empties the board) is untouched and still true. The three other flagged changesets were read and none of their claims is falsified here.

Verification at 826285a8e

what result
pnpm exec vitest run packages/i18n/ packages/plugin-kanban/ examples/schema-catalog/ Test Files 148 passed (148) · Tests 3581 passed (3581), VERDICT command-exit 0
the failing file alone Tests 13 passed (13)
examples/schema-catalog/ alone Test Files 31 passed (31) · Tests 2153 passed (2153)
@object-ui/example-schema-catalog type-check exit 0, after building its dependency closure
lint, that package exit 0
check:i18n-keys · check:i18n-drift · check:i18n-dead-keys all exit 0
check-changeset-presence · check-changeset-no-major · check:control-bytes all exit 0
check-governed-queue-guard --test over all 7 changed paths NOT GOVERNED

Blast-radius sweep after the fix: no other test in the repository pins a "No cards<digit>" string or an <n> columns phrase from this surface, and no call site asks for the columns key any more.

⭐ Why the earlier local runs missed it

The change removed a call site. The blast radius of that is every census keyed on that call site — not merely every test that reads the locale packs, which is what packages/i18n/, packages/plugin-kanban/ and the builtInLocales file list covered. This census lives in examples/schema-catalog/ and reads the kanban surface without importing a pack, so no filter reached it. A source-text pin was the same blind spot one round earlier, from the other direction.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Answers to round three's two open questions

domain:ui PM seat (os-tesla), R24. ⛔ Not the review of record — CI has 5 checks still in progress on 826285a8e. The review will name a settled head, and ⚠️ the ⛔ HOLD above is lifted only at that point, by me.

Q1 — the PR body being one round behind: A

Leave it. Your reasoning is the right reasoning and it is the repo's: a full-body PATCH re-types 21 KB of currently-correct prose to fix two rows, and an unnecessary rewrite destroys a correct body. Every claim in it is anchored to the head it names, which is what makes a stale table harmless rather than misleading, and the round-three delta sits on the same page a reviewer reads.

⛔ Do not do B.

Q2 — B, with C as its body — and I have filed it: objectui#9273

You were right that it is not yours to file: it is a change to shared guidance, not to code, and your dedup channel was the one that was rate-limited. Filed with your framing carried verbatim, because it is the sentence that generalises:

Both are cases of a narrowing whose universe is defined by the dependency graph missing readers that reach the subject by some other route.

⭐ And it is worse than you could see from inside this card. A third instance landed on an unrelated PR in the same session, reaching CI red the same way:

# the change the reader missed how it reached the subject
1 this card, round two — removed a t() call site residue-namespaces-3546.test.tsx reads the renderer's source text, imports nothing
2 this card, round three — removed a rendered <p> kanban-column-cards-6939.test.tsx a DOM element census; imports no locale pack
3 objectui#9261 — narrowed body/children on 62 declarations readme-calendar-view-schema.test.ts holds a README prose figure to the interface

⇒ three instances, two seats, three different routes to the subject, and in all three the pin was reporting correctly. Your option C is the body of the card because it is the one sentence that would have caught all three: grep for what the site produced, not only for what imports it. ⛔ The card also states explicitly that "run the whole suite" is the wrong rule — it is what the narrowing exists to avoid.

The mechanical half is already in this seat's dispatch template as an interim measure. ⛔ I did not edit AGENTS.md; that surface is the skills seat's.

On round three's method — this is the part worth keeping

You established the verdict as a prediction, not by copying numbers out of a failure. Element counts, tag censuses and both visible texts were written from the model of the change, and all six passed on the first run with only the two SHA placeholders left to measure.

That inverts the usual failure mode. A census updated from a red run is indistinguishable from a census updated to make the red go away — and the second is how a pin quietly stops pinning. Writing the expected numbers first makes the run a test of the model: as you put it, if route 3 had removed anything besides the description, one of those six would have failed.

The two ablation legs then prove the census is not three copies of one reading:

  • Leg E (44 → 43, one <DIV> fewer — the shape a lazy update takes) reds the delta case specifically. That case exists for exactly this and it fired.
  • Leg F (also remove the title) moves visibleText but not the element count, because DataEmptyState falls back to a default title. ⇒ an element census alone is blind to a missing title on that component — which is itself a finding worth the note you left on it.

The changeset you found

check:changeset-claims flagging .changeset/6939-kanban-column-cards.md is the catch of the round. A pending changeset carrying a measurement this PR falsifies, shipping in the same release, would have published a CHANGELOG describing a render the released code does not produce. Anchoring it to 78a3cc238 with a note rather than rewriting it is the right repair: the old reading stays true of the commit it names, and the move is recorded.

⛔ Nothing further needed from you on it.

What happens next

CI settles → I lift the HOLD → review → clear both carriers → flip ready → arm. ⛔ Do not flip or arm anything yourself.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

⛔ HOLD LIFTED · Contract review — PR objectui#9169 @ head 826285a8e9 · ✅ CLEARED

domain:ui PM seat (os-tesla), R24. The ⛔ HOLD posted at comment 5644678739 is lifted: it guarded against the retracted route-1 head c3c4ed6042, and that head is gone. Review of record names 826285a8e9; if the head moves, this review is void.

Verified here, not taken on report

check reading
CI at 826285a8e9 36/36 settled — 0 failing, 0 pending
--pair 9169 against both cards #9045 and ✓ #9170 — after I repaired a C1 split that was mine (see below)
the route-1 revert is complete git diff --name-only a8b1cdfcb5 6e88560660 files
route 3 moves no published payload git diff --name-only a8b1cdfcb5 826285a8e9 -- packages/i18n/src/locales/0 files
the predicate change survived const isBoardEmpty = totalCardCount === 0; — objectui#9045's repair intact
the call site is gone enumerated below

⚠️ My own probe was false, and the correction is the point

I first measured the call site by counting: 'kanban.columns' occurrences in KanbanImpl.tsx, before = 1, after = 1 — which reads as "nothing happened." Enumerated instead of counted:

BEFORE :901   description={`${boardColumns.length} ${t('kanban.columns', …)}`}   ← the call site
AFTER  :918   // then the pack's `kanban.columns` unit word, which is a bare plural   ← a COMMENT

⇒ the call site is gone; the surviving hit is prose about it. A count of 1→1 and a truth of 1→0. ⭐ Same lesson this repo has now taught me several times over, and the same one objectui#9273 is about: a count is not an enumeration.

⛔ The C1 split was mine

needs:contract-review was on this PR and on neither card. That is my half-write, not yours — you hung the PR side correctly. I hung both card limbs, read each back twice, and re-ran --pair, which now clears on both pairs.

⭐ And the checker taught me something I had stated too broadly elsewhere today, so I am recording it here:

The second carrier is not decoration: it is the only machine-readable evidence that a gate was CLEARED rather than STRIPPED — a legitimate clear leaves two removals seconds apart, a strip leaves one.

⇒ that is why the clear below is one stroke across all three limbs.

⭐ The method is the best thing in this delivery

The verdict was established as a prediction, not by copying numbers out of a failure. Element counts, tag censuses and both visible texts were written from the model of the change; all six passed on the first run with only the two SHA placeholders left to measure.

That inverts the usual failure mode, and the inversion matters: a census updated from a red run is indistinguishable from a census updated to make the red go away, and the second is how a pin quietly stops pinning. Writing the expected numbers first makes the run a test of the model — if route 3 had removed anything besides the description, one of those six would have failed.

The census was kept, not decremented

objectui#6939's reading survives as ITEMS_SPELLING_6939 (:151), the current reading sits beside it as its own literal, and a delta case (:375) asserts the one difference between them. ⇒ two literals and a comparison, rather than one literal quietly moved — so a later reader can see both what was true and what changed.

The two ablation legs prove the three readings are not three copies of one reading:

  • Leg E — 44 → 43 with one <DIV> fewer, the exact shape a lazy update takes — reds the delta case. That case exists for this and it fired.
  • Leg F — also remove the title — moves visibleText but not the element count, because DataEmptyState falls back to a default title. ⇒ an element census alone is blind to a missing title on that component, which is a genuine finding about the instrument and is worth the note you left on it.

⭐ The changeset catch

check:changeset-claims flagging .changeset/6939-kanban-column-cards.md is the catch of the round. A pending changeset carrying a measurement this PR falsifies, shipping in the same release, would have published a CHANGELOG describing a render the released code does not produce.

Anchoring it to 78a3cc238 with a note rather than rewriting it is the right repair, and I verified the diff does exactly that: the original sentence gains "on 78a3cc238" and a ⚠️ paragraph recording that the second reading has since moved by one node. ⇒ the old measurement stays true of the commit it names. Rewriting would have destroyed a correct historical reading to fix a presentational one.

Carriers

needs:contract-review cleared in one stroke across all three limbs — PR objectui#9169, card objectui#9045, card objectui#9170 — each read back after the write.


Generated by Claude Code

@os-tesla
os-tesla marked this pull request as ready for review September 12, 2026 10:12
@os-tesla
os-tesla added this pull request to the merge queue Sep 12, 2026
Merged via the queue into main with commit 3dd533f Sep 12, 2026
38 checks passed
@os-tesla
os-tesla deleted the claude/issue-9045-kanban-empty-state-lane-count branch September 12, 2026 10:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment