Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions .changeset/metadataformat-cachestrategy-single-declaration.md
Original file line number Diff line number Diff line change
@@ -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.
20 changes: 3 additions & 17 deletions content/docs/references/shared/enums.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion content/docs/references/shared/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
"http",
"identifiers",
"mapping",
"metadata-persistence",
"metadata-types",
"protection"
]
Expand Down
18 changes: 16 additions & 2 deletions content/docs/references/shared/metadata-types.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -39,3 +39,17 @@ Base metadata record fields shared across kernel and system

---

## MetadataFormat

Metadata file format

### Allowed Values

* `yaml`
* `json`
* `typescript`
* `javascript`


---

3 changes: 1 addition & 2 deletions content/docs/references/system/cache.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,6 @@ Cache eviction strategy
* `lfu`
* `fifo`
* `ttl`
* `adaptive`


---
Expand All @@ -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 |


Expand Down
4 changes: 3 additions & 1 deletion content/docs/references/system/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@
"---Content & Collaboration---",
"book",
"collaboration",
"doc"
"doc",
"---More---",
"metadata-types"
]
}
27 changes: 6 additions & 21 deletions content/docs/references/system/metadata-persistence.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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 |


---
Expand Down Expand Up @@ -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 | |
Expand Down Expand Up @@ -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 | |
Expand Down Expand Up @@ -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 |


---
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
---
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/. */}

## 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);
Expand Down
2 changes: 0 additions & 2 deletions packages/spec/api-surface.json
Original file line number Diff line number Diff line change
Expand Up @@ -4426,8 +4426,6 @@
"ApplyProtectionContext (interface)",
"BaseMetadataRecord (type)",
"BaseMetadataRecordSchema (const)",
"CacheStrategy (type)",
"CacheStrategyEnum (const)",
"CorsConfig (type)",
"CorsConfigSchema (const)",
"CronExpressionInput (type)",
Expand Down
3 changes: 0 additions & 3 deletions packages/spec/dual-source-exports.baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -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)]",
Expand All @@ -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)]",
Expand Down
1 change: 0 additions & 1 deletion packages/spec/json-schema.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -1129,7 +1129,6 @@
"security/TenancyPosture",
"shared/AppName",
"shared/BaseMetadataRecord",
"shared/CacheStrategyEnum",
"shared/CorsConfig",
"shared/CronExpressionInput",
"shared/EventName",
Expand Down
16 changes: 0 additions & 16 deletions packages/spec/src/shared/enums.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
SortItemSchema,
MutationEventEnum,
IsolationLevelEnum,
CacheStrategyEnum,
} from './enums.zod';

describe('SortDirectionEnum', () => {
Expand Down Expand Up @@ -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();
});
});
12 changes: 8 additions & 4 deletions packages/spec/src/shared/enums.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,11 @@ export const IsolationLevelEnum = z.enum([
]).describe('Transaction isolation levels (snake_case standard)');
export type IsolationLevel = z.infer<typeof IsolationLevelEnum>;

/** Cache eviction strategies */
export const CacheStrategyEnum = z.enum(['lru', 'lfu', 'ttl', 'fifo'])
.describe('Cache eviction strategy');
export type CacheStrategy = z.infer<typeof CacheStrategyEnum>;
// `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.
6 changes: 6 additions & 0 deletions packages/spec/src/shared/metadata-types.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'])
Expand Down
9 changes: 8 additions & 1 deletion packages/spec/src/system/cache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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', () => {
Expand Down
10 changes: 9 additions & 1 deletion packages/spec/src/system/cache.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof CacheStrategySchema>;
Expand Down
12 changes: 11 additions & 1 deletion packages/spec/src/system/metadata-persistence.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
Expand All @@ -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', () => {
Expand Down
Loading
Loading