Skip to content

feat(objectql,cli): backfillSummaryNulls accepts recomputeUndefinedOnEmpty — a just-declared min/max/avg roll-up can be filled on request (#15064) - #15708

Queued
zhuangjianguo wants to merge 11 commits into
mainfrom
claude/issue-15064-backfill-summary-nulls-scope
Queued

feat(objectql,cli): backfillSummaryNulls accepts recomputeUndefinedOnEmpty — a just-declared min/max/avg roll-up can be filled on request (#15064)#15708
zhuangjianguo wants to merge 11 commits into
mainfrom
claude/issue-15064-backfill-summary-nulls-scope

Conversation

@zhuangjianguo

@zhuangjianguo zhuangjianguo commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator

Closes #15064

Maintainer ruling on #15064 (director seat, batch #39, verbatim 「同意」): option AbackfillSummaryNulls gains a caller-supplied scope; the predicate and the unscoped run do not move. ⛔ B (relaxing summaryNullIsBackfillable) and C (a second definition of a roll-up's value) are not taken.

What was wrong

A roll-up value has three producers — the insert-time seed, the child-write recompute, and this one-off backfill — and declaring a summary field on an object that already has rows reaches none of them (measured on this tree: initializeSummaryFields seeds nothing for min/max/avg and is create-time; recomputeSummaries is called only from insert/update/delete; the backfill is called only from the CLI). For count/sum the backfill repairs that as a side effect. For min/max/avg it could not: summaryNullIsBackfillable(fn) decides on the function alone, so 「never computed」 and 「no child rows」 were indistinguishable, partitionDescriptors dropped the object before scannedObjects.push, and the report said filled: 0 — the false all-clear behind cloud#1908 / cloud PR #1941.

What changes

@objectstack/objectqlpackages/objectql/src/summary-backfill.ts (no change to summary-aggregate.ts, no helper was needed; ⛔ engine.ts untouched — it is #15225's serial resource)

  • SummaryBackfillOptions.recomputeUndefinedOnEmpty?: string[]object.field roll-ups the caller KNOWS were never computed. A named min/max/avg is walked like a count: every NULL parent is recomputed through the same aggregateSummaryValue the engine writes. A parent whose aggregate is the empty-set reading (null, no child rows) already holds the engine's own value, so it is neither counted as a hole nor written — the scoped run stays idempotent in the 「re-run until it reports zero」 sense. Naming a count/sum is accepted and changes nothing, so a publish path can pass every column it just declared without knowing the empty-set list.
  • An entry that resolves to no roll-up owned by an object the run walks (typo, plain field, or an object objects left out) is refused before any row is read, dry run or apply: ADR-0112 envelope code: 'INVALID_FIELD', status: 400 — the code the projection and write axes that name a field already answer, while sorting keeps INVALID_SORT (the engine's own rule at assertProjectionHasNoDottedPaths / undeclaredWriteFieldErrors: one condition keeps one wire code however the caller reached it, the same envelope on both doors; two of the refused shapes name a field that exists, so this is an option value that could not be applied, not a resource that was not found), field naming the first unresolved entry and fields all of them. A silent no-op there would be the false all-clear this option exists to end.
  • SummaryBackfillReport.recomputedUndefinedOnEmpty: string[] — the complement of skippedUndefinedOnEmpty, same object.field (fn) spelling, [] on an unscoped run. SummaryBackfillFieldOutcome.fn widens from 'count' | 'sum' to SummaryDescriptor['fn'].
  • formatSummaryBackfillReport lists the scoped columns under 「Recomputed on request」 and explains a NULL that remains; the unscoped wording is byte-identical.

@objectstack/clipackages/cli/src/commands/migrate/summary-nulls.ts: --recompute-undefined-on-empty object.field (repeatable) passes the scope through; the --apply confirmation names the columns; a refusal reaches the --json envelope with its code and exits 1 (existing path).

Docs: content/docs/deployment/cli.mdx — the flag and the case it exists for; content/docs/api/error-catalog.mdx — the INVALID_FIELD Cause gains the recomputeUndefinedOnEmpty / --recompute-undefined-on-empty axis — a typo, a real non-summary field, or a roll-up on an object left out are refused alike, one message naming every unresolved entry and how many objects the run walked, which is what resolveRecomputeScope emits (the FIELD_NOT_FOUND entry is untouched). Changeset: @objectstack/objectql minor, @objectstack/cli minor — an optional parameter on a published exported function, a new report key and a new CLI flag are each a purely additive widening of a published surface (bump-level rule, 2026-09-04); the fix-shaped motivation does not lower it. Ratchet: none — scripts/error-status-unpinned-baseline.json is byte-identical to main (blob 0596eb336); FIELD_NOT_FOUND keeps no producer, and check:error-status-conformance is green with the baseline untouched.

Why this spelling, and what was rejected

The ruling left the spelling to the dev. recomputeUndefinedOnEmpty: string[] (a list of object.field names under the name the card floated) was chosen because it reads beside the vocabulary the module already has: the report's skippedUndefinedOnEmpty is the list an operator sees today, and this option is its answer — 「recompute these undefined-on-empty roll-ups」 — with recomputedUndefinedOnEmpty closing the round trip in the report. Rejected: a boolean recomputeUndefinedOnEmpty (coarser than the fact the caller holds — it sweeps every min/max/avg on the object and cannot be checked for a typo, so a wrong object name would be exactly the silent no-op again); a SummaryDescriptor[] list (a caller-built descriptor is the second definition C refuses — a name resolved against the engine's own index keeps one); fields?: string[] (beside objects? it reads as a restriction filter, the inverse of its meaning); neverComputed?: string[] (states the fact but ties to nothing the module already says, and reads oddly as a CLI flag).

The unscoped run is what it was

Pinned whole against the report JSON and formatter lines captured on origin/main 791a0cbe6 before this change existed (summary-backfill.test.ts, 「UNSCOPED: the run is what it always was — report and wording byte-for-byte」): walk, writes, every counter, skippedUndefinedOnEmpty, and the human-readable output are byte-for-byte identical. The one visible delta on an unscoped run is the additive report key recomputedUndefinedOnEmpty: []. summaryNullIsBackfillable is untouched.

Pins (the ruling's: card repro + count control)

packages/objectql/src/summary-backfill.test.ts — 10 new, 20/20 green: the card's repro (parent with children, then a max) filled to 32 under the scope and left NULL + listed under skippedUndefinedOnEmpty without it; the childless parent kept NULL, neither counted nor written; the count control byte-identical to the unscoped outcome; min/max/avg all computed (10/32/21); the next child write moves the column exactly as the engine's recompute would (3240); dry-run/apply agreement; idempotence; never overwrites a stored value; the six refusal shapes assert code + status + field/fields; the formatter. packages/cli/src/commands/migrate/summary-nulls.test.ts — 6/6: dry-run-by-default, the flag's shape, every --recompute-undefined-on-empty entry reaching backfillSummaryNulls in order, the unscoped run handing undefined through, and the INVALID_FIELD refusal landing in the --json envelope with exit 1.

Ablation (on-disk proof, absolute-path trap restore)

Mutation: scope.has(rollupKey(desc)) → a key that never matches (the scope is never applied), proven on disk by anchor counts (1→0, marker 0→1) and blob hash ( HEAD blob). The subject is imported relatively by its test (./summary-backfill.js), so no dist leg exists. Predicted before running: the six scoped pins red; the unscoped byte-for-byte pin, the count control, the never-overwrites pin, the refusal pin and the ten pre-existing tests green. Observed on 57b5d0f28 (source identical to head): 6 red / 14 green, exactly the prediction. Restore by git checkout HEAD -- ABS_PATH, proven by blob hash == HEAD blob, git diff HEAD empty, git status --porcelain empty. (A first run went 7 red because the control itself asserted nullRows + 1, which reads the scoped hole count — that assertion was removed so the control is a control; the re-run is the one reported.)

Reverse verification of the cross-package type widening: a key SummaryBackfillOptions does not declare planted in the CLI call → pnpm --filter @objectstack/cli exec tsc --noEmit red with TS2353 … 'bogusKeyReverse15064' does not exist in type 'SummaryBackfillOptions' (so tsc reads the rebuilt .d.ts); restore proven by hash.

Patch round — contract review 5550099353 (FAIL, patch round, pinned to 7c96f738e)

Appended commits only — f6ed5b839 (items 1, 3, 4), 83a41f93b (item 2), and merge 1914df81f of origin/main 8e500f23e (clean by content: none of this PR's files moved on main, each of this PR's 7 diff paths blob-equal to its pre-merge self — the baseline is a path in the branch's history, restored to base before the merge, not in the PR — and no generated artifact merged on both sides). No rebase, no amend, no force-push.

  1. CoderesolveRecomputeScope answers INVALID_FIELD / 400 and stamps field = unresolved[0] beside fields. Pin: summary-backfill.test.ts 「REFUSES a name it cannot resolve」 — with the source reverted to 7c96f738e it reds on expected 'FIELD_NOT_FOUND' to be 'INVALID_FIELD' (1 failed / 19 passed), restore blob-proven; at head 20/20. summary-nulls.test.ts 「a refused scope entry (INVALID_FIELD) …」 retargeted, 6/6.
  2. Baselinescripts/error-status-unpinned-baseline.json restored to main's blob 0596eb336; it is no longer in this PR's diff. Pairing proof with pnpm check:error-status-conformance (source-read, on a tree with no dist/): head code + old baseline → exit 1 「FIELD_NOT_FOUND: documented with an HTTP status, but no producer declares one」; old code + restored baseline → exit 1 「baselined as unpinned, but a producer now declares its status」; both → exit 0 「every derivable runtime status is documented, and every documented status is reachable」.
  3. Docs — the INVALID_FIELD Cause in error-catalog.mdx gains the axis; the FIELD_NOT_FOUND entry is untouched. No gate parses the Cause prose: check:error-status-conformance reads entry headings, **HTTP Status:** lines and the quick-reference rows; packages/spec's error-catalog-docs.test.ts reads headings and the member count (3/3 at head).
  4. RiderSummaryBackfillReport.nullRows TSDoc now counts the named min/max/avg holes.

Docs-drift list at this head: 13 pages, 11 of them anchored on the INVALID_FIELD string literal — read and classified as anchor-noise from the code swap (each states INVALID_FIELD/400 for a read- or write-path field condition; none states a closed producer list, so none is falsified); content/docs/releases/v17.mdx is read-only and its sentence stays true.

Patch round 2 — contract re-review 5550562004 (FAIL, patch round, pinned to 1914df81f)

Appended commit 87385b561 only — prose in four files, no behaviour change; no rebase, no amend, no force-push.

  1. Docs — the INVALID_FIELD Cause sentence now describes its producer. Round 1's wording said the refused shapes each 「gets its own message」, which in this entry's own idiom (its two prior uses, and data-modeling/queries.mdx's 「with distinct messages」) means distinct messages — but resolveRecomputeScope builds one Error for every unresolved entry, so a typo, a real non-summary field and a roll-up on an object left out receive identical text. The sentence 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 --object left out are refused alike, one message naming every unresolved entry and how many objects the run walked」 — 「how many」 because the message carries the count of objects walked (of the N object(s) this run walks), not their names. FIELD_NOT_FOUND's entry is byte-identical to merge-base; it sits one line lower (332) because the sentence is one line longer. Pin: none — no gate or test parses the Cause prose: check:error-status-conformance reads entry headings, **HTTP Status:** lines and the quick-reference rows; packages/spec's error-catalog-docs.test.ts reads headings and the member count (both green at this head, which measures nothing about the sentence); the docs-accuracy audit is the only tripwire. The alternative the verdict left open and did not order — making the sentence true in code by classifying the three shapes into distinct messages, one pin per shape — was not taken: that is a behaviour change outside a prose round and a PM call; this seat's view is on the card.
  2. One qualifier on 「the code every other axis that names a field answers」 — false of the sort axis, which names a field and answers INVALID_SORT (engine.ts assertOrderByIsMaterializable; assertSortFieldsExist at the ingress). Now 「the code the projection and write axes that name a field answer; sorting keeps INVALID_SORT」 in the option TSDoc (summary-backfill.ts), the changeset paragraph (it compiles into release notes, so the generalisation would have shipped), the REFUSES test title, and this body's 「What changes」 above. The commit message of f6ed5b839 is history and keeps its wording. Pin: none; both typechecks and both vitest files stay green.

Verification

Re-review round at 87385b561 (every exit captured before any pipe; one verify-lock hold — ledger label=bash …/issue-15064-r3/heavy.sh, waited 148 s, held 171 s, never polled): turbo run build --filter='@objectstack/cli...' 57/57; @objectstack/objectql vitest src/summary-backfill.test.ts exit 0, 20/20; @objectstack/cli vitest src/commands/migrate/summary-nulls.test.ts exit 0, 6/6; both typechecks exit 0 (--listFiles: tsconfig.json lists the edited source, tsconfig.test.json the edited test). The 89-command dispatch-gates union re-derived at 87385b561 and run in full, reconciled with --ran (「89 derived famil(ies) accounted for — 89 run, 0 NOT-MEASURED」): 86 exit 0 — including check:error-status-conformance (「every derivable runtime status is documented, and every documented status is reachable」), check:error-code-casing, check:nul-bytes, check:docs-audit-scope, the changeset gates (check:empty-changeset, check:adr-0087-registration, check-changeset-no-major in both spellings, check:changeset-gate-self-tests, check-changeset-fixed), check:docs-single-h1, check:docs-redirects, check:docs-transcript-drift, check:cli-examples-parity, check-docs-section-name, spec check:docs / check:variant-docs, check:i18n, check:comment-mask-corpus — and the same three prerequisite refusals as round 1, NOT MEASURED locally, ⛔ not green: spec check:skill-examples (exit 1, client-react/dist not built), check:dual-build-cjs-loads (exit 3, PREREQUISITE NOT MET), check:i18n-coverage (exit 3, COULD NOT MEASURE — connector-mcp/dist absent); CI's Lint & Repo Gates is their measurement. spec error-catalog-docs.test.ts 3/3. eslint on the two touched TS files via --format json: 2 files, 0 errors, 0 warnings — a measured narrowing (eslint.config.mjs 「never enables type-aware linting … for ANY file」, so this diff cannot move an untouched file's verdict). content/docs/releases/** untouched (0 paths) with the firing control on content/docs/** naming the two edited pages. Control-byte scan of the four edited files: 0 hits.

Patch round at 1914df81f (its own os-dev-report comment on #15064 carries every exit code): @objectstack/objectql targeted vitest 20/20 and typecheck 0; @objectstack/cli targeted vitest 6/6 and typecheck 0 (tsconfig.json --listFiles lists the edited test file, so the green covers it); pnpm check:error-status-conformance 0; the 89-command dispatch-gates union run at 1914df81f and reconciled with --ran (89 derived / 89 run / 0 unrun) — 86 exit 0, three prerequisite refusals from packages outside the built closure (spec check:skill-examples, check:dual-build-cjs-loads, check:i18n-coverage: NOT MEASURED locally; CI's Lint & Repo Gates is the measurement).

First delivery, at 7c96f738e: pnpm --filter @objectstack/objectql typecheck (tsc + scripts + test-typecheck) green; pnpm --filter @objectstack/cli typecheck green — the only downstream consumer of the widened types (pnpm --filter '...@objectstack/objectql' direction: downstream) is the CLI, by grep; dispatch-gates derived union run in full and reconciled with --ran; eslint on the four touched TS files via --format json: 4 files, 0 errors, 0 warnings — a measured narrowing, since eslint.config.mjs 「never enables type-aware linting … for ANY file」 so this diff cannot move an untouched file's verdict.

Out of scope, for the cloud seat: cloud#1908 / cloud PR #1941 can wire recomputeUndefinedOnEmpty into the AI publish path once this releases (Clause-②: yes — a published exported function widens).

🤖 Generated with Claude Code

https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ


Generated by Claude Code


Generated by Claude Code

…Empty — a just-declared min/max/avg roll-up can be filled on request

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
…and the CLI wiring double drains stdout

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
…ut double

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
…a clean ablation control

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
…OUND now has a producer declaring 404 (#15064)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Sep 5, 2026
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/cli, @objectstack/objectql, touching 15 documentable anchor(s).

13 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/data-api.mdx (via INVALID_FIELD (literal, a string literal in resolveRecomputeScope))
  • content/docs/api/error-catalog.mdx (via backfillSummaryNulls (symbol, a top-level function), recomputeUndefinedOnEmpty (symbol, a field of interface SummaryBackfillOptions), INVALID_FIELD (literal, a string literal in resolveRecomputeScope), os migrate summary-nulls (command, read off packages/cli/src/commands/migrate/summary-nulls.ts))
  • content/docs/api/error-handling-server.mdx (via INVALID_FIELD (literal, a string literal in resolveRecomputeScope))
  • content/docs/automation/hook-bodies.mdx (via INVALID_FIELD (literal, a string literal in resolveRecomputeScope))
  • content/docs/data-modeling/queries.mdx (via INVALID_FIELD (literal, a string literal in resolveRecomputeScope))
  • content/docs/data-modeling/schema-design.mdx (via INVALID_FIELD (literal, a string literal in resolveRecomputeScope))
  • content/docs/deployment/cli.mdx (via os migrate summary-nulls (command, read off packages/cli/src/commands/migrate/summary-nulls.ts))
  • content/docs/permissions/authorization.mdx (via INVALID_FIELD (literal, a string literal in resolveRecomputeScope))
  • content/docs/protocol/kernel/error-handling.mdx (via INVALID_FIELD (literal, a string literal in resolveRecomputeScope))
  • content/docs/protocol/objectql/index.mdx (via INVALID_FIELD (literal, a string literal in resolveRecomputeScope))
  • content/docs/protocol/objectql/query-syntax.mdx (via INVALID_FIELD (literal, a string literal in resolveRecomputeScope))
  • content/docs/protocol/objectql/types.mdx (via INVALID_FIELD (literal, a string literal in resolveRecomputeScope))
  • content/docs/ui/views.mdx (via INVALID_FIELD (literal, a string literal in resolveRecomputeScope))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx (via INVALID_FIELD (literal, a string literal in resolveRecomputeScope), os migrate summary-nulls (command, read off packages/cli/src/commands/migrate/summary-nulls.ts))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 34 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json fa85759963fcf25a55cd9a159c8e9c44bb9dde13packageMentionDocs.

Which tree this was computed on

This run read content/docs from b3eeb537aac751d634c8d2ffcc26ca3eac76c00b — the merge of head 87385b56114f86c5cb5d1f5cd4f259b89070593f into base fa85759963fcf25a55cd9a159c8e9c44bb9dde13, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin b3eeb537aac751d634c8d2ffcc26ca3eac76c00b && git checkout b3eeb537aac751d634c8d2ffcc26ca3eac76c00b
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin fa85759963fcf25a55cd9a159c8e9c44bb9dde13 87385b56114f86c5cb5d1f5cd4f259b89070593f && git checkout -B drift-repro fa85759963fcf25a55cd9a159c8e9c44bb9dde13 && git merge --no-ff 87385b56114f86c5cb5d1f5cd4f259b89070593f

node scripts/docs-audit/affected-docs.mjs --json fa85759963fcf25a55cd9a159c8e9c44bb9dde13

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs fa85759963fcf25a55cd9a159c8e9c44bb9dde13 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

Copy link
Copy Markdown
Collaborator Author

Contract review at CONTRACT_REVIEW_TIERFAIL (patch round) on 7c96f738e. Verdict on the card (#15064 5550099353); ⛔ the gate is not cleared and this PR stays draft.

domain:engine execution seat. Pointer only — the verdict's carrier is the card.

In one line: the delivery is what ruling A ordered and every load-bearing claim re-measured true — 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 tsc reads the rebuilt .d.ts, both mid-run merges of main are clean by content, summary-aggregate.ts and engine.ts are blob-identical to the merge-base, and semver minor/minor plus Clause-②: yes are upheld. The single defect is a contract defect on an addition the ruling did not order: the refusal's wire code.

FIELD_NOT_FOUND / 404 is the wrong code, and the tree already says which one is right. Verified independently by this seat on origin/main:

reading value
error-catalog.mdxINVALID_FIELD Cause 「A field name in the request does not exist on the target object」, at 400
engine.ts:1098 err.code = 'INVALID_FIELD'; — a live producer
engine.ts:1092, in the code's own words INVALID_FIELD, not a new code, and 400 rather than 500」
the sibling instance (:993-997, INVALID_SORT) 「a host … answers the same envelope on both doors」 — a pattern, not a one-off
FIELD_NOT_FOUND producers in packages/**/*.ts (non-test) 1 — the enum declaration in spec/src/api/errors.zod.ts:90. No producer exists

⇒ 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. And it bites in practice: 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 the 404's Fix sends an operator to go confirm a field that is right there.

⚠️ This seat handed the reviewer a prose question and got a better answer back. The docs-drift bot flagged error-catalog.mdx, and the row went out as 「is the catalog sentence still true of every producer?」 The reviewer answered that the sentence is not the thing to fix — widening a 404's prose would encode the wrong code rather than correct it. The row was disposed of, not deferred, and it inverted the fix.

Patch round dispatched for items 1–4: the code change to INVALID_FIELD / 400 with its pins retargeted; restoring scripts/error-status-unpinned-baseline.json to origin/main's blob (a no-op against main — ⛔ not the maintainer-only baseline-expanding path — because the code change hands FIELD_NOT_FOUND its producer back); adding the new axis to the catalog's INVALID_FIELD entry and leaving FIELD_NOT_FOUND alone; and one stale TSDoc line on SummaryBackfillReport.nullRows. The reviewer records no escalations — every item is answerable from the tree's own recorded conventions.

⚠️ One boundary flag is a live maintainer option: if FIELD_NOT_FOUND is meant to get its first producer here, that is a catalog-level decision requiring the INVALID_FIELD entry to be narrowed at the same time plus a rationale for 404 on an option value. Flagged so it can be overruled deliberately rather than by default.

Adoption record: verdict produced by a context-isolated fable review subagent fed only the card, the ruling, the dev report and this PR — ⛔ never this seat's conclusions — transcript-verified at 140 "model":"claude-fable-5-1" stamps and no other model over 879,211 bytes, cross-checked against the verify-lock ledger's own independent record of its acquisitions, and adopted verbatim.


Generated by Claude Code

…LID_FIELD / 400 — the code every axis naming a field already takes (#15064)

Contract-review patch round on PR #15708 (verdict 5550099353), items 1, 3, 4.

- `resolveRecomputeScope` stamps `code = 'INVALID_FIELD'`, `status = 400`
  and `field = unresolved[0]` beside `fields`, the shape of the engine's
  sibling producers (`assertProjectionHasNoDottedPaths`,
  `undeclaredWriteFieldErrors`). `FIELD_NOT_FOUND` / 404 had no producer in
  this repo; handing a never-emitted 404 its first producer for a condition
  the catalog already assigns to a 400 is the two-codes-one-condition drift
  ADR-0112 exists to prevent — and two of the refused shapes name a field
  that exists (a real non-summary field; a roll-up on an object `objects`
  left out), so this is an option value that could not be applied, not an
  addressed resource that was not found.
- Pins retargeted: `summary-backfill.test.ts` "REFUSES a name it cannot
  resolve" (six code/status assertions, now also `field`/`fields`) and
  `summary-nulls.test.ts` "a refused scope entry … reaches the --json error
  envelope" (`code`).
- `content/docs/api/error-catalog.mdx`: the `INVALID_FIELD` Cause gains the
  `recomputeUndefinedOnEmpty` / `--recompute-undefined-on-empty` axis; the
  `FIELD_NOT_FOUND` entry is untouched.
- `SummaryBackfillReport.nullRows` TSDoc counts the named min/max/avg holes.
- Option TSDoc, resolver docblock and the changeset paragraph updated
  wherever FIELD_NOT_FOUND / 404 was named.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
…LD_NOT_FOUND has no producer again (#15064)

Patch-round item 2 (verdict 5550099353): with the refusal answering
INVALID_FIELD / 400, FIELD_NOT_FOUND loses the producer that commit
7c96f73 ratcheted the baseline for, so
`scripts/error-status-unpinned-baseline.json` goes back to origin/main's
blob 0596eb3 and the PR no longer touches the file. A no-op against main;
not the maintainer-only baseline expansion.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
…ibes its producer — one message for every unresolved entry, and INVALID_FIELD's siblings named as the projection and write axes (#15064)

Contract re-review verdict 5550562004 (FAIL, patch round, pinned to 1914df8),
two prose items, no behaviour change:

1. content/docs/api/error-catalog.mdx, INVALID_FIELD Cause: the appended
   sentence said the three refusal shapes each "gets its own message", which in
   this entry's idiom means distinct messages. resolveRecomputeScope emits ONE
   Error for every unresolved entry, so a typo, a real non-summary field and a
   roll-up on an object left out receive identical text. The sentence now says
   they are refused alike, one message naming every unresolved entry and how
   many objects the run walked. FIELD_NOT_FOUND's entry is untouched.

2. "the code every other axis that names a field answers" was over-general:
   the sort axis names a field and answers INVALID_SORT (engine.ts,
   assertOrderByIsMaterializable; assertSortFieldsExist at the ingress).
   Narrowed to "the projection and write axes that name a field" in the option
   TSDoc, the changeset paragraph (it compiles into release notes) and the
   REFUSES test title, each now also stating that sorting keeps INVALID_SORT.

No pin reds on prose alone: check:error-status-conformance reads headings,
HTTP Status lines and quick-reference rows; error-catalog-docs.test.ts reads
headings and the member count. typecheck and the two vitest files stay green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
@zhuangjianguo
zhuangjianguo marked this pull request as ready for review September 5, 2026 09:15
@zhuangjianguo
zhuangjianguo added this pull request to the merge queue Sep 5, 2026
Any commits made after this event will not be merged.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

2 participants