feat(rest): a declared OS_REST_LOG fault-log level seam, shipped default unchanged - #17863
Conversation
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TSf4DV7ziu4V5j73e46b7c
…ST_LOG seam Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TSf4DV7ziu4V5j73e46b7c
… audit discharge Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TSf4DV7ziu4V5j73e46b7c
📓 Docs Drift Check9 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. What this run could not see
Coarse fallback — 14 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 0bee54cce0bb04b8f0e09b8e42887ffc5de2b015 && git checkout 0bee54cce0bb04b8f0e09b8e42887ffc5de2b015
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 7c2c5aedd94d7b0d94c91432bc607862e9c83c6c 7e6e9e044d9551b3f22d0baacf96310205b11c2b && git checkout -B drift-repro 7c2c5aedd94d7b0d94c91432bc607862e9c83c6c && git merge --no-ff 7e6e9e044d9551b3f22d0baacf96310205b11c2b
node scripts/docs-audit/affected-docs.mjs --json 7c2c5aedd94d7b0d94c91432bc607862e9c83c6c |
|
Contract review — PR #17863 (card #15484)⛔ FIRST, a correction of this seat's own comment
⇒ the maintainer had already answered 「is a declared environment seam a contract key」 — no — on 2026-09-05, for this card. 优先序 puts a maintainer ruling above a seat's derivation from the general rule, and I derived instead of reading. ⭐ Same root cause as this seat's Head reviewed:
① Derived judgments — every acceptance-set and public-surface change, named and judged1. Before:
2. The package's own published surface did not move. — correct. The dev measured the entry two independent ways off the built artefact so a type-only export cannot hide — 12 runtime value exports ( 3. The shipped default is behaviour-identical, which is the ruling's hard condition. — correct, read off the code. 4. A new accepted environment value, 5. 6. The gate is real and is wired. ② semver level —
|
Fixes #15484
Clause-②: no
⛔ Corrected at review — the seat's act, not the dev's. This line declared⚠️ which this repo's reader does in fact accept:
`Clause-②: yes`when the PR was opened (backticked and mid-line —readClause2Linereturnsdeclared: yeson that exact string, measured. The bare, line-initial form is what the protocol asks for, and it is what this line now carries), on the operator-visible-surface axis argued under "Clause-② measurement" below. The maintainer had already judged that axis for this card, in decision batch #49's own text: 「Changeset:@objectstack/restpatch(default behaviour identical; a new declared environment seam is documented, not a contract key)」. Record: the contract review at5646806136, head7e6e9e044, which also corrects this seat's own wrong reading at5646786543. ⛔ The measurement below is left exactly as the dev wrote it.Decision batch #49 item 2 ruled option A: a declared level seam on
packages/rest'slogError, in theOS_REGISTRY_LOGshape, with the shipped default unchanged. That is what this delivers — a declaration a gate can read, not a quieter product.What landed
packages/rest/src/log.tsOS_REST_LOG, five levels, shipped default'info'— today's behaviour byte-for-byte.logWarnfalls through the same ladder.packages/rest/src/rest-log-declared-level-seam.test.tsscripts/check-rest-log-declared.mjspackage.jsonandlint.yml.packages/rest/vitest.config.tspackages/rest/README.mddocs/audits/2026-09-test-log-volume-census.mdThe seam is one contract with
OS_REGISTRY_LOG, not a second ad-hoc variable: same five-level vocabulary, same'info'default, same "unrecognised value falls back to the default" direction. The gate holds the two vocabularies equal, reading each from its own source rather than carrying a copy.The gate's population is narrow by decision, and says so in its header: the package that owns the seam (located by its environment read, never hardcoded) plus any package that opts in by declaring the key. Measured at time of writing, 21 workspace packages reference
@objectstack/restfrom their own test sources —packages/specamong them — and conscripting all of them is a bigger change than the one that was ruled.Re-derived measurement — nothing quoted from the card
The card's counts were 8 days stale and its
[Registry]control is spent. Both replaced, and the live control was validated in the same capture it was used in:atframesat file://[sql-driver] DATABASE_ERROR[REST][Registry]Attribution, by the header preceding each frame block:
error-response.ts1,197 (57.1%),rest-server.ts841 (40.1%),causechains 57. 100% of the frames arrive throughlogError— and the[sql-driver]population contributes zero, which re-confirms the disjointness claim the card asked not to lose.The default did not move, and that is a measurement, not a claim. Same suite, before and after: frames 2,095 → 2,095, control 362 → 362. Test counts move only by the new file: 190 files / 3,182 passed → 191 files / 3,196 passed.
The ablation
Two legs, direction predicted in writing first, each mutation proved on disk before anything was read, restore proved by blob hash and a clean whole-tree status.
Leg 1 — move only the shipped default (
'info'→'silent'; harness declaration untouched). On-disk proof: removed-text 1→0, injected-text 1, blob51fdb14bvs HEAD3720e0a0.REST_LOG_DEFAULT_LEVEL is 'silent', which stops at least one of this shim's two sites from reporting at all for EVERY real caller1 failed | 2 passed; the failing file is the new seam pin, and the two files carrying the four inherited pins stayed GREENThat second result was predicted, and it is the point: an explicit harness declaration outranks the default, so a default move alone is invisible to a suite that declares. Which is why there is a leg 2.
Leg 2 — the world in which the default actually governs (default
'silent'and the harness declaration removed). On-disk proof: declarations 3→0, blob70c8244cvs HEAD84331960./meta/:type上一个未分类的服务端故障被报成 HTTP 400 —— handleRouteError 的兜底把 outage 说成客户端错误 #5489) →2 failed, 8 assertions red⇒ the frames are load-bearing, and it is the shipped default that keeps them reachable.
Restore:
git checkout HEAD --on absolute paths under a trap;log.tsblob3720e0a0== HEAD,vitest.config.tsblob84331960== HEAD,git diff HEADempty,git status --porcelainempty across the whole tree. An empty hash was coded as failure, not as "nothing to compare".Clause-② measurement
Measured from the delivered diff, not inherited:
packages/rest/src/index.ts, before and after, order-insensitively — IDENTICAL. Taken two independent ways off the built entry so a type-only export cannot hide: 12 runtime value exports (Object.keysof the ESM entry) and 31 declared export names parsed out ofdist/index.d.ts.diffof the two readings is empty. ⛔ Not a[+-].*exportmatcher.packages/rest/package.json'sexportsmap — did not move.git diffagainst the merge base over that file is empty.⇒ Outcome, added at review and kept apart from the text below it: the declaration is
no. The export-axis reading in this section is correct and is confirmed independently in the review of record (log.tsgains four exports, none reachable —index.tsdoes not re-export it, theexportsmap has one"."entry with no wildcard, andfiles[]ships no source). The operator-visible-surface axis argued next is a real axis, and it is the one the ruling already answered with 「not a contract key」. ⛔ The paragraph below is left as written.⇒ On the export axis the honest answer is
no. The declaration isyeson the operator-visible-surface axis, which is the one triage actually raised: this ships a new environment variable that changes what an operator sees, documented inpackages/rest/README.md— a file inside this package's publishedfiles[].log.tsitself stays un-re-exported, an internal shim as its docblock says.The env seam's name is
OS_REST_LOG, and an operator learns it frompackages/rest/README.md's new### Environmentsection (also fromlog.ts's docblock and the audit's closing section).The opt-down: measured, and stopped
The order's file face asks
packages/rest/vitest.config.tsto opt the suite down. Measured before choosing:OS_REST_LOG: 'silent'does take frames 2,095 → 0. It also does this:vi.spyOn(console, 'error')mock, so they never printed any of the volume in the first place.expect(unhandledLogs()).toHaveLength(0)and siblings, 4 inrest-expected-error-logging.test.tsalone). A silenced suite makes those pass for the wrong reason: they stay green with every expected 4xx logged loudly. That is a gate weakened into a phantom check by a legitimate-looking declaration — the same way this card's own[Registry]control was silently spent by Ratchet the declared registry log level the waycheck-console-intercept-disarmratchets the disarm — 4 app-booting suites now carryOS_REGISTRY_LOG, and nothing holds them there #15425.Landing that needs every file asserting on the fault log to declare the loud level for itself, plus a guard pairing the two so a future test cannot assert silence into a silenced suite — ~20 files the order did not name. The order's own stop condition covers exactly this, so the declaration ships here at
'info'(the shipped default: real, valid, gate-read, behaviour-identical) and the value is left as the one-line choice it is. The measurement is recorded in the config's own rootenvblock so the next reader does not have to re-derive it.Verification
Gate set derived, not guessed —
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, 109 commands, run against7e6e9e044with every exit code captured before any pipe.check:dual-build-cjs-loadsandcheck:type-check-debtfirst returned exit 3 —PREREQUISITE NOT MET, satisfied byturbo run buildover all packages (72/72 successful) and then both exit 0;check:pm-dispatch-gatesfirst returned 124, which was a local runner timeout and not a verdict — re-run unbounded it is exit 0 (1,678 self-test cases).pnpm lint— the full union,eslint . --no-inline-config, exit 0, no narrowing. (This lane's known blind spot:dispatch-gatesdoes not name it.)pnpm --filter @objectstack/rest run typecheck— exit 0;tsc --noEmitpluscheck:test-typecheck, 0 files / 0 errors in the debt ledger.pnpm --filter @objectstack/rest test— 191 files / 3,196 passed | 1 skipped.node scripts/check-rest-log-declared.mjs --self-test— 19/19.Heavy runs were serialised through
scripts/pm/os-verify-lock.sh; every verdict above is read from a command's own printed line or from the wrapper'sVERDICT command-exitline.Acceptance notes
Out of scope, noted, not filed — each names who would meet it:
packages/rest/vitest.config.ts's comment is now accurate where it was not. It stated in the present tense that the suite "measures 528 residual[Registry]lines here", on the lines immediately above the declaration that makes it 0. That text is untouched by this PR, but the block it sits in is now the one a reader ofOS_REST_LOGlands on. Carrier: the next PR editing this config. Raised on this card at5550786137; still not filed, becausescripts/check-registry-log-declared.mjs's header repeats the same 528 as a live reading and the two should be corrected together, by whoever owns that gate.scripts/check-rest-log-declared.mjscarries a second spelling ofcheck-registry-log-declared.mjs's brace-matching and env-block reader. Stated in its own header rather than left to be discovered. Extracting one shared reader is the right follow-up; it is not done here because that gate's self-test carries a battery floor this card has no mandate to move. Carrier: whoever adds the third seam of this shape — at which point the duplication stops being a note and starts being a population.🤖 Generated with Claude Code
https://claude.ai/code/session_01TSf4DV7ziu4V5j73e46b7c
Generated by Claude Code
Generated by Claude Code