From 4f94bf07b148bf936d683dc98d86c6261d0b3b1f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 25 Aug 2026 02:22:07 +0000 Subject: [PATCH 1/3] fix(spec,core): give pluralToSingular a schema-free home on /meta-spelling (#11503) Restores the #10096 standing invariant for core's store-key fold: the defineStack manifest-collection vocabulary moves to a leaf module on the /meta-spelling entry graph, /shared re-exports it, and core imports the fold from /meta-spelling instead of /shared. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01K93P8PbH7kVjsAXkqzH1zY --- .changeset/eleven-maps-move-home.md | 21 ++++ .../core/src/metadata-service-contract.ts | 18 ++- packages/spec/README.md | 4 +- .../spec/scripts/build-meta-url-spelling.ts | 13 +- packages/spec/src/meta-spelling/index.ts | 42 +++++-- .../manifest-collection-spelling.test.ts | 66 ++++++++++ .../manifest-collection-spelling.ts | 115 ++++++++++++++++++ .../src/shared/metadata-collection.zod.ts | 83 +++---------- scripts/check-stack-collection-maps.mjs | 6 +- 9 files changed, 276 insertions(+), 92 deletions(-) create mode 100644 .changeset/eleven-maps-move-home.md create mode 100644 packages/spec/src/meta-spelling/manifest-collection-spelling.test.ts create mode 100644 packages/spec/src/meta-spelling/manifest-collection-spelling.ts diff --git a/.changeset/eleven-maps-move-home.md b/.changeset/eleven-maps-move-home.md new file mode 100644 index 0000000000..569dbbbd9d --- /dev/null +++ b/.changeset/eleven-maps-move-home.md @@ -0,0 +1,21 @@ +--- +"@objectstack/spec": minor +"@objectstack/core": patch +--- + +Restore the #10096 standing invariant (「浏览器可达的 spec 导出面必须 +schema-free」) for `@objectstack/core`'s plural→singular store-key fold. + +`@objectstack/spec`: the `defineStack()` manifest-collection vocabulary +(`PLURAL_TO_SINGULAR`, `SINGULAR_TO_PLURAL`, `pluralToSingular`, +`singularToPlural`) moved to a schema-free module and is now ALSO exported +from the sanctioned schema-free entry `@objectstack/spec/meta-spelling` +(widened per the #10096 ruling's reference pattern). `@objectstack/spec/shared` +keeps the same four symbols as re-exports — no consumer-visible removal. The +manifest map and `META_URL_TO_SINGULAR` remain deliberately distinct contracts +(#8424). + +`@objectstack/core`: `canonicalMetadataServiceType`'s one value import moves +from `@objectstack/spec/shared` to `@objectstack/spec/meta-spelling`, so +browser consumers of `@objectstack/core` (every `@objectstack/client` bundle) +no longer link the zod schema closure through the store-key fold. diff --git a/packages/core/src/metadata-service-contract.ts b/packages/core/src/metadata-service-contract.ts index aca9df9235..5372052b74 100644 --- a/packages/core/src/metadata-service-contract.ts +++ b/packages/core/src/metadata-service-contract.ts @@ -88,12 +88,18 @@ // `@objectstack/driver-memory`, 39 test files dead at load between them). The // typed literal below keeps the closed-set compile check without the runtime // import — the `packages/spec/src/contracts/storage-service.ts` pattern. -// `/shared` cannot get the same treatment: `pluralToSingular` is a runtime -// value and its map has ONE owner (#7378 row 2 — copying it here would be the -// per-implementation folk normalization the ruling forbids), so the consumer -// configs carry a `/shared` alias entry instead. +// `pluralToSingular` cannot get the same treatment: it is a runtime value and +// its map has ONE owner (#7378 row 2 — copying it here would be the +// per-implementation folk normalization the ruling forbids). It is imported +// from `/meta-spelling` — the schema-free vocabulary entry — NOT `/shared`: +// this module is on every browser consumer's eager graph through +// `@objectstack/client`, and a `/shared` value import here linked the whole +// zod schema closure into every browser bundle (#11503, measured in the +// console; #10096 standing principle: 「浏览器可达的 spec 导出面必须 +// schema-free」). Consumer vitest configs resolve the subpath through their +// anchored `@objectstack/spec/` alias rule (#9457). import type { StandardErrorCode } from '@objectstack/spec/api'; -import { pluralToSingular } from '@objectstack/spec/shared'; +import { pluralToSingular } from '@objectstack/spec/meta-spelling'; /** The standard catalog's generic argument-validation code, type-checked against the closed set. */ const REGISTER_REFUSAL_CODE: StandardErrorCode = 'VALIDATION_ERROR'; @@ -103,7 +109,7 @@ const REGISTER_REFUSAL_CODE: StandardErrorCode = 'VALIDATION_ERROR'; * (#7378 row 2). Folds a plural manifest spelling to the singular metadata * type name (`'objects'` → `'object'`, `'views'` → `'view'`, …) through the * platform's one plural↔singular map (`PLURAL_TO_SINGULAR`, - * `@objectstack/spec/shared`); a name with no plural mapping — which includes + * `@objectstack/spec/meta-spelling`); a name with no plural mapping — which includes * every canonical singular type — passes through unchanged. */ export function canonicalMetadataServiceType(type: string): string { diff --git a/packages/spec/README.md b/packages/spec/README.md index 7271cbe1d4..f149ad339e 100644 --- a/packages/spec/README.md +++ b/packages/spec/README.md @@ -16,7 +16,9 @@ The **Source of Truth** for the ObjectStack Protocol. Contains strictly typed Zo The package publishes one entry per protocol domain (`@objectstack/spec/data`, `/ui`, `/kernel`, …) plus fine-grained vocabulary entries -(`@objectstack/spec/meta-spelling` — the `/meta/:type` URL-spelling contract). +(`@objectstack/spec/meta-spelling` — the `/meta/:type` URL-spelling contract +and the `defineStack()` manifest-collection vocabulary, two deliberately +distinct maps per objectstack#8424). Each entry is a self-contained bundle: what an entry's module graph reaches is what every consumer of that entry pays for. diff --git a/packages/spec/scripts/build-meta-url-spelling.ts b/packages/spec/scripts/build-meta-url-spelling.ts index 005b5e61e7..8df3bf9895 100644 --- a/packages/spec/scripts/build-meta-url-spelling.ts +++ b/packages/spec/scripts/build-meta-url-spelling.ts @@ -7,10 +7,12 @@ * * ## Why the map is generated instead of derived at module load * - * The map's two sources are heavy on purpose-unrelated weight: + * The map's two sources were heavy on purpose-unrelated weight: * `DEFAULT_METADATA_TYPE_REGISTRY` lives in the kernel zod graph, and - * `PLURAL_TO_SINGULAR` sits in a module that reaches the ADR-0087 conversion - * layer. Deriving at module load therefore made ONE string fold cost a + * `PLURAL_TO_SINGULAR` sat in a module that reaches the ADR-0087 conversion + * layer (it has since moved to the schema-free + * `meta-spelling/manifest-collection-spelling`, #11503 — the registry input + * still makes build time the right place). Deriving at module load made ONE string fold cost a * browser consumer +60.1 KB gzipped (#10096's measurement). The 2026-08-20 * maintainer ruling minted the standing principle 「浏览器可达的 spec 导出面必 * 须 schema-free」 and approved moving the derivation to build time: this @@ -40,7 +42,10 @@ import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { DEFAULT_METADATA_TYPE_REGISTRY } from '../src/kernel/metadata-plugin.zod'; -import { PLURAL_TO_SINGULAR } from '../src/shared/metadata-collection.zod'; +// [#11503] `PLURAL_TO_SINGULAR`'s declaration moved to the schema-free +// manifest-collection module (the doc above still describes where it USED to +// sit); the derivation input is the same one owner, read at its new home. +import { PLURAL_TO_SINGULAR } from '../src/meta-spelling/manifest-collection-spelling'; import { restPluralOfMetaType, camelCaseOf } from '../src/meta-spelling/metadata-url-spelling'; const OUT = join( diff --git a/packages/spec/src/meta-spelling/index.ts b/packages/spec/src/meta-spelling/index.ts index 6868dd9503..a9e1c2163d 100644 --- a/packages/spec/src/meta-spelling/index.ts +++ b/packages/spec/src/meta-spelling/index.ts @@ -1,26 +1,39 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. /** - * `@objectstack/spec/meta-spelling` — the `/meta/:type` URL-spelling contract, + * `@objectstack/spec/meta-spelling` — metadata type-name spelling vocabulary, * schema-free (#10096, maintainer ruling 2026-08-20). * * This entry exists so a browser/client consumer who needs to spell (or fold, - * or refuse) a `/meta/:type` path segment pays a few hundred bytes instead of - * linking the zod schema graph — the measured cost of reaching the same four - * symbols through `@objectstack/spec/shared` was +60.1 KB gzipped on a graph - * that already carried `/ui` and `/kernel`. + * or refuse) a metadata type name pays a few hundred bytes instead of linking + * the zod schema graph — the measured cost of reaching the same symbols + * through `@objectstack/spec/shared` was +60.1 KB gzipped on a graph that + * already carried `/ui` and `/kernel`. * * The standing principle this entry implements (recorded verbatim, * untranslated): 「浏览器可达的 spec 导出面必须 schema-free」 — a * browser-reachable export surface carries vocabulary (maps, folds, enums, * pure predicates) without linking the zod schema/validation machinery. * - * The published surface is the same four symbols `/shared` carries (#8424 — - * `/shared` keeps them; this entry is additive, one declaration re-exported): - * the map, the fold, and the two refusal verdicts. The map is derived at BUILD - * time from `PLURAL_TO_SINGULAR` and `DEFAULT_METADATA_TYPE_REGISTRY` - * (`gen:meta-url-spelling`), and `check:meta-url-spelling` enforces both its - * freshness and the manifest/derived spelling agreement on every CI lap. + * The published surface is TWO deliberately distinct spelling contracts + * (#8424 — merging them once shipped an authorization bypass; see + * `./metadata-url-spelling`'s module doc), each also kept on `/shared` + * (`/shared` keeps its surface; this entry is additive, one declaration + * re-exported per symbol): + * + * - the **`/meta/:type` URL-spelling contract** — `META_URL_TO_SINGULAR`, + * the fold, and the two refusal verdicts. The map is derived at BUILD time + * from `PLURAL_TO_SINGULAR` and `DEFAULT_METADATA_TYPE_REGISTRY` + * (`gen:meta-url-spelling`), and `check:meta-url-spelling` enforces both + * its freshness and the manifest/derived spelling agreement on every CI + * lap; + * - the **`defineStack()` manifest-collection vocabulary** (#11503) — + * `PLURAL_TO_SINGULAR` / `SINGULAR_TO_PLURAL` and their folds, whose keys + * are the collection properties an author writes in `defineStack()`. Widened + * onto this entry because `@objectstack/core` keys every metadata store on + * `pluralToSingular` (#7378 row 2 — the map has ONE owner), and reaching it + * through `/shared` put the schema graph on every browser consumer's eager + * closure. * * @module */ @@ -31,3 +44,10 @@ export { metaUrlSpellingRefusal, unrecognisedMetaTypeRefusal, } from './metadata-url-spelling'; + +export { + PLURAL_TO_SINGULAR, + SINGULAR_TO_PLURAL, + pluralToSingular, + singularToPlural, +} from './manifest-collection-spelling'; diff --git a/packages/spec/src/meta-spelling/manifest-collection-spelling.test.ts b/packages/spec/src/meta-spelling/manifest-collection-spelling.test.ts new file mode 100644 index 0000000000..fdb076ee0c --- /dev/null +++ b/packages/spec/src/meta-spelling/manifest-collection-spelling.test.ts @@ -0,0 +1,66 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * [#11503] The manifest-collection vocabulary's schema-free home, pinned. + * + * `PLURAL_TO_SINGULAR` moved from `shared/metadata-collection.zod.ts` to this + * entry's graph so `@objectstack/core`'s store-key fold (#7378 row 2 — the map + * has ONE owner) stops linking the `/shared` zod closure into every browser + * consumer (#10096 standing principle, recorded verbatim, untranslated: + * 「浏览器可达的 spec 导出面必须 schema-free」). Two facts must hold and are + * pinned here: + * + * 1. the move is a RE-EXPORT, not a fork — `/shared` and `/meta-spelling` + * hand out reference-identical bindings (a faithful copy would pass every + * value comparison, so identity is the discriminating check); + * 2. widening the entry did NOT merge the two spelling contracts (#8424) — + * the manifest map and the URL map stay distinct symbols with distinct + * key sets. + * + * Schema-freeness of the BUILT entry is not asserted here — that is + * `check:browser-reachable-entries`' job, on the real bundle. + */ + +import { describe, expect, it } from 'vitest'; +import { + PLURAL_TO_SINGULAR, + SINGULAR_TO_PLURAL, + pluralToSingular, + singularToPlural, + META_URL_TO_SINGULAR, +} from './index'; + +describe('#11503 — the manifest-collection vocabulary is the SAME contract on both entries', () => { + it('`/meta-spelling` and `/shared` hand out identical bindings (one declaration, two entries)', async () => { + const shared = await import('../shared/metadata-collection.zod'); + expect(shared.PLURAL_TO_SINGULAR).toBe(PLURAL_TO_SINGULAR); + expect(shared.SINGULAR_TO_PLURAL).toBe(SINGULAR_TO_PLURAL); + expect(shared.pluralToSingular).toBe(pluralToSingular); + expect(shared.singularToPlural).toBe(singularToPlural); + }); + + it('folds a manifest collection key and passes unmapped names through', () => { + expect(pluralToSingular('objects')).toBe('object'); + expect(singularToPlural('object')).toBe('objects'); + expect(pluralToSingular('object')).toBe('object'); + expect(pluralToSingular('not_a_collection')).toBe('not_a_collection'); + }); +}); + +describe('#8424 — widening the entry did not merge the two spelling contracts', () => { + it('keeps the manifest map and the URL map distinct symbols', () => { + expect(PLURAL_TO_SINGULAR).not.toBe(META_URL_TO_SINGULAR); + }); + + it('the manifest map still lacks the four registry-only spellings the URL map carries', () => { + // These are URL spellings of registry types that are NOT stack-level + // collections — adding any of them to the manifest map would advertise a + // `defineStack()` collection that does not exist (the `fields:` incident, + // see metadata-url-spelling.ts). + for (const key of ['fields', 'seeds', 'translations', 'external_catalogs', 'externalCatalogs']) { + expect(PLURAL_TO_SINGULAR[key], `${key} must not enter the manifest map`).toBeUndefined(); + } + expect(META_URL_TO_SINGULAR['fields']).toBe('field'); + expect(META_URL_TO_SINGULAR['seeds']).toBe('seed'); + }); +}); diff --git a/packages/spec/src/meta-spelling/manifest-collection-spelling.ts b/packages/spec/src/meta-spelling/manifest-collection-spelling.ts new file mode 100644 index 0000000000..130512b943 --- /dev/null +++ b/packages/spec/src/meta-spelling/manifest-collection-spelling.ts @@ -0,0 +1,115 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * MANIFEST-COLLECTION SPELLING of a metadata type — the `defineStack()` half + * of the plural↔singular vocabulary, moved to a schema-free module (#11503) + * under the #10096 standing principle (recorded verbatim, untranslated): + * 「浏览器可达的 spec 导出面必须 schema-free」. + * + * ## Why this module exists, and imports NOTHING + * + * `pluralToSingular` is a runtime value with ONE owner (#7378 row 2 — copying + * the map into a consumer would be the per-implementation folk normalization + * that ruling forbids), and `@objectstack/core` — a package every browser + * client links eagerly — reads it to key metadata stores. When the map's + * declaration lived in `shared/metadata-collection.zod.ts`, that one value + * import put the whole `/shared` schema graph on every browser consumer's + * eager closure (#11503's measurement: the console's eagerly-loaded vendor + * chunk kept the `/shared` bundle through core's edge after every + * consumer-side lever was ablated). So the declaration lives HERE, in the + * schema-free `/meta-spelling` entry's graph, and `/shared` re-exports it — + * same symbols, same owner, no schema closure on the vocabulary path. + * + * ## ⚠️ This map is NOT `META_URL_TO_SINGULAR` (#8424) + * + * The two maps are deliberately different contracts that merely overlap: + * + * - **This map** (`PLURAL_TO_SINGULAR`) is the MANIFEST-COLLECTION map. Its + * keys are the properties an author writes in `defineStack()` + * (`objects: [...]`, `apps: [...]`), and + * `kernel/metadata-authoring-lint.ts` iterates it to decide WHICH + * COLLECTIONS EXIST at stack level. It has no `fields`, `seeds`, + * `externalCatalogs` or `translations` spelling, because none of those is + * a stack-level collection. + * - **`META_URL_TO_SINGULAR`** (`./metadata-url-spelling`) is the URL map: + * its keys are path segments a client may send to `/meta/:type`, derived + * at build time from this map ∪ the registry's REST plurals. + * + * Merging them would advertise stack collections that do not exist (see the + * `fields:` incident recorded in `./metadata-url-spelling`). Keep them + * distinct symbols with distinct domains. + * + * `check:stack-collection-maps` pins this map's key set against + * `ObjectStackDefinitionSchema` (both directions, waivers recorded there), + * and `check:meta-url-spelling` re-derives the URL map from it on every lap. + * + * @module + */ + +/** + * Mapping from plural manifest field names to singular metadata type names. + * + * Manifest / `defineStack()` uses plural property names because they are + * collection fields (e.g. `objects: [...]`, `apps: [...]`). The metadata + * registry and `MetadataTypeSchema` use singular names as the canonical form. + * + * Use this mapping at the boundary where manifest fields are fed into the + * metadata registry to ensure a consistent singular naming convention. + */ +export const PLURAL_TO_SINGULAR: Record = { + objects: 'object', + apps: 'app', + pages: 'page', + dashboards: 'dashboard', + reports: 'report', + datasets: 'dataset', + actions: 'action', + // `themes: 'theme'` was removed at #10485 (ADR-0049; the carrier key is + // retired). Its absence here is load-bearing twice over: the generated + // `META_URL_TO_SINGULAR` (gen:meta-url-spelling) loses the fold, so + // `/meta/theme` gets `unrecognisedMetaTypeRefusal`'s loud verdict instead of + // the pre-#10194 store-anything branch; and `applyConversionsToStoredItem` + // passes legacy `theme` rows through untouched rather than manufacturing a + // collection for them. + flows: 'flow', + jobs: 'job', + positions: 'position', + permissions: 'permission', + // [ADR-0066 D1, #5870] Package-declared authorization capabilities. The + // singular form is what `bootstrapDeclaredCapabilities` reads back + // (`readDeclared(ql, 'capability')`) and what `AppPlugin` already registers + // under; without the mapping the registration seam stored them as + // `'capabilities'`, a store nothing reads. + capabilities: 'capability', + sharingRules: 'sharing_rule', + apis: 'api', + webhooks: 'webhook', + agents: 'agent', + tools: 'tool', + skills: 'skill', + ragPipelines: 'rag_pipeline', + hooks: 'hook', + mappings: 'mapping', + analyticsCubes: 'analytics_cube', + connectors: 'connector', + datasources: 'datasource', + views: 'view', + emailTemplates: 'email_template', + docs: 'doc', + books: 'book', +}; + +/** Reverse mapping: singular metadata type → plural manifest field name. */ +export const SINGULAR_TO_PLURAL: Record = Object.fromEntries( + Object.entries(PLURAL_TO_SINGULAR).map(([plural, singular]) => [singular, plural]), +); + +/** Convert a plural manifest field name to its singular metadata type name. Returns the input unchanged if no mapping exists. */ +export function pluralToSingular(key: string): string { + return PLURAL_TO_SINGULAR[key] ?? key; +} + +/** Convert a singular metadata type name to its plural manifest field name. Returns the input unchanged if no mapping exists. */ +export function singularToPlural(key: string): string { + return SINGULAR_TO_PLURAL[key] ?? key; +} diff --git a/packages/spec/src/shared/metadata-collection.zod.ts b/packages/spec/src/shared/metadata-collection.zod.ts index 67defd69af..3456f9d4ec 100644 --- a/packages/spec/src/shared/metadata-collection.zod.ts +++ b/packages/spec/src/shared/metadata-collection.zod.ts @@ -98,74 +98,21 @@ export const MAP_SUPPORTED_FIELDS = [ export type MapSupportedField = (typeof MAP_SUPPORTED_FIELDS)[number]; -/** - * Mapping from plural manifest field names to singular metadata type names. - * - * Manifest / `defineStack()` uses plural property names because they are - * collection fields (e.g. `objects: [...]`, `apps: [...]`). The metadata - * registry and `MetadataTypeSchema` use singular names as the canonical form. - * - * Use this mapping at the boundary where manifest fields are fed into the - * metadata registry to ensure a consistent singular naming convention. - */ -export const PLURAL_TO_SINGULAR: Record = { - objects: 'object', - apps: 'app', - pages: 'page', - dashboards: 'dashboard', - reports: 'report', - datasets: 'dataset', - actions: 'action', - // `themes: 'theme'` was removed at #10485 (ADR-0049; the carrier key is - // retired). Its absence here is load-bearing twice over: the generated - // `META_URL_TO_SINGULAR` (gen:meta-url-spelling) loses the fold, so - // `/meta/theme` gets `unrecognisedMetaTypeRefusal`'s loud verdict instead of - // the pre-#10194 store-anything branch; and `applyConversionsToStoredItem` - // passes legacy `theme` rows through untouched rather than manufacturing a - // collection for them. - flows: 'flow', - jobs: 'job', - positions: 'position', - permissions: 'permission', - // [ADR-0066 D1, #5870] Package-declared authorization capabilities. The - // singular form is what `bootstrapDeclaredCapabilities` reads back - // (`readDeclared(ql, 'capability')`) and what `AppPlugin` already registers - // under; without the mapping the registration seam stored them as - // `'capabilities'`, a store nothing reads. - capabilities: 'capability', - sharingRules: 'sharing_rule', - apis: 'api', - webhooks: 'webhook', - agents: 'agent', - tools: 'tool', - skills: 'skill', - ragPipelines: 'rag_pipeline', - hooks: 'hook', - mappings: 'mapping', - analyticsCubes: 'analytics_cube', - connectors: 'connector', - datasources: 'datasource', - views: 'view', - emailTemplates: 'email_template', - docs: 'doc', - books: 'book', -}; - -/** Reverse mapping: singular metadata type → plural manifest field name. */ -export const SINGULAR_TO_PLURAL: Record = Object.fromEntries( - Object.entries(PLURAL_TO_SINGULAR).map(([plural, singular]) => [singular, plural]), -); - -/** Convert a plural manifest field name to its singular metadata type name. Returns the input unchanged if no mapping exists. */ -export function pluralToSingular(key: string): string { - return PLURAL_TO_SINGULAR[key] ?? key; -} - -/** Convert a singular metadata type name to its plural manifest field name. Returns the input unchanged if no mapping exists. */ -export function singularToPlural(key: string): string { - return SINGULAR_TO_PLURAL[key] ?? key; -} - +// [#11503] The plural↔singular manifest-collection vocabulary +// (`PLURAL_TO_SINGULAR`, `SINGULAR_TO_PLURAL`, `pluralToSingular`, +// `singularToPlural`) moved to `../meta-spelling/manifest-collection-spelling` +// so its one owner lives on a schema-free graph (#10096 standing principle: +// 「浏览器可达的 spec 导出面必须 schema-free」) — `@objectstack/core` keys every +// metadata store on the fold, and reading it from THIS module put the whole +// `/shared` zod closure on every browser consumer's eager graph. Re-exported +// here so `/shared` keeps its published surface (#8424); the declaration — +// and the map's domain documentation — lives at the new home. +export { + PLURAL_TO_SINGULAR, + SINGULAR_TO_PLURAL, + pluralToSingular, + singularToPlural, +} from '../meta-spelling/manifest-collection-spelling.js'; /** * Normalize a single metadata collection value from map format to array format. diff --git a/scripts/check-stack-collection-maps.mjs b/scripts/check-stack-collection-maps.mjs index b1cffbc8a6..772dfe91e4 100644 --- a/scripts/check-stack-collection-maps.mjs +++ b/scripts/check-stack-collection-maps.mjs @@ -371,7 +371,9 @@ const SITES = [ }, { id: 'PLURAL_TO_SINGULAR', - file: 'packages/spec/src/shared/metadata-collection.zod.ts', + // [#11503] The declaration moved out of shared/metadata-collection.zod.ts + // to the schema-free /meta-spelling graph; /shared re-exports it. + file: 'packages/spec/src/meta-spelling/manifest-collection-spelling.ts', what: 'plural stack key -> singular metadata type name', extract: (src) => { const b = sliceBody(src, 'export const PLURAL_TO_SINGULAR: Record = {'); @@ -642,7 +644,7 @@ function run({ list = false } = {}) { return 1; } - const collectionSource = readSource('packages/spec/src/shared/metadata-collection.zod.ts'); + const collectionSource = readSource('packages/spec/src/meta-spelling/manifest-collection-spelling.ts'); const singular = collectionSource ? pluralToSingularMap(collectionSource) : null; console.log( From 43986caa50ed06199c05e3ed21867e9f269ca1ce Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 25 Aug 2026 04:01:20 +0000 Subject: [PATCH 2/3] chore(spec): record the widened ./meta-spelling surface in export-origins (#11503) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit gen:export-origins — the four manifest-collection symbols land on the meta-spelling shard and shared's origins repoint to the moved declaration. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01K93P8PbH7kVjsAXkqzH1zY --- packages/spec/export-origins/meta-spelling.json | 4 ++++ packages/spec/export-origins/shared.json | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/spec/export-origins/meta-spelling.json b/packages/spec/export-origins/meta-spelling.json index 8605b44fdd..a0edf36c7b 100644 --- a/packages/spec/export-origins/meta-spelling.json +++ b/packages/spec/export-origins/meta-spelling.json @@ -3,8 +3,12 @@ "entry": "./meta-spelling", "exports": { "META_URL_TO_SINGULAR": "src/meta-spelling/meta-url-data.generated.ts#META_URL_TO_SINGULAR (const)", + "PLURAL_TO_SINGULAR": "src/meta-spelling/manifest-collection-spelling.ts#PLURAL_TO_SINGULAR (const)", + "SINGULAR_TO_PLURAL": "src/meta-spelling/manifest-collection-spelling.ts#SINGULAR_TO_PLURAL (const)", "canonicalMetaUrlType": "src/meta-spelling/metadata-url-spelling.ts#canonicalMetaUrlType (function)", "metaUrlSpellingRefusal": "src/meta-spelling/metadata-url-spelling.ts#metaUrlSpellingRefusal (function)", + "pluralToSingular": "src/meta-spelling/manifest-collection-spelling.ts#pluralToSingular (function)", + "singularToPlural": "src/meta-spelling/manifest-collection-spelling.ts#singularToPlural (function)", "unrecognisedMetaTypeRefusal": "src/meta-spelling/metadata-url-spelling.ts#unrecognisedMetaTypeRefusal (function)" } } diff --git a/packages/spec/export-origins/shared.json b/packages/spec/export-origins/shared.json index 1d7f175b07..71548df970 100644 --- a/packages/spec/export-origins/shared.json +++ b/packages/spec/export-origins/shared.json @@ -61,7 +61,7 @@ "ObjectNameSchema": "src/shared/branded-types.zod.ts#ObjectNameSchema (const)", "ObjectStackRawIssue": "src/shared/error-map.zod.ts#ObjectStackRawIssue (type)", "P": "src/shared/expression.zod.ts#P (const)", - "PLURAL_TO_SINGULAR": "src/shared/metadata-collection.zod.ts#PLURAL_TO_SINGULAR (const)", + "PLURAL_TO_SINGULAR": "src/meta-spelling/manifest-collection-spelling.ts#PLURAL_TO_SINGULAR (const)", "Predicate": "src/shared/expression.zod.ts#Predicate (type)", "PredicateInput": "src/shared/expression.zod.ts#PredicateInput (type)", "PredicateInputSchema": "src/shared/expression.zod.ts#PredicateInputSchema (const)", @@ -75,7 +75,7 @@ "RoleName": "src/shared/branded-types.zod.ts#RoleName (type)", "RoleNameParsed": "src/shared/branded-types.zod.ts#RoleNameParsed (type)", "RoleNameSchema": "src/shared/branded-types.zod.ts#RoleNameSchema (const)", - "SINGULAR_TO_PLURAL": "src/shared/metadata-collection.zod.ts#SINGULAR_TO_PLURAL (const)", + "SINGULAR_TO_PLURAL": "src/meta-spelling/manifest-collection-spelling.ts#SINGULAR_TO_PLURAL (const)", "SchemaDiffEntry": "src/shared/external-errors.ts#SchemaDiffEntry (interface)", "SchemaDiffEntryKind": "src/shared/external-errors.ts#SchemaDiffEntryKind (type)", "SnakeCaseIdentifier": "src/shared/identifiers.zod.ts#SnakeCaseIdentifier (type)", @@ -114,11 +114,11 @@ "normalizeStackInput": "src/shared/metadata-collection.zod.ts#normalizeStackInput (function)", "normalizeVisibleWhen": "src/shared/visibility.ts#normalizeVisibleWhen (function)", "objectStackErrorMap": "src/shared/error-map.zod.ts#objectStackErrorMap (const)", - "pluralToSingular": "src/shared/metadata-collection.zod.ts#pluralToSingular (function)", + "pluralToSingular": "src/meta-spelling/manifest-collection-spelling.ts#pluralToSingular (function)", "renderDiffMessage": "src/shared/external-errors.ts#renderDiffMessage (function)", "resilientFetch": "src/shared/resilient-fetch.ts#resilientFetch (function)", "safeParsePretty": "src/shared/error-map.zod.ts#safeParsePretty (function)", - "singularToPlural": "src/shared/metadata-collection.zod.ts#singularToPlural (function)", + "singularToPlural": "src/meta-spelling/manifest-collection-spelling.ts#singularToPlural (function)", "strictUnknownKeyError": "src/shared/suggestions.zod.ts#strictUnknownKeyError (function)", "suggestFieldType": "src/shared/suggestions.zod.ts#suggestFieldType (function)", "tmpl": "src/shared/expression.zod.ts#tmpl (function)", From 59cfccf50862cca92c6fb7300890207ca4db1325 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 25 Aug 2026 06:37:05 +0000 Subject: [PATCH 3/3] chore(spec): record the widened ./meta-spelling surface in api-surface (#11503) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit gen:api-surface — touched: meta-spelling.json; check:api-surface and check:generated green. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01K93P8PbH7kVjsAXkqzH1zY --- packages/spec/api-surface/meta-spelling.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/spec/api-surface/meta-spelling.json b/packages/spec/api-surface/meta-spelling.json index 36e5bffee9..3dd359f660 100644 --- a/packages/spec/api-surface/meta-spelling.json +++ b/packages/spec/api-surface/meta-spelling.json @@ -3,8 +3,12 @@ "entry": "./meta-spelling", "exports": [ "META_URL_TO_SINGULAR (const)", + "PLURAL_TO_SINGULAR (const)", + "SINGULAR_TO_PLURAL (const)", "canonicalMetaUrlType (function)", "metaUrlSpellingRefusal (function)", + "pluralToSingular (function)", + "singularToPlural (function)", "unrecognisedMetaTypeRefusal (function)" ] }