From 15b246b54634f8aff369ea280e6fe436c432b69f Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Sun, 12 Jul 2026 19:28:18 +1000 Subject: [PATCH 1/5] chore(lint): warn on type-erasing assertions via a Biome GritQL plugin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds `biome-plugins/no-type-erasing-assertions.grit`, wired into biome.json, that warns on `as any` / `as never` / `as unknown` in source — assertions that punch through the type system instead of narrowing (a real mismatch can hide behind them). `as const`, plain `as SpecificType`, and `never` as a type annotation are NOT flagged. `as unknown` also catches the inner half of an `as unknown as T` double-cast. - severity `warn`: Biome is not a CI gate here (only local `code:fix`), and there's a small existing backlog (~17 in src) — this is a ratchet to keep NEW erasures visible, not a hard block. Warnings don't change `biome check`'s exit code (verified). - test/integration/*.test-d.ts files are exempt via an `overrides` entry — test doubles legitimately force-cast; suppress a deliberate src case with `// biome-ignore lint/plugin: `. - Known limitation (documented in the plugin): matches only the bare forms, not compound casts like `as never[]`. Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w --- AGENTS.md | 6 +++++ biome-plugins/no-type-erasing-assertions.grit | 26 +++++++++++++++++++ biome.json | 16 +++++++++++- 3 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 biome-plugins/no-type-erasing-assertions.grit diff --git a/AGENTS.md b/AGENTS.md index d763293cf..e55a38776 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -177,6 +177,12 @@ Three rules to remember when editing CI or pnpm config: pnpm run code:fix ``` + A Biome GritQL plugin (`biome-plugins/no-type-erasing-assertions.grit`) warns + on `as any` / `as never` / `as unknown` in `src` — type-erasing assertions that + silence the checker instead of narrowing. Fix the type or use a specific + assertion; suppress a deliberate case with `// biome-ignore lint/plugin: + `. Test/integration files are exempt (see `overrides` in `biome.json`). + - **Build**: `pnpm run build` (Turborepo + tsup per package) - **Test**: `pnpm --filter test` for targeted iterations - **Releases**: Use Changesets diff --git a/biome-plugins/no-type-erasing-assertions.grit b/biome-plugins/no-type-erasing-assertions.grit new file mode 100644 index 000000000..cfe7deadb --- /dev/null +++ b/biome-plugins/no-type-erasing-assertions.grit @@ -0,0 +1,26 @@ +// Flags type-erasing assertions: `x as any`, `x as never`, `x as unknown` +// (the last also catches the inner half of `x as unknown as T` double-casts). +// +// These punch a hole through the type system rather than narrowing a value, so +// a real type mismatch can hide behind them (e.g. passing the wrong shape to a +// function whose signature you've silenced). Prefer fixing the underlying type, +// narrowing with a guard, or a precise `as SpecificType`. +// +// NOT flagged: `as const` (a widening-freeze, not an erasure), a plain +// `as SpecificType`, or `never` used as a *type annotation* (e.g. a contravariant +// `param: never`) — those are legitimate. +// +// Warning, not error: the repo has a large existing backlog, and this is a +// ratchet to keep NEW erasures visible. Intentional cases take a +// `// biome-ignore lint/plugin: ` suppression. +or { + `$e as any`, + `$e as never`, + `$e as unknown` +} where { + register_diagnostic( + span = $e, + message = "Avoid type-erasing assertions (`as any` / `as never` / `as unknown`) — they silence the checker instead of narrowing. Fix the underlying type, use a type guard, or assert a specific type. Suppress an intentional case with `// biome-ignore lint/plugin: `.", + severity = "warn" + ) +} diff --git a/biome.json b/biome.json index ca315148c..a43bd45c1 100644 --- a/biome.json +++ b/biome.json @@ -30,5 +30,19 @@ "noThenProperty": "off" } } - } + }, + "plugins": ["./biome-plugins/no-type-erasing-assertions.grit"], + "overrides": [ + { + "includes": [ + "**/__tests__/**", + "**/*.test.ts", + "**/*.test-d.ts", + "**/integration/**", + "**/tests/**", + "**/*.spec.ts" + ], + "plugins": [] + } + ] } From a9fe88185ffdefe3391398539c0640241d3f6912 Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Sun, 12 Jul 2026 19:41:31 +1000 Subject: [PATCH 2/5] fix(lint): scope the assertion plugin via an include-override, not global+exempt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The original wiring (top-level `plugins` + an `overrides` entry with `plugins: []` for tests) did NOT exempt tests: Biome runs top-level plugins on every file and `overrides[].plugins: []` does not disable them — verified 158 test-file hits leaking through. Enable the plugin only via an `overrides` entry whose `includes` matches source and negates the test globs instead. Now: 82 source hits, 0 test-path leaks (verified). Also ignore `**/*.grit` in files.includes so Biome stops trying to format the plugin file itself. Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w --- biome-plugins/no-type-erasing-assertions.grit | 7 +++++++ biome.json | 20 ++++++++++--------- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/biome-plugins/no-type-erasing-assertions.grit b/biome-plugins/no-type-erasing-assertions.grit index cfe7deadb..2aafe8a83 100644 --- a/biome-plugins/no-type-erasing-assertions.grit +++ b/biome-plugins/no-type-erasing-assertions.grit @@ -13,6 +13,13 @@ // Warning, not error: the repo has a large existing backlog, and this is a // ratchet to keep NEW erasures visible. Intentional cases take a // `// biome-ignore lint/plugin: ` suppression. +// +// Wiring note: this plugin is enabled via an `overrides` entry in biome.json +// that INCLUDES source globs and negates test globs — it is NOT enabled at the +// top level. Biome applies top-level `plugins` to every file, and +// `overrides[].plugins: []` does NOT disable them, so "enable globally + exempt +// tests" silently lints tests too. Scoping the plugin in via the override is the +// only way to exempt test doubles (which legitimately force-cast). or { `$e as any`, `$e as never`, diff --git a/biome.json b/biome.json index a43bd45c1..8f90da791 100644 --- a/biome.json +++ b/biome.json @@ -6,7 +6,8 @@ "!**/package.json", "!**/.turbo/", "!**/dist", - "!**/nix/store" + "!**/nix/store", + "!**/*.grit" ] }, "formatter": { @@ -31,18 +32,19 @@ } } }, - "plugins": ["./biome-plugins/no-type-erasing-assertions.grit"], "overrides": [ { "includes": [ - "**/__tests__/**", - "**/*.test.ts", - "**/*.test-d.ts", - "**/integration/**", - "**/tests/**", - "**/*.spec.ts" + "**/src/**", + "examples/**", + "!**/__tests__/**", + "!**/*.test.ts", + "!**/*.test-d.ts", + "!**/*.spec.ts", + "!**/integration/**", + "!**/tests/**" ], - "plugins": [] + "plugins": ["./biome-plugins/no-type-erasing-assertions.grit"] } ] } From 5a8e8d8fc1849b04ff9a63ce1c6280d8cedfc326 Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Sun, 12 Jul 2026 19:49:43 +1000 Subject: [PATCH 3/5] ci(lint): gate on biome check; clear auto-fixable errors; ignore generated files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Level 1 of making Biome a CI gate (errors only; warnings allowed): - Add a `code:check` script (`biome check`, read-only) and run it in tests.yml. `biome check` exits non-zero on ERRORS and zero on warnings, so this gates format/lint/organizeImports errors while the `no-type-erasing-assertions` plugin (and the ~500 pre-existing warnings) stay visible but non-blocking. - Ignore checked-in GENERATED artifacts in biome (prisma-next `contract.*` / `end-contract.*` / `*.generated.ts`, and migration `*.json` / `refs`) — they carry `DO NOT EDIT` and would re-drift on regeneration, so CI must not gate on their formatting. This dropped the error count 36 → 15. - `biome check --write` the 15 remaining errors in AUTHORED files (format + organizeImports only — no logic changes). `biome check` now exits 0. Tightening the gate to fail on warnings (level 3) is tracked as a follow-up. Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w --- .github/workflows/tests.yml | 7 +++ AGENTS.md | 11 +++- biome.json | 10 ++- e2e/tests/package-managers.e2e.test.ts | 3 +- .../app/20260709T1034_initial/migration.ts | 54 ++++++++++++---- examples/prisma/src/index.ts | 8 ++- examples/prisma/test/e2e/global-setup.ts | 20 +++--- .../prisma/test/e2e/str-range.e2e.test.ts | 4 +- package.json | 1 + packages/cli/src/__tests__/config.test.ts | 63 +++++++++---------- .../test/cipherstash-codec.test.ts | 5 +- packages/prisma-next/test/descriptor.test.ts | 3 +- .../test/operator-lowering-equality.test.ts | 4 +- .../test/operator-lowering.test.ts | 4 +- packages/stack/__tests__/v3-matrix/catalog.ts | 16 ++--- packages/stack/src/eql/v3/index.ts | 16 ++--- packages/stack/src/eql/v3/types.ts | 40 ++++++------ .../wizard/src/__tests__/interface.test.ts | 31 ++++----- 18 files changed, 177 insertions(+), 123 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3a0b5a598..fa0ed2e21 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -65,6 +65,13 @@ jobs: - name: Lint — no hardcoded package-manager runners run: pnpm run lint:runners + # Biome format + lint. Gates on ERRORS only (warnings are allowed) — the + # `no-type-erasing-assertions` plugin runs at `warn`, so it surfaces new + # `as any`/`never`/`unknown` without blocking. Tightening to fail on + # warnings is tracked as a follow-up. + - name: Lint — Biome (format + lint) + run: pnpm run code:check + - name: Test — lint script self-tests run: pnpm run test:scripts diff --git a/AGENTS.md b/AGENTS.md index e55a38776..9059f569e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -174,14 +174,21 @@ Three rules to remember when editing CI or pnpm config: - **Formatting/Linting**: Use Biome ```bash -pnpm run code:fix +pnpm run code:fix # format + lint, auto-fixing what it can +pnpm run code:check # read-only; this is what CI runs ``` + CI runs `code:check` (in `tests.yml`) and gates on **errors** — warnings are + allowed (tracked for tightening). So `code:fix` must leave the tree + error-free before you push. + A Biome GritQL plugin (`biome-plugins/no-type-erasing-assertions.grit`) warns on `as any` / `as never` / `as unknown` in `src` — type-erasing assertions that silence the checker instead of narrowing. Fix the type or use a specific assertion; suppress a deliberate case with `// biome-ignore lint/plugin: - `. Test/integration files are exempt (see `overrides` in `biome.json`). + `. The plugin is scoped to source via an `overrides` entry in + `biome.json` (test/integration files excluded) — see the plugin file's header + for why it must be scoped-in rather than globally-enabled-and-exempted. - **Build**: `pnpm run build` (Turborepo + tsup per package) - **Test**: `pnpm --filter test` for targeted iterations diff --git a/biome.json b/biome.json index 8f90da791..17d9c3f47 100644 --- a/biome.json +++ b/biome.json @@ -7,7 +7,15 @@ "!**/.turbo/", "!**/dist", "!**/nix/store", - "!**/*.grit" + "!**/*.grit", + "!**/*.generated.ts", + "!**/contract.json", + "!**/contract.d.ts", + "!**/end-contract.json", + "!**/end-contract.d.ts", + "!**/migrations/**/migration.json", + "!**/migrations/**/ops.json", + "!**/migrations/**/refs" ] }, "formatter": { diff --git a/e2e/tests/package-managers.e2e.test.ts b/e2e/tests/package-managers.e2e.test.ts index 85f65414b..52df50afc 100644 --- a/e2e/tests/package-managers.e2e.test.ts +++ b/e2e/tests/package-managers.e2e.test.ts @@ -56,7 +56,8 @@ describe('CLI init providers — package-manager-aware Next Steps', () => { { label: 'drizzle', create: createDrizzleProvider, - firstStep: (r) => `Set up your database: ${r} stash eql install --drizzle`, + firstStep: (r) => + `Set up your database: ${r} stash eql install --drizzle`, }, { label: 'supabase', diff --git a/examples/prisma/migrations/app/20260709T1034_initial/migration.ts b/examples/prisma/migrations/app/20260709T1034_initial/migration.ts index 11c035c3a..711d9eb64 100755 --- a/examples/prisma/migrations/app/20260709T1034_initial/migration.ts +++ b/examples/prisma/migrations/app/20260709T1034_initial/migration.ts @@ -1,13 +1,18 @@ #!/usr/bin/env -S node -import { cipherstashAddSearchConfig } from '@prisma-next/extension-cipherstash/migration'; -import { Migration, MigrationCLI, col, primaryKey } from '@prisma-next/postgres/migration'; +import { cipherstashAddSearchConfig } from '@prisma-next/extension-cipherstash/migration' +import { + col, + Migration, + MigrationCLI, + primaryKey, +} from '@prisma-next/postgres/migration' export default class M extends Migration { override describe() { return { from: null, to: 'sha256:b8c4febc9397cf1b68293cdb3b2afe9f568db6967f428e3f207e32575a2bc2fa', - }; + } } override get operations() { @@ -34,17 +39,30 @@ export default class M extends Migration { notNull: true, codecRef: { codecId: 'cipherstash/string@1', - typeParams: { equality: true, freeTextSearch: true, orderAndRange: true }, + typeParams: { + equality: true, + freeTextSearch: true, + orderAndRange: true, + }, }, }), col('emailverified', 'eql_v2_encrypted', { notNull: true, - codecRef: { codecId: 'cipherstash/boolean@1', typeParams: { equality: true } }, + codecRef: { + codecId: 'cipherstash/boolean@1', + typeParams: { equality: true }, + }, + }), + col('id', 'text', { + notNull: true, + codecRef: { codecId: 'pg/text@1' }, }), - col('id', 'text', { notNull: true, codecRef: { codecId: 'pg/text@1' } }), col('preferences', 'eql_v2_encrypted', { notNull: true, - codecRef: { codecId: 'cipherstash/json@1', typeParams: { searchableJson: true } }, + codecRef: { + codecId: 'cipherstash/json@1', + typeParams: { searchableJson: true }, + }, }), col('salary', 'eql_v2_encrypted', { notNull: true, @@ -80,9 +98,21 @@ export default class M extends Migration { index: 'ore', castAs: 'date', }), - cipherstashAddSearchConfig({ table: 'users', column: 'email', index: 'unique' }), - cipherstashAddSearchConfig({ table: 'users', column: 'email', index: 'match' }), - cipherstashAddSearchConfig({ table: 'users', column: 'email', index: 'ore' }), + cipherstashAddSearchConfig({ + table: 'users', + column: 'email', + index: 'unique', + }), + cipherstashAddSearchConfig({ + table: 'users', + column: 'email', + index: 'match', + }), + cipherstashAddSearchConfig({ + table: 'users', + column: 'email', + index: 'ore', + }), cipherstashAddSearchConfig({ table: 'users', column: 'emailverified', @@ -107,8 +137,8 @@ export default class M extends Migration { index: 'ore', castAs: 'double', }), - ]; + ] } } -MigrationCLI.run(import.meta.url, M); +MigrationCLI.run(import.meta.url, M) diff --git a/examples/prisma/src/index.ts b/examples/prisma/src/index.ts index 885d3cddf..02320c81c 100644 --- a/examples/prisma/src/index.ts +++ b/examples/prisma/src/index.ts @@ -118,7 +118,9 @@ async function main() { } async function clearUsers(): Promise { - const removed = await db.orm.public.User.where((u) => u.id.isNotNull()).deleteCount() + const removed = await db.orm.public.User.where((u) => + u.id.isNotNull(), + ).deleteCount() if (removed > 0) { console.log(`--- Cleanup ---\nRemoved ${removed} existing user row(s).\n`) } @@ -212,7 +214,9 @@ async function equalityQueryOnEmailVerified(): Promise { async function sortByEmailAsc(): Promise { console.log('\n--- cipherstashAsc (bare-column ORDER BY) ---') - const rows = await db.orm.public.User.orderBy((u) => cipherstashAsc(u.email)).all() + const rows = await db.orm.public.User.orderBy((u) => + cipherstashAsc(u.email), + ).all() await decryptAll(rows) for (const row of rows) { console.log(` ${row.id}: email=${await row.email.decrypt()}`) diff --git a/examples/prisma/test/e2e/global-setup.ts b/examples/prisma/test/e2e/global-setup.ts index 3fb218342..82c7dafdb 100644 --- a/examples/prisma/test/e2e/global-setup.ts +++ b/examples/prisma/test/e2e/global-setup.ts @@ -119,20 +119,14 @@ export default async function setup(): Promise<() => Promise> { // is for the `pnpm start` demo loop). process.env['DATABASE_URL'] = HARNESS_DATABASE_URL - const apply = spawnSync( - 'pnpm', - ['exec', 'prisma-next', 'migrate', '--yes'], - { - cwd: exampleDir, - stdio: 'pipe', - env: process.env, - timeout: MIGRATION_APPLY_TIMEOUT_MS, - }, - ) + const apply = spawnSync('pnpm', ['exec', 'prisma-next', 'migrate', '--yes'], { + cwd: exampleDir, + stdio: 'pipe', + env: process.env, + timeout: MIGRATION_APPLY_TIMEOUT_MS, + }) if (apply.error || apply.signal || apply.status !== 0) { - throw new Error( - describeSpawnFailure('`prisma-next migrate`', apply), - ) + throw new Error(describeSpawnFailure('`prisma-next migrate`', apply)) } // Clean slate for the suite. The `users` table is the only data-bearing diff --git a/examples/prisma/test/e2e/str-range.e2e.test.ts b/examples/prisma/test/e2e/str-range.e2e.test.ts index c98bda626..e8d4e2295 100644 --- a/examples/prisma/test/e2e/str-range.e2e.test.ts +++ b/examples/prisma/test/e2e/str-range.e2e.test.ts @@ -74,7 +74,9 @@ describe('EncryptedString orderAndRange e2e (live PG + EQL + ZeroKMS)', () => { }) it('cipherstashAsc orders alphabetically (bare-column ORDER BY on string)', async () => { - const rows = await db.orm.public.User.orderBy((u) => cipherstashAsc(u.email)).all() + const rows = await db.orm.public.User.orderBy((u) => + cipherstashAsc(u.email), + ).all() expect(rows.map((r) => r.id)).toEqual([ 'e2e-str-0', 'e2e-str-1', diff --git a/package.json b/package.json index 9638c0ebb..fc0d7ef6c 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "dev": "turbo dev --filter './packages/*'", "clean": "rimraf --glob **/.next **/.turbo **/dist **/node_modules", "code:fix": "biome check --write", + "code:check": "biome check", "lint:runners": "node scripts/lint-no-hardcoded-runners.mjs", "lint:workflow-cache": "node scripts/lint-no-workflow-caching.mjs", "release": "pnpm run build && changeset publish", diff --git a/packages/cli/src/__tests__/config.test.ts b/packages/cli/src/__tests__/config.test.ts index d3094584f..5189bc2f7 100644 --- a/packages/cli/src/__tests__/config.test.ts +++ b/packages/cli/src/__tests__/config.test.ts @@ -54,37 +54,34 @@ describe('loadStashConfig', () => { it.each([ ['stash', `Cannot find module 'stash'`], ['@cipherstash/stack', `Cannot find package '@cipherstash/stack'`], - ])( - 'translates a missing `%s` module into actionable guidance (#579)', - async (pkg, message) => { - fs.writeFileSync( - path.join(tmpDir, 'stash.config.ts'), - `import 'stash'\nexport default {}`, - ) - process.cwd = () => tmpDir - vi.spyOn(process, 'exit').mockImplementation(() => { - throw new Error('process.exit') - }) - const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) - - const moduleErr = Object.assign(new Error(message), { - code: 'MODULE_NOT_FOUND', - }) - const { createJiti } = await import('jiti') - vi.mocked(createJiti).mockReturnValue({ - import: vi.fn().mockRejectedValue(moduleErr), - } as never) - - const { loadStashConfig } = await import('@/config/index.ts') - await expect(loadStashConfig()).rejects.toThrow('process.exit') - - const output = errorSpy.mock.calls.map((c) => c.join(' ')).join('\n') - expect(output).toContain(`\`${pkg}\` is not installed`) - expect(output).toContain('stash init') - // The raw jiti stack trace must NOT be forwarded to the user. - expect(output).not.toContain('Failed to load') - }, - ) + ])('translates a missing `%s` module into actionable guidance (#579)', async (pkg, message) => { + fs.writeFileSync( + path.join(tmpDir, 'stash.config.ts'), + `import 'stash'\nexport default {}`, + ) + process.cwd = () => tmpDir + vi.spyOn(process, 'exit').mockImplementation(() => { + throw new Error('process.exit') + }) + const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) + + const moduleErr = Object.assign(new Error(message), { + code: 'MODULE_NOT_FOUND', + }) + const { createJiti } = await import('jiti') + vi.mocked(createJiti).mockReturnValue({ + import: vi.fn().mockRejectedValue(moduleErr), + } as never) + + const { loadStashConfig } = await import('@/config/index.ts') + await expect(loadStashConfig()).rejects.toThrow('process.exit') + + const output = errorSpy.mock.calls.map((c) => c.join(' ')).join('\n') + expect(output).toContain(`\`${pkg}\` is not installed`) + expect(output).toContain('stash init') + // The raw jiti stack trace must NOT be forwarded to the user. + expect(output).not.toContain('Failed to load') + }) it('still surfaces the raw error for unrelated config load failures', async () => { fs.writeFileSync(path.join(tmpDir, 'stash.config.ts'), 'export default {}') @@ -157,7 +154,9 @@ describe('loadEncryptConfig', () => { let originalCwd: () => string beforeEach(() => { - tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'stash-encrypt-config-test-')) + tmpDir = fs.mkdtempSync( + path.join(os.tmpdir(), 'stash-encrypt-config-test-'), + ) originalCwd = process.cwd }) diff --git a/packages/prisma-next/test/cipherstash-codec.test.ts b/packages/prisma-next/test/cipherstash-codec.test.ts index 07896f6f6..3375bf4c6 100644 --- a/packages/prisma-next/test/cipherstash-codec.test.ts +++ b/packages/prisma-next/test/cipherstash-codec.test.ts @@ -124,7 +124,10 @@ describe('planFieldEventOperations driving the cipherstash hook', () => { storage: new SqlStorage({ storageHash: 'sha256:test' as StorageHashBase, namespaces: { - __unbound__: buildSqlNamespace({ id: '__unbound__', entries: { table: tables } }), + __unbound__: buildSqlNamespace({ + id: '__unbound__', + entries: { table: tables }, + }), }, }), domain: { namespaces: { __unbound__: { models: {} } } }, diff --git a/packages/prisma-next/test/descriptor.test.ts b/packages/prisma-next/test/descriptor.test.ts index 6956128cd..96cfe2df6 100644 --- a/packages/prisma-next/test/descriptor.test.ts +++ b/packages/prisma-next/test/descriptor.test.ts @@ -121,7 +121,8 @@ describe('cipherstash extension descriptor (contract-space package layout)', () headRefHash: space.headRef.hash, // The emit pipeline hashes through the SQL family's // canonicalization hooks; the recompute must use the same ones. - shouldPreserveEmpty: sqlContractCanonicalizationHooks.shouldPreserveEmpty, + shouldPreserveEmpty: + sqlContractCanonicalizationHooks.shouldPreserveEmpty, sortStorage: sqlContractCanonicalizationHooks.sortStorage, }), ).not.toThrow() diff --git a/packages/prisma-next/test/operator-lowering-equality.test.ts b/packages/prisma-next/test/operator-lowering-equality.test.ts index f74290ffe..694d0759b 100644 --- a/packages/prisma-next/test/operator-lowering-equality.test.ts +++ b/packages/prisma-next/test/operator-lowering-equality.test.ts @@ -116,9 +116,7 @@ describe('cipherstash operator lowering — equality extensions', () => { `"SELECT "user"."id" AS "id" FROM "user" WHERE NOT eql_v2.eq("user"."email", $1::eql_v2_encrypted)"`, ) expect(lowered.params).toHaveLength(1) - expect(literalParamValue(lowered.params[0])).toBeInstanceOf( - EncryptedString, - ) + expect(literalParamValue(lowered.params[0])).toBeInstanceOf(EncryptedString) }) it('lowers cipherstashInArray with a single element to a one-term OR', () => { diff --git a/packages/prisma-next/test/operator-lowering.test.ts b/packages/prisma-next/test/operator-lowering.test.ts index 3ec1038f2..6d276bb76 100644 --- a/packages/prisma-next/test/operator-lowering.test.ts +++ b/packages/prisma-next/test/operator-lowering.test.ts @@ -145,9 +145,7 @@ describe('cipherstash operator lowering — per-codec envelope dispatch', () => const lowered = makeAdapter().lower(selectWithWhere(predicate), { contract, }) - expect(literalParamValue(lowered.params[0])).toBeInstanceOf( - EncryptedBigInt, - ) + expect(literalParamValue(lowered.params[0])).toBeInstanceOf(EncryptedBigInt) }) it('cipherstashGt on a date column wraps the value in EncryptedDate', () => { diff --git a/packages/stack/__tests__/v3-matrix/catalog.ts b/packages/stack/__tests__/v3-matrix/catalog.ts index 994d9a227..eedd20307 100644 --- a/packages/stack/__tests__/v3-matrix/catalog.ts +++ b/packages/stack/__tests__/v3-matrix/catalog.ts @@ -28,18 +28,10 @@ import { EncryptedDateEqColumn, EncryptedDateOrdColumn, EncryptedDateOrdOreColumn, - EncryptedRealColumn, - EncryptedRealEqColumn, - EncryptedRealOrdColumn, - EncryptedRealOrdOreColumn, EncryptedDoubleColumn, EncryptedDoubleEqColumn, EncryptedDoubleOrdColumn, EncryptedDoubleOrdOreColumn, - EncryptedSmallintColumn, - EncryptedSmallintEqColumn, - EncryptedSmallintOrdColumn, - EncryptedSmallintOrdOreColumn, EncryptedIntegerColumn, EncryptedIntegerEqColumn, EncryptedIntegerOrdColumn, @@ -48,6 +40,14 @@ import { EncryptedNumericEqColumn, EncryptedNumericOrdColumn, EncryptedNumericOrdOreColumn, + EncryptedRealColumn, + EncryptedRealEqColumn, + EncryptedRealOrdColumn, + EncryptedRealOrdOreColumn, + EncryptedSmallintColumn, + EncryptedSmallintEqColumn, + EncryptedSmallintOrdColumn, + EncryptedSmallintOrdOreColumn, EncryptedTextColumn, EncryptedTextEqColumn, EncryptedTextMatchColumn, diff --git a/packages/stack/src/eql/v3/index.ts b/packages/stack/src/eql/v3/index.ts index 3889f1f20..31702c59d 100644 --- a/packages/stack/src/eql/v3/index.ts +++ b/packages/stack/src/eql/v3/index.ts @@ -23,18 +23,10 @@ export { EncryptedDateEqColumn, EncryptedDateOrdColumn, EncryptedDateOrdOreColumn, - EncryptedRealColumn, - EncryptedRealEqColumn, - EncryptedRealOrdColumn, - EncryptedRealOrdOreColumn, EncryptedDoubleColumn, EncryptedDoubleEqColumn, EncryptedDoubleOrdColumn, EncryptedDoubleOrdOreColumn, - EncryptedSmallintColumn, - EncryptedSmallintEqColumn, - EncryptedSmallintOrdColumn, - EncryptedSmallintOrdOreColumn, EncryptedIntegerColumn, EncryptedIntegerEqColumn, EncryptedIntegerOrdColumn, @@ -43,6 +35,14 @@ export { EncryptedNumericEqColumn, EncryptedNumericOrdColumn, EncryptedNumericOrdOreColumn, + EncryptedRealColumn, + EncryptedRealEqColumn, + EncryptedRealOrdColumn, + EncryptedRealOrdOreColumn, + EncryptedSmallintColumn, + EncryptedSmallintEqColumn, + EncryptedSmallintOrdColumn, + EncryptedSmallintOrdOreColumn, EncryptedTextColumn, EncryptedTextEqColumn, EncryptedTextMatchColumn, diff --git a/packages/stack/src/eql/v3/types.ts b/packages/stack/src/eql/v3/types.ts index df2d55e42..7a688570f 100644 --- a/packages/stack/src/eql/v3/types.ts +++ b/packages/stack/src/eql/v3/types.ts @@ -4,23 +4,19 @@ import { DATE_EQ, DATE_ORD, DATE_ORD_ORE, + DOUBLE, + DOUBLE_EQ, + DOUBLE_ORD, + DOUBLE_ORD_ORE, EncryptedBooleanColumn, EncryptedDateColumn, EncryptedDateEqColumn, EncryptedDateOrdColumn, EncryptedDateOrdOreColumn, - EncryptedRealColumn, - EncryptedRealEqColumn, - EncryptedRealOrdColumn, - EncryptedRealOrdOreColumn, EncryptedDoubleColumn, EncryptedDoubleEqColumn, EncryptedDoubleOrdColumn, EncryptedDoubleOrdOreColumn, - EncryptedSmallintColumn, - EncryptedSmallintEqColumn, - EncryptedSmallintOrdColumn, - EncryptedSmallintOrdOreColumn, EncryptedIntegerColumn, EncryptedIntegerEqColumn, EncryptedIntegerOrdColumn, @@ -29,6 +25,14 @@ import { EncryptedNumericEqColumn, EncryptedNumericOrdColumn, EncryptedNumericOrdOreColumn, + EncryptedRealColumn, + EncryptedRealEqColumn, + EncryptedRealOrdColumn, + EncryptedRealOrdOreColumn, + EncryptedSmallintColumn, + EncryptedSmallintEqColumn, + EncryptedSmallintOrdColumn, + EncryptedSmallintOrdOreColumn, EncryptedTextColumn, EncryptedTextEqColumn, EncryptedTextMatchColumn, @@ -39,18 +43,6 @@ import { EncryptedTimestampEqColumn, EncryptedTimestampOrdColumn, EncryptedTimestampOrdOreColumn, - REAL, - REAL_EQ, - REAL_ORD, - REAL_ORD_ORE, - DOUBLE, - DOUBLE_EQ, - DOUBLE_ORD, - DOUBLE_ORD_ORE, - SMALLINT, - SMALLINT_EQ, - SMALLINT_ORD, - SMALLINT_ORD_ORE, INTEGER, INTEGER_EQ, INTEGER_ORD, @@ -59,6 +51,14 @@ import { NUMERIC_EQ, NUMERIC_ORD, NUMERIC_ORD_ORE, + REAL, + REAL_EQ, + REAL_ORD, + REAL_ORD_ORE, + SMALLINT, + SMALLINT_EQ, + SMALLINT_ORD, + SMALLINT_ORD_ORE, TEXT, TEXT_EQ, TEXT_MATCH, diff --git a/packages/wizard/src/__tests__/interface.test.ts b/packages/wizard/src/__tests__/interface.test.ts index 33172a8b8..b81a25911 100644 --- a/packages/wizard/src/__tests__/interface.test.ts +++ b/packages/wizard/src/__tests__/interface.test.ts @@ -76,21 +76,22 @@ describe('wizardCanUseTool', () => { // The doctrine tells the agent to add placeholder keys to `.env.example`. // The guard covers Edit and Write as well as Read, so a blanket `.env.` // rule made the file the agent is told to write unreachable. - it.each(['.env.example', '.env.sample', '.env.template'])( - 'allows Read/Edit/Write/Glob on %s', - (name) => { - expect(wizardCanUseTool('Read', { file_path: `/project/${name}` })).toBe( - true, - ) - expect(wizardCanUseTool('Edit', { file_path: `/project/${name}` })).toBe( - true, - ) - expect( - wizardCanUseTool('Write', { file_path: `/project/${name}` }), - ).toBe(true) - expect(wizardCanUseTool('Glob', { pattern: name })).toBe(true) - }, - ) + it.each([ + '.env.example', + '.env.sample', + '.env.template', + ])('allows Read/Edit/Write/Glob on %s', (name) => { + expect(wizardCanUseTool('Read', { file_path: `/project/${name}` })).toBe( + true, + ) + expect(wizardCanUseTool('Edit', { file_path: `/project/${name}` })).toBe( + true, + ) + expect(wizardCanUseTool('Write', { file_path: `/project/${name}` })).toBe( + true, + ) + expect(wizardCanUseTool('Glob', { pattern: name })).toBe(true) + }) it('still blocks value-bearing files that only start with a template name', () => { expect( From 0bf20cda3165ba3fba5bb68c8fb2208613321622 Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Sun, 12 Jul 2026 19:56:51 +1000 Subject: [PATCH 4/5] ci(lint): move Biome check into its own dedicated job MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Was a step inside the matrixed "Run Tests" job (ran per Node leg, no distinct check line). Split into a standalone `lint` job — no DB/credentials, single Node, so it surfaces as its own "Lint (Biome)" check and runs once. Still gates on errors only (warnings allowed); #625 tracks tightening. Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w --- .github/workflows/tests.yml | 41 ++++++++++++++++++++++++++++++------- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index fa0ed2e21..66b543a40 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -9,6 +9,40 @@ on: - "**" jobs: + # Biome format + lint. Its own job (no DB, no credentials, node-agnostic) so it + # surfaces as a distinct check and runs once rather than per Node matrix leg. + # Gates on ERRORS only — warnings are allowed, so the `no-type-erasing-assertions` + # plugin (at `warn`) surfaces new `as any`/`never`/`unknown` without blocking. + # Tightening to fail on warnings is tracked in #625. + lint: + name: Lint (Biome) + runs-on: blacksmith-4vcpu-ubuntu-2404 + steps: + - name: Checkout Repo + uses: actions/checkout@v6 + + - uses: pnpm/action-setup@v6.0.8 + name: Install pnpm + with: + run_install: false + + - name: Install Node.js + uses: actions/setup-node@v6 + with: + node-version: 24 + cache: 'pnpm' + + # node-pty's install hook falls back to `node-gyp rebuild` when no + # linux-x64 prebuild matches; pnpm/action-setup v6 no longer ships it. + - name: Install node-gyp + run: npm install -g node-gyp + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Biome check (format + lint) + run: pnpm run code:check + run-tests: name: Run Tests (Node ${{ matrix.node-version }}) runs-on: blacksmith-4vcpu-ubuntu-2404 @@ -65,13 +99,6 @@ jobs: - name: Lint — no hardcoded package-manager runners run: pnpm run lint:runners - # Biome format + lint. Gates on ERRORS only (warnings are allowed) — the - # `no-type-erasing-assertions` plugin runs at `warn`, so it surfaces new - # `as any`/`never`/`unknown` without blocking. Tightening to fail on - # warnings is tracked as a follow-up. - - name: Lint — Biome (format + lint) - run: pnpm run code:check - - name: Test — lint script self-tests run: pnpm run test:scripts From 8651f0f1329cc3ad65f2144d997dbcef62b9ca4e Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Sun, 12 Jul 2026 20:01:22 +1000 Subject: [PATCH 5/5] ci(lint): pin the lint job to Node 22 (supply-chain hardening baseline) The dedicated lint job hardcoded Node 24, which trips the supply-chain gate (e2e/tests/supply-chain.e2e.test.ts) requiring every pnpm-using job to pin Node 22 (literal or a matrix including 22). Lint is runtime-agnostic, so pin 22. Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w --- .github/workflows/tests.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 66b543a40..036cf48d1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -29,7 +29,10 @@ jobs: - name: Install Node.js uses: actions/setup-node@v6 with: - node-version: 24 + # Node 22 is the supply-chain hardening baseline every pnpm-using job + # must pin (enforced by e2e/tests/supply-chain.e2e.test.ts). Lint is + # runtime-agnostic, so a single pinned version is fine. + node-version: 22 cache: 'pnpm' # node-pty's install hook falls back to `node-gyp rebuild` when no