Skip to content

fix(components): fold the operator at FilterBuilder value-input gate (objectui#9302) - #9358

Merged
claude[bot] merged 7 commits into
mainfrom
claude/issue-9302-needsvalueinput-fold
Sep 14, 2026
Merged

claude[bot] merged 7 commits into
mainfrom
claude/issue-9302-needsvalueinput-fold

Conversation

@os-tesla

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

Copy link
Copy Markdown
Collaborator

Fixes #9302

FilterBuilder's value-input gate now reads the operator through the same fold
the rest of the component already performs, so one operator draws one row
whichever spelling it arrives in.

The one site

needsValueInput did a raw has() against VALUELESS_FILTER_BUILDER_OPERATORS,
whose members are this builder's six camelCase dropdown ids. Every OTHER spelling
the spec publishes for those same operators missed the set and was treated as
value-taking: the canonical members of VIEW_FILTER_OPERATORS those ids fold
onto — what foldFilterGroupToSpecRules persists and what any spec-side producer
emits — and every row of VIEW_FILTER_OPERATOR_ALIASES pointing at one of them.
The row drew a box to type a value into, directly beside a trigger reading
Is null.

⛔ Which spellings those are is not written down in this PR, in the changeset, or
in the pin's prose. The pin walks those two published tables and
--reporter=verbose names every row it measured, so the population is
re-derived on each run instead of restated in text that cannot move with it
(AGENTS.md #9). The first pass of this PR named four spellings where the
behaviour covers twice that, which is exactly the failure that rule describes —
repaired on objectui#9358.

Both sides of the lookup are now folded through the spec's
normalizeFilterOperator. That is the same fold filterValueArity and
reconcileOperatorForField in this file already use, so this is one more site
joining a fold the component performs rather than a new dialect, and the
canonical lookup set is DERIVED from the exported one rather than restated
beside it.

Premise re-measured on this branch's base

The card's table was measured on objectui#7561's branch. Re-measured on
origin/main at 5a41ce733 — with that card's trigger repair landed — the
base is unchanged and the table reproduces exactly
. The trigger column reads
Is null / Is empty, not blank.

⚠️ That citation is stale. This branch has since merged main, so the real fork
point — from git merge-base, not base.sha — is dfb5850594. The table still
reproduces there: the contract review re-derived it at that base and found the
four canonical rows red in its base-content arm.

Measured through the real builder on one text column, with the rows keyed by
what DERIVES them rather than by a list of spellings:

row dialect what enumerates it value inputs, base value inputs, after
dropdown id a member of VALUELESS_FILTER_BUILDER_OPERATORS 0 0
canonical VIEW_FILTER_OPERATORS ∩ that set folded through normalizeFilterOperator 1 0
alias a row of VIEW_FILTER_OPERATOR_ALIASES pointing at one of those canonicals and not itself a dropdown id 1 0
firing control equals — in neither, and it really does take a value 1 1

The control is what makes the "after" column a reading: without a row that keeps
its input in both arms, a uniform zero and a broken renderer look the same. The
dropdown-id row is the over-reach guard in the other direction.

What this deliberately does NOT do

  • The exported set is not widened. Its stated job is which rows this
    builder leaves value-less
    — a fact about the dropdown's own ids — and two
    layers read it: plugin-list's convertFilterGroupToAST (what the live grid
    queries) and app-shell's foldFilterGroupToSpecRules (what a saved view
    persists, already documented as this set plus the canonical spellings only
    that layer sees). Widening here would have made that layer's deliberate
    compensation redundant by side effect, in a file nobody is editing. The defect
    was never a set missing members; it was a reader that forgot to normalize its
    input, so the reader is what was repaired.
  • ⛔ No row's stored operator is rewritten — rendering is not an edit, pinned.
  • ⛔ No second spelling joins the dropdown; the emitted vocabulary is unchanged.
  • ⛔ Which operator vocabulary should WIN is not decided here. The
    contains / icontains boundary recorded on objectui#7379 is untouched and
    pinned: the fold this gate routes through maps neither onto the other.
  • packages/components/src/ui/ is not touched. The edit is in custom/,
    which is where wrapping belongs.

Evidence

Red-first, pin written before the code and run on the unmodified tree
(git diff HEAD empty for every tracked file, HEAD = 5a41ce733):

× 'canonical' `'is_null'` draws "'Is null'" and +0 value input(s)
× 'canonical' `'is_not_null'` draws "'Is not null'" and +0 value input(s)
× 'canonical' `'is_empty'` draws "'Is empty'" and +0 value input(s)
× 'canonical' `'is_not_empty'` draws "'Is not empty'" and +0 value input(s)
AssertionError: the builder drew 1 value input(s) for "is_null" but the row
reads "Is null". Two spellings of one operator must draw one row:
expected 1 to be +0
Tests  4 failed | 13 passed (17)

Ablation, two legs. Each mutates on disk, proves the mutation reached disk
before any result is read, and restores by blob-hash equality against the HEAD
blob plus an empty git diff HEAD, under a trap with absolute paths. The pin
imports the component by a relative SOURCE specifier inside its own package, so
no build step gates whether a mutation reaches the subject.

leg mutation predicted measured
1 gate reverted to the raw has(), derived set kept the red-first set red, same rows
2 derivation's .map(normalizeFilterOperator) dropped, gate fold kept canonical + camelCase red; exists / notExists / equals green red, exactly those
3 (objectui#9358) whole file put back to its base blob 6ec603fca6, extended pin kept every canonical and every non-dropdown alias red; dropdown ids and equals green exactly that — every alias row the first pin never named is among the failures

Both halves are load-bearing. Restore verified both times:
RESTORED-OK blob=4c0b108908d3e8f6c1413f8aaaf3d14c55436311 git-diff-HEAD=empty.

Clause-② verified mechanically, not by inspection. Built the package, put the
file back to its base content, rebuilt, and fingerprinted every emitted type
declaration: 208 files, rollup sha256 92f47f826185fb70e804b1c74e57f107b9b311be5505ce5882aec84634115b84
in BOTH arms — byte-identical.

RETRACTED as a claim about this PR, and kept only as the dated run record
it is.
That reading was taken at 30e320ab7, before this branch published any
doc-comment repair. Two heads have moved the emitted bytes of
dist/custom/filter-builder.d.ts on purpose since, so the rollup is NOT
byte-identical at the current head and "no published surface moved" is false as
written. The narrower claim clause-② actually asks — no exported symbol and no
declared type added, removed or changed — is re-measured per head in the two
review sections below, and holds there rather than being inherited from here.

Gates and suites (targeted per this repo's own scripts and workflows; the
full farm is CI's):

run result
@object-ui/components test 271 files, 2620 tests passed
@object-ui/components type-check exit 0
build of the package and its dependency closure exit 0
check:control-bytes exit 0, 7530 files scanned
check:spec-symbols exit 0
check:doc-example-readers, check:test-path-roots, check:vi-mock-override-shape, check:vi-mock-specifiers, check:new-line-citations, check:unreferenced-sources exit 0
check:changeset-claims exit 0, report-only — handled below
check:readme-exports NOT MEASURED — "the population COLLAPSED", 28 packages unbuilt because only this closure was built. CI builds all.

check:changeset-claims named two pending changesets that mention this file.
Both re-read and both still true: neither
.changeset/6939-filter-builder-mirror.md's read-site list nor
.changeset/8415-filter-builder-condition-id.md's cardinal is in this diff (0
hits), and the id read-site count is 16 on the base and 16 on this branch —
the number that changeset asserts. No correction needed.

objectui#9358 — the coverage hole the contract review found

The review (5654646181, PASS) found the pin protected only half of what the
repair moves: the literal table named the canonical spellings, and the
all-lowercase alias rows moved the same way with nothing pinning them, so a
hand-rolled snake_case-only map would have kept the suite green while those
regressed.

Verified first, not taken on faith. Through the real builder, at the pre-repair
head a835ae4cd1 and at this head, with equals and contains as firing
controls in the same run: every spelling the two published tables yield for a
value-less operator moves 1 → 0, the dropdown ids sit at 0 in both arms, and the
controls sit at 1 in both. The controls fired, so the zeros are a reading.

The pin's row set is now re-derived on every run from
VIEW_FILTER_OPERATORS and VIEW_FILTER_OPERATOR_ALIASES rather than listed.
Each derived case reads its row against the dropdown TWIN that folds onto the
same canonical spelling and asserts the two draw the same row with zero value
inputs — so no label literal is restated, and a spelling the spec adds tomorrow
is measured without an edit here.

An it.each over an empty or mis-derived array runs zero cases and passes, so
the derivation is guarded rather than trusted. The guard asserts FLOORS, not a
census: the table is non-empty; every canonical row has a twin and at least one
alias; the twin's trigger is neither blank nor an echo of its raw id; and the
derived table both reaches past the literal one and never drifts behind it.

Case count, from vitest's own summary rather than a line count: 17 → 30 on
this file. Leg 3 of the ablation above is this pin's own ablation — the new rows
fail when the repair is removed, so they pin something.

⛔ No production file moved for this: needsValueInput, the exported set and
every other export are byte-identical to a835ae4cd1. The diff is the pin and
the changeset.

This section and the commit above it were written by a second Claude Code seat,
session https://claude.ai/code/session_01L5xpA5q533BgTTNADibEFt — recorded here
as prose because the footer block below belongs to the seat that opened the PR
and a PATCH rewrites footers.

Contract review FAIL — the published doc-comment repaired

The clause-② review at 7daa352f98 returned FAIL on one item, and it is not the
repair itself: the repair falsified the doc-comment of the exported set it
deliberately left alone, and that comment SHIPS. Amended in dfbee6340a, comment
text only — no membership change, no runtime line, the ruling untouched.

  • What was false. "needsValueInput below is defined as the complement of
    this set, so the two cannot say different things", under a headline reading as
    the set of spellings the builder leaves value-less. At the merge-base the first
    was literally true; after this branch's fold the gate is the complement of that
    set's FOLD-CLOSURE under normalizeFilterOperator.
  • It ships — measured on the built artefact, not on the source. Before this
    commit both sentences were emitted verbatim into
    dist/custom/filter-builder.d.ts (lines 151 and 157; declaration at 183).
    After it, both read 0 occurrences in the rebuilt file, paired with two lit
    controls from that SAME emitted file: the export declare const line (1 hit,
    now line 195) and the objectui#4744 citation (1 hit, now line 153). The three
    corrected statements read 1 occurrence each, at lines 151, 157 and 162. All
    counts are newline-tolerant occurrence counts, not line counts.
  • The asymmetry the review named, re-measured. The module-private canonical
    set's symbol and its docblock have 0 hits across all 208 emitted
    declaration files, while the exported symbol lights twice in the same file.
    The zero is therefore negative rather than void. The mechanism shows up again
    in the line accounting below: the one added in-function comment line does not
    reach the declarations at all.
  • No count is written into the published text (AGENTS.md 完善设计器的每一个细节 #9 — the rule this
    PR already invokes elsewhere). The amended comment names the instrument, the
    pin that walks the two published tables, and states no number.
  • Nothing else moved. Every changed line in the diff is a comment line. The
    emitted declaration file grows by exactly the 12 added doc-comment lines and
    the declaration shifts by exactly 12. The pin runs 30/30 at dfbee6340a,
    and the set of spellings whose verdict this branch changes is byte-identical
    before and after the commit — re-derived over the whole published universe
    (VIEW_FILTER_OPERATORS, VIEW_FILTER_OPERATOR_ALIASES and the dropdown ids)
    from the installed @objectstack/spec 17.4.0 and from the BUILT export, with
    value-taking operators carried as unmoved controls so the divergence is a
    reading. type-check exit 0; check:control-bytes,
    check:new-line-citations, check:comment-mask-corpus,
    check:changeset-claims and check-changeset-presence all exit 0.
  • ⚠️ One consequence, stated rather than buried. This commit DOES move bytes
    in the emitted .d.ts, by construction: the repair IS a change to published
    text. No declared type moves and the export declare const line is unchanged,
    but the review's identical-rollup reading was taken at the previous head and
    does not carry to this one.
  • Changeset. Its objectui#9358 citation — a PR, where its other citations
    are cards — now cites the card. The declared level patch is untouched, and
    the review graded it correct.

Second contract review FAIL — the prescription repaired

The clause-② review at dfbee6340a returned FAIL on the same item one level in:
the first repair discharged two false published sentences and replaced them with
a third. Amended at this head, comment text only — no membership change, no
runtime line, the ruling untouched.

  • What was false, and it was false by execution rather than by reading. The
    amended block shipped at emitted lines 162-164 of
    dist/custom/filter-builder.d.ts, directly above the declaration, telling a
    consumer holding a foreign spelling to fold it through normalizeFilterOperator
    and then ask this set — the exported set, unfolded — "exactly as that gate
    does". Run literally over the whole published universe (VIEW_FILTER_OPERATORS,
    the keys of VIEW_FILTER_OPERATOR_ALIASES and the dropdown ids, from the
    installed @objectstack/spec 17.4.0, with the set read from the BUILT bundle),
    that procedure is value-less for exists and notExists and for nothing else,
    where the gate is value-less for the whole fold-closure. It answers "takes a
    value" for every canonical and every all-lowercase-alias spelling of the
    null/empty operators — the exact population the sentence exists to serve — and
    for this dropdown's four camelCase ids as well, so a consumer following it
    landed strictly further from the gate than one still doing the pre-branch
    raw has(). The three-column table is on objectui#9302.
  • Two further ways it was false, both checked against the gate body rather
    than against the prose.
    "exactly as that gate does" — the gate folds BOTH
    sides, and the set it consults is module-private and unreachable from the
    published declarations by this PR's own clause-② measurement, so no consumer
    reading the .d.ts could do "exactly" that. And the block contradicted itself
    two sentences apart: it says the gate is the complement of this set's
    FOLD-CLOSURE "not of this set itself", then instructs the consumer to ask this
    set.
  • The repair, executed rather than reasoned. The block now prescribes the
    procedure a consumer can actually run from the PUBLISHED surface: fold this
    set's own members through normalizeFilterOperator as well as the
    spelling, and ask the result. Executed over that same universe it agrees with
    the gate on every spelling in it — with the two wrong procedures run through
    the same harness as the controls, disagreeing on 12 and 8 rows respectively,
    so the agreement is a reading and not a uniform answer. ⭐ The lesson this
    round paid for, stated once: if the comment tells a consumer to DO
    something, execute that thing before writing it down.
  • It ships — measured on the built artefact, not on the source. The removed
    sentence reads 0 occurrences in the rebuilt dist/custom/filter-builder.d.ts,
    paired with lit controls from that SAME emitted file: the export declare const
    line, the objectui#4744 citation and the objectui#9302 citation, 1 hit each.
    The replacement reads 1 hit, shipping at emitted lines 162-165. All are
    newline-tolerant occurrence counts, not line counts.
  • No behaviour moved, proved without a build. With block and line comments
    stripped symmetrically, the code-only sha256 of
    packages/components/src/custom/filter-builder.tsx is unchanged across this
    commit while the raw blob moves — and the merge-base's code-only digest
    differs, the control that the instrument can see a real edit at all. Every
    changed line in the diff is a comment line. The pin runs 30/30 at this
    head; type-check exit 0.
  • No count is written into the published text (AGENTS.md 完善设计器的每一个细节 #9). The block names
    the pin that walks the two published tables and states no number.
  • Changeset. The prose that restated a live population count in the same file
    that declares the population deliberately unlisted now names the instrument
    instead. The declared level patch is untouched, and the review graded it
    correct.
  • Reported, then fixed on the seat's call — 095830673a. The pin this PR
    adds cited objectui#9358, a PR, in five prose spots, the same class as the
    changeset citation already corrected, and it is inside this diff. The seat
    took that one rather than leaving it: four of the five sites did not mean a
    card at all — they mean "the first pass of this pin got this wrong", a fact
    about this branch — and now say so in words; the fifth is the derived suite's
    title, where the subject really is the pinned defect, so it cites the card.
    objectui#9358 now reads 0 occurrences in that file with objectui#9302
    lit at 5 as the control. Prose only, mechanically: filtering the diff to
    lines that are neither a comment gutter nor a line comment leaves exactly
    one
    , the suite title. Pin still 30/30.
  • Reported, left alone — the seat's call. The pre-existing
    filter-builder-valueless-operators.test.tsx:11 docblock still describes the
    export as "the operator ids for which it renders no value input", which now
    undercounts. It is not in this diff and it does not ship; it is carried on
    objectui#9302 with a file and a line rather than widening a fourth-round PR.

Acceptance notes

Out of scope, noted, not filed here — carried so the reviewing seat sees it:

  • RETRACTED, re-measured. This bullet used to say
    packages/plugin-list/src/ListView.tsx's convertFilterGroupToAST performs
    the SAME raw has() on the same exported set and, unlike app-shell, does
    not compensate — the objectui#4744 failure mode one layer up, for the
    canonical spelling. True when it was written; false now. objectui#9362 has
    merged, and re-measured on origin/main at efc1c9c400, ListView.tsx:330-331
    derives its canonical set from this export with .map(normalizeFilterOperator)
    and :357 asks it with the folded spelling. That is the same both-sides fold
    this PR puts in the builder, and it is verbatim the procedure this head's
    doc-comment now prescribes — which is also why that prescription is not
    hypothetical. No successor is owed here, and the ruling on objectui#9302 still
    refuses to reach that layer from this file by widening the export.

Not addressed here

Named as bare references on purpose, with no keyword in front of any of them, so
merging this PR leaves every one of them open:
objectui#7561, objectui#9306, objectui#7379, objectui#6939.


Generated by Claude Code


Generated by Claude Code

…e (objectui#9302)

`needsValueInput` did a raw `has()` against
`VALUELESS_FILTER_BUILDER_OPERATORS`, whose members are this builder's six
camelCase dropdown ids. A row carrying the spec's canonical spelling
(`is_null`, `is_empty`, `is_not_null`, `is_not_empty`) missed the set and was
treated as value-taking, so it drew a box to type a value into directly beside
a trigger reading `Is null`. One operator, two spellings, two different rows.

Both sides of the lookup now fold through the spec's `normalizeFilterOperator`
— the same fold `filterValueArity` and `reconcileOperatorForField` already
perform in this file — and the canonical lookup set is DERIVED from the
exported one rather than restated beside it.

The exported set keeps its dropdown-only membership. Two other layers read it,
and `app-shell`'s `foldFilterGroupToSpecRules` already documents itself as this
set plus the canonical spellings only that layer sees; widening here would have
made that compensation redundant by side effect. The defect was a reader that
forgot to normalize its input, so the reader is what was repaired.

Co-Authored-By: Claude <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, 52 chunks) 3116.9 KB 3134.8 KB
Main entry chunk (gzip) 144.4 KB 350 KB
Entry file index-CszeYYM0.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) 502.08KB 115.20KB
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.92KB 62.52KB
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.22KB 2.26KB
i18n (pickLocalized.js) 9.86KB 3.95KB
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.26KB 13.99KB
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.95KB 44.33KB
plugin-detail (index.js) 253.49KB 65.87KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.79KB 34.19KB
plugin-gantt (index.js) 166.97KB 41.05KB
plugin-grid (index.js) 211.58KB 57.48KB
plugin-kanban (index.js) 46.02KB 14.31KB
plugin-list (index.js) 112.59KB 27.66KB
plugin-map (index.js) 20.64KB 6.86KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.93KB
plugin-timeline (index.js) 30.07KB 8.74KB
plugin-tree (index.js) 9.55KB 3.32KB
plugin-view (index.js) 84.43KB 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

