Found while implementing #11637 (making the REST server parse config.api instead of casting to it). Filed, not fixed — the repair is a packages/spec declaration, which is domain:spec's single-owner surface and outside #11637's declared scope.
This is the mirror image of #11637: that card is a key the spec declares and the runtime never enforces; this is a key the runtime honours and the spec never declares.
What was measured
On origin/main @ 7899f5745.
RestServer.normalizeConfig reads two keys through as any because they have no declared seat:
// packages/rest/src/rest-server.ts (normalizeConfig)enableOpenApi: (apiasany).enableOpenApi??true,enableSearch: (apiasany).enableSearch??true,
...
maskObjectFields: isObjectSchemaMaskingEnabled((metadataasany).maskObjectFields),
enableOpenApiis declared (packages/spec/src/api/rest-server.zod.ts), so its as any is stale residue — harmless, but it is what makes the line beside it look equally safe.enableSearch is not. git grep -c enableSearch origin/main -- packages/spec/src → zero hits (reverse-checked with enableDiscovery, 1 hit in the same file, and projectResolution, 3 hits across the directory). It is nonetheless live: NormalizedRestServerConfig.api.enableSearch is typed, and registerRoutes / the discovery capability block both read it, so enableSearch: false really does turn the search surface off today.metadata.maskObjectFields is the same shape, and the code says so in place: "MetadataEndpointsConfigSchema lives in packages/spec and giving this key a declared seat there is a separate change" (ADR-0106 D8).
Why it is not merely cosmetic
RestApiConfigSchema is not .strict(), and a non-strict z.object()strips what it does not declare. Measured directly:
RestApiConfigSchema.parse({ version: 'v1', enableSearch: false })
→ the returned object has no `enableSearch` property at all
So the moment anything parses this config and consumes the result, a deployment that turned search off silently gets it back on — no error, no warning, nothing to grep. That is the ADR-0104 silent-strip class the tombstones in packages/spec/src/shared/retired-key.ts exist to prevent, and it is a live trap rather than a hypothetical: #11637 had to make its parse validation-only, discarding the parsed value and normalizing from the raw input, specifically to avoid tripping it. That workaround is a cost this undeclared key imposes on every future parse at that seam.
There is also the authoring face: RestApiConfig is z.input<typeof RestApiConfigSchema>, so a TypeScript author writing api: { enableSearch: false } against the declared type gets an excess-property error today and has to cast — which is why every in-repo caller passes as any.
Not prejudged
- Declare both keys in
RestApiConfigSchema / MetadataEndpointsConfigSchema with the defaults the runtime already applies (enableSearch: z.boolean().default(true), and the ADR-0106 D8 default for maskObjectFields), then delete the three as any reads. Declared = enforced, and the seam's parse can consume its own output. - Retire
enableSearch under ADR-0049 enforce-or-remove if the deployment-wide search opt-out is not a capability worth keeping — it would need a tombstone, since deleting an honoured key silently is the failure above.
Either way enableOpenApi's stale as any should go with it: it is declared, and the cast is what makes its undeclared neighbour look normal.
Generated by Claude Code
Found while implementing #11637 (making the REST server parse
config.apiinstead of casting to it). Filed, not fixed — the repair is apackages/specdeclaration, which isdomain:spec's single-owner surface and outside #11637's declared scope.This is the mirror image of #11637: that card is a key the spec declares and the runtime never enforces; this is a key the runtime honours and the spec never declares.
What was measured
On
origin/main@7899f5745.RestServer.normalizeConfigreads two keys throughas anybecause they have no declared seat:enableOpenApiis declared (packages/spec/src/api/rest-server.zod.ts), so itsas anyis stale residue — harmless, but it is what makes the line beside it look equally safe.enableSearchis not.git grep -c enableSearch origin/main -- packages/spec/src→ zero hits (reverse-checked withenableDiscovery, 1 hit in the same file, andprojectResolution, 3 hits across the directory). It is nonetheless live:NormalizedRestServerConfig.api.enableSearchis typed, andregisterRoutes/ the discovery capability block both read it, soenableSearch: falsereally does turn the search surface off today.metadata.maskObjectFieldsis the same shape, and the code says so in place: "MetadataEndpointsConfigSchemalives inpackages/specand giving this key a declared seat there is a separate change" (ADR-0106 D8).Why it is not merely cosmetic
RestApiConfigSchemais not.strict(), and a non-strictz.object()strips what it does not declare. Measured directly:So the moment anything parses this config and consumes the result, a deployment that turned search off silently gets it back on — no error, no warning, nothing to grep. That is the ADR-0104 silent-strip class the tombstones in
packages/spec/src/shared/retired-key.tsexist to prevent, and it is a live trap rather than a hypothetical: #11637 had to make its parse validation-only, discarding the parsed value and normalizing from the raw input, specifically to avoid tripping it. That workaround is a cost this undeclared key imposes on every future parse at that seam.There is also the authoring face:
RestApiConfigisz.input<typeof RestApiConfigSchema>, so a TypeScript author writingapi: { enableSearch: false }against the declared type gets an excess-property error today and has to cast — which is why every in-repo caller passesas any.Not prejudged
RestApiConfigSchema/MetadataEndpointsConfigSchemawith the defaults the runtime already applies (enableSearch: z.boolean().default(true), and the ADR-0106 D8 default formaskObjectFields), then delete the threeas anyreads. Declared = enforced, and the seam's parse can consume its own output.enableSearchunder ADR-0049 enforce-or-remove if the deployment-wide search opt-out is not a capability worth keeping — it would need a tombstone, since deleting an honoured key silently is the failure above.Either way
enableOpenApi's staleas anyshould go with it: it is declared, and the cast is what makes its undeclared neighbour look normal.Generated by Claude Code