Measured on origin/main@db39dfc1c plus the #12975 implementation branch (claude/issue-12975-data-door-strips-code-prefix). Filed unassigned from the #12975 dev seat, deliberately outside that card's ruled scope.
What the #12975 ruling moved, and what it did not
The 2026-08-29 maintainer ruling adopted option 1 on #12975: classifyDataError's declared-4xx arm strips the ADR-0111 CODE: prefix from the human-readable error string, so error is human language and code is the machine token.
packages/rest/src/error-response.ts has two declared-4xx arms, not one, and the ruling named the first:
| arm |
reached from |
ships the prefix after #12975 |
classifyDataError's declared-status arm |
mapDataError — the by-id CRUD routes |
no (the ruled fix) |
resolveErrorResponse's passThroughStatus 4xx arm |
handleRouteError / sendThrownError, and classifiedRefusalAnswer |
yes |
The second arm is checked before it delegates to mapDataError, which is the same structural fact rest-hook-refusal-message-parity.test.ts documents for the sandbox-wrapper defect: "Batch / bulk / clone exit through handleRouteError to resolveErrorResponse, whose declared-status passthrough is checked BEFORE it delegates to mapDataError."
Measured, driving the real registered route handlers
One producer for every row: a thrown Error carrying code: 'FORBIDDEN', status: 403 and the message FORBIDDEN: followed by the localized sentence — the exact shape plugin-sharing/src/sharing-plugin.ts's by-id write gate throws.
PATCH /api/v1/data/:object/:id
{"error":"您无权修改或删除这条记录,如需修改请联系该记录的负责人或管理员。","code":"FORBIDDEN","object":"showcase_inquiry"} ← converged
POST /api/v1/data/:object/batch
{"error":"FORBIDDEN: 您无权修改或删除这条记录,如需修改请联系该记录的负责人或管理员。","code":"FORBIDDEN"} ← still prefixed
GET /api/v1/data/:object/:id/shares
DELETE /api/v1/data/:object/:id/shares/:shareId
{"success":false,"error":{"code":"FORBIDDEN","message":"FORBIDDEN: 您无权修改或删除这条记录,如需修改请联系该记录的负责人或管理员。"}} ← still prefixed
So after #12975 lands, the same refusal reads two ways depending on which route caught it — the door-disagreement shape #7525 / #8016 / #11588 keep producing, now one arm over.
Why the share family is the sharper half
respondSharingError has two arms and only one of them strips:
That means rest-server.ts's own #8111 comment — "The prefix is a SERVER-INTERNAL service to REST derivation: it is stripped below and never reaches the wire, so no consumer can read it" — is true for the producers it was written about (the 11 bare-Error throw sites in sharing-service.ts, re-censused at #11683) and not true for the newer classified limb beside it.
Not a regression from #12975
Both rows above ship the prefix on main today as well; #12975 does not change either. What #12975 changes is that the by-id route stops agreeing with them.
Options, not a recommendation
- Apply the same declared-code-anchored strip in
resolveErrorResponse's 4xx arm, which converges every /data exit and — because the share family re-dresses that same classification — the record-share classified arm with it. Moves the pins that assert the prefixed text on those doors.
- Leave
resolveErrorResponse alone and rule that the by-id door is the only one converging, correcting the #8111 comment to name the limb it does not cover.
- Converge the record-share family only, leaving
/data's bulk exits as they are.
Option 1 is the one that matches #12975's stated goal ("one envelope semantics"), but it moves pins beyond the two the #12975 ruling authorised, which is why it was not taken in that PR and is filed here instead of decided at a dev seat.
Evidence in the tree
packages/rest/src/rest-data-door-code-prefix.test.ts carries a case titled "MEASURED, NOT REPAIRED HERE" that pins both rows above. It reddens the day either exit converges, so whichever option is adopted moves it deliberately.
Measured on
origin/main@db39dfc1cplus the #12975 implementation branch (claude/issue-12975-data-door-strips-code-prefix). Filed unassigned from the #12975 dev seat, deliberately outside that card's ruled scope.What the #12975 ruling moved, and what it did not
The 2026-08-29 maintainer ruling adopted option 1 on #12975:
classifyDataError's declared-4xx arm strips the ADR-0111CODE:prefix from the human-readableerrorstring, soerroris human language andcodeis the machine token.packages/rest/src/error-response.tshas two declared-4xx arms, not one, and the ruling named the first:classifyDataError's declared-status armmapDataError— the by-id CRUD routesresolveErrorResponse'spassThroughStatus4xx armhandleRouteError/sendThrownError, andclassifiedRefusalAnswerThe second arm is checked before it delegates to
mapDataError, which is the same structural factrest-hook-refusal-message-parity.test.tsdocuments for the sandbox-wrapper defect: "Batch / bulk / clone exit throughhandleRouteErrortoresolveErrorResponse, whose declared-status passthrough is checked BEFORE it delegates tomapDataError."Measured, driving the real registered route handlers
One producer for every row: a thrown
Errorcarryingcode: 'FORBIDDEN',status: 403and the messageFORBIDDEN:followed by the localized sentence — the exact shapeplugin-sharing/src/sharing-plugin.ts's by-id write gate throws.So after #12975 lands, the same refusal reads two ways depending on which route caught it — the door-disagreement shape #7525 / #8016 / #11588 keep producing, now one arm over.
Why the share family is the sharper half
respondSharingErrorhas two arms and only one of them strips:Errorwhose message opens with one of the five codes, nocode/status) strips, and always has —sharing-envelope.test.ts'sstripsThePrefixpins it;classifiedRefusalAnswer, added by rest: the record-share routes classify refusals bymessage.startsWith(CODE)and interpolateerror.messageinto a hand-built 500 — a declaredstatus/codeis ignored and the sandbox wrapper reaches the client #11683 for producers that declarecodeplusstatus) re-dresses whateverresolveErrorResponseanswered, prefix included.That means
rest-server.ts's own#8111comment — "The prefix is a SERVER-INTERNAL service to REST derivation: it is stripped below and never reaches the wire, so no consumer can read it" — is true for the producers it was written about (the 11 bare-Errorthrow sites insharing-service.ts, re-censused at #11683) and not true for the newer classified limb beside it.Not a regression from #12975
Both rows above ship the prefix on
maintoday as well; #12975 does not change either. What #12975 changes is that the by-id route stops agreeing with them.Options, not a recommendation
resolveErrorResponse's 4xx arm, which converges every/dataexit and — because the share family re-dresses that same classification — the record-share classified arm with it. Moves the pins that assert the prefixed text on those doors.resolveErrorResponsealone and rule that the by-id door is the only one converging, correcting the#8111comment to name the limb it does not cover./data's bulk exits as they are.Option 1 is the one that matches #12975's stated goal ("one envelope semantics"), but it moves pins beyond the two the #12975 ruling authorised, which is why it was not taken in that PR and is filed here instead of decided at a dev seat.
Evidence in the tree
packages/rest/src/rest-data-door-code-prefix.test.tscarries a case titled "MEASURED, NOT REPAIRED HERE" that pins both rows above. It reddens the day either exit converges, so whichever option is adopted moves it deliberately.