Repository navigation
Commit 58da8ae
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)
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](https://claude.ai/code/session_011p7ikEivgXefNDaE5S5Uec)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 1b30c0f commit 58da8ae
12 files changed
Lines changed: 612 additions & 117 deletions
File tree
- .changeset
- apps/console/src/components
- content/docs/guide
- packages
- core/src/evaluator
- __tests__
- plugin-form/src
- types/src
- __tests__
- zod
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
159 | 159 | | |
160 | 160 | | |
161 | 161 | | |
162 | | - | |
163 | | - | |
164 | | - | |
165 | | - | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
166 | 174 | | |
167 | | - | |
| 175 | + | |
168 | 176 | | |
169 | | - | |
| 177 | + | |
170 | 178 | | |
171 | 179 | | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
172 | 183 | | |
173 | 184 | | |
174 | 185 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
186 | 186 | | |
187 | 187 | | |
188 | 188 | | |
189 | | - | |
190 | | - | |
191 | | - | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
192 | 198 | | |
193 | 199 | | |
194 | 200 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
362 | 362 | | |
363 | 363 | | |
364 | 364 | | |
365 | | - | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
366 | 380 | | |
367 | 381 | | |
368 | 382 | | |
| |||
372 | 386 | | |
373 | 387 | | |
374 | 388 | | |
375 | | - | |
376 | | - | |
377 | | - | |
378 | 389 | | |
379 | 390 | | |
380 | 391 | | |
| |||
420 | 431 | | |
421 | 432 | | |
422 | 433 | | |
423 | | - | |
424 | | - | |
425 | | - | |
426 | | - | |
427 | | - | |
428 | | - | |
429 | | - | |
430 | | - | |
431 | | - | |
432 | | - | |
433 | | - | |
434 | | - | |
435 | | - | |
436 | | - | |
437 | | - | |
438 | | - | |
439 | | - | |
440 | | - | |
441 | | - | |
442 | | - | |
443 | | - | |
444 | | - | |
445 | | - | |
| 434 | + | |
446 | 435 | | |
447 | 436 | | |
448 | 437 | | |
| |||
480 | 469 | | |
481 | 470 | | |
482 | 471 | | |
| 472 | + | |
| 473 | + | |
| 474 | + | |
| 475 | + | |
| 476 | + | |
| 477 | + | |
| 478 | + | |
| 479 | + | |
| 480 | + | |
| 481 | + | |
| 482 | + | |
| 483 | + | |
| 484 | + | |
| 485 | + | |
| 486 | + | |
| 487 | + | |
| 488 | + | |
| 489 | + | |
| 490 | + | |
| 491 | + | |
| 492 | + | |
| 493 | + | |
| 494 | + | |
| 495 | + | |
| 496 | + | |
| 497 | + | |
| 498 | + | |
| 499 | + | |
| 500 | + | |
| 501 | + | |
| 502 | + | |
| 503 | + | |
| 504 | + | |
| 505 | + | |
| 506 | + | |
483 | 507 | | |
484 | 508 | | |
485 | 509 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
0 commit comments