feat(cli): objectui check refuses a ${…} on a text key its node never evaluates (objectui#4795) - #11126
Conversation
… evaluates (objectui#4795)
Ruling item 2: a `${…}` in `title` / `label` / `value` / `description` on a
component node whose `expressionBindableTextKeysFor(type)` answer excludes
that key is refused, no-row types included. The walk follows the root and
`children` only (sub-rule i; the page document root's own keys are page
keys), and a type no registered component answers to warns instead
(sub-rule ii).
The vocabulary and the carriage map are imported from `@objectstack/spec`,
now a declared dependency of the CLI, and the type string is passed
verbatim, as SchemaRenderer's evaluation memo passes it. A runtime-agreement
suite renders every registered type through the real SchemaRenderer and
compares what it evaluated with what the gate refuses.
Claude-Session: https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm
Co-authored-by: Claude <noreply@anthropic.com>
…hangeset (objectui#4795)
The `objectui check` page said only unreadable JSON fails the run; a refused
`${…}` on a text key its node never evaluates now does too. The page names
the rule, the two sub-rules and the real output, and the changeset declares
the new refusal as a minor for `@object-ui/cli`.
Claude-Session: https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm
Co-authored-by: Claude <noreply@anthropic.com>
…in the unit tier (objectui#4795) The DOM suite that rendered every registered type through SchemaRenderer imported `@object-ui/react` from a CLI test, which turns the CLI's generated-source allowance for that dependency stale in `check:unused-deps`. The agreement is now pinned in two halves against the same verbatim `expressionBindableTextKeysFor(type)`: the runtime's by the `@object-ui/react` suite, and the gate's here over every registered type, with the namespaced spellings a prefix-stripping gate would get wrong derived and asserted non-empty. Claude-Session: https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm Co-authored-by: Claude <noreply@anthropic.com>
|
changeset-claim-re-read
|
✅ 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
|
…bjectui check passes on this repository (objectui#4795)
Its `value` used template literals, which the new `objectui check` refusal
reads as unevaluated expressions: `code-editor` has no carriage row, and
ruling 5479466110 includes no-row types. The sample now builds the same
strings by concatenation and logs and returns exactly what it did before.
The docs page and the changeset state the limitation this exposes: a
`${…}` meant as literal text on one of the four keys is refused, with no
escape spelling.
Claude-Session: https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm
Co-authored-by: Claude <noreply@anthropic.com>
✅ 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
|
Contract reviewServed-tier: Director seat's at-tier review (objectstack#12708, on the maintainer's 「项目总监契约复审」, 2026-09-30) — the review this PR was drafted for. Inputs: card objectui#4795 (body; the maintainer's rulings 5406135987 and 5479466110; the claim 5893308301; the two os-dev-reports 5894488882 / 5894716866 and the seat's ACCEPT 5894769255, read as claims to test, not as findings); PR #11126 (body, 8 files, +654/−8, the net diff against objectui Check-runs on the head, read 2026-09-30T03:2xZ: 45 runs — 42 ① Derived judgmentsAgainst ruling 5479466110 (item 2's scope = the carriage map; sub-rules (i) and (ii); location
② Semver levelClause-②: yes — as the maintainer ruled on 5406135987 ("both halves change accept/reject behaviour"): ③ Boundary flags
Implemented-by: VERDICT: PASS State: |
…jectui#4795) Brings in PR #11086 (objectui#11073), which moves the workspace to @objectstack/spec 17.5.0 and zod 4.6.5. The textual merge is clean; the CLI importer's lockfile entry still names the 17.4.0 snapshot main no longer carries, and the next commit regenerates it. Claude-Session: https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm Co-authored-by: Claude <noreply@anthropic.com>
…he 17.5.0 snapshot (objectui#4795) `pnpm install` after merging origin/main: the `packages/cli` importer's `@objectstack/spec` (`^17.1.0`) now resolves the same `17.5.0(ai@7.0.65(zod@4.6.5))` snapshot as the rest of the workspace. The 17.4.0 snapshot it named is gone from main since PR #11086, which is what broke `pnpm install --frozen-lockfile` in the merge group. Generated, not hand-edited; no manifest moved. Claude-Session: https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYm Co-authored-by: Claude <noreply@anthropic.com>
✅ 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
|
|
Regen-provenance: 5903644098 · c985c4d → 4f29cc3 · git merge origin/main (81f8498) → pnpm install (pnpm-lock.yaml regenerated) From the
The PR is re-enqueued once CI on |
Fixes #4795
Clause-②: yes —
objectui checknewly refuses${…}in the four closed keys whereexpressionBindableTextKeysFor(type)excludes the key, as ruled (5406135987,5479466110). The PR waits as a draft for the director seat's contract review.Regen-provenance: 5903644098 · c985c4d → 4f29cc3 · git merge origin/main (81f8498) → pnpm install (pnpm-lock.yaml regenerated) — non-lockfile
git patch-id --stableunchanged (fddcc0fbfcd30e517fa521b4af5cf528c5b4cf55); details in comment 5903953260.Ruling item 2 of #4795 (maintainer, 2026-08-31, comment
5479466110), built inobjectui checkas ruled. Draft for the director seat's contract review.The repository's own
pnpm check: red in round 1, resolved by the seat's answer A.github/workflows/lint.ymlrunspnpm checkon this repository (step "Verify the CLI's own check command passes on this repository"). Measured with the CLI built from each tree:pnpm checkexitmaind1e683fa1(no gate)ed32e7c6bc985c4d7dThe round-1 refusal. It was
examples/schema-catalog/src/schemas/plugin-editor/javascript-editor.json: a rootcode-editorwhosevaluewas a JavaScript sample using template literals. That is literal content, not a misplaced expression, and it had no mechanical channel move.The fix. The seat chose A under ruling
5479466110: 「no-row types included」, and 「A row can always be added later with a named need + its own measurement」. The sample now builds the same strings by concatenation. Run, it still logsHello, Developer!and returnsWelcome, Developer.Nothing pinned the old content. No test, snapshot or docs fence reproduces it:
safe-validate-corpus-6318.test.tsasserts only that it validates;catalog-gallery-render.test.tsxskips it, because Monaco cannot load under happy-dom;plugin-editor.mdxembeds it by id throughSchemaExample.The run's closing tally is the same as on
main:265 validated, 167 recognised but not validated, 1 did not validate.What it does
findUnbindableTextExpressions(new,packages/cli/src/utils/unbindable-text-expressions.ts), called bycheckon every recognised file, from either recognition arm:EXPRESSION_BINDABLE_TEXT_KEYSon a component node is judged againstexpressionBindableTextKeysFor(type). Both are imported from@objectstack/spec/ui: the vocabulary and the carriage map are consumed, not copied.childrenholds (an array or a single node), recursively. A root of typepageis not judged itself, because itstitleis a page key; itschildrenstill are.ExpressionEvaluator.evaluatematches it. A lone${or an empty${}is not an expression there, so it is not one here.isKnownSchemaTypeuniverse) is refused: a redx Unevaluated expression in FILE at PATH: …line plus a channel line, and it counts toward the run'sFound N errors, so the exit code is 1. A type no registered component answers to gets a yellow⚠️ Expression not judged in …line, and the run still passes.describeFirstIssueshape objectui#11007 / PR fix(cli): objectui check prints the refused key and path, and never calls an unvalidated file passed (objectui#11007) #11053 gavecheck. The path is spelled byformatIssuePath, the formatterobjectui validateprints with. It names the key, the path, the type, the keys that type does evaluate, and the channelsSchemaRendererevaluates: the type's own evaluated keys,content,properties.KEY(evaluated, then hoisted onto the node), or resolving the value in the host.@objectstack/specbecomes a declared dependency of@object-ui/cliat^17.1.0. That is the range@object-ui/reactdeclares, and@object-ui/reactis the runtime consumer of the same lookup. The lockfile gains only the importer entry, which resolves the installed version.Docs:
content/docs/utilities/cli.mdxgains the rule, the two sub-rules and the real output. It also says a refused expression now fails the run, where the page used to say only unreadable JSON did, and that a literal${…}has no escape spelling (the changeset says so too). Changeset:@object-ui/climinor.Premises re-derived on
maind1e683fa1@objectstack/spec, and the installednode_modules/@objectstack/spec/package.jsonreads 17.4.0. It exports both symbols. Its answers:statisticgiveslabel/value/description;cardgivestitle/description;buttongiveslabel;text,action:button,ui:cardandpagegive the empty set.check:spec-floorsfetched the declared-floor tarball and passed: the CLI artifact's two./uisymbols are present at the floor. Nothing here depends on 17.5.0.SchemaRenderercallsexpressionBindableTextKeysFor(typeof newSchema.type === 'string' ? newSchema.type : ''), verbatim, with no prefix stripping. The comment "keys on the bare registry name" describes the spec map's keys; it does not describe a normalization. The gate passesnode.typeverbatim.SchemaRenderer(ate2f8e48d9; the gate's code is unchanged since): every one of the 649 registered types was rendered with the four keys set to${data.total}. On every type, the keys it left unevaluated equalled the gate's refusals, 0 disagreements. The types it evaluated anything on were exactly those with a row.@object-ui/reactturns the CLI's generated-source allowance for that dependency stale incheck:unused-deps(DECLARED_WITHOUT_IMPORT). That ledger is outside this claim's file surface.@object-ui/reactsuiteSchemaRenderer.bindableTextKeys.test.tsx("a namespaced spelling is not silently normalized").SchemaRendererdoes not recurse on its own. Each renderer hands keys back throughrenderChildren/renderNodeSlot/ a directSchemaRenderer.fields[]entries are rendered by the form'srenderFieldComponent(field widgets underfield:*), never throughSchemaRenderer.checkjudged the root only before this change.childrenalone. That is the one composition keyBaseSchemadeclares, and the same single spelling core'svalidateChildrenand the SDUI parser'sCHILD_LIST_KEYfollow.typethat are not nodes sit underformfields[],filter-builderfields[],gridcolumns[],dashboardwidgets[]/globalFilters[], and evenobject-grid'scolumns[].summary({ "type": "count_unique" }) andselection({ "type": "multiple" }). A walk over every typed object would judge all of them.stat-cardwarns and the run passes.text.value,action:button.label,card.labelandui:card.titleare refused.statistic.value,card.titleandbutton.labelpass.objectui validateandobjectui checkjudge through the strict authoring face (objectui#5250, slice A) #11069. Not read from and not edited toward. The second of the two to land mergesmain.Tests
packages/cli/src/__tests__/check-unbindable-text-expression-4795.test.ts(unit tier, 28 tests). Expected text is derived from the command's own formatters, fed from the gate's real findings. It covers:${and an empty${};children(array and single node) and multiple refusals per file;titleexclusion, with the page's children still judged;labelnon-refusal, with a control: the same object underchildrenIS refused;stat-cardwarning;pnpm exec vitest run packages/cli/atc985c4d7d, under the verify lock:Test Files 23 passed (23),Tests 330 passed (330).pnpm exec vitest run examples/schema-catalog/atc985c4d7d, under the verify lock:Test Files 34 passed (34),Tests 2199 passed (2199). Within it,safe-validate-corpus-6318.test.tspassesplugin-editor/javascript-editor validates unchanged.Ablations
Each direction was predicted before the run. Mutations went through
ablation-replace.mjs: in each run the anchor went 1 to 0, the marker 0 to 1, and the restore proved blob == HEAD withgit diff HEADempty. All three were re-run oned32e7c6b. Round 2 touched neither the gate nor its tests, so these readings stand.warning). Predicted: the refusal pins go red. Observed:15 failed | 13 passed. The passes, the page-root exclusion, thestat-cardwarning and the unrecognised-file pins stayed green.type. Predicted: only the form-field pin goes red. Observed: exactly that,1 failed | 27 passed, and the failure prints the refusal atfields → 0 → label.ui:card.titlego red. Observed: 5 red, those 3 plusaction:button.labeland "refuses every offending node".action:buttonstrips tobutton, which has alabelrow. The direction matched the prediction, with two more reds than predicted, both explained.Gates (exit codes; at
c985c4d7dunless noted)Heavy runs (builds, test suites,
pnpm check, the doc compile gates,check:node-esm-load) went through the shared verify lock in round 2.type-check0 ·lint0 (0 errors; 5 warnings, all in files this PR does not touch) · tests 0 (above).pnpm check0: 0 refusals, 0 warnings.check-changeset-presence0 ·check-changeset-no-major0 ·check-changeset-overwrite0 ·check-changeset-fixed0 ·check:changeset-claims0 (report-only; it names the same five pending changesets as round 1, and none is falsified by this diff) ·check:pending-changeset-literals0.check:control-bytes0 ·check:new-line-citations0.check:phantom-deps0 ·check:unused-deps0 ·check:lockfile-integrity0 ·check:lockfile-dedupe0. Ated32e7c6b(round 2 moved no manifest and no CLI source):check:spec-floors0 ·check:installed-pin-claims0 ·check:esm-specifiers0 ·check:spec-symbols0 ·check:self-import0.check:doc-snippets0 (681 of 681 blocks) ·check:doc-examples0 ·check-doc-component-types0 ·check-prompt-component-keys0 ·check-doc-example-ids0 ·check-doc-fence-languages0 ·check-doc-links0 ·check-doc-expression-carriage0 (report-only) ·toc-anchor-parity.test.tsx15 passed.ed32e7c6b(round 2 touched no source or test file):check-lint-coverage0 ·check:test-path-roots0 ·check:vi-mock-*(three) 0 ·type-check:coverage0 ·check:unreferenced-sources0 ·check:dist-completeness0 ·check:published-dist0 ·check:shell-escape-residue0.check-governed-queue-guard --testover the eight paths: NOT GOVERNED ·check:governed-queue-guardself-test 0 (ated32e7c6b).check:node-esm-load: 1, left to CI. Under the lock atc985c4d7d, it again refused@object-ui/authand@object-ui/react-runtimeon provenance: the turbo cache is shared across worktrees and replayed another tree's build. The@object-ui/clientry was among the 32 graded loads. CI builds fresh.Acceptance notes
${…}in a closed key of a type with no carriage row is refused, and nothing lets it through. That covers a code sample in acode-editorvalueand shell${VAR}text.trigger,header,footer, a page'sregions[].components[], a tab'sitems[].content,panels[].contentand carouselitems. No declaration of per-renderer node slots exists to derive them from. The boundary fails quiet in the safe direction: it never produces a false refusal.badge.text) are not judged.5479466110set item 2's scope to the carriage map, which narrows5406135987's "any key outside that closed list". Theprops-envelope blank render is outside that scope too.checkreads.jsononly..yaml/.ymlare globbed and never read, ascheck.tsalready stated.check.ts. The objectui#11007 triage ruling declined a nested validation walk incheck. Its pin "no nested walk of its own" stays green, because this walk validates nothing and reads only the four keys. Named here so the reviewer sees both rulings.text.value, whichTextSchemaretired and whose renderer readscontentalone,properties.valueis evaluated and hoisted but not rendered. The seat kept this wording (answer A, one vocabulary with the runtime diagnostic); the contract review may still choose B, which would be a one-line change inworkingChannels.SchemaRendererwas measured once over all 649 registered types, with 0 disagreements. The committed pins are the two verbatim-lookup halves. The seat chose not to add the DOM suite (answer A), so the CLI's@object-ui/reactallowance incheck:unused-depsis untouched.Session:
https://claude.ai/code/session_012UwY3ahMixEFkfTUxMVkYmGenerated by Claude Code