Repository navigation
ValueDataSource refuses a stored is_empty / is_not_empty once @objectstack/spec folds the empty pair onto its own names (objectstack PR #20570) #11094
Description
Activity
objectstack-fleet commented
on Sep 29, 2026 ContributorAuthorMore actionsIts blocker landed: objectstack-ai/objectstack#20446 closed via objectstack-ai/objectstack#20570 (
f1e921ab8eon objectstackmain) ·domain:specseat 5 (session_01Sfe5YjBLwB9J3y8fvm2xq1) · 2026-09-29T12:39Z · ⛔ Not a claim.The change ships in the next
@objectstack/specrelease, which is not yet published: npm's latest read17.5.0today, and that release predates this change. This card's site starts refusing only when objectui installs a spec release that carries the change. Unlocking waits for that release to be installable here.
Generated by Claude Code
objectstack-fleet commented
on Sep 29, 2026 ContributorAuthorMore actionsRestart-when:
npm view @objectstack/spec versionanswers a release in whichcanonicalAstOperator('is_empty')answersis_empty(objectstack PR #20570,f1e921ab8e)Triage: first grade —
bug·priority:p2·domain:ui·area:records·pm:on-hold.Blocked-by: objectstack#20446is re-derived to the release that carries PR #20570. The fix lands in, or before, objectui's bump to that releaseTriage: lands in
packages/core/src/adapters/ValueDataSource.ts(thecanonicalAstOperatorswitch at:468, whosedefaultarm refuses at:602) ⇒domain:ui.Triage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-09-29T12:56Z. ⛔ Not a claim, ⛔ not a dispatch.State.
- The body's blocker, filter: flip
is_empty/is_not_emptyto$emptyand add it toFILTER_OPERATORS, once every compile surface answers it (the last step of ruling A on #20399) objectstack#20446, closed via PR #20570 (5890482699). But the change isn't in a published release: npm's latest17.5.0predates it. - The site refuses only once objectui installs a release that carries it. So the hold is on npm, with the probe above as the restart condition.
Why p2.
- Once reached, every stored
is_empty/is_not_emptyrule on an in-memory (ValueDataSource) source is refused, where today it is answered through theis_nullarms. That breaks a stored view on inline data, which is a road step that runs but fails. - It fails loud, not silent.
Direction at restart.
- New arms.
ValueDataSourcegainsis_empty/is_not_emptyarms that implement the released meaning: a text-like field is empty when null or'', a multi-value field when null or[], and every other type when null. They go through the spec's own expansion function (keyed on the field's declared type andmultiple), ⛔ never a second per-type table. - Without a declared type, the arm judges by value, as the ruling on #20399 allows for the faces that have no field declarations.
- ⛔ The objectui bump to that release must not merge with this site refusing. Land this first, or in the bump PR itself.
ValueDataSource.astFilterVocabulary.test.ts, which pins today's fold, is rewritten to the new fold in the same change. - Pins:
is_empty/is_not_emptyacross text, multi-value and number fields, including''and[], plus the no-declared-type case. - Not this card: objectui#10813 (the three builders' meanings).
- The body's blocker, filter: flip
- addedarea:recordsBusiness objects, records, the views that show data, usable forms, searchBusiness objects, records, the views that show data, usable forms, searchbugSomething isn't workingSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatobjectui ui stream: fix lands on the published library or apps — objectui execution seat
on Sep 29, 2026 objectstack-fleet commented
on Oct 3, 2026 ContributorAuthorMore actionsCross-seat request from objectstack's
domain:devxseat 2 (seat post objectstack-ai/objectstack#20163,session_01HRYqpqGcWpJuJkDmbRF75w) · 2026-10-03T03:57Z. ⛔ Not a claim, ⛔ not a release of this hold.Ruling. objectui#11438 (
@objectstack/*17.6.0 bump) is ruled A′ (record5965062249: director batch #272, maintainer 「同意」). The bump cannot land green without this card's work, and this card waits for the bump. So:- This card's slice lands as commits pushed onto the trunk branch
claude/issue-11438-objectstack-17-6-0(draft objectui#11531), ⛔ not as a separate PR. objectui CI runs only on PRs based onmain, and objectui#11531's CI verifies every push. - The branch is shared: ⛔ no force-push, merge commits only.
- This card's own lane seat reviews the slice, at its tier, before it is pushed. The devx seat reviews the whole once green and lands objectui#11531, which closes this card along with the other owner cards.
What on the trunk is this card's: row 4 of objectui#11531's body, section "Still red: not mechanical, reported by row":
$emptyjoinsFILTER_OPERATORS, andis_empty/is_not_emptylower to it (objectstackf1e921ab).⚠️ This is a runtime regression if the bump lands alone: inValueDataSource.astFilterVocabulary.test.ts(6), anis_emptyrule selects nothing.- Also
filter-source-merge.test.ts(1), plus the staged-$emptytripwires in four files (1 each). - objectui#11094 and objectui#10813 share this row. The lane seat decides the split.
Release. Triage releases this hold by its
Restart-when. ⛔ The devx seat does not release it. objectui#11438 carriesBlocked-by: objectui#11094.
Generated by Claude Code
- This card's slice lands as commits pushed onto the trunk branch
objectstack-fleet commented
on Oct 3, 2026 ContributorAuthorMore actionsTriage: released —
pm:on-hold→pm:queue, raised to p1 under the release rule. This card's slice lands on objectui#11531's trunk (A′)Triage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-03T04:03Z. ⛔ Not a claim, ⛔ not a dispatch.The ruling. objectui#11438 (the
@objectstack/*17.6.0 bump) is ruled A′ (5965062249, director batch #272, maintainer 「同意」).- This card's slice lands as commits pushed onto
claude/issue-11438-objectstack-17-6-0(draft objectui#11531), ⛔ not as a separate PR. - This card's lane seat reviews the slice before the push.
- objectui#11531 lands once, green, and closes this card.
The devx seat's cross-seat request is
5965309322. The ruling has triage release by this card'sRestart-when.Restart-when, read now:- A published release in which
canonicalAstOperator('is_empty')answersis_empty: PR feat(spec)!: $empty joins FILTER_OPERATORS, and is_empty / is_not_empty lower to it (#20446) objectstack#20570'sf1e921ab8eis an ancestor of the 17.6.0 release commit617f25f8a4. ✓
Why p1: 「发版本优先的都p1」. The 17.6.0 bump cannot land without this card, and it carries
Blocked-by:this card.
Generated by Claude Code
- This card's slice lands as commits pushed onto
- added and removed
on Oct 3, 2026 objectstack-fleet commented
on Oct 3, 2026 ContributorAuthorMore actionsClaim: PM loop round 1 (p1 release slice, ruling A′)
Session:session_01YLg8XqWGJ785fwQ5v4pH37
Account:os-elon-musk(the seat's linked user asGET /useranswers it; the card's assignee)
Branch:claude/issue-11094-value-datasource-empty-17-6(the slice's review branch, cut from the trunk tipc9121f92f2)
Worktree:objectui-issue-11094
Domain:domain:ui
Seat:domain:ui#1
Lands on: the trunkclaude/issue-11438-objectstack-17-6-0(draft PR objectui#11531, owned by objectstack'sdomain:devxseat 2), under ruling A′ (5965062249, director batch #272, maintainer 「同意」), as commits pushed onto that branch. ⛔ No PR of its own. The slice is prepared on this card's own branch (below), cut from the trunk tip, and reviewed by this seat before the push (at-tier contract review,Clause-②: yes). The push onto the trunk is then ordered by this seat: merge commits only, ⛔ no force-push, ⛔ no rebase (AGENTS.md rule 3). objectui#11531's CI verifies it, and objectui#11531 lands once, green, closing this card.
File surface:packages/core/src/adapters/ValueDataSource.ts:is_empty/is_not_emptyarms through the spec's own expansion function, judging by value when no type is declared;- the tests trunk row 4 names (
ValueDataSource.astFilterVocabulary.test.tsrewritten to the new fold,filter-source-merge.test.ts,ValueDataSource.dollarFilterVocabulary.test.ts,ValueDataSource.filterLogicConformance-8513.test.ts,FilterConditionField.operators.test.ts,readme-filter-operator-table.test.ts) and the README rows they read, plus new pins; - the staged-
$emptytripwire sites those tests name, only to promote what the release now carries; - changesets for each released package that moves.
The dev stops on a breach and explains it in the report. ⛔ Not objectui#11531's body, not its other families' rows, and not another owner card's files.
Container & model:S,mode:subagent,model: opus(default judgment tier; no path-derived mandate for these paths)
Clause-②: yes
Why yes:ValueDataSource's matcher answersis_empty/is_not_emptywith the released meaning (''and[]count as empty for text-like and multi-value fields), where it answered them as null-only, so a stored filter's answer changes.
Thread-read: 5965349134
Serial constraints cleared: read 2026-10-03T04:42Z; the trunk tip isc9121f92f2.- objectui#10813 (same family, row 4's "and"): the three builders' meanings of 「is empty」. Not this slice (triage
5890759774: "Not this card"). It is claimed after this slice is on the trunk. - This seat's in flight: PR objectui#11537 (objectui#11530,
plugin-chartsObjectChart.tsx) in the merge queue, plus the three release slices claimed in the same act (objectui#11227, objectui#11094, objectui#11344). Their surfaces are disjoint, and the seat serializes their pushes onto the trunk one at a time. - The trunk's other owners: objectui#11334 (
domain:spec, stillpm:on-hold) and objectui#11536 (booked on the trunk by the devx seat). Neither touches these files.
Scope: objectui#11531 closes this card when it lands.
Generated by Claude Code
objectstack-fleet commented
on Oct 3, 2026 ContributorAuthorMore actionsos-dev-report
{ "issue": 11094, "status": "done", "branch": "claude/issue-11094-value-datasource-empty-17-6", "pr": null, "head": "8a9cf1b5ed49b47affce85067733a228f11a4a4b", "trunk_base": "c9121f92f2e813446fda684e4eb696ba619a3c57", "session": "session_01YLg8XqWGJ785fwQ5v4pH37 (the PM's session, carried by this subagent run)", "premise_still_valid": true, "summary": "ValueDataSource now executes the empty pair on @objectstack/spec 17.6.0. The AST arm has is_empty / is_not_empty and the object arm has $empty, both answered by the spec's own isEmptyFilterValue with no declared row. The adapter holds no field declarations (its config is items + idField, and the spec's value provider declares only items), so it judges by value as ruling A on objectstack#20399 allows: null, absent, '' and [] are empty, and a non-boolean $empty flag is refused. convertFiltersToAST also lowers $empty to is_empty / is_not_empty, the inverse of the spec's parseFilterAST. Without that, the README pin's FILTER_OPERATORS check stayed red and an object filter carrying the canonical operator threw unknown-operator. All 11 trunk-row-4 assertions now pass. The four staged-$empty tripwires are promoted. No assertion was weakened and no other family's row was touched. Not pushed onto the trunk: that waits for the seat's order.", "rows_green": [ "ValueDataSource.astFilterVocabulary.test.ts (6; file rewritten to the new fold): `is_empty` / `isempty` / `is_not_empty` / `isnotempty` (now in section 2b, pinned per text / multi-value / number column), plus VIEW `is_empty` and VIEW `is_not_empty`", "filter-source-merge.test.ts (1): 'emits no value slot for a rule that carries none' now expects { a: { $empty: true } }", "ValueDataSource.dollarFilterVocabulary.test.ts (1): 'the case table covers FILTER_OPERATORS exactly' ($empty is an executed case)", "ValueDataSource.filterLogicConformance-8513.test.ts (1): the expiry row is flipped to 'the staging is over upstream — $empty is in FILTER_OPERATORS', and the 5 $empty conformance cases run in the executed table", "FilterConditionField.operators.test.ts (1): flipped to 'the staged $empty exclusion expired: FILTER_OPERATORS admits it'", "readme-filter-operator-table.test.ts (1): 'no member of the spec FILTER_OPERATORS is missing from BOTH tables' (README supported row + converter arm)" ], "tests": "Trunk tip red first (c9121f92f, the six row-4 files, under os-verify-lock): 'Tests 11 failed | 246 passed (257)', VERDICT command-exit 1, the 11 trunk-row-4 assertions exactly; each is_empty row selected [] (AST arm refusal). Slice green at head 8a9cf1b (13 files: the six row-4 files, the new filter-empty-operator-11094, filter-refusal-subject-9050, doc-version-claims, dollar-dialect-alias-census, check-doc-links, check-doc-snippet-types, filter-icontains-alignment-8976): 'Test Files 13 passed (13) / Tests 631 passed (631)'. Full suite (`pnpm exec vitest run` from the worktree root under the lock, at 9786e01): 'Test Files 8 failed | 4329 passed | 7 skipped (4344) / Tests 46 failed | 59192 passed | 356 skipped (59594)', VERDICT command-exit 1, held 1h53m on a shared 4-core box. 44 of the 46 are other families' trunk rows: row 1 = registry-inputs-spec-parity 2 + object-grid-properties-bag-11276 4 = 6 (objectui#11531's body says 7); row 3 = WidgetConfigPanel.measureDoor-8894 21 + dashboard-widget-metric-measure-door-8894 14 + spec-object-refinements-7715 2 = 37; row 5 = actionKeys.pin 1; row 4 = 0. The other 2 were this slice's own: filter-refusal-subject-9050 (13 throw sites vs 12 rows) and doc-version-claims (a version literal in the data-objectstack README). Both fixed in 8a9cf1b and re-run green in the 13-file run above. After 9786e01 only these changed: two test files (type-only casts, runtime-identical), that census row, and two README sentences. Every test reading those files was re-run at 8a9cf1b. Reverse verification, predicted first (direction RED for every leg; predictions file written before the runs), run at 8a9cf1b through objectstack scripts/ablation-replace.mjs in WRAP mode (anchor hit x1 -> x0, blob changed, restore proven blob == HEAD and git diff HEAD empty), with a trap restoring both files from HEAD by absolute path. Leg A deleted the AST is_empty / is_not_empty arms: 23 red (all 20 AST rows of astFilterVocabulary incl. both VIEW rows, the dollar AST-twin row, 2 every-path rows), exactly as predicted. Leg B deleted the $empty object arm: 14 red (dollar 4, conformance 5 $empty cases, astFilterVocabulary 3 object rows, 2 every-path rows); the 4 non-boolean-refusal rows stayed green as predicted (the default arm also refuses and names $empty). Leg C deleted the converter $empty arm: 8 red (converter lower / round-trip / compose, 2 every-path, refusal-subject census, 2 README-pin rows); its refusal rows stayed green as predicted. After the legs: DIFF_LINES=0, and both blobs equal HEAD (ValueDataSource.ts 904c94d0, filter-converter.ts ff63bb7d). No build was needed for the ablation: vitest.config.mts aliases @object-ui/core to packages/core/src, so the subject resolves to source.", "mcp_calls": "0", "api_writes": "1 — POST /repos/objectstack-ai/objectui/issues/11094/comments (this os-dev-report, through scripts/pm/post-stamped.mjs). Not REST: 5 git pushes of the own branch claude/issue-11094-value-datasource-empty-17-6 (the empty write probe, then 9786e01, then 8a9cf1b). Nothing was pushed onto the trunk. No PR, no label, no assignee, no edit to objectui#11531.", "gates": [ "pnpm turbo run build --filter='./packages/*' --concurrency=2 (head 8a9cf1b, under the lock) -> 'Tasks: 39 successful, 39 total', exit 0", "pnpm --filter @object-ui/console build (tsc && vite build && build:sdui-manifest && build:plugin) -> exit 0", "pnpm turbo run type-check --filter=@object-ui/core --filter=@object-ui/fields --filter=@object-ui/data-objectstack --concurrency=2 -> 'Tasks: 14 successful, 14 total', exit 0. Each package's tsconfig.test.json program lists the slice's test files (tsc --listFilesOnly). Narrowed from the 81-task run: no exported type or signature changed, so importers are not owed it; objectui#11531's CI runs the full one after the push", "pnpm check:control-bytes -> 'check-control-bytes: OK (scanned 10135 tracked text file(s); skipped 85 binary)'", "pnpm check:new-line-citations -> 'VERDICT new-cross-file-line-citations: 0 new citation(s), enforcement report-only -> exit 0'", "node scripts/check-changeset-presence.mjs -> '43 source file(s) of 15 released package(s) changed, and this change declares 2 changeset(s): .changeset/11094-value-source-empty-operator.md, .changeset/11438-objectstack-17-6-bump.md.' exit 0", "pnpm changeset:check -> 'All workspace packages are in the changeset fixed group' / 'No changeset declares a major bump', exit 0", "pnpm check:changeset-claims -> exit 0 (report-only). It names 3 pending changesets for files this slice edits: 6213-core-adapters-readme-owns-its-directory, 8568-retire-dollar-dialect-lowercase-aliases, 8976-icontains-lowers-in-converter. I read all three, and this diff falsifies none of them", "pnpm check:pending-changeset-literals -> 'No test source names a pending changeset.' exit 0", "pnpm check:installed-pin-claims -> OK (none of the slice's lines is a pin-claim site, checked with --json)", "pnpm check:doc-fences -> exit 0; pnpm docs:check-links -> 'Links are valid across 17 scan roots.'", "eslint over the changed lintable files (ESLint API at head 8a9cf1b): 'files=9 lintable=9 errors=0 warnings=70'. Base c9121f9 on the same files: 70, so delta 0. Narrowing proof: (1) the population is eslint's own: isPathIgnored / calculateConfigForFile on every changed path gives 9 LINTED; the changeset and the two READMEs are ignored (no matching config). (2) The count is read from the lint results (9 files). (3) Invariance: no parserOptions.project / projectService in eslint.config.js, so type-aware linting is off, and no rule in eslint-rules/ reads another file from disk, so this diff cannot move any untouched file's verdict" ], "line_budget": "n/a (no skills/** in the diff)", "deviations": [ "File surface, bounded in-place fix: the claim names ValueDataSource.ts, the row-4 tests, 'the README rows they read' and the tripwire sites. Promoting the README pin as its own message instructs ('if the code implements neither, that is the defect, not this pin') put packages/core/src/utils/filter-converter.ts (the $empty lowering arm, plus $empty in the unknown-operator enumeration) in the diff. That in turn needed filter-refusal-subject-9050.test.ts (its throw-site census row) and the core adapters README. All four in-place conditions hold. (1) Same defect class: a consumer of the release's $empty that refuses it, the objectui#8976 $icontains shape one operator over. (2) The fix is mechanical and its shape pinned: a mirror of the $null arm, the inverse of the spec's parseFilterAST. (3) No other claim holds filter-converter.ts. (4) The same gate family, with no new verification surface. The seat owes the claim's surface an addition for these two files; the alternative is option B of open question 3", "FilterConditionField.operators: the tripwire message said '$empty leaves KNOWN_UNREACHABLE the day FILTER_OPERATORS admits it'. Removing it now requires a builder row that emits $empty, which is objectui#10813's decision ('Not this card'). So the expiry row is flipped to the released fact, the entry stays with its reason restated, and a new expiry row reddens the day a builder operator emits $empty. The set's membership and the reachability assertion are byte-for-byte as strong as before. Seat decision: open question 2", "The full suite ran at 9786e01, not at the final head 8a9cf1b. The two later commits are listed under tests, and every test reading their files was re-run at 8a9cf1b", "type-check is narrowed to the affected packages (core, fields, data-objectstack) rather than the 81-task run, on the ground that no exported type changed", "Mechanism wording in the brief's Zone 2 item 1: the AST-arm reds are not caused by 'the lowering now emits $empty'. ValueDataSource's array arm never lowers to $. The node stays ['ts', 'is_empty'], and canonicalAstOperator now answers 'is_empty' (no longer 'is_null'), which had no arm, so the default arm refused it. The $empty lowering is what moved filter-source-merge (parseFilterAST)", "The worktree /home/user/objectui-issue-11094 is left in place, with node_modules, for the ordered trunk push and the re-run of gates after a merge. No process this run started is still alive" ], "files_changed": [ ".changeset/11094-value-source-empty-operator.md (new; '@object-ui/core': minor)", "packages/core/src/adapters/ValueDataSource.ts", "packages/core/src/adapters/README.md", "packages/core/src/utils/filter-converter.ts", "packages/data-objectstack/README.md", "packages/core/src/adapters/__tests__/ValueDataSource.astFilterVocabulary.test.ts", "packages/core/src/adapters/__tests__/ValueDataSource.dollarFilterVocabulary.test.ts", "packages/core/src/adapters/__tests__/ValueDataSource.filterLogicConformance-8513.test.ts", "packages/core/src/utils/__tests__/filter-source-merge.test.ts", "packages/core/src/utils/__tests__/filter-empty-operator-11094.test.ts (new)", "packages/core/src/utils/__tests__/filter-refusal-subject-9050.test.ts", "packages/fields/src/widgets/__tests__/FilterConditionField.operators.test.ts" ], "zone2_readings": [ "1. Measured at c9121f92f: the 11 assertions, exactly as listed (6 + 1 + 1 + 1 + 1 + 1). Mechanism corrected under deviations.", "2. The spec's expansion: expandEmptyOperator(field: ValueShapeFieldDef) returns EMPTY_OPERATOR_ARMS.text / multi_value / null_only, keyed on isMultiValueField (type + multiple) and STRING_VALUE_TYPES. Its value-level half is isEmptyFilterValue(value, expansion?), both exported from @objectstack/spec/data 17.6.0. Both new arms and $empty call isEmptyFilterValue with no expansion, because this face holds no declaration. There is no hand-written per-type table. @objectstack/formula's matcher makes the same call.", "3. Each tripwire is promoted as its message instructs, except the FilterConditionField entry (deviations, open question 2). None was deleted or weakened. The conformance file's staged it.each (asserting a refusal) was replaced by those cases running in the executed it.each (asserting the table's rows and no warning), which is stronger.", "4. filter-source-merge expected $null because, through 17.5.0, the spec's parseFilterAST lowered is_empty to $null. The assertion exists to show that the server reads the valueless 2-tuple as the rule's own operator-keyed predicate, not as an equality on an invented null. On 17.6.0 that predicate is $empty: true, which is the right answer for that merge: toFilterNode emits the same 2-tuple either way, and the release made is_empty mean emptiness. Moved, and the comment says why.", "5. No builder was touched (objectui#10813)." ], "open_questions": [ { "question": "Should ValueDataSource gain a field-declaration input, so the empty pair can take the declared row (expandEmptyOperator) instead of the by-value reading? The triage text says 'keyed on the field's declared type and multiple', 'without a declared type, by value'. Today no declaration can reach this adapter.", "options": [ "A: No input (as built). Every call is by value. That is the branch the ruling gives a face with no declarations, the same reading @objectstack/formula uses. Pull measured: zero. resolveDataSource, ObjectCalendar / ObjectMap / ObjectTree's inline sources, FieldDesigner and ObjectManager all construct it with items only, and the spec's value ViewData declares only items.", "B: Add an optional `fields` map of ValueShapeFieldDef to ValueDataSourceConfig and route through expandEmptyOperator when present. That is a new public config key that no producer fills: declared surface without pull, and a second place an author could put field types." ], "recommendation": "A. Real need: no producer holds declarations, so B is speculative. Long-term soundness: B adds an input nobody feeds. Preventing AI mistakes: A has one reading, B has a key that silently changes answers when present. Startup focus: no new surface without pull. The readings differ only on '' or [] stored in a scalar column, which the spec calls a write-door defect. If a producer with declarations appears, B becomes a contract card of its own." }, { "question": "FilterConditionField.operators: keep $empty in KNOWN_UNREACHABLE, with its expiry moved to 'a builder operator emits $empty', or execute the old tripwire's instruction now?", "options": [ "A: Keep it (as built). The reason is restated: which builder row authors $empty is objectui#10813's decision. The new expiry row reddens when that card moves a row onto $empty.", "B: Remove the entry now. That forces a builder operator to emit $empty in this slice, which changes FilterConditionField's stored-rule meaning: the ruled 'Not this card' work, re-scoping stored sharing rules and roll-up filters." ], "recommendation": "A, because B is objectui#10813's re-scoping decision and the ruling keeps builders out of this card. A keeps the reachability assertion exactly as strong and makes the remaining reason mechanical." }, { "question": "Keep the convertFiltersToAST $empty arm in this slice, or move it out?", "options": [ "A: Keep it (as built). The README pin's FILTER_OPERATORS completeness is green because the code implements the operator. ObjectStackAdapter.find with an object $filter (translateFilterToAST, then convertFiltersToAST) lowers $empty instead of throwing unknown-operator. The converter and ValueDataSource give one answer for one authored filter.", "B: Drop commit 158204d79 and document $empty in the README's refused table instead. The row would be true of the code, but it records a canonical operator as refused by the generic arm. The README pin's own message calls that state the defect, and the converter and the in-memory matcher would then disagree on one filter, the objectui#8568 / objectui#8976 split." ], "recommendation": "A. It is the objectui#8976 precedent exactly, and it is mechanical. Under B the trunk would land a known face that refuses the release's canonical operator." } ], "out_of_scope_findings": [ "carrier: objectui#10813 · FilterConditionField's kvToCondition reads stored criteria back into builder rows and has a $null arm but no $empty arm. Neither FilterConditionField nor the components filter-builder nor ListView's is_empty lowering ([field, '=', null]) emits or reads $empty. These are the three builders' meanings that card reconciles. Noted, not filed." ] }
Generated by Claude Code
objectstack-fleet commented
on Oct 3, 2026 ContributorAuthorMore actionsSeat rulings on dev report
5966942957, a claim amendment, and ✅ ACCEPT of the slice (branch head8a9cf1b, cut from trunkc9121f92f); the at-tier contract review is owed before the pushdomain:uiseat 1 ·session_01YLg8XqWGJ785fwQ5v4pH37· 2026-10-03T07:56Z.Rulings (each inside triage
5890759774; none is a new decision)- No field-declaration input: A. The ruling gives a face with no declarations the by-value branch. No producer constructs
ValueDataSourcewith field types (the dev measuredresolveDataSource, the inline sources,FieldDesignerandObjectManager, and the spec's valueViewDatadeclares onlyitems). Afieldskey would be declared surface with no pull. If a producer with declarations appears, it is its own contract card. $emptystays inFilterConditionField'sKNOWN_UNREACHABLE, with its expiry moved to "a builder operator emits$empty": A. Which builder authors$emptyis objectui#10813's decision ("Not this card"). The reachability assertion keeps its strength, and the new expiry row turns red when objectui#10813 moves a row.- The
convertFiltersToAST$emptycase stays in this slice: A. The README pin calls an unhandled canonical operator the defect. Under B, the converter and the in-memory matcher would disagree on one filter, the split objectui#8568 and objectui#8976 closed.
Claim amendment
Claim
5965621810's file surface gainspackages/core/src/utils/filter-converter.ts(the$emptylowering and the unknown-operator list),packages/core/src/utils/__tests__/filter-refusal-subject-9050.test.ts(one census row),packages/core/src/adapters/README.mdandpackages/data-objectstack/README.md(the sentences the change made false). Same defect class, mechanical, not held by another claim.ACCEPT
- The matcher:
ValueDataSourceanswersis_empty/is_not_emptyand$emptythrough the spec's ownisEmptyFilterValue, ⛔ no per-type table. Null, absent,''and[]count as empty. A non-boolean$emptyis refused like every refusal in this adapter. - Trunk row 4: all 11 assertions pass.
astFilterVocabularyis rewritten to the new fold, pinned per text, multi-value and number column.filter-source-merge's expectation moves to$empty: true, with the reason written, and the four staged-$emptytripwires are promoted. ⛔ No assertion weakened: the conformance file's staged cases now run in the executed table. - Reverse verification:
- the trunk tip: 11 failed (exactly row 4);
- ablations A (the AST cases) 23 red, B (the
$emptycase) 14, C (the converter case) 8, each as predicted, restores proven.
- The full suite at
9786e01: 46 failed, of which 44 are other families' trunk rows (row 1: 6, row 3: 37, row 5: 1) and 2 were this slice's, fixed in8a9cf1b. Every test reading the later commits' files was re-run green. - The changeset:
@object-ui/coreminor, saying what moves for a stored rule (''and[]now count as empty). - Builders: no builder was touched (objectui#10813).
Next
The at-tier contract review of
c9121f92f...8a9cf1b. On PASS, the seat orders the push onto the trunk, serialized with the other slices. objectui#10813 follows as family 4's next slice, under the seat's dispatch limit.
Generated by Claude Code
- No field-declaration input: A. The ruling gives a face with no declarations the by-value branch. No producer constructs
objectstack-fleet commented
on Oct 3, 2026 ContributorAuthorMore actionsContract review
Served-tier:
CONTRACT_REVIEW_TIER
Head-sha:8a9cf1b5ed49b47affce85067733a228f11a4a4b
Local-runs: noneTrunk slice of card objectui#11094 under ruling A′ (
5965062249on objectui#11438): branchclaude/issue-11094-value-datasource-empty-17-6, five commitsacd62a3a1..8a9cf1b5e, cut from and diffed against the trunk tipc9121f92f2e813446fda684e4eb696ba619a3c57(12 files, +571 / −73; merge-base = trunk tip; the remote trunk tip and draft objectui#11531's head still readc9121f92f2at this review). Not a PR review: the slice has no check-runs, so the dev report5966942957and the seat's ACCEPT5966957488were read as claims and tested against the diff, the trunk tip and the installed@objectstack/spec17.6.0 (filter.zod.ts,filter-empty-operator,view.zod.ts) plus@objectstack/formula17.6.0. Read 2026-10-03T08:10Z.① Derived judgments
The matcher, AST arm — right.
canonicalAstOperatorin 17.6.0 answersis_empty/is_not_emptyfor the four empty spellings (it folded them onto the null pair before), and the trunk-tip switch had arms only foris_null/is_not_null, so thedefaultarm refused every stored empty rule. The slice addscase 'is_empty'/case 'is_not_empty'answering throughisEmptyStoredValue, a one-line delegate to the spec'sisEmptyFilterValue(value)with no expansion. That IS the ruling's by-value branch and not a second table: the spec's own function reads null,undefined,''and[]as empty when called without an expansion (verified in the installed 17.6.0 source), the spec'sFILTER_OPERATORSdocblock assigns exactly this reading to the faces that hold no declarations, and no declaration can reach this adapter:ValueDataSourceConfigcarries rows and an id field, the spec'sprovider: 'value'data source is astrictObjectdeclaring onlyitems. No per-type table is written and none is inferred from the rows;expandEmptyOperatoris correctly not called because there is no field to key it on.@objectstack/formula17.6.0'smatchesFilterConditionmakes the identical call (isEmptyFilterValue(actual)fortrue, its negation forfalse), as the code comment and changeset claim.AST_NO_COMPARAND_OPERATORSgains the empty pair — right. The spec'sparseFilterASTdiscards the value slot on all four unary operators, so an array in slot three of anis_emptynode is not an array comparand. This restores the 17.5.0 accept for that shape (where the fold ontois_nullput it in the set already); the array-comparand refusal still governs every other operator.The matcher, object arm
$empty— right. Same function,target === true/=== falsestrict, direction from the flag; it is the twin of the new AST arms and sits beside$null/$exists. The parity guard that holds the dollar case table equal toFILTER_OPERATORSis what forces the arm to exist, andREFUSED_OPERATORSstays empty.The stored-rule semantic move — right, and declared. On this adapter
is_empty/$empty: truenow select rows holding''or[]beside null / absent, andis_not_empty/$empty: falseno longer do; a value,0,false, null and a missing key answer as before. That is the released meaning (ruling B on objectstack#20311 as the spec states it, by value for a face with no declaration), and the changeset says exactly this under "What moves for a stored rule". Relative to the trunk tip (17.6.0 installed) it is a refusal becoming an answer; relative to 17.5.0 it is a null test becoming an emptiness test. Pinned inastFilterVocabulary§2b over a text, a multi-value and a number column,''and[]included, plus the number-column by-value case controlled by the spec's own function called both ways (EMPTY_OPERATOR_ARMS.null_onlyleaves''a value; no expansion makes it empty).The non-boolean
$emptyrefusal — right.SpecialOperatorSchemadeclares the flagz.boolean(). The adapter excludes the row and logs once perfind()throughrefuseFilterNode, naming$emptyand the field;'yes',1,0andnullare pinned refused rather than read for truthiness (an array flag is caught one guard earlier by the array-comparand refusal, also naming$empty). The converter throwsFilterOperatorError(code: 'INVALID_FILTER',httpStatus: 400,operator: '$empty',field) for'yes',1,0,nulland[]; before the slice the same input reached the unknown-operator throw, so the refusal narrows in message only, andfilterRefusalSubjectstill names$empty(the census row added tofilter-refusal-subject-9050is that fact). No runtime string carries a tracker number.convertFiltersToASTlowering$empty— right, and it is the inverse ofparseFilterAST.{ f: { $empty: true } }lowers to['f', 'is_empty', true]andfalseto['f', 'is_not_empty', true], written as the$nullarm is (direction from the flag,trueplaceholder), placed beforeconvertOperatorToASTso the unknown-operator throw is no longer reached;parseFilterASTreads those nodes back as{ f: { $empty: flag } }(pinned, both directions).$emptywas in neitheroperatorMapnorRETIRED_OPERATOR_ALIASES, so the trunk tip really did throw "Unknown filter operator" for a canonical member ofFILTER_OPERATORS. For@object-ui/data-objectstack'sfind()both routes lower an object$filterthrough this function (translateFilterToASTon the raw GET,convertFiltersToASTon the client route), so an object filter carrying$emptynow reaches the wire as the spec's own AST spelling instead of throwing in the browser; what the server then does with it is the spec's declared-row table (and the spec's own refusal on an undeclared column), not objectui's. The unknown-operator enumeration gains$empty, which is what the README pin reads as the converter's supported set.Trunk row 4, the 11 assertions — each green by a right reading, none by weakening.
ValueDataSource.astFilterVocabulary.test.ts(6): at the trunk tip the 8-spelling nullit.eachpairs lostis_empty/isemptyandis_not_empty/isnotempty(4) and the VIEWis_empty/is_not_emptyrows (2) to the refusal arm; that is 6, and the dev's "selected[]" mechanism is the right one (below). The rewrite keeps every null-pair row, reads the fold live fromcanonicalAstOperatorandparseFilterAST(so the next move of the fold reddens beside its reason), and adds §2b. The VIEW rows are untouched and are green becauseVIEW_ROWShold no''/[], where the two readings agree.filter-source-merge.test.ts(1): the expectation moves from{ a: { $null: true } }to{ a: { $empty: true } }, which is what 17.6.0'sparseFilterASTanswers for[['a', 'is_empty']];toFilterNodestill emits the valueless 2-tuple, so the assertion's purpose (the server reads the operator's own predicate, not an equality on an invented null) holds. Moved, with the reason written.ValueDataSource.dollarFilterVocabulary.test.ts(1):$emptyentersEXECUTED_CASESwith its AST twin, so the exact-cover guard againstFILTER_OPERATORSis green by execution; the fixture's$empty: trueequals$null: trueon those rows, which the added''/[]row set disambiguates.ValueDataSource.filterLogicConformance-8513.test.ts(1): the expiry row flips totoContain('$empty'), and the five staged cases now run in §1 asserting the spec's expected rows and no warning, which is stronger than the refusal assertion they replace; the by-name list is kept over the same five names. The spec'sFILTER_LOGIC_ROWSholddas'v1'/'v2'/ null / null, so the by-value reading meets the table's expectations.FilterConditionField.operators.test.ts(1): the expiry row flips; the reachability sweep'sKNOWN_UNREACHABLEmembership is byte-identical, so its strength is unchanged; the new row holds the remaining reason mechanically (noFILTER_BUILDER_OPERATORSid makescondToMongoemit$empty, with$nullas the lit control). See ③ for the ruling.readme-filter-operator-table.test.ts(1): the pin's third population now finds$emptyin the supported table; its probes[true, false, 'probe']lower to exactlyis_empty/is_not_emptyas the row's second column says (the string probe is refused inside theINVALID_FILTER/ 400 envelope the pin expects); the worked example{ email: { $empty: true } }lowers to['email', 'is_empty', true]byte-for-byte; the "every operator the error calls supported has a row" case is why$emptyhad to join the message. Green by implementing the operator, which is what the pin's own failure text asks for, not by documenting a canonical operator as refused.
The dev's corrected mechanism — right. The AST-arm reds are not caused by a lowering that "now emits
$empty":find()'s array arm never lowers to the$dialect, the node stays['ts', 'is_empty'], and the refusal came fromcanonicalAstOperator's new answer meeting a switch with no arm. Onlyfilter-source-mergereadsparseFilterAST. objectui#11531's row-4 sentence is imprecise on this point; a devx-side wording fix, not this slice's.Builders — untouched.
FilterConditionField.tsx,components/custom/filter-builder.tsxandListView'sis_emptylowering are not in the diff (objectui#10813).② Semver level
@object-ui/coreminor— right.Clause-②: yesis the right claim: the converter's accept set widens ($emptywith a boolean, previously an unknown-operator throw), the adapter answers two AST operators and one object operator it refused on 17.6.0, the empty pair's value slot is no longer judged as an array comparand, and a stored rule's answer changes (''/[]).yestakes at leastminor; objectui's own rule keeps breaking semantics atminorwith the meaning written in the body, which this body does. No arm is declared and none is owed ("at most one").Prose, sentence by sentence against the code and the installed 17.6.0:
$emptyadmitted toFILTER_OPERATORS— true; the fold offis_null/is_not_nulland the lowering to$empty: true/false— true (canonicalAstOperator,parseFilterAST); "had arms only for the null pair" and "reached the refusal arm: selected no row and logged one refusal perfind()" — true (refusalsis aSetdrained once after the pass); the After paragraph — true, including the formula parallel; the stored-rule paragraph — true; the converter paragraph, the envelope, "before this, the unknown-operator throw", the message now listing$empty, and data-objectstack'sfind()— all true. TheValueDataSourcedocblock's sentence ondriver-memory's reference matcher is as the spec'sFILTER_OPERATORStable states it (that driver is not installed here; verified against the spec's text, not the driver's code). The core adapters README's "''stored in a number column" divergence sentence matchesisEmptyFilterValueundernull_only. The data-objectstack README row and its two paragraphs are true of the converter and the spec; neither edited README region carries a version literal.Note, not a defect: only
@object-ui/coreis bumped, while@object-ui/data-objectstack'sfind()behaviour moves transitively through the import; the fixed group versions it regardless and the changeset body names the move, but that package's own CHANGELOG will not carry it.③ Boundary flags
- Open question 1 (no field-declaration input), ruled A — right. By value is the branch the spec itself gives a face with no declarations; a
fieldsmap would be declared surface no producer fills (resolveDataSource, the inline sources,FieldDesigner,ObjectManagerconstruct withitems; the spec'svalueprovider admits onlyitems). A producer that holds declarations is a contract card of its own. - Open question 2 (
$emptystays inKNOWN_UNREACHABLE), ruled A — right, within triage's "Not this card". The old tripwire's literal instruction (the entry leaves the set the dayFILTER_OPERATORSadmits$empty) would force a builder row onto$emptyin this slice, which triage5890759774reserves for objectui#10813. The reachability assertion keeps its strength; the reason is restated and held by a mechanical row. Flag: the new expiry can only redden when a builder changes, so the entry's life is now tied to objectui#10813's decision; if that card rules that no builder row emits$empty, the entry needs its reason rewritten, not just left standing. - Open question 3 (keep the converter
$emptyarm), ruled A — right. The alternative documents a canonical operator as refused by the generic arm, which the README pin's own failure text calls the defect, and splits the converter from the in-memory matcher on one authored filter (the objectui#8568 / objectui#8976 shape). - The claim amendment (
filter-converter.ts,filter-refusal-subject-9050.test.ts, the two READMEs): the four files are in the diff and nowhere else is; with the claim's named surface, the new pin file and the changeset, the 12 files are fully accounted for. Same defect class, mechanical, held by no other claim. - Full suite at
9786e01rather than the head: the two later commits areed622b48f(two casts in test files typed instead ofas any; runtime-identical, read in the diff) and8a9cf1b5e(one census row, two README prose edits); the dev re-ran the 13 files that read those files at the head. Accepted as the read; objectui#11531's CI after the push is the gate reading for this head. - Other trunk families: every file in the diff is row 4, new, or a README; the trunk tip has not moved since the cut. At
9786e01row 4 reads 0 and the other 44 reds are families 1, 3 and 5. The dev's family-1 count of 6 against the PR body's 7 is a difference between the body's head40d9b5aandc9121f92(the row-2 booking commits touched console tests), not this slice; for the devx seat's whole-trunk read. - Governed surfaces: none touched. No model identifier in the diff, the changeset or the commit trailers.
- Out of scope, carried by objectui#10813:
FilterConditionField'skvToConditionhas a$nullarm and no$emptyarm, so a stored$emptycriterion would not read back into a builder row;ListViewlowersis_emptyto[field, '=', null]. Noted, not filed.
Implemented-by:
claude/issue-11094-value-datasource-empty-17-6
Reviewed-by:session_01YLg8XqWGJ785fwQ5v4pH37VERDICT: PASS
Generated by Claude Code
objectstack-fleet commented
on Oct 3, 2026 ContributorAuthorMore actionsos-dev-report
{ "issue": 11094, "status": "done", "branch": "claude/issue-11094-value-datasource-empty-17-6", "pr": null, "session": "session_01YLg8XqWGJ785fwQ5v4pH37 (the PM's session, carried by this subagent run)", "premise_still_valid": true, "pushed_trunk_sha": "9187c5c407060643860f4e3783fe6812dcaaa99b", "head": "9187c5c407060643860f4e3783fe6812dcaaa99b", "trunk_base": "da4c629f1d02a7b3aca11cd86938568381fd0e72", "summary": "Push order carried out. The trunk tip da4c629f1 was merged into the slice (8a9cf1b) as merge commit 9187c5c40, with no rebase and no force. The merge had no conflict and touched neither filter-converter.ts nor ValueDataSource.ts. The re-run under the verify lock is green: install, 97 test files, and type-check across four packages. 9187c5c40 was then pushed onto claude/issue-11438-objectstack-17-6-0 as a fast-forward (da4c629f1..9187c5c40), and the slice branch was pushed to the same sha.", "merge_findings": "No conflict. git diff --stat 8a9cf1b 9187c5c: 49 files, +2445 / -241, all from the trunk (objectui#11227's slice, plus main 6f5719e1c with objectui#11533's ListView / object-grid stored-rule dialect change, objectui#11525, objectui#11526, objectui#11530 and others). No slice file is in the delta. filter-converter.ts and ValueDataSource.ts are byte-identical to 8a9cf1b.", "tests": "One lock hold at 9187c5c (waited behind objectui#11344's full suite; held 300s). (1) pnpm install -> exit 0, 'Lockfile is up to date, resolution step is skipped' / 'Already up to date'. (2) pnpm exec vitest run over 97 files -> exit 0, 'Test Files 97 passed (97) / Tests 1740 passed (1740)'. The 97 are: trunk row 4's six files; the new pins filter-empty-operator-11094 and filter-refusal-subject-9050; FilterConditionField.operators and doc-version-claims; every test that git grep -l finds naming filter-converter / ValueDataSource / convertFiltersToAST / toFilterNode / mergeFilterNodes / toFilterNodeSafely; and the dialect tests the merge brought: ListView.storedRuleDialect-11533, grid-list-view-conditional-formatting-11533, ObjectMetricWidget.ruleFilterSpecShape-11526, plus filter-icontains-alignment-8976. Row 4 stays fully green (all 11 assertions), and no test in the set is red. (3) pnpm turbo run type-check --filter=@object-ui/core --filter=@object-ui/fields --filter=@object-ui/data-objectstack --filter=@object-ui/plugin-list --concurrency=2 -> exit 0, 'Tasks: 17 successful, 17 total' (all four ran with a cache miss, including the tsconfig.test.json programs). After the hold, git status shows no tracked change. Light gates at 9187c5c, outside the lock: check:control-bytes OK; check:new-line-citations 0 new; check-changeset-presence exit 0 (3 changesets declared); changeset:check exit 0; check:pending-changeset-literals exit 0.", "readback": "Trunk ref claude/issue-11438-objectstack-17-6-0 reads 9187c5c407060643860f4e3783fe6812dcaaa99b, and so does the slice branch. objectui#11531: head 9187c5c4, draft true, mergeable true, mergeable_state 'blocked'. Check-runs on 9187c5c4 at the time of reading: total_count 44, of which 41 in_progress and 3 completed (skipped). The CI conclusion is NOT MEASURED, because the runs are still in flight.", "mcp_calls": "0", "api_writes": "1 — POST /repos/objectstack-ai/objectui/issues/11094/comments (this report, through scripts/pm/post-stamped.mjs). Not REST: git push HEAD:claude/issue-11438-objectstack-17-6-0 (fast-forward da4c629f1..9187c5c40), and git push of the slice branch (8a9cf1b..9187c5c40). No force anywhere, no objectui#11531 body edit, no builder touched (objectui#10813).", "deviations": [], "open_questions": [], "out_of_scope_findings": [] }
Generated by Claude Code
objectstack-fleet commented
on Oct 3, 2026 ContributorAuthorMore actionsClaim: PM loop round 1 (takeover of the p1 release slice, ruling A″)
Session:session_01FjqrwXPfSMkSfkKYDSRkN2
Account:os-zhuang(the seat's linked user asGET /useranswers it; the card's assignee after this act)
Branch:claude/issue-11094-value-datasource-empty-17-6(continued, not re-cut; head9187c5c40)
Worktree: none in this container. The slice is already on the trunk, so no dev is dispatched.
Domain:domain:ui
Seat:domain:ui#1
File surface: unchanged from claim5965621810. This takeover adds no file.
Container & model:S,mode:subagent,model: opus(default judgment tier;dispatch-gates --tier --repo objectstack-ai/objectuiprinted "no path-derived mandate" forpackages/core/src/adapters/ValueDataSource.ts)
Clause-②: yes
Why yes: carried from5965621810. The matcher's answer to a storedis_empty/is_not_emptychanges; the at-tier record is PASS5967059614.
Thread-read: 5968152020
Serial constraints cleared: the slice is on the trunkclaude/issue-11438-objectstack-17-6-0. objectui#10813 (the director's,5968139578) pushes after it. Nothing else of this seat is in flight.Release: claim
5965621810,session_01YLg8XqWGJ785fwQ5v4pH37(os-elon-musk). Reason: that session is out of tokens. Provenance: the maintainer, in this session's chat (session_01FjqrwXPfSMkSfkKYDSRkN2), verbatim 「前任没有 token了,你继续。」. Destination: the same seat, this session.Handover record: the old branch's last pushed sha is
9187c5c407060643860f4e3783fe6812dcaaa99b. The trunk ref read the same sha at the 10:13Z push (report5968152020); the trunk has since moved on to13f07dd0f. Status: ACCEPT5966957488, contract PASS5967059614, slice pushed onto the trunk. What remains is to follow objectui#11531 to MERGED, verify by content onmain, and close this card.
Generated by Claude Code
objectstack-fleet commented
on Oct 3, 2026 ContributorAuthorMore actionsLanded and verified by content: closed
completedby objectui#11531's merge ·domain:uiseat 1 ·session_01FjqrwXPfSMkSfkKYDSRkN2· 2026-10-03T13:27Z.- The merge: PR objectui#11531 →
6158e4c93onmain, at 2026-10-03T13:14Z. Its closing lines name this card (Fixes #11094). - Content check (
git grepon objectuiorigin/main6158e4c93, with its first parent6f5719e1cas the control):packages/core/src/adapters/ValueDataSource.tscallsisEmptyFilterValue4 times onmainand 0 on the parent. The file's ownclass ValueDataSourceline reads 1 on both, so the zero is a real absence and not a missing file. - Trail: the slice
8a9cf1b(ACCEPT5966957488, contract PASS5967059614), merged onto the trunk as9187c5c40. pm:dispatchedis removed in the same act.domain:ui,area:records, the type and the priority stay.
Generated by Claude Code
- The merge: PR objectui#11531 →
- added a commit that references this issue
on Oct 7, 2026
Filing-gate category: ① a product defect with a named site, reached when objectui adopts the next
@objectstack/specrelease. Filed bare for triage. ⛔ Not a claim.Filed by the objectstack
domain:specseat 5 (session_01Sfe5YjBLwB9J3y8fvm2xq1, seat post objectstack-ai/objectstack#19357). It comes from item (h) of the contract review of objectstack-ai/objectstack#20570 (card objectstack-ai/objectstack#20446). The seat re-read the objectui sites below at the sha objectstack pins (.objectui-shadd3f7e1be3); nothing was run.What changes upstream
objectstack-ai/objectstack#20570 is an
@objectstack/specminor, so it ships in the next spec release. It makes$emptya filter operator, and it changescanonicalAstOperator:is_empty/isemptyfold tois_empty, no longer tois_null;is_not_empty/isnotemptyfold tois_not_empty, no longer tois_not_null.What that does here (read at
dd3f7e1be3)packages/core/src/adapters/ValueDataSource.ts:468switches oncanonicalAstOperator(rawOperator). It hasis_null(:509) andis_not_null(:511) arms, and nois_empty/is_not_emptyarm.defaultarm (:602) refuses the node ("filter operator … is not implemented by the in-memory matcher").is_empty/is_not_emptyis answered today through theis_nullarms. Once objectui runs that spec release, it is refused.packages/core/package.jsondeclares@objectstack/spec^17.3.0.packages/core/src/adapters/__tests__/ValueDataSource.astFilterVocabulary.test.tspins the current fold. The reviewer read that it goes red on the new release; this seat did not run it.Related
'', a multi-value field when null or[], and every other type when null.Blocked-by: objectstack-ai/objectstack#20446
Dedupe: the seat grepped the open objectui issues for
ValueDataSource,canonicalAstOperator,is_emptyand$empty. Only #10813, #9048, #8945 and #7650 matched, and none is about this site.Dedupe words:
ValueDataSource is_empty refused·canonicalAstOperator empty fold·in-memory matcher is_empty