Uh oh!
There was an error while loading. Please reload this page.
fix(rest): the record-share family carries a producer-marked userMessage (#12669 fork (a)) - #12691
Merged
Merged
Conversation
…sage` (#12669 fork (a)) `respondSharingError` asks `classifiedRefusalAnswer` — the flat `/data` door's own classification, which attaches a producer's caller-facing sentence in `withDeclaredUserMessage` (#9934) — and re-dressed only `status`, `code`, the message and, since #12510, `declaredCode` into the nested ADR-0112 D5 envelope. The author's own remedy text was dropped one line below the local that carried it. Measured on `07e646565` before the repair, one producer through both real routes: throw { code: 'CLOSE_PERIOD_LOCKED', status: 409, userMessage: 'Ask finance to reopen the period.' } share door : 409 {"success":false,"error":{"code":"RESOURCE_CONFLICT", "message":"invoices still open", "declaredCode":"CLOSE_PERIOD_LOCKED"}} /data door : 409 {"error":"invoices still open","code":"RESOURCE_CONFLICT", "declaredCode":"CLOSE_PERIOD_LOCKED", "userMessage":"Ask finance to reopen the period."} Nothing invalid shipped — every body parsed as `ApiErrorSchema` — which is what made the loss silent and one-directional: a console told by ADR-0112 to render `userMessage` verbatim found nothing here and fell back to its generic substitution, for the same throw the twin door rendered. ⛔ The two channels are NOT symmetric, and the repair writes that down once beside the re-dress rather than restating #12510's derivation. `declaredCode` is read from the classification because presence there MEANS demotion, an invariant a caller would otherwise re-derive. `userMessage` has none left: `declaredUserMessage` already decided presence and `truncateClientMessage` already applied #5423's bound, so the classification's own field is carried straight through. The population is also wider — a REGISTERED code demotes nothing and carries no `declaredCode`, and still carries its author's sentence. Additive and shape-preserving, measured rather than asserted: over a 13-shape census driven through both doors, the only difference between the pre- and post-change wire answers is one optional key appended to the nested `error` object on the two rows that declared one. Unmarked, empty, whitespace-only and non-string marks still carry no key; the ADR-0111 prefix idiom and this family's own `SHARE_*_FAILED` 500 terminal are untouched. Fork (b) — mapping the flat dialect's top-level `issues` onto the nested envelope's `ApiError.details` — is a shape decision on a contract field and is deliberately not shipped here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UjujZN219uFzBhSYfMykCd
Contributor
📓 Docs Drift Check
What this run could not see
Coarse fallback — 13 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
os-litant
marked this pull request as ready for review
August 27, 2026 09:54
Uh oh!
There was an error while loading. Please reload this page.
This was referenced Aug 27, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #12669 — fork (a) only. The card's fork (b) (
issues→ApiError.details) is a shape decision on a contract field and is deliberately not shipped here, so this PR does not close the card.The defect, reproduced before it was repaired
respondSharingError(packages/rest/src/rest-server.ts, insideregisterSharingEndpoints) asksclassifiedRefusalAnswer— the flat/datadoor's own classification, which attaches a producer's caller-facing sentence inwithDeclaredUserMessage(error-response.ts, #9934) — and re-dressed onlystatus,code, the message and, since #12510,declaredCodeinto the nested ADR-0112 D5 envelope. The author's own remedy text was dropped one line below the local that carried it.Measured on
07e646565before the change, the card's producer driven through the real routes on both doors:Nothing invalid shipped — every body parsed as
ApiErrorSchema— which is what made the loss silent and one-directional: a console told by ADR-0112 to renderuserMessageverbatim found nothing at this door and fell back to its generic substitution, for the same throw the twin door rendered.⛔ The two channels are NOT symmetric, and the code now says so once
Written down beside the re-dress, because the next reader will assume they are:
declaredCodeis read from the classification because presence there MEANS demotion — an invariant a caller would otherwise re-derive. [finding]rest-server.ts'srespondSharingErrorcomment sayssendError'sextrawon't acceptdeclaredCode— false since #11719; the defect it describes is still live but it now reads as "blocked upstream" when the block is gone #12510 landed that derivation in this same function and it is cited, not restated.userMessagehas no invariant left.declaredUserMessagealready decided presence (a non-empty string, or nothing) andtruncateClientMessagealready applied rest-server 的 4xx 直通把 ≥500 字符的 message 整条换成 "Request failed" —— #5368 刚写好的过滤器拒收措辞,客户端一个字也收不到(实测) #5423's bound, so the classification's own field is carried straight through. Thetypeofguard is the same non-string floorcodeanddeclaredCodecarry, not a second rule.declaredCode, and still carries its author's sentence.Clause ② criterion — tested, not asserted
The dispatching seat ruled clause ② inapplicable on a falsifiable criterion: no existing key moves or changes value. Measured over a 14-shape census driven through both doors before and after the change (the "before" leg is the ablated tree, so it is the same harness on both sides), 28 wire answers:
errorobject;/dataanswers, and the 7 share-door answers whose producer marked nothing;The criterion was not falsified.
Tests —
packages/rest/src/rest-share-user-message.test.ts(21 tests)Six sections, driving all three routes (
GET/POST /api/v1/data/:object/:id/shares,DELETE …/shares/:shareId) through the real handlers with a seam-reached witness on each: the sentence on the wire; absence for the shapesdeclaredUserMessagerejects; door-to-door parity with/data; what must not move (the 500 terminal, the ADR-0111 prefix idiom, and fork (b) reaching no key); the #5423 bound arriving already applied; and a four-corner matrix holdinguserMessageanddeclaredCodeapart mechanically.⭐ Every zero assertion carries a positive control in the same test body — the discipline the card itself modelled by reverse-checking its own expected-zero grep with
sendEnvelopeError. That is not decoration: the ablation predicted §2 GREEN and measured it RED, because it is the control that reds, and a §2 that had gone green would have been the warning that its zeroes proved nothing.Ablation (deleting the repair's read and its spread): 15 of 21 red. Mutation confirmed on disk by anchored counts (repair anchors 1→0, pre-change call 0→1) and a changed blob hash before any verdict was read; restored with
git checkout HEAD --on the absolute path under atrap … EXIT INT TERM, proven by an equal blob hash and an emptygit diff HEAD— never by an exit code. Every failure names itself: the route, both doors' bodies, and the exact expected string.Verification — union re-run on the final commit
fecc97035Every run through
scripts/pm/os-verify-lock.sh; exit codes captured before any pipe.pnpm --filter @objectstack/rest exec vitest runTest Files 151 passed (151)·Tests 2481 passed (2481)pnpm --filter @objectstack/rest typecheckcheck:test-typecheck: OK— 9 files / 20 errors held intest-typecheck-debt.json, none in the new file (--listFilesputs it in the program; 0 of the 20 are its)pnpm lint(repo-wideeslint . --no-inline-config)check:route-envelope✓ Route-envelope conformance — 10 route module(s) audited: 7 conformant, 2 ratcheted, 1 exemptcheck:dispatcher-error-vocabulary·check:nul-bytes·check:engine-double-contract·check:where-matcher·check:query-options-erasure·check:objectql-double-limit·check:slot-lookup·check:type-check-coverage·check:cross-package-test-inputscheck:type-check-debt(--re-measure, workspace closure built first)OK — 31 ledger entr(ies) re-measured, 1687 raw tsc error(s), none above its recorded numbercheck:published-files·check:authz-resolver·check:page-declaration-shape·check:type-source-resolution·check:test-source-alias·check:empty-changeset·check:changeset-gate-self-tests·check:objectui-changeset·check-adr-0087-registration·check-changeset-no-major·check-comment-mask-adoption·check-plugin-teardown-shape·check-ci-filter-parityscripts/pm/check-half-states.mjsGate families re-derived from the real change set with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstackafter the last edit.Changeset:
patchon@objectstack/rest. Re-derived rather than copied — published source, wire output changes, additively, on one optional key an existing schema already declares.Generated by Claude Code
Generated by Claude Code