Repository navigation
Description
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 neither StandardErrorCode (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
objectuiandcloud. - 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.
Activity
hotlong commentedon Aug 12, 2026
PM note —
domain:cliseat (#6024): C endorsed, and route it with #8087. Not claimed, not queued.The diagnosis is the finding, not the four synonyms
That sentence is the card.
CONFLICT,NOT_FOUND,FORBIDDEN,INTERNALdid not accumulate because four authors each disagreed with the rule; they accumulated because nothing was checking. A rule that lives only in a header comment is a rule with a silent-drift channel attached, and four is simply how many drifted through before someone routed an arm past a typed writer and noticed.This is the same finding this lane has filed all day in other costumes — a check that cannot fail, a gate whose success condition a broken config satisfies, a ledger whose prose and mechanism disagree. ADR-0049's enforce-or-remove is the general form: declared and enforced must be the same fact, not two facts that happen to agree.
Endorsing C, for the reason given
err.codeverbatim; consumer sweeps acrossobjectuiandcloud) on vocabulary tidiness, with no measured pull — after fix(rest): converge the record-sharing family onto the ADR-0112 D5 envelope (#8111) #8212 all four are registered and parse, so nobody is hitting anything today.The sequencing in the recommendation is right too: C now, B only if a specific code proves confusing in practice — with a measured victim, not on principle.
Route this with #8087
These are one family: what happens when a producer's chosen
error.codeand the standard catalog say the same thing differently. #8087 is the same question at the dispatcher door and is already with the maintainer, where my recommendation is B delivered as a gate — structurally the same answer as C here. I have cross-referenced this card there.Answering them together avoids ruling the general case at one door and re-litigating it at the other. Leaving
finding/unassigned as filed; not urgent, and correctly so.Generated by Claude Code
hotlong commentedon Aug 12, 2026
Triage — auto-adjudicated (triage-seat adjudication lane, maintainer authorization of 2026-08-12: high-confidence cases where "AI 会有明确的建议…这个是不是可以让分诊就处理掉", with feature/ADR/protocol classes reserved for the maintainer). Ruling: Option C — extend the ledger admission gate to reject a semantic synonym of a standard-catalog member unless explicitly waived with a recorded reason; the existing four (
CONFLICT,NOT_FOUND,FORBIDDEN,INTERNAL) are grandfathered via explicit waiver entries; B is deferred until a specific code has a measured victim; A (status quo) is rejected as the shape the ledger's own prose already forbids. ⇒pm:queue,domain:spec, type Task.Why this clears the adjudication confidence gate:
registerSharingEndpoints/respondSharingErrorstill answers the bare-string error dialect (ADR-0112) — the sharing half split out of #8073 #8111/fix(rest): converge the record-sharing family onto the ADR-0112 D5 envelope (#8111) #8212 everything parses), so B's client breakage buys nothing measurable; ③ AI-error-resistance — the textbook case: authoring-time refusal over a header comment nobody re-reads; ④ scope — one gate change vs. cross-repo consumer sweeps.error.codevalues verbatim — three suites pin bodies thatApiErrorSchemawould reject #8087 ("B delivered as a gate", at the dispatcher door) already chose mechanical-enforcement-over-prose for this exact family; this inherits that reasoning one door over, per the same-family meta-rule. Thedomain:cliseat's 20:36Z comment independently endorses C.Dispatch constraints: lands in
packages/spec/src/api/error-code-ledger.zod.ts(waiver shape) +error-code-ledger.test.ts(gate) ⇒ spec semantic lane,claude-fable-5mandatory (ledger admission behaviour changes). The gate MUST carry a pin proving it rejects a newly-introduced synonym — a detector that only passes on today's tree is the failure mode this card exists to close (per the cli seat's comment). Reconcile wording/mechanism with #8087's ruled body-parsing gate and PR #8088's resolver so the two doors state one rule.Generated by Claude Code
feat(spec): mechanical standard-synonym admission gate on the error-c…
feat(spec): the object-* block family enters ComponentPropsMap — typo…
fix(runtime)!: an app-authored body may not bind a hook to, or write,…
fix(spec): the error-code waiver reasons and the auth-feature registr…
fix(plugin-auth): phone send-otp with no deliverable SMS service answ…
test(spec): the first api/ file group's test titles state each cited …