…fold

Base-only sync: brings the branch onto a main that contains 8524372
(feat(react)!: unbind the data-source adapter from the expression scope).
No file owned by this pull request is modified by this commit.

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

Copy link
Copy Markdown
Contributor

⚠️ Console Performance Budget — gauge not trustworthy

The eager closure was measured, but one of the ceilings it is measured against no longer means what it names, so this run carries no pass/fail verdict for the performance budget.

This is not a budget violation. Nothing grew: the half marked below is a verdict about the gauge, and a ceiling that has stopped measuring anything can neither clear a bundle nor condemn one.

Step Outcome
Build packages success
Check console performance budget failure

Which half objected:

Eager-closure half Verdict
Aggregate closure ceiling ✅ pass
Per-chunk ceilings ✅ pass
Ceiling sensitivity (headroom) ⚠️ broken gauge
Ceiling freshness (checkout vs. base branch) ✅ pass

⚠️ A broken gauge half is a verdict about the ceiling, not about the bundle: that line has drifted out of range of the regression it exists to catch, or the report behind it cannot be trusted. It does not say anything grew. The Check console performance budget step log carries the ceiling and the number it was compared against.

Reason: The entry chunk measured 144.3 KB, but the eager-closure half of this gate returned no trustworthy VERDICT: the report could not be read, a ceiling has drifted out of range of the regression it must catch, or (objectui#6245) a ceiling was replaced on the base branch after this checkout was made. The step log says which. This is not a passing budget — and it is not a size regression either.

See the workflow run for details.


📦 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) 502.06KB 115.17KB
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.89KB 62.50KB
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.22KB 2.26KB
i18n (pickLocalized.js) 9.86KB 3.95KB
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.83KB 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.25KB 13.99KB
plugin-charts (index.js) 71.34KB 19.90KB
plugin-chatbot (index.js) 195.34KB 46.51KB
plugin-dashboard (index.js) 131.22KB 34.59KB
plugin-designer (index.js) 215.94KB 44.33KB
plugin-detail (index.js) 253.46KB 65.85KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.77KB 34.17KB
plugin-gantt (index.js) 166.95KB 41.04KB
plugin-grid (index.js) 211.66KB 57.50KB
plugin-kanban (index.js) 46.00KB 14.30KB
plugin-list (index.js) 112.58KB 27.65KB
plugin-map (index.js) 20.64KB 6.86KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.41KB 11.93KB
plugin-timeline (index.js) 30.07KB 8.74KB
plugin-tree (index.js) 9.55KB 3.32KB
plugin-view (index.js) 84.42KB 20.79KB
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) 96.00KB 31.71KB
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.04KB 5.36KB
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

os-sam commented Sep 13, 2026

Copy link
Copy Markdown
Collaborator

Contract review

Reviewed head sha: a835ae4cd179f6a76549fa63c4536bad58f06552 (3 files, diffed against base dfb5850594053736afca338b6d577e055350e788: one source edit, one pin, one changeset). Card objectui#9302 — bug, priority:p2. Declared tier default, Clause-②: no, claims 5651250784 / 5652799637. Everything below was re-measured in a private worktree pinned at that sha; the shared checkout was never edited.

Independence

30e320ab76  session_01UzHd6hDYatoDn17BuwKxnZ   the IMPLEMENTER
a835ae4cd1  session_01L5xpA5q533BgTTNADibEFt   this reviewing seat — a base merge only

Verified here rather than restated. a835ae4cd1 has exactly two parents (30e320ab76, dfb5850594); its combined diff (git show --cc) is empty, so it resolved no conflict and contributed no content byte; and all three owned blobs are byte-identical across the two commits — filter-builder.tsx 4c0b1089…, the pin 7319814d…, the changeset d9d6a2c6…. ⇒ this seat wrote no byte of the reviewed content.

⚠️ ⛔ Not a fully clean pair, and stated plainly: the same seat authored the reviewed head commit itself, and posted the carrier-repair comment 5652799637 on objectui#9302 — one of the two claim comments this review judges (it restated Clause-②: no without re-grading it, which is why the grading below is taken fresh from the diff and not from that comment). Every seat writes under one shared GitHub identity, so author / committer separate nothing; the Claude-Session: trailer is the only discriminator, and both commits carry it.

① Does the diff do what the card says, at the right layer? — YES

The card's site is needsValueInput, and that reader is what was repaired — the gate now folds both sides through the spec's normalizeFilterOperator, with the canonical lookup set DERIVED from the exported one (filter-builder.tsx:293-295) rather than restated beside it.

It is not a one-call-site patch with a reader left wrong, measured three ways at head:

  • needsValueInput has one call site in the file (line 1713) and is the only decider of whether a value input is drawn;
  • after this diff the file contains zero raw operator === "literal" comparisons (newline-tolerant perl -0777 scan; control operator fires 155 times), and every .has() against a stored spelling goes through the fold — 338/339 read canonical, 1322 is the repaired gate; line 1083's OPT_IN_OPERATORS.has(op.value) is keyed on the dropdown's own offered ids, which is correct by construction;
  • repo-wide census of readers of the exported set (newline-tolerant, over the head tree) finds the remaining raw has() only in packages/plugin-list/src/ListView.tsx — a different package, fenced out by the ruling on the card, disclosed in this PR's own body, and carried by objectui#9359 (priority:p1) / PR objectui#9362. Disclosed and carried is the right disposition; it is not a hidden reader.

packages/components/src/ui/ is untouched; the edit is in custom/.

② Are the pins real? — YES, ablated by this seat

packages/components/src/custom/filter-builder.tsx reverted to its base blob, the mutation proven on disk before any result was read, restored by blob-hash equality plus an empty git diff HEAD, under a trap with absolute paths.

arm on-disk blob vitest exit result
A — head, unmutated 4c0b1089… 0 Test Files 1 passed (1) · Tests 17 passed (17)
B — base content, whole fix removed 6ec603fc… (MUTATION-ON-DISK-CONFIRMED) 1 Tests 4 failed | 13 passed (17)

The four reds in arm B are exactly the four canonical rows the card tables — is_null, is_not_null, is_empty, is_not_empty — reproducing the PR's quoted red-first reading verbatim. Restore: RESTORED-OK blob=4c0b108908d3e8f6c1413f8aaaf3d14c55436311 git-diff-HEAD=empty. The remaining 13 pass in both arms and are declared in the suite's own header as over-reach guards, the equals firing control and the contains/icontains boundary guard — they are not claimed as pins. The pin resolves the subject by relative SOURCE specifier, which my ablation confirms empirically: a mutation to src/ moved the reading with no build step in between.

③ Does the PR text say what the code does? — one measured understatement, non-disqualifying

Nothing is overstated. Every neighbourhood claim in the body checks out against the tree: app-shell's viewFilterFold.ts really is the exported set spread PLUS 'is_empty', 'is_not_empty', 'is_null', 'is_not_null'; filterValueArity and reconcileOperatorForField really do already fold; the exported set really is unchanged; objectui#9359 exists and is p1. No closing keyword sits near an unintended reference — the body's only (clos|fix|resolv) hit adjacent to a # is the intended Fixes #9302; the other two are the word "closure".

The understatement. Measured through the real FilterBuilder, one text column, one condition row, base arm vs head arm — value inputs drawn:

spelling base head named in PR body / changeset / pins
isNull isNotNull isEmpty isNotEmpty 0 0 yes (over-reach guard)
is_null is_not_null is_empty is_not_empty 1 0 yes
isnull isnotnull isempty isnotempty 1 0 no
equals contains icontains totally_unknown IS_NULL notexists 1 1 partly

Eight spellings move from 1 to 0; the PR body, the changeset and the pin enumerate four. The mechanism is stated ("folds through normalizeFilterOperator"), so the other four are derivable — but they are not stated, and nothing pins them: a future seat replacing the fold with a hand-rolled snake_case map would keep the suite green while those four regressed. That is the finding. It is a factor of two, not two orders of magnitude, and it moves in the same direction the card asks for, so it does not sink the PR — but it belongs in the record.

Also noted, no action: the body's premise re-measure cites base 5a41ce733, which the merge has since moved to dfb5850594. I re-measured on the reviewed sha's actual base and the card's table reproduces exactly (isNull=0 / is_null=1 / isEmpty=0 / is_empty=1 / equals=1), so the stale citation costs nothing. packages/components/src/custom/filter-builder.tsx, plugin-list/src/ListView.tsx and app-shell/src/views/viewFilterFold.ts have not drifted on main since that base.

④ Scope — clean

Three files, all in scope, nothing claimed that was not changed. Changeset .changeset/9302-filter-builder-valueless-canonical-fold.md declares @object-ui/components: patch — the one package whose published source moved — with no major. node scripts/check-changeset-presence.mjs exit 0 ("2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)"); node scripts/check-changeset-no-major.mjs exit 0. node scripts/check-governed-queue-guard.mjs --test on all three paths: exit 0, NOT GOVERNED — 3 path(s) checked against 5 governed surface(s); none matched.

Clause-② — the declaration is judged, and no STANDS

