Repository navigation
Commit 022b4e7
feat(audit): every data export writes one run-level
<!-- claude-projects-attribution: {"github_login":"os-elon-musk"} -->
_Requested by **Elon** · [project
thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9DWeF7nbxqGwBWoay6h9xZio)_
domain:server#1 · os-elon-musk · session_016WV4cxfxbKV82MKKQzizpP
Fixes #30955
Claim: 6687338710 (full re-post of 6687178882 with this branch; file
surface amended to the writer's reach). The dogfood file
`packages/qa/dogfood/test/data-export-audit-row.dogfood.test.ts` is an
addition the seat accepted for review. The two tenant-audit census files
and the one `WRITE_SHAPED_CALLEES` line in
`scripts/measure-durability-swallow-family.mjs` are what two gates
require for the writer's new write call site and durability callee,
reported to the seat.
Clause-②: yes (widening)
Metadata-protocol: none
## 维护者速读
- 改了什么:通过 REST 导出数据(`GET /data/:object/export`,CSV / JSON /
XLSX)时,每次导出都会在审计日志里留一条 `export`
记录,写明谁导出、导出的对象、格式、行数、是否被行数上限截断、用了哪些筛选字段和运算符、是否带了搜索,以及成功还是失败。导出审计日志本身也会记。`export`
重新回到审计日志的动作列表,「配置变更」视图也会列出这些记录。
- 为什么改:此前导出完全不留痕。有导出权限的人一次能下载最多 5 万行(审计日志本身也能导),审计日志里却没有任何记录。`export`
当初在 #8147
被下线,是因为没有任何代码写它;这次先有写入方再恢复这个值,符合「审计面宁窄勿谎」。企业版的审计导出(cloud#3665)在等这张卡。
- 风险与回滚:只新增审计行,导出文件的内容和导出权限判断都不变。审计行在文件发完之后才写,写失败只记一条 error
日志,导出照常成功。记录里从不出现导出的单元格值:筛选只记字段名和运算符,搜索只记「有搜索」,测试埋了特征值并断言它不在行里。被拒绝的导出(没有导出权限、参数错误)不写记录。可整个
PR 回滚。
- 先看:`packages/plugins/plugin-audit/src/data-export-audit.ts` 的
`summarizeExportFilter` 和
`buildDataExportAuditRow`(只保留字段名和运算符);`packages/rest/src/data-action-routes.ts`
导出处理里 `exportRun` 的赋值点,以及文件结束后对 `recordDataExport`
的两处调用;`sys-audit-log.object.ts` 恢复的 `export` 值和 `config_changes` 视图筛选。
- 两分钟验证:`pnpm --filter @objectstack/plugin-audit exec vitest run
src/data-export-audit.test.ts`,预期 15 个用例全部通过,其中 `the row never carries
an exported cell value` 会埋入一个特征值,并断言整条记录里没有它。
- 需要你做的:无,席位落地。
## What changes
`GET /data/:object/export` streamed up to 50,000 rows of any object the
caller may export, `sys_audit_log` included, and wrote nothing to the
compliance ledger. `sys_audit_log.action` had no `export` value: #8147
retired it because no code wrote it. Measured on main `1b3e22f02c`:
- the enum has no `export` (`sys-audit-log.object.ts:244` "`export`
removed (#8147)", the `config_changes` filter at `:250` is
`['config_change', 'import']`, the retirement note is at `:289`);
- the export door (`data-action-routes.ts:1096`) has no ledger write.
Run against main's source (rebuilt), the new dogfood file sees zero
export rows after each of six exports.
This PR adds the writer first, then restores the value.
- **The writer (`plugin-audit`, new `data-export-audit.ts`).**
`createDataExportAuditWriter` adds `recordDataExport` to the existing
`audit` service slot, beside `recordAuthEvent` and
`recordActionInvocation`, the #27908 pattern. The door hands it an
EVENT; plugin-audit owns the row shape. Both sides declare the event
structurally, so neither package depends on the other. The row:
- `action: 'export'`, `object_name` = the exported object, `record_id:
null`, `old_value` / `new_value` null. This is the run-level shape of
the incumbent `import` row (`plugin-auth`'s `admin-import-users.ts`).
- `user_id`, `actor` (when the column exists) and `tenant_id`, taken
from the request's execution context. An agent-performed export adds
`performed_by` / `on_behalf_of`, and an impersonated one adds
`impersonated_by`, as the other ledger rows do.
- `metadata`:
- `format`, `row_count`;
- `limit`, `requested_limit`, `limit_cap`, and `limit_capped` (whether
the 50,000 cap clipped the request);
- `truncated`: the file stopped at its limit and the read did not say
the result set ended there;
- `filter`: a summary of field paths and operator tokens, never a
comparand;
- `search`: a boolean, never the term, plus `search_fields` (names) when
a search applied;
- `outcome` (`succeeded` / `failed`), plus `status` / `error_code` for a
failed one.
- **The value scrub (`summarizeExportFilter`).** It walks both filter
dialects (the object form with `$and` / `$or` / `$not` / `$nor` and
nested relation paths, and the array form `[field, op, value]` with
`and` / `or` lists). It keeps only:
- field paths that match a machine-name pattern (at most 4 dotted
segments);
- operator tokens that match an operator shape.
Anything else is recorded as `?`, so a value placed in a key position
cannot pass through. Terms are de-duplicated and bounded at 32; the
remainder is counted in `filter_terms_omitted`, and recursion depth is
capped at 16. Comparands are never read into the summary.
- **The door (`rest`, `data-action-routes.ts`).** An export is recorded
once every gate has passed and the read begins: auth, export permission,
parameter parsing. It is recorded after the response has ended,
completed or failed. The truncation reading uses the last `findData`'s
own `hasMore` / `total`; at its limit, a read that answers neither
cannot rule out more rows, so the file reads as truncated. An export
that fails after reading began records `outcome: 'failed'` with the
status and machine code `resolveThrownHttpError` reads off the error,
never the error text. The event carries the query as read under
`readQuery`, not `query`. It is no query handed to the engine, and
`rest-server-canonical-query-ast.test.ts` censuses every `query:` line
in `data-action-routes.ts` as a server-built QueryAST slot: spelled
`query`, the event's type line and its literal read as two new slots (8
against the pinned 6, the Test Core 6/8 red on `63e2ee7f`). The pin
stays 6, re-pinned nowhere.
- **The reach (`rest-server.ts`, `rest-api-plugin.ts`).**
- `rest-api-plugin.ts` adds an environment-scoped `audit` provider,
using the same helper as `security` and `i18n`.
- `RestServer` takes it as the LAST positional constructor argument
(#15256) and hands `resolveAuditService` to `DataActionRouteHost`.
- `rest-api-plugin-slot-lookups.test.ts` pins the new PROVIDERS row
(index 21) and the environment-scoped list. It also imports
`@objectstack/objectql` at module top. The real `RestServer` it extends
reaches that package only through two route handlers' dynamic imports
(`meta-object-state-routes.ts`,
`meta-record-currency-backfill-route.ts`), and `check:test-source-alias`
("The source reach") asks every test file a change edits to pay that
load during collection.
- **The enum (`sys-audit-log.object.ts`).**
- `{ label: 'Export', value: 'export' }` is back after `Import`.
- The `config_changes` view filter is `['config_change', 'import',
'export']`.
- The two ADR-0087 / #8147 comments are updated in the same diff: the
retirement note now says "at the time", `permission_change` stays
retired, and a writer-first paragraph names the writer and its views.
- `sys-audit-log-retired-actions.test.ts` moves `export` from
`RETIRED_ACTIONS` to `ACTIONS_WITH_WRITERS`, naming the writer, and the
stored-values list in `sys-audit-log-action-labels.test.ts` gains
`export`.
- The option label is in all 11 plugin-audit locale bundles
(`check-i18n-bundles` and `check:i18n-coverage` OK).
- **Tenant-audit census (regenerated).** The writer's `engine.insert`
into `sys_audit_log` is a new write call site (elevated, against a
tenancy-enabled object). The census moves from main's 264 (after #30946)
to 265 sites. `node scripts/tenant-audit-census.mjs --write` regenerated
`content/docs/permissions/tenant-audit-census.mdx` and
`docs/audits/2026-08-tenant-audit-write-call-sites.counts.md` from
main's side of both files. Eight hand-written figures on the page, which
`check-tenant-audit-census` holds to the census, move with it: the site
count 264 to 265 in five places (the unreadable-share population, the
deviation-table row, the decidable-share population, the elevated-share
population and the `2 / N` citation), decidable 187 to 188 once, and
decidably elevated 147 to 148 twice (still 56%). The only new census row
is this writer's.
- **Durability gate
(`scripts/check-durability-degradation-log-level.mjs`).**
`persistDataExportAuditRow` (the writer's named persist callee) joins
`DURABILITY_CRITICAL_CALLEES`, so its loss report cannot be downgraded
below `error` without the gate going red. Proven by downgrading it to
`warn`: exit 1. `scripts/measure-durability-swallow-family.mjs` keeps a
deliberate copy of that list (`WRITE_SHAPED_CALLEES`, origin
`gate-vocabulary`). `check:swallow-census-controls` requires the new
name there too, so it is added there.
### A refused export writes no row, and why
A request the door refuses before it reads anything writes no row. That
covers `EXPORT_NOT_PERMITTED` (403, the gate is unchanged), an unknown
or unreadable parameter (400), and an unparseable `filter` or `orderby`
(400). The import template is not an export of data and writes none
either.
- An object name the registry does not know (`404 OBJECT_NOT_FOUND`),
even past an export gate that admitted the name: there is no object, so
no data moved, and the name is only the caller's path text, which the
ledger would otherwise store unverified as `object_name`. It is the one
read refusal special-cased in the export catch (`if (code ===
'OBJECT_NOT_FOUND') return;`).
- Nothing was read, so no data left the system; the row records data
that moved.
- The caller already has a definitive refusal from the door.
- A refusal row could be minted at request rate by any caller,
unprivileged ones included, which would flood the compliance ledger with
noise.
- It is the `action_invocation` rule (#27908): a row once the call has
cleared the door's admission gates, whether it then succeeds or fails,
and none for a refusal at the gates.
- A refusal row would need its own shape and its own card; if one is
wanted, it should arrive writer-first like this one.
An export that fails after reading began DOES write a row (`outcome:
'failed'`), because part of the stream may already have reached the
caller. That includes a read the protocol refuses: an unknown field in
the filter (`400 INVALID_FIELD`, pinned in the door describe).
### When the audit write fails
The row is written after `res.end()`, so the ledger write cannot change,
delay or cut the file. The writer never throws. A failed insert is
reported at `error` once per cause (the cause key is the error's code or
name), then at `debug` for the same cause. The message names the object
and the cause, never the query or any value: `Data-export audit write
FAILED (...)`. The export's answer is unchanged: 200 and the full file.
The door also catches anything a host's own `audit` sink throws. A
kernel without plugin-audit has no ledger and exports exactly as before.
## Tests
- `plugin-audit/src/data-export-audit.test.ts` (new, 15 cases). It boots
the real `ObjectQLPlugin` + `AuditPlugin` on sqlite, reaches
`recordDataExport` through the `audit` slot, and reads every row back:
- an ordinary object gives one row of the full shape;
- the ledger's own export gives one row;
- truncation and `limit_capped`;
- a failed outcome with its status and code;
- **no cell value**: a distinctive value is placed in every comparand
position (`$eq`, `$in`, a nested relation `$contains`, under `$not`) and
as the search term. `JSON.stringify(row)` must not contain it, while the
field and operator terms, `search: true` and `search_fields` are
recorded;
- the array dialect;
- `?` for a non-machine-name key and a non-operator token;
- the 32-term bound with the omitted count;
- de-duplication;
- agent attribution and service-actor attribution, and no `actor` key
without the column;
- a refused insert logs `error` once then `debug`, with the value in no
log line;
- no engine: silent.
- `rest/src/export-integration.test.ts`, new describe (13 cases), on the
real streaming door over a real ObjectQL + sqlite, with the `audit`
provider passed as `rest-api-plugin.ts` passes it:
- csv / json / xlsx each hand ONE event with format and row count;
- the ledger's own export hands one event, and its file holds the rows
that were there before;
- truncation from the read's page markers, and the cap clipping the
request;
- the query is handed as read (the writer reduces it);
- a read refused after the gates hands one failed event (`400`,
`INVALID_FIELD`);
- no event for `EXPORT_NOT_PERMITTED`, for a 400 parameter or filter
refusal, for the template, or for an object name the registry does not
know past an admitting export gate (`404 OBJECT_NOT_FOUND`, with a
known-object export on the same boot as its control);
- a throwing sink and an empty slot leave the 200 and the file
unchanged.
- `qa/dogfood/test/data-export-audit-row.dogfood.test.ts` (new, 8
cases). It boots `bootStack` with the real `SecurityPlugin` and
`AuditPlugin`, with an exporter (read + export) and a reader (read
only):
- an ordinary export, a csv export, the ledger's own export and a
truncated export each leave one row;
- a filtered export and a searched export carry the seeded value in the
FILE and in no row;
- `EXPORT_NOT_PERMITTED` leaves no row, with an exporter control on the
same door;
- no export row anywhere in the ledger carries the value.
- On `bootStack` the signed-in admin is refused export of
`sys_audit_log` (`EXPORT_NOT_PERMITTED`). So the exporter's fixture set
grants `allowExport` on `sys_audit_log` for the ledger case. That is a
fixture grant, not a change to the gate; the gate is untouched.
- Updated: `rest-api-plugin-slot-lookups.test.ts` (the PROVIDERS row and
the environment-scoped list), `sys-audit-log-retired-actions.test.ts`,
`sys-audit-log-action-labels.test.ts`, and
`sys-audit-log-pointer-pair.test.ts` (its `config_changes` control pins
the filter literal, now with `export`).
<details><summary>Measured on main 1b3e22f (the PR's source files put
back to main, tests kept, rest + plugin-audit rebuilt)</summary>
- `export` absent from `sys_audit_log.action` (`grep "value: 'export'"`:
no match).
- Dogfood file: 7 failed / 1 passed. Six exports each left zero rows
(`expected [] to have a length of 1 but got +0`), and the ledger file
held zero export rows. The one pass is the ledger-wide "no row carries
the value" case, which is vacuous with no rows.
- Door describe: 7 failed / 5 passed. Every positive case saw no event;
the five no-event cases pass on main by construction.
- Restored with `git checkout HEAD -- packages`, then checked: `git
status` empty, `git diff HEAD` empty, and the writer's blob equals
HEAD's blob (`307d2c3064`).
</details>
<details><summary>Ablations (scripts/ablation-replace.mjs, WRAP mode:
anchor 1 → 0, blob changed, restored blob == HEAD and git diff HEAD
empty; dist reach checked with ablation-dist-preflight.mjs)</summary>
- **A. The writer dropped from the slot.** `audit-plugin.ts`'s
`...createDataExportAuditWriter(...)` was replaced by an empty object.
The preflight marker `...createDataExportAuditWriter(` is present in the
pristine build and absent after. Unit: 6 of 15 red, all six slot cases.
Dogfood: 7 of 8 red, every row case; the 8th is the vacuous ledger-wide
one.
- **A′. The door's call dropped.** `await sink.recordDataExport(event)`
was replaced by `void event`. Door describe: 7 of 12 red, every positive
case; the five no-event cases stay green.
- **B1. The filter scrub dropped.** `metadata.filter = summary.terms`
was replaced by the raw `query.filter`, and the preflight shows the
marker present in the dist. Unit: 1 of 15 red, exactly `the row never
carries an exported cell value`. Dogfood: 2 of 8 red, the filtered case
and the ledger-wide case.
- **B2. The search scrub dropped.** `metadata.search` was set to the raw
term. Unit: 1 of 15 red, the same no-cell-value case. Dogfood: 2 of 8
red, the searched case and the ledger-wide case. The preflight marker
was a comment, which esbuild strips (exit 1). The dogfood red on the
searched case alone is the evidence that the mutation reached the dist.
- A first round of A and B1 ran the dogfood file against a rest dist
that was still built from main's source, left over from the re-measure.
Those dogfood reds were discarded. HEAD was rebuilt (preflight hits in
both packages, dogfood 8/8 green) and A and B1 were re-run; the figures
above are from the re-run.
- **C. The unknown-object refusal dropped.** `if (code ===
'OBJECT_NOT_FOUND') return;` deleted from the export catch. Door
describe: 1 of 39 red in `export-integration.test.ts`, exactly the
unknown-object case (`expected [ { context: { …(2) }, …(11) } ] to
deeply equal []`). Restored: blob == HEAD (`cc48b9986d34`), `git diff
HEAD` empty.
</details>
## Acceptance notes
- Every Acceptance bullet has a test:
- one row with format and count: unit, door and dogfood;
- the `sys_audit_log` export: all three;
- truncation: all three;
- no exported cell value: unit and dogfood, with ablation B.
- Seat addition: the search term never reaches the row, only "search
present" (unit, dogfood, ablation B2).
- **Spec.** No spec enum is needed: `sys_audit_log.action` is declared
only by plugin-audit's object, and `packages/spec` declares no
audit-action enum. The semantic migration entry
`packages/spec/src/migrations/entries/semantic/17.audit-log-action-enum-retired.ts`
stays as written. It is the 17.x record and says "no export feature ever
wrote an audit row", which was true then. That is `packages/spec` (⛔ for
this card), so it is noted here, not edited.
`check-adr-0087-registration --base origin/main` is green.
- **Not edited, outside the surface.**
- `ui/packages/plugin-detail/src/HistoryTimeline.tsx` has a comment
saying `export` is retired (⛔ `ui/**`). For an action it has no style
for, the timeline falls back to an outline badge.
- The writer lists in the docblocks of `plugin-audit`'s
`audit-log-read-visibility.ts` and `audit-log-field-redaction.ts` do not
name the export writer. Their behaviour already covers it: run-level
rows (no `record_id`, a non-record action) are served under the ledger
grant.
- **Views.**
- `recent` lists export rows except an export of an object that view
already leaves out (`AUDIT_LOG_DEFAULT_VIEW_EXCLUDED_OBJECTS`, the
settings stores).
- `config_changes` and `all_events` list every export row.
- A row's `tenant_id` is the exporter's tenant. A row with `tenant_id`
null (a platform-scope export) is served to platform admins only, as
every such ledger row is.
- **Not recorded.** The exported COLUMNS (`fields=`) are not recorded,
only the filter's field names. The card did not ask for them; they could
be a follow-up. Filter field names are visible to anyone who can read
the ledger row; they are names, not values.
- The row lands just after the caller has the complete file (it is
written after `res.end()`), so the dogfood cases read the ledger until
the row lands (bounded) rather than immediately.
## Verification
**Take-over rounds, head `35743e5a`** (④-1 on `53bec3a5`, then
`origin/main` `bd1b7685` merged clean; `pnpm --filter @objectstack/rest
test` on `35743e5a`: 393 files, 7486 passed / 332 skipped). Round 1,
head `e1d50d59` (`origin/main` `e1dac192` merged, #30946 included; delta
against main 32 files, +1503 / -42), after a closure rebuild (`turbo run
build --filter='./packages/**' --filter='@objectstack/dogfood^...'`,
81/81, lock VERDICT command-exit 0):
- Reds on `63e2ee7f` and their causes: Test Core 6/8
(`rest-server-canonical-query-ast.test.ts:459`, 8 ≠ 6) was the event's
`query` member counted as two QueryAST slots, fixed at the source by
`readQuery`; reverse-verified by putting `query` back (`expected 8 to be
6`) and restoring (`git diff HEAD` empty). `Lint · CI, release & engine
gates` (`check:test-source-alias`,
`rest-api-plugin-slot-lookups.test.ts:96`) was the edited test file
reaching a dynamic `@objectstack/objectql` import; fixed by the
module-top import the gate names.
- On the pre-merge-2 head `4addb47f`: `pnpm --filter @objectstack/rest
test` 393 files, 7486 passed / 332 skipped; `pnpm --filter
@objectstack/plugin-audit test` 76 files, 1194/1194; `typecheck` exit 0
for both. (The second merge brought no change to `packages/rest` or
`packages/plugins/plugin-audit`.)
- On `e1d50d59`: the dogfood file 8/8
(`NODE_OPTIONS=--max-old-space-size=6144`); the census, door and
slot-lookup files 162 passed / 22 skipped; `data-export-audit.test.ts`
15/15; `check-tenant-audit-census` OK (265 sites, 23 prose figures
held).
- Derived gates and `pnpm lint`: in the os-dev-report comment for this
round.
Local runs on the final head are quoted, with that head, in the report
comment on #30955:
- `@objectstack/plugin-audit` and `@objectstack/rest`: `test` and
`typecheck`;
- the dogfood file;
- every family `node scripts/pm/dispatch-gates.mjs --commands` derives
from this diff, reconciled with `--ran`;
- `pnpm lint`.
CI's required checks are the authority for everything else.
<!-- signed: domain:server#2 · os-tesla ·
session_01L9qibyvxbrLP4jm4PargoH -->
---------
Co-authored-by: Claude <noreply@anthropic.com>sys_audit_log row, and the export action comes back with its writer (#31000)1 parent 78d4fe9 commit 022b4e7
32 files changed
Lines changed: 1504 additions & 42 deletions
File tree
- .changeset
- content/docs/permissions
- docs/audits
- packages
- plugins/plugin-audit/src
- objects
- translations
- qa/dogfood/test
- rest/src
- scripts
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
132 | 132 | | |
133 | 133 | | |
134 | 134 | | |
135 | | - | |
| 135 | + | |
136 | 136 | | |
137 | 137 | | |
138 | 138 | | |
| |||
197 | 197 | | |
198 | 198 | | |
199 | 199 | | |
200 | | - | |
| 200 | + | |
201 | 201 | | |
202 | | - | |
203 | | - | |
| 202 | + | |
| 203 | + | |
204 | 204 | | |
205 | 205 | | |
206 | 206 | | |
| |||
217 | 217 | | |
218 | 218 | | |
219 | 219 | | |
220 | | - | |
| 220 | + | |
221 | 221 | | |
222 | 222 | | |
223 | 223 | | |
224 | | - | |
| 224 | + | |
225 | 225 | | |
226 | 226 | | |
227 | 227 | | |
| |||
233 | 233 | | |
234 | 234 | | |
235 | 235 | | |
236 | | - | |
237 | | - | |
| 236 | + | |
| 237 | + | |
238 | 238 | | |
239 | | - | |
| 239 | + | |
240 | 240 | | |
241 | | - | |
| 241 | + | |
242 | 242 | | |
243 | 243 | | |
244 | 244 | | |
245 | 245 | | |
246 | | - | |
| 246 | + | |
247 | 247 | | |
248 | 248 | | |
249 | 249 | | |
250 | 250 | | |
251 | 251 | | |
252 | | - | |
| 252 | + | |
253 | 253 | | |
254 | 254 | | |
255 | 255 | | |
256 | 256 | | |
257 | | - | |
| 257 | + | |
258 | 258 | | |
259 | 259 | | |
260 | 260 | | |
| |||
307 | 307 | | |
308 | 308 | | |
309 | 309 | | |
310 | | - | |
| 310 | + | |
311 | 311 | | |
312 | 312 | | |
313 | 313 | | |
314 | | - | |
| 314 | + | |
315 | 315 | | |
316 | 316 | | |
317 | 317 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
33 | 33 | | |
34 | 34 | | |
35 | 35 | | |
36 | | - | |
37 | | - | |
| 36 | + | |
| 37 | + | |
38 | 38 | | |
39 | | - | |
| 39 | + | |
40 | 40 | | |
41 | | - | |
| 41 | + | |
42 | 42 | | |
43 | 43 | | |
44 | 44 | | |
45 | 45 | | |
46 | | - | |
| 46 | + | |
47 | 47 | | |
48 | 48 | | |
49 | 49 | | |
| |||
90 | 90 | | |
91 | 91 | | |
92 | 92 | | |
93 | | - | |
| 93 | + | |
94 | 94 | | |
95 | 95 | | |
96 | 96 | | |
97 | | - | |
| 97 | + | |
98 | 98 | | |
99 | 99 | | |
100 | 100 | | |
| |||
120 | 120 | | |
121 | 121 | | |
122 | 122 | | |
| 123 | + | |
123 | 124 | | |
124 | 125 | | |
125 | 126 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
30 | 30 | | |
31 | 31 | | |
32 | 32 | | |
| 33 | + | |
33 | 34 | | |
34 | 35 | | |
35 | 36 | | |
| |||
320 | 321 | | |
321 | 322 | | |
322 | 323 | | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
323 | 329 | | |
324 | 330 | | |
325 | 331 | | |
| |||
336 | 342 | | |
337 | 343 | | |
338 | 344 | | |
| 345 | + | |
339 | 346 | | |
340 | 347 | | |
341 | 348 | | |
| |||
0 commit comments