Repository navigation
feat(spec)!: retire a page header's breadcrumb switch — no renderer draws a trail for it (#20758) - #20785
Conversation
…on, registry entries, pins Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1 Co-authored-by: Claude <noreply@anthropic.com>
…ge-header-breadcrumb-retired
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 137 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 803ed6b7e33fd6a4759ba6b9fc8c5f95b1c23cce && git checkout 803ed6b7e33fd6a4759ba6b9fc8c5f95b1c23cce
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 4b4ee88fbc530a155ce3ea0570ad81d905d3afbb 70e0f052ff1369a627db2ca218ce9bd6154435f5 && git checkout -B drift-repro 4b4ee88fbc530a155ce3ea0570ad81d905d3afbb && git merge --no-ff 70e0f052ff1369a627db2ca218ce9bd6154435f5
node scripts/docs-audit/affected-docs.mjs --json 4b4ee88fbc530a155ce3ea0570ad81d905d3afbb
|
…d page-header breadcrumb Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1 Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Inputs read: card #20758 (body and its 4 comments: claim ① Derived judgmentsAccept-set and public-surface changes the diff implies, each judged:
Author-shown and AI-facing sentences tested against the tree, with the moment each is true:
② Semver level
③ Boundary flagsRound 1 (
Round 2 ( Card and claim: the claim's file surface named Check-runs on Implemented-by: VERDICT: PASS Adopted and posted by
Generated by Claude Code |
维护者速读 · 2026-09-30T13:46Z
改了什么
为什么改 风险与代价(含回滚)
席位意见 你要做的 Generated by Claude Code |
…figSchema (objectstack-ai#20856) Fixes objectstack-ai#20831 Clause-②: yes ## What this changes `ComponentPropsMap['object-grid'].grouping` and `ComponentPropsMap['object-kanban'].grouping` were `z.unknown().optional()`. Both now take the list view's own `GroupingConfigSchema` **by reference**, imported from `./view.zod` beside the six schemas `component.zod.ts` already takes from there. There is one schema, not a copy of its shape, and each row keeps its describe. A padded grouping field name, a number, a bare string, an empty `fields` list or an undeclared key inside the config is now refused on these two doors exactly as `list-view` refuses it. The prop is typed, not retired. Both renderers are live readers of this exact shape. Readings are at objectui `db11afd49`, the pin in `.objectui-sha`: - **Grid**: `ObjectGridSchema.grouping` is typed as the spec's `GroupingConfig` (`packages/types/src/objectql.ts:1123`). `ObjectGrid.tsx` reads `schema.grouping?.fields` (`:2050`, `:2839`), and `useGroupedData.ts` reads `.field`, `.order` and `.collapsed` per entry (`:281`, `:443-547`). - **Kanban**: `effectiveSwimlaneField = schema.swimlaneField || schema.grouping?.fields?.[0]?.field` (`ObjectKanban.tsx:1212`). - A read-point grep of every `grouping.` member across objectui `packages/*/src` (tests excluded) at the pin finds `fields` and nothing else. ## Files (7, +338 / -7 against `origin/main` `688ddef3c`) | File | Why | | --- | --- | | `packages/spec/src/ui/component.zod.ts` | the import, the two members, and a comment on each | | `packages/spec/src/ui/component-object-block-grouping.pin.test.ts` | the pins (below) | | `packages/spec/src/migrations/entries/semantic/18.ui-object-block-grouping-config-typed.ts` + regenerated `registry.ts` | the ADR-0087 disposition (below) | | `content/docs/references/ui/component.mdx` | regenerated by `gen:docs`: `grouping` now shows its shape on both rows | | `packages/spec/dropped-refinements.baseline.json` | **outside the claim's file surface; see below** | | `.changeset/20831-object-block-grouping-typed.md` | `minor`, BREAKING banner, migration table, `Clause-②: yes (narrowing)` and the registered ADR-0087 marker | **File-surface breach, declared.** The spec build refuses the schema change until `dropped-refinements.baseline.json` records two new sites, `grouping.fields.element.field` on `ui/ObjectGridProps` and on `ui/ObjectKanbanProps`, and moves `droppedRefinementSites` 610 to 612. The build prints the corrected entry itself. The cause is the non-padded field-name `superRefine` that `GroupingFieldSchema` carries. JSON Schema cannot express it, and the ledger already records it at every list-view grouping site (`ui/GroupingConfig`, `ui/GroupingField`, `ui/ListView`, the manifest views). The ledger is hand-edited by design, so a new site is a reviewed line. The alternatives are both out: deleting the refinement is forbidden by the ledger's own rule, and teaching the projection list a new named pattern is a public-contract decision of its own. The ledger's `refinementSitesThatDidProject` (369) is left alone. This build prints 421, but none of the grouping refinements is one of the five projected patterns, so this diff adds zero to it. ## Pins The pins are in `component-object-block-grouping.pin.test.ts`: 22 cases, each asserting issue `code` and `path`, on both rows. - **Refused:** - padded name: `custom` at `grouping.fields.0.field`, and the message names the received value and the unpadded spelling; - `grouping: 42`: `invalid_type` at `grouping`. There is no `fields[0]` for a number to fail at, so the triage's `grouping.fields[0].field` path applies to the padded case only; - bare string: `invalid_type` at `grouping`; - `{ fields: [] }`: `too_small` at `grouping.fields`; - an undeclared key: `unrecognized_keys` at `grouping`. - **Controls:** - a fully spelled grouping parses byte-identical (`toStrictEqual` on the input); - the short form `{ field }` parses clean and gains the list view's defaults (`order: 'asc'`, `collapsed: false`) on the parse output. The component-props gate discards that output and the envelope audit walks raw keys only, so nothing downstream sees them; - an absent `grouping` stays absent. - **One schema:** the member unwraps to `GroupingConfigSchema._zod.def` by identity, and answers what `GroupingConfigSchema` answers on the whole corpus, one path level down. **Ablation**, run at `69a1be23f` (the `component.zod.ts` blob `39065a62` is unchanged at HEAD): both members were reverted to `z.unknown()` through `scripts/ablation-replace.mjs` (anchor 2 to 0, `--expect 2`, trap restore). Result: **18 failed / 4 passed**. The 4 that stay green are the byte-identical and absent controls, which `z.unknown()` also passes. The restore was proven by blob equals HEAD and an empty `git diff HEAD`. No `dist/` was involved: the test imports `./component.zod` from source. ## ADR-0087 disposition: `registered ui-object-block-grouping-config-typed` A new semantic entry for protocol 18 carries the rewrite table: - padded name: write it unpadded; - bare string: wrap it as `{ fields: [{ field }] }`; - empty list, number or other value: delete the key; - on `object-kanban`, `swimlaneField` still wins over `grouping`. It is deliberately not a D2 conversion: a padded name is refused with guidance, never trimmed. The entry states what happens at rest, and it was measured: - `PageComponentSchema` does not parse `properties` against `ComponentPropsMap`. - The component-props rule is `tier: 'advisory'`, `surfaces: CLI_ONLY` (`packages/lint/src/authoring-rules.ts:1022`). - So a stored page keeps loading and rendering. The refusal reaches authors as `component-props-invalid` on `os validate` / `os build` / `os lint`. - Measured through `validateComponentProps` on the rebuilt spec: a padded kanban grouping reports at `…properties.grouping.fields.0.field`, and `42` on a grid at `…properties.grouping`. The valid control reports nothing. `node scripts/check-adr-0087-registration.mjs --base origin/main` answers exit 0, reporting `[BREAKING+clause-②-narrowing] registered ui-object-block-grouping-config-typed (new here …)`. ## Dispatch measurements 1. Both members read `z.unknown()` at base `1741c5dcb` (`:3596`, `:3993`). 2. No import cycle: `view.zod.ts` imports nothing from `component.zod.ts`, and `component.zod.ts` already takes six lazy schemas from it. The identity pin is green under the lazy proxy. 3. The grid reader consumes the `GroupingConfigSchema` shape (readings above), so both rows are typed. There is no `needs_decision` fork. 4. The ADR-0087 marker is `registered`, judged by the gate (above). 5. Consumer radius, all measured at the merge base `688ddef3c`: - `examples/**` has 2 `object-grid` nodes (`app-showcase` `command-center.page.ts:172`, `my-work.page.ts:62`). Neither authors `grouping`, and there are 0 `object-kanban` nodes. - No fixture under `packages/**` authors `grouping` on either row. - The only non-spec package that parses these rows is `@objectstack/lint` (`validate-component-props.ts`, `page-envelope-audit.ts`), downstream of spec (the `...@objectstack/spec` direction). Its suite is green (below). - Full-repo pin sweep: no test anywhere asserts the old `z.unknown()` acceptance on these rows, and the padded-name refusal text is unchanged, so no pin flips. ## Verification, at HEAD `47c76064d` (after merging `origin/main` through `os-regen-merge.sh`; no conflicts) - `pnpm --filter @objectstack/spec build`: exit 0; 612 dropped sites across 212 schemas, all declared. - `pnpm --filter @objectstack/spec check:generated --fix`: "All 15 generated artifacts are up to date", measured against a dist built by that run. authorable-surface, authorable-defaults and api-surface are unchanged. - `pnpm --filter @objectstack/spec typecheck`: exit 0. The test layer compiles with debt unchanged (53 files / 251 errors / 138 signatures). - Spec suite (`vitest run --project local --maxWorkers=2`): **579 files, 17088 passed**, 1 todo. - Consumer `@objectstack/lint` suite: **117 files, 5436 passed**, against a spec dist rebuilt from this tree. - `node scripts/pm/dispatch-gates.mjs --commands` (no paths): 112 commands. `--ran` reconciles **112 derived, 110 run (all exit 0), 2 NOT MEASURED, 0 UNRUN**. - Five gates first exited 3 on missing builds: `check:doc-formula-expressions`, `check:doc-security-posture`, `check:skill-examples`, `check:docs-transcript-drift`, `check:lean-entry-closure`. Each was rerun green after building lint, client, client-react and objectql. - `check-changeset-no-major` with this body as the event: LEVEL AXIS reads `Clause-②: yes`, and no package is graded `patch`. - **Lint narrowing** (the repo-wide `pnpm lint` is CI's): - eslint `--no-inline-config --format json` over the 4 changed TS files reports 4 files, 0 errors, 0 warnings; - `eslint.config.mjs` puts every `packages/**/*.ts` in the population, and the other 3 changed files are md, mdx and json, outside its globs; - the config never enables type-aware linting (no `parserOptions.project`), so this diff cannot move a verdict on an untouched file. **NOT MEASURED locally:** - `check:dual-build-cjs-loads` and `check:type-check-debt`, reason: both need every workspace package built (84 have no `dist/`). - The CLI `integration` tier, reason: declared for CI; this diff touches no CLI file. Its `migrate-meta-engine-guidance.test.ts` prints every semantic entry. I checked the new entry against its tracker-id regex (no hit), but not its verbatim print. - The four CI type-check lanes and the six path-scheduled CI jobs, reason: CI's own runs. ## Acceptance notes These were noted, not filed. Carrier: none. - The protocol-18 entry `ui-object-grid-page-size-positive-integer-refused` says a stored page "is refused on its next authoring-path save". Its sibling `object-grid-default-filters-rule-array`, and the rule's `surfaces: CLI_ONLY`, say the refusal is an advisory CLI finding only. This is a read-only reading: I did not exercise a save. - The `component.zod.ts` section header still lists "filter shapes … grouping configs" as the `z.unknown()` value posture. Both have since been typed. This is historical text, left as it stands. - `dropped-refinements.baseline.json` `measured.refinementSitesThatDidProject` reads 369 while the build prints 421. The gap predates this diff and is ungated. - For the objectui mirror card (the seat files it): the `object-kanban` registration description (`plugin-kanban/src/index.tsx:684`) and `structuredKeysAreDeclaredAndHonoured-8313.test.ts` both describe the spec row as `z.unknown()`. Its row-4 fixture `{ fields: [{ field: 'owner' }] }` stays valid under the typed row. Concurrent work: objectstack-ai#20785 edits `component.zod.ts` only at `PageHeaderProps` and one `component.mdx` line in that section, disjoint from these regions; this diff touches neither `authorable-surface/ui.json` nor `authorable-defaults/ui.json`. objectstack-ai#20694 is not addressed here; it runs after this lands. --- _Generated by [Claude Code](https://claude.ai/code/session_018fxqvRJW12TaHC7DUQ89Y6)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Closes #20758
Clause-②: no (narrowing)
PageHeaderProps.breadcrumbretires through the ADR-0087 route, the wayiconleft the same row. It has aretiredKey()tombstone, the D2 conversionpage-header-breadcrumb-removed(it stripstrueandfalsefrom stored and built pages with a notice, retired from the load path), theRETIRED_KEYS_BY_MAJOR[18]entry and the D3 entrypage-header-breadcrumb-retired. A stored page is never refused for the key: the props lint reports it as a warning.nav:breadcrumbstays, because objectui's Studio page palette writes it.The published skill
skills/objectstack-ui/rules/pages.mdstops teaching the key (two lines). That path is governed, so this PR lands Tier H, after an authorized approval.@objectstack/specminor, BREAKING.🤖 Generated with Claude Code
https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1