Skip to content

pm-dispatch: owe the isolated contract review on three contract faces; changeset and docs prose move to the seat's ACCEPT - #21192

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-21187-prose-review-at-accept
Oct 1, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-21187-prose-review-at-accept

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #21187

Clause-②: no

What this changes

references/contract-review.md named five faces that owe an at-tier contract review, and three of them were prose: .changeset / CHANGELOG, content/docs/** and apps/docs/**. The eligibility line in references/execution-duties.md then forbids an off-tier seat from reviewing in-seat. Together, every PR carrying a releasing changeset owed an isolated CONTRACT_REVIEW_TIER subagent whenever its seat was not spec or skills.

This PR implements the ruling. The isolated at-tier review is now owed on exactly three contract faces:

  • a Clause-②: yes declaration;
  • published schema (packages/spec/src/**, non-test);
  • governed rule text (the SKILL.md definition, .claude/** included; Tier S as today).

The prose faces (.changeset / CHANGELOG, content/docs/**, apps/docs/**) are checked sentence by sentence against the diff in the dispatching seat's ACCEPT, at the seat's own tier, and the ACCEPT names the sentences it checked. No second agent is spawned for them.

The change is text-only. No script or workflow implements the face list (search below), and the queue's contract-review gate reads only the two Clause-② legs (landing-operations.md 「条款②入队闸门」), which this PR does not touch.

Rulings implemented

  • Maintainer, ruling A, quoted in the card body: 「我已经同意了,不需要严格的规则」. It answers the seat's proposal (quoted verbatim in the card body) that changeset prose be self-checked by the seat at ACCEPT, with no at-tier subagent spawned for it.
  • Maintainer to the director seat, settling forks 1, 2 and 3 of ruling record 5934170950 on the card: 「同意,你负责派发。」
    1. The docs faces move with the prose: content/docs/** and apps/docs/** no longer owe an isolated review.
    2. Two at-tier uses stay exactly as they are (see the next section but one).
    3. priority:p1, landed this round.

Every sentence changed (before, then after)

All five files keep their line counts. Anchors are content, not line numbers. Sentences 1 to 6 landed in 4bde1f71a; sentences 7 and 8 landed in 27e976d73 (review round 1).

1. references/contract-review.md, the face list (two lines become three):

- 复核面 = 出货给用户或 agent 的五处:`content/docs/**`、`apps/docs/**`、CHANGELOG/`.changeset` 散文。
- 加已发布 schema(`packages/spec/src/**` 非测试)与 governed 规则文本(统一定义见 SKILL.md)。
- 复核面 = 契约三面:`Clause-②: yes` 申报、已发布 schema(`packages/spec/src/**` 非测试)、governed
  规则文本(统一定义见 SKILL.md);`.changeset`/CHANGELOG、`content/docs/**`、`apps/docs/**` 散文面
  ⛔ 不在其列:派发席按本席档在 ACCEPT 逐句对照 diff 核,ACCEPT 点名所核句。

The line breaks fall at a source space (governed 规则文本, 散文面 ⛔), so the rendered text is unchanged by them.

2. references/contract-review.md, 「五面皆不碰」 (renamed; meaning kept for the three-face set):

- 五面皆不碰 ⇒ CI 加席位自读 ⛔ 不起第二个 agent;谁跑 = 派发席,达档者席内审。
- 三面皆不碰 ⇒ CI 加席位自读 ⛔ 不起第二个 agent;谁跑 = 派发席,达档者席内审。

3. references/contract-review.md, the independence-pair line (this is the line payment, see "Line budget"):

- 独立性对:`Implemented-by:`/`Reviewed-by:` 取值见
  `node scripts/pm/record-recognisers.mjs --template`;子代理记采纳它的席位。
- 独立性对:`Implemented-by:`/`Reviewed-by:` 取值同见 `--template`;子代理记采纳它的席位。

4. references/landing-operations.md, 「交付后复核按面欠」:

- 交付后复核按面欠 ⛔ 不按车道(五面见 `references/contract-review.md`);双肢命中即 spec 车道。
- 交付后复核按面欠 ⛔ 不按车道(三面见 `references/contract-review.md`);双肢命中即 spec 车道。

5. references/execution-duties.md, 「条款②自审资格按车道」:

- 条款②自审资格按车道:spec 与 skills 席达档席内审;未达档 ⛔ 不自审,起隔离达档子代理。
- 契约三面自审资格按车道:spec/skills 席达档席内审;未达档 ⛔ 不自审,起隔离达档子代理。

spec 与 skills became spec/skills only to keep the line within the ratchet's 120-byte cap (118 bytes).

6. references/lanes/ui.md, 「条款②复核本席适用」:

- 条款②复核本席适用:按面欠达档复核记录,落 PR 或卡,归属与资格见 `../contract-review.md`。
- 契约三面复核本席适用:按面欠达档记录落 PR 或卡,散文面归 ACCEPT,见 `../contract-review.md`。

The ui seat is the lane whose changeset-only PRs started this card, so its line says where the prose check now sits.

7. references/review-checklist.md 〈范围与 changeset〉, the ACCEPT-side prose check (new line, review round 1). The path a seat walks at ACCEPT is execution-duties.md 〈复核〉 into this checklist, so the duty is stated here as well as in contract-review.md:

(none)
- changeset/CHANGELOG、`content/docs/**`、`apps/docs/**`:逐句核 diff,ACCEPT 点名所核句,⛔ 不起子代理。

The line is paid in the same file by compressing the two-line dev-union head check into one. The dropped words restate the dev's own obligation, which .claude/agents/os-dev.md carries (run the union after the final commit, quote the short sha in the report and the body), and the both-directions rule that the same section states two lines above (「不一致的双向都不入账」). No ⛔ line is touched.

- dev 本地并集同样先钉 head:最后一次提交之后跑,`git rev-parse --short HEAD` 抄进报告与正文。
- 与 PR 当前 `head.sha` 比一次,对不上即死树读数、双向都不入账;没抄 ⇒ 按没有读数处理。
- dev 本地并集钉 head:所抄 `git rev-parse --short HEAD` 不符 PR `head.sha` 或没抄 ⇒ 双向都不入账。

8. references/contract-review.md, the trigger (review round 1). The first contract face is a Clause-②: yes declaration, which lives in the claim comment, not in the diff:

- 欠不欠按面判 ⛔ 不按车道:diff 碰下列任一面即欠达档复核,交付后当轮完成。
- 欠不欠按面判 ⛔ 不按车道:diff 或认领申报碰下列任一面即欠达档复核,交付后当轮完成。

Deliberately unchanged (ruling point 2)

These two lines are not prose review. Each is the record that lets a red or exceptional gate pass, so they stay exactly as they are:

  • references/execution-duties.md: 「改到已有 .changeset/*.md 的 PR ⛔ 不打 skip-changeset;确认取同 head 达档 PASS 或维护者原话」
  • references/landing-operations.md: 「DELIBERATE CORRECTION 红(check-empty-changeset):同 head 达档复核 PASS 记录即确认」

Both lines grep exactly once at head, and neither is an added or removed line in the diff (the second shows up only as hunk context).

Where the rule lives: the search

Base 2c1cef334. Searched .claude/, AGENTS.md and CLAUDE.md:

  • Face-list wording, 五面|五处|复核面|按面|四面|三面: the rule hits only the anchors changed above. The other hits are unrelated: execution-duties.md 「三面都查」 (premise check), lanes/devx.md and lanes/spec.md (spec lane slicing), and platform-readings.md 「按面不按路由」 (footer readings).
  • At-tier wording, 达档|CONTRACT_REVIEW_TIER|at-tier|契约复[审核]: none of these ties prose to an at-tier record. dispatch-runbook.md 「无主阻塞项的契约面(含 packages/spec)」 names contract faces only, and SKILL.md (the skills-hunk, Tier S and governed-definition lines) and .claude/agents/os-dev.md (Tier S landing) are about governed text.
  • Prose wording, 散文, content/docs|apps/docs, contract[- ]review|prose, and changeset on the same line as 复核|review|达档|复审: no further site. review-checklist.md, SKILL.md and .claude/agents/os-dev.md carry no restatement of the face list, so nothing outside references/** needed an edit.
  • Mechanism: 7 files under scripts/ and .github/ mention both a contract-review term and one of .changeset / content/docs / apps/docs. In each of them, 0 lines put the two within 4 lines of each other. No script reads the changed sentences. The one script that reads contract-review.md as text (the dispatch-gates.mjs self-test: the CONTRACT_REVIEW_TIER constant is present and the retired fuse text is absent) is unaffected, and its gate is green.
  • Sister checkouts (objectui 0a3e540, cloud 90b1af0, hotcrm fb408a7): 0 hits for 五面|复核面|五处|按面欠|CONTRACT_REVIEW_TIER outside CHANGELOGs.

Line budget

file lines before → after ceiling bytes before → after
references/contract-review.md 40 → 40 60 3349 → 3423
references/landing-operations.md 101 → 101 101 8763 → 8763
references/execution-duties.md 183 → 183 183 17365 → 17364
references/lanes/ui.md 38 → 38 38 2601 → 2602
references/review-checklist.md 77 → 77 77 6318 → 6321

The new text in contract-review.md needs one line more than the old face list. That line is paid by deleting content in the same file: the second spelled-out copy of node scripts/pm/record-recognisers.mjs --template (sentence 3). The full command is still spelled once, eleven lines above. No line was re-wrapped to free space. review-checklist.md pays for its new line the same way (sentence 7). Every added line is at most 119 bytes.

Local gates

Run at head 27e976d73, after the review-round-1 commit. The list was derived by node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (18 commands, no paths passed; check:required-contexts joined the list with review-checklist.md). The --ran reconciliation over the exit-coded record printed: 「18 derived famil(ies) accounted for — 18 run, 0 NOT-MEASURED (a DERIVED zero — all 18 recorded an exit code and none of them is 3)」.

command exit
node scripts/check-closing-keyword-parity.mjs (and --self-test) 0, 0
node scripts/check-comment-mask-corpus.mjs 0
node scripts/pm/check-harness-current.mjs --self-test 0
pnpm --filter @objectstack/lint run check:doc-formula-expressions 0 (see note)
pnpm check:agent-test-spelling 0
pnpm check:cross-package-test-inputs 0
pnpm check:doc-authoring 0
pnpm check:driver-memory-census 0
pnpm check:gitlink-declared 0
pnpm check:nul-bytes 0
pnpm check:pm-governed-merges 0
pnpm check:pm-skill-id-lint 0, 「34 file(s) clean」
pnpm check:pm-skill-ratchet 0, the five files at 40/60, 101/101, 183/183, 38/38, 77/77
pnpm check:refd-timer-probe 0
pnpm check:required-contexts 0
pnpm check:skill-frame-sync 0
pnpm check:watch-hint-literal 0

Note: check:doc-formula-expressions first exited 3 (PREREQUISITE NOT MET: @objectstack/formula and @objectstack/lint were not built in the fresh worktree), so that first run measured nothing. After pnpm exec turbo run build --filter=@objectstack/formula --filter=@objectstack/lint --concurrency=2 under the shared verify lock (VERDICT command-exit 0), it ran with exit 0 and a 58-case self-test. The same happened at 27e976d73 in the recreated worktree: first run exit 3, rebuild (a turbo cache hit), rerun exit 0. A raw control-byte scan over the four files found nothing. The full farm is CI's.

No changeset: the diff publishes nothing (.claude/** only), so the PR takes skip-changeset.

Acceptance notes

  • Resolved in review round 1: the ACCEPT path now names the prose check (sentence 7). execution-duties.md 〈复核〉 was not touched again, because its 「核对清单结论」 already carries the checklist into the ACCEPT.
  • The card's rationale cites a changeset claim re-read that asks authors to re-read pending changeset bodies. That facility is objectui's (.github/workflows/changeset-presence.yml, scripts/render-changeset-claims-comment.mjs there), and this repository's tree has 0 hits for it. On objectstack, the ACCEPT check is the only prose control before a release consumes the changeset. Noted, not filed; no carrier.
  • Landing: Tier S (.claude/** only). The dev does not ready, queue or arm this PR. The owning seat lands it after an isolated CONTRACT_REVIEW_TIER review of record PASS on the current head, with every check green.

维护者速读(草稿)

改了什么:PM 规则里「哪些改动必须另起一个顶档 agent 做隔离契约复核」的清单,从五处缩到三处契约面:Clause-②: yes 申报、packages/spec/src/**(非测试)、受管规则文本(含 .claude/**)。changeset/CHANGELOG 与 content/docs/**、apps/docs/** 的散文改由派发席在 ACCEPT 时按本席档逐句对照 diff 核对,并在 ACCEPT 里点名核过的句子,不再另起 agent。另在 ACCEPT 复核清单(review-checklist.md)补一行,写明散文面逐句核、ACCEPT 点名、不起子代理。只动 .claude/skills/pm-dispatch/references/ 下 5 个文件共 11 行,每个文件行数不变。

为什么改:卡片实测 objectui 最近 40 个合并 PR 中有 30 个带发布 changeset,按旧规则每个都要一次隔离顶档复核;单次约 23.6 万 token,那一例只多抓出一句散文偏松。您已裁 A(「我已经同意了,不需要严格的规则」),并对三个分叉答「同意,你负责派发。」。

风险与代价(含回滚):契约面(条款②申报、spec、受管面)照旧走隔离顶档复核;入队闸门只读条款②两肢,本 PR 不碰。两条「同 head 达档 PASS 即确认」的例外条款原样保留。代价是散文核对的质量落在派发席本档,靠 ACCEPT 逐句点名留痕。回滚 = revert 本 PR,不牵连任何脚本或数据。

席位意见:

你要做的:无需操作。本 PR 属 Tier S,由席位在隔离达档复核 PASS 后落地;对措辞有异议请在本 PR 留言。


Generated by Claude Code

…ract faces; prose faces move to the seat's ACCEPT

The at-tier review face list in references/contract-review.md named five
surfaces, three of them prose (`.changeset`/CHANGELOG, `content/docs/**`,
`apps/docs/**`), so every PR with a releasing changeset owed an isolated
CONTRACT_REVIEW_TIER subagent whenever its seat was off-tier.

Per the maintainer's ruling A and the director's record settling forks
1/2/3, the isolated review is now owed on exactly three faces: a
`Clause-②: yes` declaration, published schema (`packages/spec/src/**`,
non-test), and governed rule text. The prose faces are checked sentence
by sentence against the diff in the dispatching seat's ACCEPT, at the
seat's own tier, and the ACCEPT names the sentences it checked; no
second agent is spawned for them.

The three sentences that restate the rule (landing-operations.md,
execution-duties.md, lanes/ui.md) now point at the three faces. Line
counts are unchanged in every file; contract-review.md pays its extra
line by dropping the second spelled-out copy of the record-template
command.

Claude-Session: https://claude.ai/code/session_016tKoy8NJa35Yih1FdzrVmn
Co-authored-by: Claude <noreply@anthropic.com>
…face trigger reads diff or declaration

The prose faces are checked in the dispatching seat's ACCEPT, but the path a
seat walks at ACCEPT (execution-duties 〈复核〉 into review-checklist.md) did
not say so; the duty lived only in contract-review.md and lanes/ui.md. One
line in review-checklist.md 〈范围与 changeset〉 now states it: the three prose
faces are checked sentence by sentence against the diff, the ACCEPT names the
sentences checked, and no subagent is spawned.

The line is paid in the same file by compressing the two-line dev-union head
check into one: the dropped words restate the dev's own obligation (run after
the last commit, quote the short sha in report and body), which the dev-agent
definition carries, and the both-directions rule the section states two lines
above. No ⛔ line is touched.

contract-review.md's trigger now reads diff or claim declaration, since the
first contract face is a Clause-② declaration in the claim, not in the diff.

Claude-Session: https://claude.ai/code/session_016tKoy8NJa35Yih1FdzrVmn
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 27e976d737af1e41d407248b4a7cbe37d3328a4e
Local-runs: none

Inputs: card #21187 body and all four comments (ruling record 5934170950, claim 5934212807, os-dev-report 5934679331 and 5934891734); PR #21192 body, file list and the net diff against main; the check-runs on the head. Readings taken by gh api and git show at the fetched refs only. The GitHub diff is byte-identical (index lines aside) to git diff from the merge base 2c1cef3345738b5f2486eed5b0608687dba36f11 to the head: 5 files, +11 / −11, all under .claude/skills/pm-dispatch/references/. Head repo = base repo (not a fork). Draft, Clause-②: no, labels documentation, size/s, skip-changeset, auto-merge not armed.

① Derived judgments

Accept-set and public surface: the diff touches no package, no export, no schema, no script, no workflow; it implies no accept-set change and no public-surface change. The only surface that moves is governed PM rule text (Tier S, .claude/**). Each changed sentence, judged against ruling A as quoted on the card (「我已经同意了,不需要严格的规则」, answering 「changeset 散文由席位在 ACCEPT 自核」) and the director's record 5934170950 with forks 1 / 2 / 3 settled by 「同意,你负责派发。」:

  1. contract-review.md face list — 「复核面 = 契约三面:Clause-②: yes 申报、已发布 schema(packages/spec/src/** 非测试)、governed 规则文本(统一定义见 SKILL.md);.changeset/CHANGELOG、content/docs/**、apps/docs/** 散文面 ⛔ 不在其列:派发席按本席档在 ACCEPT 逐句对照 diff 核,ACCEPT 点名所核句。」 — RIGHT. Exactly the three faces of fork 1 (declaration, published schema non-test, governed rule text); exactly the three prose faces moved out; dispatching seat, own tier, sentence-by-sentence against the diff, ACCEPT names the sentences. Nothing wider, nothing narrower. 「governed 规则文本(统一定义见 SKILL.md)」 resolves at head to SKILL.md line 184 「governed 面统一定义:docs/adr/** + .claude/**(全量)+ skills/** + docs/NORTH-STAR.md」 and line 204 「Tier S = .claude/** 全树」, so 「.claude/** included, Tier S as today」 holds without restating it.
  2. contract-review.md 「五面皆不碰」 → 「三面皆不碰 ⇒ CI 加席位自读 ⛔ 不起第二个 agent;谁跑 = 派发席,达档者席内审。」 — RIGHT. The count follows the face list; the no-second-agent rule and the in-seat-at-tier allowance are unchanged.
  3. contract-review.md trigger — 「欠不欠按面判 ⛔ 不按车道:diff 或认领申报碰下列任一面即欠达档复核,交付后当轮完成。」 — RIGHT and necessary: the first face is a claim-comment declaration, not a diff path; without 「或认领申报」 the trigger could never fire for it. Not wider than the ruling: the declaration leg is already how landing-operations.md 「声明肢」 reads it.
  4. landing-operations.md 「交付后复核按面欠 ⛔ 不按车道(三面见 references/contract-review.md);双肢命中即 spec 车道。」 — RIGHT. Pointer count only; the two-leg spec-lane rule is untouched.
  5. execution-duties.md 「契约三面自审资格按车道:spec/skills 席达档席内审;未达档 ⛔ 不自审,起隔离达档子代理。」 — RIGHT. Eligibility is renamed to the faces it now governs; who may review in-seat and the isolated-subagent fallback are unchanged. 「spec 与 skills」 → 「spec/skills」 is spelling only (118 bytes against the 120-byte cap).
  6. lanes/ui.md 「契约三面复核本席适用:按面欠达档记录落 PR 或卡,散文面归 ACCEPT,见 ../contract-review.md。」 — RIGHT. The added clause 「散文面归 ACCEPT」 is fork 1's third bullet, not an extension. Dropped 「归属与资格」 and one 「复核」 are qualifiers on a pointer whose target heading is itself 「复核归属与资格(按面)」; no operative loss.
  7. review-checklist.md new line — 「changeset/CHANGELOG、content/docs/**、apps/docs/**:逐句核 diff,ACCEPT 点名所核句,⛔ 不起子代理。」 — RIGHT. execution-duties.md 〈复核〉 line 「逐项判据展开在 references/review-checklist.md,每份报告对着它过」 makes this checklist the ACCEPT's path, so the ruling's ACCEPT duty belongs here. Same content as sentence 1's tail; 「不起子代理」 and 「不起第二个 agent」 are one rule.
  8. review-checklist.md compression (the payment for 7) — 「dev 本地并集钉 head:所抄 git rev-parse --short HEAD 不符 PR head.sha 或没抄 ⇒ 双向都不入账。」 replacing two lines — RIGHT, no operative loss on the seat side: the seat's rule (compare to head.sha; mismatch or missing ⇒ neither direction counted) survives whole, and 「按没有读数处理」 and 「双向都不入账」 are the same outcome. The dropped words (run after the final commit; quote the sha in both the report and the body) are the dev's obligation and stand at head in .claude/agents/os-dev.md lines 159–160: 「并集在最终 commit 之后跑,并从那次运行引用 git rev-parse --short HEAD。」 and 「报告的 tests 字段和 PR 正文两处都要引」. This payment was forced: review-checklist.md stands at 77 / 77 (CEILINGS, headroom 0).
  9. contract-review.md independence-pair line — 「独立性对:Implemented-by:/Reviewed-by: 取值同见 --template;子代理记采纳它的席位。」 — WIDER THAN THE RULING, declared, loss-free. It is not a face-list sentence; the PR body and both reports present it as the ratchet's price for the face list's extra line, but the CEILINGS row for contract-review.md is 60 against 40 lines at head (headroom 20), so no payment was owed there. The rule itself is intact: the full node scripts/pm/record-recognisers.mjs --template is spelled ten lines above on the Served-tier: line, and --template is distinct from --brief-template. A compression the ratchet header calls 「always legitimate and encouraged」, inside a file the PR already owns; not a reason to fail, recorded so the seat does not read 「paid」 as 「required」.

The two lines ruled unchanged (fork 2) are byte-unchanged: execution-duties.md 「改到已有 .changeset/*.md 的 PR ⛔ 不打 skip-changeset;确认取同 head 达档 PASS 或维护者原话。」 and landing-operations.md 「DELIBERATE CORRECTION 红(check-empty-changeset):同 head 达档复核 PASS 记录即确认,⛔ 不等维护者。」 each grep exactly once at base and at head with identical md5 of the matching line.

Residue: a git grep of the head tree over .claude/, AGENTS.md, CLAUDE.md for 「五面|五处|四面|按面欠|复核面」 hits only the three rewritten anchors plus one unrelated count in spec-property-retirement/SKILL.md (「实测五处、三个包、两张卡」, a retirement story, not the rule). No remaining sentence ties changeset or docs prose to an at-tier record. No added line cites an issue number; no model identifier in the five files, the PR title or the body.

Line and byte readings at head, taken with git show: 40 / 60, 101 / 101, 183 / 183, 38 / 38, 77 / 77 lines; the longest added line is 119 bytes against MAX_LINE_BYTES = 120. These are corroboration only; the gate verdict is Lint & Repo Gates, below.

② Semver level

skip-changeset is correct: the diff publishes nothing from any released package (.claude/** only; zero .changeset/*.md in the diff; no packages/** path). Clause-②: no is declared on the claim (5934212807) and on the PR body's first lines, and matches the diff — no published export, schema or accept set moves. The PR's 「Deliberately unchanged」 and 「Line budget」 sections match the trees byte for byte and count for count.

Clause-②: no

③ Boundary flags

open_questions: empty in both os-dev-reports — nothing to answer. The dev's declared deviations, each answered:

  • Round 0, contract-review.md line payment (sentence 9 above): accepted as a declared, loss-free edit; its stated reason is wrong (headroom 20, not 0), noted for the seat; not escalated.
  • Round 0, lanes/ui.md 「散文面归 ACCEPT」 clause: inside fork 1; right.
  • Round 0, 「spec 与 skills」 → 「spec/skills」: spelling for the 120-byte cap; right.
  • Round 0, PR body paraphrases the seat's proposal because it carries a model-name token: right — AGENTS.md forbids a model identifier in a PR body, the verbatim-quotation exemption covers only maintainer rulings, and both maintainer rulings are quoted verbatim.
  • Round 0, model-free commit trailer pair and session-URL footer: right; the body carries exactly one footer in the PR-body form.
  • Round 0, mechanism re-measured on base 2c1cef334: consistent with this review's own grep of the head tree.
  • Round 1, CONFLICT — the dev did not PATCH the PR body and handed pr_body_replacement to the seat: resolved — the body read for this review already is the replacement (sentences 7 and 8 present, 「All five files」, 18 gate rows, review-checklist.md budget row), one footer. No action.
  • Round 1, new-line spelling 「⛔ 不起子代理」: same rule as 「不起第二个 agent」; right.
  • Round 1, review-checklist.md compression: answered in ① item 8 — forced by headroom 0, seat-side rule intact, dev-side words carried by os-dev.md.

out_of_scope_findings: the round-0 note that the ACCEPT path did not list the prose sentences is resolved by sentence 7. The remaining note — the card's rationale cites a changeset claim re-read that is objectui's (changeset-presence.yml), with no counterpart in this tree — is correctly noted in the PR's acceptance notes and not filed: it is neither a reproducible defect, a contract violation nor an authoring trap (Prime Directive #10), and the ruling's reversibility reasoning (a pending changeset stays editable until the Version Packages PR merges) holds on objectstack without that facility. Not escalated.

Check-runs on the head at 2026-10-01T15:51Z: Lint & Repo Gates still in_progress — it carries check:pm-skill-ratchet, check:pm-skill-id-lint and check:pm-governed-prose, so the gate verdict on the five files is not yet in; the seat re-reads before landing. Completed success: TypeScript Type Check (and its four legs), Test Core, Dogfood Regression Gate, Governed Surface Queue Guard, Check Documentation Links, Check PR Size, Auto Label, the four claim/branch guards, filter. skipped by the paths filter for a .claude/**-only diff, not failures: Build Core, Build Docs, Console Pin Gate, Temporal Conformance (live PG + MySQL), Dogfood Verify CLI, the Test Core and Dogfood shards, Packed-tarball smoke (opt-in), and Check Changeset (the skip-changeset label). No check-run has failed. This verdict is on the diff; it does not stand in for the in-progress gate.

Implemented-by: claude/issue-21187-prose-review-at-accept
Reviewed-by: session_016tKoy8NJa35Yih1FdzrVmn

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 1, 2026 16:03
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 1, 2026
@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 36889153084 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Temporal Conformance (live PG + MySQL) — 失败步骤: Run the non-SQL temporal backends under the skewed process zone

    packages/services/service-analytics test:  FAIL  src/__tests__/caller-content-admission-door.test.ts > [#21177] caller-content admission — the /analytics/query door > 'NativeSQLStrategy' > 'no securit
      ↳ 失败原因: (这条 FAIL 之后 12 行内没有可识别的原因行 —— 点进 job 看)
    packages/services/service-analytics test:  FAIL  src/__tests__/caller-content-admission-door.test.ts > [#21177] caller-content admission — the /analytics/query door > 'NativeSQLStrategy' > 'admin — pr
      ↳ 失败原因: (这条 FAIL 之后 12 行内没有可识别的原因行 —— 点进 job 看)
    packages/services/service-analytics test:  FAIL  src/__tests__/caller-content-admission-door.test.ts > [#21177] caller-content admission — the /analytics/query door > 'NativeSQLStrategy' > 'non-admin 
      ↳ 失败原因: (这条 FAIL 之后 12 行内没有可识别的原因行 —— 点进 job 看)
    packages/services/service-analytics test:  FAIL  src/__tests__/caller-content-admission-door.test.ts > [#21177] caller-content admission — the /analytics/query door > 'ObjectQLStrategy' > 'no security
      ↳ 失败原因: (这条 FAIL 之后 12 行内没有可识别的原因行 —— 点进 job 看)
    packages/services/service-analytics test:  FAIL  src/__tests__/caller-content-admission-door.test.ts > [#21177] caller-content admission — the /analytics/query door > 'ObjectQLStrategy' > 'admin — pro
      ↳ 失败原因: (这条 FAIL 之后 12 行内没有可识别的原因行 —— 点进 job 看)
    packages/services/service-analytics test:  FAIL  src/__tests__/caller-content-admission-door.test.ts > [#21177] caller-content admission — the /analytics/query door > 'ObjectQLStrategy' > 'non-admin —
      ↳ 失败原因: packages/services/service-analytics test: AssertionError: expected Error: [Analytics] Access denied: member … { …(4) } to match object { code: 'INVALID_FIELD', …(3) }
    packages/services/service-analytics test:  FAIL  src/__tests__/field-read-admission-gate.test.ts > [#20965] the field-level read gate — a member that names no field is refused, never stood down > 'Nat
      ↳ 失败原因: (这条 FAIL 之后 12 行内没有可识别的原因行 —— 点进 job 看)
    packages/services/service-analytics test:  FAIL  src/__tests__/field-read-admission-gate.test.ts > [#20965] the field-level read gate — a member that names no field is refused, never stood down > 'Obj
      ↳ 失败原因: packages/services/service-analytics test: AssertionError: expected Error: [Analytics] Access denied: member … { …(4) } to match object { code: 'INVALID_FIELD', …(2) }
    

↳ 失败原因 是判读的关键:超时(Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言(AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError。 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 3 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

Merged via the queue into main with commit 5e5ce48 Oct 1, 2026
35 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-21187-prose-review-at-accept branch October 1, 2026 16:38
This was referenced Oct 1, 2026
This was referenced Oct 1, 2026
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…e policies — double accumulation, the PostgreSQL boolean cast and the empty-sum fold, hoisted into core (objectstack-ai#21042) (objectstack-ai#21209)

Fixes objectstack-ai#21042
Clause-②: yes (widening)

## What this changes

The analytics native-SQL strategy (`NativeSQLStrategy`, the default on a
SQL driver) skipped three aggregate policies that
`SqlDriver.aggregate()` applies. So one route answered different
numbers, or a `500`, depending on which strategy served it. This PR
follows the route ruling on objectstack-ai#21042 (comment `5925613967`): the operand
policies are hoisted into `@objectstack/core`, beside
`AGGREGATE_ANSWER_KIND`, and both faces read them from there.

- **`packages/core/src/utils/aggregate-answer.ts`**
(`@objectstack/core`, `minor`). It takes:
- `AGGREGATE_ACCUMULATION`, moved from `driver-sql` with its docblock
(the docblock and the table are byte-identical to the base, apart from
the `export` keyword);
- `aggregandColumnClass({ type, multiple })`, the one column-class
predicate (`'fractional'`, `'integral'`, `'boolean'`, or none);
- `POSTGRES_BOOLEAN_AGGREGAND_CAST`, the objectstack-ai#11635 cast, as a `Record` over
`AggregationFunction`: `sum` / `avg` / `min` / `max` are cast, the
counts never are;
- `doubleAccumulationOperand(operand, dialect)`, the double operand with
the dialect as a parameter;
- `aggregandOperandSql(func, columnClass, dialect, operand)`, the one
composition both faces emit (the cast inside, the double operand around
it).
  No `index.ts` line was added: the module is already exported.
- **`packages/drivers/driver-sql/src/sql-driver.ts`** (`patch`), in the
ruled regions only. The `AGGREGATE_ACCUMULATION` table becomes a pointer
plus an import. `isFractionalNumericType` is deleted. The two registry
fills fill `fractionalNumericFields` through the predicate.
`accumulatesInDouble` / `doubleAccumulationOperand` are replaced by
`aggregandColumnClassOf`, which maps the driver's registries onto the
predicate's classes. In `aggregate()`, the private boolean-cast
condition and the accumulation call become one
`aggregandOperandSql(funcName, class, this.dialectName, '??')`.
-
**`packages/services/service-analytics/src/strategies/native-sql-strategy.ts`**
(`patch`), in two places.
- **`resolveMeasureSql`** wraps the column it hands `AGGREGATE_SQL` /
`CONDITIONAL_AGGREGATE_SQL` in `aggregandOperandSql`. The column class
comes from the declaration the host already relays
(`declaredValueShape`), on the object the column lives on
(`columnObjectOf`, the one hop resolver). The dialect comes from
`sqlDialect`, which the strategy already reads.
- **The `execute` shaping point** that PR objectstack-ai#21040 added now folds a
`null` measure answer to `emptyGroupValueFor(measure.type)`
(`@objectstack/spec`). It does this for every measure, measure-scoped
ones included, before the number presenter, in `driver-sql`'s order. The
dataset door's `DatasetExecutor` fill stays, and it is idempotent on a
folded row.
- The one line outside those two regions is the `generateSql` call site,
which now passes `ctx` to `resolveMeasureSql`.
- No native copy of any policy, and no runtime hook asks the driver: the
rejected (C) route was not taken. `canHandle`, `buildFieldMeta`, the
hop-object sites, the filter / text-match rendering,
`analytics-service.ts` and `field-read-admission.ts` are untouched.

## The card's table, before and after

Measured through `AnalyticsService.query` (the cube door, which `POST
/api/v1/analytics/query` relays verbatim) and
`AnalyticsService.queryDataset` (the dataset door), on
`AnalyticsServicePlugin` over a real ObjectQL engine and `SqlDriver`.
**Native** is the plugin's own composition (`NativeSQLStrategy`
answered, with one raw statement and no engine aggregate). **ObjectQL**
is the same composition narrowed to `engine.aggregate`. "Before" is the
strategy file at the base `d34aa58a2a`; "after" is this branch at
`61aab5013a`. Neither merge since then touches the aggregate code paths,
and the pins below are green at `ef1f9d8484`.

Fixture:

- group `f`: `frac` (a `number` column) holds 0.1 and 0.2, and `flag`
holds true and false;
- group `i`: `stars` (a `rating` column) holds seven 1s and two 2s, and
`flag` holds 7 trues and 2 falses;
- group `n`: every aggregand is NULL in all three rows.

The measure-scoped measures filter on `tag = x`, which only group `f`
holds.

**PostgreSQL 16.13** (a private local server; the ObjectQL column is the
same before and after):

| measure | group | door | native before | native after | ObjectQL |
|:--|:--|:--|:--|:--|:--|
| `sum(frac)` | f | cube, dataset | `0.3` | `0.30000000000000004` |
`0.30000000000000004` |
| `avg(frac)` | f | cube, dataset | `0.15` | `0.15000000000000002` |
`0.15000000000000002` |
| `avg(stars)`, an integer column | i | cube, dataset |
`1.222222222222222` | `1.2222222222222223` | `1.2222222222222223` |
| `sum(flag)` | i | cube, dataset | `500 DATABASE_ERROR` | `7` | `7` |
| `avg(flag)` | i | cube, dataset | `500 DATABASE_ERROR` |
`0.7777777777777778` | `0.7777777777777778` |
| `min(flag)` / `max(flag)` | i | cube, dataset | `500 DATABASE_ERROR` |
`0` / `1` | `0` / `1` |
| `sum(frac)`, all-NULL group | n | cube | `null` | `0` | `0` |
| `sum(frac)`, all-NULL group | n | dataset | `0` (executor fill) | `0`
| `0` |
| `sum(flag)`, all-NULL group | n | cube | `500 DATABASE_ERROR` | `0` |
`0` |
| `avg(frac)`, all-NULL group | n | cube, dataset | `null` | `null` |
`null` |
| measure-scoped `sum(frac)`, no admitted row | i | cube | `null` | `0`
| `0` |
| measure-scoped `sum(frac)`, no admitted row | i | dataset | `0`
(executor fill) | `0` | `0` |
| measure-scoped `avg(frac)`, no admitted row | i | cube | `null` |
`null` | `null` |
| `count` control | n | cube, dataset | `3` | `3` | `3` |
| measure-scoped `count` control | i | cube, dataset | `0` | `0` | `0` |

**SQLite** (better-sqlite3): accumulation and the boolean answers
already agreed on every face (`0.30000000000000004`,
`0.15000000000000002`, `1.2222222222222223`, `7`, `0.7777777777777778`,
`0` / `1`). The fold is the policy that diverged there:

| measure | group | door | native before | native after | ObjectQL |
|:--|:--|:--|:--|:--|:--|
| `sum(frac)` / `sum(stars)` / `sum(flag)`, all-NULL group | n | cube |
`null` | `0` | `0` |
| `sum(frac)` / `sum(stars)` / `sum(flag)`, all-NULL group | n | dataset
| `0` (executor fill) | `0` | `0` |
| measure-scoped `sum(frac)`, no admitted row | i, n | cube | `null` |
`0` | `0` |
| measure-scoped `sum(frac)`, no admitted row | i, n | dataset | `0`
(executor fill) | `0` | `0` |

After the fix, the native and ObjectQL faces **differ in 0 of 144
cells** (2 drivers × 2 doors × 12 measures × 3 groups).

**MySQL is NOT MEASURED**: there is no MySQL server in this container.
The MySQL operand text is pinned offline: by `core`'s
`aggregate-answer.test.ts`, and by the `driver-sql` move proof for the
driver's own statements.

## The move proof

`driver-sql`'s aggregate statements were dumped at the base, before any
consumer changed. The dump covered `SqlDriver.aggregate()` for every
function (`count`, `count_distinct`, `sum`, `avg`, `min`, `max`, and
`count(*)`), aliased and unaliased, over 23 columns: every fractional,
integral and boolean type, the `float` / `integer` / `int` aliases,
multi-valued and untyped columns, and text / date / lookup / formula. It
ran on SQLite, PostgreSQL and MySQL, through both registration paths
(`registerObjectMetadata` and `registerExternalObject`), offline (knex
`toSQL()`).

The policies were then hoisted, `driver-sql` was switched to the
imports, and the same dump was run again:

- base dump: 1668 entries, 0 errors, md5
`8eee668372a28a7568f3eb1cc5a2bc9b`;
- after dump (at `bc8aa0cc2f`): 1668 entries, md5
`8eee668372a28a7568f3eb1cc5a2bc9b`. `cmp` printed nothing: the two dumps
are **byte-identical**.

`sql-driver.ts` and `aggregate-answer.ts` are unchanged between
`bc8aa0cc2f` and `61aab5013a`.

The committed move-proof pin,
`packages/drivers/driver-sql/src/sql-driver-21042-aggregate-policy-move.test.ts`,
holds the captured expressions for one column of each class, on each
dialect and through each registration path. It passed at the base
(`192fc0010b`: 54 / 54) and passes after (54 / 54).

## Pins (committed red first, then the fix)

| file | at the pins commit (`192fc0010b`, base code) | after |
|:--|:--|:--|
| `core` `aggregate-answer.test.ts` | 16 red (the exports did not exist)
| 22 / 22 |
| `service-analytics` `native-sql-aggregate-policies.test.ts` (each
measure on both faces at both doors, against the engine's arithmetic;
SQLite and live PostgreSQL cells) | SQLite: the cube-door folds red.
PostgreSQL: accumulation, boolean `500`, folds red | 49 / 49 |
| `service-analytics` `cube-measure-field-type-door.test.ts`, **the
lifted skip** | PostgreSQL native `max(boolean)` red (`500`) | 23 / 23 |
| `rest` `analytics-dataset-aggregate-policies-door.test.ts` (the route,
both strategies) | PostgreSQL: 7 red (accumulation and booleans). SQLite
green (that door already folded) | 19 / 19 |
| `driver-sql` move proof | 54 / 54 | 54 / 54 |

**The lifted skip:** `it.skipIf(cell.id === 'pg' && face === 'native')`
in `cube-measure-field-type-door.test.ts` (from PR objectstack-ai#21128) is gone. Its
comment now says why the cell runs on every cell and face. The
PostgreSQL native `max_flag` cell answers `1`.

`native-sql-measure-number-presentation.test.ts` gets a comment-only
edit: its header said the native statement does not carry objectstack-ai#20387's
accumulation, and it now points at the new pin.

## Ablations

There was one ablation per policy, each predicted in writing before it
ran. Each mutation was planted through `scripts/ablation-replace.mjs`,
which checks that the anchor hit and that the blob changed. Each
mutation was confirmed in the built `dist/`
(`ablation-dist-preflight.mjs`: marker present). Each restore ran by
absolute path (`git checkout HEAD`), and the file's blob was proven
equal to its `HEAD` blob with `git diff HEAD` empty. After each restore
the package was rebuilt, and the marker was proven absent from `dist/`
with the tree clean. Every prediction held exactly.

| ablation | mutation | predicted red | observed red |
|:--|:--|:--|:--|
| A1 accumulation | `core` `accumulatesInDouble`'s dialect gate never
admits PostgreSQL or MySQL | `core` 3; `driver-sql` move proof 24 (pg
and mysql × both fills × the six numeric / boolean columns);
`service-analytics` 10, PostgreSQL only (both doors × `sum` /
`avg(frac)`, `avg(stars)`, measure-scoped `sum` / `avg`); `rest` 3,
PostgreSQL only | the same 3 / 24 / 10 / 3; SQLite cells green;
`avg(flag)` green as predicted |
| A2 boolean cast | `core` `aggregandOperandSql` never casts | `core` 1;
move proof 4 (pg × both fills × `boolean` / `toggle`);
`service-analytics` 8, PostgreSQL only; the lifted cell, PostgreSQL
native and ObjectQL, 2; `rest` 4, PostgreSQL only | the same 1 / 4 / 8 /
2 / 4 |
| A3 fold | the native shaping point never folds | `service-analytics`
8, cube door only (SQLite and PostgreSQL × the three all-NULL `sum`s and
the measure-scoped `sum`); everything else green, the `rest`
dataset-door route included, because the executor fill folds there | the
same 8; `rest` 19 / 19 green |

A1 and A2 show one policy reaching both faces. Each one turned
`driver-sql`'s own statements red. Under A2 the ObjectQL face's
`max(boolean)` cell failed too, with the driver's refusal. Under A1 the
ObjectQL face answered the same exact decimal as the native face for the
plain measures: the failing assertion was the engine's number, while
native and ObjectQL still agreed.

A **reverse type check** also ran. Passing a dialect the new type
rejects (`'oracle'`) to `aggregandOperandSql` turned
`service-analytics`' typecheck red (`TS2345 ... not assignable to
parameter of type 'AggregandSqlDialect'`), which shows the rebuilt
`core` `.d.ts` was read. The file was restored byte-identical.

## Verification (at `ef1f9d8484`, after merging `origin/main` at
`cb45469e67`, which carries PR objectstack-ai#21170 and PR objectstack-ai#21173)

Everything below ran as one locked script, at `ef1f9d8484`, with each
exit code captured before any pipe. The live PostgreSQL 16.13 server ran
at `timezone = Asia/Shanghai`, and the `driver-sql` suite ran under
`TZ=America/New_York`, which are its own non-vacuity preconditions.

- **Refresh after the merge:** `pnpm turbo run build
--filter='!@objectstack/docs' --concurrency=1` exit 0, and `pnpm
--filter @objectstack/spec check:generated` exit 0.
- **Gates:** `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 67 commands from the real diff (10
paths). All 67 exited 0. Reconciliation with `--ran` and the recorded
exit codes printed: `Run reconciliation — 67 derived, 67 run, 0
NOT-MEASURED, 0 UNRUN.`
- **Typecheck:** `pnpm --filter … typecheck` exit 0 for
`@objectstack/core`, `@objectstack/driver-sql`,
`@objectstack/service-analytics` and `@objectstack/rest`.
- **Tests, every vitest project of every touched package** (`vitest run
--maxWorkers=2`, with `OS_TEST_POSTGRES_URL` set so the live cells ran):

| package / project | files | tests |
|:--|:--|:--|
| `core` local | 74 passed | 2120 passed |
| `core` repo | 3 passed | 48 passed |
| `driver-sql` | 216 passed, 3 skipped | 4300 passed, 96 skipped |
| `service-analytics` | 161 passed | 3765 passed |
| `rest` local | 256 passed | 5027 passed, 127 skipped |
| `rest` repo | 5 passed | 179 passed, 1 skipped |

- **The five pin files, run explicitly:** move proof 54 / 54, `core` 22
/ 22, policies 49 / 49 (24 SQLite + 24 PostgreSQL + the oracle),
field-type door 23 / 23, `rest` route 19 / 19 (9 SQLite + 9 PostgreSQL +
the oracle).
- **Lint, narrowed and proven:** `eslint --no-inline-config --format
json` over the 9 changed code files answered 9 results, 0 errors and 0
warnings. The population is read from eslint's own config:
`ESLint.isPathIgnored` answers `false` for each of the 9. The narrowing
excludes nothing that could move, because `eslint.config.mjs` never
enables type-aware linting (no `parserOptions.project`, no typed rules),
so this diff cannot change the verdict on an untouched file. The
repo-wide `pnpm lint` is CI's.
- **The `driver-sql` live preconditions:** the first gate run used a
private server at UTC, and the suite's four timezone non-vacuity cells
failed by design (`… start it with timezone=Asia/Shanghai`). With the
server at `Asia/Shanghai` and the process at `America/New_York` the
suite is green, as listed above.
- **`main` after the final merge:** `origin/main` moved 4 commits past
`cb45469e67` before this PR opened (objectstack-ai#21149, objectstack-ai#21188, objectstack-ai#21195, objectstack-ai#21192).
None of them touches `core`, `driver-sql`, `service-analytics` or the
analytics `rest` tests. The one `packages/spec` file in the analytics
area, `ui/dataset.zod.ts`, changes a comment only. They are not merged
here; CI runs on the merge ref.

## Acceptance notes

- **MySQL is NOT MEASURED** (no server in this container). The MySQL
operand text is pinned offline in `core` and in the `driver-sql` move
proof.
- **The live PostgreSQL cells are not run in CI.** No CI step sets
`OS_TEST_POSTGRES_URL` for `service-analytics` or `rest`. The cells
above ran against a private PostgreSQL 16.13 started for this run and
removed afterwards. In CI the SQLite cells run, and so do the offline
`core` / move-proof pins.
- **Phase 0's note on the dataset door, which is no divergence:** a
measure-scoped `avg` is **absent** from a row its supplementary query
reported no row for. That is `x_avg_frac` for groups `i` and `n`, on
both strategies and before and after. It is not `null`. The new service
pin holds this cell only to "both faces agree", not to a value.
Relatedly, a dataset-door selection made only of measure-scoped measures
reports only the groups their filter admits, so the pin asks each one
beside the base count.
- **Residual, as stated in the ruling:** a host that relays no field
declarations (`declaredValueShape`), or names no SQL dialect, gets no
column class or no policy. It keeps the native arithmetic it had, and a
PostgreSQL boolean `sum` there still answers `500`. The plugin's own
composition wires both.
- **How `driver-sql` reads the class:** it reads its own registries
rather than calling the predicate per column. `fractionalNumericFields`
is filled by the predicate. `booleanFields` and `numericFields` are
filled by the driver's coercion rules, whose populations equal the
predicate's `'boolean'` and `'fractional'` ∪ `'integral'` classes. The
move proof pins that equality per column class. Asking the predicate per
column through the driver's `valueShapeFields` would retire
`fractionalNumericFields`, but its declaration and shard-alias regions
are outside the ruled surface, so this PR does not do it.
- **The scan-order residual is unchanged.** On PostgreSQL and MySQL the
double sums are added without compensation (`AGGREGATE_ACCUMULATION`'s
docblock), so three or more fractions can still differ in the last place
from SQLite and the rows path. The pins use two addends.
- objectstack-ai#21129 (the presenter for a relationship-path `min` / `max`) is not
addressed here.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01XY5uCwTjZj7884yYtyur4H)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
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/s skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants