Skip to content

fix(cli): prisma-next init/plan/impl crash + stash-prisma-next skill (rc.2 M1)#683

Open
coderdan wants to merge 22 commits into
feat/prisma-next-eql-v3from
feat/prisma-next-cli-skill
Open

fix(cli): prisma-next init/plan/impl crash + stash-prisma-next skill (rc.2 M1)#683
coderdan wants to merge 22 commits into
feat/prisma-next-eql-v3from
feat/prisma-next-cli-skill

Conversation

@coderdan

Copy link
Copy Markdown
Contributor

What

Companion to #655. #655 releases the @cipherstash/prisma-next EQL v3 package but touches no CLI code — so the CLI-side Prisma Next experience is still broken. This PR fixes it:

  • The M1 crash. SKILL_MAP is typed Record<Integration, …> but omitted the 'prisma-next' key. installSkills and the AGENTS.md builder do SKILL_MAP[integration]undefinednot iterable for any repo the CLI detects as Prisma Next. tsc catches the missing key, but the build (tsup) transpiles without type-checking, so it shipped in rc.2 — reachable by any Prisma user running stash init (Prisma Next is auto-detected via detectPrismaNext).
  • New stash-prisma-next skill documenting the EQL v3 surface: domain-named column types (EncryptedTextSearch, EncryptedDoubleOrd, …), cipherstashFromStackV3 wiring, the runtime value envelopes, the eql* query operators, and EQL install via prisma-next migration apply (not stash eql install). Authored from Feat/prisma next eql v3 #655's converted example app and package README, so it reflects the real v3 API.

How

  • Add the prisma-next SKILL_MAP entry (stash-encryption, stash-prisma-next, stash-cli).
  • Route both consumers through a new skillsFor() helper that degrades an unmapped integration to the base skill set instead of crashing — belt-and-braces for the next Integration variant, since tsup won't type-check the map. (The real gap it papers over: the CLI package has no tsc --noEmit gate in CI.)
  • Meta files: AGENTS.md skill-map table + skills list.

