fix(sdui-parser): an html page literal of the wrong type is a compile error, not a warning - #21678
Conversation
…h error Every value reaching checkType is a literal (a braced value the parser cannot materialize is diverted to inert-expression first), so its coarse type is final at compile time, as certain as an enum's closed list. aggregate="count" on object-metric now fails the compile instead of passing os build with a warning. checkMemberTypes follows the same rule: members of a materialized literal are literals too. Claude-Session: https://claude.ai/code/session_01VDtqoecgES7ScQYGbFVDRv Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VDtqoecgES7ScQYGbFVDRv Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 3 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 48a02bf296fdd99115fce63e7d13d274770d92d1 && git checkout 48a02bf296fdd99115fce63e7d13d274770d92d1
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin eed2dee481126b6655cfb4a809099aa885c8e52b 79df1db84fb82c96586f224a63c40d7c065c71eb && git checkout -B drift-repro eed2dee481126b6655cfb4a809099aa885c8e52b && git merge --no-ff 79df1db84fb82c96586f224a63c40d7c065c71eb
node scripts/docs-audit/affected-docs.mjs --json eed2dee481126b6655cfb4a809099aa885c8e52b |
Contract reviewServed-tier: ① Changeset
|
Fixes #21671
Clause-②: no (narrowing)
What changed
The html-tier compiler (
@objectstack/sdui-parsercompile) now grades everytype-mismatchaserror, not only the ones whose input declares anenumarm.aggregate="count"on anobject-metric(repo-rootsdui.manifest.jsondeclaresaggregateastype: "object") now fails the compile, soos buildfails on it. Before, it compiledokwith one warning, the build stayed green, and the tile drew no number. The diagnostic code and message are unchanged. No new code and no new gate.The mechanism, measured
The brief assumed that literals and expressions both reach
checkType, and that a signal would have to be passed in to tell them apart. Measured on93a54b87e7, that is not how it works:validateTree(packages/sdui-parser/src/validate.ts) sends a value thatisExprmatches (the parser's deferred$exprmarker) toinert-expression, which is awarning. It callscheckTypeonly in theelsebranch. So every value that reachescheckTypeis already a literal: a quoted attribute (a string), a bare attribute (true), or a braced value thatinterpretBracematerialized in full.interpretBraceis all-or-nothing. Probed:["a", foo],{"a": foo}and{a: 1, b: x.y}each become ONE$exprmarker;[1,2]and{function: "count"}materialize in full. So a container never reaches the type check with an expression inside it.So the expression case is already separate before
checkTyperuns, and no new signal is needed. The change is the severity, plus a restated header that names the second certain fact (a literal's coarse type) next to the enum's closed list. A braced expression still gets theinert-expressionwarning, and a test pins that.checkMemberTypesfollows the same rule (the brief left this to measurement). Its members come from a container that was materialized in full, so each member is a literal too, and a member that no declared arm accepts is just as certain a mismatch. Its header already said "Severity mirrorscheckType's rule", and that stays true.member-type-mismatchis nowerror.Unchanged on purpose: the single-arm
invalid-enumdiagnostic is byte-identical, severity included (the existing pin inunion-arm-type-mismatch.test.tspasses untouched).Census (first step): every stored html page source, compiled against the committed
sdui.manifest.jsonIt was run with
compilefrom@objectstack/sdui-parser(source), not with grep. The page modules were imported and eachkind: 'html'export'ssourcewas compiled (capability-map.page.tsinterpolates, so a regex would have read a different string). Every fenced block inskills/**/*.mdandcontent/docs/ui/*.mdxthat has a lowercase tag was compiled too. A positive control (aggregate="count") was compiled in the same run.examples/app-showcase/src/ui/pages/command-center-jsx.page.ts(CommandCenterJsxPage)examples/app-showcase/src/ui/pages/capability-map.page.ts(CapabilityMapPage)examples/app-showcase/src/ui/pages/start-here.page.ts(StartHerePage)skills/objectstack-ui/rules/pages.md:154block (its line 160 is theobject-metricexample)aggregate={{"function":"count"}}, so PR #21667's fix is confirmed onorigin/mainkind: 'html'page in this repo (bothpackages/metadata/src/__fixtures__/hotcrm-*.artifact.jsonhave 0)skills/**andcontent/docs/ui/**no-rootorforbidden-tag, which shows they are not html-tier sources)aggregate="count"ok=true,[warning] type-mismatch; after:ok=false,[error] type-mismatchThere are zero writers to fix, so no example or skill file changes in this PR.
Pins (
packages/sdui-parser/src/__tests__/literal-type-mismatch-error.test.ts)The inputs are copied verbatim from the tracked
sdui.manifest.jsonand written inline, so the test reads nothing outside its package.aggregate="count": exactly one{ severity: 'error', code: 'type-mismatch', message: 'object-metric prop "aggregate" expected an object' }(the real message has the tag in angle brackets), andok === false.aggregate={{"function":"count"}}: zero diagnostics,ok === true.number(object-kanbanlimit), aboolean(invert) and anarray(filter) input: each gives oneerrortype-mismatch, andok === false.aggregate={count}), and a container that holds an expression: each gives exactly onewarninginert-expression, notype-mismatch, andok === true.Three existing pins described the old rule, and they were updated: the non-enum union case and the single string-arm case in
union-arm-type-mismatch.test.ts, and the member severity inmember-type-mismatch.test.ts. Their codes and messages are unchanged. Only severity,ok, and the wording that called these "byte-identical" were edited.os buildprobe (the html-tier path throughpackages/cli/src/utils/sdui-manifest.ts)A scratch project with one
kind: 'html'page, the committedsdui.manifest.jsoncopied beside its config, and the CLI run from source (bin/run-dev.js build):aggregate="count"aggregateprop expected an object,Build completeAuthor-time rules failed (1 issue), a failure that theaggregateprop expected an objectaggregate={{"function":"count"}}Build completeThe before leg is a one-off ablation run from the committed fix. It used
scripts/ablation-replace.mjs(anchor hit, 1 marker on disk), thenpnpm --filter @objectstack/sdui-parser build, thenablation-dist-preflight.mjs(marker present indist/, exit 0). After the probe it was restored withgit checkout HEAD --:git diff HEADwas empty and the blob matched HEAD (86cc5784). The package was rebuilt, the--absentpreflight passed for both readings, and the probe was re-run with exit 1. No permanent test file was left behind.Tests run (at
79df1db84f)pnpm --filter @objectstack/sdui-parser test: 14 files, 225 tests passed.typecheck: exit 0.sdui-parser(distchecked: 0 copies of the old ternary left).pnpm --filter @objectstack/lint exec vitest run: 119 files, 5627 tests passed.pnpm --filter @objectstack/metadata-protocol exec vitest run: 209 files passed and 3 skipped, 3463 tests passed. CLI unit tier, limited to the 4 files that compile html pages (src/utils/sdui-manifest.test.ts,test/validate-build-gate-parity.test.ts,test/platform-page-i18n-parity.test.ts,test/i18n-section-coverage.test.ts): 129 tests passed. The rest of the CLI unit tier and its integration tier are left to CI.node scripts/pm/dispatch-gates.mjs --commandsgave 63 derived commands. 61 exited 0, includingcheck:sdui-lockstep,check:nul-bytes,check:cross-package-test-inputs,check-adr-0087-registrationandcheck-changeset-no-major. NOT MEASURED:check:dual-build-cjs-loads:PREREQUISITE NOT MET, becauseembedder-openaiandservice-cluster-redishave nodist/(packages this diff does not touch). NOT MEASURED:check:type-check-debt: it is a whole-tree tsc ratchet and hit the 240s local timeout. CI runs both..tsfiles, with--no-inline-config --format json: 4 files, 0 errors, 0 warnings. The repo config has noparserOptions.project(type-aware linting is off), so this diff cannot change the lint result of any file it does not touch. CI runs the fullpnpm lint.Changeset
.changeset/21671-html-literal-type-mismatch-error.md:@objectstack/sdui-parserminor. Clause-② conflict for the seat to resolve: the claim statesClause-②: no, and this body carries that line verbatim. The changeset declaresClause-②: no (narrowing), because a page that used to compile (and save, where the host has a component manifest) is now refused. Earlier PRs treated a new refusal at an authoring door as an accept-set narrowing (21459,20827). The changeset therefore carries the**BREAKING**header, aminorbump under the launch-window convention, and an ADR-0087not-required (no-migration-prescription)disposition: no key, declaration or stored shape moves.check-adr-0087-registrationandcheck-changeset-no-majorboth pass, whether the body line has the arm or not (both were simulated locally).objectui lockstep (declared, not acted on)
objectui has its own copy of this validator,
packages/sdui-parser/src/validate.ts, at the.objectui-shapinab187972. It still has the old ternary in bothcheckMemberTypes(:418) andcheckType(:465). After this PR the two copies agree on codes, messages and the accepted grammar, and differ only in severity.check:sdui-lockstepcompares grammar, codes and the containment predicate, not severity, so it passes. This repo's copy is the stricter one (save gate andos build). The dangerous direction, a page that saves clean and then renders inert, cannot come from this difference. The lockstep header invalidate.tsnow records this lead. The port belongs to objectui's lane, and this PR does not write to objectui.Acceptance notes
label={42}). That was the narrower reading in the triage title ("a string literal against a declared non-string input"). The claim and the brief specify the general rule ("a literal whose coarse type no declared arm accepts"), and the measurement shows that every value at this point is a literal, so the general rule is the one implemented. The census found no writers of either form in the repo.Generated by Claude Code