Uh oh!
There was an error while loading. Please reload this page.
feat(types)!: retire the block schema family (#4895) - #7432
Conversation
ADR-0049 enforce-or-remove, maintainer ruling 2026-09-02 (director seat, summon #8, verbatim 「同意」) — option C1, retire the family in one change, no transition window. Gone from `@object-ui/types`, both subpaths that carried them: `.` BlockSchema, BlockSlot, BlockLibrarySchema, BlockEditorSchema, BlockInstanceSchema, BlockVariable, BlockMetadata, BlockLibraryItem, ComponentSchema `./zod` the nine mirrors above plus BlockComponentSchema — which was also AnyComponentSchema's block arm The zod half is the one that mattered. On 17.6.0 the shipped `AnyComponentSchema.safeParse({ type: 'block-library' })` returned success for a node no page can render, so an author who copied the documented shape was told green by the validator and then got OBJUI-001. All five discriminants are now refused, pinned in `phase2-schemas.test.ts` beside the theme refusals retired the same way. `blocks.ts` and `zod/blocks.zod.ts` are kept as ADR-0049 tombstones exporting nothing; `block-family-retired-4895.test.ts` pins every retired name out of them. `content/docs/blocks/block-schema.mdx` is deleted with the family and its DOC_TYPE_EXEMPTIONS entries go with it — `check:doc-types` reports `stale-exemption` otherwise. objectui#7023 dissolves into this retirement. The live slotted record-page vocabulary (`kind: 'slotted'`, `page.slots`, `usePageAssignment`, `PageBlockCanvas`, `PageBlockInspector`) shares the words "block" and "slot" with the retired family and shares no declaration, type or file with it. It is not touched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
os-project-manager
commented
Sep 3, 2026
Handoff to the project director seat — ⛔ NOT reviewed or accepted by this seatPM note from the Clause ② is engaged (published-surface removal from Verified independently by this seat, from |
| claim | this seat's reading |
|---|---|
layout.ts holds zero BlockSlot references | 0 — with the control PageSlotMap = 2 on the same file and query, so the zero is a reading |
the two slots?: keys are different declarations | blocks.ts:186 slots?: BlockSlot[] vs layout.ts:802 slots?: PageSlotMap |
the live navigation component kind is elsewhere | zod/app.zod.ts:68, componentRef — a different module |
blocks.zod.ts exported exactly ten values | 10, the last two being ComponentSchema and BlockComponentSchema |
⭐ That last row makes the PR's sharpest argument sound, and it is worth the reviewer's attention. The ruling's TypeScript list names five symbols and does not name ComponentSchema — but the same ruling says "the tenzod-mirror-parity ledger entries". There are exactly ten exports, and ComponentSchema is one of them. So the ruling's own arithmetic settles that it is in scope: nine plus one, or the "ten" does not close. That is a deletion justified by the ruling rather than beyond it.
For the review, stated as observations rather than a tier verdict
- The PR reports the ruling's enumeration incomplete in three places and declares each rather than widening silently: three support types plus
ComponentSchemain the TypeScript list;DOC_TYPE_EXEMPTIONSentries on two pages, not one (check:doc-typesfound the second pair itself, printingstale-exemption); andComponentSchema's in-repo readers. - Every edit outside the enumeration is presented as a forced consequence of an enumerated deletion. The one that most deserves a look is
empty.tsxre-annotating toBaseSchemarather thanSchemaNode, which preserves objectui#7082's recorded reasoning instead of working around it. - ⛔ A historical audit document was deliberately not edited, on the grounds that changing a dated snapshot to match today's tree would falsify it. That is the right call.
- The single red is diagnosed as environmental, with proof rather than assertion:
check-sdui-registration-pinsreads the filesystem, and this worktree is built because the eager-closure measurement required it; withpackages/app-shell/distmoved aside it passes 11/11, and the diff touches zero files in that package.
⚠️ One expectation this seat set was wrong, and the correction matters beyond this PR
The dispatch said the expected eager-closure effect was negative, since this removes code from packages/types — a member of the framework chunk group. Measured, the delta is exactly zero, byte-identical down to the entry chunk's content hash. The reason is structural: AnyComponentSchema appears in 0 of the console's dist chunks — the published zod validator surface is not bundled into the console at all — with controls on the same grep (slotted in 5 chunks, object-grid in 9) proving the zero is a reading.
⇒ This retirement banks nothing for the two PRs parked on the 177-byte framework headroom (#7399). Recording it here because this seat had raised the opposite hypothesis; it is retired by measurement, and ⛔ nothing is being posted to #7399 on the strength of it.
Generated by Claude Code
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
os-project-manager
commented
Sep 3, 2026
|
| PASS recorded | gate label | draft | enqueued | |
|---|---|---|---|---|
| #7436 (#7415) | ✅ 5520521996 | cleared | ready | yes — merged |
| #7430 (#7143) | ✅ 5520531438 | cleared | ready | yes — merged |
| #7432 (#4895) | ❌ none found | cleared | still draft | no |
This PR's updated_at (04:46:53) falls inside the same minute those two were being cleared (04:45:56 and 04:46:36), so the likeliest explanation is that this PR was touched in the same stroke and the disposition is partially applied rather than that anything went wrong. But I cannot read another seat's session, so I am reporting the state, not diagnosing it.
One hypothesis I tested and had to discard
I suspected the auto-labeler, since the surviving labels are all path-shaped. It is not the cause, on the evidence:.github/workflows/labeler.yml does run actions/labeler@v7 with sync-labels: true, but needs:contract-review occurs 0 times anywhere in .github/ — it is not in .github/labeler.yml, and that action does not remove labels its config does not manage. Recording the negative so nobody re-runs the same theory.
⭐ What that control incidentally established, which outlives this PR
needs:contract-review is named nowhere in .github/ — no workflow, no gate, no check reads it. It is a pure convention marker: it routes work between seats and has no mechanical enforcement, so removing it produces no failing check and no alarm anywhere. That is a property of the mechanism worth knowing before relying on it as a gate.
What I am doing
Nothing to the PR. It is draft and not enqueued, so it cannot merge and the gate's actual function — preventing an unreviewed Clause-② landing — is currently served by the draft state regardless of the label.
⇒ If this PR is still unreviewed at my next patrol, or if it moves out of draft with no PASS recorded, I will re-apply needs:contract-review (restoring a gate, never lowering one) and say so here. Until then this is the director seat's to complete or to correct.
Unchanged from my handoff: Clause ② is engaged (published-surface removal from @object-ui/types), this seat is not at CONTRACT_REVIEW_TIER, and ⛔ it will not self-review, enqueue or auto-merge this PR.
Generated by Claude Code
os-project-manager
commented
Sep 3, 2026
|
| reading | |
|---|---|
| PR state | still draft |
| labels before this write | documentation, data-adapter, package: types, package: components, tests — no gate label |
| contract-review PASS | none found, on this PR or on #4895 |
| my own handoff note, 02:54 | recorded the gate label as present at that time |
⇒ The label went from present to absent with no review behind it, and it has stayed absent. Restored. ⛔ Nothing else about this PR was touched: still draft, no auto-merge, not enqueued, not reviewed by this seat.
Why this is the safe direction, and what would reverse it
Re-applying a gate cannot weaken one. If the director seat cleared this label deliberately as part of a review it has not yet recorded, this write costs one label edit and the PASS comment supersedes it — say so and I will not re-apply again. What the alternative risks is worse: a Clause-② retirement of a published family carrying no marker that it needs tier review, indistinguishable from one that never needed it.
One negative worth keeping, so it is not re-investigated
I suspected the auto-labeler and it is not the cause: .github/workflows/labeler.yml runs actions/labeler@v7 with sync-labels: true, but needs:contract-review appears 0 times anywhere in .github/, so it is not in that action's config and the action does not remove labels it does not manage. Corroborated since: PR #7447's label job ran at 05:29:13, I applied the same gate label there at ~05:33, and it was still present at 06:13.
⭐ And the control that turned up while checking: nothing in CI reads needs:contract-review at all. It is a pure convention marker with no mechanical enforcement, so its removal raises no failing check anywhere — which is exactly why the before-reading above had to come from a comment I had written at the time rather than from any gate.
This PR still blocks #7344, whose population drops 12 → 8 once it lands.
Generated by Claude Code
os-project-manager
commented
Sep 3, 2026
⛔ I was wrong — the contract review PASS exists. |
Fixes#4895
Retires the block schema family whole, under the maintainer ruling recorded on the card in comment 5510092001 (director seat, summon #8, 2026-09-02, maintainer verbatim 「同意」) — option C1, one change, no transition window (创业阶段不渐进). ⛔ Not against the issue body, which predates the ruling and still frames this as an open direction question.
objectui#7023 — the narrower "drop the block arm from the validator" fix — dissolves into this retirement and should close as superseded when this lands.
This removes names from
@object-ui/typesat 17.6.0, so the PR carriesneeds:contract-reviewand is draft. The implementing seat has not self-reviewed, has not enabled auto-merge, and has not enqueued it. The project director seat reviews before it lands.The family separation — the boundary the ruling makes the implementer keep
Two unrelated things in this repo are called "block" and "slot". The retired family is measured disjoint from the live slotted record-page family, by file and by declaration:
slotskeypackages/types/src/blocks.ts:186—slots?: BlockSlot[]packages/types/src/layout.ts:802—slots?: PageSlotMaplayout.ts:775—kind?: 'full' | 'slotted' | …usePageAssignment,PageBlockCanvas,PageBlockInspector(@object-ui/app-shell)Two different declarations, two different types, two different files.
layout.tscontains zero references toBlockSlot(measured:git grep -c -w BlockSlot -- packages/types/src/layout.ts= 0), and this PR does not touchlayout.tsat all.File-level disjointness, with a control. Intersecting the 41 files that spell the slotted-record-page family (
PageBlockCanvas|PageBlockInspector|usePageAssignment|kind: 'slotted'|"slotted") against every file that spells a retiredBlock*symbol gives the empty set. The positive control on the same operator — intersecting the same 41 against files spellingPageBlockInspector— returns 22, so the empty intersection is a reading, not a broken pipeline. PR #5937 drew this line first; it is unmoved.A second look-alike, which the ruling's own enumeration does not separate and which cost measurement to get right: the
type: 'component'navigation item kind ({ type: 'component', componentRef: 'ns:name' }) is declared byNavigationItemSchemainzod/app.zod.ts(objectui#2918) — a different declaration in a different module. It is untouched, andnavigation-model.test.tsstill pins it live. What went is the block family's ownComponentSchemanode kind (componentName/props/children), the fifth arm ofBlockComponentSchema.The four acceptance pins
1.
{type: 'block-library'}is rejected by the shippedAnyComponentSchema. Measured by executing the published artifact (packages/types/dist/zod/index.zod.js, freshly built) — not by reading the source:The negative control is what makes the row above it mean something: because an unknown discriminant was already refused,
block-libraryparsing OK on 17.6.0 was a union arm claiming it, not a permissive union waving everything through. Pinned inphase2-schemas.test.ts(Phase 2: Block component kinds — retirement pins), beside the theme refusals retired the same way.2. No
Block*symbol is exported on any published subpath. Measured with this repo's own instrument —readExportSurfaces()fromscripts/check-readme-exports.mjs, which reads the TypeScript checker'sgetExportsOfModule— over all 12 subpaths in the package'sexportsmap, against a freshly builtdist:Block*/ComponentSchemaon../zodTableSchemafound3. The
zod-mirror-parityledger is clean. All tenblocks.zod.ts#…entries are gone — nine paired (BlockEditorSchema,BlockInstanceSchema,BlockLibraryItemSchema,BlockLibrarySchema,BlockMetadataSchema,BlockSchema,BlockSlotSchema,BlockVariableSchema,ComponentSchema) plus the unpairedBlockComponentSchemain the union-exclusions ledger.git grep -c 'blocks\.zod\.ts#' packages/types/src/__tests__/zod-mirror-parity.test.ts= 0. Nothing else is disturbed: the file's census assertion (MIRRORS ∪ EXCLUSIONS must account for every exported const inzod/) still passes, and the wholepackages/typessuite is green at 93 files / 1541 tests.4.
check:doc-typesis green with the exemptions removed. Its own printed verdict:The gate did its job mid-implementation: after the page was deleted it printed two
stale-exemptionfindings, and afterschema-overview.md's block tour was removed it printed two more, each naming the entry that had outlived its site. All were deleted rather than re-pointed — there is no page left to point at.Bytes — measured, and the answer is zero, not negative
check:eager-closureon a real console build (pnpm turbo run build --filter='./packages/*'thenpnpm --filter @object-ui/console build), before and after:frameworkgzippedvendor-objectstackui-componentsByte-identical, down to the entry chunk's content hash (
assets/index-DUp49asL.jsboth times) and the exact set of eager chunk filenames.Why, with the control that proves it is a reading and not a stale report.
AnyComponentSchemais spelled in 0 of the console's dist chunks — the published zod validator surface is not bundled into the console at all — so removing an arm from it cannot move a console byte. Controls on the same grep over the same directory:slottedappears in 5 chunks,object-gridin 9. So the grep works and the zero is real.⇒ This removal banks nothing for the two PRs parked on the 177-byte
frameworkheadroom (#7399). The dispatch expected a negative delta; the measurement says zero, and the reason is structural rather than incidental. No ceiling is raised, lowered or re-pinned in either direction.What the ruling enumerated, and the three places its enumeration was incomplete
zod-mirror-parityledger entries") settles the first of these; the other two are consequences the enumeration could not have named without reading the tree.ComponentSchemaare not in the ruling's TypeScript list, but must go with it. The list names five (BlockSchema,BlockSlot,BlockLibrarySchema,BlockEditorSchema,BlockInstanceSchema);blocks.tsalso declaredBlockVariable,BlockMetadata,BlockLibraryItem(each with no reader outside the five, andBlockLibraryItem.schemais typedBlockSchema, so it cannot survive them) andComponentSchema. ThatComponentSchemais included is pinned by the ruling's own count:blocks.zod.tsexported exactly ten values, the ledger carried exactly ten entries, and one of them isblocks.zod.ts#ComponentSchema. Nine + one, or the "ten" does not close.DOC_TYPE_EXEMPTIONSentries pointing at this family live on two pages, not one. The ruling names thecontent/docs/blocks/block-schema.mdxgroup (five entries plus theslotnote).content/docs/guide/schema-overview.mdcarried two more —block(theconst block: BlockSchematour) andstring(theBlockVariable.typeinside itsvariables[]). Both are removed;check:doc-typesfound them.ComponentSchemahad in-repo readers the enumeration does not mention. Enumerated and repaired below rather than left dangling.Forced consequences, each one declared
Every edit outside the enumeration exists because an enumerated deletion made an existing reference false. None widens the deletion.
packages/components/src/renderers/feedback/empty.tsx— annotated itsactionchild asComponentSchema. NowBaseSchema. ⭐ Deliberately notSchemaNode: objectui#7082 recorded why (the renderer'stypeof === 'object'guard rejects thestring | number | booleanmembersSchemaNodealso admits), andBaseSchemais exactly that object half, so that reasoning is preserved rather than worked around.packages/types/src/__tests__/overlay-node-slot-doc-types-7082.test.ts— its two type-level assertions pinned the premise of the finding(docs): six component pages publishtrigger/content/actionasComponentSchema, a different shipped type than theSchemaNodethose keys actually declare #7082 correction (ComponentSchemais a real export and is NOT a node slot). The subject is gone, so the comparison is no longer expressible; the note in its place records what stood there and why, the same shape the theme retirement used. Its source-reading assertion is inverted (blocks.tsmust NOT contain the interface, and must contain the tombstone) with a control that the file is still readable. The seven corrected rows and the finding(types):DropdownMenuSchema.triggerdeclares a singleSchemaNodewhile its zod mirror, its siblingContextMenuSchema, and its own shippeddefaultPropsall use the array form #7081 boundary pins are untouched.scripts/check-doc-snippet-types.mjs—CONTROL_REAL_EXPORTwas'ComponentSchema', the harness's positive control. It now namesBaseSchema. This is how the reader below was found: the gate exited 2 ("HARNESS CONTROL FAILED — no verdict about the documents can be read from this run") rather than reporting a document failure. It now printspositive importing 'BaseSchema' produced 0 diagnostic(s)and407 of 407 block(s) judged, 0 failed.content/docs/guide/architecture.md,content/docs/guide/troubleshooting.md(both the type and the./zodvalidator import;BaseSchemaexists on both subpaths, so the snippet's point about the PascalCase collision survives intact),packages/data-objectstack/README.md.content/docs/components/feedback/empty.mdx,content/docs/components/overlay/alert-dialog.mdx(bothBaseSchema, per the finding(docs): six component pages publishtrigger/content/actionasComponentSchema, a different shipped type than theSchemaNodethose keys actually declare #7082 reasoning above),content/docs/guide/layout.md(SchemaNode, matching what finding(docs): six component pages publishtrigger/content/actionasComponentSchema, a different shipped type than theSchemaNodethose keys actually declare #7082 used for exactly these node slots).content/docs/guide/schema-overview.md— the Block Schema tour, its/docs/blocks/block-schemalink, theBlockSchemarows in both import examples, the Quick Comparison row, and the "Reusable Components" capability bullet.content/docs/blocks/meta.json— the deleted page's nav entry (one-line diff).content/docs/guide/ci-cd-pipeline.mdand thecheck-doc-component-types.mjsheader — both usedblocks/block-schema.mdxas the worked example of "a page that mixes vocabularies, which is why whole-file exemptions are not offered". Re-pointed atapi/schema-reference.md, measured on this tree to carry"type": "action"(an exempted ActionSchema discriminant) alongside"type": "card"/"type": "table"(registered component keys).AGENTS.md— its topology table gaveComponentSchemaas an example of a@object-ui/typesinterface. NowBaseSchema. One word, in an example list; flagged here because AGENTS.md is binding and should not change unremarked.⛔ Not touched:
docs/audits/2026-08-zod-to-json-schema-fidelity.mdlists the retired names inside a blockquote that is a dated snapshot of an audit run. Editing a historical record to match today's tree would falsify it.Tombstones and the retirement pin
Per ADR-0049, following this repo's existing convention (
packages/types/src/zod/theme.zod.ts, the theme retirement):packages/types/src/blocks.tsandpackages/types/src/zod/blocks.zod.tsare kept as modules exporting nothing, each carrying the ruling, the evidence, the boundary against the slotted record-page family, and — inblocks.ts— the note separating the retiredComponentSchemanode kind from the livetype: 'component'navigation item kind.packages/types/src/__tests__/block-family-retired-4895.test.tsmakes those tombstones executable: it dynamic-imports both and pins all nineteen retired names out of them and out of the./zodbarrel, with a positive control (TableSchemaandAnyComponentSchemamust still be exported) so a failed import cannot read as a successful retirement.The pin is not vacuous — ablated, from the committed state. Re-adding one retired validator to the tombstone (
export const BlockLibrarySchema = null;) turns it RED with its own message. Mutation confirmed on disk before the run (injected-text grep = 1 hit; blob hash moved off the HEAD blob), and the restore leg confirmed after (git hash-objectmatches the HEAD blob exactly;git diff HEADclean). Both legs read source through vitest, notdist, so no rebuild is involved on either side.Verification — every verdict quoted from the gate's own printed line, exit code captured before any pipe
Run at final commit
8ff07b235.pnpm --filter @object-ui/types build✓ dist completeness: 1 package(s) complete (118 emitted files verified)vitest run packages/types/Test Files 93 passed (93)·Tests 1541 passed (1541)vitest run packages/components/ scripts/322 passed (323)/4853 passed (4854); the one failure is environmental, not this diff — see belowtype-check(types, components, data-objectstack)Done(types'type-checkincludestsc -p tsconfig.test.json, so the edited test files are covered; it is what caught theoverlay-node-slot-doc-types-7082reader)pnpm --filter @object-ui/types --filter @object-ui/components run lintempty.tsx's threeno-explicit-anywarnings are the same three sites as onmain, unmoved)check:doc-types✅ Every documented component type is registered.check:doc-snippetsEvery covered documentation snippet compiles against the built types.· all four harness controls passcheck:doc-fences✅ check:doc-fences — every TypeScript block in 224 document(s) …check:control-bytes✅ check-control-bytes: OK (scanned 6115 tracked text file(s); skipped 85 binary).check:readme-exports✅ check-readme-exports: OK (…3300 export symbol(s) read from 37 of 40 tracked package(s)…)check:spec-symbols✅ spec symbol derivation: 1342 files scanned against 4959 spec export namescheck:self-import✅ No package names itself inside its own src/.check:side-effects-arraycheck:esm-specifierscheck:eager-closure✅ Console eager closure is 3178.3 KB gzipped across 48 of 516 chunks (budget: 3191.4 KB, headroom: 13.1 KB).Gate scripts edited ⇒ their own suites run, separately from the derived family:
scripts/__tests__/check-doc-component-types.test.ts,check-doc-snippet-types.test.ts,check-doc-fence-languages.test.ts— exit 0,3 passed (3)/127 passed (127).The one red, diagnosed rather than waved through.
scripts/__tests__/check-sdui-registration-pins.test.tsexpectspackages/app-shell/src/console/connect/ConnectAgentWidget.tsxand got thedist/…jsspelling. That gate reads the filesystem (fs.existsSync),packages/app-shell/distis git-ignored, and this worktree is built because the eager-closure measurement required it. Proven, not assumed: withpackages/app-shell/distmoved aside (restored immediately after, verified present), that file passes 11/11.packages/app-shellcarries zero modifications in this diff.Local scope, declared. The repo-wide farm is CI's run, not one this seat owes. Locally: the dependency closure built first; the full suites of both packages whose
src/changed, plusscripts/; both packages' whole-package lint (not a narrowing to changed files); and the gate family derived by hand from this repo's ownpackage.jsonand.github/workflows/—scripts/pm/dispatch-gates.mjslives inobjectstackand answers only about that tree, so it was not used here.🤖 Generated with Claude Code
https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
Generated by Claude Code