Skip to content

stack-encrypt: every index is compiled into the core crate — move Equality, Match, Ore, Ope into their own crates #1063

Description

@coderdan

Background

stack-encrypt (packages/stack-encrypt) is our Rust field-level encryption library. Its indexes (Equality, Match, Ore for order-revealing encryption, Ope for order-preserving encryption) derive search terms beside a ciphertext. #1056 introduces an Index<S> trait so the plan builder only ever sees "an index" and tuples of them (design: docs/plans/2026-10-04-plan-builder.md, PR #1052, "The index crates").

Problem

All index implementations live inside stack-encrypt. An earlier effort split search-term handling into modules under src/sem, not crates, so every index's dependencies (ORE and OPE libraries, match tokenisation) are compiled into every consumer, and adding an index means changing stack-encrypt itself.

Proposal

  1. Put Index<S> and IndexSpec (the data form of an index) in a small core crate the engine depends on.
  2. Move Equality, Match, Ore and Ope (and later Json, see stack-encrypt has no searchable JSON — add a Json index that seals a document's entries under one data key #1060) into their own crates, each implementing Index<S>. Parameters stay on the struct and lower through spec().
  3. The builder needs no change, because it only sees the trait and tuples. Byte output must not change; existing fixtures prove it.

Relationship to other work

Activity

  1. self-assigned this
    on Oct 4, 2026
  2. added
    enhancementNew feature or request
    rustPull requests that update Rust code
    on Oct 4, 2026
  3. coderdan commented on Oct 5, 2026

    @coderdan
    ContributorAuthor

    The data form of an index has to be open, or this split does not reach its goal.

    The proposal above puts Index<S> and IndexSpec in the core crate. Today (#1069) IndexSpec is a closed enum with four variants: Equality, Match(options), Ore, Ope. An index crate outside core cannot add a variant to it, so every new index would still mean editing core for its data form, and a binding (the Go guest, a future napi shell) could never name an index the enum does not list. The trait side is already open; the data side is not.

    What this issue should deliver instead:

    1. IndexSpec becomes a record, not an enum: the kind name the index declares (eq, match, ore, ope, later json and whatever a new crate adds) plus that index's own serialised options. Index<S>::spec() produces it, as it does today. The wire strings do not change.
    2. The reverse direction, data to operation, needs the engine to resolve a kind name to the crate that implements it. That means the engine is compiled with a known set of indexes. Decision this issue must make: an explicit set handed to the lowering (dynamic::record, stack-encrypt: dynamic::record is a second executor that already seals differently from the derive — make it a lowering into the plan builder #1059), or compile-time registration (the inventory pattern). Either way the set is fixed at build time; a name outside it is a plan error before any key request. Indexes are cryptographic operations the engine has to know about regardless, so this is not the per-language EQL assembly question, which was settled the other way (each language assembles EQL types from standard outputs; no registry).
    3. The two enums that exist in the meantime, IndexSpec and the older dynamic::TermKind, are being collapsed into one ahead of this work so the boundary has a single spelling. This issue then replaces that one enum with the open record.

    Related: #1056 (introduced Index<S> and IndexSpec), #1059 (the lowering that consumes the data form), #1060 (the first index that would be added after the split).

  4. coderdan commented on Oct 8, 2026

    @coderdan
    ContributorAuthor

    ADR-0002 (#1139) moves order-term derivation into a new vitaminc crate,
    vitaminc-ore, with block ORE, CLLW ORE and CLLW OPE as schemes over one
    canonical plaintext encoding. The Ore and Ope index crates proposed here
    become thin wrappers over it.

    Step 3's "byte output must not change" no longer holds for three cases, all
    intended and listed in the ADR:

    • float terms for -0.0 and negative-sign NaN, which now canonicalise;
    • Bool, which loses its order terms;
    • text order terms, which normalise and fold.

    None of these is deployed. The fixtures change with #1140.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

SDKenhancementNew feature or requestrustPull requests that update Rust code

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions