Skip to content

Commit 32fa194

Browse files
feat(lint)!: component-props-invalid refuses a retired page-component prop as an error at os validate, os build and os lint (#23140)
Fixes #23026 Clause-②: no (narrowing: `os validate` stops accepting a retired page-component prop; triage's grade `6123043175` reads the same) ## What this does The component-props gate's error upgrade, the step its own header recorded ("the warning-period inventory is the acceptance baseline for the error upgrade, and the upgrade is its own step once the inventory is empty"), scoped to the class triage named: **a retired key**. - `component-props-invalid` now files a `retiredKey()` tombstone's verdict as **`error`**. `os validate`, `os build` and `os lint` refuse the stack, and the message carries the tombstone's own prescription. - Every other finding of the rule stays a **`warning`**. That covers other value refusals (including the two closed-set refusals that just landed: a url action's `target` scheme from PR #23076, and a `record:details` section `count` from PR #23080), retired enum values, undeclared keys and the missing `dataSource.object` binding. - `validateComponentProps` becomes `tier: 'gating'` in `authoring-rules.ts`, which the wiring guard requires of any rule that emits `error`. It already ran on all three commands. - The two conversion-registry sentences that promised a refusal now name the doors that actually refuse. Comment-only, see below. ## Measured before acting **Inventory (H2), at `21c86444e`, before the change:** | corpus | door | retired-key findings | every other `component-props-*` finding | |:--|:--|--:|--:| | `examples/app-crm`, `app-todo`, `app-multi-package`, `app-showcase` | `os validate --json` | 0 | 0 | | the five shipped pages outside them: `platform-objects` sys-organization, sys-position, sys-user; `plugin-approvals` sys-approval-request; `qa/downstream-contract` welcome | the rule on the normalized stack | 0 | 0 | | `packages/apps/*` (account, setup, studio), the `create-objectstack` blank template, CLI generators | none authors a page | n/a | n/a | Re-measured on the merged head `f88009a41`: the same zeros. The page instrument was checked against a positive control (one fixture page with a retired key, an invalid value and an unknown key), which it counted 1 / 1 / 1. Class (b) is therefore **0**, and that is the next step's baseline. Whether (b) should also move is an open question in the report, not part of this diff. **How a retired key is told apart (H1).** It is read off the issue, never off its text. A `retiredKey()` is `z.never(...).optional()`, which raises `invalid_type` with `expected: 'never'`. The spec's private `isRetiredKeyIssue` in `flow-node-config-refusals.ts` uses the same discriminator. A schema-graph walk of `ComponentPropsMap` finds **50** never-nodes: - 48 key tombstones; - 2 retired-element rows (`user:profile`, `ai:chat_window`; `retiredComponentProps`, the same issue shape with the element's prescription), refused the same way; - 2 of the key tombstones sit under a union arm (`object-form` / `object-master-detail-form` `sections[].fields[]`, the object arm's `publicPicker`). There zod collapses the arm failures into one `invalid_union`, so the classifier applies the existing one-arm rule of `unrecognizedKeysFromUnionArm`: exactly one arm raised tombstone issues and nothing else. A mixed arm stays a warning, and that is pinned. Retired enum **values** (`enumWithRetiredValues`) raise an enum issue, not `invalid_type`/`never`, so they are not swept in. The `element:text` variant migration text that calls that finding advisory therefore stays true. **Both doors on one fixture (H4).** The fixture was a `defineStack` app with `object-grid` `properties.defaultFilters`: | door | before (`21c86444e`) | after | |:--|:--|:--| | `os validate` | exit 0, "Validation passed", one `component-props-invalid` warning | exit 1, "Author-time rules failed (1 issue)", the prescription printed | | `os build` | exit 0, artifact written (1.8 KB), one warning | exit 1, no artifact | | `os lint` | n/a | exit 1, "1 error(s)" | No door still passes. The runtime publish door (Studio / REST `/meta` / MCP) does not run this rule (`surfaces: CLI_ONLY`), so a page saved there is not refused. That is written into the rule header, the registry sentences and the changeset, and nothing claims otherwise. **Registry text (H3).** `packages/spec/src/conversions/registry.ts`: - the record-picker reconciliation docblock said "the loader rejects it loudly with the prescription"; - the `object-grid-default-filters-removed` docblock said "refused at the parse with the prescription". The load path keeps the open `properties` bag as written (`PageComponent.properties` is `z.record`). Both sentences now name `os validate`, `os build` and `os lint` (the component-props gate). This is a comment-only edit. On this head the first round stopped at these two sentences; the patch round below corrects six more. It publishes nothing: `@objectstack/spec`'s `files` ships `dist` plus `src/**/*.zod.ts`, and on a built dist the old sentences have 0 hits while a string literal from the same entry hits (positive control). ## Tests - `packages/lint/src/validate-component-props.test.ts` changes: - 4 pins flip from warning to error: `breadcrumb`, detail `sortField`, `displayField`, and the wiring pin from `advisory` to `gating`. - 10 new pins: - the card's specimen as an error, with the prescription read back from the spec's own tombstone, not copied prose; - every top-level tombstone in the map, found from the schema graph (not from the issue shape), refused as an error at its own key; - the retired-element bag; - the union-arm tombstone as an error, and a mixed union arm as a warning; - one error plus one warning in a single bag; - the #23076 url `target` refusal and the #23080 section `count` refusal both stay warnings; - an unknown key and a missing binding stay warnings; - the shared `runAuthoringRules('validate')` pipeline carries the error. - `packages/spec/src/ui/form-field-public-picker-retirement.test.ts` gains one exclusion: the props-lint pin authors `publicPicker` to prove the union-arm refusal, exactly as the `breadcrumb` and detail-`sortField` absence pins already exclude that same file. - Readings, all at `f88009a41`, all under the shared verify lock: - `@objectstack/lint`: 137 files / 6624 tests passed; `typecheck` (tsc plus the test layer under `tsconfig.test.json`) passed, debt ledger unchanged; - `@objectstack/cli` unit layer for validate / build / author-time: 5 files / 61 tests passed (`build-warning-truncation-notice`, `generate-scaffold-validates`, `validate-build-gate-parity`, `validate-per-package-authoring-seam`, `src/utils/author-time-rules`). The integration layer is declared to CI, since no spawn entry was touched; - `@objectstack/spec`: the 20 test files that read `conversions/registry.ts` passed (1060 tests); `typecheck` passed; `check:generated` reports all 14 artifacts current against a freshly built dist; - example apps: their `os validate` reruns are in the inventory table. No example metadata changed, so no example package owes its own suite. - **Ablation.** One-time mutations, each run under an absolute-path trap that restored from `HEAD` and proved the blob equal to `HEAD`, with `git diff HEAD` empty: - classification disabled at the dispatch point: exactly the 9 retired-key pins go red, and 66 stay green (all four stays-a-warning pins among them); - the union-arm branch removed: exactly the union-arm pin goes red (1 failed / 74 passed). ## Gates `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` on the final head `f88009a41` derived 88 commands. 87 ran, each exited 0, with exit codes captured before any pipe. `--ran` reconciles as "88 derived, 87 run, 1 NOT-MEASURED, 0 UNRUN". - NOT MEASURED: `check:type-check-debt`. Reason: a whole-workspace build plus `--re-measure`; CI owns it (dispatch instruction). - `check:dual-build-cjs-loads` first refused with PREREQUISITE NOT MET (8 unrelated packages had no dist). After building them it passed. - The derivation notes `origin/main` moved 4 commits past `f88009a41` while the union ran. One family input changed in that range (`scripts/engine-double-contract.pinned.json`); CI's merge ref reads it. ## Changeset `.changeset/23026-retired-component-prop-refused.md`: `@objectstack/lint: major` on the v18 pre line, `Clause-②: no (narrowing)`, FROM → TO table, and the ADR-0087 disposition `already-registered`. It names the 20 ledger ids that retired the refused keys. `check:adr-0087-registration` and `check-changeset-no-major` judged it green. The clause-② level axis is PR-scoped and CI reads it. ## Patch round: the registry sentences the first head left false The contract-tier review FAILed `f88009a41` (record 6137697373 on this PR): two `registry.ts` docblocks still called the props gate "advisory" for keys this PR refuses as an `error`. The seat amended the claim's file surface (6137721990 on #23026) to every comment sentence in `conversions/registry.ts` and `migrations/entries/**` that states what the component-props gate does with a tombstoned key or a retired value and is false on the head. Commit `f1905ac90` is comment-only: 2 files, 6 sentences. No code, test, changeset or other file changed. **Made false by this PR, corrected:** 1. `registry.ts:10621-10625` (`page-header-breadcrumb-removed`) - BEFORE: "The tombstone on `PageHeaderProps` refuses it for a live author (advisory, via the props lint: `PageComponentSchema.properties` is an open bag)." - AFTER: "The tombstone on `PageHeaderProps` refuses it for a live author: `os validate`, `os build` and `os lint` refuse it as an `error` with the prescription (the component-props gate, `@objectstack/lint`; the load path keeps the open `properties` bag as written)." 2. `registry.ts:13565-13569` (`object-master-detail-form-detail-sort-field-removed`) - BEFORE: "The tombstone refuses the key for a live author (advisory, via the props lint: `PageComponentSchema.properties` is an open bag)." - AFTER: "The tombstone refuses the key for a live author: `os validate`, `os build` and `os lint` refuse it as an `error` with the prescription (the component-props gate, `@objectstack/lint`; the load path keeps the open `properties` bag as written)." **False before this PR, folded in under the seat's amendment 6137721990.** Each is the same defect class as the card, a mechanical sentence edit, unclaimed elsewhere, and in the same gate family. 3. `registry.ts:8333-8340` (`element-text-variant-heading-levels`; the review cited it as `:8327-8328`). This is pre-existing. - BEFORE: "`retiredFromLoadPath`: an author is refused at parse with the prescription rather than silently rewritten; stored rows replay it ..." - AFTER: "`retiredFromLoadPath`: an author is not silently rewritten — `os validate`, `os build` and `os lint` report an old spelling as an advisory `component-props-invalid` finding carrying the prescription (the component-props gate, `@objectstack/lint`: a retired enum value is a value verdict, a warning; the load path keeps the open `properties` bag as written); stored rows replay it ..." 4. `registry.ts:5836-5839` (`page-tabs-type-to-tab-style`). This is pre-existing and was found by the sweep. - BEFORE: "The tombstone still carries the refusal at the authoring site (`tsc`) and at load (the parse), whatever the walk reached." - AFTER: "The tombstone still carries the refusal at the authoring site (`tsc`) and at `os validate` / `os build` / `os lint`, where the component-props gate refuses it as an `error` (the load path keeps the open `properties` bag as written), whatever the walk reached." 5. `registry.ts:8032-8037` (`time-default-utc-suffix-dropped`). This is pre-existing and was found by the sweep; only the `element:button` position was false. - BEFORE: "Retired from the load path: an author is refused at parse, and data at rest and `os migrate meta` replay it." - AFTER: "Retired from the load path: an author is refused at parse — except on an `element:button`'s inline action, which sits in the open `properties` bag the load path keeps as written, where the refusal is an advisory `component-props-invalid` finding at `os validate` / `os build` / `os lint` (the component-props gate, `@objectstack/lint`) — and data at rest and `os migrate meta` replay it." 6. `migrations/entries/retired-keys/18.ui__PageHeaderProps__breadcrumb.ts:16-19`. The residue argument is kept whole; only the present-tense "advisory", false on this head, is dated. - BEFORE: "this row is parsed only by the advisory props lint, which writes nothing back — so no released toolchain emitted `breadcrumb: true` ..." - AFTER: "this row is parsed only by the props lint (advisory in every release that shipped the default), which writes nothing back — so no released toolchain emitted `breadcrumb: true` ..." **Measured on the head, not argued.** The built rule at `f1905ac90` gives: | case | finding | |:--|:--| | `page:tabs` `type` | `error` | | `page:header` `breadcrumb` | `error` | | `details[].sortField` | `error` | | `element:text` `variant: 'heading'` | `warning` (enum issue, prescription carried) | | `element:button` inline-action `time` param `defaultValue: '09:00:00Z'` | `warning` (a `custom` issue at `action.params.0.defaultValue`) | | `record:chatter` `position: 'sidebar'` | `warning` | | `object-form` `layout: 'grid'` | `warning` | The sweep read 21 `registry.ts` sites and every `migrations/entries/**` hit of the props-gate vocabulary. The rest are true as written; the per-site list with evidence is in the `os-dev-report` on #23026. **Gates on `f1905ac90`.** The derivation gives 89 commands: the 88 of `f88009a41` plus `check:spec-changes`, which the `migrations/entries` edit adds. 19 ran, each exit 0: - `check-spec-docblock-symbol-anchors`, both halves; - `check-issue-citations`, both spellings; - `check:docs`, `check:spec-changes`, `check:upgrade-guide`, `check:liveness`, `check:authorable-surface`; - `check:doc-authoring`, `check:nul-bytes`; - comment-mask adoption, both halves; `check-drift-comment`; `check-affected-docs`; - closing-keyword parity, both halves; - `check:pm-prior-rulings`, `check:cross-package-test-inputs`. `check:generated` also ran: all 14 artifacts are current against a freshly built dist. The spec tests that read `registry.ts` (11 files in `src/conversions`, the migrate-sentence class pin, and 20 more) passed: 32 files, 1520 tests. `--ran` reconciles as "89 derived, 19 run, 70 NOT-MEASURED, 0 UNRUN". The 69 other lines are carried from `f88009a41`, where each exited 0, because the patch adds comment text only. `check:type-check-debt` stays NOT MEASURED by dispatch. ### Patch round 2: the docs that state the gate's tier The seat found a published page that states the tier this PR changes (amendment 6138049056 on #23026), plus a sibling sentence in the same class. Commit `175a546b8` is docs-only: two files under `content/docs/`, with no code, test, changeset or `packages/**` change. 1. `content/docs/deployment/validating-metadata.mdx:473` and `:488`, the "one gate, four doors" table. Made false by this PR. - BEFORE: "SDUI component props" sat only in the row headed "Advisory: record titles, semantic field pointers (ADR-0085), form-section layout, action placement, SDUI component props, seed replay/state safety, capability references, liveness", ticked on `os validate`, `os build` and `os lint`, with no runtime publish tick. - AFTER: a new gating row, "Retired SDUI component props — a key `ComponentPropsMap` tombstones (`retiredKey()`), refused with its prescription (#23026)", ticked on `os validate`, `os build` and `os lint`, with no runtime publish tick. The advisory row now reads "SDUI component props (every finding but a retired prop)". 2. `content/docs/protocol/objectui/layout-dsl.mdx:741-745`, the callout under the `custom.*` example. Pre-existing, folded in under 6138049056. - BEFORE: "For the platform's own types the authoring rules dispatch `ComponentPropsMap` and reject a misspelled prop; a `custom.*` type has no entry there, so its props are validated only by whatever registered the component." - AFTER: "For the platform's own types the authoring rules (`os validate`, `os build`, `os lint`) dispatch `ComponentPropsMap`: a misspelled prop is reported as a warning, and a retired prop is refused as an error, with its prescription. A `custom.*` type has no entry there, so its props are validated only by whatever registered the component." **Measured on the built rule at this head:** | case | result | |:--|:--| | a misspelled prop (`page:header` `titel`) | `warning` `component-props-unknown-key` | | `object-grid` `defaultFilters`, `page:header` `breadcrumb` | `error` `component-props-invalid` | | a wrong-typed `limit`, or a missing `dataSource.object` | `warning` `component-props-invalid` | | a `custom.*` node | no finding | The registry entry reads `tier: gating`, commands `build`, `lint` and `validate`, and surfaces `cli` only, so the runtime publish column stays empty. **Sweep** of `content/docs/**`: `releases/` is excluded as release-owned, and `references/` as generated from spec `.describe()` text, which this PR does not change. The two pages above are the only sentences that state the props gate's tier. The other hits concern other rules, or schemas the load path parses: - strict-schema refusals in `ai/tools`, `automation/approvals`, `automation/flows`, `data-modeling/field-types`; - validating-metadata §5, §10 and `:357`; - layout-dsl `:219`, `:411`, `:449`, `:626`, `:754`; - `ui/views.mdx:453`; - the query `searchFields` mentions. The per-site list is in the `os-dev-report` on #23026. **Gates on `175a546b8`.** The derivation gives 109 commands: the 89 of `f1905ac90` plus 20 that the `content/docs` paths add. 32 ran, each exit 0: - the 20 added: doc frontmatter, doc route spelling, docs section name and section landing index (each with its self-test); `check:doc-security-posture`, `check:skill-examples`, `check:corpus-claim-drift`, `check:doc-anchors`, `check:docs-audit-scope`, `check:docs-redirects`, `check:docs-single-h1`, `check:docs-spec-enumerations`, `check:published-readme-links`, `check:react-page-adapter-contract`, `check:skill-identifier-liveness`, `check:vendor-version-stamps`; - `check:doc-authoring`, `check-affected-docs`, `check-drift-comment`, `check:docs`, `check:docs-transcript-drift`; - closing-keyword parity and issue citations, both halves of each; - `check:nul-bytes`, `check:cross-package-test-inputs`, `check:pm-prior-rulings`. `check:skill-examples` first refused with PREREQUISITE NOT MET (`@objectstack/client-react` was unbuilt); after building it, it exited 0. "Check Documentation Links" is lychee in CI and has no local form in this container: NOT MEASURED. These edits add no link. `--ran` reconciles as "109 derived, 32 run, 77 NOT-MEASURED, 0 UNRUN": 8 lines carried from `f1905ac90`, 68 from `f88009a41`, and `check:type-check-debt` by dispatch. Each carry holds because this round changes two `content/docs` pages and no input of those families. ### Patch round 3: the shipped docblocks in `component.zod.ts` The contract-tier review FAILed `175a546b8` (record 6138451028) on one shipped sentence: the docblock above the `breadcrumb` tombstone still called the props lint "advisory", and `component.zod.ts` ships in the tarball (`src/**/*.zod.ts`). The seat amended the surface (6138461923) to that file's comments plus four non-shipped sites. Commit `6280ca3b0` is comment-only: 4 files, +24/-15. The diff guard counts 0 non-comment lines. No schema, `retiredKey()` guidance string or `.describe()` text changed; those belong to #23145's claim. 1. `component.zod.ts:654-658`, the docblock above `breadcrumb`. Made false by this PR; dated the way its retired-keys twin was, with the residue argument kept whole. - BEFORE: "this row is parsed only by the advisory props lint, which writes nothing back — so no retired-default residue stage is owed." - AFTER: "this row is parsed only by the props lint (advisory in every release that shipped the default), which writes nothing back — so no retired-default residue stage is owed." 2. `component.zod.ts:284-303`, the #5068 header. Made false by this PR. - BEFORE: "The gate is WARNING-level in this first step. ... the inventory is the acceptance baseline for the error upgrade. Two of the three entries are now cleared: ... What remains before the upgrade to error is the page rewrites (`page:card.visible` → the component-level `visibleWhen`, #5776's tab `key` → `value`), not a declaration in this file." - AFTER: "The gate was WARNING-level in this first step. ... the inventory was the acceptance baseline for the error upgrade. Two of the three entries were cleared here: ... The page rewrites (...) closed the third, and #23026 took the upgrade for one class once the inventory measured empty: a `retiredKey()` tombstone's verdict is an `error` at `os validate` / `os build` / `os lint`. Every other finding of the gate stays a warning (#23147 carries the rest)." 3. `component.zod.ts:4849-4854`, the #7751 object-* section. The review read it as true for its subject. My probe refutes that: these rows carry tombstones (`object-grid` `defaultSort` and `navigation.view`, `object-kanban` `quickAdd`), and each is an `error` on this head. The maintainer ruling quotation is kept verbatim. - BEFORE: "findings on these entries are advisory, still gated on the #5068 inventory (the ruling: ...)." - AFTER: "findings on these entries were advisory, gated on the #5068 inventory (the ruling: ...). Once that inventory measured empty, #23026 made one class an `error` — a `retiredKey()` tombstone's verdict, such as a retired key these rows carry; every other finding on them stays advisory (#23147 carries the rest)." 4. `ui/page-header-breadcrumb-retirement.test.ts:19-21` and 5. `ui/master-detail-detail-sort-field-retirement.test.ts:22-24`. Not shipped. - BEFORE: "reached by the advisory props lint and never by the page parse: an existing page is never hard-refused." - AFTER: "reached by the props lint — which refuses this tombstone as an `error` at `os validate` / `os build` / `os lint` — and never by the page parse: an existing page is never hard-refused at load." 6. `ui/element-flat-binding-retirement.test.ts:201-202`. Not shipped. - BEFORE: "The open `properties` bag is the props lint's to judge (advisory), not the page parse's." - AFTER: "The open `properties` bag is the props lint's to judge (a retired flat key is an `error` at `os validate` / `os build` / `os lint`), not the page parse's." **Kept as written, with evidence:** - `conversions/walk.ts:371` is read in context. Its story is `page:header` `description`, which is not a tombstone; the strict row's `unrecognized_keys` makes it a `component-props-unknown-key` warning, which the probe confirms at a nested site. So "advisory" is true there. - `component.zod.ts:5686` (`exportOptions`, "advisory"): a bare array and a `pdf` format are both warnings, per the probe. - `component.zod.ts:3376` and `:3429` (the props gate "refusing every authored key" of `element:filter` / `element:form`): those keys are tombstones, now errors. **Still false after this round. Not edited, because the round's single push was spent before a wider grep found them:** - `packages/lint/src/validate-page-field-bindings.ts:233-234`: "the #5068 props gate reports the key ... That gate is advisory and CLI-only". The keys are the tombstoned `displayField` / `searchFields`, now errors. - `ui/page-header-breadcrumb-retirement.test.ts:356-357` and `ui/master-detail-detail-sort-field-retirement.test.ts:447-448`: "The props-lint pin authors the key to assert the advisory warning". Those pins now assert an error. All three are non-shipped comments. One three-sentence comment commit closes the class. **Measured on the head.** The built rule at `6280ca3b0` gives: | case | result | |:--|:--| | `page:header` `breadcrumb` | `error` | | `object-grid` `defaultSort` and `navigation.view`, `object-kanban` `quickAdd` | `error` | | an `object-grid` `pagination.pageSize` of the wrong type, the unknown key `filters` | `warning` | | `exportOptions` refusals | `warning` | | a nested `page:header` `description` | `warning` `component-props-unknown-key` | | `element:repeater` `properties.object` | `error` | | an `element:filter` key | `error` | **Gates on `6280ca3b0`.** The derivation gives the same 109 commands as `175a546b8`. 17 ran, each exit 0: - `check:docs`; - `check-spec-docblock-symbol-anchors`, both halves; - `check:spec-changes`, `check:liveness`, `check:authorable-surface`, `check:api-surface`, `check:exported-any`; - closing-keyword parity and issue citations, both halves of each; - `check:nul-bytes`, comment-mask adoption (both halves), `check:doc-authoring`, `check:cross-package-test-inputs`. `check:generated` reports all 14 artifacts current after a real spec build from this head. Tests: the three touched spec files passed (96 tests), and `validate-component-props.test.ts` passed (75 tests). `--ran` reconciles as "109 derived, 17 run, 92 NOT-MEASURED, 0 UNRUN": 24 lines carried from `175a546b8`, 1 from `f1905ac90`, 66 from `f88009a41`, and `check:type-check-debt` by dispatch. Each carry holds because this round changes comment text only. ### Patch round 4: the last three comments of the class Round 3's widened grep found three more false comments after that round's push. The seat amended the surface to cover them (6138732286 on #23026). Commit `20c00221c` is comment-only: 3 files, +9/-7. The diff guard counts 0 non-comment lines. None of the three ships. 1. `packages/lint/src/validate-page-field-bindings.ts:232-239`, about the tombstoned `displayField` / `searchFields`. The registries-differ argument is kept: the gate neither precedes nor suppresses this rule. - BEFORE: "the #5068 props gate reports the key with its rename/delete prescription. That gate is advisory and CLI-only and lives in a different registry (`authoring-rules`) from this suite, so it neither precedes nor suppresses this rule — ..." - AFTER: "the #5068 props gate refuses the key as an `error` at `os validate` / `os build` / `os lint`, with its rename/delete prescription (it stays advisory for every other finding). That gate is CLI-only and lives in a different registry (`authoring-rules`) from this suite, so it neither precedes nor suppresses this rule — ..." 2. `ui/page-header-breadcrumb-retirement.test.ts:356-357` and 3. `ui/master-detail-detail-sort-field-retirement.test.ts:447-448`, the absence pins' `EXCLUDED` comments. - BEFORE: "The props-lint pin authors the key to assert the advisory warning an author meets at `os validate` / `os build` / `os lint`." - AFTER: "The props-lint pin authors the key to assert the refusal (an `error`) an author meets at `os validate` / `os build` / `os lint`." **The class is closed.** The class grep on `20c00221c` over `packages/spec/src` and `packages/lint/src` combined round 3's patterns with the widened ones ("advisory warning", "gate is advisory"), plus the advisory-adjective and props-gate-with-tier-word sweeps. It returns no false hit. Every remaining hit falls into one of these groups: - dated or past-tense text: `component.zod.ts:284-303`, `:656`, `:4849-4854`, the retired-keys comment, and this PR's own lint text; - true for its subject: `walk.ts:371` (a `description` unknown-key warning), `component.zod.ts:5686` (`exportOptions` value verdicts), and the `registry.ts.template` typed-member strings; - another rule's own "advisory": liveness, seed safety, readonly hook writes, the flow package closure, security posture. The per-hit list is in the `os-dev-report` on #23026. **Gates on `20c00221c`.** The derivation gives the same 109 commands. 10 ran, each exit 0: comment-mask adoption and issue citations (both halves of each), `check:doc-authoring`, closing-keyword parity (both halves), `check:nul-bytes`, lint `check:doc-formula-expressions`, `check:cross-package-test-inputs`. Tests under the verify lock: the two touched spec files passed (40 tests), and `validate-page-field-bindings.test.ts` passed (38 tests). `--ran` reconciles as "109 derived, 10 run, 99 NOT-MEASURED, 0 UNRUN": 8 lines carried from `6280ca3b0`, 24 from `175a546b8`, 1 from `f1905ac90`, 65 from `f88009a41`, and `check:type-check-debt` by dispatch. Each carry holds because this round changes three comments only. ## Acceptance notes - **File surface vs the claim.** Paths outside the claim's original list, each since added by a seat amendment: - `packages/lint/src/authoring-rules.ts`: the `tier` flip that the wiring guard requires for any rule emitting `error`, plus its comment; - `packages/spec/src/conversions/registry.ts`: the two H3 sentences, which the dispatch pre-authorized, and the patch round's five docblock sentences (amendment 6137721990); plus `packages/spec/src/migrations/entries/retired-keys/18.ui__PageHeaderProps__breadcrumb.ts`, one comment; - `packages/spec/src/ui/form-field-public-picker-retirement.test.ts`: the one exclusion above. - `content/docs/deployment/validating-metadata.mdx` and `content/docs/protocol/objectui/layout-dsl.mdx`: patch round 2 (amendment 6138049056). - `packages/spec/src/ui/component.zod.ts` (comments only) and three spec test files' comments: patch round 3 (amendment 6138461923). - `packages/lint/src/validate-page-field-bindings.ts`, one comment: patch round 4 (amendment 6138732286). No example or template needed a fix (inventory 0). No CLI test pinned the old warning. - Two sibling absence pins (`page-header-breadcrumb-retirement.test.ts`, `master-detail-detail-sort-field-retirement.test.ts`) described this lint test file as asserting "the advisory warning"; patch round 4 corrected both `EXCLUDED` comments to say it asserts the refusal, an `error`. - `validateComponentProps` keeps `surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT` ("reads a stack-wide collection the per-write snapshot does not carry"). The rule is page-local, so that reason looks inaccurate, but I did not measure a page write's snapshot. Whether the gating rule should cross to the runtime door is its own rollout decision, with a false-refusal budget over stored tenant pages. Carrier: none. --- _Generated by [Claude Code](https://claude.ai/code/session_012uFegdfR6K6uqvLzt7XsdZ)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent de24373 commit 32fa194

14 files changed

Lines changed: 376 additions & 110 deletions
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
'@objectstack/lint': major
3+
---
4+
5+
A retired page-component prop is refused at `objectstack validate`, `objectstack build` and `objectstack lint`. Before this change, a key that `ComponentPropsMap` tombstones with `retiredKey()` (`object-grid` `defaultFilters`, `element:record_picker` `displayField`, `page:header` `breadcrumb` and the rest) drew only a `component-props-invalid` warning. The command printed "✓ Validation passed" and built the artifact, while the conversion registry told authors the key is refused. It is now a `component-props-invalid` error carrying the tombstone's prescription, and the stack does not pass until the key is gone.
6+
7+
Clause-②: no (narrowing)
8+
9+
<!-- adr-0087: not-required (already-registered object-grid-default-filters-removed, object-grid-default-sort-removed, object-grid-resizable-columns-removed, object-kanban-quick-add-removed, page-header-breadcrumb-removed, page-structure-inert-keys-removed, page-card-body-to-children, page-tabs-type-to-tab-style, record-details-layout-removed, record-picker-display-field-to-label-field, record-picker-inert-keys-removed, element-input-target-variable-removed, element-flat-data-binding-to-data-source, element-filter-removed, element-form-removed, object-master-detail-form-detail-sort-field-removed, list-view-navigation-view-retired, form-field-public-picker-retired, chart-config-aria-retired, ui-ai-chat-window-retired) Every key this refuses was retired, with its tombstone prescription and its ledger entry, before this change. This diff moves only the severity of the lint finding that carries the prescription, so it adds no entry and amends none. -->
10+
11+
**BREAKING**: an accept-set narrowing on a published authoring surface (the `component-props-invalid` rule of `objectstack validate`, `objectstack build` and `objectstack lint`).
12+
13+
**What is refused.** A value verdict a tombstone raises: the tombstone's own issue, wherever the key sits in the props bag, an array entry's key included. A tombstoned key inside a union member is refused when that member is the only one judging keys (`object-form` `sections[].fields[]` `publicPicker`). A retired element's props row (`user:profile`, `ai:chat_window`) raises the same issue and is refused the same way.
14+
15+
**What stays a warning.** Every other `component-props-invalid` finding: a wrong type, a missing required prop, a closed-set refusal (a url action's `target` scheme, a `record:details` section's `count`), a retired enum value and a missing `dataSource.object` binding. Every `component-props-unknown-key` finding also stays a warning.
16+
17+
**Not judged here.** The runtime publish door (the Studio, REST `/meta`, MCP) does not run this rule, so a page saved there is not refused for a retired prop. `defineStack` and the stack schema keep the open `properties` bag as written.
18+
19+
## FROM → TO
20+
21+
| you wrote | write instead |
22+
|:--|:--|
23+
| `object-grid` `properties: { defaultFilters: [...] }` with an empty `filter` | `properties: { filter: [...] }` |
24+
| `element:record_picker` `properties: { displayField: 'title' }` | `properties: { labelField: 'title' }` |
25+
| a retired prop whose prescription says to delete it, e.g. `page:header` `breadcrumb` | nothing: delete the key |
26+
27+
**The one-line fix: apply the prescription the error prints.** Where an ADR-0087 conversion covers the key, `objectstack migrate meta --from <N>` lists the mechanical edits for existing sources.
28+
29+
**Who is affected, measured.** At objectstack `21c86444e`, this rule ran over `examples/app-crm`, `app-todo`, `app-multi-package` and `app-showcase` (through `objectstack validate`), over the five shipped pages outside them (`@objectstack/platform-objects` ×3, `@objectstack/plugin-approvals`, `packages/qa/downstream-contract`) and over the `create-objectstack` template, which authors no page. Result: 0 newly refused, and 0 findings of this rule of any kind. Deployed metadata and other repositories were not measured.

‎content/docs/deployment/validating-metadata.mdx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,7 @@ orthogonal to both, and no cell here can carry it; it is written out in
470470
| Chart bindings outside dashboards (#3583) | ✓ | ✓ | ✓ | — |
471471
| Navigation vs. granted access (ADR-0090 D6) | ✓ | ✓ | ✓ | — |
472472
| SDUI scoped styling (ADR-0065) | ✓ | ✓ | ✓ | — |
473+
| Retired SDUI component props — a key `ComponentPropsMap` tombstones (`retiredKey()`), refused with its prescription (#23026) | ✓ | ✓ | ✓ | — |
473474
| JSX / React page source parses (ADR-0080/0081) | ✓ | ✓ | ✓ | — |
474475
| Approval-node approvers (ADR-0090 D3) | ✓ | ✓ | ✓ | ✓ᶠ |
475476
| Security posture (ADR-0090 — e.g. every custom object declares `sharingModel`) | ✓ | ✓ | ✓ | ✓ˢᵖᵉᵇᵒ |
@@ -484,7 +485,7 @@ orthogonal to both, and no cell here can carry it; it is written out in
484485
| Flow authoring anti-patterns (#1874) | ✓ | ✓ | ✓ | ✓ᶠ |
485486
| Flow trigger readiness — a flow that looks armed and never launches (#5762) | ✓ | ✓ | ✓ | ✓ᶠ |
486487
| `views[]` conditional-visibility predicates — CEL syntax, parse budget, bare identifiers, binding-root layer, schema path refs (ADR-0089 D3b, #7010) | ✓ | ✓ | ✓ | ✓ᵛ |
487-
| Advisory: record titles, semantic field pointers (ADR-0085), form-section layout, action placement, SDUI component props, seed replay/state safety, capability references, liveness | ✓ | ✓ | ✓ | — |
488+
| Advisory: record titles, semantic field pointers (ADR-0085), form-section layout, action placement, SDUI component props (every finding but a retired prop), seed replay/state safety, capability references, liveness | ✓ | ✓ | ✓ | — |
488489
| Package docs — flatness, prefixes, links (ADR-0046) | ✓ | ✓ | ✓ | — |
489490
| Undeclared authoring keys — every metadata collection (#3786) and the stack's own top-level keys (#4167) | ✓ | ✓ | — | — |
490491
| Declared-unique scope — a bare `unique: true` index, a field-level and index-level double declaration, legacy organization composites (ADR-0120 D5) | ✓ | ✓ | ✓ | — |

‎content/docs/protocol/objectui/layout-dsl.mdx‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -739,9 +739,10 @@ properties:
739739
<Callout type="info">
740740
`properties` is an open bag (`Record<string, unknown>`), so **it is the one part
741741
of a component the spec parse does not check**. For the platform's own types the
742-
authoring rules dispatch `ComponentPropsMap` and reject a misspelled prop; a
743-
`custom.*` type has no entry there, so its props are validated only by whatever
744-
registered the component.
742+
authoring rules (`os validate`, `os build`, `os lint`) dispatch `ComponentPropsMap`:
743+
a misspelled prop is reported as a warning, and a retired prop is refused as an
744+
error, with its prescription. A `custom.*` type has no entry there, so its props
745+
are validated only by whatever registered the component.
745746
</Callout>
746747

747748
## Advanced Layouts

‎packages/lint/src/authoring-rules.ts‎

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1155,27 +1155,18 @@ export const AUTHORING_RULES: readonly AuthoringRule[] = [
11551155
// while a schema error elsewhere in the stack cannot take these findings down
11561156
// with it.
11571157
//
1158-
// Advisory, deliberately, and this is the whole shape of #5068's first step:
1159-
// wiring the parse is the precondition for enforcement, not the enforcement
1160-
// (#5020, one surface over). The live corpus violates the declarations in two
1161-
// places that are open contract questions — inline i18n label maps on three
1162-
// published platform pages (#5728) and the record picker's declared-but-unread
1163-
// `displayField` (#5775) — so gating today would fail the platform's own pages
1164-
// to enforce declarations the platform does not keep. The error upgrade is a
1165-
// separate step, once the warning-period inventory is empty.
1166-
//
1167-
// #5775 settled the record picker's half: `displayField` is retired in favour
1168-
// of the `labelField` the renderer actually reads. Its claim that "the rest of
1169-
// the keys the renderers honour are declared" did NOT hold — #6776 found five
1170-
// more (`page:header` `recordChrome`/`showStar`/`showCopyId`,
1171-
// `page:accordion.variant`, and the tab strip's visual style, whose declared
1172-
// spelling `page:tabs.type` collided with the component node's own dispatch
1173-
// key and so was unauthorable in the flat and JSX carriers). All five are
1174-
// declared as of #6776, the last as the renamed `tabStyle`. What remains
1175-
// before the error upgrade is #5728 and two page rewrites.
1158+
// Gating since #23026, for ONE class: a `retiredKey()` tombstone's verdict is
1159+
// an `error`, so `os validate`, `os build` and `os lint` refuse a retired
1160+
// prop with its prescription. Every other finding stays a `warning`. #5068's
1161+
// first step was advisory on purpose — wiring the parse is the precondition
1162+
// for enforcement, not the enforcement (#5020, one surface over) — and the
1163+
// error upgrade was to be its own step once the warning-period inventory was
1164+
// empty. Its blockers (#5728, #5775, #6776's undeclared props, two page
1165+
// rewrites) are all closed, and the inventory measured 0. The rule's module
1166+
// header records the measurement and why the other classes stay advisory.
11761167
{
11771168
name: 'validateComponentProps',
1178-
tier: 'advisory',
1169+
tier: 'gating',
11791170
input: 'normalized',
11801171
commands: ALL,
11811172
source: 'packages/lint/src/validate-component-props.ts',

0 commit comments

Comments
 (0)