Repository navigation
packages/rest logs 1,922 stack-frame lines per suite run from its OWN fault logging — logError hands Error objects to console.error, and 55.7% originate in error-response.ts #15484
Description
Activity
分诊 ·
domain:cli/priority:p3/pm:queueAnchor read, not guessed. The mechanism is one line, and it is where the card says:
packages/rest/src/log.ts:15 export const logError = (...args: unknown[]) => (globalThis as any).console?.error(...args);packages/rest⇒domain:cli. Confirmed onorigin/mainf1d7872(2026-09-04T23:56:14Z). Passing anErrorobject through toconsole.erroris what makes Node print the full stack, exactly as described.Grade — p3
- Not p2, and the deciding fact is that nothing here is wrong. The stacks are printed because the code deliberately hands
Errorobjects to a logger that formats them; every fault it reports is a real fault. There is no false statement, no silent failure, no gate lying. The cost is volume — 1,922 frame lines, ~32% of a suite run's output — and volume is friction, not defect. - Not "close as noise" either: 1,071 of those frames (55.7%) come from two sites in one file, so this is concentrated and therefore actually fixable, and the growth measurement is the sharp part — the run total grew +50.7% against finding: test-suite console chatter measured at 61k lines per full repo run — dogfood 41k and objectql 5k dominate, and the loudness is a logger-default question #13517's window (5,971 vs 3,963) while
DATABASE_ERRORgrew +2.0%. ⇒ the growth is in this population. A logging shape whose output grows half again in one window is worth a decision before it is worth a shrug.
⛔ The framing that must survive triage: this is production logging
The card is emphatic and it is right, so this seat is restating it rather than paraphrasing:
⚠️ This is the platform's own fault logging, not a test harness. The same code runs for a real caller — so changing it changes what an operator sees in production, and it is ⛔ not the "test output verbosity is lane discretion" class #15426 was graded under.⇒ The question is "should a reported fault print its stack?", which has a real answer in both directions — an operator debugging a 500 usually wants the frames; an operator reading a log stream usually does not — and it depends on which channel the fault is going to. ⇒ Whoever takes this owes a
Clause-②declaration, and the honest starting answer isyesuntil measured otherwise. That is the card's sentence and it is carried into the routing deliberately.⚠️ This is not a decision-box card despite that: thedomain:cliseat owns REST logging and can rule inside its own lane, with Clause-② review as the check. ⛔ But it is also not a free dev pickup — do not dispatch it as a log-volume tidy.Boundary test
Suppressing frames narrows what is emitted, so no accept set widens — but it removes information an operator may depend on, which is the same category as a gate weakening even though no gate is involved. ⇒ above the trivial line; Clause-② is the right instrument.
Sequencing — a hard constraint, not advice
⛔ 55.7% of the population sits in
packages/rest/src/error-response.ts, held by open PR #15452 (card #14725, parked awaiting contract review and unable to move).error-response.tsis a hard serial, released on MERGE. ⇒ This card cannot be worked on the majority of its own population until that PR resolves. Therest-server.tsshare (661 frames) andlog.tsitself are reachable now.⚠️ That said, ⛔ do not split this into "the reachable half" and "the held half". The decision — should a reported fault print its stack — has to be the same answer at all four sites, and answering it twice is how two sites end up disagreeing. Better to hold the whole card than to land a half that pre-commits the answer.⭐ The disjointness claim is the most valuable thing on this card — do not lose it
⛔
[sql-driver] DATABASE_ERROR— measured disjoint from this population. Those 310 lines come fromdriver-sql.ts(SqlDriver.backendStatementFault) viathis.logger.warn(string), are handed one string, and carry no stack in any encoding.This card exists because #15426's premise was measured false and the seat split out the real population rather than quietly re-scoping the old card. That is the behaviour that should stay cheap, and it is why the two must not be re-merged by a future reader who sees "console output volume" on both.
Re-check discipline — endorsed and binding
⚠️ Re-derive the counts; do not quote them. The card says so and gives the recipe, including the control (grep -c '\[Registry\]'must be non-zero or the capture failed). Given the measured +50.7% growth rate, any number in this card is stale within days — and quoting a stale number as a measurement is the eleven-instance family this shift has been cataloguing.⛔ Not a claim, not a dispatch — routing only.
Generated by Claude Code
- Not p2, and the deciding fact is that nothing here is wrong. The stacks are printed because the code deliberately hands
Claim
Claimed by Claude Code, session
session_01D47qPfEWVPmhguWgBZCi5N, dispatched by thedomain:cliexecution PM seat (#6024).Branch:
claude/issue-15484-rest-log-stack-frames(pushed, empty, as the write-route probe).Two serial reads, taken before any edit
file holder claimed measured now verdict packages/rest/src/error-response.tsPR #15452 (triage comment: "hard serial, released on MERGE") #15452 is MERGED 2026-09-05T00:19:48Z serial released packages/rest/src/rest-server.tsPR #15673 open, mergeable_state: dirtyserial HELD ⇒ The triage comment's sequencing constraint ("this card cannot be worked on the majority of its own population until #15452 resolves") is discharged, and the surviving hold is on a different file than triage expected.
⚠️ Triage also ruled ⛔ do not split this into "the reachable half" and "the held half" — "the decision has to be the same answer at all four sites". I intend to honour that by repairing the mechanism inpackages/rest/src/log.ts, which is upstream of all four sites at once, sorest-server.tsneed not be edited and #15673 is not raced. If the repair turns out to requirerest-server.ts, I stop and report rather than race.Counts, attribution and the A2.5 judgement (who reads these stacks, and when) are being re-derived from my own branch ref; ⛔ nothing on this card is being quoted as a measurement.
Generated by Claude Code
os-dev-report
{ "issue": 15484, "status": "needs_decision", "branch": "claude/issue-15484-rest-log-stack-frames", "pr": null, "premise_still_valid": true, "summary": "Every factual claim on the card re-derived and CONFIRMED on my own branch ref (base c99449ab5fd); attribution matches the card to the unit on two of its four rows. A2.5 — the judgement the card turns on — is now MEASURED, and it closes the card's easy branch: the Error object handed to logError carries three things, not one (message, cause chain, frames), and FOUR existing assertions in two files already pin it deliberately with citations (#5437, #4886, #5489), including an explicit do-not-delete warning aimed at exactly this repair. An ablation of the naive route (stop passing the Error) removed 2018 of 2018 frames and 47.6% of suite output — and went RED on those four pins, one of which fails because the withheld driver text lives on error.cause and only full Error formatting prints it. So a fault's stack IS read here, and no single uniform answer is correct at all four sites: site 1 is one declared self-describing class 69 of 100 times, site 2 is 22 distinct shapes including bare TypeErrors whose message gives no location. The only route keeping both volume and diagnostic is a declared level seam — and building that seam is reserved to triage by a standing ruling in docs/audits/2026-09-test-log-volume-census.md ('No seam was added. Per triage's ruling ... which of the two candidate seams (if either) to build is triage's call'). Separately, 41.7% of the population sits in rest-server.ts, held by open PR #15673 (state dirty), so touching it would race a held file AND perform the reachable-half/held-half split triage forbade. No speculative code written; no PR opened.", "tests": "BASELINE, green, under scripts/pm/os-verify-lock.sh: VERDICT command-exit 0, 'Test Files 180 passed (180)', 'Tests 3086 passed (3086)', 5654 captured lines. Deps built first: pnpm --filter '@objectstack/rest^...' build, VERDICT command-exit 0. COUNTS re-derived, never quoted; control run FIRST. ABLATION (measurement, not a deliverable): mutated packages/rest/src/log.ts so logError coerces Error args to String(a.message). NO REBUILD NEEDED and this is stated rather than assumed: packages/rest tests import ./log.js and ./rest-server relatively inside their own package and vitest.config.ts aliases only plugin-hono-server and service-datasource, so the specifier resolves to SOURCE, not dist — which is why the mutated bytes were read with no build step. MUTATION CONFIRMED ON DISK before the run: removed-text count 1 to 0, injected marker ABLATION_15484_STRIPPED count 1, blob ce32dffc3f differing from HEAD blob 5252da904a. Direction predicted in writing first, including which assertions were expected to stay GREEN. RESULT: P1 confirmed (frames 2018 to 0; output 5654 to 2962 lines, 47.6% reduction) but P2/P3 FALSIFIED — 'Test Files 2 failed | 178 passed', 'Tests 4 failed | 3082 passed'. My prediction that nothing pinned this was WRONG because I grepped for the word 'stack' while the pins are spelled as identity assertions on the Error object. RESTORE proved, not assumed: git checkout HEAD -- ABSOLUTE path under a trap on EXIT/INT/TERM, then blob 5252da904a equal to the HEAD blob AND git diff HEAD empty; empty hash was coded as FAILURE. Final tree clean. Gate union: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack reports 'this branch changes nothing against origin/main' — empty change set, nothing to derive, no stale-tree banner. Exit codes captured after redirection, never through a pipe; verdicts quoted from the lock's own VERDICT line.", "mcp_calls": "4", "open_questions": [ { "question": "packages/rest fault logging prints 2018 stack-frame lines per suite run (35.7% of output) and the frames are genuinely load-bearing — pinned by four assertions with an explicit do-not-delete warning. Which route should close the volume without spending the diagnostic?", "options": [ "A. Declared level seam on packages/rest's logError, shipped default UNCHANGED (stacks on), and packages/rest/vitest.config.ts opts down — exactly the OS_REGISTRY_LOG pattern already used in this same file for the [Registry] population, gate-enforced by scripts/check-registry-log-declared.mjs. Production byte-identical; suite drops about 47.6% of its output. Cost: a second ad-hoc env seam unless it is designed as one contract, and the audit reserved this choice to triage.", "B. Do nothing and close as working-as-intended. The stacks are correct, pinned, and cheap in production where a fault is rare; the volume is an artifact of 3086 tests deliberately provoking faults through production fault-logging code. Cost: the measured +50.7% growth trend on this population goes unmanaged.", "C. Structured-logger route (#13986) — move these sites off console.error onto the ObjectLogger that already writes level-gated structured lines. Cost: much larger blast radius, and it is the other of the two candidate seams the audit explicitly left to triage.", "D. Narrow the emitting population instead of the format — e.g. de-duplicate the 69-of-100 repeated AuthzStoreUnavailableError at the withheld-fault site. Cost: does not generalise, and dedup logic on an error path is its own hazard." ], "recommendation": "A, but as a TRIAGE decision rather than a dev pickup — I am not taking it unilaterally, for three measured reasons. (1) The standing ruling in docs/audits/2026-09-test-log-volume-census.md reserves 'which of the two candidate seams (if either) to build' to triage, and it was made with this suite's console volume already in the table. (2) Choosing A pre-commits the answer to 'stacks stay on by default', which is a real answer to the card's own question, and this card's triage said explicitly: better to hold the whole card than to land a half that pre-commits the answer. (3) 41.7% of the population is in rest-server.ts, held by open PR #15673 (dirty), so any seam I landed now could only cover the reachable half — the split triage forbade. Four-axis reading: (2) long-term soundness, weighted at least 50% per the 2026-09-01 maintainer ruling, favours A decisively because a declared seam makes the quiet/loud decision explicit and gate-visible instead of implicit in whether an author happens to pass error or error.message; (3) anti-AI-error also favours A, since removing diagnostics is the lenient direction and a seam forces a declaration a gate can read; (1) real business need is measured at 2018 frames and 47.6% of output, real but friction not defect; (4) anti-scope-creep is the only axis arguing against A, and it argues for reusing one designed logging contract rather than adding a second ad-hoc env var — which is precisely the design call that belongs to triage." }, { "question": "Clause-2 declaration (A2.6). The card says the honest starting answer is yes until measured otherwise.", "options": [ "yes — changing what is logged is an operator-visible surface change", "no — no key is added to any published payload and no wire body changes" ], "recommendation": "NOT DECLARABLE, and deliberately left undeclared rather than pre-hung. A2.6 requires re-declaring from the DELIVERED diff; there is no diff, because the card resolved to a decision rather than to code. Recorded for whoever implements: on route A as scoped above (default unchanged) the wire answer is untouched and no published payload gains a key, so the mechanical #14696 floor is not met — but the operator-visible-surface argument is exactly the one triage flagged, so the declaration must be made from that PR's real diff, not inherited from this report." } ], "out_of_scope_findings": [ "HANDED BACK, NOT FILED (per dispatch: dedup channels rate-limited, do not file blind). The card's own re-check CONTROL is spent and now reads ZERO, not 528. packages/rest/vitest.config.ts declares env OS_REGISTRY_LOG 'warn' (landed via #15425), which is what suppressed the [Registry] population in this very suite. Measured: grep -c '\\[Registry\\]' returns 0 on a demonstrably good capture (180 files / 3086 tests green, 2018 frames, 359 DATABASE_ERROR lines all present). Impact: anyone following the card's recipe gets a zero control and must conclude 'the capture failed' when it did not — the exact empty-result trap the card built the control to prevent. A live replacement control for this suite is '[sql-driver] DATABASE_ERROR' (359) or the '[REST]' header count (265).", "OBSERVATION, not filed. packages/rest/vitest.config.ts's comment states in the present tense that the suite 'measures 528 residual [Registry] lines here', on the lines immediately above the env declaration that now makes it 0. Readable as past-tense motivation for the declaration, so possibly working as intended; flagged only because scripts/check-registry-log-declared.mjs's header repeats the 528 figure as a live reading of packages/rest." ] }The measurement behind that verdict
Base
c99449ab5fd, my own branch ref. Control run first; every count re-derived, none quoted.A2.2 — counts
pattern card measured now note total captured lines 5,971 5,654 fell because 528 [Registry]lines were declared awayindented atframes (^\s*at)1,922 2,018 ~35.7% of output, up from ~32% at file://688 730 [sql-driver] DATABASE_ERROR310 359 disjoint population, as the card says control [Registry]528 0 ⚠️ control is spent — see findingsSuite green:
Test Files 180 passed (180)·Tests 3086 passed (3086).A2.3 — attribution, located by content, never by line number
origin card measured now logWithheldServerFault(card'serror-response.ts:1960)713 803 rest-server.tsshare errors661 661 ✅ exact logUnexpectedRouteError(card's:2375, now:2416)358 364 other [REST]180 180 ✅ exact error-response.tstotal1,071 (55.7%) 1,167 (57.8%) Two rows match to the unit, which is strong corroboration that the card was honestly measured and that only the
error-response.tspopulation grew.A2.4 — mechanism, confirmed at source and then causally
packages/rest/src/log.tsline 15 hands its varargs straight toconsole.error, and Node formats anErrorargument with its full stack. Confirmed causally by the ablation: coercingErrorargs to their message took frames 2,018 to 0.A2.5 — who reads these stacks, and when
The card's easy branch was "nobody, because the same fault is already reported with its message elsewhere". That is measured FALSE. The
Errorobject is the carrier for three things:- the message — at
logWithheldServerFaultthe client never sees it; sendError 的显式状态直通覆盖 400–599,5xx 的原始驱动报错绕过全部泄漏启发式直达客户端(metadata-protocol 有活体产出方) #5437 exists so the log keeps it; - the
causechain — the withheld driver text lives there.rest-5xx-message-sanitization.test.ts'sloggedTexthelper walkscauseto depth 5, and its docblock records the measured shape:[REST] Unhandled error: Error(Failed to delete customization overlay ...) CAUSE SQLITE_ERROR: no such table: sys_metadata. Only fullErrorformatting prints that. My message-only mutation lost it and the test went red; - the frames — at
logUnexpectedRouteErrorthese are the only location diagnostic. Measured fault classes there: 22 distinct shapes across 41 events, includingTypeError: this.engine.find is not a function, whose message alone names no location.
And the repo already pins all of this on purpose. The ablation went red on exactly four assertions:
file assertion citation rest-5xx-message-sanitization.test.ts:282the withheld text still reaches the server log, in full #5437 rest-5xx-message-sanitization.test.ts:296Postgres phrasing withheld too #5437 rest-expected-error-logging.test.ts:174"a 500 still logs the full error object" — comment: "The error itself is logged, not a summary — the stack is the point here." #4886 rest-expected-error-logging.test.tsan unrecognised handler bug stays loud #5489 rest-5xx-message-sanitization.test.ts:163carries a warning written for exactly this repair:⛔ Do NOT "fix" a future red here by deleting the
loggedTextassertions. They are the only thing in this file asserting that withholding text from the CLIENT did not also delete it from the LOG, which is the failure mode a disclosure fix is most likely to introduce.A stack here is read, the guards exist, and stripping it is the blind-production-incident trade A2.5 calls worse at any volume.
Why no uniform answer fits all four sites
site frames fault classes is the stack the diagnostic? logWithheldServerFault803 69 of 100 events are one class, AuthzStoreUnavailableErrorno — but its causeislogUnexpectedRouteError364 22 distinct shapes, incl. bare TypeErrorsyes, irreducibly rest-server.tssites841 held by PR #15673 not assessable without touching a held file Triage required one answer at all four sites. The sites have opposite diagnostic profiles, so the only construction that satisfies triage and keeps both properties is a declared level seam — reserved to triage by the audit's closing ruling.
A2.1 — serial status, verified from my own branch ref
error-response.ts— PR fix(rest): the generic declared-status passthrough names its object on both error doors #15452 is MERGED (2026-09-05T00:19:48Z). The hold triage described is released; the dispatch prompt did not carry this.rest-server.ts— PR fix(rest): consume the parsedapisub-config soRestApiConfigSchemaowns its defaults #15673 is open,mergeable_state: dirty. Still held, and it carries 41.7% of the population. Not touched, not raced.
Generated by Claude Code
- the message — at
标准四棱卡面块 —
needs-user-decision落卡:
domain:cli执行席(#6024),sessionsession_01D47qPfEWVPmhguWgBZCi5N。⛔ 本席不代裁:docs/audits/2026-09-test-log-volume-census.md有一条现行裁决,把「两个候选 seam 建哪个(或都不建)」保留给 triage。没有写任何投机代码,没有开 PR。 卡片的每一条事实都在自己的分支 ref 上(base
c99449ab5fd)重新推导并确认,两行归因与卡片精确到单位。⭐⭐ 卡片的「容易那一半」已被测量关闭
派发时我坚持先答 A2.5「谁在读这些栈、什么时候读」。测出来的答案关掉了删诊断这条路:
交给
logError的Error对象带三样东西,不是一样——message、cause 链、frames。而已有四条断言、分布在两个文件刻意钉住它,各带引用(#5437 · #4886 · #5489),其中一条明确写着不要删,针对的正是这次修法。朴素修法的消融(让
logError把 Error 强制成String(a.message)):frames 2018 → 0 输出 5654 → 2962 行(−47.6%) 测试 Test Files 2 failed | 178 passed · Tests 4 failed | 3082 passed⭐ 而其中一条 pin 之所以红,是因为被扣留的 driver 文本挂在
error.cause上,只有完整 Error 格式化才会打印它。⇒ 栈确实被读。⭐ 实施者对自己预测失败的复盘很精确:它预测「没有东西钉住这些栈」,错了,因为它 grep 的是
stack这个词,而那些 pin 是写成对 Error 对象的同一性断言。四个站点没有统一答案
- 站点 1:69/100 次是同一个自描述异常类(
AuthzStoreUnavailableError) - 站点 2:22 种不同形状,含裸
TypeError——消息里没有任何位置信息
⇒ 一刀切的格式化策略在任一方向上都会错。
四轴分析
① 项目长远合理性(权重恒 ≥50%,领起) —— 维护者 2026-09-01 裁,逐字:「四维分析中,长期合理应该权重最高,至少50%」。
A(声明式 level seam) 决定性地领先:它把「安静还是响亮」变成显式且 gate 可读的决定,而不是隐含在「作者恰好传了
error还是error.message」里。这与同一个文件里已经在用的OS_REGISTRY_LOG模式一致,并有scripts/check-registry-log-declared.mjs把关。B(什么都不做) 让实测的 +50.7% 增长趋势无人管理。C(结构化 logger,#13986) 方向正确但爆炸半径大得多。② 实际业务需求(须实测) —— 2018 帧、占套件输出 35.7%,真实但属于摩擦而非缺陷:生产环境里故障是稀有的,这个量是 3086 个测试刻意触发故障、穿过生产故障日志代码的产物。
③ 防 AI 写代码犯错 ——
⚠️ 这条轴在这里指向的方向与直觉相反:删诊断是「宽容」方向,正是本轴反对的那一侧。A 强制一个 gate 能读的声明,B/D 不强制。④ 创业阶段不扩散需求 —— 唯一反对 A 的轴:它可能变成第二个临时 env 变量。这恰恰说明该由 triage 把它设计成一份统一的日志契约,而不是由执行席顺手加一个。
推荐:A,但作为 triage 决定,不是 dev 拾取
三条实测理由:
- 现行裁决把 seam 选择保留给 triage,且是在这份套件的 console 量已经在表里的情况下做出的。
- 选 A 就预先承诺了「栈默认保持开启」——那是对本卡自身问题的一个真实回答;而本卡的 triage 明说过:宁可整卡压住,也不要落一半而预先承诺答案。
⚠️ 41.7% 的population 在rest-server.ts里,被开放 PR fix(rest): consume the parsedapisub-config soRestApiConfigSchemaowns its defaults #15673 持着 ⇒ 现在只能覆盖「够得着的那一半」,正是 triage 禁止的半拉子切分。
⛔ D 不推荐(在错误路径上做去重是自身的隐患,且不可推广)。
Clause ② — ⛔ 不可声明,故意留空
A2.6 要求从交付的 diff 重新声明。没有 diff,因为这张卡解析成了一个决定而不是代码。记录给实施者:按上述范围的 A(默认不变),线上答案不动、无已发布 payload 新增键,故 #14696 的机械底线不满足;但「operator 可见面」这条正是 triage 点出的论据,所以必须从那个 PR 的真实 diff 上声明,不得继承本报告。
Generated by Claude Code
- 站点 1:69/100 次是同一个自描述异常类(
⛔ Correction — this card's own re-check CONTROL is spent and now reads ZERO
⚠️ Anyone following this card's recipe today will get a zero control and conclude "the capture failed" — when it did not. That is precisely the empty-result trap the control was written to prevent, and the control has now fallen into it.Measured
The card offers
[Registry]= 528 as the control that proves a capture is good. On a demonstrably good capture — 180 files / 3086 tests green, 2018 stack frames present, 359DATABASE_ERRORlines present — it now reads:grep -c '\[Registry\]' → 0Cause:
packages/rest/vitest.config.tsdeclaresenv OS_REGISTRY_LOG: 'warn'(landed via #15425), which suppresses exactly that population in exactly this suite. The control was measured before that landed and was silently invalidated by it.Live replacements for this suite
control count [sql-driver] DATABASE_ERROR359 [REST]header count265 ⛔ Do not use
[Registry]here again.⭐ The general lesson, which is worth more than the fix
A control is not permanent. This one was correct when written, and a later, unrelated, entirely legitimate change (declaring a log level) turned it into a silent zero. ⇒ A control has to be re-validated in the same run it is used in, not inherited from the card that proposed it. A control you did not just see fire is not a control.
⚠️ Related, recorded not filedpackages/rest/vitest.config.ts's comment states in the present tense that the suite "measures 528 residual[Registry]lines here" — on the lines immediately above the env declaration that now makes it 0. Readable as past-tense motivation for the declaration, so possibly working as intended; flagged only becausescripts/check-registry-log-declared.mjs's header repeats the 528 figure as a live reading ofpackages/rest.
Generated by Claude Code
10 remaining items
⛔ Correction to this seat's own fence reading — the serial behind #14656 never existed on the delivered face
domain:cliexecution seat (#6024), 2026-09-12T13:45Z. The card keepspm:queueand no assignee. ⛔ Nothing re-graded.What this seat wrote at 12:17Z (
5645829962): 「this card is released by that PR's MERGE, ⛔ not by its arm」, on the reading that #14656 and this card were hard-serial onpackages/rest/src/error-response.ts.⚠️ That fence came from the ANTICIPATED file face, not a measured one. It was derived from the maintainer ruling's execution line for #14656, which names 「domain:clilane (packages/rest+ the shared funnel in@objectstack/types)」. The delivered PR is now on the board and its face is measured:PR #17854 — 5 paths, none under packages/rest/ .changeset/14656-declared-capability-absence-warn-once.md packages/runtime/src/declared-capability-absence-warn-once.test.ts packages/runtime/src/dispatcher-5xx-always-logged.test.ts packages/types/src/server-fault-log.test.ts packages/types/src/server-fault-log.tsIt needed no
packages/restedit because that door does not log 5xx itself —git grep -c 'logServerFault' -- packages/restis zero across the whole package (control lit on the same paths:sendErroris spelled in at least five of its files). The REST door reaches the shared funnel throughsendError, so the predicate applied insidelogServerFaultserves it with nothing to change. ⇒ the two faces are disjoint and this card was never serial behind #14656.The live fence reading, taken now rather than inherited
A file-by-file scan of all 19 open PRs (
/pulls/N/files, per PR, 2026-09-12T13:44Z) finds zero open PRs touchingpackages/restat all — control lit in the same pass: PR #17854's own 5-path face comes back non-empty from the same query. Every path this card's ruled repair can reach is FREE:path holder packages/rest/src/log.ts— the seam's own filefree packages/rest/src/error-response.ts— 55.7% of the populationfree (PR #15452 merged 2026-09-05T00:19:50Z) packages/rest/src/rest-server.ts— 41.7%free (PR #15673 merged 2026-09-05T22:13:07Z) packages/rest/vitest.config.ts— the suite opt-downfree ⇒ ⛔ No fence to wait out. The sequencing this card carried since 09-04 is discharged, and what remains is its own pre-dispatch premise check — ⛔ which this comment is not.
⚠️ And the re-check recipe in the body is still spent, twice over.[Registry]reads 0 since #15425 declaredOS_REGISTRY_LOG: 'warn'for this suite (5550786137— use[sql-driver] DATABASE_ERRORor[REST]), and the mechanism grep is a trap of its own: the source spellsconsole?.error, sogit grep -c 'console\.error' -- packages/rest/src/log.tsreturns 0 and the obvious controlconsole\.returns 0 with it — a dead control, ⛔ not an absent mechanism. The line is there, onorigin/main:packages/rest/src/log.ts:15 export const logError = (...args: unknown[]) => (globalThis as any).console?.error(...args);
Generated by Claude Code
Claim: session_01TSf4DV7ziu4V5j73e46b7c
Branch: claude/issue-15484-rest-log-declared-level-seam
Clause-②: no⚠️ TheClause-②:line above wasyeswhen this claim was written and was corrected tonoby the seat at the contract review (2026-09-12T15:26Z) — the act §5 below reserved for the review («⛔ Do not re-declare the claim line yourself; the seat corrects it at review»). The record is5646806136on PR #17863, head7e6e9e044. ⭐ What settled it was not the seat's derivation from the general level rule — that derivation was wrong and is retracted at5646786543— but decision batch #49's own sentence: 「Changeset:@objectstack/restpatch(default behaviour identical; a new declared environment seam is documented, not a contract key)」. ⛔ §5 is left exactly as written.Dispatch —
domain:cliexecution seat (#6024), round 22, 2026-09-12T14:02Z, onorigin/main. The assignee and thisClaim:line are written by the seat on the dev's behalf: the dev inherits both, ⛔ posts no second claim and ⛔ writes no assignee.Fence: none. A file-by-file scan of all 19 open PRs at 13:44Z finds zero touching
packages/rest— control lit in the same pass (PR #17854's own 5-path face returns non-empty). The 12:17Z fence on this card was this seat's error and is corrected at5646265322: it came from #14656's ANTICIPATED face, and the PR that card delivered touches nopackages/restfile at all.
1. The maintainer ruling, quoted — this is settled, ⛔ not yours or mine to re-open
Recorded at
5551137024, decision batch #49 item 2, 2026-09-05, verbatim 「15041 应该改为实际 id 保存。选A,其他同意」:Option A.
packages/rest'slogErrorgains a declared log-level seam in theOS_REGISTRY_LOGpattern already used in the same file — a declaration a gate can read (thecheck-registry-log-declared.mjsshape), with the shipped default unchanged: a reported fault keeps printing the fullError(message,causechain, frames).packages/rest/vitest.config.tsopts the suite down. ⛔ B (close as intended), C (structured-logger route, #13986 already closednot_planned) and D (dedup on the error path) are rejected.The seam choice that
docs/audits/2026-09-test-log-volume-census.mdreserved to triage is decided here by the maintainer: the seam is the declared-level seam onlogError. That reservation is discharged; the audit's line should be updated in the same round to point at this ruling.One answer at all four sites, landed in one round — triage's 「do not split」 (
5547831042) stands.⛔ The shipped default does not move. This card buys a declaration, not a quieter product. If your design makes a real caller see less by default, you have built D (rejected), not A.
2. What the previous claimant measured — ⛔ inherit the findings, ⛔ not the conclusion
The first dispatch (
5550573945) closed the card's easy branch by measurement, and those measurements are yours to re-derive, not to re-litigate:- The
Errorhanded tologErrorcarries three things, not one — message,causechain, frames. Four existing assertions in two files pin it deliberately, each with a citation (sendError 的显式状态直通覆盖 400–599,5xx 的原始驱动报错绕过全部泄漏启发式直达客户端(metadata-protocol 有活体产出方) #5437 · Metadata routes log every expected 404 as "[REST] Unhandled error" with a stack trace — Studio's draft probe produced 45 in one browsing session #4886 ·/meta/:type上一个未分类的服务端故障被报成 HTTP 400 —— handleRouteError 的兜底把 outage 说成客户端错误 #5489), one of them an explicit do-not-delete aimed at exactly this repair. - The naive ablation (force
String(a.message)) removed 2018 of 2018 frames and 47.6% of suite output — and went RED on those four pins, one of them because the withheld driver text lives onerror.causeand only fullErrorformatting prints it. ⇒ the stack IS read. - No single uniform formatting answer is correct at all four sites: site 1 is one self-describing class 69/100 times; site 2 is 22 distinct shapes including bare
TypeErrors whose message gives no location.
⭐ That implementer also recorded why it predicted wrong: it grepped for the word
stack, while the pins are written as identity assertions about theErrorobject. ⛔ Do not repeat that matcher.3. Your file face
path what is expected packages/rest/src/log.tsthe seam. logError(andlogWarn, which falls back through it) gains a declared level, read from a named env seam, defaulting to today's behaviour.packages/rest/vitest.config.tsthe suite's own opt-down, beside the OS_REGISTRY_LOG: 'warn'lines already there.a gate the declaration must be machine-read. scripts/check-registry-log-declared.mjsis the shape to follow — extend it or add its sibling; ⛔ a declaration no gate reads is the declared≠enforced shape this repo refuses.docs/audits/2026-09-test-log-volume-census.mdlines 419–420 still read 「No seam was added. Per triage's ruling … which of the two candidate seams (if either) to build is triage's call」. That reservation is discharged by the ruling above; update the line to point at it. ⛔ This is the ruling's own instruction, not a rider. the four sites whatever the one answer requires — error-response.ts(713 + 358 frames) andrest-server.ts(661) are free, measured at 13:44Z.⛔
packages/specis OUT of your face. 「凡触packages/spec一律转domain:spec座位,不论谁需要它」 —SKILL.md:231·core-rules.md:62·SKILL.md:287·lanes/cli.md:12·lanes/spec.md:12·dispatch-runbook.md:158. If the design needs it, stop and report; ⛔ do not edit it.4. Premises to falsify FIRST — each is a claim, not a fact
- The mechanism is still there.
packages/rest/src/log.ts:15onorigin/main(last touched by refactor(rest): extract the ADR-0112 error/fault-classification prologue out of rest-server.ts #8887 — it has not moved since the card was filed):export const logError = (...args: unknown[]) => (globalThis as any).console?.error(...args);⚠️ Matcher trap, measured: the source spellsconsole?.error, sogit grep -c 'console\.error' -- packages/rest/src/log.tsreturns 0 — and the obvious controlconsole\.returns 0 with it. That is a dead control, ⛔ not an absent mechanism. - The seam has NOT already landed.
git grep -n 'OS_REGISTRY_LOG' origin/main -- packages/restreturns onlyvitest.config.tsrows — the engine's seam, a different one.log.tshas none. - The counts are stale by eight days — re-derive them, ⛔ do not quote the card. The card says so itself.
- ⛔ The card's own re-check control is SPENT.
[Registry]reads 0 in this suite since 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 declaredOS_REGISTRY_LOG: 'warn'for it (5550786137). Live replacements:[sql-driver] DATABASE_ERRORand[REST]. ⭐ A control must be re-validated in the SAME run it is used in — that comment is the general lesson and it was earned here. - The four pins still exist and still pin. Locate them by their citations (sendError 的显式状态直通覆盖 400–599,5xx 的原始驱动报错绕过全部泄漏启发式直达客户端(metadata-protocol 有活体产出方) #5437 · Metadata routes log every expected 404 as "[REST] Unhandled error" with a stack trace — Studio's draft probe produced 45 in one browsing session #4886 ·
/meta/:type上一个未分类的服务端故障被报成 HTTP 400 —— handleRouteError 的兜底把 outage 说成客户端错误 #5489) and by identity assertions on theErrorobject, ⛔ not by greppingstack. - The blocks are gone.
Blocked-by: #14366is STRUCK (404 on both endpoints; its release condition met anyway — PR fix(rest): consume the parsedapisub-config soRestApiConfigSchemaowns its defaults #15673 merged 2026-09-05T22:13:07Z). PR fix(rest): the generic declared-status passthrough names its object on both error doors #15452, theerror-response.tshold, merged 2026-09-05T00:19:50Z.
5.
Clause-②— declaredyesbecause the shape is genuinely undecided「claim 拿不准 ⇒ 按
yes挂标走席内契约复核」. The seam may or may not put a new symbol on@objectstack/rest's entry:log.tsis documented as 「deliberately NOT re-exported from the package index: an internal shim, not a logging API」, which points atno— but an env seam and a gate-readable declaration are an operator-facing surface, and the card itself says 「the honest starting answer isyesuntil measured otherwise」.⭐ What the seat needs from you is the MEASUREMENT, in your report and your PR body:
- the export list of
packages/rest/src/index.tsbefore and after, order-insensitively — ⛔ not a[+-].*exportdiff matcher alone, which cannot see a return type on a line that carries noexport; - whether
packages/rest/package.json'sexportsmap moved; - the env seam's name and where an operator learns it.
⛔ Do not re-declare the claim line yourself; the seat corrects it at review.
⚠️ And know what to expect: if the delivered surface readsnowhile every changeset ispatch,Check Changesetgoes red on the level-vs-declaration axis. That red is cleared by a one-line body edit at review (measured three times: #17758, #17844, #17854 — no push, no re-run). ⛔ Never raise a package tominorto quiet it, and ⛔ never drop the changeset.6. Gates
Derive them, ⛔ do not guess:
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack. Capture each exit code before any pipe, and read each verdict from the command's own printed line. Exit 3 isPREREQUISITE NOT MET— ⛔ neither a pass nor a finding; satisfy the prerequisite (usually a build) and re-run.⚠️ This lane's blind spot:dispatch-gatesdoes not namepnpm lint. Always add it, as the full union (eslint . --no-inline-config, ⛔ no narrowing).Heavy runs go through
scripts/pm/os-verify-lock.sh; quote the wrapper'sVERDICT command-exitline. Anything you could not run, name it rather than letting it read as green.7. Deliverable
Branch above, one changeset, a draft PR with
Fixes #15484, and a report. ⭐ An ablation is what buys the acceptance: make the seam's default actually move and show the pins go red, then restore and prove the restore by blob hash and a clean whole-tree status. ⛔ A green suite with no ablation says only that nothing was tested.⛔ Stop and report rather than racing if the design needs
packages/spec, if it needs a file this order did not name, or if the shipped default has to move to make it work.
Generated by Claude Code
- The
os-dev-report
{ "issue": 15484, "status": "needs_decision", "branch": "claude/issue-15484-rest-log-declared-level-seam", "pr": "https://github.com/objectstack-ai/objectstack/pull/17863", "premise_still_valid": true, "summary": "Ruling A is implemented and green: packages/rest/src/log.ts gains OS_REST_LOG — five levels, shipped default 'info', logWarn falling through the same ladder — with the shipped default byte-for-byte today's behaviour; a new gate scripts/check-rest-log-declared.mjs (19-case self-test, wired into package.json and lint.yml) locates the seam by its env read, reads the vocabulary from REST_LOG_LEVELS rather than copying it, holds REST_LOG_LEVELS equal to objectql's REGISTRY_LOG_LEVELS (the 'one contract' instruction), requires the declaration in the root block AND every inline vitest project, and makes the ruling's 'the shipped default does not move' mechanical by failing a default of 'error' or 'silent'. A 14-assertion pin file, the published README's new Environment section, the audit's lines 419-420 discharge, and one @objectstack/rest patch changeset land with it. ONE ruled row is deliberately NOT executed and is the decision I am handing back: the suite-wide opt-down. Measured before choosing — OS_REST_LOG:'silent' does take frames 2,095 to 0, but it reddens 28 assertions across 15 files (7x the four pins the order inherited, all of them the 'operator still gets the words' half, all reading through a console.error mock so they never printed any of the volume), AND it turns the opposite half into a phantom check: 8 files assert that an expected 4xx logs NOTHING (4 such assertions in rest-expected-error-logging.test.ts alone), and a silenced suite makes those pass for the wrong reason — green even if every expected 4xx were logged loudly. That is the same shape by which this card's own [Registry] control was silently spent by #15425. Landing it needs ~20 files the order did not name plus a pairing guard, which is the order's own stop-and-report trigger, so the declaration ships at 'info' (real, valid, gate-read, behaviour-identical) with the measurement recorded in the config's root env block and the value left as the one-line choice it is. Note also a prompt-vs-repo conflict I resolved in the repo's favour: the dispatch's commit trailer 'Co-Authored-By: Claude Opus 5' is REFUSED by this repo's pre-push check:commit-card-trailers, which requires the model-free pair; commits use 'Co-Authored-By: Claude' plus the session trailer.", "tests": "All exit codes captured BEFORE any pipe; every verdict quoted from the command's own printed line or the lock's VERDICT line. Heavy runs serialised through scripts/pm/os-verify-lock.sh (slot issue-15484-dev). Run against 7e6e9e044, tree clean. GATES: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 109 commands (stderr confirmed repo assertion holds, change set 9 paths vs merge base 7f625364b, non-shallow) — 109/109 exit 0. Three needed a second look, none a finding: check:dual-build-cjs-loads and check:type-check-debt returned exit 3 PREREQUISITE NOT MET, satisfied by turbo run build over all packages (VERDICT command-exit 0, 72/72 successful) and then BOTH exit 0; check:pm-dispatch-gates returned 124 which was my own runner's 180s timeout and not a verdict — re-run unbounded, exit 0, 1678 self-test cases. pnpm lint (full union, eslint . --no-inline-config, NO narrowing — this lane's blind spot) exit 0. node scripts/check-rest-log-declared.mjs --self-test 19/19 exit 0; real run exit 0. node scripts/pm/check-clause2-carriers.mjs --pair 17863 EXIT 0 — 'the clause-② declaration is readable in the fixed spelling and both carriers agree'. TYPECHECK: pnpm --filter @objectstack/rest run typecheck VERDICT command-exit 0 — tsc --noEmit plus check:test-typecheck, 0 files / 0 errors / 0 pinned signatures. SUITE: baseline VERDICT command-exit 0, 'Test Files 190 passed (190)' / 'Tests 3182 passed | 1 skipped'. Final VERDICT command-exit 0, 'Test Files 191 passed (191)' / 'Tests 3196 passed | 1 skipped' — the delta is exactly the new pin file. COUNTS re-derived, never quoted, CONTROL VALIDATED IN THE SAME CAPTURE IT WAS USED IN: total lines 5,709; indented '^\\s*at ' frames 2,095 (36.7%); at file:// 742; live control [sql-driver] DATABASE_ERROR 362 (fires); live control [REST] 272 (fires); spent control [Registry] 0, as documented. Attribution by preceding header: error-response.ts 1,197 (57.1%), rest-server.ts 841 (40.1%), cause chains 57 — 100% of frames arrive through logError, and [sql-driver] contributes ZERO frames, re-confirming the disjointness claim. THE DEFAULT DID NOT MOVE, MEASURED: frames 2,095 -> 2,095 and control 362 -> 362 across the change. ABLATION (two legs, direction predicted in writing FIRST, trap on EXIT/INT/TERM with absolute paths, restore HEAD-pinned not bare): LEG 1 moved ONLY the shipped default 'info'->'silent' — on-disk proof removed-text 1->0, injected-text 1, blob 51fdb14b vs HEAD 3720e0a0 — gate EXIT 1 printing \"REST_LOG_DEFAULT_LEVEL is 'silent', which stops at least one of this shim's two sites from reporting at all for EVERY real caller\", tests '1 failed | 2 passed' with the failure being the new seam pin and THE TWO FILES CARRYING THE FOUR INHERITED PINS STAYING GREEN — predicted, because an explicit harness declaration outranks the default. LEG 2 moved the default AND removed the harness declaration (the world where the default governs) — on-disk proof declarations 3->0, blob 70c8244c vs HEAD 84331960 — the four inherited pins (#5437/#4886/#5489) went RED, '2 failed', 8 assertions, and frames in that capture went to 0. Every prediction held. RESTORE PROVED, not assumed: git checkout HEAD -- on absolute paths, log.ts blob 3720e0a0 == HEAD blob, vitest.config.ts blob 84331960 == HEAD blob, git diff HEAD empty, git status --porcelain empty across the WHOLE tree; an empty hash was coded as FAILURE. RESOLUTION PATH stated rather than assumed: packages/rest tests import ./log.js relatively inside their own package, so vitest resolves to SOURCE, not dist — proved causally in this session, since the earlier silent-harness run went red against a dist built BEFORE log.ts was edited. CLAUSE-② MEASUREMENT: export list of packages/rest/src/index.ts before vs after, order-insensitively, IDENTICAL — taken two independent ways off the built entry so a type-only export cannot hide (12 runtime value exports via Object.keys of the ESM entry; 31 declared export names parsed from dist/index.d.ts); diff of the two readings empty. NOT a [+-].*export matcher. packages/rest/package.json exports map did NOT move (git diff vs merge base over that file is empty). No wire byte, no published payload key — the #14696 mechanical floor is NOT met. The seam's name is OS_REST_LOG and an operator learns it from packages/rest/README.md's new '### Environment' section (a file inside this package's published files[]), plus log.ts's docblock and the audit's closing section. BYTE DISCIPLINE: grep -naP over every changed file and the PR body for control characters — empty; pnpm check:nul-bytes exit 0. PR body read back to its last line after posting — intact, first line 'Fixes #15484', with a deliberate negative control probe correctly reading MISS. Label needs:contract-review written add-only and READ BACK — the first write had in fact FAILED (proxy requires Content-Type: application/json), which the read-back is what caught; re-written and confirmed present.", "mcp_calls": "0 — every GitHub read and write went through plain curl against the REST API, and the card/comment reads through the same; no MCP GitHub tool was called at any point.", "open_questions": [ { "question": "The ruled file face asks packages/rest/vitest.config.ts to opt the suite down, and the measured cost is 7x what the order inherited: OS_REST_LOG:'silent' takes frames 2,095 to 0 but reddens 28 assertions across 15 files, and separately makes 8 files' 'an expected 4xx logs NOTHING' assertions pass vacuously. How should the suite's level be set?", "options": [ "A. Ship as delivered — declaration at 'info' (behaviour-identical), volume unchanged at 2,095 frames, the value left as a one-line review edit. Cost: the card buys a declaration and a gate but no volume relief, so the measured +50.7%/window growth stays unmanaged.", "B. Opt the suite down to 'silent' and give each of the ~15 asserting files an explicit loud-level declaration (a two-line shared helper call; no assertion text changes), PLUS a pairing guard so a file that spies on console.error but declares no level is a gate finding. Cost: ~20 files the order did not name, a second new gate over test files, and the tautology risk is only closed by that guard actually landing.", "C. Opt down and edit nothing else. Cost: 28 red assertions — not viable as stated, and it silently guts the anti-noise half of #4886. Listed only to record that it was measured and rejected, not overlooked.", "D. Leave the suite loud permanently and close the volume question as working-as-intended, keeping the seam for operators only. Cost: contradicts the ruling's own 'vitest.config.ts opts the suite down' sentence, which only the maintainer can revise." ], "recommendation": "A now (this PR), with B filed as its own card rather than folded in. Four-axis reading, using the axes this thread already fixed at 5550784248 — ① long-term soundness weighted at least 50%: A already buys the durable half, because what rots silently is the DECLARATION and the DEFAULT, and both are now gate-held; the volume is real but it is friction, and B's machinery (a suite whose default is 'faults are not logged', re-loudened per file) is itself a long-term liability unless the pairing guard lands with it, which makes B a design job rather than a value edit. ③ anti-AI-error decides it: B without the guard leaves a trap in which asserting silence into a silenced suite reads green — the exact failure this card already suffered when #15425 spent its [Registry] control — so B must not be landed casually, and landing A first costs nothing and forecloses nothing. ④ anti-scope-creep: B is ~20 files the order did not name plus a second gate; the order's own stop condition names precisely this. ② real business need: 2,095 frames per run, 36.7% of output, real but not a defect and not on any user path. ⇒ A is the cheap-to-advance choice: flipping one string in vitest.config.ts is a one-line move forward once B's guard exists, whereas backing ~20 files out at review is expensive." }, { "question": "The changeset is @objectstack/rest patch per the ruling, and the delivered Clause-② reads yes on the operator-visible-surface axis but no on the export axis (measured: nothing moved). The seat's dispatch warned that a surface reading 'no' against an all-patch changeset turns Check Changeset red on the level-vs-declaration axis.", "options": [ "yes — keep patch and the yes declaration; if Check Changeset goes red, clear it with the one-line body edit at review, as measured three times on #17758 / #17844 / #17854", "no — raise @objectstack/rest to minor to quiet it", "drop the changeset" ], "recommendation": "The first. The ruling fixed the level at patch explicitly ('default behaviour identical; a new declared environment seam is documented, not a contract key') and the dispatch ⛔ forbids both of the other two. I have NOT re-declared the claim line myself — the seat corrects it at review — and I am flagging rather than pre-empting, since the red, if it appears, is cleared by a body edit with no push and no re-run." } ], "out_of_scope_findings": [ "noted, not filed: packages/rest/vitest.config.ts's pre-existing comment still states in the PRESENT tense that the suite 'measures 528 residual [Registry] lines here', on the lines immediately above the declaration that makes it 0. Carrier: the next PR editing this config — which this one is, so the new OS_REST_LOG block now sits beside it. Not filed because scripts/check-registry-log-declared.mjs's header repeats the same 528 as a live reading of packages/rest, and the two should be corrected together by whoever owns that gate; raised once already on this card at 5550786137 and still true.", "noted, not filed: scripts/check-rest-log-declared.mjs carries a second spelling of check-registry-log-declared.mjs's brace-matching and env-block reader. Declared in its own header rather than left to be discovered. Carrier: whoever adds the third seam of this shape — at which point it stops being a note and becomes a population. Not extracted here because that gate's self-test carries a SELF_TEST_BATTERY_FLOOR this card has no mandate to move.", "noted, not filed: the dispatch prompt's prescribed commit trailer 'Co-Authored-By: Claude Opus 5' is mechanically REFUSED by this repo's pre-push check:commit-card-trailers, which requires the model-free pair. Carrier: the seat's own dispatch template. Resolved locally in the repo gate's favour (AGENTS.md wins); reported so the template can be corrected at source rather than every dev amending one commit." ] }
Generated by Claude Code
ACCEPT — PR #17863, head
7e6e9e044d9551b3f22d0baacf96310205b11c2b· clause-② correctedyes→noand the gate cleared on both carriersdomain:cliexecution seat (#6024), 2026-09-12T15:33Z, round 22.Contract review of record:
5646806136— PASS, default judgment tier, ①②③ answered item by item,Independence: SELF-REVIEWstated in its own first paragraph. ⇒needs:contract-reviewis off both carriers (「PASS ⇒ 同席同笔剥双载体」), andnode scripts/pm/check-clause2-carriers.mjs --pair 17863reads exit 0: 「both carriers agree, and its diff carries no widening tell」 —⚠️ with that tool's own caveat travelling along, 「A tell is not a proof and its absence is not one either」.Why this is an ACCEPT
The ruling is delivered in full except one sentence, and that sentence is now its own card.
OS_REST_LOGlands onlogErrorwith five levels,logWarnfalling through the same ladder, and the shipped default byte-for-byte today's behaviour —'info', with an unset or unrecognised value resolving to it, so a typo cannot silence the package. The declaration is enforced, not documented:scripts/check-rest-log-declared.mjslocates the seam by its environment read rather than a hardcoded path, reads the vocabulary fromREST_LOG_LEVELSinstead of copying it, holds it equal to objectql'sREGISTRY_LOG_LEVELS, requires a declaration in every inline vitest project (a rootenvblock is inert for project runs), and fails a shipped default oferrororsilent. ⭐ That last rule is what makes the ruling's 「the shipped default does not move」 mechanical instead of a promise.The ablation is two-legged and each leg predicted its own direction in writing first. Leg 1 moved only the shipped default
'info'→'silent': the gate went exit 1 printing 「REST_LOG_DEFAULT_LEVEL is 'silent', which stops at least one of this shim's two sites from reporting at all for EVERY real caller」, and the two files carrying the four inherited pins stayed green — predicted, because an explicit harness declaration outranks the default. Leg 2 moved the default and removed the harness declaration, the world where the default actually governs: the four inherited pins (#5437 / #4886 / #5489) went red, 8 assertions, frames to 0. ⭐ A pin whose failure mode was predicted before it was produced is worth more than one that merely went red. Restore proved rather than assumed: both blobs back to their HEAD hashes,git diff HEADempty,git status --porcelainempty across the whole tree, with an empty hash coded as failure.And the resolution path was stated rather than assumed:
packages/rest's tests import./log.jsrelatively inside their own package, so vitest resolves to source, notdist— proved causally when an earlier run went red against adistbuilt beforelog.tswas edited.⚠️ Two corrections this seat owes, both already public- The seat's level reading was wrong. At
5646786543it said the fork's exit was «raise@objectstack/resttominor», derived from the general 「WHICH LEVEL」 rule's gloss 「a new accepted key or value」. Decision batch Add granular query operation capabilities to driver schema #49 had already answered it for this card: 「Changeset:@objectstack/restpatch(default behaviour identical; a new declared environment seam is documented, not a contract key)」. ⭐ A ruling on the case outranks a derivation from the general rule — and it has to be READ before anything is derived. Same root cause as this seat'sClause-②error on [Decision] Should a DECLARED 5xx on a polled route (501 NOT_IMPLEMENTED for an uninstalled optional service) log one error line per request under the "5xx never stays quiet" rule? #14656 four hours earlier, in the mirror direction. Retracted in the review of record. - A published claim in that review was itself corrected by measurement: it called the PR body's original backticked key 「a near miss, not a declaration」.
readClause2Linereturns{kind: 'declared', value: 'yes'}on that exact string, so both readers accepted it. ⇒ what was corrected was the declaration's value, not an unreadable spelling.
The one ruled row that did not ship, and where it went
The ruling's execution list says 「
packages/rest/vitest.config.tsopts the suite down」. Measured before choosing:'silent'takes frames 2,095 → 0, and also reddens 28 assertions across 15 files and — the part that decides it — turns 8 files' 「an expected 4xx logs NOTHING」 pins vacuous, green even if every expected 4xx were logged loudly. ⭐ That is the shape by which this card's own[Registry]control was silently spent by #15425: a gate turned into a phantom by a legitimate-looking declaration.⇒ filed as #17865, bare (⛔
domain:*,typeand the grade are triage's), carrying the measurement, the four options and the four-axis block with its 维护者速读. ⛔ This PR is not held for that answer — 「⛔ 永不整席等维护者答复」 — and the seat ⛔ does not rule it itself, because A is inconsistent with the ruling's own sentence and that is the maintainer's to revise.Out-of-scope findings — accepted, and one of them is this seat's
Two are noted, not filed with their carriers named: the stale present-tense 「528 residual
[Registry]lines」 comment invitest.config.tsand the same number in the sibling gate's header (one owner, both lines, already raised at5550786137); and the second spelling of the brace-matching / env-block reader shared by the two gate scripts (carrier: whoever adds the third seam of this shape).⚠️ The third is the seat's own and is not merely noted. The dispatch template prescribes a commit trailer carrying a model identifier, and this repo's pre-pushcheck:commit-card-trailersmechanically refuses it, requiring the model-free pair. The dev resolved it in the repo's favour and reported it instead of amending silently, which is the right call twice over — the repo's gate governs pushed artefacts, and a model identifier does not belong in one. ⇒ the template is corrected at source by this seat; ⛔ not left for every dev to rediscover.落地前检
- ① review of record on file, in the measured shape (heading · head sha code span ·
Reviewed-by:) ✅ - ②
--pair 17863→ 0 ✅ - ③ every check green on this head — not the required subset. 30
success+ 5skippedat 15:29Z withLint & Repo Gatesstill running; ⛔ the flip waits for it.
Next: ready for review → merge queue; landing record on merge.
Generated by Claude Code
- The seat's level reading was wrong. At
Landed —
3a5eaea544630e9b2475aec2e2746705440270adRelease: session
session_01TSf4DV7ziu4V5j73e46b7c· cause: work landed onmain· destination: closedcompleted(by the PR's ownFixes), assignee cleared andpm:dispatchedstripped in the same write.domain:cliexecution seat (#6024), 2026-09-12T16:46Z.PR #17863 merged 2026-09-12T16:44:57Z. Contract review of record
5646806136(PASS, default judgment tier,Independence: SELF-REVIEW); acceptance5646856066.Landing verified — two readings plus a control that can fail
reading result git rev-list --parents -n 1 3a5eaea52 fields ( 3a5eaea5 7cab0d87) ⇒ single-parent squashgit merge-base --is-ancestor 3a5eaea5 origin/mainexit 0; origin/mainis that shanegative control the same test on the pre-merge head 7e6e9e044→ exit 1, andgit cat-file -t 7e6e9e044=commit⇒ a real object, and the test can return falsesubject feat(rest): a declared OS_REST_LOG fault-log level seam, shipped default unchanged (#17863)⚠️ The queue branch head became the merge commit again (gh-readonly-queue/main/pr-17863-7cab0d87was at3a5eaea5before the merge) — second observation, ⛔ still a reading of these two merges rather than a rule.Reading 2 — the content is on
main, with both controls- The seam is there and gated:
REST_LOG_LEVELS(:58) ·RestLogLevel(:61) ·REST_LOG_DEFAULT_LEVEL(:69) ·restLogLevel()(:89), and both sites now test the ladder before speaking while theconsole?.error(...args)call itself is unchanged. scripts/check-rest-log-declared.mjsis onmainand wired at both ends — 1 hit each inpackage.jsonand.github/workflows/lint.yml.packages/rest/README.mdcarriesOS_REST_LOG(2 hits) ⇒ the operator-facing half shipped with it.- ⭐ The ruling's own housekeeping instruction is executed: the audit's 「No seam was added … which of the two candidate seams (if either) to build is triage's call」 now reads 「the reservation it held is now …」 and names decision batch Add granular query operation capabilities to driver schema #49, item 2 at line 425. That sentence was a work item, and it is discharged on the merged ref rather than promised.
- Fabricated control:
OS_REST_LOG_XYZ→ 0. Live control on the same path:logError→ 3.
What this card changed, in one line for the next reader
packages/rest's fault logging now has a declared, gate-read level seam (OS_REST_LOG), and the shipped default is byte-for-byte what it was — an unset or unrecognised value resolves to'info', so a typo cannot silence the package, andscripts/check-rest-log-declared.mjsfails a shipped default oferrororsilent. ⛔ The card bought a declaration and its enforcement, ⛔ not a quieter product: the 2,095 stack frames per suite run are unchanged.⚠️ The one ruled row that did NOT ship, and where it lives nowDecision batch #49's execution list says 「
packages/rest/vitest.config.tsopts the suite down」. Measured before choosing:'silent'takes frames 2,095 → 0 and also reddens 28 assertions across 15 files, and — the part that decided it — turns 8 files' 「an expected 4xx logs NOTHING」 pins vacuous, green even if every expected 4xx were logged loudly. ⇒ filed as #17865 (bare; ⛔domain:*,typeand the grade are triage's) with the measurement, options A/B/C/D and the four-axis block. ⛔ This landing does not answer it and ⛔ nothing is held for it.Unblocked by this landing: nothing was fenced behind it —
packages/restwas held by no open PR when this was dispatched (5646265322).
Generated by Claude Code
- The seam is there and gated:
- added 2 commits that reference this issue
on Sep 17, 2026
Blocked-by: #14366— ⛔ STRUCK by thedomain:cliexecution seat 2026-09-11 (R73):#14366returns HTTP 404 on both/issuesand/pullsand cannot be resolved; and the line’s own release condition is MET anyway — PR #15673 MERGED 2026-09-05T22:13:07Z. This card is NOT blocked. See the seat comment below. — original text: Blocked-by: #14366 — its PR #15673 holdspackages/rest/src/rest-server.ts(41.7% of this card's population); released on MERGE, then the whole card lands in one round (maintainer ruling A, batch #49).Split out of #15426 by the
domain:cliexecution PM seat after that card's premise was measured false. ⛔ Ungraded and unrouted on purpose — grading and routing are triage's.What was measured
On a full green
packages/restrun (Test Files 178 passed (178) / Tests 3023 passed (3023)), output captured to a file and counted with stated patterns:atstack-frame linesat file://frames[sql-driver] DATABASE_ERRORlines[Registry]Frame attribution, by the header line preceding each block:
packages/rest/src/error-response.ts:1960rest-server.tsshare errorspackages/rest/src/error-response.ts:2375[REST]⇒ 1,071 frames (55.7%) originate in
error-response.tsalone.The mechanism
packages/rest/src/log.ts'slogErrorpassesErrorobjects toconsole.error, and Node formats anErrorargument with its full stack. ⇒ Every fault the REST layer reports at these sites prints its frames.Clause-②declaration, and the honest starting answer isyesuntil measured otherwise.Why it is filed rather than fixed
Two independent reasons, both measured:
packages/rest/src/error-response.ts, which is held by open PR fix(rest): the generic declared-status passthrough names its object on both error doors #15452 (card rest: the GENERIC declared-status passthrough still disagrees onobjectbetween the two error doors — plus one bespoke arm (RECORD_NOT_FOUND) still reachable from one door only #14725, parked awaiting contract review and unable to move). Hot-file serial: same file is a hard serial, released on MERGE.What is NOT this card
⛔
[sql-driver] DATABASE_ERROR— measured disjoint from this population. Those 310 lines come frompackages/drivers/driver-sql/src/sql-driver.ts(SqlDriver.backendStatementFault) viathis.logger.warn(string), are handed one string, and carry no stack in any encoding. That was #15426's subject and its own premise; see #15426's closing comment.Re-check
DATABASE_ERRORgrew only +2.0% — ⇒ the growth is in this population, not the driver's. ⛔ Re-derive rather than quoting these.Refs: #15426 (closed, premise falsified — the measurement lives there) · #13517 (the origin card for console volume) · #13986 (the structured-logger half) · PR #15452 (the hot-file hold on
error-response.ts).Generated by Claude Code