From ccffa9476fbb0411854503da66ed48bff578870c Mon Sep 17 00:00:00 2001 From: earthlingdavey <15802017+earthlingdavey@users.noreply.github.com> Date: Thu, 5 Mar 2026 17:08:08 +0000 Subject: [PATCH 1/3] feat: export kit driver as injectable string for drizzle-kit - Move kit-only functions (prepareSqliteParams, normaliseSQLiteUrl) to kit.ts - Simplify API: createNodeSqlDriver(dbPath, prepareSqliteParams) - Add build script to bundle kit.ts as injectable string (dist/kit-string.ts) - Add prepublish task to generate kit-string at publish time - Configure CI workflow to run build:kit-string before publish --- .github/workflows/publish.yml | 3 ++ REVIEW.md | 92 +++++++++++++++++++++++++++++++++++ deno.jsonc | 17 ++++--- deno.lock | 51 +++++++++++++++++++ scripts/build-kit-string.ts | 49 +++++++++++++++++++ src/kit.ts | 28 ++++++----- src/shared.ts | 52 -------------------- 7 files changed, 220 insertions(+), 72 deletions(-) create mode 100644 REVIEW.md create mode 100644 scripts/build-kit-string.ts diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 716ae5f..411a1e2 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -44,5 +44,8 @@ jobs: - name: Restore lockfile (tests may modify it due to Deno version differences) run: git checkout deno.lock + - name: Build generated files + run: deno task build:kit-string + - name: Publish to JSR run: deno publish diff --git a/REVIEW.md b/REVIEW.md new file mode 100644 index 0000000..7668834 --- /dev/null +++ b/REVIEW.md @@ -0,0 +1,92 @@ +# Peer Review (re-review) + +Date: 2026-03-05 + +## Scope (current working tree changes) +Based on `git status --porcelain`: +- Modified: + - .github/workflows/publish.yml + - deno.jsonc + - deno.lock + - src/kit.ts + - src/shared.ts +- New (untracked): + - scripts/build-kit-string.ts + +Also present locally: +- Generated output: dist/kit-string.ts (currently ignored by git via .gitignore) + +## High-level summary +- Switches the “kit-string” export to point at dist/kit-string.ts and adds a build step (`deno task build:kit-string`) to generate it. +- Adds CI/publish workflow step to generate the kit-string artifact before `deno publish`. +- Refines shared `node:sqlite` array-mode logic with explicit feature detection for `columns()` and a clearer runtime error. +- Refactors the kit driver to an injectable API shape: `createNodeSqlDriver(dbPath, prepareSqliteParams)`. + +## What looks good +- **Publish pipeline awareness**: adding a build step in .github/workflows/publish.yml reduces the chance of publishing with missing generated files. +- **Cleaner injection story**: moving kit-string output to dist/ and exporting `./kit-string` from there makes it clear it’s a build artifact. +- **Better runtime guards**: `hasColumns()` + explicit error in src/shared.ts is an improvement over “hope columns exists”. +- **More explicit proxy semantics**: src/kit.ts now documents how it treats `method: 'execute'` and why `get` returns arrays (drizzle-kit expectation). + +## Concerns / questions + +### 1) dist/kit-string.ts is required by exports but is gitignored +- .gitignore contains `dist/`, so dist/kit-string.ts won’t appear in git diffs/status and won’t be present for anyone consuming directly from the repo (or switching branches) unless they run the build. +- deno.jsonc exports `"./kit-string": "./dist/kit-string.ts"`, so consumers/imports will break if the file isn’t generated. + +Mitigations you already added: +- `deno task prepublish` (task alias) +- publish workflow step that runs `deno task build:kit-string` + +**Remaining risk:** Deno doesn’t automatically run a `prepublish` task as part of `deno publish`; it must be invoked explicitly (as you do in CI). + +**Suggestion:** document in README “run `deno task build:kit-string` before `deno publish` locally” (or consider committing dist artifacts, if repo-consumption matters). + +### 2) Bundler brittleness: good new guard, still version-sensitive +scripts/build-kit-string.ts now fails fast if `export { ... }` wasn’t stripped from the bundle output. That’s a solid improvement. + +But the build still depends on an unstable Deno bundling API and output shape. + +**Suggestion:** consider pinning a Deno version for publishing (or at least documenting the minimum version needed) to reduce “works on CI, fails locally” cases. + +### 3) deno.lock churn +The `deno.lock` update is large due to adding bundling dependencies (JSR std + emit/cache-dir). Expected, but it does increase review noise. + +You already mitigate CI drift by restoring `deno.lock` after tests in publish.yml. + +**Question:** should build:kit-string be run before tests, or should its lock impacts also be normalized/controlled? (Maybe it’s fine as-is.) + +### 4) Remaining semantic footgun: sqlite-proxy `get` and `rows ?? []` +The internal sqlite-proxy drizzle wrapper in src/kit.ts still coerces `rows: result.rows ?? []`. +- This is probably OK since it’s “migration-only”. +- If that internal `drzl` is ever reused for `.get()`, a missing row would be coerced from `undefined` to `[]`. + +This is mostly a documentation/encapsulation concern at this point. + +### 5) Proxy fallback conversion path doesn’t guard `columns()` +In src/shared.ts, fallback conversion checks `hasColumns()` before calling `columns()`. + +In the drizzle-kit proxy path (inside src/kit.ts, and reflected in dist/kit-string.ts), the fallback conversion for `mode: 'array'` does: +- `const columns = asExtended(stmt).columns();` (no guard) + +If `setReturnArrays` is missing and `columns()` is also missing, it will throw a less-informative error. + +**Suggestion:** mirror the shared guard or reuse `hasColumns()` in the proxy fallback path too (even if only to throw the same descriptive error). + +## Notes on version bump +deno.jsonc version is now 0.1.2. publish.yml verifies tags against deno.jsonc, which is a good check. + +## Generated output sanity +dist/kit-string.ts: +- includes `// deno-lint-ignore-file` +- exports `drizzleKitDriverBlock: string = `...`` + +That’s consistent with excluding dist/ from lint/fmt and with the “injection block” purpose. + +## Overall +This iteration is materially better: +- clear separation between source and generated artifacts +- CI/publish path generates required output +- improved runtime capability checks + +The main decision to confirm is whether “kit-string is only for published JSR consumers” (current design) vs “kit-string should work from a fresh git checkout” (would require committing dist/ or generating in post-checkout tooling). diff --git a/deno.jsonc b/deno.jsonc index 8200a40..fec0d47 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -1,9 +1,10 @@ { "name": "@hotsauce/drizzle-runtime-sqlite", - "version": "0.1.1", + "version": "0.1.2", "exports": { ".": "./mod.ts", - "./kit": "./src/kit.ts" + "./kit": "./src/kit.ts", + "./kit-string": "./dist/kit-string.ts" }, "imports": { "drizzle-orm": "npm:drizzle-orm@0.45.1", @@ -16,17 +17,19 @@ }, "tasks": { "test": "deno run -A npm:vitest run --config vitest.config.deno.ts", - "lint": "deno lint", - "fmt": "deno fmt", - "check": "deno fmt --check && deno lint" + "lint": "deno lint --ignore=dist/ --ignore=repos/", + "fmt": "deno fmt --ignore=dist/ --ignore=repos/", + "check": "deno fmt --check && deno lint", + "build:kit-string": "deno run --allow-read=src --allow-write=dist --unstable-bundle scripts/build-kit-string.ts", + "prepublish": "deno task build:kit-string" }, "lint": { "include": ["src/", "tests/", "mod.ts"], - "exclude": ["repos/"] + "exclude": ["repos/", "dist/"] }, "fmt": { "include": ["src/", "tests/", "mod.ts"], - "exclude": ["repos/"] + "exclude": ["repos/", "dist/"] }, "compilerOptions": { "strict": true, diff --git a/deno.lock b/deno.lock index 25a5e79..3903d13 100644 --- a/deno.lock +++ b/deno.lock @@ -1,10 +1,61 @@ { "version": "5", "specifiers": { + "jsr:@deno/cache-dir@0.13.2": "0.13.2", + "jsr:@deno/emit@*": "0.46.0", + "jsr:@std/assert@0.223": "0.223.0", + "jsr:@std/bytes@0.223": "0.223.0", + "jsr:@std/fmt@0.223": "0.223.0", + "jsr:@std/fs@0.223": "0.223.0", + "jsr:@std/io@0.223": "0.223.0", + "jsr:@std/path@0.223": "0.223.0", "npm:drizzle-orm@0.45.1": "0.45.1", "npm:typescript@^5.4.0": "5.9.3", "npm:vitest@^1.6.0": "1.6.1" }, + "jsr": { + "@deno/cache-dir@0.13.2": { + "integrity": "c22419dfe27ab85f345bee487aaaadba498b005cce3644e9d2528db035c5454d", + "dependencies": [ + "jsr:@std/fmt", + "jsr:@std/fs", + "jsr:@std/io", + "jsr:@std/path" + ] + }, + "@deno/emit@0.46.0": { + "integrity": "e276be2c77bac1b93caf775762e2a49a54cb00da2d48ca2b01ed8d7cba9d082c", + "dependencies": [ + "jsr:@deno/cache-dir", + "jsr:@std/path" + ] + }, + "@std/assert@0.223.0": { + "integrity": "eb8d6d879d76e1cc431205bd346ed4d88dc051c6366365b1af47034b0670be24" + }, + "@std/bytes@0.223.0": { + "integrity": "84b75052cd8680942c397c2631318772b295019098f40aac5c36cead4cba51a8" + }, + "@std/fmt@0.223.0": { + "integrity": "6deb37794127dfc7d7bded2586b9fc6f5d50e62a8134846608baf71ffc1a5208" + }, + "@std/fs@0.223.0": { + "integrity": "3b4b0550b2c524cbaaa5a9170c90e96cbb7354e837ad1bdaf15fc9df1ae9c31c" + }, + "@std/io@0.223.0": { + "integrity": "2d8c3c2ab3a515619b90da2c6ff5ea7b75a94383259ef4d02116b228393f84f1", + "dependencies": [ + "jsr:@std/assert", + "jsr:@std/bytes" + ] + }, + "@std/path@0.223.0": { + "integrity": "593963402d7e6597f5a6e620931661053572c982fc014000459edc1f93cc3989", + "dependencies": [ + "jsr:@std/assert" + ] + } + }, "npm": { "@esbuild/aix-ppc64@0.21.5": { "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", diff --git a/scripts/build-kit-string.ts b/scripts/build-kit-string.ts new file mode 100644 index 0000000..914536e --- /dev/null +++ b/scripts/build-kit-string.ts @@ -0,0 +1,49 @@ +/** + * Build script to generate kit-string.ts from the actual kit.ts source. + * + * Usage: deno run --allow-read=src --allow-write=dist scripts/build-kit-string.ts + */ + +const result = await Deno.bundle({ + entrypoints: ["./src/kit.ts"], + write: false, + minify: false, + external: ["drizzle-orm", "drizzle-orm/*"], +}); + +const bundledCode = result.outputFiles![0].text(); + +// Remove the export statement at the end +const codeWithoutExport = bundledCode.replace(/\nexport\s*\{[^}]*\};\s*$/, "\n"); + +// Validate the export was stripped (fail fast if bundler output format changes) +if (codeWithoutExport.includes("export {")) { + console.error("ERROR: Failed to strip export statement from bundle. Bundler output format may have changed."); + Deno.exit(1); +} + +// Escape backticks and ${} for template literal +const escaped = codeWithoutExport + .replace(/\\/g, "\\\\") + .replace(/`/g, "\\`") + .replace(/\$\{/g, "\\${"); + +// Generate .ts file with explicit type annotation +const tsOutput = `// deno-lint-ignore-file +/** + * Drizzle Kit driver for node:sqlite as an injectable string. + * + * AUTO-GENERATED - DO NOT EDIT + * Run: deno task build:kit-string + * + * This exports the driver code as a string that can be injected into + * drizzle-kit's connections.ts at build time. + */ + +export const drizzleKitDriverBlock: string = \` +${escaped}\`; +`; + +await Deno.mkdir("dist", { recursive: true }); +await Deno.writeTextFile("dist/kit-string.ts", tsOutput); +console.log("Generated dist/kit-string.ts"); diff --git a/src/kit.ts b/src/kit.ts index 34ebd5c..d257684 100644 --- a/src/kit.ts +++ b/src/kit.ts @@ -4,14 +4,11 @@ * Provides a drop-in replacement for better-sqlite3 in drizzle-kit's connections. */ -import { DatabaseSync } from "node:sqlite"; import type { MigrationConfig } from "drizzle-orm/migrator"; import { asExtended, executeWithArrayMode, hasArrayMode, - normaliseSQLiteUrl, - prepareSqliteParams, rowToArray, } from "./shared.ts"; @@ -25,6 +22,13 @@ export interface SQLiteDB { * Proxy params matching drizzle-kit's ProxyParams. * This is a practical subset - drizzle-kit's full type has mode/method required, * but SQLite proxy paths don't always provide them. + * + * Method behaviors: + * - `run`: executes statement, returns [] + * - `all`: returns all rows as array + * - `get`: returns all rows (drizzle-kit expects array, not single row) + * - `values`: same as `all` + * - `execute`: treated as `run` (executes statement, returns []) */ export interface ProxyParams { sql: string; @@ -47,11 +51,6 @@ export type TransactionProxy = ( queries: TransactionQuery[], ) => Promise<(unknown[] | Error)[]>; -/** Credentials for SQLite connection */ -export interface SqliteCredentials { - url: string; -} - /** Return type of createDrizzleKitDriver */ export interface DrizzleKitDriver extends SQLiteDB { packageName: "node:sqlite"; @@ -66,24 +65,27 @@ export interface DrizzleKitDriver extends SQLiteDB { * This provides the same interface as drizzle-kit's better-sqlite3 driver, * allowing it to be used as a drop-in replacement. * - * @param credentials - SQLite credentials with url property + * @param dbPath - Path to the SQLite database file (already normalised) + * @param prepareSqliteParams - Function to prepare params (from drizzle-kit) * @returns Driver object compatible with drizzle-kit * * @example * ```ts * import { createDrizzleKitDriver } from "@hotsauce/drizzle-runtime-sqlite/kit"; * - * const driver = await createDrizzleKitDriver({ url: "./database.db" }); + * const driver = await createDrizzleKitDriver("./database.db", prepareSqliteParams); * // Use with drizzle-kit internals * ``` */ -export async function createDrizzleKitDriver( - credentials: SqliteCredentials, +export async function createNodeSqlDriver( + dbPath: string, + prepareSqliteParams: (params: unknown[]) => unknown[], ): Promise { + const { DatabaseSync } = await import("node:sqlite"); const { drizzle } = await import("drizzle-orm/sqlite-proxy"); const { migrate } = await import("drizzle-orm/sqlite-proxy/migrator"); - const sqlite = new DatabaseSync(normaliseSQLiteUrl(credentials.url)); + const sqlite = new DatabaseSync(dbPath); /** * Callback for sqlite-proxy following its expected semantics. diff --git a/src/shared.ts b/src/shared.ts index 8e2a1d7..e025368 100644 --- a/src/shared.ts +++ b/src/shared.ts @@ -125,55 +125,3 @@ export function executeWithArrayMode( throw new Error(`Unknown method: ${method}`); } } - -/** - * Prepare SQLite params, converting binary objects to byte arrays. - * Matches drizzle-kit's prepareSqliteParams behavior. - */ -export function prepareSqliteParams(params: unknown[]): unknown[] { - return params.map((param) => { - if ( - param && - typeof param === "object" && - "type" in param && - "value" in param && - (param as { type: string }).type === "binary" - ) { - const value = typeof (param as { value: unknown }).value === "object" - ? JSON.stringify((param as { value: unknown }).value) - : ((param as { value: unknown }).value as string); - // Use TextEncoder for cross-runtime compatibility (works in Node, Deno, browsers) - return new TextEncoder().encode(value); - } - return param; - }); -} - -/** - * Normalise SQLite URL for node:sqlite. - * Handles file: URLs and plain paths. - * - * - `file:./relative.db` -> `./relative.db` - * - `file:///absolute/path.db` -> `/absolute/path.db` - * - `file://localhost/path.db` -> `/path.db` - * - `./relative.db` -> `./relative.db` (unchanged) - */ -export function normaliseSQLiteUrl(url: string): string { - if (!url.startsWith("file:")) { - return url; - } - - // Handle file:// URLs (with authority) - if (url.startsWith("file://")) { - try { - const parsed = new URL(url); - return parsed.pathname; - } catch { - // If URL parsing fails, strip prefix and return - return url.slice(7); - } - } - - // Handle file: prefix without // (e.g., file:./db.sqlite) - return url.slice(5); -} From 3a152052a6cb077c19dbefcc83a2234cd329ebd5 Mon Sep 17 00:00:00 2001 From: earthlingdavey <15802017+earthlingdavey@users.noreply.github.com> Date: Thu, 5 Mar 2026 17:09:33 +0000 Subject: [PATCH 2/3] fix: address review feedback - Add hasColumns guard in proxy fallback path (consistent error handling) - Clarify migration-only usage of internal drizzle instance - Document Deno 2.x requirement for --unstable-bundle in README - Add local publishing instructions to README --- README.md | 16 ++++++++++++++++ src/kit.ts | 11 +++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8ce4db8..907e377 100644 --- a/README.md +++ b/README.md @@ -150,6 +150,7 @@ This matches the sqlite-proxy specification. For most use cases, prefer the quer ### Prerequisites +- Deno 2.x (with `--unstable-bundle` for building kit-string) - Docker and Docker Compose - Make @@ -161,6 +162,21 @@ git config core.hooksPath .githooks && \ chmod +x .githooks/pre-commit ``` +### Building Generated Files + +The `./kit-string` export is generated from `src/kit.ts` and must be built before publishing: + +```bash +# Generate dist/kit-string.ts (required before deno publish) +deno task build:kit-string +``` + +CI runs this automatically before `deno publish`. For local publishing: + +```bash +deno task build:kit-string && deno publish +``` + ### Running Tests Tests run in Docker containers to ensure consistent environments. diff --git a/src/kit.ts b/src/kit.ts index d257684..0f2dac1 100644 --- a/src/kit.ts +++ b/src/kit.ts @@ -9,6 +9,7 @@ import { asExtended, executeWithArrayMode, hasArrayMode, + hasColumns, rowToArray, } from "./shared.ts"; @@ -102,9 +103,10 @@ export async function createNodeSqlDriver( return executeWithArrayMode(stmt, params, method); }; - // Create drizzle instance for migrations (internal use only) + // Create drizzle instance for migrations (internal use only). + // This instance is NOT exposed for general queries - only migrate() uses it. // Note: drizzle's AsyncRemoteCallback expects { rows: any[] }, but sqlite-proxy - // semantics return undefined for get() with no row. We coerce here since + // semantics return undefined for get() with no row. We coerce to [] since // migrations only use "run" method which always returns { rows: [] }. const drzl = drizzle((sql, params, method) => { const result = sqliteProxyCallback(sql, params as unknown[], method); @@ -157,6 +159,11 @@ export async function createNodeSqlDriver( if (params.mode === "array" && !useArrayMode && rows.length > 0) { // Fallback: convert objects to arrays using column metadata + if (!hasColumns(stmt)) { + throw new Error( + "node:sqlite: stmt.columns() not available. Upgrade to Deno 2.6+ or Node.js 22.5+.", + ); + } const columns = asExtended(stmt).columns(); return Promise.resolve( (rows as Record[]).map((row) => From e2c074d554e08fd7392780b103b221b685f7a16c Mon Sep 17 00:00:00 2001 From: earthlingdavey <15802017+earthlingdavey@users.noreply.github.com> Date: Thu, 5 Mar 2026 17:09:49 +0000 Subject: [PATCH 3/3] Delete REVIEW.md --- REVIEW.md | 92 ------------------------------------------------------- 1 file changed, 92 deletions(-) delete mode 100644 REVIEW.md diff --git a/REVIEW.md b/REVIEW.md deleted file mode 100644 index 7668834..0000000 --- a/REVIEW.md +++ /dev/null @@ -1,92 +0,0 @@ -# Peer Review (re-review) - -Date: 2026-03-05 - -## Scope (current working tree changes) -Based on `git status --porcelain`: -- Modified: - - .github/workflows/publish.yml - - deno.jsonc - - deno.lock - - src/kit.ts - - src/shared.ts -- New (untracked): - - scripts/build-kit-string.ts - -Also present locally: -- Generated output: dist/kit-string.ts (currently ignored by git via .gitignore) - -## High-level summary -- Switches the “kit-string” export to point at dist/kit-string.ts and adds a build step (`deno task build:kit-string`) to generate it. -- Adds CI/publish workflow step to generate the kit-string artifact before `deno publish`. -- Refines shared `node:sqlite` array-mode logic with explicit feature detection for `columns()` and a clearer runtime error. -- Refactors the kit driver to an injectable API shape: `createNodeSqlDriver(dbPath, prepareSqliteParams)`. - -## What looks good -- **Publish pipeline awareness**: adding a build step in .github/workflows/publish.yml reduces the chance of publishing with missing generated files. -- **Cleaner injection story**: moving kit-string output to dist/ and exporting `./kit-string` from there makes it clear it’s a build artifact. -- **Better runtime guards**: `hasColumns()` + explicit error in src/shared.ts is an improvement over “hope columns exists”. -- **More explicit proxy semantics**: src/kit.ts now documents how it treats `method: 'execute'` and why `get` returns arrays (drizzle-kit expectation). - -## Concerns / questions - -### 1) dist/kit-string.ts is required by exports but is gitignored -- .gitignore contains `dist/`, so dist/kit-string.ts won’t appear in git diffs/status and won’t be present for anyone consuming directly from the repo (or switching branches) unless they run the build. -- deno.jsonc exports `"./kit-string": "./dist/kit-string.ts"`, so consumers/imports will break if the file isn’t generated. - -Mitigations you already added: -- `deno task prepublish` (task alias) -- publish workflow step that runs `deno task build:kit-string` - -**Remaining risk:** Deno doesn’t automatically run a `prepublish` task as part of `deno publish`; it must be invoked explicitly (as you do in CI). - -**Suggestion:** document in README “run `deno task build:kit-string` before `deno publish` locally” (or consider committing dist artifacts, if repo-consumption matters). - -### 2) Bundler brittleness: good new guard, still version-sensitive -scripts/build-kit-string.ts now fails fast if `export { ... }` wasn’t stripped from the bundle output. That’s a solid improvement. - -But the build still depends on an unstable Deno bundling API and output shape. - -**Suggestion:** consider pinning a Deno version for publishing (or at least documenting the minimum version needed) to reduce “works on CI, fails locally” cases. - -### 3) deno.lock churn -The `deno.lock` update is large due to adding bundling dependencies (JSR std + emit/cache-dir). Expected, but it does increase review noise. - -You already mitigate CI drift by restoring `deno.lock` after tests in publish.yml. - -**Question:** should build:kit-string be run before tests, or should its lock impacts also be normalized/controlled? (Maybe it’s fine as-is.) - -### 4) Remaining semantic footgun: sqlite-proxy `get` and `rows ?? []` -The internal sqlite-proxy drizzle wrapper in src/kit.ts still coerces `rows: result.rows ?? []`. -- This is probably OK since it’s “migration-only”. -- If that internal `drzl` is ever reused for `.get()`, a missing row would be coerced from `undefined` to `[]`. - -This is mostly a documentation/encapsulation concern at this point. - -### 5) Proxy fallback conversion path doesn’t guard `columns()` -In src/shared.ts, fallback conversion checks `hasColumns()` before calling `columns()`. - -In the drizzle-kit proxy path (inside src/kit.ts, and reflected in dist/kit-string.ts), the fallback conversion for `mode: 'array'` does: -- `const columns = asExtended(stmt).columns();` (no guard) - -If `setReturnArrays` is missing and `columns()` is also missing, it will throw a less-informative error. - -**Suggestion:** mirror the shared guard or reuse `hasColumns()` in the proxy fallback path too (even if only to throw the same descriptive error). - -## Notes on version bump -deno.jsonc version is now 0.1.2. publish.yml verifies tags against deno.jsonc, which is a good check. - -## Generated output sanity -dist/kit-string.ts: -- includes `// deno-lint-ignore-file` -- exports `drizzleKitDriverBlock: string = `...`` - -That’s consistent with excluding dist/ from lint/fmt and with the “injection block” purpose. - -## Overall -This iteration is materially better: -- clear separation between source and generated artifacts -- CI/publish path generates required output -- improved runtime capability checks - -The main decision to confirm is whether “kit-string is only for published JSR consumers” (current design) vs “kit-string should work from a fresh git checkout” (would require committing dist/ or generating in post-checkout tooling).