Tests

  • SKILL_MAP has a non-empty entry for every integration (the runtime guard tsc can't be in this build).
  • skillsFor() falls back for an unmapped integration.
  • buildAgentsMdBody('prisma-next', 'doctrine-plus-skills') inlines the skill — the exact call that used to throw not iterable.
  • installSkills(…, 'prisma-next') copies stash-prisma-next/SKILL.md (verified end-to-end).
  • Full CLI suite 502 green, code:check clean.

Ordering

Stacked on feat/prisma-next-eql-v3 so the skill reflects the released v3 API; retarget to main when #655 lands (GitHub does this automatically). The skill defers stash env flag detail to the stash-cli skill so it's correct regardless of #682's merge order.

Closes the rc.2 M1 finding.

https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w

calvinbrewer and others added 20 commits July 14, 2026 12:29
…MAIN_REGISTRY

Codec ids are the DOMAIN_REGISTRY key verbatim (cipherstash/eql-v3/eql_v3_*@1)
per the GA eql_v3_* domain naming; json is a first-class exposed domain.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…t test

The closed union infers eqlType from getEqlType()'s return type — the
stack's bundled d.ts drops the private definition field's type, so the
EqlTypeForColumn infer-D path widens to public.${string} downstream.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…y gating

Add cipherstashV3QueryOperations() (src/v3/operators-v3.ts): the EQL v3
operator set registered by the v3 extension descriptor only (decision 1b
— same cipherstash* method names as v2, never co-registered; the flat
OperationRegistry collision is pinned by test).

Lowering matches the stack-drizzle v3 dialect byte-for-byte: operands
are ciphertext-free query terms cast to the column domain's
eql_v3.query_<domain> type (eql_v3.eq/neq/gt/gte/lt/lte/contains,
self-parenthesised (gte AND lte) range, OR-of-eq membership), JSON
containment is OPERATOR(public.@>) with the irregular
::eql_v3.query_jsonb cast (cipherstashJsonContains, empty-needle
guarded), and ordering extracts eql_v3.ord_term / ord_term_ore by the
domain's flavour via the free-standing cipherstashV3Asc/Desc helpers.

Operands are bound as pg/text@1 params (bare $N; the template supplies
the query cast — the column codec's storage-domain cast would fail the
query term's CHECK) and each envelope is routing-key stamped plus
marked with its queryType (markV3QueryTerm / v3QueryTermTypeOf), the
seam Task 7's SDK adapter uses to route WHERE operands through
encryptQuery instead of storage bulkEncrypt.

Every operator gates on the concrete domain's catalog capabilities and
throws the v3-owned EncryptionOperatorError naming column, domain,
operator, and missing capability. v3 codec descriptors are pinned to
cipherstash:*-only traits alongside the v2 equality-trait regression.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tor + cipherstashFromStackV3

- deriveStackSchemasV3(contractJson): walks the 0.14 namespace envelope
  and maps each v3 column's public.eql_v3_* nativeType to its concrete
  @cipherstash/stack/eql/v3 factory (capabilities are intrinsic to the
  domain — no typeParams flags), preserving exact domain identity.
- createCipherstashV3Sdk(client, schemas): adapts the EncryptionV3
  TypedEncryptionClient (structural surface, no casts) to the framework
  CipherstashSdk. Consumes the Task-6 query-term seam: envelopes marked
  by markV3QueryTerm route through encryptQuery — one batch crossing
  per scalar flavour (stack-drizzle's inArray surface), single-call for
  searchableJson (stack-drizzle's JSON containment path) — while
  unmarked values take the storage bulkEncrypt path, position-stable.
- bulkEncryptMiddlewareV3 now collects pg/text@1-bound marked envelopes
  (WHERE operands), forwards the envelope itself through the SDK seam,
  and writes the returned term back as query-term JSONB text without
  stamping it into the envelope's storage-ciphertext slot.
- createCipherstashV3RuntimeDescriptor({ sdk }): the v3 extension
  descriptor under v3's own id/version (decision 1b — never
  co-registered with v2; shared method names collide by design).
- assertV3SchemasAgree: override validation on EXACT domain identity
  (integer_ord ≠ integer_ord_ore despite shared cast_as).
- cipherstashFromStackV3: the v3-only entry point — hard-errors on v2
  cipherstash codec ids and on contracts with no v3 columns; returns
  { extensions, middleware, encryptionClient } like the v2 factory.
- Query-term seam (markV3QueryTerm / v3QueryTermTypeOf +
  EncryptionOperatorError) extracted to src/v3/query-term.ts so the
  middleware/adapter don't import the operator registry; operators-v3
  re-exports it unchanged.
- Exports: v3 surface added to ./runtime and ./stack; new ./v3 subpath
  (ESM, matching the package's existing ESM-only exports map).
- bundling-isolation: allow the v3 catalog chunk (pure domain metadata)
  as a second cross-plane shared chunk alongside the v2 constants chunk.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…/eql

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ting

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bundling isolation (test/bundling-isolation.test.ts):
- v3.js joins the entry existence check and is pinned against both the
  contract-space artefacts (RUNTIME_FORBIDDEN) and the v2 wire plane
- content-fingerprinted wire markers (encodeEqlV2EncryptedWire /
  makeCipherstashCellCodec vs CipherstashV3CellCodec /
  createV3CodecDescriptors / bulkEncryptMiddlewareV3): no code-split
  chunk may reference both wire planes, the v3 entry graph never
  reaches the v2 codec-runtime chunk, and middleware.js (v2-only
  entry) never loads the v3 wire

Live-PG suites (test/live/, self-skipping via describeLivePg when
CS creds — env vars or ~/.cipherstash profile — or DATABASE_URL are
absent; `pnpm test:live` convenience script added):
- helpers: live-gate (skip posture), eql-v3 (advisory-locked
  installEqlV3IfNeeded over the Task-8 migration source SQL), harness
  (real cipherstashFromStackV3 + real bulkEncryptMiddlewareV3 driven
  through real execution plans, operator SELECTs lowered by the real
  postgres adapter, envelope fromInternal().decrypt() read path)
- operators-live-pg: eq/ne/inArray, contains (ilike), gt/lte/between,
  date + timestamp ranges, ord_term ordering, JSON containment
  (@> with eql_v3.query_jsonb — ruled decision 2), per-family decrypt
- operators-null-live-pg: NULL cells round-trip, null operands rejected
- boolean-storage-live-pg: storage-only round-trip, operators refuse
- bigint-live-pg: lossless bigint beyond MAX_SAFE_INTEGER, eq/range/order
- migration-apply-live-pg: applied SQL is byte-identical to the shipped
  ops.json, invariant id, domains + eql_v3 schema, op postchecks, no
  add_search_config
- bulk-encrypt-live-pg: real EncryptionV3 through the real middleware,
  JSONB cells, ciphertext stamping, decrypt round-trip
- side-by-side-clients-live-pg: v2 and v3 clients coexisting in-process
  against one database, distinct extension identities and wires

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Also a stack changeset for the new eql/v3 barrel exports, and the
AGENTS.md repository-layout line updated for the v2/v3 split.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Gaps surfaced by converting the example app and driving the real CLI +
runtime rather than unit fixtures:

- CodecTypes/QueryOperationTypes cover all 40 v3 codec ids with
  trait-accurate operator visibility (cipherstash:v3-* marker traits;
  cipherstashJsonContains v3-only; storage-only domains surface no
  operator methods at the type level).
- The v3 runtime descriptor presents the pack id ('cipherstash') with
  v3's own version — the runtime matches contract extensionPacks by
  descriptor id, so the old distinct id failed startup with
  RUNTIME.MISSING_EXTENSION_PACK.
- Every v3 codec id registers a control-plane expandNativeType hook
  stripping the public. qualifier (the planner requires the hook for
  typeParams-carrying columns; bare names match introspected udt_name).
  No onFieldEvent — v3 emits zero add_search_config ops.
- The v3 bundle op is reclassified 'data': the integrity checker
  rejects self-edges without a data-class op.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Schema: every column is a concrete public.eql_v3_* domain
(EncryptedTextSearch / EncryptedDoubleOrd / EncryptedBigIntOrd /
EncryptedDateOrd / EncryptedBoolean / EncryptedJson); wiring via
cipherstashFromStackV3. Migrations regenerated from the v3 contract —
initial app migration carries zero add_search_config ops; the
cipherstash space ships both bundle baselines.

E2E suites adapted to the v3 surface and green from a wiped database
(7 files / 40 tests, live PG + ZeroKMS): equality/range/free-text
tokens, order-term sorting, JSON @> containment (positive, negative,
multi-key, nested, empty-needle rejection), lossless bigint beyond
MAX_SAFE_INTEGER, and eql_v3_boolean's storage-only operator refusal
pinned as a feature. README walkthrough e2e (repo-level) also green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…he eql/v3 graduation

The branch had graduated DOMAIN_REGISTRY / factoryForDomain /
stripDomainSchema onto the public eql/v3 barrel while adapter-kit still
exported the same symbols — two doors to one registry. Revert the
graduation and import through adapter-kit, the seam the Drizzle and
Supabase adapters already consume (PR #655 review, item 3). Also make
v3Authored return a fresh descriptor per invocation so a caller
mutation cannot alter later contracts (review).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…eqlMatch

Rename the v3 query operations to the eql_v3.* function vocabulary they
lower to — eqlEq/eqlNeq/eqlIn/eqlNotIn/eqlGt/eqlGte/eqlLt/eqlLte/
eqlBetween/eqlNotBetween/eqlJsonContains, ordering via eqlAsc/eqlDesc.
The eql prefix (not bare eq/gt) is required because the framework's
native scope-field methods already claim eq/neq/gt/…, which lower to
plain SQL comparisons (PR #655 review, items 1+2). The v2 surface keeps
its cipherstash* names; the sets are now disjoint.

Free-text search is eqlMatch, not ilike: eql_v3.contains is fuzzy bloom
token matching, not SQL pattern semantics. Two guards run before
encryption: wildcard normalisation (leading/trailing % stripped,
interior % or any _ rejected) and the shared adapter-kit
matchNeedleError short-needle rejection. cipherstashNotIlike is removed
outright — negating a may-false-positive bloom test silently drops
matching rows.

Type-level dispatch mirrors the split: v3 codec entries carry only
cipherstash:v3-* marker traits, so eql* methods never surface on v2
columns and cipherstash* methods never surface on v3 columns.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- wire-v3: v3ToDriver coerces JSON.stringify's undefined branch to null;
  v3FromDriver gains overloads so a nullable input yields a nullable
  result instead of laundering null into T.
- sdk-adapter-v3: bulkDecrypt validates response cardinality (a
  truncated response would silently misalign plaintexts); the
  deliberate as-never bridges carry the repo's biome suppression.
- pack meta: extensionPacks.cipherstash now registers the 40 v3 codec
  metadata instances + storage rows (derived from the catalog, never
  hand-listed) and the EncryptedNumber type import, so consumer
  contracts describe the codecs their fields use.
- live suites: direct dotenv/config imports, canonical column-order
  binding in insertEncryptedRows (a differing key order could bind
  values to the wrong SQL columns), and migration-apply now uninstalls
  EQL v3 first so the customer-facing migration SQL genuinely executes
  on a reused database (test:live is serial now — the reinstall is
  destructive across files).
- bundling-isolation: entry checks scan the complete import graph for
  every v2 wire/codec-factory marker, not just entry bodies.

Verified live: 599 unit + 30 live-PG + 582 integration + 40 example-e2e
+ 7 README-walkthrough tests green against real ZeroKMS + Postgres.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… contract

Rename every v3 operator call to the EQL-derived vocabulary (eqlEq,
eqlMatch, eqlAsc/eqlDesc, …), fully qualify the README's domain table
(public.eql_v3_*), describe free-text search as fuzzy bloom token
matching under its real needle semantics, drop the removed
cipherstashNotIlike coverage, add a direct dotenv/config import to the
str-range suite, and remove the type-erasing probe assertions in the
bool suite. pnpm emit regenerates contract.{json,d.ts} — the
extensionPacks.cipherstash block now carries the 40 v3 codec instances
and storage rows.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Implement makePrismaNextAdapter() behind the shared
@cipherstash/test-kit IntegrationAdapter seam and run runFamilySuite
over every family — the same derived capability matrix, plaintext
oracle, single-vs-bulk insert crossover, ordering assertions, and
absent/null handling that pin the Drizzle and Supabase adapters (PR
#655 review, item 4). Queries lower through the real v3 operator
registry + postgres adapter + bulk-encrypt middleware; inserts run
one-row vs multi-row INSERT statements, prisma-next's two batching
shapes. New CI workflow mirrors the Drizzle job on both database
variants. 582 tests green against real ZeroKMS + live Postgres.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…sma-next skill

SKILL_MAP was typed Record<Integration, ...> but omitted the 'prisma-next'
key, so installSkills and the AGENTS.md builder hit SKILL_MAP['prisma-next']
= undefined and threw 'not iterable' for any repo the CLI detected as Prisma
Next. tsc catches the missing key, but the build (tsup) transpiles without
type-checking, so the error shipped in rc.2 — reachable by any Prisma user
running stash init (Prisma Next is auto-detected).

- Add the prisma-next SKILL_MAP entry.
- Route both consumers through a new skillsFor() helper that degrades an
  unmapped integration to the base skill set (stash-encryption + stash-cli)
  instead of crashing — belt-and-braces for the next Integration variant,
  since tsup won't type-check the map.
- New skills/stash-prisma-next/SKILL.md documenting the EQL v3 Prisma Next
  surface: domain-named column types (EncryptedTextSearch, EncryptedDoubleOrd,
  ...), cipherstashFromStackV3 wiring, runtime envelopes, the eql* operators,
  and EQL install via prisma-next migration apply (not stash eql install).
- Tests: SKILL_MAP has a non-empty entry for every integration; skillsFor
  falls back for an unmapped one; buildAgentsMdBody('prisma-next', ...) inlines
  the skill (the exact call that used to crash); installSkills copies it.
- Meta: AGENTS.md skill-map table + skills list updated.

Companion to #655 (the @cipherstash/prisma-next EQL v3 package) — that PR
releases the package but touches no CLI code, so this crash survives it.
Stacked on feat/prisma-next-eql-v3; retarget to main when #655 lands.

Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w
@coderdan
coderdan requested a review from a team as a code owner July 17, 2026 06:20
@changeset-bot

changeset-bot Bot commented Jul 17, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 29d0ea5

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 11 packages
Name Type
stash Minor
@cipherstash/basic-example Patch
@cipherstash/e2e Patch
@cipherstash/stack Minor
@cipherstash/stack-drizzle Minor
@cipherstash/stack-supabase Minor
@cipherstash/prisma-next Minor
@cipherstash/wizard Minor
@cipherstash/bench Patch
@cipherstash/test-kit Patch
@cipherstash/prisma-next-example Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b147c4d9-1d1f-4d24-99a9-62f8e2b83c8d

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/prisma-next-cli-skill

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a CLI crash when stash init/plan/impl detects a Prisma Next project by adding the missing prisma-next skills mapping, and introduces a new shipped agent skill documenting the Prisma Next EQL v3 surface.

Changes:

  • Add prisma-next to the CLI init SKILL_MAP and route skill resolution via a new resilient skillsFor() helper.
  • Inline/install the new stash-prisma-next skill during AGENTS.md building and skills installation, with regression tests for the prior “not iterable” crash.
  • Update repo meta docs and add a changeset to ship the new skill + fix.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
skills/stash-prisma-next/SKILL.md New Prisma Next EQL v3 integration skill content (schema types, runtime wiring, operators, migrations).
packages/cli/src/commands/init/lib/install-skills.ts Adds prisma-next mapping and skillsFor() fallback used by both installers and AGENTS.md builder.
packages/cli/src/commands/init/lib/build-agents-md.ts Switches skill inlining to use skillsFor() instead of direct SKILL_MAP indexing.
packages/cli/src/commands/init/lib/tests/install-skills.test.ts Adds tests ensuring all integrations have non-empty skills; tests skillsFor() fallback + prisma-next mapping.
packages/cli/src/commands/init/lib/tests/build-agents-md.test.ts Adds regression test ensuring prisma-next inlines the new skill (and doesn’t crash).
AGENTS.md Updates the published skill list and mapping table to include stash-prisma-next.
.changeset/stash-prisma-next-skill.md Changeset describing the CLI crash fix and new shipped skill.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread skills/stash-prisma-next/SKILL.md Outdated
Comment thread skills/stash-prisma-next/SKILL.md Outdated
Comment thread packages/cli/src/commands/init/lib/install-skills.ts Outdated
Comment on lines +48 to +67
```prisma
model User {
id String @id
email cipherstash.EncryptedTextSearch() // eq + range + free-text + ORDER BY
salary cipherstash.EncryptedDoubleOrd() // eq + range + ORDER BY
accountId cipherstash.EncryptedBigIntOrd() // eq + range + ORDER BY
birthday cipherstash.EncryptedDateOrd() // eq + range + ORDER BY
emailVerified cipherstash.EncryptedBoolean() // storage-only (no operators)
preferences cipherstash.EncryptedJson() // containment (@>)
}
```

| Column type | Domain | Query capability |
|---|---|---|
| `EncryptedTextSearch()` | `eql_v3_text_search` | equality, range, free-text, ORDER BY |
| `EncryptedDoubleOrd()` | `eql_v3_double_ord` | equality, range, ORDER BY |
| `EncryptedBigIntOrd()` | `eql_v3_bigint_ord` | equality, range, ORDER BY |
| `EncryptedDateOrd()` | `eql_v3_date_ord` | equality, range, ORDER BY |
| `EncryptedBoolean()` | `eql_v3_boolean` | storage-only (no operators) |
| `EncryptedJson()` | `eql_v3_json` | containment (`@>`) |

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just noticed that these type names don't actually match the EQLv3 names. The cipherstash namespace is sufficient to disambiguate.

// Do this
cipherstash.TextSearch()
// instead of
// cipherstash.EncryptedTextSearch()

That lines up with the other adapters.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, and it's inconsistent with the other adapters — I checked: Drizzle/stack use types.TextSearch(...) / types.DoubleOrd(...) (namespace types, no Encrypted prefix). So the type-name alignment is exactly your suggestion: drop Encryptedcipherstash.TextSearch(), cipherstash.DoubleOrd(), etc.

One nuance on the namespace: in Prisma the cipherstash. prefix comes from how the extension registers into the PSL, not a free-choice TS namespace like Drizzle's types. So renaming the types (drop Encrypted) is straightforward; whether the namespace can become eql. (vs cipherstash.) is a prisma-next PSL-registration question worth confirming with CJ.

This is a @cipherstash/prisma-next package change (CJ's), not a skill fix — the skill documents today's shipped name. When the rename lands I'll update the skill in lockstep.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in #685 (stacked on this PR): cipherstash.EncryptedTextSearch()cipherstash.TextSearch() etc., at the single v3PascalName seam. Namespace stays cipherstash (it's the extension vendor identity — eql would rewrite every codec id; and Drizzle uses types., not eql.). Package builds clean, authoring/psl suites green.


```typescript
import 'dotenv/config'
import { cipherstashFromStackV3 } from '@cipherstash/prisma-next/v3'

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMHO the V3 is redundant and should be dropped from the name. V3 is the default/standard. If we want to keep supporting V2 then that can get the special name.

Similarly, it can probably be important from the root barrel.

import { cipherstashFromStack } from '@cipherstash/prisma-next'

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense — if v3 is the standard, cipherstashFromStackV3cipherstashFromStack from the root barrel (@cipherstash/prisma-next), with the v2 path taking the special name. Note it's a breaking change for existing v2 consumers of cipherstashFromStack (from /stack), so it wants a coordinated rename + changeset in the package. Package change, not a skill fix; skill tracks current naming and updates with it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in #685: cipherstashFromStackV3cipherstashFromStack; the v2 fn becomes cipherstashFromStackV2 (your pick). Root-barrel export follows from /v3 as before.

Comment thread skills/stash-prisma-next/SKILL.md Outdated
Comment on lines +158 to +163
The envelope for a `double` column is `EncryptedNumber` (JS `number`); the schema
column type is `EncryptedDoubleOrd`. Envelope ↔ column pairing: `EncryptedString`
↔ `EncryptedTextSearch`, `EncryptedNumber` ↔ `EncryptedDoubleOrd`,
`EncryptedBigInt` ↔ `EncryptedBigIntOrd`, `EncryptedDate` ↔ `EncryptedDateOrd`,
`EncryptedBoolean` ↔ `EncryptedBoolean`, `EncryptedJson` ↔ `EncryptedJson`.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tbh I find it confusing that the envelope types don't match the EQL type names.

For example, you could have:

  • Database column type: eql_v3_text_search
  • Prisma schema type (current): EncryptedTextSearch
  • Prisma runtime type (current): EncryptedString

Can we just use the same names all the way through? All the Runtime types for the EQL text variants could be aliases of each other but at least they retain semantic meaning. In other words, I can reason about a type's capabilities based on the name (core EQL V3 principle).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a real ergonomics call, and nothing here is unsafe — the design deliberately separates two axes:

  • Runtime envelope (EncryptedString.from(x)) = the plaintext shape. It's a write-side value wrapper; its job is to carry a value of the right JS type (string/number/…) and redact on serialization. It doesn't need capability info because writing doesn't involve operators.
  • Column type (EncryptedTextSearch) = the capability. This is where search capability lives, and it's enforced at query time.

They don't line up 1:1 on purpose: multiple domains share a plaintext shape (text, text_eq, text_ord, text_search all take a string), so today they share one envelope (EncryptedString). Giving each domain its own envelope would be ~40 classes that mostly alias, to make the write path carry a distinction it never uses.

So 'same names all the way through' is a legitimate preference for readability, but the safety you'd get from it is already present on the column/operator layer (see my reply on line 190/198). Worth a design chat with CJ; not a correctness issue.

Comment on lines +170 to +190
| Operator | Meaning | Requires |
|---|---|---|
| `eqlEq(v)` / `eqlNeq(v)` | equality / inequality | any searchable domain |
| `eqlIn(vs)` / `eqlNotIn(vs)` | membership | any searchable domain |
| `eqlMatch(term)` | free-text token match (`eql_v3.contains`) | `EncryptedTextSearch` |
| `eqlGt/eqlGte/eqlLt/eqlLte(v)` | range comparison | an `*Ord` domain |
| `eqlBetween(lo,hi)` / `eqlNotBetween(lo,hi)` | range window | an `*Ord` domain |
| `eqlAsc()` / `eqlDesc()` | ORDER BY | an `*Ord` or `TextSearch` domain |
| `eqlJsonContains(obj)` | encrypted JSON containment (`@>`) | `EncryptedJson` |

```typescript
// range
await db.orm.public.User.where((u) => u.salary.eqlGt(100_000)).all()
// free-text
await db.orm.public.User.where((u) => u.email.eqlMatch('example.com')).all()
// between
await db.orm.public.User.where((u) => u.birthday.eqlBetween(lo, hi)).all()
// bigint membership
await db.orm.public.User.where((u) => u.accountId.eqlIn([100_000_000_001n])).all()
// encrypted JSON containment
await db.orm.public.User.where((u) => u.preferences.eqlJsonContains({ theme: 'dark' })).all()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is why I think using runtime types for each EQL variant would be better. Because you'd get type-checked capabilities. You should never be able to call eqlEq on an EncryptedString if it doesn't map to the eql_v3_text_search (and instead maps to eql_v3_text which is the non-searchable type).

IMHO we should name the runtime types the same as the schema types and only implement the operators for the types that support them.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The capability type-checking you're describing already exists — but on the column expression, not the runtime envelope. You never call operators on EncryptedString; you call them on u.email (the column accessor from the Contract type), which is domain-specific and capability-typed.

Concretely: eqlEq is typed self: { traits: V3EqualityMarker }, eqlGtV3OrderAndRangeMarker, eqlMatchV3FreeTextSearchMarker (operation-types.ts), and each column's codec carries only the traits its capabilities grant (v3TraitsForCapabilities). A storage-only column carries no operator traits, so those methods don't exist on it → calling eqlEq on it is a compile error. That's exactly your 'never be able to call eqlEq on a non-searchable column' — it's already the case, just keyed off the column type rather than the envelope.

Comment on lines +196 to +198
Applying an operator its domain doesn't support (e.g. `eqlGt` on a
storage-only `EncryptedBoolean`, or `eqlMatch` on a non-text domain) is a typed
error at build time, not a runtime surprise.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My read of the code is that this wouldn't actually be possible at the moment. Maybe I'm missing something, but at the very least, we should have tests to cover it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is possible today, and the tests you're asking for already exist — packages/prisma-next/test/operation-types.types.test-d.ts has @ts-expect-error assertions for exactly this: eqlEq rejected on storage-only eql_v3_boolean/eql_v3_text, eqlMatch rejected on eql_v3_text_eq, eqlGt rejected on eql_v3_double_eq. I ran tsc against them — every @ts-expect-error is satisfied (none 'unused'), so each really is a compile error. There's also a runtime guard suite (operator-gating-v3.test.ts) for defense in depth.

However, you're right to want it guaranteed: the package typecheck is currently red (~38 unrelated implicit-any errors) and isn't wired into any CI workflow — so those .test-d.ts assertions aren't enforced on every change. Filed #684 to green the typecheck and gate it in CI, which turns 'true today' into 'can't regress'.

coderdan added 2 commits July 17, 2026 17:13
…#683

- init does NOT scaffold wiring for Prisma Next (it skips the
  encryption-client scaffold — schema is derived from contract.json);
  say so instead of 'scaffolds the wiring'. (Copilot)
- eqlAsc/eqlDesc are free functions taking the column expression
  (eqlAsc(u.salary)), not zero-arg methods; fix the ORDER BY table row
  to match the code example. (Copilot)
- install-skills.ts: the skillsFor regression guard is a test asserting
  SKILL_MAP has an entry for every value in a maintained ALL_INTEGRATIONS
  list, not one that reads the provider registry; correct the comment. (Copilot)

Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w
Prisma Next installs the EQL bundle through its own migration ledger
(migrations/cipherstash/ applied by `prisma-next migration apply`), so the
standalone installer is the wrong tool — running it applies EQL out-of-band
from the framework's ledger. `stash init --prisma-next` already skips the
installer; this closes the manual-invocation hole.

- New pure `prismaNextInstallGuard(cwd, { force })` (mirrors
  validateInstallFlags): returns actionable guidance when a Prisma Next
  project is detected and --force isn't set, else null. Called early in
  installCommand, before any DB I/O — fails fast with a pointer to
  `prisma-next migration apply`.
- --force overrides (deliberate standalone install escape hatch).
- messages.eql.prismaNextDetected as the stable leader.
- Tests: 4 unit (config-file detection, dep detection, --force override,
  non-prisma-next passthrough) + 1 pty-less e2e proving the wiring (refuses,
  exit 1, no DB). Skill gotcha + changeset updated.

Addresses CJ/Dan's #683 review ask to enforce this in the CLI.

Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants