From f38ee6aef6e50fc9b8e31cd12dbdd1c0c4676a1a Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Sat, 31 Oct 2026 21:30:45 +1100 Subject: [PATCH 1/4] test(encrypt): add shared conformance corpus --- .github/workflows/tests-crates.yml | 2 + .github/workflows/tests-golang.yml | 8 + conformance/stack-encrypt/README.md | 102 +++++ conformance/stack-encrypt/coverage.md | 24 ++ conformance/stack-encrypt/v1/cases.json | 117 +++++ conformance/stack-encrypt/v1/native.json | 47 ++ .../2026-10-31-stack-encrypt-conformance.md | 120 ++++++ languages/golang/encrypt/conformance_test.go | 240 +++++++++++ mise.toml | 4 + packages/stack-encrypt/tests/conformance.rs | 406 ++++++++++++++++++ .../__tests__/stack-conformance-ci.test.mjs | 34 ++ scripts/stack-conformance.sh | 29 ++ 12 files changed, 1133 insertions(+) create mode 100644 conformance/stack-encrypt/README.md create mode 100644 conformance/stack-encrypt/coverage.md create mode 100644 conformance/stack-encrypt/v1/cases.json create mode 100644 conformance/stack-encrypt/v1/native.json create mode 100644 docs/plans/2026-10-31-stack-encrypt-conformance.md create mode 100644 languages/golang/encrypt/conformance_test.go create mode 100644 packages/stack-encrypt/tests/conformance.rs create mode 100644 scripts/__tests__/stack-conformance-ci.test.mjs create mode 100644 scripts/stack-conformance.sh diff --git a/.github/workflows/tests-crates.yml b/.github/workflows/tests-crates.yml index 40cc572cc..425dca68c 100644 --- a/.github/workflows/tests-crates.yml +++ b/.github/workflows/tests-crates.yml @@ -21,6 +21,7 @@ on: - packages/stack-profile/** - packages/stack-kms/** - packages/stack-encrypt/** + - conformance/stack-encrypt/** - packages/stack-encrypt-derive/** - packages/stack-guest-abi/** - languages/typescript/packages/auth/** @@ -44,6 +45,7 @@ on: - packages/stack-profile/** - packages/stack-kms/** - packages/stack-encrypt/** + - conformance/stack-encrypt/** - packages/stack-encrypt-derive/** - packages/stack-guest-abi/** - languages/typescript/packages/auth/** diff --git a/.github/workflows/tests-golang.yml b/.github/workflows/tests-golang.yml index 868a65fc1..d9a6cc36e 100644 --- a/.github/workflows/tests-golang.yml +++ b/.github/workflows/tests-golang.yml @@ -38,6 +38,7 @@ on: - packages/stack-profile/** - packages/stack-kms/** - packages/stack-encrypt/** + - conformance/stack-encrypt/** - packages/stack-encrypt-derive/** - packages/stack-guest-abi/** # The eql guest build links eql-bindings. @@ -57,6 +58,7 @@ on: - languages/golang/** - scripts/check-wasm-imports.py - scripts/go-binding-test.sh + - scripts/stack-conformance.sh - Cargo.toml - Cargo.lock - .cargo/** @@ -74,6 +76,7 @@ on: - packages/stack-profile/** - packages/stack-kms/** - packages/stack-encrypt/** + - conformance/stack-encrypt/** - packages/stack-encrypt-derive/** - packages/stack-guest-abi/** # The eql guest build links eql-bindings. @@ -93,6 +96,7 @@ on: - languages/golang/** - scripts/check-wasm-imports.py - scripts/go-binding-test.sh + - scripts/stack-conformance.sh - Cargo.toml - Cargo.lock - .cargo/** @@ -119,6 +123,7 @@ env: RUSTFLAGS: "-D warnings" RUST_BACKTRACE: full CARGO_TERM_COLOR: always + STACK_CONFORMANCE_REQUIRED: "1" CARGO_NET_GIT_FETCH_WITH_CLI: true jobs: @@ -250,6 +255,9 @@ jobs: echo "RLIMIT_MEMLOCK: $(ulimit -l) KiB" mise run go:test + - name: Shared Stack Encrypt conformance and fresh Go to Rust opening + run: mise run test:conformance + # The Rust suite opens the committed v4_rows.json and # v4_smallint_rows.json, whose ciphertexts the Go SDK sealed when each # file was last regenerated. This also hands it the rows the Go SDK diff --git a/conformance/stack-encrypt/README.md b/conformance/stack-encrypt/README.md new file mode 100644 index 000000000..0f135fb66 --- /dev/null +++ b/conformance/stack-encrypt/README.md @@ -0,0 +1,102 @@ +# Stack Encrypt conformance + +This directory holds a shared test contract for Stack Encrypt bindings. The +[implementation plan](../../docs/plans/2026-10-31-stack-encrypt-conformance.md) +sets out the wider work; [coverage.md](coverage.md) maps existing evidence and +gaps. This first profile is **experimental**, not certification of an entire SDK. + +## Run + +From the repository root, with the pinned mise tools installed: + +```sh +mise run wasm:guest:build:deterministic +mise run test:conformance +``` + +No credentials or database are required. The task refuses a missing test guest. +It runs native Rust against frozen vectors, Go against the same vectors, then +native Rust against the ciphertexts Go has just produced. It never regenerates +expectations. CI also runs the Go cases on its other supported host platforms. + +The Rust runner is `packages/stack-encrypt/tests/conformance.rs`; the Go runner +is `languages/golang/encrypt/conformance_test.go`. Go calls the record interface +used by generated code. Testing public SDK declarations is a separate layer. +Python and TypeScript runners are pending; their absence is not a passing result. + +## `record-scalars-v1` contract + +`v1/cases.json` has a corpus format `version`, a semantic `profile`, a public +deterministic-provider `seed`, a record `context`, a `field`, an exact list of +`required_capabilities`, and uniquely identified `cases`. Unknown fields, +versions, profiles, capabilities and input types fail. Every case must run. + +The profile fixes a one-field record with ciphertext (`c`) and equality (`eq`) +outputs. Its field context is the record context followed by the field name. +Each case declares the field's `kind` and a typed `input`. `input.text` is the +literal string value, canonical base-10 integer, `true`/`false`, or empty text +for null, as determined by `input.type`. Runners construct real typed host +values, preserving integer width. JSON numbers are deliberately not used for +values: future TypeScript runners must not pass 64-bit integers through Number. + +The initial positives cover int32 and int64 bounds, uint64 max, an integer +above JavaScript's safe-integer limit, Unicode text and empty text. For each: + +1. The encoded typed frame must equal the frozen frame, including its type. +2. Record encryption and opening must preserve both value and type. +3. An equality query must produce one exact `eq` operand, encoded as `bytes`, + without an EQL domain. Its bytes must equal the frozen equality term and + the term stored with the record. +4. The frozen native owned-scalar ciphertext must open through the record + path. Native Rust additionally opens it through the owned scalar API. +5. Fresh Go ciphertext must open in Rust through both paths. Ciphertext bytes + themselves are not compared: encryption is randomized. +6. Opening with `wrong` appended to the record context (before the field name) + must refuse key retrieval as forbidden. + This expectation uses `stack_kms::DeterministicSource`, whose descriptor + binding models ZeroKMS's refusal. It is distinct from an AEAD failure and + does not imply every provider returns the same error shape. + +Negative cases have `expected_error` and exercise the equality query only. +They require the exact engine diagnostic `code`, `reason`, and field name; +Go must also expose `ErrEncoding`. Native Rust verifies rejection occurs +before data-key generation or retrieval. String/bool inputs for an integer +field and a null query are currently covered. Error prose is not contractual. + +Boolean equality is not admitted by the current engine. Boolean scalar +encryption, nullable records, other numeric kinds, bytes, JSON, EQL targets, +other operators, declarations, cancellation and provider failures need later +profiles or cases. The full SQL behavior is not part of this initial profile. + +## Artifacts and evolution + +`v1/native.json` is a frozen artifact set produced by the Rust owned scalar API. +It records `version`, `profile`, `producer`, and a case-ID map containing hex +`frame`, `ciphertext` and `equality` bytes. The map must contain exactly every +positive case; missing or extra cases fail. Every key and plaintext here is +public test material. The provider seed is not a production key mechanism. + +The runners accept `STACK_CONFORMANCE_OUTPUT` to export fresh artifacts; Rust +accepts `STACK_CONFORMANCE_INPUT` to validate another producer. The task isolates +these variables and sets `STACK_CONFORMANCE_REQUIRED=1`, preventing a missing +Go test guest from turning conformance into a skipped test. Ordinary Go test +runs may skip this suite when the guest has not been built. + +To intentionally create or update native artifacts: + +```sh +STACK_CONFORMANCE_UPDATE=1 mise x --env test -- cargo nextest run --locked \ + -p stack-encrypt --all-features --test conformance +mise run test:conformance +``` + +Review the case and artifact diff together. Never regenerate artifacts merely +to make a regression pass. Existing IDs retain their meaning; incompatible +expectations require a new profile. Corpus version 1 is a test format, not a +production wire version. Ciphertext/storage versions remain owned by their +existing codecs. + +Both runners currently share the Rust engine. This checks host value conversion, +native/WASI interoperability and regressions against frozen evidence; it cannot +detect every bug shared by both paths. Independent SQL oracles, broader negative +cases, release-version matrices and richer version reports remain in the plan. diff --git a/conformance/stack-encrypt/coverage.md b/conformance/stack-encrypt/coverage.md new file mode 100644 index 000000000..0bd3a08eb --- /dev/null +++ b/conformance/stack-encrypt/coverage.md @@ -0,0 +1,24 @@ +# Coverage map + +Snapshot: PR #1404 at `b684a217e`, including the JSON matching work through #1452. +This is an inventory of evidence, not a claim that every suite was rerun here. +Paths are relative to the repository root. New profile cases run from `v1/`. + +| Requirement | Existing evidence | Remaining work | +|---|---|---| +| Scalar value widths and lossless transport | `packages/stack-encrypt/tests/value_kinds.rs`; `languages/golang/encrypt/value_kinds_test.go`; initial shared scalar cases | Expand shared cases to decimals, dates, timestamps, floats, bytes and all widths; Python/TS mappings | +| Owned opaque ciphertext and storage envelope | `packages/stack-encrypt/tests/scalar.rs`, `frozen_bytes.rs`; initial cross-opening cases | Blob profile; encoded targets; supported version matrix | +| Record declaration and lowering | `packages/stack-encrypt/tests/record_lowering.rs`; `languages/golang/encrypt/fixture_test.go` | Normative grammar, normalization, accepted/rejected definition corpus | +| Context binding and composition | `packages/stack-encrypt/tests/target.rs`, `descriptor.rs`, `record_lowering.rs`; `languages/golang/encrypt/context_field_test.go`; initial wrong-context cases | Shared composition vectors; expected-context and required-input matrix | +| Query terms and diagnostics | `packages/stack-encrypt/tests/query_operators.rs`; `languages/golang/encrypt/query_test.go`; initial equality/query-error cases | Common rejection corpus for every operator, capability and input shape | +| Missing, SQL NULL and sealed null | `packages/stack-encrypt/src/dynamic/record.rs` null tests; `languages/golang/encrypt/null_test.go` | Explicit language mappings and common missing/null fixtures | +| Fresh cross-language storage | `.github/workflows/tests-golang.yml` exports fresh Go EQL rows for Rust; initial scalar artifact exchange | Every producer/consumer/backend pair and supported released versions | +| JSON queries, extraction and tampering | `packages/stack-encrypt/tests/json_tree_vectors.rs`; `packages/eql/tests/encryption/tests/json_tree.rs`; `languages/golang/encrypt/json_tree_test.go`, `json_postgres_test.go` | Formal operation profile; JSON vectors consumed by all runners | +| Element and predicate matching | Go `json_elements_test.go`, `json_match_test.go` and corresponding PostgreSQL tests; Rust/SQL predicate suites | Map exact supported grammar and documented semantic differences to case IDs | +| Generated SDK declarations | Go stashgen and generated-code fixture tests | Python and TypeScript public-API runners; compiler/type-checker cases | +| Validation before key I/O and batching | Rust `tests/common::CountingSource` and record/query tests; guest preflight tests | Requirement-by-requirement coverage; common provider observations | +| Binding lifecycle and cleanup | `packages/stack-guest-abi`; Go `cancel_test.go`, `step_test.go`, guest memory tests | Native Python/TS and WASM backend-specific matrices; no blanket host-erasure claim | +| Version/capability negotiation | Existing ABI status table and target/operator discovery | Normative supported-version rules; reject unknown required capabilities | + +Python and future TypeScript runners have no evidence in this checkout. Their +absence is a delivery gap, not a test skip that counts toward certification. diff --git a/conformance/stack-encrypt/v1/cases.json b/conformance/stack-encrypt/v1/cases.json new file mode 100644 index 000000000..53bc64bb1 --- /dev/null +++ b/conformance/stack-encrypt/v1/cases.json @@ -0,0 +1,117 @@ +{ + "version": 1, + "profile": "record-scalars-v1", + "seed": "0000000000000000000000000000000000000000000000000000000000000000", + "context": [ + "conformance" + ], + "field": "value", + "required_capabilities": [ + "records", + "equality", + "context-binding", + "query-errors" + ], + "cases": [ + { + "id": "i32-min", + "kind": "int32", + "input": { + "type": "int32", + "text": "-2147483648" + } + }, + { + "id": "i32-max", + "kind": "int32", + "input": { + "type": "int32", + "text": "2147483647" + } + }, + { + "id": "i64-min", + "kind": "int64", + "input": { + "type": "int64", + "text": "-9223372036854775808" + } + }, + { + "id": "i64-max", + "kind": "int64", + "input": { + "type": "int64", + "text": "9223372036854775807" + } + }, + { + "id": "u64-max", + "kind": "uint64", + "input": { + "type": "uint64", + "text": "18446744073709551615" + } + }, + { + "id": "above-js-safe-integer", + "kind": "int64", + "input": { + "type": "int64", + "text": "9007199254740993" + } + }, + { + "id": "text-unicode", + "kind": "string", + "input": { + "type": "string", + "text": "café 東京 🔐" + } + }, + { + "id": "text-empty", + "kind": "string", + "input": { + "type": "string", + "text": "" + } + }, + { + "id": "query-text-for-integer", + "kind": "int64", + "input": { + "type": "string", + "text": "sensitive-query-value" + }, + "expected_error": { + "code": "stack_encrypt::dynamic_source", + "reason": "field_type" + } + }, + { + "id": "query-bool-for-integer", + "kind": "int64", + "input": { + "type": "bool", + "text": "true" + }, + "expected_error": { + "code": "stack_encrypt::dynamic_source", + "reason": "field_type" + } + }, + { + "id": "query-null", + "kind": "int64", + "input": { + "type": "null", + "text": "" + }, + "expected_error": { + "code": "stack_encrypt::dynamic_source", + "reason": "null_query" + } + } + ] +} diff --git a/conformance/stack-encrypt/v1/native.json b/conformance/stack-encrypt/v1/native.json new file mode 100644 index 000000000..a3d3ebdf6 --- /dev/null +++ b/conformance/stack-encrypt/v1/native.json @@ -0,0 +1,47 @@ +{ + "version": 1, + "profile": "record-scalars-v1", + "producer": "rust-native/stack-encrypt-0.3.0", + "cases": { + "above-js-safe-integer": { + "frame": "050100000000002000", + "ciphertext": "0100000000000000000000000000000000d7f53b2f05f735b1a761ae2897e32cb9200080a7c97b8885087c0530fac2a364a5259fc2177478b543e972247f3af59a3a80020fe1a86efa34927b061b60bcac42a74c0a35e21e8e84c58973e29cd30edc21b9f017bca00c", + "equality": "ac6c5f70a992a8b5d835ceb5bfb459b728a94c220e5e0ffca3f858aa66db7db4" + }, + "i32-max": { + "frame": "04ffffff7f", + "ciphertext": "0100000000000000000000000000000000df8645b1e40e43f85ac7f5e826cdc74a20000c6e5a55e60affd88eb9de36776909b2a62f0647fafd4b7e3540ef8d957793530208f96d6eb25ce8eaee6dddf6f8f78015539889051e72eddd5d96456b5487231923", + "equality": "d679831312fc2b397f9b7fa5dba2ac3918431668c9923c379eadf8fedf4a7c4c" + }, + "i32-min": { + "frame": "0400000080", + "ciphertext": "01000000000000000000000000000000003b55fcd86eac559b36435b57fb2d00d62000956e7ffef9fa6b6f0c58944a4df74aeec52dd16c166b567014a3eaab79ff63a802fefc11d37697e01980445e5adef82ec75c3840346dacca3e359e197c91ccfeeb8a", + "equality": "e9b219761c0e549e3b5540744af1ceeebe241b6ed739e0f463d1c4af6d00eb7f" + }, + "i64-max": { + "frame": "05ffffffffffffff7f", + "ciphertext": "01000000000000000000000000000000001689636dabf977a1d7fce31594bfa15420003414223db76e9c96ce9603416a47e602911f24c04522e5dcbf303b3b10c2db160296dd082dbdf1bce3876b148fcf2d28f1753c3e2c93c4ea6afc146c23fdc9434e9d770ad716", + "equality": "b3ea26e6833e7665ef374b18f7123b0a795fbeea7f1696f3e3b63971f3201bcb" + }, + "i64-min": { + "frame": "050000000000000080", + "ciphertext": "01000000000000000000000000000000005fe9f26245bf484c734b5299be73d83a200051ae2d51b5d42745b2a586e663829d635deeed00a5408cf6e427b74d6213b9de0240237777e11809dbe6f3e3b857440c2f7cba391c95284d5269daf03de849ba7563ed296ec1", + "equality": "f504a4593ee7972726e6bd6420e11d5bc30c5d36285b2029df394a1d373f76db" + }, + "text-empty": { + "frame": "0a00000000", + "ciphertext": "010000000000000000000000000000000079a06bc059ef87dec9f58a8876c0112f20005d48c836a336273250cd8150593c25f4be53b49e3ae8bfe4e0d007cb0aa720d30259e10ebabb074b442c43bd77aa42dbab6b369f7e2725e656f936480796", + "equality": "cdf6de2f0cae47435a2a3fcd8d0ec5e29670302f65567a746555507def9aad52" + }, + "text-unicode": { + "frame": "0a11000000636166c3a920e69db1e4baac20f09f9490", + "ciphertext": "0100000000000000000000000000000000aba3b145ab9dddbcf4d5424461fd45c32000ef3f0cd3daac23097d97a85e8405923e9d95d29436c6ef5d848dee41ceeca24e0298a6c8ff53f67ff24459e4db609400f9f0f2c4f5c0818fafe2aecee06aba4e9500aceb4ec3c5c13d2bfeb335d766", + "equality": "d44828c3c9a883bd802b0c3fb3728f240bbdc244095ce71f39e430a40d7e4b07" + }, + "u64-max": { + "frame": "07ffffffffffffffff", + "ciphertext": "01000000000000000000000000000000001aaf730b6c6bc77770144918b59a35932000ef235e515a8e50ff41f3a26ba62e5de76f5331373f245ced6bce3a5638acffd302e975cc6e364604b6b1c0563752772c462fc353b94531ddca1436504262400183186ff2f004", + "equality": "04c8d7f02d9555152bdd770106eab974e517a0eba6b339e3ad1b0108796359d8" + } + } +} diff --git a/docs/plans/2026-10-31-stack-encrypt-conformance.md b/docs/plans/2026-10-31-stack-encrypt-conformance.md new file mode 100644 index 000000000..51750c03d --- /dev/null +++ b/docs/plans/2026-10-31-stack-encrypt-conformance.md @@ -0,0 +1,120 @@ +# Stack Encrypt cross-language conformance + +Date: 2026-10-31. Status: first Rust/Go slice implemented; the complete contract is not frozen. + +This work is stacked on [PR #1404](https://github.com/cipherstash/stack/pull/1404), +at `b684a217e2da629c762efa70fac958ec7e21646a`. It follows the accepted +[SDK principles](../sdk-design-principles.md) and the interoperability deliverable +in [the API redesign](https://github.com/cipherstash/stack/pull/1365). + +## Objective + +Equivalent declarations and operations have the same meaning in native Rust, +Go, Python, and TypeScript. SDKs retain idiomatic APIs and find errors at the +earliest stage their languages permit. Encryption and semantic validation stay +in the shared Rust engine. + +Python bindings are underway; TypeScript bindings are planned. Neither runner +is present in this checkout. Do not report their conformance as passing or +assume they will use Go's WASI backend. Native and WASM backends implement the +same semantic contract and have additional lifecycle tests of their own. + +Vitamin C's transport and storage codecs are dependencies of this contract. +Reference their versions and vectors rather than copying their specifications. +Codec agreement alone does not prove declaration or operation agreement. + +## Approach and order + +1. **Define the layers.** Specify value representation, declarations, operations, + persistence/SQL, binding lifecycle, and language SDK mappings separately. + Keep public SDK syntax distinct from the engine's data grammar and from the + test corpus format. +2. **Inventory the implemented profile.** Map existing tests to requirements, + identifying covered, partial, and missing evidence. Document current `next` + behavior and explicit differences from plaintext SQL. Proposed context roles + and future declarations must not silently become requirements of this profile. +3. **Create one portable corpus.** Give cases stable IDs, required capabilities, + typed inputs, declarations, operations, and expected values or diagnostics. + Use text for integers and other values JSON cannot carry losslessly. Run the + same cases in Rust and Go first; enroll Python early and TypeScript when its + binding exists. Unknown versions, operations, or required cases must fail. +4. **Test SDK mappings separately.** Exercise real host values and public SDK + declarations, not just pre-encoded frames. Cover TypeScript number/bigint, + undefined/null, byte arrays and timestamps; Python integer ranges, bool/int, + bytes/text, decimals and timezones; Go integer widths, pointers and typed nil. + Make unsupported or lossy conversions explicit refusals. +5. **Use independent evidence.** Combine fixed vectors, cross-language writes + and reads, plaintext SQL oracles, malformed requests and tampering. Compare + randomized ciphertext by opening it and querying it, not by byte equality. + Instrument test key providers for batching and validation-before-I/O checks. + Verify cleanup, cancellation and ownership separately for each backend; + do not promise equivalent erasure of host-language values. +6. **Gate claims in CI.** Record corpus/profile, SDK, engine, backend and supported + capabilities. Every required case must run; an absent guest or unknown case + must not create a green conformance result. Keep a fast credential-free lane, + with database, live-provider and cross-version lanes alongside it. Version + expected behavior and require review for changes to frozen expectations. + +## First slice + +The initial `record-scalars-v1` test profile is intentionally small. It covers +typed scalar boundaries, a one-field record declaration, exact equality terms, +cross-opening native scalar leaves through the Go record path, wrong-context +opening, and rejected query inputs. It uses the existing deterministic test +provider, with public test keys only. + +The Rust runner exercises both the owned scalar API from #1404 and dynamic +record/query execution. The Go runner exercises the internal record interface +used by generated SDK code. This proves engine/binding behavior; it does not +replace tests of generated declarations or certify the complete Go SDK. + +The shared corpus and runner contract live in +[`conformance/stack-encrypt`](../../conformance/stack-encrypt/README.md). +The coverage map there identifies existing evidence and remaining gaps. + +## First-slice checkpoint + +Implemented on `feat/stack-encrypt-conformance`, based directly on #1404. +The corpus has eight positive cases and three query refusals. Local validation +passes for native Rust, Go WASI, and fresh Go-to-Rust artifact exchange through +`mise run test:conformance`. Rust clippy, the enabled Go linters, and 33 CI wiring +checks also pass. The workflow changes are local; hosted CI has not run them. + +The first profile includes frozen typed frames and equality terms, frozen native +ciphertexts, fresh native scalar and record round trips, and forbidden retrieval +under a wrong context. The implementation adds no production API changes. +The wider grammar, SQL requirements and Python/TypeScript runners remain open. + +## Follow-up slices + +- Add a formal grammar for the implemented declaration/request profile, including + unknown-field rules, defaults, normalized declarations, and unsupported features. + Generate structural schemas from shared models where faithful; keep semantic + validation normative and executable rather than assuming JSON Schema covers it. +- Add shared positive and negative definition/request cases, stable diagnostic + fields, missing/null cases, full numeric kinds, and context composition vectors. +- Bring the existing JSON/SQL suites under named requirements. Add common JSON + vectors across runners and pin the supported predicate grammar and its limits. +- Add Python's runner in its owning repository/worktree, with a declared capability + set and the same artifact exchange. Establish its actual backend and APIs with + that implementation; do not design them by inference here. +- Add TypeScript's runner and SDK mapping tests when its binding exists. Include + native and WASM variants if both are supported. +- Extend artifact exchange to every producer/consumer pair for common profiles, + then supported release versions. Pin SQL bundle and engine versions in reports. +- Add backend lifecycle, cancellation, no-secret-diagnostics, key-I/O and batching + requirements. Reuse current tests and instrument providers where evidence is absent. + +## Completion criteria + +A profile is conformant only when every mandatory requirement has executable +evidence for the claimed language/backend/version. Optional capabilities have +named tests and explicit support status. No unsupported feature is silently +ignored. Released profiles define supported version combinations and evolution +rules; the corpus format version is not a new production wire version. + +Initial success means Rust and Go run the same cases, freshly written Go values +open in native Rust, fixed native values open in Go, and malformed query inputs +produce the specified diagnostics. Python and TypeScript remain visibly pending +until their runners execute. The complete specification, SQL profile, and all +language SDK mappings remain follow-up work. diff --git a/languages/golang/encrypt/conformance_test.go b/languages/golang/encrypt/conformance_test.go new file mode 100644 index 000000000..ea74d6f3d --- /dev/null +++ b/languages/golang/encrypt/conformance_test.go @@ -0,0 +1,240 @@ +package encrypt_test + +import ( + "bytes" + "context" + "encoding/hex" + "encoding/json" + "errors" + "io" + "os" + "path/filepath" + "reflect" + "strconv" + "testing" + + "github.com/cipherstash/stack/languages/golang/encrypt" + "github.com/cipherstash/stack/languages/golang/internal/record" + "github.com/cipherstash/vitaminc/bindings/go/vcffi" +) + +// This runner tests the record interface used by generated SDK code. Generated +// declarations have separate tests; this is not a substitute for SDK conformance. +const conformanceProfile = "record-scalars-v1" + +type conformanceCorpus struct { + Version int `json:"version"` + Profile string `json:"profile"` + Seed string `json:"seed"` + Context []string `json:"context"` + Field string `json:"field"` + RequiredCapabilities []string `json:"required_capabilities"` + Cases []conformanceCase `json:"cases"` +} + +type conformanceCase struct { + ID string `json:"id"` + Kind string `json:"kind"` + Input struct { + Type string `json:"type"` + Text string `json:"text"` + } `json:"input"` + ExpectedError *struct { + Code string `json:"code"` + Reason string `json:"reason"` + } `json:"expected_error"` +} + +type conformanceArtifact struct { + Frame string `json:"frame"` + Ciphertext string `json:"ciphertext"` + Equality string `json:"equality"` +} + +type conformanceArtifacts struct { + Version int `json:"version"` + Profile string `json:"profile"` + Producer string `json:"producer"` + Cases map[string]conformanceArtifact `json:"cases"` +} + +func readConformanceJSON(t *testing.T, path string, out any) { + t.Helper() + data, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + d := json.NewDecoder(bytes.NewReader(data)) + d.DisallowUnknownFields() + if err := d.Decode(out); err != nil { + t.Fatal(err) + } + if err := d.Decode(new(any)); err != io.EOF { + t.Fatalf("%s: trailing JSON or invalid suffix: %v", path, err) + } +} + +func conformanceValue(t *testing.T, c conformanceCase) any { + t.Helper() + switch c.Input.Type { + case "int32", "int64": + bits := 64 + if c.Input.Type == "int32" { + bits = 32 + } + n, err := strconv.ParseInt(c.Input.Text, 10, bits) + if err != nil { + t.Fatal(err) + } + if bits == 32 { + return int32(n) + } + return n + case "uint64": + n, err := strconv.ParseUint(c.Input.Text, 10, 64) + if err != nil { + t.Fatal(err) + } + return n + case "bool": + if c.Input.Text != "true" && c.Input.Text != "false" { + t.Fatal("bool must be true or false") + } + return c.Input.Text == "true" + case "string": + return c.Input.Text + case "null": + if c.Input.Text != "" { + t.Fatal("null has empty text") + } + return nil + default: + t.Fatalf("unsupported input type %q", c.Input.Type) + return nil + } +} + +func TestStackConformance(t *testing.T) { + root := filepath.Join("..", "..", "..", "conformance", "stack-encrypt", "v1") + var corpus conformanceCorpus + readConformanceJSON(t, filepath.Join(root, "cases.json"), &corpus) + if corpus.Version != 1 || corpus.Profile != conformanceProfile || len(corpus.Cases) == 0 { + t.Fatal("unsupported or empty conformance corpus") + } + if !reflect.DeepEqual(corpus.RequiredCapabilities, []string{"records", "equality", "context-binding", "query-errors"}) { + t.Fatal("unsupported required capabilities") + } + seen := make(map[string]bool) + positive := make(map[string]bool) + for _, c := range corpus.Cases { + if c.ID == "" || seen[c.ID] { + t.Fatalf("empty or duplicate case ID %q", c.ID) + } + seen[c.ID] = true + if c.ExpectedError == nil { + positive[c.ID] = true + } + } + var native conformanceArtifacts + readConformanceJSON(t, filepath.Join(root, "native.json"), &native) + if native.Version != 1 || native.Profile != conformanceProfile || native.Producer == "" || len(native.Cases) != len(positive) || len(positive) == 0 { + t.Fatal("artifact profile or case set differs from the corpus") + } + for id := range native.Cases { + if !positive[id] { + t.Fatalf("unexpected artifact %q", id) + } + } + seed, err := hex.DecodeString(corpus.Seed) + if err != nil || len(seed) != 32 { + t.Fatal("expected 32-byte deterministic test seed") + } + ctx := context.Background() + client, err := encrypt.NewDeterministicClient(ctx, [32]byte(seed)) + if errors.Is(err, encrypt.ErrDeterministicGuestNotBuilt) && os.Getenv("STACK_CONFORMANCE_REQUIRED") != "1" { + t.Skip(err) + } + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = client.Close() }) + cipher := client.DefaultKeyset() + output := conformanceArtifacts{Version: 1, Profile: conformanceProfile, Producer: "go-wasi", Cases: make(map[string]conformanceArtifact)} + for _, c := range corpus.Cases { + t.Run(c.ID, func(t *testing.T) { + plan := &record.Plan{Context: corpus.Context, Fields: []record.Field{{ + Name: corpus.Field, Kind: record.Kind(c.Kind), Outputs: []record.Output{record.Ciphertext, record.Equality}, + }}} + input := conformanceValue(t, c) + query, err := cipher.QueryTerm(ctx, plan, corpus.Field, "eq", input) + if c.ExpectedError != nil { + var diagnostic *encrypt.Diagnostic + if !errors.Is(err, encrypt.ErrEncoding) || !errors.As(err, &diagnostic) { + t.Fatalf("expected encoding diagnostic, got %v", err) + } + if diagnostic.Code != c.ExpectedError.Code || diagnostic.Reason() != c.ExpectedError.Reason || diagnostic.Field() != corpus.Field { + t.Fatalf("unexpected diagnostic: %s reason=%s field=%s", diagnostic.Code, diagnostic.Reason(), diagnostic.Field()) + } + return + } + if c.Kind != c.Input.Type { + t.Fatal("positive case must use its declared type") + } + if err != nil { + t.Fatal(err) + } + if query.Operator != "eq" || query.Index != record.Equality || query.Fidelity != record.Exact || query.Encoding != "bytes" || query.Domain != "" || len(query.Terms) != 1 { + t.Fatal("unexpected equality operand metadata") + } + want := native.Cases[c.ID] + encoded, err := vcffi.Marshal(input) + if err != nil { + t.Fatal(err) + } + if hex.EncodeToString(encoded) != want.Frame || hex.EncodeToString(query.Terms[0]) != want.Equality { + t.Fatal("typed frame or equality bytes disagree with the native vector") + } + decode := func(text string) []byte { + t.Helper() + b, err := hex.DecodeString(text) + if err != nil { + t.Fatal(err) + } + return b + } + source := []record.Source{{corpus.Field: input}} + fresh, err := cipher.Seal(ctx, plan, source) + if err != nil { + t.Fatal(err) + } + if len(fresh) != 1 || !bytes.Equal(fresh[0][corpus.Field].Terms[record.Equality], query.Terms[0]) { + t.Fatal("stored equality differs from query equality") + } + frozen := []record.Sealed{{corpus.Field: {Ciphertext: decode(want.Ciphertext), Terms: map[record.Output][]byte{record.Equality: decode(want.Equality)}}}} + for name, rows := range map[string][]record.Sealed{"native": frozen, "go": fresh} { + opened, err := cipher.Open(ctx, plan, rows) + if err != nil || !reflect.DeepEqual(opened, source) { + t.Fatalf("%s cross-open: err=%v; typed value mismatch=%v", name, err, !reflect.DeepEqual(opened, source)) + } + } + wrong := *plan + wrong.Context = append(append([]string{}, corpus.Context...), "wrong") + if _, err := cipher.Open(ctx, &wrong, frozen); !errors.Is(err, encrypt.ErrForbidden) { + t.Fatalf("wrong context: expected forbidden key retrieval, got %v", err) + } + output.Cases[c.ID] = conformanceArtifact{Frame: want.Frame, Ciphertext: hex.EncodeToString(fresh[0][corpus.Field].Ciphertext), Equality: hex.EncodeToString(query.Terms[0])} + }) + } + if t.Failed() { + return + } + if path := os.Getenv("STACK_CONFORMANCE_OUTPUT"); path != "" { + data, err := json.MarshalIndent(output, "", " ") + if err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, append(data, '\n'), 0o600); err != nil { + t.Fatal(err) + } + } +} diff --git a/mise.toml b/mise.toml index 1164e8e88..9e174844d 100644 --- a/mise.toml +++ b/mise.toml @@ -40,6 +40,10 @@ golangci-lint = "2.14.0" # short name in mise's registry). "aqua:wasm-bindgen/wasm-pack" = "0.13.1" +[tasks."test:conformance"] +description = "Run shared Rust/Go cases (build the deterministic guest first)" +run = "bash scripts/stack-conformance.sh" + [tasks."test:doc"] description = "Run documentation tests" run = "mise x --env test -- cargo test --doc --all-features --workspace" diff --git a/packages/stack-encrypt/tests/conformance.rs b/packages/stack-encrypt/tests/conformance.rs new file mode 100644 index 000000000..e7e9b1d5a --- /dev/null +++ b/packages/stack-encrypt/tests/conformance.rs @@ -0,0 +1,406 @@ +//! The language-neutral record-scalars-v1 profile. See conformance/stack-encrypt. +#![cfg(feature = "dynamic")] + +mod common; + +use std::collections::{BTreeMap, BTreeSet}; +use std::path::PathBuf; +use std::sync::atomic::Ordering; + +use common::frames::{frame, object}; +use miette::Diagnostic; +use serde::{Deserialize, Serialize}; +use serde_json::json; +use stack_encrypt::dynamic::{ + self, record, Fidelity, NoTargets, OpSpec, Operand, Scope, ValueFrame, +}; +use stack_encrypt::target::IndexSpec; +use stack_encrypt::{ + CipherText, Ciphertext, Error, KeysetCipher, Label, SealedValue, StackCipherText, +}; + +const PROFILE: &str = "record-scalars-v1"; + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct Corpus { + version: u32, + profile: String, + seed: String, + context: Vec, + field: String, + required_capabilities: Vec, + cases: Vec, +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct Case { + id: String, + kind: String, + input: Input, + expected_error: Option, +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct Input { + #[serde(rename = "type")] + kind: String, + text: String, +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct ExpectedError { + code: String, + reason: String, +} + +#[derive(Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +struct Artifacts { + version: u32, + profile: String, + producer: String, + cases: BTreeMap, +} + +#[derive(Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +struct Artifact { + frame: String, + ciphertext: String, + equality: String, +} + +fn root() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../conformance/stack-encrypt/v1") +} + +fn hex(bytes: &[u8]) -> String { + bytes.iter().map(|b| format!("{b:02x}")).collect() +} + +fn unhex(text: &str) -> Vec { + assert!( + text.is_ascii() && text.len().is_multiple_of(2), + "invalid hex" + ); + (0..text.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&text[i..i + 2], 16).expect("hex byte")) + .collect() +} + +fn value(input: &Input) -> ValueFrame { + match input.kind.as_str() { + "int32" => frame(input.text.parse::().expect("i32")), + "int64" => frame(input.text.parse::().expect("i64")), + "uint64" => frame(input.text.parse::().expect("u64")), + "bool" => frame(input.text.parse::().expect("bool")), + "string" => frame(input.text.clone()), + "null" => { + assert!(input.text.is_empty()); + use vitaminc_aead::plaintext::{Physical, Raw}; + frame(Raw(None, Physical::Null)) + } + other => panic!("unsupported input kind {other}"), + } +} + +fn plan(corpus: &Corpus, case: &Case) -> record::Plan { + let mut context = corpus.context.clone(); + context.push(corpus.field.clone()); + let declaration = json!({corpus.field.clone(): { + "context": context, "outputs": ["c", "eq"], "type": case.kind, + }}); + record::plan(&vitaminc_aead_frame::serde::to_vec(&declaration).unwrap()).unwrap() +} + +fn stored(corpus: &Corpus, artifact: &Artifact) -> StackCipherText { + CipherText::Map(vec![( + corpus.field.clone(), + CipherText::Map(vec![ + ( + "c".into(), + CipherText::Single(SealedValue::from_bytes(&unhex(&artifact.ciphertext)).unwrap()), + ), + ( + "eq".into(), + CipherText::Passthrough(common::passthrough(unhex(&artifact.equality))), + ), + ]), + )]) +} + +fn stored_equality(tree: &StackCipherText, field: &str) -> Vec { + let CipherText::Map(fields) = tree else { + panic!("expected record") + }; + let (_, CipherText::Map(outputs)) = fields.iter().find(|(name, _)| name == field).unwrap() + else { + panic!("expected outputs") + }; + let (_, CipherText::Passthrough(term)) = outputs.iter().find(|(name, _)| name == "eq").unwrap() + else { + panic!("expected equality bytes") + }; + vitaminc_aead_frame::serde::from_slice::<&[u8]>(&term.0) + .unwrap() + .to_vec() +} + +// Exercise the owned scalar API independently of dynamic record lowering. +async fn scalar( + keyset: &KeysetCipher<'_, common::DeterministicSource>, + input: &Input, + context: &Label, +) -> Ciphertext { + macro_rules! seal { + ($value:expr) => { + keyset + .encrypt_with_context($value, context.clone()) + .await + .unwrap() + }; + } + match input.kind.as_str() { + "int32" => seal!(input.text.parse::().unwrap()), + "int64" => seal!(input.text.parse::().unwrap()), + "uint64" => seal!(input.text.parse::().unwrap()), + "bool" => seal!(input.text.parse::().unwrap()), + "string" => seal!(input.text.clone()), + other => panic!("unsupported scalar {other}"), + } +} + +async fn open_scalar( + keyset: &KeysetCipher<'_, common::DeterministicSource>, + bytes: &[u8], + kind: &str, + context: &Label, +) -> ValueFrame { + let ct = Ciphertext::from_bytes(bytes).unwrap(); + macro_rules! open { + ($ty:ty) => { + frame( + keyset + .decrypt_with_context::<$ty>(ct, context.clone()) + .await + .unwrap(), + ) + }; + } + match kind { + "int32" => open!(i32), + "int64" => open!(i64), + "uint64" => open!(u64), + "bool" => open!(bool), + "string" => open!(String), + other => panic!("unsupported scalar {other}"), + } +} + +#[tokio::test] +async fn shared_record_scalar_profile() { + let corpus: Corpus = + serde_json::from_slice(&std::fs::read(root().join("cases.json")).unwrap()).unwrap(); + assert_eq!(corpus.version, 1); + assert_eq!(corpus.profile, PROFILE); + assert_eq!( + corpus.required_capabilities, + ["records", "equality", "context-binding", "query-errors"] + ); + assert!(!corpus.cases.is_empty()); + let ids: BTreeSet<_> = corpus.cases.iter().map(|c| c.id.as_str()).collect(); + assert_eq!(ids.len(), corpus.cases.len(), "duplicate case IDs"); + assert!(!ids.contains("")); + let positive_ids: BTreeSet<_> = corpus + .cases + .iter() + .filter(|c| c.expected_error.is_none()) + .map(|c| c.id.as_str()) + .collect(); + assert!(!positive_ids.is_empty()); + + let update = std::env::var("STACK_CONFORMANCE_UPDATE").as_deref() == Ok("1"); + let imported = std::env::var_os("STACK_CONFORMANCE_INPUT"); + assert!( + !(update && imported.is_some()), + "cannot update from external artifacts" + ); + let baseline: Option = if update { + None + } else { + let file = imported + .map(PathBuf::from) + .unwrap_or_else(|| root().join("native.json")); + let artifacts: Artifacts = serde_json::from_slice(&std::fs::read(file).unwrap()).unwrap(); + assert_eq!(artifacts.version, 1); + assert_eq!(artifacts.profile, PROFILE); + assert!(!artifacts.producer.is_empty()); + assert_eq!( + artifacts + .cases + .keys() + .map(String::as_str) + .collect::>(), + positive_ids + ); + Some(artifacts) + }; + + let cipher = + common::deterministic_cipher(unhex(&corpus.seed).try_into().expect("32-byte seed")).await; + let keyset = cipher.default_keyset(); + let context = Label::new(corpus.context.iter().chain(std::iter::once(&corpus.field))).unwrap(); + let (counting, generates, retrieves) = common::counting_cipher().await; + let mut output = Artifacts { + version: 1, + profile: PROFILE.into(), + producer: format!("rust-native/stack-encrypt-{}", env!("CARGO_PKG_VERSION")), + cases: BTreeMap::new(), + }; + + for case in &corpus.cases { + eprintln!("conformance: {}", case.id); + let plan = plan(&corpus, case); + if let Some(expected) = &case.expected_error { + let scope = counting.default_keyset(); + let error = match dynamic::query_term( + &scope, + &plan, + &corpus.field, + OpSpec::Eq, + value(&case.input), + &NoTargets, + ) { + Err(error) => error, + Ok(_) => panic!("{}: expected preflight rejection", case.id), + }; + assert_eq!( + error.code().unwrap().to_string(), + expected.code, + "{}", + case.id + ); + assert_eq!( + error.reason().unwrap().as_str(), + expected.reason, + "{}", + case.id + ); + assert_eq!(error.field(), Some(corpus.field.as_str())); + assert_eq!(generates.load(Ordering::SeqCst), 0); + assert_eq!(retrieves.load(Ordering::SeqCst), 0); + continue; + } + assert_eq!(case.kind, case.input.kind); + let input = value(&case.input); + let expected_row = object(vec![(corpus.field.clone(), input.clone())]); + let fresh = record::encrypt(&keyset, expected_row.clone(), &plan) + .unwrap() + .await + .unwrap(); + let stored_term = stored_equality(&fresh, &corpus.field); + let opened = record::decrypt(Scope::Keyset(keyset.clone()), fresh, &plan, None) + .unwrap() + .await + .unwrap(); + assert_eq!( + opened.risky_ref(), + expected_row.risky_ref(), + "{}: record roundtrip", + case.id + ); + + let term = dynamic::query_term( + &keyset, + &plan, + &corpus.field, + OpSpec::Eq, + input.clone(), + &NoTargets, + ) + .unwrap() + .await + .unwrap(); + assert_eq!(term.operator(), OpSpec::Eq); + assert_eq!(term.index(), &IndexSpec::Equality); + assert_eq!(term.fidelity(), Fidelity::Exact); + assert_eq!(term.encoding(), "bytes"); + assert_eq!(term.domain(), None); + let Operand::One(term) = term.operand() else { + panic!("expected one equality term") + }; + assert_eq!(&stored_term, term, "{}: stored equality", case.id); + let ct = scalar(&keyset, &case.input, &context).await; + let artifact = Artifact { + frame: hex(input.risky_ref()), + ciphertext: hex(&ct.to_bytes()), + equality: hex(term), + }; + let reference = baseline.as_ref().map_or(&artifact, |a| &a.cases[&case.id]); + assert_eq!(artifact.frame, reference.frame, "{}: typed frame", case.id); + assert_eq!( + artifact.equality, reference.equality, + "{}: equality bytes", + case.id + ); + for (origin, candidate) in [("fresh", &artifact), ("reference", reference)] { + let opened = + open_scalar(&keyset, &unhex(&candidate.ciphertext), &case.kind, &context).await; + assert_eq!( + opened.risky_ref(), + input.risky_ref(), + "{}: {origin} scalar cross-open", + case.id + ); + let opened = record::decrypt( + Scope::Keyset(keyset.clone()), + stored(&corpus, candidate), + &plan, + None, + ) + .unwrap() + .await + .unwrap(); + assert_eq!( + opened.risky_ref(), + expected_row.risky_ref(), + "{}: {origin} record cross-open", + case.id + ); + } + + let mut wrong = corpus.context.clone(); + wrong.push("wrong".into()); + wrong.push(corpus.field.clone()); + let wrong = Label::new(wrong).unwrap(); + let result = keyset + .decrypt_tree::( + CipherText::Single(SealedValue::from_bytes(&unhex(&reference.ciphertext)).unwrap()), + wrong, + ) + .await; + assert!( + matches!(result, Err(Error::Kms(stack_kms::Error::RetrieveKey( + stack_kms::RetrieveKeyError::RequestFailed(ref error) + ))) if matches!(error.kind, zerokms_protocol::ViturRequestErrorKind::Forbidden)), + "{}: wrong context: {result:?}", + case.id + ); + output.cases.insert(case.id.clone(), artifact); + } + if update { + std::fs::write( + root().join("native.json"), + serde_json::to_string_pretty(&output).unwrap() + "\n", + ) + .unwrap(); + } + if let Some(path) = std::env::var_os("STACK_CONFORMANCE_OUTPUT") { + std::fs::write(path, serde_json::to_string_pretty(&output).unwrap() + "\n").unwrap(); + } +} diff --git a/scripts/__tests__/stack-conformance-ci.test.mjs b/scripts/__tests__/stack-conformance-ci.test.mjs new file mode 100644 index 000000000..bb6f4da2a --- /dev/null +++ b/scripts/__tests__/stack-conformance-ci.test.mjs @@ -0,0 +1,34 @@ +import { describe, expect, it } from 'vitest' +import { readWorkflow } from './lib/workflows.mjs' + +describe('shared Stack Encrypt conformance is required in CI', () => { + it('runs both consumers when the corpus changes', () => { + for (const file of ['tests-crates.yml', 'tests-golang.yml']) { + const workflow = readWorkflow(`.github/workflows/${file}`) + const triggers = workflow.on ?? workflow[true] + for (const event of ['pull_request', 'push']) { + expect(triggers[event].paths).toContain('conformance/stack-encrypt/**') + } + } + }) + + it('requires the Go guest and runs fresh exchange after building it', () => { + const workflow = readWorkflow('.github/workflows/tests-golang.yml') + expect(workflow.env.STACK_CONFORMANCE_REQUIRED).toBe('1') + const triggers = workflow.on ?? workflow[true] + for (const event of ['pull_request', 'push']) { + expect(triggers[event].paths).toContain('scripts/stack-conformance.sh') + } + const steps = workflow.jobs['wasi-check'].steps + const build = steps.findIndex( + (step) => step.run === 'mise run wasm:guest:build:deterministic', + ) + const exchange = steps.findIndex( + (step) => step.run === 'mise run test:conformance', + ) + expect(build).toBeGreaterThanOrEqual(0) + expect(exchange).toBeGreaterThan(build) + expect(steps[exchange].if).toBeUndefined() + expect(steps[exchange]['continue-on-error']).toBeUndefined() + }) +}) diff --git a/scripts/stack-conformance.sh b/scripts/stack-conformance.sh new file mode 100644 index 000000000..502027af4 --- /dev/null +++ b/scripts/stack-conformance.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# Credential-free shared cases plus fresh Go -> native Rust artifact exchange. +set -euo pipefail +cd "$(dirname "$0")/.." + +if [[ -n "${STACK_CONFORMANCE_UPDATE:-}" ]]; then + echo 'Conformance checks must not update frozen artifacts.' >&2 + exit 1 +fi +unset STACK_CONFORMANCE_INPUT STACK_CONFORMANCE_OUTPUT +export STACK_CONFORMANCE_REQUIRED=1 + +if [[ ! -s languages/golang/encrypt/testdata/stack_encrypt_guest_deterministic.wasm ]]; then + echo 'Build the test guest first: mise run wasm:guest:build:deterministic' >&2 + exit 1 +fi + +artifacts=$(mktemp -d) +trap 'rm -rf "$artifacts"' EXIT + +mise x --env test -- cargo nextest run --locked -p stack-encrypt --all-features --test conformance +( + cd languages/golang + STACK_CONFORMANCE_OUTPUT="$artifacts/go.json" CGO_ENABLED=0 \ + go test -count=1 -v -run '^TestStackConformance$' ./encrypt +) +test -s "$artifacts/go.json" +STACK_CONFORMANCE_INPUT="$artifacts/go.json" mise x --env test -- \ + cargo nextest run --locked -p stack-encrypt --all-features --test conformance From cc89cc0d8e04f73142322bd75160e355896eecf2 Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Sat, 31 Oct 2026 22:32:19 +1100 Subject: [PATCH 2/4] test(encrypt): specify declaration grammar --- conformance/stack-encrypt/README.md | 19 +- conformance/stack-encrypt/coverage.md | 29 +- .../stack-encrypt/declaration-grammar.md | 276 ++ conformance/stack-encrypt/v1/cases.json | 4 +- .../stack-encrypt/v1/declarations.json | 3278 +++++++++++++++++ .../2026-10-31-stack-encrypt-conformance.md | 40 +- .../encrypt/declaration_conformance_test.go | 167 + languages/golang/encrypt/export_test.go | 10 + packages/stack-encrypt/tests/declarations.rs | 167 + .../__tests__/stack-conformance-ci.test.mjs | 15 + scripts/stack-conformance.sh | 4 +- 11 files changed, 3993 insertions(+), 16 deletions(-) create mode 100644 conformance/stack-encrypt/declaration-grammar.md create mode 100644 conformance/stack-encrypt/v1/declarations.json create mode 100644 languages/golang/encrypt/declaration_conformance_test.go create mode 100644 packages/stack-encrypt/tests/declarations.rs diff --git a/conformance/stack-encrypt/README.md b/conformance/stack-encrypt/README.md index 0f135fb66..61eb84625 100644 --- a/conformance/stack-encrypt/README.md +++ b/conformance/stack-encrypt/README.md @@ -3,7 +3,7 @@ This directory holds a shared test contract for Stack Encrypt bindings. The [implementation plan](../../docs/plans/2026-10-31-stack-encrypt-conformance.md) sets out the wider work; [coverage.md](coverage.md) maps existing evidence and -gaps. This first profile is **experimental**, not certification of an entire SDK. +gaps. These profiles are **experimental**, not certification of an entire SDK. ## Run @@ -18,12 +18,29 @@ No credentials or database are required. The task refuses a missing test guest. It runs native Rust against frozen vectors, Go against the same vectors, then native Rust against the ciphertexts Go has just produced. It never regenerates expectations. CI also runs the Go cases on its other supported host platforms. +The same task runs the shared declaration acceptance/rejection corpus in both +languages, with native observations of normalization. The Rust runner is `packages/stack-encrypt/tests/conformance.rs`; the Go runner is `languages/golang/encrypt/conformance_test.go`. Go calls the record interface used by generated code. Testing public SDK declarations is a separate layer. Python and TypeScript runners are pending; their absence is not a passing result. +## `record-declarations-v1` contract + +The [declaration grammar](declaration-grammar.md) defines the engine's current +record-plan syntax, defaults, context rules, kind/index table and null policies. +`v1/declarations.json` contains 201 cases, including malformed frames and all +21 kinds against five scalar indexes. The resolver is explicitly `none`; EQL +targets need their own profile. + +The runners are `packages/stack-encrypt/tests/declarations.rs` and +`languages/golang/encrypt/declaration_conformance_test.go`. Both call the engine +without a cipher. Valid-frame declaration diagnostics agree exactly; malformed +frames document the different native-parser and WASI-boundary classifications. +Native Rust also checks normalization observations. Go's checker does not expose +the parsed plan, so it asserts acceptance and diagnostics. + ## `record-scalars-v1` contract `v1/cases.json` has a corpus format `version`, a semantic `profile`, a public diff --git a/conformance/stack-encrypt/coverage.md b/conformance/stack-encrypt/coverage.md index 0bd3a08eb..53ff44d27 100644 --- a/conformance/stack-encrypt/coverage.md +++ b/conformance/stack-encrypt/coverage.md @@ -8,7 +8,7 @@ Paths are relative to the repository root. New profile cases run from `v1/`. |---|---|---| | Scalar value widths and lossless transport | `packages/stack-encrypt/tests/value_kinds.rs`; `languages/golang/encrypt/value_kinds_test.go`; initial shared scalar cases | Expand shared cases to decimals, dates, timestamps, floats, bytes and all widths; Python/TS mappings | | Owned opaque ciphertext and storage envelope | `packages/stack-encrypt/tests/scalar.rs`, `frozen_bytes.rs`; initial cross-opening cases | Blob profile; encoded targets; supported version matrix | -| Record declaration and lowering | `packages/stack-encrypt/tests/record_lowering.rs`; `languages/golang/encrypt/fixture_test.go` | Normative grammar, normalization, accepted/rejected definition corpus | +| Record declaration and lowering | [Declaration grammar](declaration-grammar.md); `v1/declarations.json` shared by native Rust and Go; native normalization observations; existing record lowering suites | EQL-resolver profile; SDK-to-declaration mappings; runtime source/read shapes | | Context binding and composition | `packages/stack-encrypt/tests/target.rs`, `descriptor.rs`, `record_lowering.rs`; `languages/golang/encrypt/context_field_test.go`; initial wrong-context cases | Shared composition vectors; expected-context and required-input matrix | | Query terms and diagnostics | `packages/stack-encrypt/tests/query_operators.rs`; `languages/golang/encrypt/query_test.go`; initial equality/query-error cases | Common rejection corpus for every operator, capability and input shape | | Missing, SQL NULL and sealed null | `packages/stack-encrypt/src/dynamic/record.rs` null tests; `languages/golang/encrypt/null_test.go` | Explicit language mappings and common missing/null fixtures | @@ -22,3 +22,30 @@ Paths are relative to the repository root. New profile cases run from `v1/`. Python and future TypeScript runners have no evidence in this checkout. Their absence is a delivery gap, not a test skip that counts toward certification. + +## Consolidation policy + +Shared semantic expectations should be maintained once and executed by each +language runner. Conformance can replace duplicate case tables and fixture +maintenance. It does not eliminate the need to execute the Rust and Go paths. +No existing test has been deleted in these first two slices. + +| Existing evidence | Relationship to conformance | Consolidation action | +|---|---|---| +| Rust `dynamic/record.rs` tests `a_malformed_plan`, `a_null_is_refused_as_its_key`, `a_missing_or_null_index_payload_is_the_default` | Many inputs and diagnostic outcomes are now shared declaration cases | Move the remaining cases and retain any unique display/low-level assertions before removing duplicate rows | +| Rust kind/index admission tests | The corpus has the complete 21-kind/five-index acceptance matrix | Share case data; retain direct tests that compare declaration admission with actual term execution | +| Rust `value_kinds.rs`, Go `value_kinds_test.go` | Already share `value_kinds.json`; cover more types and generated SDK code than the initial scalar profile | Enroll that existing fixture under named requirements instead of copying it; keep generated-code assertions | +| Rust `query_operators.rs`, Go `query_test.go` | Already share operator vectors, including verification and target limits | Reuse their fixture and cases when adding an operator profile; preserve host/generated query mapping tests | +| Go checker and host validation tests in `guest_test.go` | Exercise host rejection and diagnostic forwarding through public checker methods | Keep adapter-specific assertions; raw-frame conformance bypasses host validation deliberately | +| Native scalar/record round trips and cross-opening | Some behavior overlaps, but existing tests cover more types and API paths | Replace only exact duplicates after matching requirements and execution paths | + +Keep Rust compile-fail, ownership/zeroization, batching, fuzz/property, and +implementation regression tests. Keep Go generation, host type conversion, +error wrapping, cancellation and guest lifecycle tests. Keep independent +PostgreSQL oracles and live-provider tests. These observe guarantees a shared +engine corpus cannot prove by itself. + +For each removal, link the old assertion to stable case IDs, compare coverage +and failure behavior, and run both runners in CI. Preserve unrelated assertions +from mixed tests. Enroll Python and TypeScript against the existing corpus so +adding a language does not create another copy of the expected behavior. diff --git a/conformance/stack-encrypt/declaration-grammar.md b/conformance/stack-encrypt/declaration-grammar.md new file mode 100644 index 000000000..518e47195 --- /dev/null +++ b/conformance/stack-encrypt/declaration-grammar.md @@ -0,0 +1,276 @@ +# Record declaration grammar + +Use this contract when a binding lowers an SDK declaration to the engine's record +plan. Send the complete plan to the engine checker before executing it. + +For example, this declaration seals and equality-indexes an int64 field: + +```json +{ + "value": { + "context": ["users", "value"], + "outputs": ["c", "eq"], + "type": "int64" + } +} +``` + +Run the executable cases with `mise run test:conformance`, after building the +deterministic test guest as described in [README.md](README.md). +`ciphertext-untyped`, `index-only`, and every `type-*-eq` case exercise these forms. + +## Scope and authority + +`record-declarations-v1` describes the engine at PR #1404's `b684a217e`, not future +SDK syntax. It is an experimental conformance profile, not a new production wire +version. The input is a Vitamin C **typed transport frame**. JSON below is a +readable projection; it cannot distinguish every integer width or carry bytes. + +The executable authority is `stack_encrypt::dynamic::record::plan_with`, with the +selected target resolver. Native `record::plan` and the Go guest without EQL use +`NoTargets`. The corpus fixes that resolver explicitly as `none`. It checks +target syntax and unavailable-target refusals; it does not certify EQL targets. + +The full contract has three parts: frame validity, the structural grammar below, +and the semantic constraints. A shape-only JSON Schema would not enforce the +context, index, null or resolver rules. SDK tooling must ask the engine instead +of implementing those rules again. Test runners convert fixture values and +observe results; they do not provide another semantic validator. + +## Structural grammar + +This is a typed grammar. `Object{...}` denotes a transport object, `List` a +transport array, `?` an optional member, and `|` an alternative. Literal words +denote text values. Each object has exactly the listed keys, except `Plan`, whose +non-reserved keys are field names. Object key order is unrestricted. + +```text +Plan = Object{ + "context_field"?: Text, + (FieldName: FieldSpec)+ +} +FieldName = Text except "context_field" + +FieldSpec = Object{ + "context": FieldContext, + ("outputs": NonEmptyList | "target": Target), + "type"?: ValueKind, + "null"?: "sealed" | "sql" +} + +FieldContext = Label | [FieldContext, ContextScalar] +Label = NonEmptyList +ContextScalar = Text | Bytes | Int32 | Int64 | UInt32 | UInt64 + +UnitOutput = "c" | "passthrough" | "eq" | "ore" | "ope" | "ore_block" +Output = UnitOutput | Object{UnitOutput: Null} + | "match" | Object{"match": Null | MatchOptions} + | "json" | Object{"json": Null | JsonOptions} + +MatchOptions = Object{ + "tokenizer"?: Tokenizer, + "downcase"?: Bool, + "short_words"?: Bool, + "k"?: Integer, + "m"?: Integer +} +Tokenizer = "standard" | Object{"standard": Null} + | Object{"ngram": Integer} + +Target = NonEmptyText | Object{ + "name": NonEmptyText, + "options"?: JsonOptions +} +JsonOptions = Object{ + "sealed_root"?: Bool, + "ordered"?: List +} + +ValueKind = "bool" + | "int8" | "int16" | "int32" | "int64" | "int128" + | "uint8" | "uint16" | "uint32" | "uint64" | "uint128" + | "float32" | "float64" | "decimal" | "date" | "timestamp" + | "string" | "bytes" | "array" | "object" | "json" +``` + +`outputs` and `target` are exclusive and one must be present. Present optional +members must have the declared type: `"type": null` is not omission. +The explicit null alternatives for enum payloads are exceptions, pinned by the +`unit-output-null-*` and `match-default-*` cases. Unknown keys and output/type +names fail; no unknown option is ignored. + +`Integer` here means an integer-valued transport tag accepted by the option's +integer reader, within its bounds. It does not mean a floating-point value that +happens to be integral. Width is not an option's identity. Context integer width, +in contrast, is part of context identity and must survive parsing unchanged. + +Every object key must be unique, including keys nested inside options. The frame +must contain exactly one complete value within the codec's depth and size bounds. +Duplicate keys, truncation and trailing data fail before declaration semantics. +Do not change these codec rules in this specification. + +## Context semantics + +Try the `Label` alternative before decomposing an extension. A flat text list +is a label; `["users", "region", "value"]` has three segments. It is not the +extended context `[["users", "value"], "region"]`. Extensions nest to the left. +An extension is scalar, so `[["users", "value"], ["tenant"]]` is refused. +Bare `"users/value"` is not a field label. + +A plain segment is nonempty and cannot contain `/`, parentheses, control +characters, or the invisible format characters listed by `Label::check_segment` +in `packages/stack-encrypt/src/descriptor.rs`. It cannot start with `b64:`, `-`, +or an ASCII digit. That shared label rule is referenced rather than reimplemented +by a binding. It applies to label segments, not SDK variable names. + +Without `context_field`: + +- Every field label has at least two segments. +- The last segment is the field's identity. All preceding segments form the + plan label, which must agree across fields. +- All fields have the same ordered extension values, including their types. +- Sealed/indexed field identities cannot collide. The field's map key and its + identity may differ; renaming a map key does not implicitly change its label. + +With `context_field`: + +- Its value names a declared field, whose sole output is `passthrough`. +- That field declares `string` or omits `type`; omission resolves to `string`. +- Every field label has exactly one segment: its identity. +- Extensions still agree across fields. No target field is admitted. +- At execution, each record supplies the named field's label value. Its validity + and the opener's expected-context constraint are runtime requirements. + +The grammar does not add call-time context roles or defaults. Context composition +is observed by `extended-context`, `flat-label-is-not-extension`, the +`typed-extension-*` cases, and the mixed-context refusals. + +## Output and kind semantics + +An output list is nonempty. Output keys are unique after reading their options: +two differently configured match indexes still collide on `match`. +`passthrough` is always the sole output. Ciphertext is explicit; an index-only +declaration does not implicitly acquire `c`. + +Every field with a scalar index declares `type`. Ciphertext-only and passthrough +fields may omit it. Declaring a kind checks values during execution; it does not +select a different leaf encoding. + +| Declared kind | `eq` | `match` | `ore`, `ope`, `ore_block` | +|---|---|---|---| +| bool | Refused | Refused | Refused | +| int8, uint8 | Admitted | Refused | Refused | +| Other integer widths | Admitted | Refused | Admitted | +| float32, float64, decimal, date, timestamp | Admitted | Refused | Admitted | +| string | Admitted | Admitted | Admitted | +| bytes | Admitted | Refused | Admitted | +| array, object, json | Refused | Refused | Refused | + +The corpus explicitly tests all 21 kinds against all five scalar indexes. +`array` and `object` may still be sealed without indexes. A `json` field requires +a document target. A raw `json` output has a recognized syntax but is refused as +`json_index_pending`; it is not an alternative to a document target. + +Match defaults are 3-grams, `downcase: true`, `short_words: false`, `k: 3`, and +`m: 256`. Every missing option takes its default. The bounds are: + +- `3 <= k <= 16`. +- `m` is a power of two from 32 through 65536. +- N-gram length is nonzero and representable by the implementation's integer reader. +- `short_words: true` requires n-grams of length at least three. + +Standard tokenization cannot enable short words. An invalid option fails when +the declaration is checked, before values or keys are needed. + +## Null semantics + +Omitting `null` keeps the field required. A typed field then refuses null values; +an untyped value's ability to represent null is not an implicit nullable column. +`null` is a storage policy, distinct from a null option payload or a missing key. + +| Policy | Declaration requirements | Runtime meaning | +|---|---|---| +| `sql` | Any non-passthrough field | Missing/null source leaves no encrypted field output; the host stores SQL NULL | +| `sealed` | A fixed-width bool, number, decimal, date or timestamp; only equality indexes, if any | Missing/null source is sealed with type-appropriate padding; equality uses the field's null term | + +Text, bytes and composite kinds cannot request sealed null. Passthrough cannot +request either policy. These cases check declaration admissibility only; runtime +padding, queries, missing values and round trips retain their existing tests. + +## Targets and JSON options + +Target syntax is parsed before resolver lookup. `options` takes JSON-index +options; a null options object or unknown option is an invalid target. +`sealed_root` defaults to true, and `ordered` to an empty set. +Paths are parsed by `JsonPath::parse`; ordered paths cannot select array positions. +Repeated equivalent paths collapse into the set. + +Under this profile's `none` resolver, every otherwise valid target produces +`stack_encrypt::target_none` with its target name. A missing target capability +must never silently turn into a ciphertext-only field. + +For a resolver that supplies EQL types, additional existing rules apply: the +target must be producible, its column label has exactly two segments, and the +plan has neither extensions nor a context field. An explicit kind matches the +target's plaintext kind; otherwise that kind is inferred. Only a document target +accepts JSON options. Sealed-null rules include the target's own indexes. +Those resolver-dependent outcomes and normalization need an EQL profile; this +corpus does not claim they have been verified across bindings. + +## Normalization observations + +The native runner observes the parsed plan directly. Its `normalized` fixture +field is a test report, not a canonical production serialization: + +- `label` and `context_field` identify the resolved context source. +- `extension` lists typed scalar parts, preserving width and order. +- `fields` retains transport field order, label segments, resolved kind, + null policy and output order. +- Unit output aliases become bare keys. Default match options become `"match"`. + Non-default match options are explicit; `short_words` is emitted only when true. + +No binding should sort production fields, flatten contexts, coerce context +integers, or infer omitted ciphertext outputs from these observations. +`frame-field-order` checks field order independently of the JSON projection. + +The Go plan-check export returns success or a diagnostic, not a normalized plan. +Its runner therefore verifies acceptance and diagnostics. It does not claim an +independent observation of normalization or add a production export for testing. + +## Diagnostics and corpus format + +The corpus is `v1/declarations.json`. Its version, profile and resolver are +required. Case IDs are unique and stable. Every case contains exactly one input: + +- `{"json": value}` converts JSON objects in lexical key order. Nonnegative + integers become uint64, negative integers int64, and fractional values float64. + Arrays preserve order. This is fixture conversion, not SDK policy. +- `{"frame_hex": text}` supplies exact transport bytes, including typed contexts, + original field order and malformed frames that JSON cannot represent. + +`accepted` is mandatory. Refused cases require `expected_error`, containing the +native engine diagnostic code and structured payload. Accepted cases may include +`normalized`. Runners reject unknown corpus keys and contradictory outcomes. + +A malformed transport frame also has `boundary_error`. The native parser reports +`stack_encrypt::dynamic_plan` with `not_an_object`; the WASI frame check reports +`stack_guest_abi::malformed_input` before reaching that parser. Both runners verify +the frame really is malformed. Valid-frame declaration errors must agree exactly +on code and payload; Go also requires `ErrEncoding`. Error prose is not contractual. + +Cases isolate a failure where practical. This profile does not define universal +error precedence for declarations containing several independent faults. +Neither runner initializes a cipher to check declarations. The Go checker has +no credentials and uses a refusing transport. + +## Evidence and limits + +The corpus covers 201 cases: 91 accepted and 110 refused, including the complete +scalar kind/index matrix. The native runner also checks selected normalization +observations. Existing codec tests own exhaustive frame validity, depth limits, +and integer-reader behavior. Existing EQL tests own resolver/catalog behavior. +Runtime operations and language-specific declarations remain separate layers. + +See [coverage.md](coverage.md) for the consolidation policy. New Python and +TypeScript runners can adopt these inputs and expectations without copying the +engine's validation rules. diff --git a/conformance/stack-encrypt/v1/cases.json b/conformance/stack-encrypt/v1/cases.json index 53bc64bb1..ecb01f633 100644 --- a/conformance/stack-encrypt/v1/cases.json +++ b/conformance/stack-encrypt/v1/cases.json @@ -2,9 +2,7 @@ "version": 1, "profile": "record-scalars-v1", "seed": "0000000000000000000000000000000000000000000000000000000000000000", - "context": [ - "conformance" - ], + "context": ["conformance"], "field": "value", "required_capabilities": [ "records", diff --git a/conformance/stack-encrypt/v1/declarations.json b/conformance/stack-encrypt/v1/declarations.json new file mode 100644 index 000000000..a45223608 --- /dev/null +++ b/conformance/stack-encrypt/v1/declarations.json @@ -0,0 +1,3278 @@ +{ + "version": 1, + "profile": "record-declarations-v1", + "resolver": "none", + "cases": [ + { + "id": "ciphertext-untyped", + "input": { + "json": { "value": { "context": ["users", "value"], "outputs": ["c"] } } + }, + "accepted": true, + "normalized": { + "label": ["users"], + "context_field": null, + "extension": [], + "fields": [ + { + "name": "value", + "label": ["users", "value"], + "type": null, + "null": null, + "outputs": ["c"] + } + ] + } + }, + { + "id": "passthrough-untyped", + "input": { + "json": { + "value": { "context": ["users", "value"], "outputs": ["passthrough"] } + } + }, + "accepted": true, + "normalized": { + "label": ["users"], + "context_field": null, + "extension": [], + "fields": [ + { + "name": "value", + "label": ["users", "value"], + "type": null, + "null": null, + "outputs": ["passthrough"] + } + ] + } + }, + { + "id": "index-only", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["eq"], + "type": "int64" + } + } + }, + "accepted": true, + "normalized": { + "label": ["users"], + "context_field": null, + "extension": [], + "fields": [ + { + "name": "value", + "label": ["users", "value"], + "type": "int64", + "null": null, + "outputs": ["eq"] + } + ] + } + }, + { + "id": "outputs-retain-order", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["ore", "c", "eq"], + "type": "int64" + } + } + }, + "accepted": true, + "normalized": { + "label": ["users"], + "context_field": null, + "extension": [], + "fields": [ + { + "name": "value", + "label": ["users", "value"], + "type": "int64", + "null": null, + "outputs": ["ore", "c", "eq"] + } + ] + } + }, + { + "id": "unit-output-null-c", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": [{ "c": null }], + "type": "string" + } + } + }, + "accepted": true, + "normalized": { + "label": ["users"], + "context_field": null, + "extension": [], + "fields": [ + { + "name": "value", + "label": ["users", "value"], + "type": "string", + "null": null, + "outputs": ["c"] + } + ] + } + }, + { + "id": "unit-output-null-passthrough", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": [{ "passthrough": null }], + "type": "string" + } + } + }, + "accepted": true, + "normalized": { + "label": ["users"], + "context_field": null, + "extension": [], + "fields": [ + { + "name": "value", + "label": ["users", "value"], + "type": "string", + "null": null, + "outputs": ["passthrough"] + } + ] + } + }, + { + "id": "unit-output-null-eq", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": [{ "eq": null }], + "type": "string" + } + } + }, + "accepted": true, + "normalized": { + "label": ["users"], + "context_field": null, + "extension": [], + "fields": [ + { + "name": "value", + "label": ["users", "value"], + "type": "string", + "null": null, + "outputs": ["eq"] + } + ] + } + }, + { + "id": "unit-output-null-ore", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": [{ "ore": null }], + "type": "string" + } + } + }, + "accepted": true, + "normalized": { + "label": ["users"], + "context_field": null, + "extension": [], + "fields": [ + { + "name": "value", + "label": ["users", "value"], + "type": "string", + "null": null, + "outputs": ["ore"] + } + ] + } + }, + { + "id": "unit-output-null-ope", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": [{ "ope": null }], + "type": "string" + } + } + }, + "accepted": true, + "normalized": { + "label": ["users"], + "context_field": null, + "extension": [], + "fields": [ + { + "name": "value", + "label": ["users", "value"], + "type": "string", + "null": null, + "outputs": ["ope"] + } + ] + } + }, + { + "id": "unit-output-null-ore_block", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": [{ "ore_block": null }], + "type": "string" + } + } + }, + "accepted": true, + "normalized": { + "label": ["users"], + "context_field": null, + "extension": [], + "fields": [ + { + "name": "value", + "label": ["users", "value"], + "type": "string", + "null": null, + "outputs": ["ore_block"] + } + ] + } + }, + { + "id": "match-default-bare", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["match"], + "type": "string" + } + } + }, + "accepted": true, + "normalized": { + "label": ["users"], + "context_field": null, + "extension": [], + "fields": [ + { + "name": "value", + "label": ["users", "value"], + "type": "string", + "null": null, + "outputs": ["match"] + } + ] + } + }, + { + "id": "match-default-null", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": [{ "match": null }], + "type": "string" + } + } + }, + "accepted": true, + "normalized": { + "label": ["users"], + "context_field": null, + "extension": [], + "fields": [ + { + "name": "value", + "label": ["users", "value"], + "type": "string", + "null": null, + "outputs": ["match"] + } + ] + } + }, + { + "id": "match-default-empty", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": [{ "match": {} }], + "type": "string" + } + } + }, + "accepted": true, + "normalized": { + "label": ["users"], + "context_field": null, + "extension": [], + "fields": [ + { + "name": "value", + "label": ["users", "value"], + "type": "string", + "null": null, + "outputs": ["match"] + } + ] + } + }, + { + "id": "match-default-explicit", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": [ + { + "match": { + "tokenizer": { "ngram": 3 }, + "downcase": true, + "short_words": false, + "k": 3, + "m": 256 + } + } + ], + "type": "string" + } + } + }, + "accepted": true, + "normalized": { + "label": ["users"], + "context_field": null, + "extension": [], + "fields": [ + { + "name": "value", + "label": ["users", "value"], + "type": "string", + "null": null, + "outputs": ["match"] + } + ] + } + }, + { + "id": "match-custom", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": [ + { + "match": { + "tokenizer": "standard", + "downcase": false, + "k": 4, + "m": 512 + } + } + ], + "type": "string" + } + } + }, + "accepted": true, + "normalized": { + "label": ["users"], + "context_field": null, + "extension": [], + "fields": [ + { + "name": "value", + "label": ["users", "value"], + "type": "string", + "null": null, + "outputs": [ + { + "match": { + "tokenizer": "standard", + "downcase": false, + "k": 4, + "m": 512 + } + } + ] + } + ] + } + }, + { + "id": "tokenizer-standard-null", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": [{ "match": { "tokenizer": { "standard": null } } }], + "type": "string" + } + } + }, + "accepted": true, + "normalized": { + "label": ["users"], + "context_field": null, + "extension": [], + "fields": [ + { + "name": "value", + "label": ["users", "value"], + "type": "string", + "null": null, + "outputs": [ + { + "match": { + "tokenizer": "standard", + "downcase": true, + "k": 3, + "m": 256 + } + } + ] + } + ] + } + }, + { + "id": "match-short-words", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": [{ "match": { "short_words": true } }], + "type": "string" + } + } + }, + "accepted": true, + "normalized": { + "label": ["users"], + "context_field": null, + "extension": [], + "fields": [ + { + "name": "value", + "label": ["users", "value"], + "type": "string", + "null": null, + "outputs": [ + { + "match": { + "tokenizer": { "ngram": 3 }, + "downcase": true, + "short_words": true, + "k": 3, + "m": 256 + } + } + ] + } + ] + } + }, + { + "id": "extended-context", + "input": { + "json": { + "value": { + "context": [[["users", "value"], 7], "eu"], + "outputs": ["c"] + } + } + }, + "accepted": true, + "normalized": { + "label": ["users"], + "context_field": null, + "extension": [ + { "type": "uint64", "text": "7" }, + { "type": "string", "text": "eu" } + ], + "fields": [ + { + "name": "value", + "label": ["users", "value"], + "type": null, + "null": null, + "outputs": ["c"] + } + ] + } + }, + { + "id": "flat-label-is-not-extension", + "input": { + "json": { + "value": { "context": ["users", "region", "value"], "outputs": ["c"] } + } + }, + "accepted": true, + "normalized": { + "label": ["users", "region"], + "context_field": null, + "extension": [], + "fields": [ + { + "name": "value", + "label": ["users", "region", "value"], + "type": null, + "null": null, + "outputs": ["c"] + } + ] + } + }, + { + "id": "sealed-null-equality", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "eq"], + "type": "int32", + "null": "sealed" + } + } + }, + "accepted": true, + "normalized": { + "label": ["users"], + "context_field": null, + "extension": [], + "fields": [ + { + "name": "value", + "label": ["users", "value"], + "type": "int32", + "null": "sealed", + "outputs": ["c", "eq"] + } + ] + } + }, + { + "id": "sealed-null-bool", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c"], + "type": "bool", + "null": "sealed" + } + } + }, + "accepted": true, + "normalized": { + "label": ["users"], + "context_field": null, + "extension": [], + "fields": [ + { + "name": "value", + "label": ["users", "value"], + "type": "bool", + "null": "sealed", + "outputs": ["c"] + } + ] + } + }, + { + "id": "sql-null-untyped", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c"], + "null": "sql" + } + } + }, + "accepted": true, + "normalized": { + "label": ["users"], + "context_field": null, + "extension": [], + "fields": [ + { + "name": "value", + "label": ["users", "value"], + "type": null, + "null": "sql", + "outputs": ["c"] + } + ] + } + }, + { + "id": "context-field-infers-string", + "input": { + "json": { + "context_field": "tenant", + "tenant": { "context": ["tenant"], "outputs": ["passthrough"] }, + "value": { "context": ["value"], "outputs": ["c"] } + } + }, + "accepted": true, + "normalized": { + "label": null, + "context_field": "tenant", + "extension": [], + "fields": [ + { + "name": "tenant", + "label": ["tenant"], + "type": "string", + "null": null, + "outputs": ["passthrough"] + }, + { + "name": "value", + "label": ["value"], + "type": null, + "null": null, + "outputs": ["c"] + } + ] + } + }, + { + "id": "type-bool-eq", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "eq"], + "type": "bool" + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "index_not_admitted", "field": "value" } + } + }, + { + "id": "type-bool-match", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "match"], + "type": "bool" + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "index_not_admitted", "field": "value" } + } + }, + { + "id": "type-bool-ore", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "ore"], + "type": "bool" + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "index_not_admitted", "field": "value" } + } + }, + { + "id": "type-bool-ope", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "ope"], + "type": "bool" + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "index_not_admitted", "field": "value" } + } + }, + { + "id": "type-bool-ore_block", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "ore_block"], + "type": "bool" + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "index_not_admitted", "field": "value" } + } + }, + { + "id": "type-int8-eq", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "eq"], + "type": "int8" + } + } + }, + "accepted": true + }, + { + "id": "type-int8-match", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "match"], + "type": "int8" + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "index_not_admitted", "field": "value" } + } + }, + { + "id": "type-int8-ore", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "ore"], + "type": "int8" + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "index_not_admitted", "field": "value" } + } + }, + { + "id": "type-int8-ope", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "ope"], + "type": "int8" + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "index_not_admitted", "field": "value" } + } + }, + { + "id": "type-int8-ore_block", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "ore_block"], + "type": "int8" + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "index_not_admitted", "field": "value" } + } + }, + { + "id": "type-int16-eq", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "eq"], + "type": "int16" + } + } + }, + "accepted": true + }, + { + "id": "type-int16-match", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "match"], + "type": "int16" + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "index_not_admitted", "field": "value" } + } + }, + { + "id": "type-int16-ore", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "ore"], + "type": "int16" + } + } + }, + "accepted": true + }, + { + "id": "type-int16-ope", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "ope"], + "type": "int16" + } + } + }, + "accepted": true + }, + { + "id": "type-int16-ore_block", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "ore_block"], + "type": "int16" + } + } + }, + "accepted": true + }, + { + "id": "type-int32-eq", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "eq"], + "type": "int32" + } + } + }, + "accepted": true + }, + { + "id": "type-int32-match", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "match"], + "type": "int32" + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "index_not_admitted", "field": "value" } + } + }, + { + "id": "type-int32-ore", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "ore"], + "type": "int32" + } + } + }, + "accepted": true + }, + { + "id": "type-int32-ope", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "ope"], + "type": "int32" + } + } + }, + "accepted": true + }, + { + "id": "type-int32-ore_block", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "ore_block"], + "type": "int32" + } + } + }, + "accepted": true + }, + { + "id": "type-int64-eq", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "eq"], + "type": "int64" + } + } + }, + "accepted": true + }, + { + "id": "type-int64-match", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "match"], + "type": "int64" + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "index_not_admitted", "field": "value" } + } + }, + { + "id": "type-int64-ore", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "ore"], + "type": "int64" + } + } + }, + "accepted": true + }, + { + "id": "type-int64-ope", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "ope"], + "type": "int64" + } + } + }, + "accepted": true + }, + { + "id": "type-int64-ore_block", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "ore_block"], + "type": "int64" + } + } + }, + "accepted": true + }, + { + "id": "type-int128-eq", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "eq"], + "type": "int128" + } + } + }, + "accepted": true + }, + { + "id": "type-int128-match", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "match"], + "type": "int128" + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "index_not_admitted", "field": "value" } + } + }, + { + "id": "type-int128-ore", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "ore"], + "type": "int128" + } + } + }, + "accepted": true + }, + { + "id": "type-int128-ope", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "ope"], + "type": "int128" + } + } + }, + "accepted": true + }, + { + "id": "type-int128-ore_block", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "ore_block"], + "type": "int128" + } + } + }, + "accepted": true + }, + { + "id": "type-uint8-eq", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "eq"], + "type": "uint8" + } + } + }, + "accepted": true + }, + { + "id": "type-uint8-match", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "match"], + "type": "uint8" + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "index_not_admitted", "field": "value" } + } + }, + { + "id": "type-uint8-ore", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "ore"], + "type": "uint8" + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "index_not_admitted", "field": "value" } + } + }, + { + "id": "type-uint8-ope", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "ope"], + "type": "uint8" + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "index_not_admitted", "field": "value" } + } + }, + { + "id": "type-uint8-ore_block", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "ore_block"], + "type": "uint8" + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "index_not_admitted", "field": "value" } + } + }, + { + "id": "type-uint16-eq", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "eq"], + "type": "uint16" + } + } + }, + "accepted": true + }, + { + "id": "type-uint16-match", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "match"], + "type": "uint16" + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "index_not_admitted", "field": "value" } + } + }, + { + "id": "type-uint16-ore", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "ore"], + "type": "uint16" + } + } + }, + "accepted": true + }, + { + "id": "type-uint16-ope", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "ope"], + "type": "uint16" + } + } + }, + "accepted": true + }, + { + "id": "type-uint16-ore_block", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "ore_block"], + "type": "uint16" + } + } + }, + "accepted": true + }, + { + "id": "type-uint32-eq", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "eq"], + "type": "uint32" + } + } + }, + "accepted": true + }, + { + "id": "type-uint32-match", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "match"], + "type": "uint32" + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "index_not_admitted", "field": "value" } + } + }, + { + "id": "type-uint32-ore", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "ore"], + "type": "uint32" + } + } + }, + "accepted": true + }, + { + "id": "type-uint32-ope", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "ope"], + "type": "uint32" + } + } + }, + "accepted": true + }, + { + "id": "type-uint32-ore_block", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "ore_block"], + "type": "uint32" + } + } + }, + "accepted": true + }, + { + "id": "type-uint64-eq", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "eq"], + "type": "uint64" + } + } + }, + "accepted": true + }, + { + "id": "type-uint64-match", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "match"], + "type": "uint64" + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "index_not_admitted", "field": "value" } + } + }, + { + "id": "type-uint64-ore", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "ore"], + "type": "uint64" + } + } + }, + "accepted": true + }, + { + "id": "type-uint64-ope", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "ope"], + "type": "uint64" + } + } + }, + "accepted": true + }, + { + "id": "type-uint64-ore_block", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "ore_block"], + "type": "uint64" + } + } + }, + "accepted": true + }, + { + "id": "type-uint128-eq", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "eq"], + "type": "uint128" + } + } + }, + "accepted": true + }, + { + "id": "type-uint128-match", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "match"], + "type": "uint128" + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "index_not_admitted", "field": "value" } + } + }, + { + "id": "type-uint128-ore", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "ore"], + "type": "uint128" + } + } + }, + "accepted": true + }, + { + "id": "type-uint128-ope", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "ope"], + "type": "uint128" + } + } + }, + "accepted": true + }, + { + "id": "type-uint128-ore_block", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "ore_block"], + "type": "uint128" + } + } + }, + "accepted": true + }, + { + "id": "type-float32-eq", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "eq"], + "type": "float32" + } + } + }, + "accepted": true + }, + { + "id": "type-float32-match", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "match"], + "type": "float32" + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "index_not_admitted", "field": "value" } + } + }, + { + "id": "type-float32-ore", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "ore"], + "type": "float32" + } + } + }, + "accepted": true + }, + { + "id": "type-float32-ope", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "ope"], + "type": "float32" + } + } + }, + "accepted": true + }, + { + "id": "type-float32-ore_block", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "ore_block"], + "type": "float32" + } + } + }, + "accepted": true + }, + { + "id": "type-float64-eq", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "eq"], + "type": "float64" + } + } + }, + "accepted": true + }, + { + "id": "type-float64-match", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "match"], + "type": "float64" + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "index_not_admitted", "field": "value" } + } + }, + { + "id": "type-float64-ore", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "ore"], + "type": "float64" + } + } + }, + "accepted": true + }, + { + "id": "type-float64-ope", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "ope"], + "type": "float64" + } + } + }, + "accepted": true + }, + { + "id": "type-float64-ore_block", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "ore_block"], + "type": "float64" + } + } + }, + "accepted": true + }, + { + "id": "type-decimal-eq", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "eq"], + "type": "decimal" + } + } + }, + "accepted": true + }, + { + "id": "type-decimal-match", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "match"], + "type": "decimal" + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "index_not_admitted", "field": "value" } + } + }, + { + "id": "type-decimal-ore", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "ore"], + "type": "decimal" + } + } + }, + "accepted": true + }, + { + "id": "type-decimal-ope", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "ope"], + "type": "decimal" + } + } + }, + "accepted": true + }, + { + "id": "type-decimal-ore_block", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "ore_block"], + "type": "decimal" + } + } + }, + "accepted": true + }, + { + "id": "type-date-eq", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "eq"], + "type": "date" + } + } + }, + "accepted": true + }, + { + "id": "type-date-match", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "match"], + "type": "date" + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "index_not_admitted", "field": "value" } + } + }, + { + "id": "type-date-ore", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "ore"], + "type": "date" + } + } + }, + "accepted": true + }, + { + "id": "type-date-ope", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "ope"], + "type": "date" + } + } + }, + "accepted": true + }, + { + "id": "type-date-ore_block", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "ore_block"], + "type": "date" + } + } + }, + "accepted": true + }, + { + "id": "type-timestamp-eq", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "eq"], + "type": "timestamp" + } + } + }, + "accepted": true + }, + { + "id": "type-timestamp-match", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "match"], + "type": "timestamp" + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "index_not_admitted", "field": "value" } + } + }, + { + "id": "type-timestamp-ore", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "ore"], + "type": "timestamp" + } + } + }, + "accepted": true + }, + { + "id": "type-timestamp-ope", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "ope"], + "type": "timestamp" + } + } + }, + "accepted": true + }, + { + "id": "type-timestamp-ore_block", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "ore_block"], + "type": "timestamp" + } + } + }, + "accepted": true + }, + { + "id": "type-string-eq", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "eq"], + "type": "string" + } + } + }, + "accepted": true + }, + { + "id": "type-string-match", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "match"], + "type": "string" + } + } + }, + "accepted": true + }, + { + "id": "type-string-ore", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "ore"], + "type": "string" + } + } + }, + "accepted": true + }, + { + "id": "type-string-ope", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "ope"], + "type": "string" + } + } + }, + "accepted": true + }, + { + "id": "type-string-ore_block", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "ore_block"], + "type": "string" + } + } + }, + "accepted": true + }, + { + "id": "type-bytes-eq", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "eq"], + "type": "bytes" + } + } + }, + "accepted": true + }, + { + "id": "type-bytes-match", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "match"], + "type": "bytes" + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "index_not_admitted", "field": "value" } + } + }, + { + "id": "type-bytes-ore", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "ore"], + "type": "bytes" + } + } + }, + "accepted": true + }, + { + "id": "type-bytes-ope", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "ope"], + "type": "bytes" + } + } + }, + "accepted": true + }, + { + "id": "type-bytes-ore_block", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "ore_block"], + "type": "bytes" + } + } + }, + "accepted": true + }, + { + "id": "type-array-eq", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "eq"], + "type": "array" + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "index_not_admitted", "field": "value" } + } + }, + { + "id": "type-array-match", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "match"], + "type": "array" + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "index_not_admitted", "field": "value" } + } + }, + { + "id": "type-array-ore", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "ore"], + "type": "array" + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "index_not_admitted", "field": "value" } + } + }, + { + "id": "type-array-ope", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "ope"], + "type": "array" + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "index_not_admitted", "field": "value" } + } + }, + { + "id": "type-array-ore_block", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "ore_block"], + "type": "array" + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "index_not_admitted", "field": "value" } + } + }, + { + "id": "type-object-eq", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "eq"], + "type": "object" + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "index_not_admitted", "field": "value" } + } + }, + { + "id": "type-object-match", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "match"], + "type": "object" + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "index_not_admitted", "field": "value" } + } + }, + { + "id": "type-object-ore", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "ore"], + "type": "object" + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "index_not_admitted", "field": "value" } + } + }, + { + "id": "type-object-ope", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "ope"], + "type": "object" + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "index_not_admitted", "field": "value" } + } + }, + { + "id": "type-object-ore_block", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "ore_block"], + "type": "object" + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "index_not_admitted", "field": "value" } + } + }, + { + "id": "type-json-eq", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "eq"], + "type": "json" + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "index_not_admitted", "field": "value" } + } + }, + { + "id": "type-json-match", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "match"], + "type": "json" + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "index_not_admitted", "field": "value" } + } + }, + { + "id": "type-json-ore", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "ore"], + "type": "json" + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "index_not_admitted", "field": "value" } + } + }, + { + "id": "type-json-ope", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "ope"], + "type": "json" + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "index_not_admitted", "field": "value" } + } + }, + { + "id": "type-json-ore_block", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "ore_block"], + "type": "json" + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "index_not_admitted", "field": "value" } + } + }, + { + "id": "root-null", + "input": { "json": null }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "not_an_object" } + } + }, + { + "id": "root-list", + "input": { "json": [] }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "not_an_object" } + } + }, + { + "id": "empty-plan", + "input": { "json": {} }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "no_fields" } + } + }, + { + "id": "field-not-object", + "input": { "json": { "value": "x" } }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "not_an_object", "field": "value" } + } + }, + { + "id": "field-null", + "input": { "json": { "value": null } }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "not_an_object", "field": "value" } + } + }, + { + "id": "unknown-key", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c"], + "unknown": true + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "unknown_key", "field": "value" } + } + }, + { + "id": "missing-context", + "input": { "json": { "value": { "outputs": ["c"] } } }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "missing_context", "field": "value" } + } + }, + { + "id": "missing-outputs", + "input": { "json": { "value": { "context": ["users", "value"] } } }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "missing_outputs", "field": "value" } + } + }, + { + "id": "outputs-with-target", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c"], + "target": "TextEq" + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "outputs_with_target", "field": "value" } + } + }, + { + "id": "outputs-string", + "input": { + "json": { "value": { "context": ["users", "value"], "outputs": "c" } } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "outputs_not_list", "field": "value" } + } + }, + { + "id": "outputs-null", + "input": { + "json": { "value": { "context": ["users", "value"], "outputs": null } } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "outputs_not_list", "field": "value" } + } + }, + { + "id": "outputs-empty", + "input": { + "json": { "value": { "context": ["users", "value"], "outputs": [] } } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "no_outputs", "field": "value" } + } + }, + { + "id": "output-unknown", + "input": { + "json": { + "value": { "context": ["users", "value"], "outputs": ["wat"] } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "unknown_output", "field": "value" } + } + }, + { + "id": "output-options-on-equality", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": [{ "eq": {} }], + "type": "int64" + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "unknown_output", "field": "value" } + } + }, + { + "id": "output-duplicate", + "input": { + "json": { + "value": { "context": ["users", "value"], "outputs": ["c", "c"] } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "duplicate_output", "field": "value" } + } + }, + { + "id": "match-options-duplicate-key", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["match", { "match": {} }], + "type": "string" + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "duplicate_output", "field": "value" } + } + }, + { + "id": "passthrough-with-ciphertext", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["passthrough", "c"] + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "passthrough_with_outputs", "field": "value" } + } + }, + { + "id": "type-unknown", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c"], + "type": "uuid" + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "unknown_type", "field": "value" } + } + }, + { + "id": "type-null", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c"], + "type": null + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "unknown_type", "field": "value" } + } + }, + { + "id": "type-json-without-target", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c"], + "type": "json" + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "json_needs_target", "field": "value" } + } + }, + { + "id": "json-output-pending", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["json"], + "type": "json" + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "json_index_pending", "field": "value" } + } + }, + { + "id": "context-bare-label", + "input": { + "json": { "value": { "context": "users/value", "outputs": ["c"] } } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "context_not_label", "field": "value" } + } + }, + { + "id": "context-one-segment", + "input": { + "json": { "value": { "context": ["value"], "outputs": ["c"] } } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "context_not_label", "field": "value" } + } + }, + { + "id": "context-nested-extension", + "input": { + "json": { + "value": { + "context": [["users", "value"], ["tenant"]], + "outputs": ["c"] + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "context_not_label", "field": "value" } + } + }, + { + "id": "context-segment-slash", + "input": { + "json": { "value": { "context": ["users", "a/b"], "outputs": ["c"] } } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "context_not_label", "field": "value" } + } + }, + { + "id": "context-segment-digit", + "input": { + "json": { "value": { "context": ["users", "9"], "outputs": ["c"] } } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "context_not_label", "field": "value" } + } + }, + { + "id": "mixed-prefix", + "input": { + "json": { + "a": { "context": ["users", "a"], "outputs": ["c"] }, + "b": { "context": ["orders", "b"], "outputs": ["c"] } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "mixed_contexts", "field": "b" } + } + }, + { + "id": "mixed-extension", + "input": { + "json": { + "a": { "context": [["users", "a"], 1], "outputs": ["c"] }, + "b": { "context": [["users", "b"], 2], "outputs": ["c"] } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "mixed_contexts", "field": "b" } + } + }, + { + "id": "shared-identity", + "input": { + "json": { + "a": { "context": ["users", "same"], "outputs": ["c"] }, + "b": { "context": ["users", "same"], "outputs": ["c"] } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "shared_identity", "field": "b" } + } + }, + { + "id": "context-field-null", + "input": { + "json": { + "context_field": null, + "value": { "context": ["users", "value"], "outputs": ["c"] } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "context_field" } + } + }, + { + "id": "context-field-not-declared", + "input": { + "json": { + "context_field": "tenant", + "value": { "context": ["value"], "outputs": ["c"] } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "no_such_field", "field": "tenant" } + } + }, + { + "id": "context-field-encrypted", + "input": { + "json": { + "context_field": "tenant", + "tenant": { "context": ["tenant"], "outputs": ["c"] } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "context_field", "field": "tenant" } + } + }, + { + "id": "context-field-static-label", + "input": { + "json": { + "context_field": "tenant", + "tenant": { + "context": ["users", "tenant"], + "outputs": ["passthrough"] + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "context_not_label", "field": "tenant" } + } + }, + { + "id": "null-mode-null", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c"], + "null": null + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "unknown_null", "field": "value" } + } + }, + { + "id": "null-mode-unknown", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c"], + "null": "optional" + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "unknown_null", "field": "value" } + } + }, + { + "id": "null-passthrough", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["passthrough"], + "null": "sql" + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "null_passthrough", "field": "value" } + } + }, + { + "id": "sealed-null-untyped", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c"], + "null": "sealed" + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "sealed_null_type", "field": "value" } + } + }, + { + "id": "sealed-null-text", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c"], + "type": "string", + "null": "sealed" + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "sealed_null_type", "field": "value" } + } + }, + { + "id": "sealed-null-order", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": ["c", "ore"], + "type": "int64", + "null": "sealed" + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "sealed_null_index", "field": "value" } + } + }, + { + "id": "target-null", + "input": { + "json": { "value": { "context": ["users", "value"], "target": null } } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "invalid_target", "field": "value" } + } + }, + { + "id": "target-empty", + "input": { + "json": { "value": { "context": ["users", "value"], "target": "" } } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "invalid_target", "field": "value" } + } + }, + { + "id": "target-options-null", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "target": { "name": "TextEq", "options": null } + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "invalid_target", "field": "value" } + } + }, + { + "id": "target-options-unknown", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "target": { "name": "JsonSearch", "options": { "wat": 1 } } + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "invalid_target", "field": "value" } + } + }, + { + "id": "target-options-position", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "target": { + "name": "JsonSearch", + "options": { "ordered": ["$.a[0]"] } + } + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "invalid_target", "field": "value" } + } + }, + { + "id": "context-null", + "input": { "json": { "value": { "context": null, "outputs": ["c"] } } }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_context", + "fields": { "reason": "context_kind", "field": "value" } + } + }, + { + "id": "context-bool", + "input": { "json": { "value": { "context": true, "outputs": ["c"] } } }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_context", + "fields": { "reason": "context_kind", "field": "value" } + } + }, + { + "id": "context-float", + "input": { "json": { "value": { "context": 1.25, "outputs": ["c"] } } }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_context", + "fields": { "reason": "context_kind", "field": "value" } + } + }, + { + "id": "context-empty", + "input": { "json": { "value": { "context": [], "outputs": ["c"] } } }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_context", + "fields": { "reason": "empty_context", "field": "value" } + } + }, + { + "id": "untyped-index", + "input": { + "json": { + "value": { "context": ["users", "value"], "outputs": ["eq"] } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_untyped_index", + "fields": { "field": "value" } + } + }, + { + "id": "target-unavailable-name", + "input": { + "json": { + "value": { "context": ["users", "value"], "target": "TextEq" } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::target_none", + "fields": { "target": "TextEq" } + } + }, + { + "id": "target-unavailable-configured", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "target": { "name": "TextEq" } + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::target_none", + "fields": { "target": "TextEq" } + } + }, + { + "id": "target-unavailable-json-options", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "target": { + "name": "JsonSearch", + "options": { "sealed_root": false, "ordered": ["$.age", "$.age"] } + } + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::target_none", + "fields": { "target": "JsonSearch" } + } + }, + { + "id": "match-invalid-unknown", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": [{ "match": { "wat": 1 } }], + "type": "string" + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "unknown_output", "field": "value" } + } + }, + { + "id": "match-invalid-k-low", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": [{ "match": { "k": 2 } }], + "type": "string" + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "unknown_output", "field": "value" } + } + }, + { + "id": "match-invalid-k-high", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": [{ "match": { "k": 17 } }], + "type": "string" + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "unknown_output", "field": "value" } + } + }, + { + "id": "match-invalid-m-power", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": [{ "match": { "m": 100 } }], + "type": "string" + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "unknown_output", "field": "value" } + } + }, + { + "id": "match-invalid-m-low", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": [{ "match": { "m": 16 } }], + "type": "string" + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "unknown_output", "field": "value" } + } + }, + { + "id": "match-invalid-m-high", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": [{ "match": { "m": 131072 } }], + "type": "string" + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "unknown_output", "field": "value" } + } + }, + { + "id": "match-invalid-ngram-zero", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": [{ "match": { "tokenizer": { "ngram": 0 } } }], + "type": "string" + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "unknown_output", "field": "value" } + } + }, + { + "id": "match-invalid-short-standard", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": [ + { "match": { "tokenizer": "standard", "short_words": true } } + ], + "type": "string" + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "unknown_output", "field": "value" } + } + }, + { + "id": "match-invalid-short-small", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": [ + { "match": { "tokenizer": { "ngram": 2 }, "short_words": true } } + ], + "type": "string" + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "unknown_output", "field": "value" } + } + }, + { + "id": "match-invalid-downcase-null", + "input": { + "json": { + "value": { + "context": ["users", "value"], + "outputs": [{ "match": { "downcase": null } }], + "type": "string" + } + } + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "unknown_output", "field": "value" } + } + }, + { + "id": "typed-extension-int32", + "input": { + "frame_hex": "f1010000000500000076616c7565f10200000007000000636f6e74657874f002000000f0020000000a0500000075736572730a0500000076616c75650407000000070000006f757470757473f0010000000a0100000063" + }, + "accepted": true, + "normalized": { + "label": ["users"], + "context_field": null, + "extension": [{ "type": "int32", "text": "7" }], + "fields": [ + { + "name": "value", + "label": ["users", "value"], + "type": null, + "null": null, + "outputs": ["c"] + } + ] + } + }, + { + "id": "typed-extension-int64", + "input": { + "frame_hex": "f1010000000500000076616c7565f10200000007000000636f6e74657874f002000000f0020000000a0500000075736572730a0500000076616c7565050700000000000000070000006f757470757473f0010000000a0100000063" + }, + "accepted": true, + "normalized": { + "label": ["users"], + "context_field": null, + "extension": [{ "type": "int64", "text": "7" }], + "fields": [ + { + "name": "value", + "label": ["users", "value"], + "type": null, + "null": null, + "outputs": ["c"] + } + ] + } + }, + { + "id": "typed-extension-uint32", + "input": { + "frame_hex": "f1010000000500000076616c7565f10200000007000000636f6e74657874f002000000f0020000000a0500000075736572730a0500000076616c75650607000000070000006f757470757473f0010000000a0100000063" + }, + "accepted": true, + "normalized": { + "label": ["users"], + "context_field": null, + "extension": [{ "type": "uint32", "text": "7" }], + "fields": [ + { + "name": "value", + "label": ["users", "value"], + "type": null, + "null": null, + "outputs": ["c"] + } + ] + } + }, + { + "id": "typed-extension-uint64", + "input": { + "frame_hex": "f1010000000500000076616c7565f10200000007000000636f6e74657874f002000000f0020000000a0500000075736572730a0500000076616c7565070700000000000000070000006f757470757473f0010000000a0100000063" + }, + "accepted": true, + "normalized": { + "label": ["users"], + "context_field": null, + "extension": [{ "type": "uint64", "text": "7" }], + "fields": [ + { + "name": "value", + "label": ["users", "value"], + "type": null, + "null": null, + "outputs": ["c"] + } + ] + } + }, + { + "id": "typed-extension-int8", + "input": { + "frame_hex": "f1010000000500000076616c7565f10200000007000000636f6e74657874f002000000f0020000000a0500000075736572730a0500000076616c75650c07070000006f757470757473f0010000000a0100000063" + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_context", + "fields": { "field": "value", "reason": "context_kind" } + } + }, + { + "id": "frame-empty", + "input": { "frame_hex": "" }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "not_an_object" } + }, + "boundary_error": { + "code": "stack_guest_abi::malformed_input", + "fields": {} + } + }, + { + "id": "frame-invalid", + "input": { "frame_hex": "ff" }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "not_an_object" } + }, + "boundary_error": { + "code": "stack_guest_abi::malformed_input", + "fields": {} + } + }, + { + "id": "frame-truncated", + "input": { "frame_hex": "f101" }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "not_an_object" } + }, + "boundary_error": { + "code": "stack_guest_abi::malformed_input", + "fields": {} + } + }, + { + "id": "frame-duplicate-key", + "input": { + "frame_hex": "f1020000000500000076616c7565f10200000007000000636f6e74657874f0020000000a0500000075736572730a0500000076616c7565070000006f757470757473f0010000000a01000000630500000076616c7565f10200000007000000636f6e74657874f0020000000a0500000075736572730a0500000076616c7565070000006f757470757473f0010000000a0100000063" + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "not_an_object" } + }, + "boundary_error": { + "code": "stack_guest_abi::malformed_input", + "fields": {} + } + }, + { + "id": "frame-trailing", + "input": { + "frame_hex": "f1010000000500000076616c7565f10200000007000000636f6e74657874f0020000000a0500000075736572730a0500000076616c7565070000006f757470757473f0010000000a010000006300" + }, + "accepted": false, + "expected_error": { + "code": "stack_encrypt::dynamic_plan", + "fields": { "reason": "not_an_object" } + }, + "boundary_error": { + "code": "stack_guest_abi::malformed_input", + "fields": {} + } + }, + { + "id": "frame-field-order", + "input": { + "frame_hex": "f102000000010000007af10200000007000000636f6e74657874f0020000000a0500000075736572730a010000007a070000006f757470757473f0010000000a01000000630100000061f10200000007000000636f6e74657874f0020000000a0500000075736572730a0100000061070000006f757470757473f0010000000a0100000063" + }, + "accepted": true, + "normalized": { + "label": ["users"], + "context_field": null, + "extension": [], + "fields": [ + { + "name": "z", + "label": ["users", "z"], + "type": null, + "null": null, + "outputs": ["c"] + }, + { + "name": "a", + "label": ["users", "a"], + "type": null, + "null": null, + "outputs": ["c"] + } + ] + } + } + ] +} diff --git a/docs/plans/2026-10-31-stack-encrypt-conformance.md b/docs/plans/2026-10-31-stack-encrypt-conformance.md index 51750c03d..e17849a79 100644 --- a/docs/plans/2026-10-31-stack-encrypt-conformance.md +++ b/docs/plans/2026-10-31-stack-encrypt-conformance.md @@ -1,6 +1,6 @@ # Stack Encrypt cross-language conformance -Date: 2026-10-31. Status: first Rust/Go slice implemented; the complete contract is not frozen. +Date: 2026-10-31. Status: scalar and declaration slices implemented; the complete contract is not frozen. This work is stacked on [PR #1404](https://github.com/cipherstash/stack/pull/1404), at `b684a217e2da629c762efa70fac958ec7e21646a`. It follows the accepted @@ -78,21 +78,43 @@ Implemented on `feat/stack-encrypt-conformance`, based directly on #1404. The corpus has eight positive cases and three query refusals. Local validation passes for native Rust, Go WASI, and fresh Go-to-Rust artifact exchange through `mise run test:conformance`. Rust clippy, the enabled Go linters, and 33 CI wiring -checks also pass. The workflow changes are local; hosted CI has not run them. +checks also pass. The work is in draft [PR #1460](https://github.com/cipherstash/stack/pull/1460), +targeting #1404's branch. Local validation is distinct from hosted CI status. The first profile includes frozen typed frames and equality terms, frozen native ciphertexts, fresh native scalar and record round trips, and forbidden retrieval under a wrong context. The implementation adds no production API changes. -The wider grammar, SQL requirements and Python/TypeScript runners remain open. +SQL requirements and Python/TypeScript runners remain open. + +## Declaration checkpoint + +The [declaration grammar](../../conformance/stack-encrypt/declaration-grammar.md) +records the implemented syntax and semantic rules, including accepted enum +aliases, defaults, context composition, all 21 kinds against five scalar indexes, +null policies and target-capability refusal. Its 201 cases run in native Rust +and Go WASI without a cipher. Native Rust checks selected normalized plan +observations; Go's existing checker exposes acceptance and diagnostics only. + +The profile fixes `NoTargets`. EQL resolver/catalog behavior and full JSON/SQL +operation semantics remain later profiles. Malformed frames explicitly distinguish +the native parser's diagnostic from the WASI boundary's earlier refusal. + +Local validation passes for both profiles (212 shared cases), fresh ciphertext +exchange, Rust clippy/formatting, Go lint, Biome and 34 CI wiring checks. + +The [coverage map](../../conformance/stack-encrypt/coverage.md#consolidation-policy) +records consolidation candidates. Shared case tables can replace duplicated +expectations once coverage matches; language-specific and implementation tests +remain. No existing tests have been removed. ## Follow-up slices -- Add a formal grammar for the implemented declaration/request profile, including - unknown-field rules, defaults, normalized declarations, and unsupported features. - Generate structural schemas from shared models where faithful; keep semantic - validation normative and executable rather than assuming JSON Schema covers it. -- Add shared positive and negative definition/request cases, stable diagnostic - fields, missing/null cases, full numeric kinds, and context composition vectors. +- Extend the declaration grammar to EQL resolver capabilities and the operation + request profiles. Generate structural schemas from shared models where faithful; + keep semantic validation executable rather than assuming JSON Schema covers it. +- Extend shared cases to runtime source/read/request shapes, missing/null values, + all numeric kinds, and context binding. Consolidate existing duplicate tables + only after their unique assertions have equivalent coverage. - Bring the existing JSON/SQL suites under named requirements. Add common JSON vectors across runners and pin the supported predicate grammar and its limits. - Add Python's runner in its owning repository/worktree, with a declared capability diff --git a/languages/golang/encrypt/declaration_conformance_test.go b/languages/golang/encrypt/declaration_conformance_test.go new file mode 100644 index 000000000..4e1dafc15 --- /dev/null +++ b/languages/golang/encrypt/declaration_conformance_test.go @@ -0,0 +1,167 @@ +package encrypt_test + +import ( + "bytes" + "context" + "encoding/hex" + "encoding/json" + "errors" + "os" + "path/filepath" + "reflect" + "sort" + "strconv" + "strings" + "testing" + + "github.com/cipherstash/stack/languages/golang/encrypt" + "github.com/cipherstash/vitaminc/bindings/go/vcffi" + "github.com/cipherstash/vitaminc/bindings/go/vcvalue" +) + +type declarationCase struct { + ID string `json:"id"` + Input struct { + JSON json.RawMessage `json:"json"` + FrameHex *string `json:"frame_hex"` + } `json:"input"` + Accepted *bool `json:"accepted"` + ExpectedError *declarationError `json:"expected_error"` + BoundaryError *declarationError `json:"boundary_error"` + // Only native Rust exposes the parsed plan. Go checks acceptance and the + // ABI diagnostic; it makes no claim to observe normalization independently. + Normalized json.RawMessage `json:"normalized"` +} + +type declarationError struct { + Code string `json:"code"` + Fields map[string]any `json:"fields"` +} + +// The corpus JSON projection fixes object order lexically, positive integers +// as uint64 and negative integers as int64. This is test input conversion, +// not SDK normalization. Non-JSON transport shapes use frame_hex instead. +func declarationValue(t *testing.T, value any) any { + t.Helper() + switch v := value.(type) { + case map[string]any: + keys := make([]string, 0, len(v)) + for key := range v { + keys = append(keys, key) + } + sort.Strings(keys) + object := make(vcvalue.Object, 0, len(keys)) + for _, key := range keys { + object = append(object, vcvalue.Field{Key: key, Value: declarationValue(t, v[key])}) + } + return object + case []any: + for i := range v { + v[i] = declarationValue(t, v[i]) + } + return v + case json.Number: + text := v.String() + var value any + var err error + switch { + case strings.ContainsAny(text, ".eE"): + value, err = strconv.ParseFloat(text, 64) + case strings.HasPrefix(text, "-"): + value, err = strconv.ParseInt(text, 10, 64) + default: + value, err = strconv.ParseUint(text, 10, 64) + } + if err != nil { + t.Fatal(err) + } + return value + default: + return v + } +} + +func TestDeclarationConformance(t *testing.T) { + var corpus struct { + Version int `json:"version"` + Profile string `json:"profile"` + Resolver string `json:"resolver"` + Cases []declarationCase `json:"cases"` + } + readConformanceJSON(t, filepath.Join("..", "..", "..", "conformance", "stack-encrypt", "v1", "declarations.json"), &corpus) + if corpus.Version != 1 || corpus.Profile != "record-declarations-v1" || corpus.Resolver != "none" || len(corpus.Cases) == 0 { + t.Fatal("unsupported or empty declaration corpus") + } + wasm, err := os.ReadFile("testdata/stack_encrypt_guest_deterministic.wasm") + if errors.Is(err, os.ErrNotExist) && os.Getenv("STACK_CONFORMANCE_REQUIRED") != "1" { + t.Skip(err) + } + if err != nil { + t.Fatal(err) + } + ctx := context.Background() + checker, err := encrypt.NewCheckerOver(ctx, wasm) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = checker.Close() }) + seen := map[string]bool{} + for _, c := range corpus.Cases { + if c.ID == "" || seen[c.ID] { + t.Fatalf("empty or duplicate case ID %q", c.ID) + } + seen[c.ID] = true + t.Run(c.ID, func(t *testing.T) { + if c.Accepted == nil { + t.Fatal("missing accepted outcome") + } + accepted := *c.Accepted + if accepted != (c.ExpectedError == nil) || (!accepted && len(c.Normalized) != 0) { + t.Fatal("contradictory expected outcome") + } + if (len(c.Input.JSON) == 0) == (c.Input.FrameHex == nil) { + t.Fatal("expected exactly one input representation") + } + var frame []byte + var err error + if c.Input.FrameHex != nil { + frame, err = hex.DecodeString(*c.Input.FrameHex) + } else { + decoder := json.NewDecoder(bytes.NewReader(c.Input.JSON)) + decoder.UseNumber() + var value any + if err := decoder.Decode(&value); err != nil { + t.Fatal(err) + } + frame, err = vcffi.Marshal(declarationValue(t, value)) + } + if err != nil { + t.Fatal(err) + } + err = checker.CheckFrameForTest(ctx, frame) + expected := c.ExpectedError + if c.BoundaryError != nil { + if accepted || c.Input.FrameHex == nil { + t.Fatal("boundary refusal requires a rejected raw frame") + } + if _, decodeErr := vcffi.Unmarshal(frame); decodeErr == nil { + t.Fatal("boundary refusal requires a malformed frame") + } + expected = c.BoundaryError + } + if accepted { + if err != nil { + t.Fatal(err) + } + return + } + var diagnostic *encrypt.Diagnostic + if !errors.Is(err, encrypt.ErrEncoding) || !errors.As(err, &diagnostic) { + t.Fatalf("expected encoding diagnostic, got %v", err) + } + if diagnostic.Code != expected.Code || !reflect.DeepEqual(diagnostic.Fields, expected.Fields) { + t.Fatalf("got %s %#v; expected %s %#v", diagnostic.Code, diagnostic.Fields, expected.Code, expected.Fields) + } + }) + } +} diff --git a/languages/golang/encrypt/export_test.go b/languages/golang/encrypt/export_test.go index 753d1a196..b6ffdc710 100644 --- a/languages/golang/encrypt/export_test.go +++ b/languages/golang/encrypt/export_test.go @@ -183,3 +183,13 @@ var ( WantDiagnostic = wantDiagnostic WantCode = wantCode ) + +// CheckFrameForTest reaches the engine parser without host Plan validation or +// lowering, so the shared grammar can include malformed declarations. The +// checker has no cipher, credentials or usable transport. +func (k *Checker) CheckFrameForTest(ctx context.Context, encoded []byte) error { + _, err := k.c.call(ctx, func(ctx context.Context, inst *instance) ([]byte, error) { + return inst.call(ctx, inst.planCheck, buf(encoded)) + }) + return err +} diff --git a/packages/stack-encrypt/tests/declarations.rs b/packages/stack-encrypt/tests/declarations.rs new file mode 100644 index 000000000..603ca3c2b --- /dev/null +++ b/packages/stack-encrypt/tests/declarations.rs @@ -0,0 +1,167 @@ +//! Executable acceptance and normalization contract for record declarations. +#![cfg(feature = "dynamic")] + +use std::collections::BTreeSet; +use std::path::PathBuf; + +use miette::Diagnostic; +use serde::Deserialize; +use serde_json::{json, Value}; +use stack_encrypt::dynamic::record::{self, NullMode, Output}; +use stack_encrypt::{ContextPiece, ErrorPayload}; + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct Corpus { + version: u32, + profile: String, + resolver: String, + cases: Vec, +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct Case { + id: String, + input: Input, + accepted: bool, + expected_error: Option, + boundary_error: Option, + normalized: Option, +} + +#[derive(Deserialize)] +#[serde(rename_all = "snake_case")] +enum Input { + Json(Value), + FrameHex(String), +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct ExpectedError { + code: String, + fields: serde_json::Map, +} + +fn unhex(text: &str) -> Vec { + assert!(text.is_ascii() && text.len().is_multiple_of(2)); + (0..text.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&text[i..i + 2], 16).unwrap()) + .collect() +} + +fn extension(piece: &ContextPiece<'_>) -> Value { + let (kind, text) = match piece { + ContextPiece::Text(text) => ("string", text.to_string()), + ContextPiece::Bytes(bytes) => ("bytes", bytes.iter().map(|b| format!("{b:02x}")).collect()), + ContextPiece::I32(n) => ("int32", n.to_string()), + ContextPiece::I64(n) => ("int64", n.to_string()), + ContextPiece::U32(n) => ("uint32", n.to_string()), + ContextPiece::U64(n) => ("uint64", n.to_string()), + other => panic!("unexpected extension {other:?}"), + }; + json!({"type": kind, "text": text}) +} + +// An observation of the parsed engine plan, not a second declaration parser. +// No sorting: field/output order is observable and must survive parsing. +fn normalized(plan: &record::Plan) -> Value { + let fields: Vec<_> = plan.fields().iter().map(|field| { + let outputs: Vec<_> = field.outputs().iter().map(|output| match output { + Output::Ciphertext => json!("c"), + Output::Passthrough => json!("passthrough"), + Output::Term(index) => serde_json::to_value(index).unwrap(), + }).collect(); + json!({ + "name": field.name(), + "label": field.label().segments().collect::>(), + "type": field.field_type().map(|kind| kind.name()), + "null": field.null().map(|mode| match mode { NullMode::Sql => "sql", NullMode::Sealed => "sealed" }), + "outputs": outputs, + }) + }).collect(); + json!({ + "label": plan.label().map(|label| label.segments().collect::>()), + "context_field": plan.context_field(), + "extension": plan.extension().iter().map(extension).collect::>(), + "fields": fields, + }) +} + +#[test] +fn shared_declaration_grammar() { + let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../../conformance/stack-encrypt/v1/declarations.json"); + let corpus: Corpus = serde_json::from_slice(&std::fs::read(path).unwrap()).unwrap(); + assert_eq!(corpus.version, 1); + assert_eq!(corpus.profile, "record-declarations-v1"); + assert_eq!(corpus.resolver, "none"); + assert!(!corpus.cases.is_empty()); + let mut ids = BTreeSet::new(); + let mut failures = Vec::new(); + for case in corpus.cases { + assert!( + !case.id.is_empty() && ids.insert(case.id.clone()), + "duplicate/empty ID" + ); + assert_eq!( + case.accepted, + case.expected_error.is_none(), + "{}: contradictory outcome", + case.id + ); + assert!( + case.accepted || case.normalized.is_none(), + "{}: rejected normalization", + case.id + ); + let frame = match case.input { + Input::Json(value) => vitaminc_aead_frame::serde::to_vec(&value).unwrap(), + Input::FrameHex(text) => unhex(&text), + }; + if let Some(boundary) = case.boundary_error { + assert!(!case.accepted); + assert!( + vitaminc_aead::Frame::parse(&frame).is_err(), + "{}: boundary refusal requires a malformed frame", + case.id + ); + assert_eq!(boundary.code, "stack_guest_abi::malformed_input"); + assert!(boundary.fields.is_empty()); + } + // No cipher/key source exists: declaration checking cannot do key I/O. + match (record::plan(&frame), case.expected_error) { + (Ok(plan), None) => { + if let Some(expected) = case.normalized { + let actual = normalized(&plan); + if actual != expected { + failures.push(format!( + "{}: normalized {actual}, expected {expected}", + case.id + )); + } + } + } + (Err(error), Some(expected)) => { + let code = error.code().unwrap().to_string(); + let fields = error.payload(); + if code != expected.code || fields != expected.fields { + failures.push(format!( + "{}: {code} {fields:?}, expected {} {:?}", + case.id, expected.code, expected.fields + )); + } + } + (actual, expected) => failures.push(format!( + "{}: accepted={}, expected accepted={}; error={:?}", + case.id, + actual.is_ok(), + expected.is_none(), + actual.err() + )), + } + } + assert!(failures.is_empty(), "{}", failures.join("\n")); +} diff --git a/scripts/__tests__/stack-conformance-ci.test.mjs b/scripts/__tests__/stack-conformance-ci.test.mjs index bb6f4da2a..b81e86d0a 100644 --- a/scripts/__tests__/stack-conformance-ci.test.mjs +++ b/scripts/__tests__/stack-conformance-ci.test.mjs @@ -1,7 +1,22 @@ +import { readFileSync } from 'node:fs' +import { join } from 'node:path' import { describe, expect, it } from 'vitest' +import { REPO_ROOT } from './lib/repo-root.mjs' import { readWorkflow } from './lib/workflows.mjs' describe('shared Stack Encrypt conformance is required in CI', () => { + it('the conformance task runs both profiles in both languages', () => { + const script = readFileSync( + join(REPO_ROOT, 'scripts/stack-conformance.sh'), + 'utf8', + ).replace(/^\s*#.*$/gm, '') + expect(script).toMatch(/cargo nextest run[^\n]*--test conformance/) + expect(script).toMatch(/cargo nextest run[^\n]*--test declarations/) + expect(script).toContain( + "go test -count=1 -v -run '^(TestStackConformance|TestDeclarationConformance)$' ./encrypt", + ) + }) + it('runs both consumers when the corpus changes', () => { for (const file of ['tests-crates.yml', 'tests-golang.yml']) { const workflow = readWorkflow(`.github/workflows/${file}`) diff --git a/scripts/stack-conformance.sh b/scripts/stack-conformance.sh index 502027af4..0b80b7d1d 100644 --- a/scripts/stack-conformance.sh +++ b/scripts/stack-conformance.sh @@ -18,11 +18,11 @@ fi artifacts=$(mktemp -d) trap 'rm -rf "$artifacts"' EXIT -mise x --env test -- cargo nextest run --locked -p stack-encrypt --all-features --test conformance +mise x --env test -- cargo nextest run --locked -p stack-encrypt --all-features --test conformance --test declarations ( cd languages/golang STACK_CONFORMANCE_OUTPUT="$artifacts/go.json" CGO_ENABLED=0 \ - go test -count=1 -v -run '^TestStackConformance$' ./encrypt + go test -count=1 -v -run '^(TestStackConformance|TestDeclarationConformance)$' ./encrypt ) test -s "$artifacts/go.json" STACK_CONFORMANCE_INPUT="$artifacts/go.json" mise x --env test -- \ From 8e0367f55c90a0782df407c8f62a13322128daa8 Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Sun, 1 Nov 2026 15:44:19 +1100 Subject: [PATCH 3/4] fix(conformance): align declaration runners Exercise release and deterministic guests through the host checker, preserving typed JSON numbers and sorted keys. Admit the supported ore_block output in host validation. Require fresh producer artifacts without repeating full CI suites. Refs #1460 --- .github/workflows/tests-golang.yml | 9 +- conformance/stack-encrypt/README.md | 2 +- .../stack-encrypt/declaration-grammar.md | 14 +- .../stack-encrypt/v1/declarations.json | 4 +- .../stack-encrypt/v1/go-declarations.json | 129 ++++++++++++++++++ .../2026-10-31-stack-encrypt-conformance.md | 8 +- languages/golang/encrypt/checker.go | 7 +- languages/golang/encrypt/conformance_test.go | 3 +- .../encrypt/declaration_conformance_test.go | 116 ++++++++++++---- languages/golang/encrypt/export_test.go | 18 ++- languages/golang/internal/record/record.go | 2 +- packages/stack-encrypt/Cargo.toml | 2 + packages/stack-encrypt/tests/common/mod.rs | 15 ++ packages/stack-encrypt/tests/conformance.rs | 16 +-- packages/stack-encrypt/tests/declarations.rs | 83 +++++++++-- scripts/__tests__/crates-ci.test.mjs | 4 + .../__tests__/stack-conformance-ci.test.mjs | 30 +++- scripts/go-binding-test.sh | 9 +- 18 files changed, 396 insertions(+), 75 deletions(-) create mode 100644 conformance/stack-encrypt/v1/go-declarations.json diff --git a/.github/workflows/tests-golang.yml b/.github/workflows/tests-golang.yml index d9a6cc36e..6cc483abf 100644 --- a/.github/workflows/tests-golang.yml +++ b/.github/workflows/tests-golang.yml @@ -250,13 +250,18 @@ jobs: - name: Go binding env: STACK_ENCRYPT_TESTS_REQUIRE_LOCK: "1" + STACK_CONFORMANCE_OUTPUT: ${{ runner.temp }}/stack-conformance-go.json run: | ulimit -l "$(ulimit -H -l)" echo "RLIMIT_MEMLOCK: $(ulimit -l) KiB" mise run go:test - - name: Shared Stack Encrypt conformance and fresh Go to Rust opening - run: mise run test:conformance + - name: Fresh Go scalar artifacts open in Rust + env: + STACK_CONFORMANCE_INPUT: ${{ runner.temp }}/stack-conformance-go.json + run: | + test -s "$STACK_CONFORMANCE_INPUT" + mise x --env test -- cargo nextest run --locked -p stack-encrypt --all-features --test conformance # The Rust suite opens the committed v4_rows.json and # v4_smallint_rows.json, whose ciphertexts the Go SDK sealed when each diff --git a/conformance/stack-encrypt/README.md b/conformance/stack-encrypt/README.md index 61eb84625..2932078b0 100644 --- a/conformance/stack-encrypt/README.md +++ b/conformance/stack-encrypt/README.md @@ -30,7 +30,7 @@ Python and TypeScript runners are pending; their absence is not a passing result The [declaration grammar](declaration-grammar.md) defines the engine's current record-plan syntax, defaults, context rules, kind/index table and null policies. -`v1/declarations.json` contains 201 cases, including malformed frames and all +`v1/declarations.json` contains cases, including malformed frames and all 21 kinds against five scalar indexes. The resolver is explicitly `none`; EQL targets need their own profile. diff --git a/conformance/stack-encrypt/declaration-grammar.md b/conformance/stack-encrypt/declaration-grammar.md index 518e47195..6fed321e3 100644 --- a/conformance/stack-encrypt/declaration-grammar.md +++ b/conformance/stack-encrypt/declaration-grammar.md @@ -243,7 +243,10 @@ The corpus is `v1/declarations.json`. Its version, profile and resolver are required. Case IDs are unique and stable. Every case contains exactly one input: - `{"json": value}` converts JSON objects in lexical key order. Nonnegative - integers become uint64, negative integers int64, and fractional values float64. + integer tokens become uint64; tokens beginning with `-` (including `-0`) + become int64. Decimal/exponent tokens become finite float64. Integer tokens + outside the corresponding 64-bit range and non-finite floats are invalid + corpus inputs; runners must refuse them without rounding. Arrays preserve order. This is fixture conversion, not SDK policy. - `{"frame_hex": text}` supplies exact transport bytes, including typed contexts, original field order and malformed frames that JSON cannot represent. @@ -265,7 +268,7 @@ no credentials and uses a refusing transport. ## Evidence and limits -The corpus covers 201 cases: 91 accepted and 110 refused, including the complete +The corpus covers accepted and refused cases, including the complete scalar kind/index matrix. The native runner also checks selected normalization observations. Existing codec tests own exhaustive frame validity, depth limits, and integer-reader behavior. Existing EQL tests own resolver/catalog behavior. @@ -274,3 +277,10 @@ Runtime operations and language-specific declarations remain separate layers. See [coverage.md](coverage.md) for the consolidation policy. New Python and TypeScript runners can adopt these inputs and expectations without copying the engine's validation rules. + +The Go runner checks both the release and deterministic guests. Go-specific +`v1/go-declarations.json` supplies explicit `record.Plan` inputs for the subset +representable by that host model, keyed by the shared case ID. Those cases also +run through production `Checker.Check` (Validate, Wire and the engine), with the +same acceptance outcome. Raw malformed frames and shapes the Go plan cannot +express remain engine-boundary checks. diff --git a/conformance/stack-encrypt/v1/declarations.json b/conformance/stack-encrypt/v1/declarations.json index a45223608..3ef80bb82 100644 --- a/conformance/stack-encrypt/v1/declarations.json +++ b/conformance/stack-encrypt/v1/declarations.json @@ -3273,6 +3273,8 @@ } ] } - } + }, + {"id": "json-negative-zero","input": {"json": {"value": {"context": [["users","value"],-0],"outputs": ["c"]}}},"accepted": true,"normalized": {"label": ["users"],"context_field": null,"extension": [{"type": "int64","text": "0"}],"fields": [{"name": "value","label": ["users","value"],"type": null,"null": null,"outputs": ["c"]}]}}, + {"id": "json-lexical-field-order","input": {"json": {"zeta": {"context": ["users","zeta"],"outputs": ["c"]},"alpha": {"context": ["users","alpha"],"outputs": ["c"]}}},"accepted": true,"normalized": {"label": ["users"],"context_field": null,"extension": [],"fields": [{"name": "alpha","label": ["users","alpha"],"type": null,"null": null,"outputs": ["c"]},{"name": "zeta","label": ["users","zeta"],"type": null,"null": null,"outputs": ["c"]}]}} ] } diff --git a/conformance/stack-encrypt/v1/go-declarations.json b/conformance/stack-encrypt/v1/go-declarations.json new file mode 100644 index 000000000..3d60084f0 --- /dev/null +++ b/conformance/stack-encrypt/v1/go-declarations.json @@ -0,0 +1,129 @@ +{ + "ciphertext-untyped": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "","Outputs": ["c"],"Null": ""}]}, + "index-only": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "int64","Outputs": ["eq"],"Null": ""}]}, + "outputs-retain-order": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "int64","Outputs": ["ore","c","eq"],"Null": ""}]}, + "match-default-bare": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "string","Outputs": ["match"],"Null": ""}]}, + "flat-label-is-not-extension": {"Context": ["users","region"],"Fields": [{"Name": "value","Identity": "value","Kind": "","Outputs": ["c"],"Null": ""}]}, + "sealed-null-equality": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "int32","Outputs": ["c","eq"],"Null": "sealed"}]}, + "sealed-null-bool": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "bool","Outputs": ["c"],"Null": "sealed"}]}, + "sql-null-untyped": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "","Outputs": ["c"],"Null": "sql"}]}, + "type-bool-eq": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "bool","Outputs": ["c","eq"],"Null": ""}]}, + "type-bool-match": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "bool","Outputs": ["c","match"],"Null": ""}]}, + "type-bool-ore": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "bool","Outputs": ["c","ore"],"Null": ""}]}, + "type-bool-ope": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "bool","Outputs": ["c","ope"],"Null": ""}]}, + "type-bool-ore_block": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "bool","Outputs": ["c","ore_block"],"Null": ""}]}, + "type-int8-eq": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "int8","Outputs": ["c","eq"],"Null": ""}]}, + "type-int8-match": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "int8","Outputs": ["c","match"],"Null": ""}]}, + "type-int8-ore": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "int8","Outputs": ["c","ore"],"Null": ""}]}, + "type-int8-ope": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "int8","Outputs": ["c","ope"],"Null": ""}]}, + "type-int8-ore_block": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "int8","Outputs": ["c","ore_block"],"Null": ""}]}, + "type-int16-eq": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "int16","Outputs": ["c","eq"],"Null": ""}]}, + "type-int16-match": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "int16","Outputs": ["c","match"],"Null": ""}]}, + "type-int16-ore": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "int16","Outputs": ["c","ore"],"Null": ""}]}, + "type-int16-ope": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "int16","Outputs": ["c","ope"],"Null": ""}]}, + "type-int16-ore_block": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "int16","Outputs": ["c","ore_block"],"Null": ""}]}, + "type-int32-eq": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "int32","Outputs": ["c","eq"],"Null": ""}]}, + "type-int32-match": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "int32","Outputs": ["c","match"],"Null": ""}]}, + "type-int32-ore": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "int32","Outputs": ["c","ore"],"Null": ""}]}, + "type-int32-ope": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "int32","Outputs": ["c","ope"],"Null": ""}]}, + "type-int32-ore_block": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "int32","Outputs": ["c","ore_block"],"Null": ""}]}, + "type-int64-eq": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "int64","Outputs": ["c","eq"],"Null": ""}]}, + "type-int64-match": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "int64","Outputs": ["c","match"],"Null": ""}]}, + "type-int64-ore": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "int64","Outputs": ["c","ore"],"Null": ""}]}, + "type-int64-ope": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "int64","Outputs": ["c","ope"],"Null": ""}]}, + "type-int64-ore_block": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "int64","Outputs": ["c","ore_block"],"Null": ""}]}, + "type-int128-eq": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "int128","Outputs": ["c","eq"],"Null": ""}]}, + "type-int128-match": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "int128","Outputs": ["c","match"],"Null": ""}]}, + "type-int128-ore": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "int128","Outputs": ["c","ore"],"Null": ""}]}, + "type-int128-ope": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "int128","Outputs": ["c","ope"],"Null": ""}]}, + "type-int128-ore_block": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "int128","Outputs": ["c","ore_block"],"Null": ""}]}, + "type-uint8-eq": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "uint8","Outputs": ["c","eq"],"Null": ""}]}, + "type-uint8-match": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "uint8","Outputs": ["c","match"],"Null": ""}]}, + "type-uint8-ore": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "uint8","Outputs": ["c","ore"],"Null": ""}]}, + "type-uint8-ope": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "uint8","Outputs": ["c","ope"],"Null": ""}]}, + "type-uint8-ore_block": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "uint8","Outputs": ["c","ore_block"],"Null": ""}]}, + "type-uint16-eq": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "uint16","Outputs": ["c","eq"],"Null": ""}]}, + "type-uint16-match": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "uint16","Outputs": ["c","match"],"Null": ""}]}, + "type-uint16-ore": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "uint16","Outputs": ["c","ore"],"Null": ""}]}, + "type-uint16-ope": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "uint16","Outputs": ["c","ope"],"Null": ""}]}, + "type-uint16-ore_block": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "uint16","Outputs": ["c","ore_block"],"Null": ""}]}, + "type-uint32-eq": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "uint32","Outputs": ["c","eq"],"Null": ""}]}, + "type-uint32-match": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "uint32","Outputs": ["c","match"],"Null": ""}]}, + "type-uint32-ore": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "uint32","Outputs": ["c","ore"],"Null": ""}]}, + "type-uint32-ope": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "uint32","Outputs": ["c","ope"],"Null": ""}]}, + "type-uint32-ore_block": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "uint32","Outputs": ["c","ore_block"],"Null": ""}]}, + "type-uint64-eq": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "uint64","Outputs": ["c","eq"],"Null": ""}]}, + "type-uint64-match": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "uint64","Outputs": ["c","match"],"Null": ""}]}, + "type-uint64-ore": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "uint64","Outputs": ["c","ore"],"Null": ""}]}, + "type-uint64-ope": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "uint64","Outputs": ["c","ope"],"Null": ""}]}, + "type-uint64-ore_block": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "uint64","Outputs": ["c","ore_block"],"Null": ""}]}, + "type-uint128-eq": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "uint128","Outputs": ["c","eq"],"Null": ""}]}, + "type-uint128-match": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "uint128","Outputs": ["c","match"],"Null": ""}]}, + "type-uint128-ore": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "uint128","Outputs": ["c","ore"],"Null": ""}]}, + "type-uint128-ope": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "uint128","Outputs": ["c","ope"],"Null": ""}]}, + "type-uint128-ore_block": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "uint128","Outputs": ["c","ore_block"],"Null": ""}]}, + "type-float32-eq": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "float32","Outputs": ["c","eq"],"Null": ""}]}, + "type-float32-match": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "float32","Outputs": ["c","match"],"Null": ""}]}, + "type-float32-ore": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "float32","Outputs": ["c","ore"],"Null": ""}]}, + "type-float32-ope": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "float32","Outputs": ["c","ope"],"Null": ""}]}, + "type-float32-ore_block": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "float32","Outputs": ["c","ore_block"],"Null": ""}]}, + "type-float64-eq": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "float64","Outputs": ["c","eq"],"Null": ""}]}, + "type-float64-match": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "float64","Outputs": ["c","match"],"Null": ""}]}, + "type-float64-ore": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "float64","Outputs": ["c","ore"],"Null": ""}]}, + "type-float64-ope": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "float64","Outputs": ["c","ope"],"Null": ""}]}, + "type-float64-ore_block": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "float64","Outputs": ["c","ore_block"],"Null": ""}]}, + "type-decimal-eq": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "decimal","Outputs": ["c","eq"],"Null": ""}]}, + "type-decimal-match": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "decimal","Outputs": ["c","match"],"Null": ""}]}, + "type-decimal-ore": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "decimal","Outputs": ["c","ore"],"Null": ""}]}, + "type-decimal-ope": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "decimal","Outputs": ["c","ope"],"Null": ""}]}, + "type-decimal-ore_block": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "decimal","Outputs": ["c","ore_block"],"Null": ""}]}, + "type-date-eq": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "date","Outputs": ["c","eq"],"Null": ""}]}, + "type-date-match": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "date","Outputs": ["c","match"],"Null": ""}]}, + "type-date-ore": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "date","Outputs": ["c","ore"],"Null": ""}]}, + "type-date-ope": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "date","Outputs": ["c","ope"],"Null": ""}]}, + "type-date-ore_block": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "date","Outputs": ["c","ore_block"],"Null": ""}]}, + "type-timestamp-eq": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "timestamp","Outputs": ["c","eq"],"Null": ""}]}, + "type-timestamp-match": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "timestamp","Outputs": ["c","match"],"Null": ""}]}, + "type-timestamp-ore": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "timestamp","Outputs": ["c","ore"],"Null": ""}]}, + "type-timestamp-ope": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "timestamp","Outputs": ["c","ope"],"Null": ""}]}, + "type-timestamp-ore_block": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "timestamp","Outputs": ["c","ore_block"],"Null": ""}]}, + "type-string-eq": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "string","Outputs": ["c","eq"],"Null": ""}]}, + "type-string-match": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "string","Outputs": ["c","match"],"Null": ""}]}, + "type-string-ore": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "string","Outputs": ["c","ore"],"Null": ""}]}, + "type-string-ope": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "string","Outputs": ["c","ope"],"Null": ""}]}, + "type-string-ore_block": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "string","Outputs": ["c","ore_block"],"Null": ""}]}, + "type-bytes-eq": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "bytes","Outputs": ["c","eq"],"Null": ""}]}, + "type-bytes-match": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "bytes","Outputs": ["c","match"],"Null": ""}]}, + "type-bytes-ore": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "bytes","Outputs": ["c","ore"],"Null": ""}]}, + "type-bytes-ope": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "bytes","Outputs": ["c","ope"],"Null": ""}]}, + "type-bytes-ore_block": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "bytes","Outputs": ["c","ore_block"],"Null": ""}]}, + "type-array-eq": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "array","Outputs": ["c","eq"],"Null": ""}]}, + "type-array-match": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "array","Outputs": ["c","match"],"Null": ""}]}, + "type-array-ore": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "array","Outputs": ["c","ore"],"Null": ""}]}, + "type-array-ope": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "array","Outputs": ["c","ope"],"Null": ""}]}, + "type-array-ore_block": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "array","Outputs": ["c","ore_block"],"Null": ""}]}, + "type-object-eq": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "object","Outputs": ["c","eq"],"Null": ""}]}, + "type-object-match": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "object","Outputs": ["c","match"],"Null": ""}]}, + "type-object-ore": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "object","Outputs": ["c","ore"],"Null": ""}]}, + "type-object-ope": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "object","Outputs": ["c","ope"],"Null": ""}]}, + "type-object-ore_block": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "object","Outputs": ["c","ore_block"],"Null": ""}]}, + "type-json-eq": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "json","Outputs": ["c","eq"],"Null": ""}]}, + "type-json-match": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "json","Outputs": ["c","match"],"Null": ""}]}, + "type-json-ore": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "json","Outputs": ["c","ore"],"Null": ""}]}, + "type-json-ope": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "json","Outputs": ["c","ope"],"Null": ""}]}, + "type-json-ore_block": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "json","Outputs": ["c","ore_block"],"Null": ""}]}, + "outputs-empty": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "","Outputs": [],"Null": ""}]}, + "output-unknown": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "","Outputs": ["wat"],"Null": ""}]}, + "output-duplicate": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "","Outputs": ["c","c"],"Null": ""}]}, + "type-unknown": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "uuid","Outputs": ["c"],"Null": ""}]}, + "type-json-without-target": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "json","Outputs": ["c"],"Null": ""}]}, + "json-output-pending": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "json","Outputs": ["json"],"Null": ""}]}, + "context-segment-slash": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "a/b","Kind": "","Outputs": ["c"],"Null": ""}]}, + "context-segment-digit": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "9","Kind": "","Outputs": ["c"],"Null": ""}]}, + "shared-identity": {"Context": ["users"],"Fields": [{"Name": "a","Identity": "same","Kind": "","Outputs": ["c"],"Null": ""},{"Name": "b","Identity": "same","Kind": "","Outputs": ["c"],"Null": ""}]}, + "null-mode-unknown": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "","Outputs": ["c"],"Null": "optional"}]}, + "sealed-null-untyped": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "","Outputs": ["c"],"Null": "sealed"}]}, + "sealed-null-text": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "string","Outputs": ["c"],"Null": "sealed"}]}, + "sealed-null-order": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "int64","Outputs": ["c","ore"],"Null": "sealed"}]}, + "untyped-index": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "","Outputs": ["eq"],"Null": ""}]} +} diff --git a/docs/plans/2026-10-31-stack-encrypt-conformance.md b/docs/plans/2026-10-31-stack-encrypt-conformance.md index e17849a79..9cbc4c863 100644 --- a/docs/plans/2026-10-31-stack-encrypt-conformance.md +++ b/docs/plans/2026-10-31-stack-encrypt-conformance.md @@ -77,7 +77,7 @@ The coverage map there identifies existing evidence and remaining gaps. Implemented on `feat/stack-encrypt-conformance`, based directly on #1404. The corpus has eight positive cases and three query refusals. Local validation passes for native Rust, Go WASI, and fresh Go-to-Rust artifact exchange through -`mise run test:conformance`. Rust clippy, the enabled Go linters, and 33 CI wiring +`mise run test:conformance`. Rust clippy, the enabled Go linters, and the CI wiring checks also pass. The work is in draft [PR #1460](https://github.com/cipherstash/stack/pull/1460), targeting #1404's branch. Local validation is distinct from hosted CI status. @@ -91,7 +91,7 @@ SQL requirements and Python/TypeScript runners remain open. The [declaration grammar](../../conformance/stack-encrypt/declaration-grammar.md) records the implemented syntax and semantic rules, including accepted enum aliases, defaults, context composition, all 21 kinds against five scalar indexes, -null policies and target-capability refusal. Its 201 cases run in native Rust +null policies and target-capability refusal. Its cases run in native Rust and Go WASI without a cipher. Native Rust checks selected normalized plan observations; Go's existing checker exposes acceptance and diagnostics only. @@ -99,8 +99,8 @@ The profile fixes `NoTargets`. EQL resolver/catalog behavior and full JSON/SQL operation semantics remain later profiles. Malformed frames explicitly distinguish the native parser's diagnostic from the WASI boundary's earlier refusal. -Local validation passes for both profiles (212 shared cases), fresh ciphertext -exchange, Rust clippy/formatting, Go lint, Biome and 34 CI wiring checks. +Local validation passes for both profiles, fresh ciphertext +exchange, Rust clippy/formatting, Go lint, Biome and the CI wiring checks. The [coverage map](../../conformance/stack-encrypt/coverage.md#consolidation-policy) records consolidation candidates. Shared case tables can replace duplicated diff --git a/languages/golang/encrypt/checker.go b/languages/golang/encrypt/checker.go index 430f7e752..22c16d96c 100644 --- a/languages/golang/encrypt/checker.go +++ b/languages/golang/encrypt/checker.go @@ -57,7 +57,12 @@ func (k *Checker) engineCheck(ctx context.Context, plan *record.Plan) error { if err != nil { return fmt.Errorf("%w: %v", ErrEncoding, err) } - _, err = k.c.call(ctx, func(ctx context.Context, inst *instance) ([]byte, error) { + return k.checkFrame(ctx, encoded) +} + +// checkFrame is the common engine boundary for lowered plans and raw-frame tests. +func (k *Checker) checkFrame(ctx context.Context, encoded []byte) error { + _, err := k.c.call(ctx, func(ctx context.Context, inst *instance) ([]byte, error) { return inst.call(ctx, inst.planCheck, buf(encoded)) }) return err diff --git a/languages/golang/encrypt/conformance_test.go b/languages/golang/encrypt/conformance_test.go index ea74d6f3d..a0b465c18 100644 --- a/languages/golang/encrypt/conformance_test.go +++ b/languages/golang/encrypt/conformance_test.go @@ -66,6 +66,7 @@ func readConformanceJSON(t *testing.T, path string, out any) { } d := json.NewDecoder(bytes.NewReader(data)) d.DisallowUnknownFields() + d.UseNumber() if err := d.Decode(out); err != nil { t.Fatal(err) } @@ -222,7 +223,7 @@ func TestStackConformance(t *testing.T) { if _, err := cipher.Open(ctx, &wrong, frozen); !errors.Is(err, encrypt.ErrForbidden) { t.Fatalf("wrong context: expected forbidden key retrieval, got %v", err) } - output.Cases[c.ID] = conformanceArtifact{Frame: want.Frame, Ciphertext: hex.EncodeToString(fresh[0][corpus.Field].Ciphertext), Equality: hex.EncodeToString(query.Terms[0])} + output.Cases[c.ID] = conformanceArtifact{Frame: hex.EncodeToString(encoded), Ciphertext: hex.EncodeToString(fresh[0][corpus.Field].Ciphertext), Equality: hex.EncodeToString(query.Terms[0])} }) } if t.Failed() { diff --git a/languages/golang/encrypt/declaration_conformance_test.go b/languages/golang/encrypt/declaration_conformance_test.go index 4e1dafc15..35e19e28b 100644 --- a/languages/golang/encrypt/declaration_conformance_test.go +++ b/languages/golang/encrypt/declaration_conformance_test.go @@ -8,13 +8,13 @@ import ( "errors" "os" "path/filepath" - "reflect" "sort" "strconv" "strings" "testing" "github.com/cipherstash/stack/languages/golang/encrypt" + "github.com/cipherstash/stack/languages/golang/internal/record" "github.com/cipherstash/vitaminc/bindings/go/vcffi" "github.com/cipherstash/vitaminc/bindings/go/vcvalue" ) @@ -61,17 +61,7 @@ func declarationValue(t *testing.T, value any) any { } return v case json.Number: - text := v.String() - var value any - var err error - switch { - case strings.ContainsAny(text, ".eE"): - value, err = strconv.ParseFloat(text, 64) - case strings.HasPrefix(text, "-"): - value, err = strconv.ParseInt(text, 10, 64) - default: - value, err = strconv.ParseUint(text, 10, 64) - } + value, err := declarationNumber(v.String()) if err != nil { t.Fatal(err) } @@ -92,21 +82,44 @@ func TestDeclarationConformance(t *testing.T) { if corpus.Version != 1 || corpus.Profile != "record-declarations-v1" || corpus.Resolver != "none" || len(corpus.Cases) == 0 { t.Fatal("unsupported or empty declaration corpus") } - wasm, err := os.ReadFile("testdata/stack_encrypt_guest_deterministic.wasm") - if errors.Is(err, os.ErrNotExist) && os.Getenv("STACK_CONFORMANCE_REQUIRED") != "1" { - t.Skip(err) - } - if err != nil { - t.Fatal(err) + var hostPlans map[string]record.Plan + readConformanceJSON(t, filepath.Join("..", "..", "..", "conformance", "stack-encrypt", "v1", "go-declarations.json"), &hostPlans) + for name, load := range map[string]func() ([]byte, error){"release": encrypt.PlainGuest, "deterministic": encrypt.DeterministicGuest} { + t.Run(name, func(t *testing.T) { + wasm, err := load() + if (errors.Is(err, encrypt.ErrGuestNotBuilt) || errors.Is(err, encrypt.ErrDeterministicGuestNotBuilt)) && os.Getenv("STACK_CONFORMANCE_REQUIRED") != "1" { + t.Skip(err) + } + if err != nil { + t.Fatal(err) + } + checker, err := encrypt.NewCheckerOver(context.Background(), wasm) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = checker.Close() }) + checkDeclarationCases(t, checker, corpus.Cases, hostPlans) + }) } +} + +func checkDeclarationCases(t *testing.T, checker *encrypt.Checker, cases []declarationCase, hostPlans map[string]record.Plan) { + t.Helper() ctx := context.Background() - checker, err := encrypt.NewCheckerOver(ctx, wasm) - if err != nil { - t.Fatal(err) + for id := range hostPlans { + found := false + for _, c := range cases { + if c.ID == id { + found = true + break + } + } + if !found { + t.Fatalf("host plan has no shared case: %s", id) + } } - t.Cleanup(func() { _ = checker.Close() }) seen := map[string]bool{} - for _, c := range corpus.Cases { + for _, c := range cases { if c.ID == "" || seen[c.ID] { t.Fatalf("empty or duplicate case ID %q", c.ID) } @@ -138,6 +151,15 @@ func TestDeclarationConformance(t *testing.T) { if err != nil { t.Fatal(err) } + if plan, ok := hostPlans[c.ID]; ok { + hostErr := checker.Check(ctx, &plan) + if (hostErr == nil) != accepted { + t.Fatalf("host Check: %v; accepted=%v", hostErr, accepted) + } + if hostErr != nil && !errors.Is(hostErr, encrypt.ErrEncoding) { + t.Fatalf("host Check: expected ErrEncoding, got %v", hostErr) + } + } err = checker.CheckFrameForTest(ctx, frame) expected := c.ExpectedError if c.BoundaryError != nil { @@ -159,9 +181,55 @@ func TestDeclarationConformance(t *testing.T) { if !errors.Is(err, encrypt.ErrEncoding) || !errors.As(err, &diagnostic) { t.Fatalf("expected encoding diagnostic, got %v", err) } - if diagnostic.Code != expected.Code || !reflect.DeepEqual(diagnostic.Fields, expected.Fields) { + if diagnostic.Code != expected.Code || !sameDiagnosticFields(t, diagnostic.Fields, expected.Fields) { t.Fatalf("got %s %#v; expected %s %#v", diagnostic.Code, diagnostic.Fields, expected.Code, expected.Fields) } }) } } + +// Compare JSON payload values without float64 rounding or int64/uint64 type +// accidents. UseNumber in readConformanceJSON preserves expected integers. +func sameDiagnosticFields(t *testing.T, actual, expected map[string]any) bool { + t.Helper() + a, err := json.Marshal(actual) + if err != nil { + t.Fatal(err) + } + b, err := json.Marshal(expected) + if err != nil { + t.Fatal(err) + } + return bytes.Equal(a, b) +} + +func TestDiagnosticNumericFields(t *testing.T) { + if !sameDiagnosticFields(t, map[string]any{"n": uint64(18446744073709551615), "m": int64(-1)}, map[string]any{"n": json.Number("18446744073709551615"), "m": json.Number("-1")}) { + t.Fatal("integer payload changed") + } + if sameDiagnosticFields(t, map[string]any{"n": uint64(18446744073709551615)}, map[string]any{"n": json.Number("18446744073709551614")}) { + t.Fatal("integer precision lost") + } +} + +func declarationNumber(text string) (any, error) { + switch { + case strings.ContainsAny(text, ".eE"): + return strconv.ParseFloat(text, 64) + case strings.HasPrefix(text, "-"): + return strconv.ParseInt(text, 10, 64) + default: + return strconv.ParseUint(text, 10, 64) + } +} +func TestDeclarationNumberProjection(t *testing.T) { + zero, err := declarationNumber("-0") + if err != nil || zero != int64(0) { + t.Fatalf("-0: %v %v", zero, err) + } + for _, invalid := range []string{"18446744073709551616", "-9223372036854775809", "1e400"} { + if _, err := declarationNumber(invalid); err == nil { + t.Fatalf("accepted %s", invalid) + } + } +} diff --git a/languages/golang/encrypt/export_test.go b/languages/golang/encrypt/export_test.go index b6ffdc710..47778d388 100644 --- a/languages/golang/encrypt/export_test.go +++ b/languages/golang/encrypt/export_test.go @@ -81,9 +81,9 @@ var ErrDeterministicGuestNotBuilt = errors.New("encrypt: deterministic guest not // name the build they run against (NewDeterministicEQLClient is the other), // so each build keeps its own coverage. func NewDeterministicClient(ctx context.Context, seed [32]byte) (*Client, error) { - wasm, err := os.ReadFile(deterministicGuestPath) + wasm, err := DeterministicGuest() if err != nil { - return nil, ErrDeterministicGuestNotBuilt + return nil, err } return deterministicClient(ctx, wasm, seed) } @@ -188,8 +188,14 @@ var ( // lowering, so the shared grammar can include malformed declarations. The // checker has no cipher, credentials or usable transport. func (k *Checker) CheckFrameForTest(ctx context.Context, encoded []byte) error { - _, err := k.c.call(ctx, func(ctx context.Context, inst *instance) ([]byte, error) { - return inst.call(ctx, inst.planCheck, buf(encoded)) - }) - return err + return k.checkFrame(ctx, encoded) +} + +// DeterministicGuest reads the named test build without embedding it. +func DeterministicGuest() ([]byte, error) { + wasm, err := os.ReadFile(deterministicGuestPath) + if errors.Is(err, os.ErrNotExist) { + return nil, ErrDeterministicGuestNotBuilt + } + return wasm, err } diff --git a/languages/golang/internal/record/record.go b/languages/golang/internal/record/record.go index 06a570453..768455df5 100644 --- a/languages/golang/internal/record/record.go +++ b/languages/golang/internal/record/record.go @@ -412,7 +412,7 @@ func (p *Plan) Validate() error { seen := map[Output]bool{} for _, o := range f.Outputs { switch o { - case Ciphertext, Equality, Match, Ore, Ope: + case Ciphertext, Equality, Match, Ore, Ope, OreBlock: default: return fmt.Errorf("record: field %q: unknown output %q", f.Name, o) } diff --git a/packages/stack-encrypt/Cargo.toml b/packages/stack-encrypt/Cargo.toml index 31ddbbf93..27e75f80b 100644 --- a/packages/stack-encrypt/Cargo.toml +++ b/packages/stack-encrypt/Cargo.toml @@ -122,6 +122,8 @@ rust_decimal = ["dep:rust_decimal", "vitaminc-aead/rust_decimal"] test-support = ["stack-kms/test-support"] [dev-dependencies] +# Preserve JSON number spellings for the shared declaration projection. +serde_json = { workspace = true, features = ["raw_value"] } # `default-features = false` here too: dev-dependency features unify into the # `cargo test -p stack-encrypt --no-default-features` graph, so leaving the # default on would silently pull `stack-kms/http` -> `stack-auth/http` -> diff --git a/packages/stack-encrypt/tests/common/mod.rs b/packages/stack-encrypt/tests/common/mod.rs index 660b5d36b..812d50f27 100644 --- a/packages/stack-encrypt/tests/common/mod.rs +++ b/packages/stack-encrypt/tests/common/mod.rs @@ -292,3 +292,18 @@ pub mod frames { } } } + +/// Canonical hex helpers for shared conformance artifacts. +pub fn hex(bytes: &[u8]) -> String { + bytes.iter().map(|b| format!("{b:02x}")).collect() +} +pub fn unhex(text: &str) -> Vec { + assert!( + text.is_ascii() && text.len().is_multiple_of(2), + "invalid hex" + ); + (0..text.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&text[i..i + 2], 16).expect("hex byte")) + .collect() +} diff --git a/packages/stack-encrypt/tests/conformance.rs b/packages/stack-encrypt/tests/conformance.rs index e7e9b1d5a..c32cef931 100644 --- a/packages/stack-encrypt/tests/conformance.rs +++ b/packages/stack-encrypt/tests/conformance.rs @@ -8,6 +8,7 @@ use std::path::PathBuf; use std::sync::atomic::Ordering; use common::frames::{frame, object}; +use common::{hex, unhex}; use miette::Diagnostic; use serde::{Deserialize, Serialize}; use serde_json::json; @@ -78,21 +79,6 @@ fn root() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../conformance/stack-encrypt/v1") } -fn hex(bytes: &[u8]) -> String { - bytes.iter().map(|b| format!("{b:02x}")).collect() -} - -fn unhex(text: &str) -> Vec { - assert!( - text.is_ascii() && text.len().is_multiple_of(2), - "invalid hex" - ); - (0..text.len()) - .step_by(2) - .map(|i| u8::from_str_radix(&text[i..i + 2], 16).expect("hex byte")) - .collect() -} - fn value(input: &Input) -> ValueFrame { match input.kind.as_str() { "int32" => frame(input.text.parse::().expect("i32")), diff --git a/packages/stack-encrypt/tests/declarations.rs b/packages/stack-encrypt/tests/declarations.rs index 603ca3c2b..a5e9158ee 100644 --- a/packages/stack-encrypt/tests/declarations.rs +++ b/packages/stack-encrypt/tests/declarations.rs @@ -1,11 +1,14 @@ //! Executable acceptance and normalization contract for record declarations. #![cfg(feature = "dynamic")] -use std::collections::BTreeSet; +mod common; +use common::{hex, unhex}; +use std::collections::{BTreeMap, BTreeSet}; use std::path::PathBuf; use miette::Diagnostic; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; +use serde_json::value::RawValue; use serde_json::{json, Value}; use stack_encrypt::dynamic::record::{self, NullMode, Output}; use stack_encrypt::{ContextPiece, ErrorPayload}; @@ -33,7 +36,7 @@ struct Case { #[derive(Deserialize)] #[serde(rename_all = "snake_case")] enum Input { - Json(Value), + Json(Box), FrameHex(String), } @@ -44,18 +47,10 @@ struct ExpectedError { fields: serde_json::Map, } -fn unhex(text: &str) -> Vec { - assert!(text.is_ascii() && text.len().is_multiple_of(2)); - (0..text.len()) - .step_by(2) - .map(|i| u8::from_str_radix(&text[i..i + 2], 16).unwrap()) - .collect() -} - fn extension(piece: &ContextPiece<'_>) -> Value { let (kind, text) = match piece { ContextPiece::Text(text) => ("string", text.to_string()), - ContextPiece::Bytes(bytes) => ("bytes", bytes.iter().map(|b| format!("{b:02x}")).collect()), + ContextPiece::Bytes(bytes) => ("bytes", hex(bytes)), ContextPiece::I32(n) => ("int32", n.to_string()), ContextPiece::I64(n) => ("int64", n.to_string()), ContextPiece::U32(n) => ("uint32", n.to_string()), @@ -118,7 +113,9 @@ fn shared_declaration_grammar() { case.id ); let frame = match case.input { - Input::Json(value) => vitaminc_aead_frame::serde::to_vec(&value).unwrap(), + Input::Json(value) => { + vitaminc_aead_frame::serde::to_vec(&projection(&value).unwrap()).unwrap() + } Input::FrameHex(text) => unhex(&text), }; if let Some(boundary) = case.boundary_error { @@ -165,3 +162,63 @@ fn shared_declaration_grammar() { } assert!(failures.is_empty(), "{}", failures.join("\n")); } + +// Explicit lexical JSON projection, independent of serde_json's Number and +// preserve_order features. -0 is an int64 zero; out-of-range integers are +// invalid corpus inputs, not silently rounded floats. +#[derive(Serialize)] +#[serde(untagged)] +enum Projection { + Null, + Bool(bool), + Signed(i64), + Unsigned(u64), + Float(f64), + Text(String), + List(Vec), + Object(BTreeMap), +} +fn projection(raw: &RawValue) -> Result> { + let text = raw.get().trim(); + Ok(match text.as_bytes()[0] { + b'{' => Projection::Object( + serde_json::from_str::>>(text)? + .into_iter() + .map(|(k, v)| Ok((k, projection(&v)?))) + .collect::>>()?, + ), + b'[' => Projection::List( + serde_json::from_str::>>(text)? + .iter() + .map(|v| projection(v)) + .collect::>()?, + ), + b'"' => Projection::Text(serde_json::from_str(text)?), + b'n' => Projection::Null, + b't' | b'f' => Projection::Bool(serde_json::from_str(text)?), + _ if text.contains(['.', 'e', 'E']) => { + let value: f64 = text.parse()?; + if !value.is_finite() { + return Err("non-finite JSON number".into()); + } + Projection::Float(value) + } + b'-' => Projection::Signed(text.parse()?), + _ => Projection::Unsigned(text.parse()?), + }) +} +#[test] +fn json_projection_is_lexical_and_lossless() { + fn encoded(text: &str) -> Vec { + let raw = serde_json::from_str::>(text).unwrap(); + vitaminc_aead_frame::serde::to_vec(&projection(&raw).unwrap()).unwrap() + } + assert_eq!( + encoded("-0"), + vitaminc_aead_frame::serde::to_vec(&0i64).unwrap() + ); + assert_eq!(encoded(r#"{"z":1,"a":2}"#), encoded(r#"{"a":2,"z":1}"#)); + for invalid in ["18446744073709551616", "-9223372036854775809", "1e400"] { + assert!(projection(&serde_json::from_str::>(invalid).unwrap()).is_err()); + } +} diff --git a/scripts/__tests__/crates-ci.test.mjs b/scripts/__tests__/crates-ci.test.mjs index 8b3a64bb3..4d33fe72d 100644 --- a/scripts/__tests__/crates-ci.test.mjs +++ b/scripts/__tests__/crates-ci.test.mjs @@ -199,6 +199,10 @@ function reachableFrom(workflows) { * is nothing for CI to do. */ const CI_EXEMPT_TASKS = new Map([ + [ + 'test:conformance', + 'Local aggregate: CI runs Rust profiles in tests-crates, Go profiles in go:test, and the fresh artifact consumer in tests-golang; rerunning the aggregate would duplicate both suites.', + ], ...[ 'stack-auth', 'stack-encrypt', diff --git a/scripts/__tests__/stack-conformance-ci.test.mjs b/scripts/__tests__/stack-conformance-ci.test.mjs index b81e86d0a..b85548e10 100644 --- a/scripts/__tests__/stack-conformance-ci.test.mjs +++ b/scripts/__tests__/stack-conformance-ci.test.mjs @@ -10,6 +10,13 @@ describe('shared Stack Encrypt conformance is required in CI', () => { join(REPO_ROOT, 'scripts/stack-conformance.sh'), 'utf8', ).replace(/^\s*#.*$/gm, '') + const commands = script.replace(/\\\r?\n\s*/g, ' ') + expect(commands).toMatch( + /STACK_CONFORMANCE_OUTPUT=[^\n]+go test[^\n]+TestStackConformance/, + ) + expect(commands).toMatch( + /STACK_CONFORMANCE_INPUT=[^\n]+cargo nextest run[^\n]+--test conformance/, + ) expect(script).toMatch(/cargo nextest run[^\n]*--test conformance/) expect(script).toMatch(/cargo nextest run[^\n]*--test declarations/) expect(script).toContain( @@ -39,10 +46,29 @@ describe('shared Stack Encrypt conformance is required in CI', () => { (step) => step.run === 'mise run wasm:guest:build:deterministic', ) const exchange = steps.findIndex( - (step) => step.run === 'mise run test:conformance', + (step) => step.env?.STACK_CONFORMANCE_INPUT, ) expect(build).toBeGreaterThanOrEqual(0) - expect(exchange).toBeGreaterThan(build) + const producer = steps.findIndex( + (step) => step.env?.STACK_CONFORMANCE_OUTPUT, + ) + expect(producer).toBeGreaterThan(build) + expect(steps[producer].run).toContain('mise run go:test') + expect(steps[producer].env.STACK_CONFORMANCE_OUTPUT).toBe( + steps[exchange].env.STACK_CONFORMANCE_INPUT, + ) + expect(steps[exchange].run).toContain('test -s "$STACK_CONFORMANCE_INPUT"') + expect(steps[exchange].run).toMatch( + /cargo nextest run[^\n]*--test conformance/, + ) + expect(steps[exchange].run).not.toContain('test:conformance') + expect(exchange).toBeGreaterThan(producer) + const goTests = readFileSync( + join(REPO_ROOT, 'scripts/go-binding-test.sh'), + 'utf8', + ) + expect(goTests).toContain('conformance_flags=(-count=1)') + expect(goTests).toMatch(/go test "\$\{conformance_flags\[@\]\}" \.\/\.\.\./) expect(steps[exchange].if).toBeUndefined() expect(steps[exchange]['continue-on-error']).toBeUndefined() }) diff --git a/scripts/go-binding-test.sh b/scripts/go-binding-test.sh index c5b60101b..460a112dc 100755 --- a/scripts/go-binding-test.sh +++ b/scripts/go-binding-test.sh @@ -40,14 +40,19 @@ go vet ./... # Windows. These tests exercise the platform lock implementation with two # independent guest instances sharing one auth.json. CGO_ENABLED=0 go test -v ./auth -run '^TestDeviceRefresh' -CGO_ENABLED=0 go test ./... +# A producer must execute even if Go has cached a previous successful run. +conformance_flags=() +if [[ -n "${STACK_CONFORMANCE_OUTPUT:-}" ]]; then + conformance_flags=(-count=1) +fi +CGO_ENABLED=0 go test "${conformance_flags[@]}" ./... # The transport codec's u32-bound guards are load-bearing where int is 32 # bits (vitaminc's vcffi runs this sweep too); the binding's own reflection # and buffer arithmetic must hold there as well. Linux hosts execute 386 # natively; elsewhere the binary cannot run, so only vet. if [ "$(uname -s)" = "Linux" ]; then - CGO_ENABLED=0 GOOS=linux GOARCH=386 go test ./... + CGO_ENABLED=0 GOOS=linux GOARCH=386 go test "${conformance_flags[@]}" ./... else CGO_ENABLED=0 GOOS=linux GOARCH=386 go vet ./... fi From d6c28ace8de4ad21a0e47cc76d5a76bfe349f000 Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Sun, 1 Nov 2026 16:02:58 +1100 Subject: [PATCH 4/4] style(conformance): format declaration fixtures --- .../stack-encrypt/v1/declarations.json | 56 +- .../stack-encrypt/v1/go-declarations.json | 1658 +++++++++++++++-- 2 files changed, 1585 insertions(+), 129 deletions(-) diff --git a/conformance/stack-encrypt/v1/declarations.json b/conformance/stack-encrypt/v1/declarations.json index 3ef80bb82..c9bb1627a 100644 --- a/conformance/stack-encrypt/v1/declarations.json +++ b/conformance/stack-encrypt/v1/declarations.json @@ -3274,7 +3274,59 @@ ] } }, - {"id": "json-negative-zero","input": {"json": {"value": {"context": [["users","value"],-0],"outputs": ["c"]}}},"accepted": true,"normalized": {"label": ["users"],"context_field": null,"extension": [{"type": "int64","text": "0"}],"fields": [{"name": "value","label": ["users","value"],"type": null,"null": null,"outputs": ["c"]}]}}, - {"id": "json-lexical-field-order","input": {"json": {"zeta": {"context": ["users","zeta"],"outputs": ["c"]},"alpha": {"context": ["users","alpha"],"outputs": ["c"]}}},"accepted": true,"normalized": {"label": ["users"],"context_field": null,"extension": [],"fields": [{"name": "alpha","label": ["users","alpha"],"type": null,"null": null,"outputs": ["c"]},{"name": "zeta","label": ["users","zeta"],"type": null,"null": null,"outputs": ["c"]}]}} + { + "id": "json-negative-zero", + "input": { + "json": { + "value": { "context": [["users", "value"], -0], "outputs": ["c"] } + } + }, + "accepted": true, + "normalized": { + "label": ["users"], + "context_field": null, + "extension": [{ "type": "int64", "text": "0" }], + "fields": [ + { + "name": "value", + "label": ["users", "value"], + "type": null, + "null": null, + "outputs": ["c"] + } + ] + } + }, + { + "id": "json-lexical-field-order", + "input": { + "json": { + "zeta": { "context": ["users", "zeta"], "outputs": ["c"] }, + "alpha": { "context": ["users", "alpha"], "outputs": ["c"] } + } + }, + "accepted": true, + "normalized": { + "label": ["users"], + "context_field": null, + "extension": [], + "fields": [ + { + "name": "alpha", + "label": ["users", "alpha"], + "type": null, + "null": null, + "outputs": ["c"] + }, + { + "name": "zeta", + "label": ["users", "zeta"], + "type": null, + "null": null, + "outputs": ["c"] + } + ] + } + } ] } diff --git a/conformance/stack-encrypt/v1/go-declarations.json b/conformance/stack-encrypt/v1/go-declarations.json index 3d60084f0..85fd2eb11 100644 --- a/conformance/stack-encrypt/v1/go-declarations.json +++ b/conformance/stack-encrypt/v1/go-declarations.json @@ -1,129 +1,1533 @@ { - "ciphertext-untyped": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "","Outputs": ["c"],"Null": ""}]}, - "index-only": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "int64","Outputs": ["eq"],"Null": ""}]}, - "outputs-retain-order": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "int64","Outputs": ["ore","c","eq"],"Null": ""}]}, - "match-default-bare": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "string","Outputs": ["match"],"Null": ""}]}, - "flat-label-is-not-extension": {"Context": ["users","region"],"Fields": [{"Name": "value","Identity": "value","Kind": "","Outputs": ["c"],"Null": ""}]}, - "sealed-null-equality": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "int32","Outputs": ["c","eq"],"Null": "sealed"}]}, - "sealed-null-bool": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "bool","Outputs": ["c"],"Null": "sealed"}]}, - "sql-null-untyped": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "","Outputs": ["c"],"Null": "sql"}]}, - "type-bool-eq": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "bool","Outputs": ["c","eq"],"Null": ""}]}, - "type-bool-match": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "bool","Outputs": ["c","match"],"Null": ""}]}, - "type-bool-ore": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "bool","Outputs": ["c","ore"],"Null": ""}]}, - "type-bool-ope": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "bool","Outputs": ["c","ope"],"Null": ""}]}, - "type-bool-ore_block": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "bool","Outputs": ["c","ore_block"],"Null": ""}]}, - "type-int8-eq": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "int8","Outputs": ["c","eq"],"Null": ""}]}, - "type-int8-match": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "int8","Outputs": ["c","match"],"Null": ""}]}, - "type-int8-ore": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "int8","Outputs": ["c","ore"],"Null": ""}]}, - "type-int8-ope": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "int8","Outputs": ["c","ope"],"Null": ""}]}, - "type-int8-ore_block": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "int8","Outputs": ["c","ore_block"],"Null": ""}]}, - "type-int16-eq": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "int16","Outputs": ["c","eq"],"Null": ""}]}, - "type-int16-match": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "int16","Outputs": ["c","match"],"Null": ""}]}, - "type-int16-ore": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "int16","Outputs": ["c","ore"],"Null": ""}]}, - "type-int16-ope": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "int16","Outputs": ["c","ope"],"Null": ""}]}, - "type-int16-ore_block": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "int16","Outputs": ["c","ore_block"],"Null": ""}]}, - "type-int32-eq": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "int32","Outputs": ["c","eq"],"Null": ""}]}, - "type-int32-match": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "int32","Outputs": ["c","match"],"Null": ""}]}, - "type-int32-ore": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "int32","Outputs": ["c","ore"],"Null": ""}]}, - "type-int32-ope": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "int32","Outputs": ["c","ope"],"Null": ""}]}, - "type-int32-ore_block": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "int32","Outputs": ["c","ore_block"],"Null": ""}]}, - "type-int64-eq": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "int64","Outputs": ["c","eq"],"Null": ""}]}, - "type-int64-match": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "int64","Outputs": ["c","match"],"Null": ""}]}, - "type-int64-ore": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "int64","Outputs": ["c","ore"],"Null": ""}]}, - "type-int64-ope": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "int64","Outputs": ["c","ope"],"Null": ""}]}, - "type-int64-ore_block": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "int64","Outputs": ["c","ore_block"],"Null": ""}]}, - "type-int128-eq": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "int128","Outputs": ["c","eq"],"Null": ""}]}, - "type-int128-match": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "int128","Outputs": ["c","match"],"Null": ""}]}, - "type-int128-ore": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "int128","Outputs": ["c","ore"],"Null": ""}]}, - "type-int128-ope": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "int128","Outputs": ["c","ope"],"Null": ""}]}, - "type-int128-ore_block": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "int128","Outputs": ["c","ore_block"],"Null": ""}]}, - "type-uint8-eq": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "uint8","Outputs": ["c","eq"],"Null": ""}]}, - "type-uint8-match": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "uint8","Outputs": ["c","match"],"Null": ""}]}, - "type-uint8-ore": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "uint8","Outputs": ["c","ore"],"Null": ""}]}, - "type-uint8-ope": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "uint8","Outputs": ["c","ope"],"Null": ""}]}, - "type-uint8-ore_block": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "uint8","Outputs": ["c","ore_block"],"Null": ""}]}, - "type-uint16-eq": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "uint16","Outputs": ["c","eq"],"Null": ""}]}, - "type-uint16-match": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "uint16","Outputs": ["c","match"],"Null": ""}]}, - "type-uint16-ore": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "uint16","Outputs": ["c","ore"],"Null": ""}]}, - "type-uint16-ope": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "uint16","Outputs": ["c","ope"],"Null": ""}]}, - "type-uint16-ore_block": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "uint16","Outputs": ["c","ore_block"],"Null": ""}]}, - "type-uint32-eq": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "uint32","Outputs": ["c","eq"],"Null": ""}]}, - "type-uint32-match": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "uint32","Outputs": ["c","match"],"Null": ""}]}, - "type-uint32-ore": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "uint32","Outputs": ["c","ore"],"Null": ""}]}, - "type-uint32-ope": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "uint32","Outputs": ["c","ope"],"Null": ""}]}, - "type-uint32-ore_block": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "uint32","Outputs": ["c","ore_block"],"Null": ""}]}, - "type-uint64-eq": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "uint64","Outputs": ["c","eq"],"Null": ""}]}, - "type-uint64-match": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "uint64","Outputs": ["c","match"],"Null": ""}]}, - "type-uint64-ore": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "uint64","Outputs": ["c","ore"],"Null": ""}]}, - "type-uint64-ope": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "uint64","Outputs": ["c","ope"],"Null": ""}]}, - "type-uint64-ore_block": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "uint64","Outputs": ["c","ore_block"],"Null": ""}]}, - "type-uint128-eq": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "uint128","Outputs": ["c","eq"],"Null": ""}]}, - "type-uint128-match": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "uint128","Outputs": ["c","match"],"Null": ""}]}, - "type-uint128-ore": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "uint128","Outputs": ["c","ore"],"Null": ""}]}, - "type-uint128-ope": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "uint128","Outputs": ["c","ope"],"Null": ""}]}, - "type-uint128-ore_block": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "uint128","Outputs": ["c","ore_block"],"Null": ""}]}, - "type-float32-eq": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "float32","Outputs": ["c","eq"],"Null": ""}]}, - "type-float32-match": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "float32","Outputs": ["c","match"],"Null": ""}]}, - "type-float32-ore": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "float32","Outputs": ["c","ore"],"Null": ""}]}, - "type-float32-ope": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "float32","Outputs": ["c","ope"],"Null": ""}]}, - "type-float32-ore_block": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "float32","Outputs": ["c","ore_block"],"Null": ""}]}, - "type-float64-eq": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "float64","Outputs": ["c","eq"],"Null": ""}]}, - "type-float64-match": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "float64","Outputs": ["c","match"],"Null": ""}]}, - "type-float64-ore": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "float64","Outputs": ["c","ore"],"Null": ""}]}, - "type-float64-ope": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "float64","Outputs": ["c","ope"],"Null": ""}]}, - "type-float64-ore_block": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "float64","Outputs": ["c","ore_block"],"Null": ""}]}, - "type-decimal-eq": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "decimal","Outputs": ["c","eq"],"Null": ""}]}, - "type-decimal-match": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "decimal","Outputs": ["c","match"],"Null": ""}]}, - "type-decimal-ore": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "decimal","Outputs": ["c","ore"],"Null": ""}]}, - "type-decimal-ope": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "decimal","Outputs": ["c","ope"],"Null": ""}]}, - "type-decimal-ore_block": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "decimal","Outputs": ["c","ore_block"],"Null": ""}]}, - "type-date-eq": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "date","Outputs": ["c","eq"],"Null": ""}]}, - "type-date-match": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "date","Outputs": ["c","match"],"Null": ""}]}, - "type-date-ore": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "date","Outputs": ["c","ore"],"Null": ""}]}, - "type-date-ope": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "date","Outputs": ["c","ope"],"Null": ""}]}, - "type-date-ore_block": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "date","Outputs": ["c","ore_block"],"Null": ""}]}, - "type-timestamp-eq": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "timestamp","Outputs": ["c","eq"],"Null": ""}]}, - "type-timestamp-match": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "timestamp","Outputs": ["c","match"],"Null": ""}]}, - "type-timestamp-ore": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "timestamp","Outputs": ["c","ore"],"Null": ""}]}, - "type-timestamp-ope": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "timestamp","Outputs": ["c","ope"],"Null": ""}]}, - "type-timestamp-ore_block": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "timestamp","Outputs": ["c","ore_block"],"Null": ""}]}, - "type-string-eq": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "string","Outputs": ["c","eq"],"Null": ""}]}, - "type-string-match": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "string","Outputs": ["c","match"],"Null": ""}]}, - "type-string-ore": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "string","Outputs": ["c","ore"],"Null": ""}]}, - "type-string-ope": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "string","Outputs": ["c","ope"],"Null": ""}]}, - "type-string-ore_block": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "string","Outputs": ["c","ore_block"],"Null": ""}]}, - "type-bytes-eq": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "bytes","Outputs": ["c","eq"],"Null": ""}]}, - "type-bytes-match": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "bytes","Outputs": ["c","match"],"Null": ""}]}, - "type-bytes-ore": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "bytes","Outputs": ["c","ore"],"Null": ""}]}, - "type-bytes-ope": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "bytes","Outputs": ["c","ope"],"Null": ""}]}, - "type-bytes-ore_block": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "bytes","Outputs": ["c","ore_block"],"Null": ""}]}, - "type-array-eq": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "array","Outputs": ["c","eq"],"Null": ""}]}, - "type-array-match": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "array","Outputs": ["c","match"],"Null": ""}]}, - "type-array-ore": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "array","Outputs": ["c","ore"],"Null": ""}]}, - "type-array-ope": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "array","Outputs": ["c","ope"],"Null": ""}]}, - "type-array-ore_block": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "array","Outputs": ["c","ore_block"],"Null": ""}]}, - "type-object-eq": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "object","Outputs": ["c","eq"],"Null": ""}]}, - "type-object-match": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "object","Outputs": ["c","match"],"Null": ""}]}, - "type-object-ore": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "object","Outputs": ["c","ore"],"Null": ""}]}, - "type-object-ope": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "object","Outputs": ["c","ope"],"Null": ""}]}, - "type-object-ore_block": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "object","Outputs": ["c","ore_block"],"Null": ""}]}, - "type-json-eq": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "json","Outputs": ["c","eq"],"Null": ""}]}, - "type-json-match": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "json","Outputs": ["c","match"],"Null": ""}]}, - "type-json-ore": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "json","Outputs": ["c","ore"],"Null": ""}]}, - "type-json-ope": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "json","Outputs": ["c","ope"],"Null": ""}]}, - "type-json-ore_block": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "json","Outputs": ["c","ore_block"],"Null": ""}]}, - "outputs-empty": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "","Outputs": [],"Null": ""}]}, - "output-unknown": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "","Outputs": ["wat"],"Null": ""}]}, - "output-duplicate": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "","Outputs": ["c","c"],"Null": ""}]}, - "type-unknown": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "uuid","Outputs": ["c"],"Null": ""}]}, - "type-json-without-target": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "json","Outputs": ["c"],"Null": ""}]}, - "json-output-pending": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "json","Outputs": ["json"],"Null": ""}]}, - "context-segment-slash": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "a/b","Kind": "","Outputs": ["c"],"Null": ""}]}, - "context-segment-digit": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "9","Kind": "","Outputs": ["c"],"Null": ""}]}, - "shared-identity": {"Context": ["users"],"Fields": [{"Name": "a","Identity": "same","Kind": "","Outputs": ["c"],"Null": ""},{"Name": "b","Identity": "same","Kind": "","Outputs": ["c"],"Null": ""}]}, - "null-mode-unknown": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "","Outputs": ["c"],"Null": "optional"}]}, - "sealed-null-untyped": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "","Outputs": ["c"],"Null": "sealed"}]}, - "sealed-null-text": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "string","Outputs": ["c"],"Null": "sealed"}]}, - "sealed-null-order": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "int64","Outputs": ["c","ore"],"Null": "sealed"}]}, - "untyped-index": {"Context": ["users"],"Fields": [{"Name": "value","Identity": "value","Kind": "","Outputs": ["eq"],"Null": ""}]} + "ciphertext-untyped": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "", + "Outputs": ["c"], + "Null": "" + } + ] + }, + "index-only": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "int64", + "Outputs": ["eq"], + "Null": "" + } + ] + }, + "outputs-retain-order": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "int64", + "Outputs": ["ore", "c", "eq"], + "Null": "" + } + ] + }, + "match-default-bare": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "string", + "Outputs": ["match"], + "Null": "" + } + ] + }, + "flat-label-is-not-extension": { + "Context": ["users", "region"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "", + "Outputs": ["c"], + "Null": "" + } + ] + }, + "sealed-null-equality": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "int32", + "Outputs": ["c", "eq"], + "Null": "sealed" + } + ] + }, + "sealed-null-bool": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "bool", + "Outputs": ["c"], + "Null": "sealed" + } + ] + }, + "sql-null-untyped": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "", + "Outputs": ["c"], + "Null": "sql" + } + ] + }, + "type-bool-eq": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "bool", + "Outputs": ["c", "eq"], + "Null": "" + } + ] + }, + "type-bool-match": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "bool", + "Outputs": ["c", "match"], + "Null": "" + } + ] + }, + "type-bool-ore": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "bool", + "Outputs": ["c", "ore"], + "Null": "" + } + ] + }, + "type-bool-ope": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "bool", + "Outputs": ["c", "ope"], + "Null": "" + } + ] + }, + "type-bool-ore_block": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "bool", + "Outputs": ["c", "ore_block"], + "Null": "" + } + ] + }, + "type-int8-eq": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "int8", + "Outputs": ["c", "eq"], + "Null": "" + } + ] + }, + "type-int8-match": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "int8", + "Outputs": ["c", "match"], + "Null": "" + } + ] + }, + "type-int8-ore": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "int8", + "Outputs": ["c", "ore"], + "Null": "" + } + ] + }, + "type-int8-ope": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "int8", + "Outputs": ["c", "ope"], + "Null": "" + } + ] + }, + "type-int8-ore_block": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "int8", + "Outputs": ["c", "ore_block"], + "Null": "" + } + ] + }, + "type-int16-eq": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "int16", + "Outputs": ["c", "eq"], + "Null": "" + } + ] + }, + "type-int16-match": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "int16", + "Outputs": ["c", "match"], + "Null": "" + } + ] + }, + "type-int16-ore": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "int16", + "Outputs": ["c", "ore"], + "Null": "" + } + ] + }, + "type-int16-ope": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "int16", + "Outputs": ["c", "ope"], + "Null": "" + } + ] + }, + "type-int16-ore_block": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "int16", + "Outputs": ["c", "ore_block"], + "Null": "" + } + ] + }, + "type-int32-eq": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "int32", + "Outputs": ["c", "eq"], + "Null": "" + } + ] + }, + "type-int32-match": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "int32", + "Outputs": ["c", "match"], + "Null": "" + } + ] + }, + "type-int32-ore": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "int32", + "Outputs": ["c", "ore"], + "Null": "" + } + ] + }, + "type-int32-ope": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "int32", + "Outputs": ["c", "ope"], + "Null": "" + } + ] + }, + "type-int32-ore_block": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "int32", + "Outputs": ["c", "ore_block"], + "Null": "" + } + ] + }, + "type-int64-eq": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "int64", + "Outputs": ["c", "eq"], + "Null": "" + } + ] + }, + "type-int64-match": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "int64", + "Outputs": ["c", "match"], + "Null": "" + } + ] + }, + "type-int64-ore": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "int64", + "Outputs": ["c", "ore"], + "Null": "" + } + ] + }, + "type-int64-ope": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "int64", + "Outputs": ["c", "ope"], + "Null": "" + } + ] + }, + "type-int64-ore_block": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "int64", + "Outputs": ["c", "ore_block"], + "Null": "" + } + ] + }, + "type-int128-eq": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "int128", + "Outputs": ["c", "eq"], + "Null": "" + } + ] + }, + "type-int128-match": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "int128", + "Outputs": ["c", "match"], + "Null": "" + } + ] + }, + "type-int128-ore": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "int128", + "Outputs": ["c", "ore"], + "Null": "" + } + ] + }, + "type-int128-ope": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "int128", + "Outputs": ["c", "ope"], + "Null": "" + } + ] + }, + "type-int128-ore_block": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "int128", + "Outputs": ["c", "ore_block"], + "Null": "" + } + ] + }, + "type-uint8-eq": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "uint8", + "Outputs": ["c", "eq"], + "Null": "" + } + ] + }, + "type-uint8-match": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "uint8", + "Outputs": ["c", "match"], + "Null": "" + } + ] + }, + "type-uint8-ore": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "uint8", + "Outputs": ["c", "ore"], + "Null": "" + } + ] + }, + "type-uint8-ope": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "uint8", + "Outputs": ["c", "ope"], + "Null": "" + } + ] + }, + "type-uint8-ore_block": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "uint8", + "Outputs": ["c", "ore_block"], + "Null": "" + } + ] + }, + "type-uint16-eq": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "uint16", + "Outputs": ["c", "eq"], + "Null": "" + } + ] + }, + "type-uint16-match": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "uint16", + "Outputs": ["c", "match"], + "Null": "" + } + ] + }, + "type-uint16-ore": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "uint16", + "Outputs": ["c", "ore"], + "Null": "" + } + ] + }, + "type-uint16-ope": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "uint16", + "Outputs": ["c", "ope"], + "Null": "" + } + ] + }, + "type-uint16-ore_block": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "uint16", + "Outputs": ["c", "ore_block"], + "Null": "" + } + ] + }, + "type-uint32-eq": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "uint32", + "Outputs": ["c", "eq"], + "Null": "" + } + ] + }, + "type-uint32-match": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "uint32", + "Outputs": ["c", "match"], + "Null": "" + } + ] + }, + "type-uint32-ore": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "uint32", + "Outputs": ["c", "ore"], + "Null": "" + } + ] + }, + "type-uint32-ope": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "uint32", + "Outputs": ["c", "ope"], + "Null": "" + } + ] + }, + "type-uint32-ore_block": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "uint32", + "Outputs": ["c", "ore_block"], + "Null": "" + } + ] + }, + "type-uint64-eq": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "uint64", + "Outputs": ["c", "eq"], + "Null": "" + } + ] + }, + "type-uint64-match": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "uint64", + "Outputs": ["c", "match"], + "Null": "" + } + ] + }, + "type-uint64-ore": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "uint64", + "Outputs": ["c", "ore"], + "Null": "" + } + ] + }, + "type-uint64-ope": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "uint64", + "Outputs": ["c", "ope"], + "Null": "" + } + ] + }, + "type-uint64-ore_block": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "uint64", + "Outputs": ["c", "ore_block"], + "Null": "" + } + ] + }, + "type-uint128-eq": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "uint128", + "Outputs": ["c", "eq"], + "Null": "" + } + ] + }, + "type-uint128-match": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "uint128", + "Outputs": ["c", "match"], + "Null": "" + } + ] + }, + "type-uint128-ore": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "uint128", + "Outputs": ["c", "ore"], + "Null": "" + } + ] + }, + "type-uint128-ope": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "uint128", + "Outputs": ["c", "ope"], + "Null": "" + } + ] + }, + "type-uint128-ore_block": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "uint128", + "Outputs": ["c", "ore_block"], + "Null": "" + } + ] + }, + "type-float32-eq": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "float32", + "Outputs": ["c", "eq"], + "Null": "" + } + ] + }, + "type-float32-match": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "float32", + "Outputs": ["c", "match"], + "Null": "" + } + ] + }, + "type-float32-ore": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "float32", + "Outputs": ["c", "ore"], + "Null": "" + } + ] + }, + "type-float32-ope": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "float32", + "Outputs": ["c", "ope"], + "Null": "" + } + ] + }, + "type-float32-ore_block": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "float32", + "Outputs": ["c", "ore_block"], + "Null": "" + } + ] + }, + "type-float64-eq": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "float64", + "Outputs": ["c", "eq"], + "Null": "" + } + ] + }, + "type-float64-match": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "float64", + "Outputs": ["c", "match"], + "Null": "" + } + ] + }, + "type-float64-ore": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "float64", + "Outputs": ["c", "ore"], + "Null": "" + } + ] + }, + "type-float64-ope": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "float64", + "Outputs": ["c", "ope"], + "Null": "" + } + ] + }, + "type-float64-ore_block": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "float64", + "Outputs": ["c", "ore_block"], + "Null": "" + } + ] + }, + "type-decimal-eq": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "decimal", + "Outputs": ["c", "eq"], + "Null": "" + } + ] + }, + "type-decimal-match": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "decimal", + "Outputs": ["c", "match"], + "Null": "" + } + ] + }, + "type-decimal-ore": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "decimal", + "Outputs": ["c", "ore"], + "Null": "" + } + ] + }, + "type-decimal-ope": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "decimal", + "Outputs": ["c", "ope"], + "Null": "" + } + ] + }, + "type-decimal-ore_block": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "decimal", + "Outputs": ["c", "ore_block"], + "Null": "" + } + ] + }, + "type-date-eq": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "date", + "Outputs": ["c", "eq"], + "Null": "" + } + ] + }, + "type-date-match": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "date", + "Outputs": ["c", "match"], + "Null": "" + } + ] + }, + "type-date-ore": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "date", + "Outputs": ["c", "ore"], + "Null": "" + } + ] + }, + "type-date-ope": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "date", + "Outputs": ["c", "ope"], + "Null": "" + } + ] + }, + "type-date-ore_block": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "date", + "Outputs": ["c", "ore_block"], + "Null": "" + } + ] + }, + "type-timestamp-eq": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "timestamp", + "Outputs": ["c", "eq"], + "Null": "" + } + ] + }, + "type-timestamp-match": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "timestamp", + "Outputs": ["c", "match"], + "Null": "" + } + ] + }, + "type-timestamp-ore": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "timestamp", + "Outputs": ["c", "ore"], + "Null": "" + } + ] + }, + "type-timestamp-ope": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "timestamp", + "Outputs": ["c", "ope"], + "Null": "" + } + ] + }, + "type-timestamp-ore_block": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "timestamp", + "Outputs": ["c", "ore_block"], + "Null": "" + } + ] + }, + "type-string-eq": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "string", + "Outputs": ["c", "eq"], + "Null": "" + } + ] + }, + "type-string-match": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "string", + "Outputs": ["c", "match"], + "Null": "" + } + ] + }, + "type-string-ore": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "string", + "Outputs": ["c", "ore"], + "Null": "" + } + ] + }, + "type-string-ope": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "string", + "Outputs": ["c", "ope"], + "Null": "" + } + ] + }, + "type-string-ore_block": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "string", + "Outputs": ["c", "ore_block"], + "Null": "" + } + ] + }, + "type-bytes-eq": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "bytes", + "Outputs": ["c", "eq"], + "Null": "" + } + ] + }, + "type-bytes-match": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "bytes", + "Outputs": ["c", "match"], + "Null": "" + } + ] + }, + "type-bytes-ore": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "bytes", + "Outputs": ["c", "ore"], + "Null": "" + } + ] + }, + "type-bytes-ope": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "bytes", + "Outputs": ["c", "ope"], + "Null": "" + } + ] + }, + "type-bytes-ore_block": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "bytes", + "Outputs": ["c", "ore_block"], + "Null": "" + } + ] + }, + "type-array-eq": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "array", + "Outputs": ["c", "eq"], + "Null": "" + } + ] + }, + "type-array-match": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "array", + "Outputs": ["c", "match"], + "Null": "" + } + ] + }, + "type-array-ore": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "array", + "Outputs": ["c", "ore"], + "Null": "" + } + ] + }, + "type-array-ope": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "array", + "Outputs": ["c", "ope"], + "Null": "" + } + ] + }, + "type-array-ore_block": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "array", + "Outputs": ["c", "ore_block"], + "Null": "" + } + ] + }, + "type-object-eq": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "object", + "Outputs": ["c", "eq"], + "Null": "" + } + ] + }, + "type-object-match": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "object", + "Outputs": ["c", "match"], + "Null": "" + } + ] + }, + "type-object-ore": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "object", + "Outputs": ["c", "ore"], + "Null": "" + } + ] + }, + "type-object-ope": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "object", + "Outputs": ["c", "ope"], + "Null": "" + } + ] + }, + "type-object-ore_block": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "object", + "Outputs": ["c", "ore_block"], + "Null": "" + } + ] + }, + "type-json-eq": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "json", + "Outputs": ["c", "eq"], + "Null": "" + } + ] + }, + "type-json-match": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "json", + "Outputs": ["c", "match"], + "Null": "" + } + ] + }, + "type-json-ore": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "json", + "Outputs": ["c", "ore"], + "Null": "" + } + ] + }, + "type-json-ope": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "json", + "Outputs": ["c", "ope"], + "Null": "" + } + ] + }, + "type-json-ore_block": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "json", + "Outputs": ["c", "ore_block"], + "Null": "" + } + ] + }, + "outputs-empty": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "", + "Outputs": [], + "Null": "" + } + ] + }, + "output-unknown": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "", + "Outputs": ["wat"], + "Null": "" + } + ] + }, + "output-duplicate": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "", + "Outputs": ["c", "c"], + "Null": "" + } + ] + }, + "type-unknown": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "uuid", + "Outputs": ["c"], + "Null": "" + } + ] + }, + "type-json-without-target": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "json", + "Outputs": ["c"], + "Null": "" + } + ] + }, + "json-output-pending": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "json", + "Outputs": ["json"], + "Null": "" + } + ] + }, + "context-segment-slash": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "a/b", + "Kind": "", + "Outputs": ["c"], + "Null": "" + } + ] + }, + "context-segment-digit": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "9", + "Kind": "", + "Outputs": ["c"], + "Null": "" + } + ] + }, + "shared-identity": { + "Context": ["users"], + "Fields": [ + { + "Name": "a", + "Identity": "same", + "Kind": "", + "Outputs": ["c"], + "Null": "" + }, + { + "Name": "b", + "Identity": "same", + "Kind": "", + "Outputs": ["c"], + "Null": "" + } + ] + }, + "null-mode-unknown": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "", + "Outputs": ["c"], + "Null": "optional" + } + ] + }, + "sealed-null-untyped": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "", + "Outputs": ["c"], + "Null": "sealed" + } + ] + }, + "sealed-null-text": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "string", + "Outputs": ["c"], + "Null": "sealed" + } + ] + }, + "sealed-null-order": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "int64", + "Outputs": ["c", "ore"], + "Null": "sealed" + } + ] + }, + "untyped-index": { + "Context": ["users"], + "Fields": [ + { + "Name": "value", + "Identity": "value", + "Kind": "", + "Outputs": ["eq"], + "Null": "" + } + ] + } }