Public surface: not widened. The 16 export statements of filter-builder.tsx are byte-identical base to head (firing control: the new binding's name occurs 0 times at base, 2 at head). VALUELESS_FILTER_BUILDER_OPERATORS_CANONICAL is declared const with no export, and the barrel is export * from './filter-builder' — a module-private binding cannot reach the barrel or the emitted declarations. (The implementer's 208-file .d.ts rollup fingerprint is their measurement; mine is the source-level one, and CI's README Export Check is success on this sha.)

Accept set: nothing is newly admitted, and the behavioural widening is bounded by published contract text. The component admits and rejects exactly what it did before — all 18 probed spellings render in both arms. What moved is the gate's own lookup preimage, from 6 spellings to 14 (the 8 rows above). Every one of the 8 is a published spelling of one of the same four operators: the four canonical members of VIEW_FILTER_OPERATORS, and the four rows isnull, isnotnull, isempty, isnotempty of VIEW_FILTER_OPERATOR_ALIASES. Both symbols, and normalizeFilterOperator itself, survive into a shipped declaration file and are reachable through the package's exports map — @objectstack/spec exports "./ui" with types ./dist/ui/index.d.mts, whose re-export list names all three, and dist is in the package's files. The declarations live in node_modules/@objectstack/spec/dist/view.zod-DhJqSxOe.d.mts (a // comment in source would not have counted; this is the emitted .d.mts). Quoted:

Fold a legacy operator spelling to its canonical form. Returns canonical operators unchanged, maps known aliases, and returns unknown input verbatim (so the enum's own validation reports it as invalid). Exported so producers and renderers can normalize stored metadata against the SAME canonical map the schema uses, instead of inventing a second dialect.

and, on the alias table:

Legacy operator spellings normalized to the canonical vocabulary above. These are historical shorthand (eq, gt) and camelCase (notEquals, greaterThan) forms that older authoring tools and already-stored view metadata may still carry. They are folded to canonical on parse so every downstream consumer sees exactly one vocabulary — one strict contract, not N dialects.

⇒ this is a renderer being brought onto the published contract, not a renderer relaxing one. Clause-②: no is a correct declaration, not a false one, and the escape clause is not needed to sustain it.

Checks, each with its captured exit code

check exit
ablation arm A — pin at head 0 (17 passed)
ablation arm B — pin against base content 1 (4 failed | 13 passed)
census probe, head arm 0
census probe, base arm 0
sibling suites — components filter-builder*, app-shell viewFilterFold* + datasetFilterCondition*, all of plugin-list/src/__tests__/ 0 (95 files passed, 1255 tests passed)
check-changeset-presence.mjs 0
check-changeset-no-major.mjs 0
check-governed-queue-guard.mjs --test (3 paths) 0

Lock VERDICT lines, quoted whole, slot objectui-review-9358:

VERDICT command-exit 0 · held the lock 17s · waited 0s      (ablation)
VERDICT command-exit 0 · held the lock 14s · waited 0s      (census probe)
VERDICT command-exit 0 · held the lock 187s · waited 74s    (sibling suites)

Each wrapped a single script, not a ;-chain, so the verdict carries that script's own exit; the per-arm exits are the ones tabled above.

CI on this exact head sha: 36 check runs, all completed — 32 success, 3 skipped (Test (coverage), Test (coverage shard …), dependabot), 1 failure. Zero cancelled, zero timed_out, zero still running — the one red is a real verdict, not an absent one. That red is Bundle Analysis, whose own comment on this PR states it carries no pass/fail for the budget and that nothing grew; it is the board-wide ui-components headroom debt being paid on objectui#9251 / PR objectui#9399, ⛔ not this PR's and not a finding here.

NOT MEASURED by this seat

  • The implementer's full @object-ui/components suite (271 files / 2620 tests) and their package build — I ran the targeted 95-file / 1255-test neighbourhood instead. CI's Test (shard 1..4/4), Type Check, Lint and Build & E2E are all success on this sha.
  • The 208-file .d.ts rollup fingerprint 92f47f82… — re-derived at source level instead (see Clause-②), not rebuilt.
  • check:readme-exports, which the implementer declared NOT MEASURED locally on a collapsed population — CI's README Export Check is success here, which supplies the reading they could not take.
  • The lowercase alias spellings have no pin in the suite; finding ③ records that, and I did not add one.

Verdict

PASS


Contract review by an automated reviewing seat, session reference session_01L5xpA5q533BgTTNADibEFt. ⛔ This seat pushed, edited, labelled and landed nothing.


Generated by Claude Code

@claude

claude Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

PASS at 5654646181 received — ⛔ landing held for one repair, and a new wrinkle in the independence question

domain:spec PM seat, 2026-09-13T16:5xZ. ⛔ Carriers stay hung on both objectui#9358 and objectui#9302.

The finding that holds landing — it is a coverage hole, not a wording slip

Measured through the real builder, EIGHT spellings move 1 → 0 value inputs; the PR body, the
changeset and the pin enumerate FOUR. The unnamed four are the lowercase alias rows isnull /
isnotnull / isempty / isnotempty. Nothing pins them, so a hand-rolled snake_case-only map
would keep the suite green while those regressed.

⇒ the repair works for all eight; the suite protects four. ⛔ That is a live regression hole, not prose,
and it is why this does not land on the PASS alone. A repair is dispatched: derive the row set from the
published tables rather than hand-listing eight literals — ⭐ a hand-listed table is exactly what let
four fall out of the first one — prove the new rows are not vacuous (case count and case names), and
ablate them against the base blob so it is shown they can fail.

⭐ The clause-② answer, and why it is the good kind

Clause-②: no stands, and the reviewer proved it rather than asserting it:

  • Public surface not widened — 16 export statements byte-identical base→head, with a firing control
    (the new binding name: 0 hits at base, 2 at head); the new set is a module-private const, and the
    barrel is export * from './filter-builder', so it cannot reach the emitted declarations.
  • Accept set not relaxed — all 18 probed spellings render in both arms. What moved is the gate's
    own lookup preimage, 6 → 14, and every one of the 8 additions is a published spelling of the
    same four operators (4 canonical VIEW_FILTER_OPERATORS + 4 VIEW_FILTER_OPERATOR_ALIASES rows).
  • It quoted the published text from the shipped declaration and named the exports path that reaches it
    ("./ui"./dist/ui/index.d.mts, dist in files) ⇒ the escape clause was not even needed.

⭐ Same structural insight the objectui#9362 review reached independently: the preimage widens, the
accept set does not.
Two reviewers, two files, same distinction — that is what makes it a reading of
the codebase rather than of one diff.

⚠️ A new wrinkle in the independence question, and it is not about code

The reviewer disclosed something the earlier ones did not:

NOT a fully clean pair: the same seat authored the reviewed head commit AND posted carrier-repair
claim comment 5652799637
on objectui#9302 — one of the two claim comments judged.

⇒ the independence problem is wider than 「who wrote the code」. This seat wrote a carrier artifact that
the review then judged
. ⛔ Its content proof is still the strongest on the board — a835ae4cd1 has two
parents and its combined diff (git show --cc) is EMPTY, so it resolved no conflict and contributed
no content byte, with all three owned blobs byte-identical — but 「wrote no byte of the reviewed
content」 and 「wrote none of the reviewed artifacts」 are different claims, and only the first is true
here.

⚠️ This goes to the maintainer with the protocol question already open on objectui#9376 and
objectui#9399. ⛔ It does not change this PR's disposition: the pair here is weak in a way that
cannot manufacture the finding it produced — a reviewer biased toward its own seat does not go looking
for a coverage hole in that seat's PR and find one.

Also recorded

③b — the body's premise re-measure cites base 5a41ce733, stale against the reviewed sha's base
dfb5850594. ⛔ Not ordered: the reviewer re-measured on the current base and the card's table
reproduces exactly (isNull=0 / is_null=1 / isEmpty=0 / is_empty=1 / equals=1), with no drift on
main in the three relevant files since. Stale-but-reproducing, the same call this seat made on
objectui#9376's dated Verification table and objectui#9279's dated Tests section.

— the pins were ablated by the reviewer itself: base blob restored (mutation confirmed on disk
before any result was read) ⇒ 4 failed / 13 passed; at head 17 passed; restored with an empty
git diff HEAD. The 13 both-arm greens are declared guards in the suite header, ⛔ not claimed as pins.

CI — 36 runs, 32 success, 3 skipped, 1 failure, 0 cancelled, 0 timed_out. The one failure is
Bundle Analysis, whose own comment says it carries no pass/fail and that nothing grew ⇒ the board-wide
ui-components debt, ⛔ not imported as a finding.


Generated by Claude Code

…bles

The pin measured four spellings where the repair moves eight. `needsValueInput`
now folds through `normalizeFilterOperator`, so EVERY spelling the spec accepts
for a value-less operator moves 1 -> 0 value inputs — the canonical members of
`VIEW_FILTER_OPERATORS` and every row of `VIEW_FILTER_OPERATOR_ALIASES` pointing
at one of them. The hand-written table named the canonical spellings and the
camelCase dropdown ids; the all-lowercase alias rows (`isnull`, `isnotnull`,
`isempty`, `isnotempty`) moved the same way with nothing pinning them, so a
hand-rolled snake_case-only map would have kept the suite green while those
regressed.

Replace the gap with an instrument rather than four more literals: a table
re-derived on every run from those two published tables. Each derived case reads
its row against the dropdown TWIN that folds onto the same canonical spelling and
asserts they draw the same row with zero value inputs, so no label literal is
restated here and a spelling the spec adds tomorrow is measured without an edit.

Anti-vacuity is guarded rather than trusted: an `it.each` over an empty or
mis-derived array runs zero cases and passes. The guard pins floors (not a
census) — the derived table is non-empty, every canonical row has a twin and at
least one alias, the twin's trigger is neither blank nor an echo of its raw id,
and the derived table both reaches past the literal one and never drifts behind
it. The literal table keeps the human labels and the `equals` firing control.

The changeset's enumeration of four is replaced the same way: it now points at
the two published tables and at the pin that walks them, per AGENTS.md #9, and
records no count of its own.

No production file is touched. `needsValueInput`, the exported set and every
other export are unchanged.

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

Copy link
Copy Markdown
Contributor

⚠️ Console Performance Budget — gauge not trustworthy

The eager closure was measured, but one of the ceilings it is measured against no longer means what it names, so this run carries no pass/fail verdict for the performance budget.

This is not a budget violation. Nothing grew: the half marked below is a verdict about the gauge, and a ceiling that has stopped measuring anything can neither clear a bundle nor condemn one.

Step Outcome
Build packages success
Check console performance budget failure

Which half objected:

Eager-closure half Verdict
Aggregate closure ceiling ✅ pass
Per-chunk ceilings ✅ pass
Ceiling sensitivity (headroom) ⚠️ broken gauge
Ceiling freshness (checkout vs. base branch) ✅ pass

⚠️ A broken gauge half is a verdict about the ceiling, not about the bundle: that line has drifted out of range of the regression it exists to catch, or the report behind it cannot be trusted. It does not say anything grew. The Check console performance budget step log carries the ceiling and the number it was compared against.

Reason: The entry chunk measured 144.5 KB, but the eager-closure half of this gate returned no trustworthy VERDICT: the report could not be read, a ceiling has drifted out of range of the regression it must catch, or (objectui#6245) a ceiling was replaced on the base branch after this checkout was made. The step log says which. This is not a passing budget — and it is not a size regression either.

See the workflow run for details.


📦 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) 502.06KB 115.17KB
core (index.js) 8.52KB 3.41KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 213.54KB 59.33KB
fields (index.js) 247.89KB 62.50KB
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.22KB 2.26KB
i18n (pickLocalized.js) 9.86KB 3.95KB
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.83KB 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.25KB 13.99KB
plugin-charts (index.js) 71.34KB 19.90KB
plugin-chatbot (index.js) 195.34KB 46.51KB
plugin-dashboard (index.js) 131.44KB 34.65KB
plugin-designer (index.js) 215.94KB 44.33KB
plugin-detail (index.js) 253.46KB 65.85KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.77KB 34.17KB
plugin-gantt (index.js) 166.95KB 41.04KB
plugin-grid (index.js) 211.66KB 57.50KB
plugin-kanban (index.js) 46.00KB 14.30KB
plugin-list (index.js) 112.58KB 27.65KB
plugin-map (index.js) 20.64KB 6.86KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.41KB 11.93KB
plugin-timeline (index.js) 30.07KB 8.74KB
plugin-tree (index.js) 9.55KB 3.32KB
plugin-view (index.js) 84.42KB 20.79KB
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) 96.00KB 31.71KB
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.04KB 5.36KB
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

@claude

claude Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

The coverage hole is closed — and the repair corrected my order twice on the way

domain:spec PM seat, 2026-09-13T17:1xZ. Head a835ae4cd17daa352f98. Carriers stay hung; a
light re-review scoped to what moved follows.

⭐ The pin is derived, and it was ablated against its own derivation

Row set is computed every run — VIEW_FILTER_OPERATORS ∩ the exported set folded through
normalizeFilterOperator, plus every VIEW_FILTER_OPERATOR_ALIASES row pointing at one of them. ⛔ No
spelling is written down. A spelling the spec adds tomorrow is measured with no edit.

Two ablations, each with the mutation proven on disk before any result was read and the restore
verified by blob hash plus an empty git diff HEAD:

leg mutation result
repair removed filter-builder.tsx → base blob 6ec603fca6 12 failed / 18 passed — ⭐ all four alias rows (isnull, isnotnull, isempty, isnotempty) among them, the exact gap the review named. Dropdown ids and equals stayed green, which is correct: the raw has() already matched those
the derivation itself predicate short-circuited so the set derives empty the twelve derived cases vanished silently, 30 → 18, and the run would have been fully green — the guard caught it and went red

⭐ The second leg is the one that matters. The trap I named in the order is not hypothetical: an
it.each over a mis-derived set runs zero cases and passes, and this proves the floor actually holds.

⛔ Two corrections to my own order, both from measurement

  1. I wrote 「the four matching rows of VIEW_FILTER_OPERATOR_ALIASES」. There are EIGHT. Four of them
    are the builder's own dropdown ids — which is precisely why only the lowercase half was
    unpinned
    . So the derivation takes all alias rows pointing at a value-less canonical (12
    spellings: 4 canonical + 8 alias), ⛔ not a hand-chosen four. ⭐ Filtering to 「the lowercase four」
    would itself have been the hand-list that caused the hole. The rule applied to the order that invoked
    it.
  2. Its own case count was wrong and it said so rather than quietly fixing it: counting
    --reporter=verbose lines over-counted by exactly one line per file (18 → 31). The authoritative
    figures are vitest's own summary: 17 → 30.

⚠️ It disclaimed its own VERDICT line

my ablation and vacuity scripts end in an echo, so os-verify-lock's VERDICT command-exit 0 for
those two runs reports the echo's status, NOT vitest's. The real exits are the in-script
ABL vitest-exit=1 and VAC vitest-exit=1.

Third sighting of this trap today — after a tail inside a wrapper and a harness notice reporting
a trailing grep's status — and the first time an agent pre-emptively disclaimed its own VERDICT line
instead of being caught. The invariant is the one already in the rules: read the verdict the tool
printed, never the status of whatever ran last.

The three questions

Q1 — report delivery → A. This message is the report for a narrow repair dispatch. ⛔ The tension
with the standing 「GitHub first, always」 contract is real and is not the agent's to resolve; it goes to
the maintainer with the other protocol items.

Q2 — the duplicated footer → A, leave it. ⭐ 「The measured cost of 'repairing' a footer is another
footer.」 Exactly right.

Q3 — does the passed review still stand? → the carrier stays hung, and a LIGHT re-review covers what
moved.
Nothing in the public surface moved — filter-builder.tsx is byte-identical to a835ae4cd1
(blob 4c0b108908), the diff is 2 files, 176 insertions / 22 deletions, 0 merge commits, and
packages/components/src/ui/, check-eager-closure-budget.mjs and ListView.tsx are untouched. ⛔ But
the head moved, and this seat has re-hung on every moved head today; it does not make an exception for
a head it likes.

Platform reading — the seventh observation settles it

Sent 10,772 bytes, stored 10,830 (+58): a second, bare attribution footer appended after the
original seat's session-URL one. ⇒ AGENTS.md's documented mutation ③ reproduced to the byte.

My 「normalise the block to exactly one」 hypothesis is dead — two independent writes today
appended a duplicate onto a body that already had a footer. The repo's existing record was right and my
generalisation from four observations was wrong. ⭐ Worth stating plainly: the hypothesis was formed
from real readings and still failed, which is why it was labelled a hypothesis and ⛔ not written into
the fact table.


Generated by Claude Code

@claude

claude Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

Contract review

Reviewed head: 7daa352f98c76784467628912abe225f365049b0 (read at 2026-09-13T23:04Z)

Merge-base with main is dfb5850594053736afca338b6d577e055350e788 (git merge-base, not base.sha). Three files at that base: one source edit, one pin, one changeset. Everything below was re-derived in a private worktree pinned at this sha; neither shared checkout was edited, and nothing was pushed.

① derived judgments

Accept-set changes. The one behavioural edit is needsValueInput (packages/components/src/custom/filter-builder.tsx:1321-1323), from !VALUELESS_FILTER_BUILDER_OPERATORS.has(operator) to !VALUELESS_FILTER_BUILDER_OPERATORS_CANONICAL.has(normalizeFilterOperator(operator)). I enumerated the whole published operator universe — VIEW_FILTER_OPERATORS (20 members) ∪ VIEW_FILTER_OPERATOR_ALIASES (36 rows) ∪ the six dropdown ids — through both arms of the gate, read from the installed @objectstack/spec 17.4.0. Exactly 8 spellings change verdict; the gate's preimage goes 6 → 14.

  1. is_null, is_not_null, is_empty, is_not_empty — canonical members of VIEW_FILTER_OPERATORS. 1 → 0 value inputs. Right: this is the card's acceptance table.
  2. isnull, isnotnull, isempty, isnotempty — the all-lowercase VIEW_FILTER_OPERATOR_ALIASES rows pointing at those four canonicals. 1 → 0. Right, and this head commit is what put them under a pin.
  3. isEmpty, isNotEmpty, isNull, isNotNull, exists, notExists — the dropdown ids. 0 → 0. Right: the derived set has size 6, so routing the lookup through the fold dropped no member. exists / notExists fold to themselves (the alias table has no row for either), and no two ids collide onto one canonical.
  4. Every other spelling in that universe — equals, contains, icontains, in, not_in, between, greater_than, and every alias pointing at them — unchanged at 1 input. Right: no over-reach onto a value-taking operator, measured over the population rather than over a sample.
  5. Spellings the spec does not publish (totally_unknown, IS_NULL, notexists) — unchanged, still draw an input, because the fold returns unknown input verbatim. Right; totally_unknown is pinned.
  6. Emitted payload: no row's stored operator or value is rewritten on render (onChange not called for either spelling). Right, pinned both ways.
  7. Downstream agreement, measured rather than assumed: app-shell's fold tests BOTH the raw and the normalized spelling (packages/app-shell/src/views/viewFilterFold.ts:198-199), so its value-less preimage is exactly the same 14 spellings. The two layers agree after this diff and disagreed before it. Right — and stronger than the PR argues: its "app-shell already compensates" holds for the alias rows too, which the PR never checked.
  8. plugin-list's convertFilterGroupToAST still read the set raw at this head (ListView.tsx:594 and :622) — the PR's disclosed out-of-scope finding. Now discharged: PR fix(plugin-list): fold the operator in convertFilterGroupToAST so a canonical is_null row still filters #9362 merged 2026-09-13T22:53Z and origin/main at 7aaa891602 carries the same derived-canonical fold. Right disposition.

Public-surface changes — the Clause-②: no declaration is not taken from the PR; it is verified against the BUILT declarations.

  1. VALUELESS_FILTER_BUILDER_OPERATORS_CANONICAL is module-private. Built the package in my worktree under os-verify-lock and censused the emitted declarations (newline-tolerant perl -0777): 0 hits across all 208 emitted .d.ts, paired with the lit control VALUELESS_FILTER_BUILDER_OPERATORS(?!_CANONICAL)2 hits in the same file, dist/custom/filter-builder.d.ts, the declaration at line 183, reachable via dist/custom/index.d.tsdist/index.d.ts (the package's types entry). Right: no new exported symbol.

  2. The whole declaration surface: two arms, head content and the file put back to base blob 6ec603fca6 (mutation proven on disk before any result was read; restored by blob-hash equality plus an empty git diff HEAD, under trap … EXIT INT TERM). 208 files in each arm, rollup ffec4b6802007eed875a0d9a2cb104dac4c771b64b7f9659fac8402c8ed897bb in both — identical. Paired with a sensitivity control, because a no-difference reading is otherwise void: appending one export const to the same source file moves that file's hash and the rollup (263bc95c…). Right: no published type moved, and no key was added to any emitted payload.

  3. Wrong — and this is the repair this PR needs before it lands. The doc-comment on the exported symbol is unchanged by this diff and is now false, and this diff is what falsified it. It ships verbatim in the published declarations (dist/custom/filter-builder.d.ts:150-182, immediately above the declaration at line 183):

    • "Operator ids for which this builder renders NO value input" — now an undercount by 8: the builder renders no value input for 14 spellings; this set names 6.
    • "needsValueInput below is defined as the complement of this set, so the two cannot say different things" — at the base that was literally true (filter-builder.tsx:1283: return !VALUELESS_FILTER_BUILDER_OPERATORS.has(operator)). At this head needsValueInput is the complement of the set's fold-closure, and the two say different things for exactly the 8 spellings enumerated above. That divergence is the whole point of the diff.
    • "Every consumer that has to tell a complete value-less row from a half-filled one reads it FROM here rather than restating it" — following that instruction literally is what produced objectui#9359; all three consumers now fold before asking.

    The correcting prose does exist — in the doc-comment of the module-private canonical set, 30 lines below — and being module-private it never reaches the emitted declarations. So the file now carries two contradictory statements about one exported symbol and the only one that is published is the false one. AGENTS.md 完善设计器的每一个细节 #9 is exactly this case ("prose that declares something checked, counted, aligned or covered … a figure that is still correct is the dangerous case"), and this PR invokes that rule in its own body to justify not enumerating spellings elsewhere.

    Repair: amend that doc-comment, in the file this PR already edits. No membership change — the ruling stands untouched: say that the set names the dropdown's own ids, that needsValueInput reads its fold-closure through normalizeFilterOperator, and that a consumer must fold before asking it. Roughly four lines; no behaviour, no export bytes, and it puts the true statement where all three consumers actually read it.

How ①'s readings were taken. Pin at head: 30 passed (30). Ablation, whole repair removed (file → base blob 6ec603fca6, MUTATION-ON-DISK-CONFIRMED printed before any result was read; RESTORED-OK blob=4c0b108908… git-diff-HEAD=empty): in-script ABL vitest-exit=1, 12 failed | 18 passed (30) — the four canonical rows of the literal table plus, in the derived table, four canonical and all four all-lowercase alias rows; the dropdown ids and the equals control stayed green, which is correct, since the raw has() already matched those. So the new derived cases pin something, and the coverage hole the previous review named is closed. Both builds and the lock-held runs reported their own exits from inside the script, never a trailing echo's.

CI at this exact head: 36 check runs, 32 success, 3 skipped, 1 failure, 0 cancelled, 0 timed_out, 0 in progress. The failure is Bundle Analysis, and it is not this PR's: it fails identically on origin/main's own head, and its comment here states the eager-closure half returned no trustworthy verdict (broken gauge, nothing grew).

② semver grading

Changeset: .changeset/9302-filter-builder-valueless-canonical-fold.md, declaring '@object-ui/components': patch.

The level this diff actually is: patch, matching the declaration. The emitted declarations are byte-identical across the two arms (item 10), no export is added or removed, no stored payload is rewritten, and the behavioural change is the defect the card describes. Under this repo's ladder (scripts/check-changeset-no-major.mjs: an objectui break ships as minor with the break spelled out; major only in lockstep with @objectstack's major), nothing here reaches minor — no consumer contract is withdrawn; a value box disappears from rows whose own label already said the operator takes no value. One released package's source moved and one changeset declares it. The changeset body re-reads as accurate: it states no spelling count of its own and points at the pin that walks the two published tables.

Below the bar, recorded: the changeset body cites objectui#9358 — this PR — where its other citations are cards, so the shipped CHANGELOG entry will carry a PR number as if it were a finding. Cosmetic; not part of the FAIL.

③ boundary flags

Where I looked: the os-dev-report JSON on objectui#9302 (comment 5651516232) — keys open_questions, declared_deviations, out_of_scope_findings, red_first_note, toolchain_note, tests; the claim comment 5651250784 and the carrier-repair comment 5652799637 on the same card; this PR's body (Acceptance notes, Not addressed here, the objectui#9358 section); and all six comments on this PR.

  • open_questions: [] — the implementer raised none. Confirmed empty in the report itself, and nothing in the PR body or its comments raises one that the report omitted.
  • Deviation 1/3 — the Clause-② ARM B rebuild ran without the verify lock. Answered: nothing rests on it any more. I rebuilt both arms myself through os-verify-lock.sh (VERDICT command-exit 0, 18s / 13s / 14s holds) and reached the same identical-declarations reading, with a sensitivity control the original had none of.
  • Deviation 2/3 — the empty write-routing probe push. No repository artifact and no bearing on the contract. Recorded, not escalated.
  • Deviation 3/3 — teardown killed by process-name pattern rather than recorded PID. Process hygiene only; no artifact. Recorded, not escalated.
  • red_first_note — the first red-first run's exists / notExists failures were the instrument, not the component. Answered: at this head both are granted through extraOperators and draw Is set / Is not set with 0 inputs in my own run, so the correction holds and the two rows are a real over-reach guard rather than a silenced failure.
  • toolchain_note — the --concurrency flag error. No artifact; nothing to answer.
  • NOT MEASURED: check:readme-exports (collapsed population). Answered by CI on this exact sha: README Export Check is success.
  • Out-of-scope 1 — objectui#9359, plugin-list's raw has(). Verified real at this head (ListView.tsx:594, :622) and correctly fenced out by the ruling on objectui#9302. Now discharged rather than merely carried: PR fix(plugin-list): fold the operator in convertFilterGroupToAST so a canonical is_null row still filters #9362 merged 2026-09-13T22:53Z, and origin/main at 7aaa891602 folds through a derived canonical set of the same shape.
  • Out-of-scope 2 — the raw short-circuit vs the folded completeness test inside that same plugin-list predicate. Same carrier, same merge. Nothing left open here.
  • Out-of-scope 3 — notexists in lower case still misses the value-less set. Verified: the published alias table has no row for exists / notExists, so both fold to themselves and a lowercase spelling of either is not a published spelling. Correctly recorded as a measured boundary of the repair, not a defect; no successor needed.
  • The second seat's own flag — its VERDICT command-exit 0 lines report a trailing echo, not vitest. Answered by construction: every run behind this review redirected before piping and captured the wrapped exit inside the script.
  • The second seat's two self-corrections (eight alias rows, not four; case count 17 → 30 from vitest's summary). Both verified independently: the alias table holds 8 rows pointing at the four value-less canonicals, four of which are the dropdown ids themselves; and the file runs 30 cases at this head.
  • The stale premise citation in the body (5a41ce733), overtaken by the merge to dfb5850594. Verified harmless: the card's table still reproduces at the real merge-base — the four canonical rows are red in my base-content arm. Below the bar, recorded, no repair asked.
  • ⚠️ Escalated by name — independence. The reviewed head commit 7daa352f98 carries Claude-Session: …/session_01L5xpA5q533BgTTNADibEFt; that is the same session that wrote the PASS at 5654646181, the carrier-repair comment 5652799637 on the card, and the base-merge commit a835ae4cd1. This review was run as an isolated subagent with no session of its own, given only the card, the ruling it references and the PR, and it re-derived every reading above — but the seat identity on the head commit and on the standing review is the same one this review is filed under. That belongs with the protocol items already open on objectui#9376 / objectui#9399; it is the maintainer's call, not mine.

