Skip to content

fix(types,runner): the retired-key pins and the runner sidebar compile against both the pinned spec and objectstack main (objectui#11330) - #11343

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-11330-retired-key-brand-dual-compile
Oct 1, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-11330-retired-key-brand-dual-compile

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #11330

Clause-②: no

What was red

The required Spec Main Shape Gate compiles objectui against @objectstack/spec built from objectstack main. objectstack d830d71f (objectstack#21023) changed the TypeScript type a retiredKey() tombstone gives a key: bare undefined in the pinned 17.5.0, and on main an inline, anonymous mark under the optional wrapper, { '[REMOVED] Key retired: run os validate for its migration.': never } or undefined. Five objectui sites were written against the undefined spelling:

  • four type pins in packages/types/src/__tests__/ asserted [X] extends [undefined] or Equal of X and undefined on a retired key;
  • LayoutRenderer handed the retired app version to a ReactNode slot (the sidebar's v{app.version} footer).

The pin, the manifests and pnpm-lock.yaml are untouched (objectui#9860 option C), and so are scripts/spec-main-shape-gate.mjs and its workflow.

Both compiles, reproduced with the gate's own flow

objectstack main at b3d7a70864a7 (newer than the card's 5dbeb7d7b7e1) was checked out in a separate worktree, @objectstack/spec was built and npm pack --ignore-scriptsed as the workflow does, and node scripts/spec-main-shape-gate.mjs inject put it into this worktree's install. The verdict is the gate's own report step over the compile log.

spec compiled against tree compile gate report
built from objectstack main b3d7a70864a7 base 5262f7dd3f TURBO_FORCE=true turbo run type-check --continue, narrowed to @object-ui/types + @object-ui/runner and their closure: 15 of 17 tasks ok, failed types#type-check, runner#type-check exit 1, "5 diagnostic(s) in 5 objectui file(s)", the card's five rows
built from objectstack main b3d7a70864a7 this branch f82cf777ba TURBO_FORCE=true pnpm type-check --continue, the WHOLE repository: 81 of 81 tasks ok, 0 cached, 0 error TS lines exit 0, "objectui type-checks against @objectstack/spec at that commit."
published @objectstack/spec@17.5.0 (injected copy removed with pnpm install --frozen-lockfile --force; 0 [REMOVED] Key retired hits in the installed declarations) this branch f82cf777ba pnpm --filter '@object-ui/runner^...' build, then @object-ui/types and @object-ui/runner type-check: exit 0 not applicable (the pinned build is what every other CI job compiles)

No sixth site surfaced once these five compiled.

The helper: one spelling of "a retired key's type"

packages/types/src/__tests__/retired-key-type.ts (test-only: __tests__/ is excluded from the package build, nothing re-exports it) declares IsRetiredKeyType of X, which is true for exactly two types:

  • the pinned spelling, undefined (invariant equality, so never no longer passes the way [X] extends [undefined] let it);
  • the main spelling: the mark, unioned with undefined, where the mark is matched by its STRUCTURE because the spec does not export it (it is inline on purpose): exactly one property, a literal name with the [REMOVED] prefix (not a template index signature), typed never.

It matches the prefix rather than the whole sentence, so rewording the sentence upstream does not turn every objectui pull request red again. Everything else is false: any, unknown, never, a live member, the mark without its undefined arm, the mark widened by another member, a mark that admits a value, a second property, no prefix, an empty object, a template index signature. Those are the module's own direction proofs (_NotAny and its siblings), written as Expect of an invariant Equal, so a helper that answered true or boolean where a row says false does not compile. The only any in this diff is the input of _NotAny, the row that proves the helper refuses it.

The four pins

Each still asserts the retirement exactly, through the helper, and each gains a live-key control row through the same helper:

pin retired-key assertion control (must be false)
chartconfig-aria-retired-4044.test.ts ariaAdmitsNoValue on chartConfig.aria titleIsLive on chartConfig.title
dashboard-aria-retired-contract-twins.test.ts ariaAdmitsNoValue on the dashboard's aria widgetsIsLive on widgets
record-details-top-level-9040.test.ts _LayoutOnTheContractFace on the spec props' layout _ColumnsOnTheContractFaceIsLive on columns
twins-spec-by-reference-9736.test.ts admitsNoValue on the page's assignedProfiles sourceIsLive on source

The @ts-expect-error legs on authored values were already green on both sides and are unchanged. The prose beside each assertion that said "typed undefined" now names both spellings.

LayoutRenderer: the read is dropped, not narrowed

Measured on both sides: the twin's AppComponentSchema['version'] is undefined against 17.5.0 and the mark-or-undefined against main. The key is retired on both, and the spec refuses any value at parse, so the footer could only ever render nothing on valid metadata. The read is deleted. No test or pin read it (git grep over packages/runner finds no other version read). Changeset: .changeset/11330-runner-retired-version-footer.md, @object-ui/runner patch.

One-time proofs (both sides, run from the committed tree, restored and proven restored)

Mutations went through objectstack's scripts/ablation-replace.mjs in wrap mode: the anchor must hit exactly once, the landing is checked on disk (anchor count 1 to 0, blob hash changed), and the restore is checked as blob equal to HEAD with git diff HEAD empty. The compile is tsc -p packages/types/tsconfig.test.json.

  • A: the helper loosened to "anything" (its body replaced by true). Red on 17.5.0 AND on main, identically: all 12 _Not* direction proofs, plus the four pins' control rows (titleIsLive, widgetsIsLive, _ColumnsOnTheContractFaceIsLive, sourceIsLive). The one extra line, TS6133 on the now-unused type parameter, is the mutation's own artifact.
  • C: each pin pointed at its live control key (the retired key swapped for title / widgets / columns / source in the assertion). Red on both sides, exactly four errors, one per pin's assertion line.

After both: git diff HEAD is 0 bytes.

Tests and gates (worktree root; exit codes captured before any pipe; final head f82cf777ba)

  • Gate reading on main's spec: whole-repository compile exit 0, report exit 0 (table above).
  • Pinned 17.5.0, after pnpm install --frozen-lockfile --force: pnpm --filter '@object-ui/runner^...' build (15 of 47 projects) exit 0; pnpm --filter @object-ui/types type-check and pnpm --filter @object-ui/runner type-check exit 0, both scripts echoed.
  • pnpm exec vitest run on the four pin files plus packages/runner/: 10 files, 56 tests passed.
  • tsc -p packages/types/tsconfig.test.json --listFilesOnly lists the helper and all four pins.
  • pnpm check:new-line-citations exit 0 ("0 new citation(s)"); node scripts/check-changeset-presence.mjs exit 0; pnpm changeset:check exit 0; pnpm check:control-bytes exit 0; also pnpm type-check:coverage, check:phantom-deps, check:esm-specifiers, check:published-tsconfig-exclude, check:test-path-roots, check:spec-symbols, check:installed-pin-claims, all exit 0.
  • ESLint (inline config honoured, as the packages' eslint . runs) over the 6 changed .ts/.tsx files: 0 errors, 7 warnings (--format json counts). Six warnings are on unchanged lines (five in LayoutRenderer, one in record-details-top-level-9040.test.ts); the new one is no-explicit-any on the _NotAny probe input. Type-aware linting is not enabled in eslint.config.js, so this diff cannot move a verdict on an untouched file; the repo-wide pnpm lint is CI's.

Acceptance notes

  • Narrowing. The base reading covered the types and runner closure, not the whole repository. The fix reading is the whole repository, so both directions are covered for the final tree.
  • A discarded run. The first whole-repository run on the earlier commit 8ce0077913 was stopped on purpose (SIGINT to this session's own turbo PID) when lint flagged a bare empty-object type in the helper. That run is not counted; the reading above is the later full run on f82cf777ba.
  • When the pin moves. Once the pin carries the mark, the helper's undefined arm describes no installed spec any more and can be dropped. The module docblock says so. No tripwire was added, to keep this stop-the-bleed to its file surface.
  • Stale prose, not touched. packages/types/src/app.ts's docblock on AppComponentSchema still says a key the spec retires "surfaces here as an optional member typed undefined". That is true only of the pinned spec now. It is published source outside this card's file surface. Carrier: none.
  • Runtime. An off-spec app document that still carries version no longer shows a "vVERSION" footer in the runner sidebar. The spec refuses that document at parse.
  • Install. The injected spec was removed and the normal install restored (pnpm install --frozen-lockfile --force) before the pinned-side gates ran.

Session: https://claude.ai/code/session_0122Knsowci76D2rBWReCzzZ


Generated by Claude Code

claude added 2 commits October 1, 2026 05:47
…e against both the pinned spec and objectstack main (objectui#11330)

objectstack d830d71f gave a retired key a branded `[REMOVED]` mark type in
place of bare `undefined`. Four type pins in packages/types asserted the
`undefined` spelling, and the runner sidebar handed the retired app `version`
to a ReactNode slot, so Spec Main Shape Gate went red.

- One test-only helper, `IsRetiredKeyType`, accepts exactly the two retired-key
  spellings (17.5.0 `undefined`; main's one-property `[REMOVED] …: never` mark
  under the optional wrapper) and nothing looser, with direction proofs.
- Each pin asserts its retired key through it, with a live-key control row.
- `LayoutRenderer` drops the `app.version` footer: the key is retired on both
  sides, so the read could only render nothing.

Claude-Session: https://claude.ai/code/session_0122Knsowci76D2rBWReCzzZ
Co-authored-by: Claude <noreply@anthropic.com>
…ed without a bare `{}` (objectui#11330)

`@typescript-eslint/no-empty-object-type` is an error in this repo; the row's
meaning (an object with no property is not the mark) is unchanged.

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

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 3 pending changeset(s) describe a file this change touches

Their bodies publish verbatim into the CHANGELOG at the next release, so this is a request to re-read them against your diff — addressed here because you are the one seat that can answer it without re-deriving anything.

⛔ Nothing here blocks, and nothing here is a verdict on your change. This gate exits 0, is not a required context, and judges name resolution, never meaning: it asked whether a pending body names a file you touched. "Is this sentence still true?" is the one question it will not answer, and the one you are being asked to answer.

.changeset/7719-app-menu-item-shortcut-named-refusal.md

  • names LayoutRenderer.tsx → packages/runner/src/LayoutRenderer.tsx — edited by this change

    ⛔ LayoutRenderer 未恢复任何读点,adb2a86db 的那枚 pin 断言原样保留;本次只修了它与 LayoutRenderer.tsx 里已经过期的散文(两处都在把一个已裁决的问题描述成悬而未决)。 @object-ui/runner 的改动仅为注释,无任何已发布行为变化。

.changeset/9040-record-details-top-level-spec-parity.md

  • names record-details-top-level-9040.test.ts → packages/types/src/__tests__/record-details-top-level-9040.test.ts — edited by this change

    ⚠️ The retired layout on the same interface was NOT removed by this change. The contract refuses it by name (ADR-0087 D2 tombstone, removed in @objectstack/spec 17.0.0), and it stayed a published key tsc accepted and publish rejected — signposted at the declaration and pinned by record-details-top-level-9040.test.ts rather than removed, because a retirement carries its own obligations and its own FROM/TO changeset.

.changeset/9040-retire-record-details-layout.md

  • names packages/types/src/__tests__/record-details-top-level-9040.test.ts → packages/types/src/__tests__/record-details-top-level-9040.test.ts — edited by this change

    • packages/types/src/__tests__/p1-spec-alignment.test.ts wrote layout: 'stacked' on this interface and read it back. Both lines are gone, with the reason stated at the site and a live-key read-back put in their place so the leg is not quietly shrunk. Its RecordHighlightsComponentProps neighbour keeps its own layout — a different key on a different face. - packages/types/src/__tests__/record-details-top-level-9040.test.ts ledgered the divergence as OPEN. Those legs become the retirement's own: a keyof absence assertion (with a declared key read through the identical form as its control), a @ts-expect-error literal, and a new source-text leg. - packages/types/src/__tests__/record-highlights-layout-9187.test.ts read this declaration as TEXT, using its three-value layout as the lit control for a two-member assertion about the sibling. That control MOVED to RecordChatterComponentProps.position — a three-value union the contract genuinely declares — as that file's own instruction required. It was never to be deleted outright, and it is not.

Read the paragraph, not the line: both false halves of the objectui#8617 claim sat in one paragraph, and correcting either alone would have left it asserting the same wrong thing.

If a claim did go false, correct the body. That is precedented and prose-only, frontmatter untouched; check-changeset-overwrite.mjs will report the correction as its own case 2 ("correcting a declaration on purpose … legitimate"), which is the intended shape — one gate asks for the read, the other records the write.

Not covered, stated so nobody reads this as more: a born-false claim that spells no line address at all (objectui#9495 coordinated one by ORDINAL — "a grep finds that member first" — and deciding that means reading what the sentence means), a claim spelled as a symbol or a package rather than a backticked file name, and a file named ambiguously.

Compared the checked-out tree with 5262f7dd3 (merge-base with origin/main): 6 file(s) changed outside .changeset/, read against 1892 pending declaration(s) that publish a body (2505 pending in total). · run

@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 330 chunks) 3585.7 KB 3607.4 KB
Main entry chunk (gzip) 150.4 KB 350 KB
Entry file index-Dx92d-uf.js —
Status PASS —

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.88KB 6.25KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.17KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.70KB 10.94KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.13KB 7.95KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 571.28KB 136.83KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 229.96KB 63.80KB
fields (index.js) 261.05KB 66.23KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.49KB 9.23KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 40.95KB 11.48KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.86KB 5.00KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.52KB 2.26KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.01KB 3.93KB
plugin-calendar (index.js) 52.98KB 15.39KB
plugin-charts (index.js) 84.09KB 22.93KB
plugin-chatbot (index.js) 198.22KB 46.97KB
plugin-dashboard (index.js) 139.34KB 37.30KB
plugin-designer (index.js) 216.45KB 44.60KB
plugin-detail (index.js) 244.79KB 64.47KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 173.70KB 44.63KB
plugin-gantt (index.js) 173.11KB 43.10KB
plugin-grid (index.js) 231.53KB 63.58KB
plugin-kanban (index.js) 49.32KB 15.48KB
plugin-list (index.js) 116.63KB 28.97KB
plugin-map (index.js) 23.54KB 7.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.04KB 12.21KB
plugin-timeline (index.js) 33.91KB 10.00KB
plugin-tree (index.js) 11.20KB 3.89KB
plugin-view (index.js) 90.32KB 22.76KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.81KB 3.58KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 119.55KB 39.23KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 6.06KB 2.68KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 21.42KB 7.05KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 3.19KB 1.62KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.26KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 21.59KB 7.71KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

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

Inputs read, and nothing else: card objectui#11330 (body and its three comments: the spec seat's queue note 5925423200, the claim 5925455871, the dev report 5925773522); PR objectui#11343 (body, 7-file list, the net diff 5262f7dd3f..f82cf777ba, which is the merge-base with main, so the diff is net: +170 / -18); the check-runs on the head; the governing text (ruling objectui#10916 5866275396; option C as scripts/spec-main-shape-gate.mjs's header states it; the card's direction as the claim adopted it); objectstack d830d71f's packages/spec/src/shared/retired-key.ts and its changeset, read with git show; precedent objectui#11210.

① Derived judgments

The upstream fact, verified at source. At d830d71f every retiredKey() tombstone is typed Tombstone of { '[REMOVED] Key retired: run os validate for its migration.': never } under ZodOptional, so its input type is the mark or undefined; RetiredKeySchema and Tombstone are module-local (not exported) and the mark is written inline on purpose, so a consumer can only match it by structure. Its parent (also 17.5.0 in packages/spec/package.json) returned z.never().optional(), input undefined; the d830d71f changeset says the same ("from undefined to an uninhabited mark"); objectui's pnpm-lock.yaml resolves @objectstack/spec@17.5.0, and the dev's installed-declaration scan found 0 marks there. The two spellings the helper's docblock names are the two spellings. Right.

(a) The helper, packages/types/src/__tests__/retired-key-type.ts. Traced every branch of IsRetiredKeyType of X and IsRetirementMark of M by hand. The guards, in order: IsAny first (so any never reaches the structural match); invariant Equal with undefined (so never, which [X] extends [undefined] let through on the base pins, is refused); the undefined arm is required ([undefined] extends [X]), then the remainder after Exclude must be the mark: not never, not any, not keyless (unknown, {}, object, null), not a template index signature (RemovedMarkKey extends keyof M refuses it), not a union of keys (a second property, or a union remainder such as mark-or-string whose common keys are never), and finally invariant equality with the remap of its one [REMOVED] -prefixed key to never (so a mark admitting a value, or a key without the prefix, is refused). Every checked type in every conditional is wrapped or derived, so nothing distributes and the helper cannot answer boolean. All 15 rows resolve as written: 3 positive (undefined; mark-or-undefined; a reworded sentence under the prefix) and 12 _Not* negatives. A live key cannot satisfy it unless its type is exactly undefined or exactly mark-or-undefined, which no live key has; any cannot, by the first branch and by Equal's invariance. The helper is STRICTER than the four base assertions it replaces (two of which had no any guard of their own). The plausible loosenings the card's "not loosened to anything" forbids are each a proof row. Right.

(b) The four pins. Each retired-key assertion is replaced in place, never deleted: ariaAdmitsNoValue (chartconfig, dashboard), _LayoutOnTheContractFace (record-details), admitsNoValue (twins), all now through the helper and all still asserting the retirement exactly, by direction, on either side of the bump. Each gains a live control row through the same helper: titleIsLive, widgetsIsLive, _ColumnsOnTheContractFaceIsLive, sourceIsLive, and each control is wired into the runtime expect as well. The @ts-expect-error legs on authored values are unchanged. The ablation arithmetic reconciles with the diff: a helper body of true leaves the 3 positive rows green and reddens exactly the 12 _Not* rows plus the 4 controls (16, plus the TS6133 on the then-unused type parameter), and pointing each assertion at its control reddens exactly that assertion line, 4 in all. Sound.

(c) LayoutRenderer.tsx. LayoutRendererProps.app is AppComponentSchema from @object-ui/types, which extends Omit of the spec's App over APP_SPEC_EXCLUDED (name, label, description, navigation, areas, contextSelectors); version is not withheld, so it reaches the type from the spec projection, not from BaseSchema's any index signature (which is why the base diagnostic at :194 showed the mark, not any). Spec App.version is retiredKey(...) at d830d71f and at its parent, retired since 17.0.0 by its own guidance text, so on 17.5.0 the member is undefined and the {app.version && ...} branch could never render; on main it is the mark and v{app.version} is a TS2322 into ReactNode. Deleting is the right measure, not narrowing: a typeof narrowing would keep a read on a key the spec refuses at parse and would assert, in source, a type the key does not have. No other .version read remains under packages/runner/src at the head. The changeset @object-ui/runner patch is true: the package is private: false at 17.6.0, the key is retired and parse-refused, and the footer rendered nothing on valid metadata. The one visible change is confined to an off-spec document that the runner's MetadataLoader does not parse through the spec; the PR's Acceptance notes name it. Right.

(d) File surface. Exactly the 7 files the claim allowed. No @objectstack/spec pin (the ^17.0.0 / ^17.5.0 manifests are untouched), no pnpm-lock.yaml, no scripts/spec-main-shape-gate.mjs, no workflow. Option C (objectui#9860) holds as the gate's header states it. Right.

(e) Gate verdicts on this head, read at 2026-10-01T06:16Z: 42 check-runs, 33 success, 3 skipped, 6 in_progress. Spec Main Shape Gate: completed, success, the check-run's own conclusion, not the dev's reading. Also success: Lint, Test (dist pins), Test (shard 1/8), Test (shard 7/8), Test (shard 8/8), Build & E2E, Build Docs, Changeset Declaration, Changeset Bump Policy, Changeset Fixed Group Check, Changeset Overwrite Report, Changeset Claim Re-read, Line Citation Gate, Governed Surface Queue Guard, Control Byte Scan, Doc Snippet Type Check, Bundle Analysis. NOT CONCLUDED at that reading, named and not adopted either way: Type Check and Test (shard 2/8) through Test (shard 6/8). The dev's local readings (whole repository 81/81 against spec built from objectstack b3d7a70864a7 with report exit 0; pinned side type-checks clean, 56 tests) are consistent with the diff and with the gate's conclusion, but the gate's own green is the verdict this record rests on. The ruleset requires Type Check and Test too; nothing lands on this record before they conclude.

② Semver level

  • .changeset/11330-runner-retired-version-footer.md: @object-ui/runner patch. Matches what the diff publishes: one dead internal read removed from a released package; no export, prop type, or accept set moves. The body's three claims (the key is retired on the spec's app schema, the parse refuses a value, the footer could only render nothing on valid metadata) are each verified above.
  • @object-ui/types publishes nothing from this diff: only src/__tests__/ files change, tsconfig.json excludes **/__tests__/** from the build, and a git grep at the head finds the helper imported by the four pins alone. No changeset owed; no skip-changeset used.
  • Clause-②: no is right. No published type, export or accept set changes; LayoutRendererProps.app is unchanged; behaviour on a valid document is unchanged. The Clause-②: yes (minor) belongs to objectstack d830d71f; this PR adapts objectui to it and neither widens nor narrows anything.
  • The Changeset Claim Re-read notice names three pending changesets that cite touched files. Read against this diff: 7719-... describes its OWN comment-only edit to LayoutRenderer.tsx (a claim about that change, not a standing claim about the file); 9040-record-details-top-level-spec-parity and 9040-retire-record-details-layout say layout stays pinned by record-details-top-level-9040.test.ts, which remains true, now through the helper. No body needs correcting.

③ Boundary flags

  • open_questions: none declared; none found.
  • The only any in the diff is the input of _NotAny, the row proving the helper refuses any. Judged not a breach of the claim's "no any": it types no value, slot or export, it tightens rather than loosens, and deleting it would delete the proof. It lands as an ESLint warning, not an error; Lint is green on the head.
  • No deleted assertion: all four retired-key assertions are replaced in place (see ①b); the only deletion is the runner's dead read, which no pin or test covered.
  • No tripwire for the pin bump (the dev's "when the pin moves" note). Accepted: unlike objectui#11210's | 'computed' on a published table, the helper's undefined arm is test-only and stays exact after the bump (a dead arm, not a loosening); the module docblock names its retirement. The carrier is the pin-bump card that first carries the mark.
  • Dev deviations: the narrowed base reading and the whole-repository fix reading, the --concurrency=2, and the discarded run on 8ce0077913 are all superseded by the gate's own green on f82cf777ba. The objectstack-side prepare script writing merge.os-regen.* and core.hooksPath into the shared objectstack git config is a side effect outside this repository and this PR; noted for the seat, no action here. The sibling worktree observation (objectui-issue-11331) is likewise not this PR's.
  • Out of scope (f): packages/types/src/app.ts's AppComponentSchema docblock says a retired key "surfaces here as an optional member typed undefined". True of every installed spec today (the lockfile's 17.5.0), stale only against unreleased objectstack main, false once the pin carries the mark. Reach: one docblock sentence, read by no test, emitted into the package's .d.ts. Not fileable on its own; it belongs to the pin-bump card that first carries the mark, beside the helper's undefined arm. The Acceptance-notes carrier is right; no new card.
  • Runtime: an off-spec app document still carrying version no longer shows a vVERSION sidebar footer. Named in the PR and the changeset; patch is the right level.
  • PR state: opened as a draft, mergeable_state blocked on the in-progress required contexts. The seat marks it ready and arms auto-merge only once Type Check and the five remaining Test shards conclude green, per the standing ruling's restored-queue target.

Implemented-by: claude/issue-11330-retired-key-brand-dual-compile
Reviewed-by: session_0122Knsowci76D2rBWReCzzZ

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 1, 2026 06:21
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 1, 2026 06:21
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 1, 2026
Merged via the queue into main with commit 47e3ce0 Oct 1, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-11330-retired-key-brand-dual-compile branch October 1, 2026 06:36
os-litant pushed a commit that referenced this pull request Oct 1, 2026
…jectui#11168 slice 3, round 2)

Picks up the Spec Main Shape Gate repair (PR #11343, 47e3ce0) and
objectui#11299 (770cc5b). Merged cleanly: no conflicting hunk, so nothing
was resolved by hand.

Claude-Session: https://claude.ai/code/session_01VhxTqosz7wn54ahqyxgERT
Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Oct 7, 2026
…Source are the spec's, by reference; GridColumnDefinition retires (objectui#11070, round 7) (objectstack-ai#11352)

Refs objectstack-ai#11070
Clause-②: yes (narrowing). The strict face's accept set widens (a grid
field's `columns` and an `object-chart` node's `dataSource` binding
parse), and the tolerant face narrows (both keys are now judged by their
declared type: a grid column the spec refuses, or a non-binding
`dataSource` on `object-chart`, is refused where it used to be stripped
or kept unjudged). `GridColumnDefinition` retires from
`@object-ui/types`, and `GridColumn` in `@object-ui/fields` becomes the
spec's `InlineGridColumn`. Priced in
`.changeset/11070-grid-columns-chart-binding-round7.md` as minor with a
BREAKING banner.

`Refs`, not a closing keyword, on purpose. Round 7 of the card, under
the seat's claim `5925545318` (remainder items 2 and 3 of `5919314708`).
Draft; the dev does not mark it ready.

## What changed

**(i) The grid field's `columns`, by reference to the spec.**
- `GridFieldMetadata.columns` (`field-types.ts`) and `FormField.columns`
(`form.ts`) are `SpecField['inlineColumns']`; the zod mirror
`FormFieldSchema` declares
`stripImportedDefaults(SpecFieldSchema).shape.inlineColumns`.
`form-field-zod-coverage` gains the `columns` row. `FormFieldSchema` was
already a registered, spec-derived pair in `zod-mirror-parity`; its
`SPEC_DERIVED_PAIRS` note records the new member, and no ledger total
moved.
- `GridColumnDefinition` is retired (interface and barrel export).
Nothing in production read it, and it was not the shape the widget read.
- `GridField`'s `GridColumn` is `InlineGridColumn` from
`@objectstack/spec/data`, by reference. The renderer notes its
per-member JSDoc carried move into one docblock. No read changes: the
widget already read exactly the spec's twenty keys.
- Fixtures: `fields-grid/*` already write only `name` / `label` /
`type`; all nine columns parse against the spec element, so none
changes.
- `content/docs/fields/grid.mdx`: the typed fence drops a column
`defaultValue`; the prose names the spec column and lists the nine cell
types; the plaintext fences stop teaching `boolean`, `percent` and
`user` column types; the validation sketch drops a column `min` the
column does not have.

**(ii) `object-chart` · `dataSource`, re-measured and declared.**
- `ObjectChartSchema` (TypeScript and the flat zod mirror) declares
`dataSource` as the spec's `ElementDataSource` /
`ElementDataSourceSchema`, by reference. `OBJECT_CHART_NODE_LEVEL_KEYS`
keeps it out of the authored bag, and `ObjectChartBlockSchema` carries
it at node level (the mirror's own member), as `ObjectFormBlockSchema`
and `ObjectMapBlockSchema` do.
- The stale `SHOWCASE_NODE` pin
(`object-chart-react-tier-node-10770.test.ts`) drops `dataSource: null`
and the comment describing a wrapper write that round 2 removed; a
binding row and a `null`-refused row join it.

**Rows the declarations force (fixture triage):**
`object-chart-properties-bag-11276` (the node-level key set gains
`dataSource`, on the type and runtime legs; its "left undeclared" row
becomes a "parses at node level, not a bag member" row) and
`public-block-responsive-styles-10872` (`DECLARES_DATA_SOURCE` names
`object-chart`, the control built to redden exactly when an arm gains
the key). `strict-face-read-keys-11070` moves `columns` and
`object-chart` to the declared blocks and empties its PENDING list.
Comment-only: `deriveMasterDetail.declaredSpelling.test.tsx`, and the
two `GridField` tests now type their fixtures
`GridFieldMetadata['columns']`.

**Changesets.** One new (`@object-ui/types`, `@object-ui/fields` minor).
Dated notes on the five pending changesets of this release whose
sentence the round makes false: `11276-object-chart-properties-bag`,
`10872-flat-arm-responsive-styles`, `11070-strict-face-read-keys`,
`6138-fields-schema-block-parity-pr2`, `8209-datetime-widget-faces`.

## Census (i): the grid column, measured at `5262f7dd`

| source | shape |
|:--|:--|
| installed `@objectstack/spec` 17.5.0 | No `grid` field type and no
`columns` member on `FieldSchema`. The list is `inlineColumns` (on
`master_detail`), elements `InlineGridColumnSchema`: strict, 20 keys
(`name` required; `label`, `type` in nine values, `width`, `required`,
`options`, `prefix`, `step`, `reference`, `displayField`, `idField`,
`multiple`, `accept`, `defaultHidden`, `computed`, `expr`, `scale`,
`autofill`, `readonlyWhen`, `requiredWhen`). Named refusals: `field` /
`fieldName` / `key` (→ `name`), `title` / `header` (→ `label`), `size`,
`expression`, `hidden`; `scale` refused on a `currency` column. |
| `GridField`'s local `GridColumn` | The same 20 keys and the same
nine-value `type`. Value domain differs on `readonlyWhen` /
`requiredWhen` only (local object arm had an optional free-string
`dialect`). |
| what `GridField` reads | `cfg.columns`, and per column exactly those
20 keys (a census of member reads off the column: 20 distinct, 0 outside
the spec; no spread or computed read of a column). |
| `GridColumnDefinition` | 7 keys: `name`, `label`, `type` (required,
free string), `required`, `defaultValue`, `width`, `validate`. 2 keys
not in the spec, `type` wider. Production readers: 0 (two `fields` tests
and the docs prose). |
| `fields-grid/*` fixtures | 3 documents, 9 columns, keys `name` /
`label` / `type` (`text`, `number`, `currency`, `date`); 9 of 9 parse
against `InlineGridColumnSchema`. |
| `grid.mdx` | typed fence: a column `defaultValue`; plaintext fences:
`boolean` (2), `percent`, `user` column types; validation sketch: a
column `min`. |
| objectstack example apps (`94608a7d7`) | No `grid` field (the spec has
none). One producer of the spec element: `app-showcase`
`invoice.object.ts` `inlineColumns`, seven identity-only `{ name }`
entries. |

H1 holds in substance, not as posed: the spec declares no grid
`columns`; its `inlineColumns` element is key-for-key `GridColumn`. H2
holds: `GridColumnDefinition` had no production reader, so it retires.
No non-spec column spelling is read, so nothing else retires and no
producer stop applied. The alignment is a type reference, not a renderer
redesign.

## Census (ii): `object-chart` · `dataSource` on `5262f7dd`

| fact | reading |
|:--|:--|
| renderer read | the `object-chart` registration is
`elementDataSourceBlock`-wrapped; `ElementDataSourceGate` reads the
node's `dataSource` and lands `object` on `objectName` |
| producer | the react-page wrapper builds `{ ...props, specType, type
}`, no `dataSource` write (round 2) |
| strict face | `{ type: 'object-chart', properties: { objectName,
chartType }, dataSource: { object } }` refused, unrecognized key
`dataSource` |
| tolerant face | the same document, and `dataSource: null` or an
adapter object, accepted unjudged (the arm is passthrough) |

Not resolved by PR objectstack-ai#11328 or round 2, and no ruling needed: declared
like the seven other gate-wrapped blocks.

## M3 before / after

`scripts/measure-strict-authoring-face.mjs --json` plus the PR objectstack-ai#11069
`findUndeclaredKeys` port for pairs, each on a freshly built
`@object-ui/types`.

| reading | base `5262f7dd` | main `770cc5ba` | this head `62fe84ae` |
|:--|--:|--:|--:|
| documents | 601 | 601 | 601 |
| nodes | 2229 | 2229 | 2229 |
| strict-refused nodes | 94 | 94 | 91 |
| strict-only nodes | 63 | 63 | 60 |
| red today (tolerant) | 31 | 31 | 31 |
| documents refused whole-tree | 88 | 88 | 85 |
| port: pairs | 60 | 60 | 59 |
| port: tolerant-refused | 28 | 28 | 28 |

Main to head: exactly `form · fields[].columns` (3 occurrences, 3
strict-only, the three `fields-grid` documents) leaves; no pair enters
or moves, and the tolerant face refuses the same documents. H3 holds.
`object-chart` · `dataSource` has no corpus occurrence, so it moves no
M3 figure; its pins carry it.

## Verification

All on this branch; heavy runs through `os-verify-lock` with 2 workers
and a memory watchdog.

- `@object-ui/types`: `type-check` exit 0 and `vitest packages/types/`
309 files / 7828 tests passed at `62fe84ae`; build exit 0.
- `type-check` exit 0 in all 11 census packages (types, core,
components, fields, plugin-form, plugin-charts, plugin-dashboard,
plugin-detail, plugin-tree, plugin-grid, schema-catalog) at `3eecfc2c`.
- Tests at `3eecfc2c`: `packages/fields/` 226 files passed, 1 skipped
(3515 tests); `packages/plugin-form/` 157 / 1822;
`packages/plugin-charts/` + `examples/schema-catalog/` 121 / 3233; the
86 test files outside those packages that read the zod faces, 86 / 2110.
- Reverse type check: a temporary `plugin-form` file assigning a
`boolean` column, a column `defaultValue`, an adapter `dataSource` and a
dialect-less `readonlyWhen` object made `tsc` exit 2 with exactly those
four errors (TS2322, TS2353, TS2353, TS2322), and a spec-valid control
line none; the file was removed.
- Ablations, committed first, predicted in writing before each run,
through objectstack's `ablation-replace.mjs` (anchor 1 → 0, restore blob
== HEAD, `git diff HEAD` empty; subjects resolve from source):
- A1, item (i): drop `FormFieldSchema.columns`. Predicted 7 red + tsc
red; measured 7 failed / 52 passed, `tsc -p tsconfig.test.json` exit 2
(the parity ledger).
- A1b, docs leg: plant the column `defaultValue` back in `grid.mdx`'s
typed fence. Predicted 1 block red; measured `check:doc-snippets` 698
judged, 1 failed (TS2353 on `defaultValue`).
- A2, item (ii): drop `dataSource` from `ObjectChartBlockSchema`.
Predicted 5 red, tsc green; measured 5 failed / 321 passed, tsc exit 0.
- Gates at `62fe84ae`: `check:doc-snippets` 698 of 698 judged, 0 failed
(closure rebuilt, 35 of 35 tasks); `check:doc-fences`,
`docs:check-links`, `check:control-bytes`, `check:changeset-claims`,
`check:pending-changeset-literals`, `changeset:check`,
`check-changeset-presence`, `check:new-line-citations` (0 new),
`check:spec-symbols`, `check:test-path-roots`, `check:doc-types`,
`check:doc-examples`, `check:doc-example-ids` all exit 0.
`check-governed-queue-guard --test` over the 23 net paths: NOT GOVERNED.
- Narrowed lint: the population is `**/*.{ts,tsx}` (the config's
`files`), so 16 of the 23 changed paths; `eslint --no-inline-config
--format json` on those 16 at head and at the base: 0 new errors,
warnings equal per file except `field-types.ts` 5 → 4. The one error
(`index.ts`, `no-restricted-imports`) is present at the base too. The
config has no type-aware linting and no rule reads another file, so the
diff cannot move an untouched file's verdict.

## Notes

- **The `Spec Main Shape Gate`.** objectui#11330's repair (PR objectstack-ai#11343)
landed on `main` as `47e3ce00` and is merged into this branch, so its
fleet-wide red is not expected here; the result is CI's.
- **PR objectstack-ai#11333** (round 6) shares `packages/types/src/field-types.ts` on
disjoint hunks (`FormulaFieldMetadata` there, the grid metadata here).
This branch merges `main` again once it lands.
- **Not touched:** `strict-authoring-face.ts`, PR objectstack-ai#11333's surface,
`docs/adr/0001-master-detail-subform.md` (a governed record that still
names `GridColumnDefinition`).

## Acceptance notes

- `grid.mdx`'s typed fence still teaches `allow_reorder: false`.
`GridFieldMetadata` declares `allow_reorder`, and `GridField` reads
`reorderable`: a probe rendering the widget measured two drag handles
with no key, two with `allow_reorder: false`, and none with
`reorderable: false`. The widget's other field-level reads
(`total_field`, `amount_field`, `amountField`, `allow_duplicate`,
`show_line_numbers`, `sort_field`, `add_label`, `reorderable`) are
declared on neither face. These are field-level keys, outside this
round's `columns` scope; they are in the report for the seat.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01TdiauJaVCHuj45EzZGUxHh)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants