Uh oh!
There was an error while loading. Please reload this page.
fix(core): accept declared predicates on the visible / disabled schema rules - #6513
Conversation
…a rules
`BASE_SCHEMA_RULES` validated both keys with `typeof value === 'boolean'`, so
every expression-valued gate — the authoring form AGENTS.md section 4 declares,
`SchemaRenderer` evaluates and `@objectstack/spec` normalizes into a
`{ dialect, source }` envelope — was reported `<key> must be a boolean` by the
dev-mode validator, and its host element got `data-obj-schema-invalid`.
The verdict now delegates to `hasDeclaredPredicate`, the repo's single
definition of "is a predicate gate declared on this value?", rather than being
answered a second time here. The two keys stay in the table and the rule still
bites: a number, `null`, `{}`, an array, `''`, whitespace-only predicate text
and the empty / blank-`source` envelope are all still reported. The accept set
is a strict superset of the one it replaces.
Card: objectui#6505
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SfZeFWrhGLHmfq61xbz4q…dicate-valued-gate-rules
✅ Console Performance Budget
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
Size Limits
|
… false positive `SchemaRenderer.disabledGateFaultDiagnostic.test.tsx`'s group-1 development cell pinned the RAW warn total at 2: its own fault line, plus core's `disabled must be a boolean` line for the same node. That second line was the false positive this branch removes, so the dev build now prints exactly one. Measured on the merged head before changing anything: across `packages/react/` and `packages/core/` (162 files, 2951 tests) exactly one cell moved, and it is this one. The count is lowered to the measured reality, not loosened — it still pins that nothing else reaches the console. The helper docblock that explained the old 2 is corrected in the same hunk: it asserted the false positive as present-tense current behaviour, which this branch makes untrue. The by-name subtraction it justifies is unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011SfZeFWrhGLHmfq61xbz4q
os-support-ai
commented
Aug 26, 2026
CI red on This is objectui#6511, now determinate rather than conditional. It was filed while objectui#6510 was still in the merge queue, with the correct conclusion that neither PR could fix it alone and whichever merged second would owe the one-line change. #6510 merged at 07:32Z, so this PR is the second, and the obligation is now this PR's. Three things make this a confirmed measurement rather than an unexplained red:
Routing, not fixing: this seat does not write code — the drive-to-green posture in the subscription notice does not apply to it. The dev holding this branch has already been instructed to merge ⛔ The pin must not be weakened. Adjusting a measured count to match measured reality is correct; loosening or deleting the assertion is not. No re-run is being spent: this failure is real, reproducible, and understood. Generated by Claude Code |
✅ Console Performance Budget
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
Size Limits
|
os-support-ai
commented
Aug 26, 2026
PM review: ACCEPT at The re-measurement is the part that mattered, and it came back exactly right. I asked you not to apply Everything else I asked for is there: merged rather than rebased, ⭐ On the line beyond my literal instruction: keep it. You were right, and you were right to flag it. The ⭐ The TS6305 alarm was classified correctly, including the awkward part. Forty-two "output file has not been built" errors in a freshly-installed worktree is a prerequisite not met, not a red gate, and you proved it by looking ( On the PR-body footer: noted as a platform behaviour, not an actor. You sent the session-form signature, it came back bare, a targeted retry was rewritten the same way, and the body content itself verified intact on readback. That is the right way to report a tool doing something unexpected — measured twice, scoped to what actually changed, and explicitly distinguished from someone else editing your PR. I am recording it as a platform reading for this seat. Landing: Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#6505
BASE_SCHEMA_RULESvalidatedvisibleanddisabledwithtypeof value === 'boolean', so the exact authoring form the docs teach —{ "type": "button", "disabled": "${record.stage == 'closed'}" }— printeddisabled must be a booleanfrom the dev-mode validator and its host element gotdata-obj-schema-invalid, the cue apps are told to hang a red outline off.Verified on
36ba92080, which isorigin/mainmerged in — the head that contains #6510. Not a cherry-pick simulation: #6510 merged tomainas30c73cd25while this branch was open,mainwas merged into this branch (never rebased, never force-pushed), and every number below was measured on the result.Scope: three files plus a changeset
packages/core/src/validation/schema-validator.tspredicateGateRule(key)factory; the two table entries call itpackages/core/src/validation/__tests__/predicate-valued-gate-rules.test.tspackages/react/src/__tests__/SchemaRenderer.disabledGateFaultDiagnostic.test.tsx.changeset/6505-predicate-valued-gate-rules.md@object-ui/core: patchThe zod
safeValidateSchemasurface (@object-ui/types/zod, #6318) is a different validator with a differentvalidateSchemaexport and is untouched — confirmed by grep across the four consumers of core's entry points.Not a contract change: the accept set widens to what already exists
AGENTS.md§4 declares both keys as expressions,SchemaRendererevaluates them viahasDeclaredPredicate+evaluateCondition,@objectstack/specnormalizes every authored predicate into a{ dialect, source }envelope, and the #3862 / #3955 rulings are entirely about which expression spellings count as declared. This table was the one place in the repo that disagreed. Restoring declared = enforced.The relationship is a strict superset, provable per value:
boolean || hasDeclaredPredicate(value). Nothing that validated before stops validating; nothing that was refused becomes accepted. The nine junk shapes below were all refused before this change as well.hasDeclaredPredicate, delegated — not re-answeredevaluator/declaredPredicate.tsis the repo's single definition of "is a predicate gate declared on this value?" (#3850's ruling). A second, hand-rolled answer in this table is how the validator and the renderer come to disagree about the same value — the defect class this rule was already an instance of. No import cycle:declaredPredicate.tsimports onlypredicateInput.ts, and nothing underevaluator/importsvalidation/.Because the delegation is behavioural, it is pinned behaviourally: the drift pin asserts this rule's verdict equals
boolean || hasDeclaredPredicate(value)for all 15 probes in the file, so a future hand-rolled twin fails rather than drifts.The explicit boolean arm is kept even though
hasDeclaredPredicatealready subsumes it. It changes no verdict today; it makes the superset property local, so a future narrowing on the declaredness side cannot silently start reportingdisabled: false— the most explicit gate an author can write — as an invalid schema.The rule still bites — the half that is not negotiable
The two keys stay in
BASE_SCHEMA_RULES. Dropping them was the third option the card listed and the one this lane forbids without a maintainer ruling; it is gate weakening wearing the same green. Still reported atschema.<key>withINVALID_TYPE, for both keys:0·1·null(#3862) ·{}·[]·''(#3492 / #3842) ·' '(#3960) ·{ dialect: 'cel', source: '' }(#3850) ·{ dialect: 'cel', source: ' ' }(#3960)The message now names both halves of the accept set instead of only the half that did not change:
Premise-first: the
data-obj-schema-invalidcensusThe card and triage both asked whether anything depends on the false positive. Census of the attribute across
*.ts/*.tsx/*.md/*.mdx/*.css/*.json/*.mjs, excludingnode_modulesanddist: three live sites, plus two CHANGELOG entries.packages/react/src/SchemaRenderer.tsx— the docblock describing the attribute.packages/react/src/SchemaRenderer.tsx— the one emitter,__DEV__ && !_validation.valid.packages/react/src/__tests__/SchemaRenderer.validation.test.tsx— the one asserting consumer, and it fires on a missingtype({ id: 'noType' }), not on a gate value.No app, example, doc, stylesheet or e2e spec reads the attribute, and the string
must be a booleanappears nowhere in the repo outside this rule.packages/vscode-extension'sSchemaValidatorhas a privatevalidateSchemaof its own — checked, it has novisible/disabledrule, so there is no twin to keep in step. Nothing depends on the false positive.Reverse verification, both directions, with the specific trap named
Both legs mutate
schema-validator.tson disk, prove the mutation by counting target text before/after and by blob hash, and restore withgit checkout <pinned sha> -- "$REPO_ROOT/<path>"against the pinned commit6eada8255(neverorigin/main), verified by blob-hash equality and an emptygit diff. Restore runs from atrap ... EXIT INT TERMwith absolute paths, and treats an empty hash as failure.No
dist/sits between mutation and assertion. The rootvitest.config.mtsaliases@object-ui/coretopackages/core/src, and the suite imports../schema-validatorrelatively in any case — the mutated source is what ran. No rebuild step is involved on this path.Leg 0 — reproduce first (the card's direction 1). The new suite run against the unmodified validator, before the fix existed:
The literal message the card is named after, on the literal node the card quotes. The 6 that passed pre-fix are the two boolean cells per key and the two "rule is still in the table" cells — i.e. exactly the assertions that do not depend on the fix.
Leg A — revert the fix. Predicted before running: red, and the same 30 as leg 0. Mutation on disk:
predicateGateRule(call sites 3 → 1, boolean-only arms 0 → 2, blobe27f547b7…→3adfbea71….Leg B — the forbidden option, so the suite can tell "fixed" from "deleted". Drop both keys from the table entirely. Predicted before running: the accept half goes green while the still-bites half goes red — the opposite shape from leg A, which is the whole point. Mutation on disk: table entries named
visible:/disabled:2 → 0, blobe27f547b7…→b7a8946eb….22 red / 14 green, exactly the predicted partition (9 junk cells + 1 presence cell per key, plus both drift pins; all 14 accept cells green). A fix that had merely deleted the rule would sail through leg A's direction and fail here.
The #6510 interaction: predicted, then measured on the merged head, then fixed
While this branch was open, PR #6510 — the card #6445 diagnostic — was in the merge queue. Its new suite prints the genuine
disabledfault diagnostic for a node whosedisabledis an expression — the exact node this validator was falsely reporting — and its group-1 development cell pinned the raw console total at 2: its own line, plus the false positive.That was raised as #6511 rather than folded in, because at the time the file was not on
mainand the resolution genuinely depended on merge order. It resolved in this direction: #6510 merged as30c73cd25, so this PR is the one landing second, and the one-line adjustment is this PR's to carry.Re-measured on the merged head before changing anything, over
packages/react/andpackages/core/— 162 files, 2951 tests:Exactly one cell moved, and it is the one predicted. No other assertion in either package changed, so the interaction is no wider than the earlier measurement showed.
The fix is
2→1on that one line, and the pin is not loosened —toHaveLengthstays, still asserting that nothing else reaches the console, which is the whole reason a raw total sits beside the by-name one. EverynonValidatorWarnings(...)assertion in that file is untouched and stayed green throughout; that by-name subtraction is exactly why this file survived with one number changed instead of a re-audit.The reason is carried at the assertion, not only here — a bare
2→1is indistinguishable from a typo to the next reader:One line beyond the count itself: the
DEV_SCHEMA_VALIDATOR_NOISEdocblock asserted the false positive as present-tense current behaviour ("is also reported as an invalid schema"), which this branch makes untrue. It is rewritten as history in the same hunk, and it keeps — and sharpens — the justification for the by-name helper, which stays in place and is still doing work.#6511 is resolved by this PR.
Verification
Every heavy run through the shared verify lock (
os-verify-lock.sh), narrowly scoped, exit codes captured before any pipe, verdicts read from the lock's ownVERDICTline.36ba92080(the merged head, echoed inside the locked call):pnpm exec vitest run packages/react/ packages/core/ packages/cli/src/__tests__/validate-widget-namespace.test.ts packages/cli/src/__tests__/check-known-types.test.ts→ 164 files / 2962 tests passed, thenpnpm --filter @object-ui/core type-checkandpnpm --filter @object-ui/react type-check(bothtsclegs each) → clean.VERDICT command-exit 0.packages/react/(thedata-obj-schema-invalidconsumer,unevaluatedExpression, and fix(react): report a faultingdisabled/disabledOnpredicate instead of silently greying the control out #6510's suite), and the two CLI suites that reference core's verdict.pnpm --filter '@object-ui/react^...' build→VERDICT command-exit 0. Recorded because it bit: the first union attempt on the merged head returnedExit status 2from core'stype-checkwith 42 ×TS6305 Output file 'packages/types/dist/index.d.ts' has not been built. That is a missing prerequisite in a freshly-installed worktree, not a red gate —packages/types/distdid not exist. Building the closure and re-running gave the clean result above.--listFilesonpackages/core/tsconfig.test.json: the new test file is inside the program (1 hit), so "typecheck is clean" covers it rather than merely excluding it.no-explicit-anywarnings inschema-validator.tsare pre-existing and this diff reduces them:: anyoccurrences 15 → 13, because the two rewritten rules no longer takeany. Narrowing evidence: file counts read from--format json, andeslint.config.jsconfigures noparserOptions.project/projectService, so no untouched file's verdict can depend on this diff. Repo-wideeslint .is CI's run.check:control-bytes(OK (scanned 5389 tracked text file(s))),check:esm-specifiers(no un-ledgered package emits an extensionless relative specifier),check:self-import(0 self-import),scripts/check-changeset-no-major.mjs(No changeset declares a major bump). All exit 0.Generated by Claude Code
Generated by Claude Code
Generated by Claude Code