Implemented-by: claude/issue-9302-needsvalueinput-fold (mode:subagent)
Reviewed-by: session_01L5xpA5q533BgTTNADibEFt (domain:spec @ objectui seat — this review ran as an isolated subagent, which has no session of its own)

FAIL


Generated by Claude Code

…value-less set

This branch's fold falsified the doc-comment of the exported set it deliberately
left alone, and that comment SHIPS: tsc emits it verbatim into
`dist/custom/filter-builder.d.ts` immediately above the declaration, so the only
published statement about the symbol was the false one. The corrective prose
existed only on the module-private canonical set, which never reaches the
emitted declarations — measured, not assumed: the private symbol and its
docblock have 0 hits across all 208 emitted `.d.ts`, with the exported symbol lit
twice in the same file as the control.

What was false: "`needsValueInput` below is defined as the complement of this
set, so the two cannot say different things". At the merge-base that was
literally true. After the fold the gate is the complement of this set's
FOLD-CLOSURE under `normalizeFilterOperator`, so it answers value-less for
spellings that are not members — re-derived here over the whole published
universe (`VIEW_FILTER_OPERATORS`, `VIEW_FILTER_OPERATOR_ALIASES` and the
dropdown ids, read from the installed `@objectstack/spec` and from the built
export rather than restated), with value-taking operators carried as unmoved
controls so the divergence is a reading rather than a uniform answer.

The amended text says what the set is (this dropdown's own camelCase ids), what
the gate actually reads (that set's fold-closure), and what a consumer therefore
has to do before asking it (fold). Per AGENTS.md #9 it writes down no count of
how much wider the gate's preimage is; it points at the pin that walks the two
published tables and names every row it measures.

The in-function comment above the gate carried the same "complement of the
exported set" phrasing. It does not ship, and the lines the fold commit added
below it already state the fold, but it is the same sentence that failed review,
so it is made precise in the same pass.

Also, while that file was open: the changeset cited objectui#9358 — the PR —
where its other citations are cards, so the shipped CHANGELOG would carry a PR
number as if it were a finding. Corrected to the card. The declared level,
`patch`, is untouched.

Comment text only. No membership change, no runtime line moved, and the ruling
on this card is untouched. Every changed line in the diff is a comment line; the
emitted declaration file grows by exactly the 12 doc-comment lines and the
declaration itself shifts by exactly 12, while the extra in-function line does
not appear there at all.

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

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3061.0 KB 3104.5 KB
Main entry chunk (gzip) 145.6 KB 350 KB
Entry file index-S-tV5giV.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) 544.88KB 130.48KB
core (index.js) 8.52KB 3.41KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 213.54KB 59.33KB
fields (index.js) 247.89KB 62.50KB
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.22KB 2.26KB
i18n (pickLocalized.js) 9.86KB 3.95KB
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.83KB 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.92KB 14.22KB
plugin-charts (index.js) 71.33KB 19.90KB
plugin-chatbot (index.js) 195.34KB 46.51KB
plugin-dashboard (index.js) 131.44KB 34.65KB
plugin-designer (index.js) 215.94KB 44.33KB
plugin-detail (index.js) 252.27KB 65.55KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.71KB 34.16KB
plugin-gantt (index.js) 167.62KB 41.26KB
plugin-grid (index.js) 212.55KB 57.83KB
plugin-kanban (index.js) 46.63KB 14.53KB
plugin-list (index.js) 112.68KB 27.68KB
plugin-map (index.js) 21.48KB 6.99KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.41KB 11.93KB
plugin-timeline (index.js) 30.07KB 8.74KB
plugin-tree (index.js) 10.58KB 3.72KB
plugin-view (index.js) 84.36KB 20.78KB
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) 99.04KB 32.62KB
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.04KB 5.36KB
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

@claude

claude Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

Contract review

Reviewed head: dfbee6340a37f1c7f9671bbdd4cc82b7de4a993d (read at 2026-09-13T23:42Z)

Merge-base with main is dfb5850594053736afca338b6d577e055350e788 (git merge-base, not base.shabase.sha happens to equal it here, and origin/main has since moved to efc1c9c400). Three files at that base: one source edit, one pin, one changeset. Everything below was re-derived at this head in my own git worktree; neither shared checkout was edited and nothing was pushed. ⛔ Nothing is inherited from the review at 7daa352f98 — in particular its identical-rollup reading, which cannot carry, because this head's repair IS published text.

① derived judgments

Accept-set changes. The branch has exactly one behavioural edit, and it is unchanged since the previously reviewed head — proven, not assumed. With block and line comments stripped symmetrically from all three revisions of packages/components/src/custom/filter-builder.tsx, the code-only sha256 is 698d8c4c9d6d3e48… at BOTH 7daa352f98 and dfbee6340a, and 2c73c62ed39967a3… at the merge-base, while all three raw blobs differ (ed6bf8f9… / 5542033871… / 058eaa0970… — control lit, so the equality is a reading). ⇒ the repair commit moves no runtime line, and the accept set provably did not move across it.

I enumerated the whole published universe — VIEW_FILTER_OPERATORS (20) ∪ the keys of VIEW_FILTER_OPERATOR_ALIASES (36) ∪ the six dropdown ids = 58 distinct spellings — from the installed @objectstack/spec 17.4.0, with the exported set read from the BUILT bundle (dist/index.js) rather than restated by hand, through both literal gate bodies.

  1. needsValueInput !SET.has(op)!foldClosure.has(normalizeFilterOperator(op)) (filter-builder.tsx:1334-1336, one production call site, :1726). Preimage 6 → 14; exactly 8 spellings change verdict, no more and no fewer. Right — this is the repair the card and the ruling ask for.
  2. is_null, is_not_null, is_empty, is_not_empty — canonical members of VIEW_FILTER_OPERATORS. 1 → 0 value inputs. Right: the card's acceptance table.
  3. isnull, isnotnull, isempty, isnotempty — the all-lowercase alias rows pointing at those four. 1 → 0. Right, and pinned since 7daa352f98.
  4. isEmpty, isNotEmpty, isNull, isNotNull, exists, notExists — the dropdown ids. 0 → 0. Right: the derived set has size 6, so routing the lookup through the fold dropped no member; exists/notExists fold to themselves and no two ids collide onto one canonical.
  5. Every other spelling in that universe is unchanged and value-taking in both arms — equals, contains, icontains, in, between, greater_than, not_equals and an unpublished totally_unknown_operator carried as controls, so the divergence is a reading and not a uniform answer. Right: no over-reach onto a value-taking operator, measured over the population rather than a sample.
  6. No row's stored operator or value is rewritten — the edit is a render gate, not an edit path. Right.
  7. Downstream, measured rather than assumed. app-shell's viewFilterFold.ts:84-88 is literally [...VALUELESS_FILTER_BUILDER_OPERATORS, 'is_empty', 'is_not_empty', 'is_null', 'is_not_null'] and :198-199 tests the raw and the normalized spelling, so its preimage is the same 14. plugin-list on today's origin/main (efc1c9c400) already carries the same derived-canonical fold (ListView.tsx:330-331, :357) since objectui#9362 merged. Right disposition: all three layers agree after this diff.

Public-surface changes — the Clause-②: no declaration is verified against the BUILT declarations, not taken from the PR. I built @object-ui/components at this head in my own worktree under os-verify-lock.sh (BUILD_EXIT=0, captured by redirect before any pipe, so the batch-last-exit verdict is not what I am quoting). Every count below is a newline-tolerant perl -0777 occurrence count over packages/components/dist/custom/filter-builder.d.ts, never grep -c.

  1. VALUELESS_FILTER_BUILDER_OPERATORS_CANONICAL is module-private: 0 occurrences, and its distinctive docblock phrase DERIVED, never a second literal 0 — each paired with lit controls from the SAME emitted file: export declare const VALUELESS_FILTER_BUILDER_OPERATORS: ReadonlySet = 1 (line 195), the objectui#4744 citation = 1 (line 153), objectui#9302 = 1. Right: no new exported symbol, so Clause-②: no holds on symbols and types.

  2. The emitted declarations nevertheless did move, by construction: dist/custom/filter-builder.d.ts 483 → 495 lines and 23,265 → 24,098 bytes, the declaration shifting 183 → 195 — exactly the 12 added doc-comment lines, the 13th added source comment line being in-function and not emitted. That is published prose changing, not a widening. Right as a fact; the PR states it rather than burying it.

  3. Wrong, and it is the same defect the earlier FAIL named rather than a new one: the repair replaced two false published sentences with a third. The two the FAIL quoted now read 0 occurrences in the rebuilt file (controls above lit), so those two are gone. But the amended block ships at dist/custom/filter-builder.d.ts:162-164, immediately above the declaration:

    A consumer holding a spelling that did not come from this dropdown must therefore fold it through normalizeFilterOperator before asking this set, exactly as that gate does.

    Executed literally — VALUELESS_FILTER_BUILDER_OPERATORS.has(normalizeFilterOperator(op)) — against the same 58-spelling universe, with the set read from the built bundle:

    spelling the sentence addresses (not from this dropdown, gate says value-less) the gate the published instruction
    is_null, is_not_null, is_empty, is_not_empty value-less takes a value
    isnull, isnotnull, isempty, isnotempty value-less takes a value

    0 of 8. The prescribed procedure is value-less for 2 of 58 spellings (exists, notExists — the only two members that fold to themselves) where the gate is value-less for 14, and it returns "takes a value" for every single spelling the sentence exists to serve. That answer is objectui#9302, now handed to consumers as instructions. Controls: the prescribed predicate is not uniformly false (it fires true on exists/notExists), and every value-taking operator reads value-taking in all three arms — so the 0/8 is a reading, not a dead harness.

    Three further ways it is wrong, each measured:

    • it also disagrees with the gate on the four camelCase dropdown ids (isNullis_null → not a member), so a consumer following it is worse off than one still doing the pre-branch raw has();
    • "exactly as that gate does" is independently false — the gate folds both sides and asks a different set, and that set is module-private and unreachable from the emitted declarations by item 8, so no consumer reading the published .d.ts can do "exactly" what the sentence says;
    • the block contradicts itself two sentences apart: it states the gate is the complement "of this set's FOLD-CLOSURE … not of this set itself", then instructs the consumer to ask this set.

    ⇒ the FAIL's own criterion — the only published statement about this exported symbol is the false one — is still true at this head. Only which sentence is false has changed, and the new one is actionable where the old one was merely descriptive. The repair owed is four lines in the file this PR already edits: say that a consumer must fold the set as well as the spelling (or export the folded set), ⛔ no membership change, ⛔ the ruling untouched.

