Uh oh!
There was an error while loading. Please reload this page.
fix(types): withhold a demoted declaredCode on an undeclared 5xx — one rule, every door inherits (#12509) - #12946
Conversation
…DR-0112 scope, #12509) In 5xx sanitisation a DEMOTED code — one the fallback-to-500 picked up from a producer that declared no HTTP answer, e.g. a driver errno — is withheld along with the prose; an AUTHOR-DECLARED code survives at every status. Maintainer ruling 2026-08-27, option D. The judgement is `serverFaultProvenance`, one exported function in `packages/types/src/thrown-http-error.ts`, applied inside `demotedDeclaredCode` — the read every door already makes — so all five emitting exits inherit it and no registrar carries a variant. No door file changes. The discriminator is the STATUS channel because it is the only structural one: a driver errno and an app's own spelling both arrive on `.code` as a plain string, so telling them apart by looking at the string would be a heuristic over an open channel, and unfalsifiable besides. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UjujZN219uFzBhSYfMykCd
`@objectstack/runtime`'s tsconfig excludes `**/*.test.ts`, so no tsc program its `typecheck` script runs reads this file — but `check:type-check-debt` re-measures the test layer against a frozen, shrink-only ledger, and the unused import took TEST_DEBT from 217 to 218. Fixed rather than ledgered: raising the entry is maintainer-only and hands back what an earlier PR paid to press down. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UjujZN219uFzBhSYfMykCd
📓 Docs Drift Check4 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 1 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 595e443c261465de8ca1e194e70297e883d3d393 && git checkout 595e443c261465de8ca1e194e70297e883d3d393
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin fa9018a29bcee1481d98461a89719e14665ca41c b04362571c9983273b236e93686459d97c4f8133 && git checkout -B drift-repro fa9018a29bcee1481d98461a89719e14665ca41c && git merge --no-ff b04362571c9983273b236e93686459d97c4f8133
node scripts/docs-audit/affected-docs.mjs --json fa9018a29bcee1481d98461a89719e14665ca41c |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
os-litant
commented
Aug 29, 2026
⛔ 诊断完成 —— 是 governed surface 门在 merge_group 里拒绝,不是 CI、不是 base、不是队列抖动。已按门规定的首选补救转回 DRAFT
门自己的输出( |
| 范围 | 结果 |
|---|---|
| 本席今天 arm 过的 9 条 PR(#13113#13120#13123#13124#13125#13148#13149#13153#12946) | 只有 #12946 是 governed,其余 8 条对 docs/adr/**、.claude/**、skills/**、AGENTS.md、CLAUDE.md 命中数为 0 |
| 本席今天 已合并的 14 条 PR | 全部 clean —— 逐条查合并提交的文件列表,无一触碰 governed surface |
⇒ 没有 governed surface 在无人评审的情况下落地。门起作用了。
这张卡现在的状态与需要什么
- DRAFT,不在队列,不会再自行入队。
- CI 在最新 base(head
b04362571)上 32/32 全绿,代码侧没有任何待办。 - 需要的是:
os-zhuang或hotlong之一人工合并(人工合并本身就是 governed surface 的评审记录),或由其中一位提交一个钉在当前 head 的 APPROVED review 后由人重新入队。⚠️ 任何后续 push 都会让该 approval 失效并重新触发拒绝。
Generated by Claude Code
Uh oh!
There was an error while loading. Please reload this page.
Fixes#12509
Maintainer ruling 2026-08-27 (adjudication
session_01DKWDdUJ2XNRESVVWUvcpnh, verbatim: 「同意」), option D: in 5xx sanitisation a demoted code — one the fallback-to-500 picked up from an undeclared producer — is withheld along with the prose; an author-declared code survives. Implemented once at the shared resolver so every door inherits one rule. Options A (record the leak as a decision), B (withhold the author channel too) and C (a per-door rule) were declined by name and are not attempted here.The three readings the ruling named as the first step
(a) Driver-errno reachability at the other two doors' seams — the premise does NOT hold at the dispatcher door.
The card's premise ("a driver errno cannot reach a producing seam") had been measured at
PackageService's four seams only. Re-measured onorigin/main@aef1b7e64with a realObjectQLand a driver that fails every access with a coded fault:DELETE /api/v1/packages/:idvia realObjectStackProtocolImplementation503 SERVICE_UNAVAILABLE, nodeclaredCode— the protocol converts the fault to a declared, registered refusal, so the premise holds even withcode: 'SQLITE_ERROR'present/datadoorGET/POST /api/v1/data/:objectvia real CRUD routes500 DATABASE_ERROR/500 INTERNAL_ERROR, fixed sanitised bodies, nodeclaredCode— the errno never reaches the wirePOST /api/v1/analytics/query500 INTERNAL_ERRORwithdeclaredCode: "SQLITE_ERROR"— no producer sits between the service anderrorResponseBase, so the throw reaches the resolver verbatim⇒ The premise does not hold at the dispatcher door. That is what makes this a repair rather than a tidy-up.
(b) Consumers of the demoted strings on the wire — none found in this repo. Outside the emitting doors, their tests,⚠️ Boundary stated rather than hidden: sibling repos (
packages/spec's schema declaration and the generated API reference, nothing readsdeclaredCode, and nothing anywhere branches on a driver-errno literal (SQLITE_ERROR,42P01).packages/clienthas no read of the field at all.objectui,hotcrm,cloud) are not in this checkout and were not measured here; ADR-0112's Consequences records the two earlier consumer sweeps for#9106/#9232, but those were about the demote generally, not about 5xx demoted strings. No consumer dependency was found, so the ruling's scope stands and nothing was escalated.(c) #12281's population (declared-5xx producers carrying NO code) — non-empty. Seven sites, all in
packages/runtime/src/action-execution.ts: six ×{ statusCode: 503, message: 'Data service not available' }and one{ statusCode: 501, message: 'Data query fallback cannot serve …' }.declaresServerFaultneeds a string code, so their prose travels today; #12281's rule would replace it with the generic sentence, including the 501's self-correcting operator guidance. Every other 5xx-declaring producer found (mcp,plugin-auth,objectql/action-activation.ts) carries a code. This card changes nothing for that population — they declare no code, so they never had adeclaredCodeto withhold.Reproduced first, on the real routes
Before (
origin/main@aef1b7e64), driving real routes rather than reading source:After, same harness, one edit:
One place, and it is not the doors
serverFaultProvenance(packages/types/src/thrown-http-error.ts) is the whole judgement, applied insidedemotedDeclaredCode— the read every door already makes. No door file changes in this PR. Five emitting exits inherit it:sendThrownError(packages/rest/src/package-routes.ts),thrownCodeFields(packages/rest/src/error-response.ts),HttpDispatcher.errorFromThrown,endpoint-executor.endpointErrorAnsweranddispatcher-plugin.errorResponseBase— ⭐ five, not the three the card names; the two extra are the endpoint executor and the dispatcher plugin, both measured above.Scan of the working tree including untracked files (
grep -r, nevergit grep, which reads tracked files only):serverFaultProvenance— one executable definition (packages/types/src/thrown-http-error.ts:324) and one non-test call site (:353, indemotedDeclaredCode, same file). Its other appearances are the test that imports it, the ADR anchor, the ADR and the changeset — prose, no second implementation.demotedDeclaredCode(not a substring of the term under test, in either direction) — 17.tsfiles. A zero there would have meant a dead scan; the scan also listed the then-untracked new test files, so the tracked-only trap is falsified in both directions.declaredServerFaultAnswer(packages/rest/src/error-response.ts:471) open-codes "is this a declared 5xx" for a different question — whether to relay the producer's declared status and code — and readsdeclaredHttpStatus(which also imposes a 400–599 band) rather than the resolver'sdeclaredStatus. It is not a second implementation of demoted-vs-author-declared, and the two cannot contradict each other on the wire. Whether it folds intoserverFaultProvenanceis a question for #12281, which touches that limb.packages/cloud-connection/src/cloud-connection-plugin.ts:382emitsdeclaredCodedirectly, not through the shared rule — a verbatim relay of an upstream RFC 8628 spelling on a hard-coded 400. It never emits the channel at a 5xx, so the withhold's scope is not evaded; recorded because "every emission goes through one function" would otherwise be false.The judgement the ruling's text leaves to the implementation
The discriminator is the status channel. A driver errno and an app's own spelling both arrive on
.codeas a plain string, so telling them apart by inspecting the string would be a heuristic over an open channel — the consumer-side tolerance ADR-0112 exists to forbid — and unfalsifiable, since nothing stops an app from spellingSQLITE_ERROR.packages/types/src/thrown-http-error-5xx-code-withhold.test.ts§3 pins that: the same spelling survives when declared and is withheld when not.SandboxError→VALIDATION_ERROR, theDUPLICATEwitness), untouched here.⛔ Not gated on whether
looksLikeInternalErrorLeakfired. That predicate reads a different channel; gating on it would leak the errno for exactly the dialects whose prose the heuristic misses — the ceilingsendThrownError's own note records.What #12281 still needs when it lands
The prose axis is deliberately not applied. What it needs, precisely:
errorResponseBase(packages/runtime/src/dispatcher-plugin.ts:591) changes its message gate fromdeclaresServerFault(err) || (httpStatus >= 500 && looksLikeInternalErrorLeak(raw))to the'declared'limb ofserverFaultProvenance— the same function, other limb. No new predicate is needed; that is the shape this PR put there.action-execution.ts— is what changes behaviour, and their messages are operator guidance, so runtime: a declared 5xx carrying NOcodekeeps its prose on/analytics/querywhere/datawithholds it unconditionally #12281's measurement-first step should decide whether they move touserMessage(contract: a hook refusal has no way to mark its message user-facing — the console's 403 substitution (ruled in #3821) needs a producer-side opt-in channel #9934) rather than simply losing their text.packages/runtime/src/dispatcher-5xx-demoted-code-withhold.test.ts§4 pins today's message behaviour on both shapes, so runtime: a declared 5xx carrying NOcodekeeps its prose on/analytics/querywhere/datawithholds it unconditionally #12281 lands as a visible change rather than as drift; its second expectation flips to the generic sentence.declaredServerFaultAnswer's open-coded "declared 5xx" (above) is the one place worth reconsidering in the same change.Verification
Union re-run after the final commit, on
824007d8f:pnpm --filter @objectstack/types test— 396 passed / 396pnpm --filter @objectstack/rest test— 2569 passed / 2569 (155 files)pnpm --filter @objectstack/runtime test— 2935 passed / 2935 (198 files)pnpm --filter @objectstack/types --filter @objectstack/rest --filter @objectstack/runtime run typecheck— clean.--listFilesshows the types test is in its tsc program and the rest test is inpackages/rest/tsconfig.test.json;@objectstack/runtime'stsconfig.jsonexcludes**/*.test.tsand it has no sibling test program, so the runtime test is not compiled by that script — a standing gap for that package's whole test layer, already ledgered in TEST_DEBT.pnpm lint— whole repo, exit 0, zero findings (eslint . --no-inline-config, 92s). Not a narrowed run.pnpm check:type-check-debt— OK, 31 ledger entries re-measured, none above its recorded number. It first went RED on this branch: the new runtime test added an unusedviimport, taking@objectstack/runtime's TEST_DEBT from 217 to 218. Fixed by deleting the import (commit 2), ⛔ not by raising the entry.check:adr-anchors(see below),check:type-check-coverage,check:engine-double-contract,check:where-matcher,check:cross-package-test-inputs,check:test-source-alias,check:type-source-resolution,check:published-files,check:doc-authoring,check:empty-changeset,check:adr-0087-registration,check:page-declaration-shape,check:slot-lookup,check:query-options-erasure,check:nul-bytes,check-changeset-no-major,check-comment-mask-adoption,check-adr-links,check-plugin-teardown-shape.pnpm check:adr-anchorsreports 1 problem that is not from this branch:packages/spec/src/data/object.zod.tscitesADR-0006 D4, which ADR-0006 does not declare. That file and ADR-0006 are untouched here; the failure reproduces onorigin/main. Reported to the PM rather than filed — the dedup read (GET /repos/…/issues) answers 403 from this seat.Reverse verification
Predicted before running — deleting the guard line from
demotedDeclaredCode: types 7 red, rest 4 red, runtime 10 red; §1's provenance table stays green (the ablation removes the application, not the judgement), and both door files' "wire == shared rule" comparisons stay green because both sides move together, which is precisely why the literal assertions in §1 exist.Measured: 7 / 4 / 10, exactly. Failure list matched row for row.
The ablation proved both legs on disk, not by exit code: anchor count 1 → 0 and the injected marker 1, worktree blob
ded6de18→ee521eb7;@objectstack/typesrebuilt andablation-dist-preflight … --absentconfirmed the compiled guard gone fromdist/— load-bearing, because@objectstack/restdoes not alias@objectstack/typesto source and consumes that artifact (@objectstack/runtimedoes alias). Restore leg:git checkout HEAD --with the ABSOLUTE file path, quoted, from anEXIT INT TERMtrap, then proven bygit diff HEADempty and the worktree blob back toded6de18, with the preflight showing the guard present indist/index.jsanddist/index.mjsagain.git checkout HEAD -- "$REPO_ROOT/", i.e. restoring the whole repo root. The sentence above is the corrected spelling; the command actually run named one absolute file path.Generated by Claude Code