Skip to content

feat(lint)!: every component-props finding is an error at os validate, os build and os lint - #23244

Merged
objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-23147-component-props-error-rest
Oct 13, 2026
Merged

objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-23147-component-props-error-rest

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #23147
Clause-②: no (narrowing: findings the CLI doors warn on today are refused)

What this does

This is the last step of the component-props gate's recorded plan (the maintainer's #5068 ruling, direction A). The gate's header said: "the warning-period inventory is the acceptance baseline for the error upgrade, and the upgrade is its own step once the inventory is empty." #23026 moved the retired-key class. This PR moves the rest, class (b), in one step with no staged window:

  • every component-props-invalid verdict that is not a retiredKey() tombstone: a wrong type, a missing required prop, a closed-set refusal (a url action's target scheme, a record:details section's count), a retired enum value, and a missing dataSource.object binding;
  • every component-props-unknown-key finding, on all three routes that file it: a strict schema's unrecognized_keys, a strict union arm, and the walker over a strip-mode object below the bag.

os validate, os build and os lint now refuse each of these. ComponentPropsSeverity narrows to 'error'. The retired-key classifier stays, because it picks the fix text (the tombstone's prescription, never a schema change). It no longer picks the severity. No new gate and no new rule id.

Door probe at c88fe84c1: a fixture page with page:header titel and record:related_list limit: 'five'.

  • os validate --json: exit 1, valid: false. Both findings are listed under errors with severity error: component-props-unknown-key at ...properties.titel and component-props-invalid at ...properties.limit.
  • os lint --json: exit 1, with the same two errors.
  • os build: exit 1 ("Author-time rules failed"), and no dist/ was written.
  • The four examples through os validate --json at the same head: exit 0, 0 component-props findings.

The inventory (acceptance 1 and 2), each class counted separately

The rule ran at its own input tier: loadConfig, then normalizeStackInput, then authoringRuleUnionStack, then validateComponentProps, using the built @objectstack/lint from this worktree.

population tree pages components walked / judged value verdicts unknown keys retired keys
examples/app-crm, app-todo, app-multi-package, app-showcase objectstack 8323cad97, and again at the final head c88fe84c1 29 198 / 131 0 0 0
eight shipped pages: platform-objects x3, plugin-approvals, qa/downstream-contract, cloud-connection x2, mcp same two trees 8 46 / 46 0 0 0
objectstack-ai/hotcrm: its 8 authored pages (src/sales/pages x7, src/service/pages x1) hotcrm 0f84c9c72dc2e2ced4990de28b737c5597a3d5b4 (shallow clone, read-only) 8 54 / 50 0 0 0
  • Positive controls. These ran on a clone of the stack, in the same run. A planted undeclared key on the first judged component drew 1 component-props-unknown-key finding on every population that has a page. A planted wrong-typed prop drew 1 component-props-invalid finding. The one exception is plugin-approvals, whose first judged component has no string prop to retype. On the final head, both controls file error.
  • Pages with nothing judged. app-todo, app-multi-package and the create-objectstack template author no page, and neither does packages/apps/* (git grep finds no pages, definePage or regions).
  • hotcrm. hotcrm's whole stack does not load under this tree's spec. defineStack refuses its eight emailTemplates[].category with the v18 tombstone. So its page exports were judged directly, and the stack's own load was not run. The pages import only @objectstack/spec, which resolved to this worktree's build.
  • Not measured. Deployed sys_metadata page rows, any other repository, and docs code samples that carry no os:check-yaml tag.

H2 (the split): both classes measured 0 on every population, so there is no partial move and no decision-box item.

surfaceReason (H3): measured false, corrected

RUNTIME_NEEDS_FULL_SNAPSHOT reads "reads a stack-wide collection the per-write snapshot does not carry".

  • What the rule reads: stack.pages and nothing else (validate-component-props.ts, the recordsOf(stack.pages) loop). Each page's components come through walkPageComponents (page-walk.ts:113), which reads only that page plus spec's static slot list. Each component is judged against the static ComponentPropsMap (lintUnknownKeysAgainstSchema and schema.safeParse), plus its own dataSource.
  • What a page write carries: the runtime gate maps page: 'pages' (runtime-gate.ts TYPE_TO_STACK_KEY). So a page write's snapshot carries the written page as pages[0] (snapshot keys objects, permissions, books, datasets, pages).
  • Measured over the showcase's 28 pages, through buildRuntimeWriteSnapshots, against the whole-stack run. The per-page findings were identical as authored (0 = 0, 28/28 pages equal). They were also identical with an undeclared key and a wrong-typed prop planted on each page's first judged component (19 = 19, 28/28 equal).

So the reason was false, and the entry now carries the page-local reason validateComponentTypes already states: the rule is held off the runtime door pending a measured false-refusal budget over stored tenant page rows. The rule is not added to the runtime publish door (triage 6137942125).

Pins and ablation

validate-component-props.test.ts has a new block, "every other class is an error too". It pins each class and each code route on rule id, severity and path, and leaves the text unpinned. The #23026 block keeps the retired-key pins. The sibling tests that asserted warning (validate-record-approval-decision-22472, validate-record-approvals-attachments-22537) now assert error.

Ablation. Five one-time legs ran through scripts/ablation-replace.mjs. Each leg reverted one severity site to warning. Every anchor hit exactly once (x1 to x0), and every blob moved. The tool's trap restored each leg by absolute path, and the restore was proven: blob e3ed3effd342 equals HEAD's, and git diff HEAD was empty. Nothing needed building: the test imports the rule from src. Each leg reddened exactly its own pins:

leg (site reverted) red / of 82
missing dataSource.object binding 5: the three #11509 it.each rows, the class pin, the pipeline pin
walker unknown key (strip object below the bag) 1: the walker pin
union-arm unknown key 1: the union-arm pin
strict unrecognized_keys 5: the titel pin, the #7751 specimen, two pipeline pins, the class pin
other value verdicts 8: wrong type x2, missing required, url target, record:details count, retired enum value, mixed union verdict, per-issue classification

ADR-0087 (H4) and the changeset

.changeset/23147-component-props-every-finding-refused.md:

  • @objectstack/lint: major, plus @objectstack/spec: patch (text only);
  • Clause-②: no (narrowing), a **BREAKING** line, and a FROM → TO table with its one-line fix;
  • exactly one adr-0087: not-required (already-registered …) marker. It names the 26 step-18 D3 entries whose refusal the gate carries: the 25 whose reason this PR amends, plus element-flat-data-binding-retired.

check-adr-0087-registration --base origin/main exits 0 ("1 declared-breaking changeset(s), each carrying an ADR-0087 disposition"). check-changeset-no-major exits 0. check:future-spec-major exits 0 (H5).

One correction to a pending release note, to be confirmed here. .changeset/23103-page-action-row-on-success-and-executor.md (#23164, merged, not yet released) said the props gate files its finding as "(that gate's one tier, a warning)". This PR changes that, and both notes ship in the same release. So the parenthesis now reads "(an error, like every finding of that gate)", and nothing else in the note changed. That is the DELIBERATE CORRECTION class, so check-empty-changeset stays red on purpose. Check Changeset is not a required context. I ask the approver to confirm this edit in writing on this PR.

Every gate-tier sentence this change made false, BEFORE → AFTER

The claim's file surface (6140712434) was amended by 6143270198. It now covers every comment, docblock or prose sentence in the repository that states the component-props gate's tier for a prop finding and is false on this head. The edits are text only: no schema, guidance string or .describe() changed, and no other rule's text. Published means shipped in @objectstack/spec@18.0.0-next.0 (dist/migrations carries the D3 reasons and the step-18 rationale; src/**/*.zod.ts ships as source).

