@@ -617,8 +617,21 @@ A library that maps one struct field to one column needs a model for this layout
617617### EQL types
618618
619619An EQL type is the Go type of one EQL column.
620+ It holds the EQL value as the JSON bytes that Postgres stores.
620621` eql-codegen ` writes the package ` encrypt/eql ` from the EQL catalog.
621622It is a package in the Go module, and not a module of its own.
623+
624+ The guest builds every EQL value.
625+ For a field with ` encrypt_into ` , the declaration names the EQL type, and the guest runs that type's own Rust plan.
626+ It returns the finished value, and generated code stores it as it is.
627+ No Go code assembles an EQL value.
628+
629+ The guest comes in two builds.
630+ ` encrypt ` embeds the build without the EQL types, and ` encrypt/eql ` embeds the build with them.
631+ A generated file that names an EQL type imports ` encrypt/eql ` , so a program with EQL types links the build that has them.
632+ When the program starts, ` encrypt/eql ` registers its build, and that registration cannot fail.
633+
634+ The size of the build with the EQL types is measured before the SDK ships two builds or one.
622635The same catalog gives the Rust and TypeScript types, so a type has one name in every language, such as ` TextEq ` .
623636The JSON type is the exception: its Go name is ` JSON ` .
624637
@@ -840,7 +853,9 @@ The compiler then finds a removed field and a field with a new type, and CI find
840853The generator loads the package with ` golang.org/x/tools/go/packages ` and reads types, not text.
841854It runs none of the package's code.
842855It ignores its own output file when it loads the package, so a stale file does not stop it.
856+
843857It checks each declaration with the engine: it runs the guest that the SDK embeds, and it holds no copy of the engine's rules.
858+ It asks that guest for the EQL types it holds: each name, its plaintext type, its indexes and its query forms.
844859The same input always gives the same file: fields keep their declared order, and the file carries no version and no time.
845860
846861` stashgen ` stops with an error, and writes no file, for each of these:
@@ -947,23 +962,23 @@ No function in the SDK or in generated code panics for a declaration, and none h
947962### What crosses the binding
948963
949964The engine does all encryption, decryption and term derivation.
950- A field crosses the binding only when its value does.
951- Generated code sends the engine a declaration and a value for each sealed field and each indexed field.
952- It sends nothing for a passthrough field, and it copies that value to the generated type itself.
953- It sends nothing for a field that is left out.
954965
966+ Generated code sends the engine the whole struct: every field with its value, passthrough fields included.
967+ The engine returns the whole struct the same way.
968+ Nothing is rebuilt from parts on either side.
969+ A field that is left out does not cross.
955970So every field the engine is told of has a value, and every value has a field.
956- The generated file still names every field, so a reviewer reads the whole declaration.
957971
958- Every call to the guest carries a declaration.
959- The guest's exports that take a value and no declaration have no caller in Go.
972+ The declaration crosses with the struct.
973+ It names each field's context and indexes, and the EQL type of a field with ` encrypt_into ` .
974+ The guest returns the finished EQL value for that field.
960975
961- Generated code assembles an EQL value from the engine's ciphertext and terms .
962- Two fixtures that both test suites read guard the bytes:
976+ Every call to the guest carries a declaration .
977+ The guest's value exports, ` se_encrypt ` and ` se_decrypt ` , are removed.
963978
964- - ** The EQL fixture: ** encode in Rust, decode and encode again in Go, and compare .
965- - ** The record fixture: ** the Rust chain and generated Go code each open the records that the other encrypted.
966- Both derive the same bytes for each term.
979+ One fixture that both test suites read guards the bytes .
980+ The Rust chain and generated Go code each open the records that the other encrypted.
981+ Both derive the same bytes for each term.
967982
968983The package ` encrypt/gensupport ` holds what only generated code calls.
969984No function in it panics.
@@ -1013,6 +1028,7 @@ The existing Go package has never been released, so these are removed, not depre
10131028- ` EncryptedRecord ` and ` EncryptedField ` : a generated type replaces them.
10141029- ` Cipher.Term ` : the query methods of a field entry replace it.
10151030- ` RecordOption ` , ` WithPlan ` and ` ExtendContext ` : ` Cipher.Extend ` replaces the last.
1031+ - ` WithGuest ` : the import of ` encrypt/eql ` chooses the guest build.
10161032- ` TermKind ` : ` Index ` replaces it, for generated code.
10171033- ` Plan ` , ` FieldPlan ` , ` NewPlan ` and ` Plan.Validate ` : the generator replaces them.
10181034- ` PlanFromTags ` , and every other function that reads ` stash ` tags at run time.
@@ -1135,9 +1151,9 @@ the existing SQL bundle and its `eql_v3_*` domains, which this section does
11351151not change. Depends on #971 (` TextEq ` / ` TextEqQuery ` through stack-encrypt
11361152in ` eql-bindings ` , and ` Identifier ` as a two-segment ` Label ` ).
11371153
1138- ** The engine never returns an EQL type. ** It returns standard outputs:
1139- ciphertexts, terms and passthrough values. Each language's typed layer
1140- assembles EQL types from them :
1154+ ** The engine returns an EQL type only when a plan names it as a target. **
1155+ Otherwise it returns standard outputs: ciphertexts, terms and passthrough
1156+ values. Each language names the target its own way :
11411157
11421158- ** Rust** , through the EQL type's own ` EncryptFrom ` impl, named with the
11431159 typed verb. A field target may be any ` EncryptFrom ` type, so an EQL type
@@ -1151,23 +1167,25 @@ assembles EQL types from them:
11511167 . build ()? ;
11521168 ```
11531169
1154- - ** Go** , through code the generator in #1070 writes. The generated code asks
1155- the guest for the standard outputs of the field's indexes and assembles the
1156- EQL type in Go.
1170+ - ** Go** , through the data plan. A field names the EQL type as its target,
1171+ the guest runs that type's own plan, and the generated code stores the
1172+ value the guest returns. (Decided 2026-10-05: the first version of this
1173+ section had Go assemble the EQL type from standard outputs.)
11571174
1158- So there is ** no registry** of EQL types, ** no target name in the data
1159- grammar** , and no ` Target<S> ` trait. The WASI guest stays EQL-free: it runs
1160- data plans and returns standard outputs, exactly as for any other field. A
1161- shape mismatch (the outputs do not fit the EQL type) is a run-time error in
1162- Go, and codegen makes it unreachable in practice.
1175+ The data grammar has a ** target** field form, exclusive with the output
1176+ verbs, and no ` Target<S> ` trait. A dispatch that ` eql-codegen ` generates
1177+ resolves the name in the guest build that holds the EQL types; the guest build
1178+ without them refuses a target name. ` stashgen ` writes a target name only
1179+ beside the import that supplies the dispatch, so that refusal is unreachable
1180+ in practice.
11631181
11641182An EQL type is still a target that wraps an index's output; the index itself
11651183stays in stack-encrypt. The JSON type is the clearest case: the ` Json ` index
11661184produces the searchable document, and EQL's JSON type frames it.
11671185
1168- ** The cost: the EQL byte encoding lives twice ** , in ` eql-bindings ` (Rust) and
1169- in a Go EQL package. A standing cross-language fixture guards it: encode in
1170- Rust, decode and re-encode in Go, and compare the bytes .
1186+ ** The cost: a second guest build ** , with the EQL types in it. Its size is
1187+ measured before the SDK ships two builds or one. The EQL encoding lives once,
1188+ in ` eql-bindings ` .
11711189
11721190Match and JSON index options need a wire form first (Additions, item 7): the
11731191Go side derives its terms from a data plan, and a dropped option there is a
@@ -1213,27 +1231,16 @@ Then:
12131231- Lock and audit context themselves; this plan only leaves them a place.
12141232- Named term accessors on ` Encrypted<Terms> ` (decision 7).
12151233
1216- ## Open questions
1234+ ## Deferred
12171235
12181236- ** One request for several types.**
1219- One call covers one type today , because the guest takes one declaration in a call.
1237+ One call covers one type, because the guest takes one declaration in a call.
12201238 One request for several types needs the engine to run several plans under one key request.
12211239 It also needs a guest export that takes several declarations, each with its values.
12221240 The Go call for it is designed after that work.
1223- - ** The history in this plan.**
1224- "Why the first draft was dropped" and the rejected names under "Decisions" are history.
1225- The design principles keep history out of a design document, so those parts move to ADR-0007.
1226- Dan Draper owns that move.
1227- - ** Query building in Go.**
1228- The SDK gives the values for a search, and the program writes the SQL.
1229- A design for building that SQL is separate work.
1230- - ** A ` go vet ` check.**
1231- It reports a struct with sealed fields that a program prints, and an ` Encrypt ` call inside a loop.
1232- Its design is separate work.
1233- - ** A change to a declaration over time.**
1234- This design covers one version of a declaration.
1235- Reading data that an older declaration wrote needs its own design.
1236- A policy has ` Identity ` for a column with a new name, and tags have no word for it yet.
1241+
1242+ ## Open questions
1243+
12371244- ** Converging the TypeScript schema builder onto the plan grammar** , so
12381245 ` @cipherstash/stack ` stops being a second engine beside stack-encrypt.
12391246 Out of scope here; recorded so a TS binding does not grow an executor.
0 commit comments