fix(eql): the four blockers from #885, and the upstream 3.0.5 sync - #915
fix(eql): the four blockers from #885, and the upstream 3.0.5 sync#915tobyhede wants to merge 26 commits into
Conversation
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.3.0 to 4.3.1. - [Changelog](https://github.com/nodeca/js-yaml/blob/4.3.1/CHANGELOG.md) - [Commits](nodeca/js-yaml@4.3.0...4.3.1) --- updated-dependencies: - dependency-name: js-yaml dependency-version: 4.3.1 dependency-type: direct:development ... Signed-off-by: dependabot[bot] <support@github.com>
…aml-4.3.1 build(deps-dev): bump js-yaml from 4.3.0 to 4.3.1
…-alias Restore deprecated ste_vec_contains compatibility aliases
Signed-off-by: Toby Hede <toby@cipherstash.com>
Signed-off-by: Toby Hede <toby@cipherstash.com>
The jsonwebtoken 10.4.0 bump re-resolved winapi-util's windows-sys edge from 0.61.2 down to 0.48.0. winapi-util declares a permissive range (>=0.48.0, <=0.61.*), so both satisfy it and cargo is free to pick either; pin the lockfile back to 0.61.2. Inert in practice - the edge is cfg(windows)-gated and CI is Ubuntu only - but there is no reason to build against three-generation-older bindings. Signed-off-by: Toby Hede <toby@cipherstash.com>
fix(deps): patch postcss to 8.5.26
fix(deps): bump jsonwebtoken to 10.4.0
Version Packages
Two defects that only fire after a merge, found in PR review. THE VERSION HOOK COULD NOT RUN. `release.yml` passes `version: pnpm run version`, which chains `scripts/sync-lockstep-versions.mjs` -> `mise run release:prepare_bindings_assets` -> `cargo run -p eql-codegen`. The `release` job installed pnpm, Node and node-gyp and nothing else; mise is not on GitHub's ubuntu images. `.changeset/` already holds live changesets, so changesets/action takes its version branch on the next push to main regardless of this PR — `pnpm run version` would have died with ENOENT and no Version Packages PR would have been opened at all. `release-version-hook.test.mjs` asserted the wiring existed; nothing asserted the job could execute it. `workflow-mise-setup.test.mjs` now resolves command chains — action input -> npm script -> spawned binary — and requires a mise setup step at a lower index in any job that reaches one. `add_shims_to_path: false` is load-bearing and would not survive a copy-paste from test-eql.yml: mise's shim dir is prepended to PATH for every later step, so mise's pinned Node 22 would shadow setup-node's and `changeset publish` would shell out to its bundled npm 10.x, below the 11.5.1 that OIDC trusted publishing requires. `changeset publish` HAS NO IDEA WHAT IT IS DOING. It publishes every public workspace package whose version is absent from npm — changeset or no changeset — with `Promise.all` and no dependency ordering, and `publishAPackage` returns a failure rather than throwing it. So a package that cannot publish does not stop the siblings that depend on it: they ship pointing at a version nobody can install. `publishBlockers` in release-gate.mjs answers that question before anything publishes and exits non-zero, which fails `gate` and skips `release` entirely. `FROZEN_PUBLISHERS` has to be written down rather than derived: npm exposes no way to ask "would a publish from here be accepted?", and provenance on the last release is evidence about the past, not permission for the next. Its FFI half turns lint-no-ffi-changeset.mjs's unstated assumption — that all seven are already on npm at the workspace version — into a checked one, which is precisely what the hand-applied EQL 3.0.5 bump showed it needed. The third verdict in the range check is NOT SAFE, MERELY NOT WORSE: a dependency being published in the same release is allowed, because that is every ordinary release here, and changesets' lack of ordering leaves a window nothing at this layer can close. What it can do is separate that transient window from a permanent hole. Also: `changeset:version` was a bare `changeset version` that skipped the lockstep sync while AGENTS.md documented it as the release flow, and `changeset:publish` skipped the build the same way. Both now route through the wrappers, and the test states the rule over the scripts rather than over the two names. packages/protect-ffi/Cargo.lock was stale (`cargo metadata --locked` exit 101): it records eql-bindings, whose version sync-lockstep-versions.mjs moves, and nothing refreshed it — `packages/eql`'s lock is refreshed as a side effect of the SQL build, that one by nothing at all. The script now discovers every lock recording the crate from a path and refreshes each.
… that fires
THE CACHE KEYS COULD NOT SEE THE PATH DEP. Phase 3 pointed protect-ffi at
`eql-bindings = { path = "../../../eql/crates/eql-bindings" }`, making it a
compile input to index.node and to the wasm build — but both keys in
build-ffi-binding hashed only `packages/protect-ffi/**`. A src-only edit
there changes no hashed file (path deps carry no lock checksum), so the
cache hits, the cargo step is skipped, and every credentialed job runs a
stale binding. This contradicts the action's own stated invariant, and this
PR demonstrated it: the 3.0.4 -> 4.0.0 -> 3.0.5 bumps moved neither key.
Scoped to `packages/eql/crates/**` rather than the bindings crate alone.
That crate has no path deps of its own TODAY, so the narrow glob would be
exact — and would silently stop being exact the moment it gains one, which
is the same hole one level down. `packages/eql/Cargo.toml` is in because
three of its four siblings inherit `[lints] workspace = true`, so the
workspace root is one key away from being a compile input with no other
trace. The discovery test follows path deps transitively and asserts every
git-tracked file under each is covered, so a future dep is caught the day
it lands rather than the day it breaks.
THE PG 14-16 MATRIX RAN NEVER. test-eql.yml gated the full matrix behind
`merge_group` alone and dropped `push:` on the justification that "under a
required merge queue, push-to-main validation is redundant". Every clause
of that is false here, checked against the live API: `mergeQueue(branch:
"main")` is null, `branches/main/protection` 404s, and the active ruleset
carries no `required_status_checks` rule — so `ci-required` is required by
nothing and `merge_group` is an event this repo never emits. Both failures
are invisible, because an event that is never delivered produces no runs to
notice the absence of.
`setup` is now an EXCLUSION — the full matrix is the default and the PR
fast lane is the special case — so a trigger added later gets more coverage
rather than silently none. Every gated job's `if:` was inverted to match.
`push` carries a paths filter (nothing waits on a push run, so a skip costs
nothing) and `schedule` backstops what the filter cannot see; the expensive
axis is the fan-out, not the trigger.
The fail-fast audit found SEVEN multi-command `run:` blocks across three
workflows lacking `set -e` under `shell: bash {0}`, not the one reported.
The worst is macro-expand-eql's `mise run test:matrix:expand` followed by
`git diff --exit-code` on the files it regenerates: when the regeneration
breaks, the snapshots are untouched, the diff is clean, and the job goes
green having verified that a build which never ran produced no drift.
integration-drizzle and integration-protect-ffi gained the EQL crate paths
because adding them to the WASM cache key makes them WASM build inputs, and
wasm-build-inputs-paths-filter.test.mjs enforces that relationship — both
run build-ffi-binding with `wasm: 'true'` and load protect_ffi_inline.js.
The guard exists so that a mise task invoking cargo cannot go unreached by
CI — "a check nothing invokes reads exactly like a check that passes". Its
scan missed 41 of 73 tasks, so the property held over less than half the
surface it named.
Three blind spots, established empirically against mise in a scratch
project rather than inferred:
* BARE TABLE HEADERS. In a config pulled in by `[task_config].includes`,
every top-level table is a task — `["postgres:up"]`, not
`[tasks."postgres:up"]`, which is a hard parse error there. The regex
required the latter, so tasks/postgres.toml and tasks/fixtures.toml
contributed 0 of their 6.
* FILE TASKS. `includes = ["tasks", ...]` makes every executable under
tasks/** a task. None were parsed. The inclusion rule is the executable
bit and nothing else — it correctly excludes the `source`d helpers, the
`bash tasks/...`-invoked scripts, *.sql and *.txt, while including
githooks/pre-commit (no extension) and xml-to-json.py.
* NAME COLLISIONS. Where a file task's stem is already claimed by a TOML
task, mise keeps the extension: tasks/test/known-failures.sh becomes
`test:known-failures.sh`. Collapsing those would have silently replaced
the task CI actually runs.
MUTATION-PROVEN: deleting bench-eql.yml passed all 14 tests. `test:bench`
is a file task, so removing its only caller was invisible — the exact
"suite goes quiet" failure this file exists to prevent. That case is now a
permanent test: `reachableFrom` takes the workflow list as a parameter, so
the mutation is injected rather than performed, and a SOLE_CALLER_WORKFLOWS
equality recomputes reachability with each workflow removed in turn.
The floors could not have caught any of it — mise.toml alone carries 32
tasks against a `>= 30` floor. Replaced with per-source assertions: every
config in `includes` must contribute at least one task, and so must the
file walk.
Two defects found while fixing it. The naive `mise run` body scan created
FALSE reachability, staling `test:sqlx`'s exemption off a comment in
fixtures.toml; and scriptBodies inlined scripts named in `#MISE sources`
comments, falsely clearing `release:prepare-bindings-assets`. Of 56 `mise
run` occurrences, only 20 are commands.
Four new orphans, all exempted with reasons. `test:lint` is not a reflexive
exemption: it is `cargo fmt --check` scoped to tests/sqlx, and `cargo fmt
--check -v` at the workspace root lists those files, so `test:crates` is a
strict superset. `docs:generate:json` gets a new self-expiring category —
blocked on an unported workflow — which the staleness check will demand be
removed the day _build-docs.yml is ported.
…e subtree THE LINTER NEVER OPENED pnpm-workspace.yaml. It walked every Cargo.toml and package.json, which is where a pin is DECLARED — but pnpm resolves `overrides` and `catalogs` from the workspace file, and that file's own comment says so. An override there moves what actually installs while every `workspace:^` in the tree still reads correct, which is the quietest possible way to reintroduce the skew between the Rust that emits an EQL payload and the SQL that stores one. It now reads `overrides`, `catalog` and `catalogs.<name>`, recurses into nested npm overrides, and matches selector keys (`@cipherstash/eql@<3.0.5`, `pkg>@cipherstash/eql`, yarn's `**/` globs) and aliased values (`npm:@cipherstash/eql@3.0.4`). Two judgements worth recording. A `catalog:` specifier in a manifest stays an OFFENDER — unlike Cargo's `workspace = true`, a catalog holds version ranges, so there is no in-tree answer to defer to and both ends are flagged. And YAML scalars are coerced rather than dropped, because `'@cipherstash/eql': 3.0` parses as the NUMBER 3 under YAML 1.1 — dropping non-strings would drop exactly that pin. `EXPECTED_SOURCES` is the floor the workspace file needs: it declares nothing today, so the only checkable antecedent is that it was opened and parsed. Exit 2 for a source that is missing, unparseable or emptied out, checked before offenders, on the existing contract. AGENTS.md rule 7 was not followed by the absorption itself: this adds the largest package in the repo and neither meta file mentioned it. Adds the Repository Layout entry and a "Working on EQL" section covering the seams where failures are silent — the two-level package path and why root `pnpm test` needs `./packages/**`, `working_directory: packages/eql` for mise (and EQL_ROOT, since `git rev-parse --show-toplevel` now answers the monorepo root), the dead `.github/` deposit, the lockstep version identity and the `mise run build --version` cache-key trap, the path dep and its linter, the CI-workspace-keyed SteVec constants, and publishing not having moved. Corrects two now-false claims in the protect-ffi section, including "the only Rust in this repo". The plan's Phase-5 item for a `lint-no-eql-changeset.mjs` is marked superseded: `FROZEN_PUBLISHERS` in release-gate.mjs is strictly stronger, catching the hand-applied bump that no changeset-side guard could see. It also records that `.changeset/rename-ste-vec-contains.md.deferred` must NOT be restored — it was deleted deliberately in 9b1c44d so the cutover cannot apply the same bump twice, and re-parking that `major` changeset would bump an already-released 3.0.5 to 4.0.0.
…ed space THE BLAST RADIUS WAS OVERSTATED IN ONE DIRECTION AND MISSED ENTIRELY IN ANOTHER. The changeset claimed the rename affects "Supabase / PostgREST, since PostgREST calls functions rather than operators". It does not: `eql_v3.jsonb_contains(jsonb, jsonb)` and `jsonb_contained_by(jsonb, jsonb)` are byte-identical to 3.0.4, and those are the function-form entry points a PostgREST deployment actually calls. The real exposure is hand-written SQL. The schema-wide grant scripts in docs/reference/permissions.md pick the new name up on their next run, and a stale per-function grant fails loudly rather than silently granting nothing. Underneath it sits a larger fact that had never been written down: the installer opens with `DROP SCHEMA IF EXISTS eql_v3 CASCADE`, so EVERY EQL install drops grants, functional indexes and dependent views — not just this release. That is U-002 in the new upgrade note, and it is the item with real operational consequences. A STALE VENDORED MIGRATION DIRECTORY IS NEVER DETECTED. `migrationHash` is computed over a package's own bytes, so a `migrations/cipherstash/` generated against 1.0.0 is stale but internally intact and passes every integrity check. `materialiseExtensionMigrationPackageIfMissing` skips by existence while `refs/head.json` is re-emitted unconditionally, and that asymmetry is the whole failure mode: the head ref demands an invariant the frozen genesis edge does not provide. On an existing database the upgrade still applies; on a fresh one `db init` refuses with `Operation cipherstash.upgrade-eql-v3-bundle-3.0.5 has class "data" which is not allowed by policy.` — naming neither the directory nor the remedy. There is no in-package lever for that message: the text comes from @prisma-next/target-postgres and the operation id it interpolates is frozen inside a published content-addressed artefact. So the README carries the error string verbatim with the remedy, and a test pins the two together so they cannot drift. The changeset also asserted something false: `runContractSpaceSeedPhase` is called from migration-plan.ts and nowhere else, so `migrate` alone never seeds. Without `migration plan` first, the 3.0.5 directory never reaches disk and `migrate` is a silent no-op leaving the database on the older bundle. Pre-existing since 3.0.2, but this changeset stated it as fact. The fixture is not a committed blob — the 1.0.0 baseline is reconstructed from artefacts already in the tree and PROVED faithful by hashing to sha256:fc495f7f..., the value 1.0.0 published. Content addressing makes that an exact-bytes assertion. The `problems: []` claim was verified load-bearing by tampering a byte and confirming hashMismatch fires.
Six open Dependabot alerts, all reachable because the overrides written to close earlier ones had gone stale in two distinct ways. OVERTAKEN SELECTORS go quiet. `postcss@<8.5.10` stopped matching once the tree resolved 8.5.14, so the entry sat here looking like a fix while a vulnerable version installed underneath it (#167 GHSA-r28c-9q8g-f849 HIGH, path traversal reading arbitrary .map files via attacker-controlled sourceMappingURL; #176 its incomplete-fix follow-up). `js-yaml@<3.15.0` was overtaken by its own fix — the tree resolved exactly 3.15.0, so nothing matched and nothing lifted it to 3.15.1 (#189). EXACT PINS INSIDE A STILL-MATCHING RANGE are worse: they cannot self-heal and Dependabot cannot fix them. `js-yaml@>=4.0.0 <5` kept matching, so a pin written to FIX an advisory was holding the tree at 4.2.0 and blocking 4.3.1 (#155, #188 — quadratic-CPU DoS via merge-key chains, then `!!omap` duplicate-key detection). `fast-uri@<3.1.3` did the same at 3.1.3 while #158 needs 3.1.4 and #175 needs 3.1.5. Both shapes are now written into the comments, because the lesson is reusable: keep the ceiling AHEAD of the patched floor rather than at it. THE js-yaml CAUTION IS DISCHARGED, checked rather than assumed. The comment warned that a past bump broke changesets. @changesets/parse@0.4.3 declares `js-yaml: ^4.1.1`, which 4.3.1 satisfies; 4.2.0 and 4.3.1 have identical `dependencies` and `exports`, no `engines`, no `main`; 4.3.0 and 4.3.1 are security backports only, with no API removal on the 4.x line — the breaking churn (grouped constant exports, MERGE_KEY removal, mandatory identify) is all on 5.x, which the `<5` ceiling already excludes. Verified by resolving js-yaml from @changesets/parse's own path (4.3.1, not a nested copy) and running `changeset status`, which parsed all 14 changesets and produced the correct per-package bumps. No cooldown bypass was needed: every target is 13-19 days old against the 7-day minimumReleaseAge. The comment claiming "4.3.0 is still inside the cooldown" was 54 days stale. None of the four is a runtime dependency of any published package, so nothing ships them and this needs no changeset. Also corrects dependabot.yml's "the only Rust in the repo" — there are two cargo entries, and the /packages/eql one landed with the subtree.
# Conflicts: # packages/eql/crates/eql-bindings/sql/release-manifest.json # packages/eql/package.json # packages/eql/packages/eql/sql/release-manifest.json # packages/eql/packages/eql/src/generated/release-manifest.ts # packages/eql/pnpm-lock.yaml
`biome.json` declared schema 2.5.2 while `package.json` has required `^2.5.3` and the lockfile has resolved 2.5.3 since before this branch — so `biome check` exits 1 with "The configuration schema version does not match the CLI version 2.5.3", and `code:check` fails for a reason unrelated to any code in the tree. It stayed hidden because node_modules on this machine still held 2.5.2; a `pnpm install` that brings the tree in line with its own lockfile is what surfaces it, which is exactly what CI does on every run.
The subtree sync brought upstream's actual 3.0.5 release, and it is not the bundle this branch had been carrying under that number. Upstream 142f41d restored `eql_v3.ste_vec_contains` as a deprecated delegating alias — both overloads — and retargeted its own changeset from `major` to `patch` in the same commit. The two decisions are one decision: it is a patch BECAUSE the old name still resolves. This branch had taken the other half of that trade — hard removal, still shipped as a patch — so the tree held SQL stamped 3.0.5 that differed from published 3.0.5 by 32 lines. That is the defect 9b1c44d exists to prevent, one level up, and nothing would have caught it: `verify-release-assets.mjs` compares the manifest's version to package.json and hashes nothing, and `sync-generated.mjs` preserves `src/generated/release-manifest.ts` rather than regenerating it, so `check:generated` is blind to it too. The merge landed on upstream's bytes exactly — every artefact here now hashes to `accde0030…`, the digest in the published tarball — so no regeneration was needed beyond re-emitting the two prisma migrations that bake the bundle. ONLY TWO MIGRATIONS WERE RE-EMITTED, deliberately. All four `migration.ts` files call `readVerifiedInstallSql()`, so re-running the 3.0.2 and 3.0.4 edges would silently bake 3.0.5 SQL into artefacts describing historical releases. Looping over the directory is the obvious thing to do and it is wrong; the frozen-digest test catches it, but only after the damage. The one assertion whose MEANING changed is `not.toContain('ste_vec_contains')`. Re-pinning it would have been wrong in both directions, so it now asserts the SHAPE the alias has to take: exactly two definitions, every body delegating to the new name rather than copying the implementation, and both marked deprecated with `COMMENT ON FUNCTION` so a DBA reading the schema sees it — not only the generated docs. Everything else that moved is prose that had inverted. The upgrade note was the worst of it: `v3.0.5.md` told readers to verify with a query commented "Must return zero rows" against `proname = 'ste_vec_contains'`, which now returns two — a reader following it would conclude the upgrade had failed. The release is now genuinely non-breaking, which leaves U-002 (every EQL install opens `DROP SCHEMA … CASCADE`) as the only item in it with operational consequences.
🦋 Changeset detectedLatest commit: 00d685f 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 |
…annot serve
`refreshCargoLock` passed `--offline` on the reasoning that `eql-bindings`
resolves from a path and so needs no registry. The premise is true and the
conclusion does not follow: `cargo update -p X` does not update X in
isolation. It re-resolves the whole graph and rewrites a complete lock, and
offline that means every OTHER package has to be served from the local
registry cache. `packages/protect-ffi` has 167 of them.
The release job has no such cache. `jdx/mise-action` runs there with
`install: true, cache: false` — it installs toolchains and populates nothing
under `~/.cargo/registry` — and `lint-no-workflow-caching.mjs` forbids a
cache restore anywhere an artifact is published. Reproduced against this
tree with an empty CARGO_HOME:
error: no matching package named `chrono` found
location searched: crates.io index
required by package `protect-ffi v0.1.0 (…/crates/protect-ffi)`
exit 101. `execFileSync` throws, `pnpm run version` fails, and there is no
Version Packages PR — AFTER `changeset version` has already rewritten every
manifest and CHANGELOG, in a job holding `contents: write`. That is the same
half-applied release the mise-install step above it exists to prevent, and
the sync runs both workspaces before `release:prepare_bindings_assets`, the
only step that would have warmed anything, so the first call dies.
The measurement that made `--offline` look safe was taken on a developer
machine with a warm `~/.cargo`. Both resolutions were re-checked here from
cold and from warm: byte-identical, including the `windows-sys` edge cargo
repairs along the way. So this changes where the step can run, not what it
produces.
The old test asserted `--offline` was present, which pinned the defect
rather than catching it. It now asserts the flag's ABSENCE, paired with a
test of the CI precondition that absence depends on — no cargo cache, no
warm-up step — so that if someone ever warms the registry deliberately, the
coupling is visible instead of buried in a comment. The remediation advice
in `cargo-lock-freshness.test.mjs` recommended the same broken command and
is corrected with it.
`docs_v3_grep.sh`'s new check that deprecated `ste_vec_contains` aliases
never reach Doxygen was written as
elif tasks/docs/doxygen-filter.sh src/v3/json/functions.sql | grep -q …
under `set -euo pipefail`. On the LEAK path — the one the guard exists for —
`grep -q` matches, exits 0 and closes the pipe; the filter's awk takes
SIGPIPE and exits 141; `pipefail` makes that the pipeline's status, so the
`elif` is false and the FAIL branch never runs. Only the clean path (grep
reads to EOF, exits 1) was reported correctly.
This is not latent. Measured at exit 141 against the real ~16 KB filter
output on macOS, whose pipe buffer starts at 16 KB — grepping for a token
that IS in the output returned "no match". Linux's 64 KB buffer absorbs the
write and hides it, which is the same platform split
`workflow-grep-q-pipelines.test.mjs` already documents for GNU tar vs
bsdtar, and the same fail-OPEN direction it warns about.
Fixed by capturing first. The separate assignment also means `set -e` aborts
if the filter itself crashes, where the old pipeline scored a crashed filter
as a pass. Verified in both directions: with the `@cond` markers stripped so
the aliases genuinely leak, the guard now exits 1; before the fix it printed
OK either way.
The guard against the shape now covers tracked shell scripts, not just
`run:` blocks — the hazard is a property of the shell, not of GitHub
Actions. `echo`/`printf` writers are exempt: the whole write lands in the
pipe buffer before grep can exit, so there is no blocked writer to signal.
That exemption is about SIZE, not about them being builtins (a builtin in a
pipeline still runs in a subshell and still takes SIGPIPE), and it is what
keeps the thirteen bounded upstream call sites out of scope while leaving
the one real offender in it.
Whole-line comments are skipped, because the rule has to survive being
written down: the fix comment on `docs_v3_grep.sh` quotes the very pipeline
it replaced, and without that the guard reported the explanation as the
defect.
The fix is in vendored subtree code, so it needs to go upstream to
cipherstash/encrypt-query-language as well or the next sync will bring the
broken form back.
AGENTS.md asserted a live verdict — "It fires today on the hand-applied 3.0.5 bump … and it blocks the Version Packages PR as well as the publish" — which stopped being true the moment `@cipherstash/eql@3.0.5` reached npm. This branch's own 2a140e8 re-emits against the published 3.0.5, and `node scripts/release-gate.mjs` now exits 0 with "nothing to publish". A durable agent instruction that says to expect a red gate is worse than no instruction: it turns a real blocker into an expected one and the agent stops looking. The plan doc carried the same claim in weaker form. Both now state the CONDITION under which the gate fires and point at the script for what it blocks at any given moment — that answer is computed from the registry at run time and does not belong in a checked-in sentence. `frozen-publisher-docs.test.mjs` guards both halves of the drift. No doc may assert a live gate verdict; it went red on both files before this change. And each doc documents the eql freeze if and only if `FROZEN_PUBLISHERS` still carries it — keyed on the map rather than on a date, so the Phase-5 cutover cannot delete the entry without taking the prose with it. That is the same drift in the other direction, and it is already scheduled.
The 1.0.0 -> 3.0.5 upgrade workflow landed in `packages/stack-prisma`'s
README and in a changeset, but not in the skill. `skills/` ships inside the
`stash` tarball and `installSkills()` copies it into the user's
`.claude/skills/`, so the README is what a human reads after hitting the
error and the skill is what an AGENT reads before causing it — and an agent
driving `db init` in a repo upgraded from 1.0.0 met
Operation cipherstash.upgrade-eql-v3-bundle-3.0.5 has class "data"
which is not allowed by policy.
with no route out of it. The skill's install section still showed only the
first-time `contract emit` -> `migration plan` -> `migrate` sequence and
said nothing about upgrades, or about `migration plan` being the only
command that vendors new migration packages.
`stale-vendored-space.test.ts` already pinned the README to the planner's
real refusal string, on the reasoning that the message cannot be improved
from inside the package so the shipped docs have to bridge it to the remedy.
That reasoning covers the skill identically, so the assertion is extended to
both files rather than duplicated as a naked content check — the pair now
cannot drift from the message the planner actually emits. It went red on the
skill alone.
Adds the `stash` patch changeset a skills change requires, and widens the
skill's frontmatter trigger to the upgrade path.
There was a problem hiding this comment.
Agent-assisted review (Claude Code, adversarially verified — refuted candidates dropped). Ten findings; three are inline below on the lines involved. The rest touch files or lines this PR doesn't change, so they're here:
1. Five workflows never trigger on packages/eql/crates/**. eql-bindings now compiles into index.node from the tree, but only integration-drizzle.yml and integration-protect-ffi.yml gained the trigger path. integration-supabase.yml, integration-prisma-next.yml, prisma-next-e2e.yml, prisma-example-readme-e2e.yml (all run build-ffi-binding) and tests-rust.yml (cargo-tests the dependent crate) still watch only packages/protect-ffi paths — and wasm-build-inputs-paths-filter.test.mjs scans only wasm: 'true' callers, so the native-only workflows escape the guard. A payload-encoding change under packages/eql/crates/ merges with those suites green-by-absence.
2. The @cipherstash/eql runtime pin loosened from exact 3.0.4 to workspace:^, which packs as ^3.0.5 in the published tarballs — while npm publishing rights for that package still sit with the old repo until the cutover. A 3.0.6 published from there would be resolved by a registry install of stash and applied by stash eql install with no digest check (readInstallSql is trusted verbatim; readVerifiedInstallSql verifies against the resolved package's own manifest, which a substituted package rewrites too). That's the emit/store skew this absorption exists to prevent, reintroduced through the packed-tarball path. The exact-pin rationale deleted from migration-v3.test.ts isn't re-established anywhere.
3. skills/stash-prisma/SKILL.md doesn't know about the re-emitted baseline or the upgrade edge. The README and changesets document the "run migration plan first, or db init fails with the raw policy-refusal error / migrate silently stays on 3.0.4" flow — but the skill, which is the artifact that actually lands in customer repos to answer exactly this, is untouched. Per AGENTS.md, a Prisma Next integration change checks that skill in the same PR.
4. skills/stash-postgres/SKILL.md:165 still claims "The CLI pins an exact version, so a database is only ever on one bundle" — no longer true after the workspace:^ change, and this branch's own changeset says the claim was dropped (the diff removed only the other copy). Several lines in that skill also still point at the old repo as where the bundle is developed, contradicting this PR's SECURITY.md note.
5. Missing changeset for protect-ffi's eql-bindings move (crates.io =3.0.2 → in-tree path dep at 3.0.5) — that changes what compiles into the published binding. Since main has now executed the changeset cutover, on rebase this lands as a regular .md, not a parked one.
Also worth noting: the first inline finding below (the upgrade edge cascade-dropping customer functional indexes) is tracked generally as #918 — this PR's edge is the first shipped instance of it.
Two more on files in this PR, but on lines that pre-date its hunks:
6. scripts/sync-lockstep-versions.mjs:206 — fragile main-guard. Fragile main-guard: import.meta.url percent-encodes, process.argv[1] doesn't, so on any checkout path containing a space (or other URL-encodable character) this comparison is false and the script silently no-ops with exit 0 — changeset version applies but the Cargo.toml bump, Cargo.lock refresh, and SQL-asset regen are all skipped. That's exactly the silent lockstep skew the script exists to prevent, and the SQL-asset half isn't caught by cargo-lock-freshness.test.mjs.
This same PR already has the robust form at lint-no-eql-registry-pins.mjs:733: process.argv[1] === fileURLToPath(import.meta.url).
7. .github/workflows/macro-expand-eql.yml:71 — rust-cache points at the wrong workspace. workspaces: packages/eql/tests/sqlx points at a workspace member (it's in the root Cargo.toml's members, has no Cargo.lock of its own), so rust-cache hashes no lockfile and caches a target/ directory cargo never writes — artifacts land in packages/eql/target. Every nightly run cold-compiles the full SQLx dependency tree inside the 30-minute timeout while restoring and saving a useless cache. Should be workspaces: packages/eql like the sibling jobs.
Two knock-ons: this step also omits save-if: false, breaking test-eql.yml's invariant that only build-archive saves the shared sqlx-tests key — and the eql-suite-ci guard only scans test-eql.yml, so this copy escapes it. (Same fix pattern applies to test-eql.yml's splinter job, which has no rust-cache restore at all.)
| @@ -76,9 +77,10 @@ export default class M extends Migration { | |||
| sql: "SELECT to_regtype('eql_v3.query_json') IS NOT NULL", | |||
There was a problem hiding this comment.
This upgrade edge re-runs the full install SQL, whose first statement is DROP SCHEMA IF EXISTS eql_v3 CASCADE — and the CASCADE takes every customer object built on eql_v3 functions with it, most importantly the functional indexes skills/stash-prisma tells customers to create. Those were created by a one-shot migration that never re-runs, so nothing recreates them: precheck is [], the postchecks assert only function/domain existence, the migration reports success, and every encrypted predicate silently degrades to a sequential scan.
The branch's changeset concedes the mechanism ("this is the same mechanism that drops functional indexes") but then says grants are the only part of the upgrade needing action — for a GA database with indexes, that's not true. General tracking issue: #918 (capture pg_get_indexdef() before the drop, re-issue + ANALYZE after). At minimum this edge should warn, or the changeset should stop saying no action is needed.
| continue | ||
| } | ||
| if ( | ||
| versionsOf(dep.name).some((candidate) => satisfies(candidate, range)) |
There was a problem hiding this comment.
Ordering bug with a dependency-confusion consequence: the registry-satisfaction check runs before the target.private check, so a private workspace dependency whose name is squatted on npm at a satisfying version is continued past — the private-dependency blocker never fires. Verified by direct execution: squatted name → [], absent name → blocker. That contradicts this function's own doc comment ("a private dep is a blocker either way").
Scenario: a published package gains a runtime workspace:* dep on a private member whose name exists on npm; packedRange rewrites it to a registry range at pack time, the gate exits 0, and customer installs resolve the squatter's package. Fix: hoist the target.private check above the registry check.
| 'There are exactly two ways past this, and neither is editing this gate:\n\n' + | ||
| ' 1. Publish the frozen package. For @cipherstash/eql that is the Phase 5\n' + | ||
| ' cutover in docs/plans/2026-08-13-eql-monorepo-absorption.md: repoint\n' + | ||
| ' npm trusted publishing to cipherstash/stack, release 3.0.5, and every\n' + |
There was a problem hiding this comment.
Already stale before merge: @cipherstash/eql@3.0.5 is on npm now, so the gate exits 0 today and this remediation text ("release 3.0.5, and every finding above clears") describes a registry state that no longer holds — as do the FROZEN_PUBLISHERS reason at line 131 ("npm's newest is 3.0.4"), the tests.yml comment, and AGENTS.md's "It fires today on the hand-applied 3.0.5 bump". Next time the blocker actually fires (say a 3.0.6 pre-cutover), the message tells the operator to release a version that's already out. Suggest deriving the version from the blocker data and rewording the "fires today" prose to past tense.
coderdan
left a comment
There was a problem hiding this comment.
Recommend addressing Claude's findings.
Stacked on #885 — review that first, or review this alone as "the four blockers found reviewing #885, plus the upstream 3.0.5 sync."
ea9e1401..HEAD: 47 files, ~5.3k insertions (excludingpackages/eql, which moves only via the subtree merge below).The four blockers
1 — the release job could not run the version hook it now depends on.
release.ymlpassesversion: pnpm run version, which chainssync-lockstep-versions.mjs→mise run release:prepare_bindings_assets→cargo run -p eql-codegen. The job installed pnpm, Node and node-gyp; mise is not on GitHub's ubuntu images..changeset/already held live changesets, so changesets/action takes its version branch on the next push to main regardless of this PR —pnpm run versiondies with ENOENT and no Version Packages PR is opened at all.release-version-hook.test.mjsasserted the wiring existed; nothing asserted the job could execute it.add_shims_to_path: falseis load-bearing and would not survive a copy-paste fromtest-eql.yml: mise's shim dir is prepended to PATH for every later step, so its pinned Node 22 would shadow setup-node's andchangeset publishwould shell out to npm 10.x — below the 11.5.1 OIDC trusted publishing requires.2 — merging armed a release that could not succeed.
changeset publishpublishes every public workspace package whose version is absent from npm — changeset or no changeset — withPromise.all, no dependency ordering, andpublishAPackagereturning failures rather than throwing them. So a package that cannot publish does not stop the siblings that depend on it.publishBlockersinrelease-gate.mjsanswers that before anything publishes and exits non-zero, failinggateand skippingrelease.FROZEN_PUBLISHERShas to be written down rather than derived: npm exposes no way to ask "would a publish from here be accepted?", and provenance on the last release is evidence about the past, not permission for the next.3 — the cache keys could not see the path dep. Phase 3 made
eql-bindingsa path dependency ofprotect-ffi, so it is a compile input toindex.node— but both keys hashed onlypackages/protect-ffi/**. A src-only edit there changes no hashed file, so the cache hits and every credentialed job runs a stale binding. This PR's own 3.0.4 → 4.0.0 → 3.0.5 bumps moved neither key.4 — the PG 14–16 matrix ran never.
test-eql.ymlgated the full matrix behindmerge_groupalone. Checked against the live API:mergeQueue(branch:"main")is null,branches/main/protection404s, and the active ruleset carries norequired_status_checksrule — soci-requiredis required by nothing andmerge_groupis an event this repo never emits.setupis now an exclusion, so the full matrix is the default and a trigger added later gets more coverage rather than none.The upstream 3.0.5 sync
@cipherstash/eql@3.0.5published fromcipherstash/encrypt-query-languagewhile this branch was open — and it is not the bundle this branch carried under that number. Upstream142f41d8restoredeql_v3.ste_vec_containsas a deprecated delegating alias (both overloads) and retargeted its changeset frommajortopatchin the same commit. Those are one decision: it is a patch because the old name still resolves.The subtree is re-synced to the release commit; every artefact here now hashes to
accde0030…, byte-identical to the published tarball. Nothing would have caught the divergence —verify-release-assets.mjscompares versions and hashes nothing, andsync-generated.mjspreservessrc/generated/release-manifest.tsrather than regenerating it, socheck:generatedis blind to it.Only two prisma migrations were re-emitted, deliberately. All four
migration.tsfiles callreadVerifiedInstallSql(), so re-running the 3.0.2 and 3.0.4 edges would silently bake 3.0.5 SQL into artefacts describing historical releases. Looping over the directory is the obvious move and it is wrong.Also here
bench-eql.ymlpassed all 14 tests. That case is now a permanent test with the workflow list injected rather than the file deleted.pnpm-workspace.yaml— where pnpm actually resolvesoverridesandcatalogs, and the quietest way to reintroduce the skew the whole absorption exists to prevent.migrations/cipherstash/is never detected (11 new tests).migrationHashverifies a package against its own bytes, so a 1.0.0 copy is stale but internally intact and passes everything. On a fresh databasedb initthen refuses with an error naming neither the directory nor the remedy.SECURITY.mdrow.Not in this PR
The security-override refresh and the Biome config-schema fix are independent of EQL and target
maindirectly — see the linked PRs. Both also appear in this branch's history; they will resolve whenmainmerges in.