Skip to content

feat(lint)!: component-props-invalid refuses a retired page-component prop as an error at os validate, os build and os lint - #23140

Draft
objectstack-fleet[bot] wants to merge 8 commits into
mainfrom
claude/issue-23026-retired-prop-error
Draft

objectstack-fleet[bot] wants to merge 8 commits into
mainfrom
claude/issue-23026-retired-prop-error

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Fixes #23026
Clause-②: no (narrowing: os validate stops accepting a retired page-component prop; triage's grade 6123043175 reads the same)

What this does

The component-props gate's error upgrade, the step its own header recorded ("the warning-period inventory is the acceptance baseline for the error upgrade, and the upgrade is its own step once the inventory is empty"), scoped to the class triage named: a retired key.

Measured before acting

Inventory (H2), at 21c86444e, before the change:

corpus door retired-key findings every other component-props-* finding
examples/app-crm, app-todo, app-multi-package, app-showcase os validate --json 0 0
the five shipped pages outside them: platform-objects sys-organization, sys-position, sys-user; plugin-approvals sys-approval-request; qa/downstream-contract welcome the rule on the normalized stack 0 0
packages/apps/* (account, setup, studio), the create-objectstack blank template, CLI generators none authors a page n/a n/a

Re-measured on the merged head f88009a41: the same zeros. The page instrument was checked against a positive control (one fixture page with a retired key, an invalid value and an unknown key), which it counted 1 / 1 / 1. Class (b) is therefore 0, and that is the next step's baseline. Whether (b) should also move is an open question in the report, not part of this diff.

How a retired key is told apart (H1). It is read off the issue, never off its text. A retiredKey() is z.never(...).optional(), which raises invalid_type with expected: 'never'. The spec's private isRetiredKeyIssue in flow-node-config-refusals.ts uses the same discriminator. A schema-graph walk of ComponentPropsMap finds 50 never-nodes:

  • 48 key tombstones;
  • 2 retired-element rows (user:profile, ai:chat_window; retiredComponentProps, the same issue shape with the element's prescription), refused the same way;
  • 2 of the key tombstones sit under a union arm (object-form / object-master-detail-form sections[].fields[], the object arm's publicPicker). There zod collapses the arm failures into one invalid_union, so the classifier applies the existing one-arm rule of unrecognizedKeysFromUnionArm: exactly one arm raised tombstone issues and nothing else. A mixed arm stays a warning, and that is pinned.

Retired enum values (enumWithRetiredValues) raise an enum issue, not invalid_type/never, so they are not swept in. The element:text variant migration text that calls that finding advisory therefore stays true.

Both doors on one fixture (H4). The fixture was a defineStack app with object-grid properties.defaultFilters:

door before (21c86444e) after
os validate exit 0, "Validation passed", one component-props-invalid warning exit 1, "Author-time rules failed (1 issue)", the prescription printed
os build exit 0, artifact written (1.8 KB), one warning exit 1, no artifact
os lint n/a exit 1, "1 error(s)"

No door still passes. The runtime publish door (Studio / REST /meta / MCP) does not run this rule (surfaces: CLI_ONLY), so a page saved there is not refused. That is written into the rule header, the registry sentences and the changeset, and nothing claims otherwise.

Registry text (H3). packages/spec/src/conversions/registry.ts:

  • the record-picker reconciliation docblock said "the loader rejects it loudly with the prescription";
  • the object-grid-default-filters-removed docblock said "refused at the parse with the prescription".

The load path keeps the open properties bag as written (PageComponent.properties is z.record). Both sentences now name os validate, os build and os lint (the component-props gate). This is a comment-only edit. On this head the first round stopped at these two sentences; the patch round below corrects six more. It publishes nothing: @objectstack/spec's files ships dist plus src/**/*.zod.ts, and on a built dist the old sentences have 0 hits while a string literal from the same entry hits (positive control).

Tests

  • packages/lint/src/validate-component-props.test.ts changes:
  • packages/spec/src/ui/form-field-public-picker-retirement.test.ts gains one exclusion: the props-lint pin authors publicPicker to prove the union-arm refusal, exactly as the breadcrumb and detail-sortField absence pins already exclude that same file.
  • Readings, all at f88009a41, all under the shared verify lock:
    • @objectstack/lint: 137 files / 6624 tests passed; typecheck (tsc plus the test layer under tsconfig.test.json) passed, debt ledger unchanged;
    • @objectstack/cli unit layer for validate / build / author-time: 5 files / 61 tests passed (build-warning-truncation-notice, generate-scaffold-validates, validate-build-gate-parity, validate-per-package-authoring-seam, src/utils/author-time-rules). The integration layer is declared to CI, since no spawn entry was touched;
    • @objectstack/spec: the 20 test files that read conversions/registry.ts passed (1060 tests); typecheck passed; check:generated reports all 14 artifacts current against a freshly built dist;
    • example apps: their os validate reruns are in the inventory table. No example metadata changed, so no example package owes its own suite.
  • Ablation. One-time mutations, each run under an absolute-path trap that restored from HEAD and proved the blob equal to HEAD, with git diff HEAD empty:
    • classification disabled at the dispatch point: exactly the 9 retired-key pins go red, and 66 stay green (all four stays-a-warning pins among them);
    • the union-arm branch removed: exactly the union-arm pin goes red (1 failed / 74 passed).

Gates

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack on the final head f88009a41 derived 88 commands. 87 ran, each exited 0, with exit codes captured before any pipe. --ran reconciles as "88 derived, 87 run, 1 NOT-MEASURED, 0 UNRUN".

  • NOT MEASURED: check:type-check-debt. Reason: a whole-workspace build plus --re-measure; CI owns it (dispatch instruction).
  • check:dual-build-cjs-loads first refused with PREREQUISITE NOT MET (8 unrelated packages had no dist). After building them it passed.
  • The derivation notes origin/main moved 4 commits past f88009a41 while the union ran. One family input changed in that range (scripts/engine-double-contract.pinned.json); CI's merge ref reads it.

Changeset

.changeset/23026-retired-component-prop-refused.md: @objectstack/lint: major on the v18 pre line, Clause-②: no (narrowing), FROM → TO table, and the ADR-0087 disposition already-registered. It names the 20 ledger ids that retired the refused keys. check:adr-0087-registration and check-changeset-no-major judged it green. The clause-② level axis is PR-scoped and CI reads it.

Patch round: the registry sentences the first head left false

The contract-tier review FAILed f88009a41 (record 6137697373 on this PR): two registry.ts docblocks still called the props gate "advisory" for keys this PR refuses as an error. The seat amended the claim's file surface (6137721990 on #23026) to every comment sentence in conversions/registry.ts and migrations/entries/** that states what the component-props gate does with a tombstoned key or a retired value and is false on the head. Commit f1905ac90 is comment-only: 2 files, 6 sentences. No code, test, changeset or other file changed.

Made false by this PR, corrected:

  1. registry.ts:10621-10625 (page-header-breadcrumb-removed)
    • BEFORE: "The tombstone on PageHeaderProps refuses it for a live author (advisory, via the props lint: PageComponentSchema.properties is an open bag)."
    • AFTER: "The tombstone on PageHeaderProps refuses it for a live author: os validate, os build and os lint refuse it as an error with the prescription (the component-props gate, @objectstack/lint; the load path keeps the open properties bag as written)."
  2. registry.ts:13565-13569 (object-master-detail-form-detail-sort-field-removed)
    • BEFORE: "The tombstone refuses the key for a live author (advisory, via the props lint: PageComponentSchema.properties is an open bag)."
    • AFTER: "The tombstone refuses the key for a live author: os validate, os build and os lint refuse it as an error with the prescription (the component-props gate, @objectstack/lint; the load path keeps the open properties bag as written)."

False before this PR, folded in under the seat's amendment 6137721990. Each is the same defect class as the card, a mechanical sentence edit, unclaimed elsewhere, and in the same gate family.

  1. registry.ts:8333-8340 (element-text-variant-heading-levels; the review cited it as :8327-8328). This is pre-existing.
    • BEFORE: "retiredFromLoadPath: an author is refused at parse with the prescription rather than silently rewritten; stored rows replay it ..."
    • AFTER: "retiredFromLoadPath: an author is not silently rewritten — os validate, os build and os lint report an old spelling as an advisory component-props-invalid finding carrying the prescription (the component-props gate, @objectstack/lint: a retired enum value is a value verdict, a warning; the load path keeps the open properties bag as written); stored rows replay it ..."
  2. registry.ts:5836-5839 (page-tabs-type-to-tab-style). This is pre-existing and was found by the sweep.
    • BEFORE: "The tombstone still carries the refusal at the authoring site (tsc) and at load (the parse), whatever the walk reached."
    • AFTER: "The tombstone still carries the refusal at the authoring site (tsc) and at os validate / os build / os lint, where the component-props gate refuses it as an error (the load path keeps the open properties bag as written), whatever the walk reached."
  3. registry.ts:8032-8037 (time-default-utc-suffix-dropped). This is pre-existing and was found by the sweep; only the element:button position was false.
    • BEFORE: "Retired from the load path: an author is refused at parse, and data at rest and os migrate meta replay it."
    • AFTER: "Retired from the load path: an author is refused at parse — except on an element:button's inline action, which sits in the open properties bag the load path keeps as written, where the refusal is an advisory component-props-invalid finding at os validate / os build / os lint (the component-props gate, @objectstack/lint) — and data at rest and os migrate meta replay it."
  4. migrations/entries/retired-keys/18.ui__PageHeaderProps__breadcrumb.ts:16-19. The residue argument is kept whole; only the present-tense "advisory", false on this head, is dated.
    • BEFORE: "this row is parsed only by the advisory props lint, which writes nothing back — so no released toolchain emitted breadcrumb: true ..."
    • AFTER: "this row is parsed only by the props lint (advisory in every release that shipped the default), which writes nothing back — so no released toolchain emitted breadcrumb: true ..."

Measured on the head, not argued. The built rule at f1905ac90 gives:

case finding
page:tabs type error
page:header breadcrumb error
details[].sortField error
element:text variant: 'heading' warning (enum issue, prescription carried)
element:button inline-action time param defaultValue: '09:00:00Z' warning (a custom issue at action.params.0.defaultValue)
record:chatter position: 'sidebar' warning
object-form layout: 'grid' warning

The sweep read 21 registry.ts sites and every migrations/entries/** hit of the props-gate vocabulary. The rest are true as written; the per-site list with evidence is in the os-dev-report on #23026.

Gates on f1905ac90. The derivation gives 89 commands: the 88 of f88009a41 plus check:spec-changes, which the migrations/entries edit adds. 19 ran, each exit 0:

  • check-spec-docblock-symbol-anchors, both halves;
  • check-issue-citations, both spellings;
  • check:docs, check:spec-changes, check:upgrade-guide, check:liveness, check:authorable-surface;
  • check:doc-authoring, check:nul-bytes;
  • comment-mask adoption, both halves; check-drift-comment; check-affected-docs;
  • closing-keyword parity, both halves;
  • check:pm-prior-rulings, check:cross-package-test-inputs.

check:generated also ran: all 14 artifacts are current against a freshly built dist. The spec tests that read registry.ts (11 files in src/conversions, the migrate-sentence class pin, and 20 more) passed: 32 files, 1520 tests. --ran reconciles as "89 derived, 19 run, 70 NOT-MEASURED, 0 UNRUN". The 69 other lines are carried from f88009a41, where each exited 0, because the patch adds comment text only. check:type-check-debt stays NOT MEASURED by dispatch.

Patch round 2: the docs that state the gate's tier

The seat found a published page that states the tier this PR changes (amendment 6138049056 on #23026), plus a sibling sentence in the same class. Commit 175a546b8 is docs-only: two files under content/docs/, with no code, test, changeset or packages/** change.

  1. content/docs/deployment/validating-metadata.mdx:473 and :488, the "one gate, four doors" table. Made false by this PR.
  2. content/docs/protocol/objectui/layout-dsl.mdx:741-745, the callout under the custom.* example. Pre-existing, folded in under 6138049056.
    • BEFORE: "For the platform's own types the authoring rules dispatch ComponentPropsMap and reject a misspelled prop; a custom.* type has no entry there, so its props are validated only by whatever registered the component."
    • AFTER: "For the platform's own types the authoring rules (os validate, os build, os lint) dispatch ComponentPropsMap: a misspelled prop is reported as a warning, and a retired prop is refused as an error, with its prescription. A custom.* type has no entry there, so its props are validated only by whatever registered the component."

Measured on the built rule at this head:

case result
a misspelled prop (page:header titel) warning component-props-unknown-key
object-grid defaultFilters, page:header breadcrumb error component-props-invalid
a wrong-typed limit, or a missing dataSource.object warning component-props-invalid
a custom.* node no finding

The registry entry reads tier: gating, commands build, lint and validate, and surfaces cli only, so the runtime publish column stays empty.

Sweep of content/docs/**: releases/ is excluded as release-owned, and references/ as generated from spec .describe() text, which this PR does not change. The two pages above are the only sentences that state the props gate's tier. The other hits concern other rules, or schemas the load path parses:

  • strict-schema refusals in ai/tools, automation/approvals, automation/flows, data-modeling/field-types;
  • validating-metadata §5, §10 and :357;
  • layout-dsl :219, :411, :449, :626, :754;
  • ui/views.mdx:453;
  • the query searchFields mentions.

The per-site list is in the os-dev-report on #23026.

Gates on 175a546b8. The derivation gives 109 commands: the 89 of f1905ac90 plus 20 that the content/docs paths add. 32 ran, each exit 0:

  • the 20 added: doc frontmatter, doc route spelling, docs section name and section landing index (each with its self-test); check:doc-security-posture, check:skill-examples, check:corpus-claim-drift, check:doc-anchors, check:docs-audit-scope, check:docs-redirects, check:docs-single-h1, check:docs-spec-enumerations, check:published-readme-links, check:react-page-adapter-contract, check:skill-identifier-liveness, check:vendor-version-stamps;
  • check:doc-authoring, check-affected-docs, check-drift-comment, check:docs, check:docs-transcript-drift;
  • closing-keyword parity and issue citations, both halves of each;
  • check:nul-bytes, check:cross-package-test-inputs, check:pm-prior-rulings.

check:skill-examples first refused with PREREQUISITE NOT MET (@objectstack/client-react was unbuilt); after building it, it exited 0. "Check Documentation Links" is lychee in CI and has no local form in this container: NOT MEASURED. These edits add no link. --ran reconciles as "109 derived, 32 run, 77 NOT-MEASURED, 0 UNRUN": 8 lines carried from f1905ac90, 68 from f88009a41, and check:type-check-debt by dispatch. Each carry holds because this round changes two content/docs pages and no input of those families.

Patch round 3: the shipped docblocks in component.zod.ts

The contract-tier review FAILed 175a546b8 (record 6138451028) on one shipped sentence: the docblock above the breadcrumb tombstone still called the props lint "advisory", and component.zod.ts ships in the tarball (src/**/*.zod.ts). The seat amended the surface (6138461923) to that file's comments plus four non-shipped sites. Commit 6280ca3b0 is comment-only: 4 files, +24/-15. The diff guard counts 0 non-comment lines. No schema, retiredKey() guidance string or .describe() text changed; those belong to #23145's claim.

  1. component.zod.ts:654-658, the docblock above breadcrumb. Made false by this PR; dated the way its retired-keys twin was, with the residue argument kept whole.
    • BEFORE: "this row is parsed only by the advisory props lint, which writes nothing back — so no retired-default residue stage is owed."
    • AFTER: "this row is parsed only by the props lint (advisory in every release that shipped the default), which writes nothing back — so no retired-default residue stage is owed."
  2. component.zod.ts:284-303, the SDUI 组件 props 没有解析闸门:PageComponent.properties 是开放 record,ComponentPropsMap 的 29 个站点从不被 parse(#4001 批 17 的 no gate 判定) #5068 header. Made false by this PR.
  3. component.zod.ts:4849-4854, 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 object-* section. The review read it as true for its subject. My probe refutes that: these rows carry tombstones (object-grid defaultSort and navigation.view, object-kanban quickAdd), and each is an error on this head. The maintainer ruling quotation is kept verbatim.
  4. ui/page-header-breadcrumb-retirement.test.ts:19-21 and 5. ui/master-detail-detail-sort-field-retirement.test.ts:22-24. Not shipped.
    • BEFORE: "reached by the advisory props lint and never by the page parse: an existing page is never hard-refused."
    • AFTER: "reached by the props lint — which refuses this tombstone as an error at os validate / os build / os lint — and never by the page parse: an existing page is never hard-refused at load."
  5. ui/element-flat-binding-retirement.test.ts:201-202. Not shipped.
    • BEFORE: "The open properties bag is the props lint's to judge (advisory), not the page parse's."
    • AFTER: "The open properties bag is the props lint's to judge (a retired flat key is an error at os validate / os build / os lint), not the page parse's."

Kept as written, with evidence:

  • conversions/walk.ts:371 is read in context. Its story is page:header description, which is not a tombstone; the strict row's unrecognized_keys makes it a component-props-unknown-key warning, which the probe confirms at a nested site. So "advisory" is true there.
  • component.zod.ts:5686 (exportOptions, "advisory"): a bare array and a pdf format are both warnings, per the probe.
  • component.zod.ts:3376 and :3429 (the props gate "refusing every authored key" of element:filter / element:form): those keys are tombstones, now errors.

Still false after this round. Not edited, because the round's single push was spent before a wider grep found them:

All three are non-shipped comments. One three-sentence comment commit closes the class.

Measured on the head. The built rule at 6280ca3b0 gives:

case result
page:header breadcrumb error
object-grid defaultSort and navigation.view, object-kanban quickAdd error
an object-grid pagination.pageSize of the wrong type, the unknown key filters warning
exportOptions refusals warning
a nested page:header description warning component-props-unknown-key
element:repeater properties.object error
an element:filter key error

Gates on 6280ca3b0. The derivation gives the same 109 commands as 175a546b8. 17 ran, each exit 0:

  • check:docs;
  • check-spec-docblock-symbol-anchors, both halves;
  • check:spec-changes, check:liveness, check:authorable-surface, check:api-surface, check:exported-any;
  • closing-keyword parity and issue citations, both halves of each;
  • check:nul-bytes, comment-mask adoption (both halves), check:doc-authoring, check:cross-package-test-inputs.

check:generated reports all 14 artifacts current after a real spec build from this head. Tests: the three touched spec files passed (96 tests), and validate-component-props.test.ts passed (75 tests). --ran reconciles as "109 derived, 17 run, 92 NOT-MEASURED, 0 UNRUN": 24 lines carried from 175a546b8, 1 from f1905ac90, 66 from f88009a41, and check:type-check-debt by dispatch. Each carry holds because this round changes comment text only.

Patch round 4: the last three comments of the class

Round 3's widened grep found three more false comments after that round's push. The seat amended the surface to cover them (6138732286 on #23026). Commit 20c00221c is comment-only: 3 files, +9/-7. The diff guard counts 0 non-comment lines. None of the three ships.

  1. packages/lint/src/validate-page-field-bindings.ts:232-239, about the tombstoned displayField / searchFields. The registries-differ argument is kept: the gate neither precedes nor suppresses this rule.
  2. ui/page-header-breadcrumb-retirement.test.ts:356-357 and 3. ui/master-detail-detail-sort-field-retirement.test.ts:447-448, the absence pins' EXCLUDED comments.
    • BEFORE: "The props-lint pin authors the key to assert the advisory warning an author meets at os validate / os build / os lint."
    • AFTER: "The props-lint pin authors the key to assert the refusal (an error) an author meets at os validate / os build / os lint."

The class is closed. The class grep on 20c00221c over packages/spec/src and packages/lint/src combined round 3's patterns with the widened ones ("advisory warning", "gate is advisory"), plus the advisory-adjective and props-gate-with-tier-word sweeps. It returns no false hit. Every remaining hit falls into one of these groups:

  • dated or past-tense text: component.zod.ts:284-303, :656, :4849-4854, the retired-keys comment, and this PR's own lint text;
  • true for its subject: walk.ts:371 (a description unknown-key warning), component.zod.ts:5686 (exportOptions value verdicts), and the registry.ts.template typed-member strings;
  • another rule's own "advisory": liveness, seed safety, readonly hook writes, the flow package closure, security posture.

The per-hit list is in the os-dev-report on #23026.

Gates on 20c00221c. The derivation gives the same 109 commands. 10 ran, each exit 0: comment-mask adoption and issue citations (both halves of each), check:doc-authoring, closing-keyword parity (both halves), check:nul-bytes, lint check:doc-formula-expressions, check:cross-package-test-inputs. Tests under the verify lock: the two touched spec files passed (40 tests), and validate-page-field-bindings.test.ts passed (38 tests). --ran reconciles as "109 derived, 10 run, 99 NOT-MEASURED, 0 UNRUN": 8 lines carried from 6280ca3b0, 24 from 175a546b8, 1 from f1905ac90, 65 from f88009a41, and check:type-check-debt by dispatch. Each carry holds because this round changes three comments only.

Acceptance notes

  • File surface vs the claim. Paths outside the claim's original list, each since added by a seat amendment:

    • packages/lint/src/authoring-rules.ts: the tier flip that the wiring guard requires for any rule emitting error, plus its comment;
    • packages/spec/src/conversions/registry.ts: the two H3 sentences, which the dispatch pre-authorized, and the patch round's five docblock sentences (amendment 6137721990); plus packages/spec/src/migrations/entries/retired-keys/18.ui__PageHeaderProps__breadcrumb.ts, one comment;
    • packages/spec/src/ui/form-field-public-picker-retirement.test.ts: the one exclusion above.
    • content/docs/deployment/validating-metadata.mdx and content/docs/protocol/objectui/layout-dsl.mdx: patch round 2 (amendment 6138049056).
    • packages/spec/src/ui/component.zod.ts (comments only) and three spec test files' comments: patch round 3 (amendment 6138461923).
    • packages/lint/src/validate-page-field-bindings.ts, one comment: patch round 4 (amendment 6138732286).

    No example or template needed a fix (inventory 0). No CLI test pinned the old warning.

  • Two sibling absence pins (page-header-breadcrumb-retirement.test.ts, master-detail-detail-sort-field-retirement.test.ts) described this lint test file as asserting "the advisory warning"; patch round 4 corrected both EXCLUDED comments to say it asserts the refusal, an error.

  • validateComponentProps keeps surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT ("reads a stack-wide collection the per-write snapshot does not carry"). The rule is page-local, so that reason looks inaccurate, but I did not measure a page write's snapshot. Whether the gating rule should cross to the runtime door is its own rollout decision, with a false-refusal budget over stored tenant pages. Carrier: none.


Generated by Claude Code

… prop as an error

A value verdict raised by a retiredKey() tombstone (invalid_type expecting
never, directly or as the one arm of a collapsed union that judged keys) is
now severity error, so os validate, os build and os lint refuse it with the
tombstone's prescription. Every other finding of the rule stays a warning.
validateComponentProps becomes a gating rule in the authoring registry.

The warning-period inventory the gate header named as the upgrade's
acceptance baseline measured 0 retired-key findings and 0 other findings
over the example apps, the shipped pages and the scaffold template.

Claude-Session: https://claude.ai/code/session_012uFegdfR6K6uqvLzt7XsdZ
Co-authored-by: Claude <noreply@anthropic.com>
…doors that refuse them

The record-picker reconciliation's shared docblock said the loader rejects a
tombstoned key loudly, and the object-grid defaultFilters entry said an author
is refused at the parse. The load path keeps the open properties bag as
written; the refusal is the component-props gate's, at os validate, os build
and os lint. Both sentences now say so. Comment-only; nothing in
@objectstack/spec's published files changes.

Adds the @objectstack/lint changeset for the retired-key error upgrade.

Claude-Session: https://claude.ai/code/session_012uFegdfR6K6uqvLzt7XsdZ
Co-authored-by: Claude <noreply@anthropic.com>
…that authors it

The component-props gate's new union-arm pin authors publicPicker on an
object-form field to prove a tombstone inside a union arm is refused as an
error. Excluded with its reason, as the breadcrumb and detail-sortField
absence pins already exclude the same file.

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 9 documentable anchor(s). ⚠️ 2 changed file(s) yielded no anchor (packages/spec/src/conversions/registry.ts, packages/spec/src/migrations/entries/retired-keys/18.ui__PageHeaderProps__breadcrumb.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/api/error-catalog.mdx (via invalid_type (literal, a string literal in isTombstoneIssue))
  • content/docs/api/error-handling-server.mdx (via invalid_type (literal, a string literal in isTombstoneIssue))
  • content/docs/automation/flows.mdx (via invalid_type (literal, a string literal in isTombstoneIssue))
  • content/docs/deployment/cli.mdx (via invalid_type (literal, a string literal in isTombstoneIssue))
  • content/docs/deployment/validating-metadata.mdx (via AUTHORING_RULES (symbol, a top-level const object))
  • content/docs/protocol/objectui/concept.mdx (via invalid_type (literal, a string literal in isTombstoneIssue))

⛔ 2 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17/17-0.mdx (via invalid_type (literal, a string literal in isTombstoneIssue), invalid_union (literal, a string literal in isRetiredKeyVerdict))
  • content/docs/releases/v17/17-6.mdx (via PageHeaderProps (symbol, a top-level const object), invalid_type (literal, a string literal in isTombstoneIssue))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 2 changed file(s) yielded no anchor (packages/spec/src/conversions/registry.ts, packages/spec/src/migrations/entries/retired-keys/18.ui__PageHeaderProps__breadcrumb.ts) — 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 dc93781c1947b53cff50a550ad551e462a464594 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 6e2cd4e2d5d79aef349154d5059207a79b4bfab0 — the merge of head 20c00221c6ff0a2a41e1d94f9396f69dd831fb23 into base dc93781c1947b53cff50a550ad551e462a464594, 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 6e2cd4e2d5d79aef349154d5059207a79b4bfab0 && git checkout 6e2cd4e2d5d79aef349154d5059207a79b4bfab0
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin dc93781c1947b53cff50a550ad551e462a464594 20c00221c6ff0a2a41e1d94f9396f69dd831fb23 && git checkout -B drift-repro dc93781c1947b53cff50a550ad551e462a464594 && git merge --no-ff 20c00221c6ff0a2a41e1d94f9396f69dd831fb23

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

⚠️ 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 dc93781c1947b53cff50a550ad551e462a464594 → 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

Contract review

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

Inputs: card #23026 (body and all five comments, the os-dev-report and the seat's ACCEPT included), PR #23140 (body, the six-file list, the net diff against main at this head), the 39 check-runs on the head, origin/main at 1ca7a805ca and the head fetched into a private ref — all read-only. Judged independently of the seat's ACCEPT; where I agree with it, I say what I read, not that it said so.

① Derived judgments

  1. component-props-invalid files a retiredKey() tombstone verdict as error — RIGHT, and the refused class is exactly the tombstone class. isTombstoneIssue reads code === 'invalid_type' and expected === 'never', the same discriminator as the spec's own isRetiredKeyIssue (automation/flow-node-config-refusals.ts:459). I enumerated every never-producing construct reachable from ComponentPropsMap on the head: 38 retiredKey() sites in ui/component.zod.ts (page:header icon / breadcrumb, page:tabs type, page:card actions / body, record:details layout, the ten flat binding keys of element:number / element:record_picker / element:repeater, the six keys of element:filter, the six of element:form, three targetVariables, displayField, searchFields, multiple, object-grid defaultFilters / defaultSort / resizableColumns, object-kanban quickAdd, details[].sortField); three more through imports (NavigationConfigSchema.view, FormFieldSchema.publicPicker, ChartConfigSchema.aria by way of ReportSchema.extend); and the two retiredComponentProps rows (user:profile, ai:chat_window). Nothing else reachable raises expected: 'never': strictObject() is z.object(...).strict() and zod 4 folds a never catchall into unrecognized_keys (the measured note at view.zod.ts:6806-6817; the PR's titel pin holds it); enumWithRetiredValues is a z.enum, so a retired value is an enum issue; the one hand-built invalid_type in the UI graph (view.zod.ts:6779) carries expected: 'object'; the non-tombstone z.never().optional() on ViewItemWireSchema.options (view.zod.ts:5663) is unreachable — component.zod.ts references no ViewItem schema and takes ListViewSchema members by .shape pick only; the viewItems and inline-credential nevers sit on the stack and driver schemas. No expected: 'never' literal exists in spec source outside comments, so nothing hand-built can spoof the shape.
  2. The union-arm rule (invalid_union whose arms contain exactly one all-tombstone arm) — RIGHT and conservative. It mirrors unrecognizedKeysFromUnionArm, which runs first in the loop, so a union where another arm judged keys routes to the unknown-key warning and never reaches the classifier. Live on one shape (sections[].fields[], publicPicker); the mixed-arm case is pinned a warning.
  3. Every other finding stays a warning — RIGHT at the rule (the other branches of the loop are untouched) and at the doors: os validate / os build / os lint partition by splitBySeverity on severity === 'error'; tier is read by no runtime path, only by authoring-rule-wiring.test.ts, which forces the advisory → gating flip for any rule that emits error. The feat(spec)!: a type:'url' action's target declares its closed scheme set — http(s)://, mailto: and in-app paths — at both url-action doors #23076 url target and feat(spec): a record:details section heading can show how many values a multi-value field holds (sections[].count and the {{ count }} hole) #23080 section count refusals, a retired enum value, an undeclared key and the missing dataSource.object binding remain warnings, each pinned.
  4. ComponentPropsSeverity widens to 'error' | 'warning' — a public type of @objectstack/lint; RIGHT to ride the major.
  5. The retired element rows become an error from this rule — declared in the changeset; RIGHT, and it opens no new door-level refusal: PageComponentSchema.type already refuses a retired type at the parse (page.zod.ts:557-574), so the row is reachable only by calling the rule directly. One wording note: the hint's "keeps a retired key only to refuse it" is loose for a whole-bag z.never row; unreachable through the doors, so a note, not a defect.
  6. Rule header, message and hint — TRUE. "The prescription above" holds in both shapes: describeIssue prints a tombstone's message directly and, for a collapsed union, every arm's issues with their messages.
  7. The two edited registry sentences (registry.ts:5135-5139, :9822-9824) — TRUE on the head: the load path keeps the open properties bag; the three CLI doors refuse; os migrate meta lists the edits.
  8. WRONG — two further registry sentences the diff falsifies and does not touch. registry.ts:10612-10613 (page-header-breadcrumb-removed: "The tombstone on PageHeaderProps refuses it for a live author (advisory, via the props lint ...)") and :13554-13555 (object-master-detail-form-detail-sort-field-removed: "The tombstone refuses the key for a live author (advisory, via the props lint ...)"). Both keys are retiredKey() tombstones this head files as error — the PR's own flipped pins at validate-component-props.test.ts:90 and :116 prove it. The ADR-0087 ledger now says "refuse" at :5137 and "advisory" at :10612 about one gate. This is the card's own defect class — registry text misdescribing what the props gate does — introduced in the file the amended claim covers; the PR body's "two sentences, no other region of the file" was a sweep that stopped at the two sentences the card cited. Not shipped (registry.ts is outside spec's files glob and both consts are unexported), so no consumer reads it; the ledger's maintainers do, and the spec-property-retirement playbook models the next retirement on these entries.
  9. Pre-existing, out of scope, to file: registry.ts:8327-8328 (element-text-variant-heading-subheading-retired) says "an author is refused at parse with the prescription" — element:text.variant is an enumWithRetiredValues value verdict, a props-gate WARNING before and after this PR; its own D3 entry (migrations/entries/semantic/18.element-text-variant-heading-subheading-retired.ts:36) says advisory, correctly. Same class as the card, not this PR's doing. The comment at migrations/entries/retired-keys/18.ui__PageHeaderProps__breadcrumb.ts:17 ("parsed only by the advisory props lint, which writes nothing back") belongs in the same sweep; the residue point it makes stays true.
  10. Could a test or gate pass while the classification is wrong? Not in the direction that matters. The schema-graph pin (every top-level optional(never) in the map must file an error at its own key, anti-vacuity floor of 30 plus the specimen) reds if any tombstone stops classifying; the direct, union, mixed-union, per-issue and four stays-a-warning pins red on over-reach; the pipeline pin reds if runAuthoringRules drops the error. The dev's two ablations (9 reds, then 1, exactly the expected pins) read the same way. Check-runs on the head at read time: 26 success (Lint & Repo Gates, Build Core, Governed Surface Queue Guard, Check Changeset, Type Check · source gates / consumer gates / debt ledger, Spec property liveness among them), 6 skipped (full-battery jobs), 7 in progress (Test Core 1–6/6, Type Check · workspace). No governed surface in the file list; 384 changed lines; same-repo head; draft.

② Semver level

  • @objectstack/lint: major — RIGHT. (narrowing) is BREAKING. .changeset/pre.json is mode: pre, tag next, so check-changeset-no-major's RC exemption admits a major, and the fixed group already carries many pending majors (22130-protocol-18.md among them), so this adds no release level. House precedent for this very gate is 11509-element-flat-binding-retired.md (@objectstack/lint: major). The lockstep group bumps @objectstack/cli with it; the sentence belongs in lint's CHANGELOG, where the refusal text lives — a CLI line is not owed. @objectstack/spec publishes nothing from this diff (a docblock on unexported consts, one test exclusion), so no spec line is owed and skip-changeset does not apply to a published package.
  • Clause-②: no (narrowing) — RIGHT, present in both carriers (PR body line 2, verbatim from the claim; changeset body). The level axis is not engaged.
  • ADR-0087 marker — RIGHT. Exactly one marker, the **BREAKING** token present, not-required (already-registered ...) in the ADR's grammar. The gate verifies only that the 20 ids resolve and pre-date the base (ADR-0087 says coverage is the author's honesty), so I checked coverage by hand: the 38 in-file tombstones and the three imported ones all map onto the 20 ids (page-structure-inert-keys-removed covers page:header.icon / page:card.actions; element-flat-data-binding-to-data-source the ten flat keys; record-picker-inert-keys-removed searchFields / multiple; element-input-target-variable-removed the three targetVariables; element-filter-removed / element-form-removed the two retired elements' key rows; list-view-navigation-view-retired, form-field-public-picker-retired, chart-config-aria-retired, ui-ai-chat-window-retired the rest). "Every key this refuses ... with its ledger entry" is true of every key; user:profile is an element with no ledger id of its own, but it is refused at the parse already (①.5), so nothing newly refused lacks an entry. The diff adds no entry and amends none, so registered would be false data; already-registered is the honest disposition.
  • Changeset body — TRUE. "What is refused" and "What stays a warning" match isRetiredKeyVerdict and the loop; "Not judged here" matches surfaces: CLI_ONLY (authoring-rules.ts:1194) and the open bag; the FROM → TO rows match the defaultFilters conversion's precedence and record-picker-display-field-to-label-field; "0 newly refused" is the dev's measurement over the four examples and the five shipped pages with a lit positive control, and no example or template changed in the diff.

③ Boundary flags

Why FAIL, and what lifts it. The narrowing, its severity, its doors, the changeset and the marker are right in every respect I could derive, and no shipped sentence is false. The head fails on the contract path leg alone: the diff falsifies registry.ts:10612-10613 and :13554-13555 and leaves them, so the ADR-0087 ledger describes one gate as refusing at :5137 and advisory at :10612. Remedy: rewrite those two parentheticals to say the props lint refuses the key as an error at os validate / os build / os lint (the load path keeps the open bag); sweep registry.ts and migrations/entries for any other tier claim about a tombstone's props-gate finding (:8327-8328 is a pre-existing one of the sibling class — file it, or fold it under the amended claim); push. A record on that head can take ① and ② above as read.

Implemented-by: claude/issue-23026-retired-prop-error
Reviewed-by: session_012uFegdfR6K6uqvLzt7XsdZ

VERDICT: FAIL

…r as the head enforces it

The contract-tier review of the retired-key error upgrade found two
conversion docblocks this change made false: page-header-breadcrumb-removed
and object-master-detail-form-detail-sort-field-removed said the tombstone
refuses the key "advisory, via the props lint". Both now say os validate,
os build and os lint refuse it as an error, the load path keeping the open
properties bag.

The sweep of registry.ts and migrations/entries for the same class found
three pre-existing false sentences, folded in under the seat's amendment:
element-text-variant-heading-levels said an author is refused at parse (a
retired enum value is an advisory finding); page-tabs-type-to-tab-style said
the tombstone refuses at load (the load path does not parse the props bag);
time-default-utc-suffix-dropped said refused at parse for an element:button
inline action (an advisory finding there). The breadcrumb retired-key note
keeps its residue argument and now says the lint was advisory in every
release that shipped the default.

Comment-only.

Claude-Session: https://claude.ai/code/session_012uFegdfR6K6uqvLzt7XsdZ
Co-authored-by: Claude <noreply@anthropic.com>
…nt-props gate's tier as the head enforces it

validating-metadata.mdx listed "SDUI component props" only in the
Advisory row of the one-gate, four-doors table. A retired (tombstoned)
prop is now an error at os validate, os build and os lint, so it gets its
own gating row (not on the runtime publish door: the rule is CLI-only),
and the advisory row keeps every other props finding.

layout-dsl.mdx said the authoring rules reject a misspelled prop. A
misspelled prop is a component-props-unknown-key warning; a retired prop is
refused as an error. Pre-existing, folded in under the seat's amendment.

Docs only.

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

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 175a546b8cdc7c148d79cd77ad5154565828820f
Local-runs: none

Inputs: card #23026 (body and all nine comments: triage, the claim, the seat pointer, three os-dev-reports, the ACCEPT and the two amendments), PR #23140 (body, the nine-file list, the net diff against main at this head), the two PR comments (the docs-drift notice and the FAIL record 6137697373 on f88009a41), the 46 check-runs on this head, origin/main at 02bec6ca86, the head fetched into a private ref, the @objectstack/lint@17.7.0 tag, and git diff f88009a41..175a546b8 — all read-only. The seat's ACCEPT and amendments were tested, not adopted; where I agree, I say what I read.

How far the head moved. Two commits since f88009a41 (f1905ac90, 175a546b8): four files, +32/−15. Every changed line in packages/spec/src/conversions/registry.ts (23 added, 9 removed) and in packages/spec/src/migrations/entries/retired-keys/18.ui__PageHeaderProps__breadcrumb.ts (3 added, 2 removed) is a * or // comment line — counted mechanically over the diff — plus two content/docs/** pages. No code, test, changeset or packages/lint line moved, so the rule, the doors, the classifier and the changeset are byte-identical to what record 6137697373 judged. Both commits carry the model-free trailer pair; no model identifier in title, body, changeset, comments or commits.

① Derived judgments

  1. Carried by reference from 6137697373 ①.1–①.6 and ①.10 — the code they judge did not move. I re-read the load-bearing points on this head rather than only trusting them: isTombstoneIssue reads invalid_type with expected === 'never', the spec's own isRetiredKeyIssue discriminator (automation/flow-node-config-refusals.ts:459-461); retiredKey() is z.never(...).optional() (shared/retired-key.ts:186), 42 sites in ui/component.zod.ts plus the two retiredComponentProps rows; in the dispatch loop (validate-component-props.ts:286-447) the walker's unknown keys, a union arm's and a strict row's unrecognized_keys, the missing dataSource.object binding and every non-tombstone value verdict are warning, and only isRetiredKeyVerdict files error; an unregistered type is skipped at :303. splitBySeverity (authoring-rules.ts:2252) partitions on severity === 'error', and cli/src/utils/author-time-rules.ts:146-149 turns a non-empty error set into the refusal os validate, os build and os lint exit on; the registry entry is tier: 'gating', commands: ALL, surfaces: CLI_ONLY. The refused class is exactly the tombstone class and nothing wider: enumWithRetiredValues returns z.enum(values, { error }) (retired-key.ts:422-437), an enum issue; every filter door is z.array(ViewFilterRuleSchema, { error: ruleArrayFilterError(...) }) (component.zod.ts:5254 and its siblings), so a legacy record there is invalid_type expecting array, a warning; a time default's zone refusal reaches an inline action through valueSchemaFor(def, 'stored').safeParse inside a refinement (data/default-value-shape.ts:181), never a never. "Could a test or gate pass while the classification is wrong?" — no: the schema-graph pin (every optional(never) in the map must file an error at its own key, anti-vacuity floor 30) and the four stays-a-warning pins bracket the class from both sides, and nothing in this head moved them.
  2. The FAIL's remedy on registry.ts:10621-10625 and :13565-13569 — RIGHT and TRUE. breadcrumb (component.zod.ts:660) and details[].sortField (:8336) are retiredKey() tombstones; both docblocks now say the three CLI doors refuse the key as an error with the prescription via the component-props gate, and that the load path keeps the open properties bag as written. Both halves hold: PageComponentSchema.properties is the open bag, parsed by no load-path schema. The ledger no longer calls one gate "refuse" at :5137 and "advisory" at :10622.
  3. The three folded-in registry sentences — TRUE on the head. :5836-5839 (page-tabs-type-to-tab-style): page:tabs type is retiredKey() (component.zod.ts:794), an error at the three doors, and tsc still refuses it at the authoring site. :8333-8340 (element-text-variant-heading-levels): element:text variant is enumWithRetiredValues (:2980), so an old spelling is an enum issue and stays a warning, which is what the sentence now says. :8032-8037 (time-default-utc-suffix-dropped): on an element:button the inline action sits in the open bag and the refinement's issue is a warning from this rule — TRUE; the untouched objects[] / actions[] half ("refused at parse") predates this PR, is not a props-gate sentence, and I did not measure it beyond the stack schema parsing those arrays — NOT MEASURED, as the dev recorded. :5135-5139 and :9830-9832, the first round's two edits, remain TRUE (displayField / searchFields / multiple at component.zod.ts:3571-3594 and defaultFilters at :5278 are all retiredKey()).
  4. retired-keys/18.ui__PageHeaderProps__breadcrumb.ts:16-19 — TRUE as dated. At the @objectstack/lint@17.7.0 tag the rule is tier: 'advisory' and ComponentPropsSeverity = 'warning' is a single literal, and breadcrumb is already a tombstone there, so every release that shipped the .default(true) shipped an advisory props lint; the lint writes nothing back. The residue argument stands.
  5. The two docs pages — TRUE. content/docs/deployment/validating-metadata.mdx:473 adds a gating row for a retiredKey() tombstone ticked on the three CLI doors and not on runtime publish (surfaces: CLI_ONLY), and :488 narrows the advisory row to "every finding but a retired prop" — both read off the loop. content/docs/protocol/objectui/layout-dsl.mdx:741-745: a misspelled prop is component-props-unknown-key at warning (a strict row raises unrecognized_keys), a retired prop an error with the prescription, and a custom.* type has no row and is skipped — all three read off the loop. The new row carries an issue number the way its sibling rows do, and Lint · docs & code guards is green on the head.
  6. My own sweep of the amended surfaces — no further false sentence there. registry.ts: 62 hits of the tier and door vocabulary; beyond the seven sentences above, every props-gate hit is a reach statement with no door-level tier (:5129-5133, where "parse time" is the props schema's own parse, :8330, :11687, :11712) or a correct advisory on a value verdict (:14580, a legacy filter record at properties.filter); the "refused at parse" and "loader rejects" sentences at :217, :662, :2110, :6925, :7440, :8495-8505, :8835-8850, :9201, :12108, :13391, :13869, :13999, :14132, :14888, :15421 concern schemas the stack parses at load (object, flow, dataset, template, cube, agent, view, report, permission, plugin registry entries), not the props bag. migrations/entries/**: the shipped description and acceptanceCriteria strings that say "advisory component-props-invalid / component-props-unknown-key" (the typed-member and closed-set entries, 18.ui-object-grid-page-size-positive-integer-refused.ts:43, 18.element-data-source-and-object-block-filter-rule-array.ts:89 on properties.filter) describe value and unknown-key verdicts, which stay warnings; 18.element-text-variant-heading-subheading-retired.ts:36 says advisory for an enum value, right; 18.page-header-breadcrumb-retired.ts:27 ("the props lint reports one with the prescription") states no tier and holds; 18.ui-object-master-detail-form-details-closed.ts:42-43 is scoped to entry keys and column rules, which stay warnings — details[].sortField is the separate tombstone entry's surface. content/docs/** outside releases/ and references/: the only sentences naming the props gate's tier are the three on the two edited pages; no tombstone / retired prop sentence elsewhere states a tier.
  7. WRONG — one shipped sentence the diff falsifies, outside the surfaces the seat named. packages/spec/src/ui/component.zod.ts:654 (:655 on origin/main), the docblock directly above the breadcrumb tombstone, reads: "PageComponentSchema.properties is an open bag, and this row is parsed only by the advisory props lint, which writes nothing back — so no retired-default residue stage is owed." It is the word-for-word twin of the sentence the patch round dated at retired-keys/18.ui__PageHeaderProps__breadcrumb.ts:17, and the dev's own round-1 report judged that present-tense "advisory" false on this head. On this head the props lint refuses breadcrumb as an error (the PR's flipped pin at validate-component-props.test.ts:90). Unlike registry.ts, component.zod.ts SHIPS: @objectstack/spec's files carries src/**/*.zod.ts, so the tarball's own contract text says "advisory" above the key that os validate now stops on, while registry.ts:10622 says "error" about the same key — the card's defect class (contract text contradicting the gate), created by this diff, in the one file the two sweeps never read. The sentence does not reach content/docs/references/** (0 hits) or the JSON Schema (the [REMOVED] describe string is separate), so the fix is the one docblock. No other .zod.ts docblock names the props gate with a tier: component.zod.ts:5680 ("advisory", exportOptions) is a value verdict and true; :4846-4848 ("findings on these entries are advisory, still gated on the SDUI 组件 props 没有解析闸门:PageComponent.properties 是开放 record,ComponentPropsMap 的 29 个站点从不被 parse(#4001 批 17 的 no gate 判定) #5068 inventory") is about the 未知键静默剥离仍是全仓默认:把 #3405 的 strict 收紧从一个 schema 推广到整个可授权面(ADR-0078 完整性闸门) #4001 typed-member entries, which stay warnings — true for its subject, though "gated on the inventory" now reads as the 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 decision; :708 and :2511 are historical.
  8. Not shipped, same class, noted for the record: ui/page-header-breadcrumb-retirement.test.ts:19, ui/master-detail-detail-sort-field-retirement.test.ts:22 (both already in the Acceptance notes), ui/element-flat-binding-retirement.test.ts:201 ("the props lint's to judge (advisory)" — the flat keys are tombstones, an error now) and conversions/walk.ts:371 ("the advisory props gate runs CLI-only") — test and tool comments; carrier none is acceptable for them.
  9. Check-runs on this head at read time (46): 29 completed / success — among them Lint & Repo Gates, Governed Surface Queue Guard, Check Changeset (two runs), Type Check · source gates, Spec property liveness, Build Docs, Check Documentation Links, Lint · docs & code guards, Lint · CI, release & engine gates, Lint · ESLint, Lint · PM dispatch-gates, Lint · repo & tooling gates, the three Lint · scoped jobs, Flag docs affected by code changes, Dogfood Regression Gate (gate job), filter, and the four card and claim parity checks (two runs each); 7 completed / skipped — Console Pin Gate, the Dogfood Regression Gate matrix shard, Dogfood Verify CLI, Temporal Conformance (live PG + MySQL), Packed-tarball smoke (opt-in), and one skipped duplicate each of Auto Label and Check PR Size; 10 in_progress — Build Core, Test Core 1/6 through 6/6, Type Check · consumer gates, Type Check · debt ledger, Type Check · workspace. Judged on the diff, not on the pending runs. No governed surface in the nine-file list; 431 changed lines; same-repo head; draft; mergeable_state: blocked; no auto-merge armed.

② Semver level

Carried by reference from 6137697373 ② — the changeset did not move. Re-confirmed on this head: .changeset/23026-retired-component-prop-refused.md is @objectstack/lint: major, carries Clause-②: no (narrowing) and exactly one adr-0087: not-required (already-registered ...) marker naming 20 ids with the **BREAKING** token present; .changeset/pre.json is mode: pre, tag: next on both origin/main and the head; @objectstack/lint sits in the single fixed group of .changeset/config.json; scripts/check-adr-0087-registration.mjs recognises already-registered (:143), and both Check Changeset runs on this head are green. PR body line 2 carries the claim's Clause-② line verbatim. The two patch rounds publish nothing: registry.ts is outside spec's files glob, the retired-keys entry edit is a // comment that reaches no generated string, and content/docs/** is not a package — so no @objectstack/spec line is owed and skip-changeset does not apply. (narrowing) is BREAKING, so major is the only right level in pre mode, and the body's "What is refused" / "What stays a warning" / "Not judged here" paragraphs and the FROM → TO rows match the loop, surfaces: CLI_ONLY, and the object-grid-default-filters-removed conversion's precedence (the fallback moves onto an empty filter, registry.ts:9847-9860).

③ Boundary flags

Why FAIL, and what lifts it. Everything the earlier record failed on is fixed and true, every sentence the two patch rounds edited is true against the code on this head, the patches changed nothing but comment and docs text, and ① and ② of 6137697373 hold unchanged. The head fails on one point of the same class: the diff turns the breadcrumb tombstone's verdict into an error while the shipped docblock above that tombstone, component.zod.ts:654, still says the row is "parsed only by the advisory props lint" — the one copy of the dated sentence that the tarball actually carries, left contradicting registry.ts:10622 about the same key. Remedy: date it the way retired-keys/18.ui__PageHeaderProps__breadcrumb.ts:17 was dated ("the props lint (advisory in every release that shipped the default)"), under a one-path claim amendment; push. A record on that head can take ①.1–①.6, ①.8–①.9, ② and ③ above as read, and needs to re-read only that docblock and the check-runs.

Implemented-by: claude/issue-23026-retired-prop-error
Reviewed-by: session_012uFegdfR6K6uqvLzt7XsdZ

VERDICT: FAIL

…-props gate's tier as the head enforces it

The contract-tier review of the retired-key error upgrade found the docblock
above the breadcrumb tombstone still saying the row is parsed only by the
advisory props lint; component.zod.ts ships in the tarball. It is dated the
way its retired-keys twin was: the props lint was advisory in every release
that shipped the default, and the residue argument is kept whole.

The rest of the file's tier sentences, read on the head: the #5068 header
said the gate is warning-level and that the page rewrites stood before the
error upgrade, and the #7751 section said findings on its rows are advisory,
still gated on the inventory. Both now record the upgrade for the retired-key
class and that every other finding stays a warning.

Three test comments of the same class (the breadcrumb and detail-sortField
retirement pins, the flat-binding retirement test) now say the props lint
refuses those tombstones as an error. Comment-only; no schema, guidance
string or describe text changed.

Claude-Session: https://claude.ai/code/session_012uFegdfR6K6uqvLzt7XsdZ
Co-authored-by: Claude <noreply@anthropic.com>
… a tombstoned key say it refuses it as an error

validate-page-field-bindings said the #5068 props gate is advisory about
the tombstoned displayField / searchFields; it refuses them as an error at
os validate, os build and os lint and stays advisory for every other
finding. The registries-differ argument is unchanged. The breadcrumb and
detail-sortField absence pins' EXCLUDED comments said the props-lint pin
asserts the advisory warning; it asserts the refusal, an error.

Comment-only. Closes the class the contract-tier review named.

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

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 20c00221c6ff0a2a41e1d94f9396f69dd831fb23
Local-runs: none

Inputs: card #23026 (body and all thirteen comments: triage, the claim, the seat pointer, five os-dev-reports, the ACCEPT and the four amendments), PR #23140 (body, the fourteen-file list, the net diff against main at this head), the three PR comments (the docs-drift notice and the two FAIL records 6137697373 on f88009a41 and 6138451028 on 175a546b8), the 46 check-runs on this head read twice, origin/main at e540e7ffb, the head fetched into a private ref (refs/review/pr-23140-head = 20c00221c, the branch tip), the @objectstack/lint@17.7.0 tag, and git diff 175a546b8 20c00221c — all read-only. The seat's ACCEPT and amendments were tested, not adopted; the dev's probe reports are claims I checked against the source.

How far the head moved. Two commits since 175a546b8 (6280ca3b0, 20c00221c): five files, +33/−22. Counted mechanically over the movement diff, every added and removed line begins with // or * after its indent: 0 non-comment changed lines. No code, schema, retiredKey() guidance string, .describe() text, test assertion, changeset, docs page or packages/lint logic moved, so the rule, the doors, the classifier and the changeset are byte-identical to what 6137697373 judged. Both commits carry the model-free trailer pair (Claude-Session: and Co-authored-by: Claude); no model identifier in title, body, changeset, comments or commits. Merge-base with main is dfb3ba2b2; same-repo head; draft; 486 changed lines; no governed-surface path in the file list.

① Derived judgments

  1. Carried by reference from 6137697373 ①.1–①.6 and ①.10, and 6138451028 ①.1–①.6 and ①.8 — the code they judge did not move. Re-read on this head rather than only trusted: isTombstoneIssue is invalid_type with expected === 'never'; isRetiredKeyVerdict is that, or a collapsed invalid_union with exactly one all-tombstone arm; in the dispatch loop only isRetiredKeyVerdict files severity: 'error', and the missing-binding, walker unknown-key, union-arm, strict-row unrecognized_keys and every other value verdict file warning (validate-component-props.ts:258-281, :320-447); the registry entry is tier: 'gating', commands: ALL, surfaces: CLI_ONLY (authoring-rules.ts:1189-1197); splitBySeverity partitions on severity === 'error' (:2252) and cli/src/utils/author-time-rules.ts:146-149 turns a non-empty error set into the refusal the three doors exit on. retiredKey() sites in ui/component.zod.ts on this head: 42 key tombstones plus the two retiredComponentProps rows; navigation.view and publicPicker arrive by import (view.zod.ts:2476, :3459).
  2. The FAIL's remedy, component.zod.ts:654-658 (the docblock above breadcrumb) — RIGHT and TRUE. It now reads "parsed only by the props lint (advisory in every release that shipped the default), which writes nothing back". Checked at the @objectstack/lint@17.7.0 tag (8f6e35b92): validateComponentProps is tier: 'advisory', ComponentPropsSeverity = 'warning' is a single literal, and breadcrumb is already a retiredKey() at :650 — so every release that shipped .default(true) shipped an advisory props lint; the lint returns findings and writes nothing. The residue argument stands, and the shipped docblock no longer contradicts registry.ts:10622 about the same key. It is dated exactly as its twin at retired-keys/18.ui__PageHeaderProps__breadcrumb.ts:16-19.
  3. component.zod.ts:284-303, the SDUI 组件 props 没有解析闸门:PageComponent.properties 是开放 record,ComponentPropsMap 的 29 个站点从不被 parse(#4001 批 17 的 no gate 判定) #5068 header — TRUE on the head. "was WARNING-level", "the inventory was the acceptance baseline" — past tense, right. "The page rewrites (page:card.visible → visibleWhen, showcase 的 project-detail 页在 page:tabs 上授权 items[].key,renderer 读的是 items[].value —— 两个 tab 的稳定 URL token 从未生效 #5776's tab key → value) closed the third": on this head no authored page in examples/**, platform-objects, plugin-approvals or packages/qa carries a tab item key: (0 lines across the six page:tabs sources) and every visible: hit sits on an action or view field, none inside a page:card props bag — consistent with the dev's inventory of 0. "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 took the upgrade for one class … a retiredKey() tombstone's verdict is an error at os validate / os build / os lint. Every other finding of the gate stays a warning" — read off the loop and the doors (①.1). One wording nit, not a defect: "cleared here" names component.zod.ts 的 I18nLabelSchema 键声明为纯字符串,但 3 个平台页在 31 处授权内联多语言 map #5728, whose I18nLabelSchema lives in ui/i18n.zod.ts, not this file; the bullet itself says what component.zod.ts 的 I18nLabelSchema 键声明为纯字符串,但 3 个平台页在 31 处授权内联多语言 map #5728 changed.
  4. component.zod.ts:4849-4854, 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 — TRUE, and the dev's refutation of 6138451028 ①.7's reading is RIGHT. The rows that section declares carry tombstones: object-grid defaultFilters (:5284), defaultSort (:5354), resizableColumns (:5661) and navigation.view (NavigationConfigSchema at :5535, tombstone view.zod.ts:2476); object-kanban quickAdd (:6922) and navigation (:7109). Each is invalid_type expecting never and an error on this head, so the old present-tense "findings on these entries are advisory" was false for them; the new text ("were advisory, gated on the inventory … 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 made one class an error … such as a retired key these rows carry; every other finding on them stays advisory") is right in both halves. The ruling quotation is kept verbatim: the set of 「…」 quotations in the file is byte-identical between origin/main and the head (same digest), and the line reads 「warning 层先行,error 升级仍以 inventory 为闸,本裁定不改那个闸」 on both.
  5. The three spec test comments — TRUE. page-header-breadcrumb-retirement.test.ts:19-21 and master-detail-detail-sort-field-retirement.test.ts:22-24: the props lint refuses the tombstone (:663, :8342) as an error at the three doors and the page parse never reaches it — PageComponentSchema.properties is the open z.record bag, which the tests' own safeParse success pins. element-flat-binding-retirement.test.ts:201-202: element:repeater object is retiredKey() at :4778, an error; the missing-binding warning filed beside it is a separate finding and does not make the sentence false.
  6. validate-page-field-bindings.ts:232-239 — TRUE. displayField (:3574) and searchFields (:3585) are tombstones on ElementRecordPickerPropsSchema, errors on this head with their rename/delete prescription; "it stays advisory for every other finding" matches the loop; "CLI-only" is surfaces: CLI_ONLY; "lives in a different registry (authoring-rules) from this suite" holds — validatePageFieldBindings is registered in reference-integrity-suite.ts, the props gate in authoring-rules.ts, so neither precedes nor suppresses the other. The reflow changed line breaks only; the argument's words are unchanged (read against the removed lines).
  7. The two EXCLUDED comments (page-header-breadcrumb-retirement.test.ts:356-357, master-detail-detail-sort-field-retirement.test.ts:447-448) — TRUE. The props-lint pins they name assert severity error (validate-component-props.test.ts:90, :116).
  8. My own sweep of packages/spec/src (shipped *.zod.ts docblocks included) and packages/lint/src on this head — no sentence of the class is still false. 826 raw hits of the tier and gate vocabulary, 81 after narrowing to a props-gate or tombstone subject beside a tier word, each read in context. Comments and docblocks: every props-gate hit is now dated or past-tense (the three regions above, the retired-keys comment, this PR's own lint text), a reach statement with no tier (component.zod.ts:120, :711, :3001), history (validate-component-types.ts:38 "launched advisory"), or a correct advisory on a value or unknown-key verdict — component.zod.ts:5686 (exportOptions: a bare array is invalid_type expecting an object and the retired 'pdf' is an enum refusal with its own message, view.zod.ts:2502-2515, never a never), registry.ts:14580 (a legacy record at properties.filter, z.array(ViewFilterRuleSchema, …)), walk.ts:371 (page:header description, a strict-row unrecognized_keys warning). A second sweep for other spellings of a tier claim ("non-gating", "still passes", "exit 0", "Validation passed", "warned instead") and for .describe() or guidance strings pairing a tombstone with "warning" found nothing in the class. The 16 registry.ts.template strings and the typed-member / closed-set entry strings ("Read by the component-props gate (advisory)") describe value and unknown-key verdicts, which stay warnings. The one residual is a description STRING, outside the comment-only remit and true for its subject, noted under ③.
  9. Check-runs on this head, read at 05:26Z and again at 05:33Z (46 both times). At the second read: 32 completed / success — among them Lint & Repo Gates, Build Core, Build Docs, Governed Surface Queue Guard, Check Changeset (two runs), Type Check · source gates / consumer gates / debt ledger, Spec property liveness, Check Documentation Links, Lint · docs & code guards, Lint · CI, release & engine gates (the job that runs check-issue-citations), Lint · repo & tooling gates, Lint · ESLint, Lint · PM dispatch-gates, the three Lint · scoped jobs, Flag docs affected by code changes, Dogfood Regression Gate (gate job), filter, and the four card and claim parity checks (two runs each); 7 completed / skipped — Console Pin Gate, the Dogfood Regression Gate matrix shard, Dogfood Verify CLI, Temporal Conformance (live PG + MySQL), Packed-tarball smoke (opt-in), and one skipped duplicate each of Auto Label and Check PR Size; 7 in_progress — Test Core 1/6 through 6/6 and Type Check · workspace. (At the first read Lint & Repo Gates was still queued and Build Core, Build Docs and two Type Check slices in progress; all five concluded success by the second.) Judged on the diff, not on the pending runs: the landing waits for Test Core and Type Check · workspace to conclude green.

② Semver level

Carried by reference from 6137697373 ② and 6138451028 ② — the changeset did not move (no .changeset path in the movement diff). Re-confirmed on this head: .changeset/23026-retired-component-prop-refused.md is @objectstack/lint: major, carries Clause-②: no (narrowing) and exactly one adr-0087: not-required (already-registered …) marker naming 20 ids with the **BREAKING** token; .changeset/pre.json is mode: pre, tag: next; @objectstack/lint is published (17.7.0, not private); both Check Changeset runs are green. PR body line 2 carries the claim's Clause-② line verbatim. (narrowing) is BREAKING, so major in pre mode is the only right level, and the body's "What is refused" / "What stays a warning" / "Not judged here" match the loop, surfaces: CLI_ONLY and the open bag. The two moved commits publish no behaviour: component.zod.ts does ship as source text (spec's files carries src/**/*.zod.ts), but the edit is docblock text only — no export, schema, guidance string or .describe() changed, so the JSON Schema and content/docs/references/** are untouched and no @objectstack/spec line is owed; the other four files are tests and lint source comments. skip-changeset does not apply to a diff that publishes a lint major.

③ Boundary flags

Why PASS. The sentence the last record failed on is dated and true; every sentence the two comment-only commits edited is true against the code on this head, the ruling quotation is byte-identical to main, the movement diff carries zero non-comment lines, my own sweep of both source trees (shipped *.zod.ts docblocks included) finds no comment or docblock of the class still false, and ①.1–①.6, ①.8–①.9, ② and ③ of the two earlier records hold unchanged. The landing still waits for Test Core 1–6/6 and Type Check · workspace to conclude green, which is the owning seat's read, not this record's.

Implemented-by: claude/issue-23026-retired-prop-error
Reviewed-by: session_012uFegdfR6K6uqvLzt7XsdZ

VERDICT: PASS

This branch has not been deployed

No deployments
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/m tests tooling

Projects

None yet

2 participants