Original surface (claim 6140712434)

  • packages/lint/src/validate-component-props.ts: the header's Severity section (the second step is recorded, with both inventories), the severity type, and every finding's severity. Three message tails changed: "nothing refuses it today, so the renderer receives the props as written", the missing-binding sentence's equivalent, and the unknown-key walker's "so nothing verifies it". Each now says the finding is refused and the stack does not pass until it is fixed.
  • packages/lint/src/authoring-rules.ts: the entry comment, and the surfaceReason (above).
  • content/docs/deployment/validating-metadata.mdx and content/docs/protocol/objectui/layout-dsl.mdx (published docs site):

content/docs/deployment/validating-metadata.mdx

BEFORE:
| Retired SDUI component props — a key `ComponentPropsMap` tombstones (`retiredKey()`), refused with its prescription (#23026) | ✓ | ✓ | ✓ | — |
AFTER:
| 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) | ✓ | ✓ | ✓ | — |

content/docs/deployment/validating-metadata.mdx

BEFORE:
| 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 | ✓ | ✓ | ✓ | — |
AFTER:
| Advisory: record titles, semantic field pointers (ADR-0085), form-section layout, action placement, seed replay/state safety, capability references, liveness | ✓ | ✓ | ✓ | — |

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

BEFORE:
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.
AFTER:
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.
  • 18.ui-object-master-detail-form-details-closed.ts:42-43, the string seat pointer 6139673366 names, is in the D3 list below. Every finding of the gate is now an error, so "reports a failing entry or column as a ... error" is true for the sortField tombstone too.

Published, outside the original list: 25 step-18 D3 entry reasons (packages/spec/src/migrations/entries/semantic/)

packages/spec/src/migrations/entries/semantic/18.element-data-source-and-object-block-filter-rule-array.ts

BEFORE:
+ 'the open `properties` bag — only as the component-props gate\'s advisory finding '
AFTER:
+ 'the open `properties` bag — only as the component-props gate\'s error '

packages/spec/src/migrations/entries/semantic/18.element-text-variant-heading-subheading-retired.ts

BEFORE:
+ '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 '
AFTER:
+ '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 '

packages/spec/src/migrations/entries/semantic/18.ui-action-group-menu-member-params-array-only.ts

BEFORE:
+ '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 '
AFTER:
+ '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 '

packages/spec/src/migrations/entries/semantic/18.ui-action-group-menu-members-typed.ts

BEFORE:
+ '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 '
AFTER:
+ '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 '

packages/spec/src/migrations/entries/semantic/18.ui-object-block-grouping-config-typed.ts

BEFORE:
+ '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 '
AFTER:
+ '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 '

packages/spec/src/migrations/entries/semantic/18.ui-object-form-custom-fields-typed.ts

BEFORE:
+ '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 '
AFTER:
+ '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 '

packages/spec/src/migrations/entries/semantic/18.ui-object-form-fields-names-typed.ts

BEFORE:
+ '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 '
AFTER:
+ '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 '

packages/spec/src/migrations/entries/semantic/18.ui-object-form-members-typed.ts

BEFORE:
+ 'component-props gate reports a refused value as an advisory `component-props-invalid` / '
+ '`component-props-unknown-key` finding on `objectstack validate`, `objectstack build` and '
AFTER:
+ 'component-props gate reports a refused value as a `component-props-invalid` / '
+ '`component-props-unknown-key` error on `objectstack validate`, `objectstack build` and '

packages/spec/src/migrations/entries/semantic/18.ui-object-form-sections-typed.ts

BEFORE:
+ '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 '
AFTER:
+ '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 '

packages/spec/src/migrations/entries/semantic/18.ui-object-gantt-markers-typed.ts

BEFORE:
+ '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 '
AFTER:
+ '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 '

packages/spec/src/migrations/entries/semantic/18.ui-object-grid-columns-typed.ts

BEFORE:
+ '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 '
AFTER:
+ '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 '

packages/spec/src/migrations/entries/semantic/18.ui-object-grid-export-options-closed.ts

BEFORE:
+ 'component-props gate reports these as an advisory `component-props-invalid` / '
+ '`component-props-unknown-key` finding on `objectstack validate`, `objectstack build` and '
AFTER:
+ 'component-props gate reports these as `component-props-invalid` / '
+ '`component-props-unknown-key` errors on `objectstack validate`, `objectstack build` and '

packages/spec/src/migrations/entries/semantic/18.ui-object-grid-kanban-calendar-list-members-typed.ts

BEFORE:
+ 'component-props gate reports a refused value as an advisory `component-props-invalid` / '
+ '`component-props-unknown-key` finding on `objectstack validate`, `objectstack build` and '
AFTER:
+ 'component-props gate reports a refused value as a `component-props-invalid` / '
+ '`component-props-unknown-key` error on `objectstack validate`, `objectstack build` and '

packages/spec/src/migrations/entries/semantic/18.ui-object-grid-page-size-positive-integer-refused.ts

BEFORE:
+ 'reports it as an advisory `component-props-invalid` finding at `pagination.pageSize` '
AFTER:
+ 'reports it as a `component-props-invalid` error at `pagination.pageSize` '

packages/spec/src/migrations/entries/semantic/18.ui-object-grid-row-members-typed.ts

BEFORE:
+ '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`, '
AFTER:
+ '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`, '

packages/spec/src/migrations/entries/semantic/18.ui-object-kanban-conditional-formatting-typed.ts

BEFORE:
+ '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`, '
AFTER:
+ '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`, '

packages/spec/src/migrations/entries/semantic/18.ui-object-map-gantt-tree-navigation-typed.ts

BEFORE:
+ 'component-props gate reports a refused value as an advisory `component-props-invalid` / '
+ '`component-props-unknown-key` finding on `objectstack validate`, `objectstack build` and '
AFTER:
+ 'component-props gate reports a refused value as a `component-props-invalid` / '
+ '`component-props-unknown-key` error on `objectstack validate`, `objectstack build` and '

packages/spec/src/migrations/entries/semantic/18.ui-object-master-detail-form-details-closed.ts

BEFORE:
+ 'column as an advisory `component-props-unknown-key` / `component-props-invalid` finding on '
AFTER:
+ 'column as a `component-props-unknown-key` / `component-props-invalid` error on '

packages/spec/src/migrations/entries/semantic/18.ui-object-metric-aggregate-trend-typed.ts

BEFORE:
+ 'reports a refused value as an advisory `component-props-invalid` / `component-props-unknown-key` finding '
AFTER:
+ 'reports a refused value as a `component-props-invalid` / `component-props-unknown-key` error '

packages/spec/src/migrations/entries/semantic/18.ui-object-metric-compare-to-typed.ts

BEFORE:
+ '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 '
AFTER:
+ '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 '

packages/spec/src/migrations/entries/semantic/18.ui-object-metric-drill-down-report-typed.ts

BEFORE:
+ 'reports a refused value as an advisory `component-props-invalid` / `component-props-unknown-key` finding on '
AFTER:
+ 'reports a refused value as a `component-props-invalid` / `component-props-unknown-key` error on '

packages/spec/src/migrations/entries/semantic/18.ui-object-metric-drill-down-typed.ts

BEFORE:
+ 'an advisory `component-props-invalid` / `component-props-unknown-key` finding on `objectstack validate`, '
AFTER:
+ 'a `component-props-invalid` / `component-props-unknown-key` error on `objectstack validate`, '

packages/spec/src/migrations/entries/semantic/18.ui-object-timeline-items-typed.ts

BEFORE:
+ '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`, '
AFTER:
+ '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`, '

packages/spec/src/migrations/entries/semantic/18.ui-object-timeline-mapping-typed.ts

BEFORE:
+ '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`, '
AFTER:
+ '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`, '

packages/spec/src/migrations/entries/semantic/18.ui-record-line-items-props-closed.ts

BEFORE:
+ 'component-props gate reports a failing key or column as an advisory '
+ '`component-props-unknown-key` / `component-props-invalid` finding on `objectstack validate`, '
AFTER:
+ 'component-props gate reports a failing key or column as a '
+ '`component-props-unknown-key` / `component-props-invalid` error on `objectstack validate`, '

