Skip to content

fix(driver-sql): a SQLite database whose header SQLite cannot read, and a dangling-link directory on the boot that writes, fail connect() naming the file - #25080

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-25019-sqlite-file-failure-family
Oct 21, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-25019-sqlite-file-failure-family

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Part of #25019

The card keyword above is the seat's: member 3 (the wasm quarantine) stays on #25019 under the seat's ruling 6311031589 (route A, inherited from #24960), dispatched on the card after this PR lands, so this merge does not close the card.

Clause-②: no (narrowing)

Narrowing face: three answers that read success now refuse, each naming the file. SqlDriver.connect() rejects a SQLite database whose header or schema SQLite cannot read (SQLITE_CORRUPT at the first statement), and a path whose directory is a dangling symbolic link (ENOENT), where both used to resolve. The read-only boot (sqliteAbsentFile: 'empty-in-memory') rejects a dangling symbolic link as the file itself when the directory its chain of links ends in does not exist, where it opened an empty in-memory database and read connected. Nothing exported changes shape. It owes a CONTRACT_REVIEW_TIER review before the queue (the claim's line).

Dev session session_01QK6pdbxKZiHdFApL8RYVh9 (a mode:subagent run), dispatched by the domain:engine seat 2 under claim 6307297304. This is the SQLite file-failure family's closing card. It builds on PR #25006 (SQLITE_NOTADB) and PR #25007 (the read-only boot's unopenable path), both on main.

What changed

  • Member 1, SQLITE_CORRUPT. packages/drivers/driver-sql/src/sql-driver.ts, only the applyAutoVacuumIncremental comment and SQLITE_FILE_REFUSALS: one more row, SQLITE_CORRUPT with the words "is damaged (SQLite cannot read its header or its schema), and was left as it is". packages/services/service-datasource/src/sqlite-driver-fallback.ts: isSqliteFileOpenFailure adds the same code, with its doc list and the module note (the cross-lane line declared to domain:services on the claim).
  • Member 2, a dangling-link directory on the boot that writes. packages/drivers/driver-sql/src/sqlite-database-file.ts, makeDatabaseDirectory: the walk now asks the read-only twin's own isDanglingLink at each level stat answers ENOENT for, and refuses that level with stat's error as code and the link named. Before, the level went on the "missing" list, mkdir answered EEXIST, and the creating loop accepted that as a concurrent creator. Both twins now print the link through one helper (danglingLinkWords), so their words cannot drift. ensureDatabaseExists in sql-driver.ts is untouched: it wraps what the walk throws, as it already did.
  • The dangling link as the file itself (family note 6305577138): pinned, not left out. Measured below: the read-only boot read it absent when the directory the link points into does not exist, while the writing boot refuses it with SQLITE_CANTOPEN. probeAbsentSqliteDatabaseFile now follows a dangling link that is the file to the end of its chain (danglingLinkEnd, reading each relative target from the real directory of the link that holds it, as the kernel does) and judges that directory without making it: it must exist, and allow write and search. Otherwise 'absent', as before.
  • Member 3, the wasm quarantine: measured, not changed. Closing it means changing what driver-sqlite-wasm's quarantine does, so it is the card's fork, reported to the seat with the four-axis analysis. No line of packages/drivers/driver-sqlite-wasm changed.
  • Changeset .changeset/25019-sqlite-file-failure-family.md: @objectstack/driver-sql and @objectstack/service-datasource minor, with the Clause-② line, the BREAKING banner (Before, Now, Fix) and the ADR-0087 disposition not-required (no-migration-prescription), as PR fix(driver-sql): a SQLite file that is not a database fails connect() at once, naming it, and a dev boot no longer renames and replaces it #25006's changeset does.
  • Pin files: packages/drivers/driver-sql/src/sql-driver-25019-sqlite-damaged-file-and-dangling-link.test.ts (relative src import) and packages/services/service-datasource/src/__tests__/sqlite-driver-fallback-damaged-file-and-dangling-link.test.ts (resolves @objectstack/driver-sql through dist/, an existing KNOWN_UNALIASED_TEST_IMPORTS pair).

Premise, measured on base d68aab6fd (better-sqlite3 13.0.3, SQLite 3.53.4, Node v22.22.0)

H1 holds, and both shapes were measured. Statement by statement, through better-sqlite3 directly, on a database written and then damaged:

shape first statement (PRAGMA auto_vacuum) SELECT 1 connect() on base
truncated to 10 or 16 bytes (inside the magic string) SQLITE_NOTADB SQLITE_NOTADB refused (PR #25006)
truncated to 50, 99, 100, 101, 512 or 4095 bytes SQLITE_CORRUPT SQLITE_CORRUPT resolved
truncated to its first page, or to half its pages SQLITE_CORRUPT SQLITE_CORRUPT resolved
page 1's b-tree (the schema) overwritten past the header SQLITE_CORRUPT SQLITE_CORRUPT resolved
in-header page count larger than the file SQLITE_CORRUPT SQLITE_CORRUPT resolved
one table's page overwritten (damage deeper inside) answers answers resolved; only a read of that table answers SQLITE_CORRUPT

So SQLITE_CORRUPT at the FIRST statement is a verdict on the whole file: every statement answers the same code, SELECT 1 included, and nothing can be read. A database damaged deeper inside answers the first statement and connects, and its damage surfaces on the read that reaches it. The refusal is worded for the first kind ("SQLite cannot read its header or its schema"), and a control pins the second (it still connects). No extended code (SQLITE_CORRUPT_*) appeared at the first statement. Nothing in the tree reads SQLITE_CORRUPT or "disk image is malformed" outside driver-sqlite-wasm and two unrelated REST and runtime fixtures.

H2 holds. On base, the writing boot over link/archive.db (a dangling link) resolved connect(), then the first statement failed with a code-less TypeError ("Cannot open database because the directory does not exist"). Over link/sub/archive.db it already refused (ENOENT from mkdir), without naming the link. The read-only boot refused both naming the link. After the change, both boots answer SqliteDatabaseFileError, code: 'ENOENT', naming the file and the link, at both depths. Their words differ by one verb ("could not be made" on the writing boot, "cannot be made" on the read-only one), as they already did for every other shape. A directory that is a link to an existing directory still connects and creates the file in it.

The dangling link as the file. Base and after, both boots:

file archive.db is a link to writing boot (base and after) read-only boot, base read-only boot, after
target.db in a directory that exists (absolute or relative target, direct or through a second link) connects; creates target.db absent, in memory absent, in memory (unchanged)
missing/target.db (direct, or through a second link) refused, SQLITE_CANTOPEN absent, in memory, connected refused, ENOENT, naming the target
itself, through a loop refused, SQLITE_CANTOPEN refused, ELOOP refused, ELOOP (unchanged)

Nothing on disk changed in any read-only run.

H3, member 3. With the #24950 suite's load-failure seam (acquireRawConnection answering a real ERR_DLOPEN_FAILED), development resolveSqliteDriver over a plain-text file and over a header-truncated database, after this change:

  • the addon loads: both refused (SQLITE_NOTADB, SQLITE_CORRUPT), the file untouched;
  • the addon cannot load: both resolve to sqlite-wasm, and data/ holds archive.db (an empty database) and archive.db.corrupt-TIMESTAMP;
  • controls: a real database with the addon unable to load steps down to wasm and reads its rows, not renamed; an absent file does the same.

The rename happens in driver-sqlite-wasm, WasmSqliteConnection.open() (wasm-connection.ts), which calls quarantineCorruptFile when new SQL.Database(bytes) or PRAGMA quick_check(1) fails. The wasm driver takes no option that could say "refuse"; resolveSqliteDriver's wasm rung also catches every wasm failure and goes on to the in-memory rung. A second door reaches the same quarantine with no step-down at all: a datasource declared driver: 'sqlite-wasm' over a plain-text file, through DatasourceConnectionService.connectDeclared on the built dist/, was renamed and read available in development AND in production. Every in-tree construction that reaches the quarantine is a declared datasource: the default datasource is one since #3826 (the CLI and the standalone stack hand it to DefaultDatasourcePlugin), so "the dev server's own self-heal" and "a declared datasource" are one population. The self-heal was written for torn images, which wasm's atomic flush (atomicWriteFile, write then rename) has since closed. The fork and its options are in the report to the seat.

Evidence

Public door, the card's own (bin/run-dev.js migrate apply --yes through tsx, a throwaway project whose objectstack.config.ts declares archive (driver: 'sqlite') and binds door_note to it), after turbo run build --filter='@objectstack/cli^...' (60 of 60) on the merge head cc15060f7:

  • Header-truncated archive.db (a database this driver wrote, cut to 50 bytes): datasource 'archive': connect failed — SQLite database file 'PROJECT/data/archive.db' is damaged (SQLite cannot read its header or its schema), and was left as it is: PRAGMA auto_vacuum - database disk image is malformed. (1 object(s) bind to it explicitly (door_note) …), exit 1. The md5 is unchanged (dc1e7bea… before and after), and data/ holds archive.db alone. No step-down warning printed.
  • Dangling-link directory (data/link pointing at a path that does not exist): connect failed — SQLite database file 'PROJECT/data/link/archive.db' cannot be created: its directory 'PROJECT/data/link' could not be made: 'PROJECT/data/link' is a symbolic link to a path that does not exist: ENOENT …, exit 1. No "native better-sqlite3 unavailable" line, no wasm line; the link still dangles and its target was not created.
  • Control, a real database: datasource 'archive': connected (driver=sqlite, schemaMode=managed), exit 0, not renamed.
  • The card measured the base on the same door: the truncated file was renamed to archive.db.corrupt-1792539192447 with an empty database in its place and read connected; the dangling-link directory read connected under the false native-addon warning.

Pins (both files run inside the whole-package suites on the final head cc15060f7, under os-verify-lock):

  • driver-sql, 10 cases: the header-truncated database refused with SqliteDatabaseFileError, code SQLITE_CORRUPT, filename, a message naming the file and carrying the cause, under NODE_ENV=development and production; a database cut to its first page, the same. Each asserts no .corrupt-* sibling while the refused connection is open, then, closed, the same bytes and a directory holding only that file. The header still says WAL, so SQLite keeps its -shm and an empty -wal while that connection is open, and removes both when it closes. CONTROL: a database damaged deeper inside connects, and the read of the damaged table fails with SQLITE_CORRUPT as its cause. The dangling-link directory refused on the writing boot (ENOENT, file and link named) with the read-only boot's answer for the same path asserted beside it (class, code, filename, the link words), at both depths, the link still dangling and nothing created. CONTROLS: a linked directory that exists connects; an absent file under makeable directories connects. The dangling file link: refused by the read-only boot (ENOENT, the target named), direct and through a second link, with the writing boot's SQLITE_CANTOPEN asserted beside it and nothing written; CONTROL: with the target's directory present, the read-only boot reads it absent (:memory:) and writes nothing, and the writing boot creates the target through the link (absolute and relative targets).
  • service-datasource, 10 cases: development resolveSqliteDriver over the header-truncated database rejects with the driver's refusal, warns nothing, and leaves the file and its directory as they were; production returns native unprobed and its connect() refuses; DatasourceConnectionService.connectDeclared reports availability: 'failed' with a reason naming the file and registers no driver, in development and production, and moves nothing aside. The dangling-link directory: development resolveSqliteDriver rejects (ENOENT, file and link named) with no warning, and the read-only resolve over the same path gives the same class, code, file and words; connectDeclared reports failed naming the file and the link, on the writing and the read-only factory, in development and production; nothing created. CONTROLS: a real database opens native and answers; an absent file in a makeable directory opens native and is created; a native addon that cannot load over a real database still steps down to wasm SQLite, with NATIVE_SQLITE_WASM_FALLBACK_WARNING, and reads the same rows, not renamed.
  • Whole packages on the final head cc15060f7 (re-run after the worker restart, on a full turbo run build, 77 of 77): driver-sql "Test Files 227 passed | 12 skipped (239) · Tests 3712 passed | 210 skipped (3922)" (the skips are the live PG/MySQL cells, not provisioned here); service-datasource "Test Files 53 passed (53) · Tests 930 passed (930)". tsc --noEmit exit 0 for both; tsc --listFiles includes each new test file (1 hit each).

Ablation (scripts/ablation-replace.mjs WRAP, each leg under os-verify-lock; every restore proven by blob equality with HEAD, an empty git diff HEAD, and an empty git status --porcelain; at 281d27bdf; the only later change to these files is f7ea1c503, a type annotation on two rows the damaged-page control reads):

  • Leg 1, the driver row. ['SQLITE_CORRUPT', 'is damaged became ['ABLATION_25019_CORRUPT', 'is damaged: anchor 1 to 0, blob 778b08f36ba6 to 7048da65440a. driver-sql rebuilt; ablation-dist-preflight.mjs found the marker in 2 built files. Predicted and observed: driver-sql "3 failed | 7 passed" (the two header-truncated cases and the first-page case; the controls and both link blocks green); service-datasource "4 failed | 6 passed" (development resolved to sqlite-wasm; the production refusal was undefined; both connectDeclared cases read available). Restored, rebuilt; preflight --absent found the marker in none of the 6 built files, tree clean; both pin files 10/10.
  • Leg 2, the step-down code. || code === 'SQLITE_CORRUPT' became || code === 'ABLATION_25019_SD': blob 6e42085d4512 to 4e4570fda7a2. A relative src subject, so no dist step. Predicted and observed: service-datasource "2 failed | 8 passed" (the two development cases; production, which the driver alone carries, green).
  • Leg 3, the dangling-link directory guard. The line if (isDanglingLink(level)) throw danglingLinkRefusal(level, e); was deleted: blob adf3e6b62148 to 8c7be7a55c8d. driver-sql rebuilt; preflight --absent on danglingLinkRefusal(level, e) found it in none of the built files (the same marker read present in 2 on the pristine build first). Predicted and observed: driver-sql "2 failed | 8 passed" (both depths: the first resolved, the second refused without the link's words); service-datasource "3 failed | 7 passed" (development resolve, and connectDeclared in both modes). Restored, rebuilt; preflight found the guard back in 2 built files, tree clean; both pin files 10/10.
  • Leg 4, the dangling file link. const linkEnd = danglingLinkEnd(filename); became const linkEnd = undefined as string | undefined; // ABLATION_25019_FILELINK: blob adf3e6b62148 to ad84f6bf25a0. Relative src subject. Predicted and observed: driver-sql "1 failed | 9 passed" (the refusal; its control green).

Driver ledger (pnpm check:driver-conformance): before, at d68aab6fd: "55 covered cell(s), 0 in the DEBT ledger, 0 exempt"; dialect axis 9 suites, 8 matrix and 1 named, 0 in the DIALECT ledger. After, at cc15060f7: the same counts. The ledger did not move.

Gates. node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths) at cc15060f7 derived 72 commands. I ran each one once, sequentially, with its exit captured before any pipe. 70 exited 0 on the first pass. Two answered exit 3 (PREREQUISITE NOT MET) and were re-run on the same head after their prerequisite: check:dual-build-cjs-loads needed every package's dist/ (a full turbo run build, 77 of 77), and check:type-check-debt needed a turn on the verify lock for its closure build ("1 ledger entr(ies) re-measured … none above its recorded number"). Both then exited 0. --ran reconciliation: "72 derived, 72 run, 0 NOT-MEASURED, 0 UNRUN". No exported symbol was renamed or removed, so check-platform-checklist.mjs --changed-from was not owed. CI owns the full farm and test:repo.

Dogfood battery (owed: the Clause-② line carries the (narrowing) arm): running on cc15060f7 under os-verify-lock as this PR opens; its verdict line and counts are in the dev report on the card.

Acceptance notes

  • Member 3 is the seat's fork, not this PR's. Measured above: with a native addon that cannot load, a declared file that is not a database or whose header SQLite cannot read is still renamed by wasm SQLite on a development boot; and a datasource declared driver: 'sqlite-wasm' over such a file is renamed and reads available in development and production. Both live in WasmSqliteConnection.open()'s quarantine. The options and their four-axis analysis are in this PR's dev report on the card; this PR changes nothing there.
  • A refused connect's connection is never released on the production path. DatasourceConnectionService does not disconnect a handle whose connect() rejected, and connect() does not close its own pool when it refuses the file. For a damaged file whose header says WAL, SQLite therefore keeps -shm and an empty -wal beside the file for as long as the process lives (measured at the unit level; the development path closes the probe and leaves nothing). The operator's bytes are not touched. It predates this PR (a SQLITE_NOTADB file holds the same open handle, without visible siblings) and is outside this claim's file surface. Observation, not filed; carrier: none.
  • The development telemetry sibling (packages/cli/src/utils/telemetry-datasource.ts) still catches every throw, so a damaged telemetry sibling is now neither renamed nor reported. That is its pre-existing best-effort shape (family note 6305864303); out of the claim's scope.
  • The writing boot answers the dangling file link whose target's directory is missing with SQLite's own SQLITE_CANTOPEN ("unable to open database file"), which names the file but not the link. The read-only boot names the target. Both refuse; the words were left as SQLite gives them.
  • main was merged at 157ebd008 (merge head cc15060f7). It brought eight commits touching spec, objectql, plugin-security, core, rest, lint, cli and docs, none of this diff's packages; the closure was rebuilt and both whole packages and the pins re-run on the merge head. The worker restarted during the first full build; that run's readings were discarded and the build, both suites and the two prerequisite gates re-run on the same head.
  • CI owns the full farm and test:repo; this diff touches no repo-project test.

Generated by Claude Code

…, and a dangling-link directory on the writing boot

WIP checkpoint: SQLITE_CORRUPT joins SQLITE_FILE_REFUSALS and the
step-down's file-failure codes; makeDatabaseDirectory refuses a level a
dangling symbolic link holds, as its read-only twin does; the read-only
probe follows a dangling link that is the file itself to the directory
the writing boot would create it in.

Claude-Session: https://claude.ai/code/session_01QK6pdbxKZiHdFApL8RYVh9
Co-authored-by: Claude <noreply@anthropic.com>
…the dangling-link directory on the writing boot, and the dangling file link on the read-only boot

Claude-Session: https://claude.ai/code/session_01QK6pdbxKZiHdFApL8RYVh9
Co-authored-by: Claude <noreply@anthropic.com>
…Lite file-failure family

Claude-Session: https://claude.ai/code/session_01QK6pdbxKZiHdFApL8RYVh9
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Oct 21, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/driver-sql, @objectstack/service-datasource, touching 12 documentable anchor(s).

9 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/data-modeling/drivers.mdx (via SqlDriver (symbol, a top-level class))
  • content/docs/data-modeling/index.mdx (via SqlDriver (symbol, a top-level class))
  • content/docs/data-modeling/queries.mdx (via SqlDriver (symbol, a top-level class))
  • content/docs/permissions/tenant-audit-census.mdx (via SqlDriver (symbol, a top-level class))
  • content/docs/plugins/packages.mdx (via SqlDriver (symbol, a top-level class))
  • content/docs/protocol/kernel/index.mdx (via SqlDriver (symbol, a top-level class))
  • content/docs/protocol/kernel/lifecycle.mdx (via SqlDriver (symbol, a top-level class))
  • content/docs/protocol/objectql/query-syntax.mdx (via SqlDriver (symbol, a top-level class))
  • content/docs/protocol/objectql/types.mdx (via SqlDriver (symbol, a top-level class))

⛔ 3 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17/17-0.mdx (via SqlDriver (symbol, a top-level class))
  • content/docs/releases/v17/17-5.mdx (via SqlDriver (symbol, a top-level class))
  • content/docs/releases/v17/17-6.mdx (via SqlDriver (symbol, a top-level class))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 13 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 2b1382a5b3887e4abd194411ef77a7cd368c95c2 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from d7f4667d1dbe9a8c17925d88d2ddd074aa449962 — the merge of head cc15060f7e5bb2ff2e1efc905688bc9f0dc83362 into base 2b1382a5b3887e4abd194411ef77a7cd368c95c2, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin d7f4667d1dbe9a8c17925d88d2ddd074aa449962 && git checkout d7f4667d1dbe9a8c17925d88d2ddd074aa449962
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 2b1382a5b3887e4abd194411ef77a7cd368c95c2 cc15060f7e5bb2ff2e1efc905688bc9f0dc83362 && git checkout -B drift-repro 2b1382a5b3887e4abd194411ef77a7cd368c95c2 && git merge --no-ff cc15060f7e5bb2ff2e1efc905688bc9f0dc83362

node scripts/docs-audit/affected-docs.mjs --json 2b1382a5b3887e4abd194411ef77a7cd368c95c2

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 2b1382a5b3887e4abd194411ef77a7cd368c95c2 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: cc15060f7e5bb2ff2e1efc905688bc9f0dc83362
Local-runs: none
Scope: packages/drivers/driver-sql/src/sql-driver.ts, packages/drivers/driver-sql/src/sqlite-database-file.ts, packages/drivers/driver-sql/src/sql-driver-25019-sqlite-damaged-file-and-dangling-link.test.ts, packages/services/service-datasource/src/sqlite-driver-fallback.ts, packages/services/service-datasource/src/tests/sqlite-driver-fallback-damaged-file-and-dangling-link.test.ts + packages/drivers/driver-sqlite-wasm/src/wasm-connection.ts (open, assertReadable, quarantineCorruptFile), packages/services/service-datasource/src/datasource-connection-service.ts (connect, handleFailure), packages/services/service-datasource/src/default-datasource-driver-factory.ts (the sqlite and sqlite-wasm arms), packages/services/service-datasource/src/connect-failure-remedy.ts, packages/cli/src/commands/db/clean.ts, packages/cli/src/utils/telemetry-datasource.ts, packages/drivers/driver-turso/src/turso-driver.ts (connect, disconnect), examples/app-showcase/src/system/datasources/external-fixture.ts, packages/drivers/driver-sql/src/index.ts (the export surface), scripts/check-changeset-no-major.mjs (the launch-window convention), .changeset/25019-sqlite-file-failure-family.md (the prose face, read for ②)

Inputs: card #25019 (its body and comments 6305577138, 6305864303, 6307297304, 6310958104, 6311031589); PR #25006 and PR #25007 (bodies) with their contract-review records 6305645762 and 6305566864; PR #25080 (body, 6-file list, and the net diff origin/main...cc15060f7 read locally: merge base 157ebd008, the same 6 files, +644/-13); the check-runs on the head, read ONCE, after the second suite's 07:59:01Z start, and not polled. Read-only throughout: nothing built, run or re-run; no worktree. sql-driver.ts was read at the head in the regions the hunks cite (connect, applyAutoVacuumIncremental, SQLITE_FILE_REFUSALS, ensureDatabaseExists, sqliteFilename, resolveSqliteAbsentFileTarget, knexConfigFor). The dispatch order and the dispatching seat's other conclusions were not inputs.

Check-runs on the head, exactly as read (48 runs): 38 success, 9 skipped, 1 in_progress, 0 failed. Green: Build Core; Test Core (the aggregate and shards 1/6 to 6/6); TypeScript Type Check with Type Check workspace, consumer gates, source gates and debt ledger; Lint & Repo Gates with Lint ESLint, CI release & engine gates, docs & code guards, repo & tooling gates, PM dispatch-gates and the three scoped lanes; Governed Surface Queue Guard; Dogfood Regression Gate (the unsharded run); Check Documentation Links; Flag docs affected by code changes; filter; Check PR Size and Auto Label (first suite); and the four claim guards (No other open PR may claim the same issue, No other open PR may claim the same single-writer path, Part-of PR must not also close its card, The card this PR closes must claim this branch) in both suites. skipped: 18.x upgrade harness, Packed-tarball smoke, Build Docs, Console Pin Gate, the sharded Dogfood Regression Gate, Dogfood Verify CLI, Temporal Conformance, and the second suite's Check PR Size and Auto Label. Check Changeset ran twice on this head: run 119094153764 (suite 107209126144, completed 07:25:20Z) is success; run 119107814416 (suite 107220315424, started 07:59:01Z after the PR body edit) is in_progress with no conclusion yet. The queue is armed on that second run's own verdict, not on this record.

① Derived judgments

Every accept-set and public-surface change the diff implies, each named right or wrong:

  1. SqlDriver.connect() accept set, minus SQLITE_CORRUPT at the first statement: right. SQLITE_FILE_REFUSALS grows from two rows to three; the lookup, the SqliteDatabaseFileError(filename, words, cause) throw and every other branch of applyAutoVacuumIncremental's catch are unchanged (native-load failures still warn the one-liner and return false; anything else keeps the full warning and false). The refusal is raised before applySqliteJournalMode and before any write. Only SQLite clients reach it (connect() gates on isSqlite); :memory: and the openedEmptyInMemory redirect never answer this code.
  2. SqlDriver.connect() accept set, minus a directory level a dangling symbolic link holds: right. makeDatabaseDirectory's upward walk now asks isDanglingLink(level) at each level stat answers ENOENT for, and throws danglingLinkRefusal(level, e), a plain Error carrying stat's code (ENOENT) and cause, before the level joins missing. ensureDatabaseExists wraps it as it wrapped every walk failure, so the surface is SqliteDatabaseFileError, code: 'ENOENT', message SQLite database file 'F' cannot be created: its directory 'D' could not be made: 'LINK' is a symbolic link to a path that does not exist: ENOENT .... Before, mkdir answered EEXIST on the link, the creating loop took that for a concurrent creator, and better-sqlite3's code-less TypeError was swallowed: the connect resolved over a datasource no statement could use. Nothing that worked stops working.
  3. resolveSqliteAbsentFileTarget (a value export of @objectstack/driver-sql), the SqlDriver constructor under sqliteAbsentFile: 'empty-in-memory', and the service's wasmFilename line: accept set minus a dangling link as the FILE whose chain ends in a directory that does not exist or does not allow write and search: right, and declared. probeAbsentSqliteDatabaseFile runs danglingLinkEnd only after stat(filename) answered ENOENT; a non-link returns undefined on hop 0 and the directory walk runs as before. The dev's claim allowed "pin it or name it out of scope"; the dev fixed it on the claim's own file, pinned it (two cases, direct and through a second link, with controls for an existing target directory, absolute and relative targets) and ablated it (leg 4). The changeset's third bullet, its BREAKING banner and the PR body's narrowing paragraph all name it. This is the family note 6305577138's shape, closed.
  4. resolveSqliteDriver's development step-down, minus SQLITE_CORRUPT: right. isSqliteFileOpenFailure is still judged on code alone, no message text; the dangling-link refusals carry ENOENT, which the ^E[A-Z0-9]+$ arm already classified, so member 2 needed no line here (pinned: the development resolve over the link rejects with no warning and no wasm rung). Production is untouched (native unprobed; the refusal comes from the service's handle.connect()). The native-load step-down keeps its path: the control with a real ERR_DLOPEN_FAILED over a real database still reaches wasm, warns NATIVE_SQLITE_WASM_FALLBACK_WARNING, reads the rows and renames nothing.
  5. SqliteDatabaseFileError: the code set gains SQLITE_CORRUPT and one more producer of ENOENT; the shape (name, code, filename, cause, the message form) is unchanged. src/index.ts still exports resolveSqliteAbsentFileTarget alone from this pair; the class, makeDatabaseDirectory, probeAbsentSqliteDatabaseFile and the three new helpers (danglingLinkWords, danglingLinkRefusal, danglingLinkEnd) are package-private. No published declaration moved; the PR's "Nothing exported changes shape" holds.
  6. The wasm quarantine is unchanged, by the diff. git diff origin/main...cc15060f7 -- packages/drivers/driver-sqlite-wasm is empty. At the head, WasmSqliteConnection.open() still reads the bytes, new SQL.Database(bytes) or assertReadable's PRAGMA quick_check(1) throws, and quarantineCorruptFile renames to FILE.corrupt-TIMESTAMP and starts an empty database, in every mode. What still reaches it is exactly the seat's ruled member 3: the development step-down with an addon that cannot load, and a datasource declared driver: 'sqlite-wasm' (the factory's sqlite-wasm arm builds SqliteWasmDriver directly). This PR adds no door to it and closes the two the card named.

(a) Classifying SQLITE_CORRUPT as a whole-file refusal: right. The first statement is the READ PRAGMA auto_vacuum (applyAutoVacuumIncremental; knex's better-sqlite3 client runs nothing on acquire and the driver installs no sqlite afterCreate). In SQLite's own pragma table auto_vacuum carries the NeedSchema flag, so that read forces the full schema load: page 1's header (a bad magic string is SQLITE_NOTADB; a header page count beyond the file is SQLITE_CORRUPT) and then every sqlite_schema row, whose b-tree pages must read and whose CREATE text must parse (corruptSchema answers SQLITE_CORRUPT, "malformed database schema"). A SQLITE_CORRUPT there is the answer every statement that names a table repeats, because each re-attempts the same load; the dev measured SELECT 1 answering the same code on every shape in the table (truncation at 50..4095 bytes, to one page, to half the pages, a garbled schema b-tree, a page count larger than the file). So nothing ObjectStack does on such a datasource could succeed, and refusing it turns away no working datasource. Damage below the schema (a table or index page, the freelist) loads the schema and connects, and surfaces on the read or write that reaches it; the dev's CONTROL pins that shape (the table's root page overwritten; connect resolves; the read fails with SQLITE_CORRUPT as its cause). Two shapes, by code reading and not measured, answer SQLITE_CORRUPT at the first statement with intact bytes, and neither is a working datasource: a schema written with syntax this SQLite build cannot parse (a file from a newer SQLite, for instance a STRICT table on an older engine; corruptSchema names it "malformed database schema (NAME) - ..." and the refusal carries that cause text, so the operator can tell), and a foreign or stale -wal beside a healthy main file (the refusal names the main file alone). For both, the words "is damaged" are loose, the effect is right, and before this PR a development boot renamed the file. One line on the mechanism: the catch wraps the setter PRAGMA auto_vacuum = INCREMENTAL as well as the read; the setter runs only when the read answered and touches page 1 alone (a FULL-to-INCREMENTAL switch skips compaction; a NONE file with pages is a silent no-op in SQLite), so in practice the verdict is the first statement's. Extended codes: better-sqlite3 reports sqlite3_extended_errcode, and the lookup is an exact string; SQLITE_CORRUPT_VTAB, _SEQUENCE and _INDEX arise at vtab connect, autoincrement and integrity_check, none of which a schema load runs, and the dev measured none at the first statement.

(b) The dangling-link refusals: right. Directory: the writing boot refuses exactly the levels the read-only boot refuses (the same upward walk from dirname(resolve(filename)), the same isDanglingLink test, which is lstat succeeding where stat answered ENOENT, which only a dangling link does), with the same class, the same code (stat's ENOENT on both) and the same link words through danglingLinkWords; the outer verbs differ by one word ("could not be made" on the writing boot, "cannot be made" on the read-only one), which predates this PR for every shape (#23696, #24961), and the pins assert class, code, filename and the link words on both boots at both depths. File: the read-only boot now refuses what the writing boot already refused with SQLite's own SQLITE_CANTOPEN; the codes differ (ENOENT, the target's directory named, against SQLite's answer that names the file) as ENOTDIR against EEXIST already did on #24961, each the system's own answer; pinned side by side. Bound: danglingLinkEnd runs only after stat answered ENOENT, so the kernel has already resolved the chain to an end within its own 40-link limit (a cycle answers ELOOP, refused one branch earlier as "cannot be opened or created"); the loop's bound of at most 40 hops restates that, and any readlink or realpath failure ends the walk at the current name. Relative targets: each hop is resolve(realpathSync(dirname(at)), readlinkSync(at)), the real directory of the link that holds it, as the kernel reads it; a hop whose directory is itself missing or a dangling link ends the chain there, and where then fails stat and is refused as "does not exist". Writes: danglingLinkEnd, statSync(where) and accessSync(where, W_OK | X_OK) write nothing; the writing walk throws before the level joins missing, so its creating loop never runs; both pins assert the directory listing unchanged and the link still dangling. where is a directory whenever it exists, because a regular file on the chain answers stat(filename) with ENOTDIR, refused earlier.

(c) Every caller whose answer flips: no FAIL item. No caller crashes, swallows a refusal into success, or renames or replaces an operator's file. The census: outside tests the tree holds no direct new SqlDriver({ client: 'better-sqlite3' }); every sqlite construction goes through resolveSqliteDriver (four callers) or DatasourceConnectionService.

  • DatasourceConnectionService.connect / connectDeclared, through the factory's sqlite arm (development: the resolver closes its probe and re-throws; production: native unprobed, handle.connect() rejects inside the service's try): handleFailure records failed-degraded and warns "degrading (datasource left unconnected)" when nothing binds, and throws the fail-fast line naming the file when an object binds, a mapping routes, or the host declared it boot-critical (ADR-0062 D5; OS_ALLOW_DRIVER_CONNECT_FAILURE degrades instead). New answers: a header- or schema-damaged file, both modes (before: production resolved and the first query failed; development renamed it through wasm and read connected); a dangling-link directory, both modes and both factories (before: resolved, then a code-less failure, or the wasm rung). The primary default datasource is boot-critical, so os dev over a damaged .objectstack/data file now refuses the boot naming the file instead of renaming it and starting empty: the card's own ⛔ line, and the cost the seat's ruling names. Pinned in both modes, no driver registered, nothing moved aside. Right.
  • os db clean (dev: true, the primary and the telemetry sibling): the refusal is caught per target, printed as VACUUM failed for FILE: ..., and the command exits 1; before, a damaged primary was quarantined by wasm and the empty replacement VACUUMed and reported compact. Right.
  • provisionTelemetryDatasource (the development telemetry sibling): the catch answers undefined, so a damaged sibling file is neither renamed nor reported and lifecycle-classed objects stay on the primary. Pre-existing best-effort shape, named by family note 6305864303, ⛔ out of the claim; the PR names it. Right as scoped (③ item 3).
  • TursoDriver.connect() local and replica: super.connect() runs first, before any libsql client or sync, with no catch; a local or replica file SQLite cannot read is refused at connect naming the file. Before, connect resolved and every read, all of which run through the inherited Knex engine, failed with the same code; a replica's sync() cannot read a damaged local file either. No working datasource flips. By code reading; the claim ⛔s the turso driver and the diff holds no turso pin. Right.
  • setupShowcaseExternalDatasource (examples/app-showcase): resolveSqliteDriver with dev from NODE_ENV on the fixture file; a damaged fixture now rejects onEnable naming the file (development: re-thrown by the resolver; production: fixture.connect() throws, uncaught). Before, development renamed and re-provisioned it. Not in the dev's caller list (the same omission 6305645762 noted); the card's ⛔ applied to a declared fixture. Right.
  • The read-only boots (sqliteAbsentFile: 'empty-in-memory'): a damaged file that exists is opened and refused at the first statement like any boot; the dangling file link is item 3; the dangling-link directory was already refused (driver-sql: the read-only probe's absent-file test (existsSync) reads an unopenable SQLite path as absent, so os migrate plan opens an empty in-memory database, reports the datasource connected, and plans DDL into a file that cannot exist #24961). Right.
  • driver: 'sqlite-wasm' declared: unchanged, member 3.

(d) The refusal envelope: right. Each refusal is SqliteDatabaseFileError with filename the declared path, code the cause's own (SQLITE_CORRUPT; ENOENT), cause the error itself, and the message SQLite database file 'F' WORDS: CAUSE naming the file, the link (directory member) or the chain's end and its directory (file member). It echoes nothing secret: a sqlite connection carries no credential, the messages carry paths and SQLite's or the operating system's own text, and TursoDriver's encryptionKey is not on the path. PR #25006's shape is kept to the constructor. The driver-sql pins assert toBeInstanceOf(SqliteDatabaseFileError), code, filename, the file in the message and the cause's message inside it; the service pins assert name === 'SqliteDatabaseFileError', code, filename and the file in the message (duck-typed, as the class is not published), plus the link words and the chain's target where they apply; connectDeclared's reason is asserted to carry the file and the link.

  1. Pin hygiene: both new files read their own mkdtemp scratch alone (check:cross-package-test-inputs not implicated); the service pin's unaliased @objectstack/driver-sql import is the registered KNOWN_UNALIASED_TEST_IMPORTS pair; both sit outside files[]; tsc --listFiles reaches each (the dev's reading; CI's type-check lanes are green). The ablation's four legs each turned the predicted pins red and nothing else, with restore proven; read, not re-run.

② Semver level

Clause-②: no (narrowing)

  • Declaration. Clause-②: no (narrowing) on the PR body and in .changeset/25019-sqlite-file-failure-family.md; nothing authorable is added, and the arm the diff publishes is a narrowing on three fronts: SqlDriver.connect() over a damaged file and over a dangling-link directory, and the read-only judgement over a dangling file link. Right.
  • Level. @objectstack/driver-sql: minor, @objectstack/service-datasource: minor. Right: both packages move published source (sql-driver.ts and sqlite-database-file.ts; sqlite-driver-fallback.ts), a declared narrowing is BREAKING, and check-changeset-no-major.mjs ships a breaking change as minor during the launch window with the banner and the ADR-0087 disposition as its carriers; its level axis refuses patch on a moved package under this declaration. Check Changeset is success on this head in its first run; the second run is in progress as read.
  • BREAKING banner and migration. Present, with Before / Now / Fix; the Fix is the operator's (restore or move the damaged file aside, an absent file is created empty; re-point the link or the datasource), which is all a consumer needs: no declaration, spec key, config field or stored shape moves.
  • ADR-0087 disposition. not-required (no-migration-prescription) with the other categories closed on facts. Right: objectstack migrate meta has nothing to rewrite and the body carries no consumer-code prescription. One loose clause inside the marker, prose only: "no exported declaration is ... narrowed" sits beside a narrowing of resolveSqliteAbsentFileTarget's accept set (① item 3), which the changeset's own third bullet and banner declare; the disposition does not turn on it.
  • Prose. The PR body's "What changed" says minor for both packages, matching the changeset. The changeset heading carries no conventional bang, as the family's two landed siblings did not; the banner and the Clause-② line carry the breaking-ness and no gate reads the bang. The SqliteDatabaseFileError class doc still names only runtime: a declared sqlite datasource whose file cannot be created never settles its connect, so the boot is refused only by a plugin start timeout (30 s, 120 s after PR #23694) that names neither the datasource, the path nor the open error #23696 and driver-sql: a declared SQLite file that is not a database (SQLITE_NOTADB) is accepted by connect(); on a development boot the wasm rung then renames it to FILE.corrupt-TIMESTAMP, writes an empty database in its place, and reads connected #24960 and lists codes without SQLITE_CORRUPT: doc-only residue, not a contract matter; a push to fix it would move the head.

③ Boundary flags

Every dev deviation and out-of-scope finding in 6310958104, answered or escalated; the member-3 fork is answered by the seat's ruling 6311031589.

  1. Member 3 against the ruling: agrees. The ruling binds route A to a later PR (wasm refuses, quarantineCorruptFile retires, resolveSqliteDriver re-throws a wasm file refusal, driver-sqlite-wasm and service-datasource at minor), serial after this PR because both edit sqlite-driver-fallback.ts, and orders this PR to Part of #25019. PR fix(driver-sql): a SQLite database whose header SQLite cannot read, and a dangling-link directory on the boot that writes, fail connect() naming the file #25080's first line is Part of #25019, its second paragraph states the ruling and that the merge does not close the card, the Part-of PR must not also close its card and The card this PR closes must claim this branch guards are success in both suites on this head, and the diff holds no line of driver-sqlite-wasm and no wasm re-throw in isSqliteFileOpenFailure or the wasm rung: nothing here pre-empts or contradicts what the ruling binds. The dev report's note that the PR carried Fixes is superseded by the switch.
  2. Out-of-scope finding 1 (carrier: none), a refused connect's pool stays open on the production path: ESCALATED to the seat, not a FAIL item. Read in the cited code: DatasourceConnectionService.connect catches the handle.connect() rejection, unregisters only what this attempt registered, and calls handleFailure; it never calls handle.disconnect(), and SqlDriver.connect() does not destroy its Knex pool on a refusal, whose one better-sqlite3 handle the pool keeps. For a damaged file whose header says WAL, SQLite then keeps -shm and an empty -wal beside it for the process's life (the development resolver closes its probe, pinned: the directory holds the file alone). The operator's bytes are untouched, the sidecars are SQLite's own and go at close, the same open handle predates this PR for SQLITE_CANTOPEN and SQLITE_NOTADB, and on the fail-fast path the process exits. Prime Directive 10 wants it filed: a reproducible defect with a measured mechanism and dedupe words already on the card. The carrier is the seat's to choose, domain:services (the service disconnects a handle whose connect() rejected) or domain:engine (connect() destroys its pool on a file refusal); this record does not pick.
  3. Out-of-scope finding 2, the telemetry sibling: accepted as named. The ⛔ line of the claim excludes it; the family note asked only that the PR name it, and the Acceptance notes do. No action here.
  4. Deviation 1, the dangling file link fixed rather than named out of scope: accepted. On the claim's file, within the family's governing line, measured on base as a read-only/writing-boot disagreement, pinned, ablated (leg 4) and declared in the changeset (① item 3).
  5. Deviation 2, model-free commit trailers per AGENTS.md: correct, not a deviation, as both sibling records read it; the merge commit's default message passed the pre-push trailer check.
  6. Deviations 3 and 4 (a probe process stopped by task id; the worker restart, with every reading re-taken on cc15060f7): process notes, no contract effect. The PR body and the report agree on the final head for every suite, gate and door reading.
  7. Deviation 5, test:repo and the CLI integration layer not run locally: answered by CI. Test Core and its six shards are success on this head.
  8. Own flags, none blocking. (i) The exact-string lookup misses an extended SQLITE_CORRUPT_* code; by SQLite's code none arises at a schema load, and the dev measured none, so a miss would keep today's warning and the development step-down to wasm, which is member 3's door. (ii) The words "is damaged (SQLite cannot read its header or its schema)" are loose for a schema this SQLite build cannot parse and for a foreign -wal sibling (① (a)); each refusal carries SQLite's own cause text. (iii) The SqliteDatabaseFileError class doc residue (②). (iv) Check Changeset's second run on this head is in progress as read; this record does not stand in for it.

Implemented-by: claude/issue-25019-sqlite-file-failure-family
Reviewed-by: session_01QK6pdbxKZiHdFApL8RYVh9

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

Seat review · domain:engine seat 2 · session_01QK6pdbxKZiHdFApL8RYVh9 · 2026-10-21T08:20Z · head cc15060f7 · verdict: ACCEPT (CONTRACT_REVIEW_TIER record 6311414662 PASS, adopted; the narrowing face)

  • Shape.

    • Draft PR to main from the claimed branch claude/issue-25019-sqlite-file-failure-family.
    • First line Part of #25019, set by the seat: member 3 stays on the card under the seat's ruling 6311031589. The body has no other closing keyword (scanned).
    • Part-of PR must not also close its card is success.
  • Delivered.

  • Gates on this head.

    • 48 check-runs: 39 success, 9 skipped, with all 9 skips in main's roster (check-expected-skips.mjs --pr 25080: OK).
    • Both Check Changeset runs are success, including the one after the body edit.
    • check-governed-merges.mjs --pr 25080: NOT governed, +644/−13.
    • The full dogfood battery is on this head: VERDICT command-exit 0, 285 files passed, 3 skipped.
  • Changeset, checked by the seat sentence by sentence. Every sentence holds against the code on this head:

    • the three bullets, including that the first statement is PRAGMA auto_vacuum, that resolveSqliteDriver re-throws instead of stepping down, and that damage deeper inside still connects;
    • "A real database file … keep today's behaviour";
    • Before / Now / Fix;
    • minor for both packages beside Clause-②: no (narrowing).

    "Now … The file is untouched" describes a refused connect(). The wasm rename with an addon that cannot load is member 3's, and the changeset does not claim it.

  • Accepted deviations.

    • The dangling file link was fixed on the claim's own file instead of being named out of scope. It is pinned and ablated (leg 4).
    • Readings were re-taken after the worker restart, all on cc15060f7.
  • Noted, no carrier.

    • The SqliteDatabaseFileError class doc does not list SQLITE_CORRUPT. It is package-private and not published.
    • The words "is damaged" are loose for a schema a newer SQLite wrote, and for a foreign -wal. Each refusal carries SQLite's own cause text.
    • The exact-string lookup does not see extended SQLITE_CORRUPT_* codes. None arises at a schema load.
    • The telemetry sibling's best-effort catch was named only, per the family note 6305864303.
  • Escalated by the review (③ 2), answered by the seat. After a refused connect, the production path never releases the pool: the service does not disconnect() a handle whose connect() rejected, and SqlDriver.connect() keeps its pool. This is pre-existing for SQLITE_CANTOPEN / SQLITE_NOTADB too. The seat files it as its own card for triage, naming both seams.

  • Landing.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants