Skip to content

Commit 71e8db0

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-16335-adr-0112-refusal-declaration
2 parents 2faf3bf + 4261fbc commit 71e8db0

6 files changed

Lines changed: 919 additions & 21 deletions

File tree

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
---
2+
'@objectstack/service-analytics': patch
3+
---
4+
5+
Draft-preview analytics: `avg` answers the mean of the NON-NULL operands, and `null` when there are none — matching every live face
6+
7+
A dataset measure `{ aggregate: 'avg', field: 'amount' }` compiles to the cube
8+
metric `{ type: 'avg', sql: 'amount' }`, and the draft-preview evaluator built
9+
its operand list with `rows.map((r) => Number(r[field]))`. `Number(null)` is `0`
10+
and `Number.isFinite` accepts it, so every NULL entered the average as a zero
11+
OPERAND and was counted in the divisor. `AVG(col)` is defined over non-null
12+
values in every SQL dialect, so a drafted chart showed a different number than
13+
the published one, silently — and where a group's column was NULL in every row
14+
the number it showed was `0`: a plausible-looking average that a reader cannot
15+
tell from one somebody measured.
16+
17+
Measured on one dataset, one row set, two `AnalyticsService` instances differing
18+
only in `draftRowsResolver` (the live half being `NativeSQLStrategy`'s generated
19+
SQL on a real SQLite). Rows `{meals, null}` and `{meals, null}` answered
20+
`avg_amount` null live and `0` on preview; rows `{travel, 10}`, `{travel, 20}`,
21+
`{travel, null}` answered 15 live and 10 on preview. Both cells now answer the
22+
live number.
23+
24+
The empty answer is READ from the platform's own ruling rather than restated
25+
here: `emptyGroupValueFor` (`@objectstack/spec/data`) returns the identity `0`
26+
where counting or summing nothing is a measured fact and `undefined` — spelled
27+
`null` on this wire — where there is nothing to answer. It is the same function
28+
`fillEmptyGroups`, `sql-driver` and `driver-turso` read, and the one #16203 cited
29+
when it moved `min`/`max` off the same idiom in this function.
30+
31+
Unchanged, and pinned by the same differential: `sum` over a group with no values
32+
still answers the ruled identity `0`, `count` over one still answers `0`
33+
(#16218), `min`/`max` still answer `null` (#16203), and `avg` over a group that
34+
has values still answers its mean. `sum` and the numeric `default` arm keep their
35+
existing operand list — `0` is the additive identity, so the coercion never moved
36+
`sum`'s answer, and the `default` arm serves the custom-SQL metric types, which
37+
have no live standard to be moved towards.
38+
39+
The `null` fires on an EMPTY group and never on an incoherent one. "No numeric
40+
operand" is two different situations: no row carried a value at all — the empty
41+
group the policy rules on — or rows carried values that do not read as numbers,
42+
such as a `date` column under `avg`. The second is an incoherent
43+
aggregate/field-type pair that #16099 owns and no layer refuses yet; it keeps the
44+
numeric identity it has always had, since the live face answers a different
45+
number again (SQLite's numeric affinity over a TEXT column) and a `null` there
46+
would invent a third answer. That boundary is pinned from both sides — by
47+
`preview-aggregate-operand-type.test.ts` (#16203) and by a control in the new
48+
differential.
49+
50+
The live path is unchanged.
51+
52+
Bumped `patch` rather than `minor`, on the same reasoning the sibling #16218
53+
shipped under: the package's published surface is byte-unchanged — `src/index.ts`
54+
is not in this diff and does not re-export `preview-evaluator.ts` at all, and
55+
`aggregate()` is module-private — and the only user-visible effect is a drafted
56+
chart's number moving to the number the published chart already showed. A value
57+
correcting toward the live standard is a fix, not the backwards-compatible
58+
feature addition `minor` denotes. It is a real value change for a consumer
59+
reading the preview response (`0` becomes blank), which is why the card was filed
60+
separately rather than ridden along with #16203 — but the `0` it replaces was
61+
never a number the platform promised.

.github/workflows/pr-automation.yml

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -722,9 +722,33 @@ jobs:
722722
OPPOSITE bumps for the same additive act. The commit type is not the
723723
discriminator it looked like: it correlates with the act, and when
724724
they disagree the act wins. The 64 historical `patch` precedents are
725-
pre-rule and nothing is retro-fixed. This is prose, not a gate -- no
726-
check computes it; `check-changeset-no-major.mjs` refuses `major` and
727-
says why in its header, and the two remaining levels are yours.
725+
pre-rule and nothing is retro-fixed.
726+
727+
PART OF THIS IS A GATE, and which part is the difference between
728+
grading well and being refused. `check-changeset-no-major.mjs`
729+
refuses `major` and says why in its header. Since #16055 the SAME
730+
script also runs a LEVEL AXIS over the rule above, and that
731+
script's header states its scope: "a PR that DECLARES clause ② (a
732+
new key on a published payload) must grade AT LEAST ONE package
733+
whose published source it moves `minor` or above". It mechanizes
734+
this ruling "for exactly the PRs where the widening is already
735+
DECLARED, and for no others", and its `judgeLevel` verdict table
736+
spells both directions: `not-declared`, "the declaration reads
737+
`no`", exits 0; `enforce`, "declared `yes`, moved packages graded
738+
`patch` and NONE of them graded `minor` or above", exits 1. So on
739+
a PR that declares clause ②, grading `patch` across the board is
740+
refused; on every other PR the axis stands down and the two levels
741+
below `major` are yours, as they always were.
742+
743+
WHAT THE AXIS DOES NOT JUDGE, in its own words: "WHICH of the
744+
packages above received that widening is NOT something this gate
745+
can read, and it does not claim to. Clause ② is declared ONCE, FOR
746+
THE PR" -- neither the carrier label nor the declaration line in
747+
the PR body names a package. The axis cross-checks a declaration
748+
against the levels; it never reads the diff to find the act.
749+
Judging the act is still yours, this paragraph is still where the
750+
rule lives, and everything in it the axis does not reach is still
751+
prose.
728752
729753
2. It releases nothing (.github/, .claude/, skills/, docs/, content/,
730754
examples/, tests-only, and the like)

0 commit comments

Comments
 (0)