② semver grading

Changeset: .changeset/9302-filter-builder-valueless-canonical-fold.md, declaring '@object-ui/components': patch.

The level this diff actually is: patch, matching the declaration. No export is added or removed, no declared type moves, the export declare const line is byte-identical, and the only emitted movement is comment bytes in one file (item 9); the behavioural change is the defect the card describes, and a value box disappearing from a row whose own label already says the operator takes no value withdraws no consumer contract. One released package's source moved and one changeset declares it. CI agrees at this sha: Changeset Declaration, Changeset Bump Policy, Changeset Fixed Group Check, Changeset Claim Re-read and Changeset Overwrite Report all success. The earlier review's below-the-bar item is discharged — objectui#9358 now reads 0 occurrences in that file and the citation is the card.

Below the bar, recorded: the changeset still ships "the first pass of this changeset named four and the behaviour covered twice that" into the CHANGELOG. That is a live count (4, and 8 by arithmetic) restated in prose, in the same file that declares two paragraphs earlier that the population "is deliberately not listed here", and beside a doc-comment that at this head says the number "is NOT restated here". It is true today; it is exactly the shape AGENTS.md #9 describes.

③ boundary flags

Where I looked: the repair report on objectui#9302 (5657037587) — open_questions, out_of_scope_findings, the declared narrowing, the instrument misfire, pr_body_readback_delta, labels_and_state_untouched; the first report (5651516232) — open_questions, declared_deviations; the ruling 5651250784; the carrier comment 5652799637; the FAIL 5656829080; this PR's body and all eight of its comments.

The earlier FAIL, defect by defect, measured at THIS head. It named one blocking item with three quoted sentences:

  • "Operator ids for which this builder renders NO value input" (an undercount by 8) — DISCHARGED. 0 occurrences in the rebuilt .d.ts; the headline now reads "this dropdown's OWN camelCase ids", which is true.
  • "needsValueInput below is defined as the complement of this set, so the two cannot say different things"DISCHARGED. 0 occurrences; the replacement ("the complement of this set's FOLD-CLOSURE under the spec's normalizeFilterOperator, not of this set itself") is true — I checked it against the gate body, not against the prose.
  • "Every consumer … reads it FROM here rather than restating it", the instruction whose literal application produced objectui#9359 — NOT DISCHARGED. It survives, reworded to "reads the membership FROM here", and the repair bolted onto it an explicit remedy that is wrong 8/8 (①.10).
  • The FAIL's suggested wording — "say that a consumer must fold before asking it" — was followed to the letter, and to the letter it is false. ⛔ The earlier review's phrasing is not the acceptance criterion; whether the published text is true is, and it is not.
  • FAIL's below-the-bar items: the changeset PR-number citation — fixed; the stale 5a41ce733 premise — a merge-base note added to the body, the Evidence-section mention correctly left as a run record; ⭐ app-shell agreeing on all 14 — verified at origin/main; ⭐ objectui#9359 discharged by objectui#9362 — verified at origin/main efc1c9c400; ⭐ the independence escalation — restated below.

The repair report's open_questions, by name.

  • OQ1 — "re-fingerprint the 208-file rollup at this head (B), or is 'no declared type moved, no export added' enough (A)?"Answered: the reading in A is right and I re-derived it independently (item 8), but A's framing is what let this head's defect through. The interesting object is not the rollup's hash but the bytes it summarises; those bytes moved by exactly 12 emitted lines and they are false. A hash could never have decided this either way. No rollup re-run is owed; a re-read of the moved text was, and it fails.
  • OQ2 — "keep the in-function comment fix?"Answered: keep. It is unpublished (the in-function text has 0 occurrences in the emitted file, corroborated by the 20-added-source-lines / 12-emitted-lines accounting), it now states the fold correctly, and dropping it would leave the stale half-sentence that produced the wrong mental model in the first place.
  • OQ3 — "refresh the app-shell parenthetical (A leave / B refresh / C file)?"Answered: A, and no successor is owed, because the parenthetical is accurate as written. Measured at origin/main: viewFilterFold.ts:84-88 is literally the builder set spread plus the four canonical spellings. The earlier review's ⭐ note (raw and normalized at :198-199) is also true and is what makes that layer agree on all 14; the two statements are compatible, so nothing there is falsified and B would have widened the diff into a file nobody is editing.
  • out_of_scope 1 — REST PATCH stored 14502 bytes verbatim, no footer append, no session-URL downgrade ⇒ recorded, no bearing on the contract; the body's two footers are the visible residue of the earlier MCP-channel write and I did not ask for them to be touched. Not escalated.
  • out_of_scope 2 — the second 5a41ce733 left in place as a run recordRight disposition; rewriting it would falsify a measurement.
  • Declared narrowing — the package's 2620-test suite was not re-run because the commit is comment-onlyAnswered twice over. By my own code-only hash equality (the runtime at this head is byte-identical to the head that ran it), and by CI at this exact sha.
  • Instrument misfire — check-new-line-citations.mjs MODULE_NOT_FOUND, re-run as check-new-cross-file-line-citations.mjs, exit 0Answered by CI at this sha: Line Citation Gate is success. Reporting it rather than replacing it was the right call; no artifact.
  • labels_and_state_untouchedVerified independently. Labels at this head are package: components, tests, needs:contract-review; the PR is still draft: true, no auto-merge, no review submitted. (mergeable_state: behindmain has moved past the merge-base. Not a contract item; noted for whoever lands it.)
  • First report (5651516232) — open_questions: [], confirmed empty, and nothing in the body or the comments raises one it omitted. Its three declared_deviations are unaffected by this head: the unlocked ARM B rebuild is moot (both of my builds went through the lock), and the empty probe push and the pgrep-pattern teardown leave no repository artifact. Recorded, not escalated.

The rulings this card references, checked rather than assumed.

  • The domain:ui ruling 5651250784fold LOCALLY; VALUELESS_FILTER_BUILDER_OPERATORS stays dropdown-only and its exported contents do not movehonoured, measured: the set read back from the built bundle is exactly the six camelCase ids.
  • The objectui#7379 constraint carried through objectui#7561 / objectui#9306 — contains / icontains "must never be folded onto" each otherhonoured, measured: the published alias table has no row for either, both fold to themselves, and both sit value-taking in every arm of my enumeration.

Flags of my own, below the bar for the verdict but real.

  • The body's Evidence section still asserts, unqualified, "208 files, rollup sha256 92f47f82… in BOTH arms — byte-identical. No published surface moved." That reading was taken at 30e320ab7. It is false at this head, and the later section admits the bytes move without ever retracting it — so the body contradicts itself in the section a reader uses to judge clause-②.
  • The body's Acceptance notes still say plugin-list's convertFilterGroupToAST "performs the SAME raw has()". False against origin/main efc1c9c400 since objectui#9362 merged. The body was edited in this round; this line was not.
  • The pin cites objectui#9358 — the PR — five times in its prose (:59, :187, :243, :288, :334), which is the same class as the changeset citation that was just corrected. Test prose does not ship to the CHANGELOG, so it is cosmetic.
  • The pre-existing filter-builder-valueless-operators.test.tsx docblock still describes the exported set as "the operator ids for which it renders no value input" — an undercount by the same 8, now in an unpublished test docblock. Not named by the FAIL and not published; recorded so it is not lost.
  • ⚠️ Escalated by name, and belonging with the protocol items rather than with this card: this head commit dfbee6340a again carries Claude-Session: session_01L5xpA5q533BgTTNADibEFt — the same session as the standing PASS 5654646181, the carrier comment 5652799637, the base-merge a835ae4cd1, the previous head 7daa352f98, and the identity this review is filed under. This review ran as an isolated subagent given only the card, the rulings it references and the PR, and every reading above was re-derived here; the seat identity on the head commit is still the maintainer's call, not mine.

CI at this exact head, listed separately as required: 36 check runs — 33 success, 3 skipped, 0 failure, 0 cancelled, 0 timed_out, 0 in progress (the last shard, Test (shard 2/4), completed success at 23:44Z while this was being written). Bundle Analysis, the sole failure at the previous head, is success here. My own runs at this head: the pin filter-builder-valueless-canonical-spelling-9302.test.tsx is 30 passed (30) (VERDICT command-exit 0, exit captured inside the script), and the package build exits 0. Shared-box seconds throughout — the lock excludes other locked runs, not the check:* farm.

Implemented-by: claude/issue-9302-needsvalueinput-fold (mode:subagent)
Reviewed-by: session_01L5xpA5q533BgTTNADibEFt (domain:spec @ objectui seat — this review ran as an isolated subagent, which has no session of its own)

FAIL


Generated by Claude Code

…oth sides

The previous repair replaced two false published sentences with a third. It
told a consumer holding a foreign spelling to fold that spelling through
`normalizeFilterOperator` and then ask `VALUELESS_FILTER_BUILDER_OPERATORS` —
the exported set, unfolded. That is the mirror image of what the gate does, and
it is wrong on exactly the population the sentence exists to serve: executed
literally over the whole published universe, with the set read from the built
bundle, it answers "takes a value" for every canonical and all-lowercase-alias
spelling of the null/empty operators, and for this dropdown's own camelCase ids
as well — so a consumer following it landed strictly further from the gate than
one still doing the pre-branch raw `has()`. It also claimed to be doing what the
gate does, while the gate folds BOTH sides and consults a set that is module
private and unreachable from the published declarations; and it contradicted the
sentence two lines above it, which says the gate is the complement of this set's
FOLD-CLOSURE and not of this set itself.

The block now prescribes the procedure a consumer can actually run from the
published surface: fold this set's own members as well as the spelling, and ask
the result. Executed over that same universe it agrees with the gate on every
spelling, with the two wrong procedures run through the same harness as the
controls that keep the agreement a reading rather than a uniform answer. That
prescription is not hypothetical — it is verbatim what `plugin-list` already
derives on `main`.

No membership change, no code change: the comment-stripped code-only sha256 of
this file is unchanged across this commit while the raw blob moves, and the
merge-base's differs as the control that the instrument can see a real edit.

Also folded in, ranked below the repair: the changeset stops restating a live
population count in prose and names the instrument instead (AGENTS.md #9). The
declared level is untouched.

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

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3061.0 KB 3104.5 KB
Main entry chunk (gzip) 145.6 KB 350 KB
Entry file index-S-tV5giV.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) 544.88KB 130.48KB
core (index.js) 8.52KB 3.41KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 213.54KB 59.33KB
fields (index.js) 247.89KB 62.50KB
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.22KB 2.26KB
i18n (pickLocalized.js) 9.86KB 3.95KB
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.83KB 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.92KB 14.22KB
plugin-charts (index.js) 71.33KB 19.90KB
plugin-chatbot (index.js) 195.34KB 46.51KB
plugin-dashboard (index.js) 131.44KB 34.65KB
plugin-designer (index.js) 215.94KB 44.33KB
plugin-detail (index.js) 252.27KB 65.55KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.71KB 34.16KB
plugin-gantt (index.js) 167.62KB 41.26KB
plugin-grid (index.js) 212.55KB 57.83KB
plugin-kanban (index.js) 46.63KB 14.53KB
plugin-list (index.js) 112.68KB 27.68KB
plugin-map (index.js) 21.48KB 6.99KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.41KB 11.93KB
plugin-timeline (index.js) 30.07KB 8.74KB
plugin-tree (index.js) 10.58KB 3.72KB
plugin-view (index.js) 84.36KB 20.78KB
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) 99.04KB 32.62KB
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.04KB 5.36KB
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

The pin cited a PR number five times in prose where its siblings cite cards —
the same class as the changeset citation corrected two commits ago, and the
class the contract review has now named in two consecutive rounds.

Four of the five sites did not mean a card at all: they mean "the first pass of
this pin got this wrong", a fact about the branch that adds the file. Those now
say that in words, so nothing points a reader at a number that will read as a
sibling card once this lands. The fifth is the derived suite's own title, and
there the subject really is the defect being pinned, so it cites the card.

Prose only, and mechanically so: filtering the diff to lines that are neither a
block-comment gutter nor a line comment leaves exactly one line, the suite
title. No assertion, no import, no table entry and no control moved; the suite
still runs the same number of cases.

Nothing published moves either — the only file this commit touches lives under
`src/__tests__/`, which the package tsconfig excludes by directory, and the
source of the shipped declarations is byte-identical to the previous head.

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

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3061.2 KB 3104.5 KB
Main entry chunk (gzip) 145.6 KB 350 KB
Entry file index-DRma2iBX.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) 544.93KB 130.50KB
core (index.js) 8.52KB 3.41KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 213.54KB 59.33KB
fields (index.js) 247.89KB 62.50KB
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.22KB 2.26KB
i18n (pickLocalized.js) 9.86KB 3.95KB
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.83KB 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.92KB 14.22KB
plugin-charts (index.js) 71.33KB 19.90KB
plugin-chatbot (index.js) 195.34KB 46.51KB
plugin-dashboard (index.js) 131.44KB 34.65KB
plugin-designer (index.js) 215.94KB 44.33KB
plugin-detail (index.js) 252.27KB 65.55KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.71KB 34.16KB
plugin-gantt (index.js) 167.62KB 41.26KB
plugin-grid (index.js) 212.55KB 57.83KB
plugin-kanban (index.js) 46.63KB 14.53KB
plugin-list (index.js) 112.68KB 27.68KB
plugin-map (index.js) 21.48KB 6.99KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.41KB 11.93KB
plugin-timeline (index.js) 30.07KB 8.74KB
plugin-tree (index.js) 10.58KB 3.72KB
plugin-view (index.js) 84.36KB 20.78KB
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) 99.04KB 32.62KB
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.04KB 5.36KB
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

@claude

claude Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Contract review

Reviewed head: 095830673ab717bfa432e44c8170d46c8f8a4940 (read at 2026-09-14T00:34Z)

Merge-base with main is dfb5850594053736afca338b6d577e055350e788, from git merge-basebase.sha happens to coincide with it here; origin/main has since moved to 55f39ee90f. Three files against that base: one source edit, one pin, one changeset. Everything below was re-derived in my own git worktree at the pinned sha; neither shared checkout was edited, nothing was pushed, no label or state was touched. Shallowness verified per checkout: /home/user/objectui false, /home/user/objectstack true. ⛔ Nothing is inherited from the PASS at 5654646181, the FAIL at 5656829080, the FAIL at 5657127381, or from the PR body.

① derived judgments

The instruments first, because every judgment below rests on them. The published universe is VIEW_FILTER_OPERATORS (20) union the keys of VIEW_FILTER_OPERATOR_ALIASES (36) union the exported set's members, enumerated from the installed @objectstack/spec 17.4.0 = 58 distinct spellings. The exported set is read from the BUILT bundle packages/components/dist/index.js, built here under os-verify-lock.sh (DEPS_EXIT=0 PKG_EXIT=0, captured by redirect before any pipe), never restated by hand. And the value-less verdict is not transcribed from the gate body at all: each of the 58 spellings is rendered through the real FilterBuilder on one text column and its input elements counted. The base arm is a disk ablation — the file put back to base blob 6ec603fca6, MUTATION-ON-DISK-CONFIRMED printed before any result was read, restored under trap ... EXIT INT TERM by blob-hash equality (RESTORED-OK blob=9b92ee4f9d22c54b…) and an empty git status for that path, never by an exit code.

