Skip to content

Commit b5ec61b

Browse files
committed
docs(adr): amend ADR-0004 to ADR-0007 for the Go SDK
The review of #1070 found four ADRs whose text the Go SDK design leaves wrong. Each gets a dated amendment, and each amendment names the principle it rests on, so a reader can trace the change back. ADR-0007 gains the binding and language SDK words, the rule that a field crosses the binding only with its value, the record fixture as the proof of the lowering, the generator checking declarations with the embedded guest so the engine's rules have one source, and the fact that the guest takes one plan in one call. The value exports stay for another host of the guest and are not a Go path. ADR-0005 decision 4 named bindings/go, stackencrypt and stackauth. The module is at languages/golang and the packages are encrypt and auth. ADR-0006 said the Go binding has a Label held to Rust's by a fixture. The Go SDK has no Label: the segments come from the context= tag and the field name, and the engine's own parser checks them at go generate. The Go half of the fixture is retired with the Go Label. ADR-0004 decision 5 is convention in Rust and structural in Go, because Go has no standalone term derivation. Decision 6's plan-versus-value check moves to go generate for Go, with the engine's check as backstop. Claude-Session: https://claude.ai/code/session_014jiJy4WhYH2LXojK2jSoxi
1 parent 453f107 commit b5ec61b

4 files changed

Lines changed: 122 additions & 0 deletions

‎packages/stack-encrypt/docs/adr/0004-one-context-per-target-threaded-through-the-declaration-tree.md‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,3 +315,32 @@ What it does **not** fix: a term and a ciphertext written through two separate
315315
top-level calls still have no relation to each other, because neither knows the
316316
other exists. Decision 5 narrows this to callers who deliberately bypass the
317317
target layer on the write side.
318+
319+
## Amended 2026-10-06 (#1070)
320+
321+
G1 to G8 and Go-1 to Go-13 name the principles in
322+
`docs/sdk-design-principles.md`, general and Go.
323+
324+
Two decisions change shape in the Go SDK and keep their substance.
325+
326+
**Decision 5** keeps standalone term derivation as the query path and directs
327+
a stored term to a target, by convention and documentation, because the term
328+
methods cannot tell a probe from a stored term. In Go the rule is structural.
329+
The Go SDK has no `Cipher.Term`. A stored term exists only in generated code,
330+
beside its ciphertext, under the one context the declaration gives the field.
331+
A probe comes from a field entry's query method, which exists only for an
332+
index the field declares. The compiler refuses a query the field does not
333+
declare (G4: one declaration serves the write, the query and the read; Go-2:
334+
the caller reaches every output through a field).
335+
336+
**Decision 6** puts the plan-versus-value check at the FFI boundary, as plan
337+
validation, with the field named and before any key is requested. In Go that
338+
check moves earlier. `stashgen` checks the Go type against the declaration at
339+
`go generate`, by running the guest the SDK embeds, so a field type the engine
340+
cannot seal or an index that does not fit stops the generator. The engine's
341+
own check at the boundary stays as the backstop (G3: the earliest stage the
342+
language allows; Go-1). The dynamic-to-static dispatch itself is unchanged.
343+
344+
Decisions 1 to 4, 7 and 8 do not change. Decision 2's `extend` is what
345+
`Cipher.Extend` does from Go: a caller's context appends to the declared one,
346+
and never replaces it.

‎packages/stack-encrypt/docs/adr/0005-a-separate-credential-guest-for-the-profile-and-auth.md‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,3 +248,22 @@ crypto guest. A binary that encrypts now carries both guests. Neither
248248
sandbox changes. The crypto guest still has no environment and no
249249
filesystem, and the credential guest still has one mount. With no profile
250250
directory, `stackauth.OpenWithoutProfile` gives it none.
251+
252+
## Amendment (2026-10-06, #1070): the module at `languages/golang`, the packages `encrypt` and `auth`
253+
254+
G1 to G8 and Go-1 to Go-13 name the principles in
255+
`docs/sdk-design-principles.md`, general and Go.
256+
257+
Decision 4 named the module `bindings/go` and the packages `stackencrypt` and
258+
`stackauth`. The module is at `languages/golang`, beside the other language
259+
SDKs, and the packages are `encrypt` and `auth`.
260+
261+
The reason is Go-13: a name says what the thing is for, and no name repeats
262+
its package. Go code reads a package name at every use. `encrypt.Cipher` and
263+
`auth.ProfileStore` say what they are; `stackencrypt.Cipher` repeats the
264+
module's name on every line. ADR-0008 names the user-facing code the Go SDK,
265+
not the Go binding, so the directory is not `bindings`.
266+
267+
Everything else in decision 4 holds: one module, one internal package both
268+
import, `auth` does not import `encrypt`, and `ClientKey` is one type under
269+
both names. The 2026-09-27 amendment holds too: `encrypt` imports `auth`.

