Repository navigation
Commit aeb0557
Fixes #20355
Clause-②: yes (narrowing)
## What this does
One RLS policy that compares two fields of no shared comparison class
used to get two answers: driver-sql refused the read it scopes
(`INVALID_FILTER` / 400), and the in-process write check compared the
two raw values and admitted and stored the write. Both evaluators now
read #20347's classification (`crossFieldComparisonVerdict` /
`crossFieldColumnVerdict`, `@objectstack/spec/data`), and the write
check refuses where the read refuses.
- **`@objectstack/formula` (the write-check evaluator).**
`matchesFilterCondition(record, filter, options?)` takes the object's
declared columns as `options.fields`. Given them, every `{ $field }`
comparison between two declared columns is judged by
`crossFieldComparisonVerdict` before any record is read
(record-independent, like the #5240 / #19886 shape refusals), and
`cross-class` or `no-class` throws `INVALID_FILTER` / 400. The message
names nothing from the filter (the #7929 posture the read takes for the
same comparison); the refused comparison travels on the error under a
symbol key for the server log. New exports:
`findCrossFieldClassRefusal`, `crossFieldClassRefusalCarriedBy`, types
`MatchesFilterOptions`, `CrossFieldClassRefusal`. Without `fields` the
evaluator is byte-for-byte the old one.
- **`@objectstack/plugin-security` (the write gate, step 3.6).** Hands
the evaluator the object's declared columns (`writeCheckFieldOptions`:
`ql.getSchema`, then the metadata service, the order
`loadObjectFieldNames` uses) for every image it judges: single and array
inserts, by-id updates, predicate updates. On the refusal it logs one
WARN naming the policy and both columns: `[Security] RLS check REFUSED
on insert 'OBJECT' (INVALID_FILTER): policy 'deal_guard' — the
comparison … compares "status" (type 'text') … and "amount" (type
'number') …`. The policy name comes from a WeakMap the RLS compiler now
keeps from each policy's compiled filter to the policy
(`compiledPolicyNameOf`); nothing is added to the filter objects
themselves.
- **`@objectstack/driver-sql`.** `crossFieldComparisonClass` delegates
to `crossFieldColumnVerdict` for every declared `FieldType`, and keeps
only the driver-internal aliases above it, read off its own sets
(`JSON_COLUMN_TYPES`: `object` / `array`; `NUMERIC_SCALAR_TYPES`:
`integer` / `int` / `float`). No second copy of the classification is
left.
- **`@objectstack/lint`.** `crossClassConsequence`'s write sentence now
states the runtime's answer: "the in-process write check refuses the
comparison by the same classification (`INVALID_FILTER` / 400), so every
insert or update it judges is refused and nothing is stored", and the
`check` clause closes "The policy reads as a write rule and admits no
write at all." (#20347 ACCEPT note 1) Round 2: the one sentence in the
header's #20347 section that said the write check has no class rule now
says it refuses by the same classification.
- **`@objectstack/spec` (patch, round 2).** One ADR-0087 D3 semantic
entry for the whole family,
`rls-predicate-cross-class-field-comparison-refused` under protocol
major 18
(`packages/spec/src/migrations/entries/semantic/18.rls-predicate-cross-class-field-comparison-refused.ts`).
It names both arms: #20347's authoring arm (`os validate`, build, lint
and the permission save door) and this write check.
`packages/spec/src/migrations/registry.ts` is regenerated by `pnpm
--filter @objectstack/spec gen:migration-registry` (71 lines inserted,
none removed), as PRs #19946, #20259 and #20310 did. The changeset's
marker moves to `registered` with that id, and it adds
`'@objectstack/spec': patch`. The two comments that named the retired
parity test (`filter-cross-field-comparison-class.ts`'s header and its
test's header) now say that driver-sql delegates to
`crossFieldColumnVerdict` and that
`sql-driver-20355-cross-field-class-driver-aliases.test.ts` pins the
alias layer it keeps.
- **The #20347 parity test retires.**
`sql-driver-20347-cross-field-class-parity.test.ts` held driver-sql's
private copy equal to the export over 3,025 ordered pairs. There is no
private copy any more, so the pairs are equal by construction. Before it
was deleted it ran on the rewired driver (commit 4605cc7): 56/56
green. The alias layer the rewire kept is pinned by the new
`sql-driver-20355-cross-field-class-driver-aliases.test.ts`.
## Measured, before and after
Through the real plugin-security + ObjectQL, policy `operation: 'all'`,
a member caller. Before = base 789b2ae, after = this branch. The same
answers on better-sqlite3, sqlite-wasm and PostgreSQL 16:
| policy | read (`using`) | by-id update / delete (`using`) | insert
with `using` as the check | `check` insert | `check` by-id update |
|---|---|---|---|---|---|
| `record.status != record.amount` (text vs number) | 400 → 400 | 403 →
403 | admitted, stored → **400**, nothing stored | admitted, stored →
**400** | admitted → **400** |
| `record.status != record.photo` (text vs image) | 400 → 400 | 403 →
403 | admitted, stored → **400** | admitted, stored → **400** | admitted
→ **400** |
| `record.status != record.is_open` (text vs formula; the card's formula
cell) | 400 → 400 | 403 → 403 | admitted, stored → **400** | admitted,
stored → **400** | admitted → **400** |
| `record.status != record.meta` (text vs json holding one value) | 400
→ 400 | 403 → 403 | admitted, stored → **400** | admitted, stored →
**400** | admitted → **400** |
| `record.amount > record.status` (number vs text) | 400 → 400 | 403 →
403 | 403 → **400** | 403 → **400** | 403 → **400** |
| `record.status != record.title` (text vs text, control) | rows → rows
| admitted → admitted | admitted → admitted | admitted → admitted |
admitted → admitted |
The formula cell answers exactly like the other two: the classification
gives a formula field no class (`no-class`, reason `formula`), so it is
refused on both sides.
driver-memory, measured out of tree (this package cannot declare
`@objectstack/driver-memory` without a `driver-memory-census`
disposition): the write answers as in the table (400 on every write
cell, nothing stored), because the check runs in-process before any
driver. Its **read is unchanged and still admits** (`rows=1` for every
cross-class cell): driver-memory has no `{ $field }` arm at all and
compares the marker object as a literal (#15104, closed not planned). So
"refused on read and write" holds on SQLite, sqlite-wasm and PostgreSQL,
and on memory for the write only.
A json or `multiple` column is a `no-class` column (`list-or-object`),
so a comparison against one is now refused by its declared type, for
every record. #19886 stage 2d judged it by the value each record held (a
json column holding one scalar compared). driver-sql's read has always
refused it by declared type, so this moves the write onto the read's
answer too.
## Compile faces
(`.claude/skills/pm-dispatch/references/compile-surfaces.md`,
re-verified at c80202c)
| # | face | verdict |
|---|---|---|
| 1 | `driver-sql` `applyFilterCondition` (`sql-driver.ts:16192`), and
by inheritance `driver-sqlite-wasm` and local-mode `driver-turso` |
**changed**: `crossFieldComparisonClass` reads
`crossFieldColumnVerdict`. The answers are unchanged: parity 56/56 on
the rewired driver before it retired, the cross-field conformance and
reference suites green on SQLite and PostgreSQL. |
| 2 | turso `RemoteTransport.buildWhereSQL` (`remote-transport.ts:2695`)
| **already compliant**: refuses every `{ $field }` comparand in remote
mode, whatever the classes (`uncompilableComparand`,
`remote-transport.ts:4392`). |
| 3 | service-analytics `compileScopedFilterToSql`
(`read-scope-sql.ts:696`) | **already compliant**: a read scope carrying
a `{ $field }` is declined by `NativeSQLStrategy.canHandle` and served
on the engine path, where face 1 compiles or refuses it (#7598 ruling,
`read-scope-sql.ts:284`). The `/analytics/sql` echo refuses the
reference outright. |
| 4 | service-analytics `lowerAnalyticsWhere`
(`filter-normalizer.ts:2171`) | **already compliant**: same routing: a
`{ $field }` comparand reaches face 1 (`filter-normalizer.ts:1453`). |
| 5 | `formula` `matchesFilterCondition` (`matches-filter.ts:305`) |
**changed**: judges every `{ $field }` comparison by
`crossFieldComparisonVerdict` when the caller supplies the declared
columns. |
| half | objectql `having-filter` (`applyHaving` / `matchesHaving`,
`having-filter.ts:1131` / `:1154`) | **out of scope**: it calls face 5
without declared columns, so its answers are unchanged. A `having`
reference compares columns of the AGGREGATED row (group keys, aggregate
aliases), not declared `FieldType` columns, so this classification does
not cover them (#20127 classifies them separately). HAVING is evaluated
in-process on every driver, so there is no read-side twin that could
disagree. |
| unfrozen | `driver-memory` `checkCondition` (`memory-matcher.ts:361`)
| **out of scope**: no `{ $field }` arm to attach a class rule to
(#15104, closed not planned). Measured above: its read compares the
marker as a literal. |
| unfrozen | `driver-mongodb` `translateFieldOperators`
(`mongodb-filter.ts:962`) | **already compliant**: refuses every `{
$field }` reference (#19949, `mongodb-filter.ts:264`). |
## Tests (measured head c80202c)
- `formula`: new `matches-filter-cross-field-class.test.ts`: every
declared class against every other, all six operators, expectations
written from the table's labels and not from the verdict function;
record independence; `$and` / `$or` / `$not` nesting; the `addDays`
form; undeclared, dotted and unjudged columns left alone; without
`fields` unchanged; the withheld message and the carried diagnostic.
22/22 at c80202c. Full package (at 0ee6f4c; the merge of `main`
brought no change to formula, driver-sql, lint or plugin-security): 41
files, 1213 passed; `typecheck` exit 0 (`check:test-typecheck` OK, debt
unchanged).
- `plugin-security`: new `rls-check-cross-class-field-refused.test.ts`,
through the real engine on better-sqlite3, sqlite-wasm and PostgreSQL
(opt-in, `OS_TEST_POSTGRES_URL`). Cells: the read, by-id update and
delete, the using-as-check insert, the check insert, array insert and
by-id update. Each refusal asserts `code` + `status` and that nothing
was stored or changed; the 400 names neither column; exactly one WARN
names the policy and both columns; the same-class control is admitted.
48/48 at c80202c with PostgreSQL 16. Full package: 143 files, 3046
passed, 16 skipped (the PostgreSQL cells, no URL); `typecheck` exit 0.
- `driver-sql`: new alias pin, 8/8; `cross-field-reference` +
`cross-field-conformance` + alias pin with PostgreSQL: 290 passed, 1
skipped at c80202c. Full package: 194 files passed, 11 skipped; 3172
tests passed, 178 skipped; `typecheck` exit 0.
- `lint`: 115 files, 5314 passed; `typecheck` exit 0.
`validate-rls-predicate-enforceability.cross-class-field.test.ts`:
569/569 at c80202c.
- **Ablations**, each through `scripts/ablation-replace.mjs` with a
restore trap:
- **A**: the evaluator's `if (refusal) throw
crossFieldClassError(refusal);` was replaced by `void refusal;`. Anchor
1 → 0, blob 8e92043 → 58b1e295. formula was rebuilt (exit 0).
`ablation-dist-preflight` read the marker in 2 built files on the
pristine build and absent from all 6 on the mutated one. The formula pin
went **19 failed / 3 passed** and the plugin-security pin **45 failed /
3 passed** (the three survivors are the same-class controls). Restored:
blob == HEAD 8e92043, `git diff HEAD` empty, rebuilt, marker present,
tree clean; 22/22 and 48/48 again.
- **B**: the gate's `matchesFilterCondition(image as any, f as any,
checkFieldOptions)` was stripped of its third argument (blob af0a956 →
8f254f2f). plugin-security went 45 failed / 3 passed. Restored blob ==
HEAD, 48/48.
- **C** (reverse, predicted green): driver-sql's pre-rewire body was put
back in place (blob 4760990 → 77031c84). The alias pin and
`cross-field-reference` went 56/56 green, so the rewire did not move an
alias. Restored blob == HEAD, tree clean.
- Directions observed: red, red, green, as predicted.
- **Lint (narrowed, proved)**: `eslint --no-inline-config --format json`
over the 10 `.ts` files this diff touches plus the 36 the `main` merge
brought in reported 46 files, 0 errors, 0 warnings (no file ignored).
Type-aware linting is not enabled (`eslint.config.mjs:328`: no
`parserOptions.project`, no typed rules), so this diff cannot move the
verdict of any file it does not touch. The full `pnpm lint` is CI's.
## Gates (c80202c, after merging `origin/main` 50e273f)
`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 68 families. All 68 ran, each exit
code captured before any pipe, and all are 0. Three first answered exit
3, PREREQUISITE NOT MET: `check:i18n`, `check:dual-build-cjs-loads` and
`check:type-check-debt`. They were re-run after building their stated
prerequisites, and all three are 0: i18n "OK (9 packages)", cjs "104
entry points across 66 packages load", type-check-debt "4 ledger entries
re-measured, none above its recorded number". `--ran` reconciles: 68
derived, 68 run, 0 NOT-MEASURED, 0 UNRUN. The changeset gates:
`check-adr-0087-registration` ✓ (1 declared-breaking changeset with a
disposition), `check-changeset-no-major` ✓, `check-empty-changeset` ✓.
## Patch rounds
- **Round 1 (e72518a).** `Clause-②: yes (narrowing)` in the changeset
and in this body, because formula's root entry grows.
- **Round 2 (cc0bf6e).** The D3 entry, the changeset's `registered`
marker and `'@objectstack/spec': patch`, the two spec comments, and the
one lint header sentence. `origin/main` was merged twice with true merge
commits: dbddf02, then e01d347, which carries #20106's
`reclaimSpace`. Everything below was measured at cc0bf6e.
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 96 families, the spec families now
among them. All 96 ran, each exit code captured before any pipe, and all
96 exit 0 on the first run. `--ran` reconciles: 96 derived, 96 run, 0
NOT-MEASURED, 0 UNRUN.
- `pnpm --filter @objectstack/spec check:generated`: all 15 artifacts
are up to date, including `check:migration-registry`,
`check:spec-changes` and `check:upgrade-guide`. `pnpm
check:adr-0087-registration`: 0.
- spec `--project local src/migrations` plus the classification test: 4
files, 178 passed.
- On the merged code, driver-sql's full suite passes: 195 files passed
and 11 skipped, 3176 tests passed and 178 skipped. Its typecheck exits
0.
- The formula pin passes 22/22 and the lint cross-class test 569/569.
- The plugin-security pin passes 32 with 16 skipped. PostgreSQL was not
provisioned this round; its cells passed 48/48 at c80202c, and this
round changed no code.
- **Round 3 (2698fa1).** Prose only; no logic or test change.
`origin/main` was merged twice more with true merge commits: 87c37ae
(4e430ba), then 0fcb101 (2698fa1). Everything below was measured
at 2698fa1.
- The D3 entry corrects three statements. `reason` gives the file
family's by-name refusal in the spec module's own words (the ADR-0104
dual-encoding window) instead of "no stored column", which is true of a
formula field only. `acceptanceCriteria` says the read answers 400 "on
the SQL drivers". `replacement` lists all four reference types, `tree`
included.
- `registry.ts` is regenerated by `gen:migration-registry` and changes
only in the entry's lines.
- Lint's #20347 header paragraph now says driver-sql delegates through
`crossFieldColumnVerdict`, where it had named the retired parity test.
- `dispatch-gates --commands` derived 96 families; all 96 ran and exit
0, and `--ran` reconciles 96/96 with 0 NOT-MEASURED. `pnpm --filter
@objectstack/spec check:generated`: all 15 artifacts are up to date.
spec `--project local src/migrations` plus the classification test: 4
files, 178 passed.
## Deviations from the claim's file surface
- `packages/plugins/plugin-security/src/security-plugin.ts` (step 3.6
and `writeCheckFieldOptions`) and `rls-compiler.ts`
(`compiledPolicyNameOf`) are source, where the claim named
plugin-security for tests only. H2 held: the comparison is evaluated in
`matches-filter.ts`. That evaluator has no schema, though, and the
declared columns exist only at its caller, the write gate. Naming the
policy needs the compile seam, the one place that still knows each
policy's filter.
-
`packages/lint/src/validate-rls-predicate-enforceability.cross-class-field.test.ts`:
one assertion line, because it pinned the sentence this PR changes. In
`crossClassConsequence`, the changed text is the shared `write` constant
plus the check branch's closing sentence. The `using` branch
interpolates the same constant, so the `using` finding's last clause
reads the new answer too.
-
`packages/drivers/driver-sql/src/sql-driver-20355-cross-field-class-driver-aliases.test.ts`:
new, to pin the alias layer after the parity test retired.
- All three deviations above were accepted by the seat's amended claim
5868635246. Round 2's `packages/spec` files (the D3 entry, its
regenerated `registry.ts` and the two comments) and the lint header
sentence are in the claim re-posted as 5868966379.
## Acceptance notes
- **Not fixed here; reported for the seat.** `security.explain` (served
at `/security/explain`) answers a read of a row scoped by `record.status
!= record.amount` with `visible: true, decidedBy: rls`, while `find`
answers `INVALID_FILTER` / 400. Measured through the security service on
all three SQL drivers. Its record attribution calls
`matchesFilterCondition` without the declared columns. Passing them, the
option this PR adds, would align it. This is a separate face and the
file is outside this claim.
- #20347's `listHoldingComparisons` second-spelling note is unchanged by
this PR.
- The write check now applies ruling 4 of #5222 (same comparison class).
It does not apply rulings 1–3 (dotted path, declared-only, the
tenant-isolation column). An undeclared column is the RLS compiler's
field guard's job, and the dotted and tenant arms were not measured
here.
- **The `addDays` arm (corrected in rounds 2 and 3).** driver-sql's read
refuses an `addDays` reference with 400 when its base is not a `date` or
`datetime` column, or when its offset column is not numeric. The write
check does not always fail those closed, and three shapes can be
admitted on the write:
1. A text base holding a date-shaped string is shifted and compared,
because `addWholeDays` reads it with `Date.parse`.
2. A numeric base is shifted and compared, because `addWholeDays` adds
the offset to any finite number.
3. A text offset column holding a numeric string is read as a number of
days by `resolveDayOffset`.
This is pre-existing and not made worse here: the class rule runs first
and refuses every cross-class pair. What remains is a same-class pair
with an offset on a non-temporal base (text or numeric), or with a text
offset column. Read from the code; not measured.
carrier: domain:engine seat (#6367) measures reach through the real
write door; a card follows only if a public door admits such a write
- A predicate update that matches zero rows judges no image, so it
completes as a no-op where the read answers 400. Nothing is stored.
- Seat ruling, claim 5868966379: the family gets an ADR-0087 D3 semantic
entry, registered in this PR.
- Seat ruling, claim 5868966379: execution note 3's driver-memory read
cell is accepted under #15104 (closed, not planned). The memory read
still admits; the write refuses.
- #20106 (`reclaimSpace` in `sql-driver.ts`) landed as e01d347 and is
merged here (cc0bf6e). This diff does not touch that region, and
driver-sql's full suite passes on the merged code.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01N8TPEsoJxPsdSdNKGnNGEN)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 8cdbe0c commit aeb0557
16 files changed
Lines changed: 1117 additions & 167 deletions
File tree
- .changeset
- packages
- drivers/driver-sql/src
- formula/src
- lint/src
- plugins/plugin-security/src
- spec/src
- data
- migrations
- entries/semantic
| 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 | + | |
Lines changed: 0 additions & 128 deletions
This file was deleted.
Lines changed: 94 additions & 0 deletions
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
26 | 26 | | |
27 | 27 | | |
28 | 28 | | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
29 | 35 | | |
30 | 36 | | |
31 | 37 | | |
| |||
2729 | 2735 | | |
2730 | 2736 | | |
2731 | 2737 | | |
| 2738 | + | |
| 2739 | + | |
| 2740 | + | |
| 2741 | + | |
| 2742 | + | |
| 2743 | + | |
| 2744 | + | |
| 2745 | + | |
| 2746 | + | |
| 2747 | + | |
2732 | 2748 | | |
2733 | 2749 | | |
2734 | 2750 | | |
| |||
2771 | 2787 | | |
2772 | 2788 | | |
2773 | 2789 | | |
2774 | | - | |
| 2790 | + | |
2775 | 2791 | | |
2776 | | - | |
2777 | | - | |
2778 | | - | |
| 2792 | + | |
| 2793 | + | |
| 2794 | + | |
| 2795 | + | |
| 2796 | + | |
| 2797 | + | |
| 2798 | + | |
| 2799 | + | |
| 2800 | + | |
| 2801 | + | |
| 2802 | + | |
| 2803 | + | |
| 2804 | + | |
2779 | 2805 | | |
2780 | | - | |
2781 | | - | |
2782 | | - | |
2783 | | - | |
2784 | | - | |
2785 | | - | |
2786 | | - | |
2787 | 2806 | | |
2788 | 2807 | | |
2789 | 2808 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
116 | 116 | | |
117 | 117 | | |
118 | 118 | | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
119 | 124 | | |
120 | 125 | | |
121 | 126 | | |
| |||
0 commit comments