Uh oh!
There was an error while loading. Please reload this page.
fix(rest): the record-share family carries a demoted producer code on declaredCode, and the comment that said it could not is repaired (#12510) - #12670
Conversation
… `declaredCode` (#12510) `respondSharingError` asks `classifiedRefusalAnswer` — the flat `/data` door's own classification, which already holds the demoted producer spelling — and re-dressed only `status`, `code` and the message into the nested ADR-0112 D5 envelope. The author's own code was dropped one line below the local that carried it: nothing invalid shipped, since the closed `code` still carried the member the status derives, which is what made the loss silent and one-directional. The pair is carried, not recomputed. `refusal.body.declaredCode` is the shared rule's OUTPUT (`thrownCodeFields` = `resolveThrownHttpError` + `demotedDeclaredCode`), so "presence means demotion" arrives with the value; the resolver's RAW `declaredCode` is deliberately not read, because it is set for a REGISTERED spelling too and forwarding it would put two spellings of one fact on every registered refusal. The comment that explained the omission is repaired in the same change. It said `sendError`'s `extra` would not accept the field — true when written, false since `db8c288` (PR #12403) added `declaredCode` to the writer's `Pick`. Left alone it reads as "blocked upstream, nothing to do here" to the next author, which is the direction that costs a repair rather than an understanding. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UjujZN219uFzBhSYfMykCd
📓 Docs Drift CheckThis PR changes 1 package(s): 5 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 63851fea12e0d604c186490feab3d929e3c17ce9 && git checkout 63851fea12e0d604c186490feab3d929e3c17ce9
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 68bf4efc6ac3e18385496432012eaf9aba09cc39 f2063854aeadd58e8112e89a65cdbe5580b88203 && git checkout -B drift-repro 68bf4efc6ac3e18385496432012eaf9aba09cc39 && git merge --no-ff f2063854aeadd58e8112e89a65cdbe5580b88203
node scripts/docs-audit/affected-docs.mjs --json 68bf4efc6ac3e18385496432012eaf9aba09cc39
|
Uh oh!
There was an error while loading. Please reload this page.
Fixes#12510
The record-share family drops the producer's own error-code spelling, and the
comment explaining why says the block is upstream. Both halves are repaired
here, because leaving either one costs the next reader something different: the
drop is a live defect, and the sentence has already stopped one repair.
Measured on
9a2f3dfe4(the ref this branch was cut from), not inheritedThe card anchored the comment at
:9657-9661; PR #12421 landed on this fileafter that, so I re-measured. Everything below is my own reading.
packages/rest/src/rest-server.ts:9654-9660, not the card's:9657-9661. ⛔ I do not reconcile the two: mine is the measurement, taken on9a2f3dfe4#11719/db8c288widened thePickdb8c2881e"fix(types): let sendError'sextracarrydeclaredCode(#12403)", found withgit log -S"'declaredCode'" -- packages/types/src/response-envelope.ts; thePicknow namescategory ǀ httpStatus ǀ details ǀ requestId ǀ declaredCode ǀ userMessagegit grep -n "does not accept that field" -- packages/rest/src/rest-server.tsreturns zero on a tree where the comment is present, because the phrase
wraps across a comment line break (
… does not/// accept that field). Azero there is not evidence of removal. The anchor that works is
Measured and NOT repaired here(1 on9a2f3dfe4, 0 here). Reverse control forboth greps:
sendErroranswers 7 in the same file on the same ref.The drop, reproduced before it was repaired
One producer, both doors, on the unrepaired tree:
respondSharingErrorasksclassifiedRefusalAnswer— the flat/datadoor'sown classification, which is already holding the demoted spelling — and
re-dressed only
status,codeand the message into the nested ADR-0112 D5envelope. Nothing invalid shipped, since the closed
codestill carried themember the status derives, which is what made the loss silent and
one-directional.
⭐ The call-site shape here differs from PR #12508, and why
PR #12508 (#12405) holds a
ThrownHttpErrorand passesdemotedDeclaredCode(thrown). This call site does not hold one. It holdsclassifiedRefusalAnswer's{ status, body }— the flat answer whosecodeand message it is already re-dressing. So the shape here is:
This is not a second definition of the demote rule and it is not the raw
field the twin's ruling bars.
body.declaredCodeis the shared rule's OUTPUT:the flat door computes it in
thrownCodeFields(error-response.ts, #9232),which is literally
resolveThrownHttpError(error, status)followed bydemotedDeclaredCode. Presence therefore already means demotion, and the paircode+declaredCodeleaves this door exactly as that rule produced it.Measured justification for reading the classification rather than calling the
resolver a second time at the re-dress:
{ code: 'RECORD_LOCKED', status: 409 }resolves tocode: 'RECORD_LOCKED'withdeclaredCode: 'RECORD_LOCKED'beside it — forwarding that would put twospellings of one fact on every registered refusal. The pinned answer is
absence, and §2 of the new suite asserts it directly.
deliberately asks once.
classifiedRefusalAnswerexists so this family"cannot answer a refusal differently from every other face that catches it";
a second read is the shape that let the two
/api/v1/packagesdoors driftapart in the first place. §5 pins the equivalence from the outside instead, so
either side drifting turns it red.
classification's field,
demotedDeclaredCode(resolveThrownHttpError(e, status))and the same with no status fallback. They agree on every shape measured.
Reported as measured: my prediction that the no-fallback form would diverge on
a status-derived member was WRONG — ledger membership does not depend on
status, so the demote does not either. The choice above is therefore made on
the one-classification-two-dialects ground, not on a measured divergence.
respondError's new optional parameter is typed from the shared writer's ownextratype —ParametersofsendEnvelopeErrorat index 4 — referencedrather than restated, so this wrapper keeps deciding POSITION only and a locally
narrowed copy cannot quietly stop forwarding the next channel admitted there.
hold them: GitHub's body sanitiser strips a short bracketed run even inside
backticks, and it silently ate this one from the first version of this text.
Read the type in the diff, not here.
The comment
The stale sentence is replaced by what is true now: the repair, the reason the
old reason is gone (
db8c288), the ⛔ against reading the resolver's raw fieldwith the
RECORD_LOCKEDmeasurement that shows why, and the ⭐ for re-dressingrather than re-resolving.
Tests —
packages/rest/src/rest-share-declared-code.test.ts(16 cases)Every case drives the real route through
RestServer.registerRoutes()onall three seams (
GET/POST /shares,DELETE /shares/:shareId) with awitness that the service verb was actually called, so a case that never reached
the seam cannot pass on a body it got for another reason.
statusandstatusCode, plus the sandbox limb with and without a declared status)RECORD_LOCKEDtrap case,plugin-sharing's ownFORBIDDENgate, a sandbox body with no code, and theADR-0111 prefix idiom.
'declaredCode' in errorisfalse, not=== undefinedthe reproduction turned into a pin, with an anti-vacuity case proving the
/datadoor really answers both wayscodebyte-identical, the 500terminal (
SHARES_LIST_FAILED/SHARE_GRANT_FAILED/SHARE_REVOKE_FAILED)unchanged, and a non-string numeric
codestill context rather than a wirespelling (The dispatcher puts the HTTP status in
error.codeand parks the real code indetails— pinned in #3687, still unfixed #3842)demotedDeclaredCode, plus its own anti-vacuity caseAblation
Predicted before running: §1 RED 4, §2 GREEN, §3 RED 1 of 2, §4 GREEN, §5 RED 1
of 2 → 6 red. Measured: 6 failed / 10 passed of 16 — exactly those six. The
pin names itself:
The mutation was confirmed on disk before any verdict was read: anchored
counts (
declaredCode !== undefined ? { declaredCode }1 → 0, the restoredpre-fix one-line
respondError(…)0 → 1) plus a blob hash differing from theHEADblob. Restore ran under atraponEXIT INT TERMwith absolute pathsand
git checkout HEAD -- ABSOLUTE_PATH(never the bare--form), and isproven by the working blob matching
HEAD:packages/rest/src/rest-server.ts(
1d1ab6604…) with an emptygit diff HEAD— not by an exit code. No rebuildleg applies: the suite imports the subject as
./rest-server.js, a same-packagerelative specifier vitest resolves to SOURCE, not through
exportstodist/.Verification, at the final commit
f2063854aHeavy runs went through
os-verify-lock(OS_VERIFY_LOCK_SLOT=issue-12510);exit codes captured before any pipe.
pnpm --filter @objectstack/rest exec vitest run— 150 files / 2460 testspassed, VERDICT command-exit 0
pnpm --filter @objectstack/rest typecheck— clean;check:test-typecheckOK, 9 files / 20 errors held in
test-typecheck-debt.json, unchanged (the newsuite is inside the test project:
tsconfig.test.jsonincludessrc/**/*,while the build config excludes
**/*.test.ts)node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(never a hand-built path list) — all 27 derivedpath/kind gates exit 0, including
check:route-envelope,check:dispatcher-error-vocabulary,check:published-files,check:engine-double-contract,check:where-matcher,check:cross-package-test-inputs,check:type-check-coverageand thechangeset family
pnpm check:type-check-debt— refused first as NOT MEASURED without the builtclosure, so the closure was built (
turbo run build --filter='./packages/*' --filter='./packages/*/*', 70/70) and it then reported OK: 31 ledger entriesre-measured, none above its recorded number
node scripts/check-nul-bytes.mjs— OK over 7027 filesnode scripts/pm/check-half-states.mjsexits 3 = NOT MEASURED from thisseat (nothing swept: the REST issues API answers 403 here). Recorded as
unmeasured, not as green
Changeset
.changeset/share-door-declared-code.md,@objectstack/rest: patch— ⛔ notskip-changeset. This is published source and it changes what goes on the wire:a consumer that ADR-0112 tells to read
declaredCodestarts receiving it onthis family, so the CHANGELOG is where an upgrading consumer learns the channel
arrived. Additive, and NOT breaking —
ApiErrorSchema.declaredCodehas beenz.string().optional()since #9106, so the contract's accept set does not moveand no ADR-0087 disposition is required.
Contract review
The PM ruled clause ② does not apply, and invited the ruling to be overturned by
measurement. My measurement agrees with the ruling: the accept set does not
widen (the schema already permits the field, and
check:route-envelopeand thesuite's
ApiErrorSchema.safeParseboth stay green), no consumer shape breaks(no existing key moves or changes value on any path — §4 pins that), and the
only observable delta is an optional field appearing where ADR-0112 says it
belongs, on shapes that previously carried nothing there.
Out of scope, filed not fixed
The same re-dress still drops two other declared channels the flat
/datadoorcarries for the same producer —
userMessageand the structured context(
issues). Both measured on this branch; filed rather than folded in, sinceeach is a different declared channel with its own posture. Filed as #12669,
unassigned — and note the two halves differ in difficulty:
userMessageis thesame one-line re-dress, while the flat
issuesto nesteddetailsmapping is ashape decision, not a rename.
Generated by Claude Code