Skip to content

feat(stack-encrypt): store short TextMatch text as an empty filter, refuse only short queries - #1206

Merged
coderdan merged 4 commits into
nextfrom
feat/stack-encrypt-short-match-empty-filter
Oct 17, 2026
Merged

coderdan merged 4 commits into
nextfrom
feat/stack-encrypt-short-match-empty-filter

Conversation

@coderdan

@coderdan coderdan commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Stacked on #1192. This PR implements the stored/query split of ADR-0009 for short TextMatch text: a value with no tokens is stored as the empty filter, and only a query with none is refused.

  • The stored side stores it. match_term derives the empty filter (bf: []) for text that cuts no token under the index's tokenizer: empty or separator-only text, or text shorter than the n-gram length. No PRF call is made. MatchTerms as a target, KeysetCipher::match_terms and the dynamic record path all store it, so a two-letter name no longer fails a whole row, and the value seals and opens as any other. No query matches the empty filter, because a query always sets at least one bit.
  • The query side refuses it. op::Supports::check now takes the query's value. The Match index refuses token-less text there with TermError::EmptyTermText, when the query is awaited, before any term is derived and before any keyset is loaded. The chain runs the same check in prepare, so a chain prepared on its own is held to it too. dynamic::term refuses it for a match kind, so the Go guest's term export behaves as before.
  • The error is reworded, not renamed. TermError::EmptyTermText keeps its code, stack_encrypt::empty_term_text, so a binding's mapping holds; its message and help now describe the query.

What this reaches outside the crate

The Go module builds the stack-encrypt guest by path, so this lands there at once rather than with a release. Its round-trip test of the old refusal is rewritten to the new behaviour: a short TextMatch value seals, opens, and matches no query, and a short match query is still ErrTerm. The package docs and the term-failure message follow. The guest's own leak test, which expects term to refuse separator-only match text, passes unchanged.

eql-bindings builds no TextMatch type through stack-encrypt yet, so nothing changes there.

Two tests that used token-less text as the leaf that fails before any I/O now use a match configuration with invalid options, which still fails during the synchronous build.

Tests

  • tests/op.rs: a short value stores as the empty filter, opens as written, is found by its equality term and by no match query; a match query with no tokens is refused through on, through via and in an all batch, with the named keyset never loaded; the same text is a fine equality query and a fine stored value.
  • tests/sem_terms.rs: the empty filter for every token-less input, under both tokenizers, holds no query's positions and round-trips through its bytes.
  • dynamic::term unit test: the same scalar is refused as a query and stored empty through Index<Value>.
  • Go: TestAMatchIndexStoresShortText.

Test plan

  • cargo nextest run -p stack-encrypt --all-features: 578 passed.
  • Doctests, clippy with -D warnings with and without --all-features, rustdoc with warnings as errors, cargo fmt --check.
  • The guest's native tests (58 passed) and go test ./encrypt/ against a rebuilt deterministic guest, where the one failure is the EQL guest not being built locally.
  • CI, including mutants on the diff and the Go jobs.

The issue's last checkbox, a stack-encrypt release so the Go SDK can stop refusing short text, turned out not to apply: the Go module builds the guest by path, so the Go side changes in this PR.

Closes #1185

https://claude.ai/code/session_01VYKAWbd361xDLmqBdLoUcB

Summary by CodeRabbit

  • New Features
    • Short or empty values can now be stored, encrypted, and decrypted in match-indexed records. They produce empty filters and won’t appear in match-query results.
  • Bug Fixes
    • Match queries with empty, separator-only, or too-short text are consistently rejected with a term error, including in dynamic and batched queries.
    • Invalid match options are reported before key loading or generation.

@coderdan
coderdan requested a review from a team as a code owner October 14, 2026 06:50
@changeset-bot

changeset-bot Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: c816046

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 Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

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: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 04c5cda6-8764-4519-b73c-eede24347253

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

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 563c9023-a7ab-4f12-adb6-06bb78ad0ed4
📥 Commits

Reviewing files that changed from the base of the PR and between 8aed9a6 and a9f42c1.

📒 Files selected for processing: 16
  • languages/golang/encrypt/doc.go
  • languages/golang/encrypt/locate_test.go
  • languages/golang/encrypt/records.go
  • languages/golang/encrypt/roundtrip_test.go
  • packages/stack-encrypt/CHANGELOG.md
  • packages/stack-encrypt/docs/adr/0009-operators-select-the-index-and-a-query-term-is-not-the-stored-term.md
  • packages/stack-encrypt/src/dynamic/term.rs
  • packages/stack-encrypt/src/op/mod.rs
  • packages/stack-encrypt/src/plan/chain.rs
  • packages/stack-encrypt/src/sem/mod.rs
  • packages/stack-encrypt/src/sem/tokenize.rs
  • packages/stack-encrypt/tests/derive.rs
  • packages/stack-encrypt/tests/op.rs
  • packages/stack-encrypt/tests/sem_terms.rs
  • packages/stack-encrypt/tests/target.rs
  • packages/stack-encrypt/tests/ui/op_inexact_operator_needs_probabilistic.rs

Included review availability: This review used your included allowance. Your plan provides up to 2 included reviews per hour; 0 remain after this review.

📝 Walkthrough

Match-term generation now stores text that produces no tokens as an empty filter. Match queries that produce no tokens remain rejected. Query validation checks match options and query text before query preparation. The Go record path distinguishes query-only refusals from record term failures.

Changes: Tokenless Match Text

Layer / File(s) Summary
Store tokenless text as empty filters
packages/stack-encrypt/src/sem/*, packages/stack-encrypt/tests/sem_terms.rs, packages/stack-encrypt/tests/op.rs, packages/stack-encrypt/tests/derive.rs, packages/stack-encrypt/CHANGELOG.md, packages/stack-encrypt/docs/adr/0009-operators-select-the-index-and-a-query-term-is-not-the-stored-term.md
Match-term generation returns an empty filter when stored text produces no tokens. Tests cover empty, separator-only, and short values, including filter serialization and round trips. Documentation describes the stored-value and query behavior.
Validate match queries before preparation
packages/stack-encrypt/src/op/mod.rs, packages/stack-encrypt/src/plan/chain.rs, packages/stack-encrypt/src/dynamic/term.rs, packages/stack-encrypt/tests/op.rs, packages/stack-encrypt/tests/target.rs, packages/stack-encrypt/tests/ui/op_inexact_operator_needs_probabilistic.rs
Match validation checks options and query tokens. Query operations and dynamic term construction reject tokenless match queries and invalid options. Tests cover query errors, batching, and validation before keyset loading.
Handle tokenless values in Go records
languages/golang/encrypt/records.go, languages/golang/encrypt/locate_test.go, languages/golang/encrypt/roundtrip_test.go, languages/golang/encrypt/doc.go
The Go record path excludes query-only empty-term refusals when attributing record term failures. Tests cover storing short values, refusing short queries, and preserving the ErrTerm sentinel. The documentation describes empty filters for tokenless stored text.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Feature · Severity of issue fixed: Medium

Sequence Diagram(s)

sequenceDiagram
  participant QueryOp
  participant MatchIndex
  participant QueryTokens
  QueryOp->>MatchIndex: check query value
  MatchIndex->>QueryTokens: tokenize with match options
  QueryTokens-->>MatchIndex: return no tokens
  MatchIndex-->>QueryOp: return EmptyTermText
Loading

Suggested reviewers: freshtonic

Merge Risk: 🔵 Low · up to a9f42

Normal match queries reject short text, and short values can be stored. Callers that use a stored filter as a query may still get an unintended result, so that boundary warrants follow-up, but it does not block merging.

Security architecture risk: 🔵 Low · up to a9f42

Encrypted-column readers can identify values that produce no match tokens, although they cannot distinguish the underlying values from that fact alone. Normal query paths retain tokenless-query rejection. Raw term consumers require care because they do not inherit the typed query controls.

Retained concerns

  • Low · security · observed: Newly accepted tokenless values share an observable empty match filter. Anyone who can read the indexed ciphertext can identify this tokenizer-dependent class without decryption authority. This is an intentional, documented confidentiality tradeoff: existing filters already expose approximate length through their fill, but the new empty representation exposes an exact tokenless class rather than the plaintext or exact length.
Security review details

Security Blast Radius

  • observed — The confidentiality effect applies to tokenless values newly stored through the shared match-term producer, including dynamic record indexing. Exploitation of the classification disclosure requires access to the visible indexed ciphertext, not a decryption key; the disclosed class depends on the configured tokenizer.

Security Findings and Attack Paths

  • inferred — The supported SQL match predicate is not an all-rows bypass for an empty query: its existing cardinality guard excludes nonempty stored filters. It does allow empty query filters to match empty stored filters. Directly supplied operands therefore have different guarantees from validated MatchQuery values; no downstream consumer establishing an authorization or plaintext-disclosure attack was identified.

Trust Boundaries and Controls

  • observed — Typed and dynamic query paths reject tokenless Match text, while storage intentionally accepts it. MatchQuery construction is crate-private, but stored MatchTerms and query_index output can be serialized. The Rust type distinction consequently does not enforce query validity after callers cross into raw-byte or SQL operands.

Resilience and Maintainability Implications

  • observed — The guest term export resolves the selected keyset before dynamic tokenless-query rejection. This ordering is unchanged from the base, so the typed Rust guarantee of rejection before key loading must not be generalized to the guest. No new key-loading exposure is established by this PR.

Hardening Proposals

  • proposed — If a downstream application accepts serialized match operands from less-trusted clients, validate nonempty query filters at that boundary rather than relying solely on the Rust query type. This is conditional hardening, not an observed vulnerable integration.
Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 63.16% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 38 functions across 14 files. (2 skipped:… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Linked Issues check Passed Issue #1185 requires different stored and query behavior for token-less match text. match_term now returns an empty MatchTerms without a PRF call. Supports::check and chain preparation reject to…
Out of Scope Changes check Passed The Rust implementation, dynamic path, Go behavior and diagnostic handling directly implement issue #1185. The added tests verify the required behavior. ADR, API documentation, changelog, and failure-…
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly describes the main change: short stored TextMatch text uses an empty filter, while short queries remain rejected.
Full details: Linked Issues check

Explanation

Issue #1185 requires different stored and query behavior for token-less match text. match_term now returns an empty MatchTerms without a PRF call. Supports::check and chain preparation reject token-less queries with TermError::EmptyTermText before term derivation and keyset loading. The dynamic query path applies the same refusal. Tests cover empty, separator-only, and short text; storage, round-trip, no match, equality, query refusal, batch paths, and keyset loading. The implementation uses separate stored and query paths, and the Go tests cover short match values.

Full details: Out of Scope Changes check

Explanation

The Rust implementation, dynamic path, Go behavior and diagnostic handling directly implement issue #1185. The added tests verify the required behavior. ADR, API documentation, changelog, and failure-location updates document or support that behavior. No unrelated change is established. The requested release is a non-coding task and does not affect this coding assessment.

Full details: Docstring Coverage

Explanation

Docstring coverage is 63.16% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 38 functions across 14 files. (2 skipped: 2 unsupported.)

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-14T06:54:00.450316Z b05caf6 PR opened
🔒 Security Review ✅ Completed 2026-10-14T06:55:36.404490Z b05caf6 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions

github-actions Bot commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Mutation testing (cargo-mutants, --in-diff, stack-auth + stack-encrypt)

caught missed unviable timeout
134 0 53 0

Every mutant in the changed lines was caught by a test.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b05caf67fd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/stack-encrypt/src/op/mod.rs Outdated

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

Review of this PR's diff against #1192.

What I checked. A short stored value cannot cause a false negative. A query must cut at least one n-gram, so its text has at least n characters, and it cannot be a substring of shorter stored text. Case folding runs before the tokenizer on both sides, so the two sides measure length the same way. verify on a short row returns false, which is correct. The Go refusal of a short match query (#1164/#1165) stays, through dynamic::term. The EQL payload shape does not change: the stored domain checks only that bf exists, so bf: [] is valid. Mutants: 8 caught, 0 missed. Codex already raised the Ngram { length: 0 } panic in the new pre-checks; I do not repeat it.

Blocking

  1. A caller who builds a query with KeysetCipher::match_terms now gets a match-all query in SQL with no error. See the inline comment at src/sem/mod.rs:1297.

Should fix (not blocking)

  1. The Go locateTermFailure now tests a stored value with the query rule, so it can name the wrong row. See the inline comment at records.go:485.

Suggested follow-up PR

  • Length leak. A stored bf: [] is visible to a database reader. That reader can now select every row whose value is empty, only separators, or shorter than the n-gram length. Before this PR, such values could not be stored. The Bloom fill already leaks approximate length, but this is an exact class. Add one sentence to ADR-0009 (the verify paragraph, line 284) and to languages/golang/encrypt/doc.go:107.
  • Server guard. The EQL query_text_* domains check only that bf exists (packages/eql/src/v3/scalars/text/query_text_types.sql:45). An empty bf is "contained by everything" (packages/eql/src/v3/sem/bloom_filter/functions.sql:45). A CHECK for a non-empty bf on the query domains closes the match-all case at the server for every client. File an issue if it does not go in this stack.
  • CI. The Test EQL PG17 shards fail on SteVec selector drift (v3_doc_integer $.field op-selector drifted from the pinned constant). The base #1192 fails the same way, and this PR touches no EQL file. It is not caused by this PR.

Comment thread packages/stack-encrypt/src/sem/mod.rs
Comment thread languages/golang/encrypt/records.go Outdated
@coderdan
coderdan force-pushed the feat/stack-encrypt-short-match-empty-filter branch from b05caf6 to 9dbb6b5 Compare October 16, 2026 11:16
@coderdan
coderdan force-pushed the feat/stack-encrypt-target-limit branch from 57476bf to dbe3661 Compare October 16, 2026 11:16
@coderdan

Copy link
Copy Markdown
Contributor Author

Rebased with #1190 onto the current #1177. In ADR-0009, this PR's paragraph on the empty filter (only the query is refused, when it is awaited) replaces #1177's "refused at cipher.query" sentence. Code unchanged.

@coderdan
coderdan added this pull request to stack #1230 October 16, 2026 11:24
@coderdan
coderdan force-pushed the feat/stack-encrypt-short-match-empty-filter branch from 9dbb6b5 to a9f42c1 Compare October 16, 2026 12:06
@coderdan
coderdan force-pushed the feat/stack-encrypt-target-limit branch from dbe3661 to 8aed9a6 Compare October 16, 2026 12:06
@coderdan

Copy link
Copy Markdown
Contributor Author

Review fixes are in a9f42c1; each inline thread has a reply. The suggested follow-up on the length leak is in the same commit: ADR-0009's verify paragraph and doc.go now say a stored empty filter tells a reader of the column the value is empty, separator-only or shorter than the n-gram length. Still open: the EQL non-empty bf CHECK (see the inline reply). Everything above was rebased.

@coderdan
coderdan force-pushed the feat/stack-encrypt-short-match-empty-filter branch from a9f42c1 to c8a3934 Compare October 16, 2026 13:11
@coderdan
coderdan force-pushed the feat/stack-encrypt-target-limit branch from 8aed9a6 to a794e7d Compare October 16, 2026 13:11
@coderdan

Copy link
Copy Markdown
Contributor Author

Restacked onto next with the B stack. A new commit, c8a3934, fixes a fail-open that only appears once this PR meets the EQL v4 types in next. eql-bindings builds both the stored TextMatch filter and the TextMatchQuery operand through match_terms, so after this PR a TextMatchQuery of "" or "al" became bf: [], which matches every row, through Rust and through the Go SDK. eql-bindings now refuses token-less text for v4 TextMatch, stored or queried, which is what it did before and what its docs and tests say. Splitting it into store-empty and refuse-the-query needs a query-only filter type in codegen, so that's left for later. The commit also corrects the Go and stashgen READMEs about the plain match index.

Copy link
Copy Markdown
Contributor
  • packages/stack-encrypt/src/plan/chain.rs:970-988 (minor): one awaited query(op::Matches, ..) tokenizes the query text three times before the term is derived: once in Operation::check, once more in prepare's re-check, and once in carry. Each pass allocates a fresh zeroized copy of the plaintext. packages/stack-encrypt/src/dynamic/term.rs:313-322 copies the same validate, tokenize and refuse-if-empty logic by hand, then tokenizes again inside scalar_term. One helper that returns the QueryTokens (or refuses) could serve both paths and feed carry. That removes the extra passes and stops the Rust chain and the Go guest drifting on what they refuse.

Generated by Claude Code

…er, refuse only the query

A match value that cuts no token under the index's tokenizer (empty or
separator-only text, or text shorter than the n-gram length) used to fail
term derivation on both the stored and the query side, so one two-letter
name failed a whole row. ADR-0009 splits the two: the stored term is the
empty filter, which no query matches, and only a query of such text is
refused, because its filter would match every row.

`match_term` now derives the empty filter for such text, with no PRF call,
so `MatchTerms` as a target, `KeysetCipher::match_terms` and the `dynamic`
record path all store it, and the value seals and opens as any other. The
refusal moves to the query paths: `op::Supports::check` takes the query's
value, the `Match` index refuses token-less text there with
`TermError::EmptyTermText` when the query is awaited, before any term is
derived or keyset loaded, and `dynamic::term` refuses it for a match kind.
The error keeps its code and is reworded for the query.

The Go module builds the stack-encrypt guest by path, so its round-trip test
of the old refusal is rewritten to the new behaviour: a short `TextMatch`
value seals, opens and matches no query, and a short match query is still
`ErrTerm`. The package docs and the term-failure message follow.

Two tests that used token-less text as a leaf that fails before any I/O now
use a match configuration with invalid options, which still fails during
the synchronous build.

Closes #1185

Claude-Session: https://claude.ai/code/session_01VYKAWbd361xDLmqBdLoUcB
match_terms is no longer a query. In 0.2.0 the same call served write
time and query time and refused text that cuts no token; it now returns
the empty filter for that text, and EQL treats an empty filter as
contained by every stored filter. A caller who still sends its output as
a query gets every row and no error, so the CHANGELOG entry moves to
Breaking and says so plainly, naming cipher.query(op::Matches, ..) as the
query.

The query side validated the match options only when it derived the term,
after the new no-token check had tokenised the text, so an n-gram length
of zero panicked in the tokenizer instead of returning InvalidOptions.
Both query prechecks, op::Supports::check and dynamic::term, now validate
first, as the stored path always has.

Go's term-failure locator asks the query path for each term, and the
query path refuses short match text that a record stores as the empty
filter. It could stop at an earlier row with a short email and name the
wrong row. It now skips that one refusal by its code, and a test pins it
with a short email in row 0.

ADR-0009 and the Go package doc say that a stored empty filter tells a
reader of the column the value is empty, separator-only or shorter than
the n-gram length. The tokenizer's doc no longer says match_terms rejects
that text.
…tch, stored or queried

Rebased onto next, the EQL v4 TextMatch filter met this PR's change to
match_terms. eql-bindings builds the stored TextMatch filter and the
TextMatchQuery operand from one type, BloomFilter, through the stored-side
match_terms. Before this PR that refused text that cuts no token, so both
refused it. After it, match_terms returns the empty filter for that text,
so a TextMatchQuery of "" or "al" became `bf: []`, which EQL treats as
contained by every stored filter: a query matching every row, with no
error, through Rust's encrypt_as and through the Go SDK's generated Query.

BloomFilter's visitor now refuses an empty filter with
TermError::EmptyTermText, for the stored value and the query alike, which
is what EQL v4 TextMatch did before and what its docs, the Rust tests and
the Go test already say. Storing a short value as the empty filter while
refusing it as a query needs the query twin to have a filter type of its
own, which is a codegen change left for later.

The Go SDK README and the stashgen README still said a plain `match` index
refuses short text; they now say it stores the empty filter and refuses
only the query, and that an EQL TextMatch field is stricter.
- One rule for a match query, in one place. `QueryTokens::of_query`
  validates the options, tokenises the text and refuses text that cuts
  no token. The query chain and `dynamic::term` both call it, so the Rust
  chain and the Go guest cannot drift on what they refuse; `dynamic::term`
  no longer repeats the validate, tokenise and refuse steps by hand.
- An awaited `op::Matches` query tokenises its text once fewer.
  `Supports` gains a provided `admit`, which checks and takes the carry
  together, and a match index overrides it to tokenise once for both.
  `prepare` calls `admit` instead of a second `check` and then `carry`.
  The term itself is still derived through the index's own operation,
  which tokenises again: that is the path a write takes, and sharing it
  is what makes the query's filter the stored filter's.
@coderdan
coderdan force-pushed the feat/stack-encrypt-short-match-empty-filter branch from c8a3934 to c816046 Compare October 17, 2026 02:29
@coderdan
coderdan force-pushed the feat/stack-encrypt-target-limit branch from a794e7d to 2ec5306 Compare October 17, 2026 02:29
@coderdan

Copy link
Copy Markdown
Contributor Author

Re the triple tokenising and the copied refusal logic (chain.rs:970-988, dynamic/term.rs:313-322): fixed in c816046.

  • QueryTokens::of_query is now the one rule for a match query. It validates the options, tokenises the text, and refuses text that cuts no token. The query chain's Supports::check and dynamic::term both call it, so the Rust chain and the Go guest can't drift on what they refuse. dynamic::term no longer copies those steps by hand.
  • Supports gains a provided admit that runs the check and takes the verifier together. The match index overrides it so one pass over the text does both. prepare calls admit instead of re-running check and then calling verifier, so an awaited op::Matches query now tokenises once less.
  • The term is still derived through the index's own operation, and that tokenises again. I left that pass on purpose. It's the path a write takes, and sharing it is what makes the query's filter equal the stored filter. Feeding it pre-cut tokens would split the write path from the query path.

A unit test pins of_query's order: options first, then empty text, then tokens.

On the restack: #1191 now calls Supports::Carry Supports::Verifier (and carry verifier), so admit returns the verifier. #1215's dynamic::query match path also goes through of_query now, so all three callers share the rule.

@coderdan
coderdan removed this pull request from stack #1230 October 17, 2026 05:57
Base automatically changed from feat/stack-encrypt-target-limit to next October 17, 2026 05:59
@coderdan
coderdan merged commit 25371aa into next Oct 17, 2026
94 of 112 checks passed
@coderdan
coderdan deleted the feat/stack-encrypt-short-match-empty-filter branch October 17, 2026 06:00
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.

stack-encrypt: store short TextMatch text as an empty filter, refuse only short queries

3 participants