Repository navigation
Commit 34b740f
fix(runtime,cli): one file-URL filename rule for both hosts — FILE: strips case-insensitively, file://NAME opens NAME on the CLI host (#23862)
Fixes #23794
Fixes #23796
Clause-②: yes
## What changed
One rule turns a SQLite database URL into a file name, and both hosts
call it.
- **Runtime host, #23794's half (commit `9c5853767`).**
`sqliteFilenameFromUrl` in `packages/runtime/src/standalone-stack.ts`
strips `file:` / `file://` case-insensitively on its `sqlite` path, as
`detectDriverFromUrl` classifies (`/^file:/i`) and as its `sqlite-wasm`
path already stripped. The function is now exported from
`@objectstack/runtime` (one name added to the existing standalone-stack
export line in `packages/runtime/src/index.ts`). Nothing else in
`standalone-stack.ts` moved: not `detectDriverFromUrl`, not
`createStandaloneStack`'s `mkdirSync` arms, not its config schema.
- **CLI host, #23796's half (commits `71b52fdbf`, `48728977b`).**
`resolveStorageDefinition`'s `sqlite` and `sqlite-wasm` arms in
`packages/cli/src/utils/storage-driver.ts` drop their local copy (a
case-sensitive `/^file:/` strip, plus `/^sql:\/\//` on the sqlite arm
and `/^wasm-sqlite:\/\//` on the wasm arm) and call one module-private
helper, `sqliteFileFor`: `normalizeDatabaseUrl`, then the runtime's
`sqliteFilenameFromUrl`. A URL the rule refuses is re-raised as
`UnsupportedDriverError` (recognized, `driverType` = the kind) carrying
the runtime host's sentence minus its `[StandaloneStack]` tag, the shape
the CLI host's retired-engine URL refusal took on main. The class is
load-bearing: `serve.ts` re-throws `UnsupportedDriverError` and silently
swallows every other construction error, so passing the runtime's plain
`Error` through would have booted with no storage at all.
- One changeset, `.changeset/23794-file-url-filename-rule.md`:
`@objectstack/runtime` minor (the new export, `Clause-②: yes
(widening)`), `@objectstack/cli` patch.
## #23794's open question, measured
The card left this unmeasured: does the CLI host strip the prefix the
same way? It does not. At base `3fed1eb00`, `OS_MODE=off
OS_DATABASE_URL=FILE:./urd.db os serve --dev` made a `FILE:.` directory
on the CLI host as well (listing below). The one shared rule fixes both
hosts.
## Disk proof at the public door
Every probe ran in a scratch directory outside any repo tree, with the
built CLI (`node packages/cli/bin/run.js`) and `OS_HOME` pointed into
that scratch directory. Before = base `3fed1eb00`; after = this branch
(CLI closure rebuilt).
| host · input | before | after |
|---|---|---|
| runtime host, plain project: `OS_DATABASE_URL=FILE:./urd.db os serve`
| directory `FILE:.` holding `urd.db` (700416 bytes) | `./urd.db`
(700416 bytes), no `FILE:` entry |
| runtime host, control: `OS_DATABASE_URL=file:./urd.db os serve` |
`./urd.db` | (unchanged path) |
| CLI host (`OS_MODE=off`):
`OS_DATABASE_URL=file://proc/os23794-probe.db os serve --dev` | `Server
is ready`, then `[driver-sqlite-wasm] Writing //proc/os23794-probe.db to
disk failed (ENOENT …)` for the main and telemetry files; nothing
created under the cwd | `./proc/os23794-probe.db` plus its telemetry
sibling under the cwd, no write failures |
| CLI host: `OS_DATABASE_URL=FILE:./urd.db os serve --dev` | directory
`FILE:.` holding `urd.db` and `urd.telemetry.db*` | `./urd.db` and
`./urd.telemetry.db*`, no `FILE:` entry |
| CLI host: `OS_DATABASE_DRIVER=sqlite
OS_DATABASE_URL=postgres://PLACEHOLDER-USER:PLACEHOLDER-SECRET@127.0.0.1:5999/db
os serve --dev` | `Server is ready`; directories
`postgres:/PLACEHOLDER-USER:PLACEHOLDER-SECRET@127.0.0.1:5999/` created,
holding `db` and `db.telemetry.db*` | exit 1, `✗ sqlite driver was
selected but the URL does not look like a file path (its scheme is
"postgres://"). Use file:/path/to/db.sqlite, or set OS_DATABASE_DRIVER
explicitly.`; nothing created; `PLACEHOLDER` appears 0 times in the
output |
The runtime-host boots stop afterwards on the unrelated production
prerequisite `OS_SECRET_KEY`, before and after alike, as the card
recorded. The `/proc` aim of the CLI-host probe is kept so nothing could
be written under `/`.
## What each host answers, before and after (mechanism assumption 1)
`resolveDriverType` + `resolveStorageDefinition(…, { isDev: true })` for
the CLI host and `resolveStandaloneDatabase` for the runtime host, same
`OS_DATABASE_URL`. Before = base; after = this branch. `kind (file)`; a
refusal shows the thrown class.
| driver · URL | CLI before | CLI after | runtime before | runtime after
|
|---|---|---|---|---|
| - · `file:./x.db` | sqlite `./x.db` | sqlite `./x.db` | sqlite
`./x.db` | sqlite `./x.db` |
| - · `file://x.db` | sqlite `//x.db` | sqlite `x.db` | sqlite `x.db` |
sqlite `x.db` |
| - · `file:///abs/x.db` | sqlite `///abs/x.db` | sqlite `/abs/x.db` |
sqlite `/abs/x.db` | sqlite `/abs/x.db` |
| - · `FILE:./x.db` | sqlite `FILE:./x.db` | sqlite `./x.db` | sqlite
`FILE:./x.db` | sqlite `./x.db` |
| - · `File:///tmp/x.db` | sqlite `File:///tmp/x.db` | sqlite
`/tmp/x.db` | Error, scheme `"File://"` | sqlite `/tmp/x.db` |
| - · `sqlite:x.db` | sqlite `x.db` | sqlite `x.db` | sqlite `x.db` |
sqlite `x.db` |
| - · `sqlite://x.db` | sqlite `x.db` | sqlite `x.db` | sqlite `x.db` |
sqlite `x.db` |
| - · `./x.db` | sqlite `./x.db` | sqlite `./x.db` | sqlite `./x.db` |
sqlite `./x.db` |
| - · `x.wasm.db` | sqlite-wasm `x.wasm.db` | sqlite-wasm `x.wasm.db` |
sqlite-wasm `x.wasm.db` | sqlite-wasm `x.wasm.db` |
| - · `:memory:` | sqlite `:memory:` | sqlite `:memory:` | sqlite (no
file) | sqlite (no file) |
| - · `file://x.wasm.db` | sqlite-wasm `//x.wasm.db` | sqlite-wasm
`x.wasm.db` | sqlite-wasm `x.wasm.db` | sqlite-wasm `x.wasm.db` |
| - · `FILE:x.wasm.db` | sqlite-wasm `FILE:x.wasm.db` | sqlite-wasm
`x.wasm.db` | sqlite-wasm `x.wasm.db` | sqlite-wasm `x.wasm.db` |
| - · `WASM-SQLITE://w.db` | sqlite-wasm `WASM-SQLITE://w.db` |
sqlite-wasm `w.db` | sqlite-wasm `w.db` | sqlite-wasm `w.db` |
| - · `wasm-sqlite://` | sqlite-wasm (empty name) | sqlite-wasm
`:memory:` | sqlite-wasm (no file) | sqlite-wasm (no file) |
| sqlite · `postgres://u:p@h/db` | sqlite `postgres://u:p@h/db` |
**UnsupportedDriverError**, scheme `"postgres://"` | Error, scheme
`"postgres://"` | Error, scheme `"postgres://"` |
| sqlite · `sql://x.db` | sqlite `x.db` | **UnsupportedDriverError**,
scheme `"sql://"` | Error, scheme `"sql://"` | Error, scheme `"sql://"`
|
| sqlite · `libsql://pin.turso.io` | sqlite `libsql://pin.turso.io` |
**UnsupportedDriverError**, scheme `"libsql://"` | Error | Error |
| sqlite · `file:` | sqlite (empty name) | **UnsupportedDriverError**,
names no file | Error, names no file | Error, names no file |
| sqlite · empty or blank | sqlite (empty name) | sqlite `:memory:` |
unified default file | unified default file |
| sqlite · unset | sqlite `:memory:` | sqlite `:memory:` | unified
default file | unified default file |
### Behaviour changes on the CLI host beyond `file://` and `FILE:`
(reported, not hidden)
1. **Newly refused, fatally:** an explicit `sqlite` selection over a URL
with a `SCHEME://` that is not `file:` (`postgres://`, `libsql://`,
`wasm-sqlite://`, any other), and a `file:` / `FILE:` naming no file.
The first used to boot "ready" and create directories named after the
URL, credentials included (measured above); the second declared an empty
file name. Both are what the runtime host already refused for the same
input. Remedy in the changeset: point `OS_DATABASE_URL` at a file, or
select the driver the URL is for.
2. **The undocumented `sql://` strip is gone.** It was reachable only
under an explicit `sqlite` selection (URL inference refuses `sql://` as
an unknown scheme). `git grep` over the tree finds no other `sql://`
spelling. It is now refused like any other non-file scheme; the
changeset says to write `file:`.
3. **An empty or blank URL under an explicit `sqlite` / `sqlite-wasm`
selection is read as absent: `:memory:`.** It used to be an empty file
name. Blank is absent the way `inferDriverTypeFromUrl` reads it; without
this the rule would refuse it with "it names no file after `file:`", a
sentence about something the operator never wrote. `wasm-sqlite://` with
nothing after it is `:memory:` the same way, by the runtime rule's own
empty answer for the WASM kind.
## Public surface (mechanism assumption 2)
- `packages/runtime` has no API-surface snapshot or export ledger: its
`package.json` declares `build`, `dev`, `typecheck`,
`check:test-typecheck`, `gen:test-typecheck-debt`, `test`, `test:repo`;
the `api-surface/` snapshot and `check:exported-any` are
`packages/spec`'s. Repo gates reading built output (`check:dts-closure`,
`check:published-files`, `check:dual-build-cjs-loads`,
`check:lean-entry-closure`, `check:published-readme-exports`) take no
registration for a new named export. `check:exported-any-returns` would
flag an `any` return; the export returns `string`.
- Downstream typecheck sweep not needed, with evidence: the delta is one
added named export whose name collides with nothing. `git grep` finds no
`export * from '@objectstack/runtime'` in this repo or in `../objectui`,
so no consumer can meet a star re-export ambiguity, and no other symbol
is named `sqliteFilenameFromUrl`. The one consumer that imports it,
`@objectstack/cli`, was typechecked against the rebuilt `.d.ts` (below).
## Verification
Local runs are on the head named in each line; CI runs the full farm.
- **Runtime pins** (`packages/runtime`):
`resolve-project-database.test.ts` (an `it.each` over `FILE:./x.db`,
`File:///tmp/x.db`, `FILE://x.db`, `FILE:./x.wasm.db` and the lower-case
/ bare-path CONTROL rows, asserting `driver` and `sqliteFile`; a bare
`FILE:` refused as plain `Error` with the names-no-file sentence) and
`standalone-stack.test.ts` (`createStandaloneStack` from an empty cwd
over `FILE:./upper/x.db` declares `./upper/x.db`, equal to the `file:`
spelling, and the cwd holds no entry matching `/^file:/i`).
- **CLI pins** (`driver-vocabulary-parity.test.ts`, alias block):
`file://x.db`, `file:///var/lib/objectstack/x.db`, `file://x.wasm.db`,
`FILE:./x.db`, `File:///var/lib/objectstack/x.db`, `FILE:./x.wasm.db`
(plus the lower-case and bare-path controls) open the same file on both
hosts, dev and prod; both hosts refuse explicit `sqlite` over a
credentialed `postgres://` URL, `file:` and `FILE:`, the CLI's refusal
an `UnsupportedDriverError` with `recognized: true`, `driverType:
'sqlite'`, a message equal to the runtime's minus its tag, never the
runtime's verbatim, and with no credential; the blank-URL reading on the
CLI host.
- **Runs on the final head `48728977b`** (after merging `origin/main` at
`1eac7ad75`; every heavy run through `scripts/pm/os-verify-lock.sh`,
verdict lines quoted):
- `pnpm --filter @objectstack/runtime test` (vitest `local` project):
`Test Files 399 passed (399)`, `Tests 5990 passed | 19 skipped (6009)`,
`VERDICT command-exit 0`.
- `pnpm --filter @objectstack/cli exec vitest run --project unit
--maxWorkers=2 src/utils/driver-vocabulary-parity.test.ts
src/utils/storage-driver.test.ts
src/commands/database-driver-allowlist.pin.test.ts
src/utils/dev-self-heal-host-parity.test.ts`: `Test Files 4 passed (4)`,
`Tests 188 passed (188)`, `VERDICT command-exit 0`. The allowlist pin is
in the set because it uses `resolveStorageDefinition('sqlite', {
databaseUrl: 'libsql://pin.turso.io' })` as an oracle: it now answers
through the new typed refusal (`recognized: true`, `driverType:
'sqlite'`) and still derives `sqlite`. The CLI `integration` tier is
declared to CI: the diff touches no integration-tier file and no spawn
entry.
- `pnpm --filter @objectstack/runtime typecheck && pnpm --filter
@objectstack/cli typecheck`: `VERDICT command-exit 0`; both
`check:test-typecheck: OK` with their debt ledgers held.
- `pnpm exec eslint --no-inline-config` over the six changed `.ts`
files: exit 0, no output. The repo-wide lint run is the PR's `Lint &
Repo Gates` check-run.
- Derived gate families: `node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack` (65 commands), each run once with its
exit code captured before any pipe: 65 of 65 `exit 0`. Reconciled with
`--ran`: `65 derived, 65 run, 0 NOT-MEASURED, 0 UNRUN` (`a DERIVED zero
— all 65 recorded an exit code and none of them is 3`). Also `pnpm
--filter @objectstack/spec check:generated` (spec moved on `main`'s side
of the merge): exit 0, `All 14 generated artifacts are up to date`.
- **Ablations** (run on `71b52fdbf`, before the merge and the tag-strip
follow-up, neither of which touches the mutated lines), each through
`node scripts/ablation-replace.mjs` (anchor hit 1 time, blob changed,
restored to the `HEAD` blob with `git diff HEAD` empty). Both subjects
are reached through relative source imports (the runtime pins import
`./standalone-stack.js`, the parity file imports `./storage-driver.js`),
so neither needed a `dist/` rebuild.
- Runtime: `/^file:(\/\/)?/i` back to `/^file:(\/\/)?/` on the `sqlite`
path: `Tests 5 failed | 63 passed (68)`. Red: the `FILE:./x.db`,
`File:///tmp/x.db`, `FILE://x.db` rows, the bare-`FILE:` refusal, and
the `createStandaloneStack` case (`expected 'FILE:./upper/x.db' to be
'./upper/x.db'`). The lower-case and bare-path CONTROL rows stayed
green.
- CLI: `return sqliteFilenameFromUrl(url, kind);` replaced by the old
local strip `return url.replace(/^file:/, '');`: `Tests 9 failed | 90
passed (99)` in the parity file. Red: the six `file://` / upper-case
rows and the three refusal rows. The `sqlite:` / `sqlite://` rows, the
lower-case and bare-path controls and the blank-URL case stayed green.
- **Reverse verification of the cross-package type** (on `71b52fdbf`):
with the CLI call changed to `sqliteFilenameFromUrl(url, 'postgres')`,
`tsc --noEmit` in `packages/cli` failed with `error TS2345: Argument of
type '"postgres"' is not assignable to parameter of type '"sqlite" |
"sqlite-wasm"'`, so the CLI read the rebuilt runtime `.d.ts`; restored
and re-checked as above.
## Acceptance notes
- **Out of scope, measured at the public door, for the seat to file:**
the rule's `sqlite-wasm` kind refuses nothing. On the runtime host,
`OS_DATABASE_DRIVER=sqlite-wasm
OS_DATABASE_URL=postgres://PLACEHOLDER-USER:PLACEHOLDER-SECRET@127.0.0.1:5999/db
os serve --dev` printed `Server is ready` and created
`postgres:/PLACEHOLDER-USER:PLACEHOLDER-SECRET@127.0.0.1:5999/db` under
the cwd: credentials in a directory name, against the module header's
rule that a URL is never treated as a file path to make bogus
directories. The CLI host's wasm arm gave the same file name before this
PR and gives it now by the same rule (table rows `sqlite-wasm ·
libsql://…`). Not fixed here: the refusal for the WASM kind is its own
wording and behaviour decision, and this card's surface is the `file:`
strip.
- The two hosts still differ on an **absent** URL under an explicit
`sqlite` selection: the CLI host's legacy path opens `:memory:`, the
runtime host its unified default file. That difference predates this PR
(rows "unset" and "empty or blank" above) and is not judged here.
- `#23796` recorded a `[driver-sqlite-wasm]` prefix on write-failure
lines while the banner read `SqlDriver(better-sqlite3)`; the base-tree
probe here showed the same pairing. Not investigated.
- `origin/main` moved to `a31ff2eb5` after the merge, by two commits
that touch neither changed file nor `storage-driver.ts`'s neighbours
(scaffold `create` / `init` and translations); not merged, the merge
queue rebuilds on the current `main`.
Overlap, in non-closing sentences: #23765 (`detectDriverFromUrl`,
another dev's) is untouched here, as are its parity rows. #23737's
`mkdirSync` arms in `createStandaloneStack` are untouched. #23792's
memory-URL refusal landed on main as PR #23840 before this PR opened;
`origin/main` was merged at `1eac7ad75` and both sides kept. #23633's
`sqlite://` alias fix is the twin this completes for `file://`.
---
_Generated by [Claude
Code](https://claude.ai/code/session_018t6a5rvqPRvEDMCVVW9kYT)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 7a6ba03 commit 34b740f
7 files changed
Lines changed: 191 additions & 20 deletions
File tree
- .changeset
- packages
- cli/src/utils
- runtime/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
533 | 533 | | |
534 | 534 | | |
535 | 535 | | |
536 | | - | |
537 | | - | |
538 | | - | |
539 | | - | |
| 536 | + | |
| 537 | + | |
| 538 | + | |
| 539 | + | |
| 540 | + | |
| 541 | + | |
| 542 | + | |
| 543 | + | |
540 | 544 | | |
541 | 545 | | |
542 | 546 | | |
543 | 547 | | |
544 | 548 | | |
| 549 | + | |
| 550 | + | |
| 551 | + | |
| 552 | + | |
| 553 | + | |
| 554 | + | |
| 555 | + | |
| 556 | + | |
545 | 557 | | |
546 | 558 | | |
547 | 559 | | |
| |||
553 | 565 | | |
554 | 566 | | |
555 | 567 | | |
| 568 | + | |
| 569 | + | |
| 570 | + | |
| 571 | + | |
| 572 | + | |
| 573 | + | |
| 574 | + | |
| 575 | + | |
| 576 | + | |
| 577 | + | |
| 578 | + | |
| 579 | + | |
| 580 | + | |
| 581 | + | |
| 582 | + | |
| 583 | + | |
| 584 | + | |
| 585 | + | |
| 586 | + | |
| 587 | + | |
| 588 | + | |
| 589 | + | |
| 590 | + | |
| 591 | + | |
| 592 | + | |
| 593 | + | |
| 594 | + | |
| 595 | + | |
| 596 | + | |
| 597 | + | |
| 598 | + | |
| 599 | + | |
| 600 | + | |
| 601 | + | |
| 602 | + | |
| 603 | + | |
| 604 | + | |
| 605 | + | |
| 606 | + | |
| 607 | + | |
| 608 | + | |
| 609 | + | |
| 610 | + | |
| 611 | + | |
| 612 | + | |
| 613 | + | |
556 | 614 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
106 | 106 | | |
107 | 107 | | |
108 | 108 | | |
| 109 | + | |
109 | 110 | | |
110 | 111 | | |
111 | 112 | | |
| |||
527 | 528 | | |
528 | 529 | | |
529 | 530 | | |
530 | | - | |
531 | | - | |
532 | | - | |
533 | | - | |
534 | | - | |
535 | | - | |
536 | | - | |
| 531 | + | |
| 532 | + | |
| 533 | + | |
| 534 | + | |
| 535 | + | |
537 | 536 | | |
538 | 537 | | |
539 | 538 | | |
| |||
548 | 547 | | |
549 | 548 | | |
550 | 549 | | |
551 | | - | |
552 | | - | |
553 | | - | |
554 | | - | |
| 550 | + | |
| 551 | + | |
| 552 | + | |
555 | 553 | | |
556 | 554 | | |
557 | 555 | | |
| |||
629 | 627 | | |
630 | 628 | | |
631 | 629 | | |
| 630 | + | |
| 631 | + | |
| 632 | + | |
| 633 | + | |
| 634 | + | |
| 635 | + | |
| 636 | + | |
| 637 | + | |
| 638 | + | |
| 639 | + | |
| 640 | + | |
| 641 | + | |
| 642 | + | |
| 643 | + | |
| 644 | + | |
| 645 | + | |
| 646 | + | |
| 647 | + | |
| 648 | + | |
| 649 | + | |
| 650 | + | |
| 651 | + | |
| 652 | + | |
| 653 | + | |
| 654 | + | |
| 655 | + | |
| 656 | + | |
| 657 | + | |
| 658 | + | |
| 659 | + | |
| 660 | + | |
| 661 | + | |
| 662 | + | |
| 663 | + | |
632 | 664 | | |
633 | 665 | | |
634 | 666 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
8 | 8 | | |
9 | 9 | | |
10 | 10 | | |
11 | | - | |
| 11 | + | |
12 | 12 | | |
13 | 13 | | |
14 | 14 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
335 | 335 | | |
336 | 336 | | |
337 | 337 | | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
338 | 368 | | |
339 | 369 | | |
340 | 370 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
15 | 15 | | |
16 | 16 | | |
17 | 17 | | |
18 | | - | |
| 18 | + | |
19 | 19 | | |
20 | 20 | | |
21 | 21 | | |
| |||
397 | 397 | | |
398 | 398 | | |
399 | 399 | | |
| 400 | + | |
| 401 | + | |
| 402 | + | |
| 403 | + | |
| 404 | + | |
| 405 | + | |
| 406 | + | |
| 407 | + | |
| 408 | + | |
| 409 | + | |
| 410 | + | |
| 411 | + | |
| 412 | + | |
| 413 | + | |
| 414 | + | |
| 415 | + | |
| 416 | + | |
| 417 | + | |
| 418 | + | |
| 419 | + | |
| 420 | + | |
400 | 421 | | |
401 | 422 | | |
402 | 423 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
582 | 582 | | |
583 | 583 | | |
584 | 584 | | |
585 | | - | |
586 | | - | |
| 585 | + | |
| 586 | + | |
| 587 | + | |
| 588 | + | |
| 589 | + | |
| 590 | + | |
| 591 | + | |
| 592 | + | |
| 593 | + | |
| 594 | + | |
| 595 | + | |
| 596 | + | |
| 597 | + | |
| 598 | + | |
| 599 | + | |
| 600 | + | |
| 601 | + | |
| 602 | + | |
| 603 | + | |
587 | 604 | | |
588 | 605 | | |
589 | 606 | | |
590 | 607 | | |
591 | 608 | | |
592 | | - | |
| 609 | + | |
593 | 610 | | |
594 | 611 | | |
595 | 612 | | |
| |||
0 commit comments