Context
The EQL v3 install migrations in @cipherstash/stack-prisma bake the install SQL into their ops.json at emit time. Each baked copy is digest-verified against the @cipherstash/eql release manifest (installSqlSha256).
This replaced a runtime-injection design. That design produced environment-dependent migration hashes, which wedged consumer repos (PN-MIG-5002) on every EQL bump. Its recomputed hash could also never detect a tampered EQL package.
Three guards from that work are in CI and need no further action:
- Provenance pin — the committed SQL's sha256 must equal the installed
@cipherstash/eql manifest's digest. An EQL bump without the matching new migration fails.
- Frozen-hash pins — published migration hashes are hard-coded in tests. Any byte change to published history fails.
- Vendored-space parity — the example's
migrations/cipherstash/ must be byte-identical to the shipped artefacts.
This issue tracks the follow-ups. It was reviewed against main at 013e3ff3 on 2026-09-07, and the body below is the state after that review. Two of the original five tasks have changed materially, so do not work from the pre-review text in the history.
Landed since this was filed
Do not redo these.
- CLI bundle digest verification — #885 (
801868d1). packages/cli/src/installer/bundle-digest.ts hashes the bundle and refuses on mismatch before any database connection opens, so a refusal means nothing was attempted. It covers all three readers: eql install, the SQL embedded by eql migration, and the expected-surface baseline for eql verify. It caught a real skew — a tree declaring 3.0.5 whose bundle hashed 7ad9c9f8… against npm's accde0030…. Task 2 below is what remains of this.
- Dependency-safe EQL reinstall — #961, closing #918 / CIP-3844.
stash eql install and stash eql upgrade now capture dependent functional indexes, replace the schemas, then rebuild and verify the indexes in one transaction, rolling back on failure. They refuse before mutation when a customer object cannot be reconstructed — views, policies, constraints, generated columns, triggers, customer operators, partitioned index trees. Recorded as accepted ADR docs/adr/0001-eql-data-survives-disposable-schema-reinstall.md. This removed most of the original upgrade-window task, and created task 1.
- EQL monorepo absorption — #885. This removed the Dependabot problem originally described here. See task 3 for what replaced it.
Remaining work
1. Correct the stale index-loss guidance in the shipped skills
skills/stash-indexing/SKILL.md:269 still says that stash eql upgrade and eql install --force cascade-drop every functional index, and that the reader must add a new migration to recreate them. skills/stash-prisma/SKILL.md:325 still cites #918 as open. Both files predate the safe-reinstall work.
These files ship inside the stash tarball and installSkills() copies them into customer repos. The guidance is therefore wrong in someone else's codebase today. It tells an operator to hand-write recovery DDL that the CLI now performs and verifies.
Scope the claim rather than delete it. It stays true for two paths that do not go through the CLI: the Prisma Next migration path, and a bundle applied by hand (for example supabase db push). The stash-supabase and stash-cli warnings are already framed that way and read correctly.
This is the most urgent item here, because it is the only one already affecting users.
2. Anchor the CLI's bundle digest outside the EQL package (pre-GA, security)
assertBundledEqlSqlDigest compares the EQL package's SQL against releaseManifest.installSqlSha256 from that same package. An attacker who edits the SQL also edits the manifest, and the check passes. The changeset for #885 is explicit that it protects against "a corrupt, partially-updated, or tampered package" in a broken dependency tree, not against a hostile one.
stack-prisma already has the strong form: frozen digest literals in PUBLISHED_MIGRATIONS (test/v3/migration-v3.test.ts:120-173) over SQL baked into committed ops.json. The CLI has no equivalent, because it deliberately keeps @cipherstash/eql external at bundle time. Its only anchor is the lockfile.
Two unverified paths also remain:
packages/stack-prisma/test/live/helpers/eql-v3.ts:77 runs raw readInstallSql() through the re-export at src/migration/eql-bundle-v3.ts:68. It is a live-test helper, not shipped runtime code, but it bypasses the check that exists.
packages/stack/scripts/install-eql-v3.ts imports a helper file that does not exist, so the db:eql-v3:install script is dead.
Work: add a frozen-digest constant or test to packages/cli, mirroring PUBLISHED_MIGRATIONS. Point the live helper at readVerifiedInstallSql(), or remove the raw re-export so the bypass is impossible. Delete or repair the dead packages/stack script.
Two premises in the original text were wrong. readV3InstallSql never existed — the stale stack-prisma comment that named it was corrected in 801868d1. installEqlV3IfNeeded is not in packages/stack; the only definition is the stack-prisma live-test helper above.
3. Keep Dependabot away from @cipherstash/eql-upgrade-baseline
The original ask was to add @cipherstash/eql to the Dependabot ignores. That entry would now be dead config. Every dependant declares the package as workspace:* or workspace:^ (packages/cli, packages/stack, packages/stack-prisma, packages/protect-ffi/integration-tests), enforced by scripts/lint-no-eql-registry-pins.mjs. Nothing resolves it from the registry, so Dependabot cannot raise the red PR this issue was written about. pnpm-workspace.yaml:178-184 records the same conclusion for its own removed entry.
One registry-resolved EQL pin does remain, and it is not ignored. packages/cli/package.json:78 carries "@cipherstash/eql-upgrade-baseline": "npm:@cipherstash/eql@3.0.2" (pnpm-lock.yaml:252). It is the single sanctioned exemption in lint-no-eql-registry-pins.mjs:186-193, and upgrade-encrypted-indexes.live.test.ts consumes it.
It is an immutable baseline by design: the live test must install a genuinely previously-released bundle before the workspace installer upgrades it. A Dependabot bump would not fail a provenance check — it would quietly destroy the property that makes the test meaningful. Add it to the ignore block at .github/dependabot.yml:47,56, with a comment saying why, and note it in skills/stash-supply-chain-security/SKILL.md, which currently describes the auth lockstep policy and mentions neither this pin nor the provenance rule.
4. Document the upgrade window
The index-loss half of this is now handled mechanically (see "Landed") and documented in packages/eql/docs/upgrading/v3.0.5.md, packages/eql/README.md, and four skills. Two gaps remain.
The in-flight-query failure is documented nowhere. No surface in the repo tells a customer that queries against eql_v3.* fail while the replacement transaction runs, or how long that takes. The number exists internally — derived-search-index-restoration.ts:11-13 puts it at 10–30s against a local container and longer on managed Postgres — and has never reached a user. What the docs do say is about concurrent DDL and the advisory lock, which is a different hazard.
Nothing warns at the point of use. packages/cli/src/commands/db/upgrade.ts prints no destructive-operation notice and asks for no confirmation. Its --dry-run branch says only that it "would re-run the pinned EQL v3 install SQL against the database", naming neither the DROP, the indexes, the grants, nor the window.
The docs-site sentence originally asked for here is still unwritten. cipherstash.com/docs is outside this repo and was not checked in the review.
5. Decide the re-baseline cadence
The cost this predicted has arrived. packages/stack-prisma/migrations/ now holds four baked migrations totalling ~10.8 MB — genesis, 3.0.2, 3.0.4 and 3.0.5, each about 2.7 MB. src/exports/control.ts imports all four, so all four ship in the tarball whatever the files array says. Every EQL release adds one more, and one more replayed install on a fresh database.
The economics are argued in docs/plans/2026-08-13-eql-monorepo-absorption.md:187, but that entry defers the decision rather than making it. The 3.0.5 baseline re-emit was taken as a one-time exception, while @cipherstash/stack-prisma@1.0.0 was 14 days old at roughly 253 monthly downloads. The note says the trade "should be re-argued on adoption numbers at the next bump, not defaulted to". That bump has since happened and the argument was not had.
No cadence rule exists anywhere in the repo — no "every N releases", no size budget, no adoption threshold, and no statement about retaining the old chain for existing markers. The append-only rule any re-baseline must bend is at packages/stack-prisma/DEVELOPING.md:353-360. Decide the trigger now, as scheduled maintenance, rather than in reaction to a tarball complaint.
6. Record a db ref in examples/prisma
A second prisma-next migration plan in the example replans the whole schema from scratch. The diagnosis is confirmed against the vendored upstream source: resolveFromForPlan (@prisma-next/cli/src/utils/plan-resolution.ts:189-206) returns { kind: 'greenfield', fromHash: null } when there is no --from flag and no db ref.
examples/prisma/migrations/app/ contains only 20260714T2142_initial. There is no refs/ directory, and no db.json has ever existed anywhere in this repo's history. The example's only ref is migrations/cipherstash/refs/head.json, which is a head ref. ref set is invoked nowhere in the repo, though the upstream command exists (@prisma-next/cli/src/commands/ref.ts) and migration:ref is deprecated in its favour. The example README documents one plan step and no ref step.
This affects anyone using the example as a template. It also blocks the pending two-model include() e2e test — though that test needs more than a ref: examples/prisma/prisma/schema.prisma declares one model (User) with no relations, so include() has nothing to traverse. Growing the example schema is a prerequisite.
Out of scope
More machinery around the seed phase or hash scheme. Each failure mode found is covered by a specific tripwire above, and the content-addressing makes the rest structurally impossible.
Context
The EQL v3 install migrations in
@cipherstash/stack-prismabake the install SQL into theirops.jsonat emit time. Each baked copy is digest-verified against the@cipherstash/eqlrelease manifest (installSqlSha256).This replaced a runtime-injection design. That design produced environment-dependent migration hashes, which wedged consumer repos (
PN-MIG-5002) on every EQL bump. Its recomputed hash could also never detect a tampered EQL package.Three guards from that work are in CI and need no further action:
@cipherstash/eqlmanifest's digest. An EQL bump without the matching new migration fails.migrations/cipherstash/must be byte-identical to the shipped artefacts.This issue tracks the follow-ups. It was reviewed against
mainat013e3ff3on 2026-09-07, and the body below is the state after that review. Two of the original five tasks have changed materially, so do not work from the pre-review text in the history.Landed since this was filed
Do not redo these.
801868d1).packages/cli/src/installer/bundle-digest.tshashes the bundle and refuses on mismatch before any database connection opens, so a refusal means nothing was attempted. It covers all three readers:eql install, the SQL embedded byeql migration, and the expected-surface baseline foreql verify. It caught a real skew — a tree declaring 3.0.5 whose bundle hashed7ad9c9f8…against npm'saccde0030…. Task 2 below is what remains of this.stash eql installandstash eql upgradenow capture dependent functional indexes, replace the schemas, then rebuild and verify the indexes in one transaction, rolling back on failure. They refuse before mutation when a customer object cannot be reconstructed — views, policies, constraints, generated columns, triggers, customer operators, partitioned index trees. Recorded as accepted ADRdocs/adr/0001-eql-data-survives-disposable-schema-reinstall.md. This removed most of the original upgrade-window task, and created task 1.Remaining work
1. Correct the stale index-loss guidance in the shipped skills
skills/stash-indexing/SKILL.md:269still says thatstash eql upgradeandeql install --forcecascade-drop every functional index, and that the reader must add a new migration to recreate them.skills/stash-prisma/SKILL.md:325still cites #918 as open. Both files predate the safe-reinstall work.These files ship inside the
stashtarball andinstallSkills()copies them into customer repos. The guidance is therefore wrong in someone else's codebase today. It tells an operator to hand-write recovery DDL that the CLI now performs and verifies.Scope the claim rather than delete it. It stays true for two paths that do not go through the CLI: the Prisma Next migration path, and a bundle applied by hand (for example
supabase db push). Thestash-supabaseandstash-cliwarnings are already framed that way and read correctly.This is the most urgent item here, because it is the only one already affecting users.
2. Anchor the CLI's bundle digest outside the EQL package (pre-GA, security)
assertBundledEqlSqlDigestcompares the EQL package's SQL againstreleaseManifest.installSqlSha256from that same package. An attacker who edits the SQL also edits the manifest, and the check passes. The changeset for #885 is explicit that it protects against "a corrupt, partially-updated, or tampered package" in a broken dependency tree, not against a hostile one.stack-prismaalready has the strong form: frozen digest literals inPUBLISHED_MIGRATIONS(test/v3/migration-v3.test.ts:120-173) over SQL baked into committedops.json. The CLI has no equivalent, because it deliberately keeps@cipherstash/eqlexternal at bundle time. Its only anchor is the lockfile.Two unverified paths also remain:
packages/stack-prisma/test/live/helpers/eql-v3.ts:77runs rawreadInstallSql()through the re-export atsrc/migration/eql-bundle-v3.ts:68. It is a live-test helper, not shipped runtime code, but it bypasses the check that exists.packages/stack/scripts/install-eql-v3.tsimports a helper file that does not exist, so thedb:eql-v3:installscript is dead.Work: add a frozen-digest constant or test to
packages/cli, mirroringPUBLISHED_MIGRATIONS. Point the live helper atreadVerifiedInstallSql(), or remove the raw re-export so the bypass is impossible. Delete or repair the deadpackages/stackscript.Two premises in the original text were wrong.
readV3InstallSqlnever existed — the stalestack-prismacomment that named it was corrected in801868d1.installEqlV3IfNeededis not inpackages/stack; the only definition is thestack-prismalive-test helper above.3. Keep Dependabot away from
@cipherstash/eql-upgrade-baselineThe original ask was to add
@cipherstash/eqlto the Dependabot ignores. That entry would now be dead config. Every dependant declares the package asworkspace:*orworkspace:^(packages/cli,packages/stack,packages/stack-prisma,packages/protect-ffi/integration-tests), enforced byscripts/lint-no-eql-registry-pins.mjs. Nothing resolves it from the registry, so Dependabot cannot raise the red PR this issue was written about.pnpm-workspace.yaml:178-184records the same conclusion for its own removed entry.One registry-resolved EQL pin does remain, and it is not ignored.
packages/cli/package.json:78carries"@cipherstash/eql-upgrade-baseline": "npm:@cipherstash/eql@3.0.2"(pnpm-lock.yaml:252). It is the single sanctioned exemption inlint-no-eql-registry-pins.mjs:186-193, andupgrade-encrypted-indexes.live.test.tsconsumes it.It is an immutable baseline by design: the live test must install a genuinely previously-released bundle before the workspace installer upgrades it. A Dependabot bump would not fail a provenance check — it would quietly destroy the property that makes the test meaningful. Add it to the ignore block at
.github/dependabot.yml:47,56, with a comment saying why, and note it inskills/stash-supply-chain-security/SKILL.md, which currently describes the auth lockstep policy and mentions neither this pin nor the provenance rule.4. Document the upgrade window
The index-loss half of this is now handled mechanically (see "Landed") and documented in
packages/eql/docs/upgrading/v3.0.5.md,packages/eql/README.md, and four skills. Two gaps remain.The in-flight-query failure is documented nowhere. No surface in the repo tells a customer that queries against
eql_v3.*fail while the replacement transaction runs, or how long that takes. The number exists internally —derived-search-index-restoration.ts:11-13puts it at 10–30s against a local container and longer on managed Postgres — and has never reached a user. What the docs do say is about concurrent DDL and the advisory lock, which is a different hazard.Nothing warns at the point of use.
packages/cli/src/commands/db/upgrade.tsprints no destructive-operation notice and asks for no confirmation. Its--dry-runbranch says only that it "would re-run the pinned EQL v3 install SQL against the database", naming neither the DROP, the indexes, the grants, nor the window.The docs-site sentence originally asked for here is still unwritten. cipherstash.com/docs is outside this repo and was not checked in the review.
5. Decide the re-baseline cadence
The cost this predicted has arrived.
packages/stack-prisma/migrations/now holds four baked migrations totalling ~10.8 MB — genesis, 3.0.2, 3.0.4 and 3.0.5, each about 2.7 MB.src/exports/control.tsimports all four, so all four ship in the tarball whatever thefilesarray says. Every EQL release adds one more, and one more replayed install on a fresh database.The economics are argued in
docs/plans/2026-08-13-eql-monorepo-absorption.md:187, but that entry defers the decision rather than making it. The 3.0.5 baseline re-emit was taken as a one-time exception, while@cipherstash/stack-prisma@1.0.0was 14 days old at roughly 253 monthly downloads. The note says the trade "should be re-argued on adoption numbers at the next bump, not defaulted to". That bump has since happened and the argument was not had.No cadence rule exists anywhere in the repo — no "every N releases", no size budget, no adoption threshold, and no statement about retaining the old chain for existing markers. The append-only rule any re-baseline must bend is at
packages/stack-prisma/DEVELOPING.md:353-360. Decide the trigger now, as scheduled maintenance, rather than in reaction to a tarball complaint.6. Record a
dbref inexamples/prismaA second
prisma-next migration planin the example replans the whole schema from scratch. The diagnosis is confirmed against the vendored upstream source:resolveFromForPlan(@prisma-next/cli/src/utils/plan-resolution.ts:189-206) returns{ kind: 'greenfield', fromHash: null }when there is no--fromflag and nodbref.examples/prisma/migrations/app/contains only20260714T2142_initial. There is norefs/directory, and nodb.jsonhas ever existed anywhere in this repo's history. The example's only ref ismigrations/cipherstash/refs/head.json, which is aheadref.ref setis invoked nowhere in the repo, though the upstream command exists (@prisma-next/cli/src/commands/ref.ts) andmigration:refis deprecated in its favour. The example README documents one plan step and no ref step.This affects anyone using the example as a template. It also blocks the pending two-model
include()e2e test — though that test needs more than a ref:examples/prisma/prisma/schema.prismadeclares one model (User) with no relations, soinclude()has nothing to traverse. Growing the example schema is a prerequisite.Out of scope
More machinery around the seed phase or hash scheme. Each failure mode found is covered by a specific tripwire above, and the content-addressing makes the rest structurally impossible.