Skip to content

fix(app-shell): the drill URL spells $empty as filter[F][empty], so a composed 「is empty」 drill no longer opens on a superset on spec 17.6.0 (objectui#11547) - #11548

Merged
objectstack-fleet[bot] merged 41 commits into
mainfrom
claude/issue-11547-drill-url-empty-17-6
Oct 3, 2026
Merged

objectstack-fleet[bot] merged 41 commits into
mainfrom
claude/issue-11547-drill-url-empty-17-6

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #11547

Clause-②: yes (widening)

What this PR is

On @objectstack/spec 17.6.0, a composed drill whose widget filter says 「is empty」 dropped that condition from the drill URL. The spec now lowers is_empty / is_not_empty to $empty, and the filter[…] dialect spelled emptiness only from $null / $exists. So the /data list opened on a superset. This is a regression the 17.6.0 bump (objectui#11531) introduced, against 17.5.0's filter[F][null]=true.

The fix follows objectui#9159's precedent for [null]:

  • Serializer: { F: { $empty: true | false } } → filter[F][empty]=true|false, beside any [null] param and range bound. A non-boolean $empty writes nothing.
  • Reader (parseUrlFilterTriples, the ADR-0055 /data surface): [empty]=true|false → [F, 'is_empty' | 'is_not_empty', true], the triples convertFiltersToAST emits. Any other value is dropped. The pair stays out of URL_FILTER_OPS, for the reason the NULL_FILTER docblock gives ("Save as view" correctness).
  • Chips: the existing filterBuilder.operators.is_empty / is_not_empty keys. No new string.
  • $empty and $null on one field are two params, both written and both read back. That matches convertFiltersToAST's ['and', …] for the same object, so nothing degrades.
  • The NULL_FILTER docblock's "a COMPOSED drill filter only ever arrives spelled $null" is corrected for 17.6.0.

⛔ The equality route's operator arm (parseUrlEqualityFilterTriples, objectui#9196) is untouched, as are composeDrillFilter, the dataset inspector and the i18n packs.

Tests

  • Red first on the trunk tip 13f07dd0f: 10 of 12 new cases failed (for example '?filter[stage]=won' with no emptiness param). The 2 [null] controls passed.
  • Green: the module's import radius, 14 files and 213 tests; the 40 ObjectView.* importers, 394 tests; app-shell type-check exit 0.
  • Ablations (each restored by blob hash):
    • the serializer arm → 17 red;
    • the reader arm → 21 red (Save-as-view and the zh/en chip pins included).
  • Gates: changeset presence and no-major, citations (0 new), control bytes, check:i18n-keys, the vi-mock family, check:spec-symbols. Governed-queue guard: NOT GOVERNED.

Changeset

.changeset/11547-drill-url-is-empty-operator.md: @object-ui/app-shell minor. That is the level objectui#9159 and objectui#9508 used for a URL-dialect widening.

Measured reach: 0 authored dashboard or chart widget filters with an empty operator across objectstack examples, hotcrm, cloud and objectui. Stored customer dashboards are not measured. This lands before the next objectui release, which is held on #5400 until it does.

Claim 5969214761 · dev report 5969555930 · the at-tier contract review follows on this PR.

🤖 Generated with Claude Code

https://claude.ai/code/session_016tKoy8NJa35Yih1FdzrVmn

claude added 30 commits October 2, 2026 22:23
…i#11438)

Regenerated with pnpm 10.31.0 only, by the raise-and-restore route the
17.5.0 bump used: every @objectstack/* specifier raised to ^17.6.0 for one
`pnpm install`, every manifest restored byte-for-byte to HEAD, then a second
`pnpm install` reconciles the importers to their declared ranges.

Resolution census against the base lock: the seven @objectstack/* packages
(spec, types, core, client, formula, lint, sdui-parser) move 17.5.0 -> 17.6.0
and nothing else moves. No importer specifier changes. The registry's new
`deprecated` field on twelve @yuku-analyzer/binding-* 0.8.7 entries is
written into the lock by the same tooling.

Claude-Session: https://claude.ai/code/session_01HRYqpqGcWpJuJkDmbRF75w
Co-authored-by: Claude <noreply@anthropic.com>
…, a component under a name @objectstack/spec 17.6.0 now exports (objectui#11438)

@objectstack/spec 17.6.0 exports `EmptyState` from `@objectstack/spec/ui`:
the authoring type of a list view's empty-state config (title, message,
icon). @object-ui/components publishes `EmptyState` as an alias of the
`DataEmptyState` React component. A component is not a config shape, so
neither import nor derivation fits; the alias is a published name, so its
retirement or rename belongs to its own card. Same disposition objectui#11073
gave the four `Object*Props` collisions 17.5.0 surfaced.

Claude-Session: https://claude.ai/code/session_01HRYqpqGcWpJuJkDmbRF75w
Co-authored-by: Claude <noreply@anthropic.com>
…ing now that @objectstack/spec 17.6.0 carries the arm (objectui#11438)

objectui#11206 widened `STRIPPED_LINE`'s key type by `| 'computed'` so it
compiled against both a three-arm pin and objectstack `main`, and left a
tripwire in `writeWarningToast.test.ts` for the bump that carries the arm.
17.6.0's `DroppedFieldsEventSchema.reason` lists `computed`, so this does what
the tripwire says: the key type is the spec union again, the `computed` case
is spelled as the literal, the spent tripwire is deleted, and
spec-symbol-batch6's `_ReasonIsTheEnum` pins the exact four-member union.

Claude-Session: https://claude.ai/code/session_01HRYqpqGcWpJuJkDmbRF75w
Co-authored-by: Claude <noreply@anthropic.com>
…spec/migrations, where 17.6.0 moved it (objectui#11438)

17.6.0 carries the spec's migrations-entry-split: `MIGRATIONS_BY_MAJOR` is
exported from `@objectstack/spec/migrations` and no longer from the root. The
test read the root dynamically so it compiled against both the old pin and
objectstack `main`, with no fallback, and its docblock asked the bump that
moves the pin past the split to replace that with a static import from the
subpath. This does that.

Claude-Session: https://claude.ai/code/session_01HRYqpqGcWpJuJkDmbRF75w
Co-authored-by: Claude <noreply@anthropic.com>
…jectstack/spec 17.6.0 refuses by name (objectui#11438)

17.6.0 carries objectstack#20758: `PageHeaderProps.breadcrumb` is a tombstone
refused by name. objectui's renderer has not read it since objectui#11166,
and its `inputs` entry said it stayed only while the spec accepted the key and
would leave with the retirement. It leaves now, so the registry no longer
publishes an input the contract refuses
(`registry-inputs-spec-parity.test.ts`, "page:header declares no top-level
input the spec does not accept").

The slotted-pages guide example drops `breadcrumb: true`, which the contract
now refuses (its pin parses the example against the spec row), and the
page-header doc and the renderer comment say the key is refused rather than
"accepted until retired".

Claude-Session: https://claude.ai/code/session_01HRYqpqGcWpJuJkDmbRF75w
Co-authored-by: Claude <noreply@anthropic.com>
…7.6.0 made stale (objectui#11438)

objectui#11111 decision 3 = B booked `action:button.endpoint` and
`action:icon.endpoint` as spec keys held unpublished, owed to objectui#11168.
17.6.0 refuses `endpoint` on both blocks (objectstack b3917d90, the rename to
`target`), so neither entry names a key the spec declares, and the
dangling-exemption check reds on them. Both are struck and the ledger cap
falls from 3 to 1 (`undoable` stays, still owed to objectui#11168).

Claude-Session: https://claude.ai/code/session_01HRYqpqGcWpJuJkDmbRF75w
Co-authored-by: Claude <noreply@anthropic.com>
… group, which @objectstack/spec 17.6.0 adds (objectui#11438)

17.6.0's `GetTranslationsResponseSchema.translations` carries a `picklists`
group (objectstack addbbf02, the `picklist` metadata kind). The recogniser's
group list is written out, and the console's `loadLanguage.test.tsx` walks the
spec's list so a group the spec adds turns it red instead of a bundle carrying
only that group being returned untransformed and read by nothing. Added; the
transform already copies an unlisted group through onto `app`.

Claude-Session: https://claude.ai/code/session_01HRYqpqGcWpJuJkDmbRF75w
Co-authored-by: Claude <noreply@anthropic.com>
…t its key, as @objectstack/spec 17.6.0 raises it (objectui#11438)

Through 17.5.0 the spec raised the reserved-name refusal on the `fields` map
(`custom @ fields`). objectstack#20997 names the key, and 17.6.0 carries it
(`custom @ fields.constructor`). The pin said only this reading moves when
that lands; it moves, in the instrument row and in the banner row, which now
names `fields.constructor`.

Claude-Session: https://claude.ai/code/session_01HRYqpqGcWpJuJkDmbRF75w
Co-authored-by: Claude <noreply@anthropic.com>
…@objectstack/spec 17.6.0 leaves true, and ledger the four that name a past release (objectui#11438)

`check:installed-pin-claims` reads every present-tense claim about the
installed `@objectstack/*` version against the resolved artifact. 38 sites
named 17.5.0. Each was decided, never find-and-replaced:

- RESTAMPED after re-measuring the fact the sentence states against the
  installed 17.6.0 (each probe was also run against a 17.5.0 tarball as the
  control, and answered the same on both): `FieldSchema`'s `dependsOn`, `rows`
  and option `description` readings; `object-calendar.data` and `.calendar`;
  `record:details.columns`; `record:highlights` `fields[]` and `layout`;
  `SelectOptionSchema` labels; the `@objectstack/lint` field-rule verdict text;
  `referenceTo` and its four alias spellings; `CalendarConfigSchema`'s required
  key, options and refusal hints; `record:alert.visible`; `object-kanban`'s
  fifteen keys; the screen node's `waitForInput`; `object-calendar`'s ten flat
  members; `ObjectListViewSchema`'s members; the `view.list.navigation.view`
  tombstone in the gantt README pin; and the sites whose own tests measure the
  fact and pass on 17.6.0.
- REWORDED where the fact moved: the retired-key type is the branded
  `[REMOVED]` mark at the pinned 17.6.0 (bare `undefined` through 17.5.0), on
  `retired-key-type.ts` and the five pins that cite its spellings.
- LEDGERED as `historical` (four files, seven sites): lines whose version says
  WHEN something changed or names a release's record set, where `pinned` is an
  unrelated verb or the sentence sits under a dated update heading.

Claude-Session: https://claude.ai/code/session_01HRYqpqGcWpJuJkDmbRF75w
Co-authored-by: Claude <noreply@anthropic.com>
…ack/spec 17.6.0 moves, and pair the live-e2e backend with it (objectui#11438)

Each pin names what moved, measured against a 17.5.0 control:

- `ObjectSchema`'s accepted keys are 44, not 43: 17.6.0 adopts `imageField`
  (gained set exactly that one, lost set empty). Both corpus-guard counts are
  re-pointed as the 17.3.0 adoption was.
- The spec's exports map has 21 entries, not 20: `./migrations` is added (no
  `browser` arm), nothing removed. Every count and its prose follow.
- `check-lockfile-integrity`'s two lockfile samples are rebuilt on the 17.6.0
  snapshot keys their own tripwire named.
- The objectui#11111 owner total for objectui#11168 follows the cap to 1 (the
  two `endpoint` entries struck earlier on this branch).
- `e2e/live/ci/backend.env` pins `OBJECTSTACK_VERSION=17.6.0`, the version the
  lockfile resolves, as the pin requires (`@objectstack/cli@17.6.0` resolves
  to the 17.6.0 tag commit).
- The page-header doc drops the version literal it gained on this branch;
  `doc-version-claims` holds docs to pointers, not version numbers.

Claude-Session: https://claude.ai/code/session_01HRYqpqGcWpJuJkDmbRF75w
Co-authored-by: Claude <noreply@anthropic.com>
…k/spec 17.6.0, so it joins the covered arms (objectui#11438)

17.6.0 types `ComponentPropsMap['object-grid'].grouping` as the grouping
config instead of `z.unknown()`. The authored arm's `properties` is that row
by reference, so a padded grouping field name is now refused at its entry on
all four faces. The census's recorded reason for leaving the bag member
uncovered stopped holding; it moves to the declaring table and is pinned like
every other arm. The node-level `grouping` stays uncovered: it is the flat
spelling's by-name refusal.

Claude-Session: https://claude.ai/code/session_01HRYqpqGcWpJuJkDmbRF75w
Co-authored-by: Claude <noreply@anthropic.com>
…stack/spec 17.6.0's walk descends it (objectui#11438)

objectui#11223's block (e) held `page:card`'s `properties.footer` as the one
slot the spec's exported walk did not descend, and said to retire itself when
that changed. 17.6.0's walk descends `properties.footer[]`, objectui's faces
now judge a footer child, and block (d) asked for the position to be added to
(a). It is: the footer is a row of (a) on both faces, (e) is deleted, and the
CLI doc no longer names the footer as an unjudged slot.

Claude-Session: https://claude.ai/code/session_01HRYqpqGcWpJuJkDmbRF75w
Co-authored-by: Claude <noreply@anthropic.com>
… @objectstack/spec 17.6.0's root entry shed (objectui#11438)

The second payback of objectui#11073's ruled raise, objectstack#20646, reaches
objectui with the 17.6.0 bump: the spec root no longer exports its migration
chain. Measured with two console builds in one lock hold (`CI=true pnpm exec
vite build` after the packages build): `main` at `2d576e46` (17.5.0) weighs
3,636,289 gzipped eager bytes, this branch at `2ba091c` weighs 3,386,193;
`vendor-objectstack` alone is -250,108, the other 329 chunks +12.

Left alone, the ceiling would sit 3.01x the regression above the payload and
the gate's own sensitivity check exits 2 (a blind gauge). So, as objectui#11101
did for the first payback: `MAX_EAGER_CLOSURE_GZIP_BYTES` 3,660,380 ->
3,410,284 and `vendor-objectstack` 1,689,265 -> 1,439,157, each lowered by
exactly what it recovered; BASELINE and PER_CHUNK_BASELINE move to the
`2ba091c` reading; headroom 0.26x and 0.20x, the bands `main` already had.
The test's rendered literal is re-taken (3306.8). The changeset says what the
console gains.

Claude-Session: https://claude.ai/code/session_01HRYqpqGcWpJuJkDmbRF75w
Co-authored-by: Claude <noreply@anthropic.com>
…#11536 under objectui#11438 ruling A′ (objectui#11438)

@objectstack/spec 17.6.0 gives `record:line_items` a ComponentPropsMap row
(objectstack 24c554da, #21142 / #21244), so registry-inputs-spec-parity
judges it from that pin on. objectui#11438 ruling A′ (record 5965062249)
applies objectui#11111 decision 3 = B to the bump: the population is
re-pinned and every difference is booked to the owner card filed for the
group, objectui#11536, with an expiry. Nothing is declared or pinned here.

- EXPECTED_COVERED gains `MINOR_17_6_BLOCKS` (`record:line_items`), armed
  when the installed spec carries it; the census goes 55 -> 56 carried and
  39 -> 40 judged.
- UNPUBLISHED_EXEMPTIONS books the ten keys `inputs` omit; MEMBER_PIN_EXEMPTIONS
  books `columns` and `dataSource`, and MEMBER_PIN_EXEMPTION_CEILING goes
  0 -> 2 with the booking.
- The objectui#11111 ledger gains objectui#11536 as a fourth owner, routed by
  block. Its entries cite ruling A′ and decision 3 = B and expire 2026-11-02
  (thirty days after the ruling, as the 17.5.0 entries' 2026-10-30 is after
  theirs). Caps: unpublishedKeys 1 -> 11, memberPins 0 -> 2, owner total 12.

Claude-Session: https://claude.ai/code/session_01HRYqpqGcWpJuJkDmbRF75w
Co-authored-by: Claude <noreply@anthropic.com>
… beside a pin marker (objectui#11438)

`check:installed-pin-claims` read two of the lines the booking added as
present-tense claims about the installed spec: the `MINOR_17_6_BLOCKS`
guard's docblock ("Does the installed ... carry the 17.6.0 set?") and the
`memberPins` cap comment, where the version sat beside the objectui#11168
slice's "pinned". Both were true at 17.6.0 and would go stale at the next
bump. The guard's docblock now asks about the spec this tree resolves, and
the two cap comments name the ruling that booked the entries instead of the
version. No assertion moves.

Claude-Session: https://claude.ai/code/session_01HRYqpqGcWpJuJkDmbRF75w
Co-authored-by: Claude <noreply@anthropic.com>
…y on @objectstack/spec 17.6.0 (objectui#11094)

17.6.0 stops folding the empty pair onto is_null / is_not_null and lowers
it to $empty, which joins FILTER_OPERATORS (objectstack#20570,
objectstack#20446). The matcher had arms only for the null pair, so a
stored is_empty rule on a provider: 'value' source reached the refusal
arm and selected no row.

The AST arm gains is_empty / is_not_empty and the object arm gains
$empty, both through the spec's isEmptyFilterValue with no declared row:
this adapter holds no field declarations, so it judges by value (null,
absent, '' and [] are empty). A non-boolean $empty flag is refused.

ValueDataSource.astFilterVocabulary is rewritten to the new fold, the
dollar-vocabulary parity table gains $empty, and the conformance file's
staged-$empty partition moves into the executed table as its expiry row
instructed.

Claude-Session: https://claude.ai/code/session_01YLg8XqWGJ785fwQ5v4pH37
Co-authored-by: Claude <noreply@anthropic.com>
…it refused (objectui#11094)

$empty is a member of FILTER_OPERATORS since @objectstack/spec 17.6.0,
and ValueDataSource now executes it, but the $-dialect lowering had no
arm and threw "Unknown filter operator". The objectui#8976 shape, one
operator over: the README pin's FILTER_OPERATORS completeness check
reddened on it.

$empty: true lowers to [f, 'is_empty', true] and false to
[f, 'is_not_empty', true], the inverse of the spec's parseFilterAST,
written the way $null is. A non-boolean flag throws the INVALID_FILTER /
400 envelope, matching ValueDataSource's refusal. The unknown-operator
message lists $empty, and the data-objectstack README gains its row.

Claude-Session: https://claude.ai/code/session_01YLg8XqWGJ785fwQ5v4pH37
Co-authored-by: Claude <noreply@anthropic.com>
… and declare the slice's changeset (objectui#11094)

filter-source-merge: the server now reads the valueless is_empty
2-tuple as $empty: true, not $null: true; the assertion's point (no
invented value slot) is unchanged.

FilterConditionField.operators: the expiry row flips to "FILTER_OPERATORS
admits $empty". The entry stays in KNOWN_UNREACHABLE with its reason
restated: which builder row authors $empty is objectui#10813's decision,
and a new expiry row reddens the day a builder operator emits it.

Claude-Session: https://claude.ai/code/session_01YLg8XqWGJ785fwQ5v4pH37
Co-authored-by: Claude <noreply@anthropic.com>
…ription / emptyState rows — publish both, type emptyState text as I18nLabel, resolve maps at both renderers (objectui#11227)

@objectstack/spec 17.6.0 declares description, emptyState and
keyboardNavigation on its object-grid row (objectstack#20694). This slice
follows the first two:

- GRID_QUERY_INPUTS publishes description (string and object arms, an
  I18nLabel the grid already resolves) and emptyState.
- ObjectGridSchema.emptyState is the spec's EmptyState; the zod twin takes
  EmptyStateSchema by reference, not a second shape. NamedListView.emptyState
  indexes ListViewSchema's own member. title and message are I18nLabel.
- ObjectGrid and ListView resolve emptyState.title / .message with the spec's
  resolveI18nLabel against the display locale. ObjectGrid handed a map to
  DataEmptyState as an object child, which React refuses; ListView's typeof
  guard drew the default copy for every map.
- The objectui#11276 bag pins flip to the 17.6.0 readings (not deleted), the
  objectui#11068 twin pin reads invalid_union, member pins are registered for
  object-grid.description and .emptyState, and the docs move description into
  the bag (written flat it is now refused toward properties.description).

keyboardNavigation is not published: nothing reads it yet. The parity gate
stays red on that one key, reported for a decision.

Claude-Session: https://claude.ai/code/session_01YLg8XqWGJ785fwQ5v4pH37
Co-authored-by: Claude <noreply@anthropic.com>
…e pending changesets its slice falsified

.changeset/11227-object-grid-17-6-keys.md declares plugin-grid and types
minor and plugin-list patch. Three pending changesets of the same release
said emptyState was three optional strings or not authorable in a document;
each gets a dated correction paragraph instead of a rewrite.

Claude-Session: https://claude.ai/code/session_01YLg8XqWGJ785fwQ5v4pH37
Co-authored-by: Claude <noreply@anthropic.com>
…ng to any (objectui#11094)

The new $empty pin's row helper casts to QueryParams['$filter'], and the
FilterConditionField expiry row needs no cast at all (BuilderCondition's
value is already loose). Runtime-identical; eslint's no-explicit-any
count on the touched files returns to its base reading.

Claude-Session: https://claude.ai/code/session_01YLg8XqWGJ785fwQ5v4pH37
Co-authored-by: Claude <noreply@anthropic.com>
… and the READMEs drop the version literal (objectui#11094)

The full suite caught two things the targeted run could not:

- filter-refusal-subject-9050 enumerates every FilterOperatorError throw
  site in filter-converter.ts and reddened on the new $empty one (13
  sites, 12 rows). Its row names $empty as the subject.
- doc-version-claims refuses an unledgered version literal on a scanned
  doc surface; the data-objectstack README now cites objectstack#20446
  instead of the release number, and the core adapters README cites
  objectstack#20570 the same way.

Claude-Session: https://claude.ai/code/session_01YLg8XqWGJ785fwQ5v4pH37
Co-authored-by: Claude <noreply@anthropic.com>
…e-read the NamedListView loose census, cite objectstack#20694 instead of a version literal (objectui#11227)

- imported-defaults-8317: the flat mirror now imports the spec's
  EmptyStateSchema, so it joins IMPORTED and the differential measures it.
- object-view-unmirrored-keys-7779: NamedListView.emptyState no longer
  restates a nested object literal, so the loose regex reads 73 (gap 9),
  not 76 (gap 12). The member census is unchanged at 64.
- The plugin-grid README, plugin-grid.mdx and schema-reference.md cite the
  upstream card instead of a spec version literal, which
  doc-version-claims refuses unless ledgered.

Claude-Session: https://claude.ai/code/session_01YLg8XqWGJ785fwQ5v4pH37
Co-authored-by: Claude <noreply@anthropic.com>
One conflict, resolved mechanically: packages/plugin-grid/src/index.tsx,
GRID_QUERY_INPUTS. main's rewritten `conditionalFormatting` input
(objectui#11533) is kept exactly as main has it, and objectui#11227's
`emptyState` comment and entry follow it. No other edit.

Claude-Session: https://claude.ai/code/session_01YLg8XqWGJ785fwQ5v4pH37
Co-authored-by: Claude <noreply@anthropic.com>
…ensus to objectui#11334 under objectui#11438 ruling A″

The spec that objectui#11531 resolves adds `checkDashboardWidgetDimensionlessMeasureArity`
to `DashboardWidgetSchema` (objectstack `11d28c17`, #21053). objectui's mirror cannot
re-attach it by static import: objectstack `main` renamed the export (`32d57690`), and
the Spec Main Shape Gate refuses the 17.6 name. objectui#11438 ruling A″ (record
5968177777) books row 3 to objectui#11334 with an expiry, in objectui#11111 decision
3 = B's shape.

- The `DashboardWidgetSchema (complex.zod.ts)` row's `notAttachable` list books the
  check, written OWED TO objectui#11334. Expires when objectui resolves an
  `@objectstack/spec` carrying `32d57690`, or 2026-11-02, whichever is first.
- The census bounds the booking: the row's count equality and the exact export-name
  match admit nothing beyond the listed entry, and the expiry condition turns the
  name census red by name.
- An empty-frontmatter changeset declares no release; the test is excluded from
  `dist`.

Claude-Session: https://claude.ai/code/session_016tKoy8NJa35Yih1FdzrVmn
Co-authored-by: Claude <noreply@anthropic.com>
…ws of both 8894 measure-door tests to objectui#11334 under objectui#11438 ruling A″

Both files derive FAMILY by effect, so the spec's dimensionless measure-arity
check (objectstack `11d28c17`, #21053) put pie, donut, funnel, scatter, treemap,
sankey and radar into it, and objectui's door does not refuse them: the mirror
waits for objectui#11334, because objectstack `main` renamed the export
(`32d57690`). The seat ruled Q1 = A on objectui#11334 (record 5968413148): a
bounded in-file ledger with inverted pins, under ruling A″ (record 5968177777)
and objectui#11111 decision 3 = B (record 5902351047).

- `OWED_TO_OBJECTUI_11334` names exactly the seven types in each file, with the
  owner, the rulings and the expiry: objectui resolves an `@objectstack/spec`
  carrying `32d57690`, or 2026-11-02, whichever is first.
- The listed types' rows assert today's difference: the spec refuses, and
  objectui's door accepts (types); the add control is offered and no stored
  refusal is shown (panel).
- Cap rows require the FAMILY types objectui fails to refuse to equal the ledger
  exactly, so a new difference is red, and so is a listed type objectui starts
  refusing once the mirror attaches.
- The metric family's rows keep their assertions byte for byte; they iterate
  FAMILY minus the ledger, and a row keeps that set non-empty.

Claude-Session: https://claude.ai/code/session_016tKoy8NJa35Yih1FdzrVmn
Co-authored-by: Claude <noreply@anthropic.com>
claude added 11 commits October 3, 2026 11:29
…igation, to objectui#11068 under objectui#11438 ruling A″

@objectstack/spec 17.6.0's object-grid row declares `keyboardNavigation`, and
the grid's `inputs` do not publish it: objectui#11068 is building the key's
reader and publishes the key with it. objectui#11438 ruling A″ (record
5968177777) books a row whose slice is not accepted when the row-3 booking is
pushed "the same way, to their own cards", in objectui#11111 decision 3 = B's
shape (record 5902351047). On the trunk tip c4775ed the parity file had
exactly two red rows, both reading ['keyboardNavigation']; with this booking
it reads 255 / 255.

- UNPUBLISHED_EXEMPTIONS books `object-grid.keyboardNavigation` OWED TO
  objectui#11068, with an expiry: 2026-11-02 (ruling A″ is of the same day as
  ruling A′), or when objectui#11068 lands, whichever is first.
- objectui#11068 joins the ledger's owners as the fifth, routed by exact id;
  the `unpublishedKeys` cap goes 11 -> 12 and the per-owner totals gain
  objectui#11068: 1. No other cap moves (the key is boolean-armed, so no
  member pin is owed).
- `the four GA blocks resolve their ruled split` refused any exemption on the
  four GA blocks except the ruled carve-out, so the ledger entry alone left it
  red. It now admits this one OWED id BY NAME: the OWED entries on the four
  blocks must equal ['object-grid.keyboardNavigation'] exactly, the undeclared
  set must equal the booked set (so the build's landing turns it red until the
  booking is struck), and every other exemption on the four blocks is still
  refused. objectui#4648's docblock states the exception.
- Changeset: empty frontmatter; the file is unreachable from the console
  bundle and `plugin.*`.

Claude-Session: https://claude.ai/code/session_016tKoy8NJa35Yih1FdzrVmn
Co-authored-by: Claude <noreply@anthropic.com>
…, in English (objectui#11068)

The owner paragraph quoted the maintainer's order in Chinese; this repo's
codebase rule keeps code comments English, so it now cites record 5968177777
and says what the order was. No assertion moves.

Claude-Session: https://claude.ai/code/session_016tKoy8NJa35Yih1FdzrVmn
Co-authored-by: Claude <noreply@anthropic.com>
…es, to objectui#11344 under objectui#11438 ruling A″

@objectstack/spec 17.6.0's ActionSchema declares `outcomeMessages`, and
objectui does not read it yet: objectui#11344 is building the reader and lists
the key with it. objectui#11438 ruling A″ (record 5968177777) books a row whose
slice is not accepted when the row-3 booking is pushed "the same way, to their
own cards", in objectui#11111 decision 3 = B's shape (record 5902351047). On the
trunk tip bebd7ec the pin file had exactly one red row, "lists every key the
spec ActionSchema declares", reading missing ['outcomeMessages'] and stale [];
with this booking it reads 20 / 20.

- `OWED_TO_OBJECTUI_11344` books `outcomeMessages`, with the owner, the rulings
  and the expiry: 2026-11-02, or when objectui#11344's slice lands, whichever
  is first.
- "lists every key the spec ActionSchema declares" now requires `missing` to
  EQUAL the ledger exactly, and `stale` stays []. A new spec key is red, and so
  is the booked key once objectui lists it, so the booking cannot outlive the
  reader.
- A new OWED row asserts today's difference: the spec declares the key, and
  neither `SPEC_ACTION_KEYS` nor `KNOWN_ACTION_KEYS` knows it.
- Changeset: empty frontmatter; the file is under `src/**/__tests__/**`, which
  the package's build program excludes.

Claude-Session: https://claude.ai/code/session_016tKoy8NJa35Yih1FdzrVmn
Co-authored-by: Claude <noreply@anthropic.com>
…jectui#11227)

`@object-ui/types` publishes types that read `EmptyState` and a zod mirror
that reads `EmptyStateSchema`, both from `@objectstack/spec/ui`. Neither
export exists in `@objectstack/spec` 17.5.0, which the old `^17.5.0` range
admitted; both ship from 17.6.0. `pnpm check:spec-floors`, over a full
workspace build of the trunk tip, named exactly this floor (two
floor-too-low findings, this package only).

The lockfile moves only this importer's `specifier:` line; the resolved
`@objectstack/spec` stays 17.6.0.

Claude-Session: https://claude.ai/code/session_016tKoy8NJa35Yih1FdzrVmn
Co-authored-by: Claude <noreply@anthropic.com>
…de/issue-11227-types-spec-floor-17-6

The trunk moved by the row-5 booking only, which is disjoint from this
slice's three files.

Claude-Session: https://claude.ai/code/session_016tKoy8NJa35Yih1FdzrVmn
Co-authored-by: Claude <noreply@anthropic.com>
… that moves (objectui#11227)

The objectui#11438 changeset said no declared range moves and that
check:spec-floors finds no floor that has to rise. That was true when it
was written and went false once @object-ui/types began reading
EmptyState / EmptyStateSchema, which @objectstack/spec 17.5.0 does not
export. The sentence now names that floor and where it is declared; front
matter untouched, prose only.

Claude-Session: https://claude.ai/code/session_016tKoy8NJa35Yih1FdzrVmn
Co-authored-by: Claude <noreply@anthropic.com>
…d 17.6.0 drill keeps the condition (objectui#11547)

From @objectstack/spec 17.6.0, parseFilterAST lowers is_empty / is_not_empty
to { $empty: true | false } instead of $null. composeDrillFilter goes through
that lowering, so a widget filter saying "is empty" reached
serializeDrillFilterParams as $empty, hit no arm, and wrote no param: the
/data list opened on a superset of what the widget counted.

Following the objectui#9159 precedent, drillUrlFilters gains EMPTY_FILTER:

- write: { $empty: bool } -> filter[F][empty]=true|false, beside any [null]
  param and range bound; a non-boolean writes nothing;
- read: parseUrlFilterTriples reads [empty]=true|false back as is_empty /
  is_not_empty (comparand true), kept out of URL_FILTER_OPS so "Save as
  view" keeps the canonical rule; any other value is dropped;
- chips: the filter builder's existing is_empty / is_not_empty keys.

$empty and $null on one field are two params, so both are written and read
back, matching convertFiltersToAST. The NULL_FILTER docblock's "a COMPOSED
drill filter only ever arrives spelled $null" is corrected for 17.6.0.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tKoy8NJa35Yih1FdzrVmn
… operator, not the operator object (objectui#11547)

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tKoy8NJa35Yih1FdzrVmn
…ctui#11547's slice

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tKoy8NJa35Yih1FdzrVmn
… (objectui#11547)

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tKoy8NJa35Yih1FdzrVmn
…slice

The trunk claude/issue-11438-objectstack-17-6-0 landed on main as 6158e4c and its branch is gone; main's tree is byte-identical to the trunk tip 0507d67 this slice already carries, so this merge moves no file and only re-anchors the slice on main.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tKoy8NJa35Yih1FdzrVmn
@github-actions

github-actions Bot commented Oct 3, 2026

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 1 pending changeset(s) describe a file this change touches

Their bodies publish verbatim into the CHANGELOG at the next release, so this is a request to re-read them against your diff — addressed here because you are the one seat that can answer it without re-deriving anything.

⛔ Nothing here blocks, and nothing here is a verdict on your change. This gate exits 0, is not a required context, and judges name resolution, never meaning: it asked whether a pending body names a file you touched. "Is this sentence still true?" is the one question it will not answer, and the one you are being asked to answer.

.changeset/9159-drill-url-isnull-operator.md

  • names ObjectDataPage.saveAsViewFilterFold.test.ts → packages/app-shell/src/views/ObjectDataPage.saveAsViewFilterFold.test.ts — edited by this change

    The range maps are deliberately untouched: is_null is already a canonical ViewFilterRule word, and ObjectDataPage inverts URL_FILTER_OPS to bridge a triple's operator to the spec's alias spelling. An entry there would have sent it through that bridge as the alias null, which the rule schema refuses — a "Save as view" that silently loses the condition. ObjectDataPage.saveAsViewFilterFold.test.ts pins the fold for the new operator, and drillEmptyBucketNavHost-9085.test.ts — which recorded this boundary as open — is updated to record it closed.

Read the paragraph, not the line: both false halves of the objectui#8617 claim sat in one paragraph, and correcting either alone would have left it asserting the same wrong thing.

If a claim did go false, correct the body. That is precedented and prose-only, frontmatter untouched; check-changeset-overwrite.mjs will report the correction as its own case 2 ("correcting a declaration on purpose … legitimate"), which is the intended shape — one gate asks for the read, the other records the write.

Not covered, stated so nobody reads this as more: a born-false claim that spells no line address at all (objectui#9495 coordinated one by ORDINAL — "a grep finds that member first" — and deciding that means reading what the sentence means), a claim spelled as a symbol or a package rather than a backticked file name, and a file named ambiguously.

Compared the checked-out tree with 6158e4c93 (merge-base with origin/main): 5 file(s) changed outside .changeset/, read against 2076 pending declaration(s) that publish a body (2711 pending in total). · run

@github-actions

github-actions Bot commented Oct 3, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 330 chunks) 3307.5 KB 3330.4 KB
Main entry chunk (gzip) 150.3 KB 350 KB
Entry file index-BdeU-0Dd.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.88KB 6.25KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.17KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.70KB 10.94KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
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) 27.11KB 7.97KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.50KB 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) 570.92KB 136.80KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 231.08KB 64.07KB
fields (index.js) 261.51KB 66.22KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.49KB 9.23KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 39.47KB 11.25KB
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) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.86KB 5.00KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.52KB 2.26KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
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) 16.04KB 3.92KB
plugin-calendar (index.js) 53.17KB 15.46KB
plugin-charts (index.js) 83.60KB 22.89KB
plugin-chatbot (index.js) 198.22KB 46.97KB
plugin-dashboard (index.js) 142.26KB 38.44KB
plugin-designer (index.js) 231.28KB 48.76KB
plugin-detail (index.js) 245.43KB 64.53KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 172.73KB 44.50KB
plugin-gantt (index.js) 179.16KB 45.06KB
plugin-grid (index.js) 234.37KB 64.34KB
plugin-kanban (index.js) 49.59KB 15.58KB
plugin-list (index.js) 116.55KB 28.95KB
plugin-map (index.js) 25.60KB 8.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.12KB 12.29KB
plugin-timeline (index.js) 38.80KB 11.71KB
plugin-tree (index.js) 14.51KB 5.15KB
plugin-view (index.js) 90.23KB 22.73KB
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.81KB 3.58KB
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) 120.63KB 39.56KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 6.06KB 2.68KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 21.42KB 7.05KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.16KB 1.96KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
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) 5.07KB 2.39KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
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) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.93KB 7.25KB
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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 1fac6ff4a7a82cd3afcd2993af5b77b24baaa029
Local-runs: none

Read: card objectui#11547 (body, claim 5969214761, dev report 5969555930), PR #11548 (body, files, net diff against main), the head's check-runs, the NULL_FILTER precedent on main, and packages/spec/src/data/filter.zod.ts at @objectstack/spec@17.6.0 (and at 17.5.0 for the before state). The merge-base with main is main's tip 6158e4c93 (objectui#11531's landing), so the branch fast-forwards. The net diff is exactly the 6 files the claim scoped: drillUrlFilters.ts, its test, the new drillEmptyDialect-11547.test.tsx, the 9159 chip-i18n and Save-as-view fold pins, and one changeset. The head matches the dev report's 1fac6ff4a.

Check-runs on the head (gate verdicts): 26 success, 3 skipped (the coverage jobs and dependabot), 0 failure. Build & E2E, Test (dist pins), Bundle Analysis, Live E2E, README Export Check, Changeset Declaration / Bump Policy / Fixed Group, Line Citation Gate, Control Byte Scan, Governed Surface Queue Guard and Inert vi.mock Specifier Check are all green. ⚠️ Still in_progress when this record was written (13 jobs): Test shards 1 to 8, Type Check, Lint, Skill Example Check, Spec Main Shape Gate and Doc Snippet Type Check. This verdict judges the contract. It does not stand in for those gates, and merge waits for them to go green.

① Derived judgments

The spec lowering, at 17.6.0. All four empty spellings now lower to $empty:

  • AST_OPERATOR_MAP at :2553–2556;
  • convertComparison at :2748–2753, taking the direction from the operator name;
  • canonicalAstOperator at :2613–2614, which keeps is_empty / is_not_empty as their own canonical names;
  • $empty sits in FILTER_OPERATORS (:3167).

At 17.5.0 the same four lowered to $null (:2509–2512). The card's premise holds: on 17.6.0, a composed drill hands the serializer { F: { $empty: bool } }. On main, collectFilterParams had no arm for it, so the condition was dropped and the list opened on a superset.

The serializer's $empty arm: correct.

  • Booleans only. The typeof empty === 'boolean' gate copies nullDirection's gate. Pinned: 'yes', 1, 0, 'true', null and {} each write nothing, with a positive control that both boolean forms do write.
  • Beside the other params. The arm is its own params.set on its own key, filter[F][empty]. It sits between the null arm and the range loop, so it never replaces the [null] param or a range bound. Pinned with { $empty: false, $gte: '5' }.
  • Two params when $empty and $null share a field. This decision is sound, and narrower than the $null-vs-$exists posture. The two keys are distinct params, so the URL carries both conditions and the list ANDs them. convertFiltersToAST on main pushes one condition per operator key in object key order, then wraps them as ['and', …] (filter-converter.ts :1071–1076). The URL's triples are that conjunction's body, and the pin checks ['and', ...triples] against the converter for three objects.
  • ⚠️ Non-blocking: the match is exact only up to conjunct order. All three pinned objects list the null key first. For { $empty: …, $null: … } the converter emits the is-empty triple first, while the URL always reads [null] first. AND is commutative, so the selected rows are the same, but the docblock's "exactly as convertFiltersToAST does" holds only when conjunct order is ignored.

The /data reader's [empty] suffix: correct.

  • parseUrlFilterTriples reads [empty]=true as [F, 'is_empty', true] and [empty]=false as [F, 'is_not_empty', true]. These are the exact triples convertFiltersToAST emits for $empty on main (objectui#11094, filter-converter.ts :880). The comparand is true in both, matching the [null] precedent.
  • Any other value is dropped: 1, 0, yes, TRUE, False, null, empty and the empty string are all pinned.
  • The sink lowers the triples back to $empty. This is pinned through mergeFilterNodes, isFilterAST and parseFilterAST.
  • Kept out of URL_FILTER_OPS. The match is a strict suffix === EMPTY_FILTER.param checked before the range lookup. URL_FILTER_OPS and RANGE_OP_PARAM are byte-unchanged, and a control pins the map at { gte, lte, gt, lt }.
  • "Save as view". TRIPLE_OP_TO_SPEC_ALIAS is the inverse of URL_FILTER_OPS and has no is_empty entry, so the fold passes the operator to normalizeFilterOperator unbridged. At 17.6.0, is_empty / is_not_empty are canonical VIEW_FILTER_OPERATORS words (ui/view.zod.ts :261), and unary operators accept value: true (the spec's own value-shape sweep). The fold pin reads from the URL and checks the record gate, in both directions. The emittable census grew from 7 to 9 entries. That strengthens it; nothing was relaxed.
  • Removal. The prefix delete already covers the new suffix. Pinned with [empty] beside [null].

Chips: correct. They reuse filterBuilder.operators.is_empty / is_not_empty, which are present under filterBuilder.operators in all ten packs (ar, de, en, es, fr, ja, ko, pt, ru, zh). The diff adds no new string and touches nothing under packages/i18n. The chip pin renders the chip through the real page with the real zh and en packs, and checks that the two directions differ from each other and from the is-null label. When both pairs sit on one field, the is-null label wins (pinned). That is the existing one-chip-per-field posture: the chip under-describes the field while the list applies both conditions. Non-blocking.

Docblocks, judged at 17.6.0: true.

  • The NULL_FILTER sentence now says the COMPOSED route's is-null condition "only ever arrives spelled $null". That is true, because is_null / is_not_null still lower to $null.
  • The added note is true: is_empty / is_not_empty lower to $empty from 17.6.0, and all four lowered to $null before it.
  • EMPTY_FILTER's gloss of $empty (declared-type judgment: '' on text-like fields, [] on multi-value fields) matches EMPTY_PREDICATE_DESCRIPTION.
  • The module header gains the [empty] row, and the row that was "emptiness" is renamed "is null".
  • ⚠️ Non-blocking: the serializer, deleteFieldFilterParams and groupFilterChips docblocks still call NULL_FILTER "the emptiness pair". That is now ambiguous beside EMPTY_FILTER.

Accepted set of the ADR-0055 /data surface: widens by exactly one suffix.

  • Before, the reader accepted gte, lte, gt, lt and null. It now also accepts empty, nothing else.
  • Every URL main already read keeps its meaning. On main, an [empty] param fell to the unknown-suffix drop.
  • ⛔ The equality route is untouched. The parseUrlEqualityFilterTriples docblock and body (32 lines) are byte-identical between main and the head, so its operator arm stays reserved (objectui#9196). A new control pins that filter[owner][empty]=true yields [] there.
  • The drillUrlFilters module is not re-exported, and app-shell's exports map is only . and ./styles.css. EMPTY_FILTER is therefore package-internal, and the only public widening is the URL surface.

Pins: red first, ablations, nothing weakened or skipped.

  • Red first. The dev reports 10 failed and 2 passed on 13f07dd0f. That matches the new file exactly: 12 cases, 2 of them [null] controls.
  • Ablations. I re-derived both counts case by case from the diff:
    • Serializer arm: 7 cases in drillUrlFilters.test.ts plus 10 in the 11547 file give 17. The two fold pins and the three chip-i18n pins read the URL directly and stay green. The report says 17.
    • Reader arm: 7, 9, 2 and 3 across the four files give 21. The is-empty-only case asserts only the search string and stays green. The report says 21.
  • Nothing weakened. The only removed test lines are two import lines and the old 7-entry emittable census, which was replaced by a strict superset. No .skip, .only or .todo, no xit, no eslint-disable, @ts-ignore, @ts-expect-error or as any in the diff.
  • Card obligations. Each one is pinned: both directions of the composed drill, the round trip to is_empty, "Save as view", the non-boolean drop, and the unchanged $null / $exists controls.

② Semver level

@object-ui/app-shell minor is right.

  • Precedent. .changeset/9159-drill-url-isnull-operator.md and .changeset/9508-drill-url-dialect-is-not-null.md both declare '@object-ui/app-shell': minor for a widening of this same dialect. This PR is the same shape: one more suffix, read on both sides, with a chip.
  • Not a patch. The 17.6.0 regression is the cause, but the fix grows the accepted set of an addressable public URL surface. A patch would understate that.
  • Not a major. No URL that main accepted changes meaning, and no export leaves the package.

The Clause-②: yes (widening) arm is correct and present in the PR body. The claim's Why yes names the same widening ([empty], as objectui#9159's [null]). There is one changeset, for one package, and the Changeset Bump Policy, Declaration and Fixed Group checks are green.

③ Boundary flags

  1. objectui#10813's pending slice must reconcile three things. Carrier: objectui#10813. Not blocking here.
    • The inspector sentence goes stale. NULL_FILTER's docblock still says the dataset filter inspector's "is not empty" row "writes exactly that pair" ($exists). That is true on main today and becomes false the moment objectui#10813 makes the inspector write { $empty: true or false }. The slice must:
      • remove or rewrite that sentence on NULL_FILTER;
      • point the inspector's rows at EMPTY_FILTER, which already carries $empty on the UNCOMPOSED route (ObjectMetricWidget's drawer into OpenInListButton);
      • keep the $exists read arm, which other authors may still feed verbatim.
    • Rebase without reverting. This PR rewrote the COMPOSED bullet of the same docblock, so objectui#10813 has to rebase onto this head's text, not reapply its old drill-sentence edits. The claim records that its rework drops those edits in favour of this card. Only the inspector sentence remains its own.
    • The drill URL changes for inspector widgets. The inspector's "is not empty" drill moves from filter[F][null]=false (IS NOT NULL) to filter[F][empty]=false (declared-type not-empty). That matches the widget's own count, but it changes which rows the list shows. objectui#10813 should pin the inspector-to-drill path end to end and say so in its changeset.
    • Landing order. objectui#10813 must land after this PR. Landed first, the inspector's $empty rows would drill to nothing, which is this card's superset again.
  2. Dev finding: the objectui#9508 it.each title prints the operator object in its second %s. Carrier: none. Not filed, cosmetic, and this card's own block was already fixed in 846ee2bf9. Agreed.
  3. New, the dev did not report it: a stale count in ObjectDataPage.tsx. The foldUrlFilterTriplesToSpecRules docblock still says "parseUrlFilterTriples only ever emits the five operators bridged above". That was stale from objectui#9159 (7 operators) and is now 9. The file is outside this PR, and the behaviour is pinned correct by the emittable census. Carrier: none, or whichever card next edits ObjectDataPage.tsx. Non-blocking.
  4. Conjunct order (from ①). It is not a defect. If a future pin compares the URL against the converter, it should either sort the conjuncts or also cover the $empty-first key order.
  5. NOT MEASURED, carried from the card and the report:
    • Stored customer dashboards with an empty operator. The authored reach is 0 across objectstack examples, hotcrm, cloud and objectui.
    • The whole app-shell test suite run locally (timed out at 560 s). It is declared to CI, and the CI test shards are among the 13 in_progress jobs above.
  6. Scope held. No edit to composeDrillFilter, the spec, the dataset inspector, the i18n packs or parseUrlEqualityFilterTriples. NOT GOVERNED by the queue guard, which is green.

Implemented-by: claude/issue-11547-drill-url-empty-17-6
Reviewed-by: session_016tKoy8NJa35Yih1FdzrVmn

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 3, 2026 13:28
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 3, 2026 13:28
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 3, 2026
Merged via the queue into main with commit 7121221 Oct 3, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-11547-drill-url-empty-17-6 branch October 3, 2026 13:52
hotlong pushed a commit that referenced this pull request Oct 3, 2026
objectui#11547 (PR #11548) landed the drill URL's [empty] arm. The merge
is clean; drillUrlFilters.ts auto-merged, and the merged tree equals
git merge-tree of the slice head and 7121221. The slice's two drill
sentences are reconciled with the new arm in the next commit, prose only.

Claude-Session: https://claude.ai/code/session_016tKoy8NJa35Yih1FdzrVmn
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

1 participant