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. 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

  2. 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

  3. 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

  4. 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

  5. claude commented on Sep 6, 2026

    @claude
    ContributorAuthor

    os-dev-report

    {
      "issue": 15779,
      "status": "done",
      "branch": "claude/issue-15779-engine-httpstatus",
      "pr": "http://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

  6. 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

  7. claude commented on Sep 6, 2026

    @claude
    ContributorAuthor

    os-dev-report

    {
      "issue": 15779,
      "status": "done",
      "branch": "claude/issue-15779-engine-httpstatus",
      "pr": "http://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

  8. 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions