Skip to content

fix(skills,stack,scripts): shipped decrypt guidance, and a sound package-path linter (#772 review findings 14, 15)#789

Open
tobyhede wants to merge 1 commit into
remove-v2from
fix/shipped-docs-and-lint-gates
Open

fix(skills,stack,scripts): shipped decrypt guidance, and a sound package-path linter (#772 review findings 14, 15)#789
tobyhede wants to merge 1 commit into
remove-v2from
fix/shipped-docs-and-lint-gates

Conversation

@tobyhede

Copy link
Copy Markdown
Contributor

Review remediation for #772 — findings 14 and 15, plus the contradictory-changeset item from the cut-for-length list.

Finding 14 — the shipped docs contradict the code

decryptModel / bulkDecryptModels return an AuditableDecryptModelOperation: thenable, carrying .withLockContext() and .audit(). Verified both ways — seven chaining forms compile, and a credential-free runtime test (protect-ffi mocked) passes 7/7.

Three sites in skills/stash-encryption/SKILL.md and four in packages/stack/README.md said the opposite ("a plain Promise<Result<...>>", "no .withLockContext() to chain"), steering agents away from the exact .audit() chain the audit-on-decrypt changeset advertises. The skill contradicted its own reference table, which was already correct: 8b744309 fixed that table and nothing else, despite a commit message claiming it had updated the guidance.

Both ship — the skill into customer repos via installSkills(), the README in the @cipherstash/stack tarball.

SKILL.md:483 says the same thing about the WASM entry and is correct — that client really does return a bare promise with no lock-context argument. Deliberately untouched.

Also: protectOps.eq in the setup prompt stash init writes for coding agents. One occurrence repo-wide, and no such export exists anywhere; the real API is createEncryptionOperators(client), conventionally ops.

Finding 15 — the linter had a false positive and a false negative

  • False positive: the name capture had no right anchor, so a sentence-final packages/stack. swallowed the period and reported a live package as dead. Uppercase was excluded from the class too, so packages/Foo was never checked at all.
  • False negative: livePackages came from readdirSync — the working tree. Deleting a package leaves dist/ and node_modules/ behind, so every reference to it kept passing. That is precisely what this linter was commissioned to catch, silently unenforced on any checkout that had previously built the package. Now derived from git ls-files. Deliberately not "has a package.json" — packages/utils has none and is live.
  • scripts/ was never scanned, so the linters never checked themselves. Adding it immediately surfaced a dead packages/drizzle allowlist entry in lint-no-hardcoded-runners.mjs for a path that never existed in git history. Self-test fixtures stay exempt; they must name dead packages.

Severity, for the record: CI was never affected — actions/checkout gives a fresh workspace with caching disabled — and main carries no required status checks. This was a local-developer papercut plus a real soundness hole, not the broken build the review implies.

Verified the false negative is closed by re-creating a stale untracked packages/protect/dist shell: the reference is now flagged where it previously passed.

Cut-list — contradictory changesets in one release

.changeset/dynamodb-eql-v3.md claimed "EQL v2 tables continue to work unchanged" and "no existing caller needs to change", while stack-dynamodb-v2-write-removal.md announced the v2 encrypt overloads as removed — both unreleased, so both render into the same CHANGELOG. The code sides with removal (dynamodb/types.ts declares Table extends AnyV3Table with no v2 overload). The claims are now scoped to the decrypt path, which genuinely is unchanged.

Verification

pnpm run test:scripts → 93 passed (90 + 3 new fixture-driven cases). lint:package-paths and lint:runners both clean. Biome: 0 errors.

… the package-path linter sound

Finding 14 — decryptModel/bulkDecryptModels return an
AuditableDecryptModelOperation: thenable, with .withLockContext() and .audit().
Three sites in skills/stash-encryption and four in packages/stack/README.md
said the opposite ("a plain Promise<Result<...>>", "no .withLockContext() to
chain"), steering agents away from the very .audit() chain the audit-on-decrypt
changeset advertises. The skill contradicted its own reference table, which was
already right — 8b74430 fixed that table and nothing else, despite a commit
message claiming otherwise.

Both files ship: the skill inside the stash tarball and thence into customer
repos via installSkills(), the README inside the @cipherstash/stack tarball.
The equivalent statement about the WASM entry is CORRECT and deliberately
untouched — that client really does return a bare promise.

Also `protectOps.eq` in the setup prompt stash init writes for coding agents.
One occurrence repo-wide, and no such export exists; the real API is
createEncryptionOperators(client), conventionally `ops`.

Finding 15 — the package-path linter had a false positive and a false negative,
both fixed with the fixture-driven self-tests the suite already uses:

- The name capture had no right anchor, so a sentence-final `packages/stack.`
  swallowed the period and reported a LIVE package as dead. Uppercase was also
  excluded from the class, so a capitalised name was never checked at all.
- livePackages came from readdirSync, i.e. the working tree. Deleting a package
  leaves dist/ and node_modules/ behind, so every reference to it kept passing —
  the exact case the linter was commissioned to catch, silently unenforced on
  any checkout that had built the package. Now derived from `git ls-files`.
  Deliberately not "has a package.json": packages/utils has none and is live.
- scripts/ was not scanned, so the linters never checked themselves. Adding it
  immediately surfaced a dead allowlist entry in lint-no-hardcoded-runners for
  a path that never existed in git history. Self-test fixtures stay exempt —
  they must name dead packages.

Severity note for the record: CI was never affected (fresh checkout, caching
disabled), and main carries no required status checks — this was a local
developer papercut plus a real soundness hole, not a broken build.

Finally, resolves two changesets that contradicted each other in the same
release: dynamodb-eql-v3 claimed "EQL v2 tables continue to work unchanged" and
"no existing caller needs to change" while stack-dynamodb-v2-write-removal
announced the v2 encrypt overloads as removed. The code sides with removal;
the claims are now scoped to the decrypt path.
@tobyhede
tobyhede requested a review from a team as a code owner July 25, 2026 04:44
@changeset-bot

changeset-bot Bot commented Jul 25, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: df047ef

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/stack Minor
@cipherstash/basic-example Patch
@cipherstash/e2e Patch
@cipherstash/bench Patch
@cipherstash/prisma-next Minor
@cipherstash/stack-drizzle Minor
@cipherstash/stack-supabase Minor
@cipherstash/test-kit Patch
@cipherstash/prisma-next-example Patch
@cipherstash/wizard Minor

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 25, 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 Plus

Run ID: 573ce139-c568-4e44-9bf4-090993ef68bc

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 fix/shipped-docs-and-lint-gates

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.

@freshtonic freshtonic 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.

Approve.

All three parts check out. Confirmed the typed client genuinely returns a chainable AuditableDecryptModelOperation with .audit()/.withLockContext(), so the corrected decrypt-chaining docs now match the code (the old "plain Promise" wording was the bug), and the WASM-entry statement was correctly left intact. The package-path linter's readdirSync → git ls-files change is the right soundness fix, and protectOps no longer appears in any shipped surface. Changeset targeting is correct (stash patch + @cipherstash/stack patch), and the PR also repairs a real contradiction between two unreleased changesets.

Non-blocking:

  • The highest-value fix — livePackages from git ls-files — has no dedicated regression test. It's awkward to test (needs a tracked-vs-untracked dir fixture) but achievable; without it a future revert to readdirSync would leave every test green.
  • setup-prompt.ts:283 now points at createEncryptionOperators, but this prompt sets up the v3 flow, where the Drizzle factory is createEncryptionOperatorsV3. Not wrong the way protectOps was (the v2 name is a real export), just off-version — consider createEncryptionOperatorsV3 or keeping it generic.

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.

2 participants