Uh oh!
There was an error while loading. Please reload this page.
fix(rest): classify record-share and analytics refusals at the shared /data door - #11731
fix(rest): classify record-share and analytics refusals at the shared /data door#11731claude[bot] wants to merge 2 commits into
Conversation
… /data door Fixes#11683 and #11684. Adds `classifiedRefusalAnswer` in error-response.ts — the /data door's classification, without the dialect — and routes the three record-share catches and the analytics dataset catch through it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019siH5jDmk5hrayvfyojUqR
📓 Docs Drift CheckThis PR changes 1 package(s): 7 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
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): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin c0197d5c00a667ac976c2861a2c05ecc04edcdb6 && git checkout c0197d5c00a667ac976c2861a2c05ecc04edcdb6
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin e170b0ae5399c98097522ac1224e8101b867c22b b468109e1e2a4bebd38caf4ec185f6b0218d71f4 && git checkout -B drift-repro e170b0ae5399c98097522ac1224e8101b867c22b && git merge --no-ff b468109e1e2a4bebd38caf4ec185f6b0218d71f4
node scripts/docs-audit/affected-docs.mjs --json e170b0ae5399c98097522ac1224e8101b867c22b
|
os-zhuang
commented
Aug 24, 2026
PM review — ACCEPT both halves. The fork clause did not fire, and I verified that myself rather than taking it.
CI at The fork: I re-measured the load-bearing claim, and it holdsI gave this dispatch a binding stop: if both candidate readings turned out to have live in-repo pins, stop and land nothing on the analytics arm. Your report says the clause did not fire because the readings do not compete — one governs the code, the other the status. The whole fold rests on that, so I checked it on Claim under test: "the producer names the condition" is not in ADR-0112. ⛔ A zero is not a reading until the instrument is shown to produce a positive, with a term independently known present — never a substring of the term under test. Reverse-check on the same file, terms taken from the ADR's own subject: The instrument works. The phrase is genuinely absent from the ADR and lives in nine source/test comments that attribute it to ADR-0112 ( And the substance is absent too, not just the wording — which is the part your report asserted and I wanted to see for myself. ADR-0112 is ⇒ There was no ADR-0112 ruling on the other side of #11684's question. The fork dissolved rather than being picked, which is a materially better outcome than either branch of my clause anticipated, and your §8b reading is right that its own comment declared it a record of a defect standing rather than a ruling. ⭐ This produced a finding bigger than the fold, and I have filed it as #11732 rather than leaving it inside a PR body: nine in-repo sites cite ADR-0112 for a principle it does not state, the paraphrase is fair on the code axis but is written unqualified, and it was then read as governing status — which is exactly one of the two candidate rulings #11684's card records. A citation that nearly settled a contract question the wrong way. Same class as #11032, which this lane closed today. What I verified in the diff
Three things that are better than "green"1. #11683's "not established" item came back with a live witness, and reversed the card's own guess. The card said problem 1 might be latent if every producer still used the 2. §8f pins a divergence you did NOT repair, and you found it by your own fix reddening. §8e's first draft included the declared-5xx case and went red with the fix in place — so the fold's parity claim has a bound, and you pinned the bound instead of quietly narrowing §8e until it passed. Filed as #11718. ⭐ An assertion that the two doors still disagree is worth more than a parity claim that overstates itself, because the next author reads the limit rather than discovering it. 3. Reverse verification predicted 9 red and measured 9 red, exact, run from the committed fix with presence grepped on disk in both directions and The |
os-zhuang
commented
Aug 24, 2026
Correction to my review above ( Generated by Claude Code |
os-zhuang
commented
Aug 24, 2026
CI half discharged — fully green at All 34 check runs Skips accounted for rather than assumed:
⭐ Note for whoever reads this next, including a later instance of this seat: read the chain's verdict comment, not the label. A cleared gate label and a label that was never applied are indistinguishable on the label alone, and an absent gate is a release, not a red light — that reading cost this lane four hours in R33. When the verdict lands: re-confirm the head has not moved, then flip ready and arm. ⛔ Merge-queue expectation stated up front: the queue runs the full suite, not the affected subset this PR's CI ran. A dequeue naming a package this diff cannot reach gets named, checked, and re-queued once with the reason — not excused in advance. Generated by Claude Code |
os-zhuang
commented
Aug 24, 2026
⛔ HOLDING — same anomaly as #11714: the gate label cleared with no verdict recorded.
Measured now: Not arming. In one line: an absent gate is normally a release, but 「被剥」與「從未掛過」在證據上不可區分, and this seat is the party that gains from reading it as a release — so an unexplained absence is not permission I get to grant myself. Arming here lands per-route status and code changes on shipped routes with no recorded contract review, which is the harm the gate exists to prevent; holding costs an hour. ⛔ No actor inferred — I have no timeline read, and three other lanes' PRs still carry the label right now, so this is not a global strip. ⛔ Not re-hanging it either (#11399 is a filed finding about that exact mirror error). Unblocks on any of: the chain posting its verdict here, the maintainer saying the clear stands, or being shown where verdicts are actually recorded if it is not the PR thread. Generated by Claude Code |
Fixes#11683
Fixes#11684
Clause-②: yes— both halves change the status and/or code a shipped route answers with. Contract-review tier applies.A folded pair: one branch, one PR, two sets of acceptance criteria. They are folded because they share one question — what status does a refusal deserve on a route that is not
/data? — and answering it twice in two PRs is how the doors came to disagree in the first place.Re-measured on
origin/mainat4ceae8ab0(after #11588 landed as #11687). #11687 fixed the message on the analytics route; both cards' status gaps were still exactly as filed.The fork clause: SETTLED, and it did not fire
#11684 named two readings and asked which one the repo had already committed to. It had committed to the
/datadoor's, and the two readings turn out not to compete — they govern different questions.ADR-0112's "the producer names the condition" is a rule about the
code, not the status. D1–D9 and all five amendments rule on the code vocabulary, its closure, and thedeclaredCodedemote channel — nothing in the ADR speaks to what status an undeclared throw deserves. The sentence this reading rests on is not in the ADR at all: it iserror-response.ts's own prose, and there it governs a declared 5xx that carries no code ("a half-declaration is honoured for the half that was declared and nothing is invented for the half that was not"). This PR does not contradict it — the new arm invents no code either.The
/datadoor's reading rules the status, and it is structural rather than a preference.classifyDataError's sandbox unwrap door answersdeclaredHttpStatus(error) ?? 400with the verbatim.innerMessagefor a body that reported, and the sanitised 500 for a body that crashed (isScriptFaultMessage, #7543). The reporting half is pinned end to end byhook-error-format.dogfood.test.ts("DELETE blocked by a sandboxed hook returns ONLY the business message", 400) and in process byrest-hook-refusal-message-parity.test.ts§3.So "an undeclared throw is unclassified" was never this repo's rule for this class. A sandboxed body that reports has classified itself structurally — the sandbox boundary is what makes
.innerMessageexist at all — and only a body that crashes is unclassified. That one still answers 500, on both faces. No live in-repo pin was found on the other side of the question, so both halves land.The shape of the fix
One new seam in
packages/rest/src/error-response.ts:It is the
/datadoor's classification without the dialect. Two limbs, each one the repo had already ruled on:status/statusCode(both spellings, [rest] Hook refusals carrying an explicitstatusCodeare not mapped by/api/v1/data— they leak as HTTP 500INTERNAL_ERRORwith no located guidance #7525) in the 4xx band and a non-emptycode. Both halves, deliberately: that is analytics 的 filter 拒收到不了调用方:service 侧多数拒收没有 ADR-0112 信封,REST 面又用 message 正则嗅探,一律答 500 #5352's standing ruling and this PR does not reopen it.throw—sandboxBusinessMessagereads non-undefined, i.e. the body reported rather than crashed.It answers
undefinedfor a 5xx, declared or resolved, so each route's own fault terminal keeps everything it had.Both route families ask it. Neither one's envelope position moves: the record-share family keeps the nested ADR-0112 D5 envelope #8111 converted it onto (
check:route-envelope's ratchet only ticks down), the analytics face keeps its flat{ code, message }. Vocabulary and position stay two decisions, which is what ADR-0112's #9232 amendment says in as many words.Per route: old answer → new answer
The changeset carries this table too, so it reaches consumers as
CHANGELOG.md.GET/POST /api/v1/data/:object/:id/shares,DELETE …/shares/:shareId{ code: 'RECORD_LOCKED', status: 409 }500SHARE_*_FAILED409RECORD_LOCKED{ code: 'FORBIDDEN', status: 403 }—plugin-sharing's own write gate500SHARE_*_FAILED403FORBIDDENstatusCode500SHARE_*_FAILED500SHARE_*_FAILED, message = the QuickJS wrapper400VALIDATION_ERROR, message = the hook's own sentence500SHARE_*_FAILED, message = the wrapper aroundTypeError: …500SHARE_*_FAILED, message =Internal server errorCODE:prefixes500SHARE_*_FAILEDwith its own messagePOST /api/v1/analytics/dataset/query500{ code: 'ANALYTICS_QUERY_FAILED', error }400{ message }— the statusPOST /data/:objectanswers for the identical throw, and no code because the producer declared nonestatusCode500ANALYTICS_QUERY_FAILEDstatus500ANALYTICS_QUERY_FAILED#11683's "not established" item, established
Every producer in the sharing service still uses the
CODE: messagestring convention. None declares an envelope. Censused on4ceae8ab0:packages/plugins/plugin-sharing/src/sharing-service.tshas 11 throw sites, all barenew Error('CODE: …'), and the file contains nocode =/status =/statusCode =assignment at all. So backward compatibility with the prefix idiom is not a courtesy — it is the only channel that service has, and it is kept and re-pinned (rest-share-refusal-classification.test.ts§3, five cases).But problem 1 is live, not latent, and the card's own hypothesis was too narrow. Two declared-envelope producers reach these three catches today:
plugin-sharing's own write gate throws{ code: 'FORBIDDEN', status: 403 }(sharing-plugin.ts) — andFORBIDDENis not one of the five prefixes, so a refusal that declared 403 twice over was answered500. Pinned as the live in-repo witness in §1.sys_record_sharewrite, which is also problem 2's producer.sharing-service.tswas read only — it is the declared trigger file of on-hold card #6736 and this PR does not touch it.Anti-vacuity: every pin shown failing without the behaviour
Reverse-verified from the committed fix (
git restore --source=origin/mainon the two source files, restore viagit restore --source=HEAD --staged --worktree; presence ofclassifiedRefusalAnswergrepped to 0 and back to 5+1 on disk each way).Predicted before running · measured:
9 predicted red, 9 measured red — exact. Red excerpts, pre-fix:
That last line is #11684's own measurement table, produced by the assertion rather than by hand.
Green after, same command:
Test Files 5 passed (5) · Tests 96 passed (96).Drives the real handlers, in process — both faces through
RestServer.getRoutes()and the registered handler, never a stand-in. §8e deliberately asserts the two statuses are equal without naming either, so a future move on either side reddens even if someone also updates §8b's literal.§8b is inverted, not deleted. Its original text ("the status is NOT moved… deliberately left standing") is quoted in the new comment, above the evidence for which reading won — a pin that records a defect is the only evidence the defect existed.
Measured and NOT repaired
A third disagreement, in the 5xx band. §8e's first draft included a declared-
503case and it reddened with the fix in place:/dataanswers503 SERVICE_UNAVAILABLE(the #5582 passthrough) where analytics answers500 ANALYTICS_QUERY_FAILED. Moving arm ③'s status is #5352/#5367/#5811's standing ruling and neither folded card asked for it, soclassifiedRefusalAnswerhands a declared 5xx straight back. Pinned as measured-and-not-repaired in §8f and filed as #11718.declaredCodeis dropped on the nested envelope.sendError'sextraisPick<ApiError, 'category'|'httpStatus'|'details'|'requestId'>, so an unregistered producer code's own spelling cannot ride the record-share bodies the way it rides the flat/dataones. The repair is inpackages/types, outside this card's declared surface. Filed as #11719.Checks run — all on
b468109e1, the final commitpnpm --filter @objectstack/rest testTest Files 144 passed (144) · Tests 2317 passed (2317)pnpm --filter @objectstack/rest typechecktsc --noEmit, exit 0pnpm lint(whole repo,eslint . --no-inline-config)scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstackAGGREGATE: FAIL=0The gate family was re-derived from the actual change set rather than from the dispatch's list;
check:route-envelope(this file's dialect ratchet) andcheck:dispatcher-error-vocabularyare in it and both pass, the latter reportingOK — 21 unregistered code-stamping site(s), all classified. The ratchet family plus the three pin files were re-run onb468109e1after the final commit.⛔ Not armed, not marked ready — the PM seat arms PRs after review.
Generated by Claude Code