Found while running the read-only two-repo coverage audit for #6234 / #2657. Not fixed there (audit card, no code PR). Filed unassigned.
All line numbers are origin/main @ 8e2bbba24f76d56daad166b6b776c69d8c3ac0dc.
The shape
ObjectStackDefinitionSchema (packages/spec/src/stack.zod.ts) is the source of truth for which collections a stack may declare. Six other places re-enumerate that same set, each hand-maintained, and nothing compares any of them to the schema or to each other. They have drifted independently:
| # |
enumeration |
site |
observed drift |
| 1 |
ObjectStackDefinitionSchema |
packages/spec/src/stack.zod.ts |
— (the source of truth) |
| 2 |
PLURAL_TO_SINGULAR / MAP_SUPPORTED_FIELDS |
packages/spec/src/shared/metadata-collection.zod.ts:80, :110 |
carries ragPipelines → rag_pipeline; stack.zod.ts declares no ragPipelines |
| 3 |
metadataArrayKeys (generic registration) |
packages/objectql/src/engine.ts:2283, duplicated at :2459 |
lists workflows, approvals, roles, profiles, policies, ragPipelines — none of which stack.zod.ts declares (ADR-0019 / ADR-0020 / ADR-0088 / ADR-0090 retirements) |
| 4 |
ARTIFACT_FIELD_TO_TYPE |
packages/metadata/src/plugin.ts:60 |
see "the concrete part" below |
| 5 |
MetadataCategoryEnum |
packages/spec/src/kernel/package-artifact.zod.ts:49 |
lists retired triggers and workflows; omits ~12 live collections (hooks, mappings, docs, books, jobs, positions, sharingRules, webhooks, connectors, emailTemplates, datasources, capabilities) |
| 6 |
STACK_COLLECTION_COVERAGE |
examples/app-showcase/src/coverage.ts:181 |
tracks 3 of ~9 non-kind collections, and — unlike its sibling KIND_COVERAGE — is not ratcheted against anything |
The concrete part — ARTIFACT_FIELD_TO_TYPE
Two distinct problems in packages/metadata/src/plugin.ts:60-93:
(a) data: 'dataset' is mis-aimed. The stack key data: is the seed collection (stack.zod.ts:510 — z.array(SeedSchema); the runtime reads it as seeds at packages/runtime/src/app-plugin.ts:927). It is mapped here to 'dataset', which since ADR-0021 is the analytics semantic layer kind — the exact collision the registry entry warns about in prose (metadata-plugin.zod.ts:620: "NOTE: distinct from the (analytics-bound) dataset name").
Honest scoping: this entry is provably inert today, not a live mis-registration. SeedSchema is a strictObject with no name key (packages/spec/src/data/seed.zod.ts:33), and the ingest loop skips any item without one (plugin.ts:667-674, if (!name) continue;) — the file's own comment at :718 says so: "seeds under data have no name". So it is a dead entry aimed at the wrong kind, which would begin mis-registering the day either side moves.
(b) Five live collections are absent from the map — datasets, jobs, datasources, translations, capabilities. This is exactly the regression class that packages/metadata/src/plugin.test.ts:114-121 pins for docs:
a compiled artifact carries package docs in a top-level docs: DocSchema[] array. The artifact loader registers only the metadata fields enumerated in ARTIFACT_FIELD_TO_TYPE; docs was omitted, so the bundle's docs were silently dropped and GET /meta/doc returned an empty list even though the package shipped docs.
For four of the five, AppPlugin handles the functional consumption directly off the bundle (app-plugin.ts:503 datasources, :825 jobs, :927 seeds, :1421 translations), so this is not a boot breakage — the gap is that they never register as metadata items, so under bootstrap: 'artifact-only' (edge / serverless / immutable-image, metadata-plugin.zod.ts:505-511) GET /meta/job, /meta/translation, /meta/datasource and /meta/dataset would answer empty for a package that ships them. datasets has no AppPlugin handling at all in that path. Not measured on a real artifact-only boot — that verification is part of the fix, not of this filing.
Why file it as one issue
Each row above looks like a one-line typo in isolation, and each has been fixed one-line-at-a-time before (docs in ARTIFACT_FIELD_TO_TYPE, roles → positions in the same map, capabilities in metadataArrayKeys — every one of those carries a "this key was missing and it silently dropped X" comment today). The cause is structural: KIND_COVERAGE is answerable to DEFAULT_METADATA_TYPE_REGISTRY and fails CI when a kind is added without an entry (examples/app-showcase/test/coverage.test.ts:96-104), and the liveness ledger is answerable to the same registry (packages/spec/scripts/liveness/check-liveness.mts:145-153). The collection-key maps have no such gate. A check that compares each of these six against stack.zod.ts — declared-here-not-there and there-not-here, with an explicit waiver list for the deliberate omissions (views has no name, data seeds key by object, translations is a record) — would have caught all of the above and every future instance.
Relevance to in-flight work
Directly relevant to #2657: promoting any declared collection to a registered metadata kind means hand-editing several of these maps, with no gate to catch a miss. PR #5312 (api) had to touch them by hand.
Suggested scope
- A
scripts/check-stack-collection-maps.mjs-style gate comparing all six against stack.zod.ts, with a named-waiver list carrying a reason per exemption (the KIND_COVERAGE / liveness-ratchet pattern).
- Fix the drift the gate then reports — including deciding whether
data: should map to seed, be removed, or stay waived.
Filed per AGENTS.md Prime Directive #10 while auditing for #6234. Not assigned; no PR.
Found while running the read-only two-repo coverage audit for #6234 / #2657. Not fixed there (audit card, no code PR). Filed unassigned.
All line numbers are
origin/main@8e2bbba24f76d56daad166b6b776c69d8c3ac0dc.The shape
ObjectStackDefinitionSchema(packages/spec/src/stack.zod.ts) is the source of truth for which collections a stack may declare. Six other places re-enumerate that same set, each hand-maintained, and nothing compares any of them to the schema or to each other. They have drifted independently:ObjectStackDefinitionSchemapackages/spec/src/stack.zod.tsPLURAL_TO_SINGULAR/MAP_SUPPORTED_FIELDSpackages/spec/src/shared/metadata-collection.zod.ts:80,:110ragPipelines→rag_pipeline;stack.zod.tsdeclares noragPipelinesmetadataArrayKeys(generic registration)packages/objectql/src/engine.ts:2283, duplicated at:2459workflows,approvals,roles,profiles,policies,ragPipelines— none of whichstack.zod.tsdeclares (ADR-0019 / ADR-0020 / ADR-0088 / ADR-0090 retirements)ARTIFACT_FIELD_TO_TYPEpackages/metadata/src/plugin.ts:60MetadataCategoryEnumpackages/spec/src/kernel/package-artifact.zod.ts:49triggersandworkflows; omits ~12 live collections (hooks,mappings,docs,books,jobs,positions,sharingRules,webhooks,connectors,emailTemplates,datasources,capabilities)STACK_COLLECTION_COVERAGEexamples/app-showcase/src/coverage.ts:181KIND_COVERAGE— is not ratcheted against anythingThe concrete part —
ARTIFACT_FIELD_TO_TYPETwo distinct problems in
packages/metadata/src/plugin.ts:60-93:(a)
data: 'dataset'is mis-aimed. The stack keydata:is the seed collection (stack.zod.ts:510—z.array(SeedSchema); the runtime reads it as seeds atpackages/runtime/src/app-plugin.ts:927). It is mapped here to'dataset', which since ADR-0021 is the analytics semantic layer kind — the exact collision the registry entry warns about in prose (metadata-plugin.zod.ts:620: "NOTE: distinct from the (analytics-bound)datasetname").Honest scoping: this entry is provably inert today, not a live mis-registration.
SeedSchemais astrictObjectwith nonamekey (packages/spec/src/data/seed.zod.ts:33), and the ingest loop skips any item without one (plugin.ts:667-674,if (!name) continue;) — the file's own comment at:718says so: "seeds underdatahave noname". So it is a dead entry aimed at the wrong kind, which would begin mis-registering the day either side moves.(b) Five live collections are absent from the map —
datasets,jobs,datasources,translations,capabilities. This is exactly the regression class thatpackages/metadata/src/plugin.test.ts:114-121pins fordocs:For four of the five,
AppPluginhandles the functional consumption directly off the bundle (app-plugin.ts:503datasources,:825jobs,:927seeds,:1421translations), so this is not a boot breakage — the gap is that they never register as metadata items, so underbootstrap: 'artifact-only'(edge / serverless / immutable-image,metadata-plugin.zod.ts:505-511)GET /meta/job,/meta/translation,/meta/datasourceand/meta/datasetwould answer empty for a package that ships them.datasetshas noAppPluginhandling at all in that path. Not measured on a real artifact-only boot — that verification is part of the fix, not of this filing.Why file it as one issue
Each row above looks like a one-line typo in isolation, and each has been fixed one-line-at-a-time before (
docsinARTIFACT_FIELD_TO_TYPE,roles→positionsin the same map,capabilitiesinmetadataArrayKeys— every one of those carries a "this key was missing and it silently dropped X" comment today). The cause is structural:KIND_COVERAGEis answerable toDEFAULT_METADATA_TYPE_REGISTRYand fails CI when a kind is added without an entry (examples/app-showcase/test/coverage.test.ts:96-104), and the liveness ledger is answerable to the same registry (packages/spec/scripts/liveness/check-liveness.mts:145-153). The collection-key maps have no such gate. A check that compares each of these six againststack.zod.ts— declared-here-not-there and there-not-here, with an explicit waiver list for the deliberate omissions (viewshas noname,dataseeds key byobject,translationsis a record) — would have caught all of the above and every future instance.Relevance to in-flight work
Directly relevant to #2657: promoting any declared collection to a registered metadata kind means hand-editing several of these maps, with no gate to catch a miss. PR #5312 (
api) had to touch them by hand.Suggested scope
scripts/check-stack-collection-maps.mjs-style gate comparing all six againststack.zod.ts, with a named-waiver list carrying a reason per exemption (theKIND_COVERAGE/ liveness-ratchet pattern).data:should map toseed, be removed, or stay waived.Filed per AGENTS.md Prime Directive #10 while auditing for #6234. Not assigned; no PR.