diff --git a/.changeset/metadata-watch-event-canonical-enum.md b/.changeset/metadata-watch-event-canonical-enum.md new file mode 100644 index 0000000000..d37e5f282e --- /dev/null +++ b/.changeset/metadata-watch-event-canonical-enum.md @@ -0,0 +1,12 @@ +--- +"@objectstack/spec": major +--- + +`MetadataWatchEvent.type` now carries only the values the runtime emits: the enum narrows FROM `'add' | 'change' | 'unlink' | 'added' | 'changed' | 'deleted'` TO `'added' | 'changed' | 'deleted'` (#4536, follow-up to #4411). + +The three raw chokidar values had zero producers: both emit sites normalize before constructing the event — `packages/metadata/src/node-metadata-manager.ts` translates chokidar's `add`/`change`/`unlink` in the watcher callbacks (`handleFileEvent` accepts only the canonical three), and `packages/metadata/src/metadata-manager.ts` normalizes repository ops (`create`/`update`/`delete` → `added`/`changed`/`deleted`). Consumers parsing events therefore never received the raw values, and no runtime behavior changes. + +- FROM: an external implementor could construct events typed `'add'`/`'change'`/`'unlink'` and readers had to (needlessly) branch on six values. +- TO: an implementor constructing events with the raw values must emit `added`/`changed`/`deleted` instead; readers may delete any branches on `add`/`change`/`unlink` — they were unreachable. + +No tombstone / ADR-0087 conversion: this is a runtime event envelope type, not authorable metadata — nothing parses it on a load path (the #4411 route). diff --git a/content/docs/references/system/metadata-persistence.mdx b/content/docs/references/system/metadata-persistence.mdx index 5d5e3f1814..fb1ec85371 100644 --- a/content/docs/references/system/metadata-persistence.mdx +++ b/content/docs/references/system/metadata-persistence.mdx @@ -353,7 +353,7 @@ const result = MetadataCollectionInfo.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **type** | `Enum<'add' \| 'change' \| 'unlink' \| 'added' \| 'changed' \| 'deleted'>` | ✅ | | +| **type** | `Enum<'added' \| 'changed' \| 'deleted'>` | ✅ | | | **path** | `string` | ✅ | | | **name** | `string` | optional | | | **stats** | `{ path?: string; size?: number; mtime?: string; hash?: string; … }` | optional | | diff --git a/packages/spec/src/contracts/metadata-service.ts b/packages/spec/src/contracts/metadata-service.ts index 62fc8051f8..ff0b4de561 100644 --- a/packages/spec/src/contracts/metadata-service.ts +++ b/packages/spec/src/contracts/metadata-service.ts @@ -36,7 +36,7 @@ */ import type { MetadataQuery, MetadataQueryResult, MetadataValidationResult, MetadataBulkResult, MetadataDependency } from '../kernel/metadata-plugin.zod'; -// The PERSISTENCE-side watch event (`add`/`added`/`changed`/`deleted`/…, path + +// The PERSISTENCE-side watch event (`added`/`changed`/`deleted`, path + // file stats) — what `MetadataManager.subscribe` relays, as opposed to the // registration-level events `watch` forwards (`MetadataWatchCallback` below). // Spec used to carry a second, differently-shaped `MetadataWatchEvent` on @@ -423,7 +423,7 @@ export interface IMetadataService { * NOT {@link watch} with a different return shape: the two carry different * events. `watch` reports registration-level transitions * (`registered`/`updated`/`unregistered`); `subscribe` relays the loader - * pipeline's {@link MetadataWatchEvent} (`add`/`changed`/`deleted`, with + * pipeline's {@link MetadataWatchEvent} (`added`/`changed`/`deleted`, with * path and file stats) — the granularity ObjectQLPlugin's metadata bridge * re-syncs runtime-authored hooks/actions from. (The first draft of this * member reused `watch`'s callback type; `MetadataManager implements diff --git a/packages/spec/src/system/metadata-persistence.test.ts b/packages/spec/src/system/metadata-persistence.test.ts index eb1d5e887f..9af0b997d4 100644 --- a/packages/spec/src/system/metadata-persistence.test.ts +++ b/packages/spec/src/system/metadata-persistence.test.ts @@ -381,16 +381,26 @@ describe('MetadataSaveResultSchema', () => { }); describe('MetadataWatchEventSchema', () => { - it('should accept valid event types', () => { - const types = ['add', 'change', 'unlink', 'added', 'changed', 'deleted']; + it('should accept the canonical event types', () => { + const types = ['added', 'changed', 'deleted']; types.forEach((type) => { expect(() => MetadataWatchEventSchema.parse({ type, path: '/test' })).not.toThrow(); }); }); + // Pin (#4536): the raw chokidar vocabulary is translated in + // NodeMetadataManager's watcher callbacks and never reaches the event + // surface — the schema must reject it, not smuggle it back in. + it('should reject the raw chokidar event types', () => { + const rawTypes = ['add', 'change', 'unlink']; + rawTypes.forEach((type) => { + expect(() => MetadataWatchEventSchema.parse({ type, path: '/test' })).toThrow(); + }); + }); + it('should accept full event', () => { const event = MetadataWatchEventSchema.parse({ - type: 'change', + type: 'changed', path: '/metadata/view.json', name: 'account_view', stats: { size: 512 }, @@ -408,7 +418,7 @@ describe('MetadataWatchEventSchema', () => { }); it('should reject missing path', () => { - expect(() => MetadataWatchEventSchema.parse({ type: 'add' })).toThrow(); + expect(() => MetadataWatchEventSchema.parse({ type: 'added' })).toThrow(); }); }); diff --git a/packages/spec/src/system/metadata-persistence.zod.ts b/packages/spec/src/system/metadata-persistence.zod.ts index b94c8fc6c1..74d2e81b30 100644 --- a/packages/spec/src/system/metadata-persistence.zod.ts +++ b/packages/spec/src/system/metadata-persistence.zod.ts @@ -275,9 +275,15 @@ export const MetadataSaveResultSchema = lazySchema(() => z.object({ /** * Metadata Watch Event + * + * `type` carries only the values the runtime emits. The raw chokidar + * vocabulary (`add`/`change`/`unlink`) is translated in NodeMetadataManager's + * watcher callbacks (`packages/metadata/src/node-metadata-manager.ts` + * `handleFileEvent`) and never reaches the event surface — the raw values had + * zero producers when they were declared here (#4536, follow-up to #4411). */ export const MetadataWatchEventSchema = lazySchema(() => z.object({ - type: z.enum(['add', 'change', 'unlink', 'added', 'changed', 'deleted']), + type: z.enum(['added', 'changed', 'deleted']), path: z.string(), name: z.string().optional(), stats: MetadataStatsSchema.optional(),