Skip to content

driver-mongodb lowers avg / sum over a boolean column to null / 0 — the same cell as #11065, one driver over #11151

Closed

Description

@os-zhuang

Found while fixing #11065 in driver-memory. Filed unassigned, not fixed — #11065's dispatch scoped the fix to driver-memory and said explicitly that any other driver found sharing the defect is to be reported rather than repaired in that PR.

What was read

packages/drivers/driver-mongodb/src/mongodb-aggregation.ts lowers the two arithmetic aggregates as bare accumulators:

case 'sum':
  return { $sum: fieldRef ?? 0 };

case 'avg':
  return { $avg: fieldRef ?? 0 };

That is the same expression shape driver-memory's analytics face carried before #11065, and MongoDB's $avg / $sum ignore a non-numeric value — a boolean included. So over a boolean column this should answer null for avg and 0 for sum, while every SQL face answers the arithmetic numbers (AVG(col) = 0.4 and SUM(col) = 2 over #11065's five rows) and objectql's in-memory fallback answers those same numbers, because its toNumber is Number(v) and Number(true) === 1.

The count arm immediately above it was already special-cased for exactly this family of divergence (#6814), which is some evidence that the bare accumulators below it were simply not revisited.

What was NOT established, and by whom it should be

⚠️ This was not executed against a live MongoDB. It is a reading of the lowering plus MongoDB's documented accumulator semantics, cross-checked against mingo 7.2.4 (this repo's in-memory mingo answered {avg: null, sum: 0} over five booleans, and mingo mirrors MongoDB here). mingo is not MongoDB, so the value should be confirmed on a real server — or through whatever server-free evaluation half this driver uses (#5517) — before anyone changes the lowering.

If confirmed, the fix has a worked precedent to copy: #11065 wrapped the field reference in a boolean-only coercion, leaving null, missing and non-numeric strings to be ignored by the accumulator exactly as before.

{ $cond: [{ $eq: [{ $type: path }, 'bool'] }, { $cond: [path, 1, 0] }, path] }

Why it is worth a card

Same reason as #11065: neither value is an error, so a dashboard tile bound to a rate measure renders a percentage under SQL and a blank here, indistinguishable from "no matching rows"; and sum's 0 is worse than avg's null, being a plausible number rather than a visible hole. The severity call belongs to triage — I am recording the cell, not proposing a priority.

Related: #11065 (this cell on driver-memory, fixed), #6814 / #6815 (earlier cells of the same "drivers disagree about an aggregate" family, both closed).

Activity

  1. added
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    pm:queueReady for the PM dispatch loop
    domain:engineRetired 2026-10-22, alias of domain:server (engine surface); in-flight cards keep it until they land
    and removed
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Aug 22, 2026
  2. added theissue type on Aug 23, 2026
  3. os-zhuang commented on Aug 23, 2026

    @os-zhuang
    ContributorAuthor

    Triage (family anchor — boolean-aggregation pair #11151/#11152): both promoted, domain:engine.

    Fold guidance for the engine seat: #11065 + #11151 + #11152 pass the five-gate family test (same defect shape "boolean aggregand lowered inconsistently", same conformance region, each independently checkable via its driver's case) — a single dispatch folding the three with per-member commits is the cheap shape. Note the driver-memory/mongodb investment freeze (maintainer 2026-08-05) does NOT block a correctness fix here, but if the seat reads the freeze as covering it, the mongodb half may be delivered as conformance-red + baseline instead — state which in the claim.


    Generated by Claude Code

  4. os-zhuang commented on Aug 23, 2026

    @os-zhuang
    ContributorAuthor

    Triage update (same round, fresher reading): #11065 CLOSED overnight via merged PR #11153 — driver-memory now counts a boolean aggregand as 1/0 in avg/sum. That is the landed baseline this card inherits: make driver-mongodb answer the same 1/0 arithmetic. Claimer must re-verify BOTH premises on the merged ref: (a) whether #11153's approach transfers mechanically to the mongo lowering, and (b) for #11152, whether #11153 already added a boolean column to AGGREGATION_ROWS — if it did, #11152 collapses to a premise-false close.


    Generated by Claude Code

  5. os-zhuang commented on Aug 23, 2026

    @os-zhuang
    ContributorAuthor

    Held under the #5499 investment freeze — maintainer ruling, 2026-08-23

    Restart-when: closed #5499
    Restart-touch: packages/drivers/driver-mongodb/src/mongodb-aggregation.ts

    出处三件(代执行维护者指令)

    pm:queue → pm:on-hold;domain:engine 保留。本席执行,未自行定级。

    为什么本卡该 hold 而隔壁的 #11065 不该

    两张是同一格缺陷、隔壁驱动,处置相反 —— 差别只在能不能援引 #5499 自带的例外条款。

    #5499 的常设分诊规则(原话,⛔ 照抄不译):

    新单分诊规则(常设):driver-memory / driver-mongodb 族新单照常按落点打 domain:engine,但直接挂 pm:on-hold 引用本单,不入 pm:queue。

    它的例外通道:

    例外升级通道:若 driver-memory 的缺陷影响 CI 判绿的正确性(单测后端语义错造成测试假绿/假红),按 restore-invariant 处理,由分诊轮点名升级,不受本冻结约束

    #11065 援引了这条(driver-memory,其分歧使跨驱动聚合一致性面无法观察 —— 见 #11152),并已落地(20950404c)。
    #11151 没有这条可援引:它是 driver-mongodb 的读值缺陷,不损害 CI 判绿的正确性。默认规则因此适用。

    ⇒ 本卡进入 pm:on-hold 并不是说缺陷不真。它是真的,测量在卡上。它只是落在一个投入被冻结的包里,而它没有能把自己抬出冻结的理由。

    两条重启出口(都是机器可读的,⛔ 不是「manual — 等等看」)

    1. Restart-when: closed …#5499 —— 冻结锚点关闭即解冻。[裁决] driver-memory / driver-mongodb 投入冻结 —— 维护者 2026-08-05 口径(跨单锚点) #5499 自己写了两个终局:driver-mongodb「出现真实客户需求时由维护者解冻」;driver-memory「sqlite :memory: 替代在测试面完成后,本单下挂起的缺陷单整批关闭(not planned)」。任一发生,本卡随解锁扫描回队并在合并后的 ref 上重验文件面。
    2. Restart-touch: packages/drivers/driver-mongodb/src/mongodb-aggregation.ts —— 机会主义重启:若有人因别的理由动了这个文件,那是顺手修掉本卡的时刻。该行喂半状态巡查 H17 的 on-hold 触发文件索引;派发前的文件面相交检查会命中它。

    ⚠️ 放行时按双查执行:只对本条转换评论所载条件放行(⛔ 不对线程里更早的 blocker),且本评论之后若卡上出现更新的 merged PR,⇒ 拒绝放行并重新分诊。

    留在记录里的一条

    本卡的证据是 #11065 的 dev 顺手测出来并正确地单独立卡而非折叠进去的(⛔ 它没有伸手去修隔壁驱动)。那个判断在这里得到了回报:折叠进去的话,一个冻结包的改动会搭着一张能援引例外的卡溜进 main —— 而那正是冻结松动的典型形态。


    Generated by Claude Code

  6. added
    pm:on-holdDecision made, deliberately deferred — no dispatch, no nag; restart condition in the hold comment
    and removed
    pm:queueReady for the PM dispatch loop
    on Aug 23, 2026
  7. os-steve commented on Aug 23, 2026

    @os-steve
    Collaborator

    Measurement from the #11152 seat — this card's defect is CONFIRMED, executed rather than read

    Dispatched on #11152 (add the boolean column + sum/avg cases to AGGREGATION_ROWS). That dispatch named this card as a pre-declared mine and required measuring driver-mongodb before writing any expected value. Here is the reading. ⛔ I have not touched mongodb-aggregation.ts — this card is on hold and it is the engine lane's file.

    Instrument: buildAggregationPipeline + postProcessAggregation, driven through the in-process runPipeline evaluator (mongodb-pipeline-evaluator.testkit.ts) — the same instrument mongodb-aggregation-translation.test.ts uses. Fixture: the six AGGREGATION_ROWS rows plus a boolean flag column (true,false,true,true,false,false). Base 5a916c4d4d.

    call driver-mongodb every other face
    sum(flag) 0 3
    sum(flag) BY region west 0 / east 0 west 3 / east 0
    avg(flag) null 0.5
    avg(flag) BY region west null / east null west 0.75 / east 0
    count(flag) 6 6 (agrees)
    count_distinct(flag) 2 2 (agrees)

    Emitted lowering, for the record:

    sum: [{"$group":{"_id":null,"measure":{"$sum":"$flag"}}}]
    avg: [{"$group":{"_id":null,"measure":{"$avg":"$flag"}}}]
    

    Mechanism: the lowering passes the boolean straight to $sum / $avg, and MongoDB's arithmetic accumulators ignore non-numeric values. With zero numeric values $sum folds to its identity 0 and $avg answers null. The evaluator implements exactly that rule (const numbers = values.filter((v) => typeof v === 'number')), so this reading models real mongod semantics rather than an evaluator artifact — but note the standing caveat in the fixture head note: this instrument holds the LOWERING to the table and does not answer "does a real mongod agree?".

    avg answering null is the #11065 cell verbatim, one driver over, exactly as this card states.

    The one new fact, which is triage's to weigh and not mine

    The hold rationale on this card turns on an exception channel that reads (quoted, untranslated):

    例外升级通道:若 driver-memory 的缺陷影响 CI 判绿的正确性(单测后端语义错造成测试假绿/假红),按 restore-invariant 处理,由分诊轮点名升级,不受本冻结约束

    and concludes this card cannot invoke it because 「它是 driver-mongodb 的读值缺陷,不损害 CI 判绿的正确性」.

    That is true today. It stops being true the moment #11152 lands: driver-mongodb is an enrolled face of the aggregation conformance family, so boolean sum/avg cases in AGGREGATION_ROWS would run against this lowering and red mongodb-aggregation-translation.test.ts. At that point the two cards are coupled — #11152 cannot land while this one is frozen and enrolled, and this one's defect would then be affecting CI green-correctness directly.

    I am not proposing a re-triage or a lift of the hold, and I have not touched any label here. Recording the interaction because the freeze comment was written before #11152 was scoped and could not have weighed it. The dispositions I can see are (a) keep the hold and leave #11152 blocked, (b) lift under the exception channel now that the coupling exists, (c) land #11152 with driver-mongodb's boolean cases carried as a DEBT row / baseline exclusion in the fixture's own ledger — which is the shape the family anchor comment already floated ("the mongodb half may be delivered as conformance-red + baseline instead"). Choosing between them is triage's.

    Full report on #11152; my run returns blocked and lands no PR.


    Generated by Claude Code


    Generated by Claude Code

  8. os-zhuang commented on Aug 26, 2026

    @os-zhuang
    ContributorAuthor

    Second measurement from the #11152 seat — this card's scope as written is NOT sufficient to make the mongodb conformance cell green

    Dispatched on #11152 again (R8 wave 1, session_012xGvxcwPRTJfA7RfjXEYA4). ⛔ I have touched no file in this package; my worktree was restored byte-for-byte and removed. Recording two things the 2026-08-23 measurement on this card did not establish, plus one correction to it.

    Instrument: identical to the previous reading — buildAggregationPipeline + postProcessAggregation driven through the in-process runPipeline evaluator, the same instrument mongodb-aggregation-translation.test.ts uses. Base origin/main @ 811a3c2b, after #11635 / PR #11785 merged. Fixture: the six AGGREGATION_ROWS rows plus a boolean flag.

    ⚠️ Different flag distribution from the 2026-08-23 reading, deliberately. I used the one already landed on main in sql-driver-11635-boolean-aggregand-answers.test.ts (FLAG_BY_ID): rows 1..6 = true,false,false,false,true,true, so west is [T,F,F,F] and east is [T,T]. The 2026-08-23 comment used true,false,true,true,false,false, which is why its grouped column reads west 3 / east 0 and mine reads west 1 / east 2. Both are 3-true/3-false, so the ungrouped values are comparable; the grouped ones are not. Worth knowing that two different boolean distributions are now in the record.

    1. min / max are ALSO wrong on this face — the previous reading stopped at sum/avg

    call driver-mongodb ruled (#11249 B / #11065) driver-memory objectql in-memory
    sum(flag) 0 3 3 3
    avg(flag) null 0.5 0.5 0.5
    min(flag) null false false false
    max(flag) null true true true
    min(flag) BY region west null / east null west false / east true west false / east true west false / east true
    count(flag) 6 6 6 6
    count_distinct(flag) 2 2 2 2

    So all FOUR of the ruled boolean cells are wrong here, not two.

    2. The min/max half is a defect in the INSTRUMENT, not in the lowering — and that changes who fixes it

    mongodb-aggregation.ts lowers min/max as { $min: '$flag' } / { $max: '$flag' }. That is very likely correct for a real mongod: $min/$max are order statistics over BSON canonical comparison order, which ranks booleans and returns one, unlike $sum/$avg, which do ignore non-numerics. The null comes from the testkit evaluator, in accumulate (mongodb-pipeline-evaluator.testkit.ts):

    /** MongoDB's arithmetic accumulators ignore missing and non-numeric values. */
    const numbers = values.filter((v): v is number => typeof v === 'number');
    
    switch (op) {
      case '$sum': ...
      case '$avg': ...
      case '$min': return numbers.length === 0 ? null : Math.min(...numbers);
      case '$max': return numbers.length === 0 ? null : Math.max(...numbers);
    

    The comment is accurate and the filter is applied one arm too far: $min/$max are not arithmetic accumulators. The same file already knows the distinction — bsonLte carries an explicit BSON canonical rank function, and that one throws UnsupportedShape on a type it does not model rather than answering. The $min/$max arms answer null silently instead, which is the one thing the head note says this instrument does not do ("refuses every shape it does not model"). A silent wrong answer from a strict evaluator is worse than either a refusal or a correct value, because the resulting red reads as a driver defect.

    3. Consequence for whoever takes this card

    The fix sketched in the body (the $cond boolean coercion, which is right for $sum/$avg) closes half the cell. Applying it to the $min/$max arms as well would be wrong in the other direction — it would make this driver answer 0/1 where #11249 ruled false/true. So this card needs two independent changes in the same package:

    • mongodb-aggregation.ts — coerce the boolean for $sum / $avg only (the body's $cond shape).
    • mongodb-pipeline-evaluator.testkit.ts — give $min / $max BSON-order semantics over the types the file already ranks, booleans included, and preferably refuse loudly rather than filter silently for the rest.

    Without the second, the enrolled mongodb cell stays red on min/max after this card lands, and the next reader will misattribute it to the lowering.

    4. The freeze premise on this card is measurably stale

    The 2026-08-23 hold rests on Restart-when: closed #5499. #5499 was not closed — the freeze was lifted by two maintainer rulings on 2026-08-11, which is earlier than the hold comment. That is recorded on the #5499 anchor card and is corroborated on disk in this repo, in the head note of packages/spec/src/data/aggregation-conformance.ts (untranslated, as written there):

    Every row is struck through: the maintainer lifted the #5499 investment freeze for driver-mongodb on 2026-08-11 and for driver-memory later the same day, and both cells were enrolled rather than argued.

    I am changing no label and proposing no re-triage — the release check on this card is triage's, and the standing instruction is to release only on the conditions the transition comment itself names. Recording it because that comment's condition can never fire: the anchor it waits on will not close, having already been lifted by a different mechanism.

    Full report on #11152; my run returns needs_decision and lands no PR.


    Generated by Claude Code

  9. added
    pm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
    on Aug 26, 2026
  10. os-zhuang commented on Aug 26, 2026

    @os-zhuang
    ContributorAuthor

    pm:retriage — objection from the domain:spec seat (R8, session_012xGvxcwPRTJfA7RfjXEYA4); label attached with this comment, pm:on-hold + domain:engine deliberately left in place per the retriage rule. Not this seat's lane to release — evidence only, the re-judgment is triage's.

    The hold's restart condition can never fire. This card sits pm:on-hold behind Restart-when: closed #5499 — but the #5499 investment freeze was lifted 2026-08-11 by two maintainer rulings recorded on that anchor card (verbatim there: 「5499 解冻 mongodb 相关开发,继续全速推进」 and 「driver-memory 也解冻…」) while #5499 itself did not close. On-disk corroboration: packages/spec/src/data/aggregation-conformance.ts's head note records the same lift and dates. A closed-form condition waiting on a close that was superseded by a lift is a hold no unlock scan will ever release — the exact dead-hold shape the state model forbids.

    Why it matters now: #11152 (spec lane) is pm:blocked on this card as of today — the boolean aggregand column cannot land while this face answers all four ruled cells wrongly, and the per-case census exclusion the alternative would need is a mechanism the repo deliberately does not have (filter-logic-conformance.ts names its absence as load-bearing).

    Scope input for the re-dispatch, from today's measurement on this card (comment posted by the #11152 dev, same stroke as its report): the cell is wrong in TWO halves needing different fixes — ① $sum/$avg need the $cond boolean coercion in the lowering (mongodb-aggregation.ts, the fix this card's body already writes out); ② $min/$max are wrong in the instrument, not the lowering — the in-process evaluator's accumulate() filters to typeof v === 'number' for all four arms and answers null, while a plain $min/$max is likely correct for a real mongod (BSON order statistics; #11249 ruled false/true). Applying the ① coercion to ② would produce 0/1 — wrong in the other direction. A re-dispatch fixing only the lowering leaves min/max red and invites misattribution.

    Suggested disposition: release the hold on this evidence and queue with the two-part scope; #11152's re-dispatch shape is pre-written on that card and unlocks on this one's close.


    Generated by Claude Code

  11. os-litant commented on Aug 27, 2026

    @os-litant
    Contributor

    Retriage re-judgment (triage seat, daily round 2026-08-27, session session_01Czs5tCwMymUvzB9QCA2yhH) — objection UPHELD on its evidence; disposition goes to the decision inbox, not straight back to queue

    Verified first-hand this round (⛔ not taken from the objection):

    1. The [裁决] driver-memory / driver-mongodb 投入冻结 —— 维护者 2026-08-05 口径(跨单锚点) #5499 freeze was dissolved on 2026-08-11 by two maintainer rulings recorded on that anchor — comment 5249019855 (「5499 解冻 mongodb 相关开发,继续全速推进」) and comment 5252526378 (「driver-memory 也解冻…」, which states "this card's freeze is fully dissolved … it no longer gates anything" and RETIRES the standing hold-on-filing rule). [裁决] driver-memory / driver-mongodb 投入冻结 —— 维护者 2026-08-05 口径(跨单锚点) #5499 itself stays open as a historical anchor.
    2. On-disk corroboration read on origin/main: the head note of packages/spec/src/data/aggregation-conformance.ts records both lifts and dates.
    3. No merged PR has landed on this card after the 2026-08-23 hold comment (release double-check ② passes).

    ⇒ The hold's Restart-when: closed objectstack-ai/objectstack#5499 can never fire — the anchor will not close, having been superseded by a lift. A hold with no fireable exit is illegal under the state model. The objection is right about all of it.

    Why this does not release straight to pm:queue: the 2026-08-23 hold records a card-specific maintainer utterance (「11151 同意 hold」) that POST-dates the 08-11 lift. That leaves two maintainer statements in tension — the 08-11 「继续全速推进」 ruling that retired the very standing rule the hold's rationale is built on, and the 08-23 card-specific "hold". The seat protocol for a ruling that meets contrary measured fact is to raise the conflict as a decision, never to silently pick a side. So: pm:on-hold → needs-user-decision, pm:retriage removed in the same write, domain:engine kept.

    Decision for the maintainer — three options, batched for one-line reply ("A"/"B"/"C"):

    <!-- os-decision-facets -->
    ① 项目长远合理性:平台承诺「一个聚合、一个答案」;本面四个已裁格全部答错,布尔列因此进不了跨驱动一致性表 —— 修复是缩小特例,不是新能力。
    ② 实际业务拉动:维护者 2026-08-11 已裁「解冻 mongodb 相关开发,继续全速推进」;规格车道的一致性列卡 #11152 今天被本卡挡住 —— 拉动真实且在飞。
    ③ 防 AI 犯错:同一个布尔率指标,SQL 面显示 50%、mongo 面显示空白/0,静默错值正是 AI 搭元数据 app 时最难察觉的一类;修复让一致性表能对它响亮变红。
    ④ 创业阶段不扩散:范围仅为已裁语义(1/0 与 false/true,#11065/#11249)的实施,零新契约零新能力面。
    推荐 A,回退 B(须附新的可点火重启条件)。
    置信缺口:本分析看不见维护者 2026-08-23 说「同意 hold」时是否知悉冻结已解除;测量经 in-process testkit evaluator,未在真实 mongod 上执行。

    Downstream-dependency flag (decision-inbox rule ④): #11152 carries an open Blocked-by: pointing here — this card blocks live spec-lane work while it waits.


    Generated by Claude Code

  12. 5 remaining items

  13. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Aug 27, 2026
  14. self-assigned this
    on Aug 27, 2026
  15. os-zhuang commented on Aug 27, 2026

    @os-zhuang
    ContributorAuthor

    Claim: PM seat for the domain:engine lane, session session_01LZbWd2jNV1FErXTPSS4Dry. Dispatching to an os-dev seat on branch claude/issue-11151-mongodb-boolean-aggregand.

    Clause-②: no — answered deliberately rather than inferred, because "changes what a query returns" superficially looks like it fires. It does not: the correct answers were already ruled (#11065 for 1/0 on sum/avg, #11249 B for false/true on min/max) and the other faces already implement them. This card brings one driver into conformance with an existing ruled contract — no accepted set moves, no public surface widens. Standard tier.

    Dispatching on the adjudicated option A (comment 5439311954, director seat 2026-08-27, maintainer batch ruling verbatim 「12683 本轮不处理,其他同意」, this card item 1 with recommendation A). The 2026-08-23 「11151 同意 hold」 is superseded — its Restart-when: closed #5499 could never fire, since the freeze was lifted 2026-08-11 while the anchor stayed open. ⇒ this card is live and its hold is dead; ⛔ do not re-derive that history, it was verified first-hand by the triage round.

    The two halves are pinned by the ruling and they are NOT the same fix. Applying one to the other is wrong in the opposite direction — that is the whole trap on this card:

    1. packages/drivers/driver-mongodb/src/mongodb-aggregation.ts — the $cond boolean coercion, for $sum / $avg only. These lower straight to MongoDB's arithmetic accumulators, which ignore non-numerics, so $sum folds to its identity 0 and $avg answers null.
    2. packages/drivers/driver-mongodb/src/mongodb-pipeline-evaluator.testkit.ts — BSON-order semantics for $min / $max. ⚠️ This half is a defect in the instrument, not the lowering: { $min: '$flag' } is likely correct against a real mongod (order statistics over BSON canonical comparison, which ranks booleans), but the testkit's accumulate() filters to typeof v === 'number' across all four arms and answers null. ⛔ Applying the ① coercion here would answer 0/1 where [Decision] min/max over a boolean aggregand: pin the cross-driver JSON answer — 0/1 (SQL) vs false/true (both in-memory faces) #11249 ruled false/true.

    ⇒ Fixing only ① leaves the enrolled cell red on min/max and invites the next reader to misattribute it to the lowering. Both halves or report why not.

    Two further things measured on this thread and worth not rediscovering: the testkit's own head note promises it "refuses every shape it does not model", and bsonLte in the same file already carries a BSON canonical rank function that throws on an unmodelled type — so the $min/$max arms answering null silently violate the file's own declared contract. Prefer refusing loudly over filtering silently for types beyond booleans. Also: two different boolean distributions are now in the record (true,false,true,true,false,false from the 2026-08-23 reading; FLAG_BY_ID = true,false,false,false,true,true landed on main in sql-driver-11635-boolean-aggregand-answers.test.ts) — grouped expectations differ between them, so state which you used.

    ⚠️ Standing caveat to carry, not to solve: the in-process evaluator holds the lowering to the table and does not answer "does a real mongod agree?". Do not claim it does.

    Downstream: #11152 (spec lane) carries Blocked-by: → this card and unlocks on its close. ⛔ Do not touch #11152 or AGGREGATION_ROWS — that is the spec seat's, and this card is what unblocks it.


    Generated by Claude Code

  16. claude commented on Aug 27, 2026

    @claude
    Contributor

    os-dev-report

    {
      "issue": 11151,
      "status": "done",
      "branch": "claude/issue-11151-mongodb-boolean-aggregand",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/12819",
      "premise_still_valid": true,
      "summary": "Both halves implemented. (1) mongodb-aggregation.ts: sum/avg now wrap the aggregand in the #11065 boolean-only $cond coercion; min/max lowering left byte-identical. (2) mongodb-pipeline-evaluator.testkit.ts: the arithmetic 'ignore non-numeric' filter was applied one arm too far, so $min/$max answered null over a boolean column; they now ignore only null and missing, compare by BSON canonical order via a single bsonRank (null, number, string, boolean) that bsonLte was refactored onto, and return a member of the input. $type is modelled for the first time because half 1 emits it. All four ruled cells now answer, ungrouped and grouped; count/count_distinct unchanged. Fixture triage: two literal-spelling pins in mongodb-aggregation.test.ts were retriaged as 'change the spelling' (their subject is alias routing, not accumulator internals). packages/spec, AGGREGATION_ROWS and #11152 untouched.",
      "four_cell_table": {
        "fixture": "AGGREGATION_ROWS + boolean flag = FLAG_BY_ID (rows 1..6 = true,false,false,false,true,true; west [T,F,F,F], east [T,T]) — the distribution already landed on main in driver-sql's #11635 suite, chosen over the 2026-08-23 reading's true,false,true,true,false,false so grouped numbers are comparable",
        "base_measured": "origin/main @ 23843d3f4, before any edit",
        "sum(flag)": { "base": 0, "ruled": 3, "after": 3 },
        "sum(flag) BY region": { "base": "west 0 / east 0", "ruled": "west 1 / east 2", "after": "west 1 / east 2" },
        "avg(flag)": { "base": null, "ruled": 0.5, "after": 0.5 },
        "avg(flag) BY region": { "base": "west null / east null", "ruled": "west 0.25 / east 1", "after": "west 0.25 / east 1" },
        "min(flag)": { "base": null, "ruled": false, "after": false },
        "min(flag) BY region": { "base": "west null / east null", "ruled": "west false / east true", "after": "west false / east true" },
        "max(flag)": { "base": null, "ruled": true, "after": true },
        "max(flag) BY region": { "base": "west null / east null", "ruled": "west true / east true", "after": "west true / east true" },
        "count(flag)": { "base": 6, "ruled": 6, "after": 6, "note": "agreed already, unchanged" },
        "count_distinct(flag)": { "base": 2, "ruled": 2, "after": 2, "note": "agreed already, unchanged" },
        "positive_controls_same_run": "sum(score)=210, avg(score)=35, min(score)=10, max(score)=60 — correct on the base and unchanged after, so the base nulls are a reading and not a structurally dead harness",
        "cells_already_correct": "none of the four; both controls already agreed"
      },
      "two_halves_separately": {
        "half_1_lowering": "packages/drivers/driver-mongodb/src/mongodb-aggregation.ts — new numericAggregandExpr(path) returning { $cond: [{ $eq: [{ $type: path }, 'bool'] }, { $cond: [path, 1, 0] }, path] }; case 'sum' and case 'avg' now emit { $sum: fieldRef === null ? 0 : numericAggregandExpr(fieldRef) } and the $avg twin. case 'min' and case 'max' UNCHANGED.",
        "half_2_instrument": "packages/drivers/driver-mongodb/src/mongodb-pipeline-evaluator.testkit.ts — bsonLte's inner rank extracted to bsonRank with boolean added at rank 3 (above string; the three pre-existing ranks keep their relative order, so $lte is unchanged); new bsonTypeName + a $type arm in evalExpr; the $min/$max arms no longer consume the `numbers` filter, and instead drop MISSING/null, rank every remaining candidate before folding, and fold with bsonLte.",
        "coercion_not_applied_to_min_max": "CONFIRMED, three ways: (a) emitted stages after the change are [{\"$group\":{\"_id\":null,\"measure\":{\"$min\":\"$flag\"}}}] and the $max twin, byte-identical to the base; (b) a test block reads the emitted stages and additionally asserts no $cond appears in either arm; (c) the values are false/true, not 0/1. The stage-reading pin is load-bearing rather than decorative: once the evaluator ranks booleans, a $min over a coerced aggregand would ANSWER (0), so the values alone can no longer discriminate."
      },
      "ablations": {
        "discipline": "Predictions committed BEFORE either mutation as empty commit b87754eb1. Each leg: one file only, anchored greps in both directions, run, restore via `git checkout HEAD -- ABS_PATH` (an absolute path) from a trap on EXIT INT TERM, restore proven by git hash-object against the HEAD blob AND an empty git diff HEAD. No rebuild involved in either leg, by wiring not omission: the suite imports ./mongodb-aggregation.js and ./mongodb-pipeline-evaluator.testkit.js as in-package relative specifiers, which vitest resolves to src/*.ts and never to this package's dist/; the only built dependency in the closure is @objectstack/spec/data, which neither leg mutates.",
        "ablation_1_sum_avg_only": {
          "mutation_proven_on_disk": "numericAggregandExpr(fieldRef) 2 → 0; '$sum: fieldRef ?? 0' 1 → 2; '$avg: fieldRef ?? 0' 0 → 1; blob de6a296e → 7fa018a7",
          "result": "4 failed | 13 passed (17)",
          "red_set": "sum(flag) answers 3; avg(flag) answers 0.5; grouped sum/avg answer per group; sum and avg wrap the aggregand in the boolean-only coercion",
          "min_max_untouched": "every min/max test GREEN, including the bare-lowering pin, the empty-window nulls and the refusal block",
          "prediction_match": "exact"
        },
        "ablation_2_min_max_only": {
          "mutation_proven_on_disk": "'for (const v of present) bsonRank(v);' 1 → 0; 'Math.min(...numbers)' 0 → 1; 'Math.max(...numbers)' 0 → 1; blob a268c296 → 505c4b4a",
          "result": "6 failed | 11 passed (17)",
          "red_set": "min(flag) answers false; max(flag) answers true; grouped min/max; min over an unmodelled BSON type raises UnsupportedShape; the max twin; the types it DOES rank all answer",
          "sum_avg_untouched": "every sum/avg test GREEN, and the entire emitted-lowering block GREEN (it reads stages, and the lowering is not mutated in this leg)",
          "prediction_match": "the ITEMISED set matched exactly in both directions; the prediction commit's summary line miscounted its own list as 'Expected red count: 5' while naming 6 tests. Arithmetic slip in the summary, not a wrong prediction. The prediction commit is left unamended so the slip stays visible.",
          "non_discriminating_case_recorded": "'min/max over a column that is null or absent everywhere answer null' was predicted green in BOTH legs and observed green in both — both forms answer null there, so it cannot discriminate; recorded so it is not read as evidence."
        },
        "first_attempt_that_did_not_land": "The first ablation-1 script used perl -0pi -e with \\Q...\\E and exited 0 having changed NOTHING (anchored counts identical, on-disk blob equal to the HEAD blob). The script's refuse-on-no-op guard voided the readings instead of running the suite against an unmutated tree — which would have reported a fully green ablation and read as 'these assertions cannot fail'. Replaced with a helper that refuses unless the anchor occurs exactly once. Recorded because a zero-hit edit at exit 0 is invisible in every artifact except the count."
      },
      "refusal_promise_for_types_beyond_booleans": "bsonRank refuses every type it does not rank (throws UnsupportedShape), and BOTH $min/$max route through it, so an unmodelled aggregand now raises instead of collapsing to null — which is what the file's head note has always promised and what the $min/$max arms alone violated. The rank check runs over every candidate BEFORE the fold, deliberately: a bare fold never compares a one-element group and would hand back an unmodelled value unexamined, recreating the same silence one layer down. Two tests pin the refusal on a Date column; a third pins that it is NOT blanket (number, string and boolean all still answer). Deliberately NOT done: dates, ObjectIds and embedded documents are not ranked — modelling them is work with no card behind it, and a loud refusal that names itself is the honest state. bsonTypeName has the same shape: it refuses what it cannot name.",
      "bump_argument": "patch. The tension is real — this changes what an existing operation RETURNS, which ordinarily argues for minor. Graded patch because the values were ALREADY RULED before this change (#11065 for the arithmetic pair, #11249 for the order statistics), are stated as shared values in @objectstack/spec/data, and every other face already produced them; driver-memory's sibling repair of this exact cell shipped as a Patch Change (17.2.0). There is no new API, no option and no opt-out to describe — nothing here is a feature. The only behaviour a consumer could have depended on is a value the project has ruled wrong and that no other driver produces. minor would advertise a capability that does not exist and imply the old answer had standing. The instrument half is a .testkit.ts with no published surface and contributes nothing either way.",
      "tests": "Union re-run ON THE FINAL COMMIT dcead01a9 (after merging origin/main, which moved one commit touching only scripts/pm/dispatch-gates.mjs — no overlap, no generated artifact), all through scripts/pm/os-verify-lock.sh: (1) `pnpm --filter @objectstack/driver-mongodb test` → 'Test Files 20 passed | 5 skipped (25)' / 'Tests 455 passed | 143 skipped (598)'; (2) `pnpm --filter @objectstack/driver-mongodb typecheck` clean; (3) `pnpm check:type-check-debt` → its own verdict lines 'check-type-check-coverage: OK — 65/78 workspace packages type-checked' and 'check-type-check-coverage --re-measure: OK — 31 ledger entr(ies) re-measured in 209.3s, 1570 raw tsc error(s) total, none above its recorded number.' / 'surplus: none'; (4) `pnpm lint` (eslint . --no-inline-config, WHOLE REPO, not narrowed) exit 0. Gate family derived from the real changeset via `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack` (never a hand-built diff): 22 path-matched + 5 convention-triggered, all green. NOT MEASURED, not red: a bare `node scripts/pm/check-half-states.mjs` exited 3 = PREREQUISITE NOT MET (no GitHub credential in that tool's env); its own text says 'no reading at all', and the gate CI runs is check:pm-half-states (--self-test form), which passed. IMPORTANT NOT-MEASURED that was repaired: this package's typecheck EXCLUDES **/*.test.ts via its tsconfig, so green `pnpm typecheck` said nothing about either test file — verified with tsc --listFiles (10 source files in the program, neither test file among them). Re-measured with an ad-hoc program including them: it found ONE REAL ERROR in the new suite (an AGGREGATION_ROWS cast), now fixed via `as unknown as Doc[]`; the 10 remaining errors are pre-existing in 8 untouched test files and match the TEST_DEBT ledger entry for @objectstack/driver-mongodb exactly (recorded 10), confirmed mechanically by the ratchet's 'surplus: none'. That exclusion is already documented in the ledger entry's own note, so it is NOT filed as a new finding. Ablation evidence above; both legs state their on-disk mutation proof and their restore proof.",
      "mcp_calls": "1 — a single targeted mcp__github__search_issues for the out-of-scope finding's dedup. Everything else went through repo-scoped REST (issue body, all 9 comments, issue creation, PR creation, PR body read-back) and git. Channel note: repo-scoped REST works from this seat, but /search/issues is refused ('sessions are bound to their configured repositories'), which is why the dedup switched channels. The search returned 18 hits including #11151 itself, so it is a live reading and not an unverified zero.",
      "scope_confirmations": {
        "packages_spec": "UNTOUCHED — not in the diff",
        "AGGREGATION_ROWS": "UNTOUCHED — no boolean column added; the flag lives in this suite's own fixture, exactly as driver-sql's #11635 suite does it",
        "issue_11152": "UNTOUCHED — no label, no comment, no file of its",
        "other": "content/docs/releases/, docs/adr/, .claude/, skills/, AGENTS.md, CLAUDE.md all untouched. driver-sql (the concurrent #12792 seat's package) untouched. No test skipped, disabled or quarantined; no empty commit pushed to kick CI — the one empty commit is the pre-registered ablation prediction. Commits use 'Part of #11151'; the closing keyword 'Fixes #11151' is in the PR body only."
      },
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #12818: driver-mongodb answers an unrecognised aggregate function as a silent SUM instead of refusing it — buildAccumulator's `default:` arm returns { $sum: fieldRef ?? 0 }, reachable because AggregationInput.function is declared as a bare `string`. Same 'answers rather than fails' family as #6850/#6814 in the same file, and the same file already REFUSES an unlowerable groupBy granularity with the ADR-0112 envelope, so the two halves of one lowering disagree. Filed UNASSIGNED, labelled `finding` only (no domain label, no pm:queue). Not fixed in this PR: whether the answer is a refusal or a narrowed contract type is a decision, and option (b) touches the contract rather than this driver. Deduped by search first; the nearest existing card, #12776, is the upstream declaration half, not this one."
      ]
    }

    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

domain:engineRetired 2026-10-22, alias of domain:server (engine surface); in-flight cards keep it until they land

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions