Skip to content

fix(core,plugin-form,types): a blank gate on the three remaining paths is diagnosed, never a silent true; mirror keys the spec narrowed refuse a blank (objectui#11262) - #11283

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-11262-blank-gates-diagnosed
Sep 30, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-11262-blank-gates-diagnosed

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #11262
Clause-②: no (narrowing)

ADR-0137 D4 says a blank gate predicate is "diagnosed, never a silent true". objectui#8069 (PR objectui#11233) diagnosed the dialect: 'cel' route. Three paths still drew a blank gate in silence, and each now reports through that same guard: isBlankPredicateText decides, and evalFieldPredicate's [blank] report speaks. No drawn verdict moves. A blank gate is still "no gate" (D3/D4). ADR-0137 D1 now also covers objectui's two gate mirrors that override a spec key the spec narrowed: they refuse a blank at authoring, as the spec does. Direction: triage comment 5918741052. Claim: 5919879003.

Narrowing

  • Accepted before: objectui's SelectOptionSchema.visibleWhen and FormFieldSchema.visibleOn parsed '', whitespace, and an envelope whose source is blank.
  • Refused now (@object-ui/types, minor): the same values, at the key, with the spec's own sentence (EVALUATED_EXPRESSION_SOURCE_REQUIRED).
  • Why: @objectstack/spec 17.5.0 declares both keys as EvaluatedExpressionInputSchema, which refuses a blank. That is the data SelectOptionSchema.visibleWhen and the form view field's visibleWhen / deprecated visibleOn. objectui's mirror overrides each key to keep its own wire, and it was wider than the spec on the same key.
  • Unchanged: the accepted shape, since the refined union's arms are ExpressionWireSchema.options by reference and the parsed value is the authored one. The runtime verdict of a stored blank gate is also unchanged ("no gate" plus a diagnostic). BaseSchema's visible / hidden / disabled have no spec twin, so they stay D4-diagnosed and are not refused. The triage recorded this, and the FieldRulePredicateWireSchema docblock records it too.
  • Nothing widens. The diagnostics add console output only.

What changed

  1. ExpressionEvaluator.evaluateCondition's legacy path (packages/core). The CEL route's blank block moved into one private method, answerBlankGate, and both routes call it. The legacy path used an if (!condition) and a whitespace-only trim() to answer true in silence. Now, after the envelope unwrap, a blank text (a bare '' or whitespace, or a dialect-less envelope's blank source) goes to the same method. Every mode still answers true, throwOnError included. onFault receives the [blank] reason, and every other caller gets the built-in warning. Both routes share one locator, so '' and { dialect: 'cel', source: '' } land on one dedupe key and print one line. This path goes first because SchemaRenderer's visibleWhen / visible / visibleOn / visibility legs pass the raw authored value into it.
  2. evalRowPredicate (listConditional.ts). The bare-blank early return is removed. A blank string now takes the route its blank-envelope twin already took (no legacy marker can match blank text). It gets the same verdict, which is the caller's fallback, and the same report: labelled through warnEvalError on the warnOnError route, and through the canonical helper on the single-eval route. This removes a blank test instead of adding one.
  3. sectionFields' attachVisibility (packages/plugin-form). A blank predicate is still dropped, so the field draws with no view-level gate. The drop is now reported, naming the field. isBlankPredicateText replaces the two local trim() tests. The function reports only what it drops (see the premise note below).
  4. form.zod.ts. SelectOptionSchema.visibleWhen and FormFieldSchema.visibleOn now carry FieldRulePredicateWireSchema, the refinement PR objectui#11233 put on the triad. That refinement passes the predicate TEXT to stripImportedDefaults(EvaluatedExpressionInputSchema) from @objectstack/spec/shared and restates no rule of its own. The const moved above SelectOptionSchema, its first reader, to avoid the temporal dead zone. It keeps its name, so the fieldRules.ts docblock that cites it stays true. The spec import is the same one PR objectui#11233 added, so imported-defaults-8317 and terminal-unknown-key-refusal-11073 need no new crossing row. Both suites are green.
  5. Docs: content/docs/guide/metadata-diagnostics.md now covers blank gates. The two declaredPredicate.ts docblock passages that quoted the removed if (!trimmed) return true / if (!source.trim()) lines are corrected. These are comment-only edits, made because this diff made the quotes false.

Premise check (card body read against origin/main f61dab169)

  • All three silencers hold. They are narrower than the card said:
    • Path 1: { dialect: 'cel', source: '' } was already diagnosed. Silent were a bare '', whitespace, and a dialect-less blank envelope.
    • Path 2: { source: '' } was already diagnosed. Only the bare-string spelling was silent.
    • Path 3: attachVisibility drops a blank on two chains, meta.visible_on ?? meta.visibleOn and a form-view entry's fd.visibleWhen ?? fd.visibleOn. It does not drop one on the runtime-field chain (fd.visibleOn). There the field is the output, the blank stays on it, and form.tsx reports it at render (pinned by the existing form.tsx control "a blank view-level visibleOn is a layout GATE: no gate plus a diagnostic"). attachVisibility therefore reports only when the output does not carry the blank. The new pin records the runtime-field chain as "carried, not reported a second time".
  • The page control: FormPage does not use sectionFields. It carries the view predicate onto its row and judges it in isFieldVisible through evalFieldPredicate, so a blank view-level predicate was already reported there on origin/main. Measured: the diagnostic assertion passes on its own (-t on that row only, with FormPage untouched by this diff). In the full file the line was swallowed, because the earlier stored-blank row put '' on the same field (notes) under the same locator ("visibleWhen of field 'notes'"), and the one-time dedupe is module state per file. The restated control moves the view-level blank to status and asserts the [blank] line naming it. Its comment, which said the page normalizes through sectionFields, is corrected.

Census before narrowing (the triage's "no silent narrowing" clause)

  • A literal-blank search over every tracked file for visibleOn / visibleWhen found no stored or example fixture carrying a blank value, in either the string form or the source form. The matches are test inputs to renderers or to the triad, plus the two control rows below.
  • Measured: the origin/main copy of base-schema-predicate-envelope-7530.test.ts was run against the narrowed form.zod.ts. Result: 2 failed / 60 passed. The two are exactly the rows that recorded PR objectui#11233's wider reading: "the form GATE legs carry the same object" and "FormFieldSchema.visibleOn (the view-level gate) and an option's visibleWhen still parse a blank". Those two rows are restated below. No stored or first-party fixture turned red, so the narrowing went ahead.
  • The whole packages/types/src/ suite is green with the narrowing (see Verification). That includes the parity ledgers, select-option-spec-parity, imported-defaults-8317, terminal-unknown-key-refusal-11073 and detail-view-field-options-10296.

Pins

  • packages/core/src/evaluator/__tests__/blankGateDiagnosed-11262.test.ts covers paths 1 and 2 for '', whitespace and { source: '' }. Each row checks the verdict and the report. Path 1 also covers onFault and throwOnError. Path 2 covers both routes and both fallback directions, plus rowless. There is a one-diagnosis-point row per path: a legacy/bare blank and its CEL/envelope twin print ONE line between them. Controls: undefined / null and written predicates are silent. Each case gets a fresh module graph.
  • packages/plugin-form/src/sectionFields.blankGate-11262.test.ts covers the four dropping chains × the three spellings (no gate, plus one [blank] line naming the field). It also pins that the runtime-field chain is carried and not reported again. Controls: written predicates are carried silently, and junk is dropped silently.
  • PR objectui#11233's wizard control (wizardVisibleWhenFault-8069) and page control (FormPage.visibleWhenFault-8069) are restated with the diagnostic added.
  • base-schema-predicate-envelope-7530.test.ts:
    • Both gate mirrors refuse the four blank spellings at the key with the spec's sentence.
    • They accept a non-blank string, a CEL envelope and a dialect-less envelope with the value unchanged, and still refuse junk.
    • They carry the triad's refined wire as one object (identity).
    • Control: BaseSchema's three gates still parse a blank.

Ablation (each leg run with ablation-replace.mjs from objectstack, at adaa4072b)

The mutation was proven on disk each time: anchor count went from 1 to 0 and the blob changed. The restore was proven each time: blob equals HEAD and git diff HEAD is empty. Every mutated file is imported relatively by its pins, so no dist/ is on the path. Baseline for all five pin files: 153 passed.

leg mutation red rows where
path 1 legacy blank branch returns true without answerBlankGate 10 / 153 exactly path 1's rows in the core pin
path 2 bare-blank return fallback restored 7 / 153 exactly path 2's '' / whitespace rows, both one-diagnosis-point rows, and rowless. The { source: '' } rows stay green, since they were already diagnosed
path 3 attachVisibility's report removed 13 / 153 the 12 dropping-chain rows plus the restated wizard control
mirror: option SelectOptionSchema.visibleWhen back to ExpressionWireSchema 5 / 153 its 4 blank rows plus the identity row
mirror: visibleOn FormFieldSchema.visibleOn back to ExpressionWireSchema 5 / 153 its 4 blank rows plus the identity row

The first path 2 attempt was a null op, and the tool refused it (the replacement contained the anchor). It was re-anchored and re-run, and only the re-run is reported above.

Verification

All heavy runs went through os-verify-lock.sh (slot issue-11262). Verdicts are read from pass counts and the lock's VERDICT line. Base is f61dab169. fac88b93e is the implementation commit. adaa4072b (HEAD) adds only a type annotation to the new plugin-form pin, which the plugin-form type-check asked for.

  • At HEAD adaa4072b:
    • the five pin files: 5 files, 153 passed;
    • scripts/__tests__/: 177 files passed (2 skipped), 5371 tests passed;
    • @object-ui/plugin-form type-check EXIT 0;
    • @object-ui/console type-check EXIT 0, after turbo run build --filter='@object-ui/console^...': 34 tasks successful;
    • eslint on the 10 changed TS files: 0 errors (warning counts equal base on ExpressionEvaluator.ts, one fewer on sectionFields.ts);
    • check-changeset-presence, check-changeset-no-major, check:control-bytes, check:new-line-citations (0 new), markdown-test-inputs --audit: all EXIT 0.
  • At fac88b93e:
    • packages/core/src/ plus 46 consumer test files: 240 files, 4712 passed. The consumers are every test outside core that names evaluateCondition / evalRowPredicate / useCondition / useRowPredicate or authors a blank gate literal, across react, app-shell, components, plugin-grid, plugin-detail, permissions, types, plugin-form and console;
    • packages/plugin-form/src/: 156 files in three chunks (27 / 65 / 64), 1819 passed, 1 skipped;
    • apps/console/src/components/: 36 files, 437 passed;
    • @object-ui/types and @object-ui/core type-check EXIT 0. The four packages' type-check programs list every changed test file (checked with --listFiles).
  • packages/types/src/ together with packages/core/src/evaluator/ and the sectionFields tests: 317 files, 7956 passed. This ran on the tree before the comment-only declaredPredicate.ts / docs / changeset edits that went into fac88b93e.
  • NOT MEASURED, and superseded by the runs above:
    • the whole-plugin-form single run hit its 300 s timeout under contention (exit 124, 0 failures before the kill);
    • one attempt took the lock's queue-timeout (99);
    • one invocation was refused by vitest-invocation-guard (overlapping filters).
  • Declared narrowing: the full pnpm test / pnpm lint farm is CI's.

Acceptance notes (observations, not filed)

  • Same-family blank-gate silencers outside this card's three paths (source reads only, no public-door measurement):

    • callers that pre-screen pred === '' before evalRowPredicate: useRowPredicate (packages/react), evalRowActionVisibility in both plugin-grid's RowActionMenu and components' data-table, and evalCreatePredicate in app-shell's RelatedRecordActionsBridge;
    • partitionRowsByPredicate's pred === '' returns every row in silence, while ' ' reaches evalRowPredicate with fallback false and excludes every row (on origin/main too; diagnosed since this diff). That is a verdict split between two blank spellings, which needs a ruling rather than a mechanical change;
    • conditional formatting's blank expression falls through ruleToPredicate silently;
    • ObjectForm's flat path drops visible_on: '' by truthiness.

    Named in the report for the seat to route to this family.

  • On FormPage, a field's object-level visibleWhen and its view-level visibleWhen share one locator, so a blank in both slots prints one line.

  • The flat Modal/Drawer builder (flatFields.ts) does not carry an object field's visible_on at all. This is a drift observation that was not exercised.


Generated by Claude Code

…s is diagnosed, never a silent true; mirror keys the spec narrowed refuse a blank (objectui#11262)

ADR-0137 D4: evaluateCondition's legacy path, evalRowPredicate's bare-blank
fallback and sectionFields' attachVisibility now report a blank gate through
the [blank] guard objectui#8069 put on the dialect: 'cel' route. No drawn
verdict moves.

ADR-0137 D1 on objectui's two gate mirrors whose spec key refuses a blank:
SelectOptionSchema.visibleWhen and FormFieldSchema.visibleOn carry the triad's
refined wire. BaseSchema's objectui-only gates stay diagnosed, not refused.

Claude-Session: https://claude.ai/code/session_011p7ikEivgXefNDaE5S5Uec
Co-authored-by: Claude <noreply@anthropic.com>
…ui#11262 pin

The package's test type-check (tsconfig.test.json) reads mock.calls as any,
so the filter callback's parameter needs its own annotation.

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

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

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

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

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

.changeset/5905-componentinput-inputtype-tombstone.md

  • names zod/form.zod.ts → packages/types/src/zod/form.zod.ts — edited by this change

    The write was measured as a no-op before it was deleted, and re-measured on this branch's base rather than inherited from the card. A structural census over every inputs: array in the repository (211 regions, all tracked TS/TSX/JS sources) scores inputType at exactly ONE authoring site — the plugin-markdown registration — against name 953, type 969, label 966, description 194, enum 119, required 86 and binding 4 in the same pass over the same regions, so the instrument was not blind. The other 192 in-repo inputType hits are a DIFFERENT face: FormField.inputType (zod/form.zod.ts), the text-input renderer's prop, and SchemaBuilder.inputType, none of which sit on a ComponentInput. The publication path is unchanged and was re-confirmed: packages/sdui-parser/src/index.ts forwards exactly seven keys per input — name, type, of, required, enum, binding, description — so an authored inputType could not reach the published sdui.manifest.json even in principle.

.changeset/6349-name-authority-batch-3.md

  • names form.zod.ts → packages/types/src/zod/form.zod.ts — edited by this change

    @object-ui/components — ComboboxOption now IS @object-ui/types' declaration. The component declared its own { value, label }, a strict subset of the ComboboxOption that @object-ui/types declares for ComboboxSchema.options and mirrors in form.zod.ts ({ value, label, disabled? }). The component now re-exports the types declaration (through the @object-ui/types/form subpath — the root barrel does not publish the name), so the name ComboboxOption exported from @object-ui/components gains the optional disabled?: boolean member. Every value that type-checked before still does — nothing narrows and no key changes type; the one thing that moves is keyof ComboboxOption, so a consumer that EXHAUSTS the type (a Record over its keys) will need the new key. Note that the Combobox component itself does not read option.disabled — that member was already declared on the @object-ui/types face and is now visible on this one too; it is recorded as a separate finding, not changed here.

.changeset/6396-previous-values-dom-leak.md

  • names packages/types/src/zod/form.zod.ts → packages/types/src/zod/form.zod.ts — edited by this change

    Scope is the runtime leak only. The declared key stays exactly as declared (packages/types/src/form.ts, packages/types/src/zod/form.zod.ts are untouched): it has a live consumer, so there is nothing here for the enforce-or-remove channel.

.changeset/6505-predicate-valued-gate-rules.md

  • names evaluator/declaredPredicate.ts → packages/core/src/evaluator/declaredPredicate.ts — edited by this change

    The verdict is delegated to hasDeclaredPredicate (evaluator/declaredPredicate.ts), the repo's single definition of "is a predicate gate declared on this value?" (objectui#3850's ruling), rather than answered a second time in the validator — a hand-rolled twin that agrees today and drifts tomorrow is the defect class this rule was already an instance of. packages/core/src/validation/__tests__/predicate-valued-gate-rules.test.ts pins the delegation behaviourally: the rule's verdict must equal boolean || hasDeclaredPredicate(value) across every probe in the file.

.changeset/6938-checkbox-wrapper-class.md

  • names zod/form.zod.ts → packages/types/src/zod/form.zod.ts — edited by this change

    packages/components/src/renderers/form/checkbox.tsx:36 reads cn("flex items-center space-x-2", schema.wrapperClass) — classes on the wrapper div around the box and its label — and neither the TypeScript interface in packages/types/src/form.ts nor the zod mirror in zod/form.zod.ts declared the key. It compiled through BaseSchema's index signature and parsed through .passthrough(), admitted unexamined. The same key, on the same class of read, is declared on FileUploadSchema and FilterBuilderSchema (objectui#6150); the checkbox was left out only because its doc page's schema block is a six-line summary.

.changeset/7113-chart-data-model.md

  • names form.zod.ts → packages/types/src/zod/form.zod.ts — edited by this change

    .extend() with a NEW key still works and preserves the fold and the refinement; .optional(), z.discriminatedUnion, z.toJSONSchema and safeValidateSchema are all unaffected. Nothing in this repository calls the throwing combinators on either const, and the published surface already ships refined mirrors (objectql.zod.ts, complex.zod.ts, form.zod.ts, app.zod.ts), so the class is not new — but it is a real behaviour change on a published export and it belongs in the release note rather than in a reviewer's file.

.changeset/7530-predicate-envelope-declared.md

  • names zod/form.zod.ts → packages/types/src/zod/form.zod.ts — edited by this change

    • ExpressionWire (type, main entry) — the TypeScript wire union, in packages/types/src/expression.ts. - ExpressionWireSchema (@object-ui/types/zod) — its runtime twin, hoisted out of zod/form.zod.ts (where it was module-private) into zod/expression.zod.ts and imported by both base.zod.ts and form.zod.ts. One envelope type, reused by reference; no second spelling.
  • names form.zod.ts → packages/types/src/zod/form.zod.ts — edited by this change

    • ExpressionWire (type, main entry) — the TypeScript wire union, in packages/types/src/expression.ts. - ExpressionWireSchema (@object-ui/types/zod) — its runtime twin, hoisted out of zod/form.zod.ts (where it was module-private) into zod/expression.zod.ts and imported by both base.zod.ts and form.zod.ts. One envelope type, reused by reference; no second spelling.

.changeset/7722-wrapper-class-five-more.md

  • names zod/form.zod.ts → packages/types/src/zod/form.zod.ts — edited by this change

    Each of renderers/form/switch.tsx, textarea.tsx, date-picker.tsx, select.tsx and renderers/data-display/list.tsx reads schema.wrapperClass onto its wrapper element, and neither the TypeScript interface (form.ts, data-display.ts) nor the zod mirror (zod/form.zod.ts, zod/data-display.zod.ts) declared the key. The reads compiled through BaseSchema's index signature (objectui#5155) and the values parsed through .passthrough(), admitted unexamined. The same key, on the same class of read, is declared on CheckboxSchema (b74a8598d), FileUploadSchema and FilterBuilderSchema (objectui#6150); these five were left out only because their doc pages never listed it.

.changeset/7735-zod-mirrors-stop-authoring-defaults.md

  • names form.zod.ts → packages/types/src/zod/form.zod.ts — edited by this change

    What changed. All 41 .default() call sites under packages/types/src/zod/ are removed — layout.zod.ts 22, crud.zod.ts 11, form.zod.ts 5, views.zod.ts 2, app.zod.ts 1. @object-ui/components reconciles the third face a separate finding found: flex's registration defaultProps.align seeded 'center', the value its own renderer never applies, so a designer-made node laid out differently from a hand-authored one; it now seeds 'start'.

.changeset/8069-field-rule-fault-direction-declared.md

  • names evaluator/declaredPredicate.ts → packages/core/src/evaluator/declaredPredicate.ts — edited by this change

    Both spellings now report [blank] the predicate is declared but empty — nothing to evaluate through the same single reporting site as every other fault, on both channels (the built-in console.warn and the onFault passback, so the fault-probing callers that pass warn: false are not silenced either). Every verdict is unchanged, including the envelope spelling: { source: '' } used to reach the engine and come back "AST-only evaluation not yet supported; persist source" and { source: ' ' } "Unexpected token: EOF" — two misleading reasons for one author mistake, both already resolving to the same fallback this change keeps. Blankness is decided by isBlankPredicateText (evaluator/declaredPredicate.ts), the repo's one definition of that question since objectui#3960, now exported for this second consumer rather than copied.

.changeset/8478-describe-line-addresses.md

.changeset/8478-zod-pins-form-layout.md

.changeset/8499-node-slot-registered-arms.md

  • names zod/form.zod.ts → packages/types/src/zod/form.zod.ts — edited by this change

    • SemanticElementSchema (zod/layout.zod.ts) — the seven HTML sectioning tags renderers/layout/semantic.tsx registers: aside main header nav footer section article. - HtmlElementSchema (zod/layout.zod.ts) — the 37 safe flow/inline tags renderers/basic/html-elements.tsx registers (h1…h6, p, a, ul, img, …), plus the per-tag keys that module forwards to the DOM (href, target, rel, title, src, alt, width, height, dateTime, cite). ⚠️ Dated note, 2026-09-27 — that set has since gained code — objectui#10756. At this change TAGS and this arm both named 37 tags; both now name 38, and the parity pin counts 38. The rest of this entry is kept as the reading of this change. - InputShorthandSchema (zod/form.zod.ts) — email / password, the two aliases renderers/form/input.tsx registers onto the input renderer with inputType pinned. inputType is deliberately NOT declared on this arm: the wrapper spreads its own value last, so an authored one is overwritten. ⚠️ Dated note, 2026-09-28 — inputType is now declared on this arm, as a refusal — objectui#8762. Later in this same release the arm declares inputType on both faces and refuses it by name (?: never on the TypeScript face, a retirementTombstone on the zod mirror, at path inputType), with guidance pointing at { "type": "input", "inputType": "email" }. So "inputType is deliberately NOT declared on this arm" no longer holds; the reason does, since the wrapper still spreads its own value last. The rest of this entry is kept as the reading of this change. - UiCalendarSchema (zod/form.zod.ts) — ui:calendar, the date-picker primitive renderers/form/calendar.tsx registers under exactly that key (skipFallback, because bare calendar belongs to the plugin-calendar view).

.changeset/8738-fields-warn-route1.md

  • names sectionFields.ts → packages/plugin-form/src/sectionFields.ts — edited by this change

    Top-level fields reads only bare field-name strings ({ name } tolerated) — a different vocabulary from sections[].fields, which also accepts the spec FormFieldSchema object (identity key field, e.g. { field: 'note', colSpan: 2 }). Moving one of those objects into a top-level fields array resolves to no name and used to vanish without a word; it is now reported once per distinct offender via console.warn, naming the skipped shape and the vocabulary difference, modelled on sectionFields.ts's existing warnOnMixedVocabulary.

.changeset/9067-zod-barrel-named-arms.md

  • names zod/form.zod.ts → packages/types/src/zod/form.zod.ts — edited by this change

    • InputShorthandSchema (zod/form.zod.ts) — the email / password shorthand arm. - UiCalendarSchema (zod/form.zod.ts) — ui:calendar, the date-picker primitive renderers/form/calendar.tsx registers, a different component from the calendar plugin view that owns the bare literal.

.changeset/9884-master-detail-fields-intersection.md

  • names sectionFields.ts → packages/plugin-form/src/sectionFields.ts — edited by this change

    So the rendered outcome is unchanged and the intersection stands: the parent field pool is built from fields first, and each section resolves its members against that pool. What changed is that the loss is audible. warnSectionMemberExcludedByFields (sectionFields.ts, beside the two warnings objectui#8738 and objectui#3090 added) names the section, the member and the two keys that collided, once per distinct pair, whenever a member the object really declares is dropped for the sole reason that fields omits it — including the expensive case where it was the section's last surviving member and the section disappears with its heading. A member the object never declares at all is deliberately NOT recruited into this warning: it resolves to nothing whether or not fields is authored, which is a different silence with a different remedy.

.changeset/console-formpage-visible-predicates-5594.md

.changeset/evaluateexpression-jsdoc-links-5580.md

  • names ExpressionEvaluator.ts → packages/core/src/evaluator/ExpressionEvaluator.ts — edited by this change

    ExpressionEvaluator.ts declares two things spelled evaluateExpression: the method on ExpressionEvaluator (bare expression, throws) and the module-level export (context bag, fail-soft, delegating to evaluate). The registerFunction block referred to both under the one spelling, four lines apart.

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

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

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

Compared the checked-out tree with be0ad007b (merge-base with origin/main): 11 file(s) changed outside .changeset/, read against 1854 pending declaration(s) that publish a body (2466 pending in total). · run

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 330 chunks) 3584.0 KB 3607.4 KB
Main entry chunk (gzip) 150.0 KB 350 KB
Entry file index-BPwJCJnt.js —
Status PASS —

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


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.88KB 6.25KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.17KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.70KB 10.94KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.13KB 7.95KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 570.18KB 136.44KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 228.86KB 63.53KB
fields (index.js) 261.11KB 66.26KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.35KB 9.18KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 41.23KB 11.53KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.86KB 5.00KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.52KB 2.26KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.01KB 3.93KB
plugin-calendar (index.js) 52.17KB 15.06KB
plugin-charts (index.js) 84.09KB 22.93KB
plugin-chatbot (index.js) 198.22KB 46.97KB
plugin-dashboard (index.js) 139.27KB 37.26KB
plugin-designer (index.js) 216.32KB 44.56KB
plugin-detail (index.js) 244.72KB 64.45KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 173.75KB 44.64KB
plugin-gantt (index.js) 173.03KB 43.07KB
plugin-grid (index.js) 231.61KB 63.61KB
plugin-kanban (index.js) 48.62KB 15.21KB
plugin-list (index.js) 116.63KB 28.97KB
plugin-map (index.js) 23.50KB 7.82KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.04KB 12.21KB
plugin-timeline (index.js) 33.05KB 9.67KB
plugin-tree (index.js) 11.20KB 3.89KB
plugin-view (index.js) 90.32KB 22.76KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.81KB 3.58KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 119.55KB 39.23KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 6.06KB 2.68KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 21.42KB 7.05KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 3.19KB 1.62KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.26KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 21.59KB 7.71KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: adaa4072ba2257986739cb0b49a3d67fa712239b
Local-runs: none

Inputs: card #11262 (body; triage 5918741052; claim 5919879003; dev report 5921294954), PR #11283 (body, 12-file list, net diff against main at the head), the head's check-runs with filter=latest, and head-ref reads of the touched sources and their callers (contents/PATH?ref=SHA, nothing built or run). Waited for CI per the Mechanics rule: all 43 check-runs on the head completed before judging — 40 success (the 8 test shards and the Test rollup, Test (dist pins), Build & E2E, Live E2E (informational), Type Check, Lint, Build Docs, Spec Main Shape Gate, the five changeset gates, Governed Surface Queue Guard, Line Citation Gate, Control Byte Scan, Bundle Analysis and the doc / skill / convention checks), 3 skipped (Test (coverage), its shard matrix, dependabot), 0 failure, 0 in progress. Those conclusions are the gate verdicts this record reads.

① Derived judgments

  1. One diagnosis point (path 1) — right. answerBlankGate is the CEL route's block lifted verbatim into one private method (same evalFieldPredicate(text, {}, true, undefined, undefined, onFault ? { warn: false, onFault } : { context: 'a CEL gate predicate, read as no gate' }), same return true); the CEL route now reads if (isBlankPredicateText(source)) return this.answerBlankGate(source, options) and the legacy path calls the same method after the envelope unwrap. The two silent returns (if (!condition) for '', if (!trimmed) for whitespace) are gone, and isBlankPredicateText is the only blank test on the file — no second test. Same locator on both routes, so warnPredicateFailure's blank-class key is ["cel", text, "a CEL gate predicate, read as no gate"] for '', a dialect-less blank envelope and a blank CEL envelope alike (toExpression stamps cel on a bare string; an envelope is unwrapped to its source before the test), and the message is one function's. The condition as string cast is sound: the unwrap above turns every object with a string source into that string, so a true isBlankPredicateText after it can only be a string.
  2. No drawn verdict moved — right, on every caller read. Path 1: true in every mode; throwOnError cannot throw (no engine call); undefined / null / junk keep the untouched if (!condition) / Boolean(condition) lines. SchemaRenderer's four visibility legs hand the raw value in (evaluateCondition(raw, …) at both branches); its onFault only calls the deduped reportUnresolvableVisibilityPredicate, and on the enablement leg sets a local flag that gates a dev-only second report — the returned verdict is untouched, and the disabled / hidden chains fold a blank in hasDeclaredPredicate before any evaluator. Path 2: with the early return gone a blank string cannot match LEGACY_DIALECT_MARKERS, so it takes the CEL path like its envelope twin: on the single-eval route evalFieldPredicate answers the blank branch with the caller's fallback and one [blank] line keyed on ["cel", text, label]; on the warnOnError route evalCel's two probes track their fallbacks, ok:false carries the [blank] reason into warnEvalError keyed on [label, text], and the caller's fallback is returned; rowless takes the same guard. That is the value the removed line gave, both fallback directions, so resolveConditionalFormatting (a blank condition: '' still misses), partitionRowsByPredicate (' ' still excludes) and every external caller keep their verdicts; the four that pre-screen pred === '' never reach it. Path 3: non-text (undefined, a number, a boolean, {}) is returned unchanged and silent as before; blank text is still dropped; written text is still attached. Nothing else in the file changed.
  3. Path 3 reports only what it drops — consistent with "on every chain"; no chain is left silent. At main chain (3) — attachVisibility(fd, fd.visibleOn) on an inline runtime field — returned fd itself with its blank still in visibleOn, so the card's "drops on every chain" over-read that chain: nothing was dropped there. The carried blank is evaluated wherever a runtime field is drawn: @object-ui/components renderers/form/form.tsx (three sites, evalFieldPredicate(visibleOn, …, true, …, { context: "visibleOn of field '…'" }) — visible, plus the [blank] line), the wizard's final gate (WizardForm.tsx, same call), and the wizard's steps draw through SchemaRenderer type: 'form' into that same renderer. The existing control packages/components/src/renderers/form/__tests__/visibleWhen-fault-refuses-submit-8069.test.tsx ("a blank view-level visibleOn is a layout GATE: no gate plus a diagnostic, never a refusal (D4)") pins it at the head. Reporting the runtime chain in attachVisibility too would be the second line the direction's "one diagnosis point" refuses; the formField.visibleOn !== expr identity test is exact on that chain (the same reference is passed) for a string and an envelope alike. Residue, not a finding: a spec entry whose base (pooled or member) already carries a blank in a different spelling prints two lines, never none.
  4. The mirrors — right. FieldRulePredicateWireSchema's body is byte-identical to the removed one (only the docblock grew); its three inputs (ExpressionWireSchema, stripImportedDefaults, the spec's EvaluatedExpressionInputSchema) are imported at lines 28–30, above its new line 117, and SelectOptionSchema at line 140 is its first reader — a module-level const read during module evaluation before its declaration throws, so the move is needed, and the name is kept (cited in core's fieldRules.ts line 377). No restated rule: the refinement hands the predicate text to the spec schema and copies its issues. SelectOptionSchema.visibleWhen and FormFieldSchema.visibleOn carry it; BaseSchema's three gates are untouched and their control row ('' and a blank envelope still parse) is kept. Census: not re-runnable here; the head's full test farm and Build & E2E are green, which is what "no stored or first-party fixture turned red" looks like from the gates. The restatement of the 7530 pin is right: the identity row now asserts both mirrors are the triad's refined wire (one object) and that its options are ExpressionWireSchema.options by reference, which is the reuse-by-reference [Decision] The CEL envelope object is accepted on visible / hidden / disabled by the shared evaluator but declared on none of them — declare it on all three, or refuse it on all three #7530 protected, one level down; the "still parse a blank" row recorded exactly the wider reading this direction retires and is replaced by the absent-key control plus a GATE_MIRRORS describe (four blank spellings refused at the key with EVALUATED_EXPRESSION_SOURCE_REQUIRED; non-blank string, CEL envelope and dialect-less envelope pass with the value unchanged; junk still refused).
  5. The FormPage control's move from notes to status — a real, structural interaction, and the control still proves what it proved. FormPage.tsx judges the view-level predicate in isFieldVisible with context: "visibleWhen of field '…'" and its object-level rules through resolveFieldRuleState(…, "field '…'"), whose diag('visibleWhen') builds the identical "visibleWhen of field '…'"; the blank-class dedupe key joins text and locator, warnedPredicates is module state, the file neither resets modules nor clears it, and the stored-blank row (notes: { visibleWhen: '' }) runs first in file order. On notes the control's line would be swallowed by construction. status is a plain text field in BASE_FIELDS with no rule and no default. The control still asserts what it did — a blank VIEW-level visibleWhen on a spec form-view entry is not refused, exactly one write — and adds the [blank] line naming the field. Its corrected comment is true at the head: FormPage builds its own rows (override.visibleWhen ?? override.visibleOn, line 790) and never imports sectionFields.
  6. declaredPredicate.ts — comment-only and needed. Both hunks sit inside docblocks (every changed line is a * line); they quoted if (!trimmed) return true and if (!source.trim()), which this diff removes. Rightly declared as outside the claim's files.
  7. Docs (content/docs/guide/metadata-diagnostics.md) — right. Each sentence holds at the head: a blank gate at runtime is "no gate", never refused, reported once with the [blank] reason (once on every channel — warnPredicateFailure dedupes per text and locator, SchemaRenderer's reporter dedupes in visibilityDiagnostic.ts); the two mirror keys refuse a blank at parse; visible / hidden / disabled parse and are only diagnosed. One wording nit, not a defect: "a form view's own field visibleWhen" and "a form field's view-level visibleOn" are one key in two spellings.
  8. Ablation legs against the pins — arithmetic consistent with the files as written. Path 1 leg: 3 spellings × (verdict-and-said, onFault, throwOnError) + the one-diagnosis-point row = 10. Path 2 leg (early return restored): per route '' + whitespace + one-diagnosis-point = 3, × 2 routes + rowless = 7, with the already-diagnosed { source: '' } rows green. Path 3 leg: 4 dropping chains × 3 spellings + the wizard control = 13, and the FormPage control stays green because that page does not use sectionFields (item 5). Each mirror leg: 4 blank rows + the identity row = 5, pass-through and junk rows green under the plain wire. Every leg's red set is the set its pin owns and nothing else.
  9. PR body — every sentence checked reads true at the head, including: the head-vs-implementation delta is one type annotation in the plugin-form pin (compare of the two commits shows exactly that line); path 1's CEL envelope and path 2's { source: '' } were already diagnosed at main (the base's guard sat on the CEL route and after the string-only early return); the Clause line is line 2 of the body, key-initial. Verification counts are the dev's measurements and are not re-run here; the gates they map to are green on the head.

② Semver level

  • What the diff publishes: @object-ui/types narrows the accept set at two keys — the blank VALUE is refused at SelectOptionSchema.visibleWhen and FormFieldSchema.visibleOn, the shape (both ExpressionWireSchema arms by reference, output unchanged) is not — and adds no public symbol (FieldRulePredicateWireSchema stays a module-private const). @object-ui/core: no signature or type moves; answerBlankGate is private; evaluateCondition and evalRowPredicate return what they returned; console output is added — patch. @object-ui/plugin-form: attachVisibility is module-private; its new import names two exports @object-ui/core already publishes (evaluator/index.js via the package index) from a dependency it already declares — patch. apps/console: a test file only. content/docs: docs.
  • The declaration: Clause-②: no (narrowing) (PR body line 2, and the claim) reads through the one reader as declared, value no, arm narrowing — not a widening, breaking. Under the level axis in check-changeset-no-major.mjs a declared narrowing owes the launch-window grade minor on the moved package, which is what the changeset carries on @object-ui/types; the repo's fixed group carries core, plugin-form and console along at that grade. The five changeset gates on the head are green. The claim's no (narrowing) and minor match the built diff.
  • Clause-②: no (narrowing)

③ Boundary flags

  • open_questions: none filed, and none found.
  • Deviations, each answered: (1) declaredPredicate.ts — accepted (① 6). (2) path 3 narrower than "every chain" — accepted (① 3). (3) the FormPage control moved to status — accepted (① 5). (4) the schema kept its name and moved above its first reader — accepted (① 4). (5) the trailer pair — both commits end with the session-URL trailer and the plain co-author trailer, the model-free pair objectstack's AGENTS.md mandates and objectui's AGENTS.md quotes verbatim; the harness reminder's own precedence clause yields to that rule; no model identifier in the PR title or body, the changeset, the docs or a code comment (scanned) — accepted, no breach.
  • Out-of-scope notes, judged against the filing classes (the card's own gate: class (b), an ADR's stated behaviour not delivered, with a public door measured on at least one path):
    • the pred === '' pre-screens (useRowPredicate, evalRowActionVisibility in plugin-grid and components, evalCreatePredicate), ruleToPredicate's blank expression, and ObjectForm's flat path dropping visible_on: '' — the same class (b) defect as this card, with no measurement; "noted, not filed" with dedupe words is the right carrier for a source read.
    • partitionRowsByPredicate's split by spelling is PRE-EXISTING, not caused or widened by this diff: at main '' was pre-screened to every row in silence and ' ' reached the removed line and returned false in silence (every row excluded); at the head both verdicts are the same and only the whitespace half is audible. Not a finding on this PR. Which verdict a blank bulk gate has ("no gate, all rows" against "fail-closed, none") needs a ruling.
    • the FormPage shared locator (two blank slots on one field print one line) is dedupe granularity, the trade reportBlankGate already documents — the blank is still said; flatFields.ts never carrying visible_on is unmeasured drift. Observations for a card body, not cards.
    • What the seat should file next: ONE successor card for the pred === '' pre-screen family, ordered public-door first — a row action authored visible: '' drawn in the grid kebab with nothing on the console is the measurable door — listing all six sites, and carrying partitionRowsByPredicate's verdict split as the question it also carries (the way this card carried the mirrors), with the two observations in its body. File it once that one door is measured.

Implemented-by: claude/issue-11262-blank-gates-diagnosed
Reviewed-by: session_011p7ikEivgXefNDaE5S5Uec

VERDICT: PASS


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants