Skip to content

feat(stack-encrypt): order text over ADR-0002's folded order key - #1235

Open
coderdan wants to merge 3 commits into
nextfrom
feat/stack-encrypt-text-ordering
Open

coderdan wants to merge 3 commits into
nextfrom
feat/stack-encrypt-text-ordering

Conversation

@coderdan

@coderdan coderdan commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Stack Encrypt (CipherStash's Rust encryption engine) could not order text: no <, >, BETWEEN or ORDER BY query was answered on a text field. The order terms it stored for text went through cllw-ore's own collation, which keeps case, so Zoe ordered before alice and é after z. This PR makes text orderable over the folded order key that ADR-0002 defines and vitaminc 0.6 ships: case and accents folded, under Unicode 16. A Go string field with index=ore or index=ope now gets Lt, Lte, Gt, Gte and Between.

Draft.

Changes

  • ADR-0009: the "if the catalog keeps text ordering" hedges are gone. Text ordering is decided, and decision 7 names text beside the integer types.
  • Term derivation (stack-encrypt sem):
    • Text ORE and OPE terms derive over vitaminc_ore::text's folded key. The folded bytes go through cllw-ore's byte encryption, whose ciphertext type is the one it gives text, so OreTerm<String>/OpeTerm<String> keep their type.
    • A text term's key framing adds the fold's frozen label (vitaminc/ore/value/text-folded/unicode-16/v1). Integer, float and bytes order terms keep their bytes and framing.
    • Text with a code point Unicode 16 does not assign has no order term (TermError::Normalize).
  • Operators (typed path): op::Ordered covers String. A new sealed Ordered::inverted makes Between judge an inverted range on folded keys: ("b", "A") is inverted, ("A", "a") is one value.
  • Operators (data-plan path):
    • ordered(kind) includes text, so bindings get the orderings.
    • An inverted text range, and order text that can't be folded, are refused at the boundary.
    • ore_of/ope_of produce the typed path's bytes.
  • Go:
    • stashgen writes the ordering methods on string fields with ore/ope.
    • The notice stays only for order indexes on bytes or a float, and for a second order index.
    • Generated files and goldens are regenerated, and enginetest.Static mirrors the engine.
    • The README, doc.go and the stashgen README describe the fold.
  • CHANGELOGs:
    • stack-encrypt, Breaking: text order terms change bytes (none deployed).
    • Go module, Added: ordering methods on text.
  • Dependency: vitaminc-ore = "0.6.0" in the root workspace and stack-encrypt. The lockfiles of the root, packages/eql, the encrypt guest and the stack-encrypt fuzz crate are refreshed and pass --locked.

Verification

  • mise x --env test -- cargo nextest run --workspace --all-features: 1183 passed.
  • clippy -D warnings (workspace, all targets and features), cargo fmt --check, stack-encrypt rustdoc -Dwarnings and mise run test:doc: clean.
  • Trybuild UI: op_order_on_text (ordering text no longer fails to compile) is now op_order_on_bytes. pass/plan_chain.rs gains Lt and Between on a text plan. op_between_takes_a_pair.stderr is re-recorded for the new Ordered message.
  • New Rust tests:
    • pinned text ORE/OPE bytes (term_bytes.rs);
    • folded equivalents are one term in every owned text form;
    • a text term is keyed apart from the same bytes as bytes;
    • unfoldable text has no order term;
    • typed Lt/Between on text: ties, alice < eclair < Élan < emu < Zoe in OPE byte order, ORE alice < Zoe, folded inverted ranges (tests/op.rs);
    • data-plan inverted text range, folded tie and boundary refusals.
  • EQL workspace: cargo test --locked -p eql-bindings --all-features and mise run types:check pass.
  • All five guests build, mise run wasm:guest:test passes, mise run go:test passes, mise run go:lint reports 0 issues, and go generate ./... leaves no diff.
  • Go:
    • The order property test's string case now checks the fold: random already-folded strings order by bytes; a/A, e/é/É/NFD é, ss/ß/SS and café/CAFE tie; mixed-case names order by the fold.
    • A new test checks that Probe.S.Lt of Café equals the stored term of cafe, and that Between("b", "A") is ErrTerm.
  • Not run locally: the Postgres query test (needs DATABASE_URL) and the CI mutants gate.

Related

Closes #1234. Stacked on #1216. Builds toward #1131 and #1132 (the EQL text order types).

Review notes

Text ordering is decided: the order key is the system ADR-0002's text fold, which vitaminc 0.6 ships in vitaminc-ore, and the ORE and OPE indexes answer the orderings on text over it. The ADR no longer says "if the catalog keeps text ordering" or that the paragraph goes if text ordering is dropped, and decision 7 names text beside the integer types.
@changeset-bot

changeset-bot Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 54a204d

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 17, 2026 •

Copy link
Copy Markdown
Contributor

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: 4b25098e-6ed1-4b8d-926a-69511a41c9c9

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
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

…the orderings on text

Text could not be ordered: no ordering operator was answered on a text
field, and the ORE and OPE terms stored for text went through cllw-ore's
own collation, which keeps case, so Zoe ordered before alice and é after
z. ADR-0002 defines text's order key as its fold (Unicode 16 NFC with case
and accents folded), and vitaminc 0.6 ships it in vitaminc-ore.

