From ff1a6e105d4ee87f2231f00a9ffd858b7021227e Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 2 Aug 2026 02:57:00 +0000 Subject: [PATCH] feat(spec)!: converge the dual-source MetadataFormat and CacheStrategy enum declarations (#4537) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two enum vocabularies were declared on both ./shared and ./system and had diverged on their VALUES — which accepted value set you got depended on nothing but the import path (the #4411 trap; #4535 A2, #4506 baseline). Value-level divergence is the silent kind: a mismatched value never fails to compile. MetadataFormat / MetadataFormatSchema — shared declaration is the single source: - system/metadata-persistence.zod.ts no longer declares its own 7-member copy; it re-exports shared/metadata-types.zod (the MetadataManagerConfig pattern, in reverse). The extension-style aliases yml/ts/js it carried had zero producers in this repo, objectui and cloud: FilesystemLoader.detectFormat normalizes at the boundary (.yml -> 'yaml', .ts -> 'typescript', .js -> 'javascript') and the database/remote/memory loaders always emit 'json'. - Direction: shared, not system — kernel/metadata-loader.zod has imported the shared schema since #4411 (MetadataManagerConfig.formats needs no change), and system already re-exports MetadataManagerConfigSchema FROM kernel, so a system-side source would close a module cycle (shared -> system -> kernel -> shared). CacheStrategy — system/cache.zod.ts (CacheStrategySchema) is the single declaration: - shared/enums.zod.ts CacheStrategyEnum (+ its CacheStrategy type export) removed: zero importers in all three repos, while the system schema is the one CacheTier.strategy gates on. Same disposition as AggregationFunctionEnum in the same file (objectui#2945): removed rather than reconciled. - 'adaptive', declared only on the system side with zero producers, is dropped — the enum carries the four values both declarations agreed on (lru | lfu | fifo | ttl). Bookkeeping: - dual-source-exports.baseline.json: the three converged rows deleted (shrink-only ratchet). - json-schema.manifest.json: shared/CacheStrategyEnum key deleted (deliberate retirement, #2978 flow). - Pin tests: yml/ts/js rejected on the system entry; 'adaptive' rejected on CacheStrategySchema. - api-surface.json, references docs, skill refs regenerated via check:generated --fix. - Changeset (major) carries the FROM -> TO migration lines. - No ADR-0087 conversion / tombstone: loader envelope + config vocabulary with no authorable-metadata producers (the #4411/#4536 route). Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01M9uWvoEp9CoLzYjNExj9sL --- ...format-cachestrategy-single-declaration.md | 38 +++++++++++++++++++ content/docs/references/shared/enums.mdx | 20 ++-------- content/docs/references/shared/meta.json | 1 - .../docs/references/shared/metadata-types.mdx | 18 ++++++++- content/docs/references/system/cache.mdx | 3 +- content/docs/references/system/meta.json | 4 +- .../system/metadata-persistence.mdx | 27 +++---------- .../metadata-types.mdx} | 8 ++-- packages/spec/api-surface.json | 2 - .../spec/dual-source-exports.baseline.json | 3 -- packages/spec/json-schema.manifest.json | 1 - packages/spec/src/shared/enums.test.ts | 16 -------- packages/spec/src/shared/enums.zod.ts | 12 ++++-- .../spec/src/shared/metadata-types.zod.ts | 6 +++ packages/spec/src/system/cache.test.ts | 9 ++++- packages/spec/src/system/cache.zod.ts | 10 ++++- .../src/system/metadata-persistence.test.ts | 12 +++++- .../src/system/metadata-persistence.zod.ts | 24 +++++++++--- skills/objectstack-ui/references/_index.md | 2 +- 19 files changed, 132 insertions(+), 84 deletions(-) create mode 100644 .changeset/metadataformat-cachestrategy-single-declaration.md rename content/docs/references/{shared/metadata-persistence.mdx => system/metadata-types.mdx} (65%) diff --git a/.changeset/metadataformat-cachestrategy-single-declaration.md b/.changeset/metadataformat-cachestrategy-single-declaration.md new file mode 100644 index 0000000000..edf0b514c7 --- /dev/null +++ b/.changeset/metadataformat-cachestrategy-single-declaration.md @@ -0,0 +1,38 @@ +--- +"@objectstack/spec": major +--- + +feat(spec)!: converge the dual-source `MetadataFormat` and `CacheStrategy` enum declarations (#4537) + +Two enum vocabularies were declared twice, on `./shared` and `./system`, and had +diverged on their **values** — which type (and which accepted value set) you got +depended on nothing but the import path (the #4411 trap; #4535/#4506 baseline). +Both converge on one declaration each; the three +`dual-source-exports.baseline.json` rows are deleted. + +**`MetadataFormat` / `MetadataFormatSchema` — the shared declaration is the +single source.** `system/metadata-persistence.zod` no longer declares its own +7-member copy; it re-exports `shared/metadata-types.zod` (the +`MetadataManagerConfig` pattern — `kernel/metadata-loader.zod` already imported +the shared one since #4411). Breaking on the `./system` entry only: the +extension-style aliases `'yml'`/`'ts'`/`'js'` are no longer accepted. They had +zero producers in this repo, objectui and cloud — every loader normalizes at the +boundary (`FilesystemLoader.detectFormat` maps `.yml` → `'yaml'`, `.ts` → +`'typescript'`, `.js` → `'javascript'`; the database/remote/memory loaders +always emit `'json'`). Migration: write the canonical name — +`'yml'` → `'yaml'`, `'ts'` → `'typescript'`, `'js'` → `'javascript'`. + +**`CacheStrategy` — `system/cache.zod` (`CacheStrategySchema`) is the single +declaration.** The `./shared` copy `CacheStrategyEnum` (and its `CacheStrategy` +type export) is removed: it had zero importers in all three repos, while the +system schema is the one `CacheTier.strategy` gates on — same disposition as +`AggregationFunctionEnum` (objectui#2945): removed rather than reconciled. +Migration: `import { CacheStrategySchema, type CacheStrategy } from +'@objectstack/spec/system'`. The value `'adaptive'`, declared only on the +system side with zero producers, is dropped — the enum carries the four values +both declarations agreed on (`'lru' | 'lfu' | 'fifo' | 'ttl'`); pick one of +those. + +No ADR-0087 conversion / tombstone: loader envelope + config vocabulary with no +authorable-metadata producers (the #4411 / #4536 route), verified by three-repo +scan on the issue. diff --git a/content/docs/references/shared/enums.mdx b/content/docs/references/shared/enums.mdx index 146295df72..714c5d46f0 100644 --- a/content/docs/references/shared/enums.mdx +++ b/content/docs/references/shared/enums.mdx @@ -14,27 +14,13 @@ Sort direction used across query, data-engine, analytics ## TypeScript Usage ```typescript -import { CacheStrategyEnum, IsolationLevelEnum, MutationEventEnum, SortDirectionEnum, SortItem } from '@objectstack/spec/shared'; -import type { CacheStrategyEnum, IsolationLevelEnum, MutationEventEnum, SortDirectionEnum, SortItem } from '@objectstack/spec/shared'; +import { IsolationLevelEnum, MutationEventEnum, SortDirectionEnum, SortItem } from '@objectstack/spec/shared'; +import type { IsolationLevelEnum, MutationEventEnum, SortDirectionEnum, SortItem } from '@objectstack/spec/shared'; // Validate data -const result = CacheStrategyEnum.parse(data); +const result = IsolationLevelEnum.parse(data); ``` ---- - -## CacheStrategyEnum - -Cache eviction strategy - -### Allowed Values - -* `lru` -* `lfu` -* `ttl` -* `fifo` - - --- ## IsolationLevelEnum diff --git a/content/docs/references/shared/meta.json b/content/docs/references/shared/meta.json index 8ef0fa19a3..39f5f91ea3 100644 --- a/content/docs/references/shared/meta.json +++ b/content/docs/references/shared/meta.json @@ -7,7 +7,6 @@ "http", "identifiers", "mapping", - "metadata-persistence", "metadata-types", "protection" ] diff --git a/content/docs/references/shared/metadata-types.mdx b/content/docs/references/shared/metadata-types.mdx index 864682673e..2cc084bbb1 100644 --- a/content/docs/references/shared/metadata-types.mdx +++ b/content/docs/references/shared/metadata-types.mdx @@ -14,8 +14,8 @@ Supported metadata file formats ## TypeScript Usage ```typescript -import { BaseMetadataRecord } from '@objectstack/spec/shared'; -import type { BaseMetadataRecord } from '@objectstack/spec/shared'; +import { BaseMetadataRecord, MetadataFormat } from '@objectstack/spec/shared'; +import type { BaseMetadataRecord, MetadataFormat } from '@objectstack/spec/shared'; // Validate data const result = BaseMetadataRecord.parse(data); @@ -39,3 +39,17 @@ Base metadata record fields shared across kernel and system --- +## MetadataFormat + +Metadata file format + +### Allowed Values + +* `yaml` +* `json` +* `typescript` +* `javascript` + + +--- + diff --git a/content/docs/references/system/cache.mdx b/content/docs/references/system/cache.mdx index d44637d713..5d78cff063 100644 --- a/content/docs/references/system/cache.mdx +++ b/content/docs/references/system/cache.mdx @@ -128,7 +128,6 @@ Cache eviction strategy * `lfu` * `fifo` * `ttl` -* `adaptive` --- @@ -145,7 +144,7 @@ Configuration for a single cache tier in the hierarchy | **type** | `Enum<'memory' \| 'redis' \| 'memcached' \| 'cdn'>` | ✅ | Cache backend type | | **maxSize** | `number` | optional | Max size in MB | | **ttl** | `number` | ✅ | Default TTL in seconds | -| **strategy** | `Enum<'lru' \| 'lfu' \| 'fifo' \| 'ttl' \| 'adaptive'>` | ✅ | Eviction strategy | +| **strategy** | `Enum<'lru' \| 'lfu' \| 'fifo' \| 'ttl'>` | ✅ | Eviction strategy | | **warmup** | `boolean` | ✅ | Pre-populate cache on startup | diff --git a/content/docs/references/system/meta.json b/content/docs/references/system/meta.json index 6f4076046b..42f1559f7a 100644 --- a/content/docs/references/system/meta.json +++ b/content/docs/references/system/meta.json @@ -43,6 +43,8 @@ "---Content & Collaboration---", "book", "collaboration", - "doc" + "doc", + "---More---", + "metadata-types" ] } \ No newline at end of file diff --git a/content/docs/references/system/metadata-persistence.mdx b/content/docs/references/system/metadata-persistence.mdx index fb1ec85371..d10d4cd0f2 100644 --- a/content/docs/references/system/metadata-persistence.mdx +++ b/content/docs/references/system/metadata-persistence.mdx @@ -16,8 +16,8 @@ Defines the lifecycle and mutability of a metadata item. ## TypeScript Usage ```typescript -import { MetadataCollectionInfo, MetadataDiffResult, MetadataExportOptions, MetadataFormat, MetadataHistoryQueryOptions, MetadataHistoryQueryResult, MetadataHistoryRecord, MetadataHistoryRetentionPolicy, MetadataImportOptions, MetadataLoadOptions, MetadataLoadResult, MetadataLoaderContract, MetadataRecord, MetadataSaveOptions, MetadataSaveResult, MetadataScope, MetadataSource, MetadataState, MetadataStats, MetadataWatchEvent, PackagePublishResult } from '@objectstack/spec/system'; -import type { MetadataCollectionInfo, MetadataDiffResult, MetadataExportOptions, MetadataFormat, MetadataHistoryQueryOptions, MetadataHistoryQueryResult, MetadataHistoryRecord, MetadataHistoryRetentionPolicy, MetadataImportOptions, MetadataLoadOptions, MetadataLoadResult, MetadataLoaderContract, MetadataRecord, MetadataSaveOptions, MetadataSaveResult, MetadataScope, MetadataSource, MetadataState, MetadataStats, MetadataWatchEvent, PackagePublishResult } from '@objectstack/spec/system'; +import { MetadataCollectionInfo, MetadataDiffResult, MetadataExportOptions, MetadataHistoryQueryOptions, MetadataHistoryQueryResult, MetadataHistoryRecord, MetadataHistoryRetentionPolicy, MetadataImportOptions, MetadataLoadOptions, MetadataLoadResult, MetadataLoaderContract, MetadataRecord, MetadataSaveOptions, MetadataSaveResult, MetadataScope, MetadataSource, MetadataState, MetadataStats, MetadataWatchEvent, PackagePublishResult } from '@objectstack/spec/system'; +import type { MetadataCollectionInfo, MetadataDiffResult, MetadataExportOptions, MetadataHistoryQueryOptions, MetadataHistoryQueryResult, MetadataHistoryRecord, MetadataHistoryRetentionPolicy, MetadataImportOptions, MetadataLoadOptions, MetadataLoadResult, MetadataLoaderContract, MetadataRecord, MetadataSaveOptions, MetadataSaveResult, MetadataScope, MetadataSource, MetadataState, MetadataStats, MetadataWatchEvent, PackagePublishResult } from '@objectstack/spec/system'; // Validate data const result = MetadataCollectionInfo.parse(data); @@ -66,22 +66,7 @@ const result = MetadataCollectionInfo.parse(data); | **types** | `string[]` | optional | | | **namespaces** | `string[]` | optional | | | **output** | `string` | ✅ | Output directory or file | -| **format** | `Enum<'json' \| 'yaml' \| 'yml' \| 'ts' \| 'js' \| 'typescript' \| 'javascript'>` | ✅ | | - - ---- - -## MetadataFormat - -### Allowed Values - -* `json` -* `yaml` -* `yml` -* `ts` -* `js` -* `typescript` -* `javascript` +| **format** | `Enum<'yaml' \| 'json' \| 'typescript' \| 'javascript'>` | ✅ | Metadata file format | --- @@ -194,7 +179,7 @@ const result = MetadataCollectionInfo.parse(data); | :--- | :--- | :--- | :--- | | **data** | `any` | ✅ | | | **stats** | `{ path?: string; size?: number; mtime?: string; hash?: string; … }` | optional | | -| **format** | `Enum<'json' \| 'yaml' \| 'yml' \| 'ts' \| 'js' \| 'typescript' \| 'javascript'>` | optional | | +| **format** | `Enum<'yaml' \| 'json' \| 'typescript' \| 'javascript'>` | optional | Metadata file format | | **source** | `string` | optional | | | **fromCache** | `boolean` | optional | | | **etag** | `string` | optional | | @@ -264,7 +249,7 @@ const result = MetadataCollectionInfo.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **format** | `Enum<'json' \| 'yaml' \| 'yml' \| 'ts' \| 'js' \| 'typescript' \| 'javascript'>` | optional | | +| **format** | `Enum<'yaml' \| 'json' \| 'typescript' \| 'javascript'>` | optional | Metadata file format | | **create** | `boolean` | ✅ | | | **overwrite** | `boolean` | ✅ | | | **path** | `string` | optional | | @@ -342,7 +327,7 @@ const result = MetadataCollectionInfo.parse(data); | **hash** | `string` | optional | | | **etag** | `string` | optional | | | **modifiedAt** | `string` | optional | | -| **format** | `Enum<'json' \| 'yaml' \| 'yml' \| 'ts' \| 'js' \| 'typescript' \| 'javascript'>` | optional | | +| **format** | `Enum<'yaml' \| 'json' \| 'typescript' \| 'javascript'>` | optional | Metadata file format | --- diff --git a/content/docs/references/shared/metadata-persistence.mdx b/content/docs/references/system/metadata-types.mdx similarity index 65% rename from content/docs/references/shared/metadata-persistence.mdx rename to content/docs/references/system/metadata-types.mdx index 30669611cf..d6728eec77 100644 --- a/content/docs/references/shared/metadata-persistence.mdx +++ b/content/docs/references/system/metadata-types.mdx @@ -1,6 +1,6 @@ --- -title: Metadata Persistence -description: Metadata Persistence protocol schemas +title: Metadata Types +description: Metadata Types protocol schemas --- {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} @@ -8,8 +8,8 @@ description: Metadata Persistence protocol schemas ## TypeScript Usage ```typescript -import { MetadataFormat } from '@objectstack/spec/shared'; -import type { MetadataFormat } from '@objectstack/spec/shared'; +import { MetadataFormat } from '@objectstack/spec/system'; +import type { MetadataFormat } from '@objectstack/spec/system'; // Validate data const result = MetadataFormat.parse(data); diff --git a/packages/spec/api-surface.json b/packages/spec/api-surface.json index 68239f63d3..da4ad891fc 100644 --- a/packages/spec/api-surface.json +++ b/packages/spec/api-surface.json @@ -4426,8 +4426,6 @@ "ApplyProtectionContext (interface)", "BaseMetadataRecord (type)", "BaseMetadataRecordSchema (const)", - "CacheStrategy (type)", - "CacheStrategyEnum (const)", "CorsConfig (type)", "CorsConfigSchema (const)", "CronExpressionInput (type)", diff --git a/packages/spec/dual-source-exports.baseline.json b/packages/spec/dual-source-exports.baseline.json index 7f8d8673de..3f28016eb5 100644 --- a/packages/spec/dual-source-exports.baseline.json +++ b/packages/spec/dual-source-exports.baseline.json @@ -4,7 +4,6 @@ "ActionLocationSchema — [./studio (const)] ≠ [./ui (const)]", "ActivationEventSchema — [./kernel (const)] ≠ [./studio (const)]", "AnalyticsQuery — [./contracts (interface)] ≠ [./data (type)]", - "CacheStrategy — [./shared (type)] ≠ [./system (type)]", "ConflictResolution — [./automation (type)] ≠ [./integration (type)] ≠ [./ui (type)]", "ConflictResolutionSchema — [./automation (const)] ≠ [./integration (const)] ≠ [./ui (const)]", "DataSyncConfig — [./automation (type)] ≠ [./integration (type)]", @@ -25,8 +24,6 @@ "MetadataEvent — [./api (type)] ≠ [./kernel (type)]", "MetadataEventSchema — [./api (const)] ≠ [./kernel (const)]", "MetadataExportOptions — [./contracts (interface)] ≠ [./system (type)]", - "MetadataFormat — [./shared (type)] ≠ [./system (type)]", - "MetadataFormatSchema — [./shared (const)] ≠ [./system (const)]", "MetadataImportOptions — [./contracts (interface)] ≠ [./system (type)]", "Notification — [./api (type)] ≠ [./ui (type)]", "NotificationChannel — [./contracts (type)] ≠ [./system (type)]", diff --git a/packages/spec/json-schema.manifest.json b/packages/spec/json-schema.manifest.json index afd74714db..95041dcfa7 100644 --- a/packages/spec/json-schema.manifest.json +++ b/packages/spec/json-schema.manifest.json @@ -1129,7 +1129,6 @@ "security/TenancyPosture", "shared/AppName", "shared/BaseMetadataRecord", - "shared/CacheStrategyEnum", "shared/CorsConfig", "shared/CronExpressionInput", "shared/EventName", diff --git a/packages/spec/src/shared/enums.test.ts b/packages/spec/src/shared/enums.test.ts index f227283f44..862972c879 100644 --- a/packages/spec/src/shared/enums.test.ts +++ b/packages/spec/src/shared/enums.test.ts @@ -4,7 +4,6 @@ import { SortItemSchema, MutationEventEnum, IsolationLevelEnum, - CacheStrategyEnum, } from './enums.zod'; describe('SortDirectionEnum', () => { @@ -72,18 +71,3 @@ describe('IsolationLevelEnum', () => { expect(() => IsolationLevelEnum.parse('none')).toThrow(); }); }); - -describe('CacheStrategyEnum', () => { - it('should accept all cache strategies', () => { - const valid = ['lru', 'lfu', 'ttl', 'fifo']; - valid.forEach((v) => { - expect(CacheStrategyEnum.parse(v)).toBe(v); - }); - }); - - it('should reject invalid values', () => { - expect(() => CacheStrategyEnum.parse('LRU')).toThrow(); - expect(() => CacheStrategyEnum.parse('random')).toThrow(); - expect(() => CacheStrategyEnum.parse('')).toThrow(); - }); -}); diff --git a/packages/spec/src/shared/enums.zod.ts b/packages/spec/src/shared/enums.zod.ts index 8974c7e973..b9519888f9 100644 --- a/packages/spec/src/shared/enums.zod.ts +++ b/packages/spec/src/shared/enums.zod.ts @@ -41,7 +41,11 @@ export const IsolationLevelEnum = z.enum([ ]).describe('Transaction isolation levels (snake_case standard)'); export type IsolationLevel = z.infer; -/** Cache eviction strategies */ -export const CacheStrategyEnum = z.enum(['lru', 'lfu', 'ttl', 'fifo']) - .describe('Cache eviction strategy'); -export type CacheStrategy = z.infer; +// `CacheStrategyEnum` lived here as a second declaration of the cache eviction +// vocabulary next to `CacheStrategySchema` (`system/cache.zod.ts`) — same +// `CacheStrategy` type name on two entry points, diverged on the values (this +// one lacked `adaptive`). It had zero importers in this repo, objectui, or +// cloud, while the system schema is the one `CacheTier.strategy` gates on. +// Removed rather than reconciled, for the same reason as +// `AggregationFunctionEnum` above: a second name for one concept is how the +// vocabularies drifted apart in the first place. #4537. diff --git a/packages/spec/src/shared/metadata-types.zod.ts b/packages/spec/src/shared/metadata-types.zod.ts index e97e5a1199..c7ef9bdefb 100644 --- a/packages/spec/src/shared/metadata-types.zod.ts +++ b/packages/spec/src/shared/metadata-types.zod.ts @@ -7,6 +7,12 @@ import { SnakeCaseIdentifierSchema } from './identifiers.zod'; // Shared Metadata Types // ============================================================================ +// The single declaration of the metadata-format vocabulary (#4537), re-exported +// by `system/metadata-persistence.zod` and consumed by +// `kernel/metadata-loader.zod` (`MetadataManagerConfig.formats`, #4411). Only +// the four canonical names: extension-style aliases (`yml`/`ts`/`js`) are +// normalized away at the loader boundary (`FilesystemLoader.detectFormat`) +// and never reach a `format` field. /** Supported metadata file formats */ import { lazySchema } from './lazy-schema'; export const MetadataFormatSchema = lazySchema(() => z.enum(['yaml', 'json', 'typescript', 'javascript']) diff --git a/packages/spec/src/system/cache.test.ts b/packages/spec/src/system/cache.test.ts index 10124032e6..840fd1c941 100644 --- a/packages/spec/src/system/cache.test.ts +++ b/packages/spec/src/system/cache.test.ts @@ -12,7 +12,7 @@ import { describe('CacheStrategySchema', () => { it('should accept valid strategies', () => { - const strategies = ['lru', 'lfu', 'fifo', 'ttl', 'adaptive']; + const strategies = ['lru', 'lfu', 'fifo', 'ttl']; strategies.forEach((strategy) => { expect(() => CacheStrategySchema.parse(strategy)).not.toThrow(); @@ -23,6 +23,13 @@ describe('CacheStrategySchema', () => { expect(() => CacheStrategySchema.parse('invalid')).toThrow(); expect(() => CacheStrategySchema.parse('random')).toThrow(); }); + + // Pin (#4537): 'adaptive' was declared with zero producers (three-repo scan) + // and removed when the shared/system declarations converged. It must stay + // rejected so a declared-but-unimplemented strategy cannot silently return. + it('should reject the retired adaptive strategy', () => { + expect(() => CacheStrategySchema.parse('adaptive')).toThrow(); + }); }); describe('CacheTierSchema', () => { diff --git a/packages/spec/src/system/cache.zod.ts b/packages/spec/src/system/cache.zod.ts index 9781a5d950..9cbda0ad8e 100644 --- a/packages/spec/src/system/cache.zod.ts +++ b/packages/spec/src/system/cache.zod.ts @@ -28,12 +28,20 @@ import { CronExpressionInputSchema } from '../shared/expression.zod'; * @see ../../api/http-cache.zod.ts for HTTP-level caching */ import { lazySchema } from '../shared/lazy-schema'; + +/** + * Cache eviction strategy — the single declaration of the `CacheStrategy` + * vocabulary (#4537). `shared/enums.zod.ts` carried a second, diverged copy + * (`CacheStrategyEnum`, no importers anywhere) that was removed rather than + * reconciled. `adaptive` ("dynamic strategy selection") was declared only on + * this side and had zero producers in this repo, objectui and cloud — dropped + * so the enum carries only the values both declarations ever agreed on. + */ export const CacheStrategySchema = lazySchema(() => z.enum([ 'lru', // Least Recently Used 'lfu', // Least Frequently Used 'fifo', // First In First Out 'ttl', // Time To Live only - 'adaptive', // Dynamic strategy selection ]).describe('Cache eviction strategy')); export type CacheStrategy = z.infer; diff --git a/packages/spec/src/system/metadata-persistence.test.ts b/packages/spec/src/system/metadata-persistence.test.ts index 9af0b997d4..61caafa790 100644 --- a/packages/spec/src/system/metadata-persistence.test.ts +++ b/packages/spec/src/system/metadata-persistence.test.ts @@ -182,7 +182,7 @@ describe('MetadataRecordSchema', () => { describe('MetadataFormatSchema', () => { it('should accept valid formats', () => { - const formats = ['json', 'yaml', 'yml', 'ts', 'js', 'typescript', 'javascript']; + const formats = ['json', 'yaml', 'typescript', 'javascript']; formats.forEach((format) => { expect(() => MetadataFormatSchema.parse(format)).not.toThrow(); }); @@ -191,6 +191,16 @@ describe('MetadataFormatSchema', () => { it('should reject invalid formats', () => { expect(() => MetadataFormatSchema.parse('xml')).toThrow(); }); + + // Pin (#4537): the extension-style aliases the system-side copy used to + // carry had zero producers — every loader normalizes at the boundary + // (FilesystemLoader.detectFormat maps .yml/.ts/.js to the canonical names). + // They must stay rejected so the alias vocabulary cannot silently return. + it('should reject the retired yml/ts/js aliases', () => { + ['yml', 'ts', 'js'].forEach((alias) => { + expect(() => MetadataFormatSchema.parse(alias)).toThrow(); + }); + }); }); describe('MetadataStatsSchema', () => { diff --git a/packages/spec/src/system/metadata-persistence.zod.ts b/packages/spec/src/system/metadata-persistence.zod.ts index 74d2e81b30..00dcc871bb 100644 --- a/packages/spec/src/system/metadata-persistence.zod.ts +++ b/packages/spec/src/system/metadata-persistence.zod.ts @@ -1,6 +1,7 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. import { z } from 'zod'; +import { MetadataFormatSchema } from '../shared/metadata-types.zod'; /** * Metadata Scope Enum @@ -164,7 +165,9 @@ export type PackagePublishResult = z.infer; // ─── Loader / watch envelope types ─────────────────────────────────────────── // // Everything from here to `MetadataSource` below is the SINGLE source for the -// metadata loader + watch vocabulary. `kernel/metadata-loader.zod` used to +// metadata loader + watch vocabulary (except `MetadataFormat`, whose one +// declaration sits in `shared/metadata-types.zod` and is re-exported here — +// see the note on the re-export). `kernel/metadata-loader.zod` used to // declare a differently-shaped copy of each of these names on the // `@objectstack/spec/kernel` entry — an import-path coin-flip that no consumer // ever won on purpose (every one of them imported from here). The kernel copies @@ -173,11 +176,20 @@ export type PackagePublishResult = z.infer; /** * Metadata Format * Supported file formats for metadata serialization. + * + * Re-exported from `../shared/metadata-types.zod` — the single declaration + * (#4537, same shape as the `MetadataManagerConfig` re-export below). This + * file used to declare its own 7-member copy with `yml`/`ts`/`js` "aliases"; + * those values had zero producers in this repo, objectui and cloud: every + * loader normalizes at the boundary (`FilesystemLoader.detectFormat` maps + * `.yml` → `'yaml'`, `.ts` → `'typescript'`, `.js` → `'javascript'`) and the + * database/remote/memory loaders always emit `'json'`. The declaration lives + * in `shared/` because both this file and `kernel/metadata-loader.zod` + * (`MetadataManagerConfig.formats`, #4411) need it, and kernel ← system is + * already taken by the `MetadataManagerConfig` re-export — a system-side + * source would close a module cycle. */ -export const MetadataFormatSchema = lazySchema(() => z.enum([ - 'json', 'yaml', 'yml', 'ts', 'js', - 'typescript', 'javascript' // Aliases -])); +export { MetadataFormatSchema }; /** * Metadata Stats @@ -347,7 +359,7 @@ export { MetadataManagerConfigSchema, } from '../kernel/metadata-loader.zod'; -export type MetadataFormat = z.infer; +export type { MetadataFormat } from '../shared/metadata-types.zod'; export type MetadataStats = z.infer; export type MetadataLoaderContract = z.input; export type MetadataLoadOptions = z.infer; diff --git a/skills/objectstack-ui/references/_index.md b/skills/objectstack-ui/references/_index.md index f33063235d..18be463b71 100644 --- a/skills/objectstack-ui/references/_index.md +++ b/skills/objectstack-ui/references/_index.md @@ -29,7 +29,7 @@ from `node_modules` — there is no local copy in the skill bundle. - `node_modules/@objectstack/spec/src/data/hook-body.zod.ts` — Capability tokens a script body may request. - `node_modules/@objectstack/spec/src/data/query.zod.ts` — Sort Node - `node_modules/@objectstack/spec/src/kernel/metadata-protection.zod.ts` — Metadata Protection Model — Phase 1 (ADR-0010) -- `node_modules/@objectstack/spec/src/shared/enums.zod.ts` — Exports: SortDirectionEnum, SortItemSchema, MutationEventEnum, IsolationLevelEnum, CacheStrategyEnum +- `node_modules/@objectstack/spec/src/shared/enums.zod.ts` — Exports: SortDirectionEnum, SortItemSchema, MutationEventEnum, IsolationLevelEnum - `node_modules/@objectstack/spec/src/shared/expression.zod.ts` — Expression Protocol - `node_modules/@objectstack/spec/src/shared/http.zod.ts` — Shared HTTP Schemas - `node_modules/@objectstack/spec/src/shared/identifiers.zod.ts` — System Identifier Schema