Uh oh!
There was an error while loading. Please reload this page.
fix(app-shell): DraftChangesPanel addresses /meta item routes in the singular - #5361
Conversation
…singular (#5356) The panel is data-driven: each draft's `type` comes straight from the `/api/v1/meta/_drafts` feed's stored rows with a `typeof` check and no fold, so a row stored under a pre-#7894 plural spelling made the Console emit `/api/v1/meta/objects` and `/api/v1/meta/objects/:name`. The `/meta` type segment is singular — always (objectstack#9180). Fold the stored spelling once, at the boundary where feed rows become panel entries; both `/meta` item routes, the grouping and the type heading read that one value, so a route added later has no raw spelling in scope. The table mirrors `@objectstack/spec/shared`'s `META_URL_TO_SINGULAR` / `canonicalMetaUrlType` — the `/meta`-specific contract, not the manifest-level `PLURAL_TO_SINGULAR` — and a parity test pins the mirror against the real export key for key. It is mirrored rather than imported because the import measures +213.4 KB min / +60.1 KB gz on the console's eager graph, which lazy loading cannot move (the `vendor-objectstack` chunk group claims it); the table's doc comment carries the measurement. Emit-side only: at-rest plural residue is untouched, with no write path, no migration and no change to what the server accepts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RV6yuVCxymHYE16PL9vQkE
…a local copy of its table (#5356) `scripts/check-spec-symbol-derivation.mjs` refuses a local `META_URL_TO_SINGULAR` / `canonicalMetaUrlType`: a spec-named symbol must be derived from `@objectstack/spec`, and its header explains that a faithful copy is exactly the fork the guard exists to prevent — reference identity is the only thing that distinguishes a re-export from one. So the panel imports the real fold. The measured price is +213.4 KB minified / +60.1 KB gzipped on the console's eager graph, which lazy loading cannot move (the `vendor-objectstack` chunk group claims every `@objectstack/*` module but the linter). That is recorded at the import site and tracked as #5359, whose levers both live in `apps/console/vite.config.ts`, outside this card's file surface. The mirror's parity test goes with the mirror; the behavioural route pins, which assert the addresses the panel actually emits from a plural fixture, are what carry the fix. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RV6yuVCxymHYE16PL9vQkE
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
os-support-ai
commented
Aug 20, 2026
Checked: the fold is reachable from |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#5356
Part of objectstack-ai/objectstack#9180
The
/metatype segment is singular — always (objectstack#9180, maintainer 2026-08-16; step ③ scoped to internal spelling correction by the 2026-08-17 re-weigh). This is the objectui half.The defect
packages/app-shell/src/preview/DraftChangesPanel.tsxis data-driven, not authored: each entry'stypecomes straight from the/api/v1/meta/_draftsfeed's stored rows, guarded by atypeofcheck and nothing else (:62on the branch point,bdf8cf76e). It is then interpolated into two/metaitem routes — the published-name list at:76and the item body read at:112— so a row stored under a pre-#7894 plural spelling made the Console request/api/v1/meta/objectsand/api/v1/meta/objects/ticket.Re-verified at the branch point: all four measured sites present as stated, and these two remain the only variable-typed
/metaroute emitters in objectui — every other site interpolates a name into a literal singular type (meta/object/…,meta/dataset/…,meta/app/…).The fix
One fold, at the one boundary where a stored spelling enters the module — where feed rows become
DraftChangeEntry. Everything below (both item routes, the grouping, the type heading) reads the folded value, so a route added later has no raw spelling in scope to interpolate. That is the durable shaperest-server.tsargues for after its own per-gate normalization came back as the same hole eight days later (objectstack#6241).Which helper, and why that one. objectui has no singular-folding helper of its own — this is the first place that needs one. The ecosystem's is
canonicalMetaUrlTypefrom@objectstack/spec/shared(a declared dependency of@object-ui/app-shell), the published fold for exactly a/meta/:typepath segment (objectstack#7894, #8424), and the same oneRestServerfolds incoming requests with.Deliberately not
pluralToSingular: that map's keys aredefineStack()collection properties, andfield,seed,external_catalogandtranslationare absent from it because none of them is a stack-level collection. That absence is also how the residue being folded here got written —PUT /meta/fields/…fell through to the permissive plugin-type path and minted rows undertype='fields'whilePUT /meta/field/…was refused. The spellings this fold accepts are exactly the ones such residue can carry.Fold, never strip.
capabilitiesmaps tocapability; areplace(/s$/, '')emitscapabilitie. The pin below asserts against that specific wrong answer.No laundering. Emit-side only: residue stored under a plural type stays exactly as it is on the server. No write path, no migration, no server-side tolerance, and nothing about what the server accepts changes.
A local copy of the table was tried first, and is refused mechanically
The first implementation mirrored the 35-entry spelling table locally with a parity test pinning it against the real export, to avoid the weight below.
node scripts/check-spec-symbol-derivation.mjsrejects that: a spec-named symbol must be derived from@objectstack/spec, and the guard's own header explains that a faithful copy is precisely the fork it exists to prevent — reference identity is the only thing distinguishing a re-export from one, and itsALLOWmap governs deliberate divergence, which a mirror is not. So the import is the sanctioned spelling, and the gate is green on this branch.The price, measured, so it is not invisible: +213.4 KB minified / +60.1 KB gzipped on the console's eager graph (esbuild, on a graph already holding the
/ui+/kernelentries app-shell imports statically). The panel is reached through static imports fromConsoleLayout, and lazy-loading cannot move those bytes — the console'svendor-objectstackchunk group claims every@objectstack/*module except the linter, and that group's chunk is a static import of the app entry (objectui#5266). Recorded at the import site and filed as #5359; both levers for paying it down live inapps/console/vite.config.ts, outside this card's file surface.Verification
2efc90d19, all from the repo root (17 packages own a standalonevitest.config.ts, so a package-scoped run would use a different config than CI does):pnpm exec vitest run packages/app-shell/src/preview/__tests__/DraftChangesPanel.test.tsx— 13 passed, 5 of them new route pins driven by a fixture that carries a plural storedtype(a route-spelling fix passes vacuously against an already-singular fixture).pnpm --filter @object-ui/app-shell type-check— green (tsc --noEmit+tsconfig.test.json), after building the dependency closure.eslinton both changed files — 0 errors (2 pre-existing warnings on untouched lines).check-spec-symbol-derivation,check-control-bytes,check-changeset-presence,check-changeset-no-major,check-phantom-dependencies,check-package-self-import— all exit 0.Reverse verification. Reverting the fold to
type: d.type as stringturns 4 of the 5 pins red, with the plural addresses in the failure text:The fifth pin — an already-canonical type and a plugin-registered kind pass through unchanged — stays green in both legs by design; it is the passthrough control. Restoring the fold returns 13/13.
Per leg: no build artifact sits between the edit and the thing under test. The test imports
../DraftChangesPanelby relative path and vitest transforms it from source, so neither leg needed a rebuild. The only prebuilt artifact in play is the installed@objectstack/specdist providing the fold, which the ablation does not touch.Changeset:
patchfor@object-ui/app-shell.Generated by Claude Code
Generated by Claude Code