Text ORE and OPE terms now derive over that key on the typed and the
data-plan paths alike: the text is folded with vitaminc_ore::text and the
folded bytes go to cllw-ore's byte encryption, whose ciphertext type is
the one it gives text, so OreTerm<String> and OpeTerm<String> keep their
type. The released cllw-ore and ore-rs do not implement vitaminc-ore's
OrderScheme yet, so the CLLW schemes the engine already uses encrypt the
key. A text term's key is derived under a framing that adds the fold's
frozen label; integer, float and bytes order terms keep their bytes and
framing. Café, cafe, CAFÉ and cafe with a combining accent are one term.
Text holding a code point Unicode 16 does not assign has no fold and no
order term.

op::Ordered covers String, whose Between judges an inverted range on the
folded keys through a new sealed method, Ordered::inverted: ("b", "A") is
inverted, ("A", "a") is one value. The data path answers the orderings on
text too, refuses an inverted text range and unfoldable order text at its
boundary, and the Go generator now writes Lt, Lte, Gt, Gte and Between on
a string field with ore or ope. Its notice is left for order indexes on
bytes or a float, and for a second order index.

Closes #1234.
@github-actions

Copy link
Copy Markdown
Contributor

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

caught missed unviable timeout
24 0 18 0

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

Copilot AI 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.

🔵 Needs a closer look

Changes to cryptographic term derivation, key framing, and stored-byte compatibility warrant final human review.

No open issues 🎉

2 files not reviewed
  • languages/golang/encrypt/internal/testusers/kinds_stash.go: Generated file
  • languages/golang/encrypt/internal/testusers/probe_stash.go: Generated file
What changed in this PR

Enables folded text ordering in the Rust encryption engine and Go SDK, preparing for EQL text-order types.

Changes:

  • Derives text order terms using Unicode 16 case-and-accent folding.
  • Enables text comparisons and validates folded range bounds.
  • Updates generated Go methods, regression tests, documentation, and dependencies.
File Description
packages/​stack-encrypt/​tests/​ui/​pass/​plan_chain.rs Adds compiling text queries.
packages/​stack-encrypt/​tests/​ui/​op_order_on_text.stderr Removes obsolete rejection snapshot.
packages/​stack-encrypt/​tests/​ui/​op_order_on_text.rs Removes text-order rejection test.
packages/​stack-encrypt/​tests/​ui/​op_order_on_bytes.stderr Adds bytes-order rejection snapshot.
packages/​stack-encrypt/​tests/​ui/​op_order_on_bytes.rs Checks bytes ordering remains unavailable.
packages/​stack-encrypt/​tests/​ui/​op_between_takes_a_pair.stderr Updates ordering diagnostics.
packages/​stack-encrypt/​tests/​term_bytes.rs Pins folded terms and rejection behavior.
packages/​stack-encrypt/​tests/​op.rs Tests folded comparisons and ranges.
packages/​stack-encrypt/​src/​sem/​mod.rs Folds text and separates its key framing.
packages/​stack-encrypt/​src/​op/​mod.rs Enables typed text ordering.
packages/​stack-encrypt/​src/​dynamic/​term.rs Updates text-order documentation.
packages/​stack-encrypt/​src/​dynamic/​query.rs Enables and validates dynamic text queries.
packages/​stack-encrypt/​fuzz/​Cargo.lock Locks the folding dependency.
packages/​stack-encrypt/​docs/​adr/​0009-operators-select-the-index-and-a-query-term-is-not-the-stored-term.md Records text ordering as decided.
packages/​stack-encrypt/​CHANGELOG.md Documents changed text term bytes.
packages/​stack-encrypt/​Cargo.toml Adds vitaminc-ore.
packages/​eql/​Cargo.lock Updates engine dependencies.
languages/​golang/​stashgen/​testdata/​cases/​orders/​order_stash.go.golden Adds expected string ordering methods.
languages/​golang/​stashgen/​refusal_test.go Updates unused-index notice coverage.
languages/​golang/​stashgen/​enginetest/​enginetest.go Mirrors text-order capability.
languages/​golang/​stashgen/​emit.go Updates unused-index explanations.
languages/​golang/​encrypt/​README.md Explains folded ordering and refusals.
languages/​golang/​encrypt/​order_test.go Tests folded ordering and generated queries.
languages/​golang/​encrypt/​internal/​testusers/​probe_stash.go Regenerates string ordering methods.
languages/​golang/​encrypt/​internal/​testusers/​kinds_stash.go Adds named-string ordering methods.
languages/​golang/​encrypt/​guest/​Cargo.lock Updates guest dependencies.
languages/​golang/​encrypt/​doc.go Documents text ordering semantics.
languages/​golang/​cmd/​stashgen/​README.md Documents generated string comparisons.
languages/​golang/​CHANGELOG.md Records text ordering support.
Cargo.toml Declares workspace folding dependency.
Cargo.lock Locks vitaminc-ore and dependencies.