Published, outside the original list: step-18 rationale (packages/spec/src/migrations/registry.ts.template, STEP18_RATIONALE)

Eighteen fragments carried the same clause. BEFORE "Read by the component-props gate (advisory);" → AFTER "Read by the component-props gate (refused as an error);". The fragments are ui-action-group-menu-member-params-array-only, ui-action-group-menu-members-typed, ui-object-form-custom-fields-typed, ui-object-form-fields-names-typed, ui-object-form-members-typed, ui-object-form-sections-typed, ui-object-gantt-markers-typed, ui-object-grid-columns-typed, ui-object-grid-kanban-calendar-list-members-typed, ui-object-grid-row-members-typed, ui-object-kanban-conditional-formatting-typed, ui-object-map-gantt-tree-navigation-typed, ui-object-metric-aggregate-trend-typed, ui-object-metric-compare-to-typed, ui-object-metric-drill-down-report-typed, ui-object-metric-drill-down-typed, ui-object-timeline-items-typed and ui-object-timeline-mapping-typed. Two more were worded differently:

BEFORE (ui-object-grid-export-options-closed):
'`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, ...'
AFTER:
'`properties` is read by the component-props gate, which reports these as '
'errors, and is not parsed on the metadata save or load path, ...'
BEFORE (ui-object-master-detail-form-details-closed):
'component-props gate, which reports a failing entry or column as an advisory finding, '
AFTER:
'component-props gate, which reports a failing entry or column as an error, '

The generated projections move with them. render-projection-diff.ts --base the merge base gives spec-changes.json +48 −48 and protocol-upgrade-guide.md +26 −26. All 74 removed lines carry "advisory", all 74 added lines carry "error", and no added line carries "advisory".

Published, outside the original list: three packages/spec/src/ui/component.zod.ts docblocks (shipped as source)

The 「…」 ruling quotation in the #7751 section is byte-identical: 18 quotation lines before and after.

packages/spec/src/ui/component.zod.ts

BEFORE:
// #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).
AFTER:
// #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.

packages/spec/src/ui/component.zod.ts

BEFORE:
* `retiredKey()` tombstone's verdict, such as a retired key these rows carry;
* every other finding on them stays advisory (#23147 carries the rest).
AFTER:
* `retiredKey()` tombstone's verdict, such as a retired key these rows carry —
* and #23147 made every other finding on them an `error` too.

packages/spec/src/ui/component.zod.ts

BEFORE:
* 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).
AFTER:
* 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).

Unpublished, outside the original list (amendment 6143270198)

packages/spec/src/conversions/registry.ts

BEFORE:
* load path keeps as written, where the refusal is an advisory
* `component-props-invalid` finding at `os validate` / `os build` / `os lint`
AFTER:
* load path keeps as written, where the refusal is a
* `component-props-invalid` error at `os validate` / `os build` / `os lint`

packages/spec/src/conversions/registry.ts

BEFORE:
* `os build` and `os lint` report an old spelling as an advisory
* `component-props-invalid` finding carrying the prescription (the
AFTER:
* `os build` and `os lint` report an old spelling as a
* `component-props-invalid` error carrying the prescription (the

packages/spec/src/conversions/registry.ts

BEFORE:
* verdict, a warning; the load path keeps the open `properties` bag as
* written); stored rows replay it at rehydration
AFTER:
* verdict, an error like every other; the load path keeps the open `properties`
* bag as written); stored rows replay it at rehydration

packages/spec/src/conversions/registry.ts

BEFORE:
* the refusal is the component-props gate's (`@objectstack/lint`, advisory),
* and a re-save goes through.
AFTER:
* 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.

packages/spec/src/conversions/walk.ts

BEFORE:
*     fire, `PageSchema` stayed green, and the advisory props gate runs CLI-only
*     and on already-converted metadata. A slotted record page — the shape
AFTER:
*     fire, `PageSchema` stayed green, and the props gate (advisory then) runs
*     CLI-only and on already-converted metadata. A slotted record page — the shape

packages/lint/src/validate-page-field-bindings.ts

BEFORE:
// `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
AFTER:
// `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

packages/metadata-protocol/src/protocol.stored-migration.test.ts

BEFORE:
// `@objectstack/lint`'s, advisory). `dataSource.filter` is a declared key
// of the strict component schema, so the save door refuses it there.
AFTER:
// `@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.

packages/spec/src/ui/component.test.ts

BEFORE:
// 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.
AFTER:
// 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.

packages/lint/src/validate-component-props-url-target.test.ts

BEFORE:
// warning, the gate's one tier (its module header says why).
AFTER:
// error, like every finding of the gate (its module header says why).

packages/lint/src/validate-component-props-row-on-success.test.ts

BEFORE:
// 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.
AFTER:
// 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.

.changeset/23103-page-action-row-on-success-and-executor.md

BEFORE:
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.
AFTER:
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.

Class grep on the final head c88fe84c1

