Skip to content
Merged
29 changes: 29 additions & 0 deletions .changeset/23026-retired-component-prop-refused.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
'@objectstack/lint': major
---

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.

Clause-②: no (narrowing)

<!-- 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. -->

**BREAKING**: an accept-set narrowing on a published authoring surface (the `component-props-invalid` rule of `objectstack validate`, `objectstack build` and `objectstack lint`).

**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.

**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.

**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.

## FROM → TO

| you wrote | write instead |
|:--|:--|
| `object-grid` `properties: { defaultFilters: [...] }` with an empty `filter` | `properties: { filter: [...] }` |
| `element:record_picker` `properties: { displayField: 'title' }` | `properties: { labelField: 'title' }` |
| a retired prop whose prescription says to delete it, e.g. `page:header` `breadcrumb` | nothing: delete the key |

**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.

**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.
3 changes: 2 additions & 1 deletion content/docs/deployment/validating-metadata.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,7 @@ orthogonal to both, and no cell here can carry it; it is written out in
| Chart bindings outside dashboards (#3583) | ✓ | ✓ | ✓ | — |
| Navigation vs. granted access (ADR-0090 D6) | ✓ | ✓ | ✓ | — |
| SDUI scoped styling (ADR-0065) | ✓ | ✓ | ✓ | — |
| Retired SDUI component props — a key `ComponentPropsMap` tombstones (`retiredKey()`), refused with its prescription (#23026) | ✓ | ✓ | ✓ | — |
| JSX / React page source parses (ADR-0080/0081) | ✓ | ✓ | ✓ | — |
| Approval-node approvers (ADR-0090 D3) | ✓ | ✓ | ✓ | ✓ᶠ |
| Security posture (ADR-0090 — e.g. every custom object declares `sharingModel`) | ✓ | ✓ | ✓ | ✓ˢᵖᵉᵇᵒ |
Expand All @@ -484,7 +485,7 @@ orthogonal to both, and no cell here can carry it; it is written out in
| Flow authoring anti-patterns (#1874) | ✓ | ✓ | ✓ | ✓ᶠ |
| Flow trigger readiness — a flow that looks armed and never launches (#5762) | ✓ | ✓ | ✓ | ✓ᶠ |
| `views[]` conditional-visibility predicates — CEL syntax, parse budget, bare identifiers, binding-root layer, schema path refs (ADR-0089 D3b, #7010) | ✓ | ✓ | ✓ | ✓ᵛ |
| Advisory: record titles, semantic field pointers (ADR-0085), form-section layout, action placement, SDUI component props, seed replay/state safety, capability references, liveness | ✓ | ✓ | ✓ | — |
| 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 | ✓ | ✓ | ✓ | — |
| Package docs — flatness, prefixes, links (ADR-0046) | ✓ | ✓ | ✓ | — |
| Undeclared authoring keys — every metadata collection (#3786) and the stack's own top-level keys (#4167) | ✓ | ✓ | — | — |
| Declared-unique scope — a bare `unique: true` index, a field-level and index-level double declaration, legacy organization composites (ADR-0120 D5) | ✓ | ✓ | ✓ | — |
Expand Down
7 changes: 4 additions & 3 deletions content/docs/protocol/objectui/layout-dsl.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -739,9 +739,10 @@ properties:
<Callout type="info">
`properties` is an open bag (`Record<string, unknown>`), so **it is the one part
of a component the spec parse does not check**. 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.
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.
</Callout>

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