From 5035bd3a0e438b5f6435102adabc1264b73709c2 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 2 Aug 2026 02:06:41 +0000 Subject: [PATCH] feat(spec)!: MetadataWatchEvent.type carries only the values the runtime emits (#4536) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The enum declared six values but three of them — the raw chokidar vocabulary add/change/unlink — had zero producers (declared-but- unenforced, Prime Directive #10). Both event construction sites normalize before the event exists: - packages/metadata/src/node-metadata-manager.ts translates chokidar's add/change/unlink in the watcher callbacks (handleFileEvent accepts only 'added' | 'changed' | 'deleted') - packages/metadata/src/metadata-manager.ts normalizes repository ops (create/update/delete -> added/changed/deleted) so the raw values never reached the event surface and no consumer branches on them (three-repo scan on the parent issue; re-verified here: the only other raw-vocabulary hits are chokidar-level wiring in cli/dev.ts, metadata/plugin.ts and metadata-fs/repository.ts, which emit different types entirely). Changes: - system/metadata-persistence.zod.ts: MetadataWatchEventSchema.type narrows to z.enum(['added', 'changed', 'deleted']), with a comment pointing at the translation site - contracts/metadata-service.ts: subscribe? TSDoc (and the import-site comment) stop mixing the two vocabularies - system/metadata-persistence.test.ts: canonical three parse; new pin test asserts add/change/unlink are rejected - generated references docs regenerated (enum cell only); api-surface.json unchanged (no export added or removed) - changeset (major): breaking only for an external implementor constructing events with the raw values — emit the canonical three; readers may delete branches on the raw values (they were unreachable). No tombstone / ADR-0087 conversion: runtime event envelope, not authorable metadata (the #4411 route). Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01M9uWvoEp9CoLzYjNExj9sL --- .../metadata-watch-event-canonical-enum.md | 12 ++++++++++++ .../references/system/metadata-persistence.mdx | 2 +- .../spec/src/contracts/metadata-service.ts | 4 ++-- .../src/system/metadata-persistence.test.ts | 18 ++++++++++++++---- .../src/system/metadata-persistence.zod.ts | 8 +++++++- 5 files changed, 36 insertions(+), 8 deletions(-) create mode 100644 .changeset/metadata-watch-event-canonical-enum.md 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(),