diff --git a/.gitattributes b/.gitattributes index ad82c7eb..86883b05 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,6 @@ # Preserve concurrent additions to the shared agent instructions. AGENTS.md merge=union + +# Preserve exact checked provider corpus bytes across checkout configurations. +schemas/edict-provider/generated/v1/** -text +schemas/edict-provider/generated/v1/**/*.cbor binary diff --git a/CHANGELOG.md b/CHANGELOG.md index 0388bbd4..e6d43b1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,65 @@ ### Added +- `echo-wesley-gen` now checks in the first exact 22-file Edict provider + artifact corpus: five canonical-CBOR primaries, fourteen canonical-CBOR + resources, the self-contained CDDL, Wesley provenance JSON, and + non-authoritative review JSON. A dedicated generator binds an explicit, + compile-time-enumerated source/dependency-lock bundle rather than executable, + Git, path, or environment discovery. Its `--check` mode reports sorted + missing, changed, and unexpected paths without creating, deleting, or + rewriting files, while generation refuses observed unexpected entries before + writing and retains no-follow directory capabilities through temporary-file + replacement. The crate test suite checks the committed snapshot. +- `echo-wesley-gen` now derives Wesley's canonical `GenerationReviewV1` from + verified provider provenance. The deterministic JSON copies the exact input, + provenance, generator, projection-role, source, and emitted-output identities, + is structurally unable to claim authority, and preserves typed Wesley + failures when its input and provenance disagree. The semantic source now + identifies both provenance and review contracts as Wesley #728 artifacts. +- `echo-wesley-gen` now constructs canonical Wesley provider-generation + provenance from explicit material only. The manifest binds the exact three + authored source artifacts, checked settings digest, caller-supplied generator + component bytes, and exactly six non-derived primary outputs, then immediately + re-verifies all referenced bytes. Generated resources remain transitively + bound through the primary artifacts, while provenance and review stay outside + the emitted set to prevent circular digests. Typed Wesley failures are + preserved without exposing Rust debug spelling as a stable diagnostic. The + primary closure retains its producing input digest so outputs cannot be + attributed to another invocation, and generator coordinates must remain + disjoint from every exact source, declared artifact, resource, and package + coordinate. +- `echo-wesley-gen` now deterministically projects the validated Echo Edict + source into a canonical lawpack, target profile, two source-partitioned + authority-facts documents, generated operation profile, fourteen declarative + resources, and a self-contained provider CDDL artifact. Every output passes + its owning generated root, Edict-owned values also pass the independently + admitted upstream roots, manifest edges use domain-framed digests, and Wesley + references bind exact output bytes. The projection preserves direct adapters, + operation-local obstructions, and optic contracts, and keeps read-class + operations as bounded observers rather than mutation DPOs. These artifacts + describe provider semantics and confer no Echo runtime authority. +- `echo-wesley-gen` now admits the exact Apache-2.0 Edict provider contract + pack merged in Edict PR #162 as an explicit generator input. The pure + boundary pins the CDDL and manifest publication, verifies strict contract and + domain inventories plus every embedded resource byte, digest, and provenance + record, rejects tampering with stable structured error kinds, and performs no + filesystem, registry, environment, or network discovery. +- `echo-wesley-gen` now implements the exact `edict.canonical-cbor/v1` value, + encoding, nesting, map-ordering, and domain-framed SHA-256 contracts. Named + provider artifacts must both use those canonical bytes and satisfy their + owning root in the admitted Edict CDDL; typed failures distinguish unknown + contracts, invalid canonical encoding, and schema mismatch, and oversized + declared lengths produce platform-stable truncation failures before host-width + conversion. This validates a generation artifact and does not grant Echo + runtime authority or admission. +- `echo-wesley-gen` now constructs a canonical Wesley extension-generation + input from exact Echo semantic-source bytes, the admitted Edict CDDL and + manifest, and checked versioned settings. The first provider closure uses an + explicitly empty GraphQL Shape/operation catalog, derives six primary output + roles without circular provenance/review digests, preserves the normalized + semantic model across set reordering, and moves the generation-input digest + when exact authored source or settings bytes change. - `warp-core` now separates application-requested causal-anchor claims from Echo admission. `CausalAnchorAdmissionRequest` contains no admission receipt, `CausalAnchorClaim` is an opaque canonical value over only the caller's claim, @@ -917,6 +976,18 @@ Applied, Rejected, Obstructed}` with receipt evidence and typed contract ### Changed +- `echo-wesley-gen` now follows Wesley's operation-neutral adapter names: + `import_runtime_optic_artifact(...)` accepts `OperationArtifact`, and + `import_registration_descriptor(...)` accepts + `OperationRegistrationDescriptor`. Source consumers using Wesley's former + `OpticArtifact` or `OpticRegistrationDescriptor` names must update when they + adopt the corresponding Wesley 0.3 prerelease; Echo's runtime-local optic + artifact types and handles are unchanged. +- Provider semantic-source, generation-input, and contract-pack `Display` + diagnostics now render explicit kebab-case failure labels instead of Rust + `Debug` variant spellings. Typed error enums remain the programmatic + contract, while human/CLI diagnostics no longer move when Rust variants are + refactored. - Restored the durable ADR 0001–0011 namespace and moved the post-Method decisions to ADR 0012–0019, preserving the meaning of existing source citations. CI now rejects missing, duplicate, non-contiguous, unindexed, or @@ -1027,6 +1098,9 @@ Applied, Rejected, Obstructed}` with receipt evidence and typed contract ### Fixed +- Direct GraphQL SDL lowering in `echo-wesley-gen` now binds the exact pinned + `wesley-core` version into generated Rust artifact-hash provenance, with a + regression test that refuses dependency/provenance version drift. - Generated-rule architecture now distinguishes Wesley's current raw `RewriteRule` fixture path, Edict's fixture-only Target IR bridge, and the still-target package-registration corridor. It also records diff --git a/Cargo.lock b/Cargo.lock index 94860307..a5ad3413 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "ambient-authority" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9d4ee0d472d1cd2e28c97dfa124b3d8d992e10eb0a035f33f5d12e3a177ba3b" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -62,7 +68,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -73,7 +79,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -93,6 +99,12 @@ dependencies = [ "thiserror 2.0.17", ] +[[package]] +name = "arraydeque" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" + [[package]] name = "arrayref" version = "0.3.9" @@ -128,7 +140,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.111", ] [[package]] @@ -137,6 +149,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bit-set" version = "0.8.0" @@ -214,7 +232,7 @@ checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.111", ] [[package]] @@ -229,6 +247,48 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +[[package]] +name = "cap-fs-ext" +version = "4.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78e5a3368ae89b7cb68186411452b4b9fac8b41be9c19bf3f47c2d2c8e36e6b" +dependencies = [ + "cap-primitives", + "cap-std", + "io-lifetimes 3.0.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "cap-primitives" +version = "4.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdadbd7c002d3a484b35243669abdae85a0ebaded5a61117169dc3400f9a7ff0" +dependencies = [ + "ambient-authority", + "fs-set-times", + "io-extras", + "io-lifetimes 3.0.1", + "ipnet", + "maybe-owned", + "rustix", + "rustix-linux-procfs", + "windows-sys 0.61.2", + "winx", +] + +[[package]] +name = "cap-std" +version = "4.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7281235d6e96d3544ca18bba9049be92f4190f8d923e3caef1b5f66cfa752608" +dependencies = [ + "cap-primitives", + "io-extras", + "io-lifetimes 3.0.1", + "rustix", +] + [[package]] name = "cast" version = "0.3.0" @@ -245,6 +305,24 @@ dependencies = [ "shlex", ] +[[package]] +name = "cddl-cat" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0def7310489015a41757b6ae8a0126ad1d91c4a3f77089f862eea7c000638825" +dependencies = [ + "base64", + "ciborium", + "escape8259", + "float-ord", + "hex", + "nom", + "regex", + "serde", + "strum_macros", + "thiserror 1.0.69", +] + [[package]] name = "cfg-if" version = "1.0.4" @@ -320,10 +398,10 @@ version = "4.5.49" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", - "syn", + "syn 2.0.111", ] [[package]] @@ -645,13 +723,19 @@ version = "0.1.0" dependencies = [ "anyhow", "blake3", + "cap-fs-ext", + "cap-std", + "cddl-cat", + "ciborium", "clap", + "hex", "prettyplease", "proc-macro2", "quote", "serde", "serde_json", - "syn", + "sha2", + "syn 2.0.111", "warp-core", "wesley-core", ] @@ -675,9 +759,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.61.2", ] +[[package]] +name = "escape8259" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5692dd7b5a1978a5aeb0ce83b7655c58ca8efdcb79d21036ea249da95afec2c6" + [[package]] name = "fastrand" version = "2.3.0" @@ -699,12 +789,35 @@ dependencies = [ "num-traits", ] +[[package]] +name = "float-ord" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce81f49ae8a0482e4c55ea62ebbd7e5a686af544c00b9d090bba3ff9be97b3d" + [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "fs-set-times" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94e7099f6313ecacbe1256e8ff9d617b75d1bcb16a6fddef94866d225a01a14a" +dependencies = [ + "io-lifetimes 2.0.4", + "rustix", + "windows-sys 0.59.0", +] + [[package]] name = "futures" version = "0.3.32" @@ -761,7 +874,7 @@ checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.111", ] [[package]] @@ -843,12 +956,30 @@ version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "foldhash", +] + [[package]] name = "hashbrown" version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +[[package]] +name = "hashlink" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824e001ac4f3012dd16a264bec811403a67ca9deb6c102fc5049b32c4574b35f" +dependencies = [ + "hashbrown 0.16.1", +] + [[package]] name = "hdrhistogram" version = "7.5.4" @@ -859,6 +990,15 @@ dependencies = [ "num-traits", ] +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "heck" version = "0.5.0" @@ -913,6 +1053,34 @@ dependencies = [ "serde_core", ] +[[package]] +name = "io-extras" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20fd6de4ccfcc187e38bc21cfa543cb5a302cb86a8b114eb7f0bf0dc9f8ac00f" +dependencies = [ + "io-lifetimes 3.0.1", + "windows-sys 0.60.2", +] + +[[package]] +name = "io-lifetimes" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06432fb54d3be7964ecd3649233cddf80db2832f47fec34c01f65b3d9d774983" + +[[package]] +name = "io-lifetimes" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0fb0570afe1fed943c5c3d4102d5358592d8625fda6a0007fdbe65a92fba96" + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + [[package]] name = "is-terminal" version = "0.4.17" @@ -921,7 +1089,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -994,6 +1162,12 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "maybe-owned" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4facc753ae494aeb6e3c22f839b158aebd4f9270f55cd3c79906c45476c47ab4" + [[package]] name = "memchr" version = "2.7.6" @@ -1017,9 +1191,15 @@ checksum = "bd2209fff77f705b00c737016a48e73733d7fbccb8b007194db148f03561fb70" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.111", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "mio" version = "1.1.1" @@ -1028,7 +1208,7 @@ checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", "wasi", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -1047,6 +1227,16 @@ dependencies = [ "tracing", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "normalize-line-endings" version = "0.3.0" @@ -1135,7 +1325,7 @@ checksum = "a990e22f43e84855daf260dded30524ef4a9021cc7541c26540500a50b624389" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.111", ] [[package]] @@ -1196,7 +1386,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn", + "syn 2.0.111", ] [[package]] @@ -1425,7 +1615,17 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustix-linux-procfs" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fc84bf7e9aa16c4f2c758f27412dc9841341e16aa682d9c7ac308fe3ee12056" +dependencies = [ + "once_cell", + "rustix", ] [[package]] @@ -1498,7 +1698,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.111", ] [[package]] @@ -1561,7 +1761,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -1570,6 +1770,30 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum_macros" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bb0dc7ee9c15cea6199cde9a127fa16a4c5819af85395457ad72d68edc85a38" +dependencies = [ + "heck 0.3.3", + "proc-macro2", + "quote", + "rustversion", + "syn 1.0.109", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.111" @@ -1597,7 +1821,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -1638,7 +1862,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.111", ] [[package]] @@ -1649,7 +1873,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.111", ] [[package]] @@ -1707,7 +1931,7 @@ dependencies = [ "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -1718,7 +1942,7 @@ checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.111", ] [[package]] @@ -1801,7 +2025,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.111", ] [[package]] @@ -2012,7 +2236,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.111", "wasm-bindgen-shared", ] @@ -2037,9 +2261,9 @@ dependencies = [ [[package]] name = "wesley-core" -version = "0.0.4" +version = "0.3.0-alpha.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96faa36a92a4b0b92d27c467b34a67e006943795f2b9a0bc1e1237729afd17c0" +checksum = "77ca44ae03bac1966eaa6d7660621160f10d3e6979bac26f5929a25a3829c1f4" dependencies = [ "apollo-parser", "async-trait", @@ -2053,6 +2277,7 @@ dependencies = [ "thiserror 1.0.69", "tokio", "tower 0.4.13", + "yaml-rust2", ] [[package]] @@ -2077,7 +2302,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -2107,7 +2332,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.111", ] [[package]] @@ -2118,7 +2343,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.111", ] [[package]] @@ -2145,6 +2370,24 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -2154,6 +2397,145 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winx" +version = "0.36.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f3fd376f71958b862e7afb20cfe5a22830e1963462f3a17f49d82a6c1d1f42d" +dependencies = [ + "bitflags", + "windows-sys 0.59.0", +] + [[package]] name = "wit-bindgen" version = "0.46.0" @@ -2179,6 +2561,16 @@ dependencies = [ "warp-core", ] +[[package]] +name = "yaml-rust2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "631a50d867fafb7093e709d75aaee9e0e0d5deb934021fcea25ac2fe09edc51e" +dependencies = [ + "arraydeque", + "hashlink", +] + [[package]] name = "zerocopy" version = "0.8.31" @@ -2196,7 +2588,7 @@ checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.111", ] [[package]] diff --git a/crates/echo-wesley-gen/Cargo.toml b/crates/echo-wesley-gen/Cargo.toml index 70684c1f..a75ce68e 100644 --- a/crates/echo-wesley-gen/Cargo.toml +++ b/crates/echo-wesley-gen/Cargo.toml @@ -3,6 +3,7 @@ [package] name = "echo-wesley-gen" version = "0.1.0" +default-run = "echo-wesley-gen" edition = "2021" rust-version = "1.90.0" description = "Code generator for Echo: consumes Wesley IR (JSON) and emits Rust artifacts via syn/quote." @@ -15,16 +16,21 @@ categories = ["development-tools", "command-line-utilities", "encoding", "wasm"] [dependencies] anyhow = "1.0" blake3 = "1.5" +cap-fs-ext = "=4.0.2" +cap-std = "=4.0.2" +cddl-cat = { version = "0.7.1", default-features = false, features = ["ciborium"] } +ciborium = "0.2.2" clap = { version = "4.4", features = ["derive"] } +hex = "0.4" proc-macro2 = "1.0" quote = "1.0" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +sha2 = "0.10" syn = { version = "2.0", features = ["full", "extra-traits"] } prettyplease = "0.2" warp-core = { workspace = true } -wesley-core = "0.0.4" - +wesley-core = "=0.3.0-alpha.1" [lints] workspace = true diff --git a/crates/echo-wesley-gen/README.md b/crates/echo-wesley-gen/README.md index e12f5835..bdb45b60 100644 --- a/crates/echo-wesley-gen/README.md +++ b/crates/echo-wesley-gen/README.md @@ -15,7 +15,8 @@ authority. The preferred input is GraphQL SDL lowered directly through the published `wesley-core` crate. The older `echo-ir/v1` JSON stdin path is retained for fixtures and compatibility while consumers move off the historical JavaScript -generator. +generator. Direct SDL lowering records the exact pinned `wesley-core` version +in the provenance bound into each generated Rust artifact hash. Echo's external Edict provider uses a separate strict source contract: [`schemas/edict-provider/echo-provider-semantics-v1.json`](../../schemas/edict-provider/echo-provider-semantics-v1.json). @@ -34,6 +35,96 @@ The separately declared provider manifest pins the later package root's exact ABI and provider coordinate and cannot inventory itself. This source is not accepted through the tolerant historical `echo-ir/v1` path. +The Edict-owned schema authority is a separate explicit input. The exact +Apache-2.0 contract pack checked under +[`schemas/edict-provider/contracts/v1/`](../../schemas/edict-provider/contracts/v1/README.md) +is admitted through +`provider_contract_pack::admit_provider_contract_pack_v1(...)`. Admission +requires the pinned Edict PR #162 CDDL and manifest publication, verifies every +embedded contract resource and provenance record, and performs no discovery or +mutable coordinate resolution. This authenticates the schema publication; it +does not by itself claim that a generated artifact is a valid schema instance. + +`provider_canonical` implements the publication's exact +`edict.canonical-cbor/v1` subset and `edict.digest/v1` domain frame. +`AdmittedProviderContractPackV1::validate_contract_bytes(...)` first requires +those exact canonical bytes and then validates the decoded value against the +named contract's owning root in the authenticated CDDL. Canonical decoding or +hashing alone is not schema admission, and even successful owning-root +validation does not install an artifact or confer Echo runtime authority. + +`provider_generation::build_provider_generation_input_v1(...)` joins that +admitted pack with exact Echo semantic-source bytes and the checked versioned +generation settings. It constructs Wesley's canonical extension-generation +input in memory, binds exact source materials for later provenance verification, +and derives the six primary output roles from the validated source. The current +closure carries an empty Wesley Shape and operation catalog because it declares +no GraphQL authority source; it does not synthesize `a.b@1.t` as GraphQL. +While the checked settings select no Shape source, any semantic input that +declares GraphQL authority fails closed until explicit SDL bytes are supported. +The normalized semantic model is stable under set ordering, while the +generation-input digest changes when raw authored bytes change because it binds +the exact source artifact. + +`provider_artifacts::generate_provider_primary_artifacts_v1(...)` projects the +normalized semantic model into five canonical-CBOR primary artifacts, fourteen +declarative generated resources, and one exact self-contained CDDL artifact. +Every canonical value is validated against its generated owning root; the +Edict-owned lawpack, target-profile, authority-facts, export, intrinsic, and +operation-profile values are also checked independently against the admitted +contract pack. Manifest edges use Edict domain-framed digests, while Wesley +content references bind exact emitted bytes. Direct adapters and +operation-local obstruction mappings remain explicit, and invocation posture +comes from the admitted optic: affect/reintegration produces a mutation while +revelation/projection produces a bounded observer. These artifacts describe +provider semantics only; they do not install a package or grant Echo runtime +authority. + +`provider_provenance::generate_provider_generation_provenance_v1(...)` builds +Wesley's canonical provenance manifest from the generation input, primary +closure, and caller-supplied exact generator component bytes. It immediately +verifies the generator, all three source artifacts, and the five canonical-CBOR +artifacts plus raw CDDL schema. The fourteen resources are transitively bound by +the primary bytes and are not restated as top-level emissions. Provenance and +review are likewise excluded from that set so neither document claims its own +digest. The API performs no executable, path, environment, process, registry, +clock, or network discovery. Each primary closure records the exact Wesley input +digest that produced it, preventing mixed-input attribution even when requested +roles match. Generator coordinates must also be distinct from all declared +source-artifact, generated-artifact, resource, provider, and package-manifest +coordinates. + +`provider_review::generate_provider_generation_review_v1(...)` derives Wesley's +canonical `GenerationReviewV1` from that verified provenance wrapper. It copies +the exact input, provenance, generator, projection-role, source, and emitted +identities into deterministic JSON while Wesley keeps the `authoritative` field +false by construction and deserialization. Review is derived tooling evidence; +it neither replaces provenance verification nor admits or authorizes anything +in Echo. + +`provider_corpus` frames the generator's exact source and dependency-lock +closure, re-verifies the complete generation chain, and renders the checked +22-file corpus under `schemas/edict-provider/generated/v1/`. The frame uses a +fixed coordinate, crate version, repo-relative source paths, and exact +compile-time bytes for the provider modules, dedicated corpus CLI, manifests, +workspace lockfile, and Rust toolchain. It deliberately excludes authored +semantic/settings/contract inputs already bound by Wesley and every generated +output, preventing a circular generator identity. This is source/dependency-lock +identity, not an executable-build or supply-chain attestation. + +The dedicated `echo-edict-provider-artifacts` binary is the explicit filesystem +boundary. The caller-supplied `--out` path is resolved once under ambient +filesystem authority; its final corpus-root entry is opened or created without +following a symlink. Every operation beneath that acquired root inventories and +writes through retained, no-follow directory capabilities. Generation refuses +every unexpected entry it observes before creating or replacing an expected +path; otherwise it replaces only the 22 expected leaves and preserves an +existing destination if replacement fails. `--check` renders expected bytes in +memory, reads the target tree through the same bounded handles, reports sorted +missing/changed/unexpected drift, and returns before every directory-creation or +write path. This boundary does not claim that unrelated ancestors used to locate +the requested root are symlink-free. + ## Usage ```bash @@ -52,6 +143,14 @@ cat ir.json | cargo run -p echo-wesley-gen -- --out generated.rs # Emit std-only warp-core contract-host helpers for installed mutation handlers # and query observers cat ir.json | cargo run -p echo-wesley-gen -- --contract-host --out generated.rs + +# Rebuild the checked Edict provider artifact corpus from exact inputs +cargo +1.90.0 run --locked -p echo-wesley-gen \ + --bin echo-edict-provider-artifacts -- + +# Report checked-corpus drift without rewriting anything +cargo +1.90.0 run --locked -p echo-wesley-gen \ + --bin echo-edict-provider-artifacts -- --check ``` ## Notes diff --git a/crates/echo-wesley-gen/src/bin/echo-edict-provider-artifacts.rs b/crates/echo-wesley-gen/src/bin/echo-edict-provider-artifacts.rs new file mode 100644 index 00000000..1bfd0a80 --- /dev/null +++ b/crates/echo-wesley-gen/src/bin/echo-edict-provider-artifacts.rs @@ -0,0 +1,626 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +#![allow(clippy::print_stdout, clippy::print_stderr)] +//! Generates or checks Echo's deterministic Edict provider artifact corpus. + +use anyhow::{anyhow, bail, Context, Result}; +use cap_fs_ext::{FollowSymlinks, OpenOptionsFollowExt, OpenOptionsMaybeDirExt}; +use cap_std::{ + ambient_authority, + fs::{Dir, File, OpenOptions}, +}; +use clap::Parser; +use echo_wesley_gen::provider_artifacts::generate_provider_primary_artifacts_v1; +use echo_wesley_gen::provider_contract_pack::admit_provider_contract_pack_v1; +use echo_wesley_gen::provider_corpus::{ + checked_provider_generator_source_bundle_v1, diff_provider_artifact_corpus_v1, + render_provider_artifact_corpus_v1, ProviderArtifactCorpusFileV1, +}; +use echo_wesley_gen::provider_generation::build_provider_generation_input_v1; +use echo_wesley_gen::provider_provenance::generate_provider_generation_provenance_v1; +use echo_wesley_gen::provider_review::generate_provider_generation_review_v1; +use std::collections::{BTreeMap, BTreeSet}; +use std::ffi::{OsStr, OsString}; +use std::io::{Read, Write}; +use std::path::{Path, PathBuf}; +use std::sync::atomic::{AtomicU64, Ordering}; + +const SOURCE: &[u8] = + include_bytes!("../../../../schemas/edict-provider/echo-provider-semantics-v1.json"); +const SETTINGS: &[u8] = + include_bytes!("../../../../schemas/edict-provider/generation-settings-v1.json"); +const CONTRACT_CDDL: &[u8] = + include_bytes!("../../../../schemas/edict-provider/contracts/v1/edict-provider-contracts.cddl"); +const CONTRACT_MANIFEST: &[u8] = + include_bytes!("../../../../schemas/edict-provider/contracts/v1/manifest.json"); +const NON_REGULAR_ENTRY_BYTES: &[u8] = b"echo.provider-artifact-corpus.non-regular/v1"; +const OVERSIZED_ENTRY_BYTES: &[u8] = b"echo.provider-artifact-corpus.oversized/v1"; +const MAX_ACTUAL_CORPUS_FILE_BYTES: usize = 32 * 1024 * 1024; +const MAX_ACTUAL_CORPUS_FILE_BYTES_U64: u64 = 32 * 1024 * 1024; +const MAX_TEMP_FILE_ATTEMPTS: usize = 1_024; + +static NEXT_TEMP_FILE: AtomicU64 = AtomicU64::new(0); + +#[derive(Parser)] +#[command( + author, + version, + about = "Generates or checks Echo's deterministic Edict provider artifact corpus" +)] +struct Args { + /// Output directory for the checked provider artifact corpus. + #[arg(long, default_value = "schemas/edict-provider/generated/v1")] + out: PathBuf, + + /// Report corpus drift without creating, deleting, or rewriting files. + #[arg(long)] + check: bool, +} + +fn main() -> Result<()> { + let args = Args::parse(); + let contract_pack = admit_provider_contract_pack_v1(CONTRACT_CDDL, CONTRACT_MANIFEST)?; + let input = build_provider_generation_input_v1(SOURCE, &contract_pack, SETTINGS)?; + let primary = generate_provider_primary_artifacts_v1(&input, &contract_pack)?; + let generator = checked_provider_generator_source_bundle_v1()?.generator_material()?; + let provenance = generate_provider_generation_provenance_v1(&input, &primary, &generator)?; + let review = generate_provider_generation_review_v1(&input, &provenance)?; + let corpus = + render_provider_artifact_corpus_v1(&input, &primary, &generator, &provenance, &review)?; + + if args.check { + let actual = read_actual_corpus(&args.out, corpus.files())?; + let drift = diff_provider_artifact_corpus_v1(&corpus, &actual)?; + if drift.is_empty() { + println!( + "Provider artifact corpus is current in {}", + args.out.display() + ); + return Ok(()); + } + + eprintln!("Provider artifact corpus drift:"); + for entry in &drift { + eprintln!(" {}: {}", entry.kind().as_str(), entry.relative_path()); + } + bail!("provider artifact corpus is not current"); + } + + write_corpus(&args.out, corpus.files())?; + println!( + "Provider artifact corpus generated in {}", + args.out.display() + ); + Ok(()) +} + +fn read_actual_corpus( + root: &Path, + expected: &[ProviderArtifactCorpusFileV1], +) -> Result> { + let Some(root_directory) = open_corpus_root(root, false)? else { + return Ok(Vec::new()); + }; + + read_actual_corpus_from_directory(&root_directory, expected) +} + +fn read_actual_corpus_from_directory( + root: &Dir, + expected: &[ProviderArtifactCorpusFileV1], +) -> Result> { + let expected_directories = expected_directories(expected); + let mut actual = Vec::new(); + read_actual_directory(root, "", &expected_directories, &mut actual)?; + Ok(actual) +} + +fn expected_directories(expected: &[ProviderArtifactCorpusFileV1]) -> BTreeSet { + let mut directories = BTreeSet::new(); + for file in expected { + let components = file.relative_path().split('/').collect::>(); + for end in 1..components.len() { + directories.insert(components[..end].join("/")); + } + } + directories +} + +fn read_actual_directory( + directory: &Dir, + relative_directory: &str, + expected_directories: &BTreeSet, + actual: &mut Vec, +) -> Result<()> { + let mut entries = directory + .entries() + .with_context(|| format!("failed to read corpus directory {relative_directory}"))? + .collect::>>() + .with_context(|| format!("failed to enumerate corpus directory {relative_directory}"))?; + entries.sort_by_key(cap_std::fs::DirEntry::file_name); + + for entry in entries { + let raw_file_name = entry.file_name(); + let diagnostic_path = Path::new(relative_directory).join(&raw_file_name); + let file_name = utf8_file_name(raw_file_name.clone(), &diagnostic_path)?; + let relative_path = if relative_directory.is_empty() { + file_name + } else { + format!("{relative_directory}/{file_name}") + }; + let file_type = entry + .file_type() + .with_context(|| format!("failed to inspect corpus entry {relative_path}"))?; + + if file_type.is_file() { + let file = open_file_nofollow(directory, &raw_file_name) + .with_context(|| format!("failed to open corpus file {relative_path}"))?; + let metadata = file + .metadata() + .with_context(|| format!("failed to inspect corpus file {relative_path}"))?; + if !metadata.is_file() { + actual.push(ProviderArtifactCorpusFileV1::new( + relative_path, + NON_REGULAR_ENTRY_BYTES, + )?); + } else if metadata.len() > MAX_ACTUAL_CORPUS_FILE_BYTES_U64 { + actual.push(ProviderArtifactCorpusFileV1::new( + relative_path, + OVERSIZED_ENTRY_BYTES, + )?); + } else { + let mut bytes = Vec::new(); + file.take(MAX_ACTUAL_CORPUS_FILE_BYTES_U64 + 1) + .read_to_end(&mut bytes) + .with_context(|| format!("failed to read corpus file {relative_path}"))?; + let exact_bytes = if bytes.len() > MAX_ACTUAL_CORPUS_FILE_BYTES { + OVERSIZED_ENTRY_BYTES + } else { + &bytes + }; + actual.push(ProviderArtifactCorpusFileV1::new( + relative_path, + exact_bytes, + )?); + } + } else if file_type.is_dir() && expected_directories.contains(&relative_path) { + let child = open_dir_nofollow(directory, &raw_file_name) + .with_context(|| format!("failed to open corpus directory {relative_path}"))?; + read_actual_directory(&child, &relative_path, expected_directories, actual)?; + } else { + actual.push(ProviderArtifactCorpusFileV1::new( + relative_path, + NON_REGULAR_ENTRY_BYTES, + )?); + } + } + Ok(()) +} + +fn utf8_file_name(file_name: OsString, path: &Path) -> Result { + file_name.into_string().map_err(|name| { + anyhow!( + "corpus entry name is not UTF-8 at {}: {}", + path.display(), + name.to_string_lossy() + ) + }) +} + +fn open_corpus_root(root: &Path, create: bool) -> Result> { + let absolute = std::path::absolute(root) + .with_context(|| format!("failed to resolve corpus root {}", root.display()))?; + let Some(name) = absolute.file_name() else { + return Dir::open_ambient_dir(&absolute, ambient_authority()) + .map(Some) + .with_context(|| format!("failed to open corpus root {}", absolute.display())); + }; + let parent_path = absolute.parent().ok_or_else(|| { + anyhow!( + "corpus root has no parent directory: {}", + absolute.display() + ) + })?; + + if create { + std::fs::create_dir_all(parent_path).with_context(|| { + format!( + "failed to create corpus root parent {}", + parent_path.display() + ) + })?; + } + let parent = match Dir::open_ambient_dir(parent_path, ambient_authority()) { + Ok(parent) => parent, + Err(error) if !create && error.kind() == std::io::ErrorKind::NotFound => return Ok(None), + Err(error) => { + return Err(error).with_context(|| { + format!( + "failed to open corpus root parent {}", + parent_path.display() + ) + }); + } + }; + + if create { + ensure_corpus_directory(&parent, name, &absolute).map(Some) + } else { + open_existing_corpus_directory(&parent, name, &absolute) + } +} + +fn open_existing_corpus_directory( + parent: &Dir, + name: &OsStr, + display_path: &Path, +) -> Result> { + match parent.symlink_metadata(name) { + Ok(metadata) if metadata.file_type().is_symlink() => { + bail!( + "refusing to traverse corpus directory symlink {}", + display_path.display() + ); + } + Ok(metadata) if !metadata.is_dir() => { + bail!( + "corpus directory path is not a directory: {}", + display_path.display() + ); + } + Ok(_) => open_dir_nofollow(parent, name) + .map(Some) + .with_context(|| format!("failed to open corpus directory {}", display_path.display())), + Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None), + Err(error) => Err(error).with_context(|| { + format!( + "failed to inspect corpus directory {}", + display_path.display() + ) + }), + } +} + +fn ensure_corpus_directory(parent: &Dir, name: &OsStr, display_path: &Path) -> Result { + if let Some(directory) = open_existing_corpus_directory(parent, name, display_path)? { + return Ok(directory); + } + + match parent.create_dir(name) { + Ok(()) => {} + Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {} + Err(error) => { + return Err(error).with_context(|| { + format!( + "failed to create corpus directory {}", + display_path.display() + ) + }); + } + } + open_existing_corpus_directory(parent, name, display_path)?.ok_or_else(|| { + anyhow!( + "created corpus directory disappeared before it could be opened: {}", + display_path.display() + ) + }) +} + +fn open_dir_nofollow(parent: &Dir, name: &OsStr) -> std::io::Result { + let mut options = OpenOptions::new(); + options.read(true); + options.follow(FollowSymlinks::No); + options.maybe_dir(true); + let file = parent.open_with(name, &options)?; + if !file.metadata()?.is_dir() { + return Err(std::io::Error::new( + std::io::ErrorKind::NotADirectory, + "corpus path component is not a directory", + )); + } + Ok(Dir::from_std_file(file.into_std())) +} + +fn open_file_nofollow(parent: &Dir, name: &OsStr) -> std::io::Result { + let mut options = OpenOptions::new(); + options.read(true); + options.follow(FollowSymlinks::No); + parent.open_with(name, &options) +} + +fn write_corpus(root: &Path, files: &[ProviderArtifactCorpusFileV1]) -> Result<()> { + let root_directory = open_corpus_root(root, true)?.ok_or_else(|| { + anyhow!( + "created corpus root could not be opened: {}", + root.display() + ) + })?; + let actual = read_actual_corpus_from_directory(&root_directory, files)?; + let expected_paths = files + .iter() + .map(ProviderArtifactCorpusFileV1::relative_path) + .collect::>(); + if let Some(unexpected) = actual + .iter() + .find(|file| !expected_paths.contains(file.relative_path())) + { + bail!( + "refusing to generate over unexpected corpus entry {}", + unexpected.relative_path() + ); + } + + let directories = prepare_corpus_directories(&root_directory, root, files)?; + preflight_corpus_write(&directories, files)?; + for file in files { + let (parent, leaf) = corpus_file_parts(file.relative_path()); + let directory = directories.get(parent).ok_or_else(|| { + anyhow!( + "corpus file parent was not retained: {}", + file.relative_path() + ) + })?; + let path = root.join(file.relative_path()); + replace_corpus_file(directory, OsStr::new(leaf), file.bytes(), &path)?; + println!(" wrote {}", path.display()); + } + Ok(()) +} + +fn prepare_corpus_directories( + root: &Dir, + root_display: &Path, + files: &[ProviderArtifactCorpusFileV1], +) -> Result> { + let mut directories = BTreeMap::new(); + directories.insert( + String::new(), + root.try_clone() + .context("failed to retain the corpus root directory")?, + ); + + for relative_path in expected_directories(files) { + let (parent, name) = corpus_file_parts(&relative_path); + let parent_directory = directories + .get(parent) + .ok_or_else(|| anyhow!("corpus directory parent was not retained: {relative_path}"))?; + let directory = ensure_corpus_directory( + parent_directory, + OsStr::new(name), + &root_display.join(&relative_path), + )?; + directories.insert(relative_path, directory); + } + + Ok(directories) +} + +fn preflight_corpus_write( + directories: &BTreeMap, + files: &[ProviderArtifactCorpusFileV1], +) -> Result<()> { + for file in files { + let (parent, leaf) = corpus_file_parts(file.relative_path()); + let directory = directories.get(parent).ok_or_else(|| { + anyhow!( + "corpus file parent was not retained: {}", + file.relative_path() + ) + })?; + validate_existing_corpus_file( + directory, + OsStr::new(leaf), + Path::new(file.relative_path()), + )?; + } + Ok(()) +} + +fn corpus_file_parts(relative_path: &str) -> (&str, &str) { + relative_path + .rsplit_once('/') + .unwrap_or(("", relative_path)) +} + +fn validate_existing_corpus_file(parent: &Dir, leaf: &OsStr, display_path: &Path) -> Result<()> { + match parent.symlink_metadata(leaf) { + Ok(metadata) if metadata.file_type().is_symlink() => { + bail!( + "refusing to replace corpus symlink {}", + display_path.display() + ); + } + Ok(metadata) if !metadata.is_file() => { + bail!( + "refusing to replace non-file corpus entry {}", + display_path.display() + ); + } + Ok(_) => Ok(()), + Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()), + Err(error) => Err(error) + .with_context(|| format!("failed to inspect corpus file {}", display_path.display())), + } +} + +fn replace_corpus_file( + parent: &Dir, + leaf: &OsStr, + bytes: &[u8], + display_path: &Path, +) -> Result<()> { + validate_existing_corpus_file(parent, leaf, display_path)?; + let display_parent = display_path.parent().ok_or_else(|| { + anyhow!( + "corpus file has no display parent: {}", + display_path.display() + ) + })?; + let (temporary_name, mut temporary_file) = create_temporary_file(parent, display_parent)?; + + let write_result = temporary_file + .write_all(bytes) + .with_context(|| { + format!( + "failed to write temporary corpus file {}", + display_parent.join(&temporary_name).display() + ) + }) + .and_then(|()| { + temporary_file.sync_all().with_context(|| { + format!( + "failed to sync temporary corpus file {}", + display_parent.join(&temporary_name).display() + ) + }) + }); + drop(temporary_file); + if let Err(error) = write_result { + remove_temporary_file(parent, &temporary_name); + return Err(error); + } + + validate_existing_corpus_file(parent, leaf, display_path)?; + if let Err(error) = replace_temporary_file(parent, &temporary_name, leaf, display_path) { + remove_temporary_file(parent, &temporary_name); + return Err(error); + } + Ok(()) +} + +fn create_temporary_file(parent: &Dir, display_parent: &Path) -> Result<(OsString, File)> { + for _ in 0..MAX_TEMP_FILE_ATTEMPTS { + let sequence = NEXT_TEMP_FILE.fetch_add(1, Ordering::Relaxed); + let name = OsString::from(format!( + ".echo-provider-artifact-{}-{sequence}.tmp", + std::process::id() + )); + let mut options = OpenOptions::new(); + options.write(true).create_new(true); + options.follow(FollowSymlinks::No); + match parent.open_with(&name, &options) { + Ok(file) => return Ok((name, file)), + Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {} + Err(error) => { + return Err(error).with_context(|| { + format!( + "failed to create temporary corpus file {}", + display_parent.join(&name).display() + ) + }); + } + } + } + bail!( + "failed to reserve a temporary corpus file in {} after {} attempts", + display_parent.display(), + MAX_TEMP_FILE_ATTEMPTS + ); +} + +fn replace_temporary_file( + parent: &Dir, + temporary_name: &OsStr, + destination_name: &OsStr, + destination_display: &Path, +) -> Result<()> { + replace_temporary_file_with( + temporary_name, + destination_name, + destination_display, + |from, to| parent.rename(from, parent, to), + ) +} + +fn replace_temporary_file_with( + temporary_name: &OsStr, + destination_name: &OsStr, + destination_display: &Path, + replace: impl FnOnce(&OsStr, &OsStr) -> std::io::Result<()>, +) -> Result<()> { + replace(temporary_name, destination_name).with_context(|| { + format!( + "failed to replace corpus file {} from {}", + destination_display.display(), + temporary_name.to_string_lossy() + ) + }) +} + +fn remove_temporary_file(parent: &Dir, name: &OsStr) { + drop(parent.remove_file(name)); +} + +#[cfg(test)] +mod tests { + use super::{ + open_dir_nofollow, replace_corpus_file, replace_temporary_file_with, NEXT_TEMP_FILE, + }; + use anyhow::Result; + use cap_std::{ambient_authority, fs::Dir}; + use std::ffi::OsStr; + use std::path::{Path, PathBuf}; + use std::sync::atomic::Ordering; + + fn test_directory(label: &str) -> PathBuf { + std::env::temp_dir().join(format!( + "echo-provider-{label}-{}-{}", + std::process::id(), + NEXT_TEMP_FILE.fetch_add(1, Ordering::Relaxed) + )) + } + + #[test] + fn failed_replacement_preserves_existing_destination() -> Result<()> { + let root = test_directory("replacement-test"); + std::fs::create_dir(&root)?; + let temporary = root.join("temporary"); + let destination = root.join("destination"); + std::fs::write(&temporary, b"replacement")?; + std::fs::write(&destination, b"admitted")?; + + let result = replace_temporary_file_with( + OsStr::new("temporary"), + OsStr::new("destination"), + &destination, + |_, _| Err(std::io::Error::other("injected replacement failure")), + ); + + assert!(result.is_err()); + assert_eq!(std::fs::read(&destination)?, b"admitted"); + assert_eq!(std::fs::read(&temporary)?, b"replacement"); + std::fs::remove_dir_all(root)?; + Ok(()) + } + + #[cfg(unix)] + #[test] + fn retained_parent_handle_prevents_symlink_redirection() -> Result<()> { + use std::os::unix::fs::symlink; + + let root = test_directory("parent-swap"); + let outside = test_directory("parent-swap-outside"); + let evidence = root.join("evidence"); + let parked = root.join("evidence-parked"); + std::fs::create_dir_all(&evidence)?; + std::fs::create_dir_all(&outside)?; + let root_directory = Dir::open_ambient_dir(&root, ambient_authority())?; + let evidence_directory = open_dir_nofollow(&root_directory, OsStr::new("evidence"))?; + + std::fs::rename(&evidence, &parked)?; + symlink(&outside, &evidence)?; + + replace_corpus_file( + &evidence_directory, + OsStr::new("artifact"), + b"canonical", + Path::new("evidence/artifact"), + )?; + + assert!(!outside.join("artifact").exists()); + assert_eq!(std::fs::read(parked.join("artifact"))?, b"canonical"); + std::fs::remove_dir_all(root)?; + std::fs::remove_dir_all(outside)?; + Ok(()) + } +} diff --git a/crates/echo-wesley-gen/src/lib.rs b/crates/echo-wesley-gen/src/lib.rs index 26e70a49..cff73a6c 100644 --- a/crates/echo-wesley-gen/src/lib.rs +++ b/crates/echo-wesley-gen/src/lib.rs @@ -15,6 +15,27 @@ /// Strict Echo-owned semantic source for generated Edict provider artifacts. pub mod provider_semantics; +/// Explicit admission boundary for the Edict-owned provider contract pack. +pub mod provider_contract_pack; + +/// Edict canonical values, CBOR encoding, and domain-framed artifact digests. +pub mod provider_canonical; + +/// Canonical Wesley input assembly for Echo provider artifact generation. +pub mod provider_generation; + +/// Deterministic, schema-validated primary Edict provider artifacts. +pub mod provider_artifacts; + +/// Exact-material Wesley provenance for provider artifact generation. +pub mod provider_provenance; + +/// Deterministic non-authoritative Wesley review of provider generation. +pub mod provider_review; + +/// Exact generator source identity and checked provider artifact corpus. +pub mod provider_corpus; + /// Imported Wesley runtime optic artifact ready for Echo registration. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ImportedRuntimeOpticArtifact { @@ -30,7 +51,7 @@ pub struct ImportedRuntimeOpticArtifact { /// verifies the descriptor through [`warp_core::OpticArtifactRegistry`] and /// returns the opaque runtime-local handle only after registration succeeds. pub fn import_runtime_optic_artifact( - artifact: &wesley_core::OpticArtifact, + artifact: &wesley_core::OperationArtifact, ) -> anyhow::Result { if artifact.requirements_digest != artifact.requirements_artifact.digest { anyhow::bail!( @@ -59,7 +80,7 @@ pub fn import_runtime_optic_artifact( /// Imports a Wesley registration descriptor into Echo's registration shape. pub fn import_registration_descriptor( - descriptor: &wesley_core::OpticRegistrationDescriptor, + descriptor: &wesley_core::OperationRegistrationDescriptor, ) -> warp_core::OpticRegistrationDescriptor { warp_core::OpticRegistrationDescriptor { artifact_id: descriptor.artifact_id.clone(), diff --git a/crates/echo-wesley-gen/src/main.rs b/crates/echo-wesley-gen/src/main.rs index a91db2c2..efbb9584 100644 --- a/crates/echo-wesley-gen/src/main.rs +++ b/crates/echo-wesley-gen/src/main.rs @@ -25,7 +25,7 @@ const ECHO_IR_VERSION: &str = "echo-ir/v1"; const DEFAULT_CODEC_ID: &str = "le-binary-v1"; const DEFAULT_REGISTRY_VERSION: u32 = 1; const RESERVED_CONTROL_OP_ID: u32 = u32::MAX; -const WESLEY_CORE_VERSION: &str = "0.0.4"; +const WESLEY_CORE_VERSION: &str = "0.3.0-alpha.1"; #[derive(Parser)] #[command( @@ -2123,6 +2123,23 @@ fn map_helper_type(gql_type: &str, args: &Args) -> TokenStream { } } +#[cfg(test)] +mod wesley_core_version_pinned { + use super::WESLEY_CORE_VERSION; + + #[test] + fn provenance_version_matches_exact_dependency() { + let exact_dependency = format!("wesley-core = \"={WESLEY_CORE_VERSION}\""); + + assert!( + include_str!("../Cargo.toml") + .lines() + .any(|line| line.trim() == exact_dependency), + "Wesley provenance must match the exact wesley-core dependency" + ); + } +} + #[cfg(test)] mod stable_op_id_pinned { use super::stable_op_id; diff --git a/crates/echo-wesley-gen/src/provider_artifacts.rs b/crates/echo-wesley-gen/src/provider_artifacts.rs new file mode 100644 index 00000000..4bb48026 --- /dev/null +++ b/crates/echo-wesley-gen/src/provider_artifacts.rs @@ -0,0 +1,2131 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Pure deterministic construction of the first Echo provider artifact closure. +//! +//! These values describe Edict-authored provider semantics. They do not admit +//! artifacts into Echo, install operations, grant runtime authority, schedule +//! work, or observe causal history. Every byte consumed by this module is an +//! explicit argument, and every emitted CBOR value is checked against both the +//! generated self-contained schema and, when Edict owns the format, the +//! independently admitted upstream root. + +use std::fmt; +use std::sync::Arc; + +use cddl_cat::cbor::validate_cbor; +use cddl_cat::context::BasicContext; +use cddl_cat::flatten::flatten_from_str; +use serde_json::{json, Map as JsonMap, Value as JsonValue}; +use wesley_core::{ + GenerationArtifactReferenceV1, GenerationContractError, GenerationContractErrorKind, +}; + +use crate::provider_canonical::{ + decode_canonical_cbor_v1, digest_canonical_value_v1, encode_canonical_cbor_v1, + CanonicalValueErrorKind, CanonicalValueV1, +}; +use crate::provider_contract_pack::{ + AdmittedProviderContractPackV1, ProviderContractValidationErrorKind, +}; +use crate::provider_generation::ProviderGenerationInputV1; +use crate::provider_semantics::{ + ArtifactResourceDeclaration, ArtifactResourceProvision, AuthorityClass, + AuthorityFactSourceKind, EffectKindHint, ExecutionClass, GeneratedArtifactDeclaration, + GeneratedArtifactKind, LawpackVerifierDeclaration, OpticKind, ProviderSemanticSourceV1, +}; + +const SCHEMA_ROLE: &str = "schema.echo-provider-artifacts"; +const GENERATED_SCHEMA_DOMAIN: &str = "echo.provider-artifacts.cddl@1"; +const GENERATED_PROFILE_ROOT: &str = "generated-artifact-profile"; +const GENERATED_PROFILE_DOMAIN: &str = "echo.generated-artifact-profile/v1"; + +const PROVIDER_SCHEMA_SUFFIX: &str = r#" + +; --- Echo provider-generated declarative contracts ----------------------- +; These roots validate provider semantic descriptions. They confer no Echo +; runtime authority and contain no package, component, or installation state. + +generated-artifact-profile = { + apiVersion: "echo.generated-artifact-profile/v1", + targetProfile: tstr, + types: { * tstr => echo-generated-type }, + operations: { * tstr => generated-operation }, +} + +echo-generated-type = echo-generated-string-alias / echo-generated-record +echo-generated-string-alias = { + kind: "coreStringAlias", + maxScalarValues: uint, + canonical: "raw-utf8", +} +echo-generated-record = { + kind: "record", + fields: [* echo-generated-record-field], +} +echo-generated-record-field = { name: tstr, type: tstr } + +generated-operation = { + inputType: tstr, + outputType: tstr, + effect: tstr, + operationProfile: tstr, + opticContract: tstr, + budget: tstr, + invocationKind: "mutation" / "observer", + implementation: { kind: "native" / "directAdapter", coordinate: tstr }, + obstructionMappings: { * tstr => tstr }, +} + +echo-provider-conformance-corpus = { + apiVersion: "echo.edict-provider.conformance-corpus/v1", + class: "declarative", + operations: { * tstr => null }, + capabilities: { * tstr => null }, + semanticEffects: { * tstr => null }, + cases: [], +} + +echo-provider-lawpack-compatibility = { + apiVersion: "echo.edict-provider.lawpack-compatibility/v1", + class: "declarative", + acceptedCoreAbi: { * tstr => null }, + acceptedTargetProfiles: { * tstr => null }, + semanticEffects: { * tstr => null }, +} + +echo-provider-lawpack-target-adapter = { + apiVersion: "echo.edict-provider.lawpack-target-adapter/v1", + class: "declarative", + targetProfile: tstr, + targetIrDomain: tstr, + effectImplementations: { * tstr => echo-effect-implementation }, +} + +echo-effect-implementation = echo-native-effect-implementation / echo-direct-effect-implementation +echo-native-effect-implementation = { + kind: "native", + capability: tstr, + writeClass: tstr, +} +echo-direct-effect-implementation = { + kind: "directAdapter", + adapter: tstr, + capability: tstr, + writeClass: tstr, +} + +echo-provider-lawpack-verifier = { + apiVersion: "echo.edict-provider.lawpack-verifier/v1", + class: "declarative", + operationObstructions: { * tstr => echo-operation-obstructions }, +} +echo-operation-obstructions = { + effect: tstr, + failureMappings: { * failure-ident => tstr }, +} + +echo-dpo-bundle = { + apiVersion: "echo.dpo.bundle/v1", + class: "declarative", + applicationModel: tstr, + readConsistency: tstr, + operationProfiles: { * tstr => null }, +} + +echo-dpo-cost = { + apiVersion: "echo.dpo.cost/v1", + class: "declarative", + capabilities: { * tstr => echo-cost-capability }, +} +echo-cost-capability = { + effect: tstr, + costTemplate: tstr, + semanticObligation: tstr, +} + +echo-dpo-footprint = { + apiVersion: "echo.dpo.footprint/v1", + class: "declarative", + capabilities: { * tstr => echo-footprint-capability }, +} +echo-footprint-capability = { + effect: tstr, + footprintTemplate: tstr, + semanticObligation: tstr, + writeClass: tstr, +} + +echo-span-ir = { + apiVersion: "echo.span-ir/v1", + class: "declarative", + domain: tstr, + targetProfile: tstr, + capabilities: { * tstr => null }, +} + +echo-dpo-lowerer = { + apiVersion: "echo.dpo.lowerer/v1", + class: "declarative", + acceptedCoreAbi: { * tstr => null }, + outputDomain: tstr, + targetProfile: tstr, + effectImplementations: { * tstr => echo-effect-implementation }, + opticContracts: { * tstr => tstr }, +} + +echo-dpo-obstructions = { + apiVersion: "echo.dpo.obstructions/v1", + class: "declarative", + effectFailures: { * tstr => { authorityClass: authority-class, payloadType: tstr } }, + domainObstructions: { * tstr => { authorityClass: authority-class, payloadSchema: tstr } }, +} + +echo-dpo-verifier = { + apiVersion: "echo.dpo.verifier/v1", + class: "declarative", + targetProfile: tstr, + targetIrDomain: tstr, + capabilities: { * tstr => null }, + operationProfiles: { * tstr => null }, + opticContracts: { * tstr => tstr }, +} + +echo-nonempty-tstr = tstr .regexp "(?s).+" + +generated-artifact = { + apiVersion: "echo.generated-artifact/v1", + profile: resource-ref, + operation: echo-nonempty-tstr, + mediaType: echo-nonempty-tstr, + bytes: bstr, +} + +review-payload = { + apiVersion: "echo.review-payload/v1", + authoritative: false, + subject: resource-ref, + mediaType: echo-nonempty-tstr, + bytes: bstr, +} + +verifier-report = { + apiVersion: "echo.verifier-report/v1", + targetIr: resource-ref, + outcome: "accepted" / "rejected", + diagnosticAbi: resource-ref, + diagnosticBytes: bstr, +} +"#; + +/// Stable failure categories returned by provider artifact construction. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ProviderArtifactGenerationErrorKind { + /// Requested primary projection roles differed from the validated source. + ProjectionClosureMismatch, + /// A required generated or external resource could not be resolved. + ResourceClosureMismatch, + /// The self-contained provider CDDL could not be compiled or lacked a root. + SchemaGenerationFailed, + /// A provider value could not be encoded as canonical CBOR. + CanonicalEncodingFailed, + /// Canonical bytes did not satisfy their generated or Edict-owned root. + OwningRootRejected, + /// A raw or domain-framed digest could not be constructed. + DigestConstructionFailed, + /// A validated semantic declaration needed by projection was absent. + SemanticProjectionMismatch, + /// Wesley rejected an emitted exact-byte content reference. + WesleyContractRejected, +} + +impl ProviderArtifactGenerationErrorKind { + const fn label(self) -> &'static str { + match self { + Self::ProjectionClosureMismatch => "projection-closure-mismatch", + Self::ResourceClosureMismatch => "resource-closure-mismatch", + Self::SchemaGenerationFailed => "schema-generation-failed", + Self::CanonicalEncodingFailed => "canonical-encoding-failed", + Self::OwningRootRejected => "owning-root-rejected", + Self::DigestConstructionFailed => "digest-construction-failed", + Self::SemanticProjectionMismatch => "semantic-projection-mismatch", + Self::WesleyContractRejected => "wesley-contract-rejected", + } + } +} + +/// Structured, stable failure from provider artifact construction. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ProviderArtifactGenerationError { + kind: ProviderArtifactGenerationErrorKind, + subject: String, + reference: String, + canonical_kind: Option, + contract_kind: Option, + wesley_kind: Option, +} + +impl ProviderArtifactGenerationError { + /// Returns the stable high-level failure category. + #[must_use] + pub const fn kind(&self) -> ProviderArtifactGenerationErrorKind { + self.kind + } + + /// Returns the role, coordinate, or schema root that failed. + #[must_use] + pub fn subject(&self) -> &str { + &self.subject + } + + /// Returns the expected coordinate, root, or other stable reference. + #[must_use] + pub fn reference(&self) -> &str { + &self.reference + } + + /// Returns the typed canonical-value cause, when applicable. + #[must_use] + pub const fn canonical_value_kind(&self) -> Option { + self.canonical_kind + } + + /// Returns the typed upstream contract cause, when applicable. + #[must_use] + pub const fn contract_validation_kind(&self) -> Option { + self.contract_kind + } + + /// Returns the typed Wesley content-reference failure, when applicable. + #[must_use] + pub const fn wesley_contract_kind(&self) -> Option { + self.wesley_kind + } + + fn new( + kind: ProviderArtifactGenerationErrorKind, + subject: impl Into, + reference: impl Into, + ) -> Self { + Self { + kind, + subject: subject.into(), + reference: reference.into(), + canonical_kind: None, + contract_kind: None, + wesley_kind: None, + } + } + + fn canonical(subject: &str, error: CanonicalValueErrorKind) -> Self { + let mut result = Self::new( + ProviderArtifactGenerationErrorKind::CanonicalEncodingFailed, + subject, + "edict.canonical-cbor/v1", + ); + result.canonical_kind = Some(error); + result + } + + fn wesley(error: GenerationContractError) -> Self { + let kind = error.kind; + let mut result = Self::new( + ProviderArtifactGenerationErrorKind::WesleyContractRejected, + error.subject, + kind.as_str(), + ); + result.wesley_kind = Some(kind); + result + } +} + +impl fmt::Display for ProviderArtifactGenerationError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + formatter, + "provider artifact generation {}: {} -> {}", + self.kind.label(), + self.subject, + self.reference + ) + } +} + +impl std::error::Error for ProviderArtifactGenerationError {} + +/// One canonical provider output that has passed its generated owning root. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct SchemaValidatedCanonicalProviderOutputV1 { + role: String, + coordinate: String, + schema_contract: String, + owning_root: String, + digest_domain: String, + canonical_value: CanonicalValueV1, + canonical_bytes: Vec, + domain_framed_digest: String, + content_reference: GenerationArtifactReferenceV1, +} + +impl SchemaValidatedCanonicalProviderOutputV1 { + /// Returns the source-local artifact or resource role. + #[must_use] + pub fn role(&self) -> &str { + &self.role + } + /// Returns the stable artifact coordinate. + #[must_use] + pub fn coordinate(&self) -> &str { + &self.coordinate + } + /// Returns the declared ABI or schema contract. + #[must_use] + pub fn schema_contract(&self) -> &str { + &self.schema_contract + } + /// Returns the exact CDDL root used to validate the value. + #[must_use] + pub fn owning_root(&self) -> &str { + &self.owning_root + } + /// Returns the domain used for the Edict digest frame. + #[must_use] + pub fn digest_domain(&self) -> &str { + &self.digest_domain + } + /// Returns the admitted canonical value. + #[must_use] + pub const fn canonical_value(&self) -> &CanonicalValueV1 { + &self.canonical_value + } + /// Returns exact Edict canonical-CBOR bytes. + #[must_use] + pub fn canonical_bytes(&self) -> &[u8] { + &self.canonical_bytes + } + /// Returns the Edict domain-framed digest. + #[must_use] + pub fn domain_framed_digest(&self) -> &str { + &self.domain_framed_digest + } + /// Returns the Wesley exact-byte content reference. + #[must_use] + pub const fn content_reference(&self) -> &GenerationArtifactReferenceV1 { + &self.content_reference + } +} + +/// Self-contained CDDL emitted alongside the provider artifact closure. +#[derive(Clone)] +pub struct GeneratedProviderSchemaV1 { + role: String, + coordinate: String, + bytes: Vec, + content_reference: GenerationArtifactReferenceV1, + context: Arc, +} + +impl fmt::Debug for GeneratedProviderSchemaV1 { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("GeneratedProviderSchemaV1") + .field("role", &self.role) + .field("coordinate", &self.coordinate) + .field("bytes", &self.bytes) + .field("content_reference", &self.content_reference) + .finish_non_exhaustive() + } +} + +impl PartialEq for GeneratedProviderSchemaV1 { + fn eq(&self, other: &Self) -> bool { + self.role == other.role + && self.coordinate == other.coordinate + && self.bytes == other.bytes + && self.content_reference == other.content_reference + } +} + +impl Eq for GeneratedProviderSchemaV1 {} + +impl GeneratedProviderSchemaV1 { + /// Returns the generated artifact role. + #[must_use] + pub fn role(&self) -> &str { + &self.role + } + /// Returns the generated schema coordinate. + #[must_use] + pub fn coordinate(&self) -> &str { + &self.coordinate + } + /// Returns exact self-contained UTF-8 CDDL bytes. + #[must_use] + pub fn bytes(&self) -> &[u8] { + &self.bytes + } + /// Returns the exact-byte schema content reference. + #[must_use] + pub const fn content_reference(&self) -> &GenerationArtifactReferenceV1 { + &self.content_reference + } + + /// Revalidates one canonical provider output against its declared root. + /// + /// # Errors + /// + /// Returns a stable error when the root is absent, bytes are not exact + /// canonical CBOR, or the decoded value fails its owning CDDL rule. + pub fn validate_output( + &self, + output: &SchemaValidatedCanonicalProviderOutputV1, + ) -> Result { + validate_schema_bytes(&self.context, &output.owning_root, &output.canonical_bytes) + } + + /// Authenticates exact canonical bytes against one named provider root. + /// + /// This is the raw component-boundary form of [`Self::validate_output`]. It + /// validates encoding and schema shape only; it does not install an + /// artifact or grant Echo runtime authority. + /// + /// # Errors + /// + /// Returns a stable error when the root is absent, the bytes are not exact + /// Edict canonical CBOR, or the decoded value fails the selected CDDL rule. + pub fn validate_root_bytes( + &self, + root: &str, + bytes: &[u8], + ) -> Result { + validate_schema_bytes(&self.context, root, bytes) + } +} + +/// Complete first-slice primary provider artifact closure. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ProviderPrimaryArtifactsV1 { + generation_input_digest: String, + projection_roles: Vec, + artifacts: Vec, + resources: Vec, + schema: GeneratedProviderSchemaV1, +} + +impl ProviderPrimaryArtifactsV1 { + /// Returns the exact Wesley generation input that produced this closure. + #[must_use] + pub fn generation_input_digest(&self) -> &str { + &self.generation_input_digest + } + /// Returns the exact sorted primary role closure, including the schema. + #[must_use] + pub fn projection_roles(&self) -> &[String] { + &self.projection_roles + } + /// Returns the five canonical-CBOR primary artifacts in role order. + #[must_use] + pub fn artifacts(&self) -> &[SchemaValidatedCanonicalProviderOutputV1] { + &self.artifacts + } + /// Returns the fourteen generated canonical resources in role order. + #[must_use] + pub fn resources(&self) -> &[SchemaValidatedCanonicalProviderOutputV1] { + &self.resources + } + /// Returns the generated self-contained CDDL artifact. + #[must_use] + pub const fn schema(&self) -> &GeneratedProviderSchemaV1 { + &self.schema + } + /// Looks up one primary canonical artifact by role. + #[must_use] + pub fn artifact(&self, role: &str) -> Option<&SchemaValidatedCanonicalProviderOutputV1> { + self.artifacts.iter().find(|artifact| artifact.role == role) + } + /// Looks up one generated canonical resource by role. + #[must_use] + pub fn resource(&self, role: &str) -> Option<&SchemaValidatedCanonicalProviderOutputV1> { + self.resources.iter().find(|resource| resource.role == role) + } +} + +/// Generates the deterministic primary provider artifact closure from explicit input. +/// +/// The function is pure: it performs no filesystem, registry, environment, +/// process, clock, or network discovery. The returned values are provider +/// semantic descriptions, not Echo runtime admissions or grants. +/// +/// # Errors +/// +/// Returns a structured error if the requested projection closure disagrees +/// with the validated semantic source, a required resource cannot be resolved, +/// schema construction fails, canonical encoding fails, or any emitted value +/// is rejected by its owning CDDL root. +pub fn generate_provider_primary_artifacts_v1( + input: &ProviderGenerationInputV1, + contract_pack: &AdmittedProviderContractPackV1, +) -> Result { + let source = input.semantic_source().source(); + let projection_roles = expected_projection_roles(source); + if input.wesley_input().projection_roles != projection_roles { + return Err(ProviderArtifactGenerationError::new( + ProviderArtifactGenerationErrorKind::ProjectionClosureMismatch, + "projectionRoles", + projection_roles.join(","), + )); + } + let schema = build_schema(source, contract_pack)?; + + let mut resources = Vec::new(); + for declaration in source + .artifact_resources + .iter() + .filter(|resource| resource.provision == ArtifactResourceProvision::Generated) + { + let value = build_resource_value(source, declaration)?; + resources.push(make_output( + declaration.role.clone(), + declaration.coordinate.clone(), + declaration.schema_contract.clone(), + resource_root(&declaration.role)?, + declaration.coordinate.clone(), + value, + resource_edict_contract(&declaration.role), + &schema, + contract_pack, + )?); + } + resources.sort_by(|left, right| left.role.cmp(&right.role)); + if resources.len() != 14 { + return Err(ProviderArtifactGenerationError::new( + ProviderArtifactGenerationErrorKind::ResourceClosureMismatch, + "generatedResources", + "14", + )); + } + + let profile_declaration = + artifact_by_kind(source, GeneratedArtifactKind::GeneratedArtifactProfile)?; + let profile = make_output( + profile_declaration.role.clone(), + profile_declaration.coordinate.clone(), + profile_declaration.schema_contract.clone(), + GENERATED_PROFILE_ROOT, + GENERATED_PROFILE_DOMAIN.to_owned(), + build_generated_profile(source)?, + None, + &schema, + contract_pack, + )?; + + let target_declaration = artifact_by_kind(source, GeneratedArtifactKind::TargetProfile)?; + let target = make_output( + target_declaration.role.clone(), + target_declaration.coordinate.clone(), + target_declaration.schema_contract.clone(), + "target-profile-manifest", + "edict.target-profile/v1".to_owned(), + build_target_profile(source, &resources, &profile, contract_pack)?, + Some("target-profile-manifest"), + &schema, + contract_pack, + )?; + + let lawpack_declaration = artifact_by_kind(source, GeneratedArtifactKind::Lawpack)?; + let lawpack = make_output( + lawpack_declaration.role.clone(), + lawpack_declaration.coordinate.clone(), + lawpack_declaration.schema_contract.clone(), + "lawpack-manifest", + "edict.lawpack/v1".to_owned(), + build_lawpack(source, &resources, &target)?, + Some("lawpack-manifest"), + &schema, + contract_pack, + )?; + + let mut artifacts = vec![profile, target, lawpack]; + for declaration in source + .generated_artifacts + .iter() + .filter(|artifact| artifact.kind == GeneratedArtifactKind::AuthorityFacts) + { + let fact_source = declaration.authority_fact_source.as_ref().ok_or_else(|| { + ProviderArtifactGenerationError::new( + ProviderArtifactGenerationErrorKind::SemanticProjectionMismatch, + &declaration.role, + "authorityFactSource", + ) + })?; + let source_artifact = match fact_source.kind { + AuthorityFactSourceKind::TargetProfile => artifacts + .iter() + .find(|artifact| artifact.role == source.target_profile_projection.artifact_role), + AuthorityFactSourceKind::Lawpack => artifacts + .iter() + .find(|artifact| artifact.role == source.lawpack_projection.artifact_role), + } + .ok_or_else(|| { + ProviderArtifactGenerationError::new( + ProviderArtifactGenerationErrorKind::ProjectionClosureMismatch, + &declaration.role, + &fact_source.coordinate, + ) + })?; + artifacts.push(make_output( + declaration.role.clone(), + declaration.coordinate.clone(), + declaration.schema_contract.clone(), + "authority-facts", + "edict.authority-facts/v1".to_owned(), + build_authority_facts(source, declaration, source_artifact)?, + Some("authority-facts"), + &schema, + contract_pack, + )?); + } + artifacts.sort_by(|left, right| left.role.cmp(&right.role)); + if artifacts.len() != 5 { + return Err(ProviderArtifactGenerationError::new( + ProviderArtifactGenerationErrorKind::ProjectionClosureMismatch, + "canonicalPrimaryArtifacts", + "5", + )); + } + + Ok(ProviderPrimaryArtifactsV1 { + generation_input_digest: input.digest().to_owned(), + projection_roles, + artifacts, + resources, + schema, + }) +} + +fn expected_projection_roles(source: &ProviderSemanticSourceV1) -> Vec { + source + .generated_artifacts + .iter() + .filter(|artifact| { + !matches!( + artifact.kind, + GeneratedArtifactKind::GenerationProvenance | GeneratedArtifactKind::ReviewArtifact + ) + }) + .map(|artifact| artifact.role.clone()) + .collect() +} + +fn artifact_by_kind( + source: &ProviderSemanticSourceV1, + kind: GeneratedArtifactKind, +) -> Result<&GeneratedArtifactDeclaration, ProviderArtifactGenerationError> { + source + .generated_artifacts + .iter() + .find(|artifact| artifact.kind == kind) + .ok_or_else(|| { + ProviderArtifactGenerationError::new( + ProviderArtifactGenerationErrorKind::ProjectionClosureMismatch, + "generatedArtifacts", + artifact_kind_label(kind), + ) + }) +} + +const fn artifact_kind_label(kind: GeneratedArtifactKind) -> &'static str { + match kind { + GeneratedArtifactKind::Lawpack => "lawpack", + GeneratedArtifactKind::TargetProfile => "target-profile", + GeneratedArtifactKind::AuthorityFacts => "authority-facts", + GeneratedArtifactKind::ProviderManifest => "provider-manifest", + GeneratedArtifactKind::ReviewArtifact => "review-artifact", + GeneratedArtifactKind::GeneratedArtifactProfile => "generated-artifact-profile", + GeneratedArtifactKind::GenerationProvenance => "generation-provenance", + GeneratedArtifactKind::ArtifactSchema => "artifact-schema", + } +} + +fn build_schema( + source: &ProviderSemanticSourceV1, + contract_pack: &AdmittedProviderContractPackV1, +) -> Result { + let declaration = artifact_by_kind(source, GeneratedArtifactKind::ArtifactSchema)?; + if declaration.role != SCHEMA_ROLE || declaration.coordinate != GENERATED_SCHEMA_DOMAIN { + return Err(ProviderArtifactGenerationError::new( + ProviderArtifactGenerationErrorKind::ProjectionClosureMismatch, + &declaration.role, + GENERATED_SCHEMA_DOMAIN, + )); + } + let mut bytes = contract_pack.schema_bytes().to_vec(); + bytes.extend_from_slice(PROVIDER_SCHEMA_SUFFIX.as_bytes()); + let admitted_schema = std::str::from_utf8(contract_pack.schema_bytes()).map_err(|_| { + ProviderArtifactGenerationError::new( + ProviderArtifactGenerationErrorKind::SchemaGenerationFailed, + contract_pack.coordinate(), + "utf-8", + ) + })?; + let admitted_rules = flatten_from_str(admitted_schema).map_err(|_| { + ProviderArtifactGenerationError::new( + ProviderArtifactGenerationErrorKind::SchemaGenerationFailed, + contract_pack.coordinate(), + "compiled-admitted-cddl", + ) + })?; + let generated_rules = flatten_from_str(PROVIDER_SCHEMA_SUFFIX).map_err(|_| { + ProviderArtifactGenerationError::new( + ProviderArtifactGenerationErrorKind::SchemaGenerationFailed, + &declaration.coordinate, + "compiled-generated-cddl", + ) + })?; + if let Some(rule) = generated_rules + .keys() + .find(|rule| admitted_rules.contains_key(*rule)) + { + return Err(ProviderArtifactGenerationError::new( + ProviderArtifactGenerationErrorKind::SchemaGenerationFailed, + rule, + "duplicate-cddl-rule", + )); + } + let text = std::str::from_utf8(&bytes).map_err(|_| { + ProviderArtifactGenerationError::new( + ProviderArtifactGenerationErrorKind::SchemaGenerationFailed, + &declaration.coordinate, + "utf-8", + ) + })?; + let rules = flatten_from_str(text).map_err(|_| { + ProviderArtifactGenerationError::new( + ProviderArtifactGenerationErrorKind::SchemaGenerationFailed, + &declaration.coordinate, + "compiled-cddl", + ) + })?; + let context = Arc::new(BasicContext::new(rules)); + for root in required_generated_roots(source) { + if !context.rules.contains_key(root) { + return Err(ProviderArtifactGenerationError::new( + ProviderArtifactGenerationErrorKind::SchemaGenerationFailed, + root, + &declaration.coordinate, + )); + } + } + validate_invocation_root_fixtures(&context)?; + let content_reference = + GenerationArtifactReferenceV1::for_bytes(declaration.coordinate.clone(), &bytes) + .map_err(ProviderArtifactGenerationError::wesley)?; + Ok(GeneratedProviderSchemaV1 { + role: declaration.role.clone(), + coordinate: declaration.coordinate.clone(), + bytes, + content_reference, + context, + }) +} + +fn validate_invocation_root_fixtures( + context: &BasicContext, +) -> Result<(), ProviderArtifactGenerationError> { + let zero_digest = format!("sha256:{}", "00".repeat(32)); + let reference = resource_reference("echo.fixture@1", &zero_digest)?; + for (root, value) in [ + ( + "generated-artifact", + json!({ + "apiVersion": "echo.generated-artifact/v1", + "profile": reference, + "operation": "echo.fixture@1.operation", + "mediaType": "application/cbor", + "bytes": { "$canonicalBytes": "00" }, + }), + ), + ( + "review-payload", + json!({ + "apiVersion": "echo.review-payload/v1", + "authoritative": false, + "subject": reference, + "mediaType": "application/json", + "bytes": { "$canonicalBytes": "00" }, + }), + ), + ( + "verifier-report", + json!({ + "apiVersion": "echo.verifier-report/v1", + "targetIr": reference, + "outcome": "accepted", + "diagnosticAbi": reference, + "diagnosticBytes": { "$canonicalBytes": "" }, + }), + ), + ] { + let canonical = canonical_from_json(value, root)?; + let bytes = encode_canonical_cbor_v1(&canonical) + .map_err(|error| ProviderArtifactGenerationError::canonical(root, error.kind()))?; + validate_schema_bytes(context, root, &bytes)?; + } + Ok(()) +} + +fn required_generated_roots(source: &ProviderSemanticSourceV1) -> Vec<&str> { + let mut roots = source + .artifact_resources + .iter() + .filter(|resource| resource.provision == ArtifactResourceProvision::Generated) + .filter_map(|resource| resource_root(&resource.role).ok()) + .collect::>(); + roots.extend([ + GENERATED_PROFILE_ROOT, + "lawpack-manifest", + "target-profile-manifest", + "authority-facts", + ]); + roots.extend( + source + .schema_bindings + .iter() + .map(|binding| binding.root_rule.as_str()), + ); + roots.sort_unstable(); + roots.dedup(); + roots +} + +fn resource_root(role: &str) -> Result<&'static str, ProviderArtifactGenerationError> { + let root = match role { + "resource.conformance-corpus" => "echo-provider-conformance-corpus", + "resource.lawpack-compatibility" => "echo-provider-lawpack-compatibility", + "resource.lawpack-exports" => "lawpack-exports", + "resource.lawpack-target-adapter" => "echo-provider-lawpack-target-adapter", + "resource.lawpack-verifier" => "echo-provider-lawpack-verifier", + "resource.target-bundle-profile" => "echo-dpo-bundle", + "resource.target-cost-algebra" => "echo-dpo-cost", + "resource.target-footprint-algebra" => "echo-dpo-footprint", + "resource.target-intrinsics" => "intrinsics-document", + "resource.target-ir" => "echo-span-ir", + "resource.target-lowerer-contract" => "echo-dpo-lowerer", + "resource.target-obstruction-taxonomy" => "echo-dpo-obstructions", + "resource.target-operation-profiles" => "operation-profiles-document", + "resource.target-verifier-contract" => "echo-dpo-verifier", + _ => { + return Err(ProviderArtifactGenerationError::new( + ProviderArtifactGenerationErrorKind::ResourceClosureMismatch, + role, + "generated-resource-root", + )); + } + }; + Ok(root) +} + +fn resource_edict_contract(role: &str) -> Option<&'static str> { + match role { + "resource.lawpack-exports" => Some("lawpack-exports"), + "resource.target-intrinsics" => Some("target-profile-intrinsics"), + "resource.target-operation-profiles" => Some("target-profile-operation-profiles"), + _ => None, + } +} + +#[allow(clippy::too_many_arguments)] +fn make_output( + role: String, + coordinate: String, + schema_contract: String, + owning_root: impl Into, + digest_domain: String, + json_value: JsonValue, + edict_contract: Option<&str>, + schema: &GeneratedProviderSchemaV1, + contract_pack: &AdmittedProviderContractPackV1, +) -> Result { + let owning_root = owning_root.into(); + let canonical_value = canonical_from_json(json_value, &role)?; + let canonical_bytes = encode_canonical_cbor_v1(&canonical_value) + .map_err(|error| ProviderArtifactGenerationError::canonical(&role, error.kind()))?; + validate_schema_bytes(&schema.context, &owning_root, &canonical_bytes)?; + if let Some(contract) = edict_contract { + contract_pack + .validate_contract_bytes(contract, &canonical_bytes) + .map_err(|error| { + let mut result = ProviderArtifactGenerationError::new( + ProviderArtifactGenerationErrorKind::OwningRootRejected, + &role, + contract, + ); + result.contract_kind = Some(error.kind()); + result + })?; + } + let domain_framed_digest = digest_canonical_value_v1(&digest_domain, &canonical_value) + .map_err(|error| { + let mut result = ProviderArtifactGenerationError::new( + ProviderArtifactGenerationErrorKind::DigestConstructionFailed, + &role, + &digest_domain, + ); + result.canonical_kind = Some(error.kind()); + result + })?; + let content_reference = + GenerationArtifactReferenceV1::for_bytes(coordinate.clone(), &canonical_bytes) + .map_err(ProviderArtifactGenerationError::wesley)?; + Ok(SchemaValidatedCanonicalProviderOutputV1 { + role, + coordinate, + schema_contract, + owning_root, + digest_domain, + canonical_value, + canonical_bytes, + domain_framed_digest, + content_reference, + }) +} + +fn validate_schema_bytes( + context: &BasicContext, + root: &str, + bytes: &[u8], +) -> Result { + let value = decode_canonical_cbor_v1(bytes).map_err(|error| { + let mut result = ProviderArtifactGenerationError::new( + ProviderArtifactGenerationErrorKind::CanonicalEncodingFailed, + root, + "edict.canonical-cbor/v1", + ); + result.canonical_kind = Some(error.kind()); + result + })?; + let cbor_value: ciborium::Value = ciborium::from_reader(bytes).map_err(|_| { + ProviderArtifactGenerationError::new( + ProviderArtifactGenerationErrorKind::CanonicalEncodingFailed, + root, + "cbor-value", + ) + })?; + let rule = context.rules.get(root).ok_or_else(|| { + ProviderArtifactGenerationError::new( + ProviderArtifactGenerationErrorKind::SchemaGenerationFailed, + root, + "owning-root", + ) + })?; + validate_cbor(rule, &cbor_value, context).map_err(|_| { + ProviderArtifactGenerationError::new( + ProviderArtifactGenerationErrorKind::OwningRootRejected, + root, + "generated-provider-schema", + ) + })?; + Ok(value) +} + +fn canonical_from_json( + value: JsonValue, + subject: &str, +) -> Result { + match value { + JsonValue::Null => Ok(CanonicalValueV1::Null), + JsonValue::Bool(value) => Ok(CanonicalValueV1::Bool(value)), + JsonValue::Number(value) => value + .as_u64() + .map(|value| CanonicalValueV1::Integer(i128::from(value))) + .or_else(|| { + value + .as_i64() + .map(|value| CanonicalValueV1::Integer(i128::from(value))) + }) + .ok_or_else(|| { + ProviderArtifactGenerationError::new( + ProviderArtifactGenerationErrorKind::CanonicalEncodingFailed, + subject, + "integer-only-json", + ) + }), + JsonValue::String(value) => Ok(CanonicalValueV1::Text(value)), + JsonValue::Array(values) => values + .into_iter() + .map(|value| canonical_from_json(value, subject)) + .collect::, _>>() + .map(CanonicalValueV1::Array), + JsonValue::Object(mut entries) => { + if entries.len() == 1 { + if let Some(JsonValue::String(bytes)) = entries.remove("$canonicalBytes") { + return hex::decode(bytes) + .map(CanonicalValueV1::Bytes) + .map_err(|_| { + ProviderArtifactGenerationError::new( + ProviderArtifactGenerationErrorKind::DigestConstructionFailed, + subject, + "lowercase-hex-bytes", + ) + }); + } + } + entries + .into_iter() + .map(|(key, value)| { + Ok(( + CanonicalValueV1::Text(key), + canonical_from_json(value, subject)?, + )) + }) + .collect::, _>>() + .map(CanonicalValueV1::Map) + } + } +} + +fn build_target_profile( + source: &ProviderSemanticSourceV1, + resources: &[SchemaValidatedCanonicalProviderOutputV1], + generated_profile: &SchemaValidatedCanonicalProviderOutputV1, + contract_pack: &AdmittedProviderContractPackV1, +) -> Result { + let target = &source.target_profile_projection; + if target.generated_artifact_profile_roles != [generated_profile.role.clone()] { + return Err(ProviderArtifactGenerationError::new( + ProviderArtifactGenerationErrorKind::ProjectionClosureMismatch, + "targetProfile.generatedArtifactProfiles", + &generated_profile.role, + )); + } + let mut value = JsonMap::new(); + value.insert("apiVersion".to_owned(), json!("edict.target-profile/v1")); + value.insert("id".to_owned(), json!(target.id)); + value.insert("version".to_owned(), json!(target.version)); + value.insert( + "acceptedCoreAbi".to_owned(), + json!(target.accepted_core_abis), + ); + for (field, role) in [ + ("intrinsics", target.intrinsics_resource.as_str()), + ( + "operationProfiles", + target.operation_profiles_resource.as_str(), + ), + ( + "footprintAlgebra", + target.footprint_algebra_resource.as_str(), + ), + ("costAlgebra", target.cost_algebra_resource.as_str()), + ("targetIr", target.target_ir_resource.as_str()), + ( + "obstructionTaxonomy", + target.obstruction_taxonomy_resource.as_str(), + ), + ("verifier", target.verifier_resource.as_str()), + ("lowerer", target.lowerer_resource.as_str()), + ("sandbox", target.sandbox_resource.as_str()), + ("fuelModel", target.fuel_model_resource.as_str()), + ("bundleProfile", target.bundle_profile_resource.as_str()), + ( + "canonicalEncodingRules", + target.canonical_encoding_rules_resource.as_str(), + ), + ("diagnosticAbi", target.diagnostic_abi_resource.as_str()), + ( + "deterministicExecution", + target.deterministic_execution_resource.as_str(), + ), + ( + "conformanceFixtureCorpus", + target.conformance_fixture_corpus_resource.as_str(), + ), + ] { + value.insert( + field.to_owned(), + resource_reference_for_role(source, resources, contract_pack, role)?, + ); + } + value.insert( + "intrinsicNamespace".to_owned(), + json!(target.intrinsic_namespace), + ); + value.insert( + "generatedArtifactProfiles".to_owned(), + JsonValue::Array(vec![output_reference(generated_profile)?]), + ); + if !target.accepted_lawpack_adapter_abis.is_empty() { + return Err(ProviderArtifactGenerationError::new( + ProviderArtifactGenerationErrorKind::SemanticProjectionMismatch, + "targetProfile.acceptedLawpackAdapterAbi", + "empty-v1-reservation", + )); + } + value.insert( + "applicationModel".to_owned(), + json!(target.application_model), + ); + value.insert("readConsistency".to_owned(), json!(target.read_consistency)); + value.insert("guardEvaluation".to_owned(), json!(target.guard_evaluation)); + value.insert( + "obstructionRollback".to_owned(), + json!(target.obstruction_rollback), + ); + value.insert("multiTarget".to_owned(), json!(target.multi_target)); + value.insert( + "postconditionSupport".to_owned(), + json!(target.postcondition_support), + ); + Ok(JsonValue::Object(value)) +} + +fn build_lawpack( + source: &ProviderSemanticSourceV1, + resources: &[SchemaValidatedCanonicalProviderOutputV1], + target_profile: &SchemaValidatedCanonicalProviderOutputV1, +) -> Result { + let lawpack = &source.lawpack_projection; + if !lawpack.dependencies.is_empty() { + return Err(ProviderArtifactGenerationError::new( + ProviderArtifactGenerationErrorKind::SemanticProjectionMismatch, + "lawpack.dependencies", + "empty-first-closure", + )); + } + let adapters = lawpack + .target_adapters + .iter() + .map(|adapter| { + if adapter.accepted_target_profile_role != target_profile.role { + return Err(ProviderArtifactGenerationError::new( + ProviderArtifactGenerationErrorKind::ProjectionClosureMismatch, + &adapter.accepted_target_profile_role, + &target_profile.role, + )); + } + Ok(json!({ + "acceptedTargetProfile": output_reference(target_profile)?, + "acceptedTargetIr": generated_resource_reference( + source, + resources, + &adapter.accepted_target_ir_resource, + )?, + "adapter": generated_resource_reference( + source, + resources, + &adapter.adapter_resource, + )?, + })) + }) + .collect::, ProviderArtifactGenerationError>>()?; + let LawpackVerifierDeclaration::Declarative { ruleset_resource } = &lawpack.verifier; + Ok(json!({ + "apiVersion": "edict.lawpack/v1", + "id": lawpack.id, + "version": lawpack.version, + "acceptedCoreAbi": lawpack.accepted_core_abis, + "dependencies": [], + "exports": generated_resource_reference(source, resources, &lawpack.exports_resource)?, + "targetAdapters": adapters, + "verifier": { + "class": "declarative", + "ruleset": generated_resource_reference(source, resources, ruleset_resource)?, + }, + "compatibility": generated_resource_reference( + source, + resources, + &lawpack.compatibility_resource, + )?, + "conformanceFixtureCorpus": generated_resource_reference( + source, + resources, + &lawpack.conformance_fixture_corpus_resource, + )?, + })) +} + +fn build_authority_facts( + source: &ProviderSemanticSourceV1, + declaration: &GeneratedArtifactDeclaration, + source_artifact: &SchemaValidatedCanonicalProviderOutputV1, +) -> Result { + let fact_source = declaration.authority_fact_source.as_ref().ok_or_else(|| { + ProviderArtifactGenerationError::new( + ProviderArtifactGenerationErrorKind::SemanticProjectionMismatch, + &declaration.role, + "authorityFactSource", + ) + })?; + if fact_source.coordinate != source_artifact.coordinate { + return Err(ProviderArtifactGenerationError::new( + ProviderArtifactGenerationErrorKind::ProjectionClosureMismatch, + &fact_source.coordinate, + &source_artifact.coordinate, + )); + } + let (kind, operation_profiles, effect_write_classes, budgets) = match fact_source.kind { + AuthorityFactSourceKind::TargetProfile => ( + "targetProfile", + target_authority_profiles(source), + target_effect_write_classes(source)?, + JsonValue::Object(JsonMap::new()), + ), + AuthorityFactSourceKind::Lawpack => ( + "lawpack", + JsonValue::Object(JsonMap::new()), + JsonValue::Object(JsonMap::new()), + lawpack_authority_budgets(source), + ), + }; + Ok(json!({ + "apiVersion": "edict.authority-facts/v1", + "source": { + "kind": kind, + "coordinate": fact_source.coordinate, + "digest": typed_digest(&source_artifact.domain_framed_digest)?, + }, + "operationProfiles": operation_profiles, + "effectWriteClasses": effect_write_classes, + "budgets": budgets, + })) +} + +fn target_authority_profiles(source: &ProviderSemanticSourceV1) -> JsonValue { + let mut profiles = JsonMap::new(); + for profile in &source.profiles { + let allowed = profile + .allowed_write_classes + .iter() + .map(|write_class| (write_class.clone(), JsonValue::Null)) + .collect::>(); + for source_name in &profile.source_names { + profiles.insert( + source_name.clone(), + json!({ + "core": profile.identity.coordinate, + "allowedWriteClasses": allowed, + }), + ); + } + } + JsonValue::Object(profiles) +} + +fn target_effect_write_classes( + source: &ProviderSemanticSourceV1, +) -> Result { + let mut write_classes = JsonMap::new(); + for capability in &source.capabilities { + insert_effect_projection( + &mut write_classes, + &capability.effect, + JsonValue::String(capability.write_class.clone()), + )?; + } + for adapter in &source.direct_adapters { + let capability = find_capability(source, &adapter.capability)?; + insert_effect_projection( + &mut write_classes, + &adapter.consumes_effect, + JsonValue::String(capability.write_class.clone()), + )?; + } + Ok(JsonValue::Object(write_classes)) +} + +fn lawpack_authority_budgets(source: &ProviderSemanticSourceV1) -> JsonValue { + JsonValue::Object( + source + .budgets + .iter() + .map(|budget| { + ( + budget.identity.coordinate.clone(), + json!({ + "maxSteps": budget.max_steps, + "maxAllocatedBytes": budget.max_allocated_bytes, + "maxOutputBytes": budget.max_output_bytes, + }), + ) + }) + .collect(), + ) +} + +fn resource_reference_for_role( + source: &ProviderSemanticSourceV1, + resources: &[SchemaValidatedCanonicalProviderOutputV1], + contract_pack: &AdmittedProviderContractPackV1, + role: &str, +) -> Result { + let declaration = source + .artifact_resources + .iter() + .find(|resource| resource.role == role) + .ok_or_else(|| { + ProviderArtifactGenerationError::new( + ProviderArtifactGenerationErrorKind::ResourceClosureMismatch, + role, + "artifact-resource-declaration", + ) + })?; + match declaration.provision { + ArtifactResourceProvision::Generated => { + generated_resource_reference(source, resources, role) + } + ArtifactResourceProvision::External => { + let resource = contract_pack + .resource(&declaration.coordinate) + .ok_or_else(|| { + ProviderArtifactGenerationError::new( + ProviderArtifactGenerationErrorKind::ResourceClosureMismatch, + role, + &declaration.coordinate, + ) + })?; + resource_reference(resource.coordinate(), resource.domain_framed_digest()) + } + } +} + +fn generated_resource_reference( + source: &ProviderSemanticSourceV1, + resources: &[SchemaValidatedCanonicalProviderOutputV1], + role: &str, +) -> Result { + let declaration = source + .artifact_resources + .iter() + .find(|resource| resource.role == role) + .ok_or_else(|| { + ProviderArtifactGenerationError::new( + ProviderArtifactGenerationErrorKind::ResourceClosureMismatch, + role, + "generated-resource-declaration", + ) + })?; + if declaration.provision != ArtifactResourceProvision::Generated { + return Err(ProviderArtifactGenerationError::new( + ProviderArtifactGenerationErrorKind::ResourceClosureMismatch, + role, + "generated-resource", + )); + } + let output = resources + .iter() + .find(|resource| resource.role == role) + .ok_or_else(|| { + ProviderArtifactGenerationError::new( + ProviderArtifactGenerationErrorKind::ResourceClosureMismatch, + role, + &declaration.coordinate, + ) + })?; + output_reference(output) +} + +fn output_reference( + output: &SchemaValidatedCanonicalProviderOutputV1, +) -> Result { + resource_reference(&output.coordinate, &output.domain_framed_digest) +} + +fn resource_reference( + coordinate: &str, + digest: &str, +) -> Result { + Ok(json!({ + "id": coordinate, + "digest": typed_digest(digest)?, + })) +} + +fn typed_digest(digest: &str) -> Result { + let hex_digest = digest.strip_prefix("sha256:").ok_or_else(|| { + ProviderArtifactGenerationError::new( + ProviderArtifactGenerationErrorKind::DigestConstructionFailed, + digest, + "sha256:<64-lowercase-hex>", + ) + })?; + let decoded = hex::decode(hex_digest).map_err(|_| { + ProviderArtifactGenerationError::new( + ProviderArtifactGenerationErrorKind::DigestConstructionFailed, + digest, + "sha256:<64-lowercase-hex>", + ) + })?; + if decoded.len() != 32 || hex::encode(&decoded) != hex_digest { + return Err(ProviderArtifactGenerationError::new( + ProviderArtifactGenerationErrorKind::DigestConstructionFailed, + digest, + "sha256:<64-lowercase-hex>", + )); + } + Ok(json!(["sha256", { "$canonicalBytes": hex_digest }])) +} + +fn build_resource_value( + source: &ProviderSemanticSourceV1, + declaration: &ArtifactResourceDeclaration, +) -> Result { + match declaration.role.as_str() { + "resource.conformance-corpus" => Ok(json!({ + "apiVersion": "echo.edict-provider.conformance-corpus/v1", + "class": "declarative", + "operations": coordinate_set(source.operations.iter().map(|item| &item.identity.coordinate)), + "capabilities": coordinate_set(source.capabilities.iter().map(|item| &item.identity.coordinate)), + "semanticEffects": coordinate_set(source.effects.iter().map(|item| &item.identity.coordinate)), + "cases": [], + })), + "resource.lawpack-compatibility" => Ok(json!({ + "apiVersion": "echo.edict-provider.lawpack-compatibility/v1", + "class": "declarative", + "acceptedCoreAbi": coordinate_set(source.lawpack_projection.accepted_core_abis.iter()), + "acceptedTargetProfiles": string_set([format!( + "{}@{}", + source.target_profile_projection.id, + source.target_profile_projection.version + )]), + "semanticEffects": coordinate_set(source.effects.iter().map(|item| &item.identity.coordinate)), + })), + "resource.lawpack-exports" => build_lawpack_exports(source), + "resource.lawpack-target-adapter" => build_target_adapter(source, &declaration.role), + "resource.lawpack-verifier" => Ok(build_lawpack_verifier(source)), + "resource.target-bundle-profile" => Ok(json!({ + "apiVersion": "echo.dpo.bundle/v1", + "class": "declarative", + "applicationModel": source.target_profile_projection.application_model, + "readConsistency": source.target_profile_projection.read_consistency, + "operationProfiles": coordinate_set(source.profiles.iter().map(|item| &item.identity.coordinate)), + })), + "resource.target-cost-algebra" => Ok(build_cost_algebra(source)), + "resource.target-footprint-algebra" => Ok(build_footprint_algebra(source)), + "resource.target-intrinsics" => build_intrinsics(source), + "resource.target-ir" => Ok(json!({ + "apiVersion": "echo.span-ir/v1", + "class": "declarative", + "domain": declaration.coordinate, + "targetProfile": format!( + "{}@{}", + source.target_profile_projection.id, + source.target_profile_projection.version + ), + "capabilities": coordinate_set(source.capabilities.iter().map(|item| &item.identity.coordinate)), + })), + "resource.target-lowerer-contract" => build_lowerer_contract(source), + "resource.target-obstruction-taxonomy" => Ok(build_obstruction_taxonomy(source)), + "resource.target-operation-profiles" => build_operation_profiles(source), + "resource.target-verifier-contract" => build_verifier_contract(source), + _ => Err(ProviderArtifactGenerationError::new( + ProviderArtifactGenerationErrorKind::ResourceClosureMismatch, + &declaration.role, + &declaration.coordinate, + )), + } +} + +fn coordinate_set<'a>(values: impl Iterator) -> JsonValue { + string_set(values.cloned()) +} + +fn string_set(values: impl IntoIterator) -> JsonValue { + JsonValue::Object( + values + .into_iter() + .map(|value| (value, JsonValue::Null)) + .collect(), + ) +} + +fn build_lawpack_exports( + source: &ProviderSemanticSourceV1, +) -> Result { + let types = source + .types + .iter() + .filter_map(|declaration| { + declaration.shape.core_type_coordinate().map(|definition| { + json!({ + "coordinate": declaration.identity.coordinate, + "definition": definition, + }) + }) + }) + .collect::>(); + let effects = source + .effects + .iter() + .map(|effect| { + let input_type = effect.parameter_types.first().ok_or_else(|| { + ProviderArtifactGenerationError::new( + ProviderArtifactGenerationErrorKind::SemanticProjectionMismatch, + &effect.identity.coordinate, + "one-effect-input-type", + ) + })?; + Ok(json!({ + "coordinate": effect.identity.coordinate, + "typeParameters": [], + "inputType": input_type, + "outputType": effect.result_type, + "executionClass": execution_class(effect.execution_class), + "effectKindHint": effect_kind(effect.effect_kind_hint), + "footprintObligation": effect.footprint_obligation, + "costObligation": effect.cost_obligation, + "effectFailures": effect_failure_map(effect), + "guardSupport": effect.guard_support, + })) + }) + .collect::, ProviderArtifactGenerationError>>()?; + let obstructions = source + .obstructions + .iter() + .map(|obstruction| { + json!({ + "coordinate": obstruction.identity.coordinate, + "authorityClass": authority_class(obstruction.authority_class), + "payloadSchema": obstruction.payload_schema, + }) + }) + .collect::>(); + Ok(json!({ + "types": types, + "constants": [], + "pureFunctions": [], + "effects": effects, + "obstructions": obstructions, + "operationProfiles": {}, + })) +} + +fn build_target_adapter( + source: &ProviderSemanticSourceV1, + resource_role: &str, +) -> Result { + let target_ir_domain = one_target_ir_domain(source)?; + let adapter = source + .lawpack_projection + .target_adapters + .iter() + .find(|adapter| adapter.adapter_resource == resource_role) + .ok_or_else(|| { + ProviderArtifactGenerationError::new( + ProviderArtifactGenerationErrorKind::SemanticProjectionMismatch, + resource_role, + "lawpack-target-adapter", + ) + })?; + Ok(json!({ + "apiVersion": "echo.edict-provider.lawpack-target-adapter/v1", + "class": "declarative", + "targetProfile": format!( + "{}@{}", + source.target_profile_projection.id, + source.target_profile_projection.version + ), + "targetIrDomain": target_ir_domain, + "effectImplementations": selected_effect_implementation_map(source, &adapter.effects)?, + })) +} + +fn build_lawpack_verifier(source: &ProviderSemanticSourceV1) -> JsonValue { + let mut operations = JsonMap::new(); + for operation in &source.operations { + let mut mappings = JsonMap::new(); + for mapping in &operation.obstruction_mappings { + mappings.insert( + mapping.failure.clone(), + JsonValue::String(mapping.obstruction.clone()), + ); + } + operations.insert( + operation.identity.coordinate.clone(), + json!({ + "effect": operation.effect, + "failureMappings": mappings, + }), + ); + } + json!({ + "apiVersion": "echo.edict-provider.lawpack-verifier/v1", + "class": "declarative", + "operationObstructions": operations, + }) +} + +fn build_cost_algebra(source: &ProviderSemanticSourceV1) -> JsonValue { + let mut capabilities = JsonMap::new(); + for capability in &source.capabilities { + capabilities.insert( + capability.identity.coordinate.clone(), + json!({ + "effect": capability.effect, + "costTemplate": capability.cost_template, + "semanticObligation": capability.semantic_discharge.cost_obligation, + }), + ); + } + json!({ + "apiVersion": "echo.dpo.cost/v1", + "class": "declarative", + "capabilities": capabilities, + }) +} + +fn build_footprint_algebra(source: &ProviderSemanticSourceV1) -> JsonValue { + let mut capabilities = JsonMap::new(); + for capability in &source.capabilities { + capabilities.insert( + capability.identity.coordinate.clone(), + json!({ + "effect": capability.effect, + "footprintTemplate": capability.footprint_template, + "semanticObligation": capability.semantic_discharge.footprint_obligation, + "writeClass": capability.write_class, + }), + ); + } + json!({ + "apiVersion": "echo.dpo.footprint/v1", + "class": "declarative", + "capabilities": capabilities, + }) +} + +fn build_intrinsics( + source: &ProviderSemanticSourceV1, +) -> Result { + let mut intrinsics = JsonMap::new(); + for capability in &source.capabilities { + let effect = find_effect(source, &capability.effect)?; + intrinsics.insert( + capability.identity.coordinate.clone(), + json!({ + "intrinsicClass": "effect", + "typeParameters": [], + "argumentTypes": effect.parameter_types, + "returnType": effect.result_type, + "effectKind": effect_kind(capability.effect_kind), + "effectFailures": effect_failure_map(effect), + "guardSupport": capability.guard_support, + "footprintTemplate": capability.footprint_template, + "costTemplate": capability.cost_template, + "writeClass": capability.write_class, + "canParticipateInAtomicGuard": capability.can_participate_in_atomic_guard, + }), + ); + } + Ok(json!({ + "apiVersion": "edict.target-profile.intrinsics/v1", + "intrinsics": intrinsics, + })) +} + +fn build_lowerer_contract( + source: &ProviderSemanticSourceV1, +) -> Result { + Ok(json!({ + "apiVersion": "echo.dpo.lowerer/v1", + "class": "declarative", + "acceptedCoreAbi": coordinate_set(source.target_profile_projection.accepted_core_abis.iter()), + "outputDomain": one_target_ir_domain(source)?, + "targetProfile": format!( + "{}@{}", + source.target_profile_projection.id, + source.target_profile_projection.version + ), + "effectImplementations": effect_implementation_map(source)?, + "opticContracts": profile_optic_contracts(source), + })) +} + +fn build_obstruction_taxonomy(source: &ProviderSemanticSourceV1) -> JsonValue { + let mut effect_failures = JsonMap::new(); + for effect in &source.effects { + for failure in &effect.failures { + effect_failures.insert( + format!("{}.{}", effect.identity.coordinate, failure.key), + json!({ + "authorityClass": authority_class(failure.authority_class), + "payloadType": failure.payload_type, + }), + ); + } + } + let domain_obstructions = source + .obstructions + .iter() + .map(|obstruction| { + ( + obstruction.identity.coordinate.clone(), + json!({ + "authorityClass": authority_class(obstruction.authority_class), + "payloadSchema": obstruction.payload_schema, + }), + ) + }) + .collect::>(); + json!({ + "apiVersion": "echo.dpo.obstructions/v1", + "class": "declarative", + "effectFailures": effect_failures, + "domainObstructions": domain_obstructions, + }) +} + +fn build_operation_profiles( + source: &ProviderSemanticSourceV1, +) -> Result { + let mut profiles = JsonMap::new(); + for profile in &source.profiles { + profiles.insert( + profile.identity.coordinate.clone(), + json!({ + "opticTemplate": json_from_serializable( + &profile.optic_template, + &profile.identity.coordinate, + )?, + "effectPredicate": profile.effect_predicate, + }), + ); + } + Ok(json!({ + "apiVersion": "edict.target-profile.operation-profiles/v1", + "profiles": profiles, + })) +} + +fn build_verifier_contract( + source: &ProviderSemanticSourceV1, +) -> Result { + Ok(json!({ + "apiVersion": "echo.dpo.verifier/v1", + "class": "declarative", + "targetProfile": format!( + "{}@{}", + source.target_profile_projection.id, + source.target_profile_projection.version + ), + "targetIrDomain": one_target_ir_domain(source)?, + "capabilities": coordinate_set(source.capabilities.iter().map(|item| &item.identity.coordinate)), + "operationProfiles": coordinate_set(source.profiles.iter().map(|item| &item.identity.coordinate)), + "opticContracts": profile_optic_contracts(source), + })) +} + +fn build_generated_profile( + source: &ProviderSemanticSourceV1, +) -> Result { + let mut types = JsonMap::new(); + for declaration in &source.types { + types.insert( + declaration.identity.coordinate.clone(), + json_from_serializable(&declaration.shape, &declaration.identity.coordinate)?, + ); + } + let mut operations = JsonMap::new(); + for operation in &source.operations { + let profile = source + .profiles + .iter() + .find(|profile| profile.identity.coordinate == operation.profile) + .ok_or_else(|| { + ProviderArtifactGenerationError::new( + ProviderArtifactGenerationErrorKind::SemanticProjectionMismatch, + &operation.identity.coordinate, + &operation.profile, + ) + })?; + let invocation_kind = match profile.optic_template.optic_kind { + OpticKind::Revelation => "observer", + OpticKind::AffectReintegration => "mutation", + }; + let implementation = implementation_projection(operation)?; + let obstruction_mappings = operation + .obstruction_mappings + .iter() + .map(|mapping| { + ( + mapping.failure.clone(), + JsonValue::String(mapping.obstruction.clone()), + ) + }) + .collect::>(); + operations.insert( + operation.identity.coordinate.clone(), + json!({ + "inputType": operation.input_type, + "outputType": operation.output_type, + "effect": operation.effect, + "operationProfile": operation.profile, + "opticContract": profile.optic_contract, + "budget": operation.budget, + "invocationKind": invocation_kind, + "implementation": implementation, + "obstructionMappings": obstruction_mappings, + }), + ); + } + Ok(json!({ + "apiVersion": GENERATED_PROFILE_DOMAIN, + "targetProfile": format!( + "{}@{}", + source.target_profile_projection.id, + source.target_profile_projection.version + ), + "types": types, + "operations": operations, + })) +} + +fn implementation_projection( + operation: &crate::provider_semantics::ProviderOperationDeclaration, +) -> Result { + let serialized = + json_from_serializable(&operation.implementation, &operation.identity.coordinate)?; + let object = serialized.as_object().ok_or_else(|| { + ProviderArtifactGenerationError::new( + ProviderArtifactGenerationErrorKind::SemanticProjectionMismatch, + &operation.identity.coordinate, + "implementation-object", + ) + })?; + let kind = object + .get("kind") + .and_then(JsonValue::as_str) + .ok_or_else(|| { + ProviderArtifactGenerationError::new( + ProviderArtifactGenerationErrorKind::SemanticProjectionMismatch, + &operation.identity.coordinate, + "implementation-kind", + ) + })?; + let coordinate_field = match kind { + "native" => "capability", + "directAdapter" => "adapter", + _ => { + return Err(ProviderArtifactGenerationError::new( + ProviderArtifactGenerationErrorKind::SemanticProjectionMismatch, + &operation.identity.coordinate, + "known-implementation-kind", + )); + } + }; + let coordinate = object + .get(coordinate_field) + .and_then(JsonValue::as_str) + .ok_or_else(|| { + ProviderArtifactGenerationError::new( + ProviderArtifactGenerationErrorKind::SemanticProjectionMismatch, + &operation.identity.coordinate, + coordinate_field, + ) + })?; + Ok(json!({ "kind": kind, "coordinate": coordinate })) +} + +fn effect_failure_map(effect: &crate::provider_semantics::SemanticEffectDeclaration) -> JsonValue { + JsonValue::Object( + effect + .failures + .iter() + .map(|failure| { + ( + failure.key.clone(), + json!({ + "authorityClass": authority_class(failure.authority_class), + "payloadType": failure.payload_type, + }), + ) + }) + .collect(), + ) +} + +fn effect_implementation_map( + source: &ProviderSemanticSourceV1, +) -> Result { + let mut implementations = JsonMap::new(); + for capability in &source.capabilities { + insert_effect_projection( + &mut implementations, + &capability.effect, + json!({ + "kind": "native", + "capability": capability.identity.coordinate, + "writeClass": capability.write_class, + }), + )?; + } + for adapter in &source.direct_adapters { + let capability = find_capability(source, &adapter.capability)?; + insert_effect_projection( + &mut implementations, + &adapter.consumes_effect, + json!({ + "kind": "directAdapter", + "adapter": adapter.identity.coordinate, + "capability": capability.identity.coordinate, + "writeClass": capability.write_class, + }), + )?; + } + Ok(JsonValue::Object(implementations)) +} + +fn selected_effect_implementation_map( + source: &ProviderSemanticSourceV1, + effects: &[String], +) -> Result { + let JsonValue::Object(all) = effect_implementation_map(source)? else { + return Err(ProviderArtifactGenerationError::new( + ProviderArtifactGenerationErrorKind::SemanticProjectionMismatch, + "effectImplementations", + "object", + )); + }; + let selected = effects + .iter() + .map(|effect| { + all.get(effect).cloned().map_or_else( + || { + Err(ProviderArtifactGenerationError::new( + ProviderArtifactGenerationErrorKind::SemanticProjectionMismatch, + "lawpack.targetAdapters.effects", + effect, + )) + }, + |implementation| Ok((effect.clone(), implementation)), + ) + }) + .collect::, _>>()?; + Ok(JsonValue::Object(selected)) +} + +fn profile_optic_contracts(source: &ProviderSemanticSourceV1) -> JsonValue { + JsonValue::Object( + source + .profiles + .iter() + .map(|profile| { + ( + profile.identity.coordinate.clone(), + JsonValue::String(profile.optic_contract.clone()), + ) + }) + .collect(), + ) +} + +fn insert_effect_projection( + projections: &mut JsonMap, + effect: &str, + projection: JsonValue, +) -> Result<(), ProviderArtifactGenerationError> { + if projections.insert(effect.to_owned(), projection).is_some() { + return Err(ProviderArtifactGenerationError::new( + ProviderArtifactGenerationErrorKind::SemanticProjectionMismatch, + "effectImplementations", + effect, + )); + } + Ok(()) +} + +fn find_capability<'a>( + source: &'a ProviderSemanticSourceV1, + coordinate: &str, +) -> Result< + &'a crate::provider_semantics::TargetCapabilityDeclaration, + ProviderArtifactGenerationError, +> { + source + .capabilities + .iter() + .find(|capability| capability.identity.coordinate == coordinate) + .ok_or_else(|| { + ProviderArtifactGenerationError::new( + ProviderArtifactGenerationErrorKind::SemanticProjectionMismatch, + coordinate, + "target-capability", + ) + }) +} + +fn one_target_ir_domain( + source: &ProviderSemanticSourceV1, +) -> Result<&str, ProviderArtifactGenerationError> { + source + .capabilities + .first() + .map(|capability| capability.target_ir_domain.as_str()) + .ok_or_else(|| { + ProviderArtifactGenerationError::new( + ProviderArtifactGenerationErrorKind::SemanticProjectionMismatch, + "capabilities", + "targetIrDomain", + ) + }) +} + +fn find_effect<'a>( + source: &'a ProviderSemanticSourceV1, + coordinate: &str, +) -> Result<&'a crate::provider_semantics::SemanticEffectDeclaration, ProviderArtifactGenerationError> +{ + source + .effects + .iter() + .find(|effect| effect.identity.coordinate == coordinate) + .ok_or_else(|| { + ProviderArtifactGenerationError::new( + ProviderArtifactGenerationErrorKind::SemanticProjectionMismatch, + coordinate, + "semantic-effect", + ) + }) +} + +fn json_from_serializable( + value: &impl serde::Serialize, + subject: &str, +) -> Result { + serde_json::to_value(value).map_err(|_| { + ProviderArtifactGenerationError::new( + ProviderArtifactGenerationErrorKind::CanonicalEncodingFailed, + subject, + "semantic-json-projection", + ) + }) +} + +const fn authority_class(value: AuthorityClass) -> &'static str { + match value { + AuthorityClass::DomainMappable => "domainMappable", + AuthorityClass::ParticipantOwned => "participantOwned", + AuthorityClass::IntegrityFault => "integrityFault", + AuthorityClass::ResourceFault => "resourceFault", + AuthorityClass::InternalFault => "internalFault", + } +} + +const fn execution_class(value: ExecutionClass) -> &'static str { + match value { + ExecutionClass::ProofOnly => "proofOnly", + ExecutionClass::Runtime => "runtime", + } +} + +const fn effect_kind(value: EffectKindHint) -> &'static str { + match value { + EffectKindHint::Read => "read", + EffectKindHint::Create => "create", + EffectKindHint::Ensure => "ensure", + EffectKindHint::Replace => "replace", + EffectKindHint::Delete => "delete", + EffectKindHint::Append => "append", + EffectKindHint::Reduce => "reduce", + EffectKindHint::SemanticEmit => "semantic.emit", + EffectKindHint::Custom => "custom", + } +} diff --git a/crates/echo-wesley-gen/src/provider_canonical.rs b/crates/echo-wesley-gen/src/provider_canonical.rs new file mode 100644 index 00000000..f91f1ba0 --- /dev/null +++ b/crates/echo-wesley-gen/src/provider_canonical.rs @@ -0,0 +1,521 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Exact `edict.canonical-cbor/v1` values, bytes, and artifact digests. +//! +//! This is a pure compatibility boundary for the Edict-owned provider +//! contract. It deliberately does not reuse Echo's WASM ABI codec: that codec +//! admits a different value model. The implementation accepts only the +//! definite-length Edict v1 subset, enforces the published nesting bound, and +//! validates decoded bytes by exact canonical re-encoding. + +use std::collections::BTreeSet; +use std::fmt; +use std::str; + +use sha2::{Digest, Sha256}; + +/// Coordinate of the canonical encoding profile implemented by this module. +pub const EDICT_CANONICAL_CBOR_V1: &str = "edict.canonical-cbor/v1"; + +/// Domain marker at the head of every Edict v1 artifact digest frame. +pub const EDICT_DIGEST_FRAME_V1: &str = "edict.digest/v1"; + +/// Maximum child nesting depth accepted by the Edict v1 encoder and decoder. +/// +/// The root value is at depth zero. A scalar wrapped in exactly 128 containers +/// is accepted; one additional container is rejected. +pub const MAX_CANONICAL_NESTING_DEPTH_V1: usize = 128; + +/// Stable failure categories for Edict canonical values and byte streams. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum CanonicalValueErrorKind { + /// A value or collection length is outside the supported value model. + UnsupportedValue, + /// An integer is outside the CBOR major-zero or major-one `u64` range. + InvalidInteger, + /// The byte stream ended before one complete value was decoded. + UnexpectedEof, + /// Bytes remained after one complete canonical value. + TrailingData, + /// The byte stream used an unsupported CBOR major or additional-info form. + UnsupportedCbor, + /// The decoded value did not re-encode to the exact supplied bytes. + NonCanonical, + /// The value exceeded the published 128-level nesting bound. + NestingLimitExceeded, + /// A map contained two keys with identical canonical encodings. + DuplicateMapKey, + /// A CBOR text string was not valid UTF-8. + InvalidUtf8, +} + +impl CanonicalValueErrorKind { + const fn label(self) -> &'static str { + match self { + Self::UnsupportedValue => "unsupported-value", + Self::InvalidInteger => "invalid-integer", + Self::UnexpectedEof => "unexpected-eof", + Self::TrailingData => "trailing-data", + Self::UnsupportedCbor => "unsupported-cbor", + Self::NonCanonical => "noncanonical", + Self::NestingLimitExceeded => "nesting-limit-exceeded", + Self::DuplicateMapKey => "duplicate-map-key", + Self::InvalidUtf8 => "invalid-utf8", + } + } +} + +/// Structured canonical-value failure with a stable kind and diagnostic detail. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct CanonicalValueError { + kind: CanonicalValueErrorKind, + detail: String, +} + +impl CanonicalValueError { + /// Returns the stable machine-readable failure category. + #[must_use] + pub const fn kind(&self) -> CanonicalValueErrorKind { + self.kind + } + + /// Returns deterministic diagnostic detail for the failed boundary. + #[must_use] + pub fn detail(&self) -> &str { + &self.detail + } + + fn new(kind: CanonicalValueErrorKind, detail: impl Into) -> Self { + Self { + kind, + detail: detail.into(), + } + } +} + +impl fmt::Display for CanonicalValueError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(formatter, "{}: {}", self.kind.label(), self.detail) + } +} + +impl std::error::Error for CanonicalValueError {} + +/// Value tree admitted by `edict.canonical-cbor/v1`. +#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub enum CanonicalValueV1 { + /// CBOR null. + Null, + /// CBOR false or true. + Bool(bool), + /// CBOR major-zero or major-one integer stored in a convenient host type. + Integer(i128), + /// Definite-length byte string. + Bytes(Vec), + /// Definite-length UTF-8 text string, without Unicode normalization. + Text(String), + /// Definite-length ordered array. + Array(Vec), + /// Definite-length map sorted during encoding by canonical key bytes. + Map(Vec<(CanonicalValueV1, CanonicalValueV1)>), +} + +/// Encodes one value using the exact `edict.canonical-cbor/v1` byte contract. +/// +/// # Errors +/// +/// Returns a stable failure when an integer or collection cannot be represented, +/// a map repeats a canonical key, or the value exceeds the nesting bound. +pub fn encode_canonical_cbor_v1(value: &CanonicalValueV1) -> Result, CanonicalValueError> { + let mut output = Vec::new(); + encode_value(value, &mut output, 0)?; + Ok(output) +} + +/// Decodes and authenticates one `edict.canonical-cbor/v1` value. +/// +/// Decoding is followed by exact canonical re-encoding. This rejects +/// non-minimal integers and lengths, unsorted maps, and any other supported +/// value whose supplied bytes differ from the unique Edict encoding. +/// Successful decoding authenticates the encoding only: callers must still +/// validate the value against its owning CDDL root, and Echo must separately +/// admit any runtime artifact or authority. +/// +/// # Errors +/// +/// Returns a stable failure for malformed, unsupported, trailing, +/// noncanonical, duplicate-key, invalid-UTF-8, or over-nested input. +pub fn decode_canonical_cbor_v1(bytes: &[u8]) -> Result { + let mut decoder = Decoder::new(bytes); + let value = decoder.value(0)?; + if decoder.remaining() != 0 { + return Err(CanonicalValueError::new( + CanonicalValueErrorKind::TrailingData, + "canonical CBOR stream contains bytes after the first value", + )); + } + if encode_canonical_cbor_v1(&value)? != bytes { + return Err(CanonicalValueError::new( + CanonicalValueErrorKind::NonCanonical, + "decoded value does not re-encode to the supplied bytes", + )); + } + Ok(value) +} + +/// Computes an Edict v1 domain-framed SHA-256 digest for a canonical value. +/// +/// The returned review rendering is `sha256:<64 lowercase hex>`. The preimage +/// is canonical CBOR for `["edict.digest/v1", domain, value]`. Each tuple member +/// is encoded at its own root so digest framing does not consume the artifact's +/// 128-level nesting budget. +/// A digest binds bytes and a caller-selected domain; it does not prove the +/// owning CDDL root or grant Echo runtime authority. +/// +/// # Errors +/// +/// Returns a stable failure for an empty domain or an unencodable value. +pub fn digest_canonical_value_v1( + domain: &str, + value: &CanonicalValueV1, +) -> Result { + if domain.is_empty() { + return Err(CanonicalValueError::new( + CanonicalValueErrorKind::UnsupportedValue, + "canonical artifact digest domain is empty", + )); + } + + let mut preimage = vec![0x83]; + preimage.extend(encode_canonical_cbor_v1(&CanonicalValueV1::Text( + EDICT_DIGEST_FRAME_V1.to_owned(), + ))?); + preimage.extend(encode_canonical_cbor_v1(&CanonicalValueV1::Text( + domain.to_owned(), + ))?); + preimage.extend(encode_canonical_cbor_v1(value)?); + + Ok(format!("sha256:{}", hex::encode(Sha256::digest(preimage)))) +} + +fn encode_value( + value: &CanonicalValueV1, + output: &mut Vec, + depth: usize, +) -> Result<(), CanonicalValueError> { + check_depth(depth)?; + match value { + CanonicalValueV1::Null => output.push(0xf6), + CanonicalValueV1::Bool(false) => output.push(0xf4), + CanonicalValueV1::Bool(true) => output.push(0xf5), + CanonicalValueV1::Integer(value) => encode_integer(*value, output)?, + CanonicalValueV1::Bytes(bytes) => { + encode_type_value(2, usize_to_u64(bytes.len())?, output); + output.extend_from_slice(bytes); + } + CanonicalValueV1::Text(text) => { + encode_type_value(3, usize_to_u64(text.len())?, output); + output.extend_from_slice(text.as_bytes()); + } + CanonicalValueV1::Array(values) => { + check_container_depth(depth)?; + encode_type_value(4, usize_to_u64(values.len())?, output); + for value in values { + encode_value(value, output, depth + 1)?; + } + } + CanonicalValueV1::Map(entries) => { + check_container_depth(depth)?; + let mut encoded_entries = Vec::with_capacity(entries.len()); + let mut encoded_keys = BTreeSet::new(); + for (key, value) in entries { + let mut key_bytes = Vec::new(); + encode_value(key, &mut key_bytes, depth + 1)?; + if !encoded_keys.insert(key_bytes.clone()) { + return Err(CanonicalValueError::new( + CanonicalValueErrorKind::DuplicateMapKey, + "canonical CBOR map contains duplicate keys", + )); + } + encoded_entries.push((key_bytes, value)); + } + encoded_entries.sort_by(|(left, _), (right, _)| left.cmp(right)); + encode_type_value(5, usize_to_u64(encoded_entries.len())?, output); + for (key_bytes, value) in encoded_entries { + output.extend_from_slice(&key_bytes); + encode_value(value, output, depth + 1)?; + } + } + } + Ok(()) +} + +fn check_depth(depth: usize) -> Result<(), CanonicalValueError> { + if depth > MAX_CANONICAL_NESTING_DEPTH_V1 { + return Err(CanonicalValueError::new( + CanonicalValueErrorKind::NestingLimitExceeded, + format!( + "canonical value nesting exceeds maximum depth {MAX_CANONICAL_NESTING_DEPTH_V1}" + ), + )); + } + Ok(()) +} + +fn check_container_depth(depth: usize) -> Result<(), CanonicalValueError> { + if depth >= MAX_CANONICAL_NESTING_DEPTH_V1 { + return Err(CanonicalValueError::new( + CanonicalValueErrorKind::NestingLimitExceeded, + format!( + "canonical container nesting exceeds maximum depth {MAX_CANONICAL_NESTING_DEPTH_V1}" + ), + )); + } + Ok(()) +} + +fn encode_integer(value: i128, output: &mut Vec) -> Result<(), CanonicalValueError> { + if value >= 0 { + let value = u64::try_from(value).map_err(|_| { + CanonicalValueError::new( + CanonicalValueErrorKind::InvalidInteger, + "positive integer exceeds the canonical CBOR uint range", + ) + })?; + encode_type_value(0, value, output); + return Ok(()); + } + + let magnitude = (-1i128).checked_sub(value).ok_or_else(|| { + CanonicalValueError::new( + CanonicalValueErrorKind::InvalidInteger, + "negative integer cannot be converted to the canonical CBOR range", + ) + })?; + let magnitude = u64::try_from(magnitude).map_err(|_| { + CanonicalValueError::new( + CanonicalValueErrorKind::InvalidInteger, + "negative integer exceeds the canonical CBOR negative range", + ) + })?; + encode_type_value(1, magnitude, output); + Ok(()) +} + +fn encode_type_value(major: u8, value: u64, output: &mut Vec) { + let prefix = major << 5; + let bytes = value.to_be_bytes(); + match value { + 0..=23 => output.push(prefix | bytes[7]), + 24..=0xff => { + output.push(prefix | 0x18); + output.push(bytes[7]); + } + 0x100..=0xffff => { + output.push(prefix | 0x19); + output.extend_from_slice(&bytes[6..]); + } + 0x1_0000..=0xffff_ffff => { + output.push(prefix | 0x1a); + output.extend_from_slice(&bytes[4..]); + } + _ => { + output.push(prefix | 0x1b); + output.extend_from_slice(&bytes); + } + } +} + +fn usize_to_u64(value: usize) -> Result { + u64::try_from(value).map_err(|_| { + CanonicalValueError::new( + CanonicalValueErrorKind::UnsupportedValue, + "canonical collection length does not fit the CBOR uint range", + ) + }) +} + +fn checked_collection_length( + declared: u64, + remaining: u64, +) -> Result +where + HostLength: TryFrom, +{ + if declared > remaining { + return Err(CanonicalValueError::new( + CanonicalValueErrorKind::UnexpectedEof, + "canonical CBOR declared length exceeds the remaining bytes", + )); + } + + HostLength::try_from(declared).map_err(|_| { + CanonicalValueError::new( + CanonicalValueErrorKind::UnsupportedCbor, + "canonical CBOR collection length does not fit usize", + ) + }) +} + +struct Decoder<'a> { + bytes: &'a [u8], + position: usize, +} + +impl<'a> Decoder<'a> { + const fn new(bytes: &'a [u8]) -> Self { + Self { bytes, position: 0 } + } + + const fn remaining(&self) -> usize { + self.bytes.len() - self.position + } + + fn value(&mut self, depth: usize) -> Result { + check_depth(depth)?; + let initial = self.byte()?; + let major = initial >> 5; + let additional = initial & 0x1f; + match major { + 0 => Ok(CanonicalValueV1::Integer(i128::from( + self.argument(additional)?, + ))), + 1 => Ok(CanonicalValueV1::Integer( + -1 - i128::from(self.argument(additional)?), + )), + 2 => { + let length = self.length(additional)?; + Ok(CanonicalValueV1::Bytes(self.take(length)?.to_vec())) + } + 3 => { + let length = self.length(additional)?; + let bytes = self.take(length)?; + let text = str::from_utf8(bytes).map_err(|_| { + CanonicalValueError::new( + CanonicalValueErrorKind::InvalidUtf8, + "canonical CBOR text string is not valid UTF-8", + ) + })?; + Ok(CanonicalValueV1::Text(text.to_owned())) + } + 4 => { + check_container_depth(depth)?; + let length = self.length(additional)?; + let mut values = Vec::with_capacity(length); + for _ in 0..length { + values.push(self.value(depth + 1)?); + } + Ok(CanonicalValueV1::Array(values)) + } + 5 => { + check_container_depth(depth)?; + let length = self.length(additional)?; + let mut entries = Vec::with_capacity(length); + let mut encoded_keys = BTreeSet::new(); + for _ in 0..length { + let key = self.value(depth + 1)?; + let mut key_bytes = Vec::new(); + encode_value(&key, &mut key_bytes, depth + 1)?; + if !encoded_keys.insert(key_bytes) { + return Err(CanonicalValueError::new( + CanonicalValueErrorKind::DuplicateMapKey, + "canonical CBOR map contains duplicate keys", + )); + } + let value = self.value(depth + 1)?; + entries.push((key, value)); + } + Ok(CanonicalValueV1::Map(entries)) + } + 7 => match additional { + 20 => Ok(CanonicalValueV1::Bool(false)), + 21 => Ok(CanonicalValueV1::Bool(true)), + 22 => Ok(CanonicalValueV1::Null), + _ => Err(CanonicalValueError::new( + CanonicalValueErrorKind::UnsupportedCbor, + "canonical CBOR simple value is unsupported", + )), + }, + _ => Err(CanonicalValueError::new( + CanonicalValueErrorKind::UnsupportedCbor, + "canonical CBOR major type is unsupported", + )), + } + } + + fn argument(&mut self, additional: u8) -> Result { + match additional { + 0..=23 => Ok(u64::from(additional)), + 24 => Ok(u64::from(self.byte()?)), + 25 => Ok(u64::from(u16::from_be_bytes(self.take_array::<2>()?))), + 26 => Ok(u64::from(u32::from_be_bytes(self.take_array::<4>()?))), + 27 => Ok(u64::from_be_bytes(self.take_array::<8>()?)), + _ => Err(CanonicalValueError::new( + CanonicalValueErrorKind::UnsupportedCbor, + "indefinite or reserved canonical CBOR length is unsupported", + )), + } + } + + fn length(&mut self, additional: u8) -> Result { + let declared = self.argument(additional)?; + let remaining = u64::try_from(self.remaining()).map_err(|_| { + CanonicalValueError::new( + CanonicalValueErrorKind::UnsupportedCbor, + "remaining canonical CBOR input does not fit the CBOR uint range", + ) + })?; + + checked_collection_length::(declared, remaining) + } + + fn byte(&mut self) -> Result { + let Some(value) = self.bytes.get(self.position).copied() else { + return Err(CanonicalValueError::new( + CanonicalValueErrorKind::UnexpectedEof, + "canonical CBOR expected another byte", + )); + }; + self.position += 1; + Ok(value) + } + + fn take(&mut self, length: usize) -> Result<&'a [u8], CanonicalValueError> { + let end = self.position.checked_add(length).ok_or_else(|| { + CanonicalValueError::new( + CanonicalValueErrorKind::UnexpectedEof, + "canonical CBOR length overflowed the input position", + ) + })?; + let Some(bytes) = self.bytes.get(self.position..end) else { + return Err(CanonicalValueError::new( + CanonicalValueErrorKind::UnexpectedEof, + "canonical CBOR value extends past the input", + )); + }; + self.position = end; + Ok(bytes) + } + + fn take_array(&mut self) -> Result<[u8; LENGTH], CanonicalValueError> { + let mut output = [0u8; LENGTH]; + output.copy_from_slice(self.take(LENGTH)?); + Ok(output) + } +} + +#[cfg(test)] +mod tests { + use super::{checked_collection_length, CanonicalValueError, CanonicalValueErrorKind}; + + #[test] + fn declared_length_bounds_precede_host_width_conversion() { + assert_eq!( + checked_collection_length::(u64::MAX, u64::from(u32::MAX)), + Err(CanonicalValueError::new( + CanonicalValueErrorKind::UnexpectedEof, + "canonical CBOR declared length exceeds the remaining bytes" + )) + ); + } +} diff --git a/crates/echo-wesley-gen/src/provider_contract_pack.rs b/crates/echo-wesley-gen/src/provider_contract_pack.rs new file mode 100644 index 00000000..60ea5a43 --- /dev/null +++ b/crates/echo-wesley-gen/src/provider_contract_pack.rs @@ -0,0 +1,929 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Strict admission of the Edict-owned provider contract pack. +//! +//! The generator receives the CDDL and manifest bytes explicitly. This module +//! performs no filesystem, registry, environment, or network discovery. It +//! admits only the publication merged in Edict PR #162: internal manifest +//! consistency is necessary, but does not substitute for the pinned external +//! identity checked here. + +use std::fmt; +use std::sync::Arc; + +use cddl_cat::cbor::validate_cbor; +use cddl_cat::context::BasicContext; +use cddl_cat::flatten::flatten_from_str; +use serde::Deserialize; +use sha2::{Digest, Sha256}; + +use crate::provider_canonical::{ + decode_canonical_cbor_v1, CanonicalValueErrorKind, CanonicalValueV1, +}; + +/// Exact Edict contract-pack API accepted by Echo provider generation. +pub const EDICT_PROVIDER_CONTRACT_PACK_API_V1: &str = "edict.provider-contract-pack/v1"; + +/// Exact Edict contract-pack coordinate accepted by Echo provider generation. +pub const EDICT_PROVIDER_CONTRACT_PACK_COORDINATE_V1: &str = "edict.provider-contract-pack.cddl@1"; + +/// License carried by the admitted Edict publication. +pub const EDICT_PROVIDER_CONTRACT_PACK_LICENSE: &str = "Apache-2.0"; + +/// SHA-256 of the admitted self-contained CDDL bytes. +pub const EDICT_PROVIDER_CONTRACT_PACK_SCHEMA_SHA256: &str = + "92697bc9a5262c68258be9ee451ee8c144aeb363b92142915b8224430b85cf74"; + +/// SHA-256 of the admitted Edict publication manifest bytes. +pub const EDICT_PROVIDER_CONTRACT_PACK_MANIFEST_SHA256: &str = + "6902467149fec3e0338bb90e8cd7963ee21b8ce24f368f9b12e748343cbe0e4f"; + +/// Maximum manifest size parsed at the contract-pack authority boundary. +pub const EDICT_PROVIDER_CONTRACT_PACK_MANIFEST_MAX_BYTES: usize = 61_713; + +const EXPECTED_CONTRACTS: [(&str, &str); 9] = [ + ("authority-facts", "authority-facts"), + ("core-module", "core-module"), + ("lawpack-exports", "lawpack-exports"), + ("lawpack-manifest", "lawpack-manifest"), + ("lowering-requirements", "lowering-requirements"), + ("target-ir-artifact", "target-ir-artifact"), + ("target-profile-intrinsics", "intrinsics-document"), + ("target-profile-manifest", "target-profile-manifest"), + ( + "target-profile-operation-profiles", + "operation-profiles-document", + ), +]; + +const EXPECTED_DOMAINS: [(&str, &str); 6] = [ + ("edict.authority-facts/v1", "authority-facts"), + ("edict.core.module/v1", "core-module"), + ("edict.lawpack/v1", "lawpack-manifest"), + ("edict.lowering-requirements/v1", "lowering-requirements"), + ("edict.target-ir.artifact/v1", "target-ir-artifact"), + ("edict.target-profile/v1", "target-profile-manifest"), +]; + +const EXPECTED_RESOURCES: [ExpectedResource; 5] = [ + ExpectedResource { + coordinate: "edict.canonical-cbor/v1", + raw_sha256: "8306e4f08c1e4e7d29ab22bcf55c324312712aac3eeeb675857ced57c3e48bdc", + domain_framed_digest: + "sha256:d1ea6d3de2a9762a438cbf4fac1d5ae2f357a4b27d13e0347e94ea655bf40f9d", + source_path: "fixtures/target-profile/contract-resources/canonical-cbor.cbor", + }, + ExpectedResource { + coordinate: "edict.determinism/v1", + raw_sha256: "84073f5c1734b625e16799048e28458d43e0a10befdae56a79d906f5e37ef76a", + domain_framed_digest: + "sha256:af4e6c774d5ea82db30e680be2ba7abc5ba04c6aead146139de32d8c5bb4981e", + source_path: "fixtures/target-profile/contract-resources/determinism.cbor", + }, + ExpectedResource { + coordinate: "edict.diagnostics/v1", + raw_sha256: "e465a28f1170fe478db5ff65d96a1fdbfbbcb95d327e6965021c252437991e4b", + domain_framed_digest: + "sha256:28fd72a98223153982ca084c29dbb1b2d430623967ab3b6db9d7fee668e614b9", + source_path: "fixtures/target-profile/contract-resources/diagnostics.cbor", + }, + ExpectedResource { + coordinate: "edict.fuel/v1", + raw_sha256: "c712c2d831cc8e731bdc1dfb8ea536f4630f38f0b0e0c6448b2df57176d3d0bd", + domain_framed_digest: + "sha256:006c6ebc01a3c5d36d50bd390b69d47e42378be61074e3b5c96ecb9f5ee53207", + source_path: "fixtures/target-profile/contract-resources/fuel.cbor", + }, + ExpectedResource { + coordinate: "edict.wasm-component/v1", + raw_sha256: "cd09d702db1d10be825e72effe35627bec56963cb3d78601b69478c58787e34d", + domain_framed_digest: + "sha256:095b4dd18f1a6a7276533f758665be319c5f476e7bdc70cc56d30b6b3e9f0a80", + source_path: "fixtures/target-profile/contract-resources/wasm-component.cbor", + }, +]; + +const EDICT_REPOSITORY: &str = "https://github.com/flyingrobots/edict"; + +#[derive(Clone, Copy)] +struct ExpectedResource { + coordinate: &'static str, + raw_sha256: &'static str, + domain_framed_digest: &'static str, + source_path: &'static str, +} + +/// One admitted contract name to CDDL root-rule binding. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ProviderContractBindingV1 { + contract: String, + root_rule: String, +} + +impl ProviderContractBindingV1 { + /// Returns the stable contract name. + #[must_use] + pub fn contract(&self) -> &str { + &self.contract + } + + /// Returns the CDDL root rule for this contract. + #[must_use] + pub fn root_rule(&self) -> &str { + &self.root_rule + } +} + +/// One admitted artifact domain to CDDL root-rule binding. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ProviderDomainBindingV1 { + domain: String, + root_rule: String, +} + +impl ProviderDomainBindingV1 { + /// Returns the canonical artifact domain. + #[must_use] + pub fn domain(&self) -> &str { + &self.domain + } + + /// Returns the CDDL root rule for this artifact domain. + #[must_use] + pub fn root_rule(&self) -> &str { + &self.root_rule + } +} + +/// One immutable contract resource admitted from the Edict publication. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ProviderContractResourceV1 { + coordinate: String, + canonical_bytes: Vec, + raw_sha256: String, + domain_framed_digest: String, + repository: String, + source_path: String, +} + +impl ProviderContractResourceV1 { + /// Returns the resource coordinate. + #[must_use] + pub fn coordinate(&self) -> &str { + &self.coordinate + } + + /// Returns the exact canonical resource bytes. + #[must_use] + pub fn canonical_bytes(&self) -> &[u8] { + &self.canonical_bytes + } + + /// Returns the SHA-256 of the canonical resource bytes. + #[must_use] + pub fn raw_sha256(&self) -> &str { + &self.raw_sha256 + } + + /// Returns the Edict domain-framed digest published for this resource. + #[must_use] + pub fn domain_framed_digest(&self) -> &str { + &self.domain_framed_digest + } + + /// Returns the upstream repository that owns this resource. + #[must_use] + pub fn repository(&self) -> &str { + &self.repository + } + + /// Returns the upstream repository-relative source path. + #[must_use] + pub fn source_path(&self) -> &str { + &self.source_path + } +} + +/// Opaque proof that explicit bytes match Echo's pinned Edict publication. +#[derive(Clone)] +pub struct AdmittedProviderContractPackV1 { + schema_bytes: Vec, + manifest_bytes: Vec, + contracts: Vec, + domains: Vec, + resources: Vec, + schema_context: Arc, +} + +impl fmt::Debug for AdmittedProviderContractPackV1 { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("AdmittedProviderContractPackV1") + .field("schema_bytes", &self.schema_bytes) + .field("manifest_bytes", &self.manifest_bytes) + .field("contracts", &self.contracts) + .field("domains", &self.domains) + .field("resources", &self.resources) + .finish_non_exhaustive() + } +} + +impl PartialEq for AdmittedProviderContractPackV1 { + fn eq(&self, other: &Self) -> bool { + self.schema_bytes == other.schema_bytes + && self.manifest_bytes == other.manifest_bytes + && self.contracts == other.contracts + && self.domains == other.domains + && self.resources == other.resources + } +} + +impl Eq for AdmittedProviderContractPackV1 {} + +impl AdmittedProviderContractPackV1 { + /// Returns the exact admitted API version. + #[must_use] + pub const fn api_version(&self) -> &'static str { + EDICT_PROVIDER_CONTRACT_PACK_API_V1 + } + + /// Returns the exact admitted pack coordinate. + #[must_use] + pub const fn coordinate(&self) -> &'static str { + EDICT_PROVIDER_CONTRACT_PACK_COORDINATE_V1 + } + + /// Returns the license declared by the admitted publication. + #[must_use] + pub const fn license(&self) -> &'static str { + EDICT_PROVIDER_CONTRACT_PACK_LICENSE + } + + /// Returns the exact admitted self-contained CDDL bytes. + #[must_use] + pub fn schema_bytes(&self) -> &[u8] { + &self.schema_bytes + } + + /// Returns the SHA-256 of the admitted CDDL bytes. + #[must_use] + pub const fn schema_sha256(&self) -> &'static str { + EDICT_PROVIDER_CONTRACT_PACK_SCHEMA_SHA256 + } + + /// Returns the exact admitted publication manifest bytes. + #[must_use] + pub fn manifest_bytes(&self) -> &[u8] { + &self.manifest_bytes + } + + /// Returns the SHA-256 of the admitted publication manifest bytes. + #[must_use] + pub const fn manifest_sha256(&self) -> &'static str { + EDICT_PROVIDER_CONTRACT_PACK_MANIFEST_SHA256 + } + + /// Returns all contract to root-rule bindings in publication order. + #[must_use] + pub fn contracts(&self) -> &[ProviderContractBindingV1] { + &self.contracts + } + + /// Returns all artifact-domain to root-rule bindings in publication order. + #[must_use] + pub fn domains(&self) -> &[ProviderDomainBindingV1] { + &self.domains + } + + /// Returns all immutable contract resources in publication order. + #[must_use] + pub fn resources(&self) -> &[ProviderContractResourceV1] { + &self.resources + } + + /// Returns the number of admitted named contracts. + #[must_use] + pub fn contract_count(&self) -> usize { + self.contracts.len() + } + + /// Returns the number of admitted artifact-domain bindings. + #[must_use] + pub fn domain_count(&self) -> usize { + self.domains.len() + } + + /// Returns the number of admitted immutable contract resources. + #[must_use] + pub fn resource_count(&self) -> usize { + self.resources.len() + } + + /// Returns the CDDL root rule bound to an admitted artifact domain. + #[must_use] + pub fn root_for_domain(&self, domain: &str) -> Option<&str> { + self.domains + .iter() + .find(|binding| binding.domain == domain) + .map(ProviderDomainBindingV1::root_rule) + } + + /// Returns an admitted immutable resource by exact coordinate. + #[must_use] + pub fn resource(&self, coordinate: &str) -> Option<&ProviderContractResourceV1> { + self.resources + .iter() + .find(|resource| resource.coordinate == coordinate) + } + + /// Decode canonical bytes and validate them against one named owning root. + /// + /// This boundary validates only the trusted, digest-pinned Edict publication. + /// It does not admit an artifact into the Echo runtime or grant runtime + /// authority. + /// + /// # Errors + /// + /// Returns a stable structured error when the contract is unknown, the + /// supplied bytes are not exact Edict canonical CBOR, or the decoded value + /// does not satisfy the named contract's admitted CDDL root. + pub fn validate_contract_bytes( + &self, + contract: &str, + bytes: &[u8], + ) -> Result { + let root = self + .contracts + .iter() + .find(|binding| binding.contract == contract) + .map(ProviderContractBindingV1::root_rule) + .ok_or_else(|| { + ProviderContractValidationError::new( + ProviderContractValidationErrorKind::UnknownContract, + contract, + None, + ) + })?; + let value = decode_canonical_cbor_v1(bytes).map_err(|error| { + ProviderContractValidationError::new( + ProviderContractValidationErrorKind::CanonicalEncodingInvalid, + contract, + Some(error.kind()), + ) + })?; + let cbor_value: ciborium::Value = ciborium::from_reader(bytes).map_err(|_| { + ProviderContractValidationError::new( + ProviderContractValidationErrorKind::CanonicalEncodingInvalid, + contract, + None, + ) + })?; + let rule = self.schema_context.rules.get(root).ok_or_else(|| { + ProviderContractValidationError::new( + ProviderContractValidationErrorKind::SchemaMismatch, + contract, + None, + ) + })?; + validate_cbor(rule, &cbor_value, self.schema_context.as_ref()).map_err(|_| { + ProviderContractValidationError::new( + ProviderContractValidationErrorKind::SchemaMismatch, + contract, + None, + ) + })?; + Ok(value) + } +} + +/// Stable failure categories returned by named provider-contract validation. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ProviderContractValidationErrorKind { + /// The admitted publication does not declare the requested contract name. + UnknownContract, + /// The supplied bytes are not exact `edict.canonical-cbor/v1` bytes. + CanonicalEncodingInvalid, + /// The canonical value does not satisfy the contract's owning CDDL root. + SchemaMismatch, +} + +impl ProviderContractValidationErrorKind { + const fn label(self) -> &'static str { + match self { + Self::UnknownContract => "unknown-contract", + Self::CanonicalEncodingInvalid => "canonical-encoding-invalid", + Self::SchemaMismatch => "schema-mismatch", + } + } +} + +/// Structured failure from canonical bytes and owning-root validation. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ProviderContractValidationError { + kind: ProviderContractValidationErrorKind, + subject: String, + canonical_value_kind: Option, +} + +impl ProviderContractValidationError { + /// Returns the stable validation failure category. + #[must_use] + pub const fn kind(&self) -> ProviderContractValidationErrorKind { + self.kind + } + + /// Returns the requested logical contract name. + #[must_use] + pub fn subject(&self) -> &str { + &self.subject + } + + /// Returns the typed canonical-value cause, when decoding failed. + #[must_use] + pub const fn canonical_value_kind(&self) -> Option { + self.canonical_value_kind + } + + fn new( + kind: ProviderContractValidationErrorKind, + subject: impl Into, + canonical_value_kind: Option, + ) -> Self { + Self { + kind, + subject: subject.into(), + canonical_value_kind, + } + } +} + +impl fmt::Display for ProviderContractValidationError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + formatter, + "provider contract validation {}: {}", + self.kind.label(), + self.subject + ) + } +} + +impl std::error::Error for ProviderContractValidationError {} + +/// Stable failure categories returned by contract-pack admission. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ProviderContractPackErrorKind { + /// Manifest input exceeded the pinned publication's byte length. + ManifestSizeExceeded, + /// Manifest JSON did not match the strict publication shape. + ManifestMalformed, + /// The manifest selected an unsupported API version. + UnsupportedApiVersion, + /// The manifest selected the wrong publication coordinate. + CoordinateMismatch, + /// The manifest did not declare the required Apache-2.0 license. + LicenseMismatch, + /// A schema byte string was not canonical lowercase hexadecimal. + SchemaHexInvalid, + /// Supplied CDDL bytes differed from the bytes embedded in the manifest. + SchemaBytesMismatch, + /// The CDDL digest differed from the manifest or pinned publication. + SchemaDigestMismatch, + /// The named contract and root-rule inventory differed from the publication. + ContractInventoryMismatch, + /// The artifact-domain and root-rule inventory differed from the publication. + DomainInventoryMismatch, + /// A resource was missing, reordered, duplicated, or substituted. + ResourceInventoryMismatch, + /// A resource byte string was not canonical lowercase hexadecimal. + ResourceHexInvalid, + /// A resource raw digest did not bind its bytes and pinned publication. + ResourceRawDigestMismatch, + /// A resource domain-framed digest differed from the pinned publication. + ResourceDomainDigestMismatch, + /// A resource provenance record differed from the pinned publication. + ResourceProvenanceMismatch, + /// The manifest bytes differed despite matching known semantic fields. + ManifestDigestMismatch, + /// The authenticated self-contained CDDL publication failed to compile. + SchemaCompilationFailed, + /// The compiled publication did not contain one of its declared roots. + SchemaRootMissing, +} + +impl ProviderContractPackErrorKind { + const fn label(self) -> &'static str { + match self { + Self::ManifestSizeExceeded => "manifest-size-exceeded", + Self::ManifestMalformed => "manifest-malformed", + Self::UnsupportedApiVersion => "unsupported-api-version", + Self::CoordinateMismatch => "coordinate-mismatch", + Self::LicenseMismatch => "license-mismatch", + Self::SchemaHexInvalid => "schema-hex-invalid", + Self::SchemaBytesMismatch => "schema-bytes-mismatch", + Self::SchemaDigestMismatch => "schema-digest-mismatch", + Self::ContractInventoryMismatch => "contract-inventory-mismatch", + Self::DomainInventoryMismatch => "domain-inventory-mismatch", + Self::ResourceInventoryMismatch => "resource-inventory-mismatch", + Self::ResourceHexInvalid => "resource-hex-invalid", + Self::ResourceRawDigestMismatch => "resource-raw-digest-mismatch", + Self::ResourceDomainDigestMismatch => "resource-domain-digest-mismatch", + Self::ResourceProvenanceMismatch => "resource-provenance-mismatch", + Self::ManifestDigestMismatch => "manifest-digest-mismatch", + Self::SchemaCompilationFailed => "schema-compilation-failed", + Self::SchemaRootMissing => "schema-root-missing", + } + } +} + +/// Structured contract-pack parsing or admission failure. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ProviderContractPackError { + kind: ProviderContractPackErrorKind, + subject: String, + reference: String, +} + +impl ProviderContractPackError { + /// Returns the stable failure category. + #[must_use] + pub const fn kind(&self) -> ProviderContractPackErrorKind { + self.kind + } + + /// Returns the field or resource being validated. + #[must_use] + pub fn subject(&self) -> &str { + &self.subject + } + + /// Returns the conflicting or expected publication value. + #[must_use] + pub fn reference(&self) -> &str { + &self.reference + } + + fn new( + kind: ProviderContractPackErrorKind, + subject: impl Into, + reference: impl Into, + ) -> Self { + Self { + kind, + subject: subject.into(), + reference: reference.into(), + } + } +} + +impl fmt::Display for ProviderContractPackError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + formatter, + "provider contract pack {}: {} -> {}", + self.kind.label(), + self.subject, + self.reference + ) + } +} + +impl std::error::Error for ProviderContractPackError {} + +/// Authenticate and admit one explicit Edict provider contract-pack publication. +/// +/// This function performs no discovery or I/O. Both byte slices must be the +/// exact reviewed publication selected by Echo. +/// +/// # Errors +/// +/// Returns a stable structured error when the manifest is malformed, its +/// inventory differs, supplied CDDL differs, a resource is tampered, or any +/// byte or semantic identity differs from the pinned Edict publication. +pub fn admit_provider_contract_pack_v1( + schema_bytes: &[u8], + manifest_bytes: &[u8], +) -> Result { + if manifest_bytes.len() > EDICT_PROVIDER_CONTRACT_PACK_MANIFEST_MAX_BYTES { + return Err(ProviderContractPackError::new( + ProviderContractPackErrorKind::ManifestSizeExceeded, + "manifest.bytes", + EDICT_PROVIDER_CONTRACT_PACK_MANIFEST_MAX_BYTES.to_string(), + )); + } + + let manifest = + serde_json::from_slice::(manifest_bytes).map_err(|_| { + ProviderContractPackError::new( + ProviderContractPackErrorKind::ManifestMalformed, + "manifest", + EDICT_PROVIDER_CONTRACT_PACK_API_V1, + ) + })?; + + require_exact( + &manifest.api_version, + EDICT_PROVIDER_CONTRACT_PACK_API_V1, + ProviderContractPackErrorKind::UnsupportedApiVersion, + "apiVersion", + )?; + require_exact( + &manifest.coordinate, + EDICT_PROVIDER_CONTRACT_PACK_COORDINATE_V1, + ProviderContractPackErrorKind::CoordinateMismatch, + "coordinate", + )?; + require_exact( + &manifest.license, + EDICT_PROVIDER_CONTRACT_PACK_LICENSE, + ProviderContractPackErrorKind::LicenseMismatch, + "license", + )?; + + let embedded_schema = decode_lower_hex( + &manifest.schema.bytes_hex, + ProviderContractPackErrorKind::SchemaHexInvalid, + "schema.bytesHex", + )?; + if embedded_schema != schema_bytes { + return Err(ProviderContractPackError::new( + ProviderContractPackErrorKind::SchemaBytesMismatch, + "schema.bytesHex", + EDICT_PROVIDER_CONTRACT_PACK_SCHEMA_SHA256, + )); + } + + let schema_digest = sha256_hex(schema_bytes); + if manifest.schema.raw_sha256 != schema_digest + || schema_digest != EDICT_PROVIDER_CONTRACT_PACK_SCHEMA_SHA256 + { + return Err(ProviderContractPackError::new( + ProviderContractPackErrorKind::SchemaDigestMismatch, + "schema.rawSha256", + EDICT_PROVIDER_CONTRACT_PACK_SCHEMA_SHA256, + )); + } + + validate_bindings( + &manifest.contracts, + &EXPECTED_CONTRACTS, + ProviderContractPackErrorKind::ContractInventoryMismatch, + "contracts", + |binding| (&binding.contract, &binding.root_rule), + )?; + validate_bindings( + &manifest.domains, + &EXPECTED_DOMAINS, + ProviderContractPackErrorKind::DomainInventoryMismatch, + "domains", + |binding| (&binding.domain, &binding.root_rule), + )?; + + let resources = validate_resources(manifest.resources)?; + + let manifest_digest = sha256_hex(manifest_bytes); + if manifest_digest != EDICT_PROVIDER_CONTRACT_PACK_MANIFEST_SHA256 { + return Err(ProviderContractPackError::new( + ProviderContractPackErrorKind::ManifestDigestMismatch, + "manifest", + EDICT_PROVIDER_CONTRACT_PACK_MANIFEST_SHA256, + )); + } + + let schema_context = compile_schema_context(schema_bytes)?; + + Ok(AdmittedProviderContractPackV1 { + schema_bytes: schema_bytes.to_vec(), + manifest_bytes: manifest_bytes.to_vec(), + contracts: manifest + .contracts + .into_iter() + .map(|binding| ProviderContractBindingV1 { + contract: binding.contract, + root_rule: binding.root_rule, + }) + .collect(), + domains: manifest + .domains + .into_iter() + .map(|binding| ProviderDomainBindingV1 { + domain: binding.domain, + root_rule: binding.root_rule, + }) + .collect(), + resources, + schema_context, + }) +} + +fn compile_schema_context( + schema_bytes: &[u8], +) -> Result, ProviderContractPackError> { + let schema = std::str::from_utf8(schema_bytes).map_err(|_| { + ProviderContractPackError::new( + ProviderContractPackErrorKind::SchemaCompilationFailed, + EDICT_PROVIDER_CONTRACT_PACK_COORDINATE_V1, + "utf-8 CDDL", + ) + })?; + let rules = flatten_from_str(schema).map_err(|_| { + ProviderContractPackError::new( + ProviderContractPackErrorKind::SchemaCompilationFailed, + EDICT_PROVIDER_CONTRACT_PACK_COORDINATE_V1, + "compiled CDDL", + ) + })?; + let context = Arc::new(BasicContext::new(rules)); + for (_, root) in EXPECTED_CONTRACTS.into_iter().chain(EXPECTED_DOMAINS) { + if !context.rules.contains_key(root) { + return Err(ProviderContractPackError::new( + ProviderContractPackErrorKind::SchemaRootMissing, + root, + EDICT_PROVIDER_CONTRACT_PACK_COORDINATE_V1, + )); + } + } + Ok(context) +} + +fn validate_resources( + resources: Vec, +) -> Result, ProviderContractPackError> { + if resources.len() != EXPECTED_RESOURCES.len() { + return Err(ProviderContractPackError::new( + ProviderContractPackErrorKind::ResourceInventoryMismatch, + "resources", + EXPECTED_RESOURCES.len().to_string(), + )); + } + + resources + .into_iter() + .zip(EXPECTED_RESOURCES) + .map(|(resource, expected)| validate_resource(resource, expected)) + .collect() +} + +fn validate_resource( + resource: ManifestResource, + expected: ExpectedResource, +) -> Result { + if resource.coordinate != expected.coordinate { + return Err(ProviderContractPackError::new( + ProviderContractPackErrorKind::ResourceInventoryMismatch, + resource.coordinate, + expected.coordinate, + )); + } + + let canonical_bytes = decode_lower_hex( + &resource.canonical_bytes_hex, + ProviderContractPackErrorKind::ResourceHexInvalid, + expected.coordinate, + )?; + if sha256_hex(&canonical_bytes) != resource.raw_sha256 + || resource.raw_sha256 != expected.raw_sha256 + { + return Err(ProviderContractPackError::new( + ProviderContractPackErrorKind::ResourceRawDigestMismatch, + expected.coordinate, + expected.raw_sha256, + )); + } + if resource.domain_framed_digest != expected.domain_framed_digest { + return Err(ProviderContractPackError::new( + ProviderContractPackErrorKind::ResourceDomainDigestMismatch, + expected.coordinate, + expected.domain_framed_digest, + )); + } + if resource.provenance.repository != EDICT_REPOSITORY + || resource.provenance.source_path != expected.source_path + { + return Err(ProviderContractPackError::new( + ProviderContractPackErrorKind::ResourceProvenanceMismatch, + expected.coordinate, + expected.source_path, + )); + } + + Ok(ProviderContractResourceV1 { + coordinate: resource.coordinate, + canonical_bytes, + raw_sha256: resource.raw_sha256, + domain_framed_digest: resource.domain_framed_digest, + repository: resource.provenance.repository, + source_path: resource.provenance.source_path, + }) +} + +fn validate_bindings( + actual: &[T], + expected: &[(&str, &str)], + kind: ProviderContractPackErrorKind, + subject: &'static str, + fields: F, +) -> Result<(), ProviderContractPackError> +where + F: Fn(&T) -> (&String, &String), +{ + let matches = actual.len() == expected.len() + && actual + .iter() + .zip(expected) + .all(|(binding, (expected_name, expected_root))| { + let (name, root) = fields(binding); + name == expected_name && root == expected_root + }); + if matches { + Ok(()) + } else { + Err(ProviderContractPackError::new( + kind, + subject, + EDICT_PROVIDER_CONTRACT_PACK_COORDINATE_V1, + )) + } +} + +fn require_exact( + actual: &str, + expected: &'static str, + kind: ProviderContractPackErrorKind, + subject: &'static str, +) -> Result<(), ProviderContractPackError> { + if actual == expected { + Ok(()) + } else { + Err(ProviderContractPackError::new(kind, subject, expected)) + } +} + +fn decode_lower_hex( + value: &str, + kind: ProviderContractPackErrorKind, + subject: impl Into, +) -> Result, ProviderContractPackError> { + if !value.len().is_multiple_of(2) + || !value + .bytes() + .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) + { + return Err(ProviderContractPackError::new(kind, subject, "lower-hex")); + } + hex::decode(value).map_err(|_| ProviderContractPackError::new(kind, subject, "lower-hex")) +} + +fn sha256_hex(bytes: &[u8]) -> String { + hex::encode(Sha256::digest(bytes)) +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct ContractPackManifest { + api_version: String, + coordinate: String, + license: String, + schema: ManifestSchema, + contracts: Vec, + domains: Vec, + resources: Vec, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct ManifestSchema { + bytes_hex: String, + raw_sha256: String, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct ManifestContractBinding { + contract: String, + root_rule: String, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct ManifestDomainBinding { + domain: String, + root_rule: String, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct ManifestResource { + coordinate: String, + canonical_bytes_hex: String, + raw_sha256: String, + domain_framed_digest: String, + provenance: ManifestResourceProvenance, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct ManifestResourceProvenance { + repository: String, + source_path: String, +} diff --git a/crates/echo-wesley-gen/src/provider_corpus.rs b/crates/echo-wesley-gen/src/provider_corpus.rs new file mode 100644 index 00000000..f2d31339 --- /dev/null +++ b/crates/echo-wesley-gen/src/provider_corpus.rs @@ -0,0 +1,788 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Pure construction and comparison of the checked Edict provider artifact corpus. +//! +//! The generator identity is an explicit, versioned frame of exact source and +//! dependency-lock bytes. The artifact corpus is assembled only from immutable, +//! already validated generation values. This module performs no filesystem, +//! environment, registry, process, clock, or network discovery. + +use crate::provider_artifacts::ProviderPrimaryArtifactsV1; +use crate::provider_generation::ProviderGenerationInputV1; +use crate::provider_provenance::{ + ProviderGenerationProvenanceV1, ProviderGeneratorMaterialV1, ProviderProvenanceError, + ProviderProvenanceErrorKind, +}; +use crate::provider_review::{ + generate_provider_generation_review_v1, ProviderGenerationReviewV1, ProviderReviewError, + ProviderReviewErrorKind, +}; +use std::collections::BTreeMap; +use std::fmt; +use wesley_core::GenerationContractErrorKind; + +const SOURCE_BUNDLE_PREFIX: &[u8] = b"echo.provider-artifact-generator.source-bundle/v1\0"; +const GENERATOR_COORDINATE: &str = "echo-wesley-gen.provider-artifact-generator@1"; +const GENERATED_CORPUS_PREFIX: &str = "schemas/edict-provider/generated/v1"; +const EXPECTED_CORPUS_FILE_COUNT: usize = 22; +const MAX_SOURCE_FILE_COUNT: usize = 64; +const MAX_SOURCE_PATH_BYTES: usize = 512; +const MAX_SOURCE_FILE_BYTES: usize = 8 * 1024 * 1024; +const MAX_SOURCE_BUNDLE_BYTES: usize = 32 * 1024 * 1024; +const MAX_CORPUS_PATH_BYTES: usize = 512; + +/// Stable failure categories for generator-source and corpus construction. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +pub enum ProviderArtifactCorpusErrorKind { + /// No generator source file was supplied. + GeneratorSourceMissing, + /// A generator source path is unsafe or non-canonical. + GeneratorSourcePathInvalid, + /// A generator source file has no exact bytes. + GeneratorSourceEmpty, + /// A generator source input exceeds a fixed v1 bound. + GeneratorSourceTooLarge, + /// The same generator source path was supplied more than once. + GeneratorSourceDuplicate, + /// The exact source frame could not form a Wesley generator identity. + GeneratorMaterialInvalid, + /// Provenance does not verify against the supplied exact materials. + ProvenanceInvalid, + /// The deterministic Wesley review could not be re-derived. + ReviewInvalid, + /// The supplied review differs from the review re-derived from provenance. + ReviewMismatch, + /// A corpus-relative path is unsafe or non-canonical. + CorpusPathInvalid, + /// The same corpus-relative path was supplied more than once. + CorpusPathDuplicate, + /// The rendered corpus does not contain the exact v1 file count. + CorpusInventoryInvalid, +} + +impl ProviderArtifactCorpusErrorKind { + const fn label(self) -> &'static str { + match self { + Self::GeneratorSourceMissing => "generator-source-missing", + Self::GeneratorSourcePathInvalid => "generator-source-path-invalid", + Self::GeneratorSourceEmpty => "generator-source-empty", + Self::GeneratorSourceTooLarge => "generator-source-too-large", + Self::GeneratorSourceDuplicate => "generator-source-duplicate", + Self::GeneratorMaterialInvalid => "generator-material-invalid", + Self::ProvenanceInvalid => "provenance-invalid", + Self::ReviewInvalid => "review-invalid", + Self::ReviewMismatch => "review-mismatch", + Self::CorpusPathInvalid => "corpus-path-invalid", + Self::CorpusPathDuplicate => "corpus-path-duplicate", + Self::CorpusInventoryInvalid => "corpus-inventory-invalid", + } + } +} + +/// Stable structured generator-source or corpus construction failure. +#[derive(Debug, PartialEq, Eq)] +pub struct ProviderArtifactCorpusError { + kind: ProviderArtifactCorpusErrorKind, + subject: String, + reference: String, + provenance_kind: Option, + review_kind: Option, + wesley_contract_kind: Option, +} + +impl ProviderArtifactCorpusError { + /// Returns the stable top-level failure category. + #[must_use] + pub const fn kind(&self) -> ProviderArtifactCorpusErrorKind { + self.kind + } + + /// Returns the stable subject that failed validation. + #[must_use] + pub fn subject(&self) -> &str { + &self.subject + } + + /// Returns the stable offending reference or field value. + #[must_use] + pub fn reference(&self) -> &str { + &self.reference + } + + /// Returns the typed provenance failure when provenance verification failed. + #[must_use] + pub const fn provenance_kind(&self) -> Option { + self.provenance_kind + } + + /// Returns the typed review failure when review derivation failed. + #[must_use] + pub const fn review_kind(&self) -> Option { + self.review_kind + } + + /// Returns the nested typed Wesley failure when one caused this error. + #[must_use] + pub const fn wesley_contract_kind(&self) -> Option { + self.wesley_contract_kind + } + + fn new( + kind: ProviderArtifactCorpusErrorKind, + subject: impl Into, + reference: impl Into, + ) -> Self { + Self { + kind, + subject: subject.into(), + reference: reference.into(), + provenance_kind: None, + review_kind: None, + wesley_contract_kind: None, + } + } + + fn generator_material(error: ProviderProvenanceError) -> Self { + Self { + kind: ProviderArtifactCorpusErrorKind::GeneratorMaterialInvalid, + subject: error.subject().to_owned(), + reference: error.reference().to_owned(), + provenance_kind: Some(error.kind()), + review_kind: None, + wesley_contract_kind: error.wesley_contract_kind(), + } + } + + fn provenance(error: ProviderProvenanceError) -> Self { + Self { + kind: ProviderArtifactCorpusErrorKind::ProvenanceInvalid, + subject: error.subject().to_owned(), + reference: error.reference().to_owned(), + provenance_kind: Some(error.kind()), + review_kind: None, + wesley_contract_kind: error.wesley_contract_kind(), + } + } + + fn review(error: ProviderReviewError) -> Self { + Self { + kind: ProviderArtifactCorpusErrorKind::ReviewInvalid, + subject: error.subject().to_owned(), + reference: error.reference().to_owned(), + provenance_kind: None, + review_kind: Some(error.kind()), + wesley_contract_kind: error.wesley_contract_kind(), + } + } +} + +impl fmt::Display for ProviderArtifactCorpusError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + formatter, + "{}: {} ({})", + self.kind.label(), + self.subject, + self.reference + ) + } +} + +impl std::error::Error for ProviderArtifactCorpusError {} + +/// One exact source file included in provider generator identity v1. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ProviderGeneratorSourceFileV1 { + path: String, + bytes: Vec, +} + +impl ProviderGeneratorSourceFileV1 { + /// Constructs one exact source file after validating its repo-relative path. + /// + /// # Errors + /// + /// Returns a stable failure for unsafe paths, generated-output paths, empty + /// content, or content beyond the fixed v1 source-file bound. + pub fn new(path: impl Into, bytes: &[u8]) -> Result { + let path = path.into(); + validate_source_path(&path)?; + if bytes.is_empty() { + return Err(ProviderArtifactCorpusError::new( + ProviderArtifactCorpusErrorKind::GeneratorSourceEmpty, + &path, + "0", + )); + } + if bytes.len() > MAX_SOURCE_FILE_BYTES { + return Err(ProviderArtifactCorpusError::new( + ProviderArtifactCorpusErrorKind::GeneratorSourceTooLarge, + &path, + bytes.len().to_string(), + )); + } + Ok(Self { + path, + bytes: bytes.to_vec(), + }) + } + + /// Returns the canonical repo-relative POSIX path. + #[must_use] + pub fn path(&self) -> &str { + &self.path + } + + /// Returns the exact, unnormalized file bytes. + #[must_use] + pub fn bytes(&self) -> &[u8] { + &self.bytes + } +} + +/// Canonical v1 frame of the exact provider generator source closure. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ProviderGeneratorSourceBundleV1 { + source_files: Vec, + canonical_bytes: Vec, +} + +impl ProviderGeneratorSourceBundleV1 { + /// Returns source files sorted by raw UTF-8 path bytes. + #[must_use] + pub fn source_files(&self) -> &[ProviderGeneratorSourceFileV1] { + &self.source_files + } + + /// Returns the exact canonical source-bundle frame. + #[must_use] + pub fn canonical_bytes(&self) -> &[u8] { + &self.canonical_bytes + } + + /// Constructs Wesley generator material from the exact source-bundle frame. + /// + /// # Errors + /// + /// Returns a stable failure if Wesley rejects the fixed coordinate, crate + /// version, or exact source-bundle bytes. + pub fn generator_material( + &self, + ) -> Result { + ProviderGeneratorMaterialV1::new( + GENERATOR_COORDINATE, + env!("CARGO_PKG_VERSION"), + &self.canonical_bytes, + ) + .map_err(ProviderArtifactCorpusError::generator_material) + } +} + +/// Builds a canonical source-bundle frame from explicit exact file bytes. +/// +/// Source declaration order is set-like: paths are sorted by raw UTF-8 bytes. +/// File paths and contents remain exact and are never discovered or normalized. +/// +/// # Errors +/// +/// Returns a stable failure for an empty or oversized collection, duplicate +/// paths, or a source frame beyond the fixed v1 bound. +pub fn build_provider_generator_source_bundle_v1( + mut source_files: Vec, +) -> Result { + if source_files.is_empty() { + return Err(ProviderArtifactCorpusError::new( + ProviderArtifactCorpusErrorKind::GeneratorSourceMissing, + GENERATOR_COORDINATE, + "source-files", + )); + } + if source_files.len() > MAX_SOURCE_FILE_COUNT { + return Err(ProviderArtifactCorpusError::new( + ProviderArtifactCorpusErrorKind::GeneratorSourceTooLarge, + GENERATOR_COORDINATE, + source_files.len().to_string(), + )); + } + + source_files.sort_by(|left, right| left.path.as_bytes().cmp(right.path.as_bytes())); + for pair in source_files.windows(2) { + if pair[0].path == pair[1].path { + return Err(ProviderArtifactCorpusError::new( + ProviderArtifactCorpusErrorKind::GeneratorSourceDuplicate, + &pair[0].path, + "source-files", + )); + } + } + + let mut canonical_bytes = Vec::new(); + extend_source_bundle(&mut canonical_bytes, SOURCE_BUNDLE_PREFIX, "bundle-prefix")?; + append_u64( + &mut canonical_bytes, + source_files.len(), + "source-file-count", + )?; + for source in &source_files { + append_u64(&mut canonical_bytes, source.path.len(), &source.path)?; + extend_source_bundle(&mut canonical_bytes, source.path.as_bytes(), &source.path)?; + append_u64(&mut canonical_bytes, source.bytes.len(), &source.path)?; + extend_source_bundle(&mut canonical_bytes, &source.bytes, &source.path)?; + } + + Ok(ProviderGeneratorSourceBundleV1 { + source_files, + canonical_bytes, + }) +} + +/// Builds the exact, compile-time-enumerated provider generator source closure. +/// +/// Authored semantic input, settings, admitted contract bytes, and every +/// generated output are deliberately absent. Wesley binds those materials in +/// the generation input, provenance sources, and emitted-output closure. +/// +/// # Errors +/// +/// Returns a stable failure if a pinned source entry violates the v1 framing +/// contract. No filesystem or environment discovery is performed. +pub fn checked_provider_generator_source_bundle_v1( +) -> Result { + let sources: [(&str, &[u8]); 14] = [ + ("Cargo.lock", include_bytes!("../../../Cargo.lock")), + ("Cargo.toml", include_bytes!("../../../Cargo.toml")), + ( + "crates/echo-wesley-gen/Cargo.toml", + include_bytes!("../Cargo.toml"), + ), + ( + "crates/echo-wesley-gen/src/bin/echo-edict-provider-artifacts.rs", + include_bytes!("bin/echo-edict-provider-artifacts.rs"), + ), + ( + "crates/echo-wesley-gen/src/lib.rs", + include_bytes!("lib.rs"), + ), + ( + "crates/echo-wesley-gen/src/provider_artifacts.rs", + include_bytes!("provider_artifacts.rs"), + ), + ( + "crates/echo-wesley-gen/src/provider_canonical.rs", + include_bytes!("provider_canonical.rs"), + ), + ( + "crates/echo-wesley-gen/src/provider_contract_pack.rs", + include_bytes!("provider_contract_pack.rs"), + ), + ( + "crates/echo-wesley-gen/src/provider_corpus.rs", + include_bytes!("provider_corpus.rs"), + ), + ( + "crates/echo-wesley-gen/src/provider_generation.rs", + include_bytes!("provider_generation.rs"), + ), + ( + "crates/echo-wesley-gen/src/provider_provenance.rs", + include_bytes!("provider_provenance.rs"), + ), + ( + "crates/echo-wesley-gen/src/provider_review.rs", + include_bytes!("provider_review.rs"), + ), + ( + "crates/echo-wesley-gen/src/provider_semantics.rs", + include_bytes!("provider_semantics.rs"), + ), + ( + "rust-toolchain.toml", + include_bytes!("../../../rust-toolchain.toml"), + ), + ]; + let mut files = Vec::with_capacity(sources.len()); + for (path, bytes) in sources { + files.push(ProviderGeneratorSourceFileV1::new(path, bytes)?); + } + build_provider_generator_source_bundle_v1(files) +} + +/// One exact file in the checked provider artifact corpus. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ProviderArtifactCorpusFileV1 { + relative_path: String, + bytes: Vec, +} + +impl ProviderArtifactCorpusFileV1 { + /// Constructs an exact corpus file with a safe relative POSIX path. + /// + /// # Errors + /// + /// Returns a stable failure when the path is absolute, traversing, empty, + /// contains control characters or backslashes, has a Windows drive prefix, + /// or exceeds the fixed v1 path bound. + pub fn new( + relative_path: impl Into, + bytes: &[u8], + ) -> Result { + let relative_path = relative_path.into(); + validate_corpus_path(&relative_path)?; + Ok(Self { + relative_path, + bytes: bytes.to_vec(), + }) + } + + /// Returns the slash-normalized path relative to the corpus root. + #[must_use] + pub fn relative_path(&self) -> &str { + &self.relative_path + } + + /// Returns the exact file bytes without re-encoding or normalization. + #[must_use] + pub fn bytes(&self) -> &[u8] { + &self.bytes + } +} + +/// Complete deterministic v1 provider artifact corpus. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ProviderArtifactCorpusV1 { + files: Vec, +} + +impl ProviderArtifactCorpusV1 { + /// Returns all 22 exact files in lexicographic path order. + #[must_use] + pub fn files(&self) -> &[ProviderArtifactCorpusFileV1] { + &self.files + } + + /// Returns one exact corpus file by relative path. + #[must_use] + pub fn file(&self, relative_path: &str) -> Option<&ProviderArtifactCorpusFileV1> { + self.files + .iter() + .find(|file| file.relative_path == relative_path) + } +} + +/// Stable kinds of checked-corpus drift. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum ProviderArtifactCorpusDriftKind { + /// An expected file is absent. + Missing, + /// An expected path contains different exact bytes. + Changed, + /// An actual path is not part of the exact v1 corpus. + Unexpected, +} + +impl ProviderArtifactCorpusDriftKind { + /// Returns the stable diagnostic spelling for this drift kind. + #[must_use] + pub const fn as_str(self) -> &'static str { + match self { + Self::Missing => "missing", + Self::Changed => "changed", + Self::Unexpected => "unexpected", + } + } +} + +/// One stable exact-byte corpus drift diagnostic. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ProviderArtifactCorpusDriftV1 { + kind: ProviderArtifactCorpusDriftKind, + relative_path: String, +} + +impl ProviderArtifactCorpusDriftV1 { + /// Returns whether this file is missing, changed, or unexpected. + #[must_use] + pub const fn kind(&self) -> ProviderArtifactCorpusDriftKind { + self.kind + } + + /// Returns the stable corpus-relative path. + #[must_use] + pub fn relative_path(&self) -> &str { + &self.relative_path + } +} + +/// Renders the exact 22-file provider corpus from verified generation values. +/// +/// Before exposing bytes, this re-verifies provenance against the supplied +/// input, primary outputs, and generator material, then re-derives and compares +/// the non-authoritative review. CBOR, CDDL, and JSON bytes are copied exactly. +/// +/// # Errors +/// +/// Returns a stable structured failure for incoherent generation materials, +/// review mismatch, unsafe role-derived paths, duplicates, or wrong inventory. +pub fn render_provider_artifact_corpus_v1( + input: &ProviderGenerationInputV1, + primary: &ProviderPrimaryArtifactsV1, + generator: &ProviderGeneratorMaterialV1, + provenance: &ProviderGenerationProvenanceV1, + review: &ProviderGenerationReviewV1, +) -> Result { + provenance + .verify_exact_materials(input, primary, generator) + .map_err(ProviderArtifactCorpusError::provenance)?; + let expected_review = generate_provider_generation_review_v1(input, provenance) + .map_err(ProviderArtifactCorpusError::review)?; + if &expected_review != review { + return Err(ProviderArtifactCorpusError::new( + ProviderArtifactCorpusErrorKind::ReviewMismatch, + review.coordinate(), + &review.content_reference().digest, + )); + } + + let mut files = Vec::with_capacity(EXPECTED_CORPUS_FILE_COUNT); + for artifact in primary.artifacts() { + validate_filename_role(artifact.role())?; + files.push(ProviderArtifactCorpusFileV1::new( + format!("primary/{}.cbor", artifact.role()), + artifact.canonical_bytes(), + )?); + } + validate_filename_role(primary.schema().role())?; + files.push(ProviderArtifactCorpusFileV1::new( + format!("primary/{}.cddl", primary.schema().role()), + primary.schema().bytes(), + )?); + for resource in primary.resources() { + validate_filename_role(resource.role())?; + files.push(ProviderArtifactCorpusFileV1::new( + format!("resources/{}.cbor", resource.role()), + resource.canonical_bytes(), + )?); + } + validate_filename_role(provenance.role())?; + files.push(ProviderArtifactCorpusFileV1::new( + format!("evidence/{}.json", provenance.role()), + provenance.canonical_bytes(), + )?); + validate_filename_role(review.role())?; + files.push(ProviderArtifactCorpusFileV1::new( + format!("evidence/{}.json", review.role()), + review.canonical_bytes(), + )?); + + files.sort_by(|left, right| { + left.relative_path + .as_bytes() + .cmp(right.relative_path.as_bytes()) + }); + for pair in files.windows(2) { + if pair[0].relative_path == pair[1].relative_path { + return Err(ProviderArtifactCorpusError::new( + ProviderArtifactCorpusErrorKind::CorpusPathDuplicate, + &pair[0].relative_path, + "rendered-corpus", + )); + } + } + if files.len() != EXPECTED_CORPUS_FILE_COUNT { + return Err(ProviderArtifactCorpusError::new( + ProviderArtifactCorpusErrorKind::CorpusInventoryInvalid, + "provider-artifact-corpus/v1", + files.len().to_string(), + )); + } + Ok(ProviderArtifactCorpusV1 { files }) +} + +/// Compares an actual file inventory with the exact rendered corpus. +/// +/// The returned drift is sorted first by `missing`, `changed`, `unexpected`, +/// and then lexicographically by relative path. This function is pure and does +/// not read, write, create, normalize, or delete filesystem entries. +/// +/// # Errors +/// +/// Returns a stable failure if the actual inventory repeats a relative path. +pub fn diff_provider_artifact_corpus_v1( + expected: &ProviderArtifactCorpusV1, + actual: &[ProviderArtifactCorpusFileV1], +) -> Result, ProviderArtifactCorpusError> { + let expected_by_path = corpus_inventory(&expected.files, "expected-corpus")?; + let actual_by_path = corpus_inventory(actual, "actual-corpus")?; + let mut drift = Vec::new(); + + for (path, expected_bytes) in &expected_by_path { + match actual_by_path.get(path) { + None => drift.push(ProviderArtifactCorpusDriftV1 { + kind: ProviderArtifactCorpusDriftKind::Missing, + relative_path: (*path).to_owned(), + }), + Some(actual_bytes) if *actual_bytes != *expected_bytes => { + drift.push(ProviderArtifactCorpusDriftV1 { + kind: ProviderArtifactCorpusDriftKind::Changed, + relative_path: (*path).to_owned(), + }); + } + Some(_) => {} + } + } + for path in actual_by_path.keys() { + if !expected_by_path.contains_key(path) { + drift.push(ProviderArtifactCorpusDriftV1 { + kind: ProviderArtifactCorpusDriftKind::Unexpected, + relative_path: (*path).to_owned(), + }); + } + } + drift.sort_by(|left, right| { + left.kind.cmp(&right.kind).then_with(|| { + left.relative_path + .as_bytes() + .cmp(right.relative_path.as_bytes()) + }) + }); + Ok(drift) +} + +fn validate_source_path(path: &str) -> Result<(), ProviderArtifactCorpusError> { + if path.len() > MAX_SOURCE_PATH_BYTES { + return Err(ProviderArtifactCorpusError::new( + ProviderArtifactCorpusErrorKind::GeneratorSourceTooLarge, + path, + path.len().to_string(), + )); + } + let valid_characters = path + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-' | b'/')); + if !valid_characters || !has_safe_relative_segments(path) { + return Err(ProviderArtifactCorpusError::new( + ProviderArtifactCorpusErrorKind::GeneratorSourcePathInvalid, + path, + "relative-posix-source-path", + )); + } + if path == GENERATED_CORPUS_PREFIX + || path + .strip_prefix(GENERATED_CORPUS_PREFIX) + .is_some_and(|suffix| suffix.starts_with('/')) + { + return Err(ProviderArtifactCorpusError::new( + ProviderArtifactCorpusErrorKind::GeneratorSourcePathInvalid, + path, + "generated-output-self-reference", + )); + } + Ok(()) +} + +fn validate_corpus_path(path: &str) -> Result<(), ProviderArtifactCorpusError> { + if path.len() > MAX_CORPUS_PATH_BYTES { + return Err(ProviderArtifactCorpusError::new( + ProviderArtifactCorpusErrorKind::CorpusPathInvalid, + path, + path.len().to_string(), + )); + } + let windows_drive_prefix = path.as_bytes().get(1) == Some(&b':') + && path.as_bytes().first().is_some_and(u8::is_ascii_alphabetic); + if path.chars().any(char::is_control) + || path.contains('\\') + || windows_drive_prefix + || !has_safe_relative_segments(path) + { + return Err(ProviderArtifactCorpusError::new( + ProviderArtifactCorpusErrorKind::CorpusPathInvalid, + path, + "relative-posix-path", + )); + } + Ok(()) +} + +fn has_safe_relative_segments(path: &str) -> bool { + !path.is_empty() + && !path.starts_with('/') + && !path.ends_with('/') + && path + .split('/') + .all(|segment| !segment.is_empty() && segment != "." && segment != "..") +} + +fn validate_filename_role(role: &str) -> Result<(), ProviderArtifactCorpusError> { + if role.is_empty() + || !role.bytes().all(|byte| { + byte.is_ascii_lowercase() || byte.is_ascii_digit() || matches!(byte, b'.' | b'-') + }) + { + return Err(ProviderArtifactCorpusError::new( + ProviderArtifactCorpusErrorKind::CorpusPathInvalid, + role, + "artifact-role-filename", + )); + } + Ok(()) +} + +fn append_u64( + output: &mut Vec, + value: usize, + subject: &str, +) -> Result<(), ProviderArtifactCorpusError> { + let value = u64::try_from(value).map_err(|_| { + ProviderArtifactCorpusError::new( + ProviderArtifactCorpusErrorKind::GeneratorSourceTooLarge, + subject, + "u64-length", + ) + })?; + extend_source_bundle(output, &value.to_be_bytes(), subject) +} + +fn extend_source_bundle( + output: &mut Vec, + bytes: &[u8], + subject: &str, +) -> Result<(), ProviderArtifactCorpusError> { + let length = output.len().checked_add(bytes.len()).ok_or_else(|| { + ProviderArtifactCorpusError::new( + ProviderArtifactCorpusErrorKind::GeneratorSourceTooLarge, + subject, + "source-bundle-length-overflow", + ) + })?; + if length > MAX_SOURCE_BUNDLE_BYTES { + return Err(ProviderArtifactCorpusError::new( + ProviderArtifactCorpusErrorKind::GeneratorSourceTooLarge, + subject, + length.to_string(), + )); + } + output.extend_from_slice(bytes); + Ok(()) +} + +fn corpus_inventory<'a>( + files: &'a [ProviderArtifactCorpusFileV1], + reference: &str, +) -> Result, ProviderArtifactCorpusError> { + let mut inventory = BTreeMap::new(); + for file in files { + if inventory + .insert(file.relative_path(), file.bytes()) + .is_some() + { + return Err(ProviderArtifactCorpusError::new( + ProviderArtifactCorpusErrorKind::CorpusPathDuplicate, + file.relative_path(), + reference, + )); + } + } + Ok(inventory) +} diff --git a/crates/echo-wesley-gen/src/provider_generation.rs b/crates/echo-wesley-gen/src/provider_generation.rs new file mode 100644 index 00000000..23f1d7af --- /dev/null +++ b/crates/echo-wesley-gen/src/provider_generation.rs @@ -0,0 +1,386 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Pure assembly of the canonical Wesley input for Echo provider generation. +//! +//! The first checked Echo provider closure deliberately has no GraphQL Shape +//! facts. Its operation is an Echo semantic declaration, not a synthetic +//! GraphQL root field. This module therefore supplies Wesley's exact empty L1 +//! Shape and operation catalog and binds the Echo source, Edict CDDL, Edict +//! manifest, and settings bytes without discovering any of them. + +use std::fmt; + +use serde::Deserialize; +use wesley_core::{ + compute_generation_artifact_digest_v1, ExtensionGenerationInputV1, GenerationArtifactContentV1, + GenerationArtifactReferenceV1, GenerationContractError, WesleyIR, + WESLEY_EXTENSION_GENERATOR_ABI_VERSION, +}; + +use crate::provider_contract_pack::AdmittedProviderContractPackV1; +use crate::provider_semantics::{ + parse_provider_semantic_source_v1, AuthoritySourceKind, GeneratedArtifactKind, + ProviderSemanticSourceError, ProviderSemanticSourceErrorKind, + ValidatedProviderSemanticSourceV1, +}; + +/// Exact API accepted for Echo provider generator settings. +pub const ECHO_PROVIDER_GENERATION_SETTINGS_API_V1: &str = + "echo.edict-provider-generation-settings/v1"; + +/// Source coordinate used for the exact Edict contract-pack manifest bytes. +pub const EDICT_PROVIDER_CONTRACT_PACK_MANIFEST_COORDINATE_V1: &str = + "edict.provider-contract-pack.manifest@1"; + +const NO_SHAPE_SOURCE: &str = "none"; +const EMPTY_WESLEY_SHAPE_VERSION: &str = "1.0.0"; +const CANONICAL_ARTIFACT_ENCODING: &str = "edict.canonical-cbor/v1"; +const MAX_SEMANTIC_SOURCE_BYTES: usize = 1_048_576; +const MAX_SETTINGS_BYTES: usize = 16_384; + +/// Canonical generation input plus exact materials needed for provenance. +#[derive(Clone, Debug, PartialEq)] +pub struct ProviderGenerationInputV1 { + semantic_source: ValidatedProviderSemanticSourceV1, + settings_bytes: Vec, + wesley_input: ExtensionGenerationInputV1, + canonical_bytes: Vec, + digest: String, + source_artifacts: Vec, +} + +impl ProviderGenerationInputV1 { + /// Returns the normalized, validated Echo semantic source. + #[must_use] + pub const fn semantic_source(&self) -> &ValidatedProviderSemanticSourceV1 { + &self.semantic_source + } + + /// Returns the exact versioned settings bytes bound by the input digest. + #[must_use] + pub fn settings_bytes(&self) -> &[u8] { + &self.settings_bytes + } + + /// Returns the normalized Wesley extension-generation input. + #[must_use] + pub const fn wesley_input(&self) -> &ExtensionGenerationInputV1 { + &self.wesley_input + } + + /// Returns canonical Wesley generation-input bytes. + #[must_use] + pub fn canonical_bytes(&self) -> &[u8] { + &self.canonical_bytes + } + + /// Returns the domain-separated Wesley generation-input digest. + #[must_use] + pub fn digest(&self) -> &str { + &self.digest + } + + /// Returns exact source materials used for later provenance verification. + #[must_use] + pub fn source_artifacts(&self) -> &[GenerationArtifactContentV1] { + &self.source_artifacts + } +} + +/// Stable failure categories returned while assembling generation input. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ProviderGenerationErrorKind { + /// Semantic source or settings input exceeded its pre-parse size bound. + InputSizeExceeded, + /// Exact semantic-source bytes were not valid UTF-8 or failed validation. + SemanticSourceInvalid, + /// Generator settings did not match the strict versioned JSON shape. + SettingsMalformed, + /// Generator settings selected an unsupported API version. + UnsupportedSettingsApiVersion, + /// Generator settings selected a different frozen input contract. + SettingsContractMismatch, + /// A GraphQL authority source had no explicit Shape source bytes. + GraphqlSourceMissing, + /// Wesley rejected owner declarations or canonical generation input. + WesleyContractRejected, +} + +impl ProviderGenerationErrorKind { + const fn label(self) -> &'static str { + match self { + Self::InputSizeExceeded => "input-size-exceeded", + Self::SemanticSourceInvalid => "semantic-source-invalid", + Self::SettingsMalformed => "settings-malformed", + Self::UnsupportedSettingsApiVersion => "unsupported-settings-api-version", + Self::SettingsContractMismatch => "settings-contract-mismatch", + Self::GraphqlSourceMissing => "graphql-source-missing", + Self::WesleyContractRejected => "wesley-contract-rejected", + } + } +} + +/// Structured provider-generation input failure. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ProviderGenerationError { + kind: ProviderGenerationErrorKind, + semantic_source_kind: Option, + subject: String, + reference: String, +} + +impl ProviderGenerationError { + /// Returns the stable failure category. + #[must_use] + pub const fn kind(&self) -> ProviderGenerationErrorKind { + self.kind + } + + /// Returns the underlying semantic-source failure category, when present. + #[must_use] + pub const fn semantic_source_kind(&self) -> Option { + self.semantic_source_kind + } + + /// Returns the input field or coordinate that failed. + #[must_use] + pub fn subject(&self) -> &str { + &self.subject + } + + /// Returns the stable conflicting or expected reference. + #[must_use] + pub fn reference(&self) -> &str { + &self.reference + } + + fn new( + kind: ProviderGenerationErrorKind, + subject: impl Into, + reference: impl Into, + ) -> Self { + Self { + kind, + semantic_source_kind: None, + subject: subject.into(), + reference: reference.into(), + } + } + + fn semantic_source(error: ProviderSemanticSourceError) -> Self { + Self { + kind: ProviderGenerationErrorKind::SemanticSourceInvalid, + semantic_source_kind: Some(error.kind()), + subject: error.subject().to_owned(), + reference: error.reference().to_owned(), + } + } + + fn wesley(error: GenerationContractError) -> Self { + Self::new( + ProviderGenerationErrorKind::WesleyContractRejected, + error.subject, + error.kind.as_str(), + ) + } +} + +impl fmt::Display for ProviderGenerationError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + formatter, + "provider generation {}: {} -> {}", + self.kind.label(), + self.subject, + self.reference + ) + } +} + +impl std::error::Error for ProviderGenerationError {} + +/// Builds the canonical Wesley input for one explicit Echo provider invocation. +/// +/// The exact semantic-source, contract-pack, and settings bytes are caller +/// inputs. This function performs no filesystem, registry, environment, clock, +/// process, or network access. +/// +/// # Errors +/// +/// Returns a stable structured error when source or settings bytes are invalid, +/// select a different frozen contract, or cannot form a canonical Wesley input. +pub fn build_provider_generation_input_v1( + semantic_source_bytes: &[u8], + contract_pack: &AdmittedProviderContractPackV1, + settings_bytes: &[u8], +) -> Result { + preflight_size( + semantic_source_bytes, + MAX_SEMANTIC_SOURCE_BYTES, + "semanticSource.bytes", + )?; + preflight_size(settings_bytes, MAX_SETTINGS_BYTES, "settings.bytes")?; + + let semantic_source_text = std::str::from_utf8(semantic_source_bytes).map_err(|_| { + ProviderGenerationError::new( + ProviderGenerationErrorKind::SemanticSourceInvalid, + "semanticSource", + "utf-8", + ) + })?; + let semantic_source = parse_provider_semantic_source_v1(semantic_source_text) + .map_err(ProviderGenerationError::semantic_source)?; + validate_settings(settings_bytes, contract_pack)?; + if let Some(authority) = semantic_source + .source() + .authority_sources + .iter() + .find(|authority| authority.kind == AuthoritySourceKind::Graphql) + { + return Err(ProviderGenerationError::new( + ProviderGenerationErrorKind::GraphqlSourceMissing, + &authority.coordinate, + &authority.artifact, + )); + } + + let shape_ir = WesleyIR { + version: EMPTY_WESLEY_SHAPE_VERSION.to_owned(), + metadata: None, + types: Vec::new(), + }; + let operations = Vec::new(); + + let source_artifacts = vec![ + GenerationArtifactContentV1::new( + semantic_source.source().coordinate.clone(), + semantic_source_bytes.to_vec(), + ), + GenerationArtifactContentV1::new( + contract_pack.coordinate(), + contract_pack.schema_bytes().to_vec(), + ), + GenerationArtifactContentV1::new( + EDICT_PROVIDER_CONTRACT_PACK_MANIFEST_COORDINATE_V1, + contract_pack.manifest_bytes().to_vec(), + ), + ]; + let owner_declarations = source_artifacts + .iter() + .map(GenerationArtifactContentV1::reference) + .collect::>(); + let projection_roles = semantic_source + .source() + .generated_artifacts + .iter() + .filter(|artifact| { + !matches!( + artifact.kind, + GeneratedArtifactKind::GenerationProvenance | GeneratedArtifactKind::ReviewArtifact + ) + }) + .map(|artifact| artifact.role.clone()) + .collect(); + + let wesley_input = ExtensionGenerationInputV1::new( + shape_ir, + operations, + None, + owner_declarations, + compute_generation_artifact_digest_v1(settings_bytes), + projection_roles, + ) + .map_err(ProviderGenerationError::wesley)?; + let canonical_bytes = wesley_input + .canonical_bytes() + .map_err(ProviderGenerationError::wesley)?; + let digest = wesley_input + .digest() + .map_err(ProviderGenerationError::wesley)?; + Ok(ProviderGenerationInputV1 { + semantic_source, + settings_bytes: settings_bytes.to_vec(), + wesley_input, + canonical_bytes, + digest, + source_artifacts, + }) +} + +fn preflight_size( + bytes: &[u8], + maximum: usize, + subject: &'static str, +) -> Result<(), ProviderGenerationError> { + if bytes.len() <= maximum { + Ok(()) + } else { + Err(ProviderGenerationError::new( + ProviderGenerationErrorKind::InputSizeExceeded, + subject, + maximum.to_string(), + )) + } +} + +fn validate_settings( + settings_bytes: &[u8], + contract_pack: &AdmittedProviderContractPackV1, +) -> Result<(), ProviderGenerationError> { + let settings = + serde_json::from_slice::(settings_bytes).map_err(|_| { + ProviderGenerationError::new( + ProviderGenerationErrorKind::SettingsMalformed, + "settings", + ECHO_PROVIDER_GENERATION_SETTINGS_API_V1, + ) + })?; + if settings.api_version != ECHO_PROVIDER_GENERATION_SETTINGS_API_V1 { + return Err(ProviderGenerationError::new( + ProviderGenerationErrorKind::UnsupportedSettingsApiVersion, + "settings.apiVersion", + ECHO_PROVIDER_GENERATION_SETTINGS_API_V1, + )); + } + for (subject, actual, expected) in [ + ( + "settings.shapeSource", + settings.shape_source.as_str(), + NO_SHAPE_SOURCE, + ), + ( + "settings.canonicalArtifactEncoding", + settings.canonical_artifact_encoding.as_str(), + CANONICAL_ARTIFACT_ENCODING, + ), + ( + "settings.contractPack", + settings.contract_pack.as_str(), + contract_pack.coordinate(), + ), + ( + "settings.generatorAbi", + settings.generator_abi.as_str(), + WESLEY_EXTENSION_GENERATOR_ABI_VERSION, + ), + ] { + if actual != expected { + return Err(ProviderGenerationError::new( + ProviderGenerationErrorKind::SettingsContractMismatch, + subject, + expected, + )); + } + } + Ok(()) +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct ProviderGenerationSettings { + api_version: String, + shape_source: String, + canonical_artifact_encoding: String, + contract_pack: String, + generator_abi: String, +} diff --git a/crates/echo-wesley-gen/src/provider_provenance.rs b/crates/echo-wesley-gen/src/provider_provenance.rs new file mode 100644 index 00000000..153831f6 --- /dev/null +++ b/crates/echo-wesley-gen/src/provider_provenance.rs @@ -0,0 +1,417 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Exact-material Wesley provenance for deterministic provider generation. +//! +//! Provenance binds the exact authored sources, versioned settings, explicit +//! generator component bytes, and six non-derived primary outputs. The +//! generated resources remain transitively bound through those primary bytes; +//! provenance and review are excluded because either would be self-referential. +//! This module performs no filesystem, process, environment, registry, clock, +//! or network discovery. + +use std::fmt; + +use wesley_core::{ + compute_generation_artifact_digest_v1, GenerationArtifactContentV1, + GenerationArtifactReferenceV1, GenerationContractError, GenerationContractErrorKind, + GenerationProvenanceManifestV1, GenerationProvenanceVerificationV1, GeneratorIdentityV1, +}; + +use crate::provider_artifacts::ProviderPrimaryArtifactsV1; +use crate::provider_generation::ProviderGenerationInputV1; +use crate::provider_semantics::GeneratedArtifactKind; + +/// Stable failure categories returned while constructing provider provenance. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ProviderProvenanceErrorKind { + /// The semantic source did not declare its required provenance evidence. + EvidenceDeclarationMissing, + /// Primary input identity or roles did not match the Wesley closure. + PrimaryOutputClosureMismatch, + /// Exact settings bytes did not reproduce the Wesley settings digest. + SettingsMaterialMismatch, + /// Supplied generator coordinate or version differed from the manifest. + GeneratorIdentityMismatch, + /// Generator coordinate collided with a declared provider coordinate. + GeneratorCoordinateConflict, + /// Wesley rejected an identity, reference, manifest, or exact material. + WesleyContractRejected, +} + +impl ProviderProvenanceErrorKind { + const fn label(self) -> &'static str { + match self { + Self::EvidenceDeclarationMissing => "evidence-declaration-missing", + Self::PrimaryOutputClosureMismatch => "primary-output-closure-mismatch", + Self::SettingsMaterialMismatch => "settings-material-mismatch", + Self::GeneratorIdentityMismatch => "generator-identity-mismatch", + Self::GeneratorCoordinateConflict => "generator-coordinate-conflict", + Self::WesleyContractRejected => "wesley-contract-rejected", + } + } +} + +/// Structured, stable failure from provider provenance construction or verification. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ProviderProvenanceError { + kind: ProviderProvenanceErrorKind, + subject: String, + reference: String, + wesley_kind: Option, +} + +impl ProviderProvenanceError { + /// Returns the stable high-level failure category. + #[must_use] + pub const fn kind(&self) -> ProviderProvenanceErrorKind { + self.kind + } + + /// Returns the coordinate or contract field that failed. + #[must_use] + pub fn subject(&self) -> &str { + &self.subject + } + + /// Returns the expected identity, digest, conflicting role, or upstream code. + #[must_use] + pub fn reference(&self) -> &str { + &self.reference + } + + /// Returns the typed Wesley contract cause, when applicable. + #[must_use] + pub const fn wesley_contract_kind(&self) -> Option { + self.wesley_kind + } + + fn new( + kind: ProviderProvenanceErrorKind, + subject: impl Into, + reference: impl Into, + ) -> Self { + Self { + kind, + subject: subject.into(), + reference: reference.into(), + wesley_kind: None, + } + } + + fn wesley(error: GenerationContractError) -> Self { + let kind = error.kind; + let mut result = Self::new( + ProviderProvenanceErrorKind::WesleyContractRejected, + error.subject, + kind.as_str(), + ); + result.wesley_kind = Some(kind); + result + } +} + +impl fmt::Display for ProviderProvenanceError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + formatter, + "provider provenance {}: {} -> {}", + self.kind.label(), + self.subject, + self.reference + ) + } +} + +impl std::error::Error for ProviderProvenanceError {} + +/// Explicit generator identity and exact bytes used for provenance verification. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ProviderGeneratorMaterialV1 { + identity: GeneratorIdentityV1, + bytes: Vec, +} + +impl ProviderGeneratorMaterialV1 { + /// Constructs generator material from explicit component bytes. + /// + /// # Errors + /// + /// Returns a typed Wesley contract error when the coordinate or version is + /// invalid. No executable, path, environment, or process discovery occurs. + pub fn new( + coordinate: impl Into, + version: impl Into, + bytes: &[u8], + ) -> Result { + let identity = GeneratorIdentityV1::for_bytes(coordinate, version, bytes) + .map_err(ProviderProvenanceError::wesley)?; + Ok(Self { + identity, + bytes: bytes.to_vec(), + }) + } + + /// Returns the identity computed from the exact supplied bytes. + #[must_use] + pub const fn identity(&self) -> &GeneratorIdentityV1 { + &self.identity + } + + /// Returns the exact generator component bytes. + #[must_use] + pub fn bytes(&self) -> &[u8] { + &self.bytes + } +} + +/// Canonical, immediately verified provider-generation provenance. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ProviderGenerationProvenanceV1 { + role: String, + coordinate: String, + schema_contract: String, + manifest: GenerationProvenanceManifestV1, + canonical_bytes: Vec, + content_reference: GenerationArtifactReferenceV1, + verification: GenerationProvenanceVerificationV1, +} + +impl ProviderGenerationProvenanceV1 { + /// Returns the source-declared provenance role. + #[must_use] + pub fn role(&self) -> &str { + &self.role + } + + /// Returns the source-declared provenance coordinate. + #[must_use] + pub fn coordinate(&self) -> &str { + &self.coordinate + } + + /// Returns the Wesley-owned provenance schema contract. + #[must_use] + pub fn schema_contract(&self) -> &str { + &self.schema_contract + } + + /// Returns the admitted Wesley provenance manifest. + #[must_use] + pub const fn manifest(&self) -> &GenerationProvenanceManifestV1 { + &self.manifest + } + + /// Returns exact canonical Wesley provenance JSON bytes. + #[must_use] + pub fn canonical_bytes(&self) -> &[u8] { + &self.canonical_bytes + } + + /// Returns the exact-byte reference for the canonical provenance JSON. + #[must_use] + pub const fn content_reference(&self) -> &GenerationArtifactReferenceV1 { + &self.content_reference + } + + /// Returns the receipt from immediate exact-material verification. + #[must_use] + pub const fn verification(&self) -> &GenerationProvenanceVerificationV1 { + &self.verification + } + + /// Re-verifies this manifest against explicit generation materials. + /// + /// # Errors + /// + /// Returns a stable structured failure when settings, projection roles, + /// generator identity, or any exact source or output byte differs. + pub fn verify_exact_materials( + &self, + input: &ProviderGenerationInputV1, + primary: &ProviderPrimaryArtifactsV1, + generator: &ProviderGeneratorMaterialV1, + ) -> Result { + validate_input_materials(input, primary)?; + if generator.identity.coordinate != self.manifest.generator.coordinate { + return Err(ProviderProvenanceError::new( + ProviderProvenanceErrorKind::GeneratorIdentityMismatch, + "generator.coordinate", + &self.manifest.generator.coordinate, + )); + } + if generator.identity.version != self.manifest.generator.version { + return Err(ProviderProvenanceError::new( + ProviderProvenanceErrorKind::GeneratorIdentityMismatch, + "generator.version", + &self.manifest.generator.version, + )); + } + let emitted = emitted_materials(primary); + self.manifest + .verify( + input.wesley_input(), + generator.bytes(), + input.source_artifacts(), + &emitted, + ) + .map_err(ProviderProvenanceError::wesley) + } +} + +/// Constructs canonical provenance and immediately verifies every exact material. +/// +/// The manifest names exactly six non-derived primary outputs: the five +/// schema-validated canonical artifacts and the raw self-contained CDDL. The +/// function is pure and performs no hidden discovery or runtime admission. +/// +/// # Errors +/// +/// Returns a structured failure when the semantic evidence declaration is +/// missing, the primary closure or settings material disagrees with the Wesley +/// input, the generator coordinate aliases the provider closure, or Wesley +/// rejects any identity, reference, manifest, or exact byte. +pub fn generate_provider_generation_provenance_v1( + input: &ProviderGenerationInputV1, + primary: &ProviderPrimaryArtifactsV1, + generator: &ProviderGeneratorMaterialV1, +) -> Result { + validate_input_materials(input, primary)?; + validate_generator_coordinate(input, generator)?; + let declaration = input + .semantic_source() + .source() + .generated_artifacts + .iter() + .find(|artifact| artifact.kind == GeneratedArtifactKind::GenerationProvenance) + .ok_or_else(|| { + ProviderProvenanceError::new( + ProviderProvenanceErrorKind::EvidenceDeclarationMissing, + "generationProvenance", + "generatedArtifacts", + ) + })?; + let emitted = emitted_materials(primary); + let manifest = GenerationProvenanceManifestV1::new( + input.wesley_input(), + generator.identity().clone(), + emitted + .iter() + .map(GenerationArtifactContentV1::reference) + .collect(), + ) + .map_err(ProviderProvenanceError::wesley)?; + let verification = manifest + .verify( + input.wesley_input(), + generator.bytes(), + input.source_artifacts(), + &emitted, + ) + .map_err(ProviderProvenanceError::wesley)?; + let canonical_bytes = manifest + .canonical_bytes() + .map_err(ProviderProvenanceError::wesley)?; + let content_reference = + GenerationArtifactReferenceV1::for_bytes(&declaration.coordinate, &canonical_bytes) + .map_err(ProviderProvenanceError::wesley)?; + + Ok(ProviderGenerationProvenanceV1 { + role: declaration.role.clone(), + coordinate: declaration.coordinate.clone(), + schema_contract: declaration.schema_contract.clone(), + manifest, + canonical_bytes, + content_reference, + verification, + }) +} + +fn validate_generator_coordinate( + input: &ProviderGenerationInputV1, + generator: &ProviderGeneratorMaterialV1, +) -> Result<(), ProviderProvenanceError> { + let source = input.semantic_source().source(); + let coordinate = &generator.identity().coordinate; + let source_conflict = input + .source_artifacts() + .iter() + .any(|artifact| artifact.coordinate == *coordinate); + let mut conflicting_roles = source + .generated_artifacts + .iter() + .filter(|artifact| artifact.coordinate == *coordinate) + .map(|artifact| artifact.role.as_str()) + .chain( + source + .artifact_resources + .iter() + .filter(|resource| resource.coordinate == *coordinate) + .map(|resource| resource.role.as_str()), + ) + .collect::>(); + if source.package_manifest.coordinate == *coordinate { + conflicting_roles.push(&source.package_manifest.role); + } + if source.package_manifest.provider_coordinate == *coordinate { + conflicting_roles.push("packageManifest.providerCoordinate"); + } + conflicting_roles.sort_unstable(); + let conflicting_role = conflicting_roles + .first() + .copied() + .or_else(|| source_conflict.then_some("sourceArtifacts")); + if let Some(role) = conflicting_role { + return Err(ProviderProvenanceError::new( + ProviderProvenanceErrorKind::GeneratorCoordinateConflict, + "generator.coordinate", + role, + )); + } + Ok(()) +} + +fn validate_input_materials( + input: &ProviderGenerationInputV1, + primary: &ProviderPrimaryArtifactsV1, +) -> Result<(), ProviderProvenanceError> { + if primary.generation_input_digest() != input.digest() { + return Err(ProviderProvenanceError::new( + ProviderProvenanceErrorKind::PrimaryOutputClosureMismatch, + "generationInputDigest", + input.digest(), + )); + } + if primary.projection_roles() != input.wesley_input().projection_roles { + return Err(ProviderProvenanceError::new( + ProviderProvenanceErrorKind::PrimaryOutputClosureMismatch, + "projectionRoles", + input.wesley_input().projection_roles.join(","), + )); + } + let settings_digest = compute_generation_artifact_digest_v1(input.settings_bytes()); + if settings_digest != input.wesley_input().settings_digest { + return Err(ProviderProvenanceError::new( + ProviderProvenanceErrorKind::SettingsMaterialMismatch, + "settingsDigest", + settings_digest, + )); + } + Ok(()) +} + +fn emitted_materials(primary: &ProviderPrimaryArtifactsV1) -> Vec { + primary + .artifacts() + .iter() + .map(|artifact| { + GenerationArtifactContentV1::new( + artifact.coordinate(), + artifact.canonical_bytes().to_vec(), + ) + }) + .chain(std::iter::once(GenerationArtifactContentV1::new( + primary.schema().coordinate(), + primary.schema().bytes().to_vec(), + ))) + .collect() +} diff --git a/crates/echo-wesley-gen/src/provider_review.rs b/crates/echo-wesley-gen/src/provider_review.rs new file mode 100644 index 00000000..29f96ad9 --- /dev/null +++ b/crates/echo-wesley-gen/src/provider_review.rs @@ -0,0 +1,203 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Deterministic non-authoritative review of verified provider generation. +//! +//! This module derives Wesley's review projection from an already-verified +//! provider provenance wrapper. Review bytes are convenient human/tooling +//! evidence, never semantic authority or Echo runtime admission. + +use std::fmt; + +use wesley_core::{ + GenerationArtifactReferenceV1, GenerationContractError, GenerationContractErrorKind, + GenerationReviewV1, +}; + +use crate::provider_generation::ProviderGenerationInputV1; +use crate::provider_provenance::ProviderGenerationProvenanceV1; +use crate::provider_semantics::GeneratedArtifactKind; + +/// Stable failure categories returned while deriving provider review evidence. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ProviderReviewErrorKind { + /// The semantic source did not declare its required review evidence. + EvidenceDeclarationMissing, + /// Wesley rejected the input, provenance, review, or content reference. + WesleyContractRejected, +} + +impl ProviderReviewErrorKind { + const fn label(self) -> &'static str { + match self { + Self::EvidenceDeclarationMissing => "evidence-declaration-missing", + Self::WesleyContractRejected => "wesley-contract-rejected", + } + } +} + +/// Structured, stable failure from provider review construction. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ProviderReviewError { + kind: ProviderReviewErrorKind, + subject: String, + reference: String, + wesley_kind: Option, +} + +impl ProviderReviewError { + /// Returns the stable high-level failure category. + #[must_use] + pub const fn kind(&self) -> ProviderReviewErrorKind { + self.kind + } + + /// Returns the coordinate or contract field that failed. + #[must_use] + pub fn subject(&self) -> &str { + &self.subject + } + + /// Returns the expected identity or stable upstream error code. + #[must_use] + pub fn reference(&self) -> &str { + &self.reference + } + + /// Returns the typed Wesley contract cause, when applicable. + #[must_use] + pub const fn wesley_contract_kind(&self) -> Option { + self.wesley_kind + } + + fn new( + kind: ProviderReviewErrorKind, + subject: impl Into, + reference: impl Into, + ) -> Self { + Self { + kind, + subject: subject.into(), + reference: reference.into(), + wesley_kind: None, + } + } + + fn wesley(error: GenerationContractError) -> Self { + let kind = error.kind; + let mut result = Self::new( + ProviderReviewErrorKind::WesleyContractRejected, + error.subject, + kind.as_str(), + ); + result.wesley_kind = Some(kind); + result + } +} + +impl fmt::Display for ProviderReviewError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + formatter, + "provider generation review {}: {} -> {}", + self.kind.label(), + self.subject, + self.reference + ) + } +} + +impl std::error::Error for ProviderReviewError {} + +/// Canonical non-authoritative review derived from verified provenance. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ProviderGenerationReviewV1 { + role: String, + coordinate: String, + schema_contract: String, + review: GenerationReviewV1, + canonical_bytes: Vec, + content_reference: GenerationArtifactReferenceV1, +} + +impl ProviderGenerationReviewV1 { + /// Returns the source-declared review role. + #[must_use] + pub fn role(&self) -> &str { + &self.role + } + + /// Returns the source-declared review coordinate. + #[must_use] + pub fn coordinate(&self) -> &str { + &self.coordinate + } + + /// Returns the Wesley-owned review schema contract. + #[must_use] + pub fn schema_contract(&self) -> &str { + &self.schema_contract + } + + /// Returns the admitted Wesley review projection. + #[must_use] + pub const fn review(&self) -> &GenerationReviewV1 { + &self.review + } + + /// Returns exact canonical Wesley review JSON bytes. + #[must_use] + pub fn canonical_bytes(&self) -> &[u8] { + &self.canonical_bytes + } + + /// Returns the exact-byte reference for the canonical review JSON. + #[must_use] + pub const fn content_reference(&self) -> &GenerationArtifactReferenceV1 { + &self.content_reference + } +} + +/// Derives canonical non-authoritative review JSON from verified provenance. +/// +/// The function is pure and performs no filesystem, process, environment, +/// registry, clock, network, package installation, or Echo runtime admission. +/// +/// # Errors +/// +/// Returns a structured failure when review evidence is undeclared or Wesley +/// rejects the input/provenance relationship, review, or content reference. +pub fn generate_provider_generation_review_v1( + input: &ProviderGenerationInputV1, + provenance: &ProviderGenerationProvenanceV1, +) -> Result { + let declaration = input + .semantic_source() + .source() + .generated_artifacts + .iter() + .find(|artifact| artifact.kind == GeneratedArtifactKind::ReviewArtifact) + .ok_or_else(|| { + ProviderReviewError::new( + ProviderReviewErrorKind::EvidenceDeclarationMissing, + "generationReview", + "generatedArtifacts", + ) + })?; + let review = GenerationReviewV1::from_manifest(input.wesley_input(), provenance.manifest()) + .map_err(ProviderReviewError::wesley)?; + let canonical_bytes = review + .canonical_bytes() + .map_err(ProviderReviewError::wesley)?; + let content_reference = + GenerationArtifactReferenceV1::for_bytes(&declaration.coordinate, &canonical_bytes) + .map_err(ProviderReviewError::wesley)?; + + Ok(ProviderGenerationReviewV1 { + role: declaration.role.clone(), + coordinate: declaration.coordinate.clone(), + schema_contract: declaration.schema_contract.clone(), + review, + canonical_bytes, + content_reference, + }) +} diff --git a/crates/echo-wesley-gen/src/provider_semantics.rs b/crates/echo-wesley-gen/src/provider_semantics.rs index f843615b..8cd50153 100644 --- a/crates/echo-wesley-gen/src/provider_semantics.rs +++ b/crates/echo-wesley-gen/src/provider_semantics.rs @@ -925,8 +925,10 @@ pub enum ProviderSemanticSourceErrorKind { AuthorityFactProjectionMismatch, /// A lawpack or target-profile projection was incomplete or contradictory. ArtifactClosureMismatch, - /// The pending Wesley generation-provenance contract was misidentified. + /// The Wesley generation-provenance contract was misidentified. GenerationProvenanceContractMismatch, + /// The Wesley generation-review contract was misidentified. + GenerationReviewContractMismatch, /// A WIT output kind named the wrong canonical artifact domain. OutputDomainMismatch, /// A provider input kind named the wrong canonical artifact domain. @@ -941,6 +943,61 @@ pub enum ProviderSemanticSourceErrorKind { SchemaRootMismatch, } +impl ProviderSemanticSourceErrorKind { + const fn label(self) -> &'static str { + match self { + Self::MalformedDocument => "malformed-document", + Self::UnsupportedApiVersion => "unsupported-api-version", + Self::MissingIdentity => "missing-identity", + Self::DuplicateCoordinate => "duplicate-coordinate", + Self::DuplicateKey => "duplicate-key", + Self::UnknownAuthority => "unknown-authority", + Self::FactDomainMismatch => "fact-domain-mismatch", + Self::FactAuthorityMismatch => "fact-authority-mismatch", + Self::UnknownType => "unknown-type", + Self::UnboundedTypeGraph => "unbounded-type-graph", + Self::CoreTypeAuthorityMismatch => "core-type-authority-mismatch", + Self::UnknownWriteClass => "unknown-write-class", + Self::InvalidWriteClass => "invalid-write-class", + Self::UnknownObstruction => "unknown-obstruction", + Self::UnknownFailure => "unknown-failure", + Self::InvalidFailureKey => "invalid-failure-key", + Self::UnknownEffect => "unknown-effect", + Self::UnknownProfile => "unknown-profile", + Self::UnknownBudget => "unknown-budget", + Self::UnknownCapability => "unknown-capability", + Self::UnknownAdapter => "unknown-adapter", + Self::UnknownSchemaRole => "unknown-schema-role", + Self::MissingSchemaBinding => "missing-schema-binding", + Self::SchemaRoleMismatch => "schema-role-mismatch", + Self::NonAuthoritativeSource => "non-authoritative-source", + Self::TargetIrDomainMismatch => "target-ir-domain-mismatch", + Self::UnsupportedAdapterChain => "unsupported-adapter-chain", + Self::AmbiguousEffectImplementation => "ambiguous-effect-implementation", + Self::MissingEffectImplementation => "missing-effect-implementation", + Self::ImplementationEffectMismatch => "implementation-effect-mismatch", + Self::ProfileEffectMismatch => "profile-effect-mismatch", + Self::UnsupportedEffectShape => "unsupported-effect-shape", + Self::ObstructionMappingMismatch => "obstruction-mapping-mismatch", + Self::UnsupportedObstructionPayloadMapping => "unsupported-obstruction-payload-mapping", + Self::ArtifactSchemaContractMismatch => "artifact-schema-contract-mismatch", + Self::ArtifactContractOwnerMismatch => "artifact-contract-owner-mismatch", + Self::SelfReferentialManifestInventory => "self-referential-manifest-inventory", + Self::ProviderManifestProjectionMismatch => "provider-manifest-projection-mismatch", + Self::AuthorityFactProjectionMismatch => "authority-fact-projection-mismatch", + Self::ArtifactClosureMismatch => "artifact-closure-mismatch", + Self::GenerationProvenanceContractMismatch => "generation-provenance-contract-mismatch", + Self::GenerationReviewContractMismatch => "generation-review-contract-mismatch", + Self::OutputDomainMismatch => "output-domain-mismatch", + Self::InputDomainMismatch => "input-domain-mismatch", + Self::InvocationInputClosureMismatch => "invocation-input-closure-mismatch", + Self::InvocationOutputClosureMismatch => "invocation-output-closure-mismatch", + Self::UnexpectedSchemaBinding => "unexpected-schema-binding", + Self::SchemaRootMismatch => "schema-root-mismatch", + } + } +} + /// Structured semantic-source parsing or validation failure. #[derive(Clone, Debug, Eq, PartialEq)] pub struct ProviderSemanticSourceError { @@ -985,8 +1042,10 @@ impl fmt::Display for ProviderSemanticSourceError { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { write!( formatter, - "provider semantic source {:?}: {} -> {}", - self.kind, self.subject, self.reference + "provider semantic source {}: {} -> {}", + self.kind.label(), + self.subject, + self.reference ) } } @@ -2458,6 +2517,21 @@ fn validate_operation_references( "projection", )); } + } else { + if profile.optic_template.optic_kind != OpticKind::Revelation { + return Err(ProviderSemanticSourceError::new( + ProviderSemanticSourceErrorKind::ProfileEffectMismatch, + subject, + "affectReintegration", + )); + } + if profile.optic_template.boundary_kind != BoundaryKind::Projection { + return Err(ProviderSemanticSourceError::new( + ProviderSemanticSourceErrorKind::ProfileEffectMismatch, + subject, + "affect", + )); + } } if effect .guard_kinds @@ -2598,34 +2672,46 @@ fn validate_generated_artifact_contracts( GeneratedArtifactKind::TargetProfile => "edict.target-profile/v1", GeneratedArtifactKind::AuthorityFacts => "edict.authority-facts/v1", GeneratedArtifactKind::ProviderManifest => "edict.provider-manifest/v1", - GeneratedArtifactKind::ReviewArtifact => "echo.edict-provider.generation-review/v1", + GeneratedArtifactKind::ReviewArtifact => "wesley:GenerationReviewV1", GeneratedArtifactKind::GeneratedArtifactProfile => "echo.generated-artifact-profile/v1", GeneratedArtifactKind::GenerationProvenance => "wesley:GenerationProvenanceManifestV1", GeneratedArtifactKind::ArtifactSchema => "selfContainedCddlV1", }; if artifact.schema_contract != expected { - return Err(ProviderSemanticSourceError::new( - if artifact.kind == GeneratedArtifactKind::GenerationProvenance { + let kind = match artifact.kind { + GeneratedArtifactKind::GenerationProvenance => { ProviderSemanticSourceErrorKind::GenerationProvenanceContractMismatch - } else { - ProviderSemanticSourceErrorKind::ArtifactSchemaContractMismatch - }, + } + GeneratedArtifactKind::ReviewArtifact => { + ProviderSemanticSourceErrorKind::GenerationReviewContractMismatch + } + _ => ProviderSemanticSourceErrorKind::ArtifactSchemaContractMismatch, + }; + return Err(ProviderSemanticSourceError::new( + kind, &artifact.role, &artifact.schema_contract, )); } let expected_contract_owner = match artifact.kind { GeneratedArtifactKind::AuthorityFacts => Some("flyingrobots/edict#157"), - GeneratedArtifactKind::GenerationProvenance => Some("flyingrobots/wesley#728"), + GeneratedArtifactKind::GenerationProvenance | GeneratedArtifactKind::ReviewArtifact => { + Some("flyingrobots/wesley#728") + } _ => None, }; if artifact.contract_owner.as_deref() != expected_contract_owner { - return Err(ProviderSemanticSourceError::new( - if artifact.kind == GeneratedArtifactKind::GenerationProvenance { + let kind = match artifact.kind { + GeneratedArtifactKind::GenerationProvenance => { ProviderSemanticSourceErrorKind::GenerationProvenanceContractMismatch - } else { - ProviderSemanticSourceErrorKind::ArtifactContractOwnerMismatch - }, + } + GeneratedArtifactKind::ReviewArtifact => { + ProviderSemanticSourceErrorKind::GenerationReviewContractMismatch + } + _ => ProviderSemanticSourceErrorKind::ArtifactContractOwnerMismatch, + }; + return Err(ProviderSemanticSourceError::new( + kind, &artifact.role, artifact.contract_owner.as_deref().unwrap_or_default(), )); diff --git a/crates/echo-wesley-gen/tests/provider_artifact_corpus.rs b/crates/echo-wesley-gen/tests/provider_artifact_corpus.rs new file mode 100644 index 00000000..5d0d63f0 --- /dev/null +++ b/crates/echo-wesley-gen/tests/provider_artifact_corpus.rs @@ -0,0 +1,639 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +#![allow(clippy::expect_used, clippy::panic)] +//! Checked deterministic corpus for Echo's generated Edict provider artifacts. + +use echo_wesley_gen::provider_artifacts::{ + generate_provider_primary_artifacts_v1, ProviderPrimaryArtifactsV1, +}; +use echo_wesley_gen::provider_contract_pack::{ + admit_provider_contract_pack_v1, AdmittedProviderContractPackV1, +}; +use echo_wesley_gen::provider_corpus::{ + build_provider_generator_source_bundle_v1, checked_provider_generator_source_bundle_v1, + diff_provider_artifact_corpus_v1, render_provider_artifact_corpus_v1, + ProviderArtifactCorpusDriftKind, ProviderArtifactCorpusErrorKind, ProviderArtifactCorpusFileV1, + ProviderGeneratorSourceFileV1, +}; +use echo_wesley_gen::provider_generation::{ + build_provider_generation_input_v1, ProviderGenerationInputV1, +}; +use echo_wesley_gen::provider_provenance::{ + generate_provider_generation_provenance_v1, ProviderGenerationProvenanceV1, + ProviderGeneratorMaterialV1, +}; +use echo_wesley_gen::provider_review::{ + generate_provider_generation_review_v1, ProviderGenerationReviewV1, +}; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::sync::atomic::{AtomicU64, Ordering}; + +const SOURCE: &[u8] = + include_bytes!("../../../schemas/edict-provider/echo-provider-semantics-v1.json"); +const SETTINGS: &[u8] = + include_bytes!("../../../schemas/edict-provider/generation-settings-v1.json"); +const CONTRACT_CDDL: &[u8] = + include_bytes!("../../../schemas/edict-provider/contracts/v1/edict-provider-contracts.cddl"); +const CONTRACT_MANIFEST: &[u8] = + include_bytes!("../../../schemas/edict-provider/contracts/v1/manifest.json"); + +const GENERATOR_SOURCE_PATHS: [&str; 14] = [ + "Cargo.lock", + "Cargo.toml", + "crates/echo-wesley-gen/Cargo.toml", + "crates/echo-wesley-gen/src/bin/echo-edict-provider-artifacts.rs", + "crates/echo-wesley-gen/src/lib.rs", + "crates/echo-wesley-gen/src/provider_artifacts.rs", + "crates/echo-wesley-gen/src/provider_canonical.rs", + "crates/echo-wesley-gen/src/provider_contract_pack.rs", + "crates/echo-wesley-gen/src/provider_corpus.rs", + "crates/echo-wesley-gen/src/provider_generation.rs", + "crates/echo-wesley-gen/src/provider_provenance.rs", + "crates/echo-wesley-gen/src/provider_review.rs", + "crates/echo-wesley-gen/src/provider_semantics.rs", + "rust-toolchain.toml", +]; + +const CORPUS_PATHS: [&str; 22] = [ + "evidence/provenance.provider-generation.json", + "evidence/review.provider-generation.json", + "primary/authority-facts.echo-dpo.cbor", + "primary/authority-facts.echo-lawpack.cbor", + "primary/generated-artifact-profile.echo-dpo-registration.cbor", + "primary/lawpack.echo-dpo.cbor", + "primary/schema.echo-provider-artifacts.cddl", + "primary/target-profile.echo-dpo.cbor", + "resources/resource.conformance-corpus.cbor", + "resources/resource.lawpack-compatibility.cbor", + "resources/resource.lawpack-exports.cbor", + "resources/resource.lawpack-target-adapter.cbor", + "resources/resource.lawpack-verifier.cbor", + "resources/resource.target-bundle-profile.cbor", + "resources/resource.target-cost-algebra.cbor", + "resources/resource.target-footprint-algebra.cbor", + "resources/resource.target-intrinsics.cbor", + "resources/resource.target-ir.cbor", + "resources/resource.target-lowerer-contract.cbor", + "resources/resource.target-obstruction-taxonomy.cbor", + "resources/resource.target-operation-profiles.cbor", + "resources/resource.target-verifier-contract.cbor", +]; + +static NEXT_TEMP_DIRECTORY: AtomicU64 = AtomicU64::new(0); + +struct TestDirectory(PathBuf); + +impl TestDirectory { + fn new(label: &str) -> Self { + let sequence = NEXT_TEMP_DIRECTORY.fetch_add(1, Ordering::Relaxed); + let path = std::env::temp_dir().join(format!( + "echo-provider-corpus-{label}-{}-{sequence}", + std::process::id() + )); + if path.exists() { + std::fs::remove_dir_all(&path).expect("stale test directory is removable"); + } + std::fs::create_dir_all(&path).expect("test directory is created"); + Self(path) + } + + fn path(&self) -> &Path { + &self.0 + } +} + +impl Drop for TestDirectory { + fn drop(&mut self) { + drop(std::fs::remove_dir_all(&self.0)); + } +} + +fn admitted_pack() -> AdmittedProviderContractPackV1 { + admit_provider_contract_pack_v1(CONTRACT_CDDL, CONTRACT_MANIFEST) + .expect("checked Edict provider contract pack is admitted") +} + +fn generate() -> ( + ProviderGenerationInputV1, + ProviderPrimaryArtifactsV1, + ProviderGeneratorMaterialV1, + ProviderGenerationProvenanceV1, + ProviderGenerationReviewV1, +) { + let pack = admitted_pack(); + let input = build_provider_generation_input_v1(SOURCE, &pack, SETTINGS) + .expect("checked provider generation input builds"); + let primary = generate_provider_primary_artifacts_v1(&input, &pack) + .expect("checked primary provider artifacts generate"); + let generator = checked_provider_generator_source_bundle_v1() + .expect("checked generator source bundle builds") + .generator_material() + .expect("checked generator material builds"); + let provenance = generate_provider_generation_provenance_v1(&input, &primary, &generator) + .expect("checked provider provenance generates"); + let review = generate_provider_generation_review_v1(&input, &provenance) + .expect("checked provider review generates"); + (input, primary, generator, provenance, review) +} + +#[test] +fn source_bundle_has_one_exact_order_independent_binary_frame() { + let first = build_provider_generator_source_bundle_v1(vec![ + ProviderGeneratorSourceFileV1::new("b.rs", b"B").expect("test source path is valid"), + ProviderGeneratorSourceFileV1::new("a.rs", b"A").expect("test source path is valid"), + ]) + .expect("test source bundle builds"); + let reordered = build_provider_generator_source_bundle_v1(vec![ + ProviderGeneratorSourceFileV1::new("a.rs", b"A").expect("test source path is valid"), + ProviderGeneratorSourceFileV1::new("b.rs", b"B").expect("test source path is valid"), + ]) + .expect("reordered test source bundle builds"); + + let mut expected = b"echo.provider-artifact-generator.source-bundle/v1\0".to_vec(); + expected.extend_from_slice(&2_u64.to_be_bytes()); + for (path, bytes) in [("a.rs", b"A".as_slice()), ("b.rs", b"B".as_slice())] { + expected.extend_from_slice( + &u64::try_from(path.len()) + .expect("test path length fits u64") + .to_be_bytes(), + ); + expected.extend_from_slice(path.as_bytes()); + expected.extend_from_slice( + &u64::try_from(bytes.len()) + .expect("test content length fits u64") + .to_be_bytes(), + ); + expected.extend_from_slice(bytes); + } + + assert_eq!(first, reordered); + assert_eq!(first.canonical_bytes(), expected); + assert_eq!( + first + .source_files() + .iter() + .map(ProviderGeneratorSourceFileV1::path) + .collect::>(), + ["a.rs", "b.rs"] + ); +} + +#[test] +fn checked_source_bundle_pins_the_exact_non_circular_generator_closure() { + let first = checked_provider_generator_source_bundle_v1() + .expect("checked generator source bundle builds"); + let second = checked_provider_generator_source_bundle_v1() + .expect("repeated checked generator source bundle builds"); + let first_material = first + .generator_material() + .expect("checked generator material builds"); + let second_material = second + .generator_material() + .expect("repeated checked generator material builds"); + + assert_eq!(first, second); + assert_eq!(first_material, second_material); + assert_eq!( + first + .source_files() + .iter() + .map(ProviderGeneratorSourceFileV1::path) + .collect::>(), + GENERATOR_SOURCE_PATHS + ); + assert_eq!( + first_material.identity().coordinate, + "echo-wesley-gen.provider-artifact-generator@1" + ); + assert_eq!(first_material.identity().version, "0.1.0"); + assert!(first.source_files().iter().all(|source| { + !source + .path() + .starts_with("schemas/edict-provider/generated/v1/") + })); +} + +#[test] +fn source_path_or_content_changes_move_identity_and_invalid_closures_fail_closed() { + let baseline = + build_provider_generator_source_bundle_v1(vec![ProviderGeneratorSourceFileV1::new( + "a.rs", b"A", + ) + .expect("test source path is valid")]) + .expect("baseline source bundle builds"); + let changed_path = + build_provider_generator_source_bundle_v1(vec![ProviderGeneratorSourceFileV1::new( + "b.rs", b"A", + ) + .expect("changed test source path is valid")]) + .expect("changed-path source bundle builds"); + let changed_content = + build_provider_generator_source_bundle_v1(vec![ProviderGeneratorSourceFileV1::new( + "a.rs", b"B", + ) + .expect("test source path is valid")]) + .expect("changed-content source bundle builds"); + + assert_ne!( + baseline + .generator_material() + .expect("baseline generator material builds") + .identity() + .digest, + changed_path + .generator_material() + .expect("changed-path generator material builds") + .identity() + .digest + ); + assert_ne!( + baseline + .generator_material() + .expect("baseline generator material builds") + .identity() + .digest, + changed_content + .generator_material() + .expect("changed-content generator material builds") + .identity() + .digest + ); + + let duplicate = build_provider_generator_source_bundle_v1(vec![ + ProviderGeneratorSourceFileV1::new("a.rs", b"A").expect("test source path is valid"), + ProviderGeneratorSourceFileV1::new("a.rs", b"B") + .expect("duplicate test source path is structurally valid"), + ]) + .expect_err("duplicate source paths fail closed"); + assert_eq!( + duplicate.kind(), + ProviderArtifactCorpusErrorKind::GeneratorSourceDuplicate + ); + assert_eq!(duplicate.subject(), "a.rs"); + + let generated_path = ProviderGeneratorSourceFileV1::new( + "schemas/edict-provider/generated/v1/evidence/provenance.provider-generation.json", + b"self reference", + ) + .expect_err("generated output paths cannot enter generator identity"); + assert_eq!( + generated_path.kind(), + ProviderArtifactCorpusErrorKind::GeneratorSourcePathInvalid + ); +} + +#[test] +fn rendered_corpus_is_byte_identical_complete_and_reports_sorted_drift() { + let (input, primary, generator, provenance, review) = generate(); + let first = + render_provider_artifact_corpus_v1(&input, &primary, &generator, &provenance, &review) + .expect("checked provider corpus renders"); + let second = + render_provider_artifact_corpus_v1(&input, &primary, &generator, &provenance, &review) + .expect("repeated checked provider corpus renders"); + + assert_eq!(first, second); + assert_eq!( + first + .files() + .iter() + .map(ProviderArtifactCorpusFileV1::relative_path) + .collect::>(), + CORPUS_PATHS + ); + assert!(first.files().iter().all(|file| !file.bytes().is_empty())); + assert!(diff_provider_artifact_corpus_v1(&first, first.files()) + .expect("identical corpus inventory compares") + .is_empty()); + + let mut actual = first.files().to_vec(); + actual.retain(|file| file.relative_path() != "primary/lawpack.echo-dpo.cbor"); + let changed = actual + .iter() + .position(|file| file.relative_path() == "evidence/review.provider-generation.json") + .expect("review file exists"); + actual[changed] = + ProviderArtifactCorpusFileV1::new("evidence/review.provider-generation.json", b"tampered") + .expect("changed corpus path is valid"); + actual.push( + ProviderArtifactCorpusFileV1::new("unexpected.bin", b"unexpected") + .expect("unexpected corpus path is valid"), + ); + + let drift = diff_provider_artifact_corpus_v1(&first, &actual) + .expect("drifted corpus inventory compares"); + assert_eq!( + drift + .iter() + .map(|entry| (entry.kind(), entry.relative_path())) + .collect::>(), + [ + ( + ProviderArtifactCorpusDriftKind::Missing, + "primary/lawpack.echo-dpo.cbor", + ), + ( + ProviderArtifactCorpusDriftKind::Changed, + "evidence/review.provider-generation.json", + ), + ( + ProviderArtifactCorpusDriftKind::Unexpected, + "unexpected.bin", + ), + ] + ); +} + +#[test] +fn corpus_rejects_provenance_that_does_not_bind_the_supplied_generator() { + let (input, primary, _generator, provenance, review) = generate(); + let other_generator = ProviderGeneratorMaterialV1::new( + "echo-wesley-gen.provider-artifact-generator@1", + "0.1.0", + b"other generator material", + ) + .expect("alternate generator material is structurally valid"); + + let error = render_provider_artifact_corpus_v1( + &input, + &primary, + &other_generator, + &provenance, + &review, + ) + .expect_err("mismatched generator material fails closed"); + assert_eq!( + error.kind(), + ProviderArtifactCorpusErrorKind::ProvenanceInvalid + ); + assert!(error.provenance_kind().is_some()); +} + +#[test] +fn oversized_corpus_paths_preserve_the_corpus_error_category() { + let path = format!("{}.cbor", "a".repeat(513)); + let error = ProviderArtifactCorpusFileV1::new(&path, b"content") + .expect_err("oversized corpus paths fail closed"); + + assert_eq!( + error.kind(), + ProviderArtifactCorpusErrorKind::CorpusPathInvalid + ); + assert_eq!(error.subject(), path); + assert_eq!(error.reference(), path.len().to_string()); +} + +#[test] +fn platform_specific_or_traversing_corpus_paths_fail_closed_as_non_posix() { + for path in [r"..\escape", r"C:\outside", "C:/outside"] { + let error = ProviderArtifactCorpusFileV1::new(path, b"content") + .expect_err("non-POSIX or prefixed corpus paths fail closed"); + assert_eq!( + error.kind(), + ProviderArtifactCorpusErrorKind::CorpusPathInvalid + ); + assert_eq!(error.subject(), path); + assert_eq!(error.reference(), "relative-posix-path"); + } +} + +#[test] +fn check_mode_reports_drift_without_rewriting_or_creating_files() { + let directory = TestDirectory::new("no-write-check"); + let changed_path = directory + .path() + .join("evidence/review.provider-generation.json"); + let unexpected_path = directory.path().join("unexpected file.txt"); + std::fs::create_dir_all( + changed_path + .parent() + .expect("changed test path has a parent"), + ) + .expect("evidence test directory is created"); + std::fs::write(&changed_path, b"tampered").expect("changed test file is written"); + std::fs::write(&unexpected_path, b"unexpected").expect("unexpected test file is written"); + + let output = Command::new(env!("CARGO_BIN_EXE_echo-edict-provider-artifacts")) + .arg("--check") + .arg("--out") + .arg(directory.path()) + .output() + .expect("provider corpus checker executes"); + + assert!(!output.status.success()); + let stderr = String::from_utf8(output.stderr).expect("checker diagnostics are UTF-8"); + let missing = stderr + .find(" missing: ") + .expect("checker reports missing files"); + let changed = stderr + .find(" changed: evidence/review.provider-generation.json") + .expect("checker reports the changed review"); + let unexpected = stderr + .find(" unexpected: unexpected file.txt") + .expect("checker reports the unexpected file"); + assert!(missing < changed); + assert!(changed < unexpected); + assert_eq!( + std::fs::read(&changed_path).expect("changed test file remains readable"), + b"tampered" + ); + assert_eq!( + std::fs::read(&unexpected_path).expect("unexpected test file remains readable"), + b"unexpected" + ); + assert!(!directory.path().join("primary").exists()); + assert!(!directory.path().join("resources").exists()); +} + +#[test] +fn generation_refuses_unexpected_entries_before_writing_expected_files() { + let directory = TestDirectory::new("generation-unexpected-entry"); + let unexpected_path = directory.path().join("operator-owned.txt"); + std::fs::write(&unexpected_path, b"operator bytes").expect("unexpected test file is written"); + + let output = Command::new(env!("CARGO_BIN_EXE_echo-edict-provider-artifacts")) + .arg("--out") + .arg(directory.path()) + .output() + .expect("provider corpus generator executes"); + + assert!(!output.status.success()); + assert!(String::from_utf8_lossy(&output.stderr) + .contains("refusing to generate over unexpected corpus entry operator-owned.txt")); + assert_eq!( + std::fs::read(&unexpected_path).expect("unexpected test file remains readable"), + b"operator bytes" + ); + assert!(!directory.path().join("evidence").exists()); + assert!(!directory.path().join("primary").exists()); + assert!(!directory.path().join("resources").exists()); +} + +#[cfg(unix)] +#[test] +fn generation_refuses_symlinked_root_parent_and_leaf_without_writing_through_them() { + use std::os::unix::fs::symlink; + + let root_parent = TestDirectory::new("symlink-root-parent"); + let root_destination = TestDirectory::new("symlink-root-destination"); + let root_link = root_parent.path().join("corpus-link"); + symlink(root_destination.path(), &root_link).expect("root test symlink is created"); + let root_output = Command::new(env!("CARGO_BIN_EXE_echo-edict-provider-artifacts")) + .arg("--out") + .arg(&root_link) + .output() + .expect("provider corpus generator executes against a symlink root"); + assert!(!root_output.status.success()); + assert!( + std::fs::read_dir(root_destination.path()) + .expect("root destination remains readable") + .next() + .is_none(), + "generator must not write through a symlinked root" + ); + + let parent_root = TestDirectory::new("symlink-parent-root"); + let parent_destination = TestDirectory::new("symlink-parent-destination"); + symlink( + parent_destination.path(), + parent_root.path().join("evidence"), + ) + .expect("parent test symlink is created"); + let parent_output = Command::new(env!("CARGO_BIN_EXE_echo-edict-provider-artifacts")) + .arg("--out") + .arg(parent_root.path()) + .output() + .expect("provider corpus generator executes against a symlink parent"); + assert!(!parent_output.status.success()); + assert!( + std::fs::read_dir(parent_destination.path()) + .expect("parent destination remains readable") + .next() + .is_none(), + "generator must not write through a symlinked parent" + ); + assert!(!parent_root.path().join("primary").exists()); + + let leaf_root = TestDirectory::new("symlink-leaf-root"); + let leaf_destination = TestDirectory::new("symlink-leaf-destination"); + let outside_file = leaf_destination.path().join("owner.txt"); + std::fs::write(&outside_file, b"owner bytes").expect("outside owner file is written"); + std::fs::create_dir_all(leaf_root.path().join("evidence")) + .expect("leaf test parent is created"); + symlink( + &outside_file, + leaf_root + .path() + .join("evidence/provenance.provider-generation.json"), + ) + .expect("leaf test symlink is created"); + let leaf_output = Command::new(env!("CARGO_BIN_EXE_echo-edict-provider-artifacts")) + .arg("--out") + .arg(leaf_root.path()) + .output() + .expect("provider corpus generator executes against a symlink leaf"); + assert!(!leaf_output.status.success()); + assert_eq!( + std::fs::read(&outside_file).expect("outside owner file remains readable"), + b"owner bytes" + ); + assert!(!leaf_root + .path() + .join("evidence/review.provider-generation.json") + .exists()); +} + +#[cfg(unix)] +#[test] +fn generation_replaces_a_hard_link_without_mutating_its_external_inode() { + let corpus_root = TestDirectory::new("hard-link-root"); + let external_root = TestDirectory::new("hard-link-external"); + let external_file = external_root.path().join("owner.txt"); + std::fs::write(&external_file, b"owner bytes").expect("outside owner file is written"); + let corpus_file = corpus_root + .path() + .join("evidence/provenance.provider-generation.json"); + std::fs::create_dir_all( + corpus_file + .parent() + .expect("hard-link corpus path has a parent"), + ) + .expect("hard-link corpus parent is created"); + std::fs::hard_link(&external_file, &corpus_file).expect("hard-link corpus leaf is created"); + + let output = Command::new(env!("CARGO_BIN_EXE_echo-edict-provider-artifacts")) + .arg("--out") + .arg(corpus_root.path()) + .output() + .expect("provider corpus generator executes against a hard-linked leaf"); + + assert!( + output.status.success(), + "safe corpus replacement failed:\n{}", + String::from_utf8_lossy(&output.stderr) + ); + assert_eq!( + std::fs::read(&external_file).expect("outside owner file remains readable"), + b"owner bytes" + ); + assert_ne!( + std::fs::read(&corpus_file).expect("generated corpus file is readable"), + b"owner bytes" + ); +} + +#[cfg(unix)] +#[test] +fn check_mode_classifies_a_non_regular_expected_leaf_without_following_it() { + use std::os::unix::fs::symlink; + + let corpus_root = TestDirectory::new("check-symlink-root"); + let external_root = TestDirectory::new("check-symlink-external"); + let external_file = external_root.path().join("owner.txt"); + std::fs::write(&external_file, b"owner bytes").expect("outside owner file is written"); + let corpus_file = corpus_root + .path() + .join("evidence/review.provider-generation.json"); + std::fs::create_dir_all( + corpus_file + .parent() + .expect("check symlink corpus path has a parent"), + ) + .expect("check symlink corpus parent is created"); + symlink(&external_file, &corpus_file).expect("check symlink corpus leaf is created"); + + let output = Command::new(env!("CARGO_BIN_EXE_echo-edict-provider-artifacts")) + .arg("--check") + .arg("--out") + .arg(corpus_root.path()) + .output() + .expect("provider corpus checker executes against a symlink leaf"); + + assert!(!output.status.success()); + assert!(String::from_utf8_lossy(&output.stderr) + .contains(" changed: evidence/review.provider-generation.json")); + assert_eq!( + std::fs::read(&external_file).expect("outside owner file remains readable"), + b"owner bytes" + ); +} + +#[test] +fn checked_first_corpus_matches_the_renderer_without_missing_or_extra_files() { + let corpus_root = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../schemas/edict-provider/generated/v1"); + let output = Command::new(env!("CARGO_BIN_EXE_echo-edict-provider-artifacts")) + .arg("--check") + .arg("--out") + .arg(&corpus_root) + .output() + .expect("provider corpus checker executes against the checked snapshot"); + + assert!( + output.status.success(), + "checked provider corpus drifted:\n{}", + String::from_utf8_lossy(&output.stderr) + ); + assert!(String::from_utf8_lossy(&output.stdout).contains("is current")); +} diff --git a/crates/echo-wesley-gen/tests/provider_canonical_contract.rs b/crates/echo-wesley-gen/tests/provider_canonical_contract.rs new file mode 100644 index 00000000..7bcedf5f --- /dev/null +++ b/crates/echo-wesley-gen/tests/provider_canonical_contract.rs @@ -0,0 +1,370 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +#![allow(clippy::expect_used)] +//! Contract tests for Edict canonical values and admitted CDDL roots. + +use echo_wesley_gen::provider_canonical::{ + decode_canonical_cbor_v1, digest_canonical_value_v1, encode_canonical_cbor_v1, + CanonicalValueErrorKind, CanonicalValueV1, MAX_CANONICAL_NESTING_DEPTH_V1, +}; +use echo_wesley_gen::provider_contract_pack::{ + admit_provider_contract_pack_v1, ProviderContractValidationErrorKind, +}; + +const CONTRACT_CDDL: &[u8] = + include_bytes!("../../../schemas/edict-provider/contracts/v1/edict-provider-contracts.cddl"); +const CONTRACT_MANIFEST: &[u8] = + include_bytes!("../../../schemas/edict-provider/contracts/v1/manifest.json"); + +fn text(value: &str) -> CanonicalValueV1 { + CanonicalValueV1::Text(value.to_owned()) +} + +fn map( + entries: impl IntoIterator, +) -> CanonicalValueV1 { + CanonicalValueV1::Map(entries.into_iter().collect()) +} + +fn string_map( + entries: impl IntoIterator, +) -> CanonicalValueV1 { + map(entries.into_iter().map(|(key, value)| (text(key), value))) +} + +fn typed_digest(byte: u8) -> CanonicalValueV1 { + CanonicalValueV1::Array(vec![ + text("sha256"), + CanonicalValueV1::Bytes(vec![byte; 32]), + ]) +} + +fn authority_facts() -> CanonicalValueV1 { + string_map([ + ("apiVersion", text("edict.authority-facts/v1")), + ( + "source", + string_map([ + ("kind", text("lawpack")), + ("coordinate", text("echo.dpo-lawpack@1")), + ("digest", typed_digest(0x11)), + ]), + ), + ("operationProfiles", map([])), + ("effectWriteClasses", map([])), + ("budgets", map([])), + ]) +} + +fn nested_array(depth: usize) -> CanonicalValueV1 { + (0..depth).fold(CanonicalValueV1::Null, |value, _| { + CanonicalValueV1::Array(vec![value]) + }) +} + +fn nested_array_bytes(depth: usize) -> Vec { + let mut bytes = vec![0x81; depth]; + bytes.push(0xf6); + bytes +} + +fn nested_empty_array(container_count: usize) -> CanonicalValueV1 { + assert!(container_count > 0); + (1..container_count).fold(CanonicalValueV1::Array(Vec::new()), |value, _| { + CanonicalValueV1::Array(vec![value]) + }) +} + +fn nested_empty_array_bytes(container_count: usize) -> Vec { + assert!(container_count > 0); + let mut bytes = vec![0x81; container_count - 1]; + bytes.push(0x80); + bytes +} + +fn nested_empty_map(container_count: usize) -> CanonicalValueV1 { + assert!(container_count > 0); + (1..container_count).fold(CanonicalValueV1::Map(Vec::new()), |value, _| { + CanonicalValueV1::Map(vec![(text("k"), value)]) + }) +} + +fn nested_empty_map_bytes(container_count: usize) -> Vec { + assert!(container_count > 0); + let mut bytes = Vec::with_capacity((container_count - 1) * 3 + 1); + for _ in 1..container_count { + bytes.extend([0xa1, 0x61, b'k']); + } + bytes.push(0xa0); + bytes +} + +#[test] +fn admitted_resources_are_exact_canonical_byte_and_digest_oracles() { + let pack = admit_provider_contract_pack_v1(CONTRACT_CDDL, CONTRACT_MANIFEST) + .expect("checked Edict provider contract pack is admitted"); + + for resource in pack.resources() { + let decoded = decode_canonical_cbor_v1(resource.canonical_bytes()) + .expect("admitted resource uses canonical CBOR"); + assert_eq!( + encode_canonical_cbor_v1(&decoded).expect("decoded resource re-encodes"), + resource.canonical_bytes() + ); + assert_eq!( + digest_canonical_value_v1(resource.coordinate(), &decoded) + .expect("resource domain digest computes"), + resource.domain_framed_digest() + ); + } +} + +#[test] +fn canonical_maps_are_order_independent_and_fail_closed() { + let forward = string_map([("z", text("last")), ("a", text("first"))]); + let reversed = string_map([("a", text("first")), ("z", text("last"))]); + assert_eq!( + encode_canonical_cbor_v1(&forward).expect("forward map encodes"), + encode_canonical_cbor_v1(&reversed).expect("reversed map encodes") + ); + let heterogeneous = map([ + (text(""), CanonicalValueV1::Null), + (CanonicalValueV1::Integer(24), CanonicalValueV1::Null), + ]); + assert_eq!( + encode_canonical_cbor_v1(&heterogeneous).expect("heterogeneous map encodes"), + [0xa2, 0x18, 0x18, 0xf6, 0x60, 0xf6] + ); + + let duplicate = map([(text("same"), text("one")), (text("same"), text("two"))]); + assert_eq!( + encode_canonical_cbor_v1(&duplicate) + .expect_err("duplicate canonical keys reject") + .kind(), + CanonicalValueErrorKind::DuplicateMapKey + ); + assert_eq!( + decode_canonical_cbor_v1(&[0x18, 0x00]) + .expect_err("non-minimal integer rejects") + .kind(), + CanonicalValueErrorKind::NonCanonical + ); + assert_eq!( + decode_canonical_cbor_v1(&[0xf6, 0xf6]) + .expect_err("trailing canonical value rejects") + .kind(), + CanonicalValueErrorKind::TrailingData + ); + assert_eq!( + decode_canonical_cbor_v1(&[0xa2, 0x61, b'a', 0x01, 0x61, b'a', 0x02]) + .expect_err("duplicate decoded map keys reject") + .kind(), + CanonicalValueErrorKind::DuplicateMapKey + ); + assert_eq!( + decode_canonical_cbor_v1(&[0xa2, 0x61, b'b', 0x00, 0x61, b'a', 0x00]) + .expect_err("unsorted decoded map keys reject") + .kind(), + CanonicalValueErrorKind::NonCanonical + ); +} + +#[test] +fn canonical_integer_ranges_and_widths_are_exact() { + let cases: &[(i128, &[u8])] = &[ + (23, &[0x17]), + (24, &[0x18, 0x18]), + (255, &[0x18, 0xff]), + (256, &[0x19, 0x01, 0x00]), + (65_535, &[0x19, 0xff, 0xff]), + (65_536, &[0x1a, 0x00, 0x01, 0x00, 0x00]), + (4_294_967_295, &[0x1a, 0xff, 0xff, 0xff, 0xff]), + ( + 4_294_967_296, + &[0x1b, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00], + ), + ( + i128::from(u64::MAX), + &[0x1b, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff], + ), + ( + -1 - i128::from(u64::MAX), + &[0x3b, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff], + ), + ]; + for (integer, expected) in cases { + let value = CanonicalValueV1::Integer(*integer); + let bytes = encode_canonical_cbor_v1(&value).expect("boundary integer encodes"); + assert_eq!(bytes, *expected); + assert_eq!( + decode_canonical_cbor_v1(expected).expect("boundary integer decodes"), + value + ); + } + + for integer in [i128::from(u64::MAX) + 1, -2 - i128::from(u64::MAX)] { + assert_eq!( + encode_canonical_cbor_v1(&CanonicalValueV1::Integer(integer)) + .expect_err("out-of-range integer rejects") + .kind(), + CanonicalValueErrorKind::InvalidInteger + ); + } +} + +#[test] +fn canonical_nesting_bound_includes_empty_containers_but_not_digest_framing() { + let at_limit = nested_array(MAX_CANONICAL_NESTING_DEPTH_V1); + let at_limit_bytes = nested_array_bytes(MAX_CANONICAL_NESTING_DEPTH_V1); + assert_eq!( + encode_canonical_cbor_v1(&at_limit).expect("maximum-depth value encodes"), + at_limit_bytes + ); + assert_eq!( + decode_canonical_cbor_v1(&at_limit_bytes).expect("maximum-depth bytes decode"), + at_limit + ); + digest_canonical_value_v1("test.maximum-depth/v1", &at_limit) + .expect("digest frame preserves the full artifact depth budget"); + + let over_limit = MAX_CANONICAL_NESTING_DEPTH_V1 + 1; + assert_eq!( + encode_canonical_cbor_v1(&nested_array(over_limit)) + .expect_err("over-depth value rejects") + .kind(), + CanonicalValueErrorKind::NestingLimitExceeded + ); + assert_eq!( + decode_canonical_cbor_v1(&nested_array_bytes(over_limit)) + .expect_err("over-depth bytes reject") + .kind(), + CanonicalValueErrorKind::NestingLimitExceeded + ); + + let empty_cases = [ + ( + nested_empty_array(MAX_CANONICAL_NESTING_DEPTH_V1), + nested_empty_array(over_limit), + nested_empty_array_bytes(MAX_CANONICAL_NESTING_DEPTH_V1), + nested_empty_array_bytes(over_limit), + ), + ( + nested_empty_map(MAX_CANONICAL_NESTING_DEPTH_V1), + nested_empty_map(over_limit), + nested_empty_map_bytes(MAX_CANONICAL_NESTING_DEPTH_V1), + nested_empty_map_bytes(over_limit), + ), + ]; + for (at_limit, over_limit_value, at_limit_bytes, over_limit_bytes) in empty_cases { + assert_eq!( + encode_canonical_cbor_v1(&at_limit).expect("128 empty containers encode"), + at_limit_bytes + ); + assert_eq!( + decode_canonical_cbor_v1(&at_limit_bytes).expect("128 empty containers decode"), + at_limit + ); + assert_eq!( + encode_canonical_cbor_v1(&over_limit_value) + .expect_err("the 129th empty container rejects on encode") + .kind(), + CanonicalValueErrorKind::NestingLimitExceeded + ); + assert_eq!( + decode_canonical_cbor_v1(&over_limit_bytes) + .expect_err("the 129th empty container rejects on decode") + .kind(), + CanonicalValueErrorKind::NestingLimitExceeded + ); + } +} + +#[test] +fn unsupported_cbor_and_digest_domains_fail_closed() { + for bytes in [ + &[0xc0, 0xf6][..], + &[0xf9, 0x00, 0x00], + &[0xf7], + &[0x9f, 0xff], + &[0x1c], + ] { + assert_eq!( + decode_canonical_cbor_v1(bytes) + .expect_err("unsupported CBOR form rejects") + .kind(), + CanonicalValueErrorKind::UnsupportedCbor + ); + } + assert_eq!( + decode_canonical_cbor_v1(&[0x61, 0xff]) + .expect_err("invalid UTF-8 rejects") + .kind(), + CanonicalValueErrorKind::InvalidUtf8 + ); + assert_eq!( + decode_canonical_cbor_v1(&[0x9b, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]) + .expect_err("oversized declared collection rejects before allocation") + .kind(), + CanonicalValueErrorKind::UnexpectedEof + ); + let value = text("domain-separated"); + assert_eq!( + digest_canonical_value_v1("", &value) + .expect_err("empty domain rejects") + .kind(), + CanonicalValueErrorKind::UnsupportedValue + ); + assert_ne!( + digest_canonical_value_v1("test.first/v1", &value).expect("first digest computes"), + digest_canonical_value_v1("test.second/v1", &value).expect("second digest computes") + ); +} + +#[test] +fn contract_admission_requires_canonical_bytes_and_the_owning_root() { + let pack = admit_provider_contract_pack_v1(CONTRACT_CDDL, CONTRACT_MANIFEST) + .expect("checked Edict provider contract pack is admitted"); + let value = authority_facts(); + let bytes = encode_canonical_cbor_v1(&value).expect("authority facts encode"); + + let admitted = pack + .validate_contract_bytes("authority-facts", &bytes) + .expect("authority facts satisfy their admitted root"); + assert_eq!( + encode_canonical_cbor_v1(&admitted).expect("admitted value re-encodes"), + bytes + ); + assert_eq!( + pack.validate_contract_bytes("echo-owned", &[0x18, 0x00]) + .expect_err("unknown contract rejects before byte decoding") + .kind(), + ProviderContractValidationErrorKind::UnknownContract + ); + let canonical_error = pack + .validate_contract_bytes("authority-facts", &[0x18, 0x00]) + .expect_err("noncanonical bytes reject before schema admission"); + assert_eq!(canonical_error.subject(), "authority-facts"); + assert_eq!( + canonical_error.kind(), + ProviderContractValidationErrorKind::CanonicalEncodingInvalid + ); + assert_eq!( + canonical_error.canonical_value_kind(), + Some(CanonicalValueErrorKind::NonCanonical) + ); + let null = encode_canonical_cbor_v1(&CanonicalValueV1::Null).expect("null encodes"); + let schema_error = pack + .validate_contract_bytes("authority-facts", &null) + .expect_err("decoding is not owning-root admission"); + assert_eq!( + schema_error.kind(), + ProviderContractValidationErrorKind::SchemaMismatch + ); + assert_eq!(schema_error.canonical_value_kind(), None); + assert_eq!( + pack.validate_contract_bytes("core-module", &bytes) + .expect_err("a value is admitted only through its owning root") + .kind(), + ProviderContractValidationErrorKind::SchemaMismatch + ); +} diff --git a/crates/echo-wesley-gen/tests/provider_contract_pack.rs b/crates/echo-wesley-gen/tests/provider_contract_pack.rs new file mode 100644 index 00000000..e66f3601 --- /dev/null +++ b/crates/echo-wesley-gen/tests/provider_contract_pack.rs @@ -0,0 +1,282 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +#![allow(clippy::expect_used, clippy::panic)] +//! Integration tests for explicit admission of the Edict provider contract pack. + +use echo_wesley_gen::provider_contract_pack::{ + admit_provider_contract_pack_v1, ProviderContractPackErrorKind, +}; +use serde_json::Value; + +const CONTRACT_CDDL: &[u8] = + include_bytes!("../../../schemas/edict-provider/contracts/v1/edict-provider-contracts.cddl"); +const CONTRACT_MANIFEST: &[u8] = + include_bytes!("../../../schemas/edict-provider/contracts/v1/manifest.json"); +const DOMAIN_ROOTS: [(&str, &str); 6] = [ + ("edict.authority-facts/v1", "authority-facts"), + ("edict.core.module/v1", "core-module"), + ("edict.lawpack/v1", "lawpack-manifest"), + ("edict.lowering-requirements/v1", "lowering-requirements"), + ("edict.target-ir.artifact/v1", "target-ir-artifact"), + ("edict.target-profile/v1", "target-profile-manifest"), +]; + +fn manifest_value() -> Value { + serde_json::from_slice(CONTRACT_MANIFEST).expect("checked contract manifest is JSON") +} + +fn mutated_manifest(mutate: impl FnOnce(&mut Value)) -> Vec { + let mut manifest = manifest_value(); + mutate(&mut manifest); + serde_json::to_vec(&manifest).expect("mutated contract manifest serializes") +} + +fn assert_manifest_failure(manifest: &[u8], expected: ProviderContractPackErrorKind) { + let error = admit_provider_contract_pack_v1(CONTRACT_CDDL, manifest) + .expect_err("tampered contract manifest must fail admission"); + assert_eq!(error.kind(), expected); +} + +#[test] +fn checked_edict_provider_contract_pack_is_admitted() { + let pack = admit_provider_contract_pack_v1(CONTRACT_CDDL, CONTRACT_MANIFEST) + .expect("checked Edict provider contract pack is admitted"); + + assert_eq!(pack.api_version(), "edict.provider-contract-pack/v1"); + assert_eq!(pack.coordinate(), "edict.provider-contract-pack.cddl@1"); + assert_eq!(pack.license(), "Apache-2.0"); + assert_eq!( + pack.schema_sha256(), + "92697bc9a5262c68258be9ee451ee8c144aeb363b92142915b8224430b85cf74" + ); + assert_eq!(pack.contract_count(), 9); + assert_eq!(pack.domain_count(), 6); + assert_eq!(pack.resource_count(), 5); + assert_eq!(pack.schema_bytes(), CONTRACT_CDDL); + assert_eq!(pack.manifest_bytes(), CONTRACT_MANIFEST); + for (domain, root) in DOMAIN_ROOTS { + assert_eq!(pack.root_for_domain(domain), Some(root)); + } + let canonical_cbor = pack + .resource("edict.canonical-cbor/v1") + .expect("canonical CBOR contract resource is admitted"); + assert_eq!( + canonical_cbor.raw_sha256(), + "8306e4f08c1e4e7d29ab22bcf55c324312712aac3eeeb675857ced57c3e48bdc" + ); + assert_eq!( + canonical_cbor.repository(), + "https://github.com/flyingrobots/edict" + ); +} + +#[test] +fn tampered_contract_cddl_has_stable_failure_kind() { + let mut cddl = CONTRACT_CDDL.to_vec(); + cddl[0] ^= 1; + + let error = admit_provider_contract_pack_v1(&cddl, CONTRACT_MANIFEST) + .expect_err("tampered contract CDDL must fail admission"); + assert_eq!( + error.kind(), + ProviderContractPackErrorKind::SchemaBytesMismatch + ); + assert_eq!( + error.to_string(), + concat!( + "provider contract pack schema-bytes-mismatch: schema.bytesHex -> ", + "92697bc9a5262c68258be9ee451ee8c144aeb363b92142915b8224430b85cf74" + ) + ); +} + +#[test] +fn malformed_contract_manifest_has_stable_failure_kind() { + assert_manifest_failure(b"{", ProviderContractPackErrorKind::ManifestMalformed); +} + +#[test] +fn oversized_contract_manifest_fails_before_json_admission() { + let mut oversized = CONTRACT_MANIFEST.to_vec(); + oversized.extend(std::iter::repeat_n(b' ', CONTRACT_MANIFEST.len())); + + assert_manifest_failure( + &oversized, + ProviderContractPackErrorKind::ManifestSizeExceeded, + ); +} + +#[test] +fn unknown_contract_manifest_field_is_rejected() { + let manifest = mutated_manifest(|manifest| { + manifest + .as_object_mut() + .expect("manifest root is an object") + .insert("ambientRegistry".to_owned(), Value::Bool(true)); + }); + + assert_manifest_failure(&manifest, ProviderContractPackErrorKind::ManifestMalformed); +} + +#[test] +fn unsupported_contract_pack_api_has_stable_failure_kind() { + let manifest = mutated_manifest(|manifest| { + manifest["apiVersion"] = Value::String("edict.provider-contract-pack/v2".to_owned()); + }); + + assert_manifest_failure( + &manifest, + ProviderContractPackErrorKind::UnsupportedApiVersion, + ); +} + +#[test] +fn contract_pack_coordinate_tampering_has_stable_failure_kind() { + let manifest = mutated_manifest(|manifest| { + manifest["coordinate"] = Value::String("edict.provider-contract-pack.cddl@2".to_owned()); + }); + + assert_manifest_failure(&manifest, ProviderContractPackErrorKind::CoordinateMismatch); +} + +#[test] +fn contract_pack_license_tampering_has_stable_failure_kind() { + let manifest = mutated_manifest(|manifest| { + manifest["license"] = Value::String("substituted".to_owned()); + }); + + assert_manifest_failure(&manifest, ProviderContractPackErrorKind::LicenseMismatch); +} + +#[test] +fn noncanonical_contract_schema_hex_is_rejected() { + let manifest = mutated_manifest(|manifest| { + let bytes = manifest["schema"]["bytesHex"] + .as_str() + .expect("schema bytes are lowercase hex"); + manifest["schema"]["bytesHex"] = Value::String(bytes.replacen('a', "A", 1)); + }); + + assert_manifest_failure(&manifest, ProviderContractPackErrorKind::SchemaHexInvalid); +} + +#[test] +fn contract_schema_digest_tampering_has_stable_failure_kind() { + let manifest = mutated_manifest(|manifest| { + manifest["schema"]["rawSha256"] = Value::String("0".repeat(64)); + }); + + assert_manifest_failure( + &manifest, + ProviderContractPackErrorKind::SchemaDigestMismatch, + ); +} + +#[test] +fn contract_inventory_tampering_has_stable_failure_kind() { + let manifest = mutated_manifest(|manifest| { + manifest["contracts"][0]["contract"] = Value::String("substitute-contract".to_owned()); + }); + + assert_manifest_failure( + &manifest, + ProviderContractPackErrorKind::ContractInventoryMismatch, + ); +} + +#[test] +fn domain_inventory_tampering_has_stable_failure_kind() { + let manifest = mutated_manifest(|manifest| { + manifest["domains"] + .as_array_mut() + .expect("domain inventory is an array") + .swap(0, 1); + }); + + assert_manifest_failure( + &manifest, + ProviderContractPackErrorKind::DomainInventoryMismatch, + ); +} + +#[test] +fn resource_order_tampering_has_stable_failure_kind() { + let manifest = mutated_manifest(|manifest| { + manifest["resources"] + .as_array_mut() + .expect("resource inventory is an array") + .swap(0, 1); + }); + + assert_manifest_failure( + &manifest, + ProviderContractPackErrorKind::ResourceInventoryMismatch, + ); +} + +#[test] +fn resource_bytes_tampering_has_stable_failure_kind() { + let manifest = mutated_manifest(|manifest| { + let bytes = manifest["resources"][0]["canonicalBytesHex"] + .as_str() + .expect("resource bytes are lowercase hex"); + let mut tampered = bytes.to_owned(); + let replacement = if tampered.ends_with('0') { '1' } else { '0' }; + tampered.pop(); + tampered.push(replacement); + manifest["resources"][0]["canonicalBytesHex"] = Value::String(tampered); + }); + + assert_manifest_failure( + &manifest, + ProviderContractPackErrorKind::ResourceRawDigestMismatch, + ); +} + +#[test] +fn noncanonical_contract_resource_hex_is_rejected() { + let manifest = mutated_manifest(|manifest| { + let bytes = manifest["resources"][0]["canonicalBytesHex"] + .as_str() + .expect("resource bytes are lowercase hex"); + manifest["resources"][0]["canonicalBytesHex"] = Value::String(bytes.replacen('a', "A", 1)); + }); + + assert_manifest_failure(&manifest, ProviderContractPackErrorKind::ResourceHexInvalid); +} + +#[test] +fn resource_domain_digest_tampering_has_stable_failure_kind() { + let manifest = mutated_manifest(|manifest| { + manifest["resources"][0]["domainFramedDigest"] = + Value::String(format!("sha256:{}", "0".repeat(64))); + }); + + assert_manifest_failure( + &manifest, + ProviderContractPackErrorKind::ResourceDomainDigestMismatch, + ); +} + +#[test] +fn resource_provenance_tampering_has_stable_failure_kind() { + let manifest = mutated_manifest(|manifest| { + manifest["resources"][0]["provenance"]["sourcePath"] = + Value::String("fixtures/substituted-resource.cbor".to_owned()); + }); + + assert_manifest_failure( + &manifest, + ProviderContractPackErrorKind::ResourceProvenanceMismatch, + ); +} + +#[test] +fn semantically_equivalent_manifest_bytes_do_not_replace_the_publication() { + let reformatted = serde_json::to_vec(&manifest_value()).expect("checked manifest reformats"); + + assert_manifest_failure( + &reformatted, + ProviderContractPackErrorKind::ManifestDigestMismatch, + ); +} diff --git a/crates/echo-wesley-gen/tests/provider_generation_input.rs b/crates/echo-wesley-gen/tests/provider_generation_input.rs new file mode 100644 index 00000000..d32db651 --- /dev/null +++ b/crates/echo-wesley-gen/tests/provider_generation_input.rs @@ -0,0 +1,275 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +#![allow(clippy::expect_used, clippy::panic)] +//! Integration tests for the canonical Echo provider generation input. + +use echo_wesley_gen::provider_contract_pack::admit_provider_contract_pack_v1; +use echo_wesley_gen::provider_generation::{ + build_provider_generation_input_v1, ProviderGenerationErrorKind, +}; +use echo_wesley_gen::provider_semantics::ProviderSemanticSourceErrorKind; +use serde_json::{json, Value}; +use wesley_core::compute_generation_artifact_digest_v1; + +const SOURCE: &[u8] = + include_bytes!("../../../schemas/edict-provider/echo-provider-semantics-v1.json"); +const SETTINGS: &[u8] = + include_bytes!("../../../schemas/edict-provider/generation-settings-v1.json"); +const CONTRACT_CDDL: &[u8] = + include_bytes!("../../../schemas/edict-provider/contracts/v1/edict-provider-contracts.cddl"); +const CONTRACT_MANIFEST: &[u8] = + include_bytes!("../../../schemas/edict-provider/contracts/v1/manifest.json"); + +fn build( + source: &[u8], + settings: &[u8], +) -> echo_wesley_gen::provider_generation::ProviderGenerationInputV1 { + let pack = admit_provider_contract_pack_v1(CONTRACT_CDDL, CONTRACT_MANIFEST) + .expect("checked Edict provider contract pack is admitted"); + build_provider_generation_input_v1(source, &pack, settings) + .expect("checked provider generation inputs build") +} + +fn reordered_source() -> Vec { + let mut source = serde_json::from_slice::(SOURCE).expect("checked source is JSON"); + for pointer in [ + "/types/0/shape/fields", + "/types/1/shape/fields", + "/types/2/shape/fields", + "/effects/0/guardKinds", + "/effects/0/failures", + "/profiles/0/sourceNames", + "/profiles/0/allowedWriteClasses", + "/profiles/0/guardKinds", + "/operations/0/obstructionMappings", + "/lawpackProjection/acceptedCoreAbis", + "/lawpackProjection/dependencies", + "/lawpackProjection/targetAdapters", + "/lawpackProjection/targetAdapters/0/effects", + "/targetProfileProjection/acceptedCoreAbis", + "/targetProfileProjection/generatedArtifactProfileRoles", + "/targetProfileProjection/acceptedLawpackAdapterAbis", + ] { + source + .pointer_mut(pointer) + .and_then(Value::as_array_mut) + .expect("nested set-like field is an array") + .reverse(); + } + for key in [ + "authoritySources", + "types", + "writeClasses", + "obstructions", + "effects", + "profiles", + "budgets", + "capabilities", + "directAdapters", + "operations", + "artifactResources", + "generatedArtifacts", + "invocationInputs", + "invocationOutputs", + "schemaBindings", + ] { + source[key] + .as_array_mut() + .expect("set-like declaration family is an array") + .reverse(); + } + serde_json::to_vec(&source).expect("reordered source serializes") +} + +#[test] +fn checked_inputs_build_one_canonical_wesley_generation_input() { + let built = build(SOURCE, SETTINGS); + let input = built.wesley_input(); + + assert_eq!(input.shape_ir.version, "1.0.0"); + assert!(input.shape_ir.metadata.is_none()); + assert!(input.shape_ir.types.is_empty()); + assert!(input.operations.is_empty()); + assert!(input + .operations + .iter() + .all(|operation| operation.field_name != "a.b@1.t")); + assert!(input.law.is_none()); + assert_eq!( + input + .owner_declarations + .iter() + .map(|reference| reference.coordinate.as_str()) + .collect::>(), + vec![ + "echo.semantic-schema@1", + "edict.provider-contract-pack.cddl@1", + "edict.provider-contract-pack.manifest@1", + ] + ); + assert_eq!( + input.projection_roles, + vec![ + "authority-facts.echo-dpo", + "authority-facts.echo-lawpack", + "generated-artifact-profile.echo-dpo-registration", + "lawpack.echo-dpo", + "schema.echo-provider-artifacts", + "target-profile.echo-dpo", + ] + ); + assert_eq!( + built.canonical_bytes(), + input.canonical_bytes().expect("Wesley input encodes") + ); + assert_eq!(built.digest(), input.digest().expect("Wesley input hashes")); + assert_eq!(built.settings_bytes(), SETTINGS); + assert_eq!( + input.settings_digest, + compute_generation_artifact_digest_v1(SETTINGS) + ); + + let source_artifacts = built.source_artifacts(); + assert_eq!(source_artifacts.len(), 3); + assert_eq!(source_artifacts[0].coordinate, "echo.semantic-schema@1"); + assert_eq!(source_artifacts[0].bytes.as_slice(), SOURCE); + assert_eq!( + source_artifacts[1].coordinate, + "edict.provider-contract-pack.cddl@1" + ); + assert_eq!(source_artifacts[1].bytes.as_slice(), CONTRACT_CDDL); + assert_eq!( + source_artifacts[2].coordinate, + "edict.provider-contract-pack.manifest@1" + ); + assert_eq!(source_artifacts[2].bytes.as_slice(), CONTRACT_MANIFEST); + assert_eq!( + input.owner_declarations, + source_artifacts + .iter() + .map(wesley_core::GenerationArtifactContentV1::reference) + .collect::>() + ); +} + +#[test] +fn semantic_set_reordering_preserves_the_model_but_moves_exact_source_evidence() { + let baseline = build(SOURCE, SETTINGS); + let reordered = build(&reordered_source(), SETTINGS); + + assert_eq!(baseline.semantic_source(), reordered.semantic_source()); + assert_ne!(baseline.digest(), reordered.digest()); +} + +#[test] +fn exact_settings_bytes_move_generation_input_identity() { + let baseline = build(SOURCE, SETTINGS); + let compact = serde_json::to_vec( + &serde_json::from_slice::(SETTINGS).expect("checked settings are JSON"), + ) + .expect("settings compact"); + let reformatted = build(SOURCE, &compact); + + assert_ne!(baseline.digest(), reformatted.digest()); +} + +#[test] +fn invalid_generation_settings_have_stable_failure_kinds() { + let pack = admit_provider_contract_pack_v1(CONTRACT_CDDL, CONTRACT_MANIFEST) + .expect("checked Edict provider contract pack is admitted"); + let malformed = build_provider_generation_input_v1(SOURCE, &pack, b"{") + .expect_err("malformed settings fail"); + assert_eq!( + malformed.kind(), + ProviderGenerationErrorKind::SettingsMalformed + ); + + let unsupported_api = serde_json::to_vec(&json!({ + "apiVersion": "echo.edict-provider-generation-settings/v2", + "shapeSource": "none", + "canonicalArtifactEncoding": "edict.canonical-cbor/v1", + "contractPack": "edict.provider-contract-pack.cddl@1", + "generatorAbi": "wesley.extension-generator/v1" + })) + .expect("unsupported settings serialize"); + let unsupported = build_provider_generation_input_v1(SOURCE, &pack, &unsupported_api) + .expect_err("unsupported settings API fails"); + assert_eq!( + unsupported.kind(), + ProviderGenerationErrorKind::UnsupportedSettingsApiVersion + ); + + let wrong_shape = serde_json::to_vec(&json!({ + "apiVersion": "echo.edict-provider-generation-settings/v1", + "shapeSource": "graphql-sdl", + "canonicalArtifactEncoding": "edict.canonical-cbor/v1", + "contractPack": "edict.provider-contract-pack.cddl@1", + "generatorAbi": "wesley.extension-generator/v1" + })) + .expect("wrong settings serialize"); + let mismatch = build_provider_generation_input_v1(SOURCE, &pack, &wrong_shape) + .expect_err("undeclared GraphQL shape fails"); + assert_eq!( + mismatch.kind(), + ProviderGenerationErrorKind::SettingsContractMismatch + ); +} + +#[test] +fn graphql_authority_requires_explicit_shape_bytes() { + let mut source = serde_json::from_slice::(SOURCE).expect("checked source is JSON"); + source["authoritySources"] + .as_array_mut() + .expect("authority sources are an array") + .push(json!({ + "coordinate": "echo.provider-graphql@1", + "kind": "graphql", + "artifact": "schemas/edict-provider/provider.graphql" + })); + let id_type = source["types"] + .as_array_mut() + .expect("types are an array") + .iter_mut() + .find(|declaration| declaration["identity"]["coordinate"] == "a.b@1.Id") + .expect("id type exists"); + id_type["identity"]["authority"] = Value::String("echo.provider-graphql@1".to_owned()); + let source = serde_json::to_vec(&source).expect("GraphQL-owned source serializes"); + let pack = admit_provider_contract_pack_v1(CONTRACT_CDDL, CONTRACT_MANIFEST) + .expect("checked Edict provider contract pack is admitted"); + + let error = build_provider_generation_input_v1(&source, &pack, SETTINGS) + .expect_err("GraphQL authority without explicit SDL must fail"); + assert_eq!( + error.kind(), + ProviderGenerationErrorKind::GraphqlSourceMissing + ); + assert_eq!(error.subject(), "echo.provider-graphql@1"); + assert_eq!(error.reference(), "schemas/edict-provider/provider.graphql"); +} + +#[test] +fn semantic_source_failures_preserve_the_typed_source_kind() { + let mut source = serde_json::from_slice::(SOURCE).expect("checked source is JSON"); + source["operations"][0]["implementation"]["capability"] = + Value::String("echo.dpo@1.missing".to_owned()); + let source = serde_json::to_vec(&source).expect("invalid source serializes"); + let pack = admit_provider_contract_pack_v1(CONTRACT_CDDL, CONTRACT_MANIFEST) + .expect("checked Edict provider contract pack is admitted"); + + let error = build_provider_generation_input_v1(&source, &pack, SETTINGS) + .expect_err("invalid semantic source must fail"); + assert_eq!( + error.kind(), + ProviderGenerationErrorKind::SemanticSourceInvalid + ); + assert_eq!( + error.semantic_source_kind(), + Some(ProviderSemanticSourceErrorKind::UnknownCapability) + ); + assert_eq!(error.subject(), "a.b@1.t"); + assert_eq!(error.reference(), "echo.dpo@1.missing"); + assert_eq!( + error.to_string(), + "provider generation semantic-source-invalid: a.b@1.t -> echo.dpo@1.missing" + ); +} diff --git a/crates/echo-wesley-gen/tests/provider_generation_provenance.rs b/crates/echo-wesley-gen/tests/provider_generation_provenance.rs new file mode 100644 index 00000000..93a41030 --- /dev/null +++ b/crates/echo-wesley-gen/tests/provider_generation_provenance.rs @@ -0,0 +1,450 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +#![allow(clippy::expect_used, clippy::panic)] +//! Exact-material provenance for deterministic Echo provider generation. + +use echo_wesley_gen::provider_artifacts::{ + generate_provider_primary_artifacts_v1, ProviderPrimaryArtifactsV1, +}; +use echo_wesley_gen::provider_canonical::CanonicalValueV1; +use echo_wesley_gen::provider_contract_pack::{ + admit_provider_contract_pack_v1, AdmittedProviderContractPackV1, +}; +use echo_wesley_gen::provider_generation::{ + build_provider_generation_input_v1, ProviderGenerationInputV1, +}; +use echo_wesley_gen::provider_provenance::{ + generate_provider_generation_provenance_v1, ProviderGeneratorMaterialV1, + ProviderProvenanceErrorKind, +}; +use serde_json::{json, Value}; +use wesley_core::{ + compute_generation_artifact_digest_v1, GenerationContractErrorKind, + GenerationProvenanceManifestV1, +}; + +const SOURCE: &[u8] = + include_bytes!("../../../schemas/edict-provider/echo-provider-semantics-v1.json"); +const SETTINGS: &[u8] = + include_bytes!("../../../schemas/edict-provider/generation-settings-v1.json"); +const CONTRACT_CDDL: &[u8] = + include_bytes!("../../../schemas/edict-provider/contracts/v1/edict-provider-contracts.cddl"); +const CONTRACT_MANIFEST: &[u8] = + include_bytes!("../../../schemas/edict-provider/contracts/v1/manifest.json"); + +const GENERATOR_COORDINATE: &str = "echo-wesley-gen.provider-artifact-generator@1"; +const GENERATOR_VERSION: &str = "0.1.0"; +const GENERATOR_BYTES: &[u8] = b"echo-wesley-gen provider generator test material v1"; + +fn admitted_pack() -> AdmittedProviderContractPackV1 { + admit_provider_contract_pack_v1(CONTRACT_CDDL, CONTRACT_MANIFEST) + .expect("checked Edict provider contract pack is admitted") +} + +fn generate() -> (ProviderGenerationInputV1, ProviderPrimaryArtifactsV1) { + let pack = admitted_pack(); + let input = build_provider_generation_input_v1(SOURCE, &pack, SETTINGS) + .expect("checked provider generation input builds"); + let primary = generate_provider_primary_artifacts_v1(&input, &pack) + .expect("checked primary provider artifacts generate"); + (input, primary) +} + +fn generator() -> ProviderGeneratorMaterialV1 { + ProviderGeneratorMaterialV1::new(GENERATOR_COORDINATE, GENERATOR_VERSION, GENERATOR_BYTES) + .expect("explicit test generator material is valid") +} + +fn map_value<'a>(value: &'a CanonicalValueV1, field: &str) -> Option<&'a CanonicalValueV1> { + let CanonicalValueV1::Map(entries) = value else { + return None; + }; + entries.iter().find_map(|(key, value)| match key { + CanonicalValueV1::Text(key) if key == field => Some(value), + _ => None, + }) +} + +fn typed_digest(value: &CanonicalValueV1) -> Option { + let CanonicalValueV1::Array(parts) = value else { + return None; + }; + match parts.as_slice() { + [CanonicalValueV1::Text(algorithm), CanonicalValueV1::Bytes(bytes)] => { + Some(format!("{algorithm}:{}", hex::encode(bytes))) + } + _ => None, + } +} + +fn contains_resource_reference(value: &CanonicalValueV1, coordinate: &str, digest: &str) -> bool { + let is_reference = matches!( + map_value(value, "id"), + Some(CanonicalValueV1::Text(id)) if id == coordinate + ) && map_value(value, "digest").and_then(typed_digest).as_deref() + == Some(digest); + if is_reference { + return true; + } + match value { + CanonicalValueV1::Array(values) => values + .iter() + .any(|value| contains_resource_reference(value, coordinate, digest)), + CanonicalValueV1::Map(entries) => entries + .iter() + .any(|(_, value)| contains_resource_reference(value, coordinate, digest)), + _ => false, + } +} + +#[test] +fn exact_materials_produce_one_verified_canonical_provenance_manifest() { + let (input, primary) = generate(); + let generator = generator(); + let first = generate_provider_generation_provenance_v1(&input, &primary, &generator) + .expect("provenance generates from exact materials"); + let second = generate_provider_generation_provenance_v1(&input, &primary, &generator) + .expect("repeated provenance generates from exact materials"); + + assert_eq!(first, second); + assert_eq!(first.role(), "provenance.provider-generation"); + assert_eq!( + first.coordinate(), + "echo.edict-provider-generation-provenance@1" + ); + assert_eq!( + first.schema_contract(), + "wesley:GenerationProvenanceManifestV1" + ); + + let manifest = first.manifest(); + assert_eq!( + manifest.api_version, + "wesley.generation-provenance-manifest/v1" + ); + assert_eq!(manifest.generation_input_digest, input.digest()); + assert_eq!( + manifest.settings_digest, + compute_generation_artifact_digest_v1(SETTINGS) + ); + assert_eq!(manifest.generator.coordinate, GENERATOR_COORDINATE); + assert_eq!(manifest.generator.version, GENERATOR_VERSION); + assert_eq!( + manifest.generator.digest, + compute_generation_artifact_digest_v1(GENERATOR_BYTES) + ); + assert_eq!( + manifest.contract_versions.input_schema, + "wesley.extension-generation-input/v1" + ); + assert_eq!( + manifest.contract_versions.provenance_schema, + "wesley.generation-provenance-manifest/v1" + ); + assert_eq!( + manifest.contract_versions.generator_abi, + "wesley.extension-generator/v1" + ); + + assert_eq!( + manifest.source_artifacts, + input.wesley_input().owner_declarations + ); + assert_eq!( + manifest + .source_artifacts + .iter() + .map(|artifact| artifact.coordinate.as_str()) + .collect::>(), + vec![ + "echo.semantic-schema@1", + "edict.provider-contract-pack.cddl@1", + "edict.provider-contract-pack.manifest@1", + ] + ); + + let mut expected_emitted = primary + .artifacts() + .iter() + .map(|artifact| artifact.content_reference().clone()) + .chain(std::iter::once( + primary.schema().content_reference().clone(), + )) + .collect::>(); + expected_emitted.sort_by(|left, right| left.coordinate.cmp(&right.coordinate)); + assert_eq!(manifest.emitted_artifacts, expected_emitted); + assert_eq!(manifest.emitted_artifacts.len(), 6); + + for resource in primary.resources() { + assert!( + manifest + .emitted_artifacts + .iter() + .all(|artifact| artifact.coordinate != resource.coordinate()), + "generated resources are transitively bound, not primary emits" + ); + assert!( + primary.artifacts().iter().any(|artifact| { + contains_resource_reference( + artifact.canonical_value(), + resource.coordinate(), + resource.domain_framed_digest(), + ) + }), + "every generated resource is digest-bound by a primary artifact" + ); + } + assert!(manifest.emitted_artifacts.iter().all(|artifact| { + artifact.coordinate != "echo.edict-provider-generation-provenance@1" + && artifact.coordinate != "echo.edict-provider-generation-review@1" + })); + for artifact in primary.artifacts() { + let emitted = manifest + .emitted_artifacts + .iter() + .find(|emitted| emitted.coordinate == artifact.coordinate()) + .expect("each primary canonical artifact is emitted"); + assert_eq!(emitted, artifact.content_reference()); + assert_ne!(emitted.digest, artifact.domain_framed_digest()); + } + + assert_eq!(first.verification().generation_input_digest, input.digest()); + assert_eq!(first.verification().verified_source_count, 3); + assert_eq!(first.verification().verified_output_count, 6); + assert_eq!( + first.canonical_bytes(), + first + .manifest() + .canonical_bytes() + .expect("manifest canonicalizes") + ); + assert_eq!(first.content_reference().coordinate, first.coordinate()); + assert_eq!( + first.content_reference().digest, + compute_generation_artifact_digest_v1(first.canonical_bytes()) + ); + + let decoded: GenerationProvenanceManifestV1 = + serde_json::from_slice(first.canonical_bytes()).expect("canonical provenance decodes"); + assert_eq!(&decoded, first.manifest()); + assert_eq!( + decoded + .canonical_bytes() + .expect("decoded provenance canonicalizes"), + first.canonical_bytes() + ); +} + +#[test] +fn tampered_generator_material_preserves_the_typed_wesley_failure() { + let (input, primary) = generate(); + let provenance = generate_provider_generation_provenance_v1(&input, &primary, &generator()) + .expect("provenance generates from exact materials"); + let tampered = ProviderGeneratorMaterialV1::new( + GENERATOR_COORDINATE, + GENERATOR_VERSION, + b"tampered generator material", + ) + .expect("tampered generator material remains structurally valid"); + + let error = provenance + .verify_exact_materials(&input, &primary, &tampered) + .expect_err("changed generator bytes must not verify"); + assert_eq!( + error.kind(), + ProviderProvenanceErrorKind::WesleyContractRejected + ); + assert_eq!( + error.wesley_contract_kind(), + Some(GenerationContractErrorKind::ArtifactDigestMismatch) + ); + assert_eq!(error.subject(), GENERATOR_COORDINATE); + assert_eq!( + error.reference(), + GenerationContractErrorKind::ArtifactDigestMismatch.as_str() + ); +} + +#[test] +fn provenance_rejects_primary_outputs_from_another_generation_input() { + let (input, _) = generate(); + let pack = admitted_pack(); + let mut changed_source = + serde_json::from_slice::(SOURCE).expect("checked source is JSON"); + changed_source["budgets"][0]["maxSteps"] = json!(9); + let changed_source = serde_json::to_vec(&changed_source).expect("changed source serializes"); + let changed_input = build_provider_generation_input_v1(&changed_source, &pack, SETTINGS) + .expect("changed provider generation input builds"); + let changed_primary = generate_provider_primary_artifacts_v1(&changed_input, &pack) + .expect("changed primary provider artifacts generate"); + + let error = generate_provider_generation_provenance_v1(&input, &changed_primary, &generator()) + .expect_err("primary outputs from a different input must not be attributed"); + assert_eq!( + error.kind(), + ProviderProvenanceErrorKind::PrimaryOutputClosureMismatch + ); + assert_eq!(error.subject(), "generationInputDigest"); + assert_eq!(error.reference(), input.digest()); +} + +#[test] +fn generator_identity_mismatches_name_the_exact_stable_field() { + let (input, primary) = generate(); + let provenance = generate_provider_generation_provenance_v1(&input, &primary, &generator()) + .expect("provenance generates from exact materials"); + let changed_coordinate = ProviderGeneratorMaterialV1::new( + "echo-wesley-gen.other-generator@1", + GENERATOR_VERSION, + GENERATOR_BYTES, + ) + .expect("alternate generator coordinate is structurally valid"); + + let coordinate_error = provenance + .verify_exact_materials(&input, &primary, &changed_coordinate) + .expect_err("a different generator coordinate must not verify"); + assert_eq!( + coordinate_error.kind(), + ProviderProvenanceErrorKind::GeneratorIdentityMismatch + ); + assert_eq!(coordinate_error.subject(), "generator.coordinate"); + assert_eq!(coordinate_error.reference(), GENERATOR_COORDINATE); + assert_eq!(coordinate_error.wesley_contract_kind(), None); + + let changed_version = + ProviderGeneratorMaterialV1::new(GENERATOR_COORDINATE, "0.2.0", GENERATOR_BYTES) + .expect("alternate generator version is structurally valid"); + let version_error = provenance + .verify_exact_materials(&input, &primary, &changed_version) + .expect_err("a different generator version must not verify"); + assert_eq!( + version_error.kind(), + ProviderProvenanceErrorKind::GeneratorIdentityMismatch + ); + assert_eq!(version_error.subject(), "generator.version"); + assert_eq!(version_error.reference(), GENERATOR_VERSION); + assert_eq!(version_error.wesley_contract_kind(), None); +} + +#[test] +fn generator_coordinate_cannot_alias_provider_closure_coordinates() { + let (input, primary) = generate(); + let source = input.semantic_source().source(); + let resource = primary + .resources() + .first() + .expect("checked primary closure has generated resources"); + let conflicts = [ + ( + "echo.edict-provider-generation-provenance@1", + "provenance.provider-generation", + ), + (resource.coordinate(), resource.role()), + ( + source.package_manifest.coordinate.as_str(), + source.package_manifest.role.as_str(), + ), + ( + source.package_manifest.provider_coordinate.as_str(), + "packageManifest.providerCoordinate", + ), + ]; + + for (coordinate, role) in conflicts { + let conflicting = + ProviderGeneratorMaterialV1::new(coordinate, GENERATOR_VERSION, GENERATOR_BYTES) + .expect("provider coordinate is structurally valid as a Wesley coordinate"); + let error = generate_provider_generation_provenance_v1(&input, &primary, &conflicting) + .expect_err("generator coordinate must be unique in the provider closure"); + assert_eq!( + error.kind(), + ProviderProvenanceErrorKind::GeneratorCoordinateConflict + ); + assert_eq!(error.subject(), "generator.coordinate"); + assert_eq!(error.reference(), role); + assert_eq!(error.wesley_contract_kind(), None); + } +} + +#[test] +fn generator_coordinate_cannot_alias_source_artifact_coordinates() { + let (input, primary) = generate(); + assert_eq!( + input + .source_artifacts() + .iter() + .map(|artifact| artifact.coordinate.as_str()) + .collect::>(), + [ + "echo.semantic-schema@1", + "edict.provider-contract-pack.cddl@1", + "edict.provider-contract-pack.manifest@1", + ] + ); + + for source_artifact in input.source_artifacts() { + let conflicting = ProviderGeneratorMaterialV1::new( + &source_artifact.coordinate, + GENERATOR_VERSION, + &source_artifact.bytes, + ) + .expect("source artifact coordinate and bytes form valid generator material"); + let error = generate_provider_generation_provenance_v1(&input, &primary, &conflicting) + .expect_err("generator coordinate must not alias an exact source artifact"); + assert_eq!( + error.kind(), + ProviderProvenanceErrorKind::GeneratorCoordinateConflict + ); + assert_eq!(error.subject(), "generator.coordinate"); + assert_eq!(error.reference(), "sourceArtifacts"); + assert_eq!(error.wesley_contract_kind(), None); + } +} + +#[test] +fn exact_source_reordering_moves_provenance_but_not_primary_emitted_bytes() { + let pack = admitted_pack(); + let baseline_input = build_provider_generation_input_v1(SOURCE, &pack, SETTINGS) + .expect("baseline provider generation input builds"); + let baseline_primary = generate_provider_primary_artifacts_v1(&baseline_input, &pack) + .expect("baseline primary provider artifacts generate"); + let baseline = generate_provider_generation_provenance_v1( + &baseline_input, + &baseline_primary, + &generator(), + ) + .expect("baseline provenance generates"); + + let mut reordered_source = + serde_json::from_slice::(SOURCE).expect("checked source is JSON"); + reordered_source["types"] + .as_array_mut() + .expect("types are an array") + .reverse(); + let reordered_source = + serde_json::to_vec(&reordered_source).expect("reordered source serializes"); + let reordered_input = build_provider_generation_input_v1(&reordered_source, &pack, SETTINGS) + .expect("reordered provider generation input builds"); + let reordered_primary = generate_provider_primary_artifacts_v1(&reordered_input, &pack) + .expect("reordered primary provider artifacts generate"); + let reordered = generate_provider_generation_provenance_v1( + &reordered_input, + &reordered_primary, + &generator(), + ) + .expect("reordered provenance generates"); + + assert_eq!( + baseline.manifest().emitted_artifacts, + reordered.manifest().emitted_artifacts + ); + assert_ne!( + baseline.manifest().generation_input_digest, + reordered.manifest().generation_input_digest + ); + assert_ne!(baseline.canonical_bytes(), reordered.canonical_bytes()); + assert_ne!( + baseline.content_reference().digest, + reordered.content_reference().digest + ); +} diff --git a/crates/echo-wesley-gen/tests/provider_generation_review.rs b/crates/echo-wesley-gen/tests/provider_generation_review.rs new file mode 100644 index 00000000..108f7d94 --- /dev/null +++ b/crates/echo-wesley-gen/tests/provider_generation_review.rs @@ -0,0 +1,180 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +#![allow(clippy::expect_used, clippy::panic)] +//! Deterministic non-authoritative review of Echo provider generation. + +use echo_wesley_gen::provider_artifacts::{ + generate_provider_primary_artifacts_v1, ProviderPrimaryArtifactsV1, +}; +use echo_wesley_gen::provider_contract_pack::{ + admit_provider_contract_pack_v1, AdmittedProviderContractPackV1, +}; +use echo_wesley_gen::provider_generation::{ + build_provider_generation_input_v1, ProviderGenerationInputV1, +}; +use echo_wesley_gen::provider_provenance::{ + generate_provider_generation_provenance_v1, ProviderGenerationProvenanceV1, + ProviderGeneratorMaterialV1, +}; +use echo_wesley_gen::provider_review::{ + generate_provider_generation_review_v1, ProviderReviewErrorKind, +}; +use serde_json::{json, Value}; +use wesley_core::{ + compute_generation_artifact_digest_v1, GenerationContractErrorKind, GenerationReviewV1, +}; + +const SOURCE: &[u8] = + include_bytes!("../../../schemas/edict-provider/echo-provider-semantics-v1.json"); +const SETTINGS: &[u8] = + include_bytes!("../../../schemas/edict-provider/generation-settings-v1.json"); +const CONTRACT_CDDL: &[u8] = + include_bytes!("../../../schemas/edict-provider/contracts/v1/edict-provider-contracts.cddl"); +const CONTRACT_MANIFEST: &[u8] = + include_bytes!("../../../schemas/edict-provider/contracts/v1/manifest.json"); + +const GENERATOR_COORDINATE: &str = "echo-wesley-gen.provider-artifact-generator@1"; +const GENERATOR_VERSION: &str = "0.1.0"; +const GENERATOR_BYTES: &[u8] = b"echo-wesley-gen provider generator test material v1"; + +fn admitted_pack() -> AdmittedProviderContractPackV1 { + admit_provider_contract_pack_v1(CONTRACT_CDDL, CONTRACT_MANIFEST) + .expect("checked Edict provider contract pack is admitted") +} + +fn generate() -> ( + ProviderGenerationInputV1, + ProviderPrimaryArtifactsV1, + ProviderGenerationProvenanceV1, +) { + let pack = admitted_pack(); + let input = build_provider_generation_input_v1(SOURCE, &pack, SETTINGS) + .expect("checked provider generation input builds"); + let primary = generate_provider_primary_artifacts_v1(&input, &pack) + .expect("checked primary provider artifacts generate"); + let generator = + ProviderGeneratorMaterialV1::new(GENERATOR_COORDINATE, GENERATOR_VERSION, GENERATOR_BYTES) + .expect("explicit test generator material is valid"); + let provenance = generate_provider_generation_provenance_v1(&input, &primary, &generator) + .expect("checked provider provenance generates"); + (input, primary, provenance) +} + +#[test] +fn verified_provenance_produces_one_canonical_non_authoritative_review() { + let (input, _primary, provenance) = generate(); + let first = generate_provider_generation_review_v1(&input, &provenance) + .expect("generation review derives from verified provenance"); + let second = generate_provider_generation_review_v1(&input, &provenance) + .expect("repeated generation review derives from verified provenance"); + + assert_eq!(first, second); + assert_eq!(first.role(), "review.provider-generation"); + assert_eq!( + first.coordinate(), + "echo.edict-provider-generation-review@1" + ); + assert_eq!(first.schema_contract(), "wesley:GenerationReviewV1"); + + let review = first.review(); + assert_eq!(review.api_version, "wesley.generation-review/v1"); + assert!(!review.authoritative()); + assert_eq!(review.generation_input_digest, input.digest()); + assert_eq!( + review.provenance_manifest_digest, + provenance + .manifest() + .digest() + .expect("checked provenance manifest digests") + ); + assert_ne!( + review.provenance_manifest_digest, + provenance.content_reference().digest, + "review binds Wesley's domain-framed manifest identity, not its raw JSON digest" + ); + assert_eq!(review.generator, provenance.manifest().generator); + assert_eq!( + review.projection_roles, + input.wesley_input().projection_roles + ); + assert_eq!( + review.source_artifacts, + provenance.manifest().source_artifacts + ); + assert_eq!( + review.emitted_artifacts, + provenance.manifest().emitted_artifacts + ); + assert_eq!(review.source_artifacts.len(), 3); + assert_eq!(review.emitted_artifacts.len(), 6); + assert!(review.emitted_artifacts.iter().all(|artifact| { + artifact.coordinate != first.coordinate() && artifact.coordinate != provenance.coordinate() + })); + + assert_eq!( + first.canonical_bytes(), + review.canonical_bytes().expect("review canonicalizes") + ); + assert_eq!(first.content_reference().coordinate, first.coordinate()); + assert_eq!( + first.content_reference().digest, + compute_generation_artifact_digest_v1(first.canonical_bytes()) + ); + let decoded: GenerationReviewV1 = + serde_json::from_slice(first.canonical_bytes()).expect("canonical review decodes"); + assert_eq!(&decoded, review); + assert_eq!( + decoded + .canonical_bytes() + .expect("decoded review canonicalizes"), + first.canonical_bytes() + ); +} + +#[test] +fn review_rejects_an_authoritative_claim_during_deserialization() { + let (input, _primary, provenance) = generate(); + let review = generate_provider_generation_review_v1(&input, &provenance) + .expect("generation review derives from verified provenance"); + let mut claimed = serde_json::from_slice::(review.canonical_bytes()) + .expect("canonical review is JSON"); + claimed["authoritative"] = json!(true); + let claimed = serde_json::to_vec(&claimed).expect("tampered review serializes"); + + let error = serde_json::from_slice::(&claimed) + .expect_err("a review cannot claim authority"); + assert!( + error + .to_string() + .contains(GenerationContractErrorKind::AuthoritativeReviewRejected.as_str()), + "Wesley exposes a stable rejection code" + ); +} + +#[test] +fn review_preserves_typed_wesley_input_mismatch_failures() { + let (_input, _primary, provenance) = generate(); + let pack = admitted_pack(); + let mut changed_source = + serde_json::from_slice::(SOURCE).expect("checked source is JSON"); + changed_source["budgets"][0]["maxSteps"] = json!(9); + let changed_source = serde_json::to_vec(&changed_source).expect("changed source serializes"); + let changed_input = build_provider_generation_input_v1(&changed_source, &pack, SETTINGS) + .expect("changed provider generation input builds"); + + let error = generate_provider_generation_review_v1(&changed_input, &provenance) + .expect_err("review input must match the provenance input"); + assert_eq!( + error.kind(), + ProviderReviewErrorKind::WesleyContractRejected + ); + assert_eq!( + error.wesley_contract_kind(), + Some(GenerationContractErrorKind::GenerationInputDigestMismatch) + ); + assert_eq!(error.subject(), "generationInputDigest"); + assert_eq!( + error.reference(), + GenerationContractErrorKind::GenerationInputDigestMismatch.as_str() + ); +} diff --git a/crates/echo-wesley-gen/tests/provider_primary_artifacts.rs b/crates/echo-wesley-gen/tests/provider_primary_artifacts.rs new file mode 100644 index 00000000..9cc0a105 --- /dev/null +++ b/crates/echo-wesley-gen/tests/provider_primary_artifacts.rs @@ -0,0 +1,1049 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +#![allow(clippy::expect_used, clippy::panic)] +//! Deterministic primary artifact generation for the Echo Edict provider. + +use echo_wesley_gen::provider_artifacts::{ + generate_provider_primary_artifacts_v1, ProviderArtifactGenerationErrorKind, + ProviderPrimaryArtifactsV1, SchemaValidatedCanonicalProviderOutputV1, +}; +use echo_wesley_gen::provider_canonical::{ + digest_canonical_value_v1, encode_canonical_cbor_v1, CanonicalValueErrorKind, CanonicalValueV1, +}; +use echo_wesley_gen::provider_contract_pack::{ + admit_provider_contract_pack_v1, AdmittedProviderContractPackV1, +}; +use echo_wesley_gen::provider_generation::{ + build_provider_generation_input_v1, ProviderGenerationInputV1, +}; +use serde_json::{json, Value}; +use wesley_core::{compute_generation_artifact_digest_v1, GenerationContractErrorKind}; + +const SOURCE: &[u8] = + include_bytes!("../../../schemas/edict-provider/echo-provider-semantics-v1.json"); +const SETTINGS: &[u8] = + include_bytes!("../../../schemas/edict-provider/generation-settings-v1.json"); +const CONTRACT_CDDL: &[u8] = + include_bytes!("../../../schemas/edict-provider/contracts/v1/edict-provider-contracts.cddl"); +const CONTRACT_MANIFEST: &[u8] = + include_bytes!("../../../schemas/edict-provider/contracts/v1/manifest.json"); + +const PRIMARY_ROLES: [&str; 6] = [ + "authority-facts.echo-dpo", + "authority-facts.echo-lawpack", + "generated-artifact-profile.echo-dpo-registration", + "lawpack.echo-dpo", + "schema.echo-provider-artifacts", + "target-profile.echo-dpo", +]; + +const GENERATED_RESOURCE_ROLES: [&str; 14] = [ + "resource.conformance-corpus", + "resource.lawpack-compatibility", + "resource.lawpack-exports", + "resource.lawpack-target-adapter", + "resource.lawpack-verifier", + "resource.target-bundle-profile", + "resource.target-cost-algebra", + "resource.target-footprint-algebra", + "resource.target-intrinsics", + "resource.target-ir", + "resource.target-lowerer-contract", + "resource.target-obstruction-taxonomy", + "resource.target-operation-profiles", + "resource.target-verifier-contract", +]; + +fn admitted_pack() -> AdmittedProviderContractPackV1 { + admit_provider_contract_pack_v1(CONTRACT_CDDL, CONTRACT_MANIFEST) + .expect("checked Edict provider contract pack is admitted") +} + +fn build_input(source: &[u8], pack: &AdmittedProviderContractPackV1) -> ProviderGenerationInputV1 { + build_provider_generation_input_v1(source, pack, SETTINGS) + .expect("checked provider generation input builds") +} + +fn generate( + source: &[u8], + pack: &AdmittedProviderContractPackV1, +) -> (ProviderGenerationInputV1, ProviderPrimaryArtifactsV1) { + let input = build_input(source, pack); + let artifacts = generate_provider_primary_artifacts_v1(&input, pack) + .expect("checked primary provider artifacts generate"); + (input, artifacts) +} + +fn reordered_source() -> Vec { + let mut source = serde_json::from_slice::(SOURCE).expect("checked source is JSON"); + for pointer in [ + "/types/0/shape/fields", + "/types/1/shape/fields", + "/types/2/shape/fields", + "/effects/0/guardKinds", + "/effects/0/failures", + "/profiles/0/sourceNames", + "/profiles/0/allowedWriteClasses", + "/profiles/0/guardKinds", + "/operations/0/obstructionMappings", + "/lawpackProjection/acceptedCoreAbis", + "/lawpackProjection/dependencies", + "/lawpackProjection/targetAdapters", + "/lawpackProjection/targetAdapters/0/effects", + "/targetProfileProjection/acceptedCoreAbis", + "/targetProfileProjection/generatedArtifactProfileRoles", + "/targetProfileProjection/acceptedLawpackAdapterAbis", + ] { + source + .pointer_mut(pointer) + .and_then(Value::as_array_mut) + .expect("nested set-like field is an array") + .reverse(); + } + for key in [ + "authoritySources", + "types", + "writeClasses", + "obstructions", + "effects", + "profiles", + "budgets", + "capabilities", + "directAdapters", + "operations", + "artifactResources", + "generatedArtifacts", + "invocationInputs", + "invocationOutputs", + "schemaBindings", + ] { + source[key] + .as_array_mut() + .expect("set-like declaration family is an array") + .reverse(); + } + serde_json::to_vec(&source).expect("reordered source serializes") +} + +fn map_field<'a>(value: &'a CanonicalValueV1, field: &str) -> &'a CanonicalValueV1 { + let CanonicalValueV1::Map(entries) = value else { + panic!("{field} parent is not a map"); + }; + entries + .iter() + .find_map(|(key, value)| { + (key == &CanonicalValueV1::Text(field.to_owned())).then_some(value) + }) + .unwrap_or_else(|| panic!("missing map field {field}")) +} + +fn map_field_mut<'a>(value: &'a mut CanonicalValueV1, field: &str) -> &'a mut CanonicalValueV1 { + let CanonicalValueV1::Map(entries) = value else { + panic!("{field} parent is not a map"); + }; + entries + .iter_mut() + .find_map(|(key, value)| { + (key == &CanonicalValueV1::Text(field.to_owned())).then_some(value) + }) + .unwrap_or_else(|| panic!("missing map field {field}")) +} + +fn map_keys(value: &CanonicalValueV1) -> Vec<&str> { + let CanonicalValueV1::Map(entries) = value else { + panic!("value is not a map"); + }; + entries + .iter() + .map(|(key, _)| { + let CanonicalValueV1::Text(key) = key else { + panic!("map key is not text"); + }; + key.as_str() + }) + .collect() +} + +fn text_value(value: &CanonicalValueV1) -> &str { + let CanonicalValueV1::Text(value) = value else { + panic!("value is not text"); + }; + value +} + +fn integer_value(value: &CanonicalValueV1) -> i128 { + let CanonicalValueV1::Integer(value) = value else { + panic!("value is not an integer"); + }; + *value +} + +fn typed_digest(value: &CanonicalValueV1) -> String { + let CanonicalValueV1::Array(parts) = value else { + panic!("digest is not an array"); + }; + assert_eq!(parts.len(), 2); + assert_eq!(text_value(&parts[0]), "sha256"); + let CanonicalValueV1::Bytes(bytes) = &parts[1] else { + panic!("digest payload is not bytes"); + }; + assert_eq!(bytes.len(), 32); + format!("sha256:{}", hex::encode(bytes)) +} + +fn resource_ref_digest(value: &CanonicalValueV1) -> String { + typed_digest(map_field(value, "digest")) +} + +fn changed_artifact_roles<'a>( + baseline: &'a ProviderPrimaryArtifactsV1, + changed: &'a ProviderPrimaryArtifactsV1, +) -> Vec<&'a str> { + baseline + .artifacts() + .iter() + .filter_map(|artifact| { + let changed = changed + .artifact(artifact.role()) + .expect("changed closure retains every primary role"); + (artifact.canonical_bytes() != changed.canonical_bytes()).then_some(artifact.role()) + }) + .collect() +} + +fn output<'a>( + artifacts: &'a ProviderPrimaryArtifactsV1, + role: &str, +) -> &'a SchemaValidatedCanonicalProviderOutputV1 { + artifacts + .artifact(role) + .or_else(|| artifacts.resource(role)) + .unwrap_or_else(|| panic!("missing provider output role {role}")) +} + +#[test] +fn primary_generation_is_byte_identical_digest_closed_and_root_validated() { + let pack = admitted_pack(); + let (_, first) = generate(SOURCE, &pack); + let (_, second) = generate(SOURCE, &pack); + + assert_eq!(first, second); + assert_eq!( + first + .projection_roles() + .iter() + .map(String::as_str) + .collect::>(), + PRIMARY_ROLES + ); + assert_eq!(first.artifacts().len(), 5); + assert_eq!(first.resources().len(), 14); + assert_eq!( + first + .resources() + .iter() + .map(SchemaValidatedCanonicalProviderOutputV1::role) + .collect::>(), + GENERATED_RESOURCE_ROLES + ); + assert_eq!(first.schema().role(), "schema.echo-provider-artifacts"); + assert_eq!( + first.schema().coordinate(), + "echo.provider-artifacts.cddl@1" + ); + assert!(first.schema().bytes().starts_with(CONTRACT_CDDL)); + assert!(std::str::from_utf8(first.schema().bytes()) + .expect("generated provider CDDL is UTF-8") + .contains("generated-artifact-profile =")); + assert_eq!( + first.schema().content_reference().digest, + compute_generation_artifact_digest_v1(first.schema().bytes()) + ); + assert_eq!( + first.schema().content_reference().coordinate, + first.schema().coordinate() + ); + + for output in first.artifacts().iter().chain(first.resources()) { + let admitted = first + .schema() + .validate_output(output) + .expect("output satisfies its owning root"); + assert_eq!( + encode_canonical_cbor_v1(&admitted).expect("admitted output re-encodes"), + output.canonical_bytes() + ); + assert_eq!( + digest_canonical_value_v1(output.digest_domain(), &admitted) + .expect("admitted output domain-digests"), + output.domain_framed_digest() + ); + assert_eq!( + output.content_reference().digest, + compute_generation_artifact_digest_v1(output.canonical_bytes()) + ); + assert_eq!(output.content_reference().coordinate, output.coordinate()); + assert_ne!( + output.domain_framed_digest(), + output.content_reference().digest + ); + } + + for (role, contract) in [ + ("lawpack.echo-dpo", "lawpack-manifest"), + ("target-profile.echo-dpo", "target-profile-manifest"), + ("authority-facts.echo-dpo", "authority-facts"), + ("authority-facts.echo-lawpack", "authority-facts"), + ] { + pack.validate_contract_bytes( + contract, + first + .artifact(role) + .expect("Edict-owned primary role exists") + .canonical_bytes(), + ) + .expect("primary output satisfies the independently admitted Edict root"); + } + for (role, contract) in [ + ("resource.lawpack-exports", "lawpack-exports"), + ("resource.target-intrinsics", "target-profile-intrinsics"), + ( + "resource.target-operation-profiles", + "target-profile-operation-profiles", + ), + ] { + pack.validate_contract_bytes( + contract, + first + .resource(role) + .expect("Edict-owned generated resource exists") + .canonical_bytes(), + ) + .expect("generated resource satisfies the independently admitted Edict root"); + } +} + +#[test] +fn computed_digest_edges_and_authority_partition_are_exact() { + let pack = admitted_pack(); + let (_, generated) = generate(SOURCE, &pack); + let registration = output( + &generated, + "generated-artifact-profile.echo-dpo-registration", + ); + let target = output(&generated, "target-profile.echo-dpo"); + let lawpack = output(&generated, "lawpack.echo-dpo"); + + let generated_profiles = map_field(target.canonical_value(), "generatedArtifactProfiles"); + let CanonicalValueV1::Array(generated_profiles) = generated_profiles else { + panic!("generated artifact profiles is not an array"); + }; + assert_eq!(generated_profiles.len(), 1); + assert_eq!( + resource_ref_digest(&generated_profiles[0]), + registration.domain_framed_digest() + ); + + let CanonicalValueV1::Array(adapters) = map_field(lawpack.canonical_value(), "targetAdapters") + else { + panic!("target adapters is not an array"); + }; + assert_eq!(adapters.len(), 1); + let accepted_target = map_field(&adapters[0], "acceptedTargetProfile"); + assert_eq!(text_value(map_field(accepted_target, "id")), "echo.dpo@1"); + assert_eq!( + resource_ref_digest(accepted_target), + target.domain_framed_digest() + ); + + for (field, coordinate) in [ + ("sandbox", "edict.wasm-component/v1"), + ("fuelModel", "edict.fuel/v1"), + ("canonicalEncodingRules", "edict.canonical-cbor/v1"), + ("diagnosticAbi", "edict.diagnostics/v1"), + ("deterministicExecution", "edict.determinism/v1"), + ] { + let reference = map_field(target.canonical_value(), field); + let resource = pack + .resource(coordinate) + .expect("external contract-pack resource is admitted"); + assert_eq!(text_value(map_field(reference, "id")), coordinate); + assert_eq!( + typed_digest(map_field(reference, "digest")), + resource.domain_framed_digest() + ); + assert_ne!( + typed_digest(map_field(reference, "digest")), + format!("sha256:{}", resource.raw_sha256()) + ); + } + + for (role, source_kind, source_artifact) in [ + ( + "authority-facts.echo-dpo", + "targetProfile", + "target-profile.echo-dpo", + ), + ( + "authority-facts.echo-lawpack", + "lawpack", + "lawpack.echo-dpo", + ), + ] { + let facts = output(&generated, role); + let source = map_field(facts.canonical_value(), "source"); + assert_eq!(text_value(map_field(source, "kind")), source_kind); + assert_eq!( + typed_digest(map_field(source, "digest")), + output(&generated, source_artifact).domain_framed_digest() + ); + } + + let target_facts = output(&generated, "authority-facts.echo-dpo").canonical_value(); + assert_eq!( + map_keys(map_field(target_facts, "operationProfiles")), + ["p.effectful"] + ); + assert_eq!( + map_keys(map_field(target_facts, "effectWriteClasses")), + ["target.replace"] + ); + assert!(map_keys(map_field(target_facts, "budgets")).is_empty()); + + let lawpack_facts = output(&generated, "authority-facts.echo-lawpack").canonical_value(); + assert!(map_keys(map_field(lawpack_facts, "operationProfiles")).is_empty()); + assert!(map_keys(map_field(lawpack_facts, "effectWriteClasses")).is_empty()); + assert_eq!(map_keys(map_field(lawpack_facts, "budgets")), ["p.tiny"]); + let budget = map_field(map_field(lawpack_facts, "budgets"), "p.tiny"); + assert_eq!(integer_value(map_field(budget, "maxSteps")), 8); +} + +#[test] +fn source_set_reordering_moves_input_evidence_not_primary_semantics() { + let pack = admitted_pack(); + let (baseline_input, baseline) = generate(SOURCE, &pack); + let reordered_bytes = reordered_source(); + let (reordered_input, reordered) = generate(&reordered_bytes, &pack); + + assert_eq!( + baseline_input.semantic_source(), + reordered_input.semantic_source() + ); + assert_ne!(baseline_input.digest(), reordered_input.digest()); + assert_eq!(baseline.generation_input_digest(), baseline_input.digest()); + assert_eq!( + reordered.generation_input_digest(), + reordered_input.digest() + ); + assert_ne!( + baseline.generation_input_digest(), + reordered.generation_input_digest() + ); + assert_eq!(baseline.projection_roles(), reordered.projection_roles()); + assert_eq!(baseline.artifacts(), reordered.artifacts()); + assert_eq!(baseline.resources(), reordered.resources()); + assert_eq!(baseline.schema(), reordered.schema()); +} + +#[test] +fn budget_change_moves_only_lawpack_authority_facts() { + let pack = admitted_pack(); + let (_, baseline) = generate(SOURCE, &pack); + let mut source = serde_json::from_slice::(SOURCE).expect("checked source is JSON"); + source["budgets"][0]["maxSteps"] = json!(9); + let changed_source = serde_json::to_vec(&source).expect("changed source serializes"); + let (_, changed) = generate(&changed_source, &pack); + + assert_eq!( + changed_artifact_roles(&baseline, &changed), + ["authority-facts.echo-lawpack"] + ); + assert_eq!(baseline.resources(), changed.resources()); + assert_eq!(baseline.schema().bytes(), changed.schema().bytes()); + assert_ne!( + baseline + .artifact("authority-facts.echo-lawpack") + .expect("baseline lawpack facts exist") + .domain_framed_digest(), + changed + .artifact("authority-facts.echo-lawpack") + .expect("changed lawpack facts exist") + .domain_framed_digest() + ); +} + +#[test] +fn revelation_profiles_generate_bounded_observer_invocations() { + let pack = admitted_pack(); + let mut source = serde_json::from_slice::(SOURCE).expect("checked source is JSON"); + source["writeClasses"][0]["identity"]["coordinate"] = json!("read"); + source["profiles"][0]["allowedWriteClasses"][0] = json!("read"); + source["profiles"][0]["opticTemplate"]["opticKind"] = json!("revelation"); + source["profiles"][0]["opticTemplate"]["boundaryKind"] = json!("projection"); + source["effects"][0]["effectKindHint"] = json!("read"); + source["capabilities"][0]["effectKind"] = json!("read"); + source["capabilities"][0]["writeClass"] = json!("read"); + source["capabilities"][0]["semanticDischarge"]["effectKindHint"] = json!("read"); + let observer_source = serde_json::to_vec(&source).expect("observer semantic source serializes"); + let (_, generated) = generate(&observer_source, &pack); + + let registration = generated + .artifact("generated-artifact-profile.echo-dpo-registration") + .expect("registration profile exists") + .canonical_value(); + let operation = map_field(map_field(registration, "operations"), "a.b@1.t"); + assert_eq!( + text_value(map_field(operation, "invocationKind")), + "observer" + ); + + let profiles = map_field( + generated + .resource("resource.target-operation-profiles") + .expect("target operation profiles exist") + .canonical_value(), + "profiles", + ); + let optic = map_field( + map_field(profiles, "continuum.profile.write/v1"), + "opticTemplate", + ); + assert_eq!(text_value(map_field(optic, "opticKind")), "revelation"); + assert_eq!(text_value(map_field(optic, "boundaryKind")), "projection"); + + let target_facts = generated + .artifact("authority-facts.echo-dpo") + .expect("target facts exist") + .canonical_value(); + assert_eq!( + text_value(map_field( + map_field(target_facts, "effectWriteClasses"), + "target.replace" + )), + "read" + ); +} + +#[test] +fn primary_closure_contains_only_declared_generic_provider_semantics() { + let pack = admitted_pack(); + let (_, generated) = generate(SOURCE, &pack); + + assert_eq!( + generated + .projection_roles() + .iter() + .map(String::as_str) + .collect::>(), + PRIMARY_ROLES + ); + assert!(generated + .projection_roles() + .iter() + .all(|role| role != "provider-manifest.echo" && !role.contains("component"))); + + let exports = generated + .resource("resource.lawpack-exports") + .expect("lawpack exports exist") + .canonical_value(); + let CanonicalValueV1::Array(effects) = map_field(exports, "effects") else { + panic!("effects is not an array"); + }; + assert_eq!( + effects + .iter() + .map(|effect| text_value(map_field(effect, "coordinate"))) + .collect::>(), + ["target.replace"] + ); + + let intrinsics = map_field( + generated + .resource("resource.target-intrinsics") + .expect("target intrinsics exist") + .canonical_value(), + "intrinsics", + ); + assert_eq!(map_keys(intrinsics), ["echo.dpo@1.replace"]); + + let profiles = map_field( + generated + .resource("resource.target-operation-profiles") + .expect("operation profiles exist") + .canonical_value(), + "profiles", + ); + assert_eq!(map_keys(profiles), ["continuum.profile.write/v1"]); + + let registration = generated + .artifact("generated-artifact-profile.echo-dpo-registration") + .expect("generated registration profile exists") + .canonical_value(); + let operations = map_field(registration, "operations"); + assert_eq!(map_keys(operations), ["a.b@1.t"]); + assert_eq!( + text_value(map_field( + map_field(operations, "a.b@1.t"), + "invocationKind" + )), + "mutation" + ); + let operation = map_field(operations, "a.b@1.t"); + for (field, expected) in [ + ("inputType", "a.b@1.Input"), + ("outputType", "a.b@1.Output"), + ("effect", "target.replace"), + ("operationProfile", "continuum.profile.write/v1"), + ("opticContract", "replace-point"), + ("budget", "p.tiny"), + ] { + assert_eq!(text_value(map_field(operation, field)), expected); + } + let implementation = map_field(operation, "implementation"); + assert_eq!(text_value(map_field(implementation, "kind")), "native"); + assert_eq!( + text_value(map_field(implementation, "coordinate")), + "echo.dpo@1.replace" + ); + assert_eq!( + text_value(map_field( + map_field(operation, "obstructionMappings"), + "rejected" + )), + "domain.WriteRejected" + ); + + for role in [ + "resource.lawpack-verifier", + "resource.target-lowerer-contract", + "resource.target-verifier-contract", + ] { + assert_eq!( + text_value(map_field( + output(&generated, role).canonical_value(), + "class" + )), + "declarative" + ); + } +} + +#[test] +fn echo_owned_outputs_emit_their_declared_schema_api() { + let pack = admitted_pack(); + let (_, generated) = generate(SOURCE, &pack); + + for role in [ + "generated-artifact-profile.echo-dpo-registration", + "resource.conformance-corpus", + "resource.lawpack-compatibility", + "resource.lawpack-target-adapter", + "resource.lawpack-verifier", + "resource.target-bundle-profile", + "resource.target-cost-algebra", + "resource.target-footprint-algebra", + "resource.target-ir", + "resource.target-lowerer-contract", + "resource.target-obstruction-taxonomy", + "resource.target-verifier-contract", + ] { + let output = output(&generated, role); + assert_eq!( + text_value(map_field(output.canonical_value(), "apiVersion")), + output.schema_contract(), + "{role} must not validate against a generator-invented API marker" + ); + } +} + +#[test] +fn declarative_conformance_resource_cannot_claim_unproven_cases() { + let pack = admitted_pack(); + let (_, generated) = generate(SOURCE, &pack); + let corpus = generated + .resource("resource.conformance-corpus") + .expect("conformance resource exists"); + let mut fabricated = corpus.canonical_value().clone(); + *map_field_mut(&mut fabricated, "cases") = + CanonicalValueV1::Array(vec![CanonicalValueV1::Null]); + let fabricated_bytes = + encode_canonical_cbor_v1(&fabricated).expect("fabricated value remains canonical CBOR"); + + let error = generated + .schema() + .validate_root_bytes(corpus.owning_root(), &fabricated_bytes) + .expect_err("the empty corpus cannot fabricate executable parity evidence"); + assert_eq!( + (error.kind(), error.subject(), error.reference()), + ( + ProviderArtifactGenerationErrorKind::OwningRootRejected, + "echo-provider-conformance-corpus", + "generated-provider-schema" + ) + ); +} + +#[test] +fn generated_profile_type_catalog_is_structurally_admitted() { + let pack = admitted_pack(); + let (_, generated) = generate(SOURCE, &pack); + let profile = generated + .artifact("generated-artifact-profile.echo-dpo-registration") + .expect("generated artifact profile exists"); + let mut malformed = profile.canonical_value().clone(); + *map_field_mut(map_field_mut(&mut malformed, "types"), "a.b@1.Id") = CanonicalValueV1::Null; + let malformed_bytes = + encode_canonical_cbor_v1(&malformed).expect("malformed value remains canonical CBOR"); + + let error = generated + .schema() + .validate_root_bytes(profile.owning_root(), &malformed_bytes) + .expect_err("an untyped catalog value cannot pass the generated profile root"); + assert_eq!( + (error.kind(), error.subject(), error.reference()), + ( + ProviderArtifactGenerationErrorKind::OwningRootRejected, + "generated-artifact-profile", + "generated-provider-schema" + ) + ); +} + +#[test] +fn direct_adapter_routes_are_complete_generic_semantics() { + let pack = admitted_pack(); + let mut source = serde_json::from_slice::(SOURCE).expect("checked source is JSON"); + let mut native_effect = source["effects"][0].clone(); + native_effect["identity"]["coordinate"] = json!("target.native-replace"); + source["effects"] + .as_array_mut() + .expect("effects are an array") + .push(native_effect); + source["capabilities"][0]["effect"] = json!("target.native-replace"); + source["directAdapters"] = json!([{ + "identity": { + "coordinate": "echo.dpo@1.replace-adapter", + "domain": "echo.edict-provider/direct-adapter/v1", + "authority": "echo.provider-target-metadata@1" + }, + "consumesEffect": "target.replace", + "capability": "echo.dpo@1.replace", + "emitsEffects": [] + }]); + source["operations"][0]["implementation"] = json!({ + "kind": "directAdapter", + "adapter": "echo.dpo@1.replace-adapter" + }); + source["lawpackProjection"]["targetAdapters"][0]["effects"] = + json!(["target.native-replace", "target.replace"]); + let adapter_source = + serde_json::to_vec(&source).expect("direct-adapter semantic source serializes"); + let (_, generated) = generate(&adapter_source, &pack); + + for role in [ + "resource.lawpack-target-adapter", + "resource.target-lowerer-contract", + ] { + let implementations = map_field( + generated + .resource(role) + .expect("implementation resource exists") + .canonical_value(), + "effectImplementations", + ); + let direct = map_field(implementations, "target.replace"); + assert_eq!(text_value(map_field(direct, "kind")), "directAdapter"); + assert_eq!( + text_value(map_field(direct, "adapter")), + "echo.dpo@1.replace-adapter" + ); + assert_eq!( + text_value(map_field(direct, "capability")), + "echo.dpo@1.replace" + ); + assert_eq!(text_value(map_field(direct, "writeClass")), "replace"); + + let native = map_field(implementations, "target.native-replace"); + assert_eq!(text_value(map_field(native, "kind")), "native"); + assert_eq!( + text_value(map_field(native, "capability")), + "echo.dpo@1.replace" + ); + assert_eq!(text_value(map_field(native, "writeClass")), "replace"); + } + + let write_classes = map_field( + generated + .artifact("authority-facts.echo-dpo") + .expect("target facts exist") + .canonical_value(), + "effectWriteClasses", + ); + assert_eq!( + text_value(map_field(write_classes, "target.replace")), + "replace" + ); + assert_eq!( + text_value(map_field(write_classes, "target.native-replace")), + "replace" + ); +} + +#[test] +fn lawpack_target_adapter_does_not_overclaim_proof_only_capabilities() { + let pack = admitted_pack(); + let mut source = serde_json::from_slice::(SOURCE).expect("checked source is JSON"); + source["writeClasses"] + .as_array_mut() + .expect("write classes are an array") + .push(json!({ + "identity": { + "coordinate": "read", + "domain": "echo.edict-provider/write-class/v1", + "authority": "echo.provider-target-metadata@1" + } + })); + let mut proof_effect = source["effects"][0].clone(); + proof_effect["identity"]["coordinate"] = json!("target.proof"); + proof_effect["executionClass"] = json!("proofOnly"); + proof_effect["effectKindHint"] = json!("read"); + proof_effect["guardKinds"] = json!([]); + proof_effect["footprintObligation"] = json!("target.proof.footprint"); + proof_effect["costObligation"] = json!("target.proof.cost"); + proof_effect["guardSupport"] = json!(false); + source["effects"] + .as_array_mut() + .expect("effects are an array") + .push(proof_effect); + let mut proof_capability = source["capabilities"][0].clone(); + proof_capability["identity"]["coordinate"] = json!("echo.dpo@1.proof"); + proof_capability["effect"] = json!("target.proof"); + proof_capability["effectKind"] = json!("read"); + proof_capability["writeClass"] = json!("read"); + proof_capability["guardSupport"] = json!(false); + proof_capability["footprintTemplate"] = json!("target.proof.footprint"); + proof_capability["costTemplate"] = json!("target.proof.cost"); + proof_capability["semanticDischarge"]["effectKindHint"] = json!("read"); + proof_capability["semanticDischarge"]["footprintObligation"] = json!("target.proof.footprint"); + proof_capability["semanticDischarge"]["costObligation"] = json!("target.proof.cost"); + proof_capability["canParticipateInAtomicGuard"] = json!(false); + source["capabilities"] + .as_array_mut() + .expect("capabilities are an array") + .push(proof_capability); + let proof_source = + serde_json::to_vec(&source).expect("proof-capability semantic source serializes"); + let (_, generated) = generate(&proof_source, &pack); + + let adapter_implementations = map_field( + generated + .resource("resource.lawpack-target-adapter") + .expect("lawpack target adapter exists") + .canonical_value(), + "effectImplementations", + ); + assert_eq!(map_keys(adapter_implementations), ["target.replace"]); + + let lowerer_implementations = map_field( + generated + .resource("resource.target-lowerer-contract") + .expect("target lowerer exists") + .canonical_value(), + "effectImplementations", + ); + assert_eq!( + text_value(map_field( + map_field(lowerer_implementations, "target.proof"), + "kind" + )), + "native" + ); +} + +#[test] +fn lawpack_verifier_preserves_operation_local_obstruction_mappings() { + let pack = admitted_pack(); + let mut source = serde_json::from_slice::(SOURCE).expect("checked source is JSON"); + source["obstructions"] + .as_array_mut() + .expect("obstructions are an array") + .push(json!({ + "identity": { + "coordinate": "domain.WriteRejectedAlternate", + "domain": "echo.edict-provider/obstruction/v1", + "authority": "echo.provider-semantic-declaration@1" + }, + "authorityClass": "domainMappable", + "payloadSchema": "domain.WriteRejected.Payload" + })); + let mut operation = source["operations"][0].clone(); + operation["identity"]["coordinate"] = json!("a.b@1.u"); + operation["obstructionMappings"][0]["obstruction"] = json!("domain.WriteRejectedAlternate"); + source["operations"] + .as_array_mut() + .expect("operations are an array") + .push(operation); + let two_operation_source = + serde_json::to_vec(&source).expect("two-operation semantic source serializes"); + let (_, generated) = generate(&two_operation_source, &pack); + + let verifier = generated + .resource("resource.lawpack-verifier") + .expect("lawpack verifier exists") + .canonical_value(); + let operations = map_field(verifier, "operationObstructions"); + for (operation, obstruction) in [ + ("a.b@1.t", "domain.WriteRejected"), + ("a.b@1.u", "domain.WriteRejectedAlternate"), + ] { + let mapping = map_field(operations, operation); + assert_eq!(text_value(map_field(mapping, "effect")), "target.replace"); + assert_eq!( + text_value(map_field(map_field(mapping, "failureMappings"), "rejected")), + obstruction + ); + } +} + +#[test] +fn optic_contract_changes_move_the_bound_generated_contracts() { + let pack = admitted_pack(); + let (_, baseline) = generate(SOURCE, &pack); + let mut source = serde_json::from_slice::(SOURCE).expect("checked source is JSON"); + source["profiles"][0]["opticContract"] = json!("replace-point-v2"); + let changed_source = + serde_json::to_vec(&source).expect("changed optic-contract source serializes"); + let (_, changed) = generate(&changed_source, &pack); + + let operation = map_field( + map_field( + changed + .artifact("generated-artifact-profile.echo-dpo-registration") + .expect("changed registration profile exists") + .canonical_value(), + "operations", + ), + "a.b@1.t", + ); + assert_eq!( + text_value(map_field(operation, "opticContract")), + "replace-point-v2" + ); + for role in [ + "resource.target-lowerer-contract", + "resource.target-verifier-contract", + ] { + let contracts = map_field( + changed + .resource(role) + .expect("changed target contract exists") + .canonical_value(), + "opticContracts", + ); + assert_eq!( + text_value(map_field(contracts, "continuum.profile.write/v1")), + "replace-point-v2" + ); + assert_ne!( + baseline + .resource(role) + .expect("baseline target contract exists") + .domain_framed_digest(), + changed + .resource(role) + .expect("changed target contract exists") + .domain_framed_digest() + ); + } +} + +#[test] +fn emitted_content_references_reject_invalid_wesley_coordinates() { + let pack = admitted_pack(); + let mut source = serde_json::from_slice::(SOURCE).expect("checked source is JSON"); + let declaration = source["generatedArtifacts"] + .as_array_mut() + .expect("generated artifacts are an array") + .iter_mut() + .find(|artifact| { + artifact["role"] == json!("generated-artifact-profile.echo-dpo-registration") + }) + .expect("registration profile declaration exists"); + declaration["coordinate"] = json!(" echo.dpo.registration/v1"); + let invalid_source = + serde_json::to_vec(&source).expect("invalid-coordinate semantic source serializes"); + let input = build_input(&invalid_source, &pack); + + let error = generate_provider_primary_artifacts_v1(&input, &pack) + .expect_err("Wesley must reject an invalid emitted content coordinate"); + assert_eq!( + ( + error.kind(), + error.wesley_contract_kind(), + error.subject(), + error.reference(), + ), + ( + ProviderArtifactGenerationErrorKind::WesleyContractRejected, + Some(GenerationContractErrorKind::InvalidCoordinate), + " echo.dpo.registration/v1", + "WESLEY_GENERATION_INVALID_COORDINATE", + ) + ); +} + +#[test] +fn generated_schema_distinguishes_canonical_wire_from_root_admission() { + let pack = admitted_pack(); + let (_, generated) = generate(SOURCE, &pack); + let profile = generated + .artifact("generated-artifact-profile.echo-dpo-registration") + .expect("generated artifact profile exists"); + + let mut schema_invalid = profile.canonical_value().clone(); + *map_field_mut( + map_field_mut(map_field_mut(&mut schema_invalid, "operations"), "a.b@1.t"), + "invocationKind", + ) = CanonicalValueV1::Text("unknown".to_owned()); + let schema_invalid_bytes = encode_canonical_cbor_v1(&schema_invalid) + .expect("schema-invalid value remains canonical CBOR"); + let schema_error = generated + .schema() + .validate_root_bytes(profile.owning_root(), &schema_invalid_bytes) + .expect_err("canonical decoding alone is not owning-root admission"); + assert_eq!( + ( + schema_error.kind(), + schema_error.subject(), + schema_error.reference(), + schema_error.canonical_value_kind(), + ), + ( + ProviderArtifactGenerationErrorKind::OwningRootRejected, + "generated-artifact-profile", + "generated-provider-schema", + None, + ) + ); + + let mut trailing = profile.canonical_bytes().to_vec(); + trailing.push(0xf6); + let canonical_error = generated + .schema() + .validate_root_bytes(profile.owning_root(), &trailing) + .expect_err("trailing canonical data is rejected before schema admission"); + assert_eq!( + ( + canonical_error.kind(), + canonical_error.subject(), + canonical_error.reference(), + canonical_error.canonical_value_kind(), + ), + ( + ProviderArtifactGenerationErrorKind::CanonicalEncodingFailed, + "generated-artifact-profile", + "edict.canonical-cbor/v1", + Some(CanonicalValueErrorKind::TrailingData), + ) + ); +} diff --git a/crates/echo-wesley-gen/tests/provider_semantic_source.rs b/crates/echo-wesley-gen/tests/provider_semantic_source.rs index 7a6d6b49..69c2e982 100644 --- a/crates/echo-wesley-gen/tests/provider_semantic_source.rs +++ b/crates/echo-wesley-gen/tests/provider_semantic_source.rs @@ -337,7 +337,7 @@ fn checked_echo_provider_semantic_source_validates() { "review.provider-generation", GeneratedArtifactKind::ReviewArtifact, "echo.edict-provider-generation-review@1", - "echo.edict-provider.generation-review/v1", + "wesley:GenerationReviewV1", ), ( "schema.echo-provider-artifacts", @@ -406,6 +406,15 @@ fn checked_echo_provider_semantic_source_validates() { generation_provenance.contract_owner.as_deref(), Some("flyingrobots/wesley#728") ); + let generation_review = source + .generated_artifacts + .iter() + .find(|artifact| artifact.kind == GeneratedArtifactKind::ReviewArtifact) + .expect("generation review is a generated package member"); + assert_eq!( + generation_review.contract_owner.as_deref(), + Some("flyingrobots/wesley#728") + ); assert_eq!(source.artifact_resources.len(), 19); assert_eq!( source @@ -1055,6 +1064,20 @@ fn unknown_semantic_references_fail_with_stable_kinds() { ); } +#[test] +fn semantic_source_display_uses_a_stable_kind_label() { + let mut source = source_value(); + source["operations"][0]["implementation"]["capability"] = + Value::String("capability.unknown".to_owned()); + let text = serde_json::to_string(&source).expect("mutated source serializes"); + + let error = parse_provider_semantic_source_v1(&text).expect_err("unknown capability must fail"); + assert_eq!( + error.to_string(), + "provider semantic source unknown-capability: a.b@1.t -> capability.unknown" + ); +} + #[test] fn strict_shape_and_set_duplicates_fail_deterministically() { assert_failure( @@ -1297,6 +1320,21 @@ fn effect_profile_and_implementation_joins_fail_closed() { ), ); } + assert_failure_tuple( + |source| { + source["writeClasses"][0]["identity"]["coordinate"] = json!("read"); + source["profiles"][0]["allowedWriteClasses"][0] = json!("read"); + source["effects"][0]["effectKindHint"] = json!("read"); + source["capabilities"][0]["effectKind"] = json!("read"); + source["capabilities"][0]["writeClass"] = json!("read"); + source["capabilities"][0]["semanticDischarge"]["effectKindHint"] = json!("read"); + }, + ( + ProviderSemanticSourceErrorKind::ProfileEffectMismatch, + "a.b@1.t", + "affectReintegration", + ), + ); assert_failure_tuple( |source| { source["profiles"][0]["atomicity"] = Value::String("non-atomic".to_owned()); @@ -1618,6 +1656,28 @@ fn generated_artifact_and_output_contracts_are_exact() { }, ProviderSemanticSourceErrorKind::GenerationProvenanceContractMismatch, ); + assert_failure_tuple( + |source| { + source["generatedArtifacts"][5]["schemaContract"] = + Value::String("wesley:Unknown".to_owned()); + }, + ( + ProviderSemanticSourceErrorKind::GenerationReviewContractMismatch, + "review.provider-generation", + "wesley:Unknown", + ), + ); + assert_failure_tuple( + |source| { + source["generatedArtifacts"][5]["contractOwner"] = + Value::String("flyingrobots/echo#651".to_owned()); + }, + ( + ProviderSemanticSourceErrorKind::GenerationReviewContractMismatch, + "review.provider-generation", + "flyingrobots/echo#651", + ), + ); assert_failure( |source| { source["lawpackProjection"]["targetAdapters"] = json!([]); diff --git a/crates/echo-wesley-gen/tests/runtime_optic_import.rs b/crates/echo-wesley-gen/tests/runtime_optic_import.rs index d76e8e28..041dc785 100644 --- a/crates/echo-wesley-gen/tests/runtime_optic_import.rs +++ b/crates/echo-wesley-gen/tests/runtime_optic_import.rs @@ -7,7 +7,7 @@ use echo_wesley_gen::import_runtime_optic_artifact; use warp_core::{ OpticArtifactRegistrationError, OpticArtifactRegistry, OPTIC_ARTIFACT_HANDLE_KIND, }; -use wesley_core::compile_runtime_optic; +use wesley_core::compile_operation_artifact; const WORKSPACE_SCHEMA: &str = r" directive @wes_law(id: String!) on FIELD @@ -80,8 +80,8 @@ mutation RenameSymbol($input: RenameSymbolInput!) { } "#; -fn compile_fixture_artifact() -> wesley_core::OpticArtifact { - compile_runtime_optic( +fn compile_fixture_artifact() -> wesley_core::OperationArtifact { + compile_operation_artifact( WORKSPACE_SCHEMA, RENAME_SYMBOL_OPERATION, Some("RenameSymbol"), diff --git a/docs/architecture/application-contract-hosting.md b/docs/architecture/application-contract-hosting.md index 83b3239b..ee3a0f95 100644 --- a/docs/architecture/application-contract-hosting.md +++ b/docs/architecture/application-contract-hosting.md @@ -175,8 +175,16 @@ own artifact list. Two authority-facts documents preserve Edict's one-source rule: lawpack facts carry budgets, while target-profile facts carry operation profiles and resolved effect write classes. Their canonical byte contract is Edict-owned and landed under Edict #157 in Edict PR #159. Generated resource -declarations carry no output digests. Standard Edict resources are explicit -trusted inputs whose canonical publication is tracked by Edict #158. +declarations carry no output digests. Standard Edict resources and the +self-contained provider CDDL are explicit trusted inputs from the Apache-2.0 +contract pack merged in Edict PR #162. Echo admits its exact CDDL, manifest, +contract/domain inventories, resource bytes, digests, and provenance before +generation without searching a filesystem, registry, or network. Schema +instance validation is a separate output-admission step: exact +`edict.canonical-cbor/v1` decoding is followed by validation against the named +owning root in that authenticated CDDL. Passing both checks attests provider +artifact shape only. It does not make authority-facts runtime Echo authority, +install a package, admit an operation, or authorize a runtime consequence. The target-profile lowerer and verifier resources are generated declarative contract documents. They do not select executable implementations. The package @@ -189,6 +197,80 @@ world `edict:target-provider@1.0.0`. Generated provenance is a generic Edict `generationProvenance` package member whose document contract remains owned by Wesley #728. +The generation invocation itself is a pure Wesley extension input. It binds +the exact Echo semantic-source file, admitted Edict CDDL and manifest, and +versioned generator settings as content-addressed inputs. The first closure has +no GraphQL Shape authority, so its Wesley Shape and root-operation catalog are +empty; the Echo semantic operation is not projected into a synthetic GraphQL +operation. Primary lawpack, target-profile, facts, registration-profile, and +schema roles are selected before provenance. Provenance and review are derived +after primary output digests exist, preventing self-referential digest sets. +Set-like source reordering preserves normalized semantic projections but moves +the exact-source generation-input identity, as honest provenance requires. + +Primary provider generation is a pure digest DAG. Echo first emits and +owning-root-validates the declarative resource closure and generated operation +profile, then the target profile, then the lawpack, and finally the two +source-partitioned authority-facts documents. The target profile binds the +generated profile and resource domain digests; the lawpack binds the completed +target-profile digest; authority facts bind their completed source artifact. +No primary artifact binds review or provenance, so the graph has no +self-reference. Wesley content references separately bind exact output bytes. +Neither digest form installs an artifact or turns generated authority facts +into runtime Echo authority. + +The derived Wesley provenance manifest binds the exact semantic-source, Edict +CDDL, Edict manifest, settings, and caller-supplied generator component bytes. +Its emitted closure is exactly the five canonical primary artifacts plus the +raw self-contained CDDL. Construction immediately re-verifies all three source +and six output byte identities. The fourteen resource documents remain +transitively covered by the primary manifest DAG; restating them as primary +emissions would misrepresent the projection boundary. The generator API never +discovers an executable, path, environment, process, registry, clock, or +network input. A primary closure retains the exact Wesley input digest that +produced it, so outputs from another invocation cannot be falsely attributed +merely because their role closure matches. The generator coordinate must also +be disjoint from every exact source artifact, declared generated artifact, +resource, provider, and package coordinate. + +The deterministic review JSON is Wesley's `GenerationReviewV1`, derived only +after the provenance wrapper has verified exact materials. It copies the input, +provenance, generator, projection roles, sources, and primary emissions for +inspection, but its `authoritative` posture is permanently false. Review does +not become a second contract, provenance proof, package admission, or runtime +authority surface. + +The checked provider corpus materializes that digest DAG as exactly 22 files: +five canonical-CBOR primary artifacts, fourteen canonical-CBOR resources, raw +self-contained CDDL, canonical provenance JSON, and canonical review JSON. Its +generator identity is the Wesley digest of a versioned binary frame containing +an explicit compile-time inventory of provider generator source, Cargo +manifests and lockfile, and the pinned Rust toolchain. Authored semantic, +settings, CDDL, and contract-manifest bytes remain separate Wesley inputs, and +generated corpus bytes never re-enter generator identity. The frame therefore +attests the source/dependency-lock closure without claiming a reproducible +executable or creating a circular output digest. + +Corpus comparison is an exact-byte, exact-path check. Missing, changed, and +unexpected entries are reported in stable order. Check mode performs no +directory creation, write, deletion, normalization, or symlink traversal; +the caller's `--out` path is an ambient locator, while its final corpus-root +entry and every descendant are opened without following symlinks and retained as +directory capabilities. Generation refuses every unexpected entry it observes +before creating or replacing an expected path and leaves that entry for explicit +operator disposition. Each temporary write, sync, replacement, and failure +cleanup remains relative to the validated parent handle. Unrelated ancestors +used to locate the requested root are outside this capability boundary. This +checked build artifact remains provider evidence. Echo runtime admission and +installation are still separate acts. + +The generated operation profile preserves native versus direct-adapter +selection, operation-local obstruction mappings, and the target optic contract. +Invocation posture is derived from admitted optic semantics: mutation-capable +write classes require affect/reintegration, while non-mutating classes require +revelation/projection and remain bounded observers. This rule is generic and +does not encode application- or editor-specific behavior. + The first capability distinguishes two nested domains. `echo.span-ir/v1` is the inner Echo target IR domain selected by `echo.dpo@1.replace`. `edict.target-ir.artifact/v1` is the outer canonical artifact domain that diff --git a/schemas/edict-provider/README.md b/schemas/edict-provider/README.md index e0fc6801..ba085b35 100644 --- a/schemas/edict-provider/README.md +++ b/schemas/edict-provider/README.md @@ -82,12 +82,58 @@ evidence only. ## Generated Outputs -Issue #652 will compile this source into the declared Edict lawpack, target -profile, two source-partitioned authority-facts documents, generated-artifact -profile, self-contained CDDL schema, deterministic review artifact, manifest -subresources, and Wesley-owned `generationProvenance` metadata. Resources marked -`external` are explicit digest-locked generator inputs; placeholder digests -are forbidden. +Issue #652 now compiles the primary closure into the declared Edict lawpack, +target profile, two source-partitioned authority-facts documents, +generated-artifact profile, fourteen manifest subresources, and self-contained +CDDL schema. Every canonical output passes its owning generated root, and every +Edict-owned output also passes the independently admitted upstream root. The +Wesley-owned `generationProvenance` document binds exact source, settings, +generator, and six primary output byte identities and immediately verifies all +of them. Its emitted set contains the five canonical primary artifacts plus the +raw self-contained CDDL; fourteen resources are transitively bound through +those artifacts rather than incorrectly promoted into the primary projection. +The primary wrapper retains its producing Wesley input digest, and generator +coordinates cannot alias any declared artifact, resource, provider, or package +coordinate. Exact source reordering therefore preserves all primary emitted +bytes while intentionally moving the provenance identity. +Wesley's non-authoritative `GenerationReviewV1` is then derived from the +verified input/provenance pair and deterministically copies its generator, +roles, sources, and emitted identities. It cannot claim semantic or runtime +authority. The exact 22-file result is checked under +[`generated/v1/`](generated/README.md): five canonical-CBOR primary artifacts, +fourteen canonical-CBOR resources, raw self-contained CDDL, canonical Wesley +provenance JSON, and canonical non-authoritative review JSON. The dedicated +generator binds a fixed source/dependency-lock frame and its `--check` mode +reports missing, changed, or unexpected files without rewriting them. Resources +marked `external` are explicit digest-locked generator inputs; placeholder +digests are forbidden. + +[`generation-settings-v1.json`](generation-settings-v1.json) is the explicit, +versioned settings input for that invocation. The first closure selects no +GraphQL Shape source: `a.b@1.t` remains an Echo semantic operation and is not +invented as a GraphQL root field. The canonical Wesley generation input binds +the exact semantic-source bytes, admitted Edict CDDL and manifest bytes, and +settings bytes. Its primary projection-role set excludes provenance and review +because those are derived envelopes over the primary outputs and cannot include +their own digests. + +The validator's normalized semantic projection is insensitive to ordering of +set-like declarations. The generation-input digest intentionally still changes +when the authored JSON bytes are reordered, because later provenance binds the +exact source artifact rather than mislabeling normalized bytes as the checked +file. Generated semantic artifact bytes remain a function of the normalized +model. + +Echo-owned resource documents use their declared schema API as their wire +`apiVersion`; generated resource digests are framed by the resource coordinate. +The lawpack and target-profile manifests bind those domain-framed identities, +while later Wesley provenance binds exact-byte content references. The empty +declarative conformance corpus cannot carry cases until executable parity +evidence exists. Direct-adapter routes bind the adapter and native capability, +and operation-local failure mappings are not collapsed merely because two +operations share one semantic effect. Read-class operations must use a +revelation/projection optic and generate observer metadata rather than mutation +metadata. The two authority-facts outputs use Edict's `edict.authority-facts/v1` domain and bind their contract owner to @@ -101,10 +147,20 @@ contract documents. They are not executable component bytes. Issue #655 binds the exact lowerer and verifier components, including their frozen WIT world attestations, when it assembles the provider manifest. -External Edict contract inputs require the trusted artifact publication model -tracked by [Edict #158](https://github.com/flyingrobots/edict/issues/158). -The semantic source selects their contracts but does not authenticate arbitrary -caller bytes under those coordinates. +External Edict contract inputs come from the checked +[`contracts/v1/`](contracts/v1/README.md) publication merged in +[Edict PR #162](https://github.com/flyingrobots/edict/pull/162). Echo passes the +CDDL and manifest bytes explicitly to +`provider_contract_pack::admit_provider_contract_pack_v1(...)`, which verifies +the pinned publication identity, exact inventories, resource bytes, digests, +and provenance before generation. The semantic source selects those contracts +but does not authenticate arbitrary caller bytes under their coordinates. +Contract-pack admission authenticates schema authority; generated values must +still pass the owning CDDL root during output admission. Echo's pure +`AdmittedProviderContractPackV1::validate_contract_bytes(...)` boundary now +enforces both exact `edict.canonical-cbor/v1` bytes and that named root. This is +generation-time artifact validation, not runtime installation or authority; +Echo must still explicitly admit any package, operation, or consequence. Issue #655, after the lowerer and verifier components exist, assembles those outputs and generates the package-root `edict.provider-manifest/v1` for diff --git a/schemas/edict-provider/contracts/v1/README.md b/schemas/edict-provider/contracts/v1/README.md new file mode 100644 index 00000000..5dfc6f55 --- /dev/null +++ b/schemas/edict-provider/contracts/v1/README.md @@ -0,0 +1,23 @@ + + + +# Edict Provider Contract Pack v1 + +This directory vendors the exact Edict provider contract pack merged in +[Edict PR #162](https://github.com/flyingrobots/edict/pull/162) at commit +[`7cd8858c577fcfb6a05f0f617dfa821bb183c7df`](https://github.com/flyingrobots/edict/commit/7cd8858c577fcfb6a05f0f617dfa821bb183c7df): + +- `edict-provider-contracts.cddl` is the assembled Edict-owned CDDL contract. +- `manifest.json` binds that CDDL and its contract resources to their published + identities, digests, and provenance. + +Both files are licensed under Apache-2.0, as declared by the upstream CDDL and +manifest. They are exact external inputs, not Echo-authored schemas and not +generated Echo outputs. Do not edit either file independently. An update must +replace the pair from one reviewed Edict publication and update this provenance +record in the same change. + +The Echo generator receives these bytes explicitly. It does not discover this +directory, resolve a mutable coordinate, read a registry, or fetch the pack at +generation time. The checked path makes the selected publication reviewable; +admission of the supplied bytes is the executable authority boundary. diff --git a/schemas/edict-provider/contracts/v1/edict-provider-contracts.cddl b/schemas/edict-provider/contracts/v1/edict-provider-contracts.cddl new file mode 100644 index 00000000..7544122d --- /dev/null +++ b/schemas/edict-provider/contracts/v1/edict-provider-contracts.cddl @@ -0,0 +1,793 @@ +; SPDX-License-Identifier: Apache-2.0 +; edict-provider-contracts.cddl +; Generated from Edict-owned ABI fragments. DO NOT EDIT. + +; --- edict-common.cddl --- +; edict-common.cddl +; Shared CDDL types for the Edict ABIs, defined ONCE here so they cannot drift +; (EDICT-ABI-NODUP-001). Two groups: +; - resource-ref, sha256-digest, effect-failure-body, effect-kind, authority-class, +; core-type-ref: assembled with edict-target-profile.cddl and +; edict-lawpack.cddl by the build; those schemas do not redefine them. +; - operation-profile, optic-template, aperture-requirement and their refs: +; consumed by the Core/optic layer (edict-core.cddl) and referenced by the +; language/target-profile specs. +; Authoritative byte encoding: edict.canonical-cbor/v1. + +; A normative subcomponent referenced by identity plus digest. Manifests never +; embed their own self-digest in their preimage (EDICT-CORE-SELFHASH-001). +resource-ref = { id: tstr, digest: sha256-digest } + +; Digests are authoritative as typed byte values, never hex strings. Review JSON +; renders this as "sha256:<64 lowercase hex>" (EDICT-DIGEST-WIRE-001). +sha256-digest = [ algorithm: "sha256", bytes: bstr .size 32 ] + +; A named low-level failure an effect can raise. The source obstruction map +; binds it (by coordinate) and constructs a typed domain obstruction from its +; payload (EDICT-ABI-FAILURE-NAMED-001). +; An effect's `effectFailures` list MUST have unique `coordinate`s: since the +; obstruction map is keyed by coordinate, two failures sharing a coordinate (even +; with different authorityClass/payloadType) make exhaustive mapping and binder +; typing ambiguous and are rejected (EDICT-ABI-FAILURE-UNIQUE-001). +; +; Effects carry their failures as a map `{ failure-ident => effect-failure-body }` +; (see the target/lawpack effect schemas). The failure coordinate is the map +; KEY, so it is not repeated in the body and cannot disagree with the key. +effect-failure-body = { + authorityClass: authority-class, + payloadType: core-type-ref, ; typed, bounded (may be empty record) +} + +; A failure coordinate must be a bare Edict `ident` (letter/underscore then +; letters/digits/underscores) AND must not be a reserved keyword (e.g. `else`, +; `basis`, `where`, `require`, `for`, `if`). The source obstruction-map LHS only +; accepts a non-keyword `ident`, so a hyphen/dot/keyword coordinate would be +; ABI-valid yet impossible to map exhaustively in source. The regex captures the +; lexical shape; keyword exclusion is an additional validation rule +; (EDICT-ABI-FAILURE-IDENT-001). +failure-ident = tstr .regexp "[A-Za-z_][A-Za-z0-9_]*" + +effect-kind = "read" / "create" / "ensure" / "replace" / "delete" / + "append" / "reduce" / "semantic.emit" / "custom" + +authority-class = "domainMappable" / "participantOwned" / "integrityFault" / + "resourceFault" / "internalFault" + +core-type-ref = tstr ; canonical Core type coordinate + +; An operation profile supplies the optic template a Core intent resolves its +; opticKind/boundaryKind/supportPolicy/lossDisposition from. Target profiles and +; lawpacks publish these as a map `{ coordinate => operation-profile }`, so the +; coordinate is the KEY, not a value field (EDICT-OPTIC-TEMPLATE-OWNER-001, +; EDICT-ABI-OPPROFILE-UNIQUE-001). +operation-profile = { + opticTemplate: optic-template, + effectPredicate: tstr, ; coordinate of the operation-mode predicate +} + +optic-template = { + opticKind: "revelation" / "affectReintegration", + boundaryKind: "projection" / "affect", + supportPolicy: tstr, ; canonical support-policy coordinate + lossDisposition: tstr, ; canonical loss-disposition coordinate + ? basisTemplate: tstr, ; optional digest-locked basis template coord + ; the aperture requirement this template supplies. Required when the template + ; is the source of a Core optic's apertureRequirement (i.e. the intent has no + ; source `footprint <= ...`), since apertureRequirement is mandatory in Core + ; (EDICT-OPTIC-APERTURE-REF-001). + ? apertureRequirement: aperture-requirement, +} + +; apertureRequirement is a typed reference, never a free-form string. A review +; rendering may show its coordinate (EDICT-OPTIC-APERTURE-REF-001). +aperture-requirement = footprint-ceiling-ref / abstract-footprint-obligation-ref +footprint-ceiling-ref = { kind: "footprintCeiling", ref: tstr } +abstract-footprint-obligation-ref = { kind: "abstractFootprintObligation", ref: tstr } + +; --- edict-core.cddl --- +; edict-core.cddl +; Normative schema for the Edict Core v1 semantic model. +; +; Scope boundary: this file defines Core meaning and schema shape only. It does +; not define a canonical encoder, Core module hash fields, hash fixtures, target +; lowering, admission bundles, or target-owned IR. + +core-module = { + apiVersion: "edict.core/v1", + coordinate: tstr, + imports: [* core-import], + types: { * tstr => core-type }, + intents: { + tstr => core-intent }, + requiredCoreCapabilities: [* tstr], +} + +core-import = { + kind: "lawpack" / "target" / "core", + ref: resource-ref, + ? alias: tstr, +} + +; --- types --------------------------------------------------------------- + +core-type = core-scalar-type / core-record-type / core-variant-type / + core-option-type / core-list-type / core-map-type / + core-capability-ref-type + +core-scalar-type = core-bool-type / core-int-type / core-string-type / + core-bytes-type / core-unit-type + +core-bool-type = { kind: "Bool" } +core-unit-type = { kind: "Unit" } +core-int-type = { + kind: "I64" / "U64" / "I32" / "U32" / "I16" / "U16" / "I8" / "U8", +} +core-string-type = { + kind: "String", + max: uint, + canonical: "unicode-scalar-nfc" / "raw-utf8", +} +core-bytes-type = { + kind: "Bytes", + max: uint, +} +core-record-type = { + kind: "Record", + fields: { * tstr => core-type-ref }, +} +core-variant-type = { + kind: "Variant", + cases: { + tstr => variant-case-body }, +} +variant-case-body = { + ? payload: core-type-ref, +} +core-option-type = { + kind: "Option", + item: core-type-ref, +} +core-list-type = { + kind: "List", + item: core-type-ref, + max: uint, +} +core-map-type = { + kind: "Map", + key: core-type-ref, + value: core-type-ref, + max: uint, +} +core-capability-ref-type = { + kind: "CapabilityRef", + item: core-type-ref, +} + +; core-type-ref is defined in edict-common.cddl and assembled with this schema. + +; --- references and values ---------------------------------------------- + +; Local identity is alpha-stable. `id` is the compiler-owned local coordinate; +; `alphaName` is the normalized human/debug name. Source binder spelling is not +; identity. +local-ref = { + id: tstr, + alphaName: tstr, + type: core-type-ref, +} + +core-value = core-null-value / core-bool-value / core-int-value / + core-string-value / core-bytes-value / core-record-value / + core-variant-value / core-list-value / core-map-value / + core-capability-value + +core-null-value = { kind: "null" } +core-bool-value = { kind: "bool", value: bool } +core-int-value = { kind: "int", width: tstr, value: int } +core-string-value = { kind: "string", value: tstr } +core-bytes-value = { kind: "bytes", value: bstr } +core-record-value = { kind: "record", fields: { * tstr => core-value } } +core-variant-value = { + kind: "variant", + type: core-type-ref, + case: tstr, + ? payload: core-value, +} +core-list-value = { kind: "list", values: [* core-value] } +core-map-value = { kind: "map", entries: [* [key: core-value, value: core-value]] } +core-capability-value = { + kind: "capability", + receipt: sha256-digest, +} + +; Edict-authored pure helpers use a pure Core function body. The body can bind +; pure expressions and return an expression, but it cannot contain Core effect, +; guard, branch, loop, match-node, or proof-obligation nodes. +core-fn-body = { + params: [* local-ref], + body: core-pure-block, +} + +core-pure-block = { + locals: [* local-ref], + bindings: [* pure-let-node], + result: core-expr, +} + +pure-let-node = { + kind: "let", + binding: local-ref, + value: core-expr, +} + +; --- expressions and predicates ----------------------------------------- + +core-expr = local-expr / const-expr / record-expr / field-expr / + variant-expr / match-expr / call-expr / list-expr / map-expr / + if-expr + +local-expr = { kind: "local", ref: local-ref } +const-expr = { kind: "const", value: core-value } +record-expr = { kind: "record", fields: { * tstr => core-expr } } +field-expr = { kind: "field", base: core-expr, field: tstr } +variant-expr = { + kind: "variant", + type: core-type-ref, + case: tstr, + ? payload: core-expr, +} +match-expr = { + kind: "match", + scrutinee: core-expr, + arms: [+ match-arm], +} +match-arm = { + case: tstr, + ? binder: local-ref, + body: core-expr, +} +call-expr = { + kind: "call", + callee: tstr, + typeArgs: [* core-type-ref], + args: [* core-expr], +} +list-expr = { kind: "list", values: [* core-expr] } +map-expr = { kind: "map", entries: [* [key: core-expr, value: core-expr]] } +if-expr = { + kind: "if", + predicate: core-predicate, + then: core-expr, + else: core-expr, +} + +core-predicate = true-predicate / false-predicate / not-predicate / + all-predicate / any-predicate / compare-predicate / + call-predicate / obstruction-predicate + +true-predicate = { kind: "true" } +false-predicate = { kind: "false" } +not-predicate = { kind: "not", value: core-predicate } +all-predicate = { kind: "all", values: [+ core-predicate] } +any-predicate = { kind: "any", values: [+ core-predicate] } +compare-predicate = { + kind: "compare", + op: "==" / "!=" / "<" / "<=" / ">" / ">=", + left: core-expr, + right: core-expr, +} +call-predicate = { + kind: "call", + predicate: tstr, + args: [* core-expr], +} +obstruction-predicate = { + kind: "obstruction", + coordinate: failure-ident, + payload: core-expr, +} + +input-constraint = { + coordinate: tstr, + source: "where" / "compiler", + predicate: core-predicate, +} + +; --- intents, blocks, and nodes ----------------------------------------- + +core-intent = { + input: core-type-ref, + output: core-type-ref, + requiredOperationProfile: tstr, + inputConstraints: [* input-constraint], + coreEvaluationBudget: core-budget, + body: core-block, + ? optic: core-optic, +} + +core-budget = { + maxSteps: uint, + maxAllocatedBytes: uint, + maxOutputBytes: uint, +} + +core-optic = { + opticKind: "revelation" / "affectReintegration", + boundaryKind: "projection" / "affect", + apertureRequirement: aperture-requirement, + supportPolicy: tstr, + lossDisposition: tstr, +} + +core-block = { + locals: [* local-ref], + nodes: [* core-node], + result: core-expr, +} + +core-node = let-node / require-node / effect-node / guard-node / branch-node / + for-node / match-node / proof-obligation-node + +let-node = { + kind: "let", + binding: local-ref, + value: core-expr, +} +require-node = { + kind: "require", + predicate: core-predicate, + onFailure: require-failure-arm, +} +require-failure-arm = terminal-require-failure / + continue-obstructed-require-failure +terminal-require-failure = { + kind: "terminal", + reason: obstruction-reason, +} +continue-obstructed-require-failure = { + kind: "continueObstructed", + reason: obstruction-reason, +} +obstruction-reason = { + reasonKind: tstr, + payload: { * tstr => core-expr }, +} +effect-node = { + kind: "effect", + binding: local-ref, + effect: tstr, + input: core-expr, + obstructionMap: { * failure-ident => obstruction-arm }, +} +obstruction-arm = { + binder: local-ref, + value: core-expr, +} +guard-node = { + kind: "guard", + predicate: core-predicate, + obstruction: core-expr, +} +branch-node = { + kind: "branch", + predicate: core-predicate, + then: core-block, + else: core-block, +} +for-node = { + kind: "for", + binder: local-ref, + iter: core-expr, + bound: core-bound, + body: core-block, +} +match-node = { + kind: "match", + scrutinee: core-expr, + arms: [+ match-block-arm], +} +match-block-arm = { + case: tstr, + ? binder: local-ref, + body: core-block, +} +proof-obligation-node = { + kind: "proof", + coordinate: tstr, + predicate: core-predicate, +} + +core-bound = literal-bound / coordinate-bound +literal-bound = { kind: "literal", value: uint } +coordinate-bound = { kind: "coordinate", ref: tstr } + +; Shared resource-ref, sha256-digest, failure-ident, aperture-requirement, and +; core-type-ref are defined once in edict-common.cddl. + +; --- edict-lawpack.cddl --- +; edict-lawpack.cddl +; Canonical schema for the Edict lawpack manifest and export surface. +; Authoritative byte encoding: edict.canonical-cbor/v1. JSON in the prose specs +; is a review rendering generated from this schema (EDICT-ABI-NODUP-001). + +lawpack-manifest = { + apiVersion: "edict.lawpack/v1", + id: tstr, + version: tstr, + acceptedCoreAbi: [+ tstr], + dependencies: [* lawpack-dep], ; acyclic, digest-locked (EDICT-LAWPACK-DAG-001) + exports: resource-ref, + ? targetAdapters: [+ target-adapter], ; required only if any runtime effect exists + ? helperComponent: executable-component, ; executable helpers carry their own sandbox+fuel + verifier: verifier, ; classified: declarative or executable + compatibility: resource-ref, + conformanceFixtureCorpus: resource-ref, +} + +; A verifier is either a declarative ruleset (no runtime) or an executable +; component. An executable verifier MUST carry its own sandbox and fuel model, +; so the schema enforces that no executable component is left unbounded +; (EDICT-ABI-VERIFIER-BOUND-001). +verifier = declarative-verifier / executable-verifier +declarative-verifier = { class: "declarative", ruleset: resource-ref } +executable-verifier = { + class: "executable", + component: resource-ref, + sandbox: resource-ref, + fuelModel: resource-ref, +} + +; Any executable component is bounded by its own sandbox + fuel model. +executable-component = { + component: resource-ref, + sandbox: resource-ref, + fuelModel: resource-ref, +} + +lawpack-dep = { id: tstr, version: tstr, digest: sha256-digest } + +; Adapter selection keys SOLELY off the digest-locked `acceptedTargetProfile` +; (its `id` is the profile id; its `digest` pins the exact profile/version). There +; are no independent display strings that could disagree with the lock, so a +; resolver cannot bind an adapter to one target while the lock proves another +; (EDICT-LAWPACK-ADAPTER-TARGETIR-001). +target-adapter = { + acceptedTargetProfile: resource-ref, ; digest-locked, authoritative selector + acceptedTargetIr: resource-ref, ; digest-locked + adapter: resource-ref, +} + +; Shared types resource-ref and sha256-digest are defined once in +; edict-common.cddl and assembled with this schema (EDICT-ABI-NODUP-001). + +; --- export surface ------------------------------------------------------ + +lawpack-exports = { + types: [* exported-type], + constants: [* exported-constant], + pureFunctions: [* pure-function], + effects: [* semantic-effect], + obstructions: [* obstruction-def], + ; keyed by operation-profile coordinate → uniqueness enforced + ; (EDICT-ABI-OPPROFILE-UNIQUE-001) + ; operation-profile records this lawpack exports (optic templates that + ; `implements`/`profile` clauses resolve against). operation-profile is + ; defined in edict-common.cddl (EDICT-ABI-OPPROFILE-SLOT-001). + operationProfiles: { * tstr => operation-profile }, ; keyed by coordinate +} + +exported-type = { coordinate: tstr, definition: core-type-ref } +exported-constant = { coordinate: tstr, type: core-type-ref, value: any } + +; A pure helper is a discriminated union by `source`, so the schema itself +; guarantees an implementation exists (EDICT-LAWPACK-PURE-IMPL-001): +; - "edict": authored in Edict/Core; the Core body is carried inline (hashed +; with the export surface). The schema requires the `body` field. +; - "component": implemented outside Edict; carries no inline body and instead +; carries its own digest-locked `implementation` (sandbox + fuel). It does +; not depend on the optional manifest-level helperComponent. +pure-function = edict-pure-function / component-pure-function + +pure-function-common = ( + coordinate: tstr, + typeParameters: [* tstr], + parameterTypes: [* core-type-ref], ; all bounded + returnType: core-type-ref, ; bounded + costTemplate: tstr, + determinismClass: "total" / "total-with-typed-diagnostic", +) + +edict-pure-function = { + pure-function-common, + source: "edict", + body: core-fn-body, ; inline, hash-significant +} + +component-pure-function = { + pure-function-common, + source: "component", + ; the digest-locked component implementing this helper. Required at the schema + ; level so a component helper can never validate without a hash-bound, + ; sandbox+fuel-described implementation (EDICT-LAWPACK-PURE-IMPL-001). + implementation: executable-component, +} + +; core-fn-body is defined by edict-core.cddl and assembled with this lawpack +; schema. It is a pure Core function body, not an effect-capable core-block. + +semantic-effect = { + coordinate: tstr, + typeParameters: [* tstr], + inputType: core-type-ref, ; bounded + outputType: core-type-ref, ; bounded + executionClass: "proofOnly" / "runtime", ; orthogonal to writeClass + effectKindHint: effect-kind, + footprintObligation: tstr, + costObligation: tstr, + effectFailures: { * failure-ident => effect-failure-body }, ; keyed by coordinate; unique + guardSupport: bool, +} + +obstruction-def = { + coordinate: tstr, + authorityClass: authority-class, + payloadSchema: core-type-ref, ; typed, bounded (may be empty record) +} + +; effect-failure-body, effect-kind, authority-class, and core-type-ref are defined +; once in edict-common.cddl and assembled with this schema (EDICT-ABI-NODUP-001). + +; --- edict-target-profile.cddl --- +; edict-target-profile.cddl +; Canonical schema for the Edict target profile manifest. +; Authoritative byte encoding: edict.canonical-cbor/v1 +; (see SPEC_continuum-contract-bundle-v1.md). JSON in the prose specs is a +; review rendering generated from this schema; this CDDL is the single source +; of truth (EDICT-ABI-NODUP-001). + +target-profile-manifest = { + apiVersion: "edict.target-profile/v1", + id: tstr, ; e.g. "echo.dpo" + version: tstr, ; e.g. "1" + acceptedCoreAbi: [+ tstr], ; e.g. ["edict.core/v1"] + + intrinsics: resource-ref, + intrinsicNamespace: tstr, + ; publishes this profile's operation-profile records (optic templates that + ; `profile`/`implements` clauses resolve against). References an + ; operation-profiles-document (EDICT-ABI-OPPROFILE-SLOT-001). + operationProfiles: resource-ref, + footprintAlgebra: resource-ref, + costAlgebra: resource-ref, + targetIr: resource-ref, + obstructionTaxonomy: resource-ref, + verifier: resource-ref, + lowerer: resource-ref, + sandbox: resource-ref, + fuelModel: resource-ref, + + ; fields the language spec requires of every profile + bundleProfile: resource-ref, + generatedArtifactProfiles: [* resource-ref], + canonicalEncodingRules: resource-ref, + ; Reserved/deferred: will list accepted lawpack-adapter ABI ids once that + ; byte-level ABI (edict.lawpack-adapter/v1) is specified. The schema enforces + ; emptiness now — the type is the empty array, so non-empty values are + ; rejected, not merely discouraged (EDICT-ABI-LAWPACK-ADAPTER-DEFER-001). + ? acceptedLawpackAdapterAbi: [], + diagnosticAbi: resource-ref, + + ; application doctrine + applicationModel: "atomic", + readConsistency: "application-snapshot" / tstr, + guardEvaluation: "precommit-atomic" / tstr, + obstructionRollback: "no-visible-effects" / tstr, + multiTarget: bool, + ; whether the target can evaluate precommit postcondition (`guarantee`) checks + ; inside the atomic application unit (EDICT-TARGET-POSTCOND-001) + postconditionSupport: bool, + + deterministicExecution: resource-ref, + conformanceFixtureCorpus: resource-ref, +} + +; Shared types resource-ref and sha256-digest are defined once in +; edict-common.cddl and assembled with this schema by the build +; (EDICT-ABI-NODUP-001). They are not redefined here. + +; --- intrinsic signature ------------------------------------------------- +; The artifact referenced by the manifest's `intrinsics` resource-ref is the +; intrinsic-signature corpus document below. Its layout is fixed so two +; independent profiles validate/hash the corpus identically +; (EDICT-ABI-INTRINSICS-DOC-001). + +; intrinsics is a MAP keyed by coordinate, so the schema itself enforces +; coordinate uniqueness. A provider receives the resolved corpus as a +; digest-bound semantic input and resolves coordinates within that artifact. +; Each map key MUST equal its record's `coordinate` field +; (EDICT-ABI-INTRINSIC-UNIQUE-001). +intrinsics-document = { + apiVersion: "edict.target-profile.intrinsics/v1", + intrinsics: { * tstr => intrinsic }, +} + +; The artifact referenced by the manifest's `operationProfiles` resource-ref. +; operation-profile / optic-template are defined in edict-common.cddl. Keyed by +; coordinate so resolution can't pick between two same-coordinate profiles +; (EDICT-ABI-OPPROFILE-SLOT-001, EDICT-ABI-OPPROFILE-UNIQUE-001). +operation-profiles-document = { + apiVersion: "edict.target-profile.operation-profiles/v1", + profiles: { * tstr => operation-profile }, +} + +; A typed pre-lowering question that can be proposed by Watson or an agent and +; checked by the compiler. It is canonical-CBOR encoded under +; `edict.lowering-requirements/v1`; the compiler checks this artifact, not the +; prose that produced it. +lowering-requirements = { + apiVersion: "edict.lowering-requirements/v1", + operationProfile: tstr, + semanticEffects: [* semantic-effect-requirement], + requiredWriteClasses: [* write-class], + guardKinds: [* guard-kind], + atomicity: atomicity-requirement, + postconditionSupport: bool, + obstructionCoordinates: [* tstr], + footprintObligations: [* tstr], + costObligations: [* tstr], + opticContract: tstr, +} + +semantic-effect-requirement = { + coordinate: tstr, + writeClass: write-class, + guardKinds: [* guard-kind], + obstructionCoordinates: [* tstr], + footprintObligations: [* tstr], + costObligations: [* tstr], +} + +write-class = "none" / "read" / "create" / "ensure" / "append" / + "replace" / "delete" / tstr +guard-kind = "precommit-atomic" / tstr +atomicity-requirement = "atomic" / tstr + +; A genuine union: pure constructors carry no effect kind or failures; effect +; intrinsics must (EDICT-TARGET-INTRINSIC-CLASS-001). The schema enforces this, +; not a comment. + +; The intrinsic's coordinate is the intrinsics map KEY, not a value field, so the +; key and coordinate can never disagree (EDICT-ABI-INTRINSIC-UNIQUE-001). +intrinsic = pure-intrinsic / effect-intrinsic + +pure-intrinsic = { + intrinsicClass: "pure", + typeParameters: [* tstr], + argumentTypes: [* core-type-ref], + returnType: core-type-ref, + guardSupport: false, + footprintTemplate: tstr, + costTemplate: tstr, + writeClass: "none", +} + +effect-intrinsic = { + intrinsicClass: "effect", + typeParameters: [* tstr], + argumentTypes: [* core-type-ref], + returnType: core-type-ref, + effectKind: effect-kind, + ; map keyed by failure coordinate (failure-ident); the failure coordinate is + ; the key, not a value field, so uniqueness is structural + ; (EDICT-ABI-FAILURE-UNIQUE-001). + effectFailures: { * failure-ident => effect-failure-body }, + guardSupport: bool, + footprintTemplate: tstr, + costTemplate: tstr, + writeClass: "read" / "create" / "ensure" / "append" / "replace" / + "delete" / "custom", + canParticipateInAtomicGuard: bool, +} + +; effect-failure-body, effect-kind, authority-class, and core-type-ref are defined +; once in edict-common.cddl and assembled with this schema (EDICT-ABI-NODUP-001). + +; --- edict-authority-facts.cddl --- +; edict-authority-facts.cddl +; Canonical schema for the first compiler-context authority-facts document. +; Authoritative byte encoding: edict.canonical-cbor/v1. +; +; This schema is assembled with edict-common.cddl so source.digest uses the +; shared sha256-digest typed value. JSON is a review/input rendering: its +; `sha256:<64 hex>` source digest is projected to [`sha256`, 32 raw bytes] on +; the wire, and its fact arrays project to the coordinate-keyed maps below. + +authority-facts = { + apiVersion: "edict.authority-facts/v1", + source: authority-fact-source, + operationProfiles: { * tstr => authority-operation-profile-fact }, + effectWriteClasses: { * tstr => authority-write-class }, + budgets: { * tstr => authority-budget-fact }, +} + +authority-fact-source = { + kind: "lawpack" / "targetProfile", + coordinate: tstr, + digest: sha256-digest, +} + +; The map key is the source operation-profile coordinate. It is not repeated in +; the value, so a key and embedded coordinate cannot disagree. Allowed write +; classes are a canonical map-set: the class is the unique key and null is the +; unit marker. Canonical CBOR fixes key order without a second ordering rule. +authority-operation-profile-fact = { + core: tstr, + allowedWriteClasses: { * authority-write-class => null }, +} + +; The effectWriteClasses map key is the semantic effect coordinate. The budgets +; map key is the source budget coordinate. Canonical CBOR map-key uniqueness +; makes duplicate fact coordinates structurally unrepresentable. +authority-budget-fact = { + maxSteps: uint, + maxAllocatedBytes: uint, + maxOutputBytes: uint, +} + +; AuthorityFactsDocument v1 intentionally accepts only the write classes the +; current compiler model can consume. `custom` is the sole v1 custom spelling; +; arbitrary target-profile extension strings do not enter this compiler path. +authority-write-class = "none" / "read" / "create" / "ensure" / "append" / + "replace" / "delete" / "custom" + +; --- edict-target-ir.cddl --- +; SPDX-License-Identifier: Apache-2.0 +; edict-target-ir.cddl +; Canonical schema for the Edict-owned Target IR artifact envelope. +; Authoritative byte encoding: edict.canonical-cbor/v1. +; +; This schema is assembled with edict-common.cddl and edict-core.cddl. It +; deliberately reuses Core expressions, predicates, budgets, local references, +; obstruction reasons, and obstruction arms so the schema matches the value +; emitted by the canonical Target IR encoder rather than restating those types. +; It describes the structural shape of valid lowering-produced artifacts. The +; lowering and encoder contracts separately enforce semantic identifier rules +; and canonical ordering/deduplication for set-like values. + +; Target IR encoding rejects an empty target-profile coordinate before bytes +; exist, so this root tightens the shared structural resource-ref accordingly. +target-ir-resource-ref = { + id: tstr .regexp "(?s).+", + digest: sha256-digest, +} + +target-ir-artifact = { + kind: "targetIrArtifact", + domain: tstr, + targetProfile: target-ir-resource-ref, + sourceCoreCoordinate: tstr, + intents: { * tstr => target-ir-intent }, +} + +target-ir-intent = { + operationProfile: tstr, + inputConstraints: [* input-constraint], + coreEvaluationBudget: core-budget, + requirements: [* target-ir-requirement], + steps: [* target-ir-step], + result: core-expr, +} + +target-ir-requirement = { + id: tstr, + predicate: core-predicate, + onFailure: require-failure-arm, +} + +target-ir-step = { + id: tstr, + binding: local-ref, + effect: tstr, + targetIntrinsic: tstr, + input: core-expr, + obstructionFailures: [* failure-ident], + obstructionArms: { * failure-ident => obstruction-arm }, +} diff --git a/schemas/edict-provider/contracts/v1/manifest.json b/schemas/edict-provider/contracts/v1/manifest.json new file mode 100644 index 00000000..fe91f629 --- /dev/null +++ b/schemas/edict-provider/contracts/v1/manifest.json @@ -0,0 +1,80 @@ +{ + "apiVersion": "edict.provider-contract-pack/v1", + "coordinate": "edict.provider-contract-pack.cddl@1", + "license": "Apache-2.0", + "schema": { + "bytesHex": "3b20535044582d4c6963656e73652d4964656e7469666965723a204170616368652d322e300a3b2065646963742d70726f76696465722d636f6e7472616374732e6364646c0a3b2047656e6572617465642066726f6d2045646963742d6f776e65642041424920667261676d656e74732e20444f204e4f5420454449542e0a0a3b202d2d2d2065646963742d636f6d6d6f6e2e6364646c202d2d2d0a3b2065646963742d636f6d6d6f6e2e6364646c0a3b20536861726564204344444c20747970657320666f722074686520456469637420414249732c20646566696e6564204f4e4345206865726520736f20746865792063616e6e6f742064726966740a3b202845444943542d4142492d4e4f4455502d303031292e2054776f2067726f7570733a0a3b2020202d207265736f757263652d7265662c207368613235362d6469676573742c206566666563742d6661696c7572652d626f64792c206566666563742d6b696e642c20617574686f726974792d636c6173732c0a3b2020202020636f72652d747970652d7265663a20617373656d626c656420776974682065646963742d7461726765742d70726f66696c652e6364646c20616e640a3b202020202065646963742d6c61777061636b2e6364646c20627920746865206275696c643b2074686f736520736368656d617320646f206e6f74207265646566696e65207468656d2e0a3b2020202d206f7065726174696f6e2d70726f66696c652c206f707469632d74656d706c6174652c2061706572747572652d726571756972656d656e7420616e6420746865697220726566733a0a3b2020202020636f6e73756d65642062792074686520436f72652f6f70746963206c61796572202865646963742d636f72652e6364646c2920616e64207265666572656e636564206279207468650a3b20202020206c616e67756167652f7461726765742d70726f66696c652073706563732e0a3b20417574686f7269746174697665206279746520656e636f64696e673a2065646963742e63616e6f6e6963616c2d63626f722f76312e0a0a3b2041206e6f726d617469766520737562636f6d706f6e656e74207265666572656e636564206279206964656e7469747920706c7573206469676573742e204d616e696665737473206e657665720a3b20656d626564207468656972206f776e2073656c662d64696765737420696e20746865697220707265696d616765202845444943542d434f52452d53454c46484153482d303031292e0a7265736f757263652d726566203d207b2069643a20747374722c206469676573743a207368613235362d646967657374207d0a0a3b20446967657374732061726520617574686f726974617469766520617320747970656420627974652076616c7565732c206e657665722068657820737472696e67732e20526576696577204a534f4e0a3b2072656e64657273207468697320617320227368613235363a3c3634206c6f77657263617365206865783e22202845444943542d4449474553542d574952452d303031292e0a7368613235362d646967657374203d205b20616c676f726974686d3a2022736861323536222c2062797465733a2062737472202e73697a65203332205d0a0a3b2041206e616d6564206c6f772d6c6576656c206661696c75726520616e206566666563742063616e2072616973652e2054686520736f75726365206f62737472756374696f6e206d61700a3b2062696e64732069742028627920636f6f7264696e6174652920616e6420636f6e73747275637473206120747970656420646f6d61696e206f62737472756374696f6e2066726f6d206974730a3b207061796c6f6164202845444943542d4142492d4641494c5552452d4e414d45442d303031292e0a3b20416e20656666656374277320606566666563744661696c7572657360206c697374204d555354206861766520756e697175652060636f6f7264696e61746560733a2073696e6365207468650a3b206f62737472756374696f6e206d6170206973206b6579656420627920636f6f7264696e6174652c2074776f206661696c757265732073686172696e67206120636f6f7264696e61746520286576656e0a3b207769746820646966666572656e7420617574686f72697479436c6173732f7061796c6f61645479706529206d616b652065786861757374697665206d617070696e6720616e642062696e6465720a3b20747970696e6720616d626967756f757320616e64206172652072656a6563746564202845444943542d4142492d4641494c5552452d554e495155452d303031292e0a3b0a3b2045666665637473206361727279207468656972206661696c757265732061732061206d617020607b206661696c7572652d6964656e74203d3e206566666563742d6661696c7572652d626f6479207d600a3b202873656520746865207461726765742f6c61777061636b2065666665637420736368656d6173292e20546865206661696c75726520636f6f7264696e61746520697320746865206d61700a3b204b45592c20736f206974206973206e6f7420726570656174656420696e2074686520626f647920616e642063616e6e6f74206469736167726565207769746820746865206b65792e0a6566666563742d6661696c7572652d626f6479203d207b0a2020617574686f72697479436c6173733a20617574686f726974792d636c6173732c0a20207061796c6f6164547970653a20636f72652d747970652d7265662c202020202020202020202020203b2074797065642c20626f756e64656420286d617920626520656d707479207265636f7264290a7d0a0a3b2041206661696c75726520636f6f7264696e617465206d7573742062652061206261726520456469637420606964656e746020286c65747465722f756e64657273636f7265207468656e0a3b206c6574746572732f6469676974732f756e64657273636f7265732920414e44206d757374206e6f742062652061207265736572766564206b6579776f72642028652e672e2060656c7365602c0a3b20606261736973602c20607768657265602c206072657175697265602c2060666f72602c2060696660292e2054686520736f75726365206f62737472756374696f6e2d6d6170204c4853206f6e6c790a3b20616363657074732061206e6f6e2d6b6579776f726420606964656e74602c20736f20612068797068656e2f646f742f6b6579776f726420636f6f7264696e61746520776f756c642062650a3b204142492d76616c69642079657420696d706f737369626c6520746f206d617020657868617573746976656c7920696e20736f757263652e20546865207265676578206361707475726573207468650a3b206c65786963616c2073686170653b206b6579776f7264206578636c7573696f6e20697320616e206164646974696f6e616c2076616c69646174696f6e2072756c650a3b202845444943542d4142492d4641494c5552452d4944454e542d303031292e0a6661696c7572652d6964656e74203d2074737472202e72656765787020225b412d5a612d7a5f5d5b412d5a612d7a302d395f5d2a220a0a6566666563742d6b696e64203d20227265616422202f202263726561746522202f2022656e7375726522202f20227265706c61636522202f202264656c65746522202f0a202020202020202020202020202022617070656e6422202f202272656475636522202f202273656d616e7469632e656d697422202f2022637573746f6d220a0a617574686f726974792d636c617373203d2022646f6d61696e4d61707061626c6522202f20227061727469636970616e744f776e656422202f2022696e746567726974794661756c7422202f0a202020202020202020202020202020202020227265736f757263654661756c7422202f2022696e7465726e616c4661756c74220a0a636f72652d747970652d726566203d20747374722020203b2063616e6f6e6963616c20436f7265207479706520636f6f7264696e6174650a0a3b20416e206f7065726174696f6e2070726f66696c6520737570706c69657320746865206f707469632074656d706c617465206120436f726520696e74656e74207265736f6c766573206974730a3b206f707469634b696e642f626f756e646172794b696e642f737570706f7274506f6c6963792f6c6f7373446973706f736974696f6e2066726f6d2e205461726765742070726f66696c657320616e640a3b206c61777061636b73207075626c6973682074686573652061732061206d617020607b20636f6f7264696e617465203d3e206f7065726174696f6e2d70726f66696c65207d602c20736f207468650a3b20636f6f7264696e61746520697320746865204b45592c206e6f7420612076616c7565206669656c64202845444943542d4f505449432d54454d504c4154452d4f574e45522d3030312c0a3b2045444943542d4142492d4f5050524f46494c452d554e495155452d303031292e0a6f7065726174696f6e2d70726f66696c65203d207b0a20206f7074696354656d706c6174653a206f707469632d74656d706c6174652c0a20206566666563745072656469636174653a20747374722c20202020202020202020203b20636f6f7264696e617465206f6620746865206f7065726174696f6e2d6d6f6465207072656469636174650a7d0a0a6f707469632d74656d706c617465203d207b0a20206f707469634b696e643a2022726576656c6174696f6e22202f20226166666563745265696e746567726174696f6e222c0a2020626f756e646172794b696e643a202270726f6a656374696f6e22202f2022616666656374222c0a2020737570706f7274506f6c6963793a20747374722c202020202020202020202020203b2063616e6f6e6963616c20737570706f72742d706f6c69637920636f6f7264696e6174650a20206c6f7373446973706f736974696f6e3a20747374722c20202020202020202020203b2063616e6f6e6963616c206c6f73732d646973706f736974696f6e20636f6f7264696e6174650a20203f20626173697354656d706c6174653a20747374722c20202020202020202020203b206f7074696f6e616c206469676573742d6c6f636b65642062617369732074656d706c61746520636f6f72640a20203b2074686520617065727475726520726571756972656d656e7420746869732074656d706c61746520737570706c6965732e205265717569726564207768656e207468652074656d706c6174650a20203b2069732074686520736f75726365206f66206120436f7265206f707469632773206170657274757265526571756972656d656e742028692e652e2074686520696e74656e7420686173206e6f0a20203b20736f757263652060666f6f747072696e74203c3d202e2e2e60292c2073696e6365206170657274757265526571756972656d656e74206973206d616e6461746f727920696e20436f72650a20203b202845444943542d4f505449432d41504552545552452d5245462d303031292e0a20203f206170657274757265526571756972656d656e743a2061706572747572652d726571756972656d656e742c0a7d0a0a3b206170657274757265526571756972656d656e742069732061207479706564207265666572656e63652c206e65766572206120667265652d666f726d20737472696e672e2041207265766965770a3b2072656e646572696e67206d61792073686f772069747320636f6f7264696e617465202845444943542d4f505449432d41504552545552452d5245462d303031292e0a61706572747572652d726571756972656d656e74203d20666f6f747072696e742d6365696c696e672d726566202f2061627374726163742d666f6f747072696e742d6f626c69676174696f6e2d7265660a666f6f747072696e742d6365696c696e672d726566203d207b206b696e643a2022666f6f747072696e744365696c696e67222c207265663a2074737472207d0a61627374726163742d666f6f747072696e742d6f626c69676174696f6e2d726566203d207b206b696e643a20226162737472616374466f6f747072696e744f626c69676174696f6e222c207265663a2074737472207d0a0a3b202d2d2d2065646963742d636f72652e6364646c202d2d2d0a3b2065646963742d636f72652e6364646c0a3b204e6f726d617469766520736368656d6120666f722074686520456469637420436f72652076312073656d616e746963206d6f64656c2e0a3b0a3b2053636f706520626f756e646172793a20746869732066696c6520646566696e657320436f7265206d65616e696e6720616e6420736368656d61207368617065206f6e6c792e20497420646f65730a3b206e6f7420646566696e6520612063616e6f6e6963616c20656e636f6465722c20436f7265206d6f64756c652068617368206669656c64732c20686173682066697874757265732c207461726765740a3b206c6f776572696e672c2061646d697373696f6e2062756e646c65732c206f72207461726765742d6f776e65642049522e0a0a636f72652d6d6f64756c65203d207b0a202061706956657273696f6e3a202265646963742e636f72652f7631222c0a2020636f6f7264696e6174653a20747374722c0a2020696d706f7274733a205b2a20636f72652d696d706f72745d2c0a202074797065733a207b202a2074737472203d3e20636f72652d74797065207d2c0a2020696e74656e74733a207b202b2074737472203d3e20636f72652d696e74656e74207d2c0a20207265717569726564436f72654361706162696c69746965733a205b2a20747374725d2c0a7d0a0a636f72652d696d706f7274203d207b0a20206b696e643a20226c61777061636b22202f202274617267657422202f2022636f7265222c0a20207265663a207265736f757263652d7265662c0a20203f20616c6961733a20747374722c0a7d0a0a3b202d2d2d207479706573202d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d0a0a636f72652d74797065203d20636f72652d7363616c61722d74797065202f20636f72652d7265636f72642d74797065202f20636f72652d76617269616e742d74797065202f0a202020202020202020202020636f72652d6f7074696f6e2d74797065202f20636f72652d6c6973742d74797065202f20636f72652d6d61702d74797065202f0a202020202020202020202020636f72652d6361706162696c6974792d7265662d747970650a0a636f72652d7363616c61722d74797065203d20636f72652d626f6f6c2d74797065202f20636f72652d696e742d74797065202f20636f72652d737472696e672d74797065202f0a20202020202020202020202020202020202020636f72652d62797465732d74797065202f20636f72652d756e69742d747970650a0a636f72652d626f6f6c2d74797065203d207b206b696e643a2022426f6f6c22207d0a636f72652d756e69742d74797065203d207b206b696e643a2022556e697422207d0a636f72652d696e742d74797065203d207b0a20206b696e643a202249363422202f202255363422202f202249333222202f202255333222202f202249313622202f202255313622202f2022493822202f20225538222c0a7d0a636f72652d737472696e672d74797065203d207b0a20206b696e643a2022537472696e67222c0a20206d61783a2075696e742c0a202063616e6f6e6963616c3a2022756e69636f64652d7363616c61722d6e666322202f20227261772d75746638222c0a7d0a636f72652d62797465732d74797065203d207b0a20206b696e643a20224279746573222c0a20206d61783a2075696e742c0a7d0a636f72652d7265636f72642d74797065203d207b0a20206b696e643a20225265636f7264222c0a20206669656c64733a207b202a2074737472203d3e20636f72652d747970652d726566207d2c0a7d0a636f72652d76617269616e742d74797065203d207b0a20206b696e643a202256617269616e74222c0a202063617365733a207b202b2074737472203d3e2076617269616e742d636173652d626f6479207d2c0a7d0a76617269616e742d636173652d626f6479203d207b0a20203f207061796c6f61643a20636f72652d747970652d7265662c0a7d0a636f72652d6f7074696f6e2d74797065203d207b0a20206b696e643a20224f7074696f6e222c0a20206974656d3a20636f72652d747970652d7265662c0a7d0a636f72652d6c6973742d74797065203d207b0a20206b696e643a20224c697374222c0a20206974656d3a20636f72652d747970652d7265662c0a20206d61783a2075696e742c0a7d0a636f72652d6d61702d74797065203d207b0a20206b696e643a20224d6170222c0a20206b65793a20636f72652d747970652d7265662c0a202076616c75653a20636f72652d747970652d7265662c0a20206d61783a2075696e742c0a7d0a636f72652d6361706162696c6974792d7265662d74797065203d207b0a20206b696e643a20224361706162696c697479526566222c0a20206974656d3a20636f72652d747970652d7265662c0a7d0a0a3b20636f72652d747970652d72656620697320646566696e656420696e2065646963742d636f6d6d6f6e2e6364646c20616e6420617373656d626c65642077697468207468697320736368656d612e0a0a3b202d2d2d207265666572656e63657320616e642076616c756573202d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d0a0a3b204c6f63616c206964656e7469747920697320616c7068612d737461626c652e20606964602069732074686520636f6d70696c65722d6f776e6564206c6f63616c20636f6f7264696e6174653b0a3b2060616c7068614e616d656020697320746865206e6f726d616c697a65642068756d616e2f6465627567206e616d652e20536f757263652062696e646572207370656c6c696e67206973206e6f740a3b206964656e746974792e0a6c6f63616c2d726566203d207b0a202069643a20747374722c0a2020616c7068614e616d653a20747374722c0a2020747970653a20636f72652d747970652d7265662c0a7d0a0a636f72652d76616c7565203d20636f72652d6e756c6c2d76616c7565202f20636f72652d626f6f6c2d76616c7565202f20636f72652d696e742d76616c7565202f0a20202020202020202020202020636f72652d737472696e672d76616c7565202f20636f72652d62797465732d76616c7565202f20636f72652d7265636f72642d76616c7565202f0a20202020202020202020202020636f72652d76617269616e742d76616c7565202f20636f72652d6c6973742d76616c7565202f20636f72652d6d61702d76616c7565202f0a20202020202020202020202020636f72652d6361706162696c6974792d76616c75650a0a636f72652d6e756c6c2d76616c7565203d207b206b696e643a20226e756c6c22207d0a636f72652d626f6f6c2d76616c7565203d207b206b696e643a2022626f6f6c222c2076616c75653a20626f6f6c207d0a636f72652d696e742d76616c7565203d207b206b696e643a2022696e74222c2077696474683a20747374722c2076616c75653a20696e74207d0a636f72652d737472696e672d76616c7565203d207b206b696e643a2022737472696e67222c2076616c75653a2074737472207d0a636f72652d62797465732d76616c7565203d207b206b696e643a20226279746573222c2076616c75653a2062737472207d0a636f72652d7265636f72642d76616c7565203d207b206b696e643a20227265636f7264222c206669656c64733a207b202a2074737472203d3e20636f72652d76616c7565207d207d0a636f72652d76617269616e742d76616c7565203d207b0a20206b696e643a202276617269616e74222c0a2020747970653a20636f72652d747970652d7265662c0a2020636173653a20747374722c0a20203f207061796c6f61643a20636f72652d76616c75652c0a7d0a636f72652d6c6973742d76616c7565203d207b206b696e643a20226c697374222c2076616c7565733a205b2a20636f72652d76616c75655d207d0a636f72652d6d61702d76616c7565203d207b206b696e643a20226d6170222c20656e74726965733a205b2a205b6b65793a20636f72652d76616c75652c2076616c75653a20636f72652d76616c75655d5d207d0a636f72652d6361706162696c6974792d76616c7565203d207b0a20206b696e643a20226361706162696c697479222c0a2020726563656970743a207368613235362d6469676573742c0a7d0a0a3b2045646963742d617574686f72656420707572652068656c70657273207573652061207075726520436f72652066756e6374696f6e20626f64792e2054686520626f64792063616e2062696e640a3b20707572652065787072657373696f6e7320616e642072657475726e20616e2065787072657373696f6e2c206275742069742063616e6e6f7420636f6e7461696e20436f7265206566666563742c0a3b2067756172642c206272616e63682c206c6f6f702c206d617463682d6e6f64652c206f722070726f6f662d6f626c69676174696f6e206e6f6465732e0a636f72652d666e2d626f6479203d207b0a2020706172616d733a205b2a206c6f63616c2d7265665d2c0a2020626f64793a20636f72652d707572652d626c6f636b2c0a7d0a0a636f72652d707572652d626c6f636b203d207b0a20206c6f63616c733a205b2a206c6f63616c2d7265665d2c0a202062696e64696e67733a205b2a20707572652d6c65742d6e6f64655d2c0a2020726573756c743a20636f72652d657870722c0a7d0a0a707572652d6c65742d6e6f6465203d207b0a20206b696e643a20226c6574222c0a202062696e64696e673a206c6f63616c2d7265662c0a202076616c75653a20636f72652d657870722c0a7d0a0a3b202d2d2d2065787072657373696f6e7320616e642070726564696361746573202d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d0a0a636f72652d65787072203d206c6f63616c2d65787072202f20636f6e73742d65787072202f207265636f72642d65787072202f206669656c642d65787072202f0a20202020202020202020202076617269616e742d65787072202f206d617463682d65787072202f2063616c6c2d65787072202f206c6973742d65787072202f206d61702d65787072202f0a20202020202020202020202069662d657870720a0a6c6f63616c2d65787072203d207b206b696e643a20226c6f63616c222c207265663a206c6f63616c2d726566207d0a636f6e73742d65787072203d207b206b696e643a2022636f6e7374222c2076616c75653a20636f72652d76616c7565207d0a7265636f72642d65787072203d207b206b696e643a20227265636f7264222c206669656c64733a207b202a2074737472203d3e20636f72652d65787072207d207d0a6669656c642d65787072203d207b206b696e643a20226669656c64222c20626173653a20636f72652d657870722c206669656c643a2074737472207d0a76617269616e742d65787072203d207b0a20206b696e643a202276617269616e74222c0a2020747970653a20636f72652d747970652d7265662c0a2020636173653a20747374722c0a20203f207061796c6f61643a20636f72652d657870722c0a7d0a6d617463682d65787072203d207b0a20206b696e643a20226d61746368222c0a20207363727574696e65653a20636f72652d657870722c0a202061726d733a205b2b206d617463682d61726d5d2c0a7d0a6d617463682d61726d203d207b0a2020636173653a20747374722c0a20203f2062696e6465723a206c6f63616c2d7265662c0a2020626f64793a20636f72652d657870722c0a7d0a63616c6c2d65787072203d207b0a20206b696e643a202263616c6c222c0a202063616c6c65653a20747374722c0a202074797065417267733a205b2a20636f72652d747970652d7265665d2c0a2020617267733a205b2a20636f72652d657870725d2c0a7d0a6c6973742d65787072203d207b206b696e643a20226c697374222c2076616c7565733a205b2a20636f72652d657870725d207d0a6d61702d65787072203d207b206b696e643a20226d6170222c20656e74726965733a205b2a205b6b65793a20636f72652d657870722c2076616c75653a20636f72652d657870725d5d207d0a69662d65787072203d207b0a20206b696e643a20226966222c0a20207072656469636174653a20636f72652d7072656469636174652c0a20207468656e3a20636f72652d657870722c0a2020656c73653a20636f72652d657870722c0a7d0a0a636f72652d707265646963617465203d20747275652d707265646963617465202f2066616c73652d707265646963617465202f206e6f742d707265646963617465202f0a2020202020202020202020202020202020616c6c2d707265646963617465202f20616e792d707265646963617465202f20636f6d706172652d707265646963617465202f0a202020202020202020202020202020202063616c6c2d707265646963617465202f206f62737472756374696f6e2d7072656469636174650a0a747275652d707265646963617465203d207b206b696e643a20227472756522207d0a66616c73652d707265646963617465203d207b206b696e643a202266616c736522207d0a6e6f742d707265646963617465203d207b206b696e643a20226e6f74222c2076616c75653a20636f72652d707265646963617465207d0a616c6c2d707265646963617465203d207b206b696e643a2022616c6c222c2076616c7565733a205b2b20636f72652d7072656469636174655d207d0a616e792d707265646963617465203d207b206b696e643a2022616e79222c2076616c7565733a205b2b20636f72652d7072656469636174655d207d0a636f6d706172652d707265646963617465203d207b0a20206b696e643a2022636f6d70617265222c0a20206f703a20223d3d22202f2022213d22202f20223c22202f20223c3d22202f20223e22202f20223e3d222c0a20206c6566743a20636f72652d657870722c0a202072696768743a20636f72652d657870722c0a7d0a63616c6c2d707265646963617465203d207b0a20206b696e643a202263616c6c222c0a20207072656469636174653a20747374722c0a2020617267733a205b2a20636f72652d657870725d2c0a7d0a6f62737472756374696f6e2d707265646963617465203d207b0a20206b696e643a20226f62737472756374696f6e222c0a2020636f6f7264696e6174653a206661696c7572652d6964656e742c0a20207061796c6f61643a20636f72652d657870722c0a7d0a0a696e7075742d636f6e73747261696e74203d207b0a2020636f6f7264696e6174653a20747374722c0a2020736f757263653a2022776865726522202f2022636f6d70696c6572222c0a20207072656469636174653a20636f72652d7072656469636174652c0a7d0a0a3b202d2d2d20696e74656e74732c20626c6f636b732c20616e64206e6f646573202d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d0a0a636f72652d696e74656e74203d207b0a2020696e7075743a20636f72652d747970652d7265662c0a20206f75747075743a20636f72652d747970652d7265662c0a202072657175697265644f7065726174696f6e50726f66696c653a20747374722c0a2020696e707574436f6e73747261696e74733a205b2a20696e7075742d636f6e73747261696e745d2c0a2020636f72654576616c756174696f6e4275646765743a20636f72652d6275646765742c0a2020626f64793a20636f72652d626c6f636b2c0a20203f206f707469633a20636f72652d6f707469632c0a7d0a0a636f72652d627564676574203d207b0a20206d617853746570733a2075696e742c0a20206d6178416c6c6f636174656442797465733a2075696e742c0a20206d61784f757470757442797465733a2075696e742c0a7d0a0a636f72652d6f70746963203d207b0a20206f707469634b696e643a2022726576656c6174696f6e22202f20226166666563745265696e746567726174696f6e222c0a2020626f756e646172794b696e643a202270726f6a656374696f6e22202f2022616666656374222c0a20206170657274757265526571756972656d656e743a2061706572747572652d726571756972656d656e742c0a2020737570706f7274506f6c6963793a20747374722c0a20206c6f7373446973706f736974696f6e3a20747374722c0a7d0a0a636f72652d626c6f636b203d207b0a20206c6f63616c733a205b2a206c6f63616c2d7265665d2c0a20206e6f6465733a205b2a20636f72652d6e6f64655d2c0a2020726573756c743a20636f72652d657870722c0a7d0a0a636f72652d6e6f6465203d206c65742d6e6f6465202f20726571756972652d6e6f6465202f206566666563742d6e6f6465202f2067756172642d6e6f6465202f206272616e63682d6e6f6465202f0a202020202020202020202020666f722d6e6f6465202f206d617463682d6e6f6465202f2070726f6f662d6f626c69676174696f6e2d6e6f64650a0a6c65742d6e6f6465203d207b0a20206b696e643a20226c6574222c0a202062696e64696e673a206c6f63616c2d7265662c0a202076616c75653a20636f72652d657870722c0a7d0a726571756972652d6e6f6465203d207b0a20206b696e643a202272657175697265222c0a20207072656469636174653a20636f72652d7072656469636174652c0a20206f6e4661696c7572653a20726571756972652d6661696c7572652d61726d2c0a7d0a726571756972652d6661696c7572652d61726d203d207465726d696e616c2d726571756972652d6661696c757265202f0a20202020202020202020202020202020202020202020636f6e74696e75652d6f6273747275637465642d726571756972652d6661696c7572650a7465726d696e616c2d726571756972652d6661696c757265203d207b0a20206b696e643a20227465726d696e616c222c0a2020726561736f6e3a206f62737472756374696f6e2d726561736f6e2c0a7d0a636f6e74696e75652d6f6273747275637465642d726571756972652d6661696c757265203d207b0a20206b696e643a2022636f6e74696e75654f627374727563746564222c0a2020726561736f6e3a206f62737472756374696f6e2d726561736f6e2c0a7d0a6f62737472756374696f6e2d726561736f6e203d207b0a2020726561736f6e4b696e643a20747374722c0a20207061796c6f61643a207b202a2074737472203d3e20636f72652d65787072207d2c0a7d0a6566666563742d6e6f6465203d207b0a20206b696e643a2022656666656374222c0a202062696e64696e673a206c6f63616c2d7265662c0a20206566666563743a20747374722c0a2020696e7075743a20636f72652d657870722c0a20206f62737472756374696f6e4d61703a207b202a206661696c7572652d6964656e74203d3e206f62737472756374696f6e2d61726d207d2c0a7d0a6f62737472756374696f6e2d61726d203d207b0a202062696e6465723a206c6f63616c2d7265662c0a202076616c75653a20636f72652d657870722c0a7d0a67756172642d6e6f6465203d207b0a20206b696e643a20226775617264222c0a20207072656469636174653a20636f72652d7072656469636174652c0a20206f62737472756374696f6e3a20636f72652d657870722c0a7d0a6272616e63682d6e6f6465203d207b0a20206b696e643a20226272616e6368222c0a20207072656469636174653a20636f72652d7072656469636174652c0a20207468656e3a20636f72652d626c6f636b2c0a2020656c73653a20636f72652d626c6f636b2c0a7d0a666f722d6e6f6465203d207b0a20206b696e643a2022666f72222c0a202062696e6465723a206c6f63616c2d7265662c0a2020697465723a20636f72652d657870722c0a2020626f756e643a20636f72652d626f756e642c0a2020626f64793a20636f72652d626c6f636b2c0a7d0a6d617463682d6e6f6465203d207b0a20206b696e643a20226d61746368222c0a20207363727574696e65653a20636f72652d657870722c0a202061726d733a205b2b206d617463682d626c6f636b2d61726d5d2c0a7d0a6d617463682d626c6f636b2d61726d203d207b0a2020636173653a20747374722c0a20203f2062696e6465723a206c6f63616c2d7265662c0a2020626f64793a20636f72652d626c6f636b2c0a7d0a70726f6f662d6f626c69676174696f6e2d6e6f6465203d207b0a20206b696e643a202270726f6f66222c0a2020636f6f7264696e6174653a20747374722c0a20207072656469636174653a20636f72652d7072656469636174652c0a7d0a0a636f72652d626f756e64203d206c69746572616c2d626f756e64202f20636f6f7264696e6174652d626f756e640a6c69746572616c2d626f756e64203d207b206b696e643a20226c69746572616c222c2076616c75653a2075696e74207d0a636f6f7264696e6174652d626f756e64203d207b206b696e643a2022636f6f7264696e617465222c207265663a2074737472207d0a0a3b20536861726564207265736f757263652d7265662c207368613235362d6469676573742c206661696c7572652d6964656e742c2061706572747572652d726571756972656d656e742c20616e640a3b20636f72652d747970652d7265662061726520646566696e6564206f6e636520696e2065646963742d636f6d6d6f6e2e6364646c2e0a0a3b202d2d2d2065646963742d6c61777061636b2e6364646c202d2d2d0a3b2065646963742d6c61777061636b2e6364646c0a3b2043616e6f6e6963616c20736368656d6120666f7220746865204564696374206c61777061636b206d616e696665737420616e64206578706f727420737572666163652e0a3b20417574686f7269746174697665206279746520656e636f64696e673a2065646963742e63616e6f6e6963616c2d63626f722f76312e204a534f4e20696e207468652070726f73652073706563730a3b2069732061207265766965772072656e646572696e672067656e6572617465642066726f6d207468697320736368656d61202845444943542d4142492d4e4f4455502d303031292e0a0a6c61777061636b2d6d616e6966657374203d207b0a202061706956657273696f6e3a202265646963742e6c61777061636b2f7631222c0a202069643a20747374722c0a202076657273696f6e3a20747374722c0a20206163636570746564436f72654162693a205b2b20747374725d2c0a2020646570656e64656e636965733a205b2a206c61777061636b2d6465705d2c202020202020202020203b20616379636c69632c206469676573742d6c6f636b6564202845444943542d4c41575041434b2d4441472d303031290a20206578706f7274733a207265736f757263652d7265662c0a20203f2074617267657441646170746572733a205b2b207461726765742d616461707465725d2c2020203b207265717569726564206f6e6c7920696620616e792072756e74696d6520656666656374206578697374730a20203f2068656c706572436f6d706f6e656e743a2065786563757461626c652d636f6d706f6e656e742c203b2065786563757461626c652068656c70657273206361727279207468656972206f776e2073616e64626f782b6675656c0a202076657269666965723a2076657269666965722c202020202020202020202020202020202020202020203b20636c61737369666965643a206465636c61726174697665206f722065786563757461626c650a2020636f6d7061746962696c6974793a207265736f757263652d7265662c0a2020636f6e666f726d616e636546697874757265436f727075733a207265736f757263652d7265662c0a7d0a0a3b2041207665726966696572206973206569746865722061206465636c617261746976652072756c6573657420286e6f2072756e74696d6529206f7220616e2065786563757461626c650a3b20636f6d706f6e656e742e20416e2065786563757461626c65207665726966696572204d55535420636172727920697473206f776e2073616e64626f7820616e64206675656c206d6f64656c2c0a3b20736f2074686520736368656d6120656e666f726365732074686174206e6f2065786563757461626c6520636f6d706f6e656e74206973206c65667420756e626f756e6465640a3b202845444943542d4142492d56455249464945522d424f554e442d303031292e0a7665726966696572203d206465636c617261746976652d7665726966696572202f2065786563757461626c652d76657269666965720a6465636c617261746976652d7665726966696572203d207b20636c6173733a20226465636c61726174697665222c2072756c657365743a207265736f757263652d726566207d0a65786563757461626c652d7665726966696572203d207b0a2020636c6173733a202265786563757461626c65222c0a2020636f6d706f6e656e743a207265736f757263652d7265662c0a202073616e64626f783a207265736f757263652d7265662c0a20206675656c4d6f64656c3a207265736f757263652d7265662c0a7d0a0a3b20416e792065786563757461626c6520636f6d706f6e656e7420697320626f756e64656420627920697473206f776e2073616e64626f78202b206675656c206d6f64656c2e0a65786563757461626c652d636f6d706f6e656e74203d207b0a2020636f6d706f6e656e743a207265736f757263652d7265662c0a202073616e64626f783a207265736f757263652d7265662c0a20206675656c4d6f64656c3a207265736f757263652d7265662c0a7d0a0a6c61777061636b2d646570203d207b2069643a20747374722c2076657273696f6e3a20747374722c206469676573743a207368613235362d646967657374207d0a0a3b20416461707465722073656c656374696f6e206b65797320534f4c454c59206f666620746865206469676573742d6c6f636b65642060616363657074656454617267657450726f66696c65600a3b20286974732060696460206973207468652070726f66696c652069643b206974732060646967657374602070696e73207468652065786163742070726f66696c652f76657273696f6e292e2054686572650a3b20617265206e6f20696e646570656e64656e7420646973706c617920737472696e6773207468617420636f756c64206469736167726565207769746820746865206c6f636b2c20736f20610a3b207265736f6c7665722063616e6e6f742062696e6420616e206164617074657220746f206f6e6520746172676574207768696c6520746865206c6f636b2070726f76657320616e6f746865720a3b202845444943542d4c41575041434b2d414441505445522d54415247455449522d303031292e0a7461726765742d61646170746572203d207b0a2020616363657074656454617267657450726f66696c653a207265736f757263652d7265662c202020203b206469676573742d6c6f636b65642c20617574686f72697461746976652073656c6563746f720a2020616363657074656454617267657449723a207265736f757263652d7265662c2020202020202020203b206469676573742d6c6f636b65640a2020616461707465723a207265736f757263652d7265662c0a7d0a0a3b20536861726564207479706573207265736f757263652d72656620616e64207368613235362d6469676573742061726520646566696e6564206f6e636520696e0a3b2065646963742d636f6d6d6f6e2e6364646c20616e6420617373656d626c65642077697468207468697320736368656d61202845444943542d4142492d4e4f4455502d303031292e0a0a3b202d2d2d206578706f72742073757266616365202d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d0a0a6c61777061636b2d6578706f727473203d207b0a202074797065733a205b2a206578706f727465642d747970655d2c0a2020636f6e7374616e74733a205b2a206578706f727465642d636f6e7374616e745d2c0a20207075726546756e6374696f6e733a205b2a20707572652d66756e6374696f6e5d2c0a2020656666656374733a205b2a2073656d616e7469632d6566666563745d2c0a20206f62737472756374696f6e733a205b2a206f62737472756374696f6e2d6465665d2c0a20203b206b65796564206279206f7065726174696f6e2d70726f66696c6520636f6f7264696e61746520e2869220756e697175656e65737320656e666f726365640a20203b202845444943542d4142492d4f5050524f46494c452d554e495155452d303031290a20203b206f7065726174696f6e2d70726f66696c65207265636f7264732074686973206c61777061636b206578706f72747320286f707469632074656d706c6174657320746861740a20203b2060696d706c656d656e7473602f6070726f66696c656020636c6175736573207265736f6c766520616761696e7374292e206f7065726174696f6e2d70726f66696c652069730a20203b20646566696e656420696e2065646963742d636f6d6d6f6e2e6364646c202845444943542d4142492d4f5050524f46494c452d534c4f542d303031292e0a20206f7065726174696f6e50726f66696c65733a207b202a2074737472203d3e206f7065726174696f6e2d70726f66696c65207d2c20203b206b6579656420627920636f6f7264696e6174650a7d0a0a6578706f727465642d7479706520202020203d207b20636f6f7264696e6174653a20747374722c20646566696e6974696f6e3a20636f72652d747970652d726566207d0a6578706f727465642d636f6e7374616e74203d207b20636f6f7264696e6174653a20747374722c20747970653a20636f72652d747970652d7265662c2076616c75653a20616e79207d0a0a3b204120707572652068656c7065722069732061206469736372696d696e6174656420756e696f6e2062792060736f75726365602c20736f2074686520736368656d6120697473656c660a3b2067756172616e7465657320616e20696d706c656d656e746174696f6e20657869737473202845444943542d4c41575041434b2d505552452d494d504c2d303031293a0a3b2020202d20226564696374223a20617574686f72656420696e2045646963742f436f72653b2074686520436f726520626f6479206973206361727269656420696e6c696e6520286861736865640a3b20202020207769746820746865206578706f72742073757266616365292e2054686520736368656d61207265717569726573207468652060626f647960206669656c642e0a3b2020202d2022636f6d706f6e656e74223a20696d706c656d656e746564206f7574736964652045646963743b2063617272696573206e6f20696e6c696e6520626f647920616e6420696e73746561640a3b20202020206361727269657320697473206f776e206469676573742d6c6f636b65642060696d706c656d656e746174696f6e60202873616e64626f78202b206675656c292e20497420646f65730a3b20202020206e6f7420646570656e64206f6e20746865206f7074696f6e616c206d616e69666573742d6c6576656c2068656c706572436f6d706f6e656e742e0a707572652d66756e6374696f6e203d2065646963742d707572652d66756e6374696f6e202f20636f6d706f6e656e742d707572652d66756e6374696f6e0a0a707572652d66756e6374696f6e2d636f6d6d6f6e203d20280a2020636f6f7264696e6174653a20747374722c0a202074797065506172616d65746572733a205b2a20747374725d2c0a2020706172616d6574657254797065733a205b2a20636f72652d747970652d7265665d2c2020202020203b20616c6c20626f756e6465640a202072657475726e547970653a20636f72652d747970652d7265662c20202020202020202020202020203b20626f756e6465640a2020636f737454656d706c6174653a20747374722c0a202064657465726d696e69736d436c6173733a2022746f74616c22202f2022746f74616c2d776974682d74797065642d646961676e6f73746963222c0a290a0a65646963742d707572652d66756e6374696f6e203d207b0a2020707572652d66756e6374696f6e2d636f6d6d6f6e2c0a2020736f757263653a20226564696374222c0a2020626f64793a20636f72652d666e2d626f64792c2020202020202020202020202020202020202020203b20696e6c696e652c20686173682d7369676e69666963616e740a7d0a0a636f6d706f6e656e742d707572652d66756e6374696f6e203d207b0a2020707572652d66756e6374696f6e2d636f6d6d6f6e2c0a2020736f757263653a2022636f6d706f6e656e74222c0a20203b20746865206469676573742d6c6f636b656420636f6d706f6e656e7420696d706c656d656e74696e6720746869732068656c7065722e2052657175697265642061742074686520736368656d610a20203b206c6576656c20736f206120636f6d706f6e656e742068656c7065722063616e206e657665722076616c696461746520776974686f7574206120686173682d626f756e642c0a20203b2073616e64626f782b6675656c2d64657363726962656420696d706c656d656e746174696f6e202845444943542d4c41575041434b2d505552452d494d504c2d303031292e0a2020696d706c656d656e746174696f6e3a2065786563757461626c652d636f6d706f6e656e742c0a7d0a0a3b20636f72652d666e2d626f647920697320646566696e65642062792065646963742d636f72652e6364646c20616e6420617373656d626c656420776974682074686973206c61777061636b0a3b20736368656d612e2049742069732061207075726520436f72652066756e6374696f6e20626f64792c206e6f7420616e206566666563742d63617061626c6520636f72652d626c6f636b2e0a0a73656d616e7469632d656666656374203d207b0a2020636f6f7264696e6174653a20747374722c0a202074797065506172616d65746572733a205b2a20747374725d2c0a2020696e707574547970653a20636f72652d747970652d7265662c2020202020202020202020202020203b20626f756e6465640a20206f7574707574547970653a20636f72652d747970652d7265662c20202020202020202020202020203b20626f756e6465640a2020657865637574696f6e436c6173733a202270726f6f664f6e6c7922202f202272756e74696d65222c2020203b206f7274686f676f6e616c20746f207772697465436c6173730a20206566666563744b696e6448696e743a206566666563742d6b696e642c0a2020666f6f747072696e744f626c69676174696f6e3a20747374722c0a2020636f73744f626c69676174696f6e3a20747374722c0a20206566666563744661696c757265733a207b202a206661696c7572652d6964656e74203d3e206566666563742d6661696c7572652d626f6479207d2c20203b206b6579656420627920636f6f7264696e6174653b20756e697175650a20206775617264537570706f72743a20626f6f6c2c0a7d0a0a6f62737472756374696f6e2d646566203d207b0a2020636f6f7264696e6174653a20747374722c0a2020617574686f72697479436c6173733a20617574686f726974792d636c6173732c0a20207061796c6f6164536368656d613a20636f72652d747970652d7265662c20202020202020202020203b2074797065642c20626f756e64656420286d617920626520656d707479207265636f7264290a7d0a0a3b206566666563742d6661696c7572652d626f64792c206566666563742d6b696e642c20617574686f726974792d636c6173732c20616e6420636f72652d747970652d7265662061726520646566696e65640a3b206f6e636520696e2065646963742d636f6d6d6f6e2e6364646c20616e6420617373656d626c65642077697468207468697320736368656d61202845444943542d4142492d4e4f4455502d303031292e0a0a3b202d2d2d2065646963742d7461726765742d70726f66696c652e6364646c202d2d2d0a3b2065646963742d7461726765742d70726f66696c652e6364646c0a3b2043616e6f6e6963616c20736368656d6120666f7220746865204564696374207461726765742070726f66696c65206d616e69666573742e0a3b20417574686f7269746174697665206279746520656e636f64696e673a2065646963742e63616e6f6e6963616c2d63626f722f76310a3b202873656520535045435f636f6e74696e75756d2d636f6e74726163742d62756e646c652d76312e6d64292e204a534f4e20696e207468652070726f736520737065637320697320610a3b207265766965772072656e646572696e672067656e6572617465642066726f6d207468697320736368656d613b2074686973204344444c206973207468652073696e676c6520736f757263650a3b206f66207472757468202845444943542d4142492d4e4f4455502d303031292e0a0a7461726765742d70726f66696c652d6d616e6966657374203d207b0a202061706956657273696f6e3a202265646963742e7461726765742d70726f66696c652f7631222c0a202069643a20747374722c202020202020202020202020202020202020202020202020203b20652e672e20226563686f2e64706f220a202076657273696f6e3a20747374722c20202020202020202020202020202020202020203b20652e672e202231220a20206163636570746564436f72654162693a205b2b20747374725d2c20202020202020203b20652e672e205b2265646963742e636f72652f7631225d0a0a2020696e7472696e736963733a207265736f757263652d7265662c0a2020696e7472696e7369634e616d6573706163653a20747374722c0a20203b207075626c697368657320746869732070726f66696c652773206f7065726174696f6e2d70726f66696c65207265636f72647320286f707469632074656d706c6174657320746861740a20203b206070726f66696c65602f60696d706c656d656e74736020636c6175736573207265736f6c766520616761696e7374292e205265666572656e63657320616e0a20203b206f7065726174696f6e2d70726f66696c65732d646f63756d656e74202845444943542d4142492d4f5050524f46494c452d534c4f542d303031292e0a20206f7065726174696f6e50726f66696c65733a207265736f757263652d7265662c0a2020666f6f747072696e74416c67656272613a207265736f757263652d7265662c0a2020636f7374416c67656272613a207265736f757263652d7265662c0a202074617267657449723a207265736f757263652d7265662c0a20206f62737472756374696f6e5461786f6e6f6d793a207265736f757263652d7265662c0a202076657269666965723a207265736f757263652d7265662c0a20206c6f77657265723a207265736f757263652d7265662c0a202073616e64626f783a207265736f757263652d7265662c0a20206675656c4d6f64656c3a207265736f757263652d7265662c0a0a20203b206669656c647320746865206c616e67756167652073706563207265717569726573206f662065766572792070726f66696c650a202062756e646c6550726f66696c653a207265736f757263652d7265662c0a202067656e657261746564417274696661637450726f66696c65733a205b2a207265736f757263652d7265665d2c0a202063616e6f6e6963616c456e636f64696e6752756c65733a207265736f757263652d7265662c0a20203b2052657365727665642f64656665727265643a2077696c6c206c697374206163636570746564206c61777061636b2d616461707465722041424920696473206f6e636520746861740a20203b20627974652d6c6576656c20414249202865646963742e6c61777061636b2d616461707465722f763129206973207370656369666965642e2054686520736368656d6120656e666f726365730a20203b20656d7074696e657373206e6f7720e280942074686520747970652069732074686520656d7074792061727261792c20736f206e6f6e2d656d7074792076616c756573206172650a20203b2072656a65637465642c206e6f74206d6572656c7920646973636f757261676564202845444943542d4142492d4c41575041434b2d414441505445522d44454645522d303031292e0a20203f2061636365707465644c61777061636b416461707465724162693a205b5d2c0a2020646961676e6f737469634162693a207265736f757263652d7265662c0a0a20203b206170706c69636174696f6e20646f637472696e650a20206170706c69636174696f6e4d6f64656c3a202261746f6d6963222c0a202072656164436f6e73697374656e63793a20226170706c69636174696f6e2d736e617073686f7422202f20747374722c0a202067756172644576616c756174696f6e3a2022707265636f6d6d69742d61746f6d696322202f20747374722c0a20206f62737472756374696f6e526f6c6c6261636b3a20226e6f2d76697369626c652d6566666563747322202f20747374722c0a20206d756c74695461726765743a20626f6f6c2c0a20203b207768657468657220746865207461726765742063616e206576616c7561746520707265636f6d6d697420706f7374636f6e646974696f6e20286067756172616e746565602920636865636b730a20203b20696e73696465207468652061746f6d6963206170706c69636174696f6e20756e6974202845444943542d5441524745542d504f5354434f4e442d303031290a2020706f7374636f6e646974696f6e537570706f72743a20626f6f6c2c0a0a202064657465726d696e6973746963457865637574696f6e3a207265736f757263652d7265662c0a2020636f6e666f726d616e636546697874757265436f727075733a207265736f757263652d7265662c0a7d0a0a3b20536861726564207479706573207265736f757263652d72656620616e64207368613235362d6469676573742061726520646566696e6564206f6e636520696e0a3b2065646963742d636f6d6d6f6e2e6364646c20616e6420617373656d626c65642077697468207468697320736368656d6120627920746865206275696c640a3b202845444943542d4142492d4e4f4455502d303031292e205468657920617265206e6f74207265646566696e656420686572652e0a0a3b202d2d2d20696e7472696e736963207369676e6174757265202d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d0a3b20546865206172746966616374207265666572656e63656420627920746865206d616e696665737427732060696e7472696e7369637360207265736f757263652d726566206973207468650a3b20696e7472696e7369632d7369676e617475726520636f7270757320646f63756d656e742062656c6f772e20497473206c61796f757420697320666978656420736f2074776f0a3b20696e646570656e64656e742070726f66696c65732076616c69646174652f686173682074686520636f72707573206964656e746963616c6c790a3b202845444943542d4142492d494e5452494e534943532d444f432d303031292e0a0a3b20696e7472696e736963732069732061204d4150206b6579656420627920636f6f7264696e6174652c20736f2074686520736368656d6120697473656c6620656e666f726365730a3b20636f6f7264696e61746520756e697175656e6573732e20412070726f766964657220726563656976657320746865207265736f6c76656420636f7270757320617320610a3b206469676573742d626f756e642073656d616e74696320696e70757420616e64207265736f6c76657320636f6f7264696e617465732077697468696e20746861742061727469666163742e0a3b2045616368206d6170206b6579204d55535420657175616c20697473207265636f726427732060636f6f7264696e61746560206669656c640a3b202845444943542d4142492d494e5452494e5349432d554e495155452d303031292e0a696e7472696e736963732d646f63756d656e74203d207b0a202061706956657273696f6e3a202265646963742e7461726765742d70726f66696c652e696e7472696e736963732f7631222c0a2020696e7472696e736963733a207b202a2074737472203d3e20696e7472696e736963207d2c0a7d0a0a3b20546865206172746966616374207265666572656e63656420627920746865206d616e6966657374277320606f7065726174696f6e50726f66696c657360207265736f757263652d7265662e0a3b206f7065726174696f6e2d70726f66696c65202f206f707469632d74656d706c6174652061726520646566696e656420696e2065646963742d636f6d6d6f6e2e6364646c2e204b657965642062790a3b20636f6f7264696e61746520736f207265736f6c7574696f6e2063616e2774207069636b206265747765656e2074776f2073616d652d636f6f7264696e6174652070726f66696c65730a3b202845444943542d4142492d4f5050524f46494c452d534c4f542d3030312c2045444943542d4142492d4f5050524f46494c452d554e495155452d303031292e0a6f7065726174696f6e2d70726f66696c65732d646f63756d656e74203d207b0a202061706956657273696f6e3a202265646963742e7461726765742d70726f66696c652e6f7065726174696f6e2d70726f66696c65732f7631222c0a202070726f66696c65733a207b202a2074737472203d3e206f7065726174696f6e2d70726f66696c65207d2c0a7d0a0a3b2041207479706564207072652d6c6f776572696e67207175657374696f6e20746861742063616e2062652070726f706f73656420627920576174736f6e206f7220616e206167656e7420616e640a3b20636865636b65642062792074686520636f6d70696c65722e2049742069732063616e6f6e6963616c2d43424f5220656e636f64656420756e6465720a3b206065646963742e6c6f776572696e672d726571756972656d656e74732f7631603b2074686520636f6d70696c657220636865636b7320746869732061727469666163742c206e6f74207468650a3b2070726f736520746861742070726f64756365642069742e0a6c6f776572696e672d726571756972656d656e7473203d207b0a202061706956657273696f6e3a202265646963742e6c6f776572696e672d726571756972656d656e74732f7631222c0a20206f7065726174696f6e50726f66696c653a20747374722c0a202073656d616e746963456666656374733a205b2a2073656d616e7469632d6566666563742d726571756972656d656e745d2c0a202072657175697265645772697465436c61737365733a205b2a2077726974652d636c6173735d2c0a202067756172644b696e64733a205b2a2067756172642d6b696e645d2c0a202061746f6d69636974793a2061746f6d69636974792d726571756972656d656e742c0a2020706f7374636f6e646974696f6e537570706f72743a20626f6f6c2c0a20206f62737472756374696f6e436f6f7264696e617465733a205b2a20747374725d2c0a2020666f6f747072696e744f626c69676174696f6e733a205b2a20747374725d2c0a2020636f73744f626c69676174696f6e733a205b2a20747374725d2c0a20206f70746963436f6e74726163743a20747374722c0a7d0a0a73656d616e7469632d6566666563742d726571756972656d656e74203d207b0a2020636f6f7264696e6174653a20747374722c0a20207772697465436c6173733a2077726974652d636c6173732c0a202067756172644b696e64733a205b2a2067756172642d6b696e645d2c0a20206f62737472756374696f6e436f6f7264696e617465733a205b2a20747374725d2c0a2020666f6f747072696e744f626c69676174696f6e733a205b2a20747374725d2c0a2020636f73744f626c69676174696f6e733a205b2a20747374725d2c0a7d0a0a77726974652d636c617373203d20226e6f6e6522202f20227265616422202f202263726561746522202f2022656e7375726522202f2022617070656e6422202f0a2020202020202020202020202020227265706c61636522202f202264656c65746522202f20747374720a67756172642d6b696e64203d2022707265636f6d6d69742d61746f6d696322202f20747374720a61746f6d69636974792d726571756972656d656e74203d202261746f6d696322202f20747374720a0a3b20412067656e75696e6520756e696f6e3a207075726520636f6e7374727563746f7273206361727279206e6f20656666656374206b696e64206f72206661696c757265733b206566666563740a3b20696e7472696e73696373206d757374202845444943542d5441524745542d494e5452494e5349432d434c4153532d303031292e2054686520736368656d6120656e666f7263657320746869732c0a3b206e6f74206120636f6d6d656e742e0a0a3b2054686520696e7472696e736963277320636f6f7264696e6174652069732074686520696e7472696e73696373206d6170204b45592c206e6f7420612076616c7565206669656c642c20736f207468650a3b206b657920616e6420636f6f7264696e6174652063616e206e65766572206469736167726565202845444943542d4142492d494e5452494e5349432d554e495155452d303031292e0a696e7472696e736963203d20707572652d696e7472696e736963202f206566666563742d696e7472696e7369630a0a707572652d696e7472696e736963203d207b0a2020696e7472696e736963436c6173733a202270757265222c0a202074797065506172616d65746572733a205b2a20747374725d2c0a2020617267756d656e7454797065733a205b2a20636f72652d747970652d7265665d2c0a202072657475726e547970653a20636f72652d747970652d7265662c0a20206775617264537570706f72743a2066616c73652c0a2020666f6f747072696e7454656d706c6174653a20747374722c0a2020636f737454656d706c6174653a20747374722c0a20207772697465436c6173733a20226e6f6e65222c0a7d0a0a6566666563742d696e7472696e736963203d207b0a2020696e7472696e736963436c6173733a2022656666656374222c0a202074797065506172616d65746572733a205b2a20747374725d2c0a2020617267756d656e7454797065733a205b2a20636f72652d747970652d7265665d2c0a202072657475726e547970653a20636f72652d747970652d7265662c0a20206566666563744b696e643a206566666563742d6b696e642c0a20203b206d6170206b65796564206279206661696c75726520636f6f7264696e61746520286661696c7572652d6964656e74293b20746865206661696c75726520636f6f7264696e6174652069730a20203b20746865206b65792c206e6f7420612076616c7565206669656c642c20736f20756e697175656e657373206973207374727563747572616c0a20203b202845444943542d4142492d4641494c5552452d554e495155452d303031292e0a20206566666563744661696c757265733a207b202a206661696c7572652d6964656e74203d3e206566666563742d6661696c7572652d626f6479207d2c0a20206775617264537570706f72743a20626f6f6c2c0a2020666f6f747072696e7454656d706c6174653a20747374722c0a2020636f737454656d706c6174653a20747374722c0a20207772697465436c6173733a20227265616422202f202263726561746522202f2022656e7375726522202f2022617070656e6422202f20227265706c61636522202f0a20202020202020202020202020202264656c65746522202f2022637573746f6d222c0a202063616e5061727469636970617465496e41746f6d696347756172643a20626f6f6c2c0a7d0a0a3b206566666563742d6661696c7572652d626f64792c206566666563742d6b696e642c20617574686f726974792d636c6173732c20616e6420636f72652d747970652d7265662061726520646566696e65640a3b206f6e636520696e2065646963742d636f6d6d6f6e2e6364646c20616e6420617373656d626c65642077697468207468697320736368656d61202845444943542d4142492d4e4f4455502d303031292e0a0a3b202d2d2d2065646963742d617574686f726974792d66616374732e6364646c202d2d2d0a3b2065646963742d617574686f726974792d66616374732e6364646c0a3b2043616e6f6e6963616c20736368656d6120666f722074686520666972737420636f6d70696c65722d636f6e7465787420617574686f726974792d666163747320646f63756d656e742e0a3b20417574686f7269746174697665206279746520656e636f64696e673a2065646963742e63616e6f6e6963616c2d63626f722f76312e0a3b0a3b205468697320736368656d6120697320617373656d626c656420776974682065646963742d636f6d6d6f6e2e6364646c20736f20736f757263652e6469676573742075736573207468650a3b20736861726564207368613235362d6469676573742074797065642076616c75652e204a534f4e2069732061207265766965772f696e7075742072656e646572696e673a206974730a3b20607368613235363a3c3634206865783e6020736f75726365206469676573742069732070726f6a656374656420746f205b60736861323536602c203332207261772062797465735d206f6e0a3b2074686520776972652c20616e64206974732066616374206172726179732070726f6a65637420746f2074686520636f6f7264696e6174652d6b65796564206d6170732062656c6f772e0a0a617574686f726974792d6661637473203d207b0a202061706956657273696f6e3a202265646963742e617574686f726974792d66616374732f7631222c0a2020736f757263653a20617574686f726974792d666163742d736f757263652c0a20206f7065726174696f6e50726f66696c65733a207b202a2074737472203d3e20617574686f726974792d6f7065726174696f6e2d70726f66696c652d66616374207d2c0a20206566666563745772697465436c61737365733a207b202a2074737472203d3e20617574686f726974792d77726974652d636c617373207d2c0a2020627564676574733a207b202a2074737472203d3e20617574686f726974792d6275646765742d66616374207d2c0a7d0a0a617574686f726974792d666163742d736f75726365203d207b0a20206b696e643a20226c61777061636b22202f202274617267657450726f66696c65222c0a2020636f6f7264696e6174653a20747374722c0a20206469676573743a207368613235362d6469676573742c0a7d0a0a3b20546865206d6170206b65792069732074686520736f75726365206f7065726174696f6e2d70726f66696c6520636f6f7264696e6174652e204974206973206e6f7420726570656174656420696e0a3b207468652076616c75652c20736f2061206b657920616e6420656d62656464656420636f6f7264696e6174652063616e6e6f742064697361677265652e20416c6c6f7765642077726974650a3b20636c61737365732061726520612063616e6f6e6963616c206d61702d7365743a2074686520636c6173732069732074686520756e69717565206b657920616e64206e756c6c206973207468650a3b20756e6974206d61726b65722e2043616e6f6e6963616c2043424f52206669786573206b6579206f7264657220776974686f75742061207365636f6e64206f72646572696e672072756c652e0a617574686f726974792d6f7065726174696f6e2d70726f66696c652d66616374203d207b0a2020636f72653a20747374722c0a2020616c6c6f7765645772697465436c61737365733a207b202a20617574686f726974792d77726974652d636c617373203d3e206e756c6c207d2c0a7d0a0a3b20546865206566666563745772697465436c6173736573206d6170206b6579206973207468652073656d616e7469632065666665637420636f6f7264696e6174652e2054686520627564676574730a3b206d6170206b65792069732074686520736f757263652062756467657420636f6f7264696e6174652e2043616e6f6e6963616c2043424f52206d61702d6b657920756e697175656e6573730a3b206d616b6573206475706c6963617465206661637420636f6f7264696e61746573207374727563747572616c6c7920756e726570726573656e7461626c652e0a617574686f726974792d6275646765742d66616374203d207b0a20206d617853746570733a2075696e742c0a20206d6178416c6c6f636174656442797465733a2075696e742c0a20206d61784f757470757442797465733a2075696e742c0a7d0a0a3b20417574686f726974794661637473446f63756d656e7420763120696e74656e74696f6e616c6c792061636365707473206f6e6c792074686520777269746520636c6173736573207468650a3b2063757272656e7420636f6d70696c6572206d6f64656c2063616e20636f6e73756d652e2060637573746f6d602069732074686520736f6c6520763120637573746f6d207370656c6c696e673b0a3b20617262697472617279207461726765742d70726f66696c6520657874656e73696f6e20737472696e677320646f206e6f7420656e746572207468697320636f6d70696c657220706174682e0a617574686f726974792d77726974652d636c617373203d20226e6f6e6522202f20227265616422202f202263726561746522202f2022656e7375726522202f2022617070656e6422202f0a202020202020202020202020202020202020202020202020227265706c61636522202f202264656c65746522202f2022637573746f6d220a0a3b202d2d2d2065646963742d7461726765742d69722e6364646c202d2d2d0a3b20535044582d4c6963656e73652d4964656e7469666965723a204170616368652d322e300a3b2065646963742d7461726765742d69722e6364646c0a3b2043616e6f6e6963616c20736368656d6120666f72207468652045646963742d6f776e65642054617267657420495220617274696661637420656e76656c6f70652e0a3b20417574686f7269746174697665206279746520656e636f64696e673a2065646963742e63616e6f6e6963616c2d63626f722f76312e0a3b0a3b205468697320736368656d6120697320617373656d626c656420776974682065646963742d636f6d6d6f6e2e6364646c20616e642065646963742d636f72652e6364646c2e2049740a3b2064656c696265726174656c792072657573657320436f72652065787072657373696f6e732c20707265646963617465732c20627564676574732c206c6f63616c207265666572656e6365732c0a3b206f62737472756374696f6e20726561736f6e732c20616e64206f62737472756374696f6e2061726d7320736f2074686520736368656d61206d617463686573207468652076616c75650a3b20656d6974746564206279207468652063616e6f6e6963616c2054617267657420495220656e636f64657220726174686572207468616e20726573746174696e672074686f73652074797065732e0a3b2049742064657363726962657320746865207374727563747572616c207368617065206f662076616c6964206c6f776572696e672d70726f6475636564206172746966616374732e205468650a3b206c6f776572696e6720616e6420656e636f64657220636f6e7472616374732073657061726174656c7920656e666f7263652073656d616e746963206964656e7469666965722072756c65730a3b20616e642063616e6f6e6963616c206f72646572696e672f64656475706c69636174696f6e20666f72207365742d6c696b652076616c7565732e0a0a3b2054617267657420495220656e636f64696e672072656a6563747320616e20656d707479207461726765742d70726f66696c6520636f6f7264696e617465206265666f72652062797465730a3b2065786973742c20736f207468697320726f6f74207469676874656e732074686520736861726564207374727563747572616c207265736f757263652d726566206163636f7264696e676c792e0a7461726765742d69722d7265736f757263652d726566203d207b0a202069643a2074737472202e7265676578702022283f73292e2b222c0a20206469676573743a207368613235362d6469676573742c0a7d0a0a7461726765742d69722d6172746966616374203d207b0a20206b696e643a202274617267657449724172746966616374222c0a2020646f6d61696e3a20747374722c0a202074617267657450726f66696c653a207461726765742d69722d7265736f757263652d7265662c0a2020736f75726365436f7265436f6f7264696e6174653a20747374722c0a2020696e74656e74733a207b202a2074737472203d3e207461726765742d69722d696e74656e74207d2c0a7d0a0a7461726765742d69722d696e74656e74203d207b0a20206f7065726174696f6e50726f66696c653a20747374722c0a2020696e707574436f6e73747261696e74733a205b2a20696e7075742d636f6e73747261696e745d2c0a2020636f72654576616c756174696f6e4275646765743a20636f72652d6275646765742c0a2020726571756972656d656e74733a205b2a207461726765742d69722d726571756972656d656e745d2c0a202073746570733a205b2a207461726765742d69722d737465705d2c0a2020726573756c743a20636f72652d657870722c0a7d0a0a7461726765742d69722d726571756972656d656e74203d207b0a202069643a20747374722c0a20207072656469636174653a20636f72652d7072656469636174652c0a20206f6e4661696c7572653a20726571756972652d6661696c7572652d61726d2c0a7d0a0a7461726765742d69722d73746570203d207b0a202069643a20747374722c0a202062696e64696e673a206c6f63616c2d7265662c0a20206566666563743a20747374722c0a2020746172676574496e7472696e7369633a20747374722c0a2020696e7075743a20636f72652d657870722c0a20206f62737472756374696f6e4661696c757265733a205b2a206661696c7572652d6964656e745d2c0a20206f62737472756374696f6e41726d733a207b202a206661696c7572652d6964656e74203d3e206f62737472756374696f6e2d61726d207d2c0a7d0a", + "rawSha256": "92697bc9a5262c68258be9ee451ee8c144aeb363b92142915b8224430b85cf74" + }, + "contracts": [ + {"contract": "authority-facts", "rootRule": "authority-facts"}, + {"contract": "core-module", "rootRule": "core-module"}, + {"contract": "lawpack-exports", "rootRule": "lawpack-exports"}, + {"contract": "lawpack-manifest", "rootRule": "lawpack-manifest"}, + {"contract": "lowering-requirements", "rootRule": "lowering-requirements"}, + {"contract": "target-ir-artifact", "rootRule": "target-ir-artifact"}, + {"contract": "target-profile-intrinsics", "rootRule": "intrinsics-document"}, + {"contract": "target-profile-manifest", "rootRule": "target-profile-manifest"}, + {"contract": "target-profile-operation-profiles", "rootRule": "operation-profiles-document"} + ], + "domains": [ + {"domain": "edict.authority-facts/v1", "rootRule": "authority-facts"}, + {"domain": "edict.core.module/v1", "rootRule": "core-module"}, + {"domain": "edict.lawpack/v1", "rootRule": "lawpack-manifest"}, + {"domain": "edict.lowering-requirements/v1", "rootRule": "lowering-requirements"}, + {"domain": "edict.target-ir.artifact/v1", "rootRule": "target-ir-artifact"}, + {"domain": "edict.target-profile/v1", "rootRule": "target-profile-manifest"} + ], + "resources": [ + { + "coordinate": "edict.canonical-cbor/v1", + "canonicalBytesHex": "a368636f6e7472616374a96a76616c75654d6f64656c8765617272617964626f6f6c65627974657367696e7465676572636d6170646e756c6c64746578746b6469676573744672616d656f65646963742e6469676573742f76316b6d61704f72646572696e67781b63616e6f6e6963616c2d656e636f6465642d6b65792d62797465736c74657874456e636f64696e67657574662d386d747261696c696e6742797465736672656a6563746f696e7465676572456e636f64696e676873686f727465737470636f6e7461696e65724c656e6774687368646566696e697465706475706c69636174654d61704b6579736672656a656374736d6178696d756d4e657374696e67446570746818806a61706956657273696f6e782965646963742e7461726765742d70726f66696c652e636f6e74726163742d7265736f757263652f76316a636f6f7264696e6174657765646963742e63616e6f6e6963616c2d63626f722f7631", + "rawSha256": "8306e4f08c1e4e7d29ab22bcf55c324312712aac3eeeb675857ced57c3e48bdc", + "domainFramedDigest": "sha256:d1ea6d3de2a9762a438cbf4fac1d5ae2f357a4b27d13e0347e94ea655bf40f9d", + "provenance": { + "repository": "https://github.com/flyingrobots/edict", + "sourcePath": "fixtures/target-profile/contract-resources/canonical-cbor.cbor" + } + }, + { + "coordinate": "edict.determinism/v1", + "canonicalBytesHex": "a368636f6e7472616374aa6c616d6269656e74436c6f636bf46e616d6269656e744e6574776f726bf46e6f75747075744964656e74697479781e686f73742d636f6d70757465642d61667465722d76616c69646174696f6e6e72656769737472794c6f6f6b7570f471616d6269656e7446696c6573797374656df471616d6269656e7452616e646f6d6e657373f472616d6269656e74456e7669726f6e6d656e74f4756d757461626c654e616d655265736f6c7574696f6ef477667265736853746f7265506572496e766f636174696f6ef5782263616e6f6e6963616c526573756c74496e646570656e64656e744f664c696d697473f56a61706956657273696f6e782965646963742e7461726765742d70726f66696c652e636f6e74726163742d7265736f757263652f76316a636f6f7264696e6174657465646963742e64657465726d696e69736d2f7631", + "rawSha256": "84073f5c1734b625e16799048e28458d43e0a10befdae56a79d906f5e37ef76a", + "domainFramedDigest": "sha256:af4e6c774d5ea82db30e680be2ba7abc5ba04c6aead146139de32d8c5bb4981e", + "provenance": { + "repository": "https://github.com/flyingrobots/edict", + "sourcePath": "fixtures/target-profile/contract-resources/determinism.cbor" + } + }, + { + "coordinate": "edict.diagnostics/v1", + "canonicalBytesHex": "a368636f6e7472616374a7666669656c64738464636f6465687365766572697479676d65737361676566726570616972686f72646572696e67781c636f64652d73657665726974792d6d6573736167652d726570616972697265636f7264416269782f65646963743a7461726765742d70726f76696465722f70726f746f636f6c40312e302e3023646961676e6f737469636a7472756e636174696f6e69666f7262696464656e6d73657665726974794f7264657283656572726f72677761726e696e6764696e666f6e6c697374436f756e74426f756e64f57261676772656761746542797465426f756e64f56a61706956657273696f6e782965646963742e7461726765742d70726f66696c652e636f6e74726163742d7265736f757263652f76316a636f6f7264696e6174657465646963742e646961676e6f73746963732f7631", + "rawSha256": "e465a28f1170fe478db5ff65d96a1fdbfbbcb95d327e6965021c252437991e4b", + "domainFramedDigest": "sha256:28fd72a98223153982ca084c29dbb1b2d430623967ab3b6db9d7fee668e614b9", + "provenance": { + "repository": "https://github.com/flyingrobots/edict", + "sourcePath": "fixtures/target-profile/contract-resources/diagnostics.cbor" + } + }, + { + "coordinate": "edict.fuel/v1", + "canonicalBytesHex": "a368636f6e7472616374a6687265717569726564f56a6163636f756e74696e67781864657465726d696e69737469632d67756573742d776f726b6a627564676574556e6974646675656c6a65786861757374696f6e72686f73742d6f776e65642d6661696c7572656d706572496e766f636174696f6ef57165706f6368496e74657272757074696f6e736e6f742d612d7265706c61792d6275646765746a61706956657273696f6e782965646963742e7461726765742d70726f66696c652e636f6e74726163742d7265736f757263652f76316a636f6f7264696e6174656d65646963742e6675656c2f7631", + "rawSha256": "c712c2d831cc8e731bdc1dfb8ea536f4630f38f0b0e0c6448b2df57176d3d0bd", + "domainFramedDigest": "sha256:006c6ebc01a3c5d36d50bd390b69d47e42378be61074e3b5c96ecb9f5ee53207", + "provenance": { + "repository": "https://github.com/flyingrobots/edict", + "sourcePath": "fixtures/target-profile/contract-resources/fuel.cbor" + } + }, + { + "coordinate": "edict.wasm-component/v1", + "canonicalBytesHex": "a368636f6e7472616374a7647761736969666f7262696464656e6e70726f746f636f6c496d706f727469747970652d6f6e6c796f63616c6c61626c65496d706f72747369666f7262696464656e70636f6e74726163744964656e746974796565786163747070726f7669646572436f6e7472616374781b65646963743a7461726765742d70726f766964657240312e302e3077667265736853746f7265506572496e766f636174696f6ef5781b636f6d706f6e656e74446967657374566572696669636174696f6e6d6265666f72652d6465636f64656a61706956657273696f6e782965646963742e7461726765742d70726f66696c652e636f6e74726163742d7265736f757263652f76316a636f6f7264696e6174657765646963742e7761736d2d636f6d706f6e656e742f7631", + "rawSha256": "cd09d702db1d10be825e72effe35627bec56963cb3d78601b69478c58787e34d", + "domainFramedDigest": "sha256:095b4dd18f1a6a7276533f758665be319c5f476e7bdc70cc56d30b6b3e9f0a80", + "provenance": { + "repository": "https://github.com/flyingrobots/edict", + "sourcePath": "fixtures/target-profile/contract-resources/wasm-component.cbor" + } + } + ] +} diff --git a/schemas/edict-provider/echo-provider-semantics-v1.json b/schemas/edict-provider/echo-provider-semantics-v1.json index 84161c88..0f9c3433 100644 --- a/schemas/edict-provider/echo-provider-semantics-v1.json +++ b/schemas/edict-provider/echo-provider-semantics-v1.json @@ -447,7 +447,8 @@ "role": "review.provider-generation", "kind": "reviewArtifact", "coordinate": "echo.edict-provider-generation-review@1", - "schemaContract": "echo.edict-provider.generation-review/v1" + "schemaContract": "wesley:GenerationReviewV1", + "contractOwner": "flyingrobots/wesley#728" }, { "role": "schema.echo-provider-artifacts", diff --git a/schemas/edict-provider/generated/README.md b/schemas/edict-provider/generated/README.md new file mode 100644 index 00000000..84899a9b --- /dev/null +++ b/schemas/edict-provider/generated/README.md @@ -0,0 +1,40 @@ + + + +# Checked Edict Provider Artifacts + +`v1/` is the exact first generated corpus for Echo's Edict provider contract. +Every entry below that directory is machine-generated and participates in the +snapshot check. Explanatory material stays in this parent directory so the +corpus root can reject every unexpected file without an ignore list. + +The 22 files are five canonical-CBOR primary artifacts, fourteen +canonical-CBOR resources, one self-contained CDDL schema, one canonical Wesley +generation-provenance document, and one canonical non-authoritative Wesley +review document. Canonical bytes are copied directly from their validated +owners; the generator does not pretty-print, append newlines, or re-encode them. + +Rebuild the corpus from the exact checked semantic source, generation settings, +Edict contract pack, and compile-time provider generator source bundle: + +```bash +cargo +1.90.0 run --locked -p echo-wesley-gen \ + --bin echo-edict-provider-artifacts -- +``` + +Check exact paths and bytes without creating, deleting, or rewriting anything: + +```bash +cargo +1.90.0 run --locked -p echo-wesley-gen \ + --bin echo-edict-provider-artifacts -- --check +``` + +Generation writes only expected files and does not delete unexpected entries. +Check mode reports sorted `missing`, `changed`, and `unexpected` diagnostics and +exits unsuccessfully on any drift. + +The provenance generator identity binds exact provider source files, Cargo +manifests and lockfile, and `rust-toolchain.toml`. It excludes this generated +directory and all authored inputs already bound separately by Wesley. The +identity describes the source/dependency-lock closure; it is not executable +reproducibility, package installation, Echo runtime admission, or authority. diff --git a/schemas/edict-provider/generated/v1/evidence/provenance.provider-generation.json b/schemas/edict-provider/generated/v1/evidence/provenance.provider-generation.json new file mode 100644 index 00000000..42f527a3 --- /dev/null +++ b/schemas/edict-provider/generated/v1/evidence/provenance.provider-generation.json @@ -0,0 +1 @@ +{"apiVersion":"wesley.generation-provenance-manifest/v1","contractVersions":{"generatorAbi":"wesley.extension-generator/v1","inputSchema":"wesley.extension-generation-input/v1","provenanceSchema":"wesley.generation-provenance-manifest/v1"},"emittedArtifacts":[{"coordinate":"echo.dpo-authority-facts@1","digest":"sha256:d17b03810ecc53f288aa1de457a5ba295c537c4f64046f8e3777b8f98ff3fc86"},{"coordinate":"echo.dpo-lawpack-authority-facts@1","digest":"sha256:3911de5075d3709a3ba40419e4b67f1226961f3520ba9dfbbad78278c9bb0e96"},{"coordinate":"echo.dpo-lawpack@1","digest":"sha256:df62a4ff2b56f9553c80cf400728cab3717f5f442c4c2fc415d2c89c21c41dad"},{"coordinate":"echo.dpo.registration/v1","digest":"sha256:8cc385a3f287ad6ea522766d7b4e92bc5164226586eef6b3f2ac6c5253370dd3"},{"coordinate":"echo.dpo@1","digest":"sha256:95626e5be6e6b2c1c8aa1858277f1c67487ab6724b08408eb3c0054adce6b1eb"},{"coordinate":"echo.provider-artifacts.cddl@1","digest":"sha256:19901cc33bea0699334af3cb4f9889f752652e35b81a2a9c91e7e31a52b803af"}],"generationInputDigest":"sha256:326024c55f16dfedfc887b11c24b6f6b596b08ae6b1abcc3a7269dc7728d9730","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:13cc7656796d3f3b24a35fc3b565893d2e2f6ec018ead60d5bc7a4d5d7f894dc","version":"0.1.0"},"settingsDigest":"sha256:0f708e76898de6fdb8186352e81d0f5c445adf94bb6c7de9204952d9fe913d4a","sourceArtifacts":[{"coordinate":"echo.semantic-schema@1","digest":"sha256:656a68b126669a629643302d679712626a90dbd7a38b521932630a141fe426ac"},{"coordinate":"edict.provider-contract-pack.cddl@1","digest":"sha256:92697bc9a5262c68258be9ee451ee8c144aeb363b92142915b8224430b85cf74"},{"coordinate":"edict.provider-contract-pack.manifest@1","digest":"sha256:6902467149fec3e0338bb90e8cd7963ee21b8ce24f368f9b12e748343cbe0e4f"}]} \ No newline at end of file diff --git a/schemas/edict-provider/generated/v1/evidence/review.provider-generation.json b/schemas/edict-provider/generated/v1/evidence/review.provider-generation.json new file mode 100644 index 00000000..2f1ee704 --- /dev/null +++ b/schemas/edict-provider/generated/v1/evidence/review.provider-generation.json @@ -0,0 +1 @@ +{"apiVersion":"wesley.generation-review/v1","authoritative":false,"emittedArtifacts":[{"coordinate":"echo.dpo-authority-facts@1","digest":"sha256:d17b03810ecc53f288aa1de457a5ba295c537c4f64046f8e3777b8f98ff3fc86"},{"coordinate":"echo.dpo-lawpack-authority-facts@1","digest":"sha256:3911de5075d3709a3ba40419e4b67f1226961f3520ba9dfbbad78278c9bb0e96"},{"coordinate":"echo.dpo-lawpack@1","digest":"sha256:df62a4ff2b56f9553c80cf400728cab3717f5f442c4c2fc415d2c89c21c41dad"},{"coordinate":"echo.dpo.registration/v1","digest":"sha256:8cc385a3f287ad6ea522766d7b4e92bc5164226586eef6b3f2ac6c5253370dd3"},{"coordinate":"echo.dpo@1","digest":"sha256:95626e5be6e6b2c1c8aa1858277f1c67487ab6724b08408eb3c0054adce6b1eb"},{"coordinate":"echo.provider-artifacts.cddl@1","digest":"sha256:19901cc33bea0699334af3cb4f9889f752652e35b81a2a9c91e7e31a52b803af"}],"generationInputDigest":"sha256:326024c55f16dfedfc887b11c24b6f6b596b08ae6b1abcc3a7269dc7728d9730","generator":{"coordinate":"echo-wesley-gen.provider-artifact-generator@1","digest":"sha256:13cc7656796d3f3b24a35fc3b565893d2e2f6ec018ead60d5bc7a4d5d7f894dc","version":"0.1.0"},"projectionRoles":["authority-facts.echo-dpo","authority-facts.echo-lawpack","generated-artifact-profile.echo-dpo-registration","lawpack.echo-dpo","schema.echo-provider-artifacts","target-profile.echo-dpo"],"provenanceManifestDigest":"sha256:8ab2000629f7bd7778fb696845309f64837930d0a83689bc18fea2d3ac2c69f9","sourceArtifacts":[{"coordinate":"echo.semantic-schema@1","digest":"sha256:656a68b126669a629643302d679712626a90dbd7a38b521932630a141fe426ac"},{"coordinate":"edict.provider-contract-pack.cddl@1","digest":"sha256:92697bc9a5262c68258be9ee451ee8c144aeb363b92142915b8224430b85cf74"},{"coordinate":"edict.provider-contract-pack.manifest@1","digest":"sha256:6902467149fec3e0338bb90e8cd7963ee21b8ce24f368f9b12e748343cbe0e4f"}]} \ No newline at end of file diff --git a/schemas/edict-provider/generated/v1/primary/authority-facts.echo-dpo.cbor b/schemas/edict-provider/generated/v1/primary/authority-facts.echo-dpo.cbor new file mode 100644 index 00000000..807c2186 --- /dev/null +++ b/schemas/edict-provider/generated/v1/primary/authority-facts.echo-dpo.cbor @@ -0,0 +1 @@ +fsourcedkindmtargetProfilefdigestfsha256X VbZe/T~U2-jcoordinatejecho.dpo@1gbudgetsjapiVersionxedict.authority-facts/v1qoperationProfileskp.effectfuldcorexcontinuum.profile.write/v1sallowedWriteClassesgreplacereffectWriteClassesntarget.replacegreplace \ No newline at end of file diff --git a/schemas/edict-provider/generated/v1/primary/authority-facts.echo-lawpack.cbor b/schemas/edict-provider/generated/v1/primary/authority-facts.echo-lawpack.cbor new file mode 100644 index 00000000..a3a7b681 Binary files /dev/null and b/schemas/edict-provider/generated/v1/primary/authority-facts.echo-lawpack.cbor differ diff --git a/schemas/edict-provider/generated/v1/primary/generated-artifact-profile.echo-dpo-registration.cbor b/schemas/edict-provider/generated/v1/primary/generated-artifact-profile.echo-dpo-registration.cbor new file mode 100644 index 00000000..bb7a2d6e --- /dev/null +++ b/schemas/edict-provider/generated/v1/primary/generated-artifact-profile.echo-dpo-registration.cbor @@ -0,0 +1 @@ +etypesha.b@1.IddkindocoreStringAliasicanonicalhraw-utf8omaxScalarValueska.b@1.Inputdkindfrecordffieldsdnamebiddtypeha.b@1.Idla.b@1.Outputdkindfrecordffieldsdnamebiddtypeha.b@1.Idma.b@1.Receiptdkindfrecordffieldsdnamebiddtypeha.b@1.Idwtarget.replace.rejecteddkindfrecordffieldsxdomain.WriteRejected.PayloaddkindfrecordffieldsjapiVersionx"echo.generated-artifact-profile/v1joperationsga.b@1.tfbudgetfp.tinyfeffectntarget.replaceiinputTypeka.b@1.InputjoutputTypela.b@1.OutputmopticContractmreplace-pointnimplementationdkindfnativejcoordinaterecho.dpo@1.replaceninvocationKindhmutationpoperationProfilexcontinuum.profile.write/v1sobstructionMappingshrejectedtdomain.WriteRejectedmtargetProfilejecho.dpo@1 \ No newline at end of file diff --git a/schemas/edict-provider/generated/v1/primary/lawpack.echo-dpo.cbor b/schemas/edict-provider/generated/v1/primary/lawpack.echo-dpo.cbor new file mode 100644 index 00000000..354071e7 Binary files /dev/null and b/schemas/edict-provider/generated/v1/primary/lawpack.echo-dpo.cbor differ diff --git a/schemas/edict-provider/generated/v1/primary/schema.echo-provider-artifacts.cddl b/schemas/edict-provider/generated/v1/primary/schema.echo-provider-artifacts.cddl new file mode 100644 index 00000000..900e2fc2 --- /dev/null +++ b/schemas/edict-provider/generated/v1/primary/schema.echo-provider-artifacts.cddl @@ -0,0 +1,969 @@ +; SPDX-License-Identifier: Apache-2.0 +; edict-provider-contracts.cddl +; Generated from Edict-owned ABI fragments. DO NOT EDIT. + +; --- edict-common.cddl --- +; edict-common.cddl +; Shared CDDL types for the Edict ABIs, defined ONCE here so they cannot drift +; (EDICT-ABI-NODUP-001). Two groups: +; - resource-ref, sha256-digest, effect-failure-body, effect-kind, authority-class, +; core-type-ref: assembled with edict-target-profile.cddl and +; edict-lawpack.cddl by the build; those schemas do not redefine them. +; - operation-profile, optic-template, aperture-requirement and their refs: +; consumed by the Core/optic layer (edict-core.cddl) and referenced by the +; language/target-profile specs. +; Authoritative byte encoding: edict.canonical-cbor/v1. + +; A normative subcomponent referenced by identity plus digest. Manifests never +; embed their own self-digest in their preimage (EDICT-CORE-SELFHASH-001). +resource-ref = { id: tstr, digest: sha256-digest } + +; Digests are authoritative as typed byte values, never hex strings. Review JSON +; renders this as "sha256:<64 lowercase hex>" (EDICT-DIGEST-WIRE-001). +sha256-digest = [ algorithm: "sha256", bytes: bstr .size 32 ] + +; A named low-level failure an effect can raise. The source obstruction map +; binds it (by coordinate) and constructs a typed domain obstruction from its +; payload (EDICT-ABI-FAILURE-NAMED-001). +; An effect's `effectFailures` list MUST have unique `coordinate`s: since the +; obstruction map is keyed by coordinate, two failures sharing a coordinate (even +; with different authorityClass/payloadType) make exhaustive mapping and binder +; typing ambiguous and are rejected (EDICT-ABI-FAILURE-UNIQUE-001). +; +; Effects carry their failures as a map `{ failure-ident => effect-failure-body }` +; (see the target/lawpack effect schemas). The failure coordinate is the map +; KEY, so it is not repeated in the body and cannot disagree with the key. +effect-failure-body = { + authorityClass: authority-class, + payloadType: core-type-ref, ; typed, bounded (may be empty record) +} + +; A failure coordinate must be a bare Edict `ident` (letter/underscore then +; letters/digits/underscores) AND must not be a reserved keyword (e.g. `else`, +; `basis`, `where`, `require`, `for`, `if`). The source obstruction-map LHS only +; accepts a non-keyword `ident`, so a hyphen/dot/keyword coordinate would be +; ABI-valid yet impossible to map exhaustively in source. The regex captures the +; lexical shape; keyword exclusion is an additional validation rule +; (EDICT-ABI-FAILURE-IDENT-001). +failure-ident = tstr .regexp "[A-Za-z_][A-Za-z0-9_]*" + +effect-kind = "read" / "create" / "ensure" / "replace" / "delete" / + "append" / "reduce" / "semantic.emit" / "custom" + +authority-class = "domainMappable" / "participantOwned" / "integrityFault" / + "resourceFault" / "internalFault" + +core-type-ref = tstr ; canonical Core type coordinate + +; An operation profile supplies the optic template a Core intent resolves its +; opticKind/boundaryKind/supportPolicy/lossDisposition from. Target profiles and +; lawpacks publish these as a map `{ coordinate => operation-profile }`, so the +; coordinate is the KEY, not a value field (EDICT-OPTIC-TEMPLATE-OWNER-001, +; EDICT-ABI-OPPROFILE-UNIQUE-001). +operation-profile = { + opticTemplate: optic-template, + effectPredicate: tstr, ; coordinate of the operation-mode predicate +} + +optic-template = { + opticKind: "revelation" / "affectReintegration", + boundaryKind: "projection" / "affect", + supportPolicy: tstr, ; canonical support-policy coordinate + lossDisposition: tstr, ; canonical loss-disposition coordinate + ? basisTemplate: tstr, ; optional digest-locked basis template coord + ; the aperture requirement this template supplies. Required when the template + ; is the source of a Core optic's apertureRequirement (i.e. the intent has no + ; source `footprint <= ...`), since apertureRequirement is mandatory in Core + ; (EDICT-OPTIC-APERTURE-REF-001). + ? apertureRequirement: aperture-requirement, +} + +; apertureRequirement is a typed reference, never a free-form string. A review +; rendering may show its coordinate (EDICT-OPTIC-APERTURE-REF-001). +aperture-requirement = footprint-ceiling-ref / abstract-footprint-obligation-ref +footprint-ceiling-ref = { kind: "footprintCeiling", ref: tstr } +abstract-footprint-obligation-ref = { kind: "abstractFootprintObligation", ref: tstr } + +; --- edict-core.cddl --- +; edict-core.cddl +; Normative schema for the Edict Core v1 semantic model. +; +; Scope boundary: this file defines Core meaning and schema shape only. It does +; not define a canonical encoder, Core module hash fields, hash fixtures, target +; lowering, admission bundles, or target-owned IR. + +core-module = { + apiVersion: "edict.core/v1", + coordinate: tstr, + imports: [* core-import], + types: { * tstr => core-type }, + intents: { + tstr => core-intent }, + requiredCoreCapabilities: [* tstr], +} + +core-import = { + kind: "lawpack" / "target" / "core", + ref: resource-ref, + ? alias: tstr, +} + +; --- types --------------------------------------------------------------- + +core-type = core-scalar-type / core-record-type / core-variant-type / + core-option-type / core-list-type / core-map-type / + core-capability-ref-type + +core-scalar-type = core-bool-type / core-int-type / core-string-type / + core-bytes-type / core-unit-type + +core-bool-type = { kind: "Bool" } +core-unit-type = { kind: "Unit" } +core-int-type = { + kind: "I64" / "U64" / "I32" / "U32" / "I16" / "U16" / "I8" / "U8", +} +core-string-type = { + kind: "String", + max: uint, + canonical: "unicode-scalar-nfc" / "raw-utf8", +} +core-bytes-type = { + kind: "Bytes", + max: uint, +} +core-record-type = { + kind: "Record", + fields: { * tstr => core-type-ref }, +} +core-variant-type = { + kind: "Variant", + cases: { + tstr => variant-case-body }, +} +variant-case-body = { + ? payload: core-type-ref, +} +core-option-type = { + kind: "Option", + item: core-type-ref, +} +core-list-type = { + kind: "List", + item: core-type-ref, + max: uint, +} +core-map-type = { + kind: "Map", + key: core-type-ref, + value: core-type-ref, + max: uint, +} +core-capability-ref-type = { + kind: "CapabilityRef", + item: core-type-ref, +} + +; core-type-ref is defined in edict-common.cddl and assembled with this schema. + +; --- references and values ---------------------------------------------- + +; Local identity is alpha-stable. `id` is the compiler-owned local coordinate; +; `alphaName` is the normalized human/debug name. Source binder spelling is not +; identity. +local-ref = { + id: tstr, + alphaName: tstr, + type: core-type-ref, +} + +core-value = core-null-value / core-bool-value / core-int-value / + core-string-value / core-bytes-value / core-record-value / + core-variant-value / core-list-value / core-map-value / + core-capability-value + +core-null-value = { kind: "null" } +core-bool-value = { kind: "bool", value: bool } +core-int-value = { kind: "int", width: tstr, value: int } +core-string-value = { kind: "string", value: tstr } +core-bytes-value = { kind: "bytes", value: bstr } +core-record-value = { kind: "record", fields: { * tstr => core-value } } +core-variant-value = { + kind: "variant", + type: core-type-ref, + case: tstr, + ? payload: core-value, +} +core-list-value = { kind: "list", values: [* core-value] } +core-map-value = { kind: "map", entries: [* [key: core-value, value: core-value]] } +core-capability-value = { + kind: "capability", + receipt: sha256-digest, +} + +; Edict-authored pure helpers use a pure Core function body. The body can bind +; pure expressions and return an expression, but it cannot contain Core effect, +; guard, branch, loop, match-node, or proof-obligation nodes. +core-fn-body = { + params: [* local-ref], + body: core-pure-block, +} + +core-pure-block = { + locals: [* local-ref], + bindings: [* pure-let-node], + result: core-expr, +} + +pure-let-node = { + kind: "let", + binding: local-ref, + value: core-expr, +} + +; --- expressions and predicates ----------------------------------------- + +core-expr = local-expr / const-expr / record-expr / field-expr / + variant-expr / match-expr / call-expr / list-expr / map-expr / + if-expr + +local-expr = { kind: "local", ref: local-ref } +const-expr = { kind: "const", value: core-value } +record-expr = { kind: "record", fields: { * tstr => core-expr } } +field-expr = { kind: "field", base: core-expr, field: tstr } +variant-expr = { + kind: "variant", + type: core-type-ref, + case: tstr, + ? payload: core-expr, +} +match-expr = { + kind: "match", + scrutinee: core-expr, + arms: [+ match-arm], +} +match-arm = { + case: tstr, + ? binder: local-ref, + body: core-expr, +} +call-expr = { + kind: "call", + callee: tstr, + typeArgs: [* core-type-ref], + args: [* core-expr], +} +list-expr = { kind: "list", values: [* core-expr] } +map-expr = { kind: "map", entries: [* [key: core-expr, value: core-expr]] } +if-expr = { + kind: "if", + predicate: core-predicate, + then: core-expr, + else: core-expr, +} + +core-predicate = true-predicate / false-predicate / not-predicate / + all-predicate / any-predicate / compare-predicate / + call-predicate / obstruction-predicate + +true-predicate = { kind: "true" } +false-predicate = { kind: "false" } +not-predicate = { kind: "not", value: core-predicate } +all-predicate = { kind: "all", values: [+ core-predicate] } +any-predicate = { kind: "any", values: [+ core-predicate] } +compare-predicate = { + kind: "compare", + op: "==" / "!=" / "<" / "<=" / ">" / ">=", + left: core-expr, + right: core-expr, +} +call-predicate = { + kind: "call", + predicate: tstr, + args: [* core-expr], +} +obstruction-predicate = { + kind: "obstruction", + coordinate: failure-ident, + payload: core-expr, +} + +input-constraint = { + coordinate: tstr, + source: "where" / "compiler", + predicate: core-predicate, +} + +; --- intents, blocks, and nodes ----------------------------------------- + +core-intent = { + input: core-type-ref, + output: core-type-ref, + requiredOperationProfile: tstr, + inputConstraints: [* input-constraint], + coreEvaluationBudget: core-budget, + body: core-block, + ? optic: core-optic, +} + +core-budget = { + maxSteps: uint, + maxAllocatedBytes: uint, + maxOutputBytes: uint, +} + +core-optic = { + opticKind: "revelation" / "affectReintegration", + boundaryKind: "projection" / "affect", + apertureRequirement: aperture-requirement, + supportPolicy: tstr, + lossDisposition: tstr, +} + +core-block = { + locals: [* local-ref], + nodes: [* core-node], + result: core-expr, +} + +core-node = let-node / require-node / effect-node / guard-node / branch-node / + for-node / match-node / proof-obligation-node + +let-node = { + kind: "let", + binding: local-ref, + value: core-expr, +} +require-node = { + kind: "require", + predicate: core-predicate, + onFailure: require-failure-arm, +} +require-failure-arm = terminal-require-failure / + continue-obstructed-require-failure +terminal-require-failure = { + kind: "terminal", + reason: obstruction-reason, +} +continue-obstructed-require-failure = { + kind: "continueObstructed", + reason: obstruction-reason, +} +obstruction-reason = { + reasonKind: tstr, + payload: { * tstr => core-expr }, +} +effect-node = { + kind: "effect", + binding: local-ref, + effect: tstr, + input: core-expr, + obstructionMap: { * failure-ident => obstruction-arm }, +} +obstruction-arm = { + binder: local-ref, + value: core-expr, +} +guard-node = { + kind: "guard", + predicate: core-predicate, + obstruction: core-expr, +} +branch-node = { + kind: "branch", + predicate: core-predicate, + then: core-block, + else: core-block, +} +for-node = { + kind: "for", + binder: local-ref, + iter: core-expr, + bound: core-bound, + body: core-block, +} +match-node = { + kind: "match", + scrutinee: core-expr, + arms: [+ match-block-arm], +} +match-block-arm = { + case: tstr, + ? binder: local-ref, + body: core-block, +} +proof-obligation-node = { + kind: "proof", + coordinate: tstr, + predicate: core-predicate, +} + +core-bound = literal-bound / coordinate-bound +literal-bound = { kind: "literal", value: uint } +coordinate-bound = { kind: "coordinate", ref: tstr } + +; Shared resource-ref, sha256-digest, failure-ident, aperture-requirement, and +; core-type-ref are defined once in edict-common.cddl. + +; --- edict-lawpack.cddl --- +; edict-lawpack.cddl +; Canonical schema for the Edict lawpack manifest and export surface. +; Authoritative byte encoding: edict.canonical-cbor/v1. JSON in the prose specs +; is a review rendering generated from this schema (EDICT-ABI-NODUP-001). + +lawpack-manifest = { + apiVersion: "edict.lawpack/v1", + id: tstr, + version: tstr, + acceptedCoreAbi: [+ tstr], + dependencies: [* lawpack-dep], ; acyclic, digest-locked (EDICT-LAWPACK-DAG-001) + exports: resource-ref, + ? targetAdapters: [+ target-adapter], ; required only if any runtime effect exists + ? helperComponent: executable-component, ; executable helpers carry their own sandbox+fuel + verifier: verifier, ; classified: declarative or executable + compatibility: resource-ref, + conformanceFixtureCorpus: resource-ref, +} + +; A verifier is either a declarative ruleset (no runtime) or an executable +; component. An executable verifier MUST carry its own sandbox and fuel model, +; so the schema enforces that no executable component is left unbounded +; (EDICT-ABI-VERIFIER-BOUND-001). +verifier = declarative-verifier / executable-verifier +declarative-verifier = { class: "declarative", ruleset: resource-ref } +executable-verifier = { + class: "executable", + component: resource-ref, + sandbox: resource-ref, + fuelModel: resource-ref, +} + +; Any executable component is bounded by its own sandbox + fuel model. +executable-component = { + component: resource-ref, + sandbox: resource-ref, + fuelModel: resource-ref, +} + +lawpack-dep = { id: tstr, version: tstr, digest: sha256-digest } + +; Adapter selection keys SOLELY off the digest-locked `acceptedTargetProfile` +; (its `id` is the profile id; its `digest` pins the exact profile/version). There +; are no independent display strings that could disagree with the lock, so a +; resolver cannot bind an adapter to one target while the lock proves another +; (EDICT-LAWPACK-ADAPTER-TARGETIR-001). +target-adapter = { + acceptedTargetProfile: resource-ref, ; digest-locked, authoritative selector + acceptedTargetIr: resource-ref, ; digest-locked + adapter: resource-ref, +} + +; Shared types resource-ref and sha256-digest are defined once in +; edict-common.cddl and assembled with this schema (EDICT-ABI-NODUP-001). + +; --- export surface ------------------------------------------------------ + +lawpack-exports = { + types: [* exported-type], + constants: [* exported-constant], + pureFunctions: [* pure-function], + effects: [* semantic-effect], + obstructions: [* obstruction-def], + ; keyed by operation-profile coordinate → uniqueness enforced + ; (EDICT-ABI-OPPROFILE-UNIQUE-001) + ; operation-profile records this lawpack exports (optic templates that + ; `implements`/`profile` clauses resolve against). operation-profile is + ; defined in edict-common.cddl (EDICT-ABI-OPPROFILE-SLOT-001). + operationProfiles: { * tstr => operation-profile }, ; keyed by coordinate +} + +exported-type = { coordinate: tstr, definition: core-type-ref } +exported-constant = { coordinate: tstr, type: core-type-ref, value: any } + +; A pure helper is a discriminated union by `source`, so the schema itself +; guarantees an implementation exists (EDICT-LAWPACK-PURE-IMPL-001): +; - "edict": authored in Edict/Core; the Core body is carried inline (hashed +; with the export surface). The schema requires the `body` field. +; - "component": implemented outside Edict; carries no inline body and instead +; carries its own digest-locked `implementation` (sandbox + fuel). It does +; not depend on the optional manifest-level helperComponent. +pure-function = edict-pure-function / component-pure-function + +pure-function-common = ( + coordinate: tstr, + typeParameters: [* tstr], + parameterTypes: [* core-type-ref], ; all bounded + returnType: core-type-ref, ; bounded + costTemplate: tstr, + determinismClass: "total" / "total-with-typed-diagnostic", +) + +edict-pure-function = { + pure-function-common, + source: "edict", + body: core-fn-body, ; inline, hash-significant +} + +component-pure-function = { + pure-function-common, + source: "component", + ; the digest-locked component implementing this helper. Required at the schema + ; level so a component helper can never validate without a hash-bound, + ; sandbox+fuel-described implementation (EDICT-LAWPACK-PURE-IMPL-001). + implementation: executable-component, +} + +; core-fn-body is defined by edict-core.cddl and assembled with this lawpack +; schema. It is a pure Core function body, not an effect-capable core-block. + +semantic-effect = { + coordinate: tstr, + typeParameters: [* tstr], + inputType: core-type-ref, ; bounded + outputType: core-type-ref, ; bounded + executionClass: "proofOnly" / "runtime", ; orthogonal to writeClass + effectKindHint: effect-kind, + footprintObligation: tstr, + costObligation: tstr, + effectFailures: { * failure-ident => effect-failure-body }, ; keyed by coordinate; unique + guardSupport: bool, +} + +obstruction-def = { + coordinate: tstr, + authorityClass: authority-class, + payloadSchema: core-type-ref, ; typed, bounded (may be empty record) +} + +; effect-failure-body, effect-kind, authority-class, and core-type-ref are defined +; once in edict-common.cddl and assembled with this schema (EDICT-ABI-NODUP-001). + +; --- edict-target-profile.cddl --- +; edict-target-profile.cddl +; Canonical schema for the Edict target profile manifest. +; Authoritative byte encoding: edict.canonical-cbor/v1 +; (see SPEC_continuum-contract-bundle-v1.md). JSON in the prose specs is a +; review rendering generated from this schema; this CDDL is the single source +; of truth (EDICT-ABI-NODUP-001). + +target-profile-manifest = { + apiVersion: "edict.target-profile/v1", + id: tstr, ; e.g. "echo.dpo" + version: tstr, ; e.g. "1" + acceptedCoreAbi: [+ tstr], ; e.g. ["edict.core/v1"] + + intrinsics: resource-ref, + intrinsicNamespace: tstr, + ; publishes this profile's operation-profile records (optic templates that + ; `profile`/`implements` clauses resolve against). References an + ; operation-profiles-document (EDICT-ABI-OPPROFILE-SLOT-001). + operationProfiles: resource-ref, + footprintAlgebra: resource-ref, + costAlgebra: resource-ref, + targetIr: resource-ref, + obstructionTaxonomy: resource-ref, + verifier: resource-ref, + lowerer: resource-ref, + sandbox: resource-ref, + fuelModel: resource-ref, + + ; fields the language spec requires of every profile + bundleProfile: resource-ref, + generatedArtifactProfiles: [* resource-ref], + canonicalEncodingRules: resource-ref, + ; Reserved/deferred: will list accepted lawpack-adapter ABI ids once that + ; byte-level ABI (edict.lawpack-adapter/v1) is specified. The schema enforces + ; emptiness now — the type is the empty array, so non-empty values are + ; rejected, not merely discouraged (EDICT-ABI-LAWPACK-ADAPTER-DEFER-001). + ? acceptedLawpackAdapterAbi: [], + diagnosticAbi: resource-ref, + + ; application doctrine + applicationModel: "atomic", + readConsistency: "application-snapshot" / tstr, + guardEvaluation: "precommit-atomic" / tstr, + obstructionRollback: "no-visible-effects" / tstr, + multiTarget: bool, + ; whether the target can evaluate precommit postcondition (`guarantee`) checks + ; inside the atomic application unit (EDICT-TARGET-POSTCOND-001) + postconditionSupport: bool, + + deterministicExecution: resource-ref, + conformanceFixtureCorpus: resource-ref, +} + +; Shared types resource-ref and sha256-digest are defined once in +; edict-common.cddl and assembled with this schema by the build +; (EDICT-ABI-NODUP-001). They are not redefined here. + +; --- intrinsic signature ------------------------------------------------- +; The artifact referenced by the manifest's `intrinsics` resource-ref is the +; intrinsic-signature corpus document below. Its layout is fixed so two +; independent profiles validate/hash the corpus identically +; (EDICT-ABI-INTRINSICS-DOC-001). + +; intrinsics is a MAP keyed by coordinate, so the schema itself enforces +; coordinate uniqueness. A provider receives the resolved corpus as a +; digest-bound semantic input and resolves coordinates within that artifact. +; Each map key MUST equal its record's `coordinate` field +; (EDICT-ABI-INTRINSIC-UNIQUE-001). +intrinsics-document = { + apiVersion: "edict.target-profile.intrinsics/v1", + intrinsics: { * tstr => intrinsic }, +} + +; The artifact referenced by the manifest's `operationProfiles` resource-ref. +; operation-profile / optic-template are defined in edict-common.cddl. Keyed by +; coordinate so resolution can't pick between two same-coordinate profiles +; (EDICT-ABI-OPPROFILE-SLOT-001, EDICT-ABI-OPPROFILE-UNIQUE-001). +operation-profiles-document = { + apiVersion: "edict.target-profile.operation-profiles/v1", + profiles: { * tstr => operation-profile }, +} + +; A typed pre-lowering question that can be proposed by Watson or an agent and +; checked by the compiler. It is canonical-CBOR encoded under +; `edict.lowering-requirements/v1`; the compiler checks this artifact, not the +; prose that produced it. +lowering-requirements = { + apiVersion: "edict.lowering-requirements/v1", + operationProfile: tstr, + semanticEffects: [* semantic-effect-requirement], + requiredWriteClasses: [* write-class], + guardKinds: [* guard-kind], + atomicity: atomicity-requirement, + postconditionSupport: bool, + obstructionCoordinates: [* tstr], + footprintObligations: [* tstr], + costObligations: [* tstr], + opticContract: tstr, +} + +semantic-effect-requirement = { + coordinate: tstr, + writeClass: write-class, + guardKinds: [* guard-kind], + obstructionCoordinates: [* tstr], + footprintObligations: [* tstr], + costObligations: [* tstr], +} + +write-class = "none" / "read" / "create" / "ensure" / "append" / + "replace" / "delete" / tstr +guard-kind = "precommit-atomic" / tstr +atomicity-requirement = "atomic" / tstr + +; A genuine union: pure constructors carry no effect kind or failures; effect +; intrinsics must (EDICT-TARGET-INTRINSIC-CLASS-001). The schema enforces this, +; not a comment. + +; The intrinsic's coordinate is the intrinsics map KEY, not a value field, so the +; key and coordinate can never disagree (EDICT-ABI-INTRINSIC-UNIQUE-001). +intrinsic = pure-intrinsic / effect-intrinsic + +pure-intrinsic = { + intrinsicClass: "pure", + typeParameters: [* tstr], + argumentTypes: [* core-type-ref], + returnType: core-type-ref, + guardSupport: false, + footprintTemplate: tstr, + costTemplate: tstr, + writeClass: "none", +} + +effect-intrinsic = { + intrinsicClass: "effect", + typeParameters: [* tstr], + argumentTypes: [* core-type-ref], + returnType: core-type-ref, + effectKind: effect-kind, + ; map keyed by failure coordinate (failure-ident); the failure coordinate is + ; the key, not a value field, so uniqueness is structural + ; (EDICT-ABI-FAILURE-UNIQUE-001). + effectFailures: { * failure-ident => effect-failure-body }, + guardSupport: bool, + footprintTemplate: tstr, + costTemplate: tstr, + writeClass: "read" / "create" / "ensure" / "append" / "replace" / + "delete" / "custom", + canParticipateInAtomicGuard: bool, +} + +; effect-failure-body, effect-kind, authority-class, and core-type-ref are defined +; once in edict-common.cddl and assembled with this schema (EDICT-ABI-NODUP-001). + +; --- edict-authority-facts.cddl --- +; edict-authority-facts.cddl +; Canonical schema for the first compiler-context authority-facts document. +; Authoritative byte encoding: edict.canonical-cbor/v1. +; +; This schema is assembled with edict-common.cddl so source.digest uses the +; shared sha256-digest typed value. JSON is a review/input rendering: its +; `sha256:<64 hex>` source digest is projected to [`sha256`, 32 raw bytes] on +; the wire, and its fact arrays project to the coordinate-keyed maps below. + +authority-facts = { + apiVersion: "edict.authority-facts/v1", + source: authority-fact-source, + operationProfiles: { * tstr => authority-operation-profile-fact }, + effectWriteClasses: { * tstr => authority-write-class }, + budgets: { * tstr => authority-budget-fact }, +} + +authority-fact-source = { + kind: "lawpack" / "targetProfile", + coordinate: tstr, + digest: sha256-digest, +} + +; The map key is the source operation-profile coordinate. It is not repeated in +; the value, so a key and embedded coordinate cannot disagree. Allowed write +; classes are a canonical map-set: the class is the unique key and null is the +; unit marker. Canonical CBOR fixes key order without a second ordering rule. +authority-operation-profile-fact = { + core: tstr, + allowedWriteClasses: { * authority-write-class => null }, +} + +; The effectWriteClasses map key is the semantic effect coordinate. The budgets +; map key is the source budget coordinate. Canonical CBOR map-key uniqueness +; makes duplicate fact coordinates structurally unrepresentable. +authority-budget-fact = { + maxSteps: uint, + maxAllocatedBytes: uint, + maxOutputBytes: uint, +} + +; AuthorityFactsDocument v1 intentionally accepts only the write classes the +; current compiler model can consume. `custom` is the sole v1 custom spelling; +; arbitrary target-profile extension strings do not enter this compiler path. +authority-write-class = "none" / "read" / "create" / "ensure" / "append" / + "replace" / "delete" / "custom" + +; --- edict-target-ir.cddl --- +; SPDX-License-Identifier: Apache-2.0 +; edict-target-ir.cddl +; Canonical schema for the Edict-owned Target IR artifact envelope. +; Authoritative byte encoding: edict.canonical-cbor/v1. +; +; This schema is assembled with edict-common.cddl and edict-core.cddl. It +; deliberately reuses Core expressions, predicates, budgets, local references, +; obstruction reasons, and obstruction arms so the schema matches the value +; emitted by the canonical Target IR encoder rather than restating those types. +; It describes the structural shape of valid lowering-produced artifacts. The +; lowering and encoder contracts separately enforce semantic identifier rules +; and canonical ordering/deduplication for set-like values. + +; Target IR encoding rejects an empty target-profile coordinate before bytes +; exist, so this root tightens the shared structural resource-ref accordingly. +target-ir-resource-ref = { + id: tstr .regexp "(?s).+", + digest: sha256-digest, +} + +target-ir-artifact = { + kind: "targetIrArtifact", + domain: tstr, + targetProfile: target-ir-resource-ref, + sourceCoreCoordinate: tstr, + intents: { * tstr => target-ir-intent }, +} + +target-ir-intent = { + operationProfile: tstr, + inputConstraints: [* input-constraint], + coreEvaluationBudget: core-budget, + requirements: [* target-ir-requirement], + steps: [* target-ir-step], + result: core-expr, +} + +target-ir-requirement = { + id: tstr, + predicate: core-predicate, + onFailure: require-failure-arm, +} + +target-ir-step = { + id: tstr, + binding: local-ref, + effect: tstr, + targetIntrinsic: tstr, + input: core-expr, + obstructionFailures: [* failure-ident], + obstructionArms: { * failure-ident => obstruction-arm }, +} + + +; --- Echo provider-generated declarative contracts ----------------------- +; These roots validate provider semantic descriptions. They confer no Echo +; runtime authority and contain no package, component, or installation state. + +generated-artifact-profile = { + apiVersion: "echo.generated-artifact-profile/v1", + targetProfile: tstr, + types: { * tstr => echo-generated-type }, + operations: { * tstr => generated-operation }, +} + +echo-generated-type = echo-generated-string-alias / echo-generated-record +echo-generated-string-alias = { + kind: "coreStringAlias", + maxScalarValues: uint, + canonical: "raw-utf8", +} +echo-generated-record = { + kind: "record", + fields: [* echo-generated-record-field], +} +echo-generated-record-field = { name: tstr, type: tstr } + +generated-operation = { + inputType: tstr, + outputType: tstr, + effect: tstr, + operationProfile: tstr, + opticContract: tstr, + budget: tstr, + invocationKind: "mutation" / "observer", + implementation: { kind: "native" / "directAdapter", coordinate: tstr }, + obstructionMappings: { * tstr => tstr }, +} + +echo-provider-conformance-corpus = { + apiVersion: "echo.edict-provider.conformance-corpus/v1", + class: "declarative", + operations: { * tstr => null }, + capabilities: { * tstr => null }, + semanticEffects: { * tstr => null }, + cases: [], +} + +echo-provider-lawpack-compatibility = { + apiVersion: "echo.edict-provider.lawpack-compatibility/v1", + class: "declarative", + acceptedCoreAbi: { * tstr => null }, + acceptedTargetProfiles: { * tstr => null }, + semanticEffects: { * tstr => null }, +} + +echo-provider-lawpack-target-adapter = { + apiVersion: "echo.edict-provider.lawpack-target-adapter/v1", + class: "declarative", + targetProfile: tstr, + targetIrDomain: tstr, + effectImplementations: { * tstr => echo-effect-implementation }, +} + +echo-effect-implementation = echo-native-effect-implementation / echo-direct-effect-implementation +echo-native-effect-implementation = { + kind: "native", + capability: tstr, + writeClass: tstr, +} +echo-direct-effect-implementation = { + kind: "directAdapter", + adapter: tstr, + capability: tstr, + writeClass: tstr, +} + +echo-provider-lawpack-verifier = { + apiVersion: "echo.edict-provider.lawpack-verifier/v1", + class: "declarative", + operationObstructions: { * tstr => echo-operation-obstructions }, +} +echo-operation-obstructions = { + effect: tstr, + failureMappings: { * failure-ident => tstr }, +} + +echo-dpo-bundle = { + apiVersion: "echo.dpo.bundle/v1", + class: "declarative", + applicationModel: tstr, + readConsistency: tstr, + operationProfiles: { * tstr => null }, +} + +echo-dpo-cost = { + apiVersion: "echo.dpo.cost/v1", + class: "declarative", + capabilities: { * tstr => echo-cost-capability }, +} +echo-cost-capability = { + effect: tstr, + costTemplate: tstr, + semanticObligation: tstr, +} + +echo-dpo-footprint = { + apiVersion: "echo.dpo.footprint/v1", + class: "declarative", + capabilities: { * tstr => echo-footprint-capability }, +} +echo-footprint-capability = { + effect: tstr, + footprintTemplate: tstr, + semanticObligation: tstr, + writeClass: tstr, +} + +echo-span-ir = { + apiVersion: "echo.span-ir/v1", + class: "declarative", + domain: tstr, + targetProfile: tstr, + capabilities: { * tstr => null }, +} + +echo-dpo-lowerer = { + apiVersion: "echo.dpo.lowerer/v1", + class: "declarative", + acceptedCoreAbi: { * tstr => null }, + outputDomain: tstr, + targetProfile: tstr, + effectImplementations: { * tstr => echo-effect-implementation }, + opticContracts: { * tstr => tstr }, +} + +echo-dpo-obstructions = { + apiVersion: "echo.dpo.obstructions/v1", + class: "declarative", + effectFailures: { * tstr => { authorityClass: authority-class, payloadType: tstr } }, + domainObstructions: { * tstr => { authorityClass: authority-class, payloadSchema: tstr } }, +} + +echo-dpo-verifier = { + apiVersion: "echo.dpo.verifier/v1", + class: "declarative", + targetProfile: tstr, + targetIrDomain: tstr, + capabilities: { * tstr => null }, + operationProfiles: { * tstr => null }, + opticContracts: { * tstr => tstr }, +} + +echo-nonempty-tstr = tstr .regexp "(?s).+" + +generated-artifact = { + apiVersion: "echo.generated-artifact/v1", + profile: resource-ref, + operation: echo-nonempty-tstr, + mediaType: echo-nonempty-tstr, + bytes: bstr, +} + +review-payload = { + apiVersion: "echo.review-payload/v1", + authoritative: false, + subject: resource-ref, + mediaType: echo-nonempty-tstr, + bytes: bstr, +} + +verifier-report = { + apiVersion: "echo.verifier-report/v1", + targetIr: resource-ref, + outcome: "accepted" / "rejected", + diagnosticAbi: resource-ref, + diagnosticBytes: bstr, +} diff --git a/schemas/edict-provider/generated/v1/primary/target-profile.echo-dpo.cbor b/schemas/edict-provider/generated/v1/primary/target-profile.echo-dpo.cbor new file mode 100644 index 00000000..b3ca7861 Binary files /dev/null and b/schemas/edict-provider/generated/v1/primary/target-profile.echo-dpo.cbor differ diff --git a/schemas/edict-provider/generated/v1/resources/resource.conformance-corpus.cbor b/schemas/edict-provider/generated/v1/resources/resource.conformance-corpus.cbor new file mode 100644 index 00000000..4464f53c --- /dev/null +++ b/schemas/edict-provider/generated/v1/resources/resource.conformance-corpus.cbor @@ -0,0 +1 @@ +ecaseseclasskdeclarativejapiVersionx)echo.edict-provider.conformance-corpus/v1joperationsga.b@1.tlcapabilitiesrecho.dpo@1.replaceosemanticEffectsntarget.replace \ No newline at end of file diff --git a/schemas/edict-provider/generated/v1/resources/resource.lawpack-compatibility.cbor b/schemas/edict-provider/generated/v1/resources/resource.lawpack-compatibility.cbor new file mode 100644 index 00000000..a703b6fc --- /dev/null +++ b/schemas/edict-provider/generated/v1/resources/resource.lawpack-compatibility.cbor @@ -0,0 +1 @@ +eclasskdeclarativejapiVersionx,echo.edict-provider.lawpack-compatibility/v1oacceptedCoreAbimedict.core/v1osemanticEffectsntarget.replacevacceptedTargetProfilesjecho.dpo@1 \ No newline at end of file diff --git a/schemas/edict-provider/generated/v1/resources/resource.lawpack-exports.cbor b/schemas/edict-provider/generated/v1/resources/resource.lawpack-exports.cbor new file mode 100644 index 00000000..a0887cb0 --- /dev/null +++ b/schemas/edict-provider/generated/v1/resources/resource.lawpack-exports.cbor @@ -0,0 +1 @@ +etypesjcoordinateha.b@1.Idjdefinitionx!StringgeffectsiinputTypeha.b@1.Idjcoordinatentarget.replacejoutputTypema.b@1.ReceiptlguardSupportncostObligationstarget.replace.costneffectFailureshrejectedkpayloadTypewtarget.replace.rejectednauthorityClassndomainMappableneffectKindHintgreplacenexecutionClassgruntimentypeParameterssfootprintObligationxtarget.replace.footprinticonstantslobstructionsjcoordinatetdomain.WriteRejectedmpayloadSchemaxdomain.WriteRejected.PayloadnauthorityClassndomainMappablempureFunctionsqoperationProfiles \ No newline at end of file diff --git a/schemas/edict-provider/generated/v1/resources/resource.lawpack-target-adapter.cbor b/schemas/edict-provider/generated/v1/resources/resource.lawpack-target-adapter.cbor new file mode 100644 index 00000000..5a57d65b --- /dev/null +++ b/schemas/edict-provider/generated/v1/resources/resource.lawpack-target-adapter.cbor @@ -0,0 +1 @@ +eclasskdeclarativejapiVersionx-echo.edict-provider.lawpack-target-adapter/v1mtargetProfilejecho.dpo@1ntargetIrDomainoecho.span-ir/v1ueffectImplementationsntarget.replacedkindfnativejcapabilityrecho.dpo@1.replacejwriteClassgreplace \ No newline at end of file diff --git a/schemas/edict-provider/generated/v1/resources/resource.lawpack-verifier.cbor b/schemas/edict-provider/generated/v1/resources/resource.lawpack-verifier.cbor new file mode 100644 index 00000000..a6d10bd7 --- /dev/null +++ b/schemas/edict-provider/generated/v1/resources/resource.lawpack-verifier.cbor @@ -0,0 +1 @@ +eclasskdeclarativejapiVersionx'echo.edict-provider.lawpack-verifier/v1uoperationObstructionsga.b@1.tfeffectntarget.replaceofailureMappingshrejectedtdomain.WriteRejected \ No newline at end of file diff --git a/schemas/edict-provider/generated/v1/resources/resource.target-bundle-profile.cbor b/schemas/edict-provider/generated/v1/resources/resource.target-bundle-profile.cbor new file mode 100644 index 00000000..4c13093f --- /dev/null +++ b/schemas/edict-provider/generated/v1/resources/resource.target-bundle-profile.cbor @@ -0,0 +1 @@ +eclasskdeclarativejapiVersionrecho.dpo.bundle/v1oreadConsistencytapplication-snapshotpapplicationModelfatomicqoperationProfilesxcontinuum.profile.write/v1 \ No newline at end of file diff --git a/schemas/edict-provider/generated/v1/resources/resource.target-cost-algebra.cbor b/schemas/edict-provider/generated/v1/resources/resource.target-cost-algebra.cbor new file mode 100644 index 00000000..2242fed3 --- /dev/null +++ b/schemas/edict-provider/generated/v1/resources/resource.target-cost-algebra.cbor @@ -0,0 +1 @@ +eclasskdeclarativejapiVersionpecho.dpo.cost/v1lcapabilitiesrecho.dpo@1.replacefeffectntarget.replacelcostTemplatestarget.replace.costrsemanticObligationstarget.replace.cost \ No newline at end of file diff --git a/schemas/edict-provider/generated/v1/resources/resource.target-footprint-algebra.cbor b/schemas/edict-provider/generated/v1/resources/resource.target-footprint-algebra.cbor new file mode 100644 index 00000000..3118bbd0 --- /dev/null +++ b/schemas/edict-provider/generated/v1/resources/resource.target-footprint-algebra.cbor @@ -0,0 +1 @@ +eclasskdeclarativejapiVersionuecho.dpo.footprint/v1lcapabilitiesrecho.dpo@1.replacefeffectntarget.replacejwriteClassgreplaceqfootprintTemplatextarget.replace.footprintrsemanticObligationxtarget.replace.footprint \ No newline at end of file diff --git a/schemas/edict-provider/generated/v1/resources/resource.target-intrinsics.cbor b/schemas/edict-provider/generated/v1/resources/resource.target-intrinsics.cbor new file mode 100644 index 00000000..3250617a --- /dev/null +++ b/schemas/edict-provider/generated/v1/resources/resource.target-intrinsics.cbor @@ -0,0 +1 @@ +japiVersionx"edict.target-profile.intrinsics/v1jintrinsicsrecho.dpo@1.replacejeffectKindgreplacejreturnTypema.b@1.ReceiptjwriteClassgreplacelcostTemplatestarget.replace.costlguardSupportmargumentTypesha.b@1.IdneffectFailureshrejectedkpayloadTypewtarget.replace.rejectednauthorityClassndomainMappablenintrinsicClassfeffectntypeParametersqfootprintTemplatextarget.replace.footprintxcanParticipateInAtomicGuard \ No newline at end of file diff --git a/schemas/edict-provider/generated/v1/resources/resource.target-ir.cbor b/schemas/edict-provider/generated/v1/resources/resource.target-ir.cbor new file mode 100644 index 00000000..47cd6d6a --- /dev/null +++ b/schemas/edict-provider/generated/v1/resources/resource.target-ir.cbor @@ -0,0 +1 @@ +eclasskdeclarativefdomainoecho.span-ir/v1japiVersionoecho.span-ir/v1lcapabilitiesrecho.dpo@1.replacemtargetProfilejecho.dpo@1 \ No newline at end of file diff --git a/schemas/edict-provider/generated/v1/resources/resource.target-lowerer-contract.cbor b/schemas/edict-provider/generated/v1/resources/resource.target-lowerer-contract.cbor new file mode 100644 index 00000000..102f1d90 --- /dev/null +++ b/schemas/edict-provider/generated/v1/resources/resource.target-lowerer-contract.cbor @@ -0,0 +1 @@ +eclasskdeclarativejapiVersionsecho.dpo.lowerer/v1loutputDomainoecho.span-ir/v1mtargetProfilejecho.dpo@1nopticContractsxcontinuum.profile.write/v1mreplace-pointoacceptedCoreAbimedict.core/v1ueffectImplementationsntarget.replacedkindfnativejcapabilityrecho.dpo@1.replacejwriteClassgreplace \ No newline at end of file diff --git a/schemas/edict-provider/generated/v1/resources/resource.target-obstruction-taxonomy.cbor b/schemas/edict-provider/generated/v1/resources/resource.target-obstruction-taxonomy.cbor new file mode 100644 index 00000000..a15d8a30 --- /dev/null +++ b/schemas/edict-provider/generated/v1/resources/resource.target-obstruction-taxonomy.cbor @@ -0,0 +1 @@ +eclasskdeclarativejapiVersionxecho.dpo.obstructions/v1neffectFailureswtarget.replace.rejectedkpayloadTypewtarget.replace.rejectednauthorityClassndomainMappablerdomainObstructionstdomain.WriteRejectedmpayloadSchemaxdomain.WriteRejected.PayloadnauthorityClassndomainMappable \ No newline at end of file diff --git a/schemas/edict-provider/generated/v1/resources/resource.target-operation-profiles.cbor b/schemas/edict-provider/generated/v1/resources/resource.target-operation-profiles.cbor new file mode 100644 index 00000000..f4445c6a --- /dev/null +++ b/schemas/edict-provider/generated/v1/resources/resource.target-operation-profiles.cbor @@ -0,0 +1 @@ +hprofilesxcontinuum.profile.write/v1mopticTemplateiopticKindsaffectReintegrationlboundaryKindfaffectmsupportPolicyx&continuum.support.carry-or-obstruct/v1olossDispositionx#continuum.support.reject-on-loss/v1sapertureRequirementcrefxtarget.replace.footprintdkindxabstractFootprintObligationoeffectPredicatex'echo.dpo.operation-mode.replace-only/v1japiVersionx*edict.target-profile.operation-profiles/v1 \ No newline at end of file diff --git a/schemas/edict-provider/generated/v1/resources/resource.target-verifier-contract.cbor b/schemas/edict-provider/generated/v1/resources/resource.target-verifier-contract.cbor new file mode 100644 index 00000000..6fd4f838 --- /dev/null +++ b/schemas/edict-provider/generated/v1/resources/resource.target-verifier-contract.cbor @@ -0,0 +1 @@ +eclasskdeclarativejapiVersiontecho.dpo.verifier/v1lcapabilitiesrecho.dpo@1.replacemtargetProfilejecho.dpo@1nopticContractsxcontinuum.profile.write/v1mreplace-pointntargetIrDomainoecho.span-ir/v1qoperationProfilesxcontinuum.profile.write/v1 \ No newline at end of file diff --git a/schemas/edict-provider/generation-settings-v1.json b/schemas/edict-provider/generation-settings-v1.json new file mode 100644 index 00000000..ab8cc166 --- /dev/null +++ b/schemas/edict-provider/generation-settings-v1.json @@ -0,0 +1,7 @@ +{ + "apiVersion": "echo.edict-provider-generation-settings/v1", + "shapeSource": "none", + "canonicalArtifactEncoding": "edict.canonical-cbor/v1", + "contractPack": "edict.provider-contract-pack.cddl@1", + "generatorAbi": "wesley.extension-generator/v1" +}