Repository navigation
fix(lint)!: refuse a page component whose properties is present but not an object, nested ones included - #25067
Conversation
… but not a record Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz Co-authored-by: Claude <noreply@anthropic.com>
…sted-props-non-record
…sted-props-non-record
📓 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 — 4 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 5b1ea5cbcb99e2a9d86d3a66e73daa3dc4a452e0 && git checkout 5b1ea5cbcb99e2a9d86d3a66e73daa3dc4a452e0
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 688052e147f23a15e127070ff1bc7c1572de09ab 88d3dd057289261d4c6d95a86ca96c7823ff68c9 && git checkout -B drift-repro 688052e147f23a15e127070ff1bc7c1572de09ab && git merge --no-ff 88d3dd057289261d4c6d95a86ca96c7823ff68c9
node scripts/docs-audit/affected-docs.mjs --json 688052e147f23a15e127070ff1bc7c1572de09ab |
Seat review — PASS ·
|
Closes #24991
Clause-②: no (narrowing:
os lint --strictrefuses a nested page component whosepropertiesisnullor an array)What
validateComponentProps(packages/lint/src/validate-component-props.ts) now refuses a walked page component, top-level or nested, whosepropertiesis present but not a record (null, an array, a scalar). The refusal is onecomponent-props-invaliderror at the node's ownpropertiespath, for examplepages[0].regions[0].components[0].properties.children[0].properties. The message names what was received.type, registered or not: the bag's shape is the node's contract, not a props schema's. An unregistered type with an object bag is still skipped, as before.{}), unchanged from the absent-bag fix in lint: fiveos lint --strictblind spots measured against firing controls — aliasedctx.apiin hook handlers, action-body free identifiers, unbound visibility roots, lookup-bound detail grants, empty record:details sections #22212.Why
PageComponentSchemadeclarespropertiesa record, but that parse only reaches top-level components (regions[].components[],slots.*). A nested component (inchildren,items[].childrenorfooter) sits in az.array(z.unknown())slot that no parse judges. The rule setpropstoundefinedfor a non-record bag and skipped the node, so a nestedrecord:related_listwithproperties: nullor[], or a nestedobject-metricwithproperties: [], passedos lint --strict,os validateandtsc. The node published with none of its props.No duplicate with the schema's own top-level refusal
At both doors (CLI and the runtime page save door), the page parse runs before any authoring rule and stops on its own refusal. So a top-level non-record bag is reported once, by the parse ("expected record"), and this rule's finding never appears beside it. On a top-level node the new branch only answers a caller that hands the rule an unparsed stack, and it gives the same verdict. A test pins both halves:
PageSchema.safeParserefuses the top-level bag atregions.0.components.0.propertiesand accepts the nested one, which is the gap.Tests
On head
88d3dd0572(the lint source is byte-identical toc444510a55, where the package suite ran; the later merge brought nopackages/lintorpackages/specchange):pnpm --filter @objectstack/lint test: 153 files passed, 7080 tests passed, 5 skipped.pnpm --filter @objectstack/lint typecheck(withcheck:test-typecheck): exit 0.children, tabitems[].children, accordionitems[].children, cardfooter, a slot component'schildren) times three bags (null,[],'x'). Each draws exactly onecomponent-props-invalidat the node'spropertiespath. Each walk also has two controls: a valid bag stays silent, and an absent bag keeps the earlier verdict. Further cases: theobject-metric[]probe, an unregistered type (refused, plus a silent object-bag control), a data-source-bound element (bag plus binding verdict, nothing more), the top-level parse/no-duplicate pin, and pipeline gating aterrorforvalidate,lintandbuild.Gates:
node scripts/pm/dispatch-gates.mjs --commandsderived 65 families onc444510a55. All 65 ran, and--ranreconciled them with exit codes recorded: 65 run, 0 NOT-MEASURED (a derived zero). The--commandsderivation on88d3dd0572is byte-identical. On88d3dd0572I rerancheck-adr-0087-registration --base origin/main,check-changeset-no-major --base origin/main,check-empty-changeset --base origin/mainandcheck:nul-bytes, all exit 0.check:dts-closureandcheck:dual-build-cjs-loadsexited non-zero in the first sweep. That sweep ran while I was rebuilding shared dists withOS_SKIP_DTS=1for the producer measurement, so the dists were mid-rebuild. Both reran clean (exit 0) once that build finished, with no source change between the two runs.Producers measured
I ran the rule over every in-repo example stack (normalized, through
validateComponentProps) onc444510a55:examples/app-crmexamples/app-multi-packageexamples/app-showcaseexamples/app-todoPositive control: I planted a nested
record:related_listwithproperties: nullin memory into the showcase'sshowcase_component_gallerypage. It drew exactly 1 finding atpages[2].regions[0].components[1].properties.children[0].properties. A static grep ofexamples/**and the page sources for a literalproperties: nullorproperties: []also returns zero hits. There are no in-repo producers to fix.Changeset
.changeset/24991-nested-props-non-record.md:@objectstack/lintminor, with a BREAKING section (what moves atos build/os validate/os lintand the page save door, the remedy, what is unchanged). It carries the claim'sClause-②line. The ADR-0087 disposition isnot-required (no-migration-prescription): no authorable key, spelling, export or stored shape moves. Stored pages are not converted and keep loading until their next save, and the repair (restoring the lost props) is not derivable by a ledger entry.check-adr-0087-registrationaccepts it.Acceptance notes
packages/lint/src/validate-component-props.ts, its test and one changeset.validate-component-props.ts. I checked the files of all 11 open PRs before opening this one.pnpm lintand the full battery. This PR did not run them locally.Generated by Claude Code