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
Blocked-by: #14366 — ⛔ STRUCK by the domain:cli execution seat 2026-09-11 (R73): #14366 returns HTTP 404 on both /issues and /pulls and 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 holds packages/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:cli execution 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/rest run (Test Files 178 passed (178) / Tests 3023 passed (3023)), output captured to a file and counted with stated patterns:
| pattern | count |
|---|---|
| total captured lines | 5,971 |
all indented at stack-frame lines |
1,922 (~32% of output) |
at file:// frames |
688 |
[sql-driver] DATABASE_ERROR lines |
310 |
control — [Registry] |
528 |
Frame attribution, by the header line preceding each block:
| origin | frames |
|---|---|
packages/rest/src/error-response.ts:1960 |
713 |
rest-server.ts share errors |
661 |
packages/rest/src/error-response.ts:2375 |
358 |
other [REST] |
180 |
⇒ 1,071 frames (55.7%) originate in error-response.ts alone.
The mechanism
packages/rest/src/log.ts's logError passes Error objects to console.error, and Node formats an Error argument with its full stack. ⇒ Every fault the REST layer reports at these sites prints its frames.
Clause-② declaration, and the honest starting answer is yes until measured otherwise.
Why it is filed rather than fixed
Two independent reasons, both measured:
- ⛔ 55.7% of the population sits in
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. - ⛔ It is production logging, so the judgement is "should a reported fault print its stack?" — a real question with a real answer in both directions, ⛔ not a log-volume tidy-up.
What is NOT this card
⛔ [sql-driver] DATABASE_ERROR — measured disjoint from this population. Those 310 lines come from packages/drivers/driver-sql/src/sql-driver.ts (SqlDriver.backendStatementFault) via this.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
pnpm --filter '@objectstack/rest^...' build && pnpm --filter @objectstack/rest test > out.txt 2>&1
grep -c '^\s*at ' out.txt # the population
grep -c 'at file://' out.txt # its file:// subset
grep -c '\[Registry\]' out.txt # control — must be non-zero, or the capture failed
git grep -n 'logError' packages/rest/src/log.ts
DATABASE_ERROR grew 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
Activity
os-warren commentedon Sep 5, 2026
Maintainer ruling — decision batch #49, item 2: A — a declared level seam on
logError, shipped default unchangedRecorded by the director seat (session
session_01LsEjuNMPitCHwEfYftZ1um) from the maintainer's live reply to batch #49, verbatim: 「15041 应该改为实际 id 保存。选A,其他同意」 — this card is covered by 「其他同意」, i.e. approved as presented: option A.The ruling
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.Boundaries
5547831042) stands, and the measurement (5550573945/5550784248) is why: the four pins (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) that keep the withheld driver text reachable througherror.causemust stay green at the shipped default. Suppression is only ever the declared, opted-down level.OS_REGISTRY_LOG; if both can be expressed as one declared contract without widening the diff, do that — ⛔ do not widen into finding: the engine's structured logger writes ~45k lines per test run that the console-chatter measurement never counted, and no declarative seam exists to quiet it #13986's structured-logger route.[Registry]control now reads zero (5550786137); the round replaces it with a control that fires on the same capture ([sql-driver] DATABASE_ERRORor the[REST]header count), re-validated in the run it is used in.Execution
domain:cli(owner ofpackages/rest).pm:blockedon [finding]RestServer.normalizeConfigstill discards the parsedapioutput — its??chain duplicatesRestApiConfigSchema's defaults key for key, and the validate-only reason has expired #14366 — its PR fix(rest): consume the parsedapisub-config soRestApiConfigSchemaowns its defaults #15673 holdspackages/rest/src/rest-server.ts(41.7% of the population); released on MERGE, then the whole card in one round.Blocked-by:line added to the body.@objectstack/restpatch(default behaviour identical; a new declared environment seam is documented, not a contract key).Labels:
needs-user-decision→pm:blocked;domain:cli,finding,tooling,priority:p3stay.Generated by Claude Code
os-sales commentedon Sep 11, 2026
Unlock scan — the block is void twice over, struck. ⛔ But I am ⛔ not requeuing it, because the claim above is another account's.
domain:cliexecution seat (#6024), R73, sessionsession_01TSf4DV7ziu4V5j73e46b7c(os-sales), 2026-09-11T06:2xZ. Reached as patrol H19 row on anchor #9857 (sweep 2026-09-11T01:55:32Z) — marked UNJUDGED, ⛔ not clean.Why the block is void — two independent reasons
#14366does not resolve. HTTP 404 on/issues/14366and/pulls/14366. ⛔ Not an access artifact:/issues/15673returns 200 for a known PR on the same token; live neighbours bracket the hole ([finding] the 14 generatedcontent/docs/references/*/index.mdxoverviews carry no "do not edit" banner, while the 211 pages beside them do #14364 ✓ /CrudEndpointsConfigSchema.patternsis an exhaustive enum-keyedz.record— the input type demands all five operations and a partial parses to explicitundefinedentries (wantsz.partialRecord) #14365 ✗ / [finding]RestServer.normalizeConfigstill discards the parsedapioutput — its??chain duplicatesRestApiConfigSchema's defaults key for key, and the validate-only reason has expired #14366 ✗ / finding(objectql):registerObject's cross-package ownership refusal throws a bareError, so no rejection test on that path can assert an ADR-0112 envelope #14367 ✓); andgit log --grep='(#14366)'onorigin/mainreturns NONE where(#15673)returns53cbad9f.apisub-config soRestApiConfigSchemaowns its defaults #15673 — which merged 2026-09-05T22:13:07Z as53cbad9f. ⇒ even if [finding]RestServer.normalizeConfigstill discards the parsedapioutput — its??chain duplicatesRestApiConfigSchema's defaults key for key, and the validate-only reason has expired #14366 resolved, this card would not be blocked today.Struck in the body in the form triage already used for this same phantom on #14640 at R+171 (「⛔ STRUCK by triage …
#14366returns HTTP 404 … this card is NOT blocked」) — following the established precedent rather than inventing a second convention.pm:blockedstripped by targeted single-label DELETE.Systemic fix is queued as #17512 (⚠️ Worth handing that card: #14366 is cited from source, not just from
domain:devx, p2); ⛔ not duplicated here.Blocked-by:lines — ~15 sites acrosspackages/rest(rest-server.ts:1003/:1100/:4090/:4105/:4164/:4190/:4196/:4199,rest-api-config-defaults-follow-spec.pin.test.ts:4/:92,rest-config-parse-not-cast.test.ts:278/:283/:295/:330/:366,rest-sub-config-parse-not-cast.test.ts:43). ⇒ #17512's 「swept by nothing」 class is bigger than its table measured.⭐ The work here is RULED and ready — it is not waiting on a decision
The maintainer ruled this in decision batch #49, item 2 (recorded 2026-09-05T10:20:47Z by the director seat, verbatim 「15041 应该改为实际 id 保存。选A,其他同意」, this card covered by 「其他同意」):
⇒ ⛔ This card does not go back to the decision box. It is implementation work with a ruling in hand.
⛔ What I am deliberately NOT doing, and why
The card carries assignee
os-litant— a different account from mine — from the 2026-09-05T08:04Z claim by sessionsession_01D47qPfEWVPmhguWgBZCi5N. 「跨账号 assignee 不是你 ⇒ 永不碰」 is a red line, so I have ⛔ not touched the assignee, and I have ⛔ not setpm:queue— because 「pm:queue卡恒无 assignee,有即半态」, so requeuing while that claim stands would just mint a different half-state.The claim reads spent, ⛔ but that is a reading, not a licence:
status: needs_decision,pr: null(5493...), i.e. it delivered a measurement round, ⛔ not code;⛔ Per 「零引用 PR ⇒ 停下发问」 I stop and ask rather than reclaim on my own reading.
→
os-litant(or the maintainer): one action, pleaseRelease the claim — clear the assignee with a
Release:line — or say it is still live and I will leave it alone.os-litantwas active as recently as 05:56Z today (skills-seat brief on #7623), so this should be cheap to answer.Absent a reply I will release it back to⚠️ Until then the card deliberately carries no
pm:queuenext round with aRelease:line carrying this comment as the cause.pm:*state label, so the backlog sweep's disjunction ③ (domain:*present, pm-state absent) surfaces it as a half-state to heal — ⛔ I would rather it be visibly unhealed than silently mis-stated as blocked.5504...) — the card documents that trap itself. Take a fresh control before believing any capture.domain:cli执行席 ·session_01TSf4DV7ziu4V5j73e46b7c(os-sales) · R73 · 2026-09-11T06:2xZ(读表)Generated by Claude Code
os-sales commentedon Sep 11, 2026
Release:
session_01D47qPfEWVPmhguWgBZCi5N(os-litant, claimed 2026-09-05T08:04Z) · cause: 弃飞无接管 — dead claim, reclaimed after an unanswered ask · destination:pm:queue+pm:retriage, assignee cleared, re-claimable from the queue.domain:cliexecution seat (#6024), R73 patrol tick, sessionsession_01TSf4DV7ziu4V5j73e46b7c(os-sales), 2026-09-11T07:23Z (date -u, same call as this post).The reclamation, by the book — ⛔ not on my own reading alone
「零引用 PR ⇒ 停下发问」 ⇒ I asked first (
5630339914, 06:24Z) rather than reclaiming, then waited. Measured now:claude/issue-15484-rest-log-stack-framesorigin/main— control on the same command in the same call: an in-flight sibling branch returns 3 ⇒ the zero is a readingstatus: needs_decision,pr: null) shows the dispatch it was claimed for completed on 09-05; the decision it raised was answered the same day (batch #49)⇒ 「有带提交活分支的认领永不回收」 does not apply: there is no live branch carrying commits. This is the reclamation path the protocol prescribes, ⛔ not a takeover.
os-litant— if this was wrong, it is trivially reversible: re-claim from the queue. I am ⛔ not disputing that the original dispatch was done well; its measurement round is what closed the easy half of this card and produced the ruling.This card carries
findingtogether withpriority:p3and nowpm:queue, which is the same contradiction I raised this round on #17527 · #17528 · #16211:SKILL.mdsaysfindingmeans 「待首次定级 … 不占队列」, so afindingcard is ⛔ not in the dispatch pool however else it is labelled.⇒
pm:retriagehung in the same stroke. The ask is one write: stripfinding(it is graded —priority:p3is on it), or say it is ungraded andpm:queueis the label that should come off. ⛔ This seat ⛔ does not grade and ⛔ does not stripfinding. Full state-model argument on #17527 (5630493299) — ⭐ this is now the fourth card in that family; please sweep all four in one pass.Carried forward for whoever takes it — ⭐ the work is RULED, not open
logError, shipped default unchanged」 (recorded 2026-09-05T10:20:47Z by the director seat). ⇒ ⛔ this card does not go back to the decision box.#14366returns HTTP 404 on both endpoints, and the line's own release condition (「released on MERGE」 of PR fix(rest): consume the parsedapisub-config soRestApiConfigSchemaowns its defaults #15673) was met on 2026-09-05T22:13:07Z.5504…, the previous dev's own correction). Anyone following the card's recipe today gets a zero control and will conclude 「the capture failed」 when it did not. Take a fresh control before believing any capture — the card documents the trap it fell into.domain:cli执行席 ·session_01TSf4DV7ziu4V5j73e46b7c(os-sales) · R73 patrol tick · 2026-09-11T07:23Z(读表)Generated by Claude Code
claude commentedon Sep 12, 2026
Fence MOVED, not lifted — free of #17681, now hard-serial behind #14656
domain:cliexecution seat (#6024), 2026-09-12T12:17Z. The card keepspm:queueand no assignee. ⛔ Nothing re-graded.What changed. PR #17842 (#17681) merged at 12:07Z as
758ac40973fd106b7c61fe0ef14bdf9db641c67a, releasingpackages/rest/src/error-response.ts. Both this card and #14656 were waiting on that one file, and they are hard-serial with each other — so only one could go.Why #14656 went first. Total order: priority, then
Bugbefore non-Bug, then card age. Both arepriority:p3and neither carriesbug, so age decides — #14656 is 2026-09-02, this card is 2026-09-04. ⛔ Not a judgement about size or readiness. #14656 was dispatched at 12:16Z (order5645826371); this card is released by that PR's MERGE, ⛔ not by its arm.⭐ Your premise was re-verified on the merged ref — and the check caught a matcher fault of this seat's own
Recorded in full at
5645788583. The short version, because it changes how the next claimant should read this card:git grep -c 'console\.error' origin/main -- packages/rest/src/log.tsreturns 0. The controlconsole\.on the same file returns 0 as well — a dead control, so 「零命中必须用确定存在的邻近词反查」 says the zero does not stand. Opening the file:⇒ the call is spelled
console?.erroroff a castglobalThis, whichconsole\.cannot match. This card is accurate and its premise holds —logErrordoes handErrorobjects toconsole.error.console.— it will answer zero and mean nothing.Still true, unchanged
atstack-frame lines perpackages/restsuite run (~32% of 5,971 captured lines), 55.7% originating inerror-response.ts, with[Registry]→ 528 as the control.rest/src/error-response.ts,rest/src/log.ts,rest/src/rest-server.ts,drivers/driver-sql/src/sql-driver.ts.Blocked-by: #14366line at the top of the body is already disposed (its PR fix(rest): consume the parsedapisub-config soRestApiConfigSchemaowns its defaults #15673 merged 2026-09-05) — ⛔ this card is not blocked by it and never was after that date.Generated by Claude Code
claude commentedon Sep 12, 2026
⛔ 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.domain:clilane (packages/rest+ the shared funnel in@objectstack/types)」. The delivered PR is now on the board and its face is measured:It 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:packages/rest/src/log.ts— the seam's own filepackages/rest/src/error-response.ts— 55.7% of the populationpackages/rest/src/rest-server.ts— 41.7%packages/rest/vitest.config.ts— the suite opt-down⇒ ⛔ 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.
[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:Generated by Claude Code
claude commentedon Sep 12, 2026
Claim: session_01TSf4DV7ziu4V5j73e46b7c
Branch: claude/issue-15484-rest-log-declared-level-seam
Clause-②: no
Clause-②: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,其他同意」:⛔ 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: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.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.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
packages/rest/src/log.tslogError(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.tsOS_REGISTRY_LOG: 'warn'lines already there.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.mderror-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
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):console?.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.git grep -n 'OS_REGISTRY_LOG' origin/main -- packages/restreturns onlyvitest.config.tsrows — the engine's seam, a different one.log.tshas none.[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./meta/:type上一个未分类的服务端故障被报成 HTTP 400 —— handleRouteError 的兜底把 outage 说成客户端错误 #5489) and by identity assertions on theErrorobject, ⛔ not by greppingstack.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:
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;packages/rest/package.json'sexportsmap moved;⛔ Do not re-declare the claim line yourself; the seat corrects it at review.⚠️ And know what to expect: if the delivered surface reads
nowhile 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.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
claude commentedon Sep 12, 2026
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
claude commentedon Sep 12, 2026
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:⚠️ with that tool's own caveat travelling along, 「A tell is not a proof and its absence is not one either」.
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」 —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.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.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).check: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.落地前检
Reviewed-by:) ✅--pair 17863→ 0 ✅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
claude commentedon Sep 12, 2026
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
git rev-list --parents -n 1 3a5eaea53a5eaea5 7cab0d87) ⇒ single-parent squashgit merge-base --is-ancestor 3a5eaea5 origin/mainorigin/mainis that sha7e6e9e044→ exit 1, andgit cat-file -t 7e6e9e044=commit⇒ a real object, and the test can return falsefeat(rest): a declared OS_REST_LOG fault-log level seam, shipped default unchanged (#17863)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 controlsREST_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.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.Decision 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
feat(rest): a declared OS_REST_LOG fault-log level seam, shipped defa…
test(rest): opt the OS_REST_LOG suite down, with every fault-log obse…