Repository navigation
Commit 8538edf
fix(objectql): the rows path adds sum / avg with compensated summation, as SQLite does (#20543)
Fixes #20489
Clause-②: no
## What changed
The engine's rows path
(`packages/objectql/src/in-memory-aggregation.ts`,
`applyInMemoryAggregation`) now adds `sum` / `avg` with
Kahan-Babuska-Neumaier compensation. That is the direction triage
5875711059 ruled: fix the one face the platform owns, so the rows path
agrees with SQLite native on both SQLite paths and gives the more
accurate answer.
- One helper, `compensatedSum`, is a transcription of SQLite's
`kahanBabuskaNeumaierStep` plus its finalizers' overflow guard: when the
error term is non-finite, the plain running sum is returned. The `sum`
arm and the `avg` arm both call it. Before, each arm had its own naive
`reduce`.
- `toNumber`, the null / non-numeric handling, the empty group (`sum`
`0`, `avg` `null`) and the answer's type (a JS number) are untouched.
- Out of scope, as the claim set it: driver-sql's native aggregate,
PostgreSQL / MySQL arithmetic, driver-memory, and `packages/spec`.
Head measured: `8ad4a9d4a`. It is a true merge of `origin/main` at
`03b19d9cf`, on top of the two commits of this branch.
## H1: the defect, before and after
Base `b2b6a0643`, SQLite 3.53.4 (better-sqlite3), `driver-sql`. One
`number` column. The rows path is forced by a filtered sibling `count`.
Both `engine.aggregate` and `POST /api/v1/data/:object/query` were read,
and they gave the same values in every cell:
| group | native `sum` / `avg` | rows path, base | rows path,
`8ad4a9d4a` |
|:--|:--|:--|:--|
| `0.1, 0.2, 0.3` | `0.6` / `0.19999999999999998` | `0.6000000000000001`
/ `0.20000000000000004` | `0.6` / `0.19999999999999998` |
| `1e16, 1, -1e16` | `1` / `0.3333333333333333` | `0` / `0` | `1` /
`0.3333333333333333` |
| `1e16, 0.5, -1e16` | `0.5` / `0.16666666666666666` | `0` / `0` | `0.5`
/ `0.16666666666666666` |
| `0.1, 0.2` | `0.30000000000000004` / `0.15000000000000002` | same |
same |
| `1, 2, 3, 40, 500` | `546` / `109.2` | same | same |
`having { s: { $eq: 0.6 } }`, engine and REST, and `{ a: { $eq:
0.19999999999999998 } }`, engine:
- base: keeps the group on native and no group on rows;
- after: keeps the group on both. The REST pin also holds the `a` case
through REST, on both paths.
H1 holds as the card stated it.
## H3: the compensated fold against SQLite native
SQLite's native `sum` / `avg` was measured on three engines:
better-sqlite3 3.53.4 (driver-sql), sql.js 3.49.1 (driver-sqlite-wasm)
and @libsql/client 3.45.1 (driver-turso). better-sqlite3 and sql.js were
read over a `REAL` and a `NUMERIC` column, libsql over a `REAL` column.
The JS fold was run over the same doubles. The `2^53` row was read on
better-sqlite3 alone.
| fixture | naive (rows path before) | Neumaier (rows path after) |
SQLite native, all three engines |
|:--|:--|:--|:--|
| `0.1, 0.2, 0.3` | `0.6000000000000001` / `0.20000000000000004` | `0.6`
/ `0.19999999999999998` | `0.6` / `0.19999999999999998` |
| `1e16, 1, -1e16` | `0` / `0` | `1` / `0.3333333333333333` | `1` /
`0.3333333333333333` |
| `1e16, 0.5, -1e16` | `0` / `0` | `0.5` / `0.16666666666666666` | `0.5`
/ `0.16666666666666666` |
| `1e20, 1, -1e20` | `0` / `0` | `1` / `0.3333333333333333` | `1` /
`0.3333333333333333` |
| `0.1, 0.2` (two-addend control) | `0.30000000000000004` /
`0.15000000000000002` | the same | the same |
| `1, 2, 3, 40, 500` (integers) | `546` / `109.2` | the same | the same
|
| `2^53, 1, 1` | `9007199254740992` | `9007199254740994` |
`9007199254740994` |
- **No fixture disagrees with SQLite.** A randomized cross-check agrees
too: 5,000 groups of 3 to 22 values (cents, tenths, raw doubles, mixed
sign and magnitude up to 1e19) against better-sqlite3. The Neumaier
`sum` and `avg` matched SQLite on all 5,000 groups; the naive fold
disagreed on 1,601 of them.
- **SQLite's integer affinity.** Under `NUMERIC`, SQLite stores `1e16`
as an INTEGER and sums it in exact int64. The answer is the same `1`.
- **Integers beyond 2^53 move.** Integers are unchanged while the
running total stays within 2^53. Beyond it, the compensated total is the
exact total SQLite also answers, where the naive fold dropped the `1`s
(last row).
## H2: census of rows folds of `sum` / `avg` in `packages/**`
| site | own fold? | reading over `0.1, 0.2, 0.3`, head `8ad4a9d4a` |
|:--|:--|:--|
| objectql `applyInMemoryAggregation`, `sum` and `avg` arms | the fold
this PR changes. H2 partly held: two naive `reduce` copies in one
function, now one helper | `0.6` / `0.19999999999999998` |
| per-aggregation `filter` | shares it: filters the bucket's rows, then
the same arms | pinned in the unit test |
| `having` | no fold: it reads the aggregated row whichever path
produced it | inherits |
| roll-up `summary` (`summary-aggregate.ts`), and service-analytics
`ObjectQLStrategy` | no fold: both call `engine.aggregate` | inherit the
engine's path |
| driver-memory data face (`memory-driver.ts`, `sum` / `avg` arm) |
**own naive fold** | `0.6000000000000001` / `0.20000000000000004` |
| driver-memory analytics face (`memory-analytics.ts`, mingo `$sum` /
`$avg`) | **own fold (mingo)** | `0.6000000000000001` /
`0.20000000000000004` |
| service-analytics draft preview (`preview-evaluator.ts`, `sum` / `avg`
arms) | **own naive fold** | `0.6000000000000001` /
`0.20000000000000004` |
| service-analytics `dataset-executor.ts` `computeDerived` `sum` | not a
rows fold: it adds measure columns within one row | n/a |
| service-analytics `NativeSQLStrategy`, driver-mongodb `$sum` |
database's own arithmetic | not measured |
Three sites keep their own fold. They are measured and reported, not
edited, as the dispatch requires. One reading needs stating plainly:
- **driver-memory now splits by path.** `engine.aggregate` on
driver-memory answers `0.6000000000000001` on its native path and `0.6`
on the rows path. `having { s: { $eq: 0.6 } }` keeps the group on the
rows path only.
- **Before this PR, both of its paths were naive.** The split on that
driver is new here; SQLite's split is gone.
- It goes back to the seat as a family finding. The suggested route:
hoist the fold to `@objectstack/core`, as `bucketDateKey` was, and adopt
it on driver-memory's two faces and the preview.
- The changeset states it.
## H4: what stays as it was (pinned)
- `null` adds nothing to `sum` and is left out of the `avg` count.
- A non-numeric cell reads as `0` and counts in `avg`. A numeric string
reads as its number. A boolean reads as 0 / 1.
- The empty group, with no rows or only nulls: `sum` `0`, `avg` `null`.
- A non-finite total (`Infinity`, `-Infinity`, overflow, `NaN`) is
`Object.is`-equal to the naive answer.
- The answer's type is a number.
## H5: the residual
**Residual, stated.** PostgreSQL and MySQL add `sum` / `avg` natively in
double without compensation. That arithmetic is the database's own, and
this PR does not wrap it. So over three or more fractions, their native
path can still differ from the rows path in the last place. For `0.1 +
0.2 + 0.3`, #20387's measurement on live PostgreSQL 16.13 and MySQL
8.0.46 gave native `0.6000000000000001`; the rows path is now `0.6`. An
exact `$eq` on a fractional sum compares doubles, and a cross-dialect
last-place difference remains there: compare with a range.
- No live PostgreSQL / MySQL cell was added. The cross-path pin is
SQLite-only, as H5 asked, and the residual is stated rather than held as
a failing pin.
- **NOT MEASURED at this head: the PostgreSQL / MySQL readings.** This
container has no MySQL. A PostgreSQL cluster would have needed a data
directory outside the session's root-only scratchpad.
- The rows path's own answer is dialect-independent. It is a pure
function of the doubles `find()` returns, and #20387's pin reads those
as `[0.1, 0.2]` on both servers.
## Tests
**New pins**
- `packages/objectql/src/in-memory-aggregation-compensated-sum.test.ts`:
11 cases, covering the H3 fixtures, groupBy plus a per-aggregation
filter, and the H4 cases.
- `packages/rest/src/rest-aggregate-compensated-sum.test.ts`: 4 cases on
SQLite, through the engine and REST. It proves each path really ran (a
spy on `driver.aggregate`), checks that `find()` reads back the doubles,
pins native equal to rows for every group, and pins `having` `$eq` /
`$in` on both paths.
**Reverse verification, from the committed fix**
- Tool: `scripts/ablation-replace.mjs` (wrap mode). It swapped `return
Number.isFinite(c) ? s + c : s;` for `const ablation20489 = s; return
ablation20489;`, which is the naive running sum.
- Anchor count went 1 to 0, blob `ff8d2385a69f` to `89ea11d3bc18`.
- Then `pnpm --filter @objectstack/objectql build`, and
`ablation-dist-preflight.mjs @objectstack/objectql ablation20489` found
the marker in 4 built files.
- Mutated leg: objectql unit test 6 failed / 5 passed of 11; REST pin 2
failed / 2 passed of 4. The expected direction, red.
- Restore leg: the restore was proven (blob equal to HEAD, `git diff
HEAD` empty).
- Then a rebuild, and `--absent` found the marker absent from all 14
built files, with a clean tree.
- Unit 11/11 and REST 4/4.
**Suites**
| suite | head | result |
|:--|:--|:--|
| `pnpm --filter @objectstack/objectql test` | `db9d27230` | 334 files /
6651 tests passed |
| `pnpm --filter @objectstack/rest test` | `db9d27230` | 224 files /
4242 passed, 43 skipped |
| `typecheck` for both packages, `check:test-typecheck` included |
`db9d27230` | OK; both new test files are in the `tsconfig.test.json`
programs (`--listFilesOnly`) |
| targeted re-run: objectql aggregation files 72/72, REST pin plus
`rest-aggregate-numeric-having` 16 passed / 24 skipped | `8ad4a9d4a` |
green |
The merge brought driver-sql, cli, lint, platform-objects and spec
changes, and none in objectql or rest. After it, the
`@objectstack/rest^...` closure was rebuilt before the targeted re-run.
**Gates**
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` at `8ad4a9d4a` printed 63 commands. All 63
exit 0, and each exit code was written to a file before any pipe.
- `check:dual-build-cjs-loads` and `check:type-check-debt` first exited
3 (PREREQUISITE NOT MET: no `dist/`).
- They were re-run after `pnpm exec turbo run build --concurrency=2
--filter='./packages/*' --filter='./packages/*/*'` (71 tasks), and both
passed.
- The dist-reading gates were re-run over the full build too.
- `--ran`: 63 derived, 63 run, 0 NOT-MEASURED, 0 UNRUN.
- CI-only lanes, NOT MEASURED locally: the 5 path-scheduled CI jobs, the
workspace type-check lanes and the wide-population families that
`dispatch-gates` names.
**Lint**
- Repo-wide `pnpm lint` is CI's.
- A narrowed run, measured: `eslint --no-inline-config --format json`
over the three changed TS files counted 3 files, 0 errors and 0
warnings.
- Population: `eslint.config.mjs` `files:
['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}']`; the changeset is not a linted
extension.
- Invariance: the config enables no type-aware linting (no
`parserOptions.project`), so this diff cannot move an untouched file's
verdict.
## Acceptance notes
All three notes from the first round were corrected in patch round 1,
text only (below):
- The `AGGREGATE_ACCUMULATION` residual comment in
`packages/drivers/driver-sql/src/sql-driver.ts` ("as the rows path adds
them", "on SQLite's native face alone", "every other face
`0.6000000000000001`").
- The header sentence of
`sql-driver-20387-aggregate-double-accumulation.test.ts` that called
`rowsPathSum` the rows path's arithmetic.
- The pending `.changeset/20387-aggregate-one-double.md` residual
paragraph, as a DELIBERATE CORRECTION.
Still standing, not edited (outside the admitted text): the
`aggregate()` inline comment in `sql-driver.ts` ("so one query answers
one number on every face", beside the `accumulatesInDouble` call). It
was already an overclaim before this PR, since SQLite's native sum
compensated. It points at `AGGREGATE_ACCUMULATION`, which now states the
residual. Carrier: none.
## DELIBERATE CORRECTION
**Note:** `.changeset/20387-aggregate-one-double.md`. It is pending on
`main`, so it ships in the same release as this PR.
**Sentence corrected:** the "Residual, stated." paragraph, and only that
paragraph. It read:
- "Double sums are added in row order, one after another, as the rows
path adds them."
- "So for a group of three or more fractions, SQLite's native answer can
still differ from every other face in the last place (`0.1 + 0.2 + 0.3`:
SQLite `0.6`, every other face `0.6000000000000001`). This was already
true of SQLite's two paths before this change."
**What changed under it:** this PR makes the engine's rows path add with
compensated summation, as SQLite 3.43+ does. The rows path now answers
`0.6`, not `0.6000000000000001`, so "every other face" and "SQLite's
native face alone" are false once this PR lands.
**It now reads:** PostgreSQL and MySQL add without compensation, while
SQLite and, since #20489, the rows path compensate. So the PostgreSQL /
MySQL native answer can differ from SQLite's and the rows path's in the
last place (`0.1 + 0.2 + 0.3`: PostgreSQL / MySQL native
`0.6000000000000001`, SQLite and the rows path `0.6`). Before #20489,
SQLite's own two paths differed there too. Two addends cannot differ.
**Every other sentence was checked against this PR and left unchanged:**
- Line 16, "`11 / 9` answered `1.2222222222222222`, where every other
face answers `1.2222222222222223`", stays true. Integer addends within
2^53 sum exactly under both folds. Measured through the built rows path
at `1c4767883`: `applyInMemoryAggregation` over `1,1,1,1,1,1,1,2,2`
answers `sum` `11` and `avg` `1.2222222222222223`, and over `1, 2, 2`
answers `1.6666666666666667`.
- The title and line 10, "SQLite and the engine's rows path add JS
doubles", stay true: both still add doubles, now compensated.
- The `0.1 + 0.2` statements (base lines 11-12, 38-40 and 48-50) are two
addends, which no compensation moves.
**Gate:** `check-empty-changeset` is red on exactly this one name, by
design ("DELIBERATE CORRECTION ... do NOT restore it"). ⛔ No
`skip-changeset`; the note is not restored. The at-tier review is the
written confirmation, as the seat answer asks.
## Patch round 1
New head: `1c4767883`. One push, carrying a true merge of `origin/main`
at `288611e3e` (spec-only commits) as `925368f00`, then one text-only
commit. Seat answer 5881988201 ruled the open question A.
**Edits:**
1. `.changeset/20387-aggregate-one-double.md`: the DELIBERATE CORRECTION
above.
2. `packages/drivers/driver-sql/src/sql-driver.ts`: the
`AGGREGATE_ACCUMULATION` residual comment now states PostgreSQL / MySQL
native against SQLite and the rows path.
3.
`packages/drivers/driver-sql/src/sql-driver-20387-aggregate-double-accumulation.test.ts`:
the header sentence says the expected values are `find()`'s rows added
in row order, which is the rows path's compensated sum too over this
file's fixtures (two addends, and integers within 2^53). "a compensated
sum ... fail" is removed, since it was never true for two addends. The
one-line docs of `rowsPathSum` / `rowsPathAvg` make the same statement,
because the header links them.
4. `.changeset/20489-rows-path-compensated-sum.md`: it no longer says it
"replaces" the driver-sql residual. It says the driver-sql entry states
the same residual, and it keeps the driver-memory split and the
PostgreSQL / MySQL residual.
**Comment-only proof for items 2 and 3:** every changed line under
`packages/drivers/driver-sql/` is a comment line (`git diff` filtered to
non-comment lines is empty).
**No `@objectstack/driver-sql` line added to this PR's changeset. Why
none is needed:**
- Check Changeset counts the changesets a PR adds, not the packages it
touches.
- All 69 publishable packages are one `fixed` group in
`.changeset/config.json`, so the `@objectstack/objectql` patch releases
driver-sql in lockstep anyway.
- The edited comment does not ship. `@objectstack/driver-sql`'s built
`dist/index.{js,mjs,d.ts,d.mts}` carry 0 hits for "Kahan", "20489" or
"every other face". The positive control, `AGGREGATE_ACCUMULATION`, is
found in those same four files.
**Checks at `1c4767883`, each exit code recorded before any pipe:**
| check | exit | reading |
|:--|:--|:--|
| `node scripts/check-empty-changeset.mjs --base origin/main` | 1 | red
on exactly one name, `.changeset/20387-aggregate-one-double.md` (one
`::error` line), by design; the empty-frontmatter half is green |
| `node scripts/check-changeset-fixed.mjs` | 0 | `fixed` group in sync
with 69 public packages |
| `node scripts/check-changeset-no-major.mjs --base origin/main` | 0 |
no `major` bump |
| `node scripts/check-adr-0087-registration.mjs --base origin/main` | 0
| no declared-breaking changeset |
| `pnpm check:nul-bytes` | 0 | green |
| `node scripts/check-issue-citations.mjs` | 0 | 3 citations resolve |
| `pnpm --filter @objectstack/driver-sql typecheck` | 0 | the test file
is in its program (`--listFilesOnly`) |
| `vitest run
src/sql-driver-20387-aggregate-double-accumulation.test.ts` (driver-sql)
| 0 | 5 passed, 2 skipped (the PostgreSQL / MySQL cells, no server here)
|
| `pnpm check:driver-conformance`, `check:object-def-param-keys`,
`check:tenant-chokepoint` | 0, 0, 0 | the three families
`dispatch-gates` newly derives for the driver-sql paths |
| objectql aggregation tests, and the REST compensated-sum pin | 0 |
72/72 and 4/4 |
The other 63 derived families were run at `8ad4a9d4a` (above). This
round changes only text on top of a spec-only merge, and CI re-runs
them.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01N8TPEsoJxPsdSdNKGnNGEN)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 7e5246d commit 8538edf
7 files changed
Lines changed: 433 additions & 16 deletions
File tree
- .changeset
- packages
- drivers/driver-sql/src
- objectql/src
- rest/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
39 | 39 | | |
40 | 40 | | |
41 | 41 | | |
42 | | - | |
43 | | - | |
44 | | - | |
45 | | - | |
46 | | - | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
47 | 48 | | |
48 | 49 | | |
49 | 50 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
25 | 25 | | |
26 | 26 | | |
27 | 27 | | |
28 | | - | |
| 28 | + | |
29 | 29 | | |
30 | | - | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
31 | 33 | | |
32 | 34 | | |
33 | 35 | | |
| |||
49 | 51 | | |
50 | 52 | | |
51 | 53 | | |
52 | | - | |
| 54 | + | |
53 | 55 | | |
54 | 56 | | |
55 | | - | |
| 57 | + | |
56 | 58 | | |
57 | 59 | | |
58 | 60 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1594 | 1594 | | |
1595 | 1595 | | |
1596 | 1596 | | |
1597 | | - | |
1598 | | - | |
1599 | | - | |
1600 | | - | |
1601 | | - | |
| 1597 | + | |
| 1598 | + | |
| 1599 | + | |
| 1600 | + | |
| 1601 | + | |
| 1602 | + | |
| 1603 | + | |
1602 | 1604 | | |
1603 | 1605 | | |
1604 | 1606 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
228 | 228 | | |
229 | 229 | | |
230 | 230 | | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
231 | 234 | | |
232 | | - | |
| 235 | + | |
233 | 236 | | |
234 | 237 | | |
235 | 238 | | |
236 | | - | |
| 239 | + | |
237 | 240 | | |
238 | 241 | | |
239 | 242 | | |
| |||
284 | 287 | | |
285 | 288 | | |
286 | 289 | | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
287 | 328 | | |
288 | 329 | | |
289 | 330 | | |
| |||
0 commit comments