Found while censusing userMessage producer marks for #13624 (superseded by #14674). Out of scope for that card, filed unassigned for triage. Measured against origin/main2aa8456cf2d6.
The finding
packages/spec/src/integration/connector.zod.ts declares two authorable metadata shapes that nothing in the repo reads:
| declaration | keys | anchor |
|---|
ErrorMappingConfigSchema | 4 (defaultCategory, logUnmapped, rules, unmappedBehavior) | connector.zod.ts |
ErrorMappingRuleSchema | 7 (sourceCode, sourceMessage, targetCode, targetCategory, severity, retryable, userMessage) | connector.zod.ts:489-497 |
All 11 are on the published authorable surface (packages/spec/authorable-surface/integration.json:110-120, and authorable-surface.base.json:4072-4078), so an author can write them and they validate.
Consumers, measured:
ErrorMappingRule / ErrorMappingRuleSchema — outside connector.zod.ts and connector.test.ts, the only hit in the whole repo is packages/spec/src/type-alias-convention.pin.test.ts:840, a type-identity pin. No runtime reader.ErrorMappingConfiguration / ErrorMappingConfig / errorMapping — zero hits anywhere in packages/**/*.ts outside the declaring file.- The distinctive member names
sourceCode, targetCode, targetCategory, sourceMessage — zero hits outside the declaring file. (severity and retryable do hit elsewhere, but on unrelated schemas.)
Nothing consumes the config, so nothing can consume the rules inside it. This is a declared-but-unenforced authorable surface in the ADR-0049 enforce-or-remove sense. It is not tracked in packages/spec/liveness/ — grep -rn ErrorMappingRule packages/spec/liveness/ returns nothing — so the liveness ledger does not currently know about it either.
⭐ Why this is worse than ordinary dead surface, and why it is filed rather than left in the ledger backlog
One of the inert keys is spelled userMessage, and its describe reads:
userMessage: z.string().optional().describe('Human-readable message to show users'),That is, to the letter, what the liveuserMessage channel does — the #9934 producer-marked user-facing refusal text carried by ApiErrorSchema.userMessage / EnhancedApiErrorSchema.userMessage and emitted by eight wire doors (censused in #14674). The two are unrelated: the live channel is read by declaredUserMessage (packages/types/src/thrown-http-error.ts:270) off a thrown error; this one is a metadata property on a connector error-mapping rule that no code path reads.
So an author — human or AI — who has read the userMessage documentation and then writes an error-mapping rule for a connector will reasonably believe they are marking a refusal for an end user. They are writing a key that validates, publishes, and does nothing. The failure is silent in both directions: no validation error, and no message ever shown.
That is the "declared = enforced" property inverted, on a name that already has a meaning elsewhere in the same spec package — which is the class this repo treats as expensive precisely because AI-authored metadata cannot tell the difference between the two spellings.
Not proposed here
Whether the route is removal (ADR-0087 conversion) or implementation is a triage call, and the spec-property-retirement playbook owns the ledger discipline either way. ⛔ No route chosen, no code touched. Flagging only that if the surface is kept, the userMessage collision should be resolved by name regardless of the outcome of #14674, since the two keys will otherwise keep meaning different things under one spelling.
Refs
https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
Found while censusing
userMessageproducer marks for #13624 (superseded by #14674). Out of scope for that card, filed unassigned for triage. Measured againstorigin/main2aa8456cf2d6.The finding
packages/spec/src/integration/connector.zod.tsdeclares two authorable metadata shapes that nothing in the repo reads:ErrorMappingConfigSchemadefaultCategory,logUnmapped,rules,unmappedBehavior)connector.zod.tsErrorMappingRuleSchemasourceCode,sourceMessage,targetCode,targetCategory,severity,retryable,userMessage)connector.zod.ts:489-497All 11 are on the published authorable surface (
packages/spec/authorable-surface/integration.json:110-120, andauthorable-surface.base.json:4072-4078), so an author can write them and they validate.Consumers, measured:
ErrorMappingRule/ErrorMappingRuleSchema— outsideconnector.zod.tsandconnector.test.ts, the only hit in the whole repo ispackages/spec/src/type-alias-convention.pin.test.ts:840, a type-identity pin. No runtime reader.ErrorMappingConfiguration/ErrorMappingConfig/errorMapping— zero hits anywhere inpackages/**/*.tsoutside the declaring file.sourceCode,targetCode,targetCategory,sourceMessage— zero hits outside the declaring file. (severityandretryabledo hit elsewhere, but on unrelated schemas.)Nothing consumes the config, so nothing can consume the rules inside it. This is a declared-but-unenforced authorable surface in the ADR-0049 enforce-or-remove sense. It is not tracked in
packages/spec/liveness/—grep -rn ErrorMappingRule packages/spec/liveness/returns nothing — so the liveness ledger does not currently know about it either.⭐ Why this is worse than ordinary dead surface, and why it is filed rather than left in the ledger backlog
One of the inert keys is spelled
userMessage, and itsdescribereads:That is, to the letter, what the live
userMessagechannel does — the #9934 producer-marked user-facing refusal text carried byApiErrorSchema.userMessage/EnhancedApiErrorSchema.userMessageand emitted by eight wire doors (censused in #14674). The two are unrelated: the live channel is read bydeclaredUserMessage(packages/types/src/thrown-http-error.ts:270) off a thrown error; this one is a metadata property on a connector error-mapping rule that no code path reads.So an author — human or AI — who has read the
userMessagedocumentation and then writes an error-mapping rule for a connector will reasonably believe they are marking a refusal for an end user. They are writing a key that validates, publishes, and does nothing. The failure is silent in both directions: no validation error, and no message ever shown.That is the "declared = enforced" property inverted, on a name that already has a meaning elsewhere in the same spec package — which is the class this repo treats as expensive precisely because AI-authored metadata cannot tell the difference between the two spellings.
Not proposed here
Whether the route is removal (ADR-0087 conversion) or implementation is a triage call, and the
spec-property-retirementplaybook owns the ledger discipline either way. ⛔ No route chosen, no code touched. Flagging only that if the surface is kept, theuserMessagecollision should be resolved by name regardless of the outcome of #14674, since the two keys will otherwise keep meaning different things under one spelling.Refs
userMessagebound: 8 doors, 2 behaviours — andpackages/restholds BOTH, at two functions namedsendThrownError#14674 — theuserMessagebound decision card whose census turned this up.userMessageBOUND is decided per-boundary:/datatruncates it, the two runtime doors emit it verbatim — no ruling covers which is right #13624 — superseded by Rule theuserMessagebound: 8 doors, 2 behaviours — andpackages/restholds BOTH, at two functions namedsendThrownError#14674.userMessagechannel this key's name collides with.https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza