Found while converging the record-sharing family onto the ADR-0112 D5 envelope (#8111, PR pending). Filing unassigned — this needs a maintainer contract ruling, not a sweep.
What was measured
respondSharingError's 409 arm emits code: 'CONFLICT'. That value was registered in neitherStandardErrorCode (packages/spec/src/api/errors.zod.ts) nor ERROR_CODE_LEDGER (packages/spec/src/api/error-code-ledger.zod.ts), while ErrorCode — what ApiErrorSchema.code parses against — is exactly their union. So the 409 body could not have parsed as the declared envelope: ADR-0112's "silent fourth state", live on the wire and documented at content/docs/kernel/runtime-services/sharing-service.mdx.
The arm is live, not dormant: packages/plugins/plugin-sharing/src/sharing-service.ts:1042 throws it for revoke on a rule-materialised share.
#8111 registered the existing value under @objectstack/rest, which keeps the wire byte-identical (renaming it would change what clients read, and that card was explicitly not licensed to move a code VALUE).
The finding
Registering it is consistent with what that block already does, and that is the part worth a ruling. @objectstack/rest now registers four codes whose condition the standard catalog already covers generically:
| ledger entry | standard-catalog member for the same condition |
|---|
CONFLICT | RESOURCE_CONFLICT (409) |
NOT_FOUND | RESOURCE_NOT_FOUND (404) |
FORBIDDEN | PERMISSION_DENIED (403) |
INTERNAL | INTERNAL_ERROR (500) |
The ledger's own header rule says the opposite should happen:
If the condition is generic (not found / permission / validation / rate limit), use the standard catalog instead of registering a synonym.
That rule is prose only. The mechanical admission gate (error-code-ledger.test.ts, "no registered code shadows the standard catalog") rejects only a code that is literally a StandardErrorCode member, so every synonym above passes — which is why four accumulated without anyone deciding to allow them.
Why this is a contract call and not a cleanup
Each of the four is on the wire today, so consolidating any of them onto the standard member is a breaking change for clients branching on err.code — the SDK surfaces it verbatim (packages/client/src/index.ts, error.code). The options are not equal-cost:
- A — leave them registered. Zero client impact. Cost: the closed vocabulary carries two words per condition permanently, and the prose rule is dead letter, so the next author copies the pattern.
- B — consolidate onto the standard catalog, one card per code, with a deprecation window. Cost: a real wire change per code, needing consumer sweeps across
objectui and cloud. - C — keep them but make the rule mechanical (extend the admission gate to reject a semantic synonym of a standard member unless explicitly waived with a reason). Cost: one gate change; stops the fifth from landing without deciding the fate of the existing four.
Recommendation: C now, B only if a specific code proves confusing in practice. C is the option that makes the mistake structurally hard to repeat (the AI-authored-metadata axis) without paying a breaking change for vocabulary tidiness that no user is currently hitting — and it converts the header's prose into something enforced, which is what "declared = enforced" asks for. A is the status quo the prose already rejects, and B spends real client breakage on a problem that is presently cosmetic.
Not urgent: after #8111 every one of the four is registered, so nothing emits an unparseable body today. The exposure is future drift, which is why this is filed as a finding rather than queued.
Found while converging the record-sharing family onto the ADR-0112 D5 envelope (#8111, PR pending). Filing unassigned — this needs a maintainer contract ruling, not a sweep.
What was measured
respondSharingError's 409 arm emitscode: 'CONFLICT'. That value was registered in neitherStandardErrorCode(packages/spec/src/api/errors.zod.ts) norERROR_CODE_LEDGER(packages/spec/src/api/error-code-ledger.zod.ts), whileErrorCode— whatApiErrorSchema.codeparses against — is exactly their union. So the 409 body could not have parsed as the declared envelope: ADR-0112's "silent fourth state", live on the wire and documented atcontent/docs/kernel/runtime-services/sharing-service.mdx.The arm is live, not dormant:
packages/plugins/plugin-sharing/src/sharing-service.ts:1042throws it forrevokeon a rule-materialised share.#8111 registered the existing value under
@objectstack/rest, which keeps the wire byte-identical (renaming it would change what clients read, and that card was explicitly not licensed to move a code VALUE).The finding
Registering it is consistent with what that block already does, and that is the part worth a ruling.
@objectstack/restnow registers four codes whose condition the standard catalog already covers generically:CONFLICTRESOURCE_CONFLICT(409)NOT_FOUNDRESOURCE_NOT_FOUND(404)FORBIDDENPERMISSION_DENIED(403)INTERNALINTERNAL_ERROR(500)The ledger's own header rule says the opposite should happen:
That rule is prose only. The mechanical admission gate (
error-code-ledger.test.ts, "no registered code shadows the standard catalog") rejects only a code that is literally aStandardErrorCodemember, so every synonym above passes — which is why four accumulated without anyone deciding to allow them.Why this is a contract call and not a cleanup
Each of the four is on the wire today, so consolidating any of them onto the standard member is a breaking change for clients branching on
err.code— the SDK surfaces it verbatim (packages/client/src/index.ts,error.code). The options are not equal-cost:objectuiandcloud.Recommendation: C now, B only if a specific code proves confusing in practice. C is the option that makes the mistake structurally hard to repeat (the AI-authored-metadata axis) without paying a breaking change for vocabulary tidiness that no user is currently hitting — and it converts the header's prose into something enforced, which is what "declared = enforced" asks for. A is the status quo the prose already rejects, and B spends real client breakage on a problem that is presently cosmetic.
Not urgent: after #8111 every one of the four is registered, so nothing emits an unparseable body today. The exposure is future drift, which is why this is filed as a finding rather than queued.