Skip to content

fix(stack): address EQL v3 types-module review follow-ups#559

Merged
tobyhede merged 1 commit into
feat/eql-v3-text-search-schemafrom
fix/eql-v3-review-followups
Jul 6, 2026
Merged

fix(stack): address EQL v3 types-module review follow-ups#559
tobyhede merged 1 commit into
feat/eql-v3-text-search-schemafrom
fix/eql-v3-review-followups

Conversation

@tobyhede

@tobyhede tobyhede commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Addresses the pending review follow-ups from #541 (merged) on the EQL v3 types module. Seven findings resolved across tests, schema, encryption, eql/v3, and CI.

Stacked on top of feat/eql-v3-text-search-schema (#535).

Changes

  • test: restore the live ORE proof for text order domains. text_ord / text_ord_ore carry both hm (unique) and ob (ore), so the single-kind classifier ran only the eq_term/hmac_256 proof and silently skipped ord_term/ore_block_256 — a wrong-valued ob would pass the matrix green. Now runs both proofs; builds the ob term with queryType:'orderAndRange' since equality resolves to hm on these domains.
  • schema: extract a shared resolveMatchOpts() merge+clone helper used by both the v2 and v3 freeTextSearch builders, giving v2 the clone-on-write protection it lacked (it stored caller opts by reference).
  • eql/v3: derive EncryptedTextSearchColumn.build()'s unique/ore blocks via indexesForCapabilities (override only match) so the emitted index shape cannot drift from the shared capability mapping.
  • encryption/v3: precompute row reconstructors per schema table at typedClient construction and return a DecryptionError failure for unknown tables, so table.build()'s duplicate-column throw can no longer escape the decrypt Result contract as a promise rejection.
  • eql/v3: alias V3DecryptedModel to V3ModelInput (character-identical) to stop the input/output model shapes silently drifting.
  • ci: add src/schema/match-defaults.ts to the fta-v3 path scope — it shapes every emitted v3 match block but sat outside the gate.
  • ci: re-baseline FTA --score-cap 72 → 69 after the eql/v3 file split and refresh the spec doc's stale 71.08 baseline claim.

Review

CodeRabbit review against base feat/eql-v3-text-search-schema: 0 findings.

Test plan

  • v3 live-pg matrix runs both hm and ob proofs for text order domains
  • fta-v3 CI gate passes at score-cap 69

Summary by CodeRabbit

  • New Features
    • Improved free-text search configuration handling for encrypted columns, making search settings more consistent.
  • Bug Fixes
    • Decryption now handles unknown tables more safely and avoids runtime failures.
    • Updated encryption/decryption behavior for more reliable row reconstruction.
  • Tests
    • Refined test coverage to better match actual SQL behavior across different index setups.
  • Documentation
    • Clarified guidance around mutation-score thresholds and blocking-gate calibration.

Resolves pending review feedback from #541 (merged). Seven findings:

- test: restore the live ORE proof for text order domains. text_ord /
  text_ord_ore carry both hm (unique) and ob (ore), so the single-kind
  classifier ran only the eq_term/hmac_256 proof and silently skipped
  ord_term/ore_block_256 -- a wrong-valued ob would pass the matrix green.
  Run both proofs; build the ob term with queryType:'orderAndRange' since
  equality resolves to hm on these domains.
- schema: extract a shared resolveMatchOpts() merge+clone helper used by
  both the v2 and v3 freeTextSearch builders, giving v2 the clone-on-write
  protection it lacked (it stored caller opts by reference).
- eql/v3: derive EncryptedTextSearchColumn.build()'s unique/ore blocks via
  indexesForCapabilities (override only match) so the emitted index shape
  cannot drift from the shared capability mapping.
- encryption/v3: precompute row reconstructors per schema table at
  typedClient construction and return a DecryptionError failure for unknown
  tables, so table.build()'s duplicate-column throw can no longer escape the
  decrypt Result contract as a promise rejection.
- eql/v3: alias V3DecryptedModel to V3ModelInput (character-identical) to
  stop the input/output model shapes silently drifting.
- ci: add src/schema/match-defaults.ts to the fta-v3 path scope -- it shapes
  every emitted v3 match block but sat outside the gate.
- ci: re-baseline FTA --score-cap 72 -> 69 after the eql/v3 file split and
  refresh the spec doc's stale 71.08 baseline claim.
@tobyhede tobyhede requested a review from a team as a code owner July 6, 2026 02:01
@changeset-bot

changeset-bot Bot commented Jul 6, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 35bd9a8

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

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

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Important

Review skipped

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

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

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c117ebda-735e-4429-b081-0a39cb8da876

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:

  • ✅ Review completed - (🔄 Check again to review again)
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/eql-v3-review-followups

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/stack/src/encryption/v3.ts (1)

187-193: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Optional: freeze the shared failure object.

unknownTableFailure is a single shared object instance returned by reference on every unknown-table call. Since consumers won't mutate it in practice, this is low risk, but Object.freeze would guard against accidental mutation leaking across calls.

🛡️ Optional defensive tweak
-  const unknownTableFailure: { failure: EncryptionError } = {
+  const unknownTableFailure: { failure: EncryptionError } = Object.freeze({
     failure: {
       type: EncryptionErrorTypes.DecryptionError,
       message:
         '[eql/v3]: decryptModel received a table this client was not initialized with — pass the same table object(s) given to EncryptionV3/typedClient',
     },
-  }
+  })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/stack/src/encryption/v3.ts` around lines 187 - 193, The shared
unknown-table failure object in EncryptionV3 should be defensively immutable so
accidental mutation cannot leak across decryptModel calls. Update the
unknownTableFailure constant in v3.ts to freeze the top-level object (and, if
needed, its nested failure payload) while keeping the existing
EncryptionErrorTypes.DecryptionError message and structure unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/fta-v3.yml:
- Around line 21-26: Add the workflow file itself to the pull_request path
filters in fta-v3 so changes to .github/workflows/fta-v3.yml also trigger the
gate; update the pull_request paths alongside the existing
packages/stack/src/eql/v3 and schema/match-defaults entries so the workflow
validates its own changes before merge.

---

Nitpick comments:
In `@packages/stack/src/encryption/v3.ts`:
- Around line 187-193: The shared unknown-table failure object in EncryptionV3
should be defensively immutable so accidental mutation cannot leak across
decryptModel calls. Update the unknownTableFailure constant in v3.ts to freeze
the top-level object (and, if needed, its nested failure payload) while keeping
the existing EncryptionErrorTypes.DecryptionError message and structure
unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 209ef5e6-2ad6-47c6-b6b9-f6e2e0f95e08

📥 Commits

Reviewing files that changed from the base of the PR and between 348e4fa and 35bd9a8.

📒 Files selected for processing (9)
  • .github/workflows/fta-v3.yml
  • docs/superpowers/specs/2026-07-02-stryker-v3-ci-gate-design.md
  • packages/stack/__tests__/v3-matrix/matrix-live-pg.test.ts
  • packages/stack/package.json
  • packages/stack/src/encryption/v3.ts
  • packages/stack/src/eql/v3/columns.ts
  • packages/stack/src/eql/v3/table.ts
  • packages/stack/src/schema/index.ts
  • packages/stack/src/schema/match-defaults.ts

Comment on lines 21 to +26
pull_request:
branches:
- "**"
paths:
- 'packages/stack/src/eql/v3/**'
- 'packages/stack/src/schema/match-defaults.ts'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Trigger the gate when the workflow itself changes.

push covers .github/workflows/fta-v3.yml, but pull_request does not. That means PRs can change this CI gate without ever running the updated workflow before merge.

Suggested fix
   pull_request:
     branches:
       - "**"
     paths:
       - 'packages/stack/src/eql/v3/**'
       - 'packages/stack/src/schema/match-defaults.ts'
+      - '.github/workflows/fta-v3.yml'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pull_request:
branches:
- "**"
paths:
- 'packages/stack/src/eql/v3/**'
- 'packages/stack/src/schema/match-defaults.ts'
pull_request:
branches:
- "**"
paths:
- 'packages/stack/src/eql/v3/**'
- 'packages/stack/src/schema/match-defaults.ts'
- '.github/workflows/fta-v3.yml'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/fta-v3.yml around lines 21 - 26, Add the workflow file
itself to the pull_request path filters in fta-v3 so changes to
.github/workflows/fta-v3.yml also trigger the gate; update the pull_request
paths alongside the existing packages/stack/src/eql/v3 and schema/match-defaults
entries so the workflow validates its own changes before merge.

@tobyhede tobyhede requested a review from freshtonic July 6, 2026 02:44
@freshtonic

Copy link
Copy Markdown
Contributor

Multi-angle review (8 finder passes + verification, cross-checked against the branch, base, and main). This PR addresses the seven follow-ups from the #541 review, and all seven are faithfully and correctly implemented:

  • The super.build() derivation in EncryptedTextSearchColumn.build() is byte-identical to the old hand-written block (same key order, same cast_as, verified against indexesForCapabilities' emission order).
  • The proof-list rework is a strict expansion: no domain lost a proof, seeding is still once-per-domain, eqTerms/ordTerms don't collide for dual-list text domains, and orderAndRange on text columns produces the same indexType:'ore' FFI call the pure-ORE path uses. The live-pg matrix actually ran in this PR's CI (40 tests, passed, not skipped) — so the new ob-vs-ore_block_256 proof for text_ord/text_ord_ore is empirically proven against real Postgres, and the test-plan checkbox can be ticked.
  • resolveMatchOpts preserves the pinned merge semantics (empty token_filters override, last-call-wins) and gives v2 the clone-on-write protection; no dead imports left behind.
  • The V3DecryptedModel alias is safe — the only consumers are the client signatures and toEqualTypeOf type-tests against concrete shapes.
  • FTA cap 69 is sound (worst file measures 67.58 on this branch with the pinned fta-cli) and fix(stack): address EQL v3 types-module review follow-ups #559 merges cleanly with chore(stack): upgrade EQL v3 bundle (timestamptz → timestamp) + cast_as fix #542 (git merge-tree: zero conflicts; one widens the date filter, the other moves the factory call — they compose).

The findings below are all in the new code this PR introduces — none block merging, but 1–3 are worth fixing while the file is open.

Findings

1. src/encryption/v3.ts:206 — the reconstructor Map is keyed by object identity, which narrows previously-working behavior, and the comment explaining it away is wrong.
Before this PR, decryptModel/bulkDecryptModels accepted any structurally-valid table (the reconstructor was built inline). Now a structurally-equal-but-distinct table object misses the Map and returns DecryptionError at runtime. The comment claims a miss is "only reachable by bypassing the Table extends S[number] type constraint" — but TypeScript is structural: a second encryptedTable('users', {…same columns…}) satisfies the constraint with zero casts, as does a client typed as ReturnType<typeof typedClient> (which this repo's own matrix-lock-context.test.ts:71 uses — that widens S to readonly AnyV3Table[], so any table type-checks), or the same schema module loaded twice (ESM/CJS dual-package, Next.js HMR, per-request table construction in serverless handlers). Deeper fix: key the Map by table.tableName — table names are already forced unique on the EncryptionV3 path by buildEncryptConfig's duplicate-tableName guard (direct typedClient callers would want the same guard at construction, which is a one-loop addition). Failing that, at least fix the comment so maintainers don't trust an invariant the type system doesn't provide.

2. .github/workflows/fta-v3.yml:15 — adding match-defaults.ts to the trigger paths doesn't gate the file; the check runs vacuously green on edits to it.
The job still runs fta src/eql/v3 --score-cap 69 (packages/stack/package.json:220), which never scans src/schema/match-defaults.ts. FTA scores files individually and doesn't follow imports, so a PR touching only match-defaults.ts triggers a scan of unchanged v3 files that cannot fail because of the edit — reviewers see a green "complexity gate" that analyzed zero lines of the file it claims to guard. Either add the file to the scan (fta src/eql/v3 --score-cap 69 && fta src/schema/match-defaults.ts --score-cap 69, or an include pattern) or move it under src/eql/v3. (Nit while you're there: the spec doc's new "current worst-file score of 68.00" is already ~0.4 stale — this PR's own columns.ts edits measure 67.58.)

3. src/encryption/v3.ts:187unknownTableFailure is a single shared mutable object, and its message names the wrong method half the time.
The same instance is returned by reference from every failing decryptModel and bulkDecryptModels call for the client's lifetime — a caller that enriches the failure in place (result.failure.message += …, attaching a code before logging) silently corrupts the error for every subsequent caller. And the hardcoded "decryptModel received a table…" text points bulkDecryptModels users at the wrong call site. A small factory unknownTableFailure(method: string) constructing a fresh object per call fixes both; this path is cold by definition.

4. src/encryption/v3.ts:165 — the unknown-table guard covers 2 of 8 methods; the other 6 fall through to raw FFI errors.
encrypt/encryptQuery/encryptModel/bulkEncryptModels with the same unknown table pass straight to the FFI and fail deep inside with "column not found in Encrypt config" (or, if a column name collides across tables, silently encrypt under the wrong table's index/cast config). Validating the schemas against client.getEncryptConfig() once at typedClient construction would catch the mismatch for all eight methods instead of per-call for two — and pairs naturally with the tableName-keying fix from finding 1.

5. The new unknown-table failure branch has no test.
Neither typed-client-v3.test.ts nor any other suite calls decryptModel/bulkDecryptModels with an unregistered table — the two new if (!reconstruct) branches and the error shape they return are unpinned. One test with a second encryptedTable(…) instance would cover both branches and (usefully) document the identity-keying behavior from finding 1.

6. __tests__/v3-matrix/matrix-live-pg.test.ts:280 — stale test title.
The ord proof is still named "ord_term/ore_block_256 equality-via-ORE selects the exact row", but ordDomains now includes text_ord/text_ord_ore whose terms are deliberately built with orderAndRange — equality-via-ORE is only what's proven for the pure-ORE domains. Rename to something like "ord_term/ore_block_256 matches the seeded row" so a future reader doesn't assume SQL =-over-ob is proven for text domains.

7. src/encryption/v3.ts:145 — the no-date-table identity-function optimization is still on the table.
rowReconstructor shallow-copies every row even when dateProperties is empty (the common case). Now that the factory runs once per table at construction, if (dateProperties.length === 0) return (row) => row is a one-liner that eliminates the per-row copy on the bulk path this PR was optimizing. (Heads-up for the #542 merge: that branch widens this same filter to castAs === 'timestamp' — the guard should land after that widening.)

8. __tests__/v3-matrix/matrix-live-pg.test.ts:124storageDomains enumerates index kinds negatively in a second place.
!unique && !ore && !match must be extended every time a new index kind exists, or a domain carrying only the new index silently classifies as storage-only and its query path is never proven (green suite, no coverage). Object.keys(spec.indexes ?? {}).length === 0 — or a single proofsFor(indexes) helper returning the set of applicable proofs — states the rule once and fails loudly instead.

Automated multi-agent review (8 finder angles, verified by direct inspection and a live CI-log check); merge compatibility with #542 verified via git merge-tree (clean).

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

Approved, with some findings that should be considered for a follow-up.

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

Approving — all seven follow-ups from the #541 review are faithfully and correctly implemented, CI is green with the live-pg matrix genuinely running (40 tests), and the branch merges cleanly with #542.

Detailed feedback with 8 non-blocking findings is in #559 (comment) — the top three (identity-keyed reconstructor Map narrowing accepted tables, the FTA gate triggering on match-defaults.ts without scanning it, and the shared mutable unknownTableFailure object) are worth addressing while the file is open, but none block merging.

@tobyhede tobyhede merged commit 63fe076 into feat/eql-v3-text-search-schema Jul 6, 2026
7 checks passed
@tobyhede tobyhede deleted the fix/eql-v3-review-followups branch July 6, 2026 06:51
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