diff --git a/.changeset/23103-page-action-row-on-success-and-executor.md b/.changeset/23103-page-action-row-on-success-and-executor.md index e965ac35926..79c67c7c07f 100644 --- a/.changeset/23103-page-action-row-on-success-and-executor.md +++ b/.changeset/23103-page-action-row-on-success-and-executor.md @@ -15,7 +15,7 @@ The page action rows are `action:button` and `action:icon` (`properties`), and e - **`onSuccess`** was any value (`z.unknown()`). The runner reads only `ActionSchema`'s post-success block, `{ navigate, openIn }`, and classifies `navigate` with the url scheme set. So `onSuccess: { navigate: 'javascript:alert(1)' }` parsed clean, and so did a misspelled key or a callback shape, which the runner gives no reading at all. The rows now hold `ActionSchema.onSuccess`'s own block, the same instance by reference, so a row's verdict on any `onSuccess` value is the action's: the scheme set on `navigate`, the rename prescriptions for `url` / `redirectUrl` / `to` / `route` / `path` / `target` / `navigateTo` / `href`, and the closed `openIn` with its `'self'` default. - **The executor key** (`actionType` on a node, `type` on a member) is a free string, and the rows judged a url `target` only when it read `'url'`. The runner also dispatches `'navigation'`, its local alias of `'url'`, to the same navigator, so `actionType: 'navigation'` with `target: 'javascript:…'` parsed clean. The url executor now has one spelling on the rows: `'navigation'` is refused at the executor key, with the rename onto `'url'`. The key otherwise stays open: a handler a host registers under another name (`navigate_create`, `navigate_edit`, …) is still dispatched by name. -A page's `properties` bag is outside the stack parse. `os validate` judges it through its props gate, which parses the bag against these same rows, so it reports each refusal at the row's own path with the same text, as a `component-props-invalid` finding (that gate's one tier, a warning). A stored page still saves and loads. +A page's `properties` bag is outside the stack parse. `os validate` judges it through its props gate, which parses the bag against these same rows, so it reports each refusal at the row's own path with the same text, as a `component-props-invalid` finding (an error, like every finding of that gate). A stored page still saves and loads. ### FROM → TO diff --git a/.changeset/23147-component-props-every-finding-refused.md b/.changeset/23147-component-props-every-finding-refused.md new file mode 100644 index 00000000000..a1f6eade048 --- /dev/null +++ b/.changeset/23147-component-props-every-finding-refused.md @@ -0,0 +1,36 @@ +--- +'@objectstack/lint': major +'@objectstack/spec': patch +--- + +Every finding of the component-props gate is now an error at `objectstack validate`, `objectstack build` and `objectstack lint`. A page component whose `properties` carry a key its `ComponentPropsMap` row does not declare, a value that row rejects, or no `dataSource.object` binding where the element reads only that binding drew a warning: the command printed "✓ Validation passed" and built the artifact, and the renderer ignored the key or drew nothing. Each is now a `component-props-unknown-key` or `component-props-invalid` error, and the stack does not pass until it is fixed. A retired prop was already refused, and still is. + +Clause-②: no (narrowing) + + + +**BREAKING**: an accept-set narrowing on a published authoring surface (the `component-props-invalid` and `component-props-unknown-key` rules of `objectstack validate`, `objectstack build` and `objectstack lint`). + +**What is refused now.** Every verdict of the gate: + +- `component-props-unknown-key`: a key the component's props row does not declare, at the bag's own level, on a strict object or union arm below it, or on a strip-mode object below it. +- `component-props-invalid`: a value the props row rejects (a wrong type, a missing required prop, a closed-set refusal such as a url action's `target` scheme or a `record:details` section's `count`, a retired enum value), and a missing `dataSource.object` on `element:record_picker`, `element:number` and `element:repeater`. + +**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 any of these. `defineStack` and the stack schema keep the open `properties` bag as written. A component `type` that `ComponentPropsMap` carries no row for (a `custom.*` or plugin type) is not judged. + +`ComponentPropsSeverity`, the type `@objectstack/lint` exports for these findings, narrows from `'error' | 'warning'` to `'error'`. + +## FROM → TO + +| you wrote | write instead | +|:--|:--| +| `page:header` `properties: { titel: 'Deals' }` | `properties: { title: 'Deals' }`, the declared spelling the error names | +| `record:related_list` `properties: { limit: 'five' }` | `properties: { limit: 5 }`, the type the error names | +| `element:number` with no `dataSource` | `dataSource: { object: 'deal' }` on the component node, beside `type` | +| a key the component does not read | nothing: delete the key | + +**The one-line fix: apply what the error names.** An unknown-key error names the declared spelling when one is close, a value error names what the row expects, and the binding error names where the object goes. If the component really does read the key or value, the props row is what is wrong: correct the declaration in `@objectstack/spec`, not the page. + +The `@objectstack/spec` change is text only: the reason of each D3 migration entry named above said the gate reported its refusal as an advisory finding, and now says it is an error. + +**Who is affected, measured.** At objectstack `8323cad97`, this rule ran at its own input tier over `examples/app-crm`, `app-todo`, `app-multi-package` and `app-showcase`, and over the eight shipped pages outside them (`@objectstack/platform-objects` ×3, `@objectstack/plugin-approvals`, `packages/qa/downstream-contract`, `@objectstack/cloud-connection` ×2, `@objectstack/mcp`): 37 pages, 177 components judged. It also ran over `objectstack-ai/hotcrm` at `0f84c9c72`: 8 pages, 50 components judged. Result: 0 findings of every class in both. A planted undeclared key and a planted wrong-typed prop each drew one finding on every population. The `create-objectstack` template authors no page. Deployed metadata and other repositories were not measured. diff --git a/content/docs/deployment/validating-metadata.mdx b/content/docs/deployment/validating-metadata.mdx index b31232f1181..a1d6f37e8e6 100644 --- a/content/docs/deployment/validating-metadata.mdx +++ b/content/docs/deployment/validating-metadata.mdx @@ -470,7 +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) | ✓ | ✓ | ✓ | — | +| SDUI component props — every `ComponentPropsMap` verdict: an undeclared prop, a value its props schema rejects, a missing `dataSource.object` binding, and a retired prop (`retiredKey()`) refused with its prescription (#23026, #23147) | ✓ | ✓ | ✓ | — | | 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`) | ✓ | ✓ | ✓ | ✓ˢᵖᵉᵇᵒ | @@ -485,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 (every finding but a retired prop), seed replay/state safety, capability references, liveness | ✓ | ✓ | ✓ | — | +| Advisory: record titles, semantic field pointers (ADR-0085), form-section layout, action placement, 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) | ✓ | ✓ | ✓ | — | diff --git a/content/docs/protocol/objectui/layout-dsl.mdx b/content/docs/protocol/objectui/layout-dsl.mdx index e728d045fd4..292e0e97c85 100644 --- a/content/docs/protocol/objectui/layout-dsl.mdx +++ b/content/docs/protocol/objectui/layout-dsl.mdx @@ -740,9 +740,9 @@ properties: `properties` is an open bag (`Record`), so **it is the one part of a component the spec parse does not check**. 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. + a misspelled prop or a value its schema rejects is refused as an error, and so is + a retired prop, with its prescription. A `custom.*` type has no entry there, so + its props are validated only by whatever registered the component. ## Advanced Layouts diff --git a/packages/lint/src/authoring-rules.ts b/packages/lint/src/authoring-rules.ts index 3f2101ee379..84239cbee73 100644 --- a/packages/lint/src/authoring-rules.ts +++ b/packages/lint/src/authoring-rules.ts @@ -1155,15 +1155,17 @@ export const AUTHORING_RULES: readonly AuthoringRule[] = [ // while a schema error elsewhere in the stack cannot take these findings down // with it. // - // 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. + // Gating: every finding is an `error`, so `os validate`, `os build` and + // `os lint` refuse an undeclared prop, a value the props schema rejects, a + // missing `dataSource.object` binding and a retired prop (with its + // prescription). #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. #23026 moved the + // retired-key class and #23147 the rest, each on an inventory of 0 — the + // second one over a corpus authored outside this repository too. The rule's + // module header records both measurements. { name: 'validateComponentProps', tier: 'gating', @@ -1171,7 +1173,23 @@ export const AUTHORING_RULES: readonly AuthoringRule[] = [ commands: ALL, source: 'packages/lint/src/validate-component-props.ts', surfaces: CLI_ONLY, - surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT, + // Page-local, MEASURED (#23147): the rule reads `stack.pages` and nothing + // else — each page's components through `walkPageComponents`, judged + // against the static `ComponentPropsMap` — and a `page` write's per-write + // snapshot (`runtime-gate.ts`) carries the written page as `pages[0]`. Over + // the showcase's 28 pages, the rule's findings through that snapshot were + // identical to the whole-stack run's findings on the same page, as + // authored (0 = 0) and with an undeclared key and a wrong-typed prop + // planted (19 = 19). So RUNTIME_NEEDS_FULL_SNAPSHOT, which this entry + // carried until then, was false here. What holds the rule off the runtime + // door is the `validateComponentTypes` discipline: a measured false-refusal + // budget over STORED tenant page rows, which no population measured so far + // is. Crossing is its own rollout decision. + surfaceReason: + 'Gating rule held off the runtime door pending the crossing discipline the gating object rules ' + + 'went through: a measured false-refusal budget, here over stored tenant page rows. The rule is ' + + 'page-local and a page write\'s snapshot carries everything it reads; the 0-finding inventories ' + + 'cover authored config-file metadata only. Crossing is its own rollout card.', run: (stack) => validateComponentProps(stack), }, // The TYPE half of the same door (#12183's ruling): a component `type` inside diff --git a/packages/lint/src/validate-component-props-row-on-success.test.ts b/packages/lint/src/validate-component-props-row-on-success.test.ts index b0e3547925f..d40b16a4892 100644 --- a/packages/lint/src/validate-component-props-row-on-success.test.ts +++ b/packages/lint/src/validate-component-props-row-on-success.test.ts @@ -11,8 +11,9 @@ // whose executor key refuses `'navigation'`, the runner's alias of `'url'`. // These pins hold that the gate reports each refusal at the row's own path, // with the parse's own first sentence, and stays silent on the accepted -// control. The finding is a `component-props-invalid` warning, the gate's one -// tier (its module header says why); no code in this package changed for it. +// control. The finding is a `component-props-invalid` error, like every finding +// of the gate (its module header says why); no code in this package changed for +// it. import { describe, expect, it } from 'vitest'; import { validateComponentProps, COMPONENT_PROPS_INVALID } from './validate-component-props.js'; diff --git a/packages/lint/src/validate-component-props-url-target.test.ts b/packages/lint/src/validate-component-props-url-target.test.ts index d084df8bdd9..083ba86adf9 100644 --- a/packages/lint/src/validate-component-props-url-target.test.ts +++ b/packages/lint/src/validate-component-props-url-target.test.ts @@ -10,7 +10,7 @@ // executor's `target`. These pins hold that the gate reports that refusal at // the row's own `target` path, with the parse's own first sentence, and stays // silent on an admitted target. The finding is a `component-props-invalid` -// warning, the gate's one tier (its module header says why). +// error, like every finding of the gate (its module header says why). import { describe, expect, it } from 'vitest'; import { validateComponentProps, COMPONENT_PROPS_INVALID } from './validate-component-props.js'; diff --git a/packages/lint/src/validate-component-props.test.ts b/packages/lint/src/validate-component-props.test.ts index 6c29afcd19d..89d9fb165dc 100644 --- a/packages/lint/src/validate-component-props.test.ts +++ b/packages/lint/src/validate-component-props.test.ts @@ -49,7 +49,7 @@ describe('validateComponentProps — undeclared keys', () => { ); expect(unknownKeys(findings)).toHaveLength(1); const [f] = unknownKeys(findings); - expect(f.severity).toBe('warning'); + expect(f.severity).toBe('error'); expect(f.path).toBe('pages[0].regions[0].components[0].properties.titel'); expect(f.where).toBe('page "probe_page" · page:header'); // ⚠️ The rename PHRASING moved at #4001 batch A and the assertion follows @@ -211,7 +211,7 @@ describe('validateComponentProps — value verdicts', () => { ]), ); expect(invalid(findings)).toHaveLength(1); - expect(invalid(findings)[0].severity).toBe('warning'); + expect(invalid(findings)[0].severity).toBe('error'); expect(invalid(findings)[0].path).toBe('pages[0].regions[0].components[0].properties.limit'); expect(invalid(findings)[0].message).toContain('expected number, received string'); }); @@ -297,7 +297,7 @@ describe('validateComponentProps — value verdicts', () => { * binding keys of the three data-source-bound elements are retired now, and * the requirement sits on the binding the renderers actually read. One of * the three with no `dataSource.object` is a `component-props-invalid` - * finding AT that path — the rule's existing finding, not a new rule. + * error AT that path — the rule's existing finding, not a new rule. */ describe('the data-source-bound elements owe `dataSource.object` (#11509)', () => { const BOUND = ['element:record_picker', 'element:number', 'element:repeater'] as const; @@ -318,7 +318,7 @@ describe('validateComponentProps — value verdicts', () => { it.each(BOUND)('%s with no binding: one finding, at `dataSource.object`', (type) => { const findings = validateComponentProps(stackWith([{ type, properties: PROPS[type] }])); expect(findings.map((f) => [f.rule, f.path, f.severity])).toEqual([ - [COMPONENT_PROPS_INVALID, 'pages[0].regions[0].components[0].dataSource.object', 'warning'], + [COMPONENT_PROPS_INVALID, 'pages[0].regions[0].components[0].dataSource.object', 'error'], ]); expect(findings[0]!.message).toContain(`\`${type}\` reads its records from the node-level \`dataSource\` binding`); expect(findings[0]!.hint).toContain('dataSource: {'); @@ -622,7 +622,7 @@ describe('validateComponentProps — unregistered types are skipped', () => { * the new entries are reachable by construction. What must be pinned from THIS * side is behaviour through the real door: * - * 1. anti-vacuity — the warning FIRES on the #7750 specimen shape (an + * 1. anti-vacuity — the finding FIRES on the #7750 specimen shape (an * `object-grid` authored with plural `filters`), with the rename named; * 2. the clean-corpus control — the showcase pages' actual authored nodes * (copied, not imported) stay finding-free, so registering the family did @@ -642,13 +642,12 @@ describe('validateComponentProps — object-* blocks are dispatched (#7751)', () ); expect(unknownKeys(findings)).toHaveLength(1); const [f] = unknownKeys(findings); - expect(f.severity).toBe('warning'); + expect(f.severity).toBe('error'); expect(f.path).toBe('pages[0].regions[0].components[0].properties.filters'); expect(f.where).toBe('page "probe_page" · object-grid'); expect(f.message).toContain('`filters`'); expect(f.message).toContain('Did you mean `filters` → `filter`?'); - // Warning tier only — an undeclared key is not the retired-key class - // #23026 moved to `error`. + // An undeclared key only — no value verdict rides beside it. expect(invalid(findings)).toEqual([]); }); @@ -743,7 +742,7 @@ describe('validateComponentProps — reads the post-conversion (normalized) tier }); describe('validateComponentProps — wiring', () => { - it('is registered as a gating rule on all three authoring commands (#23026: a retired key is an error)', () => { + it('is registered as a gating rule on all three authoring commands (every finding is an error)', () => { const entry = AUTHORING_RULES.find((r) => r.name === 'validateComponentProps'); expect(entry).toBeDefined(); expect(entry?.tier).toBe('gating'); @@ -756,19 +755,19 @@ describe('validateComponentProps — wiring', () => { }); const mine = findings.filter((f) => f.rule.startsWith('component-props')); expect(mine).toHaveLength(1); - expect(mine[0].severity).toBe('warning'); + expect(mine[0].severity).toBe('error'); expect(mine[0].rule).toBe(COMPONENT_PROPS_UNKNOWN_KEY); }); }); /** - * #23026 — the error upgrade the module header planned, for ONE class: a value - * verdict a `retiredKey()` tombstone raises is an `error`; every other finding - * stays a `warning`. Asserted on rule id, severity and path — the prescription - * is asserted as CARRIED (read back from the spec's own tombstone), never as - * prose copied here. + * #23026 — the error upgrade the module header planned, for its first class: a + * value verdict a `retiredKey()` tombstone raises is an `error` carrying the + * tombstone's prescription. Asserted on rule id, severity and path — the + * prescription is asserted as CARRIED (read back from the spec's own + * tombstone), never as prose copied here. */ -describe('validateComponentProps — a retired key is an error, nothing else is (#23026)', () => { +describe('validateComponentProps — a retired key is an error (#23026)', () => { /** The prescription the spec's own tombstone raises for `type.key`, read from the schema. */ const prescription = (type: string, key: string): string => { const schema = (ComponentPropsMap as unknown as Record { error?: { issues: Array<{ path: PropertyKey[]; message: string }> } } }>)[type]; @@ -837,65 +836,117 @@ describe('validateComponentProps — a retired key is an error, nothing else is ]); }); - it('leaves that union verdict a warning when the same arm also rejects a value for another reason', () => { - const findings = validateComponentProps( - stackWith([{ - type: 'object-form', - properties: { objectName: 'task', sections: [{ label: 'S', fields: [{ field: 'owner', publicPicker: true, colSpan: 'wide' }] }] }, - }]), - ); - expect(findings.map((f) => [f.rule, f.severity])).toEqual([[COMPONENT_PROPS_INVALID, 'warning']]); + it('reaches the shared pipeline as an error — the run `os validate` / `os build` / `os lint` refuse on', () => { + const findings = runAuthoringRules('validate', { + normalized: stackWith([{ type: 'object-grid', properties: { objectName: 'task', defaultFilters: [] } }]) as never, + }); + const mine = findings.filter((f) => f.rule.startsWith('component-props')); + expect(mine.map((f) => [f.rule, f.severity])).toEqual([[COMPONENT_PROPS_INVALID, 'error']]); }); +}); - it('classifies per issue: a retired key beside a wrongly-typed prop is one error and one warning', () => { - const findings = validateComponentProps( - stackWith([{ type: 'object-grid', properties: { objectName: 'task', defaultFilters: [], pageSize: 'ten' } }]), - ); - const base = 'pages[0].regions[0].components[0].properties'; - expect(findings.map((f) => [f.rule, f.severity, f.path])).toEqual([ - [COMPONENT_PROPS_INVALID, 'error', `${base}.defaultFilters`], - [COMPONENT_PROPS_INVALID, 'warning', `${base}.pageSize`], - ]); - }); +/** + * #23147 — the rest of the plan: every other finding of the rule is an `error` + * too, after an inventory of 0 in this repository and in one corpus authored + * outside it (the module header records both). One pin per class and per + * code route that files it, so reverting any one severity reds exactly its + * own pin. Asserted on rule id, severity and path; the text is not pinned. + */ +describe('validateComponentProps — every other class is an error too (#23147)', () => { + const at = (rest: string) => `pages[0].regions[0].components[0].${rest}`; + const verdicts = (node: unknown) => + validateComponentProps(stackWith([node])).map((f) => [f.rule, f.severity, f.path]); + + describe('class: `component-props-unknown-key`', () => { + it('an undeclared key on a strict props schema (the parse\'s `unrecognized_keys`)', () => { + expect(verdicts({ type: 'page:header', properties: { title: 'T', titel: 'typo' } })).toEqual([ + [COMPONENT_PROPS_UNKNOWN_KEY, 'error', at('properties.titel')], + ]); + }); - // The two closed-set refusals that landed beside this change reach the rule - // as `component-props-invalid` too, and are not the retired-key class. - it('keeps a url action\'s refused `target` scheme a warning', () => { - const findings = validateComponentProps( - stackWith([{ type: 'element:button', properties: { label: 'Go', action: { type: 'url', target: 'javascript:alert(1)' } } }]), - ); - expect(findings.map((f) => [f.rule, f.severity, f.path])).toEqual([ - [COMPONENT_PROPS_INVALID, 'warning', 'pages[0].regions[0].components[0].properties.action.target'], - ]); + it('an undeclared key on a strip-mode object below the bag (the walker)', () => { + expect(verdicts({ + type: 'object-grid', + properties: { objectName: 'task', sort: [{ field: 'name', order: 'asc', directoin: 'x' }] }, + })).toEqual([ + [COMPONENT_PROPS_UNKNOWN_KEY, 'error', at('properties.sort.0.directoin')], + ]); + }); + + it('an undeclared key on the one union arm that judged keys (`record:highlights` `fields[]`)', () => { + expect(verdicts({ type: 'record:highlights', properties: { fields: [{ name: 'status', readOnly: true }] } })).toEqual([ + [COMPONENT_PROPS_UNKNOWN_KEY, 'error', at('properties.fields.0.readOnly')], + ]); + }); }); - it('keeps a `record:details` section `count` whose heading binds no hole a warning', () => { - const findings = validateComponentProps( - stackWith([{ type: 'record:details', properties: { sections: [{ label: 'Tags', fields: ['tags'], count: { field: 'tags' } }] } }]), - ); - expect(findings.length).toBeGreaterThan(0); - expect(findings.every((f) => f.rule === COMPONENT_PROPS_INVALID && f.severity === 'warning')).toBe(true); + describe('class: `component-props-invalid` that is not a retired key', () => { + it('a wrongly-typed prop', () => { + expect(verdicts({ type: 'record:related_list', properties: { objectName: 'task', relationshipField: 'p', limit: 'five' } })) + .toEqual([[COMPONENT_PROPS_INVALID, 'error', at('properties.limit')]]); + }); + + it('a missing required prop', () => { + expect(verdicts({ type: 'record:related_list', properties: { objectName: 'task' } })) + .toEqual([[COMPONENT_PROPS_INVALID, 'error', at('properties.relationshipField')]]); + }); + + it('a closed-set refusal — a url action\'s `target` scheme', () => { + expect(verdicts({ type: 'element:button', properties: { label: 'Go', action: { type: 'url', target: 'javascript:alert(1)' } } })) + .toEqual([[COMPONENT_PROPS_INVALID, 'error', at('properties.action.target')]]); + }); + + it('a closed-set refusal — a `record:details` section `count` whose heading binds no hole', () => { + const findings = validateComponentProps( + stackWith([{ type: 'record:details', properties: { sections: [{ label: 'Tags', fields: ['tags'], count: { field: 'tags' } }] } }]), + ); + expect(findings.length).toBeGreaterThan(0); + expect(findings.every((f) => f.rule === COMPONENT_PROPS_INVALID && f.severity === 'error')).toBe(true); + }); + + it('a retired enum VALUE — an enum issue, not a tombstone (`object-grid` `exportOptions.formats` `pdf`)', () => { + expect(verdicts({ type: 'object-grid', properties: { objectName: 'task', exportOptions: { formats: ['pdf'] } } })) + .toEqual([[COMPONENT_PROPS_INVALID, 'error', at('properties.exportOptions.formats.0')]]); + }); + + it('a missing `dataSource.object` binding on a data-source-bound element', () => { + expect(verdicts({ type: 'element:number', properties: { aggregate: 'count' } })) + .toEqual([[COMPONENT_PROPS_INVALID, 'error', at('dataSource.object')]]); + }); + + it('a union verdict where the tombstone\'s arm also rejects a value for another reason', () => { + expect(verdicts({ + type: 'object-form', + properties: { objectName: 'task', sections: [{ label: 'S', fields: [{ field: 'owner', publicPicker: true, colSpan: 'wide' }] }] }, + }).map(([rule, severity]) => [rule, severity])).toEqual([[COMPONENT_PROPS_INVALID, 'error']]); + }); }); - it('keeps an undeclared key and a missing `dataSource.object` binding warnings', () => { + it('classifies per issue for the FIX, not the severity: a retired key beside a wrongly-typed prop', () => { const findings = validateComponentProps( - stackWith([ - { type: 'page:header', properties: { title: 'T', titel: 'typo' } }, - { type: 'element:number', properties: { aggregate: 'count' } }, - ]), + stackWith([{ type: 'object-grid', properties: { objectName: 'task', defaultFilters: [], pageSize: 'ten' } }]), ); - expect(findings.map((f) => [f.rule, f.severity])).toEqual([ - [COMPONENT_PROPS_UNKNOWN_KEY, 'warning'], - [COMPONENT_PROPS_INVALID, 'warning'], + expect(findings.map((f) => [f.rule, f.severity, f.path])).toEqual([ + [COMPONENT_PROPS_INVALID, 'error', at('properties.defaultFilters')], + [COMPONENT_PROPS_INVALID, 'error', at('properties.pageSize')], ]); + // The retired key's fix is its prescription, the other's a props change: + // two different hints for one rule id and one type. + expect(findings[0].hint).not.toBe(findings[1].hint); }); - it('reaches the shared pipeline as an error — the run `os validate` / `os build` / `os lint` refuse on', () => { + it('reaches the shared pipeline as errors — the run `os validate` / `os build` / `os lint` refuse on', () => { const findings = runAuthoringRules('validate', { - normalized: stackWith([{ type: 'object-grid', properties: { objectName: 'task', defaultFilters: [] } }]) as never, + normalized: stackWith([ + { type: 'page:header', properties: { title: 'T', titel: 'typo' } }, + { type: 'element:number', properties: { aggregate: 'count' } }, + ]) as never, }); const mine = findings.filter((f) => f.rule.startsWith('component-props')); - expect(mine.map((f) => [f.rule, f.severity])).toEqual([[COMPONENT_PROPS_INVALID, 'error']]); + expect(mine.map((f) => [f.rule, f.severity])).toEqual([ + [COMPONENT_PROPS_UNKNOWN_KEY, 'error'], + [COMPONENT_PROPS_INVALID, 'error'], + ]); }); }); diff --git a/packages/lint/src/validate-component-props.ts b/packages/lint/src/validate-component-props.ts index f986c5ad729..e3ed3effd34 100644 --- a/packages/lint/src/validate-component-props.ts +++ b/packages/lint/src/validate-component-props.ts @@ -48,7 +48,7 @@ * author's view — and without this file needing to know which posture the spec * is at. * - * ## Severity — a RETIRED key is an `error`, every other finding a `warning` + * ## Severity — every finding is an `error` * * Wiring the parse was the precondition for enforcement, not the enforcement * (the #5020 lesson, one surface over), so this rule landed advisory: the @@ -63,20 +63,39 @@ * inventory is the acceptance baseline for the error upgrade, and the upgrade * is its own step once the inventory is empty. Every blocker above is closed. * - * #23026 took that step for ONE class: a value verdict raised by a - * `retiredKey()` tombstone (`shared/retired-key.ts`, `@objectstack/spec`). A - * tombstone keeps a key declared only so that writing it raises the - * prescription, so reporting it as advice had `os validate` print + * The upgrade took two steps. #23026 took it for ONE class: a value verdict + * raised by a `retiredKey()` tombstone (`shared/retired-key.ts`, + * `@objectstack/spec`), whose report as advice had `os validate` print * "✓ Validation passed" over a key the ADR-0087 conversion registry tells - * authors is refused. The inventory, measured before the upgrade at - * objectstack `21c86444e`: `os validate` over `examples/app-crm`, `app-todo`, - * `app-multi-package` and `app-showcase`, and this rule over the five shipped - * pages outside them (`platform-objects` ×3, `plugin-approvals`, - * `qa/downstream-contract`); `packages/apps/*` and the `create-objectstack` - * template author no page. Retired-key findings: 0. Every other finding of - * this rule: 0 as well. + * authors is refused. #23147 took it for the rest: a value rejected for any + * other reason (a wrong type, a missing required prop, a closed-set refusal + * such as a url action's `target` scheme or a `record:details` section's + * `count`), a retired enum VALUE (`enumWithRetiredValues` raises an enum + * issue, not a tombstone), an undeclared key, and the missing + * `dataSource.object` binding. Each of those was a success receipt for + * configuration the renderer ignores or cannot draw — the ADR-0078 shape this + * gate exists to remove — so none of them is advice any more. * - * The class is read off the ISSUE, never off its text ({@link isRetiredKeyVerdict}): + * The inventory each step was taken on, every class counted separately: + * + * - #23026, at objectstack `21c86444e`: `os validate` over `examples/app-crm`, + * `app-todo`, `app-multi-package` and `app-showcase`, and this rule over the + * five shipped pages outside them. Retired-key findings: 0; every other + * finding: 0. + * - #23147, at objectstack `8323cad97`, at this rule's own input tier (the + * normalized stack, package bodies folded): the same four examples, and the + * eight shipped pages outside them (`platform-objects` ×3, + * `plugin-approvals`, `qa/downstream-contract`, `cloud-connection` ×2, + * `mcp`) — 37 pages, 177 components judged; and one corpus authored + * outside this repository, `objectstack-ai/hotcrm` at `0f84c9c72` — 8 + * pages, 50 components judged. Value verdicts: 0; undeclared keys: 0; + * retired keys: 0. A planted undeclared key and a planted wrong-typed prop + * each drew one finding on every population. `packages/apps/*` and the + * `create-objectstack` template author no page. + * + * The retired-key class is still read off the ISSUE, never off its text + * ({@link isRetiredKeyVerdict}), because its fix differs, not its severity: + * the prescription the tombstone carries, never a schema change. It is * * - the tombstone's own issue, `invalid_type` expecting `never` — the shape a * `retiredKey()` raises, and the discriminator the spec's own flow-node @@ -89,18 +108,12 @@ * `object-form` / `object-master-detail-form` `sections[].fields[]`, whose * object arm tombstones `publicPicker`. * - * What stays a `warning`, deliberately: a value rejected for any other reason - * (a wrong type, a missing required prop, a closed-set refusal such as a url - * action's `target` scheme or a `record:details` section's `count`), a - * retired enum VALUE (`enumWithRetiredValues` raises an enum issue, not a - * tombstone), an undeclared key, and the missing `dataSource.object` binding. - * Their inventory above (0) is the next step's baseline; moving them is a - * decision of its own, not a rider on this one. - * * Where it refuses: the three CLI doors — `os validate`, `os build`, `os lint` * — run every gating rule. The runtime publish door (the Studio, REST and MCP * write path) does not run this rule (`surfaces` in `authoring-rules.ts`), so a - * page saved there is not refused for a retired key. + * page saved there is not refused for any of these findings. That is a + * rollout decision, not a missing input: a `page` write's per-write snapshot + * carries the written page, which is everything this rule reads. * * ## Unregistered types are SKIPPED — a required semantic, not leniency * @@ -136,12 +149,11 @@ export const COMPONENT_PROPS_UNKNOWN_KEY = 'component-props-unknown-key'; export const COMPONENT_PROPS_INVALID = 'component-props-invalid'; /** - * `error` for a retired key ({@link isRetiredKeyVerdict}), `warning` for every - * other finding — see the module header. Emitting `error` makes the rule - * `gating` in `authoring-rules.ts`, which `authoring-rule-wiring.test.ts` - * verifies against this file's source. + * `error` for every finding — see the module header. Emitting `error` makes + * the rule `gating` in `authoring-rules.ts`, which + * `authoring-rule-wiring.test.ts` verifies against this file's source. */ -export type ComponentPropsSeverity = 'error' | 'warning'; +export type ComponentPropsSeverity = 'error'; export interface ComponentPropsFinding { severity: ComponentPropsSeverity; @@ -190,8 +202,8 @@ const PROPS_SCHEMAS = ComponentPropsMap as unknown as Record' }\`, a ` + 'sibling of `type`, not a key inside `properties`, where a flat `object` is retired and read ' + @@ -350,14 +363,15 @@ export function validateComponentProps(stack: AnyRec): ComponentPropsFinding[] { // pins both directions of that. for (const f of lintUnknownKeysAgainstSchema(schema, props, type, base)) { findings.push({ - severity: 'warning', + severity: 'error', rule: COMPONENT_PROPS_UNKNOWN_KEY, where, path: f.path, message: - `\`${f.key}\` is not a prop \`${type}\` declares (ComponentPropsMap, @objectstack/spec/ui), ` + - 'so nothing verifies it: `properties` is an untyped bag, the renderer spreads whatever it ' + - 'carries, and a key it does not read is ignored in silence.' + + `\`${f.key}\` is not a prop \`${type}\` declares (ComponentPropsMap, @objectstack/spec/ui): ` + + '`properties` is an untyped bag, the renderer spreads whatever it carries, and a key it does ' + + 'not read is ignored in silence, so it is refused here and this stack does not pass until ' + + 'it is gone.' + (f.suggestion ? ` Did you mean \`${f.suggestion}\`?` : ''), hint: f.guidance ?? @@ -381,7 +395,7 @@ export function validateComponentProps(stack: AnyRec): ComponentPropsFinding[] { if (armIssue) { for (const key of armIssue.keys ?? []) { findings.push({ - severity: 'warning', + severity: 'error', rule: COMPONENT_PROPS_UNKNOWN_KEY, where, path: `${at}.${key}`, @@ -396,7 +410,7 @@ export function validateComponentProps(stack: AnyRec): ComponentPropsFinding[] { if (issue.code === 'unrecognized_keys') { for (const key of issue.keys ?? []) { findings.push({ - severity: 'warning', + severity: 'error', rule: COMPONENT_PROPS_UNKNOWN_KEY, where, path: `${at}.${key}`, @@ -407,10 +421,10 @@ export function validateComponentProps(stack: AnyRec): ComponentPropsFinding[] { continue; } const subject = `${at.slice(base.length + 1) || 'properties'}: ${describeIssue(issue, props)}`; - // A retired key is refused; it is the one `error` this rule files. Its - // fix is the prescription the message already carries, so the hint - // never offers the other branch below — re-declaring a retired key in - // the schema would undo its retirement. + // A retired key is refused like every other verdict here, but its fix + // is the prescription the message already carries, so the hint never + // offers the other branch below — re-declaring a retired key in the + // schema would undo its retirement. if (isRetiredKeyVerdict(issue)) { findings.push({ severity: 'error', @@ -425,15 +439,13 @@ export function validateComponentProps(stack: AnyRec): ComponentPropsFinding[] { continue; } findings.push({ - severity: 'warning', + severity: 'error', rule: COMPONENT_PROPS_INVALID, where, path: at, // [#22161] `hint` is the CLI's `fix:` line, so it states the fix; the - // advisory posture it used to explain (judged here as a warning, and - // the props bag is not parsed on the storage path) is the verdict's - // consequence and rides the message. - message: `${subject} — nothing refuses it today, so the renderer receives the props as written`, + // verdict's consequence rides the message. + message: `${subject} — refused, so this stack does not pass until the props are ones the schema accepts`, hint: `Give \`${type}\` props its schema accepts (ComponentPropsMap, @objectstack/spec/ui) — the ` + 'rejection above names what it expects — or, if the component really does honour what is ' + diff --git a/packages/lint/src/validate-page-field-bindings.ts b/packages/lint/src/validate-page-field-bindings.ts index d6448e1b5e3..ffc4a432fc3 100644 --- a/packages/lint/src/validate-page-field-bindings.ts +++ b/packages/lint/src/validate-page-field-bindings.ts @@ -231,8 +231,8 @@ export const COMPONENT_FIELD_SPECS: Readonly> // // A non-conformant page that writes one anyway is not left unattended: 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 + // `os build` / `os lint`, with its rename/delete prescription (every other + // finding of that gate is an `error` too). That gate is CLI-only and lives in a // different registry (`authoring-rules`) from this suite, so it neither // precedes nor suppresses this rule — what these two entries actually added // was a SECOND finding, saying a field named by a key that no longer exists diff --git a/packages/lint/src/validate-record-approval-decision-22472.test.ts b/packages/lint/src/validate-record-approval-decision-22472.test.ts index 95c8be585d8..8bd1ab005fa 100644 --- a/packages/lint/src/validate-record-approval-decision-22472.test.ts +++ b/packages/lint/src/validate-record-approval-decision-22472.test.ts @@ -55,7 +55,7 @@ describe('any authored key is a `component-props-unknown-key` finding, naming th expect(findings).toHaveLength(1); const [f] = findings; expect(f.rule).toBe(COMPONENT_PROPS_UNKNOWN_KEY); - expect(f.severity).toBe('warning'); + expect(f.severity).toBe('error'); expect(f.path).toBe(`pages[0].regions[0].components[0].properties.${key}`); expect(f.where).toBe('page "approval_request_detail" · record:approval_decision'); expect(f.message).toContain(`\`${key}\``); diff --git a/packages/lint/src/validate-record-approvals-attachments-22537.test.ts b/packages/lint/src/validate-record-approvals-attachments-22537.test.ts index e4805b9ef4c..4b2b8a7fec3 100644 --- a/packages/lint/src/validate-record-approvals-attachments-22537.test.ts +++ b/packages/lint/src/validate-record-approvals-attachments-22537.test.ts @@ -154,7 +154,7 @@ describe('a prop on either node is a `component-props-unknown-key` finding, nami expect(findings).toHaveLength(1); const [f] = findings; expect(f.rule).toBe(COMPONENT_PROPS_UNKNOWN_KEY); - expect(f.severity).toBe('warning'); + expect(f.severity).toBe('error'); expect(f.path).toBe(`${TABS}[2].children[0].properties.${key}`); expect(f.where).toBe(`page "invoice_record" · ${type}`); expect(f.message).toContain(`\`${key}\``); diff --git a/packages/metadata-protocol/src/protocol.stored-migration.test.ts b/packages/metadata-protocol/src/protocol.stored-migration.test.ts index 54230ae504b..ebad1f730bd 100644 --- a/packages/metadata-protocol/src/protocol.stored-migration.test.ts +++ b/packages/metadata-protocol/src/protocol.stored-migration.test.ts @@ -818,8 +818,9 @@ describe('migrateStoredMetadata — a site the chain leaves as stored is a TODO, it('MEASURED — the write path judges the two door kinds differently, and the TODO rides on either outcome', async () => { // `properties.filter` sits in the page component's open `properties` bag: // the runtime save door does not parse it by `type` (the props gate is - // `@objectstack/lint`'s, advisory). `dataSource.filter` is a declared key - // of the strict component schema, so the save door refuses it there. + // `@objectstack/lint`'s, an error at the CLI doors only). + // `dataSource.filter` is a declared key of the strict component schema, + // so the save door refuses it there. const bindingMixed = pageRow('deal_room', [ grid({ stage: 'open' }), { type: 'object-kanban', dataSource: { object: 'deal', filter: COMBINATOR }, properties: { objectName: 'deal' } }, diff --git a/packages/spec/src/conversions/registry.ts b/packages/spec/src/conversions/registry.ts index c1d9604ea79..bcbae2c6232 100644 --- a/packages/spec/src/conversions/registry.ts +++ b/packages/spec/src/conversions/registry.ts @@ -8031,8 +8031,8 @@ const translationPerAppSettingsRemoved: MetadataConversion = { * (`actions[]`, `objects[].actions[]`, an `element:button`'s inline action). * 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` + * load path keeps as written, where the refusal is a + * `component-props-invalid` error at `os validate` / `os build` / `os lint` * (the component-props gate, `@objectstack/lint`) — and data at rest and * `os migrate meta` replay it. */ @@ -8331,11 +8331,11 @@ const translationComponentSubmitLabelRemoved: MetadataConversion = { * type is that component's own vocabulary and is never read here. * * `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 + * `os build` and `os lint` report an old spelling as a + * `component-props-invalid` error 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 at rehydration + * verdict, an error like every other; the load path keeps the open `properties` + * bag as written); stored rows replay it at rehydration * (`applyConversionsToStoredItem`) and `os migrate meta` lists the edit for * existing sources. Idempotent by construction: the rewrite's output is * outside its own input set. @@ -14925,8 +14925,8 @@ function describeBlock(component: Dict): string { * of the strict page-component schema, so the row's next save is refused * there; `properties.filter` sits in the open * `properties` bag the runtime save does not refuse by component type, so there - * the refusal is the component-props gate's (`@objectstack/lint`, advisory), - * and a re-save goes through. + * the refusal is the component-props gate's (`@objectstack/lint`, an error at + * `os validate` / `os build` / `os lint`), and a re-save goes through. * * ## Every site left as stored is reported — `context.reportTodo` * diff --git a/packages/spec/src/conversions/walk.ts b/packages/spec/src/conversions/walk.ts index 0ab0a1072a3..705202d750f 100644 --- a/packages/spec/src/conversions/walk.ts +++ b/packages/spec/src/conversions/walk.ts @@ -368,8 +368,8 @@ function mapComponentTree( * `page-header-subtitle-alias`, whose retired `description` is tombstoned * nowhere (`description` is a live declared prop on other components), got * no diagnostic at a nested site from any layer: the conversion did not - * fire, `PageSchema` stayed green, and the advisory props gate runs CLI-only - * and on already-converted metadata. A slotted record page — the shape + * fire, `PageSchema` stayed green, and the props gate (advisory then) runs + * CLI-only and on already-converted metadata. A slotted record page — the shape * objectui's own guide recommends — could carry a header the rewrite never * saw, so objectui's `subtitle ?? description` fallback could not retire * without silently dropping those subtitles. diff --git a/packages/spec/src/migrations/entries/semantic/18.element-data-source-and-object-block-filter-rule-array.ts b/packages/spec/src/migrations/entries/semantic/18.element-data-source-and-object-block-filter-rule-array.ts index 46c4753c2dc..6651d710c50 100644 --- a/packages/spec/src/migrations/entries/semantic/18.element-data-source-and-object-block-filter-rule-array.ts +++ b/packages/spec/src/migrations/entries/semantic/18.element-data-source-and-object-block-filter-rule-array.ts @@ -86,7 +86,7 @@ export const entry: SemanticMigration = { + '(`applyConversionsToStoredItem` replays the chain without validating, by its own ' + 'contract), and its `filter` door refuses the form: at `dataSource.filter` on the page\'s ' + 'next save; at a block\'s `properties.filter` — like `properties.defaultFilters`, a key of ' - + 'the open `properties` bag — only as the component-props gate\'s advisory finding ' + + 'the open `properties` bag — only as the component-props gate\'s error ' + '(os validate, os build, os lint), since a re-save through the metadata API is not ' + 'refused there. For a combinator record that refusal names the combinator and says why no ' + 'rule spells it. `os migrate meta --stored` lists each such filter as a TODO under its ' diff --git a/packages/spec/src/migrations/entries/semantic/18.element-text-variant-heading-subheading-retired.ts b/packages/spec/src/migrations/entries/semantic/18.element-text-variant-heading-subheading-retired.ts index af054947adf..676def812d8 100644 --- a/packages/spec/src/migrations/entries/semantic/18.element-text-variant-heading-subheading-retired.ts +++ b/packages/spec/src/migrations/entries/semantic/18.element-text-variant-heading-subheading-retired.ts @@ -33,8 +33,8 @@ export const entry: SemanticMigration = { + 'styles. Whether the page wanted that level is the author\'s call — a heading placed for its ' + 'size rather than its place in the outline may want a deeper level. Nothing is dropped at rest: ' + 'a stored page replays the rewrite at rehydration; a page component\'s `properties` is not ' - + 'parsed on the save path, and the component-props gate reports an old spelling as an advisory ' - + '`component-props-invalid` finding, carrying the prescription, on `os validate`, `os build` and ' + + 'parsed on the save path, and the component-props gate reports an old spelling as a ' + + '`component-props-invalid` error, carrying the prescription, on `os validate`, `os build` and ' + '`os lint`. ADR-0087', acceptanceCriteria: 'No `element:text` page component carries `variant` `heading` or `subheading`; `os validate` ' diff --git a/packages/spec/src/migrations/entries/semantic/18.ui-action-group-menu-member-params-array-only.ts b/packages/spec/src/migrations/entries/semantic/18.ui-action-group-menu-member-params-array-only.ts index 2c161781243..673a84c125f 100644 --- a/packages/spec/src/migrations/entries/semantic/18.ui-action-group-menu-member-params-array-only.ts +++ b/packages/spec/src/migrations/entries/semantic/18.ui-action-group-menu-member-params-array-only.ts @@ -32,8 +32,8 @@ export const entry: SemanticMigration = { + 'values are not part of the inline action vocabulary at all, and the action that needs them is its own ' + '`action:button` node. The member now refuses a non-array `params` on a non-`api` type at the gate, at ' + '`actions.N.params`, with that prescription. The `api` member\'s object `params` is unchanged. It is read ' - + 'where every page component\'s props are: the component-props gate reports the refusal as an advisory ' - + '`component-props-invalid` finding on `objectstack validate`, `objectstack build` and `objectstack lint`, and a ' + + 'where every page component\'s props are: the component-props gate reports the refusal as a ' + + '`component-props-invalid` error on `objectstack validate`, `objectstack build` and `objectstack lint`, and a ' + 'stored page still saves and loads, because a page component\'s `properties` is not parsed on the metadata ' + 'save or load path. No conversion is registered: the static values belong on a different node, and the ' + 'census found no writer. Deployed metadata NOT MEASURED.', diff --git a/packages/spec/src/migrations/entries/semantic/18.ui-action-group-menu-members-typed.ts b/packages/spec/src/migrations/entries/semantic/18.ui-action-group-menu-members-typed.ts index d893f3a4e86..e1a268ca806 100644 --- a/packages/spec/src/migrations/entries/semantic/18.ui-action-group-menu-members-typed.ts +++ b/packages/spec/src/migrations/entries/semantic/18.ui-action-group-menu-members-typed.ts @@ -33,8 +33,8 @@ export const entry: SemanticMigration = { + 'by `type`, with the rows\' prescriptions; the keys the rows leave undecided — `outcomeMessages`, a member ' + '`className`, a member `properties.params` — are refused, and `outcomeMessages` stays undeclared on all four ' + 'action blocks as one decision. It is read where every page component\'s props are: the component-props ' - + 'gate reports a refused value as an advisory `component-props-invalid` / `component-props-unknown-key` ' - + 'finding on `objectstack validate`, `objectstack build` and `objectstack lint`, and a stored page still saves ' + + 'gate reports a refused value as a `component-props-invalid` / `component-props-unknown-key` ' + + 'error on `objectstack validate`, `objectstack build` and `objectstack lint`, and a stored page still saves ' + 'and loads, because a page component\'s `properties` is not parsed on the metadata save or load path. No ' + 'conversion is registered: nothing on the load path refuses the shape, and the authored census found no ' + 'working member to respell. Deployed metadata NOT MEASURED.', diff --git a/packages/spec/src/migrations/entries/semantic/18.ui-object-block-grouping-config-typed.ts b/packages/spec/src/migrations/entries/semantic/18.ui-object-block-grouping-config-typed.ts index d23669e842f..8752a6f6e0a 100644 --- a/packages/spec/src/migrations/entries/semantic/18.ui-object-block-grouping-config-typed.ts +++ b/packages/spec/src/migrations/entries/semantic/18.ui-object-block-grouping-config-typed.ts @@ -43,8 +43,8 @@ export const entry: SemanticMigration = { + 'and a bare string, a number or an empty list has no mapping that says what grouping was ' + 'meant. Metadata AT REST is left exactly as stored — `properties` on a page component is ' + 'not parsed on the save path, so a stored page keeps loading and renders as it does today; ' - + 'the component-props gate reports such a value as an advisory `component-props-invalid` ' - + 'finding at the offending path on `os validate`, `os build` and `os lint`, a padded name ' + + 'the component-props gate reports such a value as a `component-props-invalid` ' + + 'error at the offending path on `os validate`, `os build` and `os lint`, a padded name ' + 'with the received value and the unpadded name to write. ADR-0049 / ADR-0087.', acceptanceCriteria: 'Every `object-grid` and `object-kanban` node in your pages either omits `grouping` or ' diff --git a/packages/spec/src/migrations/entries/semantic/18.ui-object-form-custom-fields-typed.ts b/packages/spec/src/migrations/entries/semantic/18.ui-object-form-custom-fields-typed.ts index 938af4da280..2835653a4d8 100644 --- a/packages/spec/src/migrations/entries/semantic/18.ui-object-form-custom-fields-typed.ts +++ b/packages/spec/src/migrations/entries/semantic/18.ui-object-form-custom-fields-typed.ts @@ -35,8 +35,8 @@ export const entry: SemanticMigration = { + '`visibleWhen` — and its `value` is any string, number or boolean, kept as written: an inline field binds ' + 'no object column, so a stored field\'s lowercase identifier rule does not apply to it. It is read where ' + 'every page component\'s props are: the component-props gate ' - + 'reports a refused value as an advisory `component-props-invalid` / `component-props-unknown-key` ' - + 'finding on `objectstack validate`, `objectstack build` and `objectstack lint`, and a stored page still ' + + 'reports a refused value as a `component-props-invalid` / `component-props-unknown-key` ' + + 'error on `objectstack validate`, `objectstack build` and `objectstack lint`, and a stored page still ' + 'saves and loads, because a page component\'s `properties` is not parsed on the metadata save or load ' + 'path. No conversion is registered: nothing on the load path refuses the shape, and the authored census, ' + 'with every inline option list evaluated, found no working member to respell. Deployed metadata NOT ' diff --git a/packages/spec/src/migrations/entries/semantic/18.ui-object-form-fields-names-typed.ts b/packages/spec/src/migrations/entries/semantic/18.ui-object-form-fields-names-typed.ts index bebf6506984..12aa4871bbe 100644 --- a/packages/spec/src/migrations/entries/semantic/18.ui-object-form-fields-names-typed.ts +++ b/packages/spec/src/migrations/entries/semantic/18.ui-object-form-fields-names-typed.ts @@ -30,8 +30,8 @@ export const entry: SemanticMigration = { + 'fields — keeping only a STORED one readable; so both rows now take field names, and refuse an object entry with what to write instead: a ' + '`{ name }` entry is its bare name, and a `{ field }` entry — the `sections[].fields` vocabulary, which ' + 'the form skips at the top level with a console warning — is its bare name or belongs in a section. It is ' - + 'read where every page component\'s props are: the component-props gate reports a refused value as an ' - + 'advisory `component-props-invalid` finding on `objectstack validate`, `objectstack build` and ' + + 'read where every page component\'s props are: the component-props gate reports a refused value as a ' + + '`component-props-invalid` error on `objectstack validate`, `objectstack build` and ' + '`objectstack lint`, and a stored page still saves and loads, because a page component\'s `properties` is ' + 'not parsed on the metadata save or load path. No conversion is registered: nothing on the load path ' + 'refuses the shape, the form already draws a stored `{ name }` entry by its name, and an override written ' diff --git a/packages/spec/src/migrations/entries/semantic/18.ui-object-form-members-typed.ts b/packages/spec/src/migrations/entries/semantic/18.ui-object-form-members-typed.ts index a579243d979..b384035c35e 100644 --- a/packages/spec/src/migrations/entries/semantic/18.ui-object-form-members-typed.ts +++ b/packages/spec/src/migrations/entries/semantic/18.ui-object-form-members-typed.ts @@ -39,8 +39,8 @@ export const entry: SemanticMigration = { + 'open, because the form draws a `{ name }` field entry and an inline runtime field inside a section, ' + 'which the typed shapes would refuse; and `customFields` stays open until the spec declares the ' + 'runtime form field its entries are. It is read where every page component\'s props are: the ' - + 'component-props gate reports a refused value as an advisory `component-props-invalid` / ' - + '`component-props-unknown-key` finding on `objectstack validate`, `objectstack build` and ' + + 'component-props gate reports a refused value as a `component-props-invalid` / ' + + '`component-props-unknown-key` error on `objectstack validate`, `objectstack build` and ' + '`objectstack lint`, and a stored page still saves and loads, because a page component\'s ' + '`properties` is not parsed on the metadata save or load path. No conversion is registered: nothing ' + 'on the load path refuses the shape, and an off-shape value has no rewrite that both keeps what the ' diff --git a/packages/spec/src/migrations/entries/semantic/18.ui-object-form-sections-typed.ts b/packages/spec/src/migrations/entries/semantic/18.ui-object-form-sections-typed.ts index 2dff86b334e..3d571cf6c23 100644 --- a/packages/spec/src/migrations/entries/semantic/18.ui-object-form-sections-typed.ts +++ b/packages/spec/src/migrations/entries/semantic/18.ui-object-form-sections-typed.ts @@ -34,8 +34,8 @@ export const entry: SemanticMigration = { + 'their own, the stored form view unchanged: the form view\'s section keys plus the three entry arms, ' + 'canonical spellings only, a label a plain string because the form draws it as it stands, and the form ' + 'view\'s group-reference rule. It is read where every page component\'s props are: the component-props ' - + 'gate reports a refused value as an advisory `component-props-invalid` / `component-props-unknown-key` ' - + 'finding on `objectstack validate`, `objectstack build` and `objectstack lint`, and a stored page still ' + + 'gate reports a refused value as a `component-props-invalid` / `component-props-unknown-key` ' + + 'error on `objectstack validate`, `objectstack build` and `objectstack lint`, and a stored page still ' + 'saves and loads, because a page component\'s `properties` is not parsed on the metadata save or load ' + 'path. No conversion is registered: nothing on the load path refuses the shape, and the authored census, ' + 'with every inline option list evaluated, found no working section to respell. Deployed metadata NOT ' diff --git a/packages/spec/src/migrations/entries/semantic/18.ui-object-gantt-markers-typed.ts b/packages/spec/src/migrations/entries/semantic/18.ui-object-gantt-markers-typed.ts index 9d4658fe0a8..f1a93a3ef78 100644 --- a/packages/spec/src/migrations/entries/semantic/18.ui-object-gantt-markers-typed.ts +++ b/packages/spec/src/migrations/entries/semantic/18.ui-object-gantt-markers-typed.ts @@ -27,8 +27,8 @@ export const entry: SemanticMigration = { + 'and in the default colour. The spec now declares objectui\'s own authoring declaration of a marker, ' + '`{ date, label?, color? }` with `date` a string (authored metadata is JSON, which cannot carry a ' + '`Date`), closed as every element shape on that map is. It is read where every page component\'s props ' - + 'are: the component-props gate reports a refused value as an advisory `component-props-invalid` / ' - + '`component-props-unknown-key` finding on `objectstack validate`, `objectstack build` and `objectstack ' + + 'are: the component-props gate reports a refused value as a `component-props-invalid` / ' + + '`component-props-unknown-key` error on `objectstack validate`, `objectstack build` and `objectstack ' + 'lint`, and a stored page still saves and loads, because a page component\'s `properties` is not parsed ' + 'on the metadata save or load path. No conversion is registered: nothing on the load path refuses the ' + 'shape, and a misspelled member has no rewrite that says which member the author meant — which is the ' diff --git a/packages/spec/src/migrations/entries/semantic/18.ui-object-grid-columns-typed.ts b/packages/spec/src/migrations/entries/semantic/18.ui-object-grid-columns-typed.ts index 1f01615854d..8f4c8ca9bd1 100644 --- a/packages/spec/src/migrations/entries/semantic/18.ui-object-grid-columns-typed.ts +++ b/packages/spec/src/migrations/entries/semantic/18.ui-object-grid-columns-typed.ts @@ -35,8 +35,8 @@ export const entry: SemanticMigration = { + 'field\'s; the renderer has since retired that read and takes the labels from the object field only, so ' + 'the row takes the list view\'s own `columns` by reference — the column entry a list view already ' + 'refuses an undeclared key on. It is read where every page component\'s props are: the component-props ' - + 'gate reports a refused value as an advisory `component-props-invalid` / `component-props-unknown-key` ' - + 'finding on `objectstack validate`, `objectstack build` and `objectstack lint`, and a stored page still ' + + 'gate reports a refused value as a `component-props-invalid` / `component-props-unknown-key` ' + + 'error on `objectstack validate`, `objectstack build` and `objectstack lint`, and a stored page still ' + 'saves and loads, because a page component\'s `properties` is not parsed on the metadata save or load path. ' + 'No conversion is registered: nothing on the load path refuses the shape, and a refused column has no ' + 'rewrite that both keeps what the grid draws today and honours what the author wrote — which is the ' diff --git a/packages/spec/src/migrations/entries/semantic/18.ui-object-grid-export-options-closed.ts b/packages/spec/src/migrations/entries/semantic/18.ui-object-grid-export-options-closed.ts index db103d57424..7e6d6f1a4e9 100644 --- a/packages/spec/src/migrations/entries/semantic/18.ui-object-grid-export-options-closed.ts +++ b/packages/spec/src/migrations/entries/semantic/18.ui-object-grid-export-options-closed.ts @@ -33,8 +33,8 @@ export const entry: SemanticMigration = { + 'surface that never read it: a bare array is refused with the object form named, a format ' + 'outside the enum is refused at its index (`pdf` with its retirement text), and a key the ' + 'object does not declare is named. It is read where every page component\'s props are: the ' - + 'component-props gate reports these as an advisory `component-props-invalid` / ' - + '`component-props-unknown-key` finding on `objectstack validate`, `objectstack build` and ' + + 'component-props gate reports these as `component-props-invalid` / ' + + '`component-props-unknown-key` errors on `objectstack validate`, `objectstack build` and ' + '`objectstack lint`, and a stored page still saves and loads, because a page component\'s ' + '`properties` is not parsed on the metadata save or load path. No conversion is registered: ' + 'nothing on the load path refuses the shape; a bare array has no rewrite that both keeps ' diff --git a/packages/spec/src/migrations/entries/semantic/18.ui-object-grid-kanban-calendar-list-members-typed.ts b/packages/spec/src/migrations/entries/semantic/18.ui-object-grid-kanban-calendar-list-members-typed.ts index 2b90ca91d30..6c6a40257f3 100644 --- a/packages/spec/src/migrations/entries/semantic/18.ui-object-grid-kanban-calendar-list-members-typed.ts +++ b/packages/spec/src/migrations/entries/semantic/18.ui-object-grid-kanban-calendar-list-members-typed.ts @@ -41,8 +41,8 @@ export const entry: SemanticMigration = { + 'on every door that carries it. The grid\'s `columns` is not narrowed: its group-header labels read ' + 'an authored column\'s `options`, which the list view\'s column entry does not declare, so it stays ' + 'open until that read is ruled. It is read where every page component\'s props are: the ' - + 'component-props gate reports a refused value as an advisory `component-props-invalid` / ' - + '`component-props-unknown-key` finding on `objectstack validate`, `objectstack build` and ' + + 'component-props gate reports a refused value as a `component-props-invalid` / ' + + '`component-props-unknown-key` error on `objectstack validate`, `objectstack build` and ' + '`objectstack lint`, and a stored page still saves and loads, because a page component\'s ' + '`properties` is not parsed on the metadata save or load path. No conversion is registered: nothing ' + 'on the load path refuses the shape, and an off-shape value has no rewrite that both keeps what the ' diff --git a/packages/spec/src/migrations/entries/semantic/18.ui-object-grid-page-size-positive-integer-refused.ts b/packages/spec/src/migrations/entries/semantic/18.ui-object-grid-page-size-positive-integer-refused.ts index ccc995ffa03..6d8f9b85b14 100644 --- a/packages/spec/src/migrations/entries/semantic/18.ui-object-grid-page-size-positive-integer-refused.ts +++ b/packages/spec/src/migrations/entries/semantic/18.ui-object-grid-page-size-positive-integer-refused.ts @@ -41,7 +41,7 @@ export const entry: SemanticMigration = { + 'and keeps them, and absence stays absence. A stored page whose `object-grid` node ' + 'carries `pagination: { pageSize: 0 }` still saves and loads — `properties` on a page ' + 'component is not parsed on the metadata save path — and the component-props gate ' - + 'reports it as an advisory `component-props-invalid` finding at `pagination.pageSize` ' + + 'reports it as a `component-props-invalid` error at `pagination.pageSize` ' + 'on `os validate`, `os build` and `os lint`; a `pageSizeOptions` entry and the flat ' + '`pageSize` shorthand are reported the same way at their own paths. The author deletes ' + 'the key or writes the page size they meant, and `os validate` then reports no ' diff --git a/packages/spec/src/migrations/entries/semantic/18.ui-object-grid-row-members-typed.ts b/packages/spec/src/migrations/entries/semantic/18.ui-object-grid-row-members-typed.ts index ee11d9d070e..cf4599c5500 100644 --- a/packages/spec/src/migrations/entries/semantic/18.ui-object-grid-row-members-typed.ts +++ b/packages/spec/src/migrations/entries/semantic/18.ui-object-grid-row-members-typed.ts @@ -34,8 +34,8 @@ export const entry: SemanticMigration = { + 'toggle nothing reads toggled nothing. The row now takes the list view\'s own schemas for the ' + 'five members a list view declares, and the measured shape for `aggregations` and ' + '`operations`, so one value is judged the same way on both doors. It is read where every page ' - + 'component\'s props are: the component-props gate reports a refused value as an advisory ' - + '`component-props-invalid` / `component-props-unknown-key` finding on `objectstack validate`, ' + + 'component\'s props are: the component-props gate reports a refused value as a ' + + '`component-props-invalid` / `component-props-unknown-key` error on `objectstack validate`, ' + '`objectstack build` and `objectstack lint`, and a stored page still saves and loads, because a ' + 'page component\'s `properties` is not parsed on the metadata save or load path. No conversion ' + 'is registered: nothing on the load path refuses the shape, and an off-shape value has no ' diff --git a/packages/spec/src/migrations/entries/semantic/18.ui-object-kanban-conditional-formatting-typed.ts b/packages/spec/src/migrations/entries/semantic/18.ui-object-kanban-conditional-formatting-typed.ts index fe4884cdbc8..d8621393178 100644 --- a/packages/spec/src/migrations/entries/semantic/18.ui-object-kanban-conditional-formatting-typed.ts +++ b/packages/spec/src/migrations/entries/semantic/18.ui-object-kanban-conditional-formatting-typed.ts @@ -25,8 +25,8 @@ export const entry: SemanticMigration = { + 'row declared it `z.unknown()`, so `42`, a bare string or a rule with no `style` passed the ' + 'component-props gate and the board painted no card for it. The row now takes the list view\'s own ' + 'member, by reference, as `object-grid` does, so one rule is judged the same way on every door. It is ' - + 'read where every page component\'s props are: the component-props gate reports a refused value as an ' - + 'advisory `component-props-invalid` / `component-props-unknown-key` finding on `objectstack validate`, ' + + 'read where every page component\'s props are: the component-props gate reports a refused value as a ' + + '`component-props-invalid` / `component-props-unknown-key` error on `objectstack validate`, ' + '`objectstack build` and `objectstack lint`, and a stored page still saves and loads, because a page ' + 'component\'s `properties` is not parsed on the metadata save or load path. No conversion is ' + 'registered: nothing on the load path refuses the shape, and the authored census found no working rule ' diff --git a/packages/spec/src/migrations/entries/semantic/18.ui-object-map-gantt-tree-navigation-typed.ts b/packages/spec/src/migrations/entries/semantic/18.ui-object-map-gantt-tree-navigation-typed.ts index e9bddf3dbae..10c34116d99 100644 --- a/packages/spec/src/migrations/entries/semantic/18.ui-object-map-gantt-tree-navigation-typed.ts +++ b/packages/spec/src/migrations/entries/semantic/18.ui-object-map-gantt-tree-navigation-typed.ts @@ -27,8 +27,8 @@ export const entry: SemanticMigration = { + 'and a bare mode string such as `\'drawer\'` both opened the record page, whatever they named. The ' + 'three rows now take the list view\'s schema by reference, so one value is judged the same way on ' + 'every door that carries it. It is read where every page component\'s props are: the ' - + 'component-props gate reports a refused value as an advisory `component-props-invalid` / ' - + '`component-props-unknown-key` finding on `objectstack validate`, `objectstack build` and ' + + 'component-props gate reports a refused value as a `component-props-invalid` / ' + + '`component-props-unknown-key` error on `objectstack validate`, `objectstack build` and ' + '`objectstack lint`, and a stored page still saves and loads, because a page component\'s ' + '`properties` is not parsed on the metadata save or load path. No conversion is registered: ' + 'nothing on the load path refuses the shape, and an off-shape value has no rewrite that both keeps ' diff --git a/packages/spec/src/migrations/entries/semantic/18.ui-object-master-detail-form-details-closed.ts b/packages/spec/src/migrations/entries/semantic/18.ui-object-master-detail-form-details-closed.ts index 4a600a8a441..3fe236bb253 100644 --- a/packages/spec/src/migrations/entries/semantic/18.ui-object-master-detail-form-details-closed.ts +++ b/packages/spec/src/migrations/entries/semantic/18.ui-object-master-detail-form-details-closed.ts @@ -40,7 +40,7 @@ export const entry: SemanticMigration = { + 'entry is now a strict shape and its `columns` references the column schema, so every rule ' + 'that schema holds applies here too, with its own prescription. The entry half is read where ' + 'every page component\'s props are: the component-props gate reports a failing entry or ' - + 'column as an advisory `component-props-unknown-key` / `component-props-invalid` finding on ' + + 'column as a `component-props-unknown-key` / `component-props-invalid` error on ' + '`objectstack validate`, `objectstack build` and `objectstack lint`, and a stored page still ' + 'saves and loads, because a page component\'s `properties` is not parsed on the metadata save ' + 'or load path. The identity-only half is `defineStack`\'s cross-reference check, which already ' diff --git a/packages/spec/src/migrations/entries/semantic/18.ui-object-metric-aggregate-trend-typed.ts b/packages/spec/src/migrations/entries/semantic/18.ui-object-metric-aggregate-trend-typed.ts index c534011f072..f072e718af4 100644 --- a/packages/spec/src/migrations/entries/semantic/18.ui-object-metric-aggregate-trend-typed.ts +++ b/packages/spec/src/migrations/entries/semantic/18.ui-object-metric-aggregate-trend-typed.ts @@ -42,7 +42,7 @@ export const entry: SemanticMigration = { + 'declares a `filter` the tile never reads and refuses a `report` it draws, and the dashboard widget\'s ' + 'comparison declares a `dimension` this path never reads, so each waits on a ruling between the ' + 'reference and the read. It is read where every page component\'s props are: the component-props gate ' - + 'reports a refused value as an advisory `component-props-invalid` / `component-props-unknown-key` finding ' + + 'reports a refused value as a `component-props-invalid` / `component-props-unknown-key` error ' + 'on `objectstack validate`, `objectstack build` and `objectstack lint`, and a stored page still saves and ' + 'loads, because a page component\'s `properties` is not parsed on the metadata save or load path. No ' + 'conversion is registered: nothing on the load path refuses the shape, and an off-shape value has no ' diff --git a/packages/spec/src/migrations/entries/semantic/18.ui-object-metric-compare-to-typed.ts b/packages/spec/src/migrations/entries/semantic/18.ui-object-metric-compare-to-typed.ts index ec7c633e91d..e393db8d928 100644 --- a/packages/spec/src/migrations/entries/semantic/18.ui-object-metric-compare-to-typed.ts +++ b/packages/spec/src/migrations/entries/semantic/18.ui-object-metric-compare-to-typed.ts @@ -30,8 +30,8 @@ export const entry: SemanticMigration = { + 'dataset path hands `dimension` to the analytics executor. The row now takes `{ kind }`, with `kind` the ' + 'dashboard widget comparison\'s own member by reference, and refuses `dimension` by name with that ' + 'prescription rather than accepting a key the tile ignores. It is read where every page component\'s props ' - + 'are: the component-props gate reports a refused value as an advisory `component-props-invalid` / ' - + '`component-props-unknown-key` finding on `objectstack validate`, `objectstack build` and `objectstack ' + + 'are: the component-props gate reports a refused value as a `component-props-invalid` / ' + + '`component-props-unknown-key` error on `objectstack validate`, `objectstack build` and `objectstack ' + 'lint`, and a stored page still saves and loads, because a page component\'s `properties` is not parsed on ' + 'the metadata save or load path. No conversion is registered: nothing on the load path refuses the shape, ' + 'and a `dimension` has no rewrite that keeps the window the author meant — which is the judgment this entry ' diff --git a/packages/spec/src/migrations/entries/semantic/18.ui-object-metric-drill-down-report-typed.ts b/packages/spec/src/migrations/entries/semantic/18.ui-object-metric-drill-down-report-typed.ts index e1ce2ddb386..9867aac4929 100644 --- a/packages/spec/src/migrations/entries/semantic/18.ui-object-metric-drill-down-report-typed.ts +++ b/packages/spec/src/migrations/entries/semantic/18.ui-object-metric-drill-down-report-typed.ts @@ -26,7 +26,7 @@ export const entry: SemanticMigration = { + 'the records instead. The row now takes `ReportSchema` by reference — the declaration objectui already ' + 'names for the member — and, since a joined report refuses a block that binds no `dataset`, every report it ' + 'admits is one the drawer draws. It is read where every page component\'s props are: the component-props gate ' - + 'reports a refused value as an advisory `component-props-invalid` / `component-props-unknown-key` finding on ' + + 'reports a refused value as a `component-props-invalid` / `component-props-unknown-key` error on ' + '`objectstack validate`, `objectstack build` and `objectstack lint`, and a stored page still saves and loads, ' + 'because a page component\'s `properties` is not parsed on the metadata save or load path. No conversion is ' + 'registered: nothing on the load path refuses the shape, and the authored census found no drawn report to ' diff --git a/packages/spec/src/migrations/entries/semantic/18.ui-object-metric-drill-down-typed.ts b/packages/spec/src/migrations/entries/semantic/18.ui-object-metric-drill-down-typed.ts index b7e465858d6..41e35d01152 100644 --- a/packages/spec/src/migrations/entries/semantic/18.ui-object-metric-drill-down-typed.ts +++ b/packages/spec/src/migrations/entries/semantic/18.ui-object-metric-drill-down-typed.ts @@ -34,7 +34,7 @@ export const entry: SemanticMigration = { + 'shape is not taken whole, because it declares `filter`. The drill `report` stays open: the tile draws a ' + 'dataset-bound report through the shared drawer, but no spec drill shape declares a `report` member yet. ' + 'It is read where every page component\'s props are: the component-props gate reports a refused value as ' - + 'an advisory `component-props-invalid` / `component-props-unknown-key` finding on `objectstack validate`, ' + + 'a `component-props-invalid` / `component-props-unknown-key` error on `objectstack validate`, ' + '`objectstack build` and `objectstack lint`, and a stored page still saves and loads, because a page ' + 'component\'s `properties` is not parsed on the metadata save or load path. No conversion is registered: ' + 'nothing on the load path refuses the shape, and a drill `filter` has no rewrite that keeps the scope the ' diff --git a/packages/spec/src/migrations/entries/semantic/18.ui-object-timeline-items-typed.ts b/packages/spec/src/migrations/entries/semantic/18.ui-object-timeline-items-typed.ts index c94182dfdf1..3e3eb2caa85 100644 --- a/packages/spec/src/migrations/entries/semantic/18.ui-object-timeline-items-typed.ts +++ b/packages/spec/src/migrations/entries/semantic/18.ui-object-timeline-items-typed.ts @@ -29,8 +29,8 @@ export const entry: SemanticMigration = { + 'gate, and the rail drew an empty, unlabelled entry. The row now takes objectui\'s two ruled kinds, closed, ' + 'and pairs each entry with the kind its `variant` selects; a feed entry\'s `content` (child components) is ' + 'held unjudged until a writer appears. It is read where every page component\'s props are: the ' - + 'component-props gate reports a refused value as an advisory `component-props-invalid` / ' - + '`component-props-unknown-key` finding on `objectstack validate`, `objectstack build` and `objectstack lint`, ' + + 'component-props gate reports a refused value as a `component-props-invalid` / ' + + '`component-props-unknown-key` error on `objectstack validate`, `objectstack build` and `objectstack lint`, ' + 'and a stored page still saves and loads, because a page component\'s `properties` is not parsed on the ' + 'metadata save or load path. No conversion is registered: nothing on the load path refuses the shape, and ' + 'the authored census found no drawn entry to respell. Deployed metadata NOT MEASURED.', diff --git a/packages/spec/src/migrations/entries/semantic/18.ui-object-timeline-mapping-typed.ts b/packages/spec/src/migrations/entries/semantic/18.ui-object-timeline-mapping-typed.ts index 9fb9260db9a..373e386ac16 100644 --- a/packages/spec/src/migrations/entries/semantic/18.ui-object-timeline-mapping-typed.ts +++ b/packages/spec/src/migrations/entries/semantic/18.ui-object-timeline-mapping-typed.ts @@ -25,8 +25,8 @@ export const entry: SemanticMigration = { + 'bare field name, a non-string binding or a misspelled member passed the component-props gate, and the ' + 'rail bound nothing for it and drew the default field. The spec now declares objectui\'s own declaration ' + 'of the binding record, four optional field names, closed as every element shape on that map is. It is ' - + 'read where every page component\'s props are: the component-props gate reports a refused value as an ' - + 'advisory `component-props-invalid` / `component-props-unknown-key` finding on `objectstack validate`, ' + + 'read where every page component\'s props are: the component-props gate reports a refused value as a ' + + '`component-props-invalid` / `component-props-unknown-key` error on `objectstack validate`, ' + '`objectstack build` and `objectstack lint`, and a stored page still saves and loads, because a page ' + 'component\'s `properties` is not parsed on the metadata save or load path. No conversion is ' + 'registered: nothing on the load path refuses the shape, and the authored census found nothing to ' diff --git a/packages/spec/src/migrations/entries/semantic/18.ui-record-line-items-props-closed.ts b/packages/spec/src/migrations/entries/semantic/18.ui-record-line-items-props-closed.ts index bd14429b760..db812e4bfc4 100644 --- a/packages/spec/src/migrations/entries/semantic/18.ui-record-line-items-props-closed.ts +++ b/packages/spec/src/migrations/entries/semantic/18.ui-record-line-items-props-closed.ts @@ -39,8 +39,8 @@ export const entry: SemanticMigration = { + 'renderer\'s read points at the objectui pin, not from the registration\'s declared-input list, ' + 'and its `columns` references the column schema, so every rule that schema holds applies here ' + 'too, with its own prescription. It is read where every page component\'s props are: the ' - + 'component-props gate reports a failing key or column as an advisory ' - + '`component-props-unknown-key` / `component-props-invalid` finding on `objectstack validate`, ' + + 'component-props gate reports a failing key or column as a ' + + '`component-props-unknown-key` / `component-props-invalid` error on `objectstack validate`, ' + '`objectstack build` and `objectstack lint`, and a stored page still saves and loads, because a ' + 'page component\'s `properties` is not parsed on the metadata save or load path. `defineStack`\'s ' + 'identity-only column check does not reach this block: the panel hands its columns to the grid ' diff --git a/packages/spec/src/migrations/registry.ts.template b/packages/spec/src/migrations/registry.ts.template index 2b9b0cc1be6..bbb46ed73a7 100644 --- a/packages/spec/src/migrations/registry.ts.template +++ b/packages/spec/src/migrations/registry.ts.template @@ -2772,7 +2772,7 @@ const STEP18_RATIONALE: readonly RationaleFragment[] = [ + 'unless the member\'s `type` is `api`, whose object `params` keeps its request-payload window. `params` ' + 'carries one shape and no second value-bag key is declared, so an object `params` on any other member, which ' + 'parsed and then reached no action, is refused at `actions.N.params` with the prescription to author an action ' - + 'with static parameter values as its own `action:button` node. Read by the component-props gate (advisory); a ' + + 'with static parameter values as its own `action:button` node. Read by the component-props gate (refused as an error); a ' + 'stored page still saves and loads, so no conversion is registered. Its D3 record is the semantic entry ' + '`ui-action-group-menu-member-params-array-only`.', }, @@ -2787,7 +2787,7 @@ const STEP18_RATIONALE: readonly RationaleFragment[] = [ + 'reads — an `action:menu` item reads no `size` and declares none — with the rows\' prescriptions; ' + '`outcomeMessages`, a member `className` and a member `properties.params` are refused, and ' + '`outcomeMessages` stays undeclared on all four action blocks as one decision. Read by the ' - + 'component-props gate (advisory); a stored page still saves and loads, so no conversion is registered. ' + + 'component-props gate (refused as an error); a stored page still saves and loads, so no conversion is registered. ' + 'Its D3 record is the semantic entry `ui-action-group-menu-members-typed`.', }, { @@ -2832,7 +2832,7 @@ const STEP18_RATIONALE: readonly RationaleFragment[] = [ + '`name` or a misspelled member passed every door and the form drew the field without it. The spec now ' + 'declares a closed runtime form field of the members the form draws, in camelCase, keyed by `name` — ' + 'the `grid` widget\'s snake_case keys stay out until the widget reads a camelCase spelling — and the ' - + 'row takes a list of it. Read by the component-props gate (advisory); a stored page still saves and ' + + 'row takes a list of it. Read by the component-props gate (refused as an error); a stored page still saves and ' + 'loads, so no conversion is registered. Its D3 record is the semantic entry ' + '`ui-object-form-custom-fields-typed`.', }, @@ -2847,7 +2847,7 @@ const STEP18_RATIONALE: readonly RationaleFragment[] = [ + 'authoring face, drawing only a stored one by its name. So both rows take field names, objectui\'s own ' + 'declaration of the member, and refuse an object entry with what to write instead — a `{ name }` entry ' + 'is its bare name, and a `{ field }` entry belongs in a section. Read by the component-props gate ' - + '(advisory); a stored page still saves and loads, so no conversion is registered. Its D3 record is the ' + + '(refused as an error); a stored page still saves and loads, so no conversion is registered. Its D3 record is the ' + 'semantic entry `ui-object-form-fields-names-typed`.', }, { @@ -2862,7 +2862,7 @@ const STEP18_RATIONALE: readonly RationaleFragment[] = [ + 'take the measured shape. The form\'s `fields` and `sections` and the master-detail form\'s two ' + 'stay open — the form draws a `{ name }` field entry and an inline runtime field inside a section, ' + 'which the typed shapes would refuse — and `customFields` stays open until the spec declares the ' - + 'runtime form field its entries are. Read by the component-props gate (advisory); a stored page ' + + 'runtime form field its entries are. Read by the component-props gate (refused as an error); a stored page ' + 'still saves and loads, so no conversion is registered. Its D3 record is the semantic entry ' + '`ui-object-form-members-typed`.', }, @@ -2878,7 +2878,7 @@ const STEP18_RATIONALE: readonly RationaleFragment[] = [ + 'form field — in canonical spellings only: a page block\'s `properties` is never parsed on the way to ' + 'the form, so a deprecated section `visibleOn` or a string `columns`, which a form view folds at ' + 'parse, was dropped, and is refused with the canonical spelling. The stored form view is unchanged. ' - + 'Read by the component-props gate (advisory); a stored page still saves and loads, so no conversion is ' + + 'Read by the component-props gate (refused as an error); a stored page still saves and loads, so no conversion is ' + 'registered. Its D3 record is the semantic entry `ui-object-form-sections-typed`.', }, { @@ -2889,7 +2889,7 @@ const STEP18_RATIONALE: readonly RationaleFragment[] = [ + '`z.unknown()` because the marker contract lived only in objectui, so a marker with no `date`, a numeric ' + '`date` or a misspelled member passed every door and the chart drew no line, or drew it unlabelled. The ' + 'spec now declares objectui\'s own authoring declaration of a marker, `{ date, label?, color? }` with ' - + '`date` a string, and the row takes it. Read by the component-props gate (advisory); a stored page still ' + + '`date` a string, and the row takes it. Read by the component-props gate (refused as an error); a stored page still ' + 'saves and loads, so no conversion is registered. Its D3 record is the semantic entry ' + '`ui-object-gantt-markers-typed`.', }, @@ -2904,7 +2904,7 @@ const STEP18_RATIONALE: readonly RationaleFragment[] = [ + 'takes the list view\'s own `columns` by reference — all field names or all column entries — and a ' + 'column keyed `accessorKey` / `header` / `name`, a mixed list or an undeclared column key (`editable`, ' + '`options`, `reference`), which passed every door and drew no column or was ignored, is refused. Read by ' - + 'the component-props gate (advisory); a stored page still saves and loads, so no conversion is ' + + 'the component-props gate (refused as an error); a stored page still saves and loads, so no conversion is ' + 'registered. Its D3 record is the semantic entry `ui-object-grid-columns-typed`.', }, { @@ -2918,8 +2918,8 @@ const STEP18_RATIONALE: readonly RationaleFragment[] = [ + 'the author\'s list was dropped. The row now takes the list view\'s five-member export ' + 'options object by identity, not the list view\'s union, and refuses a bare array with the ' + 'object form named, a format outside the enum and an undeclared key. Page-component ' - + '`properties` is read by the component-props gate, which reports these as advisory ' - + 'findings, and is not parsed on the metadata save or load path, so a stored page still saves ' + + '`properties` is read by the component-props gate, which reports these as ' + + 'errors, and is not parsed on the metadata save or load path, so a stored page still saves ' + 'and loads and no conversion is registered: the bare array never worked here, and lifting it ' + 'would change the menu a deployed grid shows. The authored census found nothing to respell. ' + 'Its D3 record is the semantic entry `ui-object-grid-export-options-closed`.', @@ -2937,7 +2937,7 @@ const STEP18_RATIONALE: readonly RationaleFragment[] = [ + '(`batchActions`, the spelling the grid reads first, takes `bulkActions`\'s); the grid\'s `fields` ' + 'and `selectable` and the kanban lane take the measured shape. The grid\'s `columns` stays open: its ' + 'group headers draw an authored column\'s `options`, which the list view\'s column entry does not ' - + 'declare. Read by the component-props gate (advisory); a stored page still saves and loads, so no ' + + 'declare. Read by the component-props gate (refused as an error); a stored page still saves and loads, so no ' + 'conversion is registered. Its D3 record is the semantic entry ' + '`ui-object-grid-kanban-calendar-list-members-typed`.', }, @@ -2953,7 +2953,7 @@ const STEP18_RATIONALE: readonly RationaleFragment[] = [ + '`aggregations` takes the measured `[{ field, type }]` with the query AST\'s aggregation ' + 'functions, and `operations` the four booleans a grid read point names (`create`, ' + '`update`, `delete`, `export`), refusing `read` and `import`, which nothing reads. Read by ' - + 'the component-props gate (advisory); a stored page still saves and loads, so no conversion ' + + 'the component-props gate (refused as an error); a stored page still saves and loads, so no conversion ' + 'is registered. Its D3 record is the semantic entry `ui-object-grid-row-members-typed`.', }, { @@ -2966,7 +2966,7 @@ const STEP18_RATIONALE: readonly RationaleFragment[] = [ + 'passed every door and the board painted no card for it. objectui has since made the list view\'s ' + '`{ condition, style }` rule the member\'s only authoring dialect, and the board evaluates it with the ' + 'grid\'s evaluator, so the row takes the list view\'s own member by reference, as `object-grid` does. ' - + 'Read by the component-props gate (advisory); a stored page still saves and loads, so no conversion is ' + + 'Read by the component-props gate (refused as an error); a stored page still saves and loads, so no conversion is ' + 'registered. Its D3 record is the semantic entry `ui-object-kanban-conditional-formatting-typed`.', }, { @@ -2979,7 +2979,7 @@ const STEP18_RATIONALE: readonly RationaleFragment[] = [ + 'so `navigation: 42` and a bare mode string passed every door and opened the record page. The ' + 'three rows now take the list view\'s `NavigationConfigSchema` by reference, the carrier the grid, ' + 'kanban, calendar and timeline blocks already take. Read by the component-props gate ' - + '(advisory); a stored page still saves and loads, so no conversion is registered. Its D3 record ' + + '(refused as an error); a stored page still saves and loads, so no conversion is registered. Its D3 record ' + 'is the semantic entry `ui-object-map-gantt-tree-navigation-typed`.', }, { @@ -2993,7 +2993,7 @@ const STEP18_RATIONALE: readonly RationaleFragment[] = [ + 'went through `objectstack validate` green. ' + 'Each detail entry is now a strict shape of the twelve keys the renderer reads, and its ' + '`columns` references `InlineGridColumnSchema`. Page-component `properties` is read by the ' - + 'component-props gate, which reports a failing entry or column as an advisory finding, ' + + 'component-props gate, which reports a failing entry or column as an error, ' + 'and is not parsed on the metadata save or load path, so a stored page still saves and ' + 'loads and no conversion is registered; the authored census found nothing to respell. ' + '`defineStack`\'s identity-only check reaches the block wherever a page carries it, with ' @@ -3014,7 +3014,7 @@ const STEP18_RATIONALE: readonly RationaleFragment[] = [ + 'badge\'s measured shape. `drillDown` and `compareTo` stay open: each by-reference candidate declares a key ' + 'the tile never reads (the chart drill-down\'s `filter`, the dashboard comparison\'s `dimension`), and ' + 'the chart drill-down refuses the `report` the tile draws, so each waits on a ruling. Read by the ' - + 'component-props gate (advisory); a stored page still saves and loads, so no conversion is ' + + 'component-props gate (refused as an error); a stored page still saves and loads, so no conversion is ' + 'registered. Its D3 record is the semantic entry `ui-object-metric-aggregate-trend-typed`.', }, { @@ -3027,7 +3027,7 @@ const STEP18_RATIONALE: readonly RationaleFragment[] = [ + 'reference, and `dimension` refused by name, because this inline tile shifts the date macros in its own ' + '`filter` and never reads a dataset time dimension. A bare kind string, a kind outside the two and a ' + '`dimension` passed every door and compared the wrong window. Read by the component-props gate ' - + '(advisory); a stored page still saves and loads, so no conversion is registered. Its D3 record is the ' + + '(refused as an error); a stored page still saves and loads, so no conversion is registered. Its D3 record is the ' + 'semantic entry `ui-object-metric-compare-to-typed`.', }, { @@ -3039,7 +3039,7 @@ const STEP18_RATIONALE: readonly RationaleFragment[] = [ + 'reference (fork 1, letter B): it waited until a joined report refused a block that binds no dataset, ' + 'and since then every report the member admits is one the drill drawer draws — a report with no ' + '`dataset`, a bare report name or a `{ name }` reference, which the drawer answered by listing the ' - + 'records, is refused. Read by the component-props gate (advisory); a stored page still saves and loads, ' + + 'records, is refused. Read by the component-props gate (refused as an error); a stored page still saves and loads, ' + 'so no conversion is registered. Its D3 record is the semantic entry ' + '`ui-object-metric-drill-down-report-typed`.', }, @@ -3053,7 +3053,7 @@ const STEP18_RATIONALE: readonly RationaleFragment[] = [ + 'metric tile has no click event for a drill filter to resolve against and no row for `mode` to open; ' + 'both passed every door and were ignored. The drill `report` stays open: the tile draws a ' + 'dataset-bound report, but the spec declares no drill report yet, and declares that contract first. ' - + 'Read by the component-props gate (advisory); a stored page still saves and loads, so no conversion is ' + + 'Read by the component-props gate (refused as an error); a stored page still saves and loads, so no conversion is ' + 'registered. Its D3 record is the semantic entry `ui-object-metric-drill-down-typed`.', }, { @@ -3066,7 +3066,7 @@ const STEP18_RATIONALE: readonly RationaleFragment[] = [ + 'epoch milliseconds — and a row refinement pairs each entry with the kind the block\'s `variant` selects, ' + 'so a feed entry with no `title`, or a gantt row on a feed timeline, is refused instead of drawn empty. ' + 'A feed entry\'s `content` (child components) is held unjudged until a writer appears. Read by the ' - + 'component-props gate (advisory); a stored page still saves and loads, so no conversion is registered. ' + + 'component-props gate (refused as an error); a stored page still saves and loads, so no conversion is registered. ' + 'Its D3 record is the semantic entry `ui-object-timeline-items-typed`.', }, { @@ -3080,7 +3080,7 @@ const STEP18_RATIONALE: readonly RationaleFragment[] = [ + 'declaration of it, and the row takes it. The stage\'s other members — the metric drill-down\'s ' + '`report`, the form\'s `customFields` and both forms\' `sections`, the timeline\'s `items` and the ' + 'action containers\' members — stay open: each contract has more than one viable shape that no ruling ' - + 'decides yet. Read by the component-props gate (advisory); a stored page still saves and loads, so no ' + + 'decides yet. Read by the component-props gate (refused as an error); a stored page still saves and loads, so no ' + 'conversion is registered. Its D3 record is the semantic entry `ui-object-timeline-mapping-typed`.', }, { diff --git a/packages/spec/src/ui/component.test.ts b/packages/spec/src/ui/component.test.ts index 8fa14ef4092..2cdb442a71d 100644 --- a/packages/spec/src/ui/component.test.ts +++ b/packages/spec/src/ui/component.test.ts @@ -3098,9 +3098,10 @@ describe('批 17 — the carrier stays an open bag; the gate is on the lint side // Still true after #5068, and it is the sentence that keeps the gate honest: // the SCHEMA accepts and retains the key; what changed is that the authoring - // gate now REPORTS it (at `warning`). A reader who mistakes the gate for a - // closed door would be wrong in the direction that matters — the storage path - // (`saveMetaItem` / REST `/meta`) runs no such gate at all. + // gate now REFUSES it (an `error` at `os validate` / `os build` / `os lint`, + // since #23147). A reader who mistakes the gate for a closed door would be + // wrong in the direction that matters — the storage path (`saveMetaItem` / + // REST `/meta`) runs no such gate at all. it('an unknown key inside `properties` survives the LIVE page parse — with the strict sibling as negative control', () => { const page = { name: 'batch17_probe', diff --git a/packages/spec/src/ui/component.zod.ts b/packages/spec/src/ui/component.zod.ts index 34c8b368411..908ccbd62a3 100644 --- a/packages/spec/src/ui/component.zod.ts +++ b/packages/spec/src/ui/component.zod.ts @@ -301,10 +301,11 @@ import { INLINE_GRID_SORT_FIELD_LIST } from '../data/inline-grid-sort-fields'; // findings this gate reported on the platform pages (34 label + 8 content). // // The page rewrites (`page:card.visible` → the component-level `visibleWhen`, -// #5776's tab `key` → `value`) 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). +// #5776's tab `key` → `value`) closed the third, and the upgrade followed in +// two steps once the inventory measured empty: #23026 made a `retiredKey()` +// tombstone's verdict an `error` at `os validate` / `os build` / `os lint`, +// and #23147 made every other finding of the gate one too, on an inventory +// that also covered a corpus authored outside this repository. // // ⚠️ One inventory item batch A ADDED rather than closed, because measuring the // renderers turned it up: objectui's Studio block designer publishes inputs that @@ -5032,8 +5033,8 @@ export type ElementRepeaterPropsParsed = z.infer strictObject({ * own retirement text) and a key the block does not declare. * * No conversion lifts the array: it never worked on this surface. Read by the - * component-props gate (advisory); a stored page still saves and loads, - * because page-component `properties` is not parsed on the metadata save or - * load path (the `ui-object-grid-export-options-closed` semantic entry). + * component-props gate (refused as an error); a stored page still saves and + * loads, because page-component `properties` is not parsed on the metadata + * save or load path (the `ui-object-grid-export-options-closed` semantic + * entry). */ exportOptions: ListViewExportOptionsSchema.optional() .describe("Export config — the object `{ formats?, maxRecords?, includeHeaders?, fileNamePrefix?, streaming? }`, the same block a list view's `exportOptions` declares, with `formats` drawn from `csv`, `xlsx` and `json`. A bare format array is refused: the grid reads `exportOptions.formats`, so write `{ formats: ['csv', 'xlsx'] }`"),