fix(cli): prisma-next init/plan/impl crash + stash-prisma-next skill (rc.2 M1)#683
fix(cli): prisma-next init/plan/impl crash + stash-prisma-next skill (rc.2 M1)#683coderdan wants to merge 22 commits into
Conversation
…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
🦋 Changeset detectedLatest commit: 29d0ea5 The changes in this PR will be included in the next version bump. This PR includes changesets to release 11 packages
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 |
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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-nextto the CLI initSKILL_MAPand route skill resolution via a new resilientskillsFor()helper. - Inline/install the new
stash-prisma-nextskill duringAGENTS.mdbuilding 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.
| ```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 (`@>`) | |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 Encrypted → cipherstash.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.
There was a problem hiding this comment.
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' |
There was a problem hiding this comment.
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'
There was a problem hiding this comment.
Makes sense — if v3 is the standard, cipherstashFromStackV3 → cipherstashFromStack 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.
There was a problem hiding this comment.
Done in #685: cipherstashFromStackV3 → cipherstashFromStack; the v2 fn becomes cipherstashFromStackV2 (your pick). Root-barrel export follows from /v3 as before.
| 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`. | ||
|
|
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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.
| | 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() |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 }, eqlGt → V3OrderAndRangeMarker, eqlMatch → V3FreeTextSearchMarker (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.
| 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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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'.
…#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
06df347 to
b6ff7c4
Compare
What
Companion to #655. #655 releases the
@cipherstash/prisma-nextEQL v3 package but touches no CLI code — so the CLI-side Prisma Next experience is still broken. This PR fixes it:SKILL_MAPis typedRecord<Integration, …>but omitted the'prisma-next'key.installSkillsand the AGENTS.md builder doSKILL_MAP[integration]→undefined→not iterablefor any repo the CLI detects as Prisma Next.tsccatches the missing key, but the build (tsup) transpiles without type-checking, so it shipped in rc.2 — reachable by any Prisma user runningstash init(Prisma Next is auto-detected viadetectPrismaNext).stash-prisma-nextskill documenting the EQL v3 surface: domain-named column types (EncryptedTextSearch,EncryptedDoubleOrd, …),cipherstashFromStackV3wiring, the runtime value envelopes, theeql*query operators, and EQL install viaprisma-next migration apply(notstash 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
prisma-nextSKILL_MAPentry (stash-encryption,stash-prisma-next,stash-cli).skillsFor()helper that degrades an unmapped integration to the base skill set instead of crashing — belt-and-braces for the nextIntegrationvariant, sincetsupwon't type-check the map. (The real gap it papers over: the CLI package has notsc --noEmitgate in CI.)Tests
SKILL_MAPhas a non-empty entry for every integration (the runtime guardtsccan'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 thrownot iterable.installSkills(…, 'prisma-next')copiesstash-prisma-next/SKILL.md(verified end-to-end).code:checkclean.Ordering
Stacked on
feat/prisma-next-eql-v3so the skill reflects the released v3 API; retarget tomainwhen #655 lands (GitHub does this automatically). The skill defersstash envflag detail to thestash-cliskill so it's correct regardless of #682's merge order.Closes the rc.2 M1 finding.
https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w