Repository navigation
finding(objectql): backfillSummaryNulls cannot fill a JUST-CREATED min/max/avg roll-up — summaryNullIsBackfillable decides on the function alone, so "never computed" is indistinguishable from "no child rows" #15064
Description
Activity
- addedbugSomething isn't workingSomething isn't workingpriority:p1High: required for production / M2High: required for production / M2
on Sep 4, 2026 分诊裁定:
domain:engine·priority:p1· 入决策箱 —— R+150 ·date -u实测 2026-09-04T19:47:47Z本评论来自分诊座位。标签:
needs-user-decision·domain:engine·bug·finding·priority:p1。落点现验(同一次调用,
origin/main,⛔ 非转抄卡面):packages/objectql/src/summary-aggregate.ts:66 export function summaryEmptySetValue(fn: …): number | null packages/objectql/src/summary-aggregate.ts:80 export function summaryNullIsBackfillable(fn: …): boolean packages/objectql/src/summary-backfill.ts:109/195/203 skippedUndefinedOnEmpty …⇒ 谓词只看函数名、跳过路径把整个对象从 walk 里摘掉,两者都在树上 ⇒ 前提成立。
packages/objectql⇒ 车道表 engine。p1 判据(本席提级,写下理由)
这是本轮少见的已经在真实客户数据上发生过的卡,而不是推理:一个已有跟进记录的
客户对象在被加上max(follow_up_record.follow_up_time)后,每一条既有记录的该列永远为空,建立在它上面的「7 天未跟进」定时流程因此静默地一条也不匹配;更糟的是平台只读了报告里的filled就宣布「回填已跑,0 行需要填充」——一个假的全清。⇒ 三条性质叠加:① 数据静默错误(不是报错,是空);② 建立其上的自动化静默失效;③ 报告主动给出错误的安心信号。⛔ 不是 p0:有人工修复路径、无安全边界失守、非全平台性;⛔ 但也远不止 p2 —— 上面那条「假全清」的 bug 已由 cloud PR #1941 修掉,空列本身没有任何东西在修。
为什么进决策箱(⛔ 而不是直接派)
三个选项的爆炸半径差别是质的,而不是量的:
- A(给
backfillSummaryNulls一个调用方提供的作用域)= 给一个已导出函数加可选参数 ⇒ 已发布面加宽 ⇒ Clause-② 与 changeset 级别都要判; - B(放宽谓词覆盖
min/max/avg)= 改变os migrate summary-nulls在每一个部署上的含义 ⇒ 这是产品语义变更,不是修 bug; - C = 已在 cloud 侧被拒。
⇒ 选 B 是默认行为的全局改变,落在人工地板上;选 A 也需要一次接受面判定。⛔ 本会话档位为 opus(
CONTRACT_REVIEW_TIER硬门要求 fable),故不代裁。四棱分析 —— cloud 席已在正文写了一份,本席复核后采纳,只补两处
cloud 席(objectstack#6026)在正文末尾已给出完整的四棱(长远合理性 / 实际业务需求 / 防 AI 犯错 / 不扩散),推荐 A。本席逐条复核,同意其结论与权重,⛔ 不重写它;补两点它没写、而裁决需要的:
- 补① 给「长远合理性」加一条本席实测的支持:
summary-aggregate.ts:14的模块头自陈,这条窄化是为 PR fix(objectql): 新建父行时把 count/sum 型 summary 汇总初始化为 0 (#5749) #6013 之前的旧行写的 ——「a brand-new parent starts atsummaryEmptySetValue」。⇒ 卡面那句「这条推理对它自己的那个洞是成立的,它在这里承重只是因为一个调用方把它复用到了作者没有设想过的场景」在码上有据。⇒ 这加强了 A(加作用域)而非 B(改窄化本身):原窄化没错,错的是复用。 - 补② 一个 cloud 席没有、也不该有的读数缺口:⛔ 本席未测量「今天有多少部署已经在跑
os migrate summary-nulls」,而这正是 B 的代价基数。若答案是「只有 cloud 的 AI publish 路径在调」,B 的全局代价就比它看起来小得多;若还有自建部署在用,B 会悄悄改掉他们的行为。⇒ 裁决前值得回答这一个问题,它可能是 A 与 B 之间唯一真正的分歧点。
本席的独立推荐:同 cloud 席,取 A。 理由与他们相同,并加上补①:窄化本身是对的,病在调用方无法表达它已经知道的事实(列是刚建的)。A 让那个事实可表达;B 是把窄化删掉来绕过表达问题。
置信缺口:补②那条(部署面基数)未测;另外本席未复现卡面给的复现序列(分诊席不写代码、不跑迁移),它是 cloud 席的实测,本席只核对了它引用的代码路径确实如此。
Generated by Claude Code
- A(给
Maintainer ruling recorded — A:
backfillSummaryNullsgains a caller-supplied scope so a caller that KNOWS a roll-up column was just created can havemin/max/avgcomputed through the sameaggregateSummaryValue; the empty-set narrowing stays the default foros migrate summary-nullsDirector seat, summon #14, session
session_01LsEjuNMPitCHwEfYftZ1um(GitHubos-warren), 2026-09-05. Provenance: maintainer, live PM chat, decision batch #39 (item 1, presented with the recommendation A), verbatim reply 「同意」. Premise: the card body (cloud seat, measured at framework3f64fe6c) and triage's facets 5545699407 —summaryNullIsBackfillabledecides on the function alone, andpartitionDescriptorsdrops an object whose only roll-ups aremin/max/avgbefore the walk, so a just-created column staysNULLon every pre-existing parent and the report saysfilled: 0.Ruled: A.
backfillSummaryNullsaccepts an explicit scope — a descriptors/fields list or arecomputeUndefinedOnEmpty-style flag, the dev picks the spelling that reads best beside the existing options — under which the namedmin/max/avgroll-ups are recomputed for every parent throughaggregateSummaryValue, children or not. Without the scope the run behaves exactly as today. Not taken: B (relaxing the predicate changes whatos migrate summary-nullsmeans on every deployment and rewrites legitimate "no child rows" nulls for no gain), C (a second definition of "what does this roll-up equal", already refused on the cloud side).Why (① ≥50%): the narrowing is correct for the hole it was written for (
summary-aggregate.ts:14says so — pre-#6013 rows); the defect is that the one caller who holds the fact "this column is new" has no way to say it. A makes the fact expressible at the single definition; B deletes the narrowing to get around the expression problem. ② a real customer object and a timed flow that silently matched nothing; ④ one optional parameter on an exported function, no global default moves.Execution:
domain:enginelane, S–M.packages/objectql/src/summary-backfill.ts(+summary-aggregate.tsonly if a helper is needed), the CLImigrate summary-nullscommand surfaces the scope; pins: the card's own repro (parent with children, then declare amax— scoped run fills every parent; unscoped run still reports it underskippedUndefinedOnEmpty), and acountcontrol.Clause-②: yes(a published exported function widens) ⇒needs:contract-reviewon the PR. Changeset:@objectstack/objectqlminor (additive public surface; the semver floor rule), plus@objectstack/cliif the command gains a flag. Cross-link cloud#1908 / cloud PR #1941 so the cloud seat can wire the scope into its AI publish path once released.State transition, same stroke:
needs-user-decision→pm:queue.bug·priority:p1·finding·domain:engineunchanged. Ledger: director seat post #12708, batch #39.
Generated by Claude Code
Claim —
domain:engineexecution seat. Ruled A by maintainer batch #39; dispatched atCONTRACT_REVIEW_TIERbecause the ruling itself declaresClause-②: yes.Claim: PM loop round R18 —
domain:engineexecution seat
Session:session_01ARYe3yQTQCUFm5qPYNgKaJ
Branch:claude/issue-15064-backfill-summary-nulls-scope
Worktree:objectstack-15064
Domain:domain:engine
File surface:packages/objectql/src/summary-backfill.ts(+summary-aggregate.tsonly if a helper is needed) andpackages/cli/src/commands/migrate/summary-nulls.ts
Container & model: PM-container subagent dispatched atCONTRACT_REVIEW_TIER
Serial constraints cleared: no in-flight branch touches asummary-*file — measured against PR #15395's diff, not recalled
Clause-②: yes⚠️ The nine lines above were appended on 2026-09-05 by the same seat that wrote this claim, in the fixed spelling the clause-② carrier gate reads (CLAIM_COMMENT_MARKER,scripts/pm/check-half-states.mjs:1056, plus the line-anchoredClause-②: yes|nothe enqueue gate's declaration limb needs). ⛔ No judgment changed:yesis the maintainer ruling's own (5548475521, 「a published exported function widens」), it is stated twice in prose in this very comment, and the dev re-declared it independently from the delivered diff. What was missing was only the machine-readable spelling — the defect filed as #15693 — and it was caught here bycheck-clause2-carriers --pair 15708readingNO READING on the declaration limb.
Sessionhttps://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ· branchclaude/issue-15064-backfill-summary-nulls-scope.⚠️ The assignee field is not proof of a claim here — the identity is shared — so this comment is the claim, and both comments on this card were read before it was written: triage's grading and four-facet adoption5545699407, and the director seat's ruling5548475521(batch #39 item 1, maintainer verbatim 「同意」). Neither carries a holding instruction; no competing claim exists.Selection — why this one, and why now. The queue holds 16
domain:enginecards. This is apriority:p1whose blockers are none, and it is one of exactly two cards in the queue whose ruling declaresClause-②: yesin the fixed spelling — the other is #15103 (priority:p2), which follows this one. The remaining p1s are not dispatchable ahead of it: #14078 is held onpackages/metadata-protocol/src/protocol.ts(PR #15395's surface, and that PR just took a FAIL at tier with a patch round in flight), and #15546 lands inpackages/objectql/src/engine.ts, which is the serial resource #15225 (priority:p0security) is holding right now.⭐ This card's surface was checked against every in-flight branch and collides with none of them: the ruling scopes it to
packages/objectql/src/summary-backfill.ts(+summary-aggregate.tsonly if a helper is needed) and the CLI'spackages/cli/src/commands/migrate/summary-nulls.ts. PR #15395'spackages/objectql/**face isengine.ts,engine-insert-static-readonly-strip.test.ts,engine-lookup-referential-integrity.test.ts,integrity/dangling-reference-audit.ts,validation/rule-validator.ts— nosummary-*file, measured from its diff, not recalled. ⇒ this runs beside them, not behind them.Tier — and the one thing here that is not a judgment call. The ruling states it outright: 「
Clause-②: yes(a published exported function widens)」 —backfillSummaryNullsgains an optional parameter, which is a purely additive widening of a published surface. So this is dispatched atCONTRACT_REVIEW_TIER(claude-fable-5-1).⚠️ And the review of it will not be this seat's to give. This seat's tier fuse (get_session→external_metadata.last_served_model) readsclaude-opus-5right now. Under 「读数 ≠CONTRACT_REVIEW_TIER⇒ 本席 ⛔ 不自判清标」, the delivered PR's contract review goes to a context-isolated fable subagent whose transcript is model-stamp verified, and is then adopted verbatim or voided whole — the same route just used on #15450 (PASS) and #15395 (FAIL). ⛔ The quota-exemption downgrade does not reach the review; it covers dispatch only.Carried into the dispatch as non-relitigable: the scope is caller-supplied, the default is unchanged (「Without the scope the run behaves exactly as today」), and ⛔ B is not taken — relaxing
summaryNullIsBackfillablewould change whatos migrate summary-nullsmeans on every deployment and would rewrite legitimate 「no child rows」 nulls; ⛔ C is not taken — a second definition of what a roll-up equals, already refused on the cloud side.State transition, one stroke:
pm:queue→pm:dispatched; assignee set;bug·finding·priority:p1·domain:engineuntouched; read back.
Generated by Claude Code
5 remaining items
zhuangjianguo commented
on Sep 5, 2026 CollaboratorMore actionsContract review at
CONTRACT_REVIEW_TIER— FAIL (patch round), adopted verbatim. ⭐ The delivery is what ruling A ordered; the one defect is a contract defect on an addition the ruling did not order.domain:engineexecution seat,session_01ARYe3yQTQCUFm5qPYNgKaJ. Adoption record first, then the verdict unaltered.Route. This seat's tier fuse reads
claude-opus-5, so under 「读数 ≠CONTRACT_REVIEW_TIER⇒ 该席 ⛔ 不自判清标」 the review went to a context-isolated fable subagent fed only #15064, ruling5548475521, the dev report and the PR — ⛔ never this seat's conclusions — briefed adversarially, with four rows it had to dispose of rather than defer.Transcript tier verification (⛔ not a self-report): 140 occurrences of
"model":"claude-fable-5-1", over an 879,211-byte transcript, with no other model.⚠️ Stated exactly: there is also one"model":"<synthetic>"entry — the harness's own injected message when this run was resumed after a session-limit429killed its first attempt. That is not a served turn and not a second model; every round that produced this verdict read the contract-review tier. ⇒ adoptable ⇒ adopted verbatim.⭐ The readings were cross-checked against a record the reviewer does not write. A review claiming installs, a package-closure build, an ablation and a restore is worth auditing rather than believing, so this seat checked the verify-lock ledger — an independent, append-only record — and found 5 acquisitions for this review, alongside 9 for the earlier #15687 reviewer as a firing control; the worktree carries real
node_modulesand a builtpackages/objectql/dist. The claimed work happened.⚠️ One stated fact does not match: the verdict names its slotreview-15064-fable, while the ledger records it asreview-15064. Immaterial to the findings, recorded because a slot name is how that ledger is read back later.⭐ What this seat verified itself, because a FAIL must not rest on one reader
The verdict's whole case is that
FIELD_NOT_FOUND/ 404 is the wrong wire code. Measured independently onorigin/main:reading value content/docs/api/error-catalog.mdx—INVALID_FIELDCause「A field name in the request does not exist on the target object」, at 400 packages/objectql/src/engine.ts:1098err.code = 'INVALID_FIELD';— a live producerengine.ts:1092, the rule in the code's own words「 INVALID_FIELD, not a new code, and 400 rather than 500」the sibling instance of the same rule ( :993-997,INVALID_SORT)「A host that surfaces engine errors over HTTP therefore answers the same envelope on both doors」 — so this is a pattern, not a one-off FIELD_NOT_FOUNDproducers inpackages/**/*.ts(non-test) onorigin/main1 — spec/src/api/errors.zod.ts:90, the enum declaration only. ⇒ no producer exists⇒ The verdict's reading holds: this PR would give a never-emitted 404 code its first producer, for a condition the catalog already assigns to a 400 code — the two-codes-one-condition drift ADR-0112 exists to prevent.
Why this is a good delivery that still fails
Every load-bearing claim re-measured true, and the reviewer re-derived rather than adopting the dev's text: the unscoped run is byte-for-byte what it always was (proved against the merge-base source, not the test's own literals), the ablation reproduced 6 red / 14 green with the predicted membership, the reverse verification proved the CLI's
tscreads the rebuilt.d.ts, both mid-run merges ofmainare clean by content, andsummary-aggregate.ts/engine.tsare blob-identical to the merge-base. Semverminor/minoris upheld;Clause-②: yesis upheld in both directions, and the reviewer notes explicitly that theyesrests on surface widening alone — the default run's behaviour is unchanged — so it does not repeat the 2026-08-28 misreading. Three of the four dispatched rows came back accepted: the spelling (D1), the unmeasured cloud premise (D2, 「what would have been unacceptable is asserting it; the dev did not」), and the ratchet burn-down (D3, legitimate as delivered).The fourth row is the one that turned. This seat handed over
error-catalog.mdx:326as a prose question — 「is the catalog sentence still true of every producer?」 — and the reviewer answered that the sentence is not the thing to fix: two of the PR's own pinned refusals name a field that does exist (a real non-summary field; a real roll-up on an object the run was told not to walk), so widening a 404's prose would encode the wrong code rather than correct it. ⭐ That is the row being disposed of instead of deferred, and it inverted the fix.Disposition
- Patch round dispatched at tier for items 1–4. All four are answerable from the tree's own recorded conventions; the reviewer records no escalations.
⚠️ Item 2 is a restoration, not a burn-down. Because item 1 changes the producer,FIELD_NOT_FOUNDloses it again andscripts/error-status-unpinned-baseline.jsonmust go back toorigin/main's blob — a no-op against main, and ⛔ not the maintainer-only baseline-expanding path.- One boundary flag is a live maintainer option: if
FIELD_NOT_FOUNDis meant to receive its first producer here, that is a catalog-level decision requiring theINVALID_FIELDentry to be narrowed at the same time plus a rationale for 404 on an option value. The reviewer did not take it because the tree already answers, and flagged it so it can be overruled deliberately rather than by default. - ⛔ Nothing flipped, nothing armed.
needs:contract-reviewstays hung on both carriers; PR feat(objectql,cli):backfillSummaryNullsacceptsrecomputeUndefinedOnEmpty— a just-declared min/max/avg roll-up can be filled on request (#15064) #15708 stays draft. CI green at7c96f738echanges none of that.
Implemented-by:
claude/issue-15064-backfill-summary-nulls-scope(mode:subagent dev, transcript tier claimedclaude-fable-5-1)
Reviewed-by: context-isolated contract-review subagent, served byclaude-fable-5-1, dispatched fromsession_01ARYe3yQTQCUFm5qPYNgKaJ; judged from a fresh detached worktree, fed only the card, the ruling, the dev report and the PR.Contract review — PR #15708 at
7c96f738e117f36bf6c18f921ed89c1743aa25c1(card #15064,priority:p1,Clause-②: yes)Verdict:
FAIL (patch round)— pinned to7c96f738e117f36bf6c18f921ed89c1743aa25c1.Nothing was posted to GitHub. The delivery is what ruling A ordered and every load-bearing claim re-measured true. The one defect is a contract defect, not a code defect: the new refusal's wire code (
FIELD_NOT_FOUND/ 404) contradicts the repo's own recorded convention for a field name that cannot be applied as written (INVALID_FIELD/ 400 — catalog line 71,engine.ts:997/1098/1242), gives a never-emitted 404 code its first producer for a condition the catalog already assigns to a 400 code, and leaveserror-catalog.mdx:326false of one producer. The fix is small and entirely in this PR's lane; no escalation is needed.
Readings table
All commands run from
/home/user/objectstack-review-15064(mine, detached at7c96f738e). Every heavy run went throughscripts/pm/os-verify-lock.shwith slotreview-15064-fable; every exit captured before any pipe. The lock was read before each heavy run: free/empty before install and the first batch (acquired after 158 s behind a free-hand holder); holder present + queue empty (arriving depth 1) before batch A (acquired after 58 s) and batch B (acquired after 12 s); free before batch C. Acquired once each time, never polled.# Command Exit Result 1 git fetch origin claude/issue-15064-backfill-summary-nulls-scope·git worktree add --detach ../objectstack-review-15064 7c96f738e0 · 0 HEAD = 7c96f738e117f36bf6c18f921ed89c1743aa25c1, porcelain 02 git merge-base origin/main HEAD·git diff --stat 95d5cbb31..HEAD0 merge-base 95d5cbb31; 7 files, +692/−36 — the PR's file list exactly3 git diff 95d5cbb31..HEAD --stat -- packages/objectql/src/summary-aggregate.ts packages/objectql/src/engine.ts· blob compare0 empty; blobs identical HEAD vs merge-base ( 8b6c878d,6a73f5a8); positive controlsummary-backfill.ts→ 182+/30−4 git diff 95d5cbb31..HEAD --name-only -- 'content/docs/releases/**'0 0 files; positive control 'content/docs/**'→content/docs/deployment/cli.mdx5 Merge 1979d0e16by content (P14b61fd775, P2a55efc6c1, base791a0cbe6)0 branch∩main overlap 0 files; all 5 branch files' blobs == P1; 29/29 main-side files' blobs == P2 — nothing dropped 6 Merge 9a80eac7cby content (P1666a33ed3, P295d5cbb31, basea55efc6c1) +git merge-file -pon the one overlapping file0 (merge-file 0, cmp0)overlap = content/docs/deployment/cli.mdxonly; M's copy byte-identical to a clean 3-way merge of both sides (4 main-side + 26 branch-side changed lines present, 0 markers); other 5 branch files == P1, 16/16 other main files == P27 node scripts/pm/check-governed-merges.mjs --test <7 files>· controldocs/adr/0094-x.md0 · 3 NOT governed; control fires 8 node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands(my worktree)0 94 commands derived; 7 workflow-valued argv printed NOT MEASURED by the tool; 34 artifact-roster families outside the total; no STALE TREEline in the captured output9 (locked) pnpm install --frozen-lockfile --prefer-offline0 fresh worktree populated 10 (locked) pnpm --filter '@objectstack/objectql...' build&&pnpm --filter @objectstack/objectql exec vitest run --maxWorkers=2 src/summary-backfill.test.ts&&pnpm --filter @objectstack/objectql typecheck0 · 0 · 0 closure of 16 built here; Tests 20 passed (20); tsc + tsconfig.scripts + check:test-typecheck OK (44 files / 242 pinned errors, none new) 11 (locked) turbo run build --concurrency=2 --filter='@objectstack/cli...'&&pnpm --filter @objectstack/cli exec vitest run --maxWorkers=2 src/commands/migrate/summary-nulls.test.ts&&pnpm --filter @objectstack/cli typecheck0 · 0 · 0 57/57 cache hits from the shared worktree cache (replayed logs name /home/user/objectstack-15064, the dev's tree; content-hash keyed); Tests 6 passed (6); CLI tsc + test-typecheck OK12 Reverse verification: bogusKeyReverse15064: 1planted in the CLI call (blob890704ef≠ HEAD68779b93, marker 1) →pnpm --filter @objectstack/cli exec tsc --noEmit1 (expected) summary-nulls.ts(233,9): error TS2353 … 'bogusKeyReverse15064' does not exist in type 'SummaryBackfillOptions'; restored blob == HEAD, marker 0. Builtpackages/objectql/dist/index.d.tscarriesrecomputeUndefinedOnEmpty?: string[](l.917) andrecomputedUndefinedOnEmpty: string[](l.875)13 (locked) Ablation scope.has(rollupKey(desc))→+ '__ablated_15064'(anchor 1→0, marker 0→1, blobbc2eb31e≠ HEADccb8e7ce) → same vitest file1 (expected) Tests 6 failed / 14 passed (20) — red: the 5 SCOPED:pins + the formatter pin; green: the UNSCOPED byte-for-byte pin, the count control, never-overwrites, the refusal pin, all 10 pre-existing. Prediction met exactly. Restored via trap: blob == HEAD, anchor 1, marker 0,git diff HEADempty14 (locked) Unscoped re-measure: merge-base summary-backfill.ts(blob131c1ab6==95d5cbb31blob) copied beside a copy of the HEAD test with the import repointed and therecomputedUndefinedOnEmpty: []expectation line deleted →vitest run … -t UNSCOPED0 1 passed / 19 skipped — the pre-change code emits exactly the literals the head pins (report JSON, dry lines, apply lines). Positive control -t "SCOPED: naming the max"against the old source → exit 1 (1 failed). Scratch files removed, porcelain 015 pnpm check:error-status-conformance0 reconciled 26 codes / 27 pairs; unpinned 25 (baselined 25) — FIELD_NOT_FOUNDnow derives a 404 producer16 node scripts/check-changeset-no-major.mjs·check-changeset-fixed.mjs·check-empty-changeset.mjs·check-adr-0087-registration.mjs0 · 0 · 0 · 0 no major; fixed group in sync (69); 1 declaring changeset; no declared-breaking changeset 17 pnpm check:docs-transcript-drift·pnpm check:docs-audit-scope·pnpm check:error-code-casing·pnpm check:nul-bytes0 · 0 · 0 · 0 all green on the final head 18 node scripts/docs-audit/affected-docs.mjs 95d5cbb310 3 pages: content/docs/api/error-catalog.mdx,content/docs/deployment/cli.mdx,content/docs/releases/v17.mdx(read-only)19 (locked) pnpm check:type-check-debt3, then 0 first run refused: @objectstack/objectqltype entry point OLDER than sources — caused by my own ablation restore touching the source after the.d.ts;pnpm --filter @objectstack/objectql build && pnpm check:type-check-debt(locked,&&) → command-exit 0, 12 entries re-measured, none above20 pnpm exec eslint --no-inline-config --format json <4 touched TS files>0 4 files, 0 errors, 0 warnings — NOT MEASURED as evidence: three firing controls ( var/==; code inside a block comment;console.log(JSON.stringify(…))inpackages/cli/src) each produced 0 findings although--print-configresolves six rules for the path. CILint & Repo Gatessuccess at head is corroboration only21 grep -rn FIELD_NOT_FOUND packages --include=*.ts(non-test) · catalog control— producers: only summary-backfill.ts:282(new) + the enum declaration;FIELD_NOT_FOUND_ZZZin the catalog = 0 (control);error-catalog.mdx:326section present22 PR check runs at head (API) — 38 runs, every one successorskipped, none failing — corroboration, not my measurement23 Zone 2 #5: search_code backfillSummaryNulls repo:objectstack-ai/cloud·pull_request_read cloud#1941— 0 items with incomplete_results: true(not a reading); Access denied — repository not configured for this session. NOT MEASURED24 Final tree proof 0 HEAD 7c96f738e, detached; porcelain 0;git diff HEAD0 lines; blobs of every file I mutated == HEAD (ccb8e7ce,68779b93,28606b2a,19e811bc); 0 scratch leftovers
A. Is the delivery what ruling A ordered, and is the contract right?
Yes on the ordered surface; one contract defect on an addition the ruling did not order (the refusal's code — see D4).
Ruling
5548475521ordered: a caller-supplied scope onbackfillSummaryNullsunder which namedmin/max/avgare recomputed for every parent throughaggregateSummaryValue; without the scope the run behaves exactly as today; the predicate is not relaxed (B refused), no second definition (C refused); the CLI surfaces the scope; pins = the card's repro plus acountcontrol;summary-aggregate.tstouchable only for a helper;engine.tsout of scope.Measured against the tree:
SummaryBackfillOptions.recomputeUndefinedOnEmpty?: string[]is resolved once, before any row is read, against the engine's owngetOwnedSummaryDescriptorsindex over the walked candidates (resolveRecomputeScope); a namedmin/max/avgjoinsbackfillableinpartitionDescriptorsand is walked like acount; everyNULLparent is recomputed through the sameaggregateSummaryValue. A parent whose aggregate is the empty-set reading is reclassified (counter decremented, sample popped) and not written — the storednullalready equals what the engine would write, so "children or not" is honoured (the recompute runs for both) and idempotence is preserved. Naming acount/sumresolves and changes nothing. The predicatesummaryNullIsBackfillableis untouched;summary-aggregate.tsandengine.tsare byte-identical to the merge-base (row 3).- Unscoped run byte-for-byte: re-measured independently, not taken from the test's own literals — the merge-base source, run under the head test with the additive key removed from the expectation, produces exactly the pinned report JSON and both formatter line sets (row 14). The head's only unscoped delta is
recomputedUndefinedOnEmpty: []. - Ablation re-measured 6 red / 14 green with the predicted membership (row 13); the count control stays green under ablation, so it is a control.
- Reverse verification re-measured: the CLI's
tscreads the rebuilt.d.ts(row 12). - Report:
recomputedUndefinedOnEmpty: string[](sameobject.field (fn)spelling asskippedUndefinedOnEmpty),SummaryBackfillFieldOutcome.fnwidened toSummaryDescriptor['fn'], formatter gains "Recomputed on request" only when the scope is non-empty. CLI:--recompute-undefined-on-empty(multiple: true), passed through in order, confirmation prompt names the columns, refusal reaches the--jsonenvelope withcodeand exit 1 (row 11). - Both mid-run merges of
origin/mainare clean by content (rows 5–6);content/docs/releases/**untouched (row 4);check:docs-audit-scopeandcheck:type-check-debtare green on the final head, and I reproduced the dev's statedtype-check-debtmechanism myself (a stale-mtime refusal after an ablation restore, green after a direct objectql build — row 19);a3eba0759(the docs-audit residue fix the dev cited) is an ancestor of head. Both explanations hold.
The refusal path is an addition beyond the ruling's text. Its motivation is right (a silently ignored entry is the false all-clear the card was filed over) and its placement is right (before any row is read, dry run and apply alike). Its wire code is wrong — D4.
B. Semver
Judged per package, act over commit type (
b337a1308, #15380):@objectstack/objectql: minor— correct. Three acts: an optional parameter on an exported function (additive); a new required keyrecomputedUndefinedOnEmptyonSummaryBackfillReport— the report is a return payload produced and consumed within the same package version (formatSummaryBackfillReport,summaryBackfillCompleteare its only input-position consumers, by grep), so for every reader it is additive; a widened union onSummaryBackfillFieldOutcome.fn— a new value that can appear on an output type, the same class as adding an enum member. Each is a purely additive widening of a published surface, so the floor isminor; none is a declared breaking change needing the BREAKING banner + ADR-0087 disposition.majoris refused in the window anyway (row 16).@objectstack/cli: minor— correct. A new flag on a published command is an additive widening; the floor isminor.
The commit type
feat(agrees with the act; had it beenfix(, the act would still requireminor.C. Clause ②
Clause-②: yesis right, in both directions.- Toward "yes": the mechanical floor in the contract-review reference already forces it — a new key on a published payload (
recomputedUndefinedOnEmptyin the report and therefore in the CLI's--jsonoutput) and a widened exported signature are "恒yes". Add the widenedfnunion and a new CLI flag. This is the published contract face of two packages. - Toward the 2026-08-28 negative boundary (「运行时权限/安全行为变更不是条款② …… 条款②只指已发布契约面」): nothing here is a permission or security behaviour change being mislabelled as clause ②. The
yesdoes not rest on behaviour at all — the default run's behaviour is byte-identical (row 14) — it rests on surface widening alone, which is exactly what the clause names. The two prior misreadings went the other way (treating a behaviour change as contract); this card does not repeat them.
D. The four rows
D1 — Spelling: right for a published surface. A list of
object.fieldnames is the exact fact the caller holds and is typo-checkable at the engine's own index — the boolean cannot be (a wrong object name would be the silent no-op again, and the flag would sweep every siblingmin/max/avg). ASummaryDescriptor[]would let the caller build the descriptor, which is the second definition option C refused; a name resolved against the engine's index keeps one.fields?: string[]besideobjects?: string[]reads as a restriction filter, the inverse meaning.neverComputedstates the fact but pairs with nothing the module says.recomputeUndefinedOnEmptycloses the round trip withskippedUndefinedOnEmpty/recomputedUndefinedOnEmptyin the report, which is the strongest argument: the option is the answer to the list an operator already sees. I re-derived every rejection from the tree rather than adopting the dev's text. One caveat, not a patch: the name is boolean-shaped until the type is seen; the TSDoc and the documented--recompute-undefined-on-empty object.fieldform disambiguate it. Acceptingcount/sumnames as no-ops, and refusing (rather than silently widening) a roll-up on an objectobjectsleft out, are both right.D2 — Zone 2 #5 carried unmeasured: acceptable for this contract, and correctly labelled. The premise is a cloud-side fact about how the publish path calls the backfill; it conditions the card's urgency, not the delivered contract, which is caller-supplied, default-preserving and measured entirely in this repo. I tried to measure it and could not: the cloud repository is not configured for this session and code search returned an incomplete result (row 23) — it remains NOT MEASURED here too. No gate on this PR depends on it; the cloud seat measures it when it wires the scope. What would have been unacceptable is asserting it; the dev did not.
D3 — Ratchet baseline moved inside a feature PR: legitimate as delivered, but it must be reverted under the patch below. On this head the gate itself demands the change (
nowPinnedProducerMessage: "baselined as unpinned, but a producer now declares its status — ratchet the baseline down with --update"); the ratchet-DOWN remedy is the author's own (the gate's battery 11 marks only the baseline-EXPANDING path maintainer-only);--updaterewritesresult.unpinnedwholesale and the diff is exactly one line with thenotebyte-identical, so no unrelated burn-down rode along. It is the direct consequence of the new producer, not opportunistic debt work. Because the patch changes the producer's code,FIELD_NOT_FOUNDloses its producer again and the line must come back — by restoring the file toorigin/main's blob0596eb336, which is a no-op against main and not the maintainer-only expansion.D4 —
error-catalog.mdx:326: the sentence is no longer true of every producer, and the right fix is the code, not the sentence. Of the six refusal shapes the PR pins, two refuse a field that does exist on the object:project.name(a real field, not a roll-up) andproject.max_estimatewithobjects: ['task'](a real roll-up on an object the run was told not to walk) — both answeredFIELD_NOT_FOUND/ 404 whose Cause reads "The specified field does not exist on the object" and whose Fix ("check the field name against the object schema") sends the operator to confirm a field that is there. Whose lane: this PR's — the page is hand-written (neitherreferences/norreleases/), the drift bot listed it on this PR, andaffected-docs.mjslists it (row 18). But widening the 404 sentence would be the wrong repair, because the tree already answers which code this condition takes:content/docs/api/error-catalog.mdx:71INVALID_FIELD— "A field name in the request does not exist on the target object … plus every other read axis that names a field:select,expand(a real field that holds no reference gets its own message),searchFields(a real field outside the searchable set gets its own message),groupBy, andaggregations[].field." That is both "no such field" and "a real field of the wrong kind", at 400, per axis.packages/objectql/src/engine.ts:1242emitsINVALID_FIELD/ 400 forUnknown field 'x' on object 'y';engine.ts:990–997and1092–1098record the rule: "not a new code, and 400 … one condition ('this X was not applied as written') keeps ONE wire code however the caller reached it, so a host surfacing engine errors over HTTP answers the same envelope on both doors."FIELD_NOT_FOUNDhas never had a producer in this repo (it sat in the unpinned baseline). Giving it its first producer for a condition the catalog already assigns toINVALID_FIELDcreates exactly the two-codes-one-condition drift ADR-0112 exists to prevent, and 404 is the wrong HTTP class for an option value that cannot be applied (it is not an addressed resource).
recomputeUndefinedOnEmptyis one more axis that names a field. It takesINVALID_FIELD/ 400, and the catalog'sINVALID_FIELDlist gains the axis. The dev's report claimed "an existing standard-catalog code — no ledger change": true as far as the ledger goes (the enum is untouched), but the claim did not read the page the drift bot pointed at.
Patch list (every item names the pin that goes red if the fix is lost)
- Code —
packages/objectql/src/summary-backfill.ts,resolveRecomputeScope:err.code = 'INVALID_FIELD'; err.status = 400;(keepfields; addfield = unresolved[0]to match the engine's sibling producers). Update the TSDoc onSummaryBackfillOptions.recomputeUndefinedOnEmpty, the resolver docblock, the changeset paragraph and the PR body whereverFIELD_NOT_FOUND/ 404 is named. Pins:summary-backfill.test.ts"REFUSES a name it cannot resolve" (retarget its sixcode/statusassertions toINVALID_FIELD/ 400) andsummary-nulls.test.ts"a refused scope entry … reaches the --json error envelope" (retargetcode);pnpm check:error-status-conformance(anINVALID_FIELD@ 400 producer is already a reconciled pair on both pages). - Baseline —
scripts/error-status-unpinned-baseline.json: restore toorigin/main's content (blob0596eb336, i.e. drop commit7c96f738e's one-line change) so the PR does not touch the file. Pin:pnpm check:error-status-conformancereds with "FIELD_NOT_FOUND: documented with an HTTP status, but no producer declares one" if the line is not restored. This restoration is a no-op againstorigin/main, not the maintainer-only expansion. - Docs —
content/docs/api/error-catalog.mdx,INVALID_FIELDCause (line 71ff): add the axis —recomputeUndefinedOnEmpty/os migrate summary-nulls --recompute-undefined-on-empty(an entry that is not a roll-up owned by an object the run walks: a real non-summary field, or a roll-up on an object--objectleft out, gets its own message). LeaveFIELD_NOT_FOUNDat line 326 as it is. Pin: none reds on prose alone (the 400 claim is already on the page); the drift bot lists the page on every run of this PR, and the next docs-accuracy audit is the tripwire — stated plainly rather than manufactured. - Rider —
SummaryBackfillReport.nullRowsTSDoc (summary-backfill.ts:131) still says "in acount/sumroll-up"; under the scope it also counts the namedmin/max/avgholes (the sibling field-outcome doc was updated, this one was not). One line on a published type. Pin: none.
Escalations: none. Every item above is answerable from the tree's own recorded conventions.
Boundary flags
- Maintainer may want to answer D4 differently: if
FIELD_NOT_FOUNDis meant to receive its first producer here, that is a catalog-level decision — it requires narrowing theINVALID_FIELDentry at the same time and a rationale for 404 on an option value. I did not take it because the tree already answers; flagged so it can be overruled deliberately rather than by default. - Cross-lane (cloud): Zone 2 [WIP] Fix error in step four of the action run #5 stays NOT MEASURED; cloud#1908 / PR feat(objectql): accept execution context via trailing options arg on read methods #1941 wire the scope after release. Note for that seat:
formatSummaryBackfillReportreadsreport.recomputedUndefinedOnEmpty.lengthunguarded, so a report of the pre-PR shape (e.g. JSON persisted by an older CLI) fed to the new formatter throws — fine within one package version, not a patch item. - Pre-existing CLI convention gap, out of scope:
errorCodeFieldsreadshttpStatus, engine errors carrystatus, so the--jsonrefusal envelope carriescodebut no status. Not introduced by this PR. - UX, not contract: with
--applyand no--yes, the confirmation prompt precedes the refusal, so an unresolvable name is refused after the operator says yes. Nothing is written; noted only. - Semver reading: the required report key and the widened
fnunion are the two spots where a stricter semver would say "breaking for input-position / exhaustive consumers"; under this repo's rule they are additive widenings and takeminor. If the maintainer prefers the stricter reading, the vehicle is the BREAKING banner + ADR-0087 disposition, never the level. - Release-owned page:
content/docs/releases/v17.mdx:2951("backfills roll-up count and sum columns") stays true — the default is unchanged. Read-only; no action. - Instrument honesty: the eslint zero-hit is NOT MEASURED as evidence (row 20); the CLI closure's
distcame from the shared turbo worktree cache populated by the dev's tree (row 11) — content-hash keyed, and the reverse verification is what proves the CLI'stscread a.d.tscarrying the new key.
Where the tree is
/home/user/objectstack-review-15064, detached at7c96f738e117f36bf6c18f921ed89c1743aa25c1,git status --porcelainempty,git diff HEADempty, every file I mutated hash-equal to HEAD, no scratch files left (row 24). Builtdist/trees are gitignored. The worktree is left in place for the adopting seat; no other worktree and no shared checkout was touched.
Generated by Claude Code
Claim — patch round on PR #15708,
domain:engineexecution seat (verdict5550099353, FAIL (patch round))Claim: PM loop patch round —
domain:engineexecution seat, PR #15708 / verdict5550099353
Session:session_01ARYe3yQTQCUFm5qPYNgKaJ
Branch:claude/issue-15064-backfill-summary-nulls-scope(existing; append-only commits, no rebase, no amend, no force-push; verdict pinned to7c96f738e117f36bf6c18f921ed89c1743aa25c1)
Worktree:objectstack-15064-r2(own tree;objectstack-review-15064is the reviewer's and is not touched)
File surface:packages/objectql/src/summary-backfill.ts,packages/objectql/src/summary-backfill.test.ts,packages/cli/src/commands/migrate/summary-nulls.test.ts,scripts/error-status-unpinned-baseline.json(restore toorigin/main),content/docs/api/error-catalog.mdx(INVALID_FIELDCause only), the PR's changeset, PR body
Clause-②: yesImplementing the verdict's four items as written (non-relitigable):
INVALID_FIELD/ 400 withfield = unresolved[0]inresolveRecomputeScope; the two pins retargeted; baseline restored toorigin/main's blob0596eb336; theINVALID_FIELDcatalog axis; thenullRowsTSDoc rider. Zone 2 assumptions will be measured, not assumed. Report follows on this card when the branch is pushed and the PR body updated. PR stays draft;needs:contract-reviewstays on both carriers.
Generated by Claude Code
os-dev-report
{ "issue": 15064, "status": "done", "branch": "claude/issue-15064-backfill-summary-nulls-scope", "pr": "https://github.com/objectstack-ai/objectstack/pull/15708", "premise_still_valid": true, "summary": "Patch round on PR #15708 for contract-review verdict 5550099353 (FAIL, patch round, pinned to 7c96f738e), all four items discharged on the existing branch with appended commits only: f6ed5b839 (item 1 code + both pins retargeted, item 3 INVALID_FIELD Cause axis in error-catalog.mdx, item 4 nullRows TSDoc, option/resolver TSDoc and changeset paragraph), 83a41f93b (item 2: scripts/error-status-unpinned-baseline.json restored to origin/main's blob 0596eb336 via the exact spelling Zone 2 #1 assumed; the file is no longer in the PR diff), and merge 1914df81f of origin/main 8e500f23e (dispatch-gates reported the tree 36 commits stale with the conformance gate script itself changed on main; merge clean by content, all 8 of this PR's paths blob-identical to their pre-merge selves, no driver deferral). resolveRecomputeScope now stamps code INVALID_FIELD, status 400, field = unresolved[0] beside fields. Zone 2: #1 holds (git checkout origin/main -- FILE reproduced blob 0596eb336; git diff --name-only origin/main...HEAD lists 7 files, the baseline absent); #2 holds and is proven mutually load-bearing (three-state conformance runs, both single-application states red on the two predicted lines, both applied green); #3 holds in this repo (the only FIELD_NOT_FOUND readers are the enum, the two named tests, the catalog entry and generated reference pages built from the enum; cloud stays NOT MEASURED as before); #4 FALSIFIED in the literal sense — the page IS read mechanically by check:error-status-conformance (entry headings, HTTP Status lines, quick-reference rows) and by packages/spec error-catalog-docs.test.ts (headings + member count), and it sits in docs-audit's handwritten set — but none of them parses the Cause prose, so the verdict's narrower claim (item 3 reds nothing on its own) is the accurate one. Drift-bot list at this head (13 pages, 11 anchored on the INVALID_FIELD literal) read and classified as anchor-noise from the code swap: every one states INVALID_FIELD/400 for a read- or write-path field condition and none states a closed producer list; releases/v17.mdx read-only and still true. PR body PATCHed wherever FIELD_NOT_FOUND/404 was claimed (read-back identical above the footer; the platform appended a bare footer under the session-URL one — left alone). PR stays draft; needs:contract-review stays on both carriers; nothing armed.", "tests": "All at 1914df81f (final head; union run after the last commit), worktree /home/user/objectstack-15064-r2, lock slot dev-15064-r2 (7 acquisitions, longest hold 3m37s, never polled), every exit captured before any pipe. PINS: pnpm --filter @objectstack/objectql exec vitest run --maxWorkers=2 src/summary-backfill.test.ts → exit 0, Tests 20 passed (20); item-1 reverse leg with the source reverted to 7c96f738e (blob ccb8e7ce == BASE, producer lines FIELD_NOT_FOUND=1/INVALID_FIELD=0) → exit 1, 1 failed | 19 passed, AssertionError: project.nope: expected 'FIELD_NOT_FOUND' to be 'INVALID_FIELD'; restored via git checkout HEAD -- ABS_PATH under an absolute-path trap, blob 1cb674bbc == HEAD, git diff HEAD 0 lines. pnpm --filter @objectstack/cli exec vitest run --maxWorkers=2 src/commands/migrate/summary-nulls.test.ts → exit 0, Tests 6 passed (6). TYPECHECK: pnpm --filter @objectstack/objectql typecheck → 0 (tsc + tsconfig.scripts + check:test-typecheck OK, 44 files / 242 pinned errors, none new; tsconfig.test.json --listFiles lists summary-backfill.test.ts: 1); pnpm --filter @objectstack/cli typecheck → 0 (check:test-typecheck OK 3 files / 28 pinned; tsconfig.test.json --listFiles does NOT list summary-nulls.test.ts (0) but tsconfig.json --listFiles does (1), so the tsc --noEmit half covers the edited file). spec: pnpm --filter @objectstack/spec exec vitest run src/api/error-catalog-docs.test.ts → 0, 3 passed. ZONE 2 #2 PAIRING (pnpm check:error-status-conformance, source-read, tree had no dist/ at the time): (a) HEAD code + BASE baseline (blob 19e811bc2, FIELD_NOT_FOUND absent) → exit 1 「✗ FIELD_NOT_FOUND: documented with an HTTP status, but no producer declares one」; (b) BASE code + HEAD baseline → exit 1 「✗ FIELD_NOT_FOUND: baselined as unpinned, but a producer now declares its status — ratchet the baseline down with --update」; (c) HEAD → exit 0 「✓ every derivable runtime status is documented, and every documented status is reachable」, reconciled 26 codes / 27 pairs, unpinned 26 (baselined 26); every mutation and restore blob-proven, final git diff HEAD 0 / porcelain 0. BUILDS (locked): pnpm install --frozen-lockfile → 0 (twice, lockfile moved on main); turbo build @objectstack/objectql... → 15/15 in 2m50s; turbo build @objectstack/cli... → 57/57 (16 cached) in 2m42s; objectql rebuilt before type-check-debt (source touched after dist by the reverse leg). GATE UNION: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands at 1914df81f → 89 commands (7 workflow-valued argv printed NOT MEASURED by the tool; it also reports the tree 6 commits behind a moving origin/main with lint.yml differing only in comments per ef60224fc); all 89 executed through a resumable runner with per-command exit capture; --ran reconciliation → 「✓ dispatch-gates --ran: 89 derived famil(ies) accounted for — 89 run, 0 NOT-MEASURED」; 86 exit 0 including check:error-status-conformance, check:error-code-casing (not in the union, run on Zone 3's word), check:nul-bytes, check:docs-audit-scope, check:docs-transcript-drift, check:cli-examples-parity (new family after the merge), check:type-check-debt, check:cross-package-test-inputs, check:i18n, check:i18n-walk-parity, spec check:docs/liveness/empty-state/strictness-ledger/variant-docs/yaml-examples, the four changeset gates (no major; fixed group in sync 69; 1 declaring changeset; no declared-breaking changeset). NOT MEASURED locally, ⛔ not green: pnpm --filter @objectstack/spec run check:skill-examples → exit 1 「packages/client-react/dist holds no .d.ts declarations — the package is not built」; pnpm check:dual-build-cjs-loads → exit 3 「PREREQUISITE NOT MET — 11 packages have no dist/ (studio, client-react, connectors, embedder-openai, knowledge-memory …)」; pnpm check:i18n-coverage → exit 3 「COULD NOT MEASURE — 1 of 13 configs failed to lint: Cannot find module …connector-mcp/dist/index.mjs」 — all three need packages outside the cli closure built; repo-level runs are CI's (declared narrowing). RELEASES CONTROL: git diff --name-only origin/main...HEAD -- 'content/docs/releases/**' → 0; control 'content/docs/**' → error-catalog.mdx, cli.mdx. Control-byte scan of the 5 edited files → 0 hits. CI at 1914df81f (corroboration only, not waited for): 34 check runs, 32 success + 2 skipped, all six required contexts success.", "mcp_calls": "2 — both search_issues: the search-before-file for the errorCodeFields/httpStatus finding and its control query (which hit #15064 first, so the reading counts). Every other read and write — issue, verdict, PR, comments, check runs, claim POST 5550116716, PR PATCH, finding POST, this report — went through REST with the session token; the REST search endpoint itself answers 403 on this seat (target and control), which is the fork that sent the one search to MCP", "open_questions": [], "out_of_scope_findings": ["filed as #15779: `errorCodeFields` (packages/cli/src/utils/format.ts) forwards `httpStatus` only while engine-thrown refusals carry `status`, so the `--json` error envelope of `os migrate summary-nulls` (and every command spreading `errorCodeFields`) carries `code` but no HTTP status — surfaced by the verdict's boundary flags, out of this card's scope; searched first via one MCP search_issues (7 neighbours, none the same gap: #8570 closed is the mirror case in REST batch rows, #15549 open is a site spreading no envelope at all); filed unassigned, no label, for PM triage"] }Claim: PM loop patch round —
domain:engineexecution seat, PR #15708 / verdict5550099353
Session:session_01ARYe3yQTQCUFm5qPYNgKaJ
Branch:claude/issue-15064-backfill-summary-nulls-scope— appendedf6ed5b839,83a41f93b, merge1914df81f; the verdict's pin7c96f738eis untouched (no rebase, no amend, no force-push)
Clause-②: yesPer item — what changed · the pin that reds if it is lost · the exit measured
- Code
packages/objectql/src/summary-backfill.tsresolveRecomputeScope:err.code = 'INVALID_FIELD'; err.status = 400; err.field = unresolved[0]; err.fields = unresolved;— the cast widened to carryfield. Option TSDoc, resolver docblock (now cites the engine's own rule and its two sibling producersassertProjectionHasNoDottedPaths/undeclaredWriteFieldErrors), the changeset paragraph and the PR body rewritten whereverFIELD_NOT_FOUND/ 404 was named. Pins:summary-backfill.test.ts「REFUSES a name it cannot resolve — INVALID_FIELD, 400 …」 (six shapes ×code/status, now alsofield= the unresolved entry andfields=[it]) — reds with the source reverted to7c96f738e: exit 1,expected 'FIELD_NOT_FOUND' to be 'INVALID_FIELD', green at head (exit 0, 20/20);summary-nulls.test.ts「a refused scope entry (INVALID_FIELD) reaches the --json error envelope …」 (mock carriescode: 'INVALID_FIELD', status: 400, field, fields) — exit 0, 6/6.pnpm check:error-status-conformance— exit 0. - Baseline
scripts/error-status-unpinned-baseline.json:git checkout origin/main -- FILE(origin/main8e500f23eat the time) →git hash-object=0596eb336, one"FIELD_NOT_FOUND"line back, committed as83a41f93b;git diff --name-only origin/main...HEAD→ 7 files, the baseline absent. Pin: the conformance gate reds on 「FIELD_NOT_FOUND: documented with an HTTP status, but no producer declares one」 when the line is missing — measured as state (a) above, exit 1. - Docs
content/docs/api/error-catalog.mdxINVALID_FIELDCause: one appended sentence namingbackfillSummaryNulls'srecomputeUndefinedOnEmpty/os migrate summary-nulls --recompute-undefined-on-empty object.field— an entry that is not a roll-up owned by an object the run walks (a real non-summary field, or a roll-up on an object--objectleft out) gets its own message.FIELD_NOT_FOUNDat its line untouched. Pin: none reds on prose alone (measured:check:error-status-conformance0,error-catalog-docs.test.ts3/3 — neither parses Cause text). - Rider
SummaryBackfillReport.nullRowsTSDoc: 「Parent rows found holdingNULLin a roll-up this run fills — everycount/sum, plus themin/max/avgnamed inrecomputeUndefinedOnEmpty(there, aNULLwhose recompute is the empty-set reading is not a hole and is not counted)」. Pin: none;typecheck0.
Zone 2 — measured, one falsified
- Holds. The restoration is exactly
git checkout origin/main -- scripts/error-status-unpinned-baseline.json; it reproduced blob0596eb336(origin/main's blob was re-read at that moment and matched) and the file is absent fromgit diff --name-only origin/main...HEAD. Caveat stated: it holds because origin/main's blob still equals the verdict's pinned blob; the immutable spelling is the merge-base95d5cbb31or the blob itself. - Holds — mutually load-bearing, proven. Only the code change (old baseline) → exit 1 on the「no producer declares one」line; only the restoration (old code) → exit 1 on the「a producer now declares its status — ratchet down」line; both → exit 0. Source-read: the tree carried no
dist/when the three states ran. - Holds in this repo.
FIELD_NOT_FOUNDreaders repo-wide (packages, content, scripts, skills, examples, apps, docs; node_modules/dist excluded): the enumspec/src/api/errors.zod.ts:90, the two named tests, the catalog entry at line 326, the changeset (now rewritten), and five generatedcontent/docs/references/**rows produced from the untouched enum.--jsonenvelope consumers ofsummary-nullsin-repo: only the CLI test;errorCodeFieldsforwardscodegenerically. cloud (docs: regenerate references from current spec #1908 / PR feat(objectql): accept execution context via trailing options arg on read methods #1941) stays NOT MEASURED, as the verdict already recorded. - Falsified as stated. The page is read by gates:
check:error-status-conformance(entry headings,**HTTP Status:**lines, quick-reference rows — it is how the 400 claim is reconciled),packages/spec/src/api/error-catalog-docs.test.ts(heading ↔StandardErrorCodeparity + the advertised member count),check:docs-audit-scope(page indocs-audit/handwritten-docs.json),check-ci-filter-parity(path coverage). None parses the Cause prose, so the verdict's 「none reds on prose alone」 is the true sentence; 「no gate reads that page」 is not.
Docs-drift list at
83a41f93b/1914df81f— 13 pages, classified, not acted on11 rows anchored on
INVALID_FIELD (literal, a string literal in resolveRecomputeScope):api/data-api.mdx(16 mentions —select/expand/searchFields/unknown query parameter, all 400),api/error-handling-server.mdx(an enum listing),automation/hook-bodies.mdx(write path,Unknown field 'stagee'),data-modeling/queries.mdx,data-modeling/schema-design.mdx,permissions/authorization.mdx,protocol/kernel/error-handling.mdx(the#### INVALID_FIELDentry the conformance gate reconciles),protocol/objectql/index.mdx,protocol/objectql/query-syntax.mdx,protocol/objectql/types.mdx,ui/views.mdx(searchable-field refusals). Each statesINVALID_FIELD/ 400 for a read- or write-path field condition; none states a closed list of producers or 「only on the request path」, so none is falsified by one more producer — anchor-noise from the code swap, exactly the intended consequence of item 1.error-catalog.mdxanddeployment/cli.mdxare the two rows this PR edits by design.releases/v17.mdxis read-only; line 2951 「backfills roll-up count and sum」 stays true (the default run is unchanged). Nothing filed from the list.Instruments, channels, honesty lines
- Every GitHub read and write went through REST with the session token (issue, verdict, PR, comments, check runs, claim POST
5550116716, PR PATCH, this comment). The REST search endpoint is 403 on this seat (target and control both), so the search-before-file for the finding below went to one targeted MCPsearch_issues— the channel change the dispatch rules name for that fork. - Verify lock
dev-15064-r2: read--statusbefore the first heavy run (holder present, queue empty); acquired once per step, never polled; holds 5s / 3s / 2m51s / 34s / 2m44s / 53s / 3m37s; shared-box seconds, not idle-box figures. origin/mainkept moving: merged at8e500f23e; by the end it stood 6 commits further (014248855), none touching this PR's files;lint.ymlmoved only in comments. Not re-merged — the union is reported from1914df81f, the pushed head.- PR body edit: read-back above the footer byte-identical to what was sent; the platform appended a bare footer under the session-URL one, so the body now carries two — ⛔ not re-edited.
- PR stays draft;
needs:contract-reviewon both carriers; no label stripped, nothing armed, nothing merged.content/docs/releases/**untouched (firing control oncontent/docs/**fires on the two edited pages). - Worktree
/home/user/objectstack-15064-r2cleaned up after this comment (node_modulesremoved,git worktree removewithout force); the reviewer'sobjectstack-review-15064and the shared checkout were not touched.
Generated by Claude Code
- Code
zhuangjianguo commented
on Sep 5, 2026 CollaboratorMore actionsPatch round received —
1914df81f, all four items discharged. Verified at tier and by content. ⛔ Nothing flipped; the re-review at tier is held on the lock-depth gate, not skipped.domain:engineexecution seat,session_01ARYe3yQTQCUFm5qPYNgKaJ. Dev report5550332911. PR stays draft,needs:contract-reviewstays on both carriers.Transcript tier verification (⛔ not the dev's self-report): 171 occurrences of
"model":"claude-fable-5-1"and no other model anywhere, over 975,499 bytes.What this seat measured itself at
1914df81freading item 1 — the refusal summary-backfill.ts:297-299:err.code = 'INVALID_FIELD'·err.status = 400·err.field = unresolved[0]— including thefieldthe verdict asked for, to match the engine's sibling producers ✅item 2 — the baseline restoration scripts/error-status-unpinned-baseline.jsonis absent from the PR's 7-file diff againstorigin/main✅ — the file is no longer touched at allcontent/docs/releases/**0 (firing control on content/docs/**: 2) ✅CI on this head ci-failure.mjs --pr 15708⇒ exit 0, GREEN — all 34 check-run(s) completed, none failed, 0 superseded⚠️ One reading needed a second look and turned out right.FIELD_NOT_FOUNDstill appears once insummary-backfill.ts(controlINVALID_FIELD: 3). It is a TSDoc line stating the absence:Two of the shapes refused here name a field that EXISTS … so this is an option value that could not be applied, never an addressed resource that was not found (
FIELD_NOT_FOUNDhas no producer in this repo, and gains none here).⭐ That is the second time today a bare symbol count went non-zero because the code now documents an absence — the same shape as PR #15687's
platformGlobalObjectscount moving 0 → 2. Recording it as a pattern: on a file whose prose discusses the symbol, a bare count has stopped being a control, and the reading-shaped predicate is the instrument.⭐ Three things in this round worth the re-reviewer's eye
1. Zone 2 #4 was FALSIFIED — and the falsification makes the verdict more right, not less. I assumed
content/docs/api/error-catalog.mdxis read by no gate, so item 3 is pure prose. The dev measured that the page is read mechanically — bycheck:error-status-conformance(entry headings, HTTP Status lines, quick-reference rows), bypackages/spec'serror-catalog-docs.test.ts(headings + member count), and it sits in docs-audit's handwritten set. But none of them parses theCauseprose, so the verdict's narrower claim — 「item 3 reds nothing on its own」 — is the accurate one. My assumption was wrong in its reason and right in its consequence.2. The item-1 ↔ item-2 pairing was proven with a three-state experiment, which is what I asked for and more than the minimum.
check:error-status-conformancewas run in all three states, each mutation and restore blob-proven:state exit message HEAD code + BASE baseline 1 「✗ FIELD_NOT_FOUND: documented with an HTTP status, but no producer declares one」BASE code + HEAD baseline 1 「✗ FIELD_NOT_FOUND: baselined as unpinned, but a producer now declares its status — ratchet the baseline down」both applied (HEAD) 0 「✓ every derivable runtime status is documented, and every documented status is reachable」 ⇒ The restoration is a no-op against
mainonly because the code change hands the producer back — demonstrated in both directions rather than asserted.3. The drift bot's 13-page list was classified, not obeyed. The list grew from 2 pages to 13 because 11 rows anchor on the
INVALID_FIELDliteral the patch introduced. The dev read every one and classified them as anchor-noise from the code swap — each statesINVALID_FIELD/ 400 for a read- or write-path field condition, and none states a closed producer list, so none is falsified. ⛔ The other 12 pages were not edited;releases/v17.mdxis read-only and still true. That classification is stated in the report rather than left as silence, which is the difference between reading a list and ignoring it.Declared NOT MEASURED — ⛔ reported as such, never as green
Three gates need packages outside the CLI closure built and were not run to a measurement:
check:skill-examples(exit 1, 「packages/client-react/distholds no.d.tsdeclarations」),check:dual-build-cjs-loads(exit 3, PREREQUISITE NOT MET, 11 packages with nodist/),check:i18n-coverage(exit 3, COULD NOT MEASURE). The rest of the union reconciled: 89 derived, 89 run, 0 NOT-MEASURED by the tool's own--ranline, 86 at exit 0.⚠️ The branch also mergedorigin/mainthis round (permitted for this card, unlike #15395/#15687 where it was forbidden) — the dev reports the merge clean by content, all eight of this PR's paths blob-identical to their pre-merge selves. The re-reviewer evaluates that by content, ⛔ never by 「no conflict markers」.Out of scope, filed rather than ridden in: #15779 —
errorCodeFields(packages/cli/src/utils/format.ts) forwardshttpStatuswhile engine-thrown refusals carrystatus, so the--jsonenvelope of this command (and every command spreading it) carriescodebut no HTTP status. Surfaced by the verdict's own boundary flags; searched for duplicates first.Next
Re-review at tier, per the FAIL's own instruction.
⚠️ Held right now by the ruled lock-depth pre-dispatch gate:os-verify-lock.sh --statusreads a holder with the queue empty ⇒ an arriving review would be depth 2, at the ≥2 stop. It goes out on the next reading that clears. 「卡在队列外等待是安全态」.
Generated by Claude Code
zhuangjianguo commented
on Sep 5, 2026 CollaboratorMore actionsContract re-review at
CONTRACT_REVIEW_TIER— FAIL (patch round), adopted verbatim.⚠️ Two prose items, zero escalations — the smallest FAIL this card has produced.domain:engineexecution seat,session_01ARYe3yQTQCUFm5qPYNgKaJ. Adoption record first, then the verdict unaltered.Transcript tier verification: 116 occurrences of
"model":"claude-fable-5-1", no other model, over 779,965 bytes. ⇒ adoptable ⇒ adopted verbatim.The round is upheld on every load-bearing axis — all four patch items discharged in the tree, the three-state pairing re-measured leg by leg, the merge of
origin/mainclean by content (0 overlapping paths; 127/127 main-side blobs identical; the symmetric difference empty, so nothing was dropped on either side), semver and clause ② upheld, the three NOT-MEASURED exclusions honest, #15779 right to file, and theFIELD_NOT_FOUNDabsence sentence measured true.⭐ What this seat verified itself — F1 holds, and it is the same class as round one
The catalog sentence the patch round added claims the three refusal shapes each 「gets its own message」. Read at the producer (
summary-backfill.ts:290-300):const err = new Error( `[summary-backfill] recomputeUndefinedOnEmpty names ${unresolved.length} roll-up(s) this run cannot find: ` + `${unresolved.join(', ')}. …`⇒ One
Error, one message, every unresolved entry joined into it — no classification of any kind. A typo, a real non-summary field and a roll-up on an excluded object receive identical text. The catalog tells an operator to expect a shape-specific message the runtime never emits.⚠️ And the reviewer traced how it got there, neutrally: verdict5550099353's item 3 spelled the axis with a colon (「… the run walks: a real non-summary field, or …, gets its own message」); transcribing it into a parenthetical parallel to the entry's two existing ones turned a list into a sub-case claim, because in that entry's own idiom (and inqueries.mdx:507) 「gets its own message」 means 「with distinct messages」. ⇒ A faithful-looking transcription changed the claim. That is worth more than the fix.F2 is a rider of the same family: 「the code every other axis that names a field answers」 is over-general — the sort axis names a field and answers
INVALID_SORT(engine.ts:997). It appears in the option TSDoc, the changeset, a test title, the PR body and the commit message, and the changeset compiles into release notes, so the generalisation would ship.Three corrections the reviewer made to this seat's own briefing — all accepted
⚠️ My dispatch brief said 「40 commits ofmain」; the tree says 36 (git rev-list --count 95d5cbb31..8e500f23e). Mine was recalled, not measured.⚠️ My ledger cross-check was right in its conclusion and wrong in its stated mechanism. I reported verifying the earlier review against 「the verify-lock ledger's slot names」. The reviewer measured that the ledger records pid and command label, not a slot field — a slot-name grep returns 0 even for a slot that certainly ran. What my grep actually matched was the worktree path inside the command label, which is still evidence the runs happened, but 「slot name」 was the wrong description of the instrument.- The 「all 8 of this PR's paths blob-identical」 claim (PR body and dev report) counts 7 — the eighth is the baseline, restored to base before the merge, so it is a path in the branch's history and not in the PR. Trivially true, miscounted.
Disposition
Patch round dispatched for the two items — the catalog sentence rewritten to describe the producer (three shapes refused alike, one message naming every unresolved entry), and one qualifier on the over-general 「every other axis」 claim. ⛔ Nothing flipped;
needs:contract-reviewstays on both carriers; CI is GREEN on this head (37 runs, all success or skipped) and stays corroboration, ⛔ not the gate.⚠️ One out-of-scope drift row the reviewer names and did not file (it posts nothing by rule):content/docs/protocol/kernel/error-handling.mdx:302-305publishesINVALID_FIELDMeaning: 「Field value has wrong type」 while the catalog's Cause is 「a field name … does not exist」 — two published meanings for one code, pre-existing and not falsified by this PR. This seat will card it separately rather than widen this round.
Implemented-by:
claude/issue-15064-backfill-summary-nulls-scope(mode:subagent dev; patch-round claim5550116716)
Reviewed-by: context-isolated contract-review subagent, served byclaude-fable-5-1, dispatched fromsession_01ARYe3yQTQCUFm5qPYNgKaJ; judged from a fresh detached worktree, fed only the card, the ruling, the prior verdict, the dev report and the PR.Contract review — PR #15708 at
1914df81f9554760660e9a392b7050a5a9ac99ac(card #15064,priority:p1,Clause-②: yes) — RE-REVIEW of verdict5550099353Verdict:
FAIL (patch round)— pinned to1914df81f9554760660e9a392b7050a5a9ac99ac.Nothing was posted to GitHub. The round is sound on every load-bearing axis: all four patch items are discharged in the tree, the three-state pairing re-measures exactly as reported (item 2 is a restoration, not the maintainer-only expansion), the merge of
origin/mainis clean by content, semverminor/minorandClause-②: yeshold, the three NOT-MEASURED gates are correctly excluded, #15779 was right to file, and theFIELD_NOT_FOUNDabsence sentence is true. The one defect is one clause of prose the patch round added to the published error catalog (item 3's sentence), which states a message granularity the producer does not have — the same class of defect (a catalog page false of its producer) that failed round one, at one-sentence size. It is a prose fix in this PR's lane; no escalation.
Readings table
Worktree
/home/user/objectstack-rereview-15064(mine, fresh, detached at1914df81f). The single heavy run went throughscripts/pm/os-verify-lock.shwithOS_VERIFY_LOCK_SLOT=rereview-15064:--statusread first (free, queue empty, arriving depth 1); acquired once, waited 0 s, held 248 s; never polled. Every exit captured before any pipe. Light source-read gates ran unlocked, as the dev's did.# Command Exit Result 1 git fetch origin claude/issue-15064-…·git worktree add --detach ../objectstack-rereview-15064 1914df81f0 · 0 HEAD 1914df81f, porcelain 02 git merge-base 1914df81f origin/main·git diff --name-status 8e500f23e 1914df81f0 merge-base 8e500f23e(= the PR base = the merged main); 7 files, +721/−37: changeset,error-catalog.mdx,cli.mdx,summary-nulls.ts+test,summary-backfill.ts+test3 Baseline blobs: git rev-parse {HEAD,8e500f23e,origin/main@7dafaaedd,95d5cbb31}:scripts/error-status-unpinned-baseline.json·git diff --name-only 8e500f23e 1914df81f -- <file>0 all four = 0596eb336;7c96f738e's =19e811bc2; file in PR diff: 04 FIELD_NOT_FOUNDinpackages/**/*.tsnon-test, block+line comments stripped before numbering— 1 hit: spec/src/api/errors.zod.ts(enum). Positive control, same pipeline,code = 'INVALID_FIELD':summary-backfill.ts1,engine.ts3 — fires. Repo-wide all file types (excl. node_modules/dist/.git/references): 4 hits = TSDoc absence line, enum, catalog heading, baseline row. Same pipeline onorigin/main7dafaaedd: enum only5 git diff --name-only 8e500f23e 1914df81f -- 'content/docs/releases/**'· control'content/docs/**'0 0 · control fires: error-catalog.mdx,cli.mdx6 Merge 1914df81fby content: P183a41f93b, P28e500f23e, B95d5cbb31(36 commits main-side, 127 files)0 overlap 0; 7/7 branch-side blobs in M == P1; 127/127 main-side blobs in M == P2; symmetric difference of (P2..M) vs (B..P1) = ∅ (nothing dropped); git check-attr mergeon all 8 paths =unspecified(noos-regen); with 0 overlap no driver ran7 Three-state pairing, node scripts/check-error-status-conformance.mjs(source-read): (c) HEAD · (a) HEAD code +7c96f738ebaseline (blob19e811bc2) · (b)7c96f738ecode (blobccb8e7ce, 1 strippedFIELD_NOT_FOUNDproducer line) + HEAD baseline0 · 1 · 1 (c) "every derivable runtime status is documented…", unpinned 26 (baselined 26); (a) " FIELD_NOT_FOUND: documented with an HTTP status, but no producer declares one"; (b) "FIELD_NOT_FOUND: baselined as unpinned, but a producer now declares its status — ratchet the baseline down". Every mutation and restore blob-proven; porcelain 0 after8 Forward check: origin/main's newer gate (blob2696baad, +306/−12 vs head) swapped in, run on HEAD code + HEAD baseline, restored (blob == HEAD)0 unpinned 26/26, green — the gate moved on main after this head and still passes this tree 9 (locked) pnpm install --frozen-lockfile --prefer-offline0 — 10 (locked) pnpm --filter '@objectstack/objectql...' build0 closure built 11 (locked) pnpm --filter @objectstack/objectql exec vitest run --maxWorkers=2 src/summary-backfill.test.ts0 Tests 20 passed (20) 12 (locked) item-1 reverse leg: summary-backfill.ts←7c96f738e(blobccb8e7ce) → same vitest; restore via trap1 (expected) 1 failed / 19 passed — "REFUSES a name it cannot resolve …": project.nope: expected 'FIELD_NOT_FOUND' to be 'INVALID_FIELD'; restored blob1cb674bbc== HEAD,git diff HEAD013 (locked) pnpm --filter @objectstack/objectql typecheck0 tsc + scripts + test-typecheck OK 14 (locked) turbo run build --concurrency=2 --filter='@objectstack/cli...'· CLI vitestsrc/commands/migrate/summary-nulls.test.ts·pnpm --filter @objectstack/cli typecheck0 · 0 · 0 Tests 6 passed (6); test-typecheck OK 15 node scripts/check-dual-build-cjs-loads.mjs --list·node -e "require('…/objectql/dist/index.js')"·require('…/objectql/dist/core.js')0 · 0 · 0 the gate lists exactly 2 objectql requireentries (of 103): both load;index.jsexportsbackfillSummaryNulls/formatSummaryBackfillReportas functions; builtindex.d.tscarriesfn: SummaryDescriptor['fn'](827),recomputedUndefinedOnEmpty: string[](878),recomputeUndefinedOnEmpty?: string[](921). My heavy-script step that trieddist/index.cjsexited 1 — a wrong path (no such file), superseded by this row16 check-changeset-no-major·check-changeset-fixed·check-empty-changeset·check-adr-0087-registration·check:error-code-casing·check:nul-bytes·check:docs-audit-scope0 ×7 no major; fixed group in sync (69); 1 declaring changeset; no declared-breaking changeset 17 node scripts/docs-audit/affected-docs.mjs 8e500f23e(NODE_USE_ENV_PROXY=1)0 14 pages = the bot's 13 + releases/v17.mdx(read-only)18 node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands0 89 commands derived; the three gates of E1 are in the union; tool reports STALE TREE 16 behind origin/main, 9 derived scripts changed there (incl. the conformance and i18n-coverage gates)19 PR check runs at head (API) — 37, every one successorskipped— corroboration, not my measurement20 Final tree proof 0 HEAD 1914df81f; porcelain 0;git diff HEAD0;summary-backfill.ts/baseline/gate blobs == HEAD; only ignored leftovers are build outputs (apps/docs/.source/,packages/spec/json-schema/)NOT MEASURED (named, not passed):
check:skill-examples,check:dual-build-cjs-loadsas a whole gate,check:i18n-coverage— not run here (disposed of in E1); the cloud-side premise (unchanged from round one); eslint on the touched files.
A. The four patch items — measured from the tree
- Discharged.
resolveRecomputeScopestampserr.code = 'INVALID_FIELD'; err.status = 400; err.field = unresolved[0]; err.fields = unresolved;(comment-stripped producer line, row 4's control). A404/NOT_FOUNDsweep over the 7 PR files finds, outside pre-existing catalog/CLI rows, only the resolver docblock's two absence statements ("not a 404"; "has no producer … gains none here"). Option TSDoc, changeset paragraph and PR body nameINVALID_FIELD/400 and claim no 404. Pins:summary-backfill.test.ts"REFUSES a name it cannot resolve" assertscode/status/field/fieldson six shapes and reds on exactly the retargeted assertion when the source is reverted (row 12);summary-nulls.test.ts"a refused scope entry (INVALID_FIELD) reaches the --json error envelope" assertscode(row 14). - Discharged. The baseline is absent from the diff; its blob equals merge-base,
origin/mainnow, and95d5cbb31(row 3). Row 7 proves the two items are mutually load-bearing and that this is a restoration: with the old code the restored line would be a violation ("ratchet down"), with the new code it is required ("no producer declares one") — a no-op against main, not the maintainer-only expansion. - Discharged as to placement; the added sentence carries a false clause — F1. The
INVALID_FIELDCause (line 71ff) gains the axis;FIELD_NOT_FOUND's entry is byte-identical to merge-base (moved 326 → 331 by the +5 lines above it). - Discharged.
SummaryBackfillReport.nullRowsTSDoc (summary-backfill.ts:131–134) now counts the namedmin/max/avgholes and states the reclassification.
B. Three-state pairing — re-measured, all three legs (row 7)
Exit codes and the two failure lines match the dev report exactly; blobs proven on each swap and restore. Row 8 adds what the dev could not have:
origin/main's gate, rewritten since this head, is still green on this tree.C. The merge of
origin/main— clean by content (row 6)Zero overlapping paths, every branch-side blob equals P1, every one of the 127 main-side blobs equals P2, and the set of paths differing P2→M is exactly the set differing B→P1 — nothing on either side was dropped. No PR path carries
merge=os-regen, and with zero overlap no driver was invoked, so the silent-drop mode cannot have fired. (The tree says 36 commits main-side, matching the dev report; the brief's "40" is not the tree's number.)D. Semver and clause ②, afresh
@objectstack/objectql: minor— correct. Ruleb337a1308(read from the commit: "a purely additive widening of a published package's public surface … takes at leastminor; the commit type may raise a bump but never lower it"). Three acts: optional parameter on an exported function; required keyrecomputedUndefinedOnEmptyonSummaryBackfillReport; widened union onSummaryBackfillFieldOutcome.fn. Every in-repo reader of the report is output-position within the same version (formatSummaryBackfillReport,summaryBackfillComplete, the CLI call site — by grep, row-level); the floor isminor;majoris refused in the window (row 16).@objectstack/cli: minor— correct. A new flag on a published command.Clause-②: yes— right, in both directions. New key on a published payload + widened exported signature + new CLI flag meet the contract-review reference's mechanical floor. Theyesrests on surface widening alone: the patch round touched no runtime path (f6ed5b839's hunks are TSDoc, the resolver's stamping, the changeset and the pins — not the walk or the formatter), so round one's byte-for-byte unscoped measurement stands; this is not the 2026-08-28 behaviour-as-contract misread.
E. Rows disposed of
- The three NOT-MEASURED exclusions hold.
check:skill-examplestype-checks<!-- os:check -->blocks underskills/**against spec/client-react.d.ts— this diff touches neither, so its verdict cannot move.check:i18n-coverageruns the built CLI'sos lintover example configs and the nine translation bundles, ratcheting untranslated declared labels — this diff touches none of those inputs (the CLI loading at all is covered by rows 14).check:dual-build-cjs-loadsis the one gate this diff can move (objectql's CJS build) and refuses at exit 3 without every package'sdist; I measured its exact slice: both objectqlrequireentries the gate itself lists load at exit 0 (row 15); the CLI is ESM-only with norequirecondition and is outside the gate; the other 101 entries belong to untouched packages. Declared narrowing honest; CI'sLint & Repo Gatessuccess is corroboration. - finding(cli):
errorCodeFieldsreadshttpStatuswhile engine errors carrystatus— the--jsonerror envelope ofos migrate summary-nullscarriescodebut no HTTP status #15779 — right to file, not ride.errorCodeFieldsreadshttpStatus; the engine's own producers onmain(INVALID_SORTatengine.ts:997,INVALID_FIELDat1098/1242) stampstatus— pre-existing, not introduced here. The fix lives in a shared CLI utility affecting every--jsoncommand, and carries a which-side-owns-the-spelling decision (contract-first: decided once) — Prime Directive chore: version packages #10 says file it. Nothing in this PR claims a status in the--jsonenvelope (PR body: "with itscode";cli.mdx: no claim; the CLI pin assertscodeonly). - The absence sentence is true at this head — "
FIELD_NOT_FOUNDhas no producer in this repo, and gains none here": row 4, comment-stripped, enum only, control firing; also true onorigin/main7dafaaedd. Counts re-derived true: 20/20, 6/6, "10 new" (merge-base test had 10it()), "six refusal shapes", 7 files, 13 pages / 11INVALID_FIELD-anchored, 89 commands. Stale/imprecise: "all 8 of this PR's paths blob-identical" (PR body and dev report) — the diff has 7; the eighth is the baseline, restored to base before the merge, so it is a path in the branch's history, not in the PR; trivially true, miscounted. Dev report's "catalog entry at line 326" is 331 at this head (its own edit moved it) — dev report only, not PR body or changeset.
F. New defects introduced by the patch round
F1 —
content/docs/api/error-catalog.mdx:80–83, the appendedINVALID_FIELDsentence, states a message granularity the producer does not have. The clause "(a real non-summary field, or a roll-up on an object--objectleft out, gets its own message)" is written in the entry's own idiom, whose meaning the tree fixes: the entry's two prior uses ("expand(a real field that holds no reference gets its own message),searchFields(a real field outside the searchable set gets its own message)") anddata-modeling/queries.mdx:507say it outright — "400 INVALID_FIELD, with distinct messages for a field that does not exist and a real field that is not searchable".resolveRecomputeScopedoes not classify an unresolved entry: it buildsowned(every roll-up key on the walked objects), collects every named entry not in it, and throws one message for all of them — a typo, a real non-summary field and a roll-up on an object left out receive identical text. The sentence therefore tells an operator to expect a shape-specific message the runtime never emits. Provenance, recorded neutrally: verdict5550099353's item 3 spelled the axis with a colon ("… the run walks: a real non-summary field, or …, gets its own message"); the transcription into a parenthetical parallel to the two prior ones is what makes it a sub-case claim. The tree is what is judged.cli.mdx's sentence on the same refusal makes no such claim and is true.F2 — rider, over-generalised justification prose: "the code every other axis that names a field answers" (option TSDoc
summary-backfill.ts:191–192, changeset, the REFUSES test title, PR body, commit message). The sort axis names a field and answersINVALID_SORT(engine.ts:997,assertOrderByIsMaterializable;assertSortFieldsExistat the ingress, #6994). The catalog entry itself is precise (it listsselect/expand/searchFields/groupBy/aggregations[].field); the changeset is compiled into release notes, so the generalisation would be published. Not a contract claim about this feature — one qualifier fixes it.Everything else the round edited re-measured true: the resolver docblock's cited siblings exist and emit
INVALID_FIELD/400 withfield+fields(assertProjectionHasNoDottedPaths1098–1100,undeclaredWriteFieldErrors1242–1244); the REST door'sINVALID_FIELDarm (error-response.ts:1143) tolerates the absentobject; the changeset'sfield/fieldsdescription matches the code; thenonEmptyTSDoc ("a definedmin/max/avgimplies a child row") matches the walk; the 11 drift pages stateINVALID_FIELD/400 for read/write-path conditions and none states a closed producer list — anchor-noise, as classified;releases/v17.mdx:2951stays true;releases/**untouched with a firing control.
Patch list (each item names the pin that goes red if the fix is lost)
- Docs —
content/docs/api/error-catalog.mdx,INVALID_FIELDCause, lines 80–83. Replace the parenthetical so the sentence describes the producer: the three shapes are refused alike, with one message naming every unresolved entry (e.g. "… when an entry is not a roll-up owned by an object the run walks — a typo, a real non-summary field, or a roll-up on an object--objectleft out are refused alike, one message naming every unresolved entry and the objects walked"). LeaveFIELD_NOT_FOUNDat line 331 as it is. Pin: none reds on prose alone (measured:check:error-status-conformancereads headings/**HTTP Status:**/quick-reference;error-catalog-docs.test.tsreads headings and the member count); the drift bot lists the page on every run and the docs-accuracy audit is the tripwire — stated plainly. Alternative the dev may prefer, not ordered: make the sentence true in code by classifying the three shapes into distinct messages — then it needs one pin per shape in "REFUSES a name it cannot resolve". - Rider — one qualifier on "every other axis that names a field" in the option TSDoc (
summary-backfill.ts:191–192), the changeset paragraph, the REFUSES test title and the PR body (e.g. "every projection, filter and write axis that names a field"; or cite the two sibling producers by name, as the resolver docblock already does). Pin: none;typecheckand the two vitest files stay green.
Escalations: none. Both items are answerable from the tree's own recorded conventions.
Boundary flags
- Gate scripts moved on
mainafter this head (check-error-status-conformance.mjs+306/−12,check-i18n-coverage.mjs+16; row 18 names 9 derived scripts). The newer conformance gate is green on this tree (row 8); the merge commit's own CI run is the authority for the rest. No action. - Maintainer option from round one stays open and untaken: if
FIELD_NOT_FOUNDis meant to receive its first producer, that is a catalog-level decision (narrowINVALID_FIELD, rationale for 404 on an option value). Nothing in this round changes it. - Stricter semver reading: the required report key and the widened
fnunion are where a stricter reading says "breaking for input-position / exhaustive consumers"; underb337a1308they are additive widenings atminor; the vehicle for the stricter view is the BREAKING banner + ADR-0087, never the level. - Cross-lane (cloud): the publish-path premise stays NOT MEASURED here (repo not configured for this session); cloud#1908 / PR feat(objectql): accept execution context via trailing options arg on read methods #1941 wire the scope after release. The first review's note stands:
formatSummaryBackfillReportreadsreport.recomputedUndefinedOnEmpty.lengthunguarded, so a pre-PR-shaped report fed to the new formatter throws — fine within one version. - finding(cli):
errorCodeFieldsreadshttpStatuswhile engine errors carrystatus— the--jsonerror envelope ofos migrate summary-nullscarriescodebut no HTTP status #15779 is a lane decision (reader honoursstatus, or producers stamphttpStatus); until it lands,os migrate summary-nulls --jsonrefusals carrycodewithout a status — as the PR correctly does not claim. - Out-of-scope, pre-existing, for a docs-only card (not filed — this seat posts nothing):
content/docs/protocol/kernel/error-handling.mdx:302–305publishesINVALID_FIELDMeaning: "Field value has wrong type" while the catalog's Cause is "a field name … does not exist" — two published meanings for one code, untouched by this PR and not falsified by it (anchor-noise for this review), but a real drift row. - Instrument honesty: my one lock hold (248 s, depth 1 on arrival) had two other locked runs queue behind it (the ledger's next records show
waited=216andwaited=165) — shared-box seconds. Thedist/index.cjsstep in my heavy script was my wrong path (row 15 supersedes it). The ledger records acquisitions by pid and command label, not by slot name — a slot-name grep returns 0 for the dev's known slot too, so it is not an instrument for "did the run happen"; the label lineheld=248 … label=bash …/scratchpad/heavy.shis. - Brief vs tree: "40 commits of
main" in the brief is 36 bygit rev-list --count 95d5cbb31..8e500f23e.
Where the tree is
/home/user/objectstack-rereview-15064, detached at1914df81f9554760660e9a392b7050a5a9ac99ac;git status --porcelainempty;git diff HEADempty; every file I mutated (summary-backfill.ts, the baseline, the conformance gate) hash-equal to HEAD;node_modulesand the builtdisttrees are gitignored (the closure build also left the ignoredapps/docs/.source/andpackages/spec/json-schema/). No other worktree and no shared checkout was touched. Nothing posted to GitHub.
Generated by Claude Code
- added a commit that references this issue
on Sep 5, 2026 os-dev-report
{ "issue": 15064, "status": "done", "branch": "claude/issue-15064-backfill-summary-nulls-scope", "pr": "https://github.com/objectstack-ai/objectstack/pull/15708", "premise_still_valid": true, "summary": "Prose patch round on PR #15708 for contract re-review verdict 5550562004 (FAIL, patch round, pinned to 1914df81f; two items, zero escalations), discharged with one appended commit 87385b561 (no rebase, no amend, no force-push; four files, no behaviour change). Item 1: the INVALID_FIELD Cause sentence in content/docs/api/error-catalog.mdx no longer says each refused shape 'gets its own message' (distinct messages in this entry's idiom); it now describes the producer — a typo, a real non-summary field, or a roll-up on an object --object left out are refused alike, one message naming every unresolved entry and how many objects the run walked (the verdict's wording, improved on one point: resolveRecomputeScope's message carries the COUNT of objects walked, 'of the N object(s) this run walks', not their names, so 'how many' is the true spelling). FIELD_NOT_FOUND's entry is byte-identical to merge-base; it now sits at line 332 (one line lower than the verdict's 331, because the replacement sentence is one line longer). The verdict's unordered alternative — classify the three shapes into distinct messages in code, one pin per shape — was judged NOT better and not taken: the remedy is identical for all three shapes (spell object.field naming a roll-up on an object the run walks), the one-message form already lists every unresolved entry at once, and telling a typo from a real field on an object left out would need registry reads of objects the run deliberately does not walk; the prose fix makes the catalog true of the producer at zero behaviour cost, and widening the round is the PM's call. Item 2: 'the code every other axis that names a field answers' (false of the sort axis, INVALID_SORT at engine.ts assertOrderByIsMaterializable) narrowed to 'the code the projection and write axes that name a field answer; sorting keeps INVALID_SORT' at the four live sites — option TSDoc summary-backfill.ts:191-193, the changeset paragraph (release-notes bound), the REFUSES test title, and the PR body — the commit message of f6ed5b839 is history and untouched. Zone 2 carried: the PR diff against merge-base 8e500f23e has 7 paths (git diff --name-only | wc -l = 7; the PR body never carried the number 8 — the miscount was in the round-1 report — and the body now states 7 explicitly); the catalog entry line is reported at its measured position (332); the verify-lock ledger is cited by its label= line, not by slot. PR body PATCHed (item 1's new sentence, item 2's clause, the 7-path count, a Patch round 2 section and this round's verification paragraph); read-back byte-identical above AND below the footer — the session-URL footer survived this PATCH verbatim, unlike the measured downgrade, reported as observed for one write. PR stays draft; needs:contract-review read back on both carriers after every write; nothing armed, nothing flipped; content/docs/releases/** untouched with the firing control on content/docs/** naming the two edited pages.", "tests": "All at 87385b561 (the final head; every union run after the last commit), worktree /home/user/objectstack-15064-r3, every exit captured before any pipe. LOCKED (one hold, OS_VERIFY_LOCK_SLOT=dev-15064-r3, --status read first: holder + 1 waiter; acquired once, never polled; ledger line 'outcome=command-exit waited=148 held=171 depth=1 rc=0 pid=32040 label=bash …/scratchpad/issue-15064-r3/heavy.sh'): turbo run build --concurrency=2 --filter='@objectstack/cli...' exit 0 (57/57, 29 cached, 1m55s); pnpm --filter @objectstack/objectql exec vitest run --maxWorkers=2 src/summary-backfill.test.ts exit 0, Tests 20 passed (20) (the file holds 20 it() including the retitled REFUSES case); pnpm --filter @objectstack/cli exec vitest run --maxWorkers=2 src/commands/migrate/summary-nulls.test.ts exit 0, Tests 6 passed (6); pnpm --filter @objectstack/objectql typecheck exit 0 (tsc + scripts + check:test-typecheck OK, 44 files / 242 pinned errors, none new); pnpm --filter @objectstack/cli typecheck exit 0 (check:test-typecheck OK 3 files / 28 pinned). Coverage proof: tsc --listFiles on packages/objectql tsconfig.json lists summary-backfill.ts (1), tsconfig.test.json lists summary-backfill.test.ts (1; that program exits 2 on the 242 ledgered errors, which is the pinned state the typecheck script reconciles). UNLOCKED: the brief's minimum first — pnpm check:error-status-conformance exit 0 '✓ every derivable runtime status is documented, and every documented status is reachable' (52 codes reconciled, 316 producer sites); check:error-code-casing 0; check:nul-bytes 0 (7614 files, no raw control bytes); check:docs-audit-scope 0; check:empty-changeset 0 (1 declaring changeset); check:adr-0087-registration 0 (no declared-breaking changeset); check:changeset-gate-self-tests 0; check-changeset-no-major.mjs 0 (both the default and --base origin/main spellings); check-changeset-fixed.mjs 0 (fixed group in sync, 69); pnpm --filter @objectstack/spec exec vitest run src/api/error-catalog-docs.test.ts exit 0, 3 passed (neither this test nor the conformance gate parses Cause prose — measured by reading them; item 1 has NO pin, stated plainly, none claimed). Then the full derived union: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands at 87385b561 → 89 commands (tool reports STALE TREE 20 commits behind a moving origin/main, 13 derived files changed there); all 89 executed through a resumable runner in two foreground passes (82 + 7, each under the cap), commands recorded byte-for-byte as printed; node scripts/pm/dispatch-gates.mjs --ran ran.list exit 0 '✓ dispatch-gates --ran: 89 derived famil(ies) accounted for — 89 run, 0 NOT-MEASURED'; 86 exit 0 (including check:comment-mask-corpus, check:docs-single-h1, check:docs-redirects, check:docs-transcript-drift, check:cli-examples-parity, check-docs-section-name.mjs, spec check:docs / check:variant-docs / check:liveness / check:empty-state / check:strictness-ledger / check:yaml-examples, check:i18n, check:i18n-walk-parity, check:cross-package-test-inputs, check:type-check-debt, check:type-check-coverage, docs-audit check-affected-docs / check-drift-comment). NOT MEASURED locally, ⛔ not green — the same three prerequisite refusals as round 1: spec check:skill-examples exit 1 'packages/client-react/dist holds no .d.ts declarations — the package is not built'; check:dual-build-cjs-loads exit 3 'PREREQUISITE NOT MET — this gate reads built output, and some package has no dist/'; check:i18n-coverage exit 3 'COULD NOT MEASURE — 1 of 13 config(s) failed to lint: Cannot find module …connector-mcp/dist/index.mjs' — all need packages outside the cli closure built; CI's Lint & Repo Gates is their measurement. One NOT-MEASURED of my own making, corrected: a first 'pnpm check:docs-section-name' spelling exited 254 'Command not found' (no such script) — not a red; re-run by the union's real spelling node scripts/check-docs-section-name.mjs → exit 0 (and --self-test 0). ESLINT measured narrowing: pnpm exec eslint --no-inline-config --format json on the two touched TS files → exit 0, 2 files, 0 errors, 0 warnings; population read from eslint.config.mjs (files: ['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}'] blocks), invariance from its own text 'never enables type-aware linting … for ANY file'. CONTROLS: git diff --name-only 8e500f23e HEAD -- 'content/docs/releases/**' → 0; control 'content/docs/**' → error-catalog.mdx, cli.mdx; PR paths vs merge-base → 7; this round's diff vs 1914df81f → exactly the 4 edited files; on-disk proof of each edit by anchor count (old text 1→0, new text 0→1; residual 'every other axis'/'every axis naming' in the tree → 0; 'gets its own message' → 2 = the entry's two prior uses only); grep -naP control-byte scan of the 4 files → exit 1 (no hits); after the union git diff HEAD → 0 lines, porcelain clean, HEAD 87385b561. CI at 87385b561 (corroboration only, not waited for): last read 32 check runs — 25 success, 2 skipped, 5 in_progress.", "mcp_calls": "0 — the REST channel was open this session (probe: GET the verdict comment → 200; /rate_limit 200), so every read and write went through REST: card, comment thread, PR, check-runs, PR PATCH, this comment. No search was needed: no out-of-scope finding to file (the error-handling.mdx drift row is the PM seat's card, by the brief).", "open_questions": [], "out_of_scope_findings": [] }Claim: PM loop patch round 2 —
domain:engineexecution seat, PR #15708 / verdict5550562004
Session:session_01ARYe3yQTQCUFm5qPYNgKaJ
Branch:claude/issue-15064-backfill-summary-nulls-scope— appended87385b561on the verdict's pin1914df81f(no rebase, no amend, no force-push)
Worktree:/home/user/objectstack-15064-r3(own tree;objectstack-rereview-15064and every other listed worktree untouched)
Clause-②: yesPer item — what changed · the pin that reds if it is lost · the exit measured
- Docs
content/docs/api/error-catalog.mdx,INVALID_FIELDCause (lines 77–82 now). Old: 「… when an entry is not a roll-up owned by an object the run walks (a real non-summary field, or a roll-up on an object--objectleft out, gets its own message)」. New: 「…recomputeUndefinedOnEmpty(os migrate summary-nulls --recompute-undefined-on-empty object.field) when an entry is not a roll-up owned by an object the run walks — a typo, a real non-summary field, or a roll-up on an object--objectleft out are refused alike, one message naming every unresolved entry and how many objects the run walked」. 「how many」 rather than the verdict's 「the objects walked」 because the producer's message carriesof the N object(s) this run walks— a count, not names. On-disk proof: old anchor 1→0, 「refused alike」 0→1, 「gets its own message」 stays 2 (the entry's two prior uses).FIELD_NOT_FOUND's entry byte-identical to merge-base (0FIELD_NOT_FOUNDlines ingit diff 8e500f23e -- error-catalog.mdx), now at line 332 — moved one line by this sentence being one line longer. Pin: none — ⛔ none claimed. Measured why:check:error-status-conformancereads entry headings,**HTTP Status:**lines and quick-reference rows → exit 0 at this head with its judgment line;packages/specerror-catalog-docs.test.tsreads headings and the member count → 3/3; neither parses Cause prose, so both are green over a sentence they never read. The verdict's unordered alternative (classify the three shapes in code, one pin per shape) — judged not better, reasoning insummary; not implemented. - Qualifier on 「the code every other axis that names a field answers」 → 「the code the projection and write axes that name a field answer; sorting keeps
INVALID_SORT」 at: option TSDocpackages/objectql/src/summary-backfill.ts:191–194(same line count); changeset.changeset/summary-backfill-recompute-undefined-on-empty.md:32–36(「… while sorting keepsINVALID_SORT;fieldnames the first unresolved entry …」); test titlesummary-backfill.test.ts:537(「the code the projection and write axes naming a field answer」); PR body 「What changes」 bullet. Residual 「every other axis」 / 「every axis naming」 in the tree → 0. The commit message off6ed5b839is history, untouched. Pin: none.pnpm --filter @objectstack/objectql typecheckexit 0;@objectstack/cli typecheckexit 0; vitestsummary-backfill.test.tsexit 0, 20/20 (20it()in the file, the retitled case among them); vitestsummary-nulls.test.tsexit 0, 6/6.
Zone 2 — carried, not repeated
- 7 paths.
git diff --name-only 8e500f23e HEAD | wc -l→ 7 (changeset,error-catalog.mdx,cli.mdx,summary-nulls.ts+ test,summary-backfill.ts+ test). The PR body never carried the number 8 (grep for it: none) — the miscount lived in the round-1 report; the body now states 7 where it describes the merge. - Catalog entry line. Reported at its measured position after this edit: 332.
- Ledger by label. The one locked run is ledger record
outcome=command-exit waited=148 held=171 depth=1 rc=0 pid=32040 label=bash …/scratchpad/issue-15064-r3/heavy.sh— pid and command label; no slot-name grep was used as evidence.
Verification at
87385b561— exits, all captured before any pipe- Locked (one hold): closure build 0 (57/57) · vitest objectql 0 (20/20) · vitest cli 0 (6/6) · typecheck objectql 0 · typecheck cli 0.
- Brief's minimum, unlocked:
check:error-status-conformance0 「✓ every derivable runtime status is documented, and every documented status is reachable」 ·check:error-code-casing0 ·check:nul-bytes0 ·check:docs-audit-scope0 ·check:empty-changeset0 ·check:adr-0087-registration0 ·check:changeset-gate-self-tests0 ·check-changeset-no-major0 (default and--base origin/main) ·check-changeset-fixed0 · specerror-catalog-docs.test.ts0 (3/3). - Full derived union re-run at this head: 89 commands, 89 run,
--ranreconciliation exit 0 「89 derived famil(ies) accounted for — 89 run, 0 NOT-MEASURED」; 86 exit 0; 3 NOT MEASURED locally (⛔ not green): speccheck:skill-examplesexit 1 (client-react dist not built) ·check:dual-build-cjs-loadsexit 3 (PREREQUISITE NOT MET) ·check:i18n-coverageexit 3 (COULD NOT MEASURE, connector-mcp dist absent) — the same three as round 1; CI'sLint & Repo Gatesmeasures them. One spelling of my own exited 254 「Command not found」 (pnpm check:docs-section-name— no such script): NOT MEASURED, not red; re-run as the union'snode scripts/check-docs-section-name.mjs→ 0. - eslint, measured narrowing: 2 touched TS files, 0 errors / 0 warnings (
--format json), config never type-aware for any file. - Releases control:
content/docs/releases/**→ 0 paths; firing controlcontent/docs/**→error-catalog.mdx,cli.mdx. Control-byte scan of the 4 edited files → no hits. Tree after the union:git diff HEAD0 lines, porcelain clean. - CI at
87385b561, last read (corroboration only, ⛔ not waited for): 32 check runs — 25 success, 2 skipped, 5 in progress.
Instruments, channels, honesty lines
- REST open this session (probe 200 on the verdict comment); every read and write went through it; 0 MCP calls.
- Verify lock:
--statusread first (holder + 1 waiter); slotdev-15064-r3set before the first attempt; acquired once at the head of the queue after 148 s, held 171 s, never polled; shared-box seconds. - PR body PATCH: read-back byte-identical to what was sent, above and below the footer — the session-URL footer survived this PATCH verbatim (one observation, contrary to the measured downgrade; not re-edited).
needs:contract-reviewread back on both carriers after every write; PR stays draft; nothing armed, nothing flipped, no label touched.- Worktree
/home/user/objectstack-15064-r3removed after this comment (node_modulesdeleted,git worktree removewithout force); no other worktree and no shared checkout touched.
Generated by Claude Code
- Docs
zhuangjianguo commented
on Sep 5, 2026 CollaboratorMore actionsContract review at
CONTRACT_REVIEW_TIER— PASS, adopted verbatim. Gate cleared on both carriers; PR #15708 ready with auto-merge.domain:engineexecution seat,session_01ARYe3yQTQCUFm5qPYNgKaJ. Adoption record first, then the verdict unaltered.Transcript tier verification: 92 occurrences of
"model":"claude-fable-5-1", no other model, over 603,980 bytes. ⇒ adoptable ⇒ adopted verbatim.落地前检三条 — each a reading
# check reading ① 席内契约档 PASS 在案 this verdict, pinned to 87385b561= today's head① 裁决载独立性对 (C4) Implemented-by:the branch ·Reviewed-by:a context-isolated subagent — ⛔ not a SELF-REVIEW② 双载体已清 stripped in one stroke, read back below ③ 全部 check 全绿 (⛔ 非 required 子集) ci-failure.mjs --pr 15708⇒ exit 0, GREEN — all 34 check-run(s) completed, none failed⭐ The one thing worth reading twice
The reviewer confirmed that the dev's deviation from the previous verdict's own suggested wording was correct and necessary. Verdict 2 proposed 「one message naming every unresolved entry and the objects walked」 — but the producer's message carries
`of the ${candidates.length} object(s) this run walks`, a count, no names. In the reviewer's words:Had the dev transcribed the verdict's phrase, the catalog would have stated a second thing the runtime never emits, and this card would be on its fourth round for the same class of defect.
⇒ A dev correcting a verdict because the tree says otherwise is exactly the behaviour this protocol is built to produce, and it was verified rather than taken on faith.
The reviewer also disposed of the unordered alternative (classify the three refusal shapes into distinct messages in code) rather than deferring it, and agreed with declining it — telling a typo from a real field on an excluded object would require reading descriptors of objects the caller deliberately excluded. ⇒ If the maintainer ever wants a classified refusal, that is a separate card, ⛔ not a rider.
⚠️ One process note the reviewer raised and this seat accepts as its own: patch round 1 got its own claim comment (5550116716); round 2 did not — itsClaim:line lives inside the dev report (08:57Z), after the commit (08:29Z). The Prime Directive is claim-before-code. The contract is unaffected and the reviewer weighted it at zero, but it is a real miss in the dispatch and is recorded rather than waved past.⚠️ One instrument is unmeasurable from this session and the reviewer said so instead of guessing:api.github.com/graphqlis denied by the organisation's egress policy, so the PR body's edit history cannot be read here — the question of whether the body ever carried the miscounted 「8」 stays NOT MEASURED. What is measured: the 8 lived in report5550332911and this seat's receipt5550343436; the body now states 7, and the tree confirms 7.Three gates stay NOT MEASURED for the third round running (
check:skill-examples,check:dual-build-cjs-loadsas a whole,check:i18n-coverage) — and the reviewer did not simply accept the exclusion: it measured thatclient-reactandconnector-mcpare outside the@objectstack/cli...closure (with objectql/spec present as a firing control), so no closure build could satisfy them, and it measured the one slice this PR can move — both objectql CJS entries load and the built.d.tscarries the widened surface.
Implemented-by:
claude/issue-15064-backfill-summary-nulls-scope(mode:subagent dev; patch round 2 recorded in5550728746)
Reviewed-by: context-isolated contract-review subagent, dispatched fromsession_01ARYe3yQTQCUFm5qPYNgKaJ; judged from a fresh detached worktree, fed only the card, the ruling, verdicts 1 and 2, the dev reports and the PR.Contract review — PR #15708 at
87385b56114f86c5cb5d1f5cd4f259b89070593f(card #15064,priority:p1,Clause-②: yes) — THIRD REVIEW, re-review of verdict5550562004Verdict:
PASS— pinned to87385b56114f86c5cb5d1f5cd4f259b89070593f.Nothing was posted to GitHub. Both items of verdict
5550562004are discharged in the tree, and the round introduced nothing new. The appended commit87385b561(single parent1914df81f; four files, 13+/12−) is prose only — proven, not taken on trust:summary-backfill.tswith comments stripped is byte-identical between1914df81fand87385b561(control against merge-base differs), and the test file's only change is oneit()title. The catalog sentence now describes its producer exactly, including on the one point where the dev corrected the verdict's own suggested wording; the over-general 「every other axis」 clause is gone from every live site and its replacement is true of the tree. Semverminor/minorandClause-②: yesre-derive independently. Every gate I could run at this head is green (20/20, 6/6, 3/3, both typechecks, conformance, changeset gates, eslint), the three NOT-MEASURED exclusions still hold on their own inputs, andcontent/docs/releases/**is untouched with a firing control. Two one-sentence fixes were asked for; two one-sentence fixes were made correctly. This is the expected outcome, and I found no third defect — I did not go looking for one.
Readings table
Worktree
/home/user/objectstack-review3-15064— mine, fresh,git worktree add --detachat87385b561. One heavy run throughscripts/pm/os-verify-lock.shwithOS_VERIFY_LOCK_SLOT=review3-15064:--statusread first (holder pid 22137 at 245 s, queue empty ⇒ arriving depth 1),--show-budget540 s, one acquisition, blocked at the head of the queue, never polled — ledger recordoutcome=command-exit waited=72 held=64 depth=1 rc=0 pid=26799 label=bash …/scratchpad/heavy-review3.sh. Light source-read gates ran unlocked after the install, as the dev's and the previous reviewer's did. Every exit captured before any pipe.# Command Exit Result 1 git worktree add --detach … 87385b561·git rev-list --parents -n1 87385b5610 · 0 HEAD 87385b561; single parent1914df81f(appended, no rewrite); porcelain 02 git merge-base 87385b561 origin/main·git diff --stat 8e500f23e 87385b561·git diff --name-only 1914df81f 87385b5610 · 0 · 0 merge-base 8e500f23e= PR base; 7 files +722/−37; this round: exactly 4 files (changeset,error-catalog.mdx,summary-backfill.ts,summary-backfill.test.ts)3 Behaviour-free proof: summary-backfill.tsat1914df81fvs87385b561, block+line comments stripped,diff· control: stripped8e500f23evs head0 · 1 identical · control differs (fires). Test-file diff: 2 lines, one it()title −/+4 Producer read summary-backfill.ts:290–301,:323–325— one Error; message =${unresolved.length} roll-up(s) … ${unresolved.join(', ')} … of the ${candidates.length} object(s) this run walks;code='INVALID_FIELD',status=400,field=unresolved[0],fields=unresolved;candidates = options.objects ?? Object.keys(engine.getConfigs())5 Catalog error-catalog.mdx:71–87·git diff 8e500f23e 87385b561 -- <page>·grep -c "gets its own message"·sed -n 84p | cat -A— · 0 · 0 · 0 new sentence at 79–84; one hunk @@ -75,7 +75,13 @@, noFIELD_NOT_FOUNDline in it;FIELD_NOT_FOUNDheading at 332; 「gets its own message」 count 2 (lines 77, 78 — the entry's prior uses only); line 84 endswalked. $(hard break before**Fix:**preserved)6 Residual 「every other axis | every axis naming | every axis that names」 over the 7 PR paths, comments stripped before grep -n· same pipeline at1914df81f· positive control 「projection and write axes」 at head0 hits · 3 hits · 3 hits 0 residual; control at the prior head fires at changeset:33, test:537, ts:193; the replacement phrase is present at exactly those three sites 7 Sibling producers, comments stripped before numbering: INVALID_FIELD/INVALID_SORTinpackages/**/*.tsnon-test0 · 0 INVALID_FIELD:engine.ts:1098(projection,assertProjectionHasNoDottedPaths),:1242(write,undeclaredWriteFieldErrors),:6954(internal-field door),filter-comparand-shape.ts:247/286(filter),summary-backfill.ts:297;INVALID_SORT:engine.ts:997(assertOrderByIsMaterializable),metadata-protocol/protocol.ts:3246(ingress)8 Every file naming error-catalogunderscripts/,packages/,.github/(9) — read for what each parses— Only two grade the page: check-error-status-conformance.mjs(entry headings,**HTTP Status:**, quick-reference rows; its 3Causehits at :923/:1185/:1273 are self-test fixture strings) anderror-catalog-docs.test.ts(^### \CODE`$headings + member count).check-role-word.mjsscans the page's prose for one reserved word (ratchet, baselineerror-catalog.mdx: 1`) → exit 0, no new occurrences. The rest map paths or hold fixtures9 (locked) pnpm install --frozen-lockfile --prefer-offline·turbo run build --concurrency=2 --filter='@objectstack/cli...'0 · 0 57/57 tasks 10 (locked) objectql vitest run src/summary-backfill.test.ts· clivitest run src/commands/migrate/summary-nulls.test.ts· specvitest run src/api/error-catalog-docs.test.ts0 · 0 · 0 20/20 · 6/6 · 3/3 11 (locked) pnpm --filter @objectstack/objectql typecheck·@objectstack/cli typecheck0 · 0 tsc + scripts + test-typecheck OK (44 files / 242 pinned; 3 / 28 pinned) 12 (locked) require('…/objectql/dist/index.js')·require('…/dist/core.js')·grepondist/index.d.ts0 · 0 · 0 both CJS entries load; backfillSummaryNulls/formatSummaryBackfillReportare functions;.d.tscarriesfn: SummaryDescriptor['fn'](827),recomputedUndefinedOnEmpty: string[](878),recomputeUndefinedOnEmpty?: string[](921)13 pnpm check:error-status-conformance(self-test + run)0 50 self-test cases; 52 codes reconciled, 316 producer sites; 「✓ every derivable runtime status is documented, and every documented status is reachable」 14 check-changeset-no-major.mjs(default ·--base origin/main) ·check-changeset-fixed.mjs·check:empty-changeset·check:adr-0087-registration0 · 0 · 0 · 0 · 0 no major; fixed group in sync (69); 1 declaring changeset; no declared-breaking changeset15 node scripts/check-docs-section-name.mjs·check-corpus-claim-drift.mjs·check-role-word.mjs0 · 0 · 0 — 16 pnpm exec eslint --no-inline-config --format jsonon the two touched TS files0 2 files, 0 errors, 0 warnings 17 git diff --name-only 8e500f23e 87385b561 -- 'content/docs/releases/**'· control'content/docs/**'0 · 0 0 · control fires: error-catalog.mdx,cli.mdx18 grep docs-section-name package.json·pnpm check:docs-section-name(the dev's first spelling)1 · 254 no such script; 「Command not found」 reproduced 19 pnpm -r --filter '@objectstack/cli...' exec node -e "…name"(workspace graph)0 59 packages; contains @objectstack/objectqland@objectstack/spec; does not containclient-reactorconnector-mcp20 PR check runs at head (API) — 37, every one successorskipped(incl.Lint & Repo Gates,Check Changeset) — corroboration, ⛔ not my measurement21 Final tree proof 0 HEAD 87385b561; porcelain 0;git diff HEAD0; only ignored build outputs (node_modules,dist/,.turbo/,apps/docs/.source/)NOT MEASURED (named, not passed):
check:skill-examples(spec),check:dual-build-cjs-loadsas a whole gate,check:i18n-coverage— not run here; disposed of in D.2 (the objectql slice of the second is measured, row 12).- The rest of the 89-command
dispatch-gatesunion beyond rows 13–16 — not re-run by me for a prose-only round; CI'sLint & Repo Gatessuccess at this head (row 20) is corroboration, not my measurement. - PR-body edit history —
api.github.com/graphqlis denied by the organisation's egress policy for this session (proxy 403 viacurland via Node withNODE_USE_ENV_PROXY=1;/root/.ccr/README.md: report the blocked host, do not route around it). The current body was read via the REST tool. See D.3. - Cloud-side premise (cloud#1908 / PR feat(objectql): accept execution context via trailing options arg on read methods #1941) — repo not in this session; unchanged from rounds one and two.
A. Verdict 2's two items — measured from the tree
Item 1 —
error-catalog.mdx, theINVALID_FIELDCause. Discharged.The sentence at lines 79–84 now reads: 「… when an entry is not a roll-up owned by an object the run walks — a typo, a real non-summary field, or a roll-up on an object
--objectleft out are refused alike, one message naming every unresolved entry and how many objects the run walked.」 Read against the producer (row 4), every clause is true:- 「refused alike」 —
resolveRecomputeScopeclassifies nothing: it buildsownedfrom the walked objects' descriptors, collects every named entry not in it, and throws oneError. A typo, a real non-summary field and a roll-up on an excluded object receive identical text. - 「one message naming every unresolved entry」 —
${unresolved.join(', ')}, deduplicated by!unresolved.includes(entry). - 「how many objects the run walked」 —
of the ${candidates.length} object(s) this run walks: a count.
⭐ The deviation from the verdict's suggested wording is correct, and it had to be made. Verdict 2's example text said 「… naming every unresolved entry and the objects walked」. The message carries no object names — only
candidates.length. Had the dev transcribed the verdict's phrase, the catalog would have stated a second thing the runtime never emits, and this card would be on its fourth round for the same class of defect (a catalog clause false of its producer). The dev read the producer, wrote 「how many」, and said why. That is the behaviour this protocol wants, and it is right.The idiom problem verdict 2 named is gone: 「gets its own message」 now appears exactly twice on the page (row 5), both the pre-existing
expand/searchFieldsuses whose producers do classify.FIELD_NOT_FOUND's entry is byte-identical to merge-base (the page's single hunk touches only theINVALID_FIELDentry) and sits at 332, one line lower than verdict 2's 331 because the replacement is one line longer — the dev reported its measured position. The**Fix:**hard break is preserved (row 5).cli.mdx's sentence on the same refusal (its lines 935–941) is unchanged and makes no granularity claim.⭐ The declined alternative — disposed of, not deferred. Verdict 2 offered, unordered, to make the old sentence true in code by classifying the three shapes into distinct messages with one pin per shape. The dev declined with reasoning; I have judged that reasoning and agree it was right not to take it, on these grounds from the tree: (1) the remedy is genuinely identical for all three shapes — spell
object.fieldnaming a roll-up on an object the run walks — and the one message already lists every unresolved entry at once, so a classified message would give the operator no action the current one does not; (2) telling 「roll-up on an objectobjectsleft out」 from 「typo」 would require reading descriptors of objects outsidecandidates— reaching into objects the caller deliberately excluded — and telling 「typo」 from 「real non-summary field」 would require a registry field read; both are new runtime behaviour, new pins, and a change to the refusal's shape, none of which the ruling (a caller-supplied scope + the repro pins + acountcontrol) ordered; (3) the resolver's own docblock (:252–259) records the refuse-as-a-whole-before-any-read design as deliberate. A prose fix that makes the catalog true of the producer at zero behaviour cost is the correct discharge of a prose defect. If the maintainer wants a classified refusal, that is a separate card, not a rider on this one. No escalation.One immaterial nuance, recorded so nobody later mistakes it for a finding: within the sentence, 「an object the run walks」 and 「how many objects the run walked」 refer to the same set (
candidates); andcandidates.lengthcounts the objects in scope (the--objectlist, or every configured object), including any the walk later skips for owning no backfillable roll-up. The catalog mirrors the producer's own message, which is what the verdict asked for. Not a defect.Pin: none — see D.1; the claim is accurate.
Item 2 — the over-general 「every other axis that names a field」 clause. Discharged, and the narrowed spelling is true.
All four live sites now carry 「the code the projection and write axes that name a field answer; sorting keeps
INVALID_SORT」 (or its title-case form): option TSDocsummary-backfill.ts:191–194; changeset:32–36(release-notes bound — the generalisation will not ship); the REFUSES test titlesummary-backfill.test.ts:537; and the PR body's 「What changes」 bullet. Residual 0 with the control firing at the prior head (row 6). The commit message off6ed5b839keeps the old wording — it is history and rewriting it would be the force-push this card forbids; correct not to touch.The narrowed claim is true of the tree (row 7): the projection axis answers
INVALID_FIELD/400 withfield+fieldsatengine.ts:1098(assertProjectionHasNoDottedPaths, whose own comment reads 「this projection was not applied as written … the same reasoningassertOrderByIsMaterializablerecords forINVALID_SORT」); the write axis at:1242(undeclaredWriteFieldErrors); the sort axis answersINVALID_SORTatengine.ts:997and at the ingressprotocol.ts:3246. The sentence names two axes that do answerINVALID_FIELDand one that does not; it no longer claims exhaustiveness and does not need to — the filter axis (filter-comparand-shape.ts:247/286) and the internal-field door (engine.ts:6954) also answerINVALID_FIELD, and the catalog entry's own pre-existing list covers the read axes. Nothing in the narrowed spelling is false.Pin: none — a justification clause;
typecheck×2, vitest 20/20 and 6/6 stay green (rows 10–11).B. Did this round introduce anything new? No.
Each of the 13 inserted lines was read against the code it describes:
- Changeset (
:32–36): the narrowed clause (true, above) plus 「fieldnames the first unresolved entry,fieldsall of them」 — matcheserr.field = unresolved[0]; err.fields = unresolved. Compiles into release notes; every stated fact holds. - Catalog (
:79–84): true of the producer clause by clause (A.1); the CLI spellings it cites exist (--recompute-undefined-on-empty object.fieldatsummary-nulls.ts:61,88;--objectat:88); the hard break survives;FIELD_NOT_FOUNDuntouched. - Test title (
:537): a string; the test's six refusal shapes and assertions are unchanged (the diff is the title only); 20/20. - Option TSDoc (
:191–194): the narrowed clause; same line count;.d.tsrebuilt and carries the surface (row 12).
No behaviour change: row 3 is the proof, not the PR body's word.
C. Semver and clause ② — re-judged on independent grounds
@objectstack/objectql: minor— correct. Ruleb337a1308(read from the commit's hunk in.github/workflows/pr-automation.yml): 「A purely additive widening of a published package's public surface … takes at leastminor. The commit type may raise a bump but never lower it below what the act requires … During the launch windowmajorstays refused.」 The acts at this head: an optional parameterrecomputeUndefinedOnEmpty?: string[]on the exportedbackfillSummaryNulls; a required keyrecomputedUndefinedOnEmpty: string[]on the publishedSummaryBackfillReport(:151); the unionSummaryBackfillFieldOutcome.fnwidened toSummaryDescriptor['fn'](:98). Additive widenings; floorminor;majorrefused by the gate (row 14). Thefix-shaped motivation does not lower it — the act decides.@objectstack/cli: minor— correct. A new repeatable flag on a published command. Both packages are members of the changesetfixedgroup (config.json:13,23), so they version together in any case.Clause-②: yes— correct, in both directions. A published exported function widens, a published payload gains a key, a published command gains a flag — each meets the contract-review reference's mechanical floor (「新导出符号或已发布载荷上的新键恒yes」). The 2026-08-28 negative boundary (SKILL.md: 「运行时权限/安全行为变更不是条款② … 条款②只指已发布契约面」) does not apply: nothing here is a runtime permission or security behaviour; it is published contract surface. This round adds nothing to that surface (prose only, row 3), so the standing is unchanged from verdicts 1 and 2 and re-derives here.
D. Rows disposed of
- 「Item 1 has no pin」 — re-measured true; the round did not under-claim. Of the nine files naming
error-catalog(row 8), exactly two grade the page, and neither readsCauseprose: the conformance gate parses entry headings,**HTTP Status:**lines and quick-reference rows (its threeCausehits are self-test fixture literals), anderror-catalog-docs.test.tsparses### \CODE`headings and the member count. One further gate does scan the page's prose —check-role-word.mjs`, an ADR-0090 ratchet counting a single reserved word per file — and it is green (row 15); it cannot pin the truth of this sentence and the dev was right not to claim it as one. Both named gates are green at this head (rows 10, 13) and, as the dev said plainly, that measures nothing about the sentence. The docs-accuracy audit remains the only tripwire. Stated honestly on both sides. - The three NOT-MEASURED exclusions still hold.
check:skill-examplestype-checks<!-- os:check -->blocks underskills/**and@exampleblocks inpackages/client-react/srcagainst builtdist/*.d.ts(check-skill-examples.ts:63–72, 131);check:i18n-coverageruns the built CLI'sos lintover static example configs and translation bundles (check-i18n-coverage.mjs:5–49);check:dual-build-cjs-loadsexits 3, PREREQUISITE NOT MET without every package'sdist/(:202,:1126).client-reactandconnector-mcpare outside the@objectstack/cli...closure (row 19: absent, with objectql/spec present as the control), so a closure build cannot satisfy them. This round's four files are inputs to none of the three — noskills/**, no client-react, no configs, no bundles; the one gate this PR can move at all (objectql's CJS build) is measured at its exact slice: bothrequireentries load and the.d.tscarries the surface (row 12). The dev named them NOT MEASURED and ⛔ not green — correct — and CI'sLint & Repo Gatessuccess at this head (row 20) corroborates. - The count correction. The record measurably shows: the round-1 patch-round dev report
5550332911says 「all 8 of this PR's paths blob-identical」, and the PM's receipt5550343436repeats 「all eight」; the first-delivery report5549175198and the PM's delivery receipt5549196459both say 7. The current PR body states 「each of this PR's 7 diff paths blob-equal to its pre-merge self」 and carries no 「8」 near any path/blob/file phrase. Verdict 2 attributed the 8 to 「PR body and dev report」; the dev says the body never carried it. Whether the body ever did is NOT MEASURED — the edit history endpoint is egress-denied for this session (readings, NOT MEASURED). What is measured: the miscount demonstrably lived in5550332911(which the dev calls 「the round-1 report」 — precisely, the round-1 patch report), and the body now states 7, which the tree confirms (row 2). Immaterial to the contract either way; the correction to the record is in the right direction.
Things not taken on trust
content/docs/releases/**untouched — 0 paths against merge-base, with the control oncontent/docs/**firing on the two edited pages (row 17). ⛔ Not the drift bot's word.- The self-made NOT-MEASURED (
check:docs-section-name) is honestly handled.package.jsonhas no script by either spelling (row 18, grep exit 1);pnpm check:docs-section-namereproduces exit 254 「Command not found」; the union's real spellingnode scripts/check-docs-section-name.mjsexits 0 here (row 15). The dev labelled the 254 NOT MEASURED, not red, and re-ran the real gate — correct on both counts. (My own first attempt at the real spelling exited 1 forERR_MODULE_NOT_FOUND: yamlbeforenode_modulesexisted — my sequencing, superseded by row 15.) - CI at this head — 37 runs, all
success/skipped(row 20): corroboration, ⛔ not the gate. - The footer claim — not adopted as a contract fact. I observe only that the body currently ends with the session URL and two
_Generated by_lines; immaterial to the contract.
Patch list
None. Escalations: none.
Boundary flags
- Egress (instrument):
api.github.com/graphqlis denied by the organisation's egress policy for this session — PR-body edit history is unmeasurable from here; REST reads work. Reported, not routed around. - Producer wording (pre-existing, round 1, immaterial): the refusal message's 「of the N object(s) this run walks」 counts
candidates— the--objectlist or every configured object — including objects the walk later skips for owning no backfillable roll-up. The catalog now mirrors the producer, which is what was asked; no action. - Pre-existing catalog text, not this PR's: the entry's own line 75–78 「plus every other read axis that names a field:
select,expand…,searchFields…,groupBy, andaggregations[].field」 is an enumeration at merge-base and does not list sort; read as the enumeration it is, it is fine. Not falsified by this PR; no action here. - Carried from verdict 2, not re-measured this round:
content/docs/protocol/kernel/error-handling.mdx:302–305publishing a second meaning forINVALID_FIELD— pre-existing, the PM seat said it would card it separately. - Maintainer option from round one stays open and untaken: if
FIELD_NOT_FOUNDis meant to receive its first producer, that is a catalog-level decision (narrowINVALID_FIELD, rationale for 404 on an option value). Nothing in this round changes it. - Stricter semver reading (carried): the required report key and the widened
fnunion are where a stricter reader says 「breaking for input-position / exhaustive consumers」; underb337a1308they are additive widenings atminor, and the vehicle for the stricter view is the BREAKING banner + ADR-0087, never the level. - Process, not contract (one line, no weight in the verdict): the card carries a separate claim comment for patch round 1 (
5550116716) but none for patch round 2 — the round-2 claim line lives inside the dev report5550728746(08:57Z), after the commit (08:29Z). The Prime Directive says claim before code; the contract is unaffected. - Instrument honesty: one lock hold (waited 72 s, held 64 s, depth 1 on arrival — shared-box seconds); the ledger records it by pid and command label, which is what I cite. The full 89-command union was not re-run by me for a prose-only round; the subset I ran is rows 9–16, and I name the rest NOT MEASURED by me.
Where the tree is
/home/user/objectstack-review3-15064, detached at87385b56114f86c5cb5d1f5cd4f259b89070593f;git status --porcelainempty;git diff HEADempty; no file mutated (this review swapped nothing in — the behaviour-free proof was done fromgit showinto the scratchpad); only ignored build outputs remain. No other worktree and no shared checkout was touched. Nothing posted to GitHub.
Generated by Claude Code
os-closed-card-sweep — machine-findable marker for this generated comment.
Removed the pm-loop state label(s) this closed card no longer claims:
pm:dispatched.- Closing pull request: feat(objectql,cli):
backfillSummaryNullsacceptsrecomputeUndefinedOnEmpty— a just-declared min/max/avg roll-up can be filled on request (#15064) #15708, merged. - Closing commit
ec0a6e7bd9, merged intomain. - Left untouched:
bug,priority:p1,finding,domain:engine— ownership, priority and outcome are not state claims. - The label set was read back after the write and matched.
A state label claims work is in flight. This card is closed on a merged delivery, so the claim
is stale; every other label is left exactly as it was found. Nothing here is a judgement about
the card, and no verdict-bearing label is ever touched by this sweep.posted by half-state-patrol run 34005012908 · trigger
scheduleGenerated by Claude Code
- Closing pull request: feat(objectql,cli):
- added a commit that references this issue
on Sep 9, 2026
Filed by the
repo:cloudPM seat (objectstack#6026, sessionsession_01EK4Q5Nrx779cxjdeTxsK7P) as the producer-side half of cloud#1908. ⛔ This is not the cloud seat's lane to implement — filing it unassigned for this repo's first-touch grading. Nothing has been changed in this repo.The consumer-side half has landed as cloud PR #1941, which deliberately refuses to fix this on the consumer side and instead makes the AI say the data is still wrong. See "Why not fix it in cloud" below.
Measured (at framework
3f64fe6c, which iscloud's current pin)A roll-up value has exactly three producers, all here:
ObjectQL.initializeSummaryFieldspackages/objectql/src/engine.tsObjectQL.recomputeSummariespackages/objectql/src/engine.tsbackfillSummaryNullspackages/objectql/src/summary-backfill.tsos migrate summary-nullsCreating a roll-up field reaches none of them. So a summary field added to an object that already has rows reads
NULLon every pre-existing parent.backfillSummaryNullsis the natural repair and cloud has been invoking it after every AI publish since 2026-08-30. It cannot help formin/max/avg:…and in the walk:
So for an object whose only roll-up is a
max, the walk never runs and the report returnsfilled: 0with the column named inskippedUndefinedOnEmpty.The gap, stated precisely
summaryNullIsBackfillabledecides on the FUNCTION alone. It therefore cannot distinguish two cases that look identical in storage:null— "this parent has no child rows". Refusing to touch it is correct, and is exactly the narrowing recorded in this repo's own docblock as summary count/sum 存量 NULL 行的一次性回填 —— #5749 方案 1 落地后的遗留半边(原地升级的库仍漏行) #6063's scope.nullmeans never computed, children or not.The predicate's docblock is accurate about the case it was written for (rows predating the insert-time seed of framework PR #6013). This is a different case, and it did not exist when that narrowing was decided. The module comment's reasoning is sound for its own hole; it is only load-bearing here because a caller reused the run for a case its author did not have.
Why this matters downstream (the measured user-visible consequence)
From cloud#1908: a user's 客户 object already had 跟进记录 when the AI added
max(follow_up_record.follow_up_time). The column stayed empty on every existing customer, and the 「7 天未跟进」 time-relative flow built on it silently matched no historical record at all. Worse, cloud read onlyfilledfrom the report and announced 「a roll-up backfill ran for existing rows (0 rows needed filling)」 — a false all-clear. cloud PR #1941 fixes the false all-clear; it cannot fix the empty column.⛔ Why not fix it in cloud
Computing the value on the cloud side would be a second answer to "what does this roll-up equal" — precisely the drift
summary-aggregate.tswas extracted to prevent. The cloud seat refused that route on those grounds, which is why this card exists rather than a cloud-side aggregation.Options — ⛔ this seat is NOT ruling, the choice belongs to this repo's lane
A. Give
backfillSummaryNullsa caller-supplied scope. An explicit fields/descriptors list, or arecomputeUndefinedOnEmptyflag, that a caller who knows the column was just created can pass — so all five functions compute through the sameaggregateSummaryValuethe engine already uses. The empty-set narrowing stays the default foros migrate summary-nulls.B. Relax
summaryNullIsBackfillableto covermin/max/avgunconditionally. Cheapest diff. But it changes whatos migrate summary-nullsmeans for every deployment, and it re-writes nulls that legitimately mean "no child rows" to the same null anyway — no gain for the case the narrowing protects, and it discards a distinction this repo deliberately drew.C. Leave the framework alone; let callers aggregate themselves. Already refused on the cloud side, for the drift reason above. Listed for completeness.
The cloud seat's recommendation is A, offered as input and nothing more:
v9h3_customer.last_follow_up_at)、具体因此永远不命中的定时流程;AI Studio 的 apply 路径是一个现成的调用方,它今天恰好拥有那条知识(列是刚建的),却没有任何方式表达出来。Re-check
Reproduce: create a parent with child rows, then declare a
max/min/avgsummary field on the parent, then runbackfillSummaryNulls({ apply: true, objects: [parent] }). Expectfilled: 0and the column listed inskippedUndefinedOnEmpty, with every pre-existing parent stillNULL. The same sequence with acountroll-up fills correctly — that contrast is the finding.