Accept-set changes, enumerated, base arm vs head arm, both measured by rendering:

  1. needsValueInput: !VALUELESS_FILTER_BUILDER_OPERATORS.has(operator) becomes !VALUELESS_FILTER_BUILDER_OPERATORS_CANONICAL.has(normalizeFilterOperator(operator)) — one production call site (filter-builder.tsx:1728), and inside this file the exported set itself is read at exactly one place, the derivation at :308. Measured preimage 6 → 14: exactly 8 spellings change verdict, no more and no fewer. Right — this is the repair the card and ruling 5651250784 ask for, and it is a reader learning to normalize, not a set gaining members.
  2. is_null is_not_null is_empty is_not_empty — canonical members of VIEW_FILTER_OPERATORS: 1 → 0. Right; the card's acceptance table reproduces at the merge-base exactly (is_null=1 / isNull=0 / is_empty=1 / isEmpty=0 / equals=1).
  3. isnull isnotnull isempty isnotempty — the all-lowercase alias rows pointing at those four: 1 → 0. Right.
  4. isNull isNotNull isEmpty isNotEmpty — this dropdown's own ids, which are also rows of the published alias table: 0 → 0. Right: the fold closure read back from the built export has size 6, so routing the lookup through the fold dropped no member and no two ids collide onto one canonical.
  5. exists / notExists: 0 → 0. Right — measured, normalizeFilterOperator returns an unknown spelling verbatim (notExistsnotExists, while ISNULLis_null), so these two fold to themselves and the closure stays the same size as the set it derives from.
  6. The remaining 44 spellings of the universe are value-taking in both arms (between draws 2 inputs, the rest 1; the unpublished totally_unknown draws 1). Right — no over-reach onto a value-taking operator, measured over the population rather than a sample, and it is these 44 that make the 14 a reading instead of a uniform answer.
  7. No row's stored operator is rewritten on render — onChange is not called for is_null or isNull. Right: rendering is not an edit.
  8. New module-private VALUELESS_FILTER_BUILDER_OPERATORS_CANONICAL, derived [...SET].map(normalizeFilterOperator) rather than restated. Right, and the ruling is honoured measurably: the set read back from the built bundle is exactly isEmpty, isNotEmpty, isNull, isNotNull, exists, notExists.

The published prescription — re-executed over the whole universe, not accepted. The block shipping at the emitted dist/custom/filter-builder.d.ts says a consumer must "ask that FOLD-CLOSURE, not this set: fold this set's own members through normalizeFilterOperator as well as the spelling, and ask the result — the same fold the gate applies to BOTH sides". Run literally over all 58, with the set read from the built bundle:

procedure value-less agrees with the gate disagrees on
the gate itself 14 58/58
the published prescription 14 58 / 58 0
control — the previously shipped instruction (SET.has(N(op))) 2 46/58 12
control — the pre-branch raw has() 6 50/58 8
control — a second fold on top of the prescription 14 58/58 0

Two controls move, by 12 and by 8, so the 58/58 is a reading and not a dead harness; the second fold moves nothing, so "fold once" is not underspecified. Stronger than the branch's own claim: I did not compare the prescription against a transcription of the gate — I compared it against the rendered row, spelling by spelling, and the mismatch count is 0 of 58. ⇒ the defect the second FAIL named is discharged, by execution.

Public-surface changes:

  1. filter-builder.tsx carries 16 export statements and they are byte-identical base to head. No symbol added, removed or renamed. Right.

  2. Clause-② verified mechanically, two arms, with both controls. Built the package at this head, fingerprinted all 208 emitted .d.ts, put the one changed file back to its base content (mutation confirmed on disk first), rebuilt, fingerprinted again, restored and rebuilt a third time. The raw rollup MOVES (9b1cc2d2… head vs 76fed3d2… base-content) — the sensitivity control, proving the instrument can see this file at all — while the declaration-only rollup, comments stripped with this repo's own scripts/js-comment-mask.mjs (--self-test 45/45) and whitespace normalised, is 538fd75c6f4cb04d… in BOTH arms. The restored arm reproduces head's raw rollup exactly. Separately: VALUELESS_FILTER_BUILDER_OPERATORS_CANONICAL reads 0 occurrences across all 208 emitted .d.ts with the exported symbol lit 2 times in the same file. ⇒ Clause-②: no is TRUE, and here is the judgment the seat asked for rather than a deferral: a commit whose emitted .d.ts comment bytes move does not touch clause ② at all. The clause's criterion is whether the card relaxes the accept set or enlarges the public surface; comment bytes do neither, and the declaration-only rollup is the measurement that separates the two. ⛔ That is not a licence for the prose — published prose is where both earlier FAILs lived, and where this one lives.

  3. The accept set did not move across the repair commits — proved, and the instrument was proved first. Code-only sha256 of packages/components/src/custom/filter-builder.tsx (comments blanked by js-comment-mask, trailing whitespace trimmed, blank lines dropped): 0ca48c2227040d6e at 30e320ab76, 7daa352f98, dfbee6340a, 60559b4857 and this head, against 325dac8a7ce2830d at the merge-base. Three controls, because an equality with no control is one measurement twice: (a) a planted real code edit — the gate reverted to the raw has() — moves the digest to ce79d3cf9664d90a; (b) the merge-base differs, so a real code edit does exist on this branch; (c) a planted comment-only edit does not move it. ⚠️ Reported rather than rounded off: run without the whitespace normalisation, the same instrument reports the digest moving across dfbee6340a and 60559b4857, because js-comment-mask blanks comments in place and a docblock that gains lines gains blank lines. The relation is the instrument, not the digest value; both arms of that are stated here so the next reader does not mistake one for a contradiction.

  4. WRONG, and it is the third round of the same defect class: .changeset/9302-filter-builder-valueless-canonical-fold.md:20-22 ships a false measurement into published text. The sentence, present since 7daa352f98 and untouched by both repairs:

    Measured through the real builder on one text column: every spelling the pin enumerates drew 1 value input where its camelCase dropdown twin drew 0

    The changeset body is reproduced verbatim into packages/components/CHANGELOG.md, and that package's package.json files is ["dist","README.md","CHANGELOG.md","LICENSE"] — so this is text that ships inside the npm tarball, not an internal note.

    Executed rather than read. The row set the pin enumerates is SPEC_SPELLINGS, re-derived the same way the pin derives it: 12 spellings. Rendered through the real builder at the merge-base, one text column, inputs drawn:

    is_empty=1  is_not_empty=1  is_null=1  is_not_null=1
    isempty=1   isnotempty=1    isnull=1   isnotnull=1
    isEmpty=0   isNotEmpty=0    isNull=0   isNotNull=0
    

    8 of 12 drew 1; 4 drew 0. Controls in the same base-arm run, so the zeros are a reading: equals=1, contains=1, in=1, totally_unknown=1. The four that drew 0 are isEmpty, isNotEmpty, isNull, isNotNull — they are rows of VIEW_FILTER_OPERATOR_ALIASES, so the pin enumerates them (its own it.each runs them, labelled alias(dropdown id), and they are among the 18 cases that PASS when the file is reverted to base blob 6ec603fca6 — 12 failed / 18 passed, measured here). For those four, "its camelCase dropdown twin" is the spelling itself, so the sentence asserts that one render drew both 1 and 0.

    ⭐ The pin's own code already knows this. At :336 it computes SPEC_SPELLINGS.filter(r => !VALUELESS_FILTER_BUILDER_OPERATORS.has(r.spelling)) and calls the result firing — 8 rows, strictly fewer than the 12 it enumerates. The changeset quantifies over enumerates where only firing is true.

    And it is false in the way this PR has now been told twice. Two sentences earlier, the same paragraph says "Which spellings those are is deliberately not listed here … the population is re-derived on each run rather than restated in prose that cannot move with it", and then restates a universal about that population anyway — self-contradiction inside one paragraph, the exact shape the second FAIL named, in the exact file whose previous live-count prose that FAIL flagged. The standing rule this round adopted — execute the claim over the whole population before writing it down — applies to a descriptive universal as much as to a prescriptive one, and this one was never run.

    repair owed: two lines in a file already open in this diff. Either quantify over the rows the pin's own firing filter yields, or name the instrument and state no universal at all (AGENTS.md 完善设计器的每一个细节 #9, the rule the sentence above it invokes). ⛔ No membership change, ⛔ no runtime line, ⛔ the ruling untouched, ⛔ the declared level untouched.

② semver grading

Changeset: .changeset/9302-filter-builder-valueless-canonical-fold.md, declaring '@object-ui/components': patch (frontmatter read directly; one package, one level).

The level this diff actually is: patch, matching the declaration. No export is added, removed or renamed (16 export statements byte-identical); the declaration-only rollup over all 208 emitted .d.ts is identical in both build arms; export declare const VALUELESS_FILTER_BUILDER_OPERATORS: ReadonlySet is unchanged; the only behavioural movement is a value box disappearing from a row whose own trigger already reads Is null, which withdraws no consumer contract. One released package's published source moved and one changeset declares it. CI at this sha agrees: Changeset Declaration, Changeset Bump Policy, Changeset Fixed Group Check, Changeset Claim Re-read and Changeset Overwrite Report are all success. ⛔ The grading is correct; ①.12 is about what that file says, not about what it declares, and fixing it must not move the level.

③ boundary flags

PASS 5654646181 — its one finding. The pin protected only half of what the repair moves; the four all-lowercase alias rows had nothing pinning them.DISCHARGED at this head, measured. The row set is re-derived at run time from the two published tables, not listed; it yields 12 rows including all four lowercase aliases; reverting the file to base blob 6ec603fca6 turns 12 of the suite's 30 cases red and all four lowercase rows are among them. Its below-the-bar note on the stale 5a41ce733 premise: the body now carries the git merge-base correction, and the second 5a41ce733 is correctly left standing as a dated run record.

FAIL 5656829080 — defect by defect, at THIS head, on the rebuilt artefact. Counts are gutter-tolerant, whitespace-collapsed occurrence counts over the emitted dist/custom/filter-builder.d.ts, never grep -c, each paired with lit controls from that same file (export declare const … = 1, objectui#4744 = 1, objectui#9302 = 1, the pin filename = 1).

  • "Operator ids for which this builder renders NO value input"DISCHARGED, 0 occurrences.
  • "needsValueInput below is defined as the complement of this set, so the two cannot say different things"DISCHARGED, 0 occurrences; the replacement ("the complement of this set's FOLD-CLOSURE … not of this set itself") I checked against the gate body and against the rendered universe, and it is true.
  • "reads it FROM here rather than restating it"DISCHARGED as quoted, 0 occurrences; it now reads "reads the membership FROM here", 1 occurrence, and that narrower claim holds: plugin-list on origin/main derives its canonical set from this export (ListView.tsx:330-331, asked folded at :357) and app-shell spreads the import (viewFilterFold.ts:84-89).
  • Below the bar there: the changeset's objectui#9358 citation — FIXED (0 occurrences in the changeset). The app-shell and plugin-list observations — verified independently on origin/main.

FAIL 5657127381 — its blocking item and its below-the-bar list.

  • The blocking item, "must therefore fold it through normalizeFilterOperator before asking this set, exactly as that gate does"DISCHARGED, 0 occurrences, and the replacement is not merely different but correct by execution: 58/58 against the gate and 0 mismatches against the rendered row, with two moving controls (12 and 8). "the same fold the gate applies to BOTH sides" is true of the gate body, and the procedure runs from the published surface alone.
  • Body's Evidence rollup 92f47f82… asserted unqualified — DISCHARGED, now retracted where it is stated and kept as a dated run record.
  • Body's Acceptance notes plugin-list raw-has() bullet — DISCHARGED, retracted and re-measured; I re-verified it on origin/main myself.
  • The changeset's live count "named four and the behaviour covered twice that"removed. ⛔ But the repair did not reach the other population sentence in the same paragraph, which is ①.12 and is why this is a FAIL. Naming one live count and leaving the false universal three lines below it is the half-repair.
  • The pin's five objectui#9358 prose citations — DISCHARGED: objectui#9358 reads 0 in the pin with objectui#9302 lit at 5 and "first pass of this pin" at 3 as controls.
  • The pre-existing filter-builder-valueless-operators.test.tsx:11 docblock — confirmed NOT in this diff (absent from git diff --name-only against the merge-base), still undercounting, and not published. Correctly carried on objectui#9302 with a file and a line.

Implementer flags and open_questions, answered by name.

  • Report 5651516232open_questions: [], confirmed empty; nothing in the body or comments raises one it omitted. Its three declared_deviations (an unlocked ARM-B rebuild, the empty probe push, a pgrep-pattern teardown) leave no repository artifact and are moot at this head — every build and run of mine went through os-verify-lock.sh. Its three out_of_scope_findings: objectui#9359 filed and since discharged by objectui#9362 (verified on origin/main); the plugin-list vocabulary split is carried inside that card; the notexists-in-lower-case boundary is real and correctly recorded as a measured boundary rather than a defect — I reproduce it: notexists normalizes to itself and sits value-taking in both arms, among my 44 controls.
  • Report 5657037587 OQ1 (re-fingerprint the 208-file rollup, or accept "no declared type moved") ⇒ Answered: neither framing, and I did the measurement that decides it. A raw rollup can only say "prose moved", which is the point of the commit; the declaration-only rollup is the one that answers clause ②, and it is identical in both arms (①.10). No further rollup is owed.
  • OQ2 (keep the in-function comment fix) ⇒ Keep. It does not ship — it is in a function body and reads 0 occurrences in the emitted file — and it now states the fold correctly.
  • OQ3 (refresh the doc-comment's app-shell parenthetical) ⇒ Answered: A, leave it, and no successor is owed — but for a reason neither the report nor the earlier review gave. The parenthetical "its VALUELESS_FILTER_OPERATORS is this set plus the canonical spec spellings, which only that layer sees" is accurate about that layer's set composition (viewFilterFold.ts:84-89 is literally the import spread plus those four), and "only that layer sees" was already loose at the merge-base — the card's own premise is that is_null reaches this builder. So this diff does not falsify it; it inherits it. ⛔ It is therefore not a repair this PR owes, and it stays below the bar rather than being escalated into a fourth-round diff.
  • Report 5657278455 OQ1 (make this docblock descriptive-only as a rule) ⇒ Escalate, do not settle it here. ①.12 is evidence for it — the three failures on this branch are all derived-once statements, and only one of the three was prescriptive. But a rule about how this docblock may be written is a seat call across the family, not a contract-review verdict on one PR, and the block at this head is measurably correct, so ⛔ there is nothing to remove on the strength of the rule not yet existing.
  • OQ2 (fix the pin's five objectui#9358 citations) ⇒ already taken at this head; verified above, and verified prose-only: filtering that commit's diff to lines that are neither a comment gutter nor a line comment leaves exactly one line, the describe title. Corroborated independently — the shipped file's raw blob is unchanged by that commit (9b92ee4f9d22c54b at both 60559b4857 and this head), so nothing published could have moved.
  • OQ3 (mergeable_state: behind) ⇒ Flagging only, agreed. main is at 55f39ee90f as I write; the merge-base is unmoved and landing is the seat's.
  • Report 5657334346's own precision note — that the pin's code-only digest moves because a describe title is a string literal the stripper correctly keeps — is right, and I reproduce it (58645619680b149803eb1ac0a79b6087), with the shipped file's digest unmoved beside it. Reporting that rather than an unqualified "code-only unchanged" was the correct call.
  • Declared narrowing — the package's 2620-test suite not re-run for a comment-only commit ⇒ answered twice: by the code-only equality (①.11) and by CI at this exact sha. The check-new-line-citations.mjs instrument misfire (MODULE_NOT_FOUND, re-run as check-new-cross-file-line-citations.mjs) ⇒ answered by CI: Line Citation Gate is success. Both were right to report rather than replace.
  • labels_and_state_untouchedverified independently: at this head the PR is draft: true, labels are package: components, tests, needs:contract-review, no auto-merge, no review submitted.

The rulings this card references, checked rather than assumed. Ruling 5651250784fold locally; the exported set stays dropdown-onlyhonoured, measured from the built bundle. The objectui#7379 constraint carried through objectui#7561 / objectui#9306 — contains / icontains must never fold onto each otherhonoured, measured: neither has an alias row, each normalizes to itself, and both sit value-taking in every arm of my census.

My own flags, below the bar for the verdict but real.

  • The published doc-comment points a consumer at filter-builder-valueless-canonical-spelling-9302.test.tsx, which the package does not ship (files is dist + three files; src/__tests__ is excluded). Naming the instrument is what AGENTS.md 完善设计器的每一个细节 #9 asks for and is better than a written-down count; it is still an instrument the reader of the .d.ts cannot run.
  • The exported set's headline now reads "this dropdown's OWN camelCase ids". exists is not camelCase in any meaningful sense; it is a one-word id. Cosmetic, and the qualification it carries is the part doing the work.
  • CI at this exact head, listed separately as required: 36 check runs — 31 success, 3 skipped, 0 failure, 0 cancelled, 0 timed_out, 2 in_progress (Test (shard 1/4), Test (shard 2/4)) at 00:36Z. Bundle Analysis, the sole failure two heads back, is success here. My own runs in my worktree, under os-verify-lock.sh with exits captured by redirect before any pipe: build DEPS_EXIT=0 PKG_EXIT=0; the pin filter-builder-valueless-canonical-spelling-9302.test.tsx 30 passed (30), PIN_EXIT=0; @object-ui/components type-check exit 0; the base-arm ablation 12 failed | 18 passed on the pin with RESTORED-OK. ⚠️ Shared-box seconds throughout — the lock excludes other locked runs, not the unlocked check:* farm.
  • ⚠️ Escalated by name, and a protocol item rather than this card's: this head commit 095830673a and its two predecessors carry a Claude-Session: trailer naming the same seat session as the standing PASS 5654646181 and the carrier comment 5652799637. This review ran as an isolated subagent fed only the card, the rulings it links and the PR, and every reading above was re-derived here; the seat identity on the head commit remains the maintainer's call, not mine.

Implemented-by: claude/issue-9302-needsvalueinput-fold (mode:subagent)
Reviewed-by: session_01L5xpA5q533BgTTNADibEFt (domain:spec @ objectui seat — this review ran as an isolated subagent, which has no session of its own)

FAIL


Generated by Claude Code

…e rows that fired

The changeset said "every spelling the pin enumerates drew 1 value input where
its camelCase dropdown twin drew 0". Executed at the merge-base through the real
`FilterBuilder` on one `text` column, over the population the pin actually
enumerates: 8 of those 12 spellings drew 1, and four drew 0.

The four are `isEmpty`, `isNotEmpty`, `isNull` and `isNotNull` — rows of
`VIEW_FILTER_OPERATOR_ALIASES` that are also this builder's own dropdown ids, so
they are members of the exported set and the raw `has()` always matched them.
For each of those four the "camelCase dropdown twin" IS the spelling itself,
which is what makes the old sentence not merely overstated but self-refuting: it
asserted that one render drew both 1 and 0.

The distinction was already encoded in the pin, which is why the repair names an
instrument instead of a number. Its `firing` filter drops exactly the rows whose
spelling is a member of the exported set, leaving the 8 the raw lookup could not
match literally — and those are precisely the rows for which the claim holds.
The sentence now quantifies over that filter, states the arm it was measured in,
and keeps `equals` as the firing control.

Executed before writing, not after. Rendering all 12 derived spellings and their
twins through the real builder at the merge-base: the old claim holds for 8/12
(FALSE), the new claim for 8/8 (TRUE), and the sub-clause that each firing row's
twin is a different spelling for 8/8 — both claims read off one rendered data
set, so scope is the only variable between them. `equals`, `contains`, `in` and
`totally_unknown` each drew 1 in the same run, and `equals` drew 1 in both arms,
so the zeros are a reading rather than a dead harness.

Changeset prose only. The declared level, `patch`, is untouched; no assertion,
no runtime line and no membership moved. The code-only sha256 of
`filter-builder.tsx` is `0ca48c2227040d6e` across this commit and the four
before it, against `325dac8a7ce2830d` at the merge-base — with the instrument
proved first: a planted real code edit moves it, a planted comment-only edit
(verified comment-only, 1127 of 1127 code lines identical) does not, and without
whitespace normalisation that same comment-only edit reports movement that is
not there. Refs: objectui#9302.

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

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3061.2 KB 3104.5 KB
Main entry chunk (gzip) 145.6 KB 350 KB
Entry file index-DRma2iBX.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) 544.93KB 130.50KB
core (index.js) 8.52KB 3.41KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 213.54KB 59.33KB
fields (index.js) 247.89KB 62.50KB
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.22KB 2.26KB
i18n (pickLocalized.js) 9.86KB 3.95KB
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.83KB 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.92KB 14.22KB
plugin-charts (index.js) 71.33KB 19.90KB
plugin-chatbot (index.js) 195.34KB 46.51KB
plugin-dashboard (index.js) 131.44KB 34.65KB
plugin-designer (index.js) 215.94KB 44.33KB
plugin-detail (index.js) 252.27KB 65.55KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.71KB 34.16KB
plugin-gantt (index.js) 167.62KB 41.26KB
plugin-grid (index.js) 212.55KB 57.83KB
plugin-kanban (index.js) 46.63KB 14.53KB
plugin-list (index.js) 112.68KB 27.68KB
plugin-map (index.js) 21.48KB 6.99KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.41KB 11.93KB
plugin-timeline (index.js) 30.07KB 8.74KB
plugin-tree (index.js) 10.58KB 3.72KB
plugin-view (index.js) 84.36KB 20.78KB
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) 99.04KB 32.62KB
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.04KB 5.36KB
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

@claude

claude Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Contract review

Reviewed head: 4448cf70b9b8b092362667338987c3a6304f84ce (read at 2026-09-14T01:31Z)

Merge-base with main is dfb5850594053736afca338b6d577e055350e788, from git merge-basebase.sha happens to coincide with it; origin/main has since moved to 55f39ee90f. Shallowness verified per checkout: /home/user/objectui false, /home/user/objectstack true. Everything below was re-derived in my own git worktree (/home/user/objectui-review-9358-r4) pinned at this sha; no shared checkout's working tree was touched, nothing was pushed, no label or state was written. Declared: I ran git fetch and git worktree add against the shared /home/user/objectui .git (refs and worktree registry only), and I read — never wrote — the r3 worktree's already-built dist for the emitted-artefact census, after proving its filter-builder.tsx blob is byte-identical to this head's (9b92ee4f9d22c54b); the only file between the two heads is the changeset, which the build does not read.

① derived judgments

Accept set. Three files against the merge-base: one source edit, one pin, one changeset (git diff --name-status, 524 insertions / 8 deletions). The entire code-only delta of the shipped file, across the whole branch, is three added lines and one removed line — comments stripped with this repo's own scripts/js-comment-mask.mjs (--self-test 45/45) plus whitespace normalisation:

+ const VALUELESS_FILTER_BUILDER_OPERATORS_CANONICAL: ReadonlySet OF string = new Set(
+   [...VALUELESS_FILTER_BUILDER_OPERATORS].map(normalizeFilterOperator),
+     return !VALUELESS_FILTER_BUILDER_OPERATORS_CANONICAL.has(normalizeFilterOperator(operator))
-     return !VALUELESS_FILTER_BUILDER_OPERATORS.has(operator)

(type parameter spelled in words — the body sanitizer eats tag-shaped fragments.) 16 export statements, base and head, byte-identical. Rendered through the real FilterBuilder over the whole 58-spelling published universe, base arm by disk ablation to base blob 6ec603fca6: the value-less preimage moves 6 → 14, exactly 8 spellings change verdict (is_empty is_not_empty is_null is_not_null isempty isnotempty isnull isnotnull), and the reverse set is empty — no over-reach onto a value-taking operator. onChange was never called for any of the 59 renders in either arm, in either direction.

⭐ A reading nobody has taken yet, and the one that actually states the headline: grouping the whole published vocabulary by canonical spelling, there are 14 canonical operators with more than one spelling. At the merge-base 4 of 14 disagreed across their own spellings on (value inputs, trigger text); at this head 0 of 14 disagree. The base arm is the control that the instrument can see disagreement at all.

Public surface, and how I bounded "published". packages/components/package.json files is ["dist","README.md","CHANGELOG.md","LICENSE"]. So exactly three things in this diff could publish, and I measured each rather than assuming:

diff file publishes? measurement
.changeset/9302-filter-builder-valueless-canonical-fold.md YES — into packages/components/CHANGELOG.md, inside the tarball every sentence is in the population
packages/components/src/custom/filter-builder.tsx partly see the two rows below
packages/components/src/__tests__/…-9302.test.tsx NO tsconfig.json excludes **/__tests__/** by directory; not under files; check-changeset-presence counts 2 published source files of 3 changed. Its only trace anywhere in dist is its filename quoted inside the doc-comment

⭐ The warning that comments survive into emitted .js as well as .d.ts is worth taking seriously and is false for this package — measured, not assumed. Over dist/index.js and dist/index.umd.cjs: every prose probe from this diff reads 0 (complement of the exported set, FOLD-CLOSURE, DERIVED, never a second literal, objectui#9302, objectui#4744), with normalizeFilterOperator lit at 1 and 7 in those same two files as the control that the census hits them, and FOLD-CLOSURE lit at 2 in the .d.ts as the control that the probe string is findable at all. Enumerating what actually survives: 1830 of the 1831 surviving block comments in dist/index.js are /*@__PURE__*/ and the 1831st is a zero-length match — the bundler keeps annotations and nothing else. So no source prose from this file reaches the JS.

source comment in this diff reaches a published artefact? measurement
the exported set's docblock YES, dist/custom/filter-builder.d.ts 8 live probes read 1 each, gutter-tolerant
the module-private …_CANONICAL docblock NO symbol 0 and DERIVED, never a second literal 0 across all 208 emitted .d.ts; controls in the same corpus: VALUELESS_FILTER_BUILDER_OPERATORS 2, FOLD-CLOSURE 2, objectui#4744 1, objectui#9302 1
the in-function comment above the gate NO BOTH sides are folded through 0 and One operator, two spellings, two different rows 0 across the same 208, same lit controls

⇒ the published-claim population is the changeset in full, plus the added-or-changed assertions of one docblock, and nothing else. Bounded that way it is 46 claims: 35 in the changeset, 11 in the published doc-comment. Every one is below with its own measurement. All occurrence counts are newline- and gutter-tolerant perl -0777 / normalised-whitespace counts, never grep -c.

The changeset — 35 claims. Paragraph numbers so a later reader can audit against the file.

# claim measurement verdict
CS-1 headline: the gate folds the operator, so one operator draws one row whichever spelling it arrives in rendered: 14 multi-spelling canonical groups, 0 disagree at head, 4 disagree at base true
CS-2 needsValueInput did a raw has() against the named set base blob 6ec603fca6 line 1283 return !VALUELESS_FILTER_BUILDER_OPERATORS.has(operator) true
CS-3 its members are this builder's six camelCase dropdown ids all 6 are defaultOperators values (22 ids); all 6 match camelCase/single-word shape true
CS-4 every OTHER spelling the spec publishes for those operators missed the set and was treated as value-taking base arm: every non-member spelling of the four operators drew 1 input, 8 of 8 true
CS-5 gloss (a): the canonical members of VIEW_FILTER_OPERATORS those ids fold onto derived: is_empty is_not_empty is_null is_not_null true
CS-6 …which is what foldFilterGroupToSpecRules persists viewFilterFold.ts:193 normalizes, :203 stores the normalized form true
CS-7 …and what any spec-side producer emits view.zod.ts:663 operator: z.preprocess(normalizeFilterOperator, z.enum(VIEW_FILTER_OPERATORS)) — anything that validates is canonical true
CS-8 gloss (b): every row of VIEW_FILTER_OPERATOR_ALIASES pointing at one of them 8 such rows; under the sentence's head-noun scoping (every OTHER spelling) its extension is the 4 lowercase rows, all 4 measured drawing 1 at base. See ③ for the bare-gloss over-reach true as scoped
CS-9 the row drew a box to type a value into, beside a trigger reading Is null base arm: is_null → inputs 1, trigger Is null true
CS-10 which spellings those are is deliberately not listed here all 12 spellings read 0 in the changeset; controls equals 1, normalizeFilterOperator 1 true
CS-11 the pin walks those two published tables pin :203-241 reads VIEW_FILTER_OPERATORS and VIEW_FILTER_OPERATOR_ALIASES true
CS-12 …and names every row it measured ran --reporter=verbose at this head: 12 derived cases, each titling its own spelling; the twins it also renders are separately named cases true
CS-13 so the population is re-derived on each run rather than restated in prose SPEC_SPELLINGS is computed, not listed; no spelling literal in the derived table true
CS-14 an earlier draft of this changeset hand-listed that population and undercounted it the draft at 30e320ab76 lists 4 spellings; the behaviour moves 8 true
CS-15 measured through the real builder on one text column, before the repair: every spelling firing yields drew 1 value input base-arm ablation, MUTATION-ON-DISK-CONFIRMED before any result was read: 8 of 8 true
CS-16 firing = the enumerated rows that are not themselves members of the exported set pin :336-338, SPEC_SPELLINGS.filter(r . !VALUELESS_FILTER_BUILDER_OPERATORS.has(r.spelling)) true
CS-17 …so each has a DIFFERENT spelling as its twin 8 of 8 distinct; twinIsSelf false for every firing row (and true for exactly the 4 the filter drops) true
CS-18 …where that camelCase dropdown twin drew 0 8 of 8, and all 8 twins are camelCase dropdown ids true
CS-19 with equals drawing 1 in both arms base 1 / head 1, trigger Equals in both true
CS-20 the firing control makes a uniform zero a reading, not a dead harness same run: contains 1, in 1, between 2, totally_unknown 1, both arms true
CS-21 both sides of the lookup now fold through normalizeFilterOperator :307-309 folds the members, :1337 folds the argument true
CS-22 the same fold filterValueArity and reconcileOperatorForField already perform :351 and :428 both open with normalizeFilterOperator true
CS-23 the canonical set is DERIVED from the exported one, not restated beside it .map(normalizeFilterOperator) over the export; no second literal true
CS-24 pre-existing; before objectui#7561 the same stray input was drawn under a blank trigger ⭐ measured, not argued: ablated to the pre-repair blob d272295868 (parent of 9ae871d004) and rendered — is_null inputs 1, trigger ""; isNull 0 / Is null; controls equals 1 / Equals, contains 1 / Contains true
CS-25 what changed is that the contradiction became legible same run: blank trigger before, Is null at the merge-base true
CS-26 the exported set is unchanged, deliberately membership pinned and byte-identical to base true
CS-27 its stated job is which rows this builder leaves value-less — a fact about the dropdown's own ids matches the published docblock headline true
CS-28 two other layers read it — convertFilterGroupToAST and foldFilterGroupToSpecRules repo census: exactly two non-test modules import it, plugin-list/src/ListView.tsx (:594, :622, inside convertFilterGroupToAST at :581) and app-shell/src/views/viewFilterFold.ts:86 true
CS-29 app-shell's is already documented as this set plus the canonical spellings only that layer sees viewFilterFold.ts:84-90 is literally that union. The "only that layer sees" half was already loose at the merge-base — see ③ true on construction
CS-30 widening it would have made that layer's compensation redundant by side effect, in a file nobody is editing the four-spelling literal at :89 is exactly what widening would duplicate; viewFilterFold.ts is not in the diff true
CS-31 the defect was a reader that forgot to normalize, so the reader is what was repaired the whole code delta is the gate plus a private derived set true
CS-32 no published type or export changes 16 export statements identical; export declare const VALUELESS_FILTER_BUILDER_OPERATORS: ReadonlySet OF string; unchanged; the added symbol is module-private and reads 0 across all 208 emitted .d.ts true
CS-33 no row's stored operator is rewritten — rendering is not an edit onChange call count 0 for all 59 renders, both arms true
CS-34 which vocabulary should WIN is separate and still open objectui#9306 open, pm:queue true
CS-35 the contains / icontains boundary is untouched and pinned; the fold maps neither onto the other alias table has no row for either; pin's boundary guard green true

The published doc-comment — 11 claims, all in dist/custom/filter-builder.d.ts.

# claim measurement verdict
DC-1 "Operator ids — this dropdown's OWN camelCase ids — for which this builder renders no value input" all 6 rendered: 0 inputs each, with real non-blank, non-echo triggers (Is empty / Is not empty / Is null / Is not null / Is set / Is not set) true
DC-2 it is the source of truth for the distinction and lives here because this component decides it the gate is the only decider; the derived set is built from it true
DC-3 needsValueInput is the complement of this set's FOLD-CLOSURE under normalizeFilterOperator, not of this set itself the term is stipulated operationally by DC-6 three lines later, and under that stipulation the gate agrees 58/58. The isolated naive reading is recorded in ③ true under the definition the same docblock gives
DC-4 so the gate answers "no value" for spellings that are NOT members — the canonical form foldFilterGroupToSpecRules persists rendered: the 4 canonical spellings all draw 0 at head true
DC-5 …and the alias rows the spec publishes for those same operators rendered: the 4 non-member alias rows all draw 0 at head; the sentence scopes to non-members, so the 4 member rows are out of it true, 8 of 8
DC-6 the prescription: fold this set's own members through normalizeFilterOperator as well as the spelling, and ask the result ⭐ executed against the rendered gate, not a transcription of it, over all 58 spellings: 58 agree, 0 disagree. Controls in the same run, all firing: the round-2 instruction disagrees on 12, the pre-branch raw has() on 8, and a naive foldClosure.has(op) on 8 true
DC-7 "the same fold the gate applies to BOTH sides" :307-309 folds the members, :1337 folds the argument true
DC-8 how much wider the gate's preimage is is NOT restated here the only numerals in the emitted block are 4744, 9302, 9302 and AGENTS.md #9 — no population count true
DC-9 the pin walks the spec's two published tables and names every row it measures as CS-11 / CS-12; --reporter=verbose at this head, 30/30 true
DC-10 which is the only form of that answer that moves when those tables do the derivation is recomputed per run; a spelling added to either table enters SPEC_SPELLINGS with no edit true
DC-11 "Every consumer that has to tell a complete value-less row from a half-filled one reads the membership FROM here rather than restating it:" + the two-item list true of both listed consumers. Read as a repo-wide universal it has one counterexample, measured and named in ③ — which is pre-existing at the merge-base, so this diff does not falsify it true as listed; see ③

Zero of the 46 is measurably false as published. The three previously-failed sentences are gone from the artefacts that carry them, each zero paired with lit controls from the same file — retired: Operator ids for which this builder renders NO value input 0, defined as the complement of this set, so the two cannot say different things 0, must therefore fold it through normalizeFilterOperator before asking this set, exactly as that gate does 0, every spelling the pin enumerates 0 in the changeset; live in the same files: the replacement prescription 1, the same fold the gate applies to BOTH sides 1, the new headline 1, How much wider the gate's preimage is is NOT restated here 1, the export declare const line 1, objectui#4744 1, objectui#9302 1, and in the changeset the firing filter clause 1 with objectui#9302 2.

Spot-checks of what the previous rounds established — what I checked and how.

  • The accept set has not moved across any repair. Spot-checked by recomputing the code-only sha256 at all six branch revisions, and I reproduce the third review's values exactly: 0ca48c2227040d6e at 30e320ab76, a835ae4cd1, 7daa352f98, dfbee6340a, 60559b4857, 095830673a and this head, against 325dac8a7ce2830d at the merge-base. Instrument proved before it was trusted: a planted real code edit moves it (ce79d3cf9664d90a); a planted comment-only edit does not — and the planting was proved comment-only first, 2175 of 2175 stripped lines identical, first differing index NONE. ⚠️ Refinement of the inherited lesson, measured: my first comment-only planting kept the comment's line count, and then the un-normalised digest did not move, so the trap did not reproduce. Re-planted a comment-only edit that adds one comment line and the trap reproduces exactly as described (2a8fc4d3fc8e3104087fe97ba450414d without normalisation, unchanged with it). ⇒ the stripper blanks in place, so the false movement is a function of the comment's line count, not of comment text. The relation is still the instrument, not the digest.
  • Clause-②: no. I did not re-run the two-arm 208-file rollup. Spot-checked three independent ways instead: 16 export statements byte-identical base-to-head; the comment-stripped source delta is one module-private const plus one function-body line, neither an export nor a declared type; and the private symbol plus both unpublished comment blocks read 0 across all 208 emitted .d.ts with four lit controls in the same corpus. The emitted .d.ts bytes do move, in comment bytes only, exactly as the PR body states.
  • 58/58 through the real FilterBuilder. Re-run rather than spot-checked, because it is the load-bearing one — and re-run against the rendered row, with four procedures scored in the same harness so the agreement is a reading.

② semver grading

.changeset/9302-filter-builder-valueless-canonical-fold.md, frontmatter '@object-ui/components': patch. patch is correct, and it is the level this diff actually is.

No exported symbol is added, removed or changed (16 export statements byte-identical); no declared type moves; VALUELESS_FILTER_BUILDER_OPERATORS keeps its six members and its declared shape. The only behavioural movement is 8 spellings of four already-published operators ceasing to draw a value input the row's own label says it does not take — a defect repair on previously-contradictory input, not new API and not a contract removal. Nothing in the diff is capable of minor. check-changeset-presence exit 0 ("2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)"), check-changeset-fixed exit 0, check-changeset-no-major exit 0. @object-ui/components is inside the single 40-package fixed group in .changeset/config.json, so one entry carries the group and no sibling changeset is owed.

③ boundary flags

The three FAILs, each measured at THIS head.

  1. FAIL 1 (7daa352f98) — the exported set's doc-comment falsified by the repair. DISCHARGED. Both quoted sentences read 0 occurrences in the rebuilt dist/custom/filter-builder.d.ts, gutter-tolerant, with 8 live probes reading 1 in the same file. The replacement headline and the FOLD-CLOSURE sentence are true as measured (DC-1, DC-3).
  2. FAIL 2 (dfbee6340a) — the replacement prescription wrong on the population it served. DISCHARGED. must therefore fold it through normalizeFilterOperator before asking this set, exactly as that gate does reads 0; the replacement reads 1 and, executed against the rendered gate over all 58 spellings, agrees 58/58 with the three wrong procedures disagreeing on 12, 8 and 8 in the same run. It is also not hypothetical: origin/main's plugin-list/src/ListView.tsx:330-331 and :357 already run verbatim that procedure (objectui#9362, merged as c10bc4c89e).
  3. FAIL 3 (095830673a) — the changeset's false universal. DISCHARGED. every spelling the pin enumerates reads 0 in the changeset. The old scope reproduces as false in my own base arm (8 of 12; the four camelCase ids drew 0 and are their own twin) and the new scope is 8 of 8 true on every clause — both evaluated off one rendered data set, with scope the only variable, which is the control the repair claimed and I confirm.

Implementer flags and open_questions, each answered by name.

  • r1 Q1 — re-fingerprint the 208-file rollup at the repaired head? Seat took A. Upheld, and re-derived: clause-② is about exported symbols, and the three independent spot-checks above put no symbol and no declared type in motion. A rollup that did not move would mean the repair never shipped.
  • r1 Q2 — keep the in-function comment fix? Seat took A. Upheld, measured: that comment reads 0 across all 208 emitted .d.ts and 0 in both emitted JS bundles, so it costs the published surface nothing.
  • r1 Q3 / r2 Q3 — refresh the app-shell parenthetical? Seat took A, C recorded not filed. Upheld, and I can now state why by measurement rather than by reasoning: "the canonical spec spellings, which only that layer sees" was already loose at the merge-base — my base arm shows the builder itself already rendering Is null for a stored is_null there (mountedOperatorValue folds), i.e. this file already saw the canonical spelling before this diff. So the diff does not falsify it and no successor is owed here.
  • r2 Q1 — should this docblock be descriptive-only as a rule? Escalate, unchanged. Two of the three FAILs were prescriptive sentences in this one docblock. The prescription now shipping is the first one that was executed over the whole population before being written, and it measures 58/58 — so it is not the evidence for a ban. The rule the seat already adopted ("a prescriptive sentence in a published doc-comment is admissible only if the procedure it prescribes has been executed over the whole population first") is the proportionate answer; a family-wide ban is a maintainer call, not this PR's.
  • r2 Q2 — fix the pin's five objectui#9358 citations? Done at 095830673a. Verified at this head: objectui#9358 reads 0 in the pin, control objectui#9302 5.
  • r3 Q1 — may published prose name an in-repo instrument a consumer cannot run (firing, and the pin filename in the docblock)? Noted and referred, per the seat's adoption for this PR and its referral of the general rule to the maintainer. ⛔ Not re-opened and not a basis for any part of this verdict.
  • mergeable_state: behindorigin/main is at 55f39ee90f, past the merge-base. Flagging only; the queue handles it.
  • Earlier out-of-scope findings — objectui#9359 filed and since discharged by objectui#9362 on main, which I verified directly rather than inherited; the value-less/arity vocabulary split inside plugin-list is carried inside that card; the notexists lowercase boundary stands as a measured boundary, not a defect (the spec publishes no alias row for either id).
  • Declared deviations in the r1 report (unlocked ARM-B rebuild, the empty probe push, the kill-by-pattern) — all disclosed by the implementer, none of them touches a published claim or the accept set. No action.

Below the bar, real, and none of it blocks.

  • New this round — a counterexample to DC-11 that predates the diff. packages/app-shell/src/views/metadata-admin/inspectors/datasetFilterCondition.ts:55, VALUELESS_TO_MONGO, hand-lists isEmpty / isNotEmpty / isNull / isNotNull and its own docblock says "A row carrying one of these is COMPLETE without a value — the builder draws no input for it — so they are matched ahead of the value-completeness check". That is precisely a consumer telling a complete value-less row from a half-filled one, and it restates the membership rather than reading it. It also omits exists / notExists, which the exported set carries. ⛔ Not a FAIL for this PR, and measured rather than argued: VALUELESS_TO_MONGO is present at the merge-base verbatim, and the sentence is present at the merge-base with the same extension (the diff only re-wrapped it and replaced the pronoun "it" with "the membership"), so the diff neither introduced the claim's falsity nor changed its truth value — the same disposition the seat already adopted for the app-shell parenthetical. Under the reading the colon invites — the list is the extension — it is true. Carried here with a file and a line for a successor, rather than widening a fourth-round PR to repair another package's pre-existing looseness.
  • CS-8's bare appositive over-reaches by 4. every row of VIEW_FILTER_OPERATOR_ALIASES pointing at one of them has 8 members, of which isEmpty / isNotEmpty / isNull / isNotNull are the ids the sentence's own head-noun (Every OTHER spelling) excludes, and which measured 0 inputs at base. The quantifier is scoped correctly and the claim is true as written; read without its first two words the gloss is 8 of 12 — the same shape as the sentence two paragraphs below it that just failed review. Worth one word ("every other row") on any future touch; ⛔ not worth a fifth round.
  • DC-3's shorthand, executed in isolation, is wrong on 8 of 58. foldClosure.has(op) without folding the queried spelling agrees with the rendered gate 50/58 — I carried it as a fourth procedure precisely to find out. The sentence survives because the same paragraph stipulates what "ask that FOLD-CLOSURE" means (DC-6) and that procedure is 58/58, and because the intervening clause (DC-4/DC-5) states the correct extension outright. Recorded so the next reader knows it was tested and not waved through.
  • packages/components/src/__tests__/filter-builder-valueless-operators.test.tsx:11 still describes the export as "the operator ids for which it renders no value input", which now undercounts the gate by 8. Not in this diff, does not ship, already carried on the card with a file and a line.
  • CI at the pinned head: 36 runs, 33 success / 3 skipped, 0 failure, 0 cancelled, 0 in_progress. Bundle Analysis, the only prior failure in this PR's history, is green here.
  • Carriers: needs:contract-review is on both PR objectui#9358 and card objectui#9302 at the time of writing. ⛔ I wrote no label and no state; this comment is my only write.
  • Local verification: pin 30 passed (30), PIN_EXIT=0, run from the repo root (RUN v4.1.10 /home/user/objectui-review-9358-r4) under os-verify-lock.sh slot ui-9358-r4 with the exit captured by redirect before any pipe, never from a package directory. Both ablations ran under trap … EXIT INT TERM with MUTATION-ON-DISK-CONFIRMED printed before any result was read and RESTORED-OK blob=9b92ee4f9d22c54b15b09e33a2eef74ccce55f8d git-diff-HEAD=empty after each; the worktree ends with git status --porcelain empty. node_modules was never mutated.

Implemented-by: claude/issue-9302-needsvalueinput-fold (mode:subagent)
Reviewed-by: session_01L5xpA5q533BgTTNADibEFt (domain:spec @ objectui seat — this review ran as an isolated subagent, which has no session of its own)

PASS


Generated by Claude Code

@claude

claude Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

ADOPTED — the PASS at 5657815842 is adopted verbatim. Both carriers cleared; landing.

Four review rounds, three of them FAILs, each finding a different measurably false published statement. This one swept instead of hunting.

check reading
① same-form PASS at tier record 5657815842, head 4448cf70b9b8b092362667338987c3a6304f84ce, heading · sha in a code span · ①②③ itemized · independence pair · exactly one unhedged **PASS**, zero **FAIL**. Tier claude-opus-5 on all 142 harness-stamped messages, no fallback.
② both carriers, machine-read --pair 9358exit 0, run live: "both carriers agree, and its diff carries no widening tell". ⚠️ Quoting the script's own caveat rather than hiding it: a tell is not a proof and its absence is not one either — ① is what carries the clause, not the tell scan.
③ every check 36 runs, all complete: 33 success / 3 skipped / 0 failure / 0 cancelled.

⭐ The sweep, and why the dispatch was changed to demand one

Rounds 1–3 each found one false published sentence, it was repaired, and the next round found the next. That is a hunt, and a hunt terminates only by luck. This round's order required an enumeration: every claim the diff publishes, judged one by one, with the population bounded by measurement and the count stated.

46 claims enumerated — 35 in the changeset, 11 in the published doc-comment. Zero measurably false.

The bounding is the part that makes it a sweep rather than a sample: packages/components/package.json files is ["dist","README.md","CHANGELOG.md","LICENSE"]; the diff is 3 files with no README or docs edit; and the pin publishes nothingtsconfig.json excludes **/__tests__/** by directory, it is not under files, and its only trace in dist is its filename quoted inside the doc-comment.

⚠️ A warning this seat put in the order was FALSE, and the review measured it rather than obeying it

The order said "comments are kept in emitted .js as well as .d.ts". ⛔ That does not hold for this package. Measured: every prose probe reads 0 in dist/index.js and dist/index.umd.cjs, with normalizeFilterOperator lit at 1 and 7 as controls, and of 1,831 surviving block comments 1,830 are /*@__PURE__*/. So from filter-builder.tsx only the exported symbol's docblock publishes; the module-private set's docblock and the in-function comment read 0 across all 208 emitted .d.ts with four lit controls.

⭐ The general reading, corrected: whether comments reach the emitted .js is a property of the package's build toolchain, not of the repo. It was measured true for packages/react, which builds with plain tsc, and is measured false here. ⛔ Neither answer generalises; measure per package.

What was measured this round, not inherited

  • The shipped prescription agrees with the rendered gate 58/58, with three wrong procedures disagreeing on 12, 8 and 8 in the same run.
  • The repaired changeset sentence is 8/8 on every clause in a base-arm disk ablation, and the retired scope reproduces as 8/12 — old and new evaluated off one data set, scope the only variable.
  • Preimage 6 → 14, exactly 8 spellings move, zero over-reach. onChange never fired across 59 renders in both arms. Pin 30/30 from the repo root.
  • ⭐ Two things earlier rounds did not do: a pre-objectui#7561 tree was rendered (blob d272295868) to measure the "blank trigger" claim rather than argue it — is_null drew 1 input under trigger "", controls lit; and the headline was graded over the whole vocabulary — of the 14 canonical operators with more than one spelling, 4 of 14 disagreed across their own spellings at base and 0 of 14 at head.

Refinement of the instrument lesson, and it sharpens it: the reviewer's first comment-only planting did not reproduce the un-normalised false movement, because it preserved the comment's line count. Re-planted with an added comment line, the trap reproduces exactly. ⇒ the false movement is a function of comment line count, since the stripper blanks in place.

Recorded, ⛔ not blocking

  • New, named with a file and a line for a successor: packages/app-shell/src/views/metadata-admin/inspectors/datasetFilterCondition.ts:55VALUELESS_TO_MONGO hand-lists the value-less ids and is, by its own docblock, exactly the kind of consumer the published sentence quantifies over. Present verbatim at the merge-base, and so is the sentence, so ⛔ this diff does not falsify it.
  • The changeset's appositive "every row of VIEW_FILTER_OPERATOR_ALIASES pointing at one of them" has 8 members, 4 of which the sentence's own "Every OTHER spelling" excludes — true as scoped, 8/12 if the first two words are dropped. One word fixes it on any future touch.
  • "needsValueInput is the complement of this set's FOLD-CLOSURE" executed in isolation, without folding the queried spelling, agrees only 50/58. It survives because the same paragraph stipulates the operational meaning and that procedure is 58/58. ⭐ Tested, ⛔ not waved through.
  • filter-builder-valueless-operators.test.tsx:11 still undercounts — not in this diff, does not ship.
  • The unshipped-instrument question (firing, the pin filename) was recorded as noted-and-referred and ⛔ used in no part of the verdict, as the order required.

Carriers cleared from both this PR and card objectui#9302 in one pass. Landing next: ready → auto-merge → merge queue. ⛔ Card objectui#9302 keeps its state; this PR carries no closing keyword.


Generated by Claude Code

@os-sam
os-sam marked this pull request as ready for review September 14, 2026 01:37
@claude
claude Bot added this pull request to the merge queue Sep 14, 2026
Merged via the queue into main with commit 3df7c5c Sep 14, 2026
38 checks passed
@claude
claude Bot deleted the claude/issue-9302-needsvalueinput-fold branch September 14, 2026 01:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

3 participants