The grep searched every tracked file that names the gate (component-props, props gate, props lint, validateComponentProps, ComponentPropsMap, props bag), outside CHANGELOG.md, content/docs/releases/**, .changeset/pre/** and content/docs/references/**. It flagged every line within eight lines of a gate word that carries a tier word (advisory, warning, warn, non-gating, still passes, exit 0, Validation passed, WARNING-level). That is 22 hits in 13 files, every one either edited above or true as written:

  • This PR's changeset (lines 6, 10, 21, 34): states the new tier, or names the old type in FROM → TO.
  • History, true as written:
    • authoring-rules.ts:1164 ("the warning-period inventory was empty");
    • validate-component-types.ts:36,38 ("launched advisory");
    • component.zod.ts:288 ("was WARNING-level in this first step");
    • component.zod.ts:661 and retired-keys/18.ui__PageHeaderProps__breadcrumb.ts:17 ("advisory in every release that shipped the default": the default shipped only in 17.x, when the lint was advisory);
    • component.zod.ts:716 ("until this declaration … warning: undeclared");
    • validate-component-props.test.ts:530,536 ("used to warn");
    • component-objectui-held-typed-members.pin.test.ts:25 (the old pass);
    • walk.ts:371, now "(advisory then)".
  • Not the gate's tier:
    • renderer console warnings: 18.ui-action-group-menu-member-params-array-only.ts:28, 18.ui-object-form-fields-names-typed.ts:32, component-action-member-params-array-only.pin.test.ts:14, expression-conformance.ledger.ts:495,519;
    • a variant enum value: 18.ui-object-timeline-items-typed.ts:23;
    • a record:alert prop value: validate-component-props.test.ts:1062,1077.

FALSE remaining: none.

Verification, on c88fe84c1 (origin/main 01a3421f4 merged in)

All test runs went through scripts/pm/os-verify-lock.sh, each with a VERDICT command-exit 0, on a shared box.

run result
@objectstack/lint pnpm test 138 files / 6686 tests passed
@objectstack/lint typecheck passed (check:test-typecheck: OK, debt ledger unchanged)
@objectstack/spec pnpm test 667 files / 20210 passed, 1 todo
@objectstack/spec typecheck passed
@objectstack/spec check:generated, after a real build of this head "All 14 generated artifacts are up to date"
@objectstack/metadata-protocol src/protocol.stored-migration.test.ts 42/42, typecheck passed. The diff there is one comment, so the rest of the package's tests are declared to CI.

Gates.

  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derives 110 lines from this head (43 paths, +346/−224). All 110 ran.
  • --ran with an exit-coded record: "110 derived, 110 run, 0 NOT-MEASURED, 0 UNRUN". 109 exited 0. check-empty-changeset exited 1: the deliberate correction above, red on purpose.
  • I also ran the CI Type Check · source gates commands the derivation does not list. All exited 0: check:future-spec-major, check:driver-conformance, check:object-def-param-keys, the four check:stall-guard* / check:rerun-safety-verdict gates, check:swallow-census-controls, spec check:generated --reconcile-only / check:skill-docs / check:meta-url-spelling / check:react-blocks / check:template-manifests, the four skill-frame and skill-key gates, check:agent-model-declared, check:ratchet-remedy-authority, and the skills token ratchet. render-projection-diff.ts with --base the merge base exited 0.

Fix round (seat note 6144802415)

  • Test Core (1/6) was red at c88fe84c1. The spec test:repo absence pin src/ui/element-text-variant-heading-retirement.test.ts:148 refused the new class pin's element:text variant: 'heading' in packages/lint/src/validate-component-props.test.ts.
  • Option 1 taken. The retired-enum-value pin now authors object-grid exportOptions: { formats: ['pdf'] } and asserts one component-props-invalid error at properties.exportOptions.formats.0. That is zod invalid_value, carrying the 'pdf' was removed from view.exportOptions formats prescription, and it is not a tombstone.
  • The search behind option 1. Every test or script under packages/ and scripts/ that walks the tree (readdirSync, walk(, glob) was grepped for a pdf matcher, and none has one. The only hits are packages/spec/src/ui/view.test.ts, which parses and walks nothing, and a .pdf accept string in component-form-custom-fields-sections-typed.pin.test.ts, which is not a matcher for this value. No D2 conversion lifts 'pdf', so the normalized tier sees this refusal too.
  • Head 80db1670d: one file changed, +3/−3. The absence pin (repo project) is green. Its lit control planted variant: 'heading' back through ablation-replace.mjs, and the pin went red naming the file. The restore brought the blob back to HEAD's 89d9fb165dca.
  • @objectstack/spec test:repo: 58 files / 1007 passed. @objectstack/lint: 138 files / 6686 passed, typecheck passed. lint has no repo-tier project.
  • The 110 derived gates all ran with exits recorded: 109 exit 0, and check-empty-changeset exit 1, the designed red. Its verdict lines are identical to 6144550809's, so that comment's facts are unchanged.
  • The Acceptance notes' input-tier sentence is updated to match the new pin.

Acceptance notes


Generated by Claude Code

…, os build and os lint

The component-props gate's recorded plan, second step: every
component-props-invalid verdict that is not a retired key, and every
component-props-unknown-key finding, move from warning to error. The
retired-key class keeps its own fix text. The rule's surfaceReason is
corrected to the measured page-local reason.

Claude-Session: https://claude.ai/code/session_012uFegdfR6K6uqvLzt7XsdZ
Co-authored-by: Claude <noreply@anthropic.com>
…te now files

Twenty-five D3 migration entries said the component-props gate reports
their refusal as an advisory finding; it is an error now. The gate-table
rows and the layout-dsl callout, one lint comment and two spec comments
say the same. Adds the breaking changeset.

Claude-Session: https://claude.ai/code/session_012uFegdfR6K6uqvLzt7XsdZ
Co-authored-by: Claude <noreply@anthropic.com>
…ks name the props gate's error

Eighteen step-18 rationale fragments said the component-props gate reads
their refusal as advisory, and three component.zod.ts docblocks said every
finding but a retired key stays advisory. Both shipped in 18.0.0-next.0 and
both are false once the gate files every finding as an error.

Claude-Session: https://claude.ai/code/session_012uFegdfR6K6uqvLzt7XsdZ
Co-authored-by: Claude <noreply@anthropic.com>
…he props gate's error

Under the amended surface: three conversions/registry.ts docblocks, a
metadata-protocol test comment, the row-on-success test header that
arrived from main, and the pending page-action-row changeset each said
the component-props gate files a warning. It files an error now.

Claude-Session: https://claude.ai/code/session_012uFegdfR6K6uqvLzt7XsdZ
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/lint, @objectstack/spec, touching 7 documentable anchor(s). ⚠️ 3 changed file(s) yielded no anchor (packages/spec/src/conversions/registry.ts, packages/spec/src/conversions/walk.ts, packages/spec/src/migrations/registry.ts.template), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/deployment/validating-metadata.mdx (via AUTHORING_RULES (symbol, a top-level const object))
What this run could not see
  • 3 changed file(s) yielded no anchor (packages/spec/src/conversions/registry.ts, packages/spec/src/conversions/walk.ts, packages/spec/src/migrations/registry.ts.template) — pages documenting those are invisible to this run
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 141 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json afbf960c2649d87ff6503721b9c290ec679356c6 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from c6beda8b17a2820fc7256e47c10f3eb14d1f2a94 — the merge of head 80db1670d71505278815bb3dcfbe978a348be81b into base afbf960c2649d87ff6503721b9c290ec679356c6, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin c6beda8b17a2820fc7256e47c10f3eb14d1f2a94 && git checkout c6beda8b17a2820fc7256e47c10f3eb14d1f2a94
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin afbf960c2649d87ff6503721b9c290ec679356c6 80db1670d71505278815bb3dcfbe978a348be81b && git checkout -B drift-repro afbf960c2649d87ff6503721b9c290ec679356c6 && git merge --no-ff 80db1670d71505278815bb3dcfbe978a348be81b

node scripts/docs-audit/affected-docs.mjs --json afbf960c2649d87ff6503721b9c290ec679356c6

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs afbf960c2649d87ff6503721b9c290ec679356c6 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

check-empty-changeset is red by design on this PR: a DELIBERATE CORRECTION of a pending release note

The gate's verdict lines at c88fe84c1, verbatim:

This PR changes a changeset it did not add:

   .changeset/23103-page-action-row-on-success-and-executor.md
     present on the merge base and CHANGED by this PR -- this is somebody else's release note

Two things produce this refusal and their remedies are OPPOSITE, so read which one you are before you act.
  COLLISION -- you and another PR drew the same changeset filename, and yours overwrote theirs. Remedy: rename yours; restore theirs from base.
  DELIBERATE CORRECTION -- your change may have made this PENDING release note false, and you rewrote it in the same stroke. Remedy: do NOT restore it -- say so on the PR and get it confirmed; restoring it from the base would put the false sentence back.

Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: c88fe84c176f1c17abc1982b9ddc0257c8a07ef9
Local-runs: none

Inputs: card #23147 (body and all seven comments: two pointers, triage, the claim and its amendment 6143270198, the os-dev-report 6144436014, the seat ACCEPT 6144494395 — read, not taken as evidence); the predecessor #23026, its landing PR #23140 at 32fa1942e and the three contract records there (6137697373, 6138451028, 6139036777); PR #23244 (body, both PR comments including the designed-red comment 6144550809 posted at 11:30Z, the 43-file list, the net diff against main at this head); PR #23186's file list and diff for the overlap question; the check-runs on the head, read twice (11:27Z and 11:31Z); origin/main at 6f4c518b0 and the head read by its 40-hex sha only, read-only. Judged independently of the seat's ACCEPT.

① Derived judgments

The refused set, route by route (packages/lint/src/validate-component-props.ts at the head), each named RIGHT or WRONG:

  1. Value verdicts that are not a tombstone → error — RIGHT. The route is schema.safeParse(props) over the row ComponentPropsMap[type], after unrecognized_keys and the one-arm union are peeled off and after isRetiredKeyVerdict. The predicate is the row's own accept set, so what is refused is exactly what the declaration refuses: a wrong type, a missing required prop (an absent bag is judged as {} since lint: five os lint --strict blind spots measured against firing controls — aliased ctx.api in hook handlers, action-body free identifiers, unbound visibility roots, lookup-bound detail grants, empty record:details sections #22212, so a nested component owing a required prop is refused — a genuine defect, the renderer needs it), a closed-set refusal (url target scheme, record:details section count), a retired enum value. A .default() is filled by the parse and never refused; an expression-string in a prop typed number is refused, and rightly: no row in ComponentPropsMap declares an evaluated slot on a numeric prop, so the renderer would not evaluate it either. A properties value that is not an object is skipped here and left to PageComponentSchema, unchanged.
  2. Missing dataSource.object on element:record_picker / element:number / element:repeater → error — RIGHT, predicate tight for the shipped renderer, one residual named. boundObject reads component.dataSource.object as a non-empty string; an expression string passes. It does not distinguish "no dataSource" from the API binding { provider: 'api', read: { url } }, which PageComponentSchema.dataSource admits as the second union arm. page.zod.ts records that binding as "Planned, not yet rendered" — at objectui 7578323a544e useElementDataSource admits only a string object, so an element carrying the API binding today queries nothing and draws its empty state, which is the sentence the refusal states. If the API binding's renderer ever reaches these three elements, this route must learn provider: 'api'; that is a future spec change, not a defect on this head. The predicate predates this PR ([Direction · v18] Retire the flat object/filter data-binding family — dataSource as the single data-binding door (re-anchor of the deleted #6590 tracker) #11509) and only its severity moved.
  3. Unknown key through the walker (lintUnknownKeysAgainstSchema) → error — RIGHT. The walker reports only a strip-mode object with declared keys; strict is silent (the parse is loud), passthrough is silent (the key survives). properties itself is z.record with no declared keys, so the bag is judged by the row, never by its own posture. A harmless extra key the renderer reads cannot be refused by construction: a key the renderer reads and the row does not declare is the [移交自 objectui] ComponentPropsMap 未声明 objectui 渲染器实读的 5 个顶层 prop(page:header ×3 / page:tabs.tabStyle / page:accordion.variant)—— #5775 的清点未完 #6776 class, which the gate exists to surface and the schema is to be corrected for (the hint says so).
  4. Unknown key through a strict row's unrecognized_keys → error — RIGHT. All 31 rows are strict since 未知键静默剥离仍是全仓默认:把 #3405 的 strict 收紧从一个 schema 推广到整个可授权面(ADR-0078 完整性闸门) #4001 batch A; same fact as 3, other posture.
  5. Unknown key through the one-arm union (unrecognizedKeysFromUnionArm) → error — RIGHT. The one-arm condition (exactly one arm judged keys, and judged only keys) is unchanged, so no guess about the author's arm is introduced; anything else falls to route 1 with every arm's message.
  6. Retired-key verdict stays error; isRetiredKeyVerdict now picks text only — RIGHT. Same discriminator as 6137697373 ①.1 enumerated (invalid_type expecting never, directly or by the one-arm rule). The 6139036777 residual on the details[].sortField shape is dissolved: every finding is an error, so "a failing entry or column … error" is true for the tombstone and the column alike.
  7. Nothing wider is refused — RIGHT. A type with no row (custom.*, a plugin type, SDUI blocks flex / grid / object-chart) is continued; ComponentPropsMap carries no wildcard row (listed at component.zod.ts:10750-11265). Source-authored pages (kind html/react/jsx) are not walked. The input is stack.pages at the normalized tier, so a D2 conversion rewrites a retired spelling before judgment (the test pins element-text-variant-heading-levels).
  8. CLI exit codes — RIGHT. The entry is gating on ALL commands (unchanged since lint/spec: a retired page-component prop (object-grid defaultFilters) passes os validate with only a warning, while the conversion registry says the loader refuses it — the props gate's error upgrade has no carrier since its blockers closed #23026); authoring-rule-wiring.test.ts holds gating against the source emitting severity: 'error'. os validate / os lint exit 1, os build exit 1 with no dist, as the dev's door probe at this head reports.
  9. ComponentPropsSeverity narrows to 'error' — RIGHT, a public type surface of @objectstack/lint; breaking for any producer of a ComponentPropsFinding, covered by the major.
  10. surfaceReason moves from RUNTIME_NEEDS_FULL_SNAPSHOT to an inline page-local reason; surfaces: CLI_ONLY unchanged — RIGHT. The old reason was false on its face: the rule reads stack.pages and nothing else, and runtime-gate.ts TYPE_TO_STACK_KEY maps a page write into pages. The new string mirrors the validateComponentTypes entry's inline reason (1217-1221), which is also inline, so there was no shared constant to reuse. The runtime publish door is not crossed, as triage 6137942125 required.
  11. Spec leg is text only — RIGHT. 25 D3 entry reasons, 20 STEP18_RATIONALE fragments (18 "(advisory)" → "(refused as an error)", the two worded fragments rewritten), 3 component.zod.ts docblocks, four conversions/registry.ts docblocks, one walk.ts comment, one test comment. No .describe(), guidance string or schema changed; the ruling quotation in the A typo'd key inside a page component's properties is accepted in silence — the props bag is a passthrough record and the SDUI props gate has no schema to dispatch for object-* blocks #7751 section is untouched by the diff. The projections (spec-changes.json, protocol-upgrade-guide.md) are gitignored and regenerate; check:generated and check:docs are green on the head's Type Check · source gates.

The measured-zero baseline (acceptance 1 and 2): the population is the right one. The four examples (app-todo, app-multi-package author no page; examples/embed-objectql authors none; create-objectstack authors none; packages/apps/* none) plus every shipped page the CLI doors can judge, enumerated independently at the head: platform-objects sys-user / sys-organization / sys-position (re-registered unchanged by plugin-auth ×2 and plugin-security ×1), plugin-approvals ×1, qa/downstream-contract welcome, cloud-connection ×2, mcp ×1 — eight, as the dev counted. Docs samples under os:check-yaml and skill examples are judged by their own CI twins (check-yaml-examples.ts carries a twin of the dispatch; both gates green on this head); untagged docs samples are judged by no door, so leaving them unmeasured is correct. hotcrm at 0f84c9c72 could not be os validated whole (its 17.7.0 stack is refused at defineStack by the email_template.category tombstone, #23145's family); its eight page exports were normalized and judged directly, which for a rule that reads stack.pages alone is the same judgment. Residual accepted for a major in pre mode: deployed sys_metadata rows are not a population of the CLI doors (the runtime door does not run this rule), and the card named exactly one external corpus. Each population carried a planted positive control that drew one finding per class.

Overlap with PR #23186 (card #23145): no textual and no semantic conflict. #23186 edits component.zod.ts at :3107, :4088 and :5464 (tombstone prescriptions, "17" → "18"); this PR edits :301-311, :5032-5037 and :5865-5872 (docblock tier wording). Both edit .changeset/23103-… — #23186 line 35, this PR line 18 — each a DELIBERATE CORRECTION of its own. #23186 edits validate-component-props.test.ts :379 and :483 and …row-on-success.test.ts :67-70; this PR's hunks in those files are at other lines and do not shift them. Whichever lands later merges main cleanly.

origin/main has moved past the merge base 01a3421f4 to 6f4c518b0: 22 files, none in packages/lint, packages/spec, content/docs or a changeset this PR touches, so no sentence of the class arrived on main behind this head.

② Semver level

  • @objectstack/lint: major — matches what the diff publishes. An accept-set narrowing at three CLI doors (exit 0 → exit 1 on every component-props finding) plus an exported type narrowing. check-changeset-no-major admits a major in pre mode by its own header (RC exemption: .changeset/pre.json reads "mode": "pre"), and the stock already carries two: .changeset/pre/23026-retired-component-prop-refused.md (@objectstack/lint: major, consumed into 18.0.0-next.0) and [Direction · v18] Retire the flat object/filter data-binding family — dataSource as the single data-binding door (re-anchor of the deleted #6590 tracker) #11509. The **BREAKING** line, the FROM → TO table with its one-line fix and the "Who is affected, measured" paragraph are present.
  • @objectstack/spec: patch — right. Shipped text only (dist/migrations reasons and rationale, docblocks shipped as source); the fixed group makes every package move together regardless.
  • Clause-②: no (narrowing: findings the CLI doors warn on today are refused) — right, in the PR body's line 2 and in the changeset; no new key on any published payload.
  • ADR-0087: not-required (already-registered …) — the right disposition. All 26 named ids resolve to 18.*.ts semantic entries at the head and exist at the merge base; none is added by the diff (the gate's already-registered test), so registered would be false, and no-migration-prescription would be refused because the body carries a FROM → TO prescription. In substance no spec shape moves and nothing on the load path changes, so no D2 conversion or D3 entry is owed: the refusals the gate now errors on are the already-declared rows, whose closes are registered (17.authoring-schemas-strict-unknown-keys, the 18.ui-*-unknown-keys-refused family, the step-18 typed/closed entries named). One wording note, no defect: the marker's "the entries named here are the step's narrowings whose refusal the gate carries" reads wider than its list — the list is the 25 entries whose text called the refusal advisory plus element-flat-data-binding-retired; other step-18 entries the gate also carries (ui-record-blocks-unknown-keys-refused, ui-reference-rail-unknown-keys-refused, ui-cloud-connection-widgets-unknown-keys-refused, ui-mcp-connect-agent-unknown-keys-refused, ui-action-url-target-scheme-set-closed, the ui-page-action-row-* three) carry no tier word, needed no amendment, and are not named. The disposition stands either way; the marker is an HTML comment and ships to no reader.
  • Every shipped sentence read against the head, TRUE: the changeset sentence by sentence ("A retired prop was already refused, and still is" — error on the retired-key branch since 32fa1942e; "Not judged here" — surfaces: CLI_ONLY, PageComponentSchema.properties is z.record, a type with no row is skipped; the four FROM → TO rows match the pinned titel suggestion, the limit number type, the binding route and the unknown-key hint; the type narrowing; the measured paragraph's counts match the module header and the PR body); the five D3 reasons sampled (master-detail-form-details-closed, element-data-source-and-object-block-filter-rule-array, grid-export-options-closed, element-text-variant-heading-subheading-retired, grid-page-size-positive-integer-refused) and the 20 rationale fragments all say error now; the three component.zod.ts docblocks; both docs pages (the gate table row now names every verdict with ✓ ✓ ✓ —, the advisory row no longer lists component props; the layout-dsl callout keeps the custom.* sentence, true); the surfaceReason. The dev's "FALSE remaining: none" holds for shipped text. My own sweep of the head (component-props / props gate / props lint / validateComponentProps / ComponentPropsMap / props bag beside advisory / warning / warn / non-gating / still passes / exit 0 / Validation passed / WARNING-level / does not fail, outside CHANGELOG.md, releases/, references/, pre/) finds only dated history, each true as dated: validate-component-types.ts:38 ("launched advisory"), walk.ts:371 ("advisory then"), component.zod.ts:288 ("WARNING-level in this first step"), :661 and retired-keys/18.ui__PageHeaderProps__breadcrumb.ts:17 ("advisory in every release that shipped the default" — 17.x only), :716 ("until this declaration"), and one the dev's grep did not list: docs/audits/2026-07-unknown-key-strictness-ledger.md:676 ("same warning tier", "lint side, warning level"), a dated audit row as of 未知键静默剥离仍是全仓默认:把 #3405 的 strict 收紧从一个 schema 推广到整个可授权面(ADR-0078 完整性闸门) #4001 batch A, not shipped and not a docs page. skills/, packages/cli and content/docs carry nothing of the class. The consumed notes in .changeset/pre/ (11509, 22161, 22212, 23026, 23077) and the CHANGELOG.md entries say "warning" — shipped release history, RELEASE-OWNED, never edited in a code PR, true at their release.
  • The DELIBERATE CORRECTION on .changeset/23103-page-action-row-on-success-and-executor.md is CONFIRMED. The diff is one line (line 18, 1 insertion / 1 deletion); the frontmatter '@objectstack/spec': minor (35 bytes) is byte-identical at the merge base and the head; the corrected parenthesis "(an error, like every finding of that gate)" is true on the head; the note was added by 4d1e4a80c (PR feat(spec)!: a page action row's onSuccess is ActionSchema's own block, and its executor key refuses the 'navigation' alias #23164), sits in the .changeset/ root on main, is absent from .changeset/pre/ and from packages/spec/CHANGELOG.md, so it is pending and ships in the same 18.0.0-next.N as this PR's note; Check Changeset lives in pr-automation.yml, whose only trigger is pull_request, and lint.yml runs the gate's --self-test halves alone, so the merge queue never sees the red (Lint & Repo Gates is green on this head). The designed-red comment 6144550809 is accurate in every claim I checked (gate, note, provenance, one line, frontmatter, pending, the verbatim verdict lines). Restoring the base text would ship a false sentence beside a true one.

③ Boundary flags

  • Check-runs on the head at 11:31Z (44): Check Changeset failure ×2 (by design, above; the second run is the labeled event); Lint & Repo Gates, TypeScript Type Check, Build Core, Governed Surface Queue Guard, all Lint · and Type Check · lanes, Build Docs, Spec property liveness, the three claim guards, docs links and drift — success; Test Core (5/6) success, Test Core 1/6, 2/6, 3/6, 4/6, 6/6 still in progress; dogfood, console pin, temporal, pack-smoke skipped by filter. The contract is judged on the diff; the landing waits for Test Core to conclude green, which is the owning seat's read, not this record's. No governed surface, 570 changed lines, head repo is objectstack-ai/objectstack.
  • Deviation: edits outside claim 6140712434 before amendment 6143270198 — answered. The amendment covers every gate-tier sentence the change made false, and the PR body carries BEFORE → AFTER for each; the dev flagged the breach mid-run rather than carrying it. Right.
  • Deviation: the .changeset/23103 edit — answered above, CONFIRMED; open_questions option A is the answer.
  • Deviation: two main merges (f94c1c3a8, ad4759352), ablation at c0d8f8b37 — answered. The ablation's claim rests on the rule and test blobs being unchanged at the head: e3ed3effd342… and ad0d70b652… are the blob ids git rev-parse returns for both files at c88fe84c1, and the diff's index lines name the same two. Five legs reddening 5 / 1 / 1 / 5 / 8 of 82 match the pins I read in the new "lint: the rest of the component-props gate's recorded plan — every component-props-invalid class and component-props-unknown-key go to error, after an external-corpus inventory #23147" block (one per class and per code route) plus the sibling tests. Everything else was re-run on the final head.
  • Deviation: hotcrm's whole stack does not load — acceptable, with the reading in ①: the eight page exports went through the same normalize-then-judge pipeline, and the rule is page-local. The refusal hotcrm meets at defineStack is spec(ui): the object-grid defaultFilters and element flat-binding tombstones say "removed in @objectstack/spec 17", but 17.7.0 shipped those keys live; the v18 train removes them in 18 #23145's, carried by PR fix(spec): tombstones for keys live in @objectstack/spec 17.7.0 name 18 as their removal version #23186.
  • Deviation: surfaceReason as an inline string — acceptable; the sibling gating page rule declares its reason inline too, and the claim allowed the change on the measured-false condition, which H3 met (28/28 pages identical through the per-write snapshot, as authored and lit).
  • Deviations: model-free commit trailers, background builds waited on, the first lint batch ;-sequenced with per-step exits recorded — answered; none touches the contract.
  • out_of_scope_findings 1 (hotcrm tombstone text says "17") — RIGHT carrier (spec(ui): the object-grid defaultFilters and element flat-binding tombstones say "removed in @objectstack/spec 17", but 17.7.0 shipped those keys live; the v18 train removes them in 18 #23145 / PR fix(spec): tombstones for keys live in @objectstack/spec 17.7.0 name 18 as their removal version #23186), nothing to file.
  • out_of_scope_findings 2 (crossing the rule to the runtime publish door, no carrier) — acceptable as noted, not filed: triage 6137942125 put the crossing outside this card as a rollout decision with a false-refusal budget over stored tenant rows; the seat says it goes to the maintainer in the round report. The surfaceReason now records the measured reason, so the false declaration pointer 6137729161 named is corrected whichever way the rollout goes.
  • Pointer 6139673366 (18.ui-object-master-detail-form-details-closed.ts:42-43) — closed in the D3 list, and true as rewritten.
  • One note for the owning seat, not a FAIL cause: docs/audits/2026-07-unknown-key-strictness-ledger.md:676 still says the gate is "warning tier"; it is a dated audit row, not shipped, so it may stay as dated, and the dev's class grep should have named it.

Why PASS. The refused set is exactly every finding the gate files and nothing wider — each of the six routes errors on its row's own accept set, with the unregistered-type skip, the non-object bag skip and the one-arm union rule unchanged; the baseline is the right population with planted controls on each; major / patch / Clause-② / the ADR-0087 disposition each match what the diff publishes; every shipped sentence I read is true on the head and nothing of the class remains outside dated history; the one foreign-changeset edit is a confirmed DELIBERATE CORRECTION that the queue never sees; and no in-flight PR conflicts with it.

Implemented-by: claude/issue-23147-component-props-error-rest
Reviewed-by: session_012uFegdfR6K6uqvLzt7XsdZ

VERDICT: PASS

…ions pdf

The class pin spelled element:text variant heading, which the spec's
tree-scoped absence pin refuses outside its retirement kit. A retired
'pdf' export format is the same class (an enum refusal carrying its
prescription, not a tombstone) and no tree-scoped pin guards it.

Claude-Session: https://claude.ai/code/session_012uFegdfR6K6uqvLzt7XsdZ
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Test Core (3/6) is red at 80db1670d on a host port race, not on this PR's diff. The seat spent this PR's one failed-jobs re-run · domain:spec seat 3 (#18883) · session_012uFegdfR6K6uqvLzt7XsdZ · 2026-10-13T12:29Z

The failure.

  • Job 115285137112 fails packages/cli/test/serve-package-declared-capabilities.test.ts with "PORT DRIFT on os serve (bin/run-dev.js, via runServe): this harness asked for port 32947 and the child BOUND port 32948".
  • The harness's own verdict: "⛔ This is a HOST race, not a verdict about the code under test. Re-run this file in isolation."

Why it is not this PR's.

  • The diff touches no file in packages/cli.
  • The same shard passed on c88fe84c1.
  • Every other Test Core shard is green on this head, including 1/6, which this head's fix round repaired.

What was done. Failed jobs of run 38394146025 were re-run once, in this act. A second failure is treated as real.

Check Changeset stays red by design: the DELIBERATE CORRECTION described in 6144550809.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 80db1670d71505278815bb3dcfbe978a348be81b
Local-runs: none

Round 2 (delta) on card #23147. Inputs: the card body and all nine comments (two pointers, triage, the claim and its amendment 6143270198, the two os-dev-reports 6144436014 and 6146187598, the seat ACCEPT 6144494395 and the seat note 6144802415 — the last three read, not taken as evidence); PR #23244 (body with its "Fix round" section, the four PR comments — drift check, the designed-red note 6144550809, the round-1 record 6144745360 on c88fe84c1, the host-race note 6145577154 — the 43-file list, the net diff at this head, and the compare c88fe84c1...80db1670d); the 63 check-runs on this head read at about 13:10Z (latest attempt per name, plus the superseded attempts); PR #23186 at 38728b3e1 for the overlap question only; origin/main at 6d7f4df3c (12:39Z) and the three heads read by 40-hex sha, read-only; one git merge-tree simulation in a throwaway bare clone under this record's scratch directory. Judged independently of the seat's ACCEPT and the dev's reports.

① Derived judgments

1. The delta is exactly one file, packages/lint/src/validate-component-props.test.ts, +3/−3 — RIGHT. The compare is one commit (80db1670d, "test(lint): pin the retired-enum-value class on object-grid exportOptions pdf"), one file, blob 89d9fb165dcae5f46c7c724e405c36283b47637f; git diff --stat c88fe84c1 80db1670d agrees (1 file, 3 insertions, 3 deletions). The hunk is the one it(...) at :907–:910: the "retired enum VALUE — an enum issue, not a tombstone" case now authors object-grid properties: { objectName: 'task', exportOptions: { formats: ['pdf'] } } and asserts [[COMPONENT_PROPS_INVALID, 'error', at('properties.exportOptions.formats.0')]].

2. It is a genuine retired ENUM VALUE, not a retiredKey() tombstone verdict — RIGHT, so the case still pins what its title says. ObjectGridPropsSchema.exportOptions is ListViewExportOptionsSchema.optional() (component.zod.ts:5874), a closed object whose formats is z.array(ListViewExportFormatSchema); that enum is z.enum(['csv', 'xlsx', 'json']) with an error map that returns LIST_VIEW_EXPORT_PDF_RETIRED exactly when issue.input === 'pdf' (list-view-export-options.ts:70-72), whose own docblock says 'pdf' was REMOVED in 17 (#8010) and "this is an enum-VALUE narrowing, so there is no retiredKey() tombstone to hang the prescription on — the enum's own error map carries it". Under zod ^4.6.5 (both packages/spec and packages/lint) an enum refusal is an invalid_value issue carrying the 'pdf' was removed from view.exportOptions formats in @objectstack/spec 17.0.0 … prescription. On the head's rule, isRetiredKeyVerdict (validate-component-props.ts:285-297) answers true only for isTombstoneIssue (invalid_type expecting never) or a one-arm invalid_union of such issues; an invalid_value on a non-union slot is neither, so the finding takes the ordinary value-verdict branch (:442-445): severity: 'error', rule: COMPONENT_PROPS_INVALID, path: at. The classifier picks text only (round 1 ①.6), so the retired-value class is pinned on severity by a case the tombstone route cannot reach — which is what the title claims.

3. Path and severity as asserted — RIGHT on the head's rule. at is ${base}.${issue.path.join('.')} with base = pages[0].regions[0].components[0].properties; the spec's own pin component-object-grid-export-options-members.pin.test.ts:96 fixes the row's issue path as ['exportOptions', 'formats', 0], which joins to properties.exportOptions.formats.0 — the test's at('properties.exportOptions.formats.0'). Exactly one finding: object-grid is not in DATA_SOURCE_BOUND_ELEMENT_TYPES (no binding finding), objectName is supplied, exportOptions is a closed object rather than a union (one issue, no arm peeling), and the walker finds no undeclared key. Every Test Core shard is green on this head, which is the lint suite passing with this exact assertion.

4. No tree-scoped absence pin matches 'pdf' or formats: ['pdf'] — RIGHT, by my own grep. The repo-tier population at the head is the six vitest.repo-tests.json lists (core 6, objectql 1, rest 5, runtime 3, spec 58, types 1 — 74 files). Grepped for pdf, exportOptions and formats: one hit, view-list-tabs-retirement.test.ts:310, where exportOptions sits in LIST_VIEW_SIBLINGS, a key set that pin consults only for a literal that also carries tabs (keys.has('tabs') && …); the new fixture carries none. Wider than the dev's search: every file under packages/ and scripts/ that walks the tree (348 files matching readdirSync / readdir( / walk( / glob / git ls-files / git grep) carries no pdf matcher — the only pdf inside a walker is a .pdf accept string in a fixture (component-form-custom-fields-sections-typed.pin.test.ts:178), and every formats hit is prose. view.test.ts, which the dev named, is not a walker at all. The pin that tripped at c88fe84c1 (element-text-variant-heading-retirement.test.ts, its EXCLUDED set and vitest.repo-tests.json) is unchanged against the merge base: option 1 of 6144802415, nothing excluded. CI closes it: Test Core (1/6), the spec test:repo shard red at c88fe84c1 (job 115269929621), is green at this head (115297024171), and every Lint · lane is green, so no script gate matches the value either.

5. The normalized tier sees the refusal too — RIGHT. The only D2 conversion that touches 'pdf' is view-export-options-pdf-removed (conversions/registry.ts:2328), whose apply runs through mapViewPayloads (walk.ts:586), which maps the views collection alone (a ViewItem's config, a container's slots, a flattened overlay) and never visits pages[].regions[].components[].properties. A page component's bag reaches the rule with 'pdf' intact, so the Acceptance-notes sentence that the pin authors "a value no D2 conversion lifts … so the normalized tier refuses it as well" is true for the pin's location.

6. Carry-over — RIGHT, stated so this record stands alone. The compare's file list is the one file above, so each of the other 42 files carries the same blob at c88fe84c1 and 80db1670d; the rule blob is e3ed3effd342 at both. Round-1 record 6144745360's judgments ① (items 1–11: the six refusal routes, the measured-zero baseline, the overlap reading, main's movement), ② (the semver level, the shipped sentences, the DELIBERATE CORRECTION) and ③ (the boundary flags) hold unchanged on this head; this record restates them as its own and adds only the delta judgments here.

7. Overlap with PR #23186 (card #23145) at 38728b3e1 — no textual and no semantic conflict; a sequencing note for the seat, not a FAIL cause.

8. CI on 80db1670d — every check green except Check Changeset, by design. Required contexts for main by the ruleset (TypeScript Type Check, Test Core, Build Core, Lint & Repo Gates, Governed Surface Queue Guard; merge queue SQUASH / ALLGREEN) all success. Test Core (3/6): first attempt 115285137112 failure (12:00:43–12:20:14Z, the packages/cli/test/serve-package-declared-capabilities.test.ts host port race, a file outside the diff), latest attempt 115296976799 started 12:29:41Z, success at 13:01:43Z; the aggregate Test Core 115310794908 success at 13:02:11Z; CI run 38394146025 attempt 2 concluded success. Test Core (1/6) success at 12:19:21Z (the fix). Other non-green states: Check Changeset failure twice (115284988370 at 12:00Z and 115313516014 at 13:07Z, the edited/labeled re-fire of PR Automation run 38402527164) — each fails at exactly step 12 "Reject an empty-frontmatter changeset added by this PR", the check-empty-changeset --base step, with steps 11, 13 and 15 (a changeset present, the ADR-0087 disposition, the no-major guard) green, the same signature the designed-red note records for c88fe84c1; Check PR Size and Auto Label read "skipped" on that 13:07Z run only because their if: excludes labeled / unlabeled / edited events, and their 12:00Z runs succeeded; Console Pin Gate, the Dogfood matrix, Dogfood Verify CLI, Temporal Conformance and Packed-tarball smoke are filter-skips. Nothing red that is not designed.

② Semver level

Unchanged from round 1 and restated: @objectstack/lint: major with **BREAKING** and the FROM → TO table, @objectstack/spec: patch (text only), Clause-②: no (narrowing …) in the PR body's line 2 and the changeset, ADR-0087 not-required (already-registered …) naming the 26 step-18 ids — each matches what the diff publishes, and the delta (a test literal) moves no package surface, so .changeset/23147-component-props-every-finding-refused.md is rightly untouched (same blob at both heads).

The DELIBERATE CORRECTION on .changeset/23103-page-action-row-on-success-and-executor.md is CONFIRMED on this head. The blob is 79c67c7c0 at both c88fe84c1 and 80db1670d (base e965ac359); the diff against the merge base is one line, line 18 (1 insertion, 1 deletion); the frontmatter is byte-identical on the merge base and the head (sha256 1a20512260d8fd4e… on both); on origin/main 6d7f4df3c the note still sits in the .changeset/ root with the base blob, is absent from .changeset/pre/ and unmentioned in packages/spec/CHANGELOG.md, so it is pending and ships in the same 18.0.0-next.N as this PR's note; the corrected parenthesis is true on the head. Check Changeset lives in pr-automation.yml, whose only trigger is pull_request (types opened, synchronize, reopened, labeled, unlabeled, edited) — no merge_group, no push; lint.yml and ci.yml, which do run on merge_group, run only pnpm check:changeset-gate-self-tests (continue-on-error), never the --base half; and the ruleset does not require Check Changeset. Comment 6144550809 is accurate on this head in every claim re-checked (gate, note, one line, frontmatter, pending, the failing step); the dev's choice not to restate it was right, because none of its facts moved.

The PR body's "Fix round (seat note 6144802415)" section and the updated Acceptance-notes input-tier sentence — true against the head, bullet by bullet: Test Core (1/6) was red at c88fe84c1 on element-text-variant-heading-retirement.test.ts:148 (the offenders assertion); option 1; the new pin's value, path, severity, issue code and prescription (①.2–3); the walker search (①.4, independently wider); one file +3/−3 with blob 89d9fb165dca (①.1); the absence pin green in CI (①.4); the derived-gate counts are the dev's and are corroborated by the green lanes; "the Acceptance notes' input-tier sentence is updated" — it is, and the sentence is true (①.5). One wording note, no defect: the bullet "No D2 conversion lifts 'pdf'" is true only as scoped to a page component's bag — view-export-options-pdf-removed does strip 'pdf' from a list VIEW's exportOptions; the sentence's second half ("so the normalized tier sees this refusal too") is the true part.

③ Boundary flags

  • Dev deviation: the PR body was not patched; the section was left to the seat — answered. The role file has the dev write the body once; the seat wrote the section, and the body as it stands is true (② above). The seat's version replaces the dev's last bullet (which named the sentence "The retired-enum-value pin calls the rule directly." as obsolete) with the corrected Acceptance-notes sentence; the obsolete sentence is gone from the body.
  • Dev deviation: worktree re-created from the local branch after verifying it equals remote c88fe84c1; turbo cache restored two dists without .d.ts, rebuilt before the dual-build gate — answered; the head's Build Core, Type Check · and Lint · lanes are green, and the cache observation is rightly an out-of-scope note with no carrier, not this contract.
  • Dev deviation: model-free commit trailers — unchanged from round 1; not the contract.
  • Dev: the designed-red comment 6144550809 not restated — right, its facts are unchanged (②).
  • Seat's host-race note 6145577154 — consistent with the check-runs: the failed attempt is outside the diff, the shard passed at c88fe84c1, the single re-run passed.
  • open_questions: none this round. Round 1's open question (keep the correction) stays answered A and is confirmed above.
  • Overlap with PR fix(spec): tombstones for keys live in @objectstack/spec 17.7.0 name 18 as their removal version #23186 — a sequencing note only (①.7): clean in both orders, no assertion on either head contradicts the other's change, and whichever lands second owes nothing but the queue merge; the seat may want the PR body's "Concurrency" bullet to name all three shared files.
  • Carried note from round 1, still true at this head: docs/audits/2026-07-unknown-key-strictness-ledger.md:676 is a dated audit row that still says "warning tier"; not shipped, may stay as dated.
  • needs:contract-review is on the PR; main has moved 10 commits past the merge base with nothing under the PR's paths, and the head merges onto it clean (mergeable: true).

Why PASS. The delta is one test literal that pins the retired-enum-value class more faithfully than before — an invalid_value on a closed row that no tombstone route can classify, at the path the spec's own pin fixes, at the severity the head's rule emits — and it trips no tree-scoped pin or script gate, by my own grep of the repo-tier population and of every walker under packages/ and scripts/, and by the green repo-tier shard; every other file is the blob round 1 judged, so that PASS carries; the DELIBERATE CORRECTION is confirmed on this head and the queue never sees its red; the PR body is true; #23186 conflicts with nothing here; and every check on the head is green except the one that is red by design.

Implemented-by: claude/issue-23147-component-props-error-rest
Reviewed-by: session_012uFegdfR6K6uqvLzt7XsdZ

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 13, 2026 13:29
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 13, 2026 13:29
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 13, 2026
Merged via the queue into main with commit 4f68ba1 Oct 13, 2026
62 of 67 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-23147-component-props-error-rest branch October 13, 2026 14:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:ui size/l tests tooling

Projects

None yet

2 participants