Repository navigation
studio(condition builder): "is false / empty" compiles to !field and "is true / set" to a bare field — CEL refuses both on a non-boolean field, so a published validation rule blocks every matching write #11894
Description
Filing gate ① — product defect with a named location and a reproduction. reach: measured in the Studio task benchmark baseline (objectstack-ai/objectstack#22180, step 4 and 8b): a validation rule built in Studio's condition builder as record.status equals done AND record.due_date is false / empty saved and published as record.status == 'done' && !record.due_date (publish 200); afterwards every Done write was refused — PATCH /api/v1/data/repair_ticket/{id} → 400 VALIDATION_FAILED, reason unevaluable, fault runtime: no such overload: !null (no due date) or !string (with one) — shown to the end user as a red toast. Reproduced three times (two forged requests, one end-user UI save).
Who acts on it: objectui triage → the Studio owner (the producer half). ⛔ Not a claim. Found by the cloud executor of objectstack-ai/objectstack#22137 and filed by the dispatching seat from its out_of_scope_findings after an objectui dedupe the executor could not run.
What happens
The builder's two unary operators emit CEL that is only well-typed for a boolean field. CEL's ! and a bare operand of && take bool; a date, text or lookup field is not one, so the rule cannot be evaluated at runtime and the write is refused for a reason the author never sees. The builder offers no "is empty / is null" operator, so the only repair is to switch to Expression mode and type raw CEL (record.due_date == null fixed it).
Where it comes from (read in source)
ConditionBuilder (app-shell/src/views/metadata-admin/inspectors/ConditionBuilder.tsx) maps op truthy to the bare subject and op falsy to ! + subject, at the objectui pin a58626c8 and unchanged on main 82500a7.
Expected
The builder emits type-correct CEL for the field it is given: for a non-boolean field "is empty" compiles to == null (plus the empty-string arm where the platform's is-empty semantics include it) and "is set" to != null; ! and the bare form only for a boolean field. Better still, the operator list follows the field type.
Related
- finding(lint): the object save door gives no build verdict on validation conditions, field-rule slots (requiredWhen etc.), option visibleWhen or action predicates; os build refuses them, a metadata save stores them (#22019's sibling) objectstack#22032 (open) — the server half of the same failure: the save/publish door gives no verdict on validation conditions, so this rule published 200 and failed only at runtime.
Environment
Studio task benchmark baseline run, 2026-10-08: objectstack 8fc50b76, objectui a58626c8 (console built at the .objectui-sha pin), fresh showcase boot, Chromium 141 at 1440×900; source re-read on objectui main 82500a7.
Duplicate check
- Semantic issue search on objectstack-ai/objectui (open and closed), condition builder is empty false operator compiles to negation CEL no such overload on non-boolean field: 1 hit (closed, bug(core):
not_containson a non-string stored value fails the operator AND its negation — against the platform's objectstack#14079 ruling #8452 —not_containson non-string values, a different operator and layer). - Semantic issue search on objectstack-ai/objectui (open and closed), ConditionBuilder falsy truthy operator emits bare field or !field validation rule unevaluable: 9 hits, all closed and about other predicate surfaces (scope linting, boolean cell rendering, permissive fallbacks) — none about the emitted CEL.
- The executor's objectstack search found finding(lint): the object save door gives no build verdict on validation conditions, field-rule slots (requiredWhen etc.), option visibleWhen or action predicates; os build refuses them, a metadata save stores them (#22019's sibling) objectstack#22032 (related above).
Dedupe words: condition builder is empty negation CEL · no such overload !string validation rule · ConditionBuilder falsy operator non-boolean field
Filed by Claude Code (session session_01D76mrPJrSSdaKRxR2rvrMG) from the objectstack-ai/objectstack#22137 report.
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 8, 2026
Evidence moved here from objectstack-ai/objectstack#22187. The maintainer asked the benchmark session to file this defect (
ConditionBuildercompile(), itstruthy/falsyarms on a non-boolean field). That session could not search this repository, so the two cards crossed. objectstack-ai/objectstack#22187 is closed as a duplicate of this card, and this card carries the request. What it adds:op: 'truthy'(the Add condition button). A row whose operator the author never touches compiles to a bare non-boolean subject too.!xreads back as falsy, a bare identifier as truthy. A hand-typed!record.due_dateround-trips into the same broken row.ConditionBuilderonmainb6f8fc87:ObjectValidationsPanelFlowNodeConfigField(the Start entry condition, decision branches)ActionDefaultInspector,HookDefaultInspector,PageBlockInspectorwidgetsPOST /api/v1/data/OBJECTwith{"status":"done","due_date":"2026-10-20"}answers400,fault: runtime: no such overload: !string,reason: unevaluable. With the condition rewritten asrecord.due_date == null, the same write answers the rule's own message.main: the code is unchanged on objectuimainb6f8fc87(2026-10-08).Generated by Claude Code
objectstack-fleet commentedon Oct 8, 2026
Grade (triage):
pm:queueadded. p1 stands: a published validation rule built in the condition builder refuses every matching write, and end users see the refusal (measured three times in the Studio benchmark).domain:ui: the producer isConditionBuilder.tsxcompile().has()/!= null/!= ''by type), not!field;truthy.packages/lintquestion. It is not this card, and a new gate defaults to no.Triage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-08T05:56Z. ⛔ Not a claim, ⛔ not a dispatch.objectstack-fleet commentedon Oct 8, 2026
Claim: PM loop round 3
Session:
session_01CGZy1BGCjdN5cXqL9cnvB8Account:
os-support-aiBranch:
claude/issue-11894-typed-empty-opsWorktree:
objectui-issue-11894Domain:
domain:uiSeat:
domain:ui#3File surface (on
09821b1), underpackages/app-shell/src/views/metadata-admin/:inspectors/ConditionBuilder.tsx:compile()andparse()(about:479–:515), whosetruthy/falsyarms emit and read back a bare subject and!subject;OPSabout:29–:48);:824);engine.inspector.condition.op.*rows ofi18n.ts(en and zh), only if an operator gains a type-specific label;.changeset/11894-*.md.⛔ Not on it: the server half (objectstack-ai/objectstack#22032, a lint question that triage keeps separate); the six inspectors that render
ConditionBuilder(they change nothing);ConditionBuilder's field source (useObjectFieldsand thefieldsprop), which objectui#11895 may change: that is an adjacent region, declared, and whoever lands second mergesmain;packages/i18n/**. Any file outside this list: the dev reports it before opening the PR (stop on breach; explain in the report)Container & model:
M,mode:subagent,model: opus(dispatch-gates --tier --repo objectstack-ai/objectui: no path-derived mandate; default tier)Clause-②: no
Responsibility:
objectui app-shell: the condition builder compiles "is false / empty" to !field and "is true / set" to a bare field, which CEL refuses on a non-boolean field, so a published validation rule refuses every matching write with a fault the author never sees | the platform path: the builder knows each row's field type; a null or empty check is well typed for every type, and a boolean keeps today's form | every author building a condition in Studio (validation rules, flow entry conditions and decisions, actions, hooks, page blocks), and every end user whose write such a rule refusesThread-read: 6053397968
Serial constraints cleared: no in-flight claim in the lane names
ConditionBuilder.tsx's compile and parse path, and no open objectui PR touches the file (read 2026-10-08T06:12Z). This seat's objectui#11789 (5aa7f55), which addedConditionBuilder'sscopeIssue, has landed. objectui#11895 (pm:queue, p2) is declared above as an adjacent region.Why
Clause-②: no: the CEL a Studio-internal builder emits. A published export, prop, type member or language-pack key is a stop: the dev reports it before opening the PR, and the seat amends this line.Generated by Claude Code
objectstack-fleet commentedon Oct 8, 2026
os-dev-report
{
"issue": 11894,
"status": "blocked",
"branch": "claude/issue-11894-typed-empty-ops",
"pr": null,
"session": "session_01CGZy1BGCjdN5cXqL9cnvB8 (subagent run; the parent PM session id)",
"premise_still_valid": true,
"summary": "Implemented on the claimed surface and pushed (head d235d45, no PR): ConditionBuilder's two value-less operators are typed by the field catalog. A boolean field (spec BOOLEAN_VALUE_TYPES, by reference) keeps is true / is false and the bare subject / !subject emission; every other declared field gets is not empty / is empty, compiled by the spec expandEmptyOperator arm (null_only: S == null; text: S == null || S == ''; multi_value: S == null || size(S) == 0; is-not-empty is the exact complement; compound checks parenthesised when joined); an undeclared subject (context subject, flow variable, formula field, or a catalog row with no type) keeps today's words and form. A new row starts on equals; parse reads every new emission back into the same row (top-level split that respects parentheses and string literals); a stored !field on a typed non-boolean field no longer round-trips and opens in the raw editor verbatim (the component's existing round-trip-or-raw rule); a catalog fetched by objectName re-reads the value once it lands, while the author has not edited. BLOCKED: Zone 2 hypothesis 1 is falsified for the benchmark's own mount. ObjectValidationsPanel (Studio Rules tab, the validation-rule mount the card measured) passes fields as FieldOpt {name, label, hidden} built from readFields(draft.fields) and carries no type, so at that mount every field is undeclared and the benchmark rule still compiles to !record.due_date. Typing it is a two-line producer change in ObjectValidationsPanel.tsx (FieldOpt gains type and multiple; its fields memo copies e.def.type and e.def.multiple, the way useObjectFields and ResourceEditPage already do). That file is outside the claim's File surface, and the claim says to report before opening the PR, so no PR is opened and the file is untouched. The other five mounts are typed through useObjectFields (hook, action visible/disabled, page block, flow) or ResourceEditPage's catalog (schema-form ConditionWidget).",
"tests": "Pins: packages/app-shell/src/views/metadata-admin/inspectors/ConditionBuilder.typedEmptyOps-11894.test.tsx, 26 tests, mounting the real component and evaluating the emitted CEL with @objectstack/formula ExpressionEngine.evaluate called with record and previous, the call objectql checkPredicate makes. The benchmark rule on a date field: true with no due date, false with one. Text arm: true for null and for the empty string. Multi-value arm (tags; lookup with multiple true vs single lookup). Controls: boolean compiles as today; undeclared subject keeps !S; lossless round trip for 8 shapes; a new row seeds equals; fetched catalog. Instrument control: the old emission faults (no such overload) in the same engine both ways. Runs: pin file 26/26 passed (fc8bd29). Narrowed suite at fc8bd29: 363 files / 3353 tests passed (VERDICT command-exit 0); the set is every app-shell test whose relative-import closure reaches ConditionBuilder.tsx (360) plus the 6 app-shell tests that read the designer i18n table. Post-merge at d235d45: 40 mount-site files (ConditionBuilder, HookDefaultInspector, ObjectValidationsPanel, FlowNodeConfigField, ConditionWidget, ActionDefaultInspector, PageBlockInspector, inspectorStrings tests) 436/436 passed. type-check: pnpm --filter @object-ui/app-shell type-check exit 0 at d235d45 (after turbo build of the app-shell dependency closure, exit 0); it runs tsc -p tsconfig.test.json, which did type the new test file (it reported a helper-type error that was then fixed). Ablation at d235d45 via objectstack scripts/ablation-replace.mjs, WRAP mode: anchor in compileRow made the falsy arm emit !S again; mutation landed (anchor 1 to 0, blob 6121d1e2 to 8c48dd1a); pin file went 10 failed / 16 passed, the date pin among the reds (expected direction: red); restore proven (blob after restore equals HEAD 6121d1e2, git diff HEAD empty, git status clean). No build leg: the pin imports ./ConditionBuilder by relative path, not through a package export, so dist is not on the resolution path.",
"gates": [
"pnpm exec vitest run (pin file) | exit 0 | Test Files 1 passed, Tests 26 passed",
"pnpm exec vitest run (363-file closure set) at fc8bd29 | exit 0 | Test Files 363 passed (363), Tests 3353 passed (3353)",
"pnpm exec vitest run (40 mount-site files) at d235d45 | exit 0 | Test Files 40 passed (40), Tests 436 passed (436)",
"pnpm --filter @object-ui/app-shell type-check at d235d45 | exit 0 | (script name echoed: type-check, tsc --noEmit and tsc -p tsconfig.test.json)",
"pnpm exec eslint (3 touched ts/tsx files) at d235d45 | exit 0 | 0 errors, 7 warnings (baseline before the change: the same 7)",
"pnpm check:control-bytes | exit 0 | check-control-bytes: OK",
"pnpm check:test-path-roots | exit 0 | check-test-path-roots: OK",
"pnpm check:changeset-claims | exit 0 | No pending changeset names a file this change touches.",
"pnpm check:pending-changeset-literals | exit 0 | No test source names a pending changeset.",
"pnpm check:i18n-keys | exit 0 | Every in-scope call-site key resolves against the en pack",
"pnpm check:i18n-drift | exit 0 | No designer-table en value changed in this range.",
"pnpm check:i18n-dead-keys | exit 0 | report-only; none of the new condition.op rows listed",
"pnpm check:i18n-designer-parity | exit 0 | Every en row has a zh row, and every shared row carries the same placeholders.",
"pnpm check:new-line-citations | exit 0 | VERDICT new-cross-file-line-citations: 0 new citation(s)",
"pnpm check:phantom-deps | exit 0 | Every in-scope import is declared by the package that publishes it.",
"pnpm check:spec-symbols | exit 0 | nothing cites a key its spec symbol does not declare (run at fc8bd29)",
"pnpm check:vi-mock-specifiers / vi-mock-inherit / vi-mock-override-shape | exit 0 | OK",
"pnpm check:unreferenced-sources | exit 0 | Every shipped source file in every covered package is reachable.",
"node scripts/check-changeset-presence.mjs | exit 0 | 3 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)",
"NOT MEASURED: check:spec-floors, reason: prerequisite not met (needs a full workspace build); the fact it judges was read directly: npm @objectstack/spec 17.6.0 (app-shell's declared floor) exports BOOLEAN_VALUE_TYPES, expandEmptyOperator and EmptyOperatorArm from ./data",
"NOT MEASURED: check:eager-closure, reason: needs apps/console/dist/eager-closure.json (console build); CI's",
"NOT MEASURED: full app-shell package suite, reason: stopped by me after 22 minutes holding the shared lock with a queue behind it; replaced by the declared 363-file closure narrowing above; CI runs the full farm",
"NOT MEASURED: browser check of the Studio surfaces; the pins mount the real component"
],
"deviations": [
"No PR opened and status blocked: the claim's rule for a file outside the File surface (report before opening the PR). The branch carries the full on-surface change, pushed.",
"Test narrowing (declared): the closure is derived from static relative imports, dynamic import(), side-effect imports and vi.mock specifiers under packages/app-shell/src; it is blind to tests that reach ConditionBuilder through a package specifier (self-import of @object-ui/app-shell) or read sources from disk. The full package suite is CI's.",
"I stopped my own full-suite run (my process group only) after it had held the shared verify lock for 22 minutes with two runs queued behind it.",
"Commits carry the model-free trailer pair AGENTS.md prescribes, not the model-named Co-Authored-By line in the harness attribution reminder.",
"Merged main (fc82a31, via a private fetch ref) into the branch, per the dispatch; no conflicts."
],
"files_changed": [
".changeset/11894-typed-empty-ops.md",
"packages/app-shell/src/views/metadata-admin/i18n.ts",
"packages/app-shell/src/views/metadata-admin/inspectors/ConditionBuilder.tsx",
"packages/app-shell/src/views/metadata-admin/inspectors/ConditionBuilder.typedEmptyOps-11894.test.tsx"
],
"line_budget": "not applicable (no skills/** surface)",
"mcp_calls": "0",
"api_writes": "1 (the os-dev-report comment on #11894 via scripts/pm/post-stamped.mjs, POST /repos//issues/11894/comments); git push is not counted. No pr_create, no label-write (no PR exists).",
"open_questions": [
{
"question": "The validation-rule mount (ObjectValidationsPanel.tsx, the benchmark's own mount) hands ConditionBuilder no field types, so the p1 case is not fixed by the claimed surface alone. Amend the claim's File surface to add that file's FieldOpt and its fields memo (copy e.def.type and e.def.multiple), plus one pin through ObjectValidationsPanel?",
"options": [
"A: amend the claim and send a patch round on this branch: add the two-line producer change and a pin that mounts ObjectValidationsPanel and asserts the benchmark rule compiles to record.due_date == null; then open the draft PR with Fixes #11894. objectui#11820, which held the file, has landed (PR #11918), so the file has no other holder that this run can see; cost one more round.",
"B: open the PR now as Part of #11894 for the five typed mounts, and file the validations-mount half separately; cost: the p1 card's measured case stays broken after the first PR lands, and two PRs.",
"C: leave validation conditions untyped by design; cost: the benchmark failure stays as filed."
],
"recommendation": "A. It fixes the measured p1 case in one PR. The change is in the producer, contract-first: the mount passes the field types it already reads off the draft, with no consumer-side guess. All four in-place-fix conditions now hold (same defect class; mechanical, with the shape fixed by useObjectFields; no other holder since #11820 landed; same gate family). The only one I cannot meet is that the seat has to amend the claim surface."
}
],
"out_of_scope_findings": [
"class: a · reach: named producer: Studio ConditionBuilder's compare rows (greater than, less than, and the at-least / at-most symbols) emit an unguarded ordering comparison such as record.amount greater-than 100; evaluated with @objectstack/formula ExpressionEngine.evaluate on a record whose amount is null it faults (no such overload: dyn of null greater-than int), and a date field does the same, so a server-evaluated validation rule built that way refuses every write where the field is empty (fail-closed) · evidence: engine probe at objectui d235d45 with formula 17.7.0; the objectql rule-validator docblock records that ordering comparisons still fault over a total record and that a lint null-guard gate exists for it · dedupe words: condition builder greater than null field no such overload · ConditionBuilder ordering operator null guard · validation rule compare empty field refuses write",
"carrier: this branch's next round (or the PR author) · noted, not filed: useObjectFields defaults a field def with no string type to text, so such a field takes the text arm here; a spec-valid field always declares a type, so this was not measured as reachable; adjacent to objectui#11895's field-source region",
"carrier: objectui#11895's field-source change (useObjectFields) or this branch's next round · noted, not filed: neither useObjectFields (ObjectFieldInfo) nor ResourceEditPage's catalog carries multiple, so a lookup, user, select or file field declared multiple true reaches the builder as a single-value type and its is-empty check is S == null, which is well typed but reads an empty list as not empty; tags, multiselect and checkboxes are multi-value by type and are right. The list filter builder documents the same type-only limit."
]
}
objectstack-fleet commentedon Oct 8, 2026
Claim amendment —
domain:uiseat 3,session_01CGZy1BGCjdN5cXqL9cnvB8, 2026-10-08T07:49Z. It amends the claim6053620679.Why. The dev stopped before opening a PR and reported in
6055223522. Zone 2 hypothesis 1 is falsified for the card's own benchmark mount:studio-design/ObjectValidationsPanel.tsx) handsConditionBuilderfields with no type.main4ebc250, itsFieldOpt(about:123–:127) is{ name, label?, hidden? }. Itsfieldsmemo (about:637–:645) copies only those three fromreadFields(draft.fields).!record.due_date.useObjectFields, and the schema-form widget throughResourceEditPage's catalog.The dev's open question, answered by the seat: option A. It is the card's own measured case, not a neighbouring defect, and all four in-place conditions are checked:
useObjectFieldsandResourceEditPage.FieldOptgainstype?andmultiple?, and the memo copiese.def.typeande.def.multiple.b4693b7.File surface, added (under
packages/app-shell/src/views/):studio-design/ObjectValidationsPanel.tsx:FieldOptand thefieldsmemo only. The rule list, the Save gate,blockingIssues, the master-detail mount and every other region are ⛔ not on it.ObjectValidationsPaneland shows that the benchmark rule on a date field compiles torecord.due_date == null. A boolean field still compiles to the bare form, as the control.Recorded on the existing surface:
ConditionBuilder'sfieldsprop gains optionaltype/multiplemembers on its element type. The claim fenced off the field source (useObjectFields, objectui#11895's region), which stays untouched.ConditionBuilderis not reachable from@object-ui/app-shell's only entry,"."→dist/index.js. SoClause-②: nostands.Everything else in the claim is unchanged. The PR opens with
Fixes #11894once this round's evidence is in.Generated by Claude Code
objectstack-fleet commentedon Oct 8, 2026
Adjacent-edit notice from
domain:uiseat 1 (objectui#11895), for this card's devdomain:uiseat 1 ·session_01DrKzdPdyLLBW3qpZ4vtk7z· 2026-10-08T07:55Z. ⛔ Not a claim and not a request to change this card's work.useObjectFieldsread throughclient.withPreviewDrafts(true).get('object', …), which is this card's declared adjacent region (your claim's⛔ Not on itline).withPreviewDrafts. Nine of them are this card's "tests beside them":ConditionBuilder.test.tsxand its.celGate,.clientMountRoots,.contextSubjects,.mountRoots,.mountScope,.placeholderRoots,.referenceValueand.subjectVocabularysiblings.6055387753) to add, in each file'svi.mock('../useMetadata', …)double only, awithPreviewDraftsreturning the same double. No assertion, fixture or test name changes.main. If your PR lands first and adds a new test with a metadata-client double, objectui#11895's dev completes that double too.Generated by Claude Code
objectstack-fleet commentedon Oct 8, 2026
os-dev-report
{
"issue": 11894,
"status": "done",
"branch": "claude/issue-11894-typed-empty-ops",
"pr": "#11935",
"session": "session_01CGZy1BGCjdN5cXqL9cnvB8 (subagent run; the parent PM session id)",
"premise_still_valid": true,
"summary": "Patch round 2 on the amended claim (comment 6055312520), head 00a030d, draft PR 11935 opened with Fixes #11894. Merged main 4ebc250 first (clean; merge commit d198f6d). The in-place producer change: ObjectValidationsPanel's FieldOpt gains type and multiple, and its fields memo copies e.def.type when it is a string and multiple: e.def.multiple === true. No other region of the file is touched. The Studio Rules tab now hands ConditionBuilder each field's type, so the benchmark rule built there as due date is empty is written as record.status == 'done' && record.due_date == null. Round-1 builder work (typed value-less operators by BOOLEAN_VALUE_TYPES and expandEmptyOperator, undeclared subjects unchanged, equals as the new-row default, lossless parse, stored !field opening in the raw editor) is unchanged. Changeset re-read against the final diff: the multi-value sentence is qualified to a multiple-declared lookup, user, select or file field where the editor's field list carries that flag; it names the Studio Rules tab as the mount that passes multiple and says the other mounts pass the type but not multiple. PR assignee set to os-support-ai (read back).",
"tests": "New pin packages/app-shell/src/views/studio-design/ObjectValidationsPanel.typedEmptyOps-11894.test.tsx, 4 tests, mounting ObjectValidationsPanel with a draft carrying status (select), due_date (date) and urgent (boolean). Building due date is empty writes record.status == 'done' && record.due_date == null, evaluated with @objectstack/formula ExpressionEngine.evaluate with record and previous: true with no due date, false with one. The rule reloads as the is-empty row. The old builder's !record.due_date opens in the raw editor verbatim with nothing written. Control: the boolean field writes !record.urgent and record.urgent. Ablation at 00a030d via objectstack scripts/ablation-replace.mjs, WRAP mode, anchor the type copy line in the memo, --delete: mutation landed (anchor 1 to 0, blob f365b93c to 1a2ba2be); the panel pin went 4 failed / 4. The new pin is red as required. The boolean control went red too, not green: the operators are worded by declared type, so without the type copy the boolean reads as undeclared and offers is empty / false instead of is false. That is the observed direction. Restore proven: blob after restore equals HEAD f365b93c, git diff HEAD empty, git status clean. No build leg: the pin imports ./ObjectValidationsPanel by relative path. Runs at 00a030d, under the shared lock: the closure set was re-run because the merge changed five StudioDesignSurface test files and StudioDesignSurface.tsx inside it. That is 374 files: the 371 app-shell tests whose import closure reaches ConditionBuilder.tsx at this head, both pins and every ObjectValidationsPanel*.test.tsx among them, plus the app-shell tests that read the designer i18n table. Test Files 374 passed (374), Tests 3416 passed (3416), VERDICT command-exit 0. pnpm --filter @object-ui/app-shell type-check exit 0 at 00a030d. Round-1 ablation (falsy arm back to !S, at d235d45): builder pin 10 failed / 16 passed, restore proven.",
"gates": [
"pnpm exec vitest run (374-file closure set incl. both pins and all ObjectValidationsPanel*.test.tsx) at 00a030d | exit 0 | Test Files 374 passed (374), Tests 3416 passed (3416)",
"pnpm exec vitest run (new panel pin) at 00a030d | exit 0 | Test Files 1 passed, Tests 4 passed",
"pnpm --filter @object-ui/app-shell type-check at 00a030d | exit 0 | (script name echoed: type-check, tsc --noEmit and tsc -p tsconfig.test.json)",
"pnpm exec eslint (5 touched ts/tsx files) at 00a030d | exit 0 | 0 errors, 8 warnings = baselines (7 in ConditionBuilder.tsx; 1 pre-existing set-state-in-effect in ObjectValidationsPanel.tsx, measured on the pre-change blob)",
"pnpm check:changeset-claims | exit 0 | No pending changeset names a file this change touches.",
"pnpm check:pending-changeset-literals | exit 0 | No test source names a pending changeset.",
"pnpm check:new-line-citations | exit 0 | VERDICT new-cross-file-line-citations: 0 new citation(s)",
"pnpm check:control-bytes | exit 0 | check-control-bytes: OK",
"pnpm check:test-path-roots | exit 0 | check-test-path-roots: OK",
"pnpm check:i18n-keys / check:i18n-drift / check:i18n-designer-parity | exit 0 | Every en row has a zh row, and every shared row carries the same placeholders.",
"pnpm check:vi-mock-specifiers | exit 0 | OK",
"node scripts/check-changeset-presence.mjs | exit 0 | 5 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)",
"node scripts/check-changeset-no-major.mjs | exit 0 | No changeset declares a major bump.",
"NOT MEASURED: check:spec-floors, reason: needs a workspace build; the fact it judges was read directly in round 1: @objectstack/spec 17.6.0 exports BOOLEAN_VALUE_TYPES, expandEmptyOperator and EmptyOperatorArm from ./data",
"NOT MEASURED: check:eager-closure, reason: needs a console build; CI's",
"NOT MEASURED: full app-shell package suite and a browser pass; CI runs the farm",
"CI on PR 11935: not awaited; the head 00a030d had no check runs reported when read just before handback (convergence is the PM's)"
],
"deviations": [
"Test narrowing (declared): the closure is derived from static relative imports, dynamic import(), side-effect imports and vi.mock specifiers under packages/app-shell/src; it is blind to tests that reach ConditionBuilder through a package specifier or read sources from disk.",
"The ablation's boolean control went red, not green as the round brief allowed; the direction is reported as observed.",
"Commits carry the model-free trailer pair AGENTS.md prescribes, not the model-named Co-Authored-By line in the harness attribution reminder.",
"The PR body footer uses the session-URL form from the agent definition, not the harness reminder's footer text."
],
"files_changed": [
".changeset/11894-typed-empty-ops.md",
"packages/app-shell/src/views/metadata-admin/i18n.ts",
"packages/app-shell/src/views/metadata-admin/inspectors/ConditionBuilder.tsx",
"packages/app-shell/src/views/metadata-admin/inspectors/ConditionBuilder.typedEmptyOps-11894.test.tsx",
"packages/app-shell/src/views/studio-design/ObjectValidationsPanel.tsx",
"packages/app-shell/src/views/studio-design/ObjectValidationsPanel.typedEmptyOps-11894.test.tsx"
],
"line_budget": "not applicable (no skills/** surface)",
"mcp_calls": "0",
"api_writes": "3 this round, all through the fleet relay as objectstack-fleet[bot]: pr_create (POST /repos/objectstack-ai/objectui/pulls, PR 11935, draft, body read back identical 10755 bytes); label-write --assign os-support-ai (POST /repos//issues/11935/assignees, read back); this os-dev-report comment (POST /repos//issues/11894/comments). Round 1: 1 (comment 6055223522). git push not counted. No label writes.",
"open_questions": [],
"out_of_scope_findings": [
"class: a · reach: named producer: Studio ConditionBuilder's compare rows (greater than, less than, and the at-least / at-most symbols) emit an unguarded ordering comparison such as record.amount greater-than 100; evaluated with @objectstack/formula ExpressionEngine.evaluate on a record whose amount is null it faults (no such overload: dyn of null greater-than int), and a date field does the same, so a server-evaluated validation rule built that way refuses every write where the field is empty (fail-closed) · evidence: engine probe at objectui d235d45 with formula 17.7.0; the objectql rule-validator docblock records that ordering comparisons still fault over a total record and that a lint null-guard gate exists for it · dedupe words: condition builder greater than null field no such overload · ConditionBuilder ordering operator null guard · validation rule compare empty field refuses write",
"carrier: this branch's next round (or the PR author) · noted, not filed: useObjectFields defaults a field def with no string type to text, so such a field takes the text arm here; a spec-valid field always declares a type, so this was not measured as reachable; adjacent to objectui#11895's field-source region",
"carrier: objectui#11895's field-source change (useObjectFields) or this branch's next round · noted, not filed: neither useObjectFields (ObjectFieldInfo) nor ResourceEditPage's catalog carries multiple, so a lookup, user, select or file field declared multiple true reaches the builder as a single-value type and its is-empty check is S == null, which is well typed but reads an empty list as not empty; tags, multiselect and checkboxes are multi-value by type and are right. The list filter builder documents the same type-only limit."
]
}
objectstack-fleet commentedon Oct 8, 2026
os-dev-report
{
"issue": 11894,
"status": "done",
"branch": "claude/issue-11894-typed-empty-ops",
"pr": "#11935",
"session": "session_01CGZy1BGCjdN5cXqL9cnvB8 (subagent run; the parent PM session id)",
"premise_still_valid": true,
"summary": "Round 3 (merge only), head 4f3437f, pushed. PR 11935 now reads mergeable true, mergeable_state blocked (it is a draft; CI left to the seat). Merged main 19a7348 with merge commit 46e73a1 (model-free trailer pair). The one conflict, ObjectValidationsPanel.tsx, was resolved as a union. FieldOpt keeps type, system and multiple, and type carries one comment naming both readers: the New menu's presets (objectui#11861) and ConditionBuilder's value-less operators (objectui#11894). The memo keeps main's single type line and system, plus multiple: e.def.multiple === true, under an objectui#11894 comment reworded to stay true now that main copies type. The diff against main in that file is exactly those lines; presetPlans, VALIDATION_PRESETS and everything objectui#11931 added are as main has them. New pin case: a lookup declared multiple on the Rules tab builds is empty as record.status == 'done' && (record.watchers == null || size(record.watchers) == 0). The changeset now says the Rules tab passes type since objectui#11931 and this change adds multiple there. Premise correction: the merge brought four PRs, not one: objectui#11928 (ResourceEditPage, PermissionMatrixEditor, ObjectListViewInspector, PackageOwdOverviewPanel), #11927 (BuilderLanding), #11929 (RecordDetailView) and #11931. ResourceEditPage's delta has no field, catalog, condition or widget line, so the narrowed set gained only the ConditionWidget and ResourceEditPage condition tests, and the 374-file closure was not re-run. PR body NOT patched, by the agent definition: the dev writes the PR body once, at creation, and the seat applies later edits. The updated body is ready, built from the created body with the producer-change section, evidence head, pins, ablations and runs rewritten for this round: /tmp/claude-0/-home-user/613acf53-7e5b-52cd-a1e4-8670521d3bcb/scratchpad/issue-11894/11894-pr-body-r3.md (11593 bytes; first line Fixes #11894; Clause-② line kept; footer unchanged). The live body still matches the created one byte for byte.",
"tests": "Ablation at 4cc894f via objectstack scripts/ablation-replace.mjs, WRAP mode, anchor the multiple copy line in the Rules tab memo, --delete: mutation landed (anchor 1 to 0, blob 7c2400e9 to 391a19d9). The panel pin went 1 failed / 4 passed; the red one is the new multiple pin, expected the parenthesised null-or-size check and received record.status == 'done' && record.watchers == null. Expected direction: red. The pin was added before the ablation, because the round-2 draft declared no multiple field and so could not go red. Restore proven: blob after restore equals HEAD 7c2400e9, git diff HEAD empty, git status clean. No build leg (relative import). Runs under the shared lock at 4cc894f: 13 files / 110 tests passed. They are every ObjectValidationsPanel*.test.tsx (celGate, newRuleWaits-11820, presets-11861, readOnlyLook-11781, the main test, typedEmptyOps-11894, whenKey; that covers every test file objectui#11931 added or changed), ConditionBuilder.typedEmptyOps-11894, ConditionWidget.conditionRoots / conditionScope / conditionSubjects, and ResourceEditPage.celGate / conditionScope. turbo build of the app-shell dependency closure (28 tasks successful) followed by pnpm --filter @object-ui/app-shell type-check: exit 0 at 4cc894f. 4f3437f changes only the changeset.",
"gates": [
"pnpm exec vitest run (13-file round-3 set) at 4cc894f | exit 0 | Test Files 13 passed (13), Tests 110 passed (110)",
"turbo build app-shell^... then pnpm --filter @object-ui/app-shell type-check at 4cc894f | exit 0 | Tasks: 28 successful, 28 total; type-check script echoed",
"pnpm exec eslint ObjectValidationsPanel.tsx + its typedEmptyOps pin at 4f3437f | exit 0 | 1 problem (0 errors, 1 warning) = the 19a7348 blob's own 1 warning (set-state-in-effect, pre-existing)",
"pnpm check:changeset-claims | exit 0 | No pending changeset names a file this change touches.",
"pnpm check:pending-changeset-literals | exit 0 | No test source names a pending changeset.",
"pnpm check:new-line-citations | exit 0 | VERDICT new-cross-file-line-citations: 0 new citation(s)",
"pnpm check:control-bytes / check:test-path-roots / check:i18n-keys / check:i18n-drift / check:i18n-designer-parity / check:vi-mock-specifiers at 4f3437f | exit 0 | OK",
"node scripts/check-changeset-presence.mjs | exit 0 | 5 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)",
"node scripts/check-changeset-no-major.mjs | exit 0 | No changeset declares a major bump.",
"git merge-tree HEAD against main 19a7348 | exit 0 | clean",
"NOT MEASURED: the 374-file closure at the merged head, reason: not re-run per the round brief; the brief's premise was narrower than the merge, and the extra PRs' sources were checked for contact with the condition path (none in ResourceEditPage's delta)",
"NOT MEASURED: check:spec-floors, check:eager-closure, the full package suite and a browser pass; CI's",
"CI on PR 11935: left to the seat"
],
"deviations": [
"PR body not PATCHed although the round brief asked for it: the agent definition says the dev writes the PR body only once, at creation, and the seat makes later edits; it also takes precedence when the brief conflicts. The ready replacement body is named in summary.",
"The round brief said the merge brings only objectui#11931; it brought four PRs. Rather than re-run the 374-file closure, the narrowed set gained the ConditionWidget and ResourceEditPage condition tests, after a read of ResourceEditPage's delta found no contact with the field catalog or the condition widget.",
"The worktree was removed after round 2 and recreated for this round on the same branch (no rebase, no force-push); it is removed again at the end of this round.",
"Commits carry the model-free trailer pair AGENTS.md prescribes."
],
"files_changed": [
".changeset/11894-typed-empty-ops.md",
"packages/app-shell/src/views/metadata-admin/i18n.ts",
"packages/app-shell/src/views/metadata-admin/inspectors/ConditionBuilder.tsx",
"packages/app-shell/src/views/metadata-admin/inspectors/ConditionBuilder.typedEmptyOps-11894.test.tsx",
"packages/app-shell/src/views/studio-design/ObjectValidationsPanel.tsx",
"packages/app-shell/src/views/studio-design/ObjectValidationsPanel.typedEmptyOps-11894.test.tsx"
],
"line_budget": "not applicable (no skills/** surface)",
"mcp_calls": "0",
"api_writes": "1 this round: this os-dev-report comment (POST /repos//issues/11894/comments via scripts/pm/post-stamped.mjs). No PR body PATCH, no label or assignee write. git push not counted. Earlier rounds: 1 (round 1) + 3 (round 2).",
"open_questions": [],
"out_of_scope_findings": [
"class: a · reach: named producer: Studio ConditionBuilder's compare rows (greater than, less than, and the at-least / at-most symbols) emit an unguarded ordering comparison such as record.amount greater-than 100; evaluated with @objectstack/formula ExpressionEngine.evaluate on a record whose amount is null it faults (no such overload: dyn of null greater-than int), and a date field does the same, so a server-evaluated validation rule built that way refuses every write where the field is empty (fail-closed) · evidence: engine probe at objectui d235d45 with formula 17.7.0; the objectql rule-validator docblock records that ordering comparisons still fault over a total record and that a lint null-guard gate exists for it · dedupe words: condition builder greater than null field no such overload · ConditionBuilder ordering operator null guard · validation rule compare empty field refuses write",
"carrier: this branch's next round (or the PR author) · noted, not filed: useObjectFields defaults a field def with no string type to text, so such a field takes the text arm here; a spec-valid field always declares a type, so this was not measured as reachable; adjacent to objectui#11895's field-source region",
"carrier: objectui#11895's field-source change (useObjectFields) or this branch's next round · noted, not filed: neither useObjectFields (ObjectFieldInfo) nor ResourceEditPage's catalog carries multiple, so a lookup, user, select or file field declared multiple true reaches the builder as a single-value type and its is-empty check is S == null, which is well typed but reads an empty list as not empty; tags, multiselect and checkboxes are multi-value by type and are right. The list filter builder documents the same type-only limit."
]
}
objectstack-fleet commentedon Oct 8, 2026
ACCEPT —
domain:uiseat 3,session_01CGZy1BGCjdN5cXqL9cnvB8, 2026-10-08T09:16Z. PR objectui#11935, head4f3437f. p1.PR shape:
main.mainwas merged twice, asfc82a31and then19a7348, with merge commits and no rebase.git merge-treeof the currentmain(f3a0488) and this head is clean; the queue tests that merge.Fixes #11894, with no other closing keyword.Clause-②: noat line start.os-support-ai.4f3437f(read back identical).Scope: 6 files, all on the claim (
6053620679) as amended (6055312520):ConditionBuilder.tsxand its pin;engine.inspector.condition.op.*rows ini18n.ts, en and zh;ObjectValidationsPanel.tsx(FieldOptand thefieldsmemo only) and its pin;patchon@object-ui/app-shell).useObjectFields, the other five mounts andpackages/i18n/**are untouched.ConditionBuilderis not reachable from@object-ui/app-shell's only entry, so thefieldsprop's widened element type is not published. No governed path.Diff read (the seat's own):
BOOLEAN_VALUE_TYPESfield keeps is true / is false and the bare /!form.expandEmptyOperatorarm:== null; null or''for text; null orsize() == 0for multi-value. Is not empty is the exact complement, and a compound check is parenthesised when joined.formula, or a row with no type.parsesplits outside parentheses and string literals, and accepts exactly the emitted text. A stored!fieldon a typed non-boolean field opens in the raw editor verbatim.objectNamere-reads the value once it lands, while the author has not edited.The amendment, in place:
6055312520, checked against the four in-place conditions.19a7348), that memo already copiestypefor the New menu's presets. After the union merge, this PR addsmultiplethere and one comment naming both readers. Nothing else in the file differs frommain(diff read).Reverse verification (dev reports
6055223522,6056439614), each restore proven (blob equals HEAD):!S: the builder pin went 10 failed / 16 passed, the date pin among the reds.typecopy dropped (00a030d): 4 of 4 panel pins red.multiplecopy dropped (4cc894f): exactly the multi-value pin red.Tests:
ConditionBuilder.tsxpassed: 3416 tests, at00a030d, before the last merge.ObjectValidationsPanel*.test.tsx(objectui#11931's preset pins among them), both pins, and theConditionWidget/ResourceEditPagecondition tests.@objectstack/formula, the enginecheckPredicateuses. The benchmark rule is true with no due date and false with one. The old emission faults in the same engine.Gates: CI on
4f3437f(head_shaequals the PR head): 43 runs: 39 success, 3 skipped (Test (coverage), its shard template,dependabot), and 1 failure,Bundle Analysis(see Landing).LintandType Checkare success. Mergeable stateunstable, from that one non-required red.Contract review: not required (
Clause-②: no).Findings:
>/</>=/<=that faults on a null operand. The seat read objectstackpackages/lint/src/validate-null-guards.ts: the gatingvalidateStackExpressions, run byos build/os validateand the runtime publish gate, already refuses an unguarded ordering operator on a nullable declared field invalidations[]and hook conditions (has(x)reads as a null guard and is not one — a publish-time lint should reject un-guarded nullable comparisons in CEL predicates objectstack#4763 and null-guard 闸门(#4763)只覆盖了校验规则与 hook 条件 —— action / flow 条件两面待定,formula 面待判 objectstack#4811). So the "published rule refuses every write" consequence is not reached on those surfaces: the author is refused at publish. The save door's missing verdict is finding(lint): the object save door gives no build verdict on validation conditions, field-rule slots (requiredWhen etc.), option visibleWhen or action predicates; os build refuses them, a metadata save stores them (#22019's sibling) objectstack#22032. The builder-side guard on the other mounts is not measured →Acceptance notes.useObjectFieldsdefaults a field with no string type to text. A spec-valid field always declares a type, so it is not reachable as measured →Acceptance notes.useObjectFieldsandResourceEditPage's catalog carry nomultiple, so a lookup, user, select or file field declaredmultipleis checked as single-valued at those mounts. The changeset says so. The adjacent carrier is objectui#11895's field-source region (seat 1). Reach was not measured →Acceptance notes.Landing: held — waits on objectui#11937 (since 2026-10-08T09:12Z).
Bundle Analysisis red onmainsince19a7348. The eager closure is over its 3307.0 KB ceiling (MAX_EAGER_CLOSURE_GZIP_BYTES), with the same failing step as this PR (Check console performance budget). objectui#11937 is the p0 anchor, and seat 2 is fixing it forward.main's 3307.0 KB. Landing it now would widen the gap that fix has to close.Bundle Analysisis not required and does not run in the merge group, so the queue would admit it anyway.maininto this branch and lands it when every check on that head is green,Bundle Analysisincluded. If the fix leaves too little headroom for this PR's bytes, the seat comes back here with a measured option.Generated by Claude Code
objectstack-fleet commentedon Oct 8, 2026
os-dev-report
{
"issue": 11894,
"status": "done",
"branch": "claude/issue-11894-typed-empty-ops",
"pr": "#11935",
"session": "session_01CGZy1BGCjdN5cXqL9cnvB8 (subagent run; the parent PM session id)",
"premise_still_valid": true,
"summary": "Round 4, head ec8411f, pushed. Merged main 1c51e97 with merge commit 2f45634 (model-free trailer pair). The merge brought 3c888c6 (the revert of objectui#11931 and #11932), objectui#11895 (useObjectFields reads the draft overlay), #11898, #11925 and the eager-ceiling allowance #11949. The ObjectValidationsPanel conflict was resolved per the ruling. FieldOpt keeps type and multiple and drops system, and its comment names only ConditionBuilder's value-less operators. The memo keeps this PR's own type copy and multiple, drops system, and carries the round-2 objectui#11894 comment. The file's diff against main is exactly those lines; nothing of objectui#11931 is re-added. Follow-up commit: the changeset again says the Studio Rules tab now hands the builder each field's type and multiple flag (the 11931 wording is gone); the panel pin's header no longer names the presets; the builder pin's metadata-client stub answers withPreviewDrafts, which useObjectFields calls since objectui#11895 (main updated every existing ConditionBuilder test's stub the same way). Bundle Analysis on ec8411f: completed, success. The budget bot's Console Performance Budget comment (posted inside that run) reads: eager closure 3308.0 KB gzip over 336 chunks, budget 3312.0 KB, Status PASS; main entry chunk 71.3 KB against 350 KB. PR body not patched (agent definition). The live body equals the round-3 file the seat applied. The ready round-4 replacement is /tmp/claude-0/-home-user/613acf53-7e5b-52cd-a1e4-8670521d3bcb/scratchpad/issue-11894/11894-pr-body-r4.md (11586 bytes; first line Fixes #11894; Clause-② line kept; no objectui#11931 wording; footer unchanged). It rewrites the producer-change section, the evidence head, the ablations, the runs, the Bundle Analysis reading, the type-check and eslint lines, the gate head and the NOT MEASURED line.",
"tests": "Ablation at ec8411f via objectstack scripts/ablation-replace.mjs, WRAP mode, anchor the memo's type copy line, --delete: mutation landed (anchor 1 to 0, blob bf10cb2c to 34723fc5). The panel pin went 5 failed / 5, the BUILDING benchmark pin among them (expected direction: red); the multiple pin and the boolean control are red too, because without a type every field reads as undeclared. Restore proven: blob after restore equals HEAD bf10cb2c, git diff HEAD empty, git status clean. No build leg (relative import). Runs under the shared lock at ec8411f: 17 files / 177 tests passed. They are every ObjectValidationsPanel*.test.tsx (celGate, newRuleWaits-11820, readOnlyLook-11781, the main test, typedEmptyOps-11894, whenKey; the 11931 preset pin is gone with the revert), both pins, and, beyond the brief's set because the merge changed them, every ConditionBuilder*.test.tsx (objectui#11895 changed nine) plus useObjectFields.draftOverlay-11895. turbo build of the app-shell dependency closure (28 tasks successful), then pnpm --filter @object-ui/app-shell type-check: exit 0 at ec8411f.",
"gates": [
"pnpm exec vitest run (17-file round-4 set) at ec8411f | exit 0 | Test Files 17 passed (17), Tests 177 passed (177)",
"turbo build app-shell^... then pnpm --filter @object-ui/app-shell type-check at ec8411f | exit 0 | Tasks: 28 successful, 28 total; type-check script echoed",
"pnpm exec eslint ObjectValidationsPanel.tsx + both pins at ec8411f | exit 0 | 1 problem (0 errors, 1 warning) = main 1c51e97's own blob (1 warning, set-state-in-effect)",
"Bundle Analysis check-run 113285265152 on ec8411f | completed success | Console Performance Budget: eager closure 3308.0 KB (336 chunks) vs budget 3312.0 KB, Status PASS; main entry 71.3 KB vs 350 KB",
"pnpm check:changeset-claims / check:pending-changeset-literals / check:new-line-citations / check:control-bytes / check:test-path-roots / check:i18n-keys / check:i18n-drift / check:i18n-designer-parity / check:vi-mock-specifiers at ec8411f | exit 0 | OK",
"node scripts/check-changeset-presence.mjs | exit 0 | 5 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)",
"node scripts/check-changeset-no-major.mjs | exit 0 | No changeset declares a major bump.",
"NOT MEASURED: the 374-file closure at ec8411f (last full run at 00a030d), the full package suite and a browser pass; CI's",
"Other CI checks on PR 11935: left to the seat"
],
"deviations": [
"PR body not PATCHed (agent definition: the dev writes it once, at creation; the seat makes later edits). The ready round-4 body is named in summary.",
"Ran every ConditionBuilder*.test.tsx and useObjectFields.draftOverlay-11895 beyond the brief's narrowed set: the merge changed nine of those tests and the hook they mock (objectui#11895).",
"Edited the builder pin's client stub (withPreviewDrafts) and the panel pin's header in a separate commit after the merge commit: both are this PR's own files, on the claimed surface.",
"The worktree was recreated for this round on the same branch and is removed at the end; no rebase, no force-push. Commits carry the model-free trailer pair."
],
"files_changed": [
".changeset/11894-typed-empty-ops.md",
"packages/app-shell/src/views/metadata-admin/i18n.ts",
"packages/app-shell/src/views/metadata-admin/inspectors/ConditionBuilder.tsx",
"packages/app-shell/src/views/metadata-admin/inspectors/ConditionBuilder.typedEmptyOps-11894.test.tsx",
"packages/app-shell/src/views/studio-design/ObjectValidationsPanel.tsx",
"packages/app-shell/src/views/studio-design/ObjectValidationsPanel.typedEmptyOps-11894.test.tsx"
],
"line_budget": "not applicable (no skills/** surface)",
"mcp_calls": "0",
"api_writes": "1 this round: this os-dev-report comment (POST /repos//issues/11894/comments via scripts/pm/post-stamped.mjs). No PR body PATCH, no label or assignee write. GitHub reads only for the Bundle Analysis poll. git push not counted. Earlier rounds: 1 + 3 + 1.",
"open_questions": [],
"out_of_scope_findings": [
"class: a · reach: named producer: Studio ConditionBuilder's compare rows (greater than, less than, and the at-least / at-most symbols) emit an unguarded ordering comparison such as record.amount greater-than 100; evaluated with @objectstack/formula ExpressionEngine.evaluate on a record whose amount is null it faults (no such overload: dyn of null greater-than int), and a date field does the same, so a server-evaluated validation rule built that way refuses every write where the field is empty (fail-closed) · evidence: engine probe at objectui d235d45 with formula 17.7.0; the objectql rule-validator docblock records that ordering comparisons still fault over a total record and that a lint null-guard gate exists for it · dedupe words: condition builder greater than null field no such overload · ConditionBuilder ordering operator null guard · validation rule compare empty field refuses write",
"carrier: this branch's next round (or the PR author) · noted, not filed: useObjectFields defaults a field def with no string type to text, so such a field takes the text arm here; a spec-valid field always declares a type, so this was not measured as reachable; adjacent to objectui#11895's field-source region",
"carrier: objectui#11895's field-source change (useObjectFields) or this branch's next round · noted, not filed: neither useObjectFields (ObjectFieldInfo) nor ResourceEditPage's catalog carries multiple, so a lookup, user, select or file field declared multiple true reaches the builder as a single-value type and its is-empty check is S == null, which is well typed but reads an empty list as not empty; tags, multiselect and checkboxes are multi-value by type and are right. The list filter builder documents the same type-only limit."
]
}
objectstack-fleet commentedon Oct 8, 2026
ACCEPT, landing head —
domain:uiseat 3,session_01CGZy1BGCjdN5cXqL9cnvB8, 2026-10-08T11:38Z. PR objectui#11935, headec8411f3. It extends ACCEPT6056690401(head4f3437f), whose landing was held for the eager budget.What moved since the ACCEPT:
2f45634bbrought inmain1c51e973. That includes the main-red rollback3c888c6(objectui#11956, which reverted objectui#11931) and the 5,120 B allowance1c51e97(objectui#11949, ruling6056819248step 2).ec8411f3touches only this PR's own files.The resolution, read by the seat: against
main,ObjectValidationsPanel.tsxdiffers only by this card's lines.FieldOptgainstypeandmultiple.fieldsmemo copiese.def.type(when a string) andmultiple: e.def.multiple === true, under the objectui#11894 comment.system, the presets). With 11931 reverted, thetypecopy is this PR's own line again. Its ablation atec8411f3turns the Rules-tab benchmark pin red (5 of 5), and the restore is proven.The changeset, re-read sentence by sentence against the diff at
ec8411f3:multipleflag": true, it is this PR's memo.multiple: true,useObjectFieldsandResourceEditPageare untouched.The pins: the builder pin's metadata-client stub now answers
withPreviewDrafts, whichuseObjectFieldscalls since objectui#11895.mainupdated every existingConditionBuildertest's stub the same way. The panel pin's header no longer names the presets.Tests at
ec8411f3: 17 files and 177 tests passed: everyObjectValidationsPanel*andConditionBuilder*test, plususeObjectFields.draftOverlay-11895. The type-check passed.Gates: CI on
ec8411f3(head_shaequals the PR head):Test (coverage), its shard template,dependabot), 0 failure;Bundle Analysissuccess: the budget bot reads 3308.0 KB against 3312.0 KB, PASS;clean.6 files, +848/−46. No governed path. The PR body was refreshed by the seat to match this head (read back identical).
Landing: ready + auto-merge through the queue, now.
Generated by Claude Code
objectstack-fleet commentedon Oct 8, 2026
Landed —
domain:uiseat 3,session_01CGZy1BGCjdN5cXqL9cnvB8, 2026-10-08T11:58Z. PR objectui#11935 merged through the queue as247f503at 2026-10-08T11:57Z. p1.dfa15c86(objectui#11908's merge), an ancestor oforigin/main.git merge-treeof that parent and the landing headec8411f3(treead8f0cdfon both). That head's ACCEPT extension is6059052428.dfa15c86→247f503, underpackages/app-shell/src/views/:expandEmptyOperator: 1 → 4.BOOLEAN_VALUE_TYPES: 2 → 5.multiple: e.def.multiple === true: 0 → 1.engine.inspector.condition.op.isEmptyrows inmetadata-admin/i18n.ts: 0 → 2 (en, zh).metadata-admin/previews/useObjectFields.ts(objectui#11895's field source, adjacent and fenced off) is byte-identical on both sides.Fixes #11894, and this merge closed only objectui#11894, ascompletedat 11:57Z.pm:dispatchedis removed in this pass.6056690401):validate-null-guards.ts) at publish on validations and hook conditions. The draft-save half is finding(lint): the object save door gives no build verdict on validation conditions, field-rule slots (requiredWhen etc.), option visibleWhen or action predicates; os build refuses them, a metadata save stores them (#22019's sibling) objectstack#22032.useObjectFieldsandResourceEditPage's catalog carry nomultiple, so a multi-value lookup there is checked as single-valued; the changeset says so.Generated by Claude Code
fix(app-shell): the condition builder's is-empty / is-not-empty compi…