From acbf0cb1f6e01fb609ad6c143189a0e273e8a1be Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 29 Sep 2026 13:55:09 +0000 Subject: [PATCH 1/7] feat(spec)!: the ADR-0087 migration chain leaves the root entry for @objectstack/spec/migrations (wip: entry + exports) Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx Co-authored-by: Claude --- packages/spec/browser-reachable-entries.json | 1 + packages/spec/package.json | 10 ++++++++++ packages/spec/src/index.ts | 9 +++++++-- packages/spec/tsup.config.ts | 13 +++++++++---- 4 files changed, 27 insertions(+), 6 deletions(-) diff --git a/packages/spec/browser-reachable-entries.json b/packages/spec/browser-reachable-entries.json index 85619ddb0f1..af94af17455 100644 --- a/packages/spec/browser-reachable-entries.json +++ b/packages/spec/browser-reachable-entries.json @@ -22,6 +22,7 @@ "./integration", "./kernel", "./marketplace", + "./migrations", "./qa", "./security", "./shared", diff --git a/packages/spec/package.json b/packages/spec/package.json index bd40d73e102..49b762e264f 100644 --- a/packages/spec/package.json +++ b/packages/spec/package.json @@ -237,6 +237,16 @@ "default": "./dist/meta-spelling/index.js" } }, + "./migrations": { + "import": { + "types": "./dist/migrations/index.d.mts", + "default": "./dist/migrations/index.mjs" + }, + "require": { + "types": "./dist/migrations/index.d.ts", + "default": "./dist/migrations/index.js" + } + }, "./openapi.json": "./json-schema/openapi.json", "./package.json": "./package.json" }, diff --git a/packages/spec/src/index.ts b/packages/spec/src/index.ts index 242ace28a0f..36e34ad7488 100644 --- a/packages/spec/src/index.ts +++ b/packages/spec/src/index.ts @@ -223,8 +223,13 @@ export type { MetadataCollectionInput, MapSupportedField, NormalizeStackInputOpt // Metadata conversion layer (ADR-0087 D2) — old-shape → canonical-shape transforms applied at load. export * from './conversions/index.js'; -// Metadata migration chain + change manifest (ADR-0087 D3/D4). -export * from './migrations/index.js'; +// The metadata migration chain + change manifest (ADR-0087 D3/D4) is NOT re-exported +// here: it is the `@objectstack/spec/migrations` subpath. Its registry is mostly the +// `os migrate meta` guidance text, and the registry's import-time work pins all of it +// into every bundle of the entry that carries it, so a root re-export made every +// consumer of any root name (a browser first screen included) download it. The +// conversion layer above stays here: `defineStack` / `normalizeStackInput` read it at +// run time. `root-entry-migrations-split.pin.test.ts` holds both halves. export { type PluginContext } from './kernel/plugin.zod'; diff --git a/packages/spec/tsup.config.ts b/packages/spec/tsup.config.ts index 0092a0e7b82..9caabdf5f40 100644 --- a/packages/spec/tsup.config.ts +++ b/packages/spec/tsup.config.ts @@ -96,7 +96,11 @@ const entries = [ // contract — per-entry self-contained bundling is unchanged (#8133 stays on // hold); this entry's whole graph is two pure modules, so "self-contained" // costs a few hundred bytes here by construction. - 'src/meta-spelling/index.ts' + 'src/meta-spelling/index.ts', + // The ADR-0087 migration chain + change manifest, off the root entry: its + // registry is mostly `os migrate meta` guidance text, and its import-time work + // kept all of it in every bundle of whichever entry carried it (#20646). + 'src/migrations/index.ts', ]; /** @@ -189,11 +193,12 @@ const swapServerOnlyGrammarArm: Plugin = { * reachable graph again: the peak grew with entries × graph, not with the * graph. `patches/tsup@8.5.1.patch` (wired in `pnpm-workspace.yaml`'s * `patchedDependencies`) keys every entry by the tsconfig's directory, so the - * 18 entries share one program. The patch names the exact tsup version, and + * entries share one program (18 when the table below was measured, 19 since the + * `./migrations` split). The patch names the exact tsup version, and * `pnpm install` refuses a patch that matches no installed package, so a tsup * bump cannot drop it silently. ⇒ On a tsup bump, re-derive the patch or - * retire it, then re-measure this table. If this pass ever shows 18 programs - * again, the entries are back to one program each. + * retire it, then re-measure this table. If this pass ever shows one program + * per entry again, the entries are back to one program each. * * `noCheck`: rollup-plugin-dts forces `noEmitOnError`, which makes the program * also semantically CHECK each file it emits — a type check From 4dc4b0138e70bb1da13124592fc824f028ec1a09 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 29 Sep 2026 14:00:02 +0000 Subject: [PATCH 2/7] feat(spec)!: register the migrations entry split (ADR-0087 D3) and move the migration importers to @objectstack/spec/migrations Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx Co-authored-by: Claude --- packages/cli/src/commands/migrate/meta.ts | 5 +- .../test/migrate-meta-default-range.test.ts | 2 +- .../test/migrate-meta-engine-guidance.test.ts | 2 +- .../src/protocol.stored-migration.test.ts | 2 +- ...on-overlapping-edge-conditions.pin.test.ts | 3 +- .../spec/scripts/build-migration-registry.ts | 9 +-- .../semantic/18.migrations-entry-split.ts | 56 +++++++++++++++++++ packages/spec/src/migrations/index.ts | 4 +- packages/spec/src/migrations/registry.ts | 52 +++++++++++++++++ 9 files changed, 123 insertions(+), 12 deletions(-) create mode 100644 packages/spec/src/migrations/entries/semantic/18.migrations-entry-split.ts diff --git a/packages/cli/src/commands/migrate/meta.ts b/packages/cli/src/commands/migrate/meta.ts index e80aee74e3e..4f7db0ffe6f 100644 --- a/packages/cli/src/commands/migrate/meta.ts +++ b/packages/cli/src/commands/migrate/meta.ts @@ -5,15 +5,14 @@ import { writeFileSync } from 'node:fs'; import { resolve } from 'node:path'; import { createInterface } from 'node:readline'; import chalk from 'chalk'; +import { ObjectStackDefinitionSchema, normalizeStackInput } from '@objectstack/spec'; import { - ObjectStackDefinitionSchema, applyMetaMigrations, composeSpecChanges, - normalizeStackInput, MigrationFloorError, MIGRATION_MAJORS, MIGRATION_SUPPORT_FLOOR, -} from '@objectstack/spec'; +} from '@objectstack/spec/migrations'; import { PROTOCOL_MAJOR, PROTOCOL_VERSION } from '@objectstack/spec/kernel'; import { FILE_REFERENCE_TYPES, REFERENCE_VALUE_TYPES, STRUCTURED_JSON_TYPES } from '@objectstack/spec/data'; import { FILE_REFERENCES_MIGRATION_ID, VALUE_SHAPES_MIGRATION_ID } from '@objectstack/spec/system'; diff --git a/packages/cli/test/migrate-meta-default-range.test.ts b/packages/cli/test/migrate-meta-default-range.test.ts index 2f315f806ad..edf2758a9f2 100644 --- a/packages/cli/test/migrate-meta-default-range.test.ts +++ b/packages/cli/test/migrate-meta-default-range.test.ts @@ -51,7 +51,7 @@ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { MIGRATIONS_BY_MAJOR, MIGRATION_MAJORS, MIGRATION_SUPPORT_FLOOR } from '@objectstack/spec'; +import { MIGRATIONS_BY_MAJOR, MIGRATION_MAJORS, MIGRATION_SUPPORT_FLOOR } from '@objectstack/spec/migrations'; import { PROTOCOL_MAJOR } from '@objectstack/spec/kernel'; import { childEnv } from './helpers/serve-process.js'; diff --git a/packages/cli/test/migrate-meta-engine-guidance.test.ts b/packages/cli/test/migrate-meta-engine-guidance.test.ts index b5de16e3d45..f6b04eb565b 100644 --- a/packages/cli/test/migrate-meta-engine-guidance.test.ts +++ b/packages/cli/test/migrate-meta-engine-guidance.test.ts @@ -59,7 +59,7 @@ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { MIGRATIONS_BY_MAJOR, MIGRATION_SUPPORT_FLOOR } from '@objectstack/spec'; +import { MIGRATIONS_BY_MAJOR, MIGRATION_SUPPORT_FLOOR } from '@objectstack/spec/migrations'; import { childEnv } from './helpers/serve-process.js'; const execFileP = promisify(execFile); diff --git a/packages/metadata-protocol/src/protocol.stored-migration.test.ts b/packages/metadata-protocol/src/protocol.stored-migration.test.ts index bc12317fcd8..a8795409c49 100644 --- a/packages/metadata-protocol/src/protocol.stored-migration.test.ts +++ b/packages/metadata-protocol/src/protocol.stored-migration.test.ts @@ -26,7 +26,7 @@ import { describe, expect, it } from 'vitest'; // of this package's (file, verb) pairs sat in the gate's DEBT ledger until // #5619 sank the two predicates into a package both sides already depend on. import { assertEngineDeleteDispatch, assertEngineUpdateDispatch, assertEngineFindOnePredicate } from '@objectstack/metadata-core'; -import { applyMetaMigrations } from '@objectstack/spec'; +import { applyMetaMigrations } from '@objectstack/spec/migrations'; import { ObjectStackProtocolImplementation } from './protocol.js'; import { DECISION_MODE_REVIEW_CONVERSION_ID, diff --git a/packages/services/service-automation/src/builtin/decision-overlapping-edge-conditions.pin.test.ts b/packages/services/service-automation/src/builtin/decision-overlapping-edge-conditions.pin.test.ts index 254b845cf2a..bdf99725f66 100644 --- a/packages/services/service-automation/src/builtin/decision-overlapping-edge-conditions.pin.test.ts +++ b/packages/services/service-automation/src/builtin/decision-overlapping-edge-conditions.pin.test.ts @@ -1,7 +1,8 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. import { describe, it, expect, beforeEach } from 'vitest'; -import { ALL_CONVERSIONS, applyMetaMigrations } from '@objectstack/spec'; +import { ALL_CONVERSIONS } from '@objectstack/spec'; +import { applyMetaMigrations } from '@objectstack/spec/migrations'; import { AutomationEngine } from '../engine.js'; import { registerLogicNodes } from './logic-nodes.js'; diff --git a/packages/spec/scripts/build-migration-registry.ts b/packages/spec/scripts/build-migration-registry.ts index 4e45f5f2cab..6356d8d0041 100644 --- a/packages/spec/scripts/build-migration-registry.ts +++ b/packages/spec/scripts/build-migration-registry.ts @@ -44,10 +44,11 @@ * * `scripts/adr-anchors.mjs` assembles its shards with `readdirSync` at read * time, so no aggregate is checked in at all. That option does not exist for - * this registry: `MIGRATIONS_BY_MAJOR` / `RETIRED_*_BY_MAJOR` are re-exported - * from `@objectstack/spec`'s ROOT barrel and reach browser bundles through it. - * A `node:fs` read anywhere in that graph breaks every consumer that bundles - * the package — spec's `src/` is deliberately free of node builtins today. A + * this registry: `MIGRATIONS_BY_MAJOR` / `RETIRED_*_BY_MAJOR` are exported by + * the published `@objectstack/spec/migrations` entry (the ROOT barrel re-exported + * them until the #20646 entry split), so a consumer that bundles that entry + * bundles this module graph. A `node:fs` read anywhere in it breaks every such + * consumer — spec's `src/` is deliberately free of node builtins today. A * bundled library needs a STATIC module graph, and a static graph over N * entries needs one file that names all N. * diff --git a/packages/spec/src/migrations/entries/semantic/18.migrations-entry-split.ts b/packages/spec/src/migrations/entries/semantic/18.migrations-entry-split.ts new file mode 100644 index 00000000000..b6e46843f5d --- /dev/null +++ b/packages/spec/src/migrations/entries/semantic/18.migrations-entry-split.ts @@ -0,0 +1,56 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +import type { SemanticMigration } from '../../types.js'; + +// The ADR-0087 D3/D4 surface leaves the package root for its own subpath, so the +// migration registry's text stops riding in every bundle of the root entry. The +// conversion layer (D2) stays on the root: the authoring funnel reads it at run time. +// +// Form D: no tracker number anywhere in the author-shown text; the decision is +// stated in words. +// +// No backticks in `surface` — build-upgrade-guide.ts renders it inside a code +// span already, and a nested backtick would close it. +export const entry: SemanticMigration = { + id: 'migrations-entry-split', + surface: + 'The ADR-0087 migration chain and change manifest, imported from the package root ' + + '@objectstack/spec: MIGRATIONS_BY_MAJOR, MIGRATION_MAJORS, MIGRATION_SUPPORT_FLOOR, ' + + 'RETIRED_KEYS_BY_MAJOR, RETIRED_DEFS_BY_MAJOR, applyMetaMigrations, composeMigrationChain, ' + + 'MigrationFloorError, composeSpecChanges, composeReleaseChanges, the seven change-manifest ' + + 'schemas (SpecChangesSchema, SpecConvertedSchema, SpecMigratedSchema, SpecSurfaceAddSchema, ' + + 'SpecSurfaceRemoveSchema, SpecReleaseChangesSchema, SpecReleaseSurfaceSchema), and the types ' + + 'MigrationStep, MigrationApplication, MigrationChainResult, MigrationHopResult, MigrationTodo, ' + + 'SemanticMigration, SpecChanges, SpecConverted, SpecMigrated, SpecSurfaceAdd, SpecSurfaceRemove, ' + + 'SpecReleaseChanges, SpecReleaseSurface, SurfaceDiff, ReleaseSurfaceDiff and ' + + 'PreviousReleaseRegistries', + replacement: + 'the same names, unchanged, imported from `@objectstack/spec/migrations` — change the import ' + + 'path and nothing else. The chain, its steps and semantic entries, the retired-key and ' + + 'retired-def tables and the change-manifest schemas are the same objects, and ' + + '`objectstack migrate meta` replays the same chain. The ADR-0087 conversion layer stays on the ' + + 'package root: `ALL_CONVERSIONS`, `CONVERSIONS_BY_MAJOR`, `applyConversions`, ' + + '`applyConversionsToFlow`, `applyConversionsToStoredItem`, `collectConversionNotices`, the ' + + 'three `CONVERSION_*_CODE` constants and their types still import from `@objectstack/spec`.', + reason: + 'The maintainer ruled that the console first-screen size ceiling is raised now and paid back at ' + + 'the source; this split is that payback. The migration registry is mostly the guidance text ' + + '`objectstack migrate meta` prints, and the package root re-exported it. The registry does work ' + + 'when its module loads (the list of majors and each step\'s rationale are computed then), so no ' + + 'bundler could prove it unused, and all of that text rode in every bundle of the root, whatever ' + + 'the consumer imported: 1,758,310 of the root ESM bundle\'s 3,761,633 bytes. With the chain on ' + + 'its own subpath the CommonJS root is 2,008,899 bytes instead of 3,775,445, and a browser bundle ' + + 'of the ten names the Studio console imports from the root drops from 700,438 to 301,204 bytes ' + + 'gzipped. The conversion layer does not move: `defineStack` and `normalizeStackInput` read it ' + + 'at run time, so moving its names would narrow the root and shrink it by 1,828 bytes. The split ' + + 'moves an import path, which is TypeScript source rather than metadata — nothing authors, ' + + 'stores or parses it — so there is no source a D2 conversion could rewrite, and the move is ' + + 'recorded here.', + acceptanceCriteria: + 'No code imports any of these names from the package root `@objectstack/spec` — each such ' + + 'import is a TS2305 "has no exported member" error after upgrade, and at run time the binding ' + + 'is undefined. The same names import cleanly from `@objectstack/spec/migrations`. No metadata ' + + 'document, stored row or JSON Schema reference needs editing: the chain, its tables and the ' + + 'schemas did not change, and `objectstack migrate meta` rewrites the same documents it did ' + + 'before.', +}; diff --git a/packages/spec/src/migrations/index.ts b/packages/spec/src/migrations/index.ts index e93036be7c6..53a463595a5 100644 --- a/packages/spec/src/migrations/index.ts +++ b/packages/spec/src/migrations/index.ts @@ -1,7 +1,9 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. /** - * Metadata migration chain + change manifest (ADR-0087 D3/D4) — public surface. + * Metadata migration chain + change manifest (ADR-0087 D3/D4) — public surface, + * published as its own entry, `@objectstack/spec/migrations`, and deliberately NOT + * re-exported from the package root (see the note in `../index.ts`). * * The permanent, replayable chain that carries metadata from the support floor * (`MIGRATION_SUPPORT_FLOOR`, below which `--from` refuses) to current in one diff --git a/packages/spec/src/migrations/registry.ts b/packages/spec/src/migrations/registry.ts index fda8905b1ec..60e5e1a3b00 100644 --- a/packages/spec/src/migrations/registry.ts +++ b/packages/spec/src/migrations/registry.ts @@ -13271,6 +13271,58 @@ const step18: MigrationStep = { + 'set stays exactly `DEFAULT_METADATA_TYPE_REGISTRY` plus item-population growth, ' + 'before and after.', }, + // The ADR-0087 D3/D4 surface leaves the package root for its own subpath, so the + // migration registry's text stops riding in every bundle of the root entry. The + // conversion layer (D2) stays on the root: the authoring funnel reads it at run time. + // + // Form D: no tracker number anywhere in the author-shown text; the decision is + // stated in words. + // + // No backticks in `surface` — build-upgrade-guide.ts renders it inside a code + // span already, and a nested backtick would close it. + { + id: 'migrations-entry-split', + surface: + 'The ADR-0087 migration chain and change manifest, imported from the package root ' + + '@objectstack/spec: MIGRATIONS_BY_MAJOR, MIGRATION_MAJORS, MIGRATION_SUPPORT_FLOOR, ' + + 'RETIRED_KEYS_BY_MAJOR, RETIRED_DEFS_BY_MAJOR, applyMetaMigrations, composeMigrationChain, ' + + 'MigrationFloorError, composeSpecChanges, composeReleaseChanges, the seven change-manifest ' + + 'schemas (SpecChangesSchema, SpecConvertedSchema, SpecMigratedSchema, SpecSurfaceAddSchema, ' + + 'SpecSurfaceRemoveSchema, SpecReleaseChangesSchema, SpecReleaseSurfaceSchema), and the types ' + + 'MigrationStep, MigrationApplication, MigrationChainResult, MigrationHopResult, MigrationTodo, ' + + 'SemanticMigration, SpecChanges, SpecConverted, SpecMigrated, SpecSurfaceAdd, SpecSurfaceRemove, ' + + 'SpecReleaseChanges, SpecReleaseSurface, SurfaceDiff, ReleaseSurfaceDiff and ' + + 'PreviousReleaseRegistries', + replacement: + 'the same names, unchanged, imported from `@objectstack/spec/migrations` — change the import ' + + 'path and nothing else. The chain, its steps and semantic entries, the retired-key and ' + + 'retired-def tables and the change-manifest schemas are the same objects, and ' + + '`objectstack migrate meta` replays the same chain. The ADR-0087 conversion layer stays on the ' + + 'package root: `ALL_CONVERSIONS`, `CONVERSIONS_BY_MAJOR`, `applyConversions`, ' + + '`applyConversionsToFlow`, `applyConversionsToStoredItem`, `collectConversionNotices`, the ' + + 'three `CONVERSION_*_CODE` constants and their types still import from `@objectstack/spec`.', + reason: + 'The maintainer ruled that the console first-screen size ceiling is raised now and paid back at ' + + 'the source; this split is that payback. The migration registry is mostly the guidance text ' + + '`objectstack migrate meta` prints, and the package root re-exported it. The registry does work ' + + 'when its module loads (the list of majors and each step\'s rationale are computed then), so no ' + + 'bundler could prove it unused, and all of that text rode in every bundle of the root, whatever ' + + 'the consumer imported: 1,758,310 of the root ESM bundle\'s 3,761,633 bytes. With the chain on ' + + 'its own subpath the CommonJS root is 2,008,899 bytes instead of 3,775,445, and a browser bundle ' + + 'of the ten names the Studio console imports from the root drops from 700,438 to 301,204 bytes ' + + 'gzipped. The conversion layer does not move: `defineStack` and `normalizeStackInput` read it ' + + 'at run time, so moving its names would narrow the root and shrink it by 1,828 bytes. The split ' + + 'moves an import path, which is TypeScript source rather than metadata — nothing authors, ' + + 'stores or parses it — so there is no source a D2 conversion could rewrite, and the move is ' + + 'recorded here.', + acceptanceCriteria: + 'No code imports any of these names from the package root `@objectstack/spec` — each such ' + + 'import is a TS2305 "has no exported member" error after upgrade, and at run time the binding ' + + 'is undefined. The same names import cleanly from `@objectstack/spec/migrations`. No metadata ' + + 'document, stored row or JSON Schema reference needs editing: the chain, its tables and the ' + + 'schemas did not change, and `objectstack migrate meta` rewrites the same documents it did ' + + 'before.', + }, { id: 'object-block-sort-item-array', surface: From 4f3020d955671fa4363a679e7c89ac18a95330b4 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 29 Sep 2026 14:03:55 +0000 Subject: [PATCH 3/7] chore(spec): regenerate api-surface and export-origins for the ./migrations entry Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx Co-authored-by: Claude --- packages/spec/api-surface/migrations.json | 39 ++++++++++++++++++++ packages/spec/api-surface/root.json | 33 ----------------- packages/spec/export-origins/migrations.json | 39 ++++++++++++++++++++ packages/spec/export-origins/root.json | 33 ----------------- 4 files changed, 78 insertions(+), 66 deletions(-) create mode 100644 packages/spec/api-surface/migrations.json create mode 100644 packages/spec/export-origins/migrations.json diff --git a/packages/spec/api-surface/migrations.json b/packages/spec/api-surface/migrations.json new file mode 100644 index 00000000000..024adfcdcd5 --- /dev/null +++ b/packages/spec/api-surface/migrations.json @@ -0,0 +1,39 @@ +{ + "description": "Every exported `name (kind)` of one published entry point of @objectstack/spec — the breadth half of the ADR-0059 backward-compatibility gate. Sharded by entry point (#5837) so two PRs touching different entry points never share a file. Reads the BUILT dist/*.d.ts: regenerate with `pnpm --filter @objectstack/spec gen:api-surface` after a real build.", + "entry": "./migrations", + "exports": [ + "MIGRATIONS_BY_MAJOR (const)", + "MIGRATION_MAJORS (const)", + "MIGRATION_SUPPORT_FLOOR (const)", + "MigrationApplication (interface)", + "MigrationChainResult (interface)", + "MigrationFloorError (class)", + "MigrationHopResult (interface)", + "MigrationStep (interface)", + "MigrationTodo (interface)", + "PreviousReleaseRegistries (interface)", + "RETIRED_DEFS_BY_MAJOR (const)", + "RETIRED_KEYS_BY_MAJOR (const)", + "ReleaseSurfaceDiff (interface)", + "SemanticMigration (interface)", + "SpecChanges (type)", + "SpecChangesSchema (const)", + "SpecConverted (type)", + "SpecConvertedSchema (const)", + "SpecMigrated (type)", + "SpecMigratedSchema (const)", + "SpecReleaseChanges (type)", + "SpecReleaseChangesSchema (const)", + "SpecReleaseSurface (type)", + "SpecReleaseSurfaceSchema (const)", + "SpecSurfaceAdd (type)", + "SpecSurfaceAddSchema (const)", + "SpecSurfaceRemove (type)", + "SpecSurfaceRemoveSchema (const)", + "SurfaceDiff (interface)", + "applyMetaMigrations (function)", + "composeMigrationChain (function)", + "composeReleaseChanges (function)", + "composeSpecChanges (function)" + ] +} diff --git a/packages/spec/api-surface/root.json b/packages/spec/api-surface/root.json index 8c02d15cf7b..13cff2c5d59 100644 --- a/packages/spec/api-surface/root.json +++ b/packages/spec/api-surface/root.json @@ -96,18 +96,9 @@ "MEMBERSHIP_ROLE_MEMBER (const)", "MEMBERSHIP_ROLE_OWNER (const)", "METADATA_ALIASES (const)", - "MIGRATIONS_BY_MAJOR (const)", - "MIGRATION_MAJORS (const)", - "MIGRATION_SUPPORT_FLOOR (const)", "MapSupportedField (type)", "MetadataCollectionInput (type)", "MetadataConversion (type)", - "MigrationApplication (interface)", - "MigrationChainResult (interface)", - "MigrationFloorError (class)", - "MigrationHopResult (interface)", - "MigrationStep (interface)", - "MigrationTodo (interface)", "NavigationItem (type)", "NavigationItemInput (type)", "NormalizeStackInputOptions (interface)", @@ -133,36 +124,16 @@ "PredicateInput (type)", "PredicateInputSchema (const)", "PredicateSchema (const)", - "PreviousReleaseRegistries (interface)", - "RETIRED_DEFS_BY_MAJOR (const)", - "RETIRED_KEYS_BY_MAJOR (const)", "RecordStagePackageBody (type)", "RecordStagePackageBodyParsed (type)", "RecordStagePackageBodySchema (const)", - "ReleaseSurfaceDiff (interface)", "STACK_DEFINITION_KEYS (const)", "STACK_KEY_GUIDANCE (const)", "STACK_RUNTIME_MEMBERS (const)", - "SemanticMigration (interface)", "Skill (type)", - "SpecChanges (type)", - "SpecChangesSchema (const)", - "SpecConverted (type)", - "SpecConvertedSchema (const)", - "SpecMigrated (type)", - "SpecMigratedSchema (const)", - "SpecReleaseChanges (type)", - "SpecReleaseChangesSchema (const)", - "SpecReleaseSurface (type)", - "SpecReleaseSurfaceSchema (const)", - "SpecSurfaceAdd (type)", - "SpecSurfaceAddSchema (const)", - "SpecSurfaceRemove (type)", - "SpecSurfaceRemoveSchema (const)", "StackDefinitionKey (type)", "StateNodeConfig (type)", "StoredConversionOptions (type)", - "SurfaceDiff (interface)", "TemplateExpressionInputSchema (const)", "Tool (type)", "UnknownAuthoringKeyFinding (interface)", @@ -170,13 +141,9 @@ "applyConversions (function)", "applyConversionsToFlow (function)", "applyConversionsToStoredItem (function)", - "applyMetaMigrations (function)", "cel (function)", "classifyRequiredCapability (function)", "collectConversionNotices (function)", - "composeMigrationChain (function)", - "composeReleaseChanges (function)", - "composeSpecChanges (function)", "composeStacks (function)", "createEvalUser (function)", "cron (function)", diff --git a/packages/spec/export-origins/migrations.json b/packages/spec/export-origins/migrations.json new file mode 100644 index 00000000000..2e457314845 --- /dev/null +++ b/packages/spec/export-origins/migrations.json @@ -0,0 +1,39 @@ +{ + "description": "Which SOURCE DECLARATION each name exported by one public entry point of @objectstack/spec resolves to, after its alias chain is unwound: `# ()`. Two exports share an origin string iff they are the same declaration — so equal origins across two entries are a harmless re-export, and different origins under one name are the #4411 dual-source trap. Generated from src/ (no build needed) and read by the export-surface pin tests, which compare against it instead of each building their own ts.createProgram — that was ~55s of compilation per CI lap and a non-deterministic timeout that ejected unrelated PRs from the merge queue (#4796). Sharded by entry point (#5837) so two retirement PRs never share a file. Carries NO line numbers: the pins asserted the line as `\\d+`, and recording it would rewrite this artifact on every edit that shifts a line in any .zod.ts. Regenerate with `pnpm --filter @objectstack/spec gen:export-origins` and read the diff.", + "entry": "./migrations", + "exports": { + "MIGRATIONS_BY_MAJOR": "src/migrations/registry.ts#MIGRATIONS_BY_MAJOR (const)", + "MIGRATION_MAJORS": "src/migrations/registry.ts#MIGRATION_MAJORS (const)", + "MIGRATION_SUPPORT_FLOOR": "src/migrations/registry.ts#MIGRATION_SUPPORT_FLOOR (const)", + "MigrationApplication": "src/migrations/types.ts#MigrationApplication (interface)", + "MigrationChainResult": "src/migrations/types.ts#MigrationChainResult (interface)", + "MigrationFloorError": "src/migrations/chain.ts#MigrationFloorError (class)", + "MigrationHopResult": "src/migrations/types.ts#MigrationHopResult (interface)", + "MigrationStep": "src/migrations/types.ts#MigrationStep (interface)", + "MigrationTodo": "src/migrations/types.ts#MigrationTodo (interface)", + "PreviousReleaseRegistries": "src/migrations/spec-changes.ts#PreviousReleaseRegistries (interface)", + "RETIRED_DEFS_BY_MAJOR": "src/migrations/registry.ts#RETIRED_DEFS_BY_MAJOR (const)", + "RETIRED_KEYS_BY_MAJOR": "src/migrations/registry.ts#RETIRED_KEYS_BY_MAJOR (const)", + "ReleaseSurfaceDiff": "src/migrations/spec-changes.ts#ReleaseSurfaceDiff (interface)", + "SemanticMigration": "src/migrations/types.ts#SemanticMigration (interface)", + "SpecChanges": "src/migrations/spec-changes.ts#SpecChanges (type)", + "SpecChangesSchema": "src/migrations/spec-changes.ts#SpecChangesSchema (const)", + "SpecConverted": "src/migrations/spec-changes.ts#SpecConverted (type)", + "SpecConvertedSchema": "src/migrations/spec-changes.ts#SpecConvertedSchema (const)", + "SpecMigrated": "src/migrations/spec-changes.ts#SpecMigrated (type)", + "SpecMigratedSchema": "src/migrations/spec-changes.ts#SpecMigratedSchema (const)", + "SpecReleaseChanges": "src/migrations/spec-changes.ts#SpecReleaseChanges (type)", + "SpecReleaseChangesSchema": "src/migrations/spec-changes.ts#SpecReleaseChangesSchema (const)", + "SpecReleaseSurface": "src/migrations/spec-changes.ts#SpecReleaseSurface (type)", + "SpecReleaseSurfaceSchema": "src/migrations/spec-changes.ts#SpecReleaseSurfaceSchema (const)", + "SpecSurfaceAdd": "src/migrations/spec-changes.ts#SpecSurfaceAdd (type)", + "SpecSurfaceAddSchema": "src/migrations/spec-changes.ts#SpecSurfaceAddSchema (const)", + "SpecSurfaceRemove": "src/migrations/spec-changes.ts#SpecSurfaceRemove (type)", + "SpecSurfaceRemoveSchema": "src/migrations/spec-changes.ts#SpecSurfaceRemoveSchema (const)", + "SurfaceDiff": "src/migrations/spec-changes.ts#SurfaceDiff (interface)", + "applyMetaMigrations": "src/migrations/chain.ts#applyMetaMigrations (function)", + "composeMigrationChain": "src/migrations/chain.ts#composeMigrationChain (function)", + "composeReleaseChanges": "src/migrations/spec-changes.ts#composeReleaseChanges (function)", + "composeSpecChanges": "src/migrations/spec-changes.ts#composeSpecChanges (function)" + } +} diff --git a/packages/spec/export-origins/root.json b/packages/spec/export-origins/root.json index 3beef4f743f..1b8db1e9a91 100644 --- a/packages/spec/export-origins/root.json +++ b/packages/spec/export-origins/root.json @@ -95,18 +95,9 @@ "MEMBERSHIP_ROLE_MEMBER": "src/identity/membership-role.ts#MEMBERSHIP_ROLE_MEMBER (const)", "MEMBERSHIP_ROLE_OWNER": "src/identity/membership-role.ts#MEMBERSHIP_ROLE_OWNER (const)", "METADATA_ALIASES": "src/shared/metadata-collection.zod.ts#METADATA_ALIASES (const)", - "MIGRATIONS_BY_MAJOR": "src/migrations/registry.ts#MIGRATIONS_BY_MAJOR (const)", - "MIGRATION_MAJORS": "src/migrations/registry.ts#MIGRATION_MAJORS (const)", - "MIGRATION_SUPPORT_FLOOR": "src/migrations/registry.ts#MIGRATION_SUPPORT_FLOOR (const)", "MapSupportedField": "src/shared/metadata-collection.zod.ts#MapSupportedField (type)", "MetadataCollectionInput": "src/shared/metadata-collection.zod.ts#MetadataCollectionInput (type)", "MetadataConversion": "src/conversions/types.ts#MetadataConversion (type)", - "MigrationApplication": "src/migrations/types.ts#MigrationApplication (interface)", - "MigrationChainResult": "src/migrations/types.ts#MigrationChainResult (interface)", - "MigrationFloorError": "src/migrations/chain.ts#MigrationFloorError (class)", - "MigrationHopResult": "src/migrations/types.ts#MigrationHopResult (interface)", - "MigrationStep": "src/migrations/types.ts#MigrationStep (interface)", - "MigrationTodo": "src/migrations/types.ts#MigrationTodo (interface)", "NavigationItem": "src/ui/app.zod.ts#NavigationItem (type)", "NavigationItemInput": "src/ui/app.zod.ts#NavigationItemInput (type)", "NormalizeStackInputOptions": "src/shared/metadata-collection.zod.ts#NormalizeStackInputOptions (interface)", @@ -132,36 +123,16 @@ "PredicateInput": "src/shared/expression.zod.ts#PredicateInput (type)", "PredicateInputSchema": "src/shared/expression.zod.ts#PredicateInputSchema (const)", "PredicateSchema": "src/shared/expression.zod.ts#PredicateSchema (const)", - "PreviousReleaseRegistries": "src/migrations/spec-changes.ts#PreviousReleaseRegistries (interface)", - "RETIRED_DEFS_BY_MAJOR": "src/migrations/registry.ts#RETIRED_DEFS_BY_MAJOR (const)", - "RETIRED_KEYS_BY_MAJOR": "src/migrations/registry.ts#RETIRED_KEYS_BY_MAJOR (const)", "RecordStagePackageBody": "src/stack.zod.ts#RecordStagePackageBody (type)", "RecordStagePackageBodyParsed": "src/stack.zod.ts#RecordStagePackageBodyParsed (type)", "RecordStagePackageBodySchema": "src/stack.zod.ts#RecordStagePackageBodySchema (const)", - "ReleaseSurfaceDiff": "src/migrations/spec-changes.ts#ReleaseSurfaceDiff (interface)", "STACK_DEFINITION_KEYS": "src/stack.zod.ts#STACK_DEFINITION_KEYS (const)", "STACK_KEY_GUIDANCE": "src/data/authoring-key-lint.ts#STACK_KEY_GUIDANCE (const)", "STACK_RUNTIME_MEMBERS": "src/data/authoring-key-lint.ts#STACK_RUNTIME_MEMBERS (const)", - "SemanticMigration": "src/migrations/types.ts#SemanticMigration (interface)", "Skill": "src/ai/skill.zod.ts#Skill (type)", - "SpecChanges": "src/migrations/spec-changes.ts#SpecChanges (type)", - "SpecChangesSchema": "src/migrations/spec-changes.ts#SpecChangesSchema (const)", - "SpecConverted": "src/migrations/spec-changes.ts#SpecConverted (type)", - "SpecConvertedSchema": "src/migrations/spec-changes.ts#SpecConvertedSchema (const)", - "SpecMigrated": "src/migrations/spec-changes.ts#SpecMigrated (type)", - "SpecMigratedSchema": "src/migrations/spec-changes.ts#SpecMigratedSchema (const)", - "SpecReleaseChanges": "src/migrations/spec-changes.ts#SpecReleaseChanges (type)", - "SpecReleaseChangesSchema": "src/migrations/spec-changes.ts#SpecReleaseChangesSchema (const)", - "SpecReleaseSurface": "src/migrations/spec-changes.ts#SpecReleaseSurface (type)", - "SpecReleaseSurfaceSchema": "src/migrations/spec-changes.ts#SpecReleaseSurfaceSchema (const)", - "SpecSurfaceAdd": "src/migrations/spec-changes.ts#SpecSurfaceAdd (type)", - "SpecSurfaceAddSchema": "src/migrations/spec-changes.ts#SpecSurfaceAddSchema (const)", - "SpecSurfaceRemove": "src/migrations/spec-changes.ts#SpecSurfaceRemove (type)", - "SpecSurfaceRemoveSchema": "src/migrations/spec-changes.ts#SpecSurfaceRemoveSchema (const)", "StackDefinitionKey": "src/stack.zod.ts#StackDefinitionKey (type)", "StateNodeConfig": "src/automation/state-machine.zod.ts#StateNodeConfig (type)", "StoredConversionOptions": "src/conversions/stored.ts#StoredConversionOptions (type)", - "SurfaceDiff": "src/migrations/spec-changes.ts#SurfaceDiff (interface)", "TemplateExpressionInputSchema": "src/shared/expression.zod.ts#TemplateExpressionInputSchema (const)", "Tool": "src/ai/tool.zod.ts#Tool (type)", "UnknownAuthoringKeyFinding": "src/data/authoring-key-lint.ts#UnknownAuthoringKeyFinding (interface)", @@ -169,13 +140,9 @@ "applyConversions": "src/conversions/apply.ts#applyConversions (function)", "applyConversionsToFlow": "src/conversions/apply.ts#applyConversionsToFlow (function)", "applyConversionsToStoredItem": "src/conversions/stored.ts#applyConversionsToStoredItem (function)", - "applyMetaMigrations": "src/migrations/chain.ts#applyMetaMigrations (function)", "cel": "src/shared/expression.zod.ts#cel (function)", "classifyRequiredCapability": "src/kernel/platform-capabilities.ts#classifyRequiredCapability (function)", "collectConversionNotices": "src/conversions/apply.ts#collectConversionNotices (function)", - "composeMigrationChain": "src/migrations/chain.ts#composeMigrationChain (function)", - "composeReleaseChanges": "src/migrations/spec-changes.ts#composeReleaseChanges (function)", - "composeSpecChanges": "src/migrations/spec-changes.ts#composeSpecChanges (function)", "composeStacks": "src/stack.zod.ts#composeStacks (function)", "createEvalUser": "src/identity/eval-user.zod.ts#createEvalUser (function)", "cron": "src/shared/expression.zod.ts#cron (function)", From f891339cecb6e67ecb9a429729298c75f0e12e22 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 29 Sep 2026 14:05:46 +0000 Subject: [PATCH 4/7] test(spec): pin the migrations entry split; changesets for spec and cli Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx Co-authored-by: Claude --- ...20646-cli-migrate-meta-migrations-entry.md | 7 + .changeset/20646-migrations-entry-split.md | 49 +++++ .../root-entry-migrations-split.pin.test.ts | 196 ++++++++++++++++++ 3 files changed, 252 insertions(+) create mode 100644 .changeset/20646-cli-migrate-meta-migrations-entry.md create mode 100644 .changeset/20646-migrations-entry-split.md create mode 100644 packages/spec/src/root-entry-migrations-split.pin.test.ts diff --git a/.changeset/20646-cli-migrate-meta-migrations-entry.md b/.changeset/20646-cli-migrate-meta-migrations-entry.md new file mode 100644 index 00000000000..b8579c6667e --- /dev/null +++ b/.changeset/20646-cli-migrate-meta-migrations-entry.md @@ -0,0 +1,7 @@ +--- +'@objectstack/cli': patch +--- + +fix(cli): `os migrate meta` takes the migration chain from `@objectstack/spec/migrations` (#20646) + +`@objectstack/spec` moved the ADR-0087 migration chain and change-manifest names (`applyMetaMigrations`, `composeSpecChanges`, `MigrationFloorError`, `MIGRATION_MAJORS`, `MIGRATION_SUPPORT_FLOOR`, …) off the package root into the new `@objectstack/spec/migrations` entry, so the command now imports them from there. It replays the same chain and prints the same guidance; nothing a user types or reads changes. diff --git a/.changeset/20646-migrations-entry-split.md b/.changeset/20646-migrations-entry-split.md new file mode 100644 index 00000000000..bfa8b77108a --- /dev/null +++ b/.changeset/20646-migrations-entry-split.md @@ -0,0 +1,49 @@ +--- +'@objectstack/spec': minor +--- + +feat(spec)!: the ADR-0087 migration chain leaves the package root for the new `@objectstack/spec/migrations` entry (#20646) + +**BREAKING** — the migration chain and change-manifest names (ADR-0087 D3/D4), with their types, are no longer exported from the package root `@objectstack/spec`. They are exported, unchanged, from the new entry `@objectstack/spec/migrations`. + +A `major`-class change — an existing import path stops resolving for these names — recorded as `minor` under the launch-window convention. + +**Why.** The migration registry is mostly the guidance text `objectstack migrate meta` prints, and the root re-exported it. The registry does work when its module loads (the list of majors and each step's rationale are computed then), so no bundler could prove it unused, and all of that text rode in every bundle of the root, whatever the consumer imported. This is the source-side payback of the Studio console's first-screen ceiling raise that the maintainer ruled on the 17.5.0 upgrade. Measured on the splitting PR (tsup build, gzip -9): + +| | before | after | +| --- | --- | --- | +| `dist/index.js` (CommonJS root) | 3,775,445 B / 1,066,456 B gzip | 2,008,899 B / 565,285 B gzip | +| `dist/browser/index.mjs` (the ESM root a browser bundler pulls) | 3,759,705 B / 1,064,775 B gzip | 1,993,837 B / 563,676 B gzip | +| a browser bundle of the ten names the Studio console imports from the root (rolldown, minified) | 700,438 B gzip | 301,204 B gzip | + +The ADR-0087 **conversion layer stays on the root**: `defineStack` and `normalizeStackInput` read it at run time, so its names (`ALL_CONVERSIONS`, `CONVERSIONS_BY_MAJOR`, `applyConversions`, `applyConversionsToFlow`, `applyConversionsToStoredItem`, `collectConversionNotices`, the `CONVERSION_*_CODE` constants and their types) import from `@objectstack/spec` exactly as before. + +### FROM → TO + +| removed from `@objectstack/spec` | import instead from | +| --- | --- | +| `MIGRATIONS_BY_MAJOR`, `MIGRATION_MAJORS`, `MIGRATION_SUPPORT_FLOOR` | `@objectstack/spec/migrations` | +| `RETIRED_KEYS_BY_MAJOR`, `RETIRED_DEFS_BY_MAJOR` | `@objectstack/spec/migrations` | +| `applyMetaMigrations`, `composeMigrationChain`, `MigrationFloorError` | `@objectstack/spec/migrations` | +| `composeSpecChanges`, `composeReleaseChanges` | `@objectstack/spec/migrations` | +| `SpecChangesSchema`, `SpecConvertedSchema`, `SpecMigratedSchema`, `SpecSurfaceAddSchema`, `SpecSurfaceRemoveSchema`, `SpecReleaseChangesSchema`, `SpecReleaseSurfaceSchema` | `@objectstack/spec/migrations` | +| types `MigrationStep`, `MigrationApplication`, `MigrationChainResult`, `MigrationHopResult`, `MigrationTodo`, `SemanticMigration`, `SpecChanges`, `SpecConverted`, `SpecMigrated`, `SpecSurfaceAdd`, `SpecSurfaceRemove`, `SpecReleaseChanges`, `SpecReleaseSurface`, `SurfaceDiff`, `ReleaseSurfaceDiff`, `PreviousReleaseRegistries` | `@objectstack/spec/migrations` | + +**The one-line fix: change the import path.** + +```ts +// before +import { applyMetaMigrations, MIGRATION_SUPPORT_FLOOR } from '@objectstack/spec'; +// after +import { applyMetaMigrations, MIGRATION_SUPPORT_FLOOR } from '@objectstack/spec/migrations'; +``` + +The compiler finds every site: `TS2305` ("Module '"@objectstack/spec"' has no exported member …"); at run time the binding is `undefined`. Nothing else changes: the chain, its steps and semantic entries, the retired-key and retired-def tables and the change-manifest schemas are the same objects, and `objectstack migrate meta` replays the same chain. + +⚠️ **Out-of-repo consumers are NOT MEASURED beyond objectui.** Inside this repository the moved names had five importers — `os migrate meta` (the only runtime one) and four tests — all moved in the same PR. objectui at the pinned `.objectui-sha` imports none of the moved names from anywhere. The `cloud` repository was not measured. + +The ADR-0087 D3 semantic entry `migrations-entry-split` carries the judgement: an import path is TypeScript source, not metadata, so there is no source a D2 conversion could rewrite. + +Clause-②: yes (narrowing) + + diff --git a/packages/spec/src/root-entry-migrations-split.pin.test.ts b/packages/spec/src/root-entry-migrations-split.pin.test.ts new file mode 100644 index 00000000000..2efb18aa4e0 --- /dev/null +++ b/packages/spec/src/root-entry-migrations-split.pin.test.ts @@ -0,0 +1,196 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Pin: the ADR-0087 migration chain lives on `@objectstack/spec/migrations`, + * never on the package root (#20646). + * + * The migration registry is mostly the guidance text `objectstack migrate meta` + * prints, and it does work when its module loads (the list of majors and each + * step's rationale are computed then). A consumer's bundler therefore cannot + * prove it unused, so whichever entry's module graph reaches it carries ALL of + * it into every bundle of that entry. While the root re-exported it, that was + * 1,758,310 of the root ESM bundle's 3,761,633 bytes, downloaded by every + * browser first screen that imports any root name. It moved to its own subpath; + * the conversion layer (ADR-0087 D2) stays on the root, because `defineStack` / + * `normalizeStackInput` read it at run time. + * + * Nothing else holds that boundary. `browser-reachable-entries.json` leaves + * both entries unjudged and no gate weighs a bundle, so one `export *` in + * `./index.ts` — or one value import of a registry name from any module the + * root reaches — would put the text back into every root bundle with every + * gate green. So this holds four things: + * + * 1. the root module exports none of the moved names, and `./migrations` + * exports every one of them (the runtime namespaces, read from source); + * 2. the root api-surface shard lists none of them, types included, and the + * `./migrations` shard lists all of them (the published declarations); + * 3. the root's STATIC value-import graph does not reach + * `migrations/registry.ts` at all — the edge a bundler follows, which a + * re-export check alone would miss; + * 4. the exports map publishes `./migrations` to the built files. + * + * Each negative half has a positive control on the same instrument, so a walk + * or a read that silently stopped seeing anything fails instead of passing. + */ + +import { describe, expect, it } from 'vitest'; +import { existsSync, readFileSync } from 'node:fs'; +import { dirname, relative, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import * as root from './index'; +import * as migrations from './migrations/index'; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const SRC = HERE; +const PKG = resolve(HERE, '..'); + +/** The runtime values that left the root for `./migrations`. */ +const MOVED_VALUES = [ + 'MIGRATIONS_BY_MAJOR', + 'MIGRATION_MAJORS', + 'MIGRATION_SUPPORT_FLOOR', + 'RETIRED_DEFS_BY_MAJOR', + 'RETIRED_KEYS_BY_MAJOR', + 'applyMetaMigrations', + 'composeMigrationChain', + 'MigrationFloorError', + 'composeReleaseChanges', + 'composeSpecChanges', + 'SpecChangesSchema', + 'SpecConvertedSchema', + 'SpecMigratedSchema', + 'SpecSurfaceAddSchema', + 'SpecReleaseChangesSchema', + 'SpecReleaseSurfaceSchema', + 'SpecSurfaceRemoveSchema', +] as const; + +/** The types that left with them — visible only in the published declarations. */ +const MOVED_TYPES = [ + 'MigrationApplication', + 'MigrationChainResult', + 'MigrationHopResult', + 'MigrationStep', + 'MigrationTodo', + 'SemanticMigration', + 'PreviousReleaseRegistries', + 'ReleaseSurfaceDiff', + 'SpecChanges', + 'SpecConverted', + 'SpecMigrated', + 'SpecReleaseChanges', + 'SpecReleaseSurface', + 'SpecSurfaceAdd', + 'SpecSurfaceRemove', + 'SurfaceDiff', +] as const; + +/** The conversion layer: it stays on the root, read by the authoring funnel. */ +const STAYING_CONVERSION_VALUES = [ + 'ALL_CONVERSIONS', + 'CONVERSIONS_BY_MAJOR', + 'applyConversions', + 'applyConversionsToStoredItem', +] as const; + +/** The names an api-surface shard lists, `name (kind)` → `name`. */ +function shardNames(entry: string): Set { + const shard = JSON.parse(readFileSync(resolve(PKG, 'api-surface', `${entry}.json`), 'utf8')) as { + exports: string[]; + }; + return new Set(shard.exports.map((row) => row.replace(/ \([a-z-]+\)$/, ''))); +} + +// A relative specifier in a value-bearing `import … from` / `export … from` +// statement, or a bare side-effect `import '…'`. Statements that open with +// `import type` / `export type` are dropped before this runs: they are erased +// at build time and cost a bundle nothing. (The walker of +// `api/api-entry-graph.pin.test.ts`, same rules.) +const EDGE = /(?:^|\n)\s*(?:import|export)\s[^;]*?from\s*['"](\.[^'"]+)['"]|(?:^|\n)\s*import\s*['"](\.[^'"]+)['"]/g; +const TYPE_ONLY = /(?:^|\n)\s*(?:import|export)\s+type\s[^;]*?from\s*['"][^'"]+['"]\s*;?/g; + +function resolveSpecifier(fromFile: string, spec: string): string { + const base = resolve(dirname(fromFile), spec.replace(/\.js$/, '')); + for (const candidate of [`${base}.ts`, `${base}/index.ts`, base]) { + if (existsSync(candidate) && candidate.endsWith('.ts')) return candidate; + } + // An unresolved edge would make the walk incomplete, and an incomplete walk + // reporting "not reached" is the false green this pin exists to prevent. + throw new Error(`unresolved relative import ${spec} in ${relative(SRC, fromFile)}`); +} + +function valueGraph(entry: string): Set { + const seen = new Set(); + const queue = [resolve(SRC, entry)]; + while (queue.length > 0) { + const file = queue.pop()!; + if (seen.has(file)) continue; + seen.add(file); + // Comment lines are dropped line by line, never with a `/* … */` regex: + // glob strings such as `src/**/*.ts` would open a false comment. + const source = readFileSync(file, 'utf8') + .split('\n') + .filter((line) => !/^\s*(\*|\/\*|\/\/)/.test(line)) + .join('\n') + .replace(TYPE_ONLY, '\n'); + for (const m of source.matchAll(EDGE)) { + queue.push(resolveSpecifier(file, (m[1] ?? m[2])!)); + } + } + return new Set([...seen].map((f) => relative(SRC, f).split('\\').join('/'))); +} + +describe('the migration chain is `@objectstack/spec/migrations`, not the root (#20646)', () => { + it('the root module exports none of the moved values', () => { + expect(MOVED_VALUES.filter((name) => name in root)).toEqual([]); + }); + + it('`./migrations` exports every moved value, and nothing it exports is on the root', () => { + expect(MOVED_VALUES.filter((name) => !(name in migrations))).toEqual([]); + // Every runtime name of the subpath, not only the declared list: a name + // added to `./migrations` later must not also appear on the root. + const namespace = Object.keys(migrations); + expect(namespace.length).toBeGreaterThanOrEqual(MOVED_VALUES.length); + expect(namespace.filter((name) => name in root)).toEqual([]); + }); + + it('positive control: the conversion layer is still on the root', () => { + expect(STAYING_CONVERSION_VALUES.filter((name) => !(name in root))).toEqual([]); + }); + + it('the root api-surface shard lists none of the moved names, types included', () => { + const rootShard = shardNames('root'); + expect([...MOVED_VALUES, ...MOVED_TYPES].filter((name) => rootShard.has(name))).toEqual([]); + // Positive control on the same read: the shard is the real root surface. + expect(STAYING_CONVERSION_VALUES.filter((name) => !rootShard.has(name))).toEqual([]); + }); + + it('the `./migrations` api-surface shard lists every moved name', () => { + const migrationsShard = shardNames('migrations'); + expect([...MOVED_VALUES, ...MOVED_TYPES].filter((name) => !migrationsShard.has(name))).toEqual([]); + }); + + it('the root value-import graph does not reach the migration registry', () => { + const rootGraph = valueGraph('index.ts'); + expect([...rootGraph].filter((f) => f.startsWith('migrations/'))).toEqual([]); + // Anti-vacuity: the walk covers the root's real graph, conversions included + // (134 value-graph modules when the split landed; far below that means the + // edge pattern stopped matching, not that the entry shrank). + expect(rootGraph.size).toBeGreaterThan(100); + expect(rootGraph.has('conversions/registry.ts')).toBe(true); + }); + + it('positive control: the same walk DOES reach the registry from `./migrations`', () => { + expect(valueGraph('migrations/index.ts').has('migrations/registry.ts')).toBe(true); + }); + + it('the exports map publishes `./migrations` to the built entry', () => { + const manifest = JSON.parse(readFileSync(resolve(PKG, 'package.json'), 'utf8')) as { + exports: Record; + }; + expect(manifest.exports['./migrations']).toEqual({ + import: { types: './dist/migrations/index.d.mts', default: './dist/migrations/index.mjs' }, + require: { types: './dist/migrations/index.d.ts', default: './dist/migrations/index.js' }, + }); + }); +}); From c1dcebf255b1fb7d1174805c91795abb04c95d6e Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 29 Sep 2026 15:00:51 +0000 Subject: [PATCH 5/7] docs(spec): list the migrations subpath; title the migrations category as an entry, not a protocol namespace Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx Co-authored-by: Claude --- content/docs/deployment/troubleshooting.mdx | 2 +- packages/spec/scripts/lib/category-title.ts | 8 +++++++- packages/spec/scripts/lib/schema-closure.ts | 11 +++++++---- packages/spec/scripts/schema-closure.test.ts | 10 +++++++--- 4 files changed, 22 insertions(+), 9 deletions(-) diff --git a/content/docs/deployment/troubleshooting.mdx b/content/docs/deployment/troubleshooting.mdx index 011e7729463..bccc8251857 100644 --- a/content/docs/deployment/troubleshooting.mdx +++ b/content/docs/deployment/troubleshooting.mdx @@ -345,7 +345,7 @@ import { FieldSchema } from '@objectstack/spec/data'; import { ErrorResponseSchema } from '@objectstack/spec/api'; ``` -Available subpaths (the `./*` entries of the package's `exports` map, in its order): `data`, `system`, `kernel`, `ai`, `automation`, `api`, `api-assembled`, `ui`, `contracts`, `integration`, `security`, `studio`, `marketplace`, `qa`, `identity`, `shared`, `meta-spelling`. +Available subpaths (the `./*` entries of the package's `exports` map, in its order): `data`, `system`, `kernel`, `ai`, `automation`, `api`, `api-assembled`, `ui`, `contracts`, `integration`, `security`, `studio`, `marketplace`, `qa`, `identity`, `shared`, `meta-spelling`, `migrations`. --- diff --git a/packages/spec/scripts/lib/category-title.ts b/packages/spec/scripts/lib/category-title.ts index 255dc13f4d2..2f7f67bab35 100644 --- a/packages/spec/scripts/lib/category-title.ts +++ b/packages/spec/scripts/lib/category-title.ts @@ -92,7 +92,13 @@ export const CATEGORY_TITLES: Readonly> = { // "Protocol": the entry carries the spelling contract's data and folds // without the schema machinery every Protocol category links. 'meta-spelling': 'Meta-Spelling Vocabulary', - migrations: 'Migrations Protocol', + // [#20646] The ADR-0087 migration chain + change manifest, published as its + // own entry once it left the package root (its registry text rode in every + // root bundle). The protocol-upgrade TOOLING surface, not a metadata + // protocol domain: titled "Entry", not "Protocol", so + // `check-docs-spec-enumerations.mjs` counts it as a subpath and never as a + // protocol namespace — the `api-assembled` precedent above. + migrations: 'Migrations Entry', qa: 'QA Protocol', security: 'Security Protocol', shared: 'Shared Protocol', diff --git a/packages/spec/scripts/lib/schema-closure.ts b/packages/spec/scripts/lib/schema-closure.ts index 5ee292ea8c7..6f67077ca56 100644 --- a/packages/spec/scripts/lib/schema-closure.ts +++ b/packages/spec/scripts/lib/schema-closure.ts @@ -65,10 +65,13 @@ import { isSplitEntry, SPLIT_ENTRIES, type SplitEntry } from './split-entries'; * `no JSON Schema`, `without the schema machinery`) returns the `meta-spelling` * citations below and nothing for either of them; the only text that mentions * their missing directory at all is a comment in `build-docs.ts` §2 recording - * it as an asymmetry that comment's guard deliberately does NOT act on. Both - * are titled `... Protocol` in `CATEGORY_TITLES` — the same word every - * category WITH a schema closure is titled with — where `meta-spelling` is - * titled `Meta-Spelling Vocabulary` for exactly this reason. + * it as an asymmetry that comment's guard deliberately does NOT act on. Neither + * is titled with the schema-free word in `CATEGORY_TITLES`: `conversions` is + * `... Protocol` — the word every category WITH a schema closure is titled + * with — and `migrations`, a published entry since the #20646 split, is + * `Migrations Entry` (the `api-assembled` word for a published entry that is + * not a protocol namespace), where `meta-spelling` is titled + * `Meta-Spelling Vocabulary` for exactly this reason. * * `migrations` is the further one from an exemption, not the nearer: its * `spec-changes.ts` exports five real Zod schemas, so "no schema closure" is diff --git a/packages/spec/scripts/schema-closure.test.ts b/packages/spec/scripts/schema-closure.test.ts index 5e77a035a97..7b8b16263ff 100644 --- a/packages/spec/scripts/schema-closure.test.ts +++ b/packages/spec/scripts/schema-closure.test.ts @@ -83,13 +83,17 @@ describe('CATEGORIES_WITHOUT_SCHEMA_CLOSURE — the declared list', () => { it('is corroborated by the declared TITLES — Vocabulary, not Protocol', () => { // `CATEGORY_TITLES` is an independent, hand-declared surface, and it draws // the same boundary: the schema-free entry is titled "Vocabulary" while the - // two undeclared ones carry the same word every category WITH a schema - // closure carries. That agreement is why one entry is defensible and the + // two undeclared ones do not: `conversions` carries the word every category + // WITH a schema closure carries, and `migrations` — a published entry since + // the #20646 split — the `Entry` word `api-assembled` carries for a + // published entry that is not a protocol namespace. Neither claims + // "Vocabulary". That agreement is why one entry is defensible and the // other two are not. expect(CATEGORY_TITLES['meta-spelling']).toBe('Meta-Spelling Vocabulary'); expect(CATEGORY_TITLES['meta-spelling']).not.toContain('Protocol'); expect(CATEGORY_TITLES.conversions).toContain('Protocol'); - expect(CATEGORY_TITLES.migrations).toContain('Protocol'); + expect(CATEGORY_TITLES.migrations).toBe('Migrations Entry'); + expect(CATEGORY_TITLES.migrations).not.toContain('Vocabulary'); }); }); From c4fdf1105ecc4dfa0cd04f00129f486cca73754f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 29 Sep 2026 16:43:37 +0000 Subject: [PATCH 6/7] test(spec): the export-origins entry list carries ./migrations Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx Co-authored-by: Claude --- packages/spec/scripts/export-origins.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/spec/scripts/export-origins.test.ts b/packages/spec/scripts/export-origins.test.ts index 353990f7305..e430285a544 100644 --- a/packages/spec/scripts/export-origins.test.ts +++ b/packages/spec/scripts/export-origins.test.ts @@ -58,6 +58,7 @@ const ENTRY_NAMESPACES: ReadonlyArray<[string, () => Promise]> = [ ['./kernel', () => import('../src/kernel/index')], ['./marketplace', () => import('../src/marketplace/index')], ['./meta-spelling', () => import('../src/meta-spelling/index')], + ['./migrations', () => import('../src/migrations/index')], ['./qa', () => import('../src/qa/index')], ['./security', () => import('../src/security/index')], ['./shared', () => import('../src/shared/index')], From 8558334ab5b9a16fe73f898cc7dd9f3985799e51 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 29 Sep 2026 17:39:15 +0000 Subject: [PATCH 7/7] docs(spec): quote the entry split's byte readings from its merge base Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx Co-authored-by: Claude --- .changeset/20646-migrations-entry-split.md | 8 ++++---- .../entries/semantic/18.migrations-entry-split.ts | 8 ++++---- packages/spec/src/migrations/registry.ts | 8 ++++---- packages/spec/src/root-entry-migrations-split.pin.test.ts | 2 +- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.changeset/20646-migrations-entry-split.md b/.changeset/20646-migrations-entry-split.md index bfa8b77108a..1a1ccf7d65d 100644 --- a/.changeset/20646-migrations-entry-split.md +++ b/.changeset/20646-migrations-entry-split.md @@ -8,13 +8,13 @@ feat(spec)!: the ADR-0087 migration chain leaves the package root for the new `@ A `major`-class change — an existing import path stops resolving for these names — recorded as `minor` under the launch-window convention. -**Why.** The migration registry is mostly the guidance text `objectstack migrate meta` prints, and the root re-exported it. The registry does work when its module loads (the list of majors and each step's rationale are computed then), so no bundler could prove it unused, and all of that text rode in every bundle of the root, whatever the consumer imported. This is the source-side payback of the Studio console's first-screen ceiling raise that the maintainer ruled on the 17.5.0 upgrade. Measured on the splitting PR (tsup build, gzip -9): +**Why.** The migration registry is mostly the guidance text `objectstack migrate meta` prints, and the root re-exported it. The registry does work when its module loads (the list of majors and each step's rationale are computed then), so no bundler could prove it unused, and all of that text rode in every bundle of the root, whatever the consumer imported. This is the source-side payback of the Studio console's first-screen ceiling raise that the maintainer ruled on the 17.5.0 upgrade. Measured on the splitting PR against its merge base `1a75e39d4a` (tsup build, gzip -9): | | before | after | | --- | --- | --- | -| `dist/index.js` (CommonJS root) | 3,775,445 B / 1,066,456 B gzip | 2,008,899 B / 565,285 B gzip | -| `dist/browser/index.mjs` (the ESM root a browser bundler pulls) | 3,759,705 B / 1,064,775 B gzip | 1,993,837 B / 563,676 B gzip | -| a browser bundle of the ten names the Studio console imports from the root (rolldown, minified) | 700,438 B gzip | 301,204 B gzip | +| `dist/index.js` (CommonJS root) | 3,780,033 B / 1,067,061 B gzip | 2,009,810 B / 565,386 B gzip | +| `dist/browser/index.mjs` (the ESM root a browser bundler pulls) | 3,764,293 B / 1,065,388 B gzip | 1,994,748 B / 563,787 B gzip | +| a browser bundle of the ten names the Studio console imports from the root (rolldown, minified) | 700,884 B gzip | 301,287 B gzip | The ADR-0087 **conversion layer stays on the root**: `defineStack` and `normalizeStackInput` read it at run time, so its names (`ALL_CONVERSIONS`, `CONVERSIONS_BY_MAJOR`, `applyConversions`, `applyConversionsToFlow`, `applyConversionsToStoredItem`, `collectConversionNotices`, the `CONVERSION_*_CODE` constants and their types) import from `@objectstack/spec` exactly as before. diff --git a/packages/spec/src/migrations/entries/semantic/18.migrations-entry-split.ts b/packages/spec/src/migrations/entries/semantic/18.migrations-entry-split.ts index b6e46843f5d..39ebe0e087d 100644 --- a/packages/spec/src/migrations/entries/semantic/18.migrations-entry-split.ts +++ b/packages/spec/src/migrations/entries/semantic/18.migrations-entry-split.ts @@ -38,11 +38,11 @@ export const entry: SemanticMigration = { + '`objectstack migrate meta` prints, and the package root re-exported it. The registry does work ' + 'when its module loads (the list of majors and each step\'s rationale are computed then), so no ' + 'bundler could prove it unused, and all of that text rode in every bundle of the root, whatever ' - + 'the consumer imported: 1,758,310 of the root ESM bundle\'s 3,761,633 bytes. With the chain on ' - + 'its own subpath the CommonJS root is 2,008,899 bytes instead of 3,775,445, and a browser bundle ' - + 'of the ten names the Studio console imports from the root drops from 700,438 to 301,204 bytes ' + + 'the consumer imported: 1,761,987 of the root ESM bundle\'s 3,766,221 bytes. With the chain on ' + + 'its own subpath the CommonJS root is 2,009,810 bytes instead of 3,780,033, and a browser bundle ' + + 'of the ten names the Studio console imports from the root drops from 700,884 to 301,287 bytes ' + 'gzipped. The conversion layer does not move: `defineStack` and `normalizeStackInput` read it ' - + 'at run time, so moving its names would narrow the root and shrink it by 1,828 bytes. The split ' + + 'at run time, so moving its names would narrow the root and shrink it by under two kilobytes. The split ' + 'moves an import path, which is TypeScript source rather than metadata — nothing authors, ' + 'stores or parses it — so there is no source a D2 conversion could rewrite, and the move is ' + 'recorded here.', diff --git a/packages/spec/src/migrations/registry.ts b/packages/spec/src/migrations/registry.ts index 34616021845..c733f37a749 100644 --- a/packages/spec/src/migrations/registry.ts +++ b/packages/spec/src/migrations/registry.ts @@ -13307,11 +13307,11 @@ const step18: MigrationStep = { + '`objectstack migrate meta` prints, and the package root re-exported it. The registry does work ' + 'when its module loads (the list of majors and each step\'s rationale are computed then), so no ' + 'bundler could prove it unused, and all of that text rode in every bundle of the root, whatever ' - + 'the consumer imported: 1,758,310 of the root ESM bundle\'s 3,761,633 bytes. With the chain on ' - + 'its own subpath the CommonJS root is 2,008,899 bytes instead of 3,775,445, and a browser bundle ' - + 'of the ten names the Studio console imports from the root drops from 700,438 to 301,204 bytes ' + + 'the consumer imported: 1,761,987 of the root ESM bundle\'s 3,766,221 bytes. With the chain on ' + + 'its own subpath the CommonJS root is 2,009,810 bytes instead of 3,780,033, and a browser bundle ' + + 'of the ten names the Studio console imports from the root drops from 700,884 to 301,287 bytes ' + 'gzipped. The conversion layer does not move: `defineStack` and `normalizeStackInput` read it ' - + 'at run time, so moving its names would narrow the root and shrink it by 1,828 bytes. The split ' + + 'at run time, so moving its names would narrow the root and shrink it by under two kilobytes. The split ' + 'moves an import path, which is TypeScript source rather than metadata — nothing authors, ' + 'stores or parses it — so there is no source a D2 conversion could rewrite, and the move is ' + 'recorded here.', diff --git a/packages/spec/src/root-entry-migrations-split.pin.test.ts b/packages/spec/src/root-entry-migrations-split.pin.test.ts index 2efb18aa4e0..88ccd6c23cb 100644 --- a/packages/spec/src/root-entry-migrations-split.pin.test.ts +++ b/packages/spec/src/root-entry-migrations-split.pin.test.ts @@ -9,7 +9,7 @@ * step's rationale are computed then). A consumer's bundler therefore cannot * prove it unused, so whichever entry's module graph reaches it carries ALL of * it into every bundle of that entry. While the root re-exported it, that was - * 1,758,310 of the root ESM bundle's 3,761,633 bytes, downloaded by every + * 1,761,987 of the root ESM bundle's 3,766,221 bytes, downloaded by every * browser first screen that imports any root name. It moved to its own subpath; * the conversion layer (ADR-0087 D2) stays on the root, because `defineStack` / * `normalizeStackInput` read it at run time.