‎packages/stack-encrypt/docs/adr/0006-descriptors-render-with-a-slash-and-describe-is-open.md‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,3 +119,25 @@ part and otherwise the field name is used; a nested record is an ordinary typed
119119
field sealed under `<context>/<field>`, its inner layout being its own type's
120120
business. The struct-level `context = "<prefix>"` form and the rest of this
121121
decision are unchanged.
122+
123+
## Amended 2026-10-06 (#1070)
124+
125+
G1 to G8 and Go-1 to Go-13 name the principles in
126+
`docs/sdk-design-principles.md`, general and Go.
127+
128+
The `Label` decision said the Go binding has the same type and the same
129+
segment rule, held together by one fixture both test suites read. The Go SDK
130+
has no `Label`, `Context`, `NewContext` or `ParseLabel`. A field's two
131+
segments are the value of the struct's `context=` tag and the field's name
132+
(Go-7: the user never sees the plan, and no call takes a context, G4).
133+
134+
The segment rule has one implementation. `stashgen` checks a declaration by
135+
running the guest the SDK embeds (ADR-0007, amended), so a `/` or an escaped
136+
character in a `context=` value is refused at `go generate` by the engine's
137+
own `Label` parser (G1, Go-1). The Go half of the fixture is retired with the
138+
Go `Label`; the Rust half stays, and the record fixture in ADR-0007 covers the
139+
bytes Go and Rust must agree on.
140+
141+
The derive's `struct = .., context = "<prefix>"` form and the Go tag bind the
142+
same pair, so a row the derive writes opens through the Go SDK and the
143+
reverse. That is the interoperability this ADR was for.

‎packages/stack-encrypt/docs/adr/0007-bindings-enter-through-a-plan-never-a-second-executor.md‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ extends: ADR-0003, ADR-0004
1111
> (after its grammar is narrowed to the plan's), how EQL types are assembled
1212
> (per language, from standard outputs), and the consequence that the EQL
1313
> encoding lives twice.
14+
>
15+
> **Amended 2026-10-06**, by #1070. The decision is unchanged. Amended: the
16+
> words binding and language SDK, what crosses the binding from Go, the proof
17+
> of the lowering, where the generator gets the engine's rules, and what the
18+
> guest takes in one call.
1419
1520
Stack Encrypt has one execution engine: the `Encryption` and `Decryption`
1621
descriptions in `target/` and the batched `Pending` they produce. ADR-0003
@@ -111,3 +116,50 @@ Option 3.
111116

112117
The design history, the names rejected on the way and the sequencing are in
113118
`docs/plans/2026-10-04-plan-builder.md`.
119+
120+
## Amended 2026-10-06 (#1070)
121+
122+
G1 to G8 and Go-1 to Go-13 name the principles in
123+
`docs/sdk-design-principles.md`, general and Go.
124+
125+
ADR-0008 fixes two words this ADR used as one. A **binding** is the WASI or
126+
FFI interface between the engine and a language: the guest's exports and the
127+
data that crosses them. A **language SDK** is what users of that language work
128+
with. The decision above is about the binding: a plan is the one thing that
129+
crosses it. The Go SDK is struct tags, a generator and generated code, and its
130+
users never see a plan (Go-7). Each "binding" above that names Go reads as the
131+
Go SDK's generated code.
132+
133+
**A field crosses the binding only when its value does.** The first Go design
134+
sent the full declaration and skipped passthrough values, which contradicted
135+
decision 9 of the plan: every plan field is present in the value. Generated Go
136+
code now sends a declaration and a value for each sealed and each indexed
137+
field, and nothing for a passthrough or omitted field. The data grammar does
138+
not change. The generated file still names every field, so a reviewer reads
139+
the whole declaration (G1, Go-10).
140+
141+
**The record fixture is the proof of the lowering.** Sequencing rested on Go's
142+
`plantest.Golden` snapshots not changing, and the Go SDK removes the package
143+
that writes them. The proof is now a fixture both test suites read: the Rust
144+
chain and the lowering each open the records that the other encrypted, and
145+
both derive the same bytes for each term. The same fixture later holds
146+
generated Go code to the Rust chain (G7: a claim is run before it is written).
147+
148+
**The generator asks the engine, and holds no copy of its rules.** `stashgen`
149+
refuses an index that does not fit a Go type and an EQL type the engine cannot
150+
produce. A second copy of those rules in Go would be the "lives twice" cost
151+
this ADR accepts only for an encoder. `stashgen` runs the guest the SDK embeds
152+
to check each declaration at `go generate`, so the rules have one source (G1)
153+
and the check runs at the earliest stage Go allows (G3, Go-1).
154+
155+
**The guest takes one plan in one call.** `se_encrypt_record` takes one plan,
156+
and the engine runs one plan under one key request. So one Go call covers one
157+
type, and one request for several types is later work: the engine runs several
158+
plans under one key request, then a guest export takes several plans with
159+
their values. G5 allows an SDK to put several types in one request; it does
160+
not require it before the engine can.
161+
162+
**The guest's value exports have no caller in Go.** The Go SDK seals a whole
163+
value through a declaration (ADR-0008), so `se_encrypt`, `se_decrypt` and the
164+
element exports have no Go caller. They stay while another host of the guest
165+
may need them; they are not a Go path.

0 commit comments

Comments
 (0)