Skip to content

Commit ab112ea

Browse files
refactor(driver-sql): move the NULL-safe unique pre-flight and the declared-index sync out of sql-driver.ts into sql-driver-schema-sync.ts (#25521 cut 9) (#26245)
<!-- claude-projects-attribution: {"github_login":"os-project-manager"} --> _Requested by **Leehom** · [project thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1XCodTCKnBimkFG3aNs3HsTc)_ Part of #25521 Clause-②: no Metadata-protocol: none Ruling-ref: 6270097840 Ruling-ref: 6348891160 Cut 9 of the `sql-driver.ts` split (#24586 ruling A, recorded in `6270097840`). The claim is `6420465502`. Cut 8 measured no next slice, so this cut opens with a fresh seam table (below) and takes the area it prices best that breaks no series limit. The bodies of three protected `SqlDriver` methods move unedited into the existing seam module `packages/drivers/driver-sql/src/sql-driver-schema-sync.ts`, through its existing host `SchemaSyncHost`: - `applyNullSafeUniquePreflight`, the plan-side duplicate pre-flight that clears or blocks a unique index op; - `syncDeclaredIndexes`, which materializes a table's declared indexes; - `createNullSafeUniqueIndex`, the raw DDL for an organization-scoped unique's expression key. The module-level helper `formatDuplicateGroups` moves with them, byte-identical with its docblock. Its only two callers are the pre-flight and the sync, so it stays module-local and unexported, as its docblock requires. These bodies are the rest of the ADR-0120 tightening machinery. Cut 8 already moved its apply side and put the ADR-0120 D4 anchor paragraph on this module's entry. That paragraph describes the duplicate pre-flight, which now lives in the module the paragraph anchors. No anchor entry changes, there is no `docs/adr/**` edit and no `packages/spec/**` edit, and there is no new gate. The change is move-only, and the card stays open for later cuts. Branch `claude/project-thread-k34erv` (the platform assigned it to this card thread, as the claim records). One commit, `b2b00ab42`, on `origin/main` `e6dfeb08f`. **Net line count (the seat's rule).** `sql-driver.ts` goes from 21,334 lines to 20,990: **-344**. - Out: the three bodies, 354 lines (64 + 258 + 32); `formatDuplicateGroups` with its docblock and the blank line before it, 20 lines; and the `describeMissingIndexColumns` import name, 1 line, because its last reader was the moved sync body. - In: the three stubs, 29 lines (7 + 13 + 9), and 2 import-comment lines. - The module grows from 372 to 777 lines. **Serial with the file's in-flight PRs.** I measured this twice: at start, and again before `pr_create` (41 open PRs). Each time I read every open PR's file list in full. Two touch `packages/drivers/driver-sql/**`, and neither touches `src/`: #26203 changes only `tsconfig.json` and #26198 only `package.json`. No PR touches `sql-driver.ts`, `sql-driver-schema-sync.ts`, `turso-driver.ts` or the driver-sql anchor entries. ## Seam table, fresh, on `e6dfeb08f` `origin/main` is at `e6dfeb08f`, after cut 8 (`51cc7f9603`). - **Size and churn.** `sql-driver.ts` is 21,334 lines. 30-day lines are `git blame --line-porcelain` lines whose committer time is on or after 2026-09-26T15:16:49Z, 30 days before the base commit. I deepened history to 2026-09-18 first, and `scripts/pm/git-history.mjs count` proves 68 commits on the file in that window (26 in the last 7 days). In all, 3,206 lines (15.0%) are from the last 30 days. - **Class shape.** `SqlDriver` spans lines 5532-21259 with 348 members: 252 methods (49 of them public instance methods), 13 getters and 32 statics, all 32 of them private or protected. - **Inferred return types.** One public member has one: `supports`, in the class head. - **How it was measured.** A TypeScript compiler AST script, which strips casts, parentheses and `!` before it reads `this`. - **Sections.** These are the class's own banner comments (`// ===` blocks and `// ──` sub-banners), cut by contiguous line range. | Lines | Section | First … last member | Lines | 30-day | Methods (public) | Inferred public | getBuilder sites | Statics (private/protected) | |:--|:--|:--|--:|--:|:--|--:|--:|:--| | 1-5531 | Module preamble | - | 5,531 | 754 | - | - | - | - | | 5532-7123 | Class head: fields, statics, constructor, capability getters | `name` … `assertDialectEmits` | 1,592 | 290 | 14 (1) | 1 | 0 | 18 (18) | | 7124-7555 | Lifecycle | `connect` … `disconnect` | 432 | 155 | 11 (3) | 0 | 0 | 6 (6) | | 7556-10260 | CRUD — IDataDriver core | `find` … `delete` | 2,705 | 739 | 43 (7) | 0 | 12 | 0 (0) | | 10261-10872 | Bulk & Batch Operations | `bulkCreate` … `count` | 612 | 119 | 9 (6) | 0 | 7 | 0 (0) | | 10873-10969 | Raw Execution | `rawStatementFault` … `noteRuntimeIndexDdl` | 97 | 28 | 3 (1) | 0 | 0 | 0 (0) | | 10970-11062 | Transactions | `beginTransaction` … `rollbackTransaction` | 93 | 46 | 7 (5) | 0 | 0 | 0 (0) | | 11063-11594 | Aggregation | `aggregate` … `unresolvableAggregateColumnRefusal` | 532 | 60 | 5 (1) | 0 | 1 | 0 (0) | | 11595-11882 | Distinct | `distinct` … `unresolvableDistinctColumnRefusal` | 288 | 17 | 3 (1) | 0 | 1 | 0 (0) | | 11883-11998 | Window Functions | `findWithWindowFunctions` … `findWithWindowFunctions` | 116 | 4 | 1 (1) | 0 | 1 | 0 (0) | | 11999-12097 | Query Plan Analysis | `explain` … `analyzeQuery` | 99 | 1 | 2 (2) | 0 | 1 | 0 (0) | | 12098-12156 | Schema Sync (syncSchema / init) | `syncSchema` … `reclaimSpace` | 59 | 34 | 3 (3) | 0 | 0 | 0 (0) | | 12157-13898 | Schema Sync › Data-lifecycle rotation (ADR-0057 P2) | `rotationStateByTable` … `migrateMysqlTimeColumns` | 1,742 | 72 | 26 (4) | 0 | 0 | 0 (0) | | 13899-14155 | Schema Sync › Deferred schema DDL | `setDeferredDdl` … `flushDeferredSchemaDdl` | 257 | 25 | 6 (4) | 0 | 0 | 0 (0) | | 14156-15533 | Schema Sync › Managed-schema drift & reconcile | `dialectName` … `createNullSafeUniqueIndex` | 1,378 | 197 | 23 (3) | 0 | 0 | 0 (0) | | 15534-15585 | Schema Introspection | `listPhysicalColumns` … `introspectSchema` | 52 | 46 | 2 (2) | 0 | 0 | 0 (0) | | 15586-15856 | Internal helpers (head: tenancy, misc) | `getKnex` … `auditMissingTenant` | 271 | 73 | 8 (1) | 0 | 1 | 0 (0) | | 15857-18109 | Internal helpers › Filter helpers | `tableNameForBuilder` … `applyFalseConstant` | 2,253 | 250 | 42 (2) | 0 | 0 | 0 (0) | | 18110-18285 | Internal helpers › Field mapping | `mapSortField` … `mapAggregateFunc` | 176 | 2 | 6 (0) | 0 | 0 | 0 (0) | | 18286-18333 | Internal helpers › Window function builder | `buildWindowFunction` … `buildWindowFunction` | 48 | 26 | 1 (0) | 0 | 0 | 0 (0) | | 18334-20280 | Internal helpers › Column creation helper | `nowColumnDefault` … `applyDeclaredColumnDefault` | 1,947 | 92 | 21 (0) | 0 | 0 | 8 (8) | | 20281-20578 | Internal helpers › Database helpers | `sqliteFilename` … `isJsonField` | 298 | 76 | 9 (2) | 0 | 0 | 0 (0) | | 20579-21055 | Internal helpers › SQLite serialisation | `formatInput` … `formatOutput` | 477 | 27 | 2 (0) | 0 | 0 | 0 (0) | | 21056-21259 | Internal helpers › Introspection internals | `introspectColumnOrder` … `introspectUniqueConstraints` | 204 | 73 | 5 (0) | 0 | 0 | 0 (0) | | 21260-21334 | After the class | - | 75 | 0 | - | - | - | - | ### Pricing the eligible methods A method is eligible to move when it meets every condition below. 191 of the 241 instance methods do. - Its return type is annotated, or it is not public. - It has no `getBuilder` site. - It has no `this.FIELD.delete/insert/update(` site. - It reads no private or protected static. - It makes no `super` read and no bare `this` pass. - It makes no write to `this`. - It is not `insertOnlyUpsertColumns`. **The net-line rule.** Each candidate is priced as its stub minus its body. A stub costs 4 lines, plus 2 for each field or getter read and 1 for each method read. The highest-priced candidates, and why each one stays or moves: | Method (section) | Net | Disposition | |:--|--:|:--| | `createColumn` (Column creation helper) | -463 | **Stays.** ADR-0113 cites `sql-driver.ts#createColumn` for the NOT NULL mechanism in this body, so moving it would need a `docs/adr/**` edit. Five `packages/cli` `generate-*.pin.test.ts` files also slice `protected createColumn(` … `switch (type)` out of this file's text. | | `applyFilterCondition` (Filter helpers) | -357 | **Stays.** ADR-0139 cites `sql-driver.ts#applyFilterCondition`, and a governed `.claude/**` reference names it too. `sql-driver-compile-refusal-seam.test.ts` enumerates `INVALID_FILTER` builders in this file only. | | **`syncDeclaredIndexes`** (Managed-schema drift & reconcile) | **-245** | **Moves (this cut).** | | `formatOutput` (SQLite serialisation) | -236 | **Stays.** The `sql-driver.ts` anchor entry's ADR-0053 paragraph describes this body, ADR-0104 cites `sql-driver.ts#formatOutput`, and the body reads 11 fields. | | `initObjects` (Rotation) | -195 | **Stays.** `packages/spec/liveness/object.json` cites `sql-driver.ts#initObjects`, which would need a spec edit. | | `formatInput` (SQLite serialisation) | -183 | **Stays.** ADR-0053 and ADR-0104 cite `sql-driver.ts#formatInput`. | | `applyCrossFieldComparison` (Filter helpers) | -179 | Not taken here: it needs a new module, and I did not screen it against the two readers named above. Proposed below. | | **`applyNullSafeUniquePreflight`** (Managed-schema drift & reconcile) | **-57** | **Moves (this cut).** | | **`createNullSafeUniqueIndex`** (Managed-schema drift & reconcile) | **-23** | **Moves (this cut).** | ## Why this area - **It pays the most of any eligible area.** The cut is -344, and the best eligible single method is -245. No candidate priced above it is free of a pointer that a spec or ADR edit would have to move. - **It belongs in an existing module and host.** `sql-driver-schema-sync.ts` already holds the apply side of the same mechanism (cut 8's `applyIndexDriftOp`, which calls `syncDeclaredIndexes` and `probeNullSafeUniqueDuplicates`). `SchemaSyncHost` already listed 7 of the 10 members these bodies read. - **No series limit is touched:** - none of the three bodies builds through `getBuilder`; - none reads a static; - none has a `this.FIELD.delete/insert/update(` site; - none uses `super`, a bare `this` or a cast-wrapped `this`; - all three are protected, with annotated return types. - **Subclasses.** `TursoDriver` overrides `probeNullSafeUniqueDuplicates` and calls `super`. The bodies reach it through a late-bound closure, so the override is still the one that runs. No subclass overrides any of the three moved methods. - **Path readers, screened.** No script, test, doc or ADR names any of the three by `sql-driver.ts#…` path, and none slices their text out of `sql-driver.ts`. - ADR-0005, ADR-0120, `packages/cli/src/commands/generate.ts` and `driver-turso/src/remote-transport.ts` name `SqlDriver.syncDeclaredIndexes` in prose. That method is still on the class, with the same docblock, so the prose still holds. - The only reader of `formatDuplicateGroups` outside its callers is one `{@link formatDuplicateGroups}` in the docblock of `reclaimBetterSqlite3` in `sql-driver.ts`. See Acceptance notes. - **What stays on purpose.** `probeNullSafeUniqueDuplicates` stays in the class. Its body reads the module-level `uniqueProbeNotNullClause`, so moving it would drag a second helper, and Turso's override already reaches it through the host. ## The shape (#23050 shape B, as cuts 1-8 apply it) - Each class method keeps its docblock and signature, byte-identical less `async`. Its body is now one host literal plus `NAME.call(host, ...)`, marked `[#25521 cut 9]`, and it returns the moved function's own promise. - `knex`, `logger` and `isMysql` go as GETTERS that read the driver at call time. The methods go as closures that call the method on the driver at call time. So a subclass override, an instance spy, or a `knex` or `logger` replaced after construction is still the one the body reaches (see the probe below). - Each moved function types its `this` as the Pick of `SchemaSyncHost` over exactly the members it reads. Each stub annotates its literal as the ThisParameterType of the moved function. There is no cast, and no member becomes public. - `SchemaSyncHost` gains `createHashShadowUniqueIndex`, `createNullSafeUniqueIndex` and `explainUnkeyableTextColumn`, each typed as the class member's indexed access, in the interface's alphabetical order. The other members these bodies read were already there: `knex`, `logger`, `isMysql`, `getExistingIndexNames`, `logDurabilityFailure`, `probeNullSafeUniqueDuplicates` and `resolveTenantField`. - **Module-level declarations.** - The module imports `normalizeDeclaredIndex`, `describeMissingIndexColumns`, `GLOBAL_TENANT` and the type `DeclaredIndexInput` from `schema-drift.ts`, and `isUniqueViolationError` from `@objectstack/types`, the modules that declare them. No `export` was added anywhere. - `sql-driver.ts` drops only `describeMissingIndexColumns` from its import, because the moved sync body was its last reader. Everything else it imported is still read there. - `index.ts` re-exports nothing from the module, so the published surface does not move. - **Comments.** The module header gains a `[#25521 cut 9]` paragraph that cites no decision letter. The `sql-driver.ts` import comment names cut 9 in 2 lines. - **How the move was made.** A script produced it from the source text; nothing was retyped. Each body line is re-indented two columns. No line in the moved bodies starts inside template-literal text, so no line keeps its base indentation. ## ADR anchors `check:adr-anchors` counts decision-letter citations on the anchored surface: each anchor's `invariant`, plus the full text of each anchored file. - **Both files are anchored.** `sql-driver.ts` has its ADR-0053 entry, and `sql-driver-schema-sync.ts` has the ADR-0120 entry cut 8 created. - **The citations move between anchored files.** The moved bodies carry their `ADR-0120 D3` and `ADR-0120 D4` citations from one anchored file to the other, and the new header paragraph cites no letter. - **The count.** It is **1883 decision-letter citations at base `e6dfeb08f` and at head `b2b00ab42`**, over 90 anchored files at both ends. Both runs exit 0. - **No entry needs a new home.** The only paragraph that describes a moved body is the ADR-0120 D4 sentence on the duplicate pre-flight, and it already lives on `sql-driver-schema-sync.ts`'s entry. The ADR-0053 paragraph on `sql-driver.ts` describes `formatOutput` and `presentReadValue`, and neither moved. ## The published `.d.ts` does not change This is measured under the series bar (ruling `6348891160`, item 1). - **How each end was built.** I built each end with `pnpm turbo run build --filter=@objectstack/driver-sql --force`, against the same dependency tree. The base leg restores the base text of the two source files in this worktree after the commit, under an EXIT/INT/TERM trap that restores HEAD. The restore is proved by the HEAD blob hashes (`dbe720a2`, `86929118`), an empty `git diff HEAD` and 0 status entries. - **The hashes.** The raw sha256 of `dist/index.d.ts` and `dist/index.d.mts` is the same at base and at head: `5028e040e1501b0bd72b91fafcf4e1cb372393c59baa5bab8b936b240dd2d3f5` (cut 8's hash). `cmp` finds them identical. - **Positive control.** I dropped `protected` from `syncDeclaredIndexes` in the head `index.d.ts`. The result hashes to `f6a003a07258bfdcd858e4e68504f0960aa01bd85e6e9d4ab387fb26dfa81a2c`, with a 4-line diff and `cmp` exit 1. - **Export lists.** CJS `require` and ESM `import` of the built dist give 56 / 56 names at both ends, identical. - **The head dist carries the move.** `.call(host` sites in `index.mjs` go from 21 at base to 24 at head. ## Move-only proof The proof compares TypeScript compiler AST leaf tokens (JSDoc nodes skipped), base `e6dfeb08f` against head `b2b00ab42`. It is a scratch script and is not committed. | Moved body | Body tokens | Comments | Parameters less `this` | Return type | Body lines | |:--|--:|--:|--:|--:|--:| | `applyNullSafeUniquePreflight` | 316 | 14 | 5 | 4 | 66 | | `syncDeclaredIndexes` | 806 | 118 | 29 | 4 | 260 | | `createNullSafeUniqueIndex` | 212 | 8 | 17 | 4 | 34 | - **Each moved body.** In every row, 0 tokens differ and every comment is identical. `async` is on both ends, and there are no type parameters. Each body line is the base line less two leading spaces. - **The three stubs.** Leading trivia (the docblock) and the signature are identical to base, less `async`. - **`formatDuplicateGroups`.** Its 19 lines with the docblock are byte-identical, it is gone from `sql-driver.ts`, and it is still unexported. - **The class.** It has 348 members at both ends, in the same order, and the 345 unmoved members are byte-identical, trivia included. - **`sql-driver.ts` top level.** It goes from 207 to 206 statements. The only base statements that differ are the two imports, the moved helper and the class. - **`sql-driver-schema-sync.ts`.** It goes from 7 to 12 statements. The 2 base statements that differ are the first import (which carries the module header) and `SchemaSyncHost`. The 5 new statements are the `@objectstack/types` import, the helper and the three functions. - **Result.** `PROOF OK`, exit 0. - **Negative control.** I changed `was NOT created: ` to `was NOT creat3d: ` in a module copy. The proof then reports 1 token and 1 line differing, prints `PROOF FAILED`, and exits 1. ## Tests Each suite ran under its own `os-verify-lock` hold. I compared the vitest JSON per test name and status, base `e6dfeb08f` against head `b2b00ab42`: | Suite | Base | Head | Lost / changed / added | |:--|:--|:--|:--| | `@objectstack/driver-sql` | 4010 (3796 passed, 214 skipped; 248 files) | 4010 (3796 passed, 214 skipped; 248 files) | 0 / 0 / 0 | | `@objectstack/driver-turso`, against that end's built driver-sql dist | 2526 (2492 passed, 34 skipped; 95 files) | same | 0 / 0 / 0 | | `@objectstack/driver-sqlite-wasm` | 704 (704 passed; 39 files) | same | 0 / 0 / 0 | - **Type check and lint.** `pnpm exec tsc --noEmit` in driver-sql exits 0. `pnpm exec eslint --no-inline-config` on the two touched files exits 0, with 0 problems. - **Late-binding probe.** This is a scratch script, not committed. It runs node against each end's built dist on in-memory SQLite. Its 20 readings print identical output at base and head (`diff` is empty). They cover: 1. `syncDeclaredIndexes` through a `knex` replaced on the instance after construction; 2. a missing-column skip reaching an instance spy on `logDurabilityFailure`, and an instance spy on `getExistingIndexNames`; 3. the NULL-safe route reaching a subclass override of `createNullSafeUniqueIndex`, with the emitted `COALESCE(organization_id, '__global__')` DDL; 4. a plain unique over seven duplicate groups reaching an instance spy on the probe, with the durability line `formatDuplicateGroups` renders, five groups then "…and 2 more"; 5. a NULL-safe unique over duplicates logged while the sync goes on to the next index; 6. `createNullSafeUniqueIndex` through a subclass `isMysql` getter, on its functional-key-part fallback to the bare composite; 7. `applyNullSafeUniquePreflight` on a dirty create (blocked, with `applyLimit` and message), a clean tightening (`safe`), and a probe failure warned through a `logger` replaced after construction; 8. the same pre-flight through an instance spy on the probe; 9. a bad argument to each moved method answering a `TypeError` rejection, never a synchronous throw. - **Not run.** No ablation, because no tested behaviour changed. `test:repo` was not run locally: the diff touches no repo-project test, and CI's Test Core runs it. ## Gates All local runs are on head `b2b00ab42`. - **Derived gates.** `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 55 commands from the change set against merge base `e6dfeb08f` (2 paths). I captured each exit code before any pipe. - 52 exit 0 on the first run. - `check-plugin-teardown-shape.mjs --self-test` exited 3 on the shallow clone, then 0 after `git fetch origin 621a487` (its positive control). - `check:lean-entry-closure` exited 3 with no objectql dist, then 0 after `pnpm turbo run build --filter=@objectstack/objectql` under the lock. - `check:dual-build-cjs-loads` exited 3, PREREQUISITE NOT MET: it needs every package's dist. It is NOT MEASURED locally, and CI runs it. - **Reconciliation.** `--ran` reports "55 derived famil(ies) accounted for — 54 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)", exit 0. - **Card-named gates.** I read each at base in a clean detached worktree at `e6dfeb08f` and at head `b2b00ab42`. All exit 0 at both ends. | Gate | Base | Head | Note | |:--|:--|:--|:--| | `check:adr-anchors` | 90 anchored files, 1883 citations | same | | | `check:adr-symbol-anchors` | 2664 anchors resolve | same | | | `check:tenant-chokepoint` | 24 `getBuilder()` bindings across 3 files; rule 2 clean | same | See the first acceptance note. | | `check:logger-receiver-detach` | 0 detaches, 3736 non-test TS files | 0 detaches, 3737 files | The head has one more walked file; the base worktree lacks the gitignored generated `packages/spec/src/migrations/registry.ts`. | | `check:driver-conformance` | 55 covered, 0 DEBT, 0 exempt | same | | | `check:keyed-text-bounds` | 112 object files, 238 index entries | same | | | `check-tenant-audit-census` | 243 write call sites certified | same | | - **Not owed.** `check-platform-checklist --changed-from` is not owed: no exported symbol was renamed or deleted, because `formatDuplicateGroups` was never exported. Pre-push `check:commit-card-trailers` and `check:nul-bytes` pass. - **`origin/main` moved while this ran.** It moved 10 commits, to `df8aac41f`. None of them touches `packages/drivers/**`, `scripts/adr-anchors/**`, `check-adr-anchors.mjs` or `check-tenant-chokepoint.mjs`. Two touch `packages/types/src`, which driver-sql imports `isUniqueViolationError` from, so CI's merge-ref run covers that. `git merge-tree` against it is clean. I did not merge, because CI tests the merge ref. ## Acceptance notes - **`check:tenant-chokepoint` rule 2 visits a little less.** Rule 2 (the D8 null-arm rule) scans class members of the three driver files, so it no longer visits what moved: - one `knex.raw(` site, `createNullSafeUniqueIndex`'s `CREATE UNIQUE INDEX` with its `COALESCE(... '__global__')` key part (`sql-driver.ts` goes from 57 to 56 `.raw(` sites); - `syncDeclaredIndexes`' `tenantField` parameter and its `resolveTenantField` binding. None of these says `IS NULL` against a tenant column, and none is a `where` arm. The verdict is unchanged, and the gate is not empty (24 bindings, 3 files). This is the same disclosure cuts 5-7 made, and the card already carries the `SCAN_FILES` precondition. - **A docblock link no longer resolves in its file.** The docblock of `reclaimBetterSqlite3` in `sql-driver.ts` says "Module-local, like {@link formatDuplicateGroups}", and that helper now lives in `sql-driver-schema-sync.ts`. The sentence is still true, because the helper is still module-local. I left the docblock unedited, since the cut is move-only. - **Proposed next slice: `applyCrossFieldComparison`** (Filter helpers, about -179, reads 6 methods and no field). It would need a new seam module. Before pricing it at dispatch, screen it against `sql-driver-compile-refusal-seam.test.ts` (whether it builds `INVALID_FILTER`) and against `check:tenant-chokepoint` rule 2. `skip-changeset`: the change is move-only, so nothing a published package ships changes. The `.d.ts` is byte-identical, and the emitted JS runs the same bodies through the host. --- _Generated by [Claude Code](https://claude.ai/code/session_01EcqJz48p6ineaiedw6SCaX)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 7e4af20 commit ab112ea

2 files changed

Lines changed: 441 additions & 380 deletions

File tree

0 commit comments

Comments
 (0)