diff --git a/.changeset/metadata-changed-event-payload-retired.md b/.changeset/metadata-changed-event-payload-retired.md new file mode 100644 index 0000000000..61d30433f1 --- /dev/null +++ b/.changeset/metadata-changed-event-payload-retired.md @@ -0,0 +1,68 @@ +--- +"@objectstack/spec": minor +--- + +feat(spec): retire `MetadataChangedEventPayloadSchema` — the `metadata:changed` payload nothing ever emitted or consumed (#14180, ADR-0049) + + + +**BREAKING** export removal, landing after the v17.0.0 cut (the lockstep +launch-window convention ships it as `minor`; the prescription is registered +under protocol major 18 — `RETIRED_DEFS_BY_MAJOR[18]` + the D3 semantic entry +`metadata-changed-event-payload-retired` — where `os migrate meta` users will +look). + +`kernel/cluster.zod.ts` declared a "canonical payload for the +`metadata:changed` event" and said every metadata persistence layer MUST emit +it after any successful write and every reader MUST subscribe and compare its +`version` before invalidating. Nothing ever did either: zero runtime emitters, +zero subscribers, zero imports outside `packages/spec` (its own unit test, the +isomorphic alias pin and the generated artifacts), in objectstack and in +objectui at the pinned sha. It could not have been honoured as declared — the +`version` field is `z.bigint()`, which the standard JSON serializer refuses, so +the payload could not cross any pubsub transport without a codec no driver +ships. The three cluster channels that do run (`metadata.changed`, +`metadata.mutated`, `datasource.mutated`) all carry an address-only signal +whose receiver re-reads its own store — the opposite of the declared +version-compare receipt — so the one plausible future consumer was decided +against (2026-09-01 ruling), and the triage ruling (2026-09-02) chose removal +over "make a consumer". + +FROM → TO: + +- `MetadataChangedEventPayloadSchema` / `MetadataChangedEventPayload` → + *(removed)* — no replacement type is declared. Subscribe to one of the lanes + documented in `content/docs/kernel/cluster.mdx` §6.2 instead: + `metadata.changed` (`ClusterMetadataChangedPayload`, `@objectstack/metadata`), + `metadata.mutated` (`ClusterMetadataMutationPayload`, + `@objectstack/metadata-protocol`) or `datasource.mutated` + (`ClusterDatasourceMutationPayload`, `@objectstack/service-datasource`). +- `MetadataChangeOperationSchema` / `MetadataChangeOperation` → *(removed)* — + the orphan value schema of the payload's `operation` field; it had no other + consumer. + +One-line fix: delete the import — every one of the four names is TS2305 after +upgrade, and no runtime path ever produced or read a value of these types. A +host that used the retired type for a transport of its own keeps a local type. + +The retirement kit: + +- **whole-def deletion** (route 3 — not an authorable surface: no metadata-type + binding, stack collection or manifest embed ever carried it, and nothing + parsed it outside its own unit test, so there is no authored document to + rewrite and nobody who could receive a parse-time tombstone): + `kernel/MetadataChangedEventPayload` and `kernel/MetadataChangeOperation` in + `RETIRED_DEFS_BY_MAJOR[18]` plus the D3 semantic entry. The payload def was + never in `json-schema.manifest/` (the JSON Schema build skips `bigint`); the + enum was, so the manifest deletion gate adjudicates it against the entry. +- **retirement pins** in `kernel/cluster.test.ts`: runtime namespace probes + assert both names are absent from `kernel/cluster.zod` and from the + `@objectstack/spec/kernel` entry, with `ClusterCapabilityConfigSchema` as + the positive control; the two isomorphic alias pins left with the schemas. +- **docs**: `content/docs/kernel/cluster.mdx` no longer describes a planned + version-stamped payload or a version-compare reader contract — the shipped + address-only lanes and their re-read receipt are the contract; the + `ClusterMetadataChangedPayload` doc-comment in `@objectstack/metadata` no + longer claims to "align with" the retired schema (it never did). +- zero in-tree consumers, so no in-repo source changes ride along; runtime + behaviour is unchanged. diff --git a/content/docs/kernel/cluster.mdx b/content/docs/kernel/cluster.mdx index 2935677a10..93af82e1bb 100644 --- a/content/docs/kernel/cluster.mdx +++ b/content/docs/kernel/cluster.mdx @@ -296,31 +296,31 @@ they declare scope and strategy, and the runtime does the rest. Metadata is the most cache-hot data in the system. Every read path — ObjectQL planning, REST routing, UI rendering, permission checks — -touches it. Making it correct across a cluster requires two things the -current protocol lacks: a **monotonic version per item** and a -**well-defined invalidation event**. +touches it. Making it correct across a cluster needs a well-defined +invalidation signal per state owner — the three lanes of §6.2 — and, in this +protocol, deliberately **no** per-item version on the wire (§6.1). ### 6.1 Monotonic version A `version` column is already present on every persisted metadata record in `system/metadata-persistence.zod.ts` (`version: z.number()`, used today for -optimistic concurrency). This ADR proposes formalising it as a monotonic -per-item version and **widening it to `bigint`** so caches can compare -freshness across long-running clusters without overflow. - -> **Status: planned.** The column is still `version: number` in the schema; -> the widening and the cache-comparison contract below describe the target -> design, not current runtime behaviour. - -Once wired, caches store `{value, version}`. On any incoming change -notification the cache compares the incoming version with the stored one: - -- Incoming version `>` stored → invalidate (apply new value or evict) -- Incoming version `≤` stored → ignore (out-of-order notification, already - superseded) - -This eliminates a whole class of bugs where a slow-arriving "old" -invalidation evicts a "newer" value the node has already learned about. +optimistic concurrency). Earlier revisions of this page proposed formalising +it as a monotonic per-item version, widening it to `bigint`, and having every +cache compare it on receipt. + +> **Status: not declared.** The column is `version: number` in the schema, +> and nothing in the protocol carries a version across nodes. The +> version-compare invalidation this section once described as the target +> design — caches storing `{value, version}` and ignoring a notification whose +> version is not strictly greater than the cached one — went with the retired +> `MetadataChangedEventPayloadSchema` (§6.2, lane 1; ADR-0049 +> enforce-or-remove, ADR-0087 entry `metadata-changed-event-payload-retired`). +> The shipped lanes are address-only signals whose receiver re-reads its own +> store, and a re-ordered or duplicated delivery converges to the same state as +> a single one — which is what removes the "old invalidation evicts a newer +> value" class of bug without a version to compare. A version-stamped protocol +> returns only via the ENFORCE route of ADR-0049: the emitter and the reader +> first, the declaration with them. ### 6.2 The cluster-invalidation family @@ -394,30 +394,23 @@ the pre-write set (#5109). The registry entry is **deleted, never pre-filled** from the payload — the peer re-reads the shared store, which is the source of truth. -At the payload's top level there is still **no** `version` / `name` / -`tenantId` / `operation` field and **no** version comparison; the item's name -is carried only inside the replayed `event`. - -**Target spec (planned).** The richer, version-stamped payload below is -defined as `MetadataChangedEventPayloadSchema` in `kernel/cluster.zod.ts` -but is **not yet wired** into the runtime: - -```ts -// MetadataChangedEventPayloadSchema — target shape, not yet emitted -{ - type: 'object' | 'view' | 'flow' | …, - name: '', - tenantId?: '', - version: , - operation: 'create' | 'update' | 'delete' | 'publish', -} -``` - -When wired with `scope: 'cluster'` + `deliverySemantics: 'at-least-once'` -and a `partitionKey` of `` `${type}:${name}` ``, the partition key would -guarantee that two rapid updates to the same item are applied to every -node's cache in order, and the at-least-once guarantee would let a briefly -partitioned node catch up on reconnect. +At the payload's top level there is **no** `version` / `name` / `tenantId` +/ `operation` field and **no** version comparison — by design, not by +omission; the item's name is carried only inside the replayed `event`. + +**No richer payload is declared.** The spec once carried a version-stamped +target shape for this lane — `MetadataChangedEventPayloadSchema` in +`kernel/cluster.zod.ts` (`type` / `name` / `tenantId` / `version: bigint` / +`operation` / `correlationId`), whose docblock said every persistence layer +MUST emit it and every reader MUST compare `version` on receipt. Nothing ever +emitted or subscribed to it, and its `bigint` version could not have crossed a +JSON transport without a codec no driver ships, so it was retired under +ADR-0049 enforce-or-remove (ADR-0087 entry +`metadata-changed-event-payload-retired`, `RETIRED_DEFS_BY_MAJOR[18]`, +together with the `MetadataChangeOperationSchema` enum that typed its +`operation` field). The shape above **is** the lane-1 contract, and its +receipt semantics are the family's: drop the cache entry, re-read the shared +store, no version comparison. #### Lane 2 — `metadata.mutated` (the ObjectQL object registry) @@ -485,16 +478,18 @@ all; the admin service publishes on the write doors it already owns. ### 6.3 Reader contract -> **Status: planned.** Today readers are invalidated by the `metadata.changed` -> PubSub fan-out described in §6.2, which replays watch events verbatim -> without version comparison. The contract below is the target design that -> goes with the version-stamped payload. - -All metadata readers (registry, loader, query engine) should: +The reader contract is the one §6.2 states for the family, and it is what +ships — there is no version-stamped variant behind it (the payload that +carried one was retired, §6.1). A metadata reader that keeps its own +long-lived cache: -1. Subscribe to the metadata change channel on startup. -2. Compare incoming `version` with cached `version` before evicting. -3. Treat missing `version` as `0` (legacy compatibility). +1. Subscribes to the lane whose state it mirrors — `metadata.changed` for a + mirror of the metadata service's own caches; lanes 2 and 3 already keep + ObjectQL's object and driver registries converged (§7.3). +2. On receipt drops its own `originNode` messages, then evicts — never + pre-fills from the payload. +3. Re-reads the shared store on the next access; a duplicate or re-ordered + delivery converges to the same state as a single one. ## 7. Plugin author guidance @@ -572,8 +567,9 @@ cluster.pubsub.subscribe('metadata.changed', (msg) => { > already keep those registries converged across replicas, and a second > subscription would be redundant. -A higher-level cache factory (with automatic version comparison) is part of -the Phase 4 / §6 target design but is **not yet implemented**. No +There is no higher-level cache factory and no version comparison to +automate — the version-stamped payload was retired (§6.1) and the lanes are +address-only, so the subscribe-and-evict shape above is the whole contract. No `if (cluster)` branches are needed either way — the `memory` driver makes the subscribe call a no-op-equivalent local fan-out on a single node. diff --git a/content/docs/references/index.mdx b/content/docs/references/index.mdx index 8a02890e89..b1604c3ecb 100644 --- a/content/docs/references/index.mdx +++ b/content/docs/references/index.mdx @@ -1,6 +1,6 @@ --- title: Protocol Reference -description: Every schema published by @objectstack/spec — 1593 schemas across 14 protocol modules +description: Every schema published by @objectstack/spec — 1592 schemas across 14 protocol modules --- {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} @@ -26,14 +26,14 @@ counts are sums of the rows they head. Regenerate with | [Data Protocol](/docs/references/data) | 29 | 166 | Objects, fields, queries, filters, datasources and drivers — the ObjectQL layer. | | [Identity Protocol](/docs/references/identity) | 5 | 27 | Users and accounts, organizations, positions, SCIM provisioning. | | [Integration Protocol](/docs/references/integration) | 1 | 27 | The single connector protocol (ADR-0097) — catalog descriptors and provider-bound instances. | -| [Kernel Protocol](/docs/references/kernel) | 30 | 163 | Plugin lifecycle and manifests, capabilities and security, metadata loading, service registry. | +| [Kernel Protocol](/docs/references/kernel) | 30 | 162 | Plugin lifecycle and manifests, capabilities and security, metadata loading, service registry. | | [QA Protocol](/docs/references/qa) | 1 | 8 | Declarative test suites — scenarios, steps, actions and assertions. | | [Security Protocol](/docs/references/security) | 5 | 29 | Permission sets, row-level security, sharing rules, tenancy posture. | | [Shared Protocol](/docs/references/shared) | 8 | 26 | Primitives used across every protocol — identifiers, HTTP, expressions, error maps, enums. | | [Studio Protocol](/docs/references/studio) | 3 | 35 | Studio designer metadata — the authoring surfaces for the protocols above. | | [System Protocol](/docs/references/system) | 36 | 291 | The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance. | | [UI Protocol](/docs/references/ui) | 16 | 153 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. | -| **Total** | **200** | **1593** | 14 protocol modules | +| **Total** | **200** | **1592** | 14 protocol modules | --- @@ -217,14 +217,14 @@ The single connector protocol (ADR-0097) — catalog descriptors and provider-bo ## Kernel Protocol -**Source:** `packages/spec/src/kernel/` · **Import:** `@objectstack/spec/kernel` · **30 pages, 163 schemas** +**Source:** `packages/spec/src/kernel/` · **Import:** `@objectstack/spec/kernel` · **30 pages, 162 schemas** Plugin lifecycle and manifests, capabilities and security, metadata loading, service registry. | File | Schemas | | :--- | :--- | | [`cli-extension.zod.ts`](/docs/references/kernel/cli-extension) | `OclifPluginConfig` | -| [`cluster.zod.ts`](/docs/references/kernel/cluster) | `ClusterCapabilityConfig`, `ClusterDriver`, `ClusterTenantIsolation`, `EventClusterOptions`, `EventDeliverySemantics`, `EventScope`, `MetadataChangeOperation`, `ServiceClusterAnnotations`, `ServiceClusterScope`, `ServiceLeaderStrategy` | +| [`cluster.zod.ts`](/docs/references/kernel/cluster) | `ClusterCapabilityConfig`, `ClusterDriver`, `ClusterTenantIsolation`, `EventClusterOptions`, `EventDeliverySemantics`, `EventScope`, `ServiceClusterAnnotations`, `ServiceClusterScope`, `ServiceLeaderStrategy` | | [`context.zod.ts`](/docs/references/kernel/context) | `KernelContext`, `RuntimeMode`, `TenantRuntimeContext` | | [`dependency-resolution.zod.ts`](/docs/references/kernel/dependency-resolution) | `DependencyResolutionResult`, `DependencyStatusEnum`, `RequiredAction`, `ResolvedDependency` | | [`events/bus.zod.ts`](/docs/references/kernel/events-bus) | `EventBusConfig` | diff --git a/content/docs/references/kernel/cluster.mdx b/content/docs/references/kernel/cluster.mdx index 3a2c13cdb2..e51a8179fa 100644 --- a/content/docs/references/kernel/cluster.mdx +++ b/content/docs/references/kernel/cluster.mdx @@ -25,8 +25,8 @@ subsystems depend on. ## TypeScript Usage ```typescript -import { ClusterCapabilityConfigSchema, ClusterDriverSchema, ClusterTenantIsolationSchema, EventClusterOptionsSchema, EventDeliverySemanticsSchema, EventScopeSchema, MetadataChangeOperationSchema, ServiceClusterAnnotationsSchema, ServiceClusterScopeSchema, ServiceLeaderStrategySchema } from '@objectstack/spec/kernel'; -import type { ClusterCapabilityConfig, ClusterDriver, ClusterTenantIsolation, EventClusterOptions, EventDeliverySemantics, EventScope, MetadataChangeOperation, ServiceClusterAnnotations, ServiceClusterScope, ServiceLeaderStrategy } from '@objectstack/spec/kernel'; +import { ClusterCapabilityConfigSchema, ClusterDriverSchema, ClusterTenantIsolationSchema, EventClusterOptionsSchema, EventDeliverySemanticsSchema, EventScopeSchema, ServiceClusterAnnotationsSchema, ServiceClusterScopeSchema, ServiceLeaderStrategySchema } from '@objectstack/spec/kernel'; +import type { ClusterCapabilityConfig, ClusterDriver, ClusterTenantIsolation, EventClusterOptions, EventDeliverySemantics, EventScope, ServiceClusterAnnotations, ServiceClusterScope, ServiceLeaderStrategy } from '@objectstack/spec/kernel'; // Validate data const result = ClusterCapabilityConfigSchema.parse(data); @@ -118,20 +118,6 @@ Where the event must be delivered: local process, whole cluster, or tenant-scope * `tenant` ---- - -## MetadataChangeOperation - -Persistence operation that triggered the change. - -### Allowed Values - -* `create` -* `update` -* `delete` -* `publish` - - --- ## ServiceClusterAnnotations diff --git a/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md b/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md index 238d5c6684..85bd73fe7b 100644 --- a/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md +++ b/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md @@ -261,7 +261,7 @@ directory rather than per file. | `cloud/` | 83 | | `identity/` | 32 | | `integration/` | 10 | -| `kernel/` | 261 | +| `kernel/` | 260 | | `qa/` | 6 | | `shared/` | 20 | | `system/` | 370 | diff --git a/packages/metadata/src/metadata-manager.ts b/packages/metadata/src/metadata-manager.ts index 042f99b9c7..a29a87e3d5 100644 --- a/packages/metadata/src/metadata-manager.ts +++ b/packages/metadata/src/metadata-manager.ts @@ -230,8 +230,16 @@ function generateEventUuid(): string { * Payload format for cluster-wide metadata change broadcasts. * * Published on channel `metadata.changed` by any node mutating metadata - * and consumed by peers to invalidate their local caches. Aligns with - * `MetadataChangedEventPayload` in `cluster-semantics.mdx` §5. + * and consumed by peers to invalidate their local caches + * (`content/docs/kernel/cluster.mdx` §6.2, lane 1). This shape IS the whole + * contract: the origin node for loopback suppression, the metadata type, and + * the local watch event replayed verbatim — an address-only signal. There is + * no `version`, `name`, `tenantId` or `operation` at the top level and no + * version comparison on receipt; a peer drops its cache entry and re-reads + * the shared store. The spec once declared a richer, version-stamped + * `metadata:changed` payload that this shape was said to align with; it never + * did, and that schema was retired (ADR-0087 entry + * `metadata-changed-event-payload-retired`). */ export interface ClusterMetadataChangedPayload { /** Origin nodeId — used for loopback suppression. */ diff --git a/packages/spec/api-surface/kernel.json b/packages/spec/api-surface/kernel.json index 2f80c331e8..f44ed487cc 100644 --- a/packages/spec/api-surface/kernel.json +++ b/packages/spec/api-surface/kernel.json @@ -163,12 +163,8 @@ "MetadataBulkResultSchema (const)", "MetadataCategory (type)", "MetadataCategoryEnum (const)", - "MetadataChangeOperation (type)", - "MetadataChangeOperationSchema (const)", "MetadataChangeType (type)", "MetadataChangeTypeSchema (const)", - "MetadataChangedEventPayload (type)", - "MetadataChangedEventPayloadSchema (const)", "MetadataDependency (type)", "MetadataDependencySchema (const)", "MetadataDiffItem (type)", diff --git a/packages/spec/declaration-map/kernel.json b/packages/spec/declaration-map/kernel.json index 3c3b2e4c63..82d5a2b368 100644 --- a/packages/spec/declaration-map/kernel.json +++ b/packages/spec/declaration-map/kernel.json @@ -111,8 +111,6 @@ "MetadataBulkResult": "kernel/MetadataBulkResult", "MetadataBulkResultSchema": "kernel/MetadataBulkResult", "MetadataCategoryEnum": "kernel/MetadataCategoryEnum", - "MetadataChangeOperation": "kernel/MetadataChangeOperation", - "MetadataChangeOperationSchema": "kernel/MetadataChangeOperation", "MetadataChangeType": "kernel/MetadataChangeType", "MetadataChangeTypeSchema": "kernel/MetadataChangeType", "MetadataDependency": "kernel/MetadataDependency", diff --git a/packages/spec/export-origins/kernel.json b/packages/spec/export-origins/kernel.json index d392750d4f..efd1dae954 100644 --- a/packages/spec/export-origins/kernel.json +++ b/packages/spec/export-origins/kernel.json @@ -163,12 +163,8 @@ "MetadataBulkResultSchema": "src/kernel/metadata-plugin.zod.ts#MetadataBulkResultSchema (const)", "MetadataCategory": "src/kernel/package-artifact.zod.ts#MetadataCategory (type)", "MetadataCategoryEnum": "src/kernel/package-artifact.zod.ts#MetadataCategoryEnum (const)", - "MetadataChangeOperation": "src/kernel/cluster.zod.ts#MetadataChangeOperation (type)", - "MetadataChangeOperationSchema": "src/kernel/cluster.zod.ts#MetadataChangeOperationSchema (const)", "MetadataChangeType": "src/kernel/package-upgrade.zod.ts#MetadataChangeType (type)", "MetadataChangeTypeSchema": "src/kernel/package-upgrade.zod.ts#MetadataChangeTypeSchema (const)", - "MetadataChangedEventPayload": "src/kernel/cluster.zod.ts#MetadataChangedEventPayload (type)", - "MetadataChangedEventPayloadSchema": "src/kernel/cluster.zod.ts#MetadataChangedEventPayloadSchema (const)", "MetadataDependency": "src/kernel/metadata-plugin.zod.ts#MetadataDependency (type)", "MetadataDependencySchema": "src/kernel/metadata-plugin.zod.ts#MetadataDependencySchema (const)", "MetadataDiffItem": "src/kernel/package-upgrade.zod.ts#MetadataDiffItem (type)", diff --git a/packages/spec/json-schema.manifest/kernel.json b/packages/spec/json-schema.manifest/kernel.json index fa20090d66..b5b365ad59 100644 --- a/packages/spec/json-schema.manifest/kernel.json +++ b/packages/spec/json-schema.manifest/kernel.json @@ -59,7 +59,6 @@ "kernel/ManifestPermissions", "kernel/MetadataBulkResult", "kernel/MetadataCategoryEnum", - "kernel/MetadataChangeOperation", "kernel/MetadataChangeType", "kernel/MetadataDependency", "kernel/MetadataDiffItem", diff --git a/packages/spec/src/kernel/cluster.test.ts b/packages/spec/src/kernel/cluster.test.ts index 082c087bcb..639bcf2b83 100644 --- a/packages/spec/src/kernel/cluster.test.ts +++ b/packages/spec/src/kernel/cluster.test.ts @@ -10,7 +10,6 @@ import { ServiceClusterAnnotationsSchema, ClusterDriverSchema, ClusterCapabilityConfigSchema, - MetadataChangedEventPayloadSchema, } from './cluster.zod'; import { EventMetadataSchema } from './events/core.zod'; import { ServiceMetadataSchema, ServiceFactoryRegistrationSchema } from './service-registry.zod'; @@ -193,38 +192,38 @@ describe('cluster.zod', () => { }); }); - describe('MetadataChangedEventPayload', () => { - it('parses a canonical change payload', () => { - const parsed = MetadataChangedEventPayloadSchema.parse({ - type: 'object', - name: 'account', - version: 42n, - operation: 'update', - }); - expect(parsed.type).toBe('object'); - expect(parsed.version).toBe(42n); - expect(parsed.operation).toBe('update'); - }); - - it('accepts tenant-scoped overlay changes', () => { - const parsed = MetadataChangedEventPayloadSchema.parse({ - type: 'view', - name: 'account_list', - tenantId: 'tnt_abc', - version: 7n, - operation: 'publish', - }); - expect(parsed.tenantId).toBe('tnt_abc'); + describe('MetadataChangedEventPayload retirement (ADR-0049 enforce-or-remove)', () => { + // Runtime namespace probes, the registry-retirement.test.ts pattern: a + // removed export cannot be imported by name (would not compile), so the + // pin asks the namespace object. The payload schema declared a MUST-emit / + // MUST-subscribe `metadata:changed` contract that nothing ever produced or + // consumed, and its `z.bigint()` version could not cross a JSON transport; + // the `MetadataChangeOperationSchema` enum existed only to type its + // `operation` field and left with it as the orphan value schema. ADR-0087: + // `RETIRED_DEFS_BY_MAJOR[18]` + the D3 entry + // `metadata-changed-event-payload-retired`. Anti-vacuity guard on each + // probe: a neighbour that stayed. + const RETIRED = [ + 'MetadataChangedEventPayloadSchema', + 'MetadataChangeOperationSchema', + ] as const; + const SURVIVOR = 'ClusterCapabilityConfigSchema'; + + it('the payload schema and its orphan operation enum are no longer exported from kernel/cluster.zod', async () => { + const mod = (await import('./cluster.zod')) as unknown as Record; + for (const name of RETIRED) { + expect(Object.prototype.hasOwnProperty.call(mod, name)).toBe(false); + } + // Anti-vacuity: the sibling that deliberately stayed still resolves. + expect(Object.prototype.hasOwnProperty.call(mod, SURVIVOR)).toBe(true); }); - it('rejects payload without a version', () => { - expect(() => - MetadataChangedEventPayloadSchema.parse({ - type: 'object', - name: 'account', - operation: 'update', - }), - ).toThrow(); + it('nor from the `@objectstack/spec/kernel` entry', async () => { + const kernel = (await import('./index')) as unknown as Record; + for (const name of RETIRED) { + expect(Object.prototype.hasOwnProperty.call(kernel, name)).toBe(false); + } + expect(Object.prototype.hasOwnProperty.call(kernel, SURVIVOR)).toBe(true); }); }); }); diff --git a/packages/spec/src/kernel/cluster.zod.ts b/packages/spec/src/kernel/cluster.zod.ts index 37fdc590c1..61538d29e2 100644 --- a/packages/spec/src/kernel/cluster.zod.ts +++ b/packages/spec/src/kernel/cluster.zod.ts @@ -327,71 +327,24 @@ export type ClusterCapabilityConfig = z.input; // ========================================================================== -// Metadata Change Event Payload +// Retired: the `metadata:changed` event payload (ADR-0049 enforce-or-remove) // ========================================================================== - -/** - * Operation that triggered a `metadata:changed` event. - */ -export const MetadataChangeOperationSchema = z.enum([ - 'create', - 'update', - 'delete', - 'publish', -]).describe('Persistence operation that triggered the change.'); - -export type MetadataChangeOperation = z.input; - -/** - * Canonical payload for the `metadata:changed` event. - * - * All metadata persistence layers MUST emit this event after any successful - * write. Readers (registry caches, query engines, REST routers) MUST - * subscribe to it and compare `version` with their cached value before - * applying the invalidation — out-of-order older versions are ignored. - * - * @see content/docs/kernel/cluster.mdx §6 - */ -export const MetadataChangedEventPayloadSchema = lazySchema(() => z.object({ - /** - * Metadata type — e.g. `'object'`, `'view'`, `'flow'`, `'agent'`. - * Matches the `MetadataTypeSchema` enum. - */ - type: z.string().min(1).describe('Metadata type (e.g. "object", "view").'), - - /** - * Machine name of the changed item. - */ - name: z.string().min(1).describe('Machine name of the metadata item.'), - - /** - * Tenant scope when the change is tenant-overlaid. Absent for - * platform-default metadata. - */ - tenantId: z.string().optional().describe('Tenant id when the change is overlay-scoped.'), - - /** - * Monotonic version of the record after the change. Readers compare - * this with their cached value; only strictly greater versions - * invalidate. - * - * Modelled as `bigint` in the schema to support long-running clusters - * without 32-bit wraparound concerns; persisted as `numeric` / `int8` - * in storage. - */ - version: z.bigint().describe('Monotonic version of the record after the change.'), - - /** - * The operation that produced this version. - */ - operation: MetadataChangeOperationSchema, - - /** - * Optional correlation id for tracing the change back to a request / - * deploy / migration that produced it. - */ - correlationId: z.string().optional() - .describe('Trace correlation id of the originating request.'), -}).describe('Canonical payload for the metadata:changed cluster event.')); - -export type MetadataChangedEventPayload = z.input; +// +// `MetadataChangedEventPayloadSchema` (`type` / `name` / `tenantId` / +// `version: z.bigint()` / `operation` / `correlationId`) and the +// `MetadataChangeOperationSchema` enum that existed only to type its +// `operation` field were removed whole. The ADR-0087 registry is the +// declaration: `kernel/MetadataChangedEventPayload` and +// `kernel/MetadataChangeOperation` in `RETIRED_DEFS_BY_MAJOR[18]` plus the D3 +// semantic entry `metadata-changed-event-payload-retired`. The docblock said +// every metadata persistence layer MUST emit it and every reader MUST +// subscribe and version-compare; nothing ever did either, and the `bigint` +// version field could not have crossed a JSON transport without a codec no +// pubsub driver ships. The cluster channels that actually run are documented +// in `content/docs/kernel/cluster.mdx` §6.2: `metadata.changed` +// (`ClusterMetadataChangedPayload`, `@objectstack/metadata`), +// `metadata.mutated` (`ClusterMetadataMutationPayload`, +// `@objectstack/metadata-protocol`) and `datasource.mutated` — address-only +// signals whose receivers re-read their own store rather than compare +// versions. A version-stamped payload returns only via the ENFORCE route of +// ADR-0049: the emitter and the reader first, the declaration with them. diff --git a/packages/spec/src/migrations/entries/retired-defs/18.kernel__MetadataChangeOperation.ts b/packages/spec/src/migrations/entries/retired-defs/18.kernel__MetadataChangeOperation.ts new file mode 100644 index 0000000000..4e005290be --- /dev/null +++ b/packages/spec/src/migrations/entries/retired-defs/18.kernel__MetadataChangeOperation.ts @@ -0,0 +1,23 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +// #14180 — kernel/cluster.zod.ts `MetadataChangeOperationSchema` / +// `MetadataChangeOperation` (the `create` / `update` / `delete` / `publish` +// enum), the orphan value schema of the retired +// `kernel/MetadataChangedEventPayload` def registered beside it: it existed +// only to type that payload's `operation` field and had no other consumer +// anywhere — measured at the retirement's base commit 2cc461030, every hit +// was the payload itself, its own isomorphic alias pin and the generated +// artifacts; nothing in objectui at the pinned sha. An exported value schema +// with no consumer reads as a capability to whoever finds it (#3950), so it +// leaves with the payload (the playbook's orphan-value-schema rule, the +// `kernel/DistributedStateConfig` precedent). Unlike the payload this enum +// serializes, so it WAS in `json-schema.manifest/kernel.json` and the +// manifest deletion gate adjudicates its removal against this entry. Route 3, +// same declaration: this table plus the D3 semantic entry +// `metadata-changed-event-payload-retired`. +// +// Registered under 18, not 17: v17.0.0 was cut before this landed, so the +// removal ships on the 17.x line (launch-window convention: accept-set +// narrowings ride minor releases) and the prescription lives at the major +// boundary where `migrate meta` users look. +export const entry = 'kernel/MetadataChangeOperation'; diff --git a/packages/spec/src/migrations/entries/retired-defs/18.kernel__MetadataChangedEventPayload.ts b/packages/spec/src/migrations/entries/retired-defs/18.kernel__MetadataChangedEventPayload.ts new file mode 100644 index 0000000000..9aa792ebbb --- /dev/null +++ b/packages/spec/src/migrations/entries/retired-defs/18.kernel__MetadataChangedEventPayload.ts @@ -0,0 +1,36 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +// #14180 — kernel/cluster.zod.ts `MetadataChangedEventPayloadSchema` / +// `MetadataChangedEventPayload`, retired whole (ADR-0049 enforce-or-remove; +// triage ruling 2026-09-02: remove via the ADR-0087 route, ⛔ not "make a +// consumer" — that is contract growth with no pull). The docblock declared a +// MUST-emit / MUST-subscribe contract for a `metadata:changed` event — `type` +// / `name` / `tenantId` / `version: z.bigint()` / `operation` / +// `correlationId`, readers comparing `version` before invalidating — that +// nothing in the tree ever produced or consumed: zero runtime emitters, zero +// subscribers, zero imports outside `packages/spec` (its own unit test, the +// isomorphic alias pin and the generated artifacts), measured at the +// retirement's base commit 2cc461030 with positive controls in objectstack +// and objectui (pinned sha). It was unenforceable by construction: the +// `bigint` version field cannot cross a JSON transport (the standard +// serializer throws on it) without a codec no pubsub driver ships, so no +// conforming emitter could ever have existed. The three cluster channels that +// DO run — `metadata.changed` (`ClusterMetadataChangedPayload`, +// `@objectstack/metadata`), `metadata.mutated` +// (`ClusterMetadataMutationPayload`, `@objectstack/metadata-protocol`) and +// `datasource.mutated` — all carry an address-only signal whose receiver +// re-reads its own store (ruled 2026-09-01 for the registry lane), the +// opposite of the declared version-compare receipt, so the one plausible +// future consumer was decided against. Never in `json-schema.manifest/` (the +// JSON Schema build skips `bigint`), so the manifest deletion gate has nothing +// to adjudicate for this def; the entry is the declaration the retirement +// route requires. Route 3: not an authorable surface — no metadata-type +// binding, stack collection or manifest embed ever carried it — so no +// tombstone and no D2 conversion; this table plus the D3 semantic entry +// `metadata-changed-event-payload-retired` ARE the declaration. +// +// Registered under 18, not 17: v17.0.0 was cut before this landed, so the +// removal ships on the 17.x line (launch-window convention: accept-set +// narrowings ride minor releases) and the prescription lives at the major +// boundary where `migrate meta` users look. +export const entry = 'kernel/MetadataChangedEventPayload'; diff --git a/packages/spec/src/migrations/entries/semantic/18.metadata-changed-event-payload-retired.ts b/packages/spec/src/migrations/entries/semantic/18.metadata-changed-event-payload-retired.ts new file mode 100644 index 0000000000..0dfbbdbab8 --- /dev/null +++ b/packages/spec/src/migrations/entries/semantic/18.metadata-changed-event-payload-retired.ts @@ -0,0 +1,62 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +import type { SemanticMigration } from '../../types.js'; + +export const entry: SemanticMigration = { + id: 'metadata-changed-event-payload-retired', + surface: + 'kernel.cluster metadata change event payload (`MetadataChangedEventPayloadSchema` ' + + 'in kernel/cluster.zod.ts — 2 defs, 4 exported names: ' + + '`MetadataChangedEventPayloadSchema`, `MetadataChangedEventPayload`, ' + + '`MetadataChangeOperationSchema`, `MetadataChangeOperation`)', + replacement: + 'Nothing to migrate to, because nothing ever emitted or consumed it. The ' + + 'cluster invalidation channels that actually run are the three lanes ' + + 'documented in content/docs/kernel/cluster.mdx §6.2: `metadata.changed` ' + + '(`ClusterMetadataChangedPayload` in `@objectstack/metadata` — the origin ' + + 'node, the metadata type and the replayed watch event), `metadata.mutated` ' + + '(`ClusterMetadataMutationPayload` in `@objectstack/metadata-protocol`) and ' + + '`datasource.mutated` (`ClusterDatasourceMutationPayload` in ' + + '`@objectstack/service-datasource`). A host that needs cross-node cache ' + + 'invalidation subscribes to one of those; a host that held the retired type ' + + 'for a transport of its own keeps a local type — the spec no longer declares ' + + 'one.', + reason: + 'ADR-0049 enforce-or-remove (triage ruling 2026-09-02 on the spec seat: ' + + 'remove via the ADR-0087 route, not "make a consumer" — that is contract ' + + 'growth with no pull). The docblock declared that all metadata persistence ' + + 'layers MUST emit a `metadata:changed` event with this payload and that ' + + 'every reader MUST subscribe and compare `version` before invalidating. ' + + "Measured at the retirement's base commit with positive controls: zero " + + 'runtime producers, zero subscribers, zero imports outside packages/spec ' + + '(its own unit test, the isomorphic alias pin and the generated artifacts) ' + + 'in objectstack, and nothing in objectui at the pinned sha. It was ' + + 'unenforceable by construction — the `version` field is `z.bigint()`, ' + + 'which the standard JSON serializer refuses, so the payload as declared ' + + 'could not cross any pubsub transport without a codec no driver ships: a ' + + 'MUST-emit contract no conforming emitter could satisfy. The shipped ' + + 'channels all carry an address-only signal whose receiver re-reads its own ' + + 'store (the 2026-09-01 ruling for the registry lane), the opposite of the ' + + 'declared version-compare receipt, so the one plausible future consumer was ' + + 'decided against; the 2026-08-27 ruling on transitions removes a staged ' + + "window. `MetadataChangeOperationSchema` existed only to type the payload's " + + '`operation` field and leaves with it as its orphan value schema (the ' + + '`DistributedStateConfig` precedent). Route 3: not an authorable surface — ' + + 'no metadata-type binding, stack collection or manifest embed ever carried ' + + 'it, and nothing parsed it outside its own unit test — so no tombstone and ' + + 'no D2 conversion; `RETIRED_DEFS_BY_MAJOR[18]` ' + + '(`kernel/MetadataChangedEventPayload`, `kernel/MetadataChangeOperation`) ' + + 'plus this entry ARE the declaration.', + acceptanceCriteria: + 'No code imports `MetadataChangedEventPayloadSchema`, ' + + '`MetadataChangedEventPayload`, `MetadataChangeOperationSchema` or ' + + '`MetadataChangeOperation` from `@objectstack/spec` or ' + + '`@objectstack/spec/kernel` — every one is TS2305 after upgrade (pinned by ' + + 'runtime namespace probes in kernel/cluster.test.ts, with ' + + '`ClusterCapabilityConfigSchema` as the positive control). No metadata ' + + 'document needs editing: the schema was reachable from no metadata-type ' + + 'binding, stack collection or /meta door. ⚠️ Runtime behaviour is ' + + 'deliberately UNCHANGED: no emitter or subscriber ever existed, and the ' + + 'three shipped cluster lanes publish the same bytes before and after — the ' + + 'retirement removes a false declaration, not behaviour.', +}; diff --git a/packages/spec/src/migrations/registry.ts b/packages/spec/src/migrations/registry.ts index e35657ff63..bbb6c4630c 100644 --- a/packages/spec/src/migrations/registry.ts +++ b/packages/spec/src/migrations/registry.ts @@ -6988,6 +6988,64 @@ const step18: MigrationStep = { '`persistence.key`; `initialData` record values containing literal `${…}` keep parsing ' + 'byte-identically.', }, + { + id: 'metadata-changed-event-payload-retired', + surface: + 'kernel.cluster metadata change event payload (`MetadataChangedEventPayloadSchema` ' + + 'in kernel/cluster.zod.ts — 2 defs, 4 exported names: ' + + '`MetadataChangedEventPayloadSchema`, `MetadataChangedEventPayload`, ' + + '`MetadataChangeOperationSchema`, `MetadataChangeOperation`)', + replacement: + 'Nothing to migrate to, because nothing ever emitted or consumed it. The ' + + 'cluster invalidation channels that actually run are the three lanes ' + + 'documented in content/docs/kernel/cluster.mdx §6.2: `metadata.changed` ' + + '(`ClusterMetadataChangedPayload` in `@objectstack/metadata` — the origin ' + + 'node, the metadata type and the replayed watch event), `metadata.mutated` ' + + '(`ClusterMetadataMutationPayload` in `@objectstack/metadata-protocol`) and ' + + '`datasource.mutated` (`ClusterDatasourceMutationPayload` in ' + + '`@objectstack/service-datasource`). A host that needs cross-node cache ' + + 'invalidation subscribes to one of those; a host that held the retired type ' + + 'for a transport of its own keeps a local type — the spec no longer declares ' + + 'one.', + reason: + 'ADR-0049 enforce-or-remove (triage ruling 2026-09-02 on the spec seat: ' + + 'remove via the ADR-0087 route, not "make a consumer" — that is contract ' + + 'growth with no pull). The docblock declared that all metadata persistence ' + + 'layers MUST emit a `metadata:changed` event with this payload and that ' + + 'every reader MUST subscribe and compare `version` before invalidating. ' + + "Measured at the retirement's base commit with positive controls: zero " + + 'runtime producers, zero subscribers, zero imports outside packages/spec ' + + '(its own unit test, the isomorphic alias pin and the generated artifacts) ' + + 'in objectstack, and nothing in objectui at the pinned sha. It was ' + + 'unenforceable by construction — the `version` field is `z.bigint()`, ' + + 'which the standard JSON serializer refuses, so the payload as declared ' + + 'could not cross any pubsub transport without a codec no driver ships: a ' + + 'MUST-emit contract no conforming emitter could satisfy. The shipped ' + + 'channels all carry an address-only signal whose receiver re-reads its own ' + + 'store (the 2026-09-01 ruling for the registry lane), the opposite of the ' + + 'declared version-compare receipt, so the one plausible future consumer was ' + + 'decided against; the 2026-08-27 ruling on transitions removes a staged ' + + "window. `MetadataChangeOperationSchema` existed only to type the payload's " + + '`operation` field and leaves with it as its orphan value schema (the ' + + '`DistributedStateConfig` precedent). Route 3: not an authorable surface — ' + + 'no metadata-type binding, stack collection or manifest embed ever carried ' + + 'it, and nothing parsed it outside its own unit test — so no tombstone and ' + + 'no D2 conversion; `RETIRED_DEFS_BY_MAJOR[18]` ' + + '(`kernel/MetadataChangedEventPayload`, `kernel/MetadataChangeOperation`) ' + + 'plus this entry ARE the declaration.', + acceptanceCriteria: + 'No code imports `MetadataChangedEventPayloadSchema`, ' + + '`MetadataChangedEventPayload`, `MetadataChangeOperationSchema` or ' + + '`MetadataChangeOperation` from `@objectstack/spec` or ' + + '`@objectstack/spec/kernel` — every one is TS2305 after upgrade (pinned by ' + + 'runtime namespace probes in kernel/cluster.test.ts, with ' + + '`ClusterCapabilityConfigSchema` as the positive control). No metadata ' + + 'document needs editing: the schema was reachable from no metadata-type ' + + 'binding, stack collection or /meta door. ⚠️ Runtime behaviour is ' + + 'deliberately UNCHANGED: no emitter or subscriber ever existed, and the ' + + 'three shipped cluster lanes publish the same bytes before and after — the ' + + 'retirement removes a false declaration, not behaviour.', + }, { id: 'metadata-customization-protocol-retired', surface: @@ -10491,6 +10549,61 @@ export const RETIRED_DEFS_BY_MAJOR: Readonly> // `metadata-customization-protocol-retired` ARE the declaration (the #8715 // route-3 shape). 'kernel/MergeStrategyConfig', + // #14180 — kernel/cluster.zod.ts `MetadataChangeOperationSchema` / + // `MetadataChangeOperation` (the `create` / `update` / `delete` / `publish` + // enum), the orphan value schema of the retired + // `kernel/MetadataChangedEventPayload` def registered beside it: it existed + // only to type that payload's `operation` field and had no other consumer + // anywhere — measured at the retirement's base commit 2cc461030, every hit + // was the payload itself, its own isomorphic alias pin and the generated + // artifacts; nothing in objectui at the pinned sha. An exported value schema + // with no consumer reads as a capability to whoever finds it (#3950), so it + // leaves with the payload (the playbook's orphan-value-schema rule, the + // `kernel/DistributedStateConfig` precedent). Unlike the payload this enum + // serializes, so it WAS in `json-schema.manifest/kernel.json` and the + // manifest deletion gate adjudicates its removal against this entry. Route 3, + // same declaration: this table plus the D3 semantic entry + // `metadata-changed-event-payload-retired`. + // + // Registered under 18, not 17: v17.0.0 was cut before this landed, so the + // removal ships on the 17.x line (launch-window convention: accept-set + // narrowings ride minor releases) and the prescription lives at the major + // boundary where `migrate meta` users look. + 'kernel/MetadataChangeOperation', + // #14180 — kernel/cluster.zod.ts `MetadataChangedEventPayloadSchema` / + // `MetadataChangedEventPayload`, retired whole (ADR-0049 enforce-or-remove; + // triage ruling 2026-09-02: remove via the ADR-0087 route, ⛔ not "make a + // consumer" — that is contract growth with no pull). The docblock declared a + // MUST-emit / MUST-subscribe contract for a `metadata:changed` event — `type` + // / `name` / `tenantId` / `version: z.bigint()` / `operation` / + // `correlationId`, readers comparing `version` before invalidating — that + // nothing in the tree ever produced or consumed: zero runtime emitters, zero + // subscribers, zero imports outside `packages/spec` (its own unit test, the + // isomorphic alias pin and the generated artifacts), measured at the + // retirement's base commit 2cc461030 with positive controls in objectstack + // and objectui (pinned sha). It was unenforceable by construction: the + // `bigint` version field cannot cross a JSON transport (the standard + // serializer throws on it) without a codec no pubsub driver ships, so no + // conforming emitter could ever have existed. The three cluster channels that + // DO run — `metadata.changed` (`ClusterMetadataChangedPayload`, + // `@objectstack/metadata`), `metadata.mutated` + // (`ClusterMetadataMutationPayload`, `@objectstack/metadata-protocol`) and + // `datasource.mutated` — all carry an address-only signal whose receiver + // re-reads its own store (ruled 2026-09-01 for the registry lane), the + // opposite of the declared version-compare receipt, so the one plausible + // future consumer was decided against. Never in `json-schema.manifest/` (the + // JSON Schema build skips `bigint`), so the manifest deletion gate has nothing + // to adjudicate for this def; the entry is the declaration the retirement + // route requires. Route 3: not an authorable surface — no metadata-type + // binding, stack collection or manifest embed ever carried it — so no + // tombstone and no D2 conversion; this table plus the D3 semantic entry + // `metadata-changed-event-payload-retired` ARE the declaration. + // + // Registered under 18, not 17: v17.0.0 was cut before this landed, so the + // removal ships on the 17.x line (launch-window convention: accept-set + // narrowings ride minor releases) and the prescription lives at the major + // boundary where `migrate meta` users look. + 'kernel/MetadataChangedEventPayload', // #13135 — ADR-0049 enforce-or-remove (maintainer ruling 2026-08-29 on // #12057: retirement adopted, re-scope rejected; re-charter #13135 executes // the widened surface). Part of the whole-module removal of diff --git a/packages/spec/src/type-alias-convention.pin.test.ts b/packages/spec/src/type-alias-convention.pin.test.ts index a485934846..0880d747f7 100644 --- a/packages/spec/src/type-alias-convention.pin.test.ts +++ b/packages/spec/src/type-alias-convention.pin.test.ts @@ -270,7 +270,7 @@ import type * as M183 from './api/sortability.zod.js'; import type * as M184 from './shared/value-domain.zod.js'; // --------------------------------------------------------------------------- -// 830 isomorphic aliases: `z.input` === `z.infer`, so no `XParsed` is declared. +// 828 isomorphic aliases: `z.input` === `z.infer`, so no `XParsed` is declared. // // That number is machine-checked, not hand-kept. The runtime companion at the // bottom of this file recomputes the pin count from the source and asserts that @@ -860,8 +860,6 @@ export type Iso389 = Assert, export type Iso390 = Assert, z.infer< typeof M80.ServiceLeaderStrategySchema > >>; export type Iso391 = Assert, z.infer< typeof M80.ClusterDriverSchema > >>; export type Iso392 = Assert, z.infer< typeof M80.ClusterTenantIsolationSchema > >>; -export type Iso393 = Assert, z.infer< typeof M80.MetadataChangeOperationSchema > >>; -export type Iso394 = Assert, z.infer< typeof M80.MetadataChangedEventPayloadSchema > >>; // kernel/context.zod.ts export type Iso395 = Assert, z.infer< typeof M81.RuntimeMode > >>; @@ -1686,7 +1684,7 @@ describe('ADR-0122 type-alias convention', () => { // this title and the section header above the pin list — are now asserted // against the recomputed count below, so neither can go stale without a red // test naming it. - it('still declares all 830 isomorphic pins', () => { + it('still declares all 828 isomorphic pins', () => { // The truth of each pin is proved by tsc, not here — an `Assert>` // that stops holding is a compile error with the alias named. What tsc // cannot notice is a pin that was DELETED: removing the assertion removes @@ -2116,9 +2114,24 @@ describe('ADR-0122 type-alias convention', () => { // default or transform: the (RISE) case, one new pin (`Iso867`). // `SpecifierValueDomainSchema` became an alias of it, so its own pin // (`Iso758`) stays and the two hold or fall together. +1 added. + // + // 830 -> 828 is #14180's ADR-0049 retirement of the `metadata:changed` + // event payload (kernel/cluster.zod.ts): `MetadataChangedEventPayloadSchema` + // — a MUST-emit contract nothing ever produced or consumed, whose + // `z.bigint()` version could not cross a JSON transport — and the + // `MetadataChangeOperationSchema` enum that existed only to type its + // `operation` field left the module whole (RETIRED_DEFS_BY_MAJOR[18] + // `kernel/MetadataChangedEventPayload` + `kernel/MetadataChangeOperation`), + // so their pins `Iso393` / `Iso394` left with them — the aliases no longer + // exist, so there is nothing to be isomorphic. `kernel/cluster.test.ts` + // asserts the absence of both names on the module and the `./kernel` + // entry. -2 removed; the Iso numbers stay vacant (ids are claims about + // pins, not positions). (Authored as 829 -> 827; restated from the + // post-merge base after #14168's +1 landed first — the two changes touch + // disjoint pins.) const self = readFileSync(fileURLToPath(import.meta.url), 'utf8'); const pins = self.match(/^export type Iso\d+ = Assert