Review effort: Balanced

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 17, 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-17T10:37:20.784296Z 54a204d Draft marked ready
🔒 Security Review ✅ Completed 2026-10-17T10:38:11.490082Z 54a204d Draft marked ready
ℹ️ 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.

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

Automated review attestation

Source: Claude Code scheduled review (/code-review medium + /cross-check-review-findings) at head 54a204d. This is an automated review, not a human approval.

Critical / blocking issues: none.

Findings: none survived cross-check.

Reviewed (packages/stack-encrypt/src):

  • sem/mod.rs: OrderSource and text_order_key. Text goes through the folded key. The PRF context adds the frozen TEXT domain label only for text, so integer, float and bytes terms keep their bytes. The key is zeroizing and moved, not copied.
  • op/mod.rs: sealed Ordered::inverted. The String impl compares folded keys. Integers keep lo > hi.
  • dynamic/query.rs: ordered(kind), bound_order and the check_input fold refusal. They agree with the typed path (("b","A") inverted, ("A","a") a tie, an unassigned code point refused before a keyset).
  • Docs, ADR-0009, trybuild snapshots, lockfiles and the Go generated/golden files, checked for consistency with the Rust change.

Cross-check against CI on this head:

  • fmt/clippy/nextest/doctests/rustdoc, the Go matrix (Linux/macOS/Windows), Go lint and live tests, the EQL suites and the fuzz regressions are green.
  • The mutants gate caught 24 of 24 mutants (0 missed).
  • Copilot raised no issues.
  • The Codex code and security reviews were still running when this review was posted.

Notes (non-blocking):

  • OrderSource::of selects the text path by runtime Any downcast on String, Zeroizing<String> and &'static str. A new owned text type passed to an order term would silently take the non-folded Value path. This is fine today because Ordered is sealed, and the PR notes it. A test asserting every Ordered text form takes the folded path would guard it.
  • Text order terms change bytes (the CHANGELOG notes this as Breaking, with none deployed). Confirm no stored text ORE/OPE terms exist outside tests before merge.
  • Changeset-bot reports no changeset. That looks acceptable here since the touched crates and the Go module use their own CHANGELOGs, but a maintainer should confirm.

Generated by Claude Code

@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: 54a204d924

ℹ️ 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".

/// not assign, whose folding is not known.
pub(crate) fn text_order_key(text: &str) -> Result<Zeroizing<Vec<u8>>, TermError> {
use vitaminc_protected::Controlled;
Ok(Zeroizing::new(vitaminc_ore::text(text)?.risky_unwrap()))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve unassigned code points in text order keys

For any valid string containing a code point unassigned in Unicode 16—including characters introduced by later Unicode versions—propagating this error makes every ORE/OPE write and query fail. This contradicts ADR-0002's frozen-transform contract in docs/adr/0002-one-value-model-three-encodings-and-eql-v4-by-producer.md:187-195, which requires such code points to pass through unchanged so existing columns remain usable as Unicode evolves; derive the stable pass-through key rather than rejecting the value.

Useful? React with 👍 / 👎.

@coderdan

Copy link
Copy Markdown
Contributor Author

Parked until new cllw-ore and ore-rs releases.

This PR folds text inside Stack Encrypt (OrderSource in sem/mod.rs) and hands the folded bytes to cllw-ore 0.5. That's the wrong layer. The ORE crates own how each type is encoded for ordering, and the plan is for them to implement vitaminc-ore 0.6's OrderScheme and fold text internally. Stack Encrypt should pass the String to their encrypt unchanged. Folding in front of them duplicates their encoding, and the Any downcasts throw away the types their traits carry.

No released or built version of either crate implements vitaminc-ore 0.6 yet. Once they do, this PR gets reworked:

  • Bump the crates.
  • Delete OrderSource and text_order_key.
  • Pass text through, and judge Between's inverted range in the crate's order.

The engine surface stays: op::Ordered for String, the data path answering orderings on text, and the Go methods. #1234's proposal is updated to say so.

https://claude.ai/code/session_01BZhkvKLpNtKuzj3HCfMXoL

This branch has not been deployed

No deployments
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.

3 participants