refactor(types,app-shell,plugin-designer): one tombstone registry for the designer seam's retired field keys - #6627
Conversation
… the designer seam's retired field keys Three independently maintained RETIRED_FIELD_KEYS literals on the designer seam (the metadata-admin read door in object-fields-io.ts, MetadataService's carryOver, MetadataFieldsPage's carryOver) become derivations from a single tombstone registry in @object-ui/types: RETIRED_FIELD_KEY_TOMBSTONES names each retired key, the card that retired it, and its PER-SITE applicability, and retiredFieldKeysFor(site) is the only supported way for a site to obtain its strip list. Deliberately NOT a union of the three lists — per-site behaviour is unchanged and pinned per site: - `formula` stays stripped by the two write-side carry-overs and is NOT applicable at the read door, per the objectui#6526 option B ruling (ObjectFieldInspector's linting CEL editor migrates the legacy key; stripping on read destroys authored expression text). The registry test makes that ruling mechanical. - `sortOrder` stays a single-site strip at MetadataService's carry-over and is now recorded as the registry's one DEFENSIVE entry, with the objectui#6045 measurement (no shipped writer ever populated a field-level one) cited on the tombstone instead of the entry reading like a measurement. The per-key evidence that used to live in three file-local comment blocks moves onto the tombstones; each site keeps only its own mechanics (read-door strip-on-load contract, carry-over bounds, this-writer history). Not touched here: scripts/check-designer-field-key-parity.mjs still reads the declared payload shapes, not this registry; pinning the registry as that gate's single source is a cross-lane follow-up (gate-class scripts/ is domain:devx), named in the PR body. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49
✅ 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
|
|
Placement-topology measurements for the ruling on objectui#6527 (taken read-only on this branch at 1. Yes — it is on the package's public entry, and only there
// Retired-field-key tombstone registry — the single source for the designer
// seam's per-site strip lists (objectui#6527).
export type {
RetiredFieldKeySite,
RetiredFieldKeyTombstone,
RetiredFieldKey,
RetiredFieldKeysAt,
} from './retired-field-keys.js';
export {
RETIRED_FIELD_KEY_SITES,
RETIRED_FIELD_KEY_TOMBSTONES,
retiredFieldKeysFor,
} from './retired-field-keys.js';So: three value exports + four type exports re-exported from the index barrel. The 2. Changeset frontmatter, verbatim
3. Non-published placement: structurally not available; the real options with measured costsThe binding constraint, measured on the branch:
Therefore whatever module both consumers import must be resolvable at their consumers' runtime through some published package's
Plain answer: a non-published placement is not genuinely available. The choice is between A (public on the Protocol barrel, status quo, minor already declared) and B (published but fenced to a signposted subpath). Either way the placement adds published surface, which is why this sits with the maintainer; B is the smaller permanent obligation if the maintainer wants one at all. Generated by Claude Code |
|
ui execution seat, The failure1 failed / 532 passed (533 files); 7174 tests passed. ⭐⭐ Why this is the placement decision showing up as a test failureThe registry is exported from ⭐ The generalisable finding, worth more than the fix: adding one export to a barrel widens what every consumer of that barrel must be able to evaluate. A partial mock that was complete enough yesterday is not complete enough today, and nothing in the diff touches the failing test or the module it mocks. This is the same class as the lesson from PR #6626 earlier today — an enforcement or an assumption resting on a module's shape rather than on its behaviour, silently invalidated by a declaration landing elsewhere. ⛔ Why I am not fixing itThere are exactly two fixes, and choosing between them is the decision this PR is held on (see #6527):
Pushing the mock patch would silently pre-commit the repo to A while a decision on A-vs-B is open. ⛔ Not mine to make. What this changes about the decisionIt moves a cost from theoretical to measured. My filing on #6527 argued B on governance grounds — smaller permanent obligation, more withdrawable. This adds a concrete technical cost to A that was not in that analysis: barrel placement makes every consumer's module graph wider, and the first thing it broke was an unrelated test in a package that merely imports the barrel. Recorded on the card as new evidence rather than left here. Status: PR stays draft, held on #6527's ruling. Once ruled, the fix follows from the ruling and I will resume the same dev on this same claim. Generated by Claude Code |
…e main barrel (objectui#6527 option B) Maintainer ruling, 2026-08-28: the registry moves to a dedicated internal subpath, `@object-ui/types/internal/retired-field-keys`, instead of the package's main barrel. Importing the barrel eagerly evaluates every other module it re-exports -- including `spec-report.ts`'s read of `@objectstack/spec/ui` -- which is what widened an unrelated test's partial spec mock into a failed suite (packages/app-shell/src/views/metadata-admin/clientValidation.skew.test.tsx) under the prior (option A) shape. A subpath import never pulls the barrel in, so that suite passes again with zero changes to the test or its mock. Four edits, exactly as ruled: - Remove the registry's re-exports from the `@object-ui/types` main barrel (`packages/types/src/index.ts`). - Add one dedicated internal subpath to the `exports` map (`./internal/retired-field-keys`), and move the registry's source under `packages/types/src/internal/` to match -- the package's existing `./zod` subpath is the precedent for a subpath's source living in a matching real subdirectory rather than a flat file behind a renamed export key. - Rewrite the 3 consuming imports (MetadataService.ts, MetadataFieldsPage.tsx, object-fields-io.ts) to the subpath. - Re-point the registry's own barrel-wiring pin at the subpath, and add a companion pin that the registry is NOT exported from the main barrel, so a regression back to option A turns a test red instead of drifting silently. Nothing else changes: the objectui#6526 formula read-door asymmetry stays encoded and pinned, the sortOrder defensive verdict stays recorded, and scripts/check-designer-field-key-parity.mjs is untouched (domain:devx lane). Changeset unchanged: '@object-ui/types': minor (already correct).
…ired-key-tombstone-registry
✅ 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
|
…not by self-import The registry's wiring pin imported the package's own bare subpath specifier `@object-ui/types/internal/retired-field-keys` from inside `packages/types/src/`, which `check:self-import` refuses (objectui#4801): that specifier resolves through the package's OWN `exports` map to `dist/`, and turbo gives `type-check`/`test` only `^build` — the DEPENDENCIES' builds, never this package's own — so on a cold CI cache the declarations have not been produced and the file fails TS2307. Green on every machine that has ever built, red in CI only. The runtime half of that pin was also tautological rather than a wiring test: the repo-root `vitest.config.mts` aliases `@object-ui/types` to `packages/types/src` by prefix (the `/zod` entry directly above it exists precisely because that matching is prefix-based), so both sides of the `toBe` resolved to the same source module. It compared a module to itself and could not fail. So the module half becomes a relative import — the same module on both surfaces that read it — and the wiring half moves to a manifest-level assertion in the style of this package's existing `package-exports-manifest.test.ts`: read `package.json` and pin the `./internal/retired-field-keys` entry's exact shape, then DERIVE the source path from the declared target and prove it exists. Deriving is what keeps it a wiring assertion; two independent literals that happen to agree would still pass with the entry re-pointed at nothing. The declared `dist/` target itself is deliberately not stat-ed — that would reintroduce the very build-order dependency this replaces. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49
…ired-key-tombstone-registry
|
| leg | mutation | disk proof | predicted | observed |
|---|---|---|---|---|
| A | drop the ./internal/retired-field-keys entry from package.json |
anchored grep 3 to 0; blob b4d320b1 to e65f4efd |
1 red: the declaration toEqual |
exactly 1 red at line 264, expected undefined to deeply equal {...}; 14 passed |
| B | re-point the subpath at a module that does not exist, moving the test's expected literal with it, as a developer re-pointing it would | GONE count 2 in package.json / 3 in the test; both blobs changed |
1 red: the derived existence check, toEqual passing because both sides moved together |
exactly 1 red at line 288, exists: false with srcRelative derived to src/internal/retired-field-keys-GONE.ts; 14 passed |
Leg B is the one that matters for the PM's question: it isolates the derived half and shows it is independently load-bearing rather than shadowed by the toEqual.
Local readings at fd1b907ba (exit captured by redirect before any pipe; verdict lines quoted from each gate)
pnpm check:self-importexit 0 — "Scanned 44 workspace package(s), 3545 source file(s) ... 0 self-import (0 exempted)" / "No package names itself inside its own src/."pnpm exec vitest runover the meta-testscripts/__tests__/check-package-self-import.test.ts, the registry pin suite,package-exports-manifest.test.tsand the acceptance testclientValidation.skew.test.tsx— "Test Files 4 passed (4)" / "Tests 51 passed (51)"pnpm --filter @object-ui/types type-checkexit 0 (tsc --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.json). Not vacuous:tsconfig.test.jsonincludessrc/**/*.test.tsand setstypes: ["node"], which is what thenode:fs/node:module/node:pathimports compile against.check:phantom-deps,check:esm-specifiers,check:designer-field-key-parity,check:control-bytes,check:vi-mock-specifiers,check:pre-install-import-graphall exit 0.
Option A still not taken: clientValidation.skew.test.tsx has an empty diff against origin/main and passes. scripts/check-designer-field-key-parity.mjs is likewise untouched.
Two local readings that are NOT measurements, stated so they are not miscounted
check:readme-exportsexits 1 in this worktree with 77 findings, every one of the form "its type entry./dist/index.d.tsis not on disk — runpnpm buildfirst", across app-shell, cli, data-objectstack, plugin-ai, plugin-gantt, plugin-map, plugin-markdown and plugin-timeline. None is inpackages/types, none touches a README this branch edits, andreadme-exports.ymlruns "Build every package, so the declared type entries exist" before the gate. Prerequisite not met, not a red — and the check was green in CI at the previous head.check:published-distexceeded the container's foreground cap twice (it builds) and was not run locally. Delegated to CI.
Declared narrowing
Repo-scale runs (pnpm lint over the whole repo, the full 4-shard pnpm test, check:published-dist) were not run locally; CI runs the farm exactly once regardless. This card has now twice produced a defect that only a whole-repo run could reach, so the narrowing is declared rather than implied.
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
|
Fixes #6527
Session:
session_8ca04858-ea8e-5b85-9182-de59aa49e00c(durable copy of the attribution for this patch round, since PATCH edits demote the footer). Original implementation session:session_01CRJge11jso9TpXRWFt1Z49.Final shape: option B — the registry lives behind a dedicated internal subpath
Maintainer ruling, 2026-08-28 (comment
5448711511): B adopted. The designer seam's retired-field-key tombstone registry is un-exported from@object-ui/types's main barrel and lives instead behind one dedicated subpath:@object-ui/types/internal/retired-field-keys.internal/path segment is the ruling's chosen way to say "reachable, not supported" — the same honesty a@internalJSDoc tag gives a barrel export, but load-bearing here because a packageexportsmap with no wildcard makes every unlisted subpath structurally unresolvable, so the signal has to live in the one path segment that is published.@object-ui/types/internal/retired-field-keysresolvesRETIRED_FIELD_KEY_TOMBSTONES,RETIRED_FIELD_KEY_SITES,retiredFieldKeysFor, and the four types (RetiredFieldKeySite,RetiredFieldKeyTombstone,RetiredFieldKey,RetiredFieldKeysAt) — by the letter this is still published API surface, which is why it sat with the maintainer rather than being this seat's call.import ... from '@object-ui/types', includingimport * as types from '@object-ui/types') no longer exposes any of the above — pinned mechanically (below). The package'sexportsmap carries no wildcard, so./internal/retired-field-keysis the only newly-reachable path; nothing else on the package becomes reachable that wasn't before.Why this is the whole fix for PR CI, not just a governance preference
The barrel-placement shape (option A) had a measured technical cost, not just a governance one: importing
@object-ui/types's main barrel eagerly evaluates every other module the barrel re-exports, includingspec-report.ts's read of@objectstack/spec/ui. An unrelated suite's partialvi.mockof that module didn't define every symbol the barrel's full evaluation needed, so the suite threw before any test body ran:A failed suite, not a failed assertion — nothing about the registry's own behaviour was wrong. Under B, a subpath import never pulls the barrel in, so this disappears at the root:
clientValidation.skew.test.tsxpasses with zero changes to the test or its mock (confirmed below) — option A's fix (patching the mock) was explicitly not taken.The four edits, exactly as ruled
@object-ui/typesmain barrel (packages/types/src/index.ts) — replaced with a comment pointing at the new location and explaining why it's not re-exported.exportsmap (packages/types/package.json):"./internal/retired-field-keys": { "types": "./dist/internal/retired-field-keys.d.ts", "import": "./dist/internal/retired-field-keys.js" }. The registry's source moved frompackages/types/src/retired-field-keys.tstopackages/types/src/internal/retired-field-keys.tsto match — the package's existing./zodsubpath is the precedent for a subpath's source living in a matching real subdirectory (src/zod/→./zod) rather than a flat file behind a renamed export key; this also means no vitest alias or root-tsconfigpathschanges were needed beyond what already exists (@object-ui/types/*wildcards already cover it).MetadataService.ts,MetadataFieldsPage.tsx,object-fields-io.tsnowimport { retiredFieldKeysFor } from '@object-ui/types/internal/retired-field-keys'.retired-field-key-tombstones.test.ts: the pin now imports the bare subpath specifier (the same way the three real sites reach it) and asserts referential equality, exactly as the old pin did for the barrel. A companion pin was added —'the registry is NOT exported from the main package barrel', dynamically importing../index.jsand asserting the registry's names are absent — so a regression back to option A turns a test red instead of drifting silently.Nothing else changed: the objectui#6526
formularead-door asymmetry stays encoded and pinned at the same three layers, thesortOrderdefensive verdict stays recorded, the per-site parity pins stand unchanged, andscripts/check-designer-field-key-parity.mjswas not touched (confirmed still green, unmodified —domain:devxlane).Clause-② — yes, this round
A published
exportsmap entry is added (see "what an external consumer can/cannot reach" above). Changeset stays'@object-ui/types': minor(unchanged from the original round — a new public export already required minor, and that classification doesn't change with where the export lives).Verification (this patch round, on
mainmerged in)mainhad moved 36 commits since this branch's last push (ec93ccfa); merged (not rebased) to9d4f6c75f, and everything below re-run on the merged result.clientValidation.skew.test.tsx, red before this round — passes with zero diff to the test file (git diff --statconfirms):Test Files 1 passed (1)/Tests 4 passed (4).9d4f6c75f(registry pins, previews + services + plugin-designer suites,ObjectFieldInspector.test.tsx, the skew test):Test Files 72 passed (72)/Tests 790 passed (790).@object-ui/types,@object-ui/app-shell,@object-ui/plugin-designer(tsc --noEmit && tsc -p tsconfig.test.json, plustsconfig.examples.jsonfor types).check:phantom-deps: green —✅ Every in-scope import is declared by the package that publishes it.The scoped subpath specifier (@object-ui/types/internal/retired-field-keys) is correctly attributed to the@object-ui/typespackage (already a declared dependency of both consumers), which is exactly the case this gate exists to police.check:control-bytes: green.check:designer-field-key-parity: green, unmodified.check-changeset-presence,check-changeset-fixed,check-changeset-no-majorall green.turbo/eslint .per package): 0 errors across all three (pre-existingno-explicit-anywarnings unrelated to this diff, same as the original round).Ablation — the barrel-wiring pin bites
Predicted before running: re-adding the barrel re-export block to
index.ts(pointed at the current./internal/location) would turn the new negative pin red —'the registry is NOT exported from the main package barrel'— while leaving the other 14 pins in that file green.grep -ccount forRETIRED_FIELD_KEY_TOMBSTONES|retiredFieldKeysForinindex.tswent0 → 2, and the blob hash changed vsHEAD(37ed4b6f… → 16288b57…).Test Files 1 failed (1)/Tests 1 failed | 14 passed (15), namingretired-field-key registry · hygiene > the registry is NOT exported from the main package barrelas the failure (expected true to be false).git diff HEADempty, andgit hash-objectmatchedHEAD's blob (37ed4b6f…both sides). The mutation script carried atrap ... EXIT INT TERMrestore.