diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e6a210..da5158a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,8 +8,53 @@ versions still track specification maturity rather than a released product. ## [Unreleased] +### Changed + +- Rejected many-to-one target obstruction mappings before Target IR emission so + failure-coordinate collisions cannot silently discard an obstruction arm. +- Hardened standalone application builds around complete lawpack dependency + closures, selected target-adapter identity, all provider-bound schema roles, + pure-helper call closure, canonical settings and application paths, mapped + target obstructions, exact verifier acceptance, safe provider roles, shared + domain-framed artifact identities, and rollback-safe package/report + publication. + ### Added +- Added the public `edict` CLI `build` operation for standalone applications. + A settings-only JSONL request loads one exact `edict.application/v1` + manifest, source, complete lawpack dependency closure, direct target adapter, + target configuration, selected provider profile, and checked provider + package. Edict compiles and lowers the real source, invokes the provider's + lowerer and structurally separate verifier through the capability-denied + Wasmtime host, and writes only the accepted provider-emitted package and + verification-report bytes. It does not reimplement the provider encoder or + execute the package. +- Added the generator-owned portable `causal.cell@1.createIfAbsent` lawpack + closure for external applications. `cargo xtask lawpack-goldens` now + reproduces its canonical manifest, exports, direct Echo adapter, target + configuration, and digest sidecars only after validating the closure and + compiling a digest-pinned Edict witness through Target IR. +- Added the first executable `edict.lawpack/v1` loader. Exact canonical + manifests and export surfaces decode into an opaque typed bundle, corroborate + the export digest, validate the closed verifier, helper, effect, obstruction, + and operation-profile shapes, and reject incomplete, substituted, or cyclic + dependency sets. The reviewed Hello Echo fixture includes canonical bytes, + exact digests, and a real `createGreeting` source import checked by + `cargo xtask lawpack-goldens`. +- Added the direct declarative `edict.lawpack-adapter/v1` ABI. Exact canonical + adapter bytes are selected and digest-bound by a validated lawpack, must + completely discharge exported operation-profile, runtime-effect, footprint, + cost, budget, and named-failure obligations, bind each runtime effect to an + exact target-owned configuration resource, and derive compiler and Target IR + facts through the source module's exact digest-locked import. Edict preserves + the configuration identity without interpreting target semantics. The Hello + Echo source now lowers to `echo.span-ir/v1` without a caller-built + `CompilerContext` or `TargetIrLoweringFacts`, and its compiler-produced Core + and Target IR bytes are reviewed goldens. Target profiles accept only the + exact direct adapter ABI, and the self-contained provider contract pack + publishes its CDDL root. + - Added `EDICT.md`, a comprehensive cited introduction and deep-dive report: hello-world walkthrough, feature deep dive, plain-English walkthrough with glossary and diagrams, unique technical details, roadmap discussion, and a @@ -31,13 +76,17 @@ versions still track specification maturity rather than a released product. repetitions still fail before a registry exists. Recursive variable occurrences and tagged choices use an Edict-owned specialization pass; tagged choices dispatch by a required literal map key without depending on - declaration or encoded-entry order. Construction rejects any recursive shape - the finite specializer cannot preserve exactly, including ambiguous map-key - assignment and multiple or non-final variable array members. Scalar map-key - predicates retain exact pinned-validator semantics, including `.regexp`. - Specialized values select an arm before child traversal, then cross canonical - encoding and the exact 50-container limit before `cddl-cat 0.7.1` validation; - duplicate keys and one-over-limit values return the stable schema mismatch. + declaration or encoded-entry order. A two-arm recursive map choice may also + dispatch on one exact required text key that the other closed arm cannot + accept, admitting the closed-versus-legacy Target IR compatibility union + without weakening ambiguous same-tag choices. Optional or wildcard overlap + still rejects. Construction rejects any recursive shape the finite + specializer cannot preserve exactly, including ambiguous map-key assignment + and multiple or non-final variable array members. Scalar map-key predicates + retain exact pinned-validator semantics, including `.regexp`. Specialized + values select an arm before child traversal, then cross canonical encoding + and the exact 50-container limit before `cddl-cat 0.7.1` validation; duplicate + keys and one-over-limit values return the stable schema mismatch. - Added a deterministic Apache-2.0 provider contract pack for runtime-owned generators. The checked manifest binds one self-contained CDDL document, diff --git a/Cargo.lock b/Cargo.lock index ea721b0..95cb482 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -352,6 +352,8 @@ dependencies = [ name = "edict-cli" version = "0.11.0-alpha.1" dependencies = [ + "edict-provider-host-wasmtime", + "edict-provider-schema", "edict-syntax", "glob", "serde", diff --git a/crates/edict-cli/Cargo.toml b/crates/edict-cli/Cargo.toml index 754535c..f721cbb 100644 --- a/crates/edict-cli/Cargo.toml +++ b/crates/edict-cli/Cargo.toml @@ -16,6 +16,8 @@ name = "edict" path = "src/main.rs" [dependencies] +edict-provider-host-wasmtime = { path = "../edict-provider-host-wasmtime" } +edict-provider-schema = { path = "../edict-provider-schema" } edict-syntax = { path = "../edict-syntax" } glob = "0.3" serde = { version = "1", features = ["derive"] } diff --git a/crates/edict-cli/src/application_build.rs b/crates/edict-cli/src/application_build.rs new file mode 100644 index 0000000..16a87be --- /dev/null +++ b/crates/edict-cli/src/application_build.rs @@ -0,0 +1,2009 @@ +use std::collections::BTreeSet; +use std::ffi::OsString; +use std::fs::{self, OpenOptions}; +use std::io::{ErrorKind, Read, Write}; +use std::path::{Component, Path, PathBuf}; +use std::sync::Arc; +use std::time::{SystemTime, UNIX_EPOCH}; + +use edict_provider_host_wasmtime::{ + ProviderComponentHost, ProviderHostLimits, ResolvedProviderComponent, +}; +use edict_provider_schema::{ProviderArtifactSchemaRegistry, ResolvedProviderSchemaArtifact}; +use edict_syntax::{ + bind_target_provider_manifest, compile_to_core, decode_canonical_cbor, decode_lawpack_adapter, + decode_lawpack_bundle, digest_canonical_artifact, encode_canonical_cbor, encode_core_module, + encode_target_ir_artifact, lower_to_target_ir, parse_module, prepare_lawpack_compilation, + select_provider_component, validate_lawpack_dependency_graph, + validate_provider_lowering_request, validate_provider_verification_request, CanonicalValue, + ProviderArtifact, ProviderArtifactBinding, ProviderArtifactKind, ProviderArtifactRef, + ProviderBoundArtifact, ProviderDigest, ProviderDigestAlgorithm, ProviderInvocationKind, + ProviderLoweringInvocationContract, ProviderLoweringOutputKind, ProviderLoweringOutputRequest, + ProviderLoweringRequest, ProviderResourceRef, ProviderResponseLimits, ProviderSemanticInput, + ProviderSemanticInputBinding, ProviderSemanticInputKind, + ProviderVerificationInvocationContract, ProviderVerificationOutputKind, + ProviderVerificationOutputRequest, ProviderVerificationRequest, TargetLoweringStatus, + TargetProviderManifest, ValidatedLawpackBundle, ValidatedTargetProviderManifest, + CORE_MODULE_DIGEST_DOMAIN, PROVIDER_LAWPACK_ARTIFACT_DOMAIN, TARGET_IR_ARTIFACT_DIGEST_DOMAIN, + TARGET_PROFILE_API_VERSION, TARGET_PROVIDER_PROTOCOL_VERSION, +}; +use serde::Deserialize; + +const APPLICATION_SCHEMA: &str = "edict.application/v1"; +const SOURCE_DOMAIN: &str = "edict.source/v1"; +const EXPORTS_DOMAIN: &str = "edict.lawpack-exports/v1"; +const ADAPTER_DOMAIN: &str = "edict.lawpack-adapter/v1"; +const PACKAGE_ROLE: &str = "executable-operation-package.echo"; +const PACKAGE_DOMAIN: &str = "echo.operation-package/v1"; +const VERIFICATION_REPORT_ROLE: &str = "verifier-report.echo-operation"; +const VERIFICATION_REPORT_DOMAIN: &str = "echo.operation-package-verifier-report/v1"; +const MAX_APPLICATION_ARTIFACT_BYTES: u64 = 1024 * 1024; + +#[derive(Debug)] +pub(crate) struct ApplicationBuildFailure { + pub(crate) kind: &'static str, + pub(crate) message: String, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct ApplicationManifest { + schema: String, + coordinate: String, + sources: Vec, + lawpacks: Vec, + target: ApplicationTarget, + output_directory: PathBuf, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct ApplicationLawpack { + manifest: PathBuf, + exports: PathBuf, + adapter: PathBuf, + target_configuration: PathBuf, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct ApplicationTarget { + profile: String, + provider_package: PathBuf, +} + +struct LoadedLawpack { + manifest_bytes: Vec, + exports_bytes: Vec, + adapter_bytes: Vec, + configuration_bytes: Vec, + bundle: ValidatedLawpackBundle, +} + +struct ProviderInvocationContext<'a> { + coordinate: &'a str, + core_bytes: &'a [u8], + target_profile: &'a ProviderBoundArtifact, + loaded: &'a LoadedLawpack, + adapter: &'a edict_syntax::ValidatedLawpackAdapter, + source_bytes: &'a [u8], + target_ir_bytes: &'a [u8], +} + +#[allow( + clippy::too_many_lines, + reason = "the application build keeps its ordered authority-boundary crossing explicit" +)] +pub(crate) fn build_application(config_path: &Path) -> Result<(), ApplicationBuildFailure> { + let config_bytes = read( + config_path, + "ApplicationConfigReadFailed", + "application config", + )?; + let config = serde_json::from_slice::(&config_bytes).map_err(|error| { + failure( + "InvalidApplicationConfig", + format!( + "application config `{}` is not valid edict.application/v1 JSON: {error}", + config_path.display() + ), + ) + })?; + validate_application_manifest(&config)?; + let root = canonical_application_root(config_path)?; + + let source = config.sources.first().ok_or_else(|| { + failure( + "InvalidApplicationConfig", + "the executable-operation build requires exactly one Edict source", + ) + })?; + let source_path = confined_existing_path( + &root, + source, + "sources", + "ApplicationSourceReadFailed", + "Edict source", + )?; + let source_bytes = read(&source_path, "ApplicationSourceReadFailed", "Edict source")?; + let source = std::str::from_utf8(&source_bytes).map_err(|error| { + failure( + "InvalidApplicationSource", + format!( + "Edict source `{}` is not UTF-8: {error}", + source_path.display() + ), + ) + })?; + let module = parse_module(source).map_err(|error| { + failure( + "InvalidApplicationSource", + format!( + "Edict source `{}` did not parse: {error:?}", + source_path.display() + ), + ) + })?; + + let mut loaded_lawpacks = Vec::with_capacity(config.lawpacks.len()); + for lawpack in &config.lawpacks { + loaded_lawpacks.push(load_lawpack(&root, lawpack)?); + } + let bundles = loaded_lawpacks + .iter() + .map(|loaded| loaded.bundle.clone()) + .collect::>(); + validate_lawpack_dependency_graph(&bundles).map_err(|failures| { + failure( + "InvalidLawpackClosure", + format!("application lawpack dependency closure is invalid: {failures:?}"), + ) + })?; + let loaded = loaded_lawpacks.first().ok_or_else(|| { + failure( + "InvalidApplicationConfig", + "the executable-operation build requires a root lawpack", + ) + })?; + + let provider_root = confined_existing_path( + &root, + &config.target.provider_package, + "target.providerPackage", + "ProviderPackageReadFailed", + "provider package", + )?; + let manifest_path = find_provider_manifest(&provider_root)?; + let provider_manifest_bytes = read( + &manifest_path, + "ProviderPackageReadFailed", + "provider manifest", + )?; + let provider_manifest = serde_json::from_slice::( + &provider_manifest_bytes, + ) + .map_err(|error| { + failure( + "InvalidProviderPackage", + format!( + "provider manifest `{}` is not valid typed JSON: {error}", + manifest_path.display() + ), + ) + })?; + let provider_proof = bind_target_provider_manifest(&provider_manifest).map_err(|report| { + failure( + "InvalidProviderPackage", + format!("provider manifest validation failed: {report:?}"), + ) + })?; + let target_profile_artifact = unique_artifact( + &provider_manifest, + ProviderArtifactKind::TargetProfile, + Some(&config.target.profile), + )?; + let target_profile_bytes = read_provider_artifact( + &provider_root, + target_profile_artifact, + ProviderArtifactKind::TargetProfile, + )?; + let target_profile = bound_artifact( + &target_profile_artifact.resource.coordinate, + TARGET_PROFILE_API_VERSION, + &target_profile_bytes, + )?; + require_manifest_identity(target_profile_artifact, &target_profile)?; + + let schema_artifacts = provider_schema_artifacts(&provider_manifest)?; + let resolved_schema_artifacts = schema_artifacts + .into_iter() + .map(|schema_artifact| { + Ok(ResolvedProviderSchemaArtifact { + role: schema_artifact.role.clone(), + bytes: Arc::<[u8]>::from(read_provider_artifact( + &provider_root, + schema_artifact, + ProviderArtifactKind::ArtifactSchema, + )?), + }) + }) + .collect::, ApplicationBuildFailure>>()?; + let required_domains = provider_manifest + .schema_bindings + .iter() + .map(|binding| binding.domain.as_str()); + let registry = ProviderArtifactSchemaRegistry::from_manifest( + &provider_proof, + resolved_schema_artifacts, + required_domains, + ) + .map_err(|error| { + failure( + "InvalidProviderPackage", + format!("provider artifact-schema registry failed: {error}"), + ) + })?; + + let adapter = decode_lawpack_adapter( + &loaded.bundle, + &config.target.profile, + &loaded.adapter_bytes, + ) + .map_err(|failures| { + failure( + "InvalidLawpackAdapter", + format!("lawpack target adapter is invalid: {failures:?}"), + ) + })?; + if adapter.target_profile().digest_review_string() + != rendered_digest(&target_profile.reference.digest) + { + return Err(failure( + "TargetProfileMismatch", + "lawpack adapter target profile digest does not match the selected provider profile", + )); + } + validate_target_configuration_binding(&adapter, &loaded.configuration_bytes)?; + + let preparation = + prepare_lawpack_compilation(&module, &loaded.bundle, &adapter).map_err(|failures| { + failure( + "InvalidApplicationClosure", + format!("source and lawpack closure do not corroborate: {failures:?}"), + ) + })?; + let core = compile_to_core(&module, preparation.compiler_context()).map_err(|error| { + failure( + "ApplicationCompilationFailed", + format!("Edict application did not compile to Core: {error:?}"), + ) + })?; + if core.coordinate != config.coordinate { + return Err(failure( + "ApplicationCoordinateMismatch", + format!( + "application config coordinate `{}` does not match source coordinate `{}`", + config.coordinate, core.coordinate + ), + )); + } + let target_ir_report = lower_to_target_ir(&core, preparation.target_ir_facts()); + if target_ir_report.status != TargetLoweringStatus::Lowered { + return Err(failure( + "TargetLoweringFailed", + format!( + "application did not lower through the selected target adapter: {:?}", + target_ir_report.failures + ), + )); + } + let target_ir = target_ir_report.artifact.ok_or_else(|| { + failure( + "TargetLoweringFailed", + "target lowering reported success without an artifact", + ) + })?; + + let core_bytes = encode_core_module(&core).map_err(|error| { + failure( + "ApplicationEncodingFailed", + format!("Core canonical encoding failed: {error}"), + ) + })?; + let target_ir_bytes = encode_target_ir_artifact(&target_ir).map_err(|error| { + failure( + "ApplicationEncodingFailed", + format!("Target IR canonical encoding failed: {error}"), + ) + })?; + let source_artifact_bytes = encode_canonical_cbor(&CanonicalValue::Bytes(source_bytes)) + .map_err(|error| { + failure( + "ApplicationEncodingFailed", + format!("source canonical encoding failed: {error}"), + ) + })?; + + let lowerer_artifact = + unique_artifact(&provider_manifest, ProviderArtifactKind::Lowerer, None)?; + let verifier_artifact = + unique_artifact(&provider_manifest, ProviderArtifactKind::Verifier, None)?; + let lowerer_bytes = read_provider_artifact( + &provider_root, + lowerer_artifact, + ProviderArtifactKind::Lowerer, + )?; + let verifier_bytes = read_provider_artifact( + &provider_root, + verifier_artifact, + ProviderArtifactKind::Verifier, + )?; + + let host = ProviderComponentHost::new().map_err(|error| { + failure( + "ProviderHostFailed", + format!("provider host configuration failed: {error}"), + ) + })?; + let invocation = ProviderInvocationContext { + coordinate: &core.coordinate, + core_bytes: &core_bytes, + target_profile: &target_profile, + loaded, + adapter: &adapter, + source_bytes: &source_artifact_bytes, + target_ir_bytes: &target_ir_bytes, + }; + let package_bytes = invoke_lowerer( + &host, + provider_proof, + ®istry, + lowerer_artifact, + lowerer_bytes, + &invocation, + )?; + let report_bytes = invoke_verifier( + &host, + provider_proof, + ®istry, + verifier_artifact, + verifier_bytes, + &invocation, + &package_bytes, + )?; + require_accepted_report(&report_bytes)?; + + let output_directory = prepare_output_directory(&root, &config.output_directory)?; + write_outputs(&output_directory, &package_bytes, &report_bytes) +} + +fn validate_application_manifest( + config: &ApplicationManifest, +) -> Result<(), ApplicationBuildFailure> { + if config.schema != APPLICATION_SCHEMA { + return Err(failure( + "InvalidApplicationConfig", + format!( + "application schema must be `{APPLICATION_SCHEMA}`, got `{}`", + config.schema + ), + )); + } + if config.coordinate.is_empty() + || config.target.profile.is_empty() + || config.target.provider_package.as_os_str().is_empty() + || config.output_directory.as_os_str().is_empty() + { + return Err(failure( + "InvalidApplicationConfig", + "application coordinate, target profile, provider package, and output directory must be non-empty", + )); + } + if config.sources.len() != 1 { + return Err(failure( + "InvalidApplicationConfig", + "the executable-operation build currently requires exactly one Edict source", + )); + } + if config.lawpacks.is_empty() { + return Err(failure( + "InvalidApplicationConfig", + "the executable-operation build requires one root lawpack followed by its complete dependency closure", + )); + } + let paths = config + .sources + .iter() + .map(|path| ("sources", path.as_path())) + .chain(config.lawpacks.iter().flat_map(|lawpack| { + [ + ("lawpacks.manifest", lawpack.manifest.as_path()), + ("lawpacks.exports", lawpack.exports.as_path()), + ("lawpacks.adapter", lawpack.adapter.as_path()), + ( + "lawpacks.targetConfiguration", + lawpack.target_configuration.as_path(), + ), + ] + })) + .chain([ + ( + "target.providerPackage", + config.target.provider_package.as_path(), + ), + ("outputDirectory", config.output_directory.as_path()), + ]); + for (field, path) in paths { + validate_application_path(field, path)?; + } + Ok(()) +} + +fn validate_application_path(field: &str, path: &Path) -> Result<(), ApplicationBuildFailure> { + if path.as_os_str().is_empty() + || path.components().any(|component| { + matches!( + component, + Component::Prefix(_) | Component::RootDir | Component::ParentDir + ) + }) + { + return Err(failure( + "InvalidApplicationConfig", + format!( + "application field `{field}` must be a non-empty relative path without parent traversal" + ), + )); + } + Ok(()) +} + +fn load_lawpack( + root: &Path, + config: &ApplicationLawpack, +) -> Result { + let manifest_path = confined_existing_path( + root, + &config.manifest, + "lawpacks.manifest", + "LawpackReadFailed", + "lawpack manifest", + )?; + let manifest_bytes = read(&manifest_path, "LawpackReadFailed", "lawpack manifest")?; + let exports_path = confined_existing_path( + root, + &config.exports, + "lawpacks.exports", + "LawpackReadFailed", + "lawpack exports", + )?; + let exports_bytes = read(&exports_path, "LawpackReadFailed", "lawpack exports")?; + let adapter_path = confined_existing_path( + root, + &config.adapter, + "lawpacks.adapter", + "LawpackReadFailed", + "lawpack target adapter", + )?; + let adapter_bytes = read(&adapter_path, "LawpackReadFailed", "lawpack target adapter")?; + let configuration_path = confined_existing_path( + root, + &config.target_configuration, + "lawpacks.targetConfiguration", + "LawpackReadFailed", + "target configuration", + )?; + let configuration_bytes = read( + &configuration_path, + "LawpackReadFailed", + "target configuration", + )?; + let bundle = decode_lawpack_bundle(&manifest_bytes, &exports_bytes).map_err(|failures| { + failure( + "InvalidLawpackClosure", + format!("lawpack manifest and exports are invalid: {failures:?}"), + ) + })?; + Ok(LoadedLawpack { + manifest_bytes, + exports_bytes, + adapter_bytes, + configuration_bytes, + bundle, + }) +} + +fn find_provider_manifest(root: &Path) -> Result { + let entries = fs::read_dir(root).map_err(|error| { + failure( + "ProviderPackageReadFailed", + format!( + "failed to read provider package `{}`: {error}", + root.display() + ), + ) + })?; + let mut matches = Vec::new(); + for entry in entries { + let entry = entry.map_err(|error| { + failure( + "ProviderPackageReadFailed", + format!( + "failed to enumerate provider package `{}`: {error}", + root.display() + ), + ) + })?; + let name = entry.file_name(); + let name = name.to_string_lossy(); + if name.starts_with("provider-manifest.") && name.ends_with(".json") { + matches.push(entry.path()); + } + } + matches.sort(); + match matches.as_slice() { + [path] => confined_existing_path( + root, + path.strip_prefix(root).map_err(|error| { + failure( + "ProviderPackageReadFailed", + format!( + "provider manifest `{}` is not beneath package root `{}`: {error}", + path.display(), + root.display() + ), + ) + })?, + "target.providerPackage.manifest", + "ProviderPackageReadFailed", + "provider manifest", + ), + _ => Err(failure( + "InvalidProviderPackage", + format!( + "provider package `{}` must contain exactly one provider-manifest.*.json", + root.display() + ), + )), + } +} + +fn unique_artifact<'a>( + manifest: &'a TargetProviderManifest, + kind: ProviderArtifactKind, + coordinate: Option<&str>, +) -> Result<&'a ProviderArtifactRef, ApplicationBuildFailure> { + let matches = manifest + .artifacts + .iter() + .filter(|artifact| { + artifact.artifact_kind == kind + && coordinate.is_none_or(|expected| artifact.resource.coordinate == expected) + }) + .collect::>(); + match matches.as_slice() { + [artifact] => Ok(*artifact), + _ => Err(failure( + "InvalidProviderPackage", + format!( + "provider manifest must expose exactly one {kind:?} artifact{}", + coordinate.map_or(String::new(), |value| format!(" for `{value}`")) + ), + )), + } +} + +fn provider_schema_artifacts( + manifest: &TargetProviderManifest, +) -> Result, ApplicationBuildFailure> { + let bound_roles = manifest + .schema_bindings + .iter() + .map(|binding| binding.schema_role.as_str()) + .collect::>(); + let mut artifacts = manifest + .artifacts + .iter() + .filter(|artifact| { + artifact.artifact_kind == ProviderArtifactKind::ArtifactSchema + && bound_roles.contains(artifact.role.as_str()) + }) + .collect::>(); + artifacts.sort_by(|left, right| left.role.cmp(&right.role)); + if artifacts.len() != bound_roles.len() || artifacts.is_empty() { + return Err(failure( + "InvalidProviderPackage", + "provider manifest must expose every artifact schema named by its schema bindings", + )); + } + Ok(artifacts) +} + +fn read_provider_artifact( + root: &Path, + artifact: &ProviderArtifactRef, + kind: ProviderArtifactKind, +) -> Result, ApplicationBuildFailure> { + let relative = match kind { + ProviderArtifactKind::Lowerer | ProviderArtifactKind::Verifier => { + PathBuf::from("components").join(format!("{}.component.wasm", artifact.role)) + } + ProviderArtifactKind::ArtifactSchema => PathBuf::from("generated") + .join("primary") + .join(format!("{}.cddl", artifact.role)), + ProviderArtifactKind::TargetProfile => PathBuf::from("generated") + .join("primary") + .join(format!("{}.cbor", artifact.role)), + _ => { + return Err(failure( + "InvalidProviderPackage", + format!("unsupported provider artifact path kind {kind:?}"), + )); + } + }; + let path = confined_existing_path( + root, + &relative, + "target.providerPackage.artifact", + "ProviderPackageReadFailed", + "provider artifact", + )?; + read(&path, "ProviderPackageReadFailed", "provider artifact") +} + +fn validate_target_configuration_binding( + adapter: &edict_syntax::ValidatedLawpackAdapter, + bytes: &[u8], +) -> Result<(), ApplicationBuildFailure> { + let reference = single_unique_configuration( + adapter + .effects() + .values() + .map(|effect| &effect.target_configuration), + )?; + let digest = provider_digest(&reference.id, bytes)?; + if reference.digest_review_string() != rendered_digest(&digest) { + return Err(failure( + "TargetConfigurationMismatch", + "target configuration bytes do not reproduce the adapter's digest-locked reference", + )); + } + Ok(()) +} + +fn invoke_lowerer( + host: &ProviderComponentHost, + provider_proof: ValidatedTargetProviderManifest<'_>, + registry: &ProviderArtifactSchemaRegistry, + component: &ProviderArtifactRef, + component_bytes: Vec, + invocation: &ProviderInvocationContext<'_>, +) -> Result, ApplicationBuildFailure> { + let selected = select_provider_component( + &provider_proof, + &component.role, + ProviderInvocationKind::Lowering, + ) + .map_err(|error| { + failure( + "InvalidProviderPackage", + format!("provider lowerer selection failed: {error}"), + ) + })?; + let resolved = ResolvedProviderComponent::new(selected, Arc::<[u8]>::from(component_bytes)); + let prepared = host.prepare(&resolved).map_err(|error| { + failure( + "ProviderLowererFailed", + format!("provider lowerer preflight failed: {error}"), + ) + })?; + + let core = bound_artifact( + invocation.coordinate, + CORE_MODULE_DIGEST_DOMAIN, + invocation.core_bytes, + )?; + let semantic_inputs = lowering_inputs( + invocation.loaded, + invocation.adapter, + invocation.coordinate, + invocation.source_bytes, + invocation.target_ir_bytes, + )?; + let contract = lowering_contract(&core, invocation.target_profile, &semantic_inputs); + let request = ProviderLoweringRequest { + protocol_version: TARGET_PROVIDER_PROTOCOL_VERSION, + core, + target_profile: invocation.target_profile.clone(), + semantic_inputs, + requested_outputs: vec![ProviderLoweringOutputRequest { + role: PACKAGE_ROLE.to_owned(), + kind: ProviderLoweringOutputKind::GeneratedArtifact, + domain: PACKAGE_DOMAIN.to_owned(), + }], + limits: response_limits(), + }; + let request = + validate_provider_lowering_request(registry, &contract, &request).map_err(|report| { + failure( + "InvalidProviderInvocation", + format!("provider lowering request validation failed: {report:?}"), + ) + })?; + let outcome = host + .invoke_lowerer(&prepared, &request, registry, host_limits()) + .map_err(|error| { + failure( + "ProviderLowererFailed", + format!("provider lowerer invocation failed: {error}"), + ) + })?; + if let Some(refusal) = outcome.refusal() { + return Err(failure( + "ProviderLowererRefused", + format!("provider lowerer refused application semantics: {refusal:?}"), + )); + } + let response = outcome.response().ok_or_else(|| { + failure( + "ProviderLowererFailed", + "provider lowerer returned neither a response nor a refusal", + ) + })?; + if !response.diagnostics.is_empty() { + return Err(failure( + "ProviderLowererFailed", + format!( + "provider lowerer returned diagnostics: {:?}", + response.diagnostics + ), + )); + } + let [output] = response.outputs.as_slice() else { + return Err(failure( + "ProviderLowererFailed", + "provider lowerer did not emit exactly one package", + )); + }; + Ok(output.artifact.bytes.clone()) +} + +fn invoke_verifier( + host: &ProviderComponentHost, + provider_proof: ValidatedTargetProviderManifest<'_>, + registry: &ProviderArtifactSchemaRegistry, + component: &ProviderArtifactRef, + component_bytes: Vec, + invocation: &ProviderInvocationContext<'_>, + package_bytes: &[u8], +) -> Result, ApplicationBuildFailure> { + let selected = select_provider_component( + &provider_proof, + &component.role, + ProviderInvocationKind::Verification, + ) + .map_err(|error| { + failure( + "InvalidProviderPackage", + format!("provider verifier selection failed: {error}"), + ) + })?; + let resolved = ResolvedProviderComponent::new(selected, Arc::<[u8]>::from(component_bytes)); + let prepared = host.prepare(&resolved).map_err(|error| { + failure( + "ProviderVerifierFailed", + format!("provider verifier preflight failed: {error}"), + ) + })?; + + let core = bound_artifact( + invocation.coordinate, + CORE_MODULE_DIGEST_DOMAIN, + invocation.core_bytes, + )?; + let target_ir = bound_artifact( + invocation.adapter.target_ir().id.as_str(), + TARGET_IR_ARTIFACT_DIGEST_DOMAIN, + invocation.target_ir_bytes, + )?; + let semantic_inputs = verification_inputs( + invocation.loaded, + invocation.adapter, + invocation.coordinate, + invocation.source_bytes, + package_bytes, + )?; + let contract = verification_contract( + &core, + invocation.target_profile, + &target_ir, + &semantic_inputs, + ); + let request = ProviderVerificationRequest { + protocol_version: TARGET_PROVIDER_PROTOCOL_VERSION, + core, + target_profile: invocation.target_profile.clone(), + target_ir, + semantic_inputs, + requested_outputs: vec![ProviderVerificationOutputRequest { + role: VERIFICATION_REPORT_ROLE.to_owned(), + kind: ProviderVerificationOutputKind::VerifierReport, + domain: VERIFICATION_REPORT_DOMAIN.to_owned(), + }], + limits: response_limits(), + }; + let request = validate_provider_verification_request(registry, &contract, &request).map_err( + |report| { + failure( + "InvalidProviderInvocation", + format!("provider verification request validation failed: {report:?}"), + ) + }, + )?; + let outcome = host + .invoke_verifier(&prepared, &request, registry, host_limits()) + .map_err(|error| { + failure( + "ProviderVerifierFailed", + format!("provider verifier invocation failed: {error}"), + ) + })?; + if let Some(refusal) = outcome.refusal() { + return Err(failure( + "ProviderVerifierRefused", + format!("provider verifier refused application semantics: {refusal:?}"), + )); + } + let response = outcome.response().ok_or_else(|| { + failure( + "ProviderVerifierFailed", + "provider verifier returned neither a response nor a refusal", + ) + })?; + if !response.diagnostics.is_empty() { + return Err(failure( + "ProviderVerificationRejected", + format!( + "provider verifier rejected the package: {:?}", + response.diagnostics + ), + )); + } + let [output] = response.outputs.as_slice() else { + return Err(failure( + "ProviderVerifierFailed", + "provider verifier did not emit exactly one report", + )); + }; + Ok(output.artifact.bytes.clone()) +} + +fn lowering_inputs( + loaded: &LoadedLawpack, + adapter: &edict_syntax::ValidatedLawpackAdapter, + coordinate: &str, + source_bytes: &[u8], + target_ir_bytes: &[u8], +) -> Result, ApplicationBuildFailure> { + let configuration = single_configuration(adapter)?; + let adapter_reference = selected_adapter_reference( + &loaded.bundle.manifest().target_adapters, + adapter.target_profile(), + )?; + Ok(vec![ + semantic_input( + "01-lawpack-adapter", + ProviderSemanticInputKind::Auxiliary("lawpack-adapter".to_owned()), + &adapter_reference.id, + ADAPTER_DOMAIN, + &loaded.adapter_bytes, + )?, + semantic_input( + "02-lawpack-exports", + ProviderSemanticInputKind::Auxiliary("lawpack-exports".to_owned()), + &loaded.bundle.manifest().exports.id, + EXPORTS_DOMAIN, + &loaded.exports_bytes, + )?, + semantic_input( + "03-lawpack", + ProviderSemanticInputKind::Lawpack, + &format!( + "{}@{}", + loaded.bundle.manifest().id, + loaded.bundle.manifest().version + ), + PROVIDER_LAWPACK_ARTIFACT_DOMAIN, + &loaded.manifest_bytes, + )?, + semantic_input( + "04-source", + ProviderSemanticInputKind::Auxiliary("edict-source".to_owned()), + coordinate, + SOURCE_DOMAIN, + source_bytes, + )?, + semantic_input( + "05-target-configuration", + ProviderSemanticInputKind::Auxiliary("target-configuration".to_owned()), + &configuration.id, + &configuration.id, + &loaded.configuration_bytes, + )?, + semantic_input( + "06-target-ir", + ProviderSemanticInputKind::Auxiliary("target-ir".to_owned()), + &adapter.target_ir().id, + TARGET_IR_ARTIFACT_DIGEST_DOMAIN, + target_ir_bytes, + )?, + ]) +} + +fn verification_inputs( + loaded: &LoadedLawpack, + adapter: &edict_syntax::ValidatedLawpackAdapter, + coordinate: &str, + source_bytes: &[u8], + package_bytes: &[u8], +) -> Result, ApplicationBuildFailure> { + let configuration = single_configuration(adapter)?; + let adapter_reference = selected_adapter_reference( + &loaded.bundle.manifest().target_adapters, + adapter.target_profile(), + )?; + Ok(vec![ + semantic_input( + "01-lawpack-adapter", + ProviderSemanticInputKind::Auxiliary("lawpack-adapter".to_owned()), + &adapter_reference.id, + ADAPTER_DOMAIN, + &loaded.adapter_bytes, + )?, + semantic_input( + "02-executable-operation-package", + ProviderSemanticInputKind::Auxiliary("executable-operation-package".to_owned()), + PACKAGE_ROLE, + PACKAGE_DOMAIN, + package_bytes, + )?, + semantic_input( + "03-lawpack-exports", + ProviderSemanticInputKind::Auxiliary("lawpack-exports".to_owned()), + &loaded.bundle.manifest().exports.id, + EXPORTS_DOMAIN, + &loaded.exports_bytes, + )?, + semantic_input( + "04-lawpack", + ProviderSemanticInputKind::Lawpack, + &format!( + "{}@{}", + loaded.bundle.manifest().id, + loaded.bundle.manifest().version + ), + PROVIDER_LAWPACK_ARTIFACT_DOMAIN, + &loaded.manifest_bytes, + )?, + semantic_input( + "05-source", + ProviderSemanticInputKind::Auxiliary("edict-source".to_owned()), + coordinate, + SOURCE_DOMAIN, + source_bytes, + )?, + semantic_input( + "06-target-configuration", + ProviderSemanticInputKind::Auxiliary("target-configuration".to_owned()), + &configuration.id, + &configuration.id, + &loaded.configuration_bytes, + )?, + ]) +} + +fn single_configuration( + adapter: &edict_syntax::ValidatedLawpackAdapter, +) -> Result<&edict_syntax::LawpackResourceRef, ApplicationBuildFailure> { + single_unique_configuration( + adapter + .effects() + .values() + .map(|effect| &effect.target_configuration), + ) +} + +fn single_unique_configuration<'a>( + references: impl IntoIterator, +) -> Result<&'a edict_syntax::LawpackResourceRef, ApplicationBuildFailure> { + let references = references.into_iter().collect::>(); + let mut references = references.into_iter(); + match (references.next(), references.next()) { + (Some(reference), None) => Ok(reference), + _ => Err(failure( + "InvalidLawpackAdapter", + "the executable-operation adapter currently requires exactly one target configuration", + )), + } +} + +fn selected_adapter_reference<'a>( + adapters: &'a [edict_syntax::LawpackTargetAdapter], + selected_target_profile: &edict_syntax::LawpackResourceRef, +) -> Result<&'a edict_syntax::LawpackResourceRef, ApplicationBuildFailure> { + let mut matches = adapters + .iter() + .filter(|descriptor| descriptor.accepted_target_profile == *selected_target_profile); + match (matches.next(), matches.next()) { + (Some(descriptor), None) => Ok(&descriptor.adapter), + _ => Err(failure( + "InvalidLawpackAdapter", + "the selected target profile must identify exactly one lawpack adapter", + )), + } +} + +fn semantic_input( + role: &str, + kind: ProviderSemanticInputKind, + coordinate: &str, + domain: &str, + bytes: &[u8], +) -> Result { + Ok(ProviderSemanticInput { + role: role.to_owned(), + kind, + artifact: bound_artifact(coordinate, domain, bytes)?, + }) +} + +fn lowering_contract( + core: &ProviderBoundArtifact, + target_profile: &ProviderBoundArtifact, + inputs: &[ProviderSemanticInput], +) -> ProviderLoweringInvocationContract { + ProviderLoweringInvocationContract { + core: artifact_binding(core), + target_profile: artifact_binding(target_profile), + semantic_inputs: input_bindings(inputs), + } +} + +fn verification_contract( + core: &ProviderBoundArtifact, + target_profile: &ProviderBoundArtifact, + target_ir: &ProviderBoundArtifact, + inputs: &[ProviderSemanticInput], +) -> ProviderVerificationInvocationContract { + ProviderVerificationInvocationContract { + core: artifact_binding(core), + target_profile: artifact_binding(target_profile), + target_ir: artifact_binding(target_ir), + semantic_inputs: input_bindings(inputs), + } +} + +fn input_bindings(inputs: &[ProviderSemanticInput]) -> Vec { + inputs + .iter() + .map(|input| ProviderSemanticInputBinding { + role: input.role.clone(), + kind: input.kind.clone(), + artifact: artifact_binding(&input.artifact), + }) + .collect() +} + +fn artifact_binding(bound: &ProviderBoundArtifact) -> ProviderArtifactBinding { + ProviderArtifactBinding { + reference: bound.reference.clone(), + domain: bound.artifact.domain.clone(), + } +} + +fn bound_artifact( + coordinate: &str, + domain: &str, + bytes: &[u8], +) -> Result { + Ok(ProviderBoundArtifact { + reference: ProviderResourceRef { + coordinate: coordinate.to_owned(), + digest: provider_digest(domain, bytes)?, + }, + artifact: ProviderArtifact { + domain: domain.to_owned(), + bytes: bytes.to_vec(), + }, + }) +} + +fn provider_digest(domain: &str, bytes: &[u8]) -> Result { + let digest = digest_canonical_artifact(domain, bytes).map_err(|error| { + failure( + "NonCanonicalApplicationArtifact", + format!("artifact under `{domain}` is not canonical CBOR: {error}"), + ) + })?; + Ok(ProviderDigest { + algorithm: ProviderDigestAlgorithm::Sha256, + bytes: digest.bytes().to_vec(), + }) +} + +fn require_manifest_identity( + manifest_artifact: &ProviderArtifactRef, + bound: &ProviderBoundArtifact, +) -> Result<(), ApplicationBuildFailure> { + if manifest_artifact.resource.digest.as_deref() + != Some(&rendered_digest(&bound.reference.digest)) + { + return Err(failure( + "ProviderArtifactDigestMismatch", + format!( + "provider artifact `{}` bytes do not reproduce the manifest digest", + manifest_artifact.role + ), + )); + } + Ok(()) +} + +fn rendered_digest(digest: &ProviderDigest) -> String { + let mut value = String::from("sha256:"); + for byte in &digest.bytes { + use std::fmt::Write as _; + let _ = write!(value, "{byte:02x}"); + } + value +} + +fn require_accepted_report(bytes: &[u8]) -> Result<(), ApplicationBuildFailure> { + let value = decode_canonical_cbor(bytes).map_err(|error| { + failure( + "ProviderVerificationRejected", + format!("verification report is not canonical CBOR: {error}"), + ) + })?; + let CanonicalValue::Map(entries) = value else { + return Err(failure( + "ProviderVerificationRejected", + "verification report is not a canonical map", + )); + }; + let mut outcomes = entries + .iter() + .filter(|(key, _)| matches!(key, CanonicalValue::Text(key) if key == "outcome")); + let accepted = matches!( + (outcomes.next(), outcomes.next()), + (Some((_, CanonicalValue::Text(outcome))), None) if outcome == "accepted" + ); + if !accepted { + return Err(failure( + "ProviderVerificationRejected", + "independent verifier report did not accept the emitted package", + )); + } + Ok(()) +} + +#[allow( + clippy::too_many_lines, + reason = "paired output publication keeps every rollback transition explicit" +)] +fn write_outputs( + directory: &Path, + package: &[u8], + report: &[u8], +) -> Result<(), ApplicationBuildFailure> { + fs::create_dir_all(directory).map_err(|error| { + failure( + "ApplicationOutputWriteFailed", + format!( + "failed to create application output directory `{}`: {error}", + directory.display() + ), + ) + })?; + + let lock_path = output_lock_path(directory); + let lock = OpenOptions::new() + .create(true) + .truncate(false) + .read(true) + .write(true) + .open(&lock_path) + .map_err(|error| { + failure( + "ApplicationOutputWriteFailed", + format!( + "failed to open application output lock `{}`: {error}", + lock_path.display() + ), + ) + })?; + lock.try_lock().map_err(|error| { + failure( + "ApplicationOutputWriteFailed", + format!( + "another application build owns output directory `{}`: {error}", + directory.display() + ), + ) + })?; + + let package_path = directory.join("executable-operation-package.cbor"); + let report_path = directory.join("verification-report.cbor"); + let package_existed = validate_output_target(&package_path)?; + let report_existed = validate_output_target(&report_path)?; + let transaction = create_output_transaction(directory)?; + let package_temp = transaction.join("new-package.cbor"); + let report_temp = transaction.join("new-report.cbor"); + let package_backup = transaction.join("previous-package.cbor"); + let report_backup = transaction.join("previous-report.cbor"); + + if let Err(error) = write_synced(&package_temp, package) { + let _ = fs::remove_dir_all(&transaction); + return Err(failure( + "ApplicationOutputWriteFailed", + format!("failed to stage `{}`: {error}", package_path.display()), + )); + } + if let Err(error) = write_synced(&report_temp, report) { + let _ = fs::remove_dir_all(&transaction); + return Err(failure( + "ApplicationOutputWriteFailed", + format!("failed to stage `{}`: {error}", report_path.display()), + )); + } + + if package_existed { + if let Err(error) = fs::rename(&package_path, &package_backup) { + let _ = fs::remove_dir_all(&transaction); + return Err(failure( + "ApplicationOutputWriteFailed", + format!( + "failed to preserve previous output `{}`: {error}", + package_path.display() + ), + )); + } + } + if report_existed { + if let Err(error) = fs::rename(&report_path, &report_backup) { + let rollback = restore_output(&package_backup, &package_path, package_existed); + if rollback.is_ok() { + let _ = fs::remove_dir_all(&transaction); + } + return Err(output_publication_failure( + &report_path, + &error, + rollback.err(), + )); + } + } + + if let Err(error) = fs::rename(&report_temp, &report_path) { + let rollback = restore_previous_outputs(&[ + OutputRecovery { + destination: &package_path, + backup: &package_backup, + existed: package_existed, + published: false, + }, + OutputRecovery { + destination: &report_path, + backup: &report_backup, + existed: report_existed, + published: false, + }, + ]); + if rollback.is_ok() { + let _ = fs::remove_dir_all(&transaction); + } + return Err(output_publication_failure( + &report_path, + &error, + rollback.err(), + )); + } + if let Err(error) = fs::rename(&package_temp, &package_path) { + let rollback = restore_previous_outputs(&[ + OutputRecovery { + destination: &package_path, + backup: &package_backup, + existed: package_existed, + published: false, + }, + OutputRecovery { + destination: &report_path, + backup: &report_backup, + existed: report_existed, + published: true, + }, + ]); + if rollback.is_ok() { + let _ = fs::remove_dir_all(&transaction); + } + return Err(output_publication_failure( + &package_path, + &error, + rollback.err(), + )); + } + + fs::remove_dir_all(&transaction).map_err(|error| { + failure( + "ApplicationOutputWriteFailed", + format!( + "published application outputs but failed to remove transaction `{}`: {error}", + transaction.display() + ), + ) + }) +} + +fn output_lock_path(directory: &Path) -> PathBuf { + let mut name = OsString::from("."); + name.push( + directory + .file_name() + .unwrap_or_else(|| std::ffi::OsStr::new("root")), + ); + name.push(".edict-application-build.lock"); + directory + .parent() + .unwrap_or_else(|| Path::new(".")) + .join(name) +} + +fn write_synced(path: &Path, bytes: &[u8]) -> std::io::Result<()> { + let mut file = OpenOptions::new().create_new(true).write(true).open(path)?; + file.write_all(bytes)?; + file.sync_all() +} + +fn validate_output_target(path: &Path) -> Result { + match fs::symlink_metadata(path) { + Ok(metadata) if metadata.file_type().is_file() => Ok(true), + Ok(_) => Err(failure( + "ApplicationOutputWriteFailed", + format!( + "application output target `{}` must be absent or a regular file", + path.display() + ), + )), + Err(error) if error.kind() == ErrorKind::NotFound => Ok(false), + Err(error) => Err(failure( + "ApplicationOutputWriteFailed", + format!( + "failed to inspect application output target `{}`: {error}", + path.display() + ), + )), + } +} + +fn create_output_transaction(directory: &Path) -> Result { + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_err(|error| { + failure( + "ApplicationOutputWriteFailed", + format!("system clock cannot name an output transaction: {error}"), + ) + })? + .as_nanos(); + for attempt in 0..16 { + let path = directory.join(format!( + ".edict-application-build-{}-{timestamp}-{attempt}.transaction", + std::process::id() + )); + match fs::create_dir(&path) { + Ok(()) => return Ok(path), + Err(error) if error.kind() == ErrorKind::AlreadyExists => {} + Err(error) => { + return Err(failure( + "ApplicationOutputWriteFailed", + format!( + "failed to create output transaction `{}`: {error}", + path.display() + ), + )); + } + } + } + Err(failure( + "ApplicationOutputWriteFailed", + format!( + "failed to allocate a unique output transaction in `{}`", + directory.display() + ), + )) +} + +struct OutputRecovery<'a> { + destination: &'a Path, + backup: &'a Path, + existed: bool, + published: bool, +} + +fn restore_previous_outputs(outputs: &[OutputRecovery<'_>; 2]) -> Result<(), String> { + let mut failures = Vec::new(); + for output in outputs.iter().filter(|output| output.published) { + if let Err(error) = fs::remove_file(output.destination) { + failures.push(format!( + "failed to remove partial output `{}`: {error}", + output.destination.display() + )); + } + } + for output in outputs { + if let Err(error) = restore_output(output.backup, output.destination, output.existed) { + failures.push(error); + } + } + if failures.is_empty() { + Ok(()) + } else { + Err(failures.join("; ")) + } +} + +fn restore_output(backup: &Path, destination: &Path, existed: bool) -> Result<(), String> { + if !existed { + return Ok(()); + } + fs::rename(backup, destination).map_err(|error| { + format!( + "failed to restore previous output `{}`: {error}", + destination.display() + ) + }) +} + +fn output_publication_failure( + path: &Path, + error: &std::io::Error, + rollback: Option, +) -> ApplicationBuildFailure { + let rollback = rollback.map_or(String::new(), |message| { + format!("; rollback was incomplete: {message}") + }); + failure( + "ApplicationOutputWriteFailed", + format!("failed to publish `{}`: {error}{rollback}", path.display()), + ) +} + +const fn response_limits() -> ProviderResponseLimits { + ProviderResponseLimits { + max_output_count: 1, + max_diagnostic_count: 8, + max_total_response_bytes: 64 * 1024, + } +} + +const fn host_limits() -> ProviderHostLimits { + ProviderHostLimits { + max_input_bytes: 1024 * 1024, + max_output_bytes: 3 * 1024 * 1024, + max_diagnostic_bytes: 3 * 1024 * 1024, + max_wasm_memory_bytes: 16 * 1024 * 1024, + max_table_elements: 10_000, + max_instances: 100, + max_memories: 8, + max_tables: 8, + max_wasm_fuel: 50_000_000, + max_hostcall_bytes: 4 * 1024 * 1024, + max_host_diagnostic_bytes: 512, + } +} + +fn read( + path: &Path, + kind: &'static str, + subject: &str, +) -> Result, ApplicationBuildFailure> { + let mut file = OpenOptions::new().read(true).open(path).map_err(|error| { + failure( + kind, + format!("failed to read {subject} `{}`: {error}", path.display()), + ) + })?; + let length = file + .metadata() + .map_err(|error| { + failure( + kind, + format!("failed to inspect {subject} `{}`: {error}", path.display()), + ) + })? + .len(); + if length > MAX_APPLICATION_ARTIFACT_BYTES { + return Err(artifact_too_large(path, subject)); + } + let mut bytes = Vec::new(); + Read::by_ref(&mut file) + .take(MAX_APPLICATION_ARTIFACT_BYTES + 1) + .read_to_end(&mut bytes) + .map_err(|error| { + failure( + kind, + format!("failed to read {subject} `{}`: {error}", path.display()), + ) + })?; + if u64::try_from(bytes.len()).map_or(true, |length| length > MAX_APPLICATION_ARTIFACT_BYTES) { + return Err(artifact_too_large(path, subject)); + } + Ok(bytes) +} + +fn artifact_too_large(path: &Path, subject: &str) -> ApplicationBuildFailure { + failure( + "ApplicationArtifactTooLarge", + format!( + "{subject} `{}` exceeds the {} byte application-artifact limit", + path.display(), + MAX_APPLICATION_ARTIFACT_BYTES + ), + ) +} + +fn canonical_application_root(config_path: &Path) -> Result { + let root = config_path + .parent() + .filter(|path| !path.as_os_str().is_empty()) + .unwrap_or_else(|| Path::new(".")); + fs::canonicalize(root).map_err(|error| { + failure( + "ApplicationConfigReadFailed", + format!( + "failed to resolve application root `{}`: {error}", + root.display() + ), + ) + }) +} + +fn confined_existing_path( + root: &Path, + relative: &Path, + field: &str, + read_failure_kind: &'static str, + subject: &str, +) -> Result { + let candidate = root.join(relative); + let canonical = fs::canonicalize(&candidate).map_err(|error| { + failure( + read_failure_kind, + format!( + "failed to resolve {subject} `{}`: {error}", + candidate.display() + ), + ) + })?; + require_path_beneath_root(root, &canonical, field)?; + Ok(canonical) +} + +fn prepare_output_directory( + root: &Path, + relative: &Path, +) -> Result { + let candidate = root.join(relative); + let mut existing_ancestor = candidate.as_path(); + while !existing_ancestor.exists() { + existing_ancestor = existing_ancestor.parent().ok_or_else(|| { + failure( + "ApplicationOutputWriteFailed", + format!( + "application output `{}` has no existing ancestor", + candidate.display() + ), + ) + })?; + } + let canonical_ancestor = fs::canonicalize(existing_ancestor).map_err(|error| { + failure( + "ApplicationOutputWriteFailed", + format!( + "failed to resolve output ancestor `{}`: {error}", + existing_ancestor.display() + ), + ) + })?; + require_path_beneath_root(root, &canonical_ancestor, "outputDirectory")?; + fs::create_dir_all(&candidate).map_err(|error| { + failure( + "ApplicationOutputWriteFailed", + format!( + "failed to create application output directory `{}`: {error}", + candidate.display() + ), + ) + })?; + let canonical = fs::canonicalize(&candidate).map_err(|error| { + failure( + "ApplicationOutputWriteFailed", + format!( + "failed to resolve application output directory `{}`: {error}", + candidate.display() + ), + ) + })?; + require_path_beneath_root(root, &canonical, "outputDirectory")?; + Ok(canonical) +} + +fn require_path_beneath_root( + root: &Path, + canonical: &Path, + field: &str, +) -> Result<(), ApplicationBuildFailure> { + if canonical.starts_with(root) { + Ok(()) + } else { + Err(failure( + "ApplicationPathEscape", + format!( + "application field `{field}` resolves outside manifest root `{}`", + root.display() + ), + )) + } +} + +fn failure(kind: &'static str, message: impl Into) -> ApplicationBuildFailure { + ApplicationBuildFailure { + kind, + message: message.into(), + } +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeSet; + use std::fs; + use std::path::PathBuf; + use std::sync::atomic::{AtomicU64, Ordering}; + + use edict_syntax::{ + LawpackResourceRef, LawpackTargetAdapter, ProviderArtifactKind, ProviderArtifactRef, + ProviderArtifactSource, ProviderSchemaBinding, ProviderSchemaFormat, ResourceRef, + TargetProviderManifest, TARGET_PROVIDER_ABI, TARGET_PROVIDER_MANIFEST_API_VERSION, + }; + + use super::{ + build_application, canonical_application_root, output_lock_path, provider_schema_artifacts, + read, selected_adapter_reference, single_unique_configuration, + validate_application_manifest, write_outputs, ApplicationLawpack, ApplicationManifest, + ApplicationTarget, + }; + + const STRESS_SEED: u64 = 0x5eed_1a77_c105_0a11; + static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0); + + #[test] + fn application_manifest_accepts_one_root_and_its_complete_dependency_closure() { + let config = application_manifest(64); + + test_ok( + validate_application_manifest(&config), + "one root plus a bounded dependency closure must be accepted", + ); + } + + #[test] + fn relative_application_config_uses_the_current_directory_as_root() { + let actual = test_ok( + canonical_application_root(PathBuf::from("edict.application.json").as_path()), + "resolve relative application root", + ); + let expected = test_ok(fs::canonicalize("."), "resolve current directory"); + + assert_eq!(actual, expected); + } + + #[test] + fn application_manifest_rejects_absolute_and_parent_traversal_paths() { + let mut source_escape = application_manifest(1); + source_escape.sources = vec![PathBuf::from("../outside.edict")]; + let mut lawpack_escape = application_manifest(1); + lawpack_escape.lawpacks[0].manifest = PathBuf::from("/outside/manifest.cbor"); + let mut provider_escape = application_manifest(1); + provider_escape.target.provider_package = PathBuf::from("../provider"); + let mut output_escape = application_manifest(1); + output_escape.output_directory = PathBuf::from("/outside/output"); + + for config in [ + source_escape, + lawpack_escape, + provider_escape, + output_escape, + ] { + assert!( + validate_application_manifest(&config).is_err(), + "application-owned paths must remain beneath the manifest root" + ); + } + } + + #[test] + fn selected_adapter_reference_follows_the_selected_target_profile() { + let first_profile = lawpack_ref("target.first@1", 0x11); + let selected_profile = lawpack_ref("target.selected@1", 0x22); + let first = adapter_descriptor(first_profile, "adapter.first@1", 0x31); + let selected = adapter_descriptor(selected_profile.clone(), "adapter.selected@1", 0x32); + let adapters = [first, selected]; + + let actual = test_ok( + selected_adapter_reference(&adapters, &selected_profile), + "selected adapter", + ); + + assert_eq!(actual.id, "adapter.selected@1"); + } + + #[test] + fn repeated_effect_references_to_one_configuration_are_deduplicated() { + let configuration = lawpack_ref("target.configuration@1", 0x44); + + let actual = test_ok( + single_unique_configuration([&configuration, &configuration]), + "identical effect references name one configuration", + ); + + assert_eq!(actual, &configuration); + } + + #[test] + fn provider_schema_selection_loads_every_bound_role_under_bounded_stress() { + let mut manifest = provider_manifest_with_schema_count(32); + let mut state = STRESS_SEED; + for index in (1..manifest.artifacts.len()).rev() { + state ^= state << 13; + state ^= state >> 7; + state ^= state << 17; + let bound = test_ok(u64::try_from(index + 1), "stress bound fits u64"); + let swap_with = test_ok( + usize::try_from(state % bound), + "stress permutation index fits usize", + ); + manifest.artifacts.swap(index, swap_with); + } + + let actual = test_ok( + provider_schema_artifacts(&manifest), + "every schema role bound by the provider must be loaded", + ); + let roles = actual + .iter() + .map(|artifact| artifact.role.as_str()) + .collect::>(); + + assert_eq!(actual.len(), 32); + assert_eq!(roles.len(), 32); + } + + #[test] + fn failed_pair_publication_preserves_the_previous_package() { + let root = temp_tree("pair-publication"); + let package_path = root.join("executable-operation-package.cbor"); + let report_path = root.join("verification-report.cbor"); + test_ok( + fs::write(&package_path, b"previous-package"), + "write prior package", + ); + test_ok( + fs::create_dir(&report_path), + "create conflicting report directory", + ); + + let failure = test_err( + write_outputs(&root, b"new-package", b"new-report"), + "a non-file report target must reject the pair", + ); + + assert_eq!(failure.kind, "ApplicationOutputWriteFailed"); + assert_eq!( + test_ok(fs::read(&package_path), "read preserved package"), + b"previous-package" + ); + test_ok(fs::remove_dir_all(root), "remove test-owned temp tree"); + } + + #[test] + fn application_artifact_reads_are_bounded_before_allocation() { + let root = temp_tree("bounded-read"); + let path = root.join("oversized.cbor"); + let file = test_ok(fs::File::create(&path), "create oversized artifact"); + test_ok( + file.set_len(1024 * 1024 + 1), + "size oversized artifact without allocating it", + ); + + let failure = test_err( + read(&path, "ArtifactReadFailed", "test artifact"), + "oversized artifact must reject", + ); + + assert_eq!(failure.kind, "ApplicationArtifactTooLarge"); + test_ok(fs::remove_dir_all(root), "remove bounded read temp tree"); + } + + #[cfg(unix)] + #[test] + fn application_build_rejects_source_symlink_escape() { + use std::os::unix::fs::symlink; + + let root = temp_tree("source-symlink-root"); + let outside = temp_tree("source-symlink-outside"); + let outside_source = outside.join("operation.edict"); + test_ok( + fs::write(&outside_source, "package examples.test@1;"), + "write external source", + ); + test_ok( + symlink(&outside_source, root.join("operation.edict")), + "link source outside application root", + ); + let config_path = root.join("edict.application.json"); + let config = serde_json::json!({ + "schema": "edict.application/v1", + "coordinate": "examples.test@1.operation", + "sources": ["operation.edict"], + "lawpacks": [{ + "manifest": "lawpack/manifest.cbor", + "exports": "lawpack/exports.cbor", + "adapter": "lawpack/adapter.cbor", + "targetConfiguration": "lawpack/configuration.cbor" + }], + "target": { + "profile": "target.test@1", + "providerPackage": "provider" + }, + "outputDirectory": ".build/application" + }); + let config_bytes = test_ok(serde_json::to_vec(&config), "encode application config"); + test_ok( + fs::write(&config_path, config_bytes), + "write application config", + ); + + let failure = test_err( + build_application(&config_path), + "source symlink outside the manifest root must reject", + ); + + assert_eq!(failure.kind, "ApplicationPathEscape"); + test_ok(fs::remove_dir_all(root), "remove symlink root"); + test_ok(fs::remove_dir_all(outside), "remove symlink target"); + } + + #[test] + fn successful_publication_leaves_no_lock_in_output_directory() { + let root = temp_tree("output-lock-location"); + let lock_path = output_lock_path(&root); + + test_ok( + write_outputs(&root, b"package", b"report"), + "publish application outputs", + ); + + assert!(!root.join(".edict-application-build.lock").exists()); + test_ok(fs::remove_file(lock_path), "remove test output lock"); + test_ok(fs::remove_dir_all(root), "remove output lock temp tree"); + } + + fn application_manifest(lawpack_count: usize) -> ApplicationManifest { + ApplicationManifest { + schema: "edict.application/v1".to_owned(), + coordinate: "examples.test@1.operation".to_owned(), + sources: vec![PathBuf::from("operation.edict")], + lawpacks: (0..lawpack_count) + .map(|index| ApplicationLawpack { + manifest: PathBuf::from(format!("lawpacks/{index}/manifest.cbor")), + exports: PathBuf::from(format!("lawpacks/{index}/exports.cbor")), + adapter: PathBuf::from(format!("lawpacks/{index}/adapter.cbor")), + target_configuration: PathBuf::from(format!( + "lawpacks/{index}/configuration.cbor" + )), + }) + .collect(), + target: ApplicationTarget { + profile: "target.test@1".to_owned(), + provider_package: PathBuf::from("provider"), + }, + output_directory: PathBuf::from(".build/application"), + } + } + + fn lawpack_ref(id: &str, digest_byte: u8) -> LawpackResourceRef { + LawpackResourceRef { + id: id.to_owned(), + digest: [digest_byte; 32], + } + } + + fn adapter_descriptor( + accepted_target_profile: LawpackResourceRef, + adapter_id: &str, + digest_byte: u8, + ) -> LawpackTargetAdapter { + LawpackTargetAdapter { + accepted_target_profile, + accepted_target_ir: lawpack_ref("target.ir/v1", digest_byte.wrapping_add(1)), + adapter: lawpack_ref(adapter_id, digest_byte), + } + } + + fn provider_manifest_with_schema_count(count: usize) -> TargetProviderManifest { + let artifacts = (0..count).map(schema_artifact).collect::>(); + let schema_bindings = (0..count) + .map(|index| ProviderSchemaBinding { + domain: format!("example.schema-{index:02}/v1"), + schema_role: format!("schema.example-{index:02}"), + format: ProviderSchemaFormat::SelfContainedCddlV1, + root_rule: format!("example-{index:02}"), + }) + .collect(); + TargetProviderManifest { + api_version: TARGET_PROVIDER_MANIFEST_API_VERSION.to_owned(), + provider_abi: TARGET_PROVIDER_ABI.to_owned(), + provider: resource("provider.example@1", 0x51), + artifacts, + schema_bindings, + } + } + + fn schema_artifact(index: usize) -> ProviderArtifactRef { + ProviderArtifactRef { + role: format!("schema.example-{index:02}"), + artifact_kind: ProviderArtifactKind::ArtifactSchema, + resource: resource( + &format!("schema.example-{index:02}@1"), + test_ok(u8::try_from(index), "schema fixture index fits u8"), + ), + source: ProviderArtifactSource::Generated { + semantic_source: resource("source.example@1", 0x61), + generator: resource("generator.example@1", 0x62), + }, + } + } + + fn resource(coordinate: &str, digest_byte: u8) -> ResourceRef { + ResourceRef { + coordinate: coordinate.to_owned(), + digest: Some(format!( + "sha256:{}", + format!("{digest_byte:02x}").repeat(32) + )), + } + } + + fn temp_tree(name: &str) -> PathBuf { + let unique = TEMP_COUNTER.fetch_add(1, Ordering::Relaxed); + let path = std::env::temp_dir().join(format!( + "edict-application-build-{name}-{}-{unique}", + std::process::id() + )); + test_ok(fs::create_dir_all(&path), "create test temp tree"); + path + } + + fn test_ok(result: Result, context: &str) -> T { + match result { + Ok(value) => value, + Err(error) => panic!("{context}: {error:?}"), + } + } + + fn test_err(result: Result, context: &str) -> E { + match result { + Ok(value) => panic!("{context}: got {value:?}"), + Err(error) => error, + } + } +} diff --git a/crates/edict-cli/src/main.rs b/crates/edict-cli/src/main.rs index 3fbee2a..7bffee1 100644 --- a/crates/edict-cli/src/main.rs +++ b/crates/edict-cli/src/main.rs @@ -1,5 +1,7 @@ #![deny(clippy::expect_used, clippy::unwrap_used)] +mod application_build; + use std::collections::BTreeMap; use std::fs; use std::io::{self, Read, Write}; @@ -23,6 +25,7 @@ use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; const COMMAND_CHECK: &str = "check"; +const COMMAND_BUILD: &str = "build"; const COMMAND_PROJECT: &str = "project"; const EXIT_OK: i32 = 0; const EXIT_CHECK_FAILED: i32 = 1; @@ -35,6 +38,7 @@ struct CompilerSettings { #[serde(rename = "type")] record_type: String, operation: Operation, + application: Option, #[serde(default)] emit: Vec, compiler_context: Option, @@ -49,6 +53,7 @@ struct CompilerSettings { #[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] #[serde(rename_all = "camelCase")] enum Operation { + Build, Check, Project, } @@ -370,7 +375,15 @@ fn parse_request(input: &str) -> Result { line: None, message: "request missing compiler settings record".to_owned(), })?; - if inputs.is_empty() { + if settings.operation == Operation::Build && !inputs.is_empty() { + return Err(CliFailure { + command: command_for_operation(settings.operation), + kind: "InvalidInputRecord", + line: None, + message: "build requests must not contain compiler input records".to_owned(), + }); + } + if inputs.is_empty() && settings.operation != Operation::Build { return Err(CliFailure { command: command_for_operation(settings.operation), kind: "MissingInput", @@ -392,6 +405,16 @@ fn parse_settings(value: Value, line: usize) -> Result(value).map_err(|err| CliFailure { command, kind: "InvalidSettings", @@ -407,12 +430,15 @@ fn parse_settings(value: Value, line: usize) -> Result Result Option<&'static str> { + [ + "emit", + "compilerContext", + "target", + "inputRoot", + "directoryExtensions", + "followSymlinks", + ] + .into_iter() + .find(|field| value.get(field).is_some()) +} + fn null_compiler_settings_field(value: &Value) -> Option<&'static str> { - ["inputRoot", "compilerContext", "target"] + ["application", "inputRoot", "compilerContext", "target"] .into_iter() .find(|field| value.get(field).is_some_and(Value::is_null)) } @@ -450,6 +489,7 @@ fn settings_value_command(value: &Value) -> &'static str { fn command_for_operation(operation: Operation) -> &'static str { match operation { + Operation::Build => COMMAND_BUILD, Operation::Check => COMMAND_CHECK, Operation::Project => COMMAND_PROJECT, } @@ -457,6 +497,7 @@ fn command_for_operation(operation: Operation) -> &'static str { fn command_for_operation_name(operation: &str) -> Option<&'static str> { match operation { + COMMAND_BUILD => Some(COMMAND_BUILD), COMMAND_CHECK => Some(COMMAND_CHECK), COMMAND_PROJECT => Some(COMMAND_PROJECT), _ => None, @@ -465,7 +506,33 @@ fn command_for_operation_name(operation: &str) -> Option<&'static str> { fn validate_operation_settings(settings: &CompilerSettings, line: usize) -> Result<(), CliFailure> { match settings.operation { + Operation::Build => { + if !settings.emit.is_empty() + || settings.compiler_context.is_some() + || settings.target.is_some() + || settings.input_root.is_some() + || settings.follow_symlinks + || settings.directory_extensions != default_directory_extensions() + { + return Err(CliFailure { + command: COMMAND_BUILD, + kind: "InvalidSettings", + line: Some(line), + message: + "build accepts only the application path and directory-extension defaults" + .to_owned(), + }); + } + } Operation::Check => { + if settings.application.is_some() { + return Err(CliFailure { + command: COMMAND_CHECK, + kind: "InvalidSettings", + line: Some(line), + message: "`application` is a build-only setting".to_owned(), + }); + } if !settings.emit.is_empty() || settings.compiler_context.is_some() || settings.target.is_some() @@ -480,6 +547,14 @@ fn validate_operation_settings(settings: &CompilerSettings, line: usize) -> Resu } } Operation::Project => { + if settings.application.is_some() { + return Err(CliFailure { + command: COMMAND_PROJECT, + kind: "InvalidSettings", + line: Some(line), + message: "`application` is a build-only setting".to_owned(), + }); + } if settings.emit.is_empty() { return Err(CliFailure { command: COMMAND_PROJECT, @@ -663,6 +738,9 @@ fn optional_string_field(object: &serde_json::Map, key: &str) -> fn run_request(request: &Request) -> Result { let command = command_for_operation(request.settings.operation); + if request.settings.operation == Operation::Build { + return run_build_request(&request.settings); + } let sources = expand_inputs(&request.settings, &request.inputs) .map_err(|failure| failure.with_command(command))?; if sources.is_empty() { @@ -674,11 +752,33 @@ fn run_request(request: &Request) -> Result { }); } match request.settings.operation { + Operation::Build => unreachable!("build requests return before input expansion"), Operation::Check => Ok(run_check_request(&sources)), Operation::Project => run_project_request(&request.settings, &sources), } } +fn run_build_request(settings: &CompilerSettings) -> Result { + let path = settings.application.as_ref().ok_or_else(|| CliFailure { + command: COMMAND_BUILD, + kind: "InvalidSettings", + line: None, + message: "build operation requires `application`".to_owned(), + })?; + application_build::build_application(path).map_err(|failure| CliFailure { + command: COMMAND_BUILD, + kind: failure.kind, + line: None, + message: failure.message, + })?; + let mut stdout = io::stdout().lock(); + write_record( + &mut stdout, + &status_record(COMMAND_BUILD, "ok", 1, 0, EXIT_OK), + ); + Ok(EXIT_OK) +} + fn run_check_request(sources: &[SourceDocument]) -> i32 { let report = check_sources(sources); if report.diagnostics.is_empty() { @@ -998,6 +1098,7 @@ fn project_target_facts(target: &ProjectionTargetSettings) -> TargetIrLoweringFa .map(|lowering| TargetEffectLowering { effect: lowering.effect.clone(), target_intrinsic: lowering.target_intrinsic.clone(), + failure_mappings: std::collections::BTreeMap::new(), }) .collect(), } @@ -1465,6 +1566,7 @@ fn target_lowering_failure_kind_name(kind: TargetLoweringFailureKind) -> &'stati TargetLoweringFailureKind::UnsupportedCoreNode => "UnsupportedCoreNode", TargetLoweringFailureKind::MissingOperationProfile => "MissingOperationProfile", TargetLoweringFailureKind::MissingObstruction => "MissingObstruction", + TargetLoweringFailureKind::AmbiguousObstructionMapping => "AmbiguousObstructionMapping", TargetLoweringFailureKind::MissingEffectLowering => "MissingEffectLowering", TargetLoweringFailureKind::AmbiguousEffectLowering => "AmbiguousEffectLowering", TargetLoweringFailureKind::UnsupportedLowerabilityReport => "UnsupportedLowerabilityReport", diff --git a/crates/edict-cli/tests/jsonl_cli.rs b/crates/edict-cli/tests/jsonl_cli.rs index 35ec6da..e91d6c0 100644 --- a/crates/edict-cli/tests/jsonl_cli.rs +++ b/crates/edict-cli/tests/jsonl_cli.rs @@ -54,6 +54,148 @@ const ECHO_TARGET_PROFILE_DIGEST: &str = static TEMP_COUNTER: AtomicUsize = AtomicUsize::new(0); +#[test] +fn build_accepts_application_request_without_compiler_input_records() { + let root = temp_tree("missing-build-application"); + let application = root.join("missing-edict-application.json"); + let output = run_edict(&jsonl([json!({ + "schema": "edict.compiler.settings/v1", + "type": "compilerSettings", + "operation": "build", + "application": application, + })])); + + assert_eq!(output.status.code(), Some(2)); + assert!(output.stdout.is_empty()); + let stderr = assert_jsonl_stream(&output.stderr, "stderr"); + let diagnostic = stderr + .iter() + .find(|line| line.get("type").and_then(Value::as_str) == Some("diagnostic")) + .expect("build failure emits a diagnostic"); + assert_eq!( + diagnostic.get("command").and_then(Value::as_str), + Some("build") + ); + assert_eq!( + diagnostic.get("kind").and_then(Value::as_str), + Some("ApplicationConfigReadFailed") + ); + assert_status(&stderr, "error", 2); + fs::remove_dir_all(root).expect("remove missing application test tree"); +} + +#[test] +fn build_rejects_compiler_input_records_instead_of_ignoring_them() { + let root = temp_tree("build-input-record"); + let application = root.join("missing-edict-application.json"); + let output = run_edict(&jsonl([ + json!({ + "schema": "edict.compiler.settings/v1", + "type": "compilerSettings", + "operation": "build", + "application": application, + }), + json!({ + "schema": "edict.compiler.input/v1", + "type": "compilerInput", + "kind": "source", + "name": "ignored.edict", + "source": VALID_SOURCE, + }), + ])); + + assert_eq!(output.status.code(), Some(2)); + assert!(output.stdout.is_empty()); + let stderr = assert_jsonl_stream(&output.stderr, "stderr"); + let diagnostic = stderr + .iter() + .find(|line| line.get("type").and_then(Value::as_str) == Some("diagnostic")) + .expect("build input rejection emits a diagnostic"); + assert_eq!( + diagnostic.get("command").and_then(Value::as_str), + Some("build") + ); + assert_eq!( + diagnostic.get("kind").and_then(Value::as_str), + Some("InvalidInputRecord") + ); + assert_status(&stderr, "error", 2); + fs::remove_dir_all(root).expect("remove build input record test tree"); +} + +#[test] +fn build_rejects_unused_directory_extension_settings() { + let root = temp_tree("build-directory-extensions"); + let application = root.join("missing-edict-application.json"); + let output = run_edict(&jsonl([json!({ + "schema": "edict.compiler.settings/v1", + "type": "compilerSettings", + "operation": "build", + "application": application, + "directoryExtensions": [".unused"], + })])); + + assert_eq!(output.status.code(), Some(2)); + assert!(output.stdout.is_empty()); + let stderr = assert_jsonl_stream(&output.stderr, "stderr"); + let diagnostic = stderr + .iter() + .find(|line| line.get("type").and_then(Value::as_str) == Some("diagnostic")) + .expect("unused build setting emits a diagnostic"); + assert_eq!( + diagnostic.get("command").and_then(Value::as_str), + Some("build") + ); + assert_eq!( + diagnostic.get("kind").and_then(Value::as_str), + Some("InvalidSettings") + ); + assert_status(&stderr, "error", 2); + fs::remove_dir_all(root).expect("remove directory extensions test tree"); +} + +#[test] +fn build_rejects_explicit_default_values_for_forbidden_settings() { + let root = temp_tree("build-explicit-defaults"); + let application = root.join("missing-edict-application.json"); + + for (field, value) in [ + ("emit", json!([])), + ("followSymlinks", json!(false)), + ("directoryExtensions", json!([".edict"])), + ] { + let mut settings = json!({ + "schema": "edict.compiler.settings/v1", + "type": "compilerSettings", + "operation": "build", + "application": application, + }); + settings[field] = value; + let output = run_edict(&jsonl([settings])); + + assert_eq!(output.status.code(), Some(2), "field {field}"); + assert!(output.stdout.is_empty(), "field {field}"); + let stderr = assert_jsonl_stream(&output.stderr, "stderr"); + let diagnostic = stderr + .iter() + .find(|line| line.get("type").and_then(Value::as_str) == Some("diagnostic")) + .expect("explicit forbidden build setting emits a diagnostic"); + assert_eq!( + diagnostic.get("command").and_then(Value::as_str), + Some("build"), + "field {field}" + ); + assert_eq!( + diagnostic.get("kind").and_then(Value::as_str), + Some("InvalidSettings"), + "field {field}" + ); + assert_status(&stderr, "error", 2); + } + + fs::remove_dir_all(root).expect("remove explicit defaults test tree"); +} + #[test] fn check_accepts_inline_source_jsonl_and_emits_jsonl_stdout() { let output = run_edict(&jsonl([ @@ -1219,6 +1361,7 @@ fn projection_target_facts() -> TargetIrLoweringFacts { effect_lowerings: vec![TargetEffectLowering { effect: "target.replace".to_owned(), target_intrinsic: "echo.dpo@1.replace".to_owned(), + failure_mappings: std::collections::BTreeMap::new(), }], } } diff --git a/crates/edict-provider-schema/src/contract_pack.rs b/crates/edict-provider-schema/src/contract_pack.rs index d15734b..c08606a 100644 --- a/crates/edict-provider-schema/src/contract_pack.rs +++ b/crates/edict-provider-schema/src/contract_pack.rs @@ -32,9 +32,10 @@ const PACK_HEADER: &str = "; SPDX-License-Identifier: Apache-2.0\n\ ; edict-provider-contracts.cddl\n\ ; Generated from Edict-owned ABI fragments. DO NOT EDIT.\n"; -const CONTRACT_BINDINGS: [(&str, &str); 9] = [ +const CONTRACT_BINDINGS: [(&str, &str); 10] = [ ("authority-facts", "authority-facts"), ("core-module", "core-module"), + ("lawpack-adapter", "lawpack-adapter"), ("lawpack-exports", "lawpack-exports"), ("lawpack-manifest", "lawpack-manifest"), ("lowering-requirements", "lowering-requirements"), @@ -68,6 +69,7 @@ pub struct ProviderContractPackInput<'a> { pub common_cddl: &'a [u8], pub core_cddl: &'a [u8], pub lawpack_cddl: &'a [u8], + pub lawpack_adapter_cddl: &'a [u8], pub target_profile_cddl: &'a [u8], pub authority_facts_cddl: &'a [u8], pub target_ir_cddl: &'a [u8], @@ -291,6 +293,7 @@ pub fn assemble_provider_contract_pack( common_cddl, core_cddl, lawpack_cddl, + lawpack_adapter_cddl, target_profile_cddl, authority_facts_cddl, target_ir_cddl, @@ -300,6 +303,7 @@ pub fn assemble_provider_contract_pack( ("edict-common.cddl", common_cddl), ("edict-core.cddl", core_cddl), ("edict-lawpack.cddl", lawpack_cddl), + ("edict-lawpack-adapter.cddl", lawpack_adapter_cddl), ("edict-target-profile.cddl", target_profile_cddl), ("edict-authority-facts.cddl", authority_facts_cddl), ("edict-target-ir.cddl", target_ir_cddl), diff --git a/crates/edict-provider-schema/src/recursive_dispatch.rs b/crates/edict-provider-schema/src/recursive_dispatch.rs index 6c2e1fe..23a158c 100644 --- a/crates/edict-provider-schema/src/recursive_dispatch.rs +++ b/crates/edict-provider-schema/src/recursive_dispatch.rs @@ -259,7 +259,9 @@ fn audit_choice( reenters |= option_reenters; } if recursive_alternatives > 1 { - tagged_choice(choice, rules)?; + if tagged_choice(choice, rules).is_none() && required_key_choice(choice, rules).is_none() { + return None; + } *requires_dispatch = true; } Some(reenters) @@ -452,6 +454,13 @@ struct TaggedChoice<'a> { arms: BTreeMap<&'a str, &'a Node>, } +#[derive(Debug)] +struct RequiredKeyChoice<'a> { + key: String, + present: &'a Node, + absent: &'a Node, +} + fn tagged_choice<'a>(choice: &'a Choice, rules: &'a RulesByName) -> Option> { let first = choice.options.first()?; let candidates = required_text_literals(first, rules)?; @@ -478,6 +487,56 @@ fn tagged_choice<'a>(choice: &'a Choice, rules: &'a RulesByName) -> Option( + choice: &'a Choice, + rules: &'a RulesByName, +) -> Option> { + let [first, second] = choice.options.as_slice() else { + return None; + }; + let first_keys = exact_text_map_key_requirements(first, rules)?; + let second_keys = exact_text_map_key_requirements(second, rules)?; + + for (key, minimum) in &first_keys { + if *minimum > 0 && !second_keys.contains_key(key) { + return Some(RequiredKeyChoice { + key: key.clone(), + present: first, + absent: second, + }); + } + } + for (key, minimum) in &second_keys { + if *minimum > 0 && !first_keys.contains_key(key) { + return Some(RequiredKeyChoice { + key: key.clone(), + present: second, + absent: first, + }); + } + } + None +} + +fn exact_text_map_key_requirements( + node: &Node, + rules: &RulesByName, +) -> Option> { + let Node::Map(map) = terminal_node(node, rules)? else { + return None; + }; + let mut requirements = BTreeMap::new(); + for pattern in map_patterns(&map.members, rules)? { + let Literal::Text(key) = exact_scalar_literal(&pattern.pair.key, rules)? else { + return None; + }; + if requirements.insert(key.clone(), pattern.minimum).is_some() { + return None; + } + } + Some(requirements) +} + fn required_text_literals<'a>( node: &'a Node, rules: &'a RulesByName, @@ -626,6 +685,23 @@ impl Specializer<'_> { let option = dispatch.arms.get(discriminator.as_str())?; return self.specialize_value(option, value, input_depth); } + if let Some(dispatch) = required_key_choice(choice, &self.context.rules) { + let CanonicalValue::Map(entries) = value else { + return None; + }; + let mut occurrences = entries.iter().filter( + |(key, _)| matches!(key, CanonicalValue::Text(text) if text == &dispatch.key), + ); + let option = if occurrences.next().is_some() { + if occurrences.next().is_some() { + return None; + } + dispatch.present + } else { + dispatch.absent + }; + return self.specialize_value(option, value, input_depth); + } let mut options = Vec::new(); for option in &choice.options { diff --git a/crates/edict-provider-schema/tests/provider_contract_pack.rs b/crates/edict-provider-schema/tests/provider_contract_pack.rs index 80146b7..de2cec2 100644 --- a/crates/edict-provider-schema/tests/provider_contract_pack.rs +++ b/crates/edict-provider-schema/tests/provider_contract_pack.rs @@ -25,6 +25,7 @@ use sha2::{Digest, Sha256}; const COMMON_CDDL: &[u8] = include_bytes!("../../../docs/abi/edict-common.cddl"); const CORE_CDDL: &[u8] = include_bytes!("../../../docs/abi/edict-core.cddl"); const LAWPACK_CDDL: &[u8] = include_bytes!("../../../docs/abi/edict-lawpack.cddl"); +const LAWPACK_ADAPTER_CDDL: &[u8] = include_bytes!("../../../docs/abi/edict-lawpack-adapter.cddl"); const TARGET_PROFILE_CDDL: &[u8] = include_bytes!("../../../docs/abi/edict-target-profile.cddl"); const AUTHORITY_FACTS_CDDL: &[u8] = include_bytes!("../../../docs/abi/edict-authority-facts.cddl"); const TARGET_IR_CDDL: &[u8] = include_bytes!("../../../docs/abi/edict-target-ir.cddl"); @@ -37,6 +38,8 @@ const TARGET_IR_FIXTURE: &[u8] = include_bytes!("../../../fixtures/target-ir/canonical/echo-effectful.target-ir.cbor"); const ALTERNATE_TARGET_IR_FIXTURE: &[u8] = include_bytes!("../../../fixtures/target-ir/canonical/gitwarp-append.target-ir.cbor"); +const LAWPACK_ADAPTER_FIXTURE: &[u8] = + include_bytes!("../../../fixtures/lawpack/hello-echo/adapter.cbor"); const OPERATION_SOURCE: &str = include_str!("../../../fixtures/lang/operations/explicit-basis-u64.edict"); const EXPECTED_DOMAIN_BINDINGS: [(&str, &str); 6] = [ @@ -68,7 +71,7 @@ fn contract_pack_is_self_contained_and_repeatable() { PROVIDER_CONTRACT_PACK_COORDINATE ); assert_eq!(forward.manifest().license, PROVIDER_CONTRACT_PACK_LICENSE); - assert_eq!(forward.manifest().contracts.len(), 9); + assert_eq!(forward.manifest().contracts.len(), 10); assert_eq!(forward.manifest().domains.len(), 6); assert_eq!(forward.manifest().resources.len(), 5); assert!(forward @@ -512,6 +515,7 @@ fn input_with( common_cddl: COMMON_CDDL, core_cddl, lawpack_cddl: LAWPACK_CDDL, + lawpack_adapter_cddl: LAWPACK_ADAPTER_CDDL, target_profile_cddl: TARGET_PROFILE_CDDL, authority_facts_cddl: AUTHORITY_FACTS_CDDL, target_ir_cddl, @@ -559,6 +563,11 @@ fn representative_contract_instances() -> Vec<(&'static str, CanonicalValue)> { "core-module", decode_canonical_cbor(CORE_FIXTURE).expect("Core fixture is canonical"), ), + ( + "lawpack-adapter", + decode_canonical_cbor(LAWPACK_ADAPTER_FIXTURE) + .expect("lawpack adapter fixture is canonical"), + ), ("lawpack-exports", lawpack_exports()), ("lawpack-manifest", lawpack_manifest()), ("lowering-requirements", lowering_requirements()), diff --git a/crates/edict-provider-schema/tests/registry.rs b/crates/edict-provider-schema/tests/registry.rs index a20979c..80d84ab 100644 --- a/crates/edict-provider-schema/tests/registry.rs +++ b/crates/edict-provider-schema/tests/registry.rs @@ -32,6 +32,20 @@ short-child = { a: generated-artifact, kind: "short" } long-child = { kind: "long", payload: generated-artifact } "#; +const REQUIRED_KEY_RECURSIVE_SCHEMA: &[u8] = br#" +generated-artifact = closed / legacy +closed = { + kind: "artifact", + semanticClosure: uint, + ? child: generated-artifact, +} +legacy = { + kind: "artifact", + value: uint, + ? child: generated-artifact, +} +"#; + const PROVIDER_CONTRACT_SCHEMA: &[u8] = include_bytes!("../../../fixtures/provider-contracts/v1/edict-provider-contracts.cddl"); const CORE_FIXTURE: &[u8] = @@ -612,6 +626,45 @@ fn recursive_map_discriminator_dispatch_is_independent_of_encoded_key_order() { .expect("nested occurrences may select different recursive alternatives"); } +#[test] +fn recursive_required_key_dispatch_accepts_compatible_untagged_arms() { + let registry = registry_with_generated_schema(REQUIRED_KEY_RECURSIVE_SCHEMA) + .expect("one disjoint required key must admit bounded recursive dispatch"); + let closed = map(&[ + ("kind", CanonicalValue::Text("artifact".to_owned())), + ("semanticClosure", CanonicalValue::Integer(1)), + ]); + let legacy = map(&[ + ("kind", CanonicalValue::Text("artifact".to_owned())), + ("value", CanonicalValue::Integer(2)), + ]); + + for value in [ + closed.clone(), + legacy.clone(), + map(&[ + ("child", legacy), + ("kind", CanonicalValue::Text("artifact".to_owned())), + ("semanticClosure", CanonicalValue::Integer(3)), + ]), + map(&[ + ("child", closed), + ("kind", CanonicalValue::Text("artifact".to_owned())), + ("value", CanonicalValue::Integer(4)), + ]), + ] { + registry + .validate_canonical_value("runtime.generated-artifact/v1", &value) + .expect("required-key dispatch validates the selected recursive arm"); + } +} + +#[test] +fn checked_target_ir_root_constructs_through_required_key_dispatch() { + registry_with_generated_schema_root(PROVIDER_CONTRACT_SCHEMA, "target-ir-artifact") + .expect("the checked Target IR compatibility union must construct"); +} + #[test] fn recursive_map_discriminator_dispatch_rejects_invalid_discriminators() { let registry = registry_with_generated_schema(DISCRIMINATED_RECURSIVE_SCHEMA) diff --git a/crates/edict-syntax/src/authority_facts.rs b/crates/edict-syntax/src/authority_facts.rs index 33069b1..199ab75 100644 --- a/crates/edict-syntax/src/authority_facts.rs +++ b/crates/edict-syntax/src/authority_facts.rs @@ -13,8 +13,8 @@ use std::path::Path; use serde::Deserialize; use crate::canonical::{ - decode_canonical_cbor, digest_canonical_value, encode_canonical_cbor, CanonicalErrorKind, - CanonicalValue, + decode_canonical_cbor, digest_canonical_value, encode_canonical_cbor, sha256_review_string, + CanonicalErrorKind, CanonicalValue, }; use crate::compiler::CompilerContext; use crate::core_ir::{is_sha256_review_digest, CoreBudget}; @@ -646,17 +646,6 @@ fn parse_canonical_digest( Ok(sha256_review_string(bytes)) } -fn sha256_review_string(bytes: &[u8]) -> String { - const HEX: &[u8; 16] = b"0123456789abcdef"; - let mut review = String::with_capacity(7 + bytes.len() * 2); - review.push_str("sha256:"); - for byte in bytes { - review.push(char::from(HEX[usize::from(byte >> 4)])); - review.push(char::from(HEX[usize::from(byte & 0x0f)])); - } - review -} - fn non_digest_failure(coordinate: &str) -> AuthorityFactsLoadFailure { failure( AuthorityFactsLoadFailureKind::NonDigestLockedSource, diff --git a/crates/edict-syntax/src/canonical.rs b/crates/edict-syntax/src/canonical.rs index 5c64a97..53891ca 100644 --- a/crates/edict-syntax/src/canonical.rs +++ b/crates/edict-syntax/src/canonical.rs @@ -184,6 +184,32 @@ pub(crate) fn digest_canonical_value( Ok(bytes) } +/// Decode canonical CBOR and compute its domain-framed SHA-256 identity. +/// +/// This establishes canonical encoding and one shared digest frame. It does +/// not establish the artifact's owning schema; callers must validate that +/// separately. +/// +/// # Errors +/// +/// Returns an error for non-canonical CBOR, an empty domain, an unsupported +/// value, or a value beyond the canonical nesting boundary. +pub fn digest_canonical_artifact(domain: &str, bytes: &[u8]) -> Result { + let value = decode_canonical_cbor(bytes)?; + Ok(CoreDigest::sha256(digest_canonical_value(domain, &value)?)) +} + +pub(crate) fn sha256_review_string(bytes: &[u8]) -> String { + const HEX: &[u8; 16] = b"0123456789abcdef"; + let mut review = String::with_capacity(7 + bytes.len() * 2); + review.push_str("sha256:"); + for byte in bytes { + review.push(char::from(HEX[usize::from(byte >> 4)])); + review.push(char::from(HEX[usize::from(byte & 0x0f)])); + } + review +} + /// Encode a Core module as `edict.canonical-cbor/v1`. /// /// # Errors diff --git a/crates/edict-syntax/src/contract_bundle.rs b/crates/edict-syntax/src/contract_bundle.rs index 0507d9d..f6e071b 100644 --- a/crates/edict-syntax/src/contract_bundle.rs +++ b/crates/edict-syntax/src/contract_bundle.rs @@ -572,15 +572,39 @@ fn corroborate_target_ir_semantic_closure( ) })?; - match (&expected, &target_ir_artifact.semantic_closure) { - (None, None) => {} - (Some(_), None) | (None, Some(_)) => { + let expected_lawpacks = match (&expected, &target_ir_artifact.semantic_closure) { + (None, None) => Vec::new(), + (Some(_), None) => { return Err(ContractBundleAssemblyError::new( ContractBundleAssemblyErrorKind::TargetIrSourceMismatch, "target_ir_artifact.semantic_closure", "Target IR semantic closure does not match the supplied Core module", )); } + (None, Some(actual)) => { + // This branch may use the Target IR closure only as the expected set + // because the independently supplied lawpack set is corroborated + // below. Callers must derive `lawpacks` independently of a + // provider-asserted Target IR closure. + let expected_source_core = ResourceRef { + coordinate: core_module.coordinate.clone(), + digest: Some( + digest_core_module(core_module) + .map_err(|error| { + ContractBundleAssemblyError::canonical("core_module", &error) + })? + .to_review_string(), + ), + }; + if actual.source_core != expected_source_core { + return Err(ContractBundleAssemblyError::new( + ContractBundleAssemblyErrorKind::TargetIrSourceMismatch, + "target_ir_artifact.semantic_closure.source_core", + "Target IR source Core identity does not match the supplied Core module", + )); + } + canonical_resource_set(&actual.lawpacks) + } (Some(expected), Some(actual)) => { if actual.source_core != expected.source_core { return Err(ContractBundleAssemblyError::new( @@ -598,13 +622,9 @@ fn corroborate_target_ir_semantic_closure( "Target IR lawpack closure does not match the supplied Core module", )); } + canonical_resource_set(&expected.lawpacks) } - } - - let expected_lawpacks = expected - .as_ref() - .map(|closure| canonical_resource_set(&closure.lawpacks)) - .unwrap_or_default(); + }; let supplied_lawpacks = lawpacks .iter() .map(DigestLockedResource::to_resource_ref) diff --git a/crates/edict-syntax/src/lawpack.rs b/crates/edict-syntax/src/lawpack.rs new file mode 100644 index 0000000..bbbb806 --- /dev/null +++ b/crates/edict-syntax/src/lawpack.rs @@ -0,0 +1,2009 @@ +//! Canonical `edict.lawpack/v1` loading and dependency validation. +//! +//! Lawpacks are portable semantic modules. This boundary decodes their exact +//! canonical bytes, validates the closed manifest and export schemas, binds the +//! manifest to the supplied export surface, and validates complete dependency +//! sets before any export is exposed to compiler resolution. It does not +//! execute helpers, adapters, verifiers, or conformance fixtures. + +use std::collections::{BTreeMap, BTreeSet}; + +use crate::canonical::{ + decode_canonical_cbor, digest_canonical_value, sha256_review_string, CanonicalErrorKind, + CanonicalValue, +}; +use crate::core_ir::CORE_API_VERSION; +use crate::parser::is_keyword; + +/// Lawpack manifest ABI supported by this loader. +pub const LAWPACK_API_VERSION: &str = "edict.lawpack/v1"; + +const LAWPACK_CBOR_PATH: &str = ""; +const EXPORTS_CBOR_PATH: &str = ""; + +/// Stable lawpack validation failure categories. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum LawpackValidationFailureKind { + InvalidCanonicalCbor, + InvalidShape, + MissingField, + UnexpectedField, + InvalidApiVersion, + EmptyIdentity, + MissingAcceptedCoreAbi, + InvalidDigest, + ExportsDigestMismatch, + InvalidDiscriminant, + EmptyRequiredCollection, + DuplicateIdentity, + InvalidFailureIdentifier, + ReservedFailureIdentifier, + InvalidPureFunctionBody, + RuntimeEffectWithoutTargetAdapter, + SelfDependency, + MissingDependency, + DependencyDigestMismatch, + DependencyCycle, +} + +/// One failed lawpack loading or dependency obligation. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LawpackValidationFailure { + pub kind: LawpackValidationFailureKind, + pub path: String, + pub obligation: String, +} + +/// Typed SHA-256 resource reference from the canonical lawpack ABI. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct LawpackResourceRef { + pub id: String, + pub digest: [u8; 32], +} + +impl LawpackResourceRef { + /// Lowercase review rendering for the typed wire digest. + #[must_use] + pub fn digest_review_string(&self) -> String { + sha256_review_string(&self.digest) + } +} + +/// One digest-locked lawpack dependency. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LawpackDependency { + pub id: String, + pub version: String, + pub digest: [u8; 32], +} + +/// One bounded executable component reference. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LawpackExecutableComponent { + pub component: LawpackResourceRef, + pub sandbox: LawpackResourceRef, + pub fuel_model: LawpackResourceRef, +} + +/// Verifier class discriminant. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum LawpackVerifierClass { + Declarative, + Executable, +} + +/// Declarative or bounded executable lawpack verifier. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum LawpackVerifier { + Declarative { + ruleset: LawpackResourceRef, + }, + Executable { + executable: LawpackExecutableComponent, + }, +} + +impl LawpackVerifier { + /// Return the verifier class without inspecting its implementation fields. + #[must_use] + pub fn class(&self) -> LawpackVerifierClass { + match self { + Self::Declarative { .. } => LawpackVerifierClass::Declarative, + Self::Executable { .. } => LawpackVerifierClass::Executable, + } + } +} + +/// Direct target-adapter descriptor selected by exact target-profile identity. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LawpackTargetAdapter { + pub accepted_target_profile: LawpackResourceRef, + pub accepted_target_ir: LawpackResourceRef, + pub adapter: LawpackResourceRef, +} + +/// Typed `edict.lawpack/v1` manifest. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LawpackManifest { + pub api_version: String, + pub id: String, + pub version: String, + pub accepted_core_abi: Vec, + pub dependencies: Vec, + pub exports: LawpackResourceRef, + pub target_adapters: Vec, + pub helper_component: Option, + pub verifier: LawpackVerifier, + pub compatibility: LawpackResourceRef, + pub conformance_fixture_corpus: LawpackResourceRef, +} + +/// One exported type alias/reference. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LawpackExportedType { + pub coordinate: String, + pub definition: String, +} + +/// One typed exported constant. The value remains canonical and hash-significant. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LawpackExportedConstant { + pub coordinate: String, + pub ty: String, + pub value: CanonicalValue, +} + +/// Pure-helper determinism classification. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum LawpackDeterminismClass { + Total, + TotalWithTypedDiagnostic, +} + +/// Hash-bound implementation of a pure lawpack helper. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum LawpackPureFunctionImplementation { + Edict { + body: CanonicalValue, + }, + Component { + implementation: LawpackExecutableComponent, + }, +} + +/// One exported pure helper. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LawpackPureFunction { + pub coordinate: String, + pub type_parameters: Vec, + pub parameter_types: Vec, + pub return_type: String, + pub cost_template: String, + pub determinism_class: LawpackDeterminismClass, + pub implementation: LawpackPureFunctionImplementation, +} + +/// Semantic-effect execution classification. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum LawpackExecutionClass { + ProofOnly, + Runtime, +} + +/// Advisory semantic effect-kind classification. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum LawpackEffectKind { + Read, + Create, + Ensure, + Replace, + Delete, + Append, + Reduce, + SemanticEmit, + Custom, +} + +/// Authority owner for one effect failure or domain obstruction. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum LawpackAuthorityClass { + DomainMappable, + ParticipantOwned, + IntegrityFault, + ResourceFault, + InternalFault, +} + +/// One named low-level semantic-effect failure. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LawpackEffectFailure { + pub authority_class: LawpackAuthorityClass, + pub payload_type: String, +} + +/// One semantic effect exported by a lawpack. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LawpackSemanticEffect { + pub coordinate: String, + pub type_parameters: Vec, + pub input_type: String, + pub output_type: String, + pub execution_class: LawpackExecutionClass, + pub effect_kind_hint: LawpackEffectKind, + pub footprint_obligation: String, + pub cost_obligation: String, + pub effect_failures: BTreeMap, + pub guard_support: bool, +} + +/// One typed domain obstruction exported by a lawpack. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LawpackObstruction { + pub coordinate: String, + pub authority_class: LawpackAuthorityClass, + pub payload_schema: String, +} + +/// Typed aperture requirement supplied by an operation-profile optic template. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum LawpackApertureRequirement { + FootprintCeiling { reference: String }, + AbstractFootprintObligation { reference: String }, +} + +/// Runtime-neutral optic template from one operation profile. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LawpackOpticTemplate { + pub optic_kind: String, + pub boundary_kind: String, + pub support_policy: String, + pub loss_disposition: String, + pub basis_template: Option, + pub aperture_requirement: Option, +} + +/// One operation profile keyed by its export coordinate. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LawpackOperationProfile { + pub optic_template: LawpackOpticTemplate, + pub effect_predicate: String, +} + +/// Typed lawpack export surface. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LawpackExports { + pub types: Vec, + pub constants: Vec, + pub pure_functions: Vec, + pub effects: Vec, + pub obstructions: Vec, + pub operation_profiles: BTreeMap, +} + +/// A canonical manifest and export surface that passed all local obligations. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ValidatedLawpackBundle { + manifest: LawpackManifest, + exports: LawpackExports, + manifest_digest: [u8; 32], + exports_digest: [u8; 32], +} + +impl ValidatedLawpackBundle { + /// Validated typed manifest. + #[must_use] + pub fn manifest(&self) -> &LawpackManifest { + &self.manifest + } + + /// Validated typed export surface. + #[must_use] + pub fn exports(&self) -> &LawpackExports { + &self.exports + } + + /// Typed manifest digest bytes. + #[must_use] + pub fn manifest_digest(&self) -> &[u8; 32] { + &self.manifest_digest + } + + /// Typed export-surface digest bytes. + #[must_use] + pub fn exports_digest(&self) -> &[u8; 32] { + &self.exports_digest + } + + /// Lowercase review rendering used by source imports and dependency edges. + #[must_use] + pub fn manifest_digest_review_string(&self) -> String { + sha256_review_string(&self.manifest_digest) + } +} + +/// Decode and validate one manifest together with its exact export bytes. +/// +/// # Errors +/// +/// Returns stable failures for non-canonical CBOR, values outside the closed +/// lawpack schemas, an export digest mismatch, or a manifest/export invariant +/// violation. Dependency existence and cycles are checked separately once the +/// caller supplies the complete dependency set. +pub fn decode_lawpack_bundle( + manifest_bytes: &[u8], + exports_bytes: &[u8], +) -> Result> { + let manifest_value = decode_lawpack_value(manifest_bytes, LAWPACK_CBOR_PATH)?; + let exports_value = decode_lawpack_value(exports_bytes, EXPORTS_CBOR_PATH)?; + let manifest = parse_manifest(&manifest_value)?; + let exports = parse_exports(&exports_value)?; + + let exports_digest = + digest_lawpack_value(&manifest.exports.id, &exports_value, EXPORTS_CBOR_PATH)?; + if exports_digest != manifest.exports.digest { + return Err(one(failure( + LawpackValidationFailureKind::ExportsDigestMismatch, + "manifest.exports.digest", + "digest of the exact canonical export surface", + ))); + } + if exports + .effects + .iter() + .any(|effect| effect.execution_class == LawpackExecutionClass::Runtime) + && manifest.target_adapters.is_empty() + { + return Err(one(failure( + LawpackValidationFailureKind::RuntimeEffectWithoutTargetAdapter, + "manifest.targetAdapters", + "at least one digest-locked target adapter for runtime effects", + ))); + } + + let manifest_digest = + digest_lawpack_value(LAWPACK_API_VERSION, &manifest_value, LAWPACK_CBOR_PATH)?; + Ok(ValidatedLawpackBundle { + manifest, + exports, + manifest_digest, + exports_digest, + }) +} + +/// Validate that a complete supplied lawpack set satisfies every dependency +/// edge with an exact manifest digest and contains no cycle. +/// +/// Input ordering does not affect validation: identities are canonicalized in +/// ordered maps before edges are checked. +/// +/// # Errors +/// +/// Returns stable failures for duplicate identities, missing or substituted +/// dependencies, self-dependencies, or dependency cycles. +pub fn validate_lawpack_dependency_graph( + bundles: &[ValidatedLawpackBundle], +) -> Result<(), Vec> { + let mut by_identity = BTreeMap::<(String, String), &ValidatedLawpackBundle>::new(); + for bundle in bundles { + let key = (bundle.manifest.id.clone(), bundle.manifest.version.clone()); + if by_identity.insert(key.clone(), bundle).is_some() { + return Err(one(failure( + LawpackValidationFailureKind::DuplicateIdentity, + format!("lawpacks.{}@{}", key.0, key.1), + "one manifest per lawpack id and version", + ))); + } + } + + let mut visiting = BTreeSet::new(); + let mut visited = BTreeSet::new(); + for identity in by_identity.keys() { + visit_dependency(identity, &by_identity, &mut visiting, &mut visited)?; + } + + for (identity, bundle) in &by_identity { + for dependency in &bundle.manifest.dependencies { + let dependency_identity = (dependency.id.clone(), dependency.version.clone()); + if dependency_identity == *identity { + return Err(one(failure( + LawpackValidationFailureKind::SelfDependency, + format!("lawpacks.{}@{}.dependencies", identity.0, identity.1), + "a lawpack cannot depend on itself", + ))); + } + let Some(resolved) = by_identity.get(&dependency_identity) else { + return Err(one(failure( + LawpackValidationFailureKind::MissingDependency, + format!( + "lawpacks.{}@{}.dependencies.{}@{}", + identity.0, identity.1, dependency.id, dependency.version + ), + "every dependency must be present in the supplied closed set", + ))); + }; + if resolved.manifest_digest != dependency.digest { + return Err(one(failure( + LawpackValidationFailureKind::DependencyDigestMismatch, + format!( + "lawpacks.{}@{}.dependencies.{}@{}", + identity.0, identity.1, dependency.id, dependency.version + ), + "dependency digest must equal the resolved manifest digest", + ))); + } + } + } + + Ok(()) +} + +fn visit_dependency( + identity: &(String, String), + bundles: &BTreeMap<(String, String), &ValidatedLawpackBundle>, + visiting: &mut BTreeSet<(String, String)>, + visited: &mut BTreeSet<(String, String)>, +) -> Result<(), Vec> { + if visited.contains(identity) { + return Ok(()); + } + if !visiting.insert(identity.clone()) { + return Err(one(failure( + LawpackValidationFailureKind::DependencyCycle, + format!("lawpacks.{}@{}.dependencies", identity.0, identity.1), + "acyclic dependency graph", + ))); + } + if let Some(bundle) = bundles.get(identity) { + for dependency in &bundle.manifest.dependencies { + let dependency_identity = (dependency.id.clone(), dependency.version.clone()); + if bundles.contains_key(&dependency_identity) { + visit_dependency(&dependency_identity, bundles, visiting, visited)?; + } + } + } + visiting.remove(identity); + visited.insert(identity.clone()); + Ok(()) +} + +fn parse_manifest( + value: &CanonicalValue, +) -> Result> { + let path = "manifest"; + let fields = closed_map( + value, + path, + &[ + "apiVersion", + "id", + "version", + "acceptedCoreAbi", + "dependencies", + "exports", + "targetAdapters", + "helperComponent", + "verifier", + "compatibility", + "conformanceFixtureCorpus", + ], + )?; + let (api_version, id, version, accepted_core_abi) = parse_manifest_identity(&fields, path)?; + let dependencies = parse_dependencies(&fields, &id, &version, path)?; + + let exports = parse_resource_ref(required(&fields, "exports", path)?, "manifest.exports")?; + let target_adapters = parse_target_adapters(&fields)?; + let helper_component = fields + .get("helperComponent") + .map(|value| parse_executable_component(value, "manifest.helperComponent")) + .transpose()?; + let verifier = parse_verifier(required(&fields, "verifier", path)?, "manifest.verifier")?; + let compatibility = parse_resource_ref( + required(&fields, "compatibility", path)?, + "manifest.compatibility", + )?; + let conformance_fixture_corpus = parse_resource_ref( + required(&fields, "conformanceFixtureCorpus", path)?, + "manifest.conformanceFixtureCorpus", + )?; + + Ok(LawpackManifest { + api_version, + id, + version, + accepted_core_abi, + dependencies, + exports, + target_adapters, + helper_component, + verifier, + compatibility, + conformance_fixture_corpus, + }) +} + +fn parse_manifest_identity( + fields: &BTreeMap<&str, &CanonicalValue>, + path: &str, +) -> Result<(String, String, String, Vec), Vec> { + let api_version = required_text(fields, "apiVersion", path)?; + if api_version != LAWPACK_API_VERSION { + return Err(one(failure( + LawpackValidationFailureKind::InvalidApiVersion, + "manifest.apiVersion", + LAWPACK_API_VERSION, + ))); + } + let id = required_nonempty_text(fields, "id", path)?; + let version = required_nonempty_text(fields, "version", path)?; + let accepted_core_abi = required_text_array(fields, "acceptedCoreAbi", path, true)?; + if !accepted_core_abi.iter().any(|abi| abi == CORE_API_VERSION) { + return Err(one(failure( + LawpackValidationFailureKind::MissingAcceptedCoreAbi, + "manifest.acceptedCoreAbi", + CORE_API_VERSION, + ))); + } + Ok((api_version, id, version, accepted_core_abi)) +} + +fn parse_dependencies( + fields: &BTreeMap<&str, &CanonicalValue>, + manifest_id: &str, + manifest_version: &str, + path: &str, +) -> Result, Vec> { + let values = array( + required(fields, "dependencies", path)?, + "manifest.dependencies", + )?; + let mut dependencies = Vec::with_capacity(values.len()); + let mut identities = BTreeSet::new(); + for (index, dependency) in values.iter().enumerate() { + let dependency_path = format!("manifest.dependencies[{index}]"); + let parsed = parse_dependency(dependency, &dependency_path)?; + if !identities.insert((parsed.id.clone(), parsed.version.clone())) { + return Err(one(failure( + LawpackValidationFailureKind::DuplicateIdentity, + dependency_path, + "unique dependency id and version", + ))); + } + if parsed.id == manifest_id && parsed.version == manifest_version { + return Err(one(failure( + LawpackValidationFailureKind::SelfDependency, + "manifest.dependencies", + "a lawpack cannot depend on itself", + ))); + } + dependencies.push(parsed); + } + Ok(dependencies) +} + +fn parse_target_adapters( + fields: &BTreeMap<&str, &CanonicalValue>, +) -> Result, Vec> { + let Some(value) = fields.get("targetAdapters") else { + return Ok(Vec::new()); + }; + let values = array(value, "manifest.targetAdapters")?; + if values.is_empty() { + return Err(one(failure( + LawpackValidationFailureKind::EmptyRequiredCollection, + "manifest.targetAdapters", + "the optional field must contain at least one adapter when present", + ))); + } + let mut adapters = Vec::with_capacity(values.len()); + let mut selectors = BTreeSet::new(); + for (index, adapter) in values.iter().enumerate() { + let adapter_path = format!("manifest.targetAdapters[{index}]"); + let parsed = parse_target_adapter(adapter, &adapter_path)?; + let selector = ( + parsed.accepted_target_profile.id.clone(), + parsed.accepted_target_profile.digest, + ); + if !selectors.insert(selector) { + return Err(one(failure( + LawpackValidationFailureKind::DuplicateIdentity, + adapter_path, + "one adapter per exact accepted target-profile identity", + ))); + } + adapters.push(parsed); + } + Ok(adapters) +} + +fn parse_dependency( + value: &CanonicalValue, + path: &str, +) -> Result> { + let fields = closed_map(value, path, &["id", "version", "digest"])?; + Ok(LawpackDependency { + id: required_nonempty_text(&fields, "id", path)?, + version: required_nonempty_text(&fields, "version", path)?, + digest: parse_digest( + required(&fields, "digest", path)?, + &format!("{path}.digest"), + )?, + }) +} + +fn parse_target_adapter( + value: &CanonicalValue, + path: &str, +) -> Result> { + let fields = closed_map( + value, + path, + &["acceptedTargetProfile", "acceptedTargetIr", "adapter"], + )?; + Ok(LawpackTargetAdapter { + accepted_target_profile: parse_resource_ref( + required(&fields, "acceptedTargetProfile", path)?, + &format!("{path}.acceptedTargetProfile"), + )?, + accepted_target_ir: parse_resource_ref( + required(&fields, "acceptedTargetIr", path)?, + &format!("{path}.acceptedTargetIr"), + )?, + adapter: parse_resource_ref( + required(&fields, "adapter", path)?, + &format!("{path}.adapter"), + )?, + }) +} + +fn parse_executable_component( + value: &CanonicalValue, + path: &str, +) -> Result> { + let fields = closed_map(value, path, &["component", "sandbox", "fuelModel"])?; + Ok(LawpackExecutableComponent { + component: parse_resource_ref( + required(&fields, "component", path)?, + &format!("{path}.component"), + )?, + sandbox: parse_resource_ref( + required(&fields, "sandbox", path)?, + &format!("{path}.sandbox"), + )?, + fuel_model: parse_resource_ref( + required(&fields, "fuelModel", path)?, + &format!("{path}.fuelModel"), + )?, + }) +} + +fn parse_verifier( + value: &CanonicalValue, + path: &str, +) -> Result> { + let fields = string_keyed_map(value, path)?; + let class = required_text(&fields, "class", path)?; + match class.as_str() { + "declarative" => { + ensure_allowed_fields(&fields, path, &["class", "ruleset"])?; + Ok(LawpackVerifier::Declarative { + ruleset: parse_resource_ref( + required(&fields, "ruleset", path)?, + &format!("{path}.ruleset"), + )?, + }) + } + "executable" => { + ensure_allowed_fields( + &fields, + path, + &["class", "component", "sandbox", "fuelModel"], + )?; + Ok(LawpackVerifier::Executable { + executable: LawpackExecutableComponent { + component: parse_resource_ref( + required(&fields, "component", path)?, + &format!("{path}.component"), + )?, + sandbox: parse_resource_ref( + required(&fields, "sandbox", path)?, + &format!("{path}.sandbox"), + )?, + fuel_model: parse_resource_ref( + required(&fields, "fuelModel", path)?, + &format!("{path}.fuelModel"), + )?, + }, + }) + } + _ => Err(one(failure( + LawpackValidationFailureKind::InvalidDiscriminant, + format!("{path}.class"), + "declarative or executable", + ))), + } +} + +fn parse_exports(value: &CanonicalValue) -> Result> { + let path = "exports"; + let fields = closed_map( + value, + path, + &[ + "types", + "constants", + "pureFunctions", + "effects", + "obstructions", + "operationProfiles", + ], + )?; + let types = parse_array_field(&fields, "types", path, parse_exported_type)?; + let constants = parse_array_field(&fields, "constants", path, parse_exported_constant)?; + let pure_functions = parse_array_field(&fields, "pureFunctions", path, parse_pure_function)?; + validate_pure_function_callees(&pure_functions)?; + let effects = parse_array_field(&fields, "effects", path, parse_semantic_effect)?; + let obstructions = parse_array_field(&fields, "obstructions", path, parse_obstruction)?; + let operation_profiles = parse_operation_profiles( + required(&fields, "operationProfiles", path)?, + "exports.operationProfiles", + )?; + + Ok(LawpackExports { + types, + constants, + pure_functions, + effects, + obstructions, + operation_profiles, + }) +} + +fn validate_pure_function_callees( + pure_functions: &[LawpackPureFunction], +) -> Result<(), Vec> { + let pure_coordinates = pure_functions + .iter() + .map(|function| function.coordinate.as_str()) + .collect::>(); + for (index, function) in pure_functions.iter().enumerate() { + if let LawpackPureFunctionImplementation::Edict { body } = &function.implementation { + validate_pure_callees_in_value( + body, + &format!("exports.pureFunctions[{index}].body"), + &pure_coordinates, + )?; + } + } + Ok(()) +} + +fn validate_pure_callees_in_value( + value: &CanonicalValue, + path: &str, + pure_coordinates: &BTreeSet<&str>, +) -> Result<(), Vec> { + match value { + CanonicalValue::Array(values) => { + for (index, value) in values.iter().enumerate() { + validate_pure_callees_in_value( + value, + &format!("{path}[{index}]"), + pure_coordinates, + )?; + } + } + CanonicalValue::Map(entries) => { + let kind = entries.iter().find_map(|(key, value)| match (key, value) { + (CanonicalValue::Text(key), CanonicalValue::Text(value)) if key == "kind" => { + Some(value.as_str()) + } + _ => None, + }); + if kind == Some("call") { + let callee = entries.iter().find_map(|(key, value)| match (key, value) { + (CanonicalValue::Text(key), CanonicalValue::Text(value)) if key == "callee" => { + Some(value.as_str()) + } + _ => None, + }); + if callee.is_some_and(|callee| !pure_coordinates.contains(callee)) { + return Err(pure_body_failure( + &format!("{path}.callee"), + "the coordinate of an exported pure function", + )); + } + } + for (index, (_, value)) in entries.iter().enumerate() { + validate_pure_callees_in_value( + value, + &format!("{path}.entry[{index}]"), + pure_coordinates, + )?; + } + } + CanonicalValue::Null + | CanonicalValue::Bool(_) + | CanonicalValue::Integer(_) + | CanonicalValue::Bytes(_) + | CanonicalValue::Text(_) => {} + } + Ok(()) +} + +fn parse_array_field( + fields: &BTreeMap<&str, &CanonicalValue>, + field: &str, + parent: &str, + parse: fn(&CanonicalValue, &str) -> Result>, +) -> Result, Vec> { + let path = format!("{parent}.{field}"); + let values = array(required(fields, field, parent)?, &path)?; + let mut parsed = Vec::with_capacity(values.len()); + let mut coordinates = BTreeSet::new(); + for (index, value) in values.iter().enumerate() { + let item_path = format!("{path}[{index}]"); + let item = parse(value, &item_path)?; + let coordinate = export_coordinate(value, &item_path)?; + if !coordinates.insert(coordinate) { + return Err(one(failure( + LawpackValidationFailureKind::DuplicateIdentity, + item_path, + "unique export coordinate within the category", + ))); + } + parsed.push(item); + } + Ok(parsed) +} + +fn export_coordinate( + value: &CanonicalValue, + path: &str, +) -> Result> { + let fields = string_keyed_map(value, path)?; + required_nonempty_text(&fields, "coordinate", path) +} + +fn parse_exported_type( + value: &CanonicalValue, + path: &str, +) -> Result> { + let fields = closed_map(value, path, &["coordinate", "definition"])?; + Ok(LawpackExportedType { + coordinate: required_nonempty_text(&fields, "coordinate", path)?, + definition: required_nonempty_text(&fields, "definition", path)?, + }) +} + +fn parse_exported_constant( + value: &CanonicalValue, + path: &str, +) -> Result> { + let fields = closed_map(value, path, &["coordinate", "type", "value"])?; + Ok(LawpackExportedConstant { + coordinate: required_nonempty_text(&fields, "coordinate", path)?, + ty: required_nonempty_text(&fields, "type", path)?, + value: required(&fields, "value", path)?.clone(), + }) +} + +fn parse_pure_function( + value: &CanonicalValue, + path: &str, +) -> Result> { + let fields = string_keyed_map(value, path)?; + let source = required_text(&fields, "source", path)?; + let implementation = match source.as_str() { + "edict" => { + ensure_allowed_fields( + &fields, + path, + &[ + "coordinate", + "typeParameters", + "parameterTypes", + "returnType", + "costTemplate", + "determinismClass", + "source", + "body", + ], + )?; + let body = required(&fields, "body", path)?; + validate_core_fn_body(body, &format!("{path}.body"))?; + LawpackPureFunctionImplementation::Edict { body: body.clone() } + } + "component" => { + ensure_allowed_fields( + &fields, + path, + &[ + "coordinate", + "typeParameters", + "parameterTypes", + "returnType", + "costTemplate", + "determinismClass", + "source", + "implementation", + ], + )?; + LawpackPureFunctionImplementation::Component { + implementation: parse_executable_component( + required(&fields, "implementation", path)?, + &format!("{path}.implementation"), + )?, + } + } + _ => { + return Err(one(failure( + LawpackValidationFailureKind::InvalidDiscriminant, + format!("{path}.source"), + "edict or component", + ))); + } + }; + let determinism_class = match required_text(&fields, "determinismClass", path)?.as_str() { + "total" => LawpackDeterminismClass::Total, + "total-with-typed-diagnostic" => LawpackDeterminismClass::TotalWithTypedDiagnostic, + _ => { + return Err(one(failure( + LawpackValidationFailureKind::InvalidDiscriminant, + format!("{path}.determinismClass"), + "total or total-with-typed-diagnostic", + ))); + } + }; + Ok(LawpackPureFunction { + coordinate: required_nonempty_text(&fields, "coordinate", path)?, + type_parameters: required_text_array(&fields, "typeParameters", path, false)?, + parameter_types: required_text_array(&fields, "parameterTypes", path, false)?, + return_type: required_nonempty_text(&fields, "returnType", path)?, + cost_template: required_nonempty_text(&fields, "costTemplate", path)?, + determinism_class, + implementation, + }) +} + +fn parse_semantic_effect( + value: &CanonicalValue, + path: &str, +) -> Result> { + let fields = closed_map( + value, + path, + &[ + "coordinate", + "typeParameters", + "inputType", + "outputType", + "executionClass", + "effectKindHint", + "footprintObligation", + "costObligation", + "effectFailures", + "guardSupport", + ], + )?; + let execution_class = match required_text(&fields, "executionClass", path)?.as_str() { + "proofOnly" => LawpackExecutionClass::ProofOnly, + "runtime" => LawpackExecutionClass::Runtime, + _ => { + return Err(one(failure( + LawpackValidationFailureKind::InvalidDiscriminant, + format!("{path}.executionClass"), + "proofOnly or runtime", + ))); + } + }; + let effect_kind_hint = match required_text(&fields, "effectKindHint", path)?.as_str() { + "read" => LawpackEffectKind::Read, + "create" => LawpackEffectKind::Create, + "ensure" => LawpackEffectKind::Ensure, + "replace" => LawpackEffectKind::Replace, + "delete" => LawpackEffectKind::Delete, + "append" => LawpackEffectKind::Append, + "reduce" => LawpackEffectKind::Reduce, + "semantic.emit" => LawpackEffectKind::SemanticEmit, + "custom" => LawpackEffectKind::Custom, + _ => { + return Err(one(failure( + LawpackValidationFailureKind::InvalidDiscriminant, + format!("{path}.effectKindHint"), + "a supported semantic effect-kind hint", + ))); + } + }; + let effect_failures = parse_effect_failures( + required(&fields, "effectFailures", path)?, + &format!("{path}.effectFailures"), + )?; + Ok(LawpackSemanticEffect { + coordinate: required_nonempty_text(&fields, "coordinate", path)?, + type_parameters: required_text_array(&fields, "typeParameters", path, false)?, + input_type: required_nonempty_text(&fields, "inputType", path)?, + output_type: required_nonempty_text(&fields, "outputType", path)?, + execution_class, + effect_kind_hint, + footprint_obligation: required_nonempty_text(&fields, "footprintObligation", path)?, + cost_obligation: required_nonempty_text(&fields, "costObligation", path)?, + effect_failures, + guard_support: required_bool(&fields, "guardSupport", path)?, + }) +} + +fn parse_effect_failures( + value: &CanonicalValue, + path: &str, +) -> Result, Vec> { + let fields = string_keyed_map(value, path)?; + let mut failures = BTreeMap::new(); + for (identifier, value) in fields { + validate_failure_identifier(identifier, &format!("{path}.{identifier}"))?; + let failure_path = format!("{path}.{identifier}"); + let body = closed_map(value, &failure_path, &["authorityClass", "payloadType"])?; + failures.insert( + identifier.to_owned(), + LawpackEffectFailure { + authority_class: parse_authority_class( + &required_text(&body, "authorityClass", &failure_path)?, + &format!("{failure_path}.authorityClass"), + )?, + payload_type: required_nonempty_text(&body, "payloadType", &failure_path)?, + }, + ); + } + Ok(failures) +} + +fn validate_failure_identifier( + identifier: &str, + path: &str, +) -> Result<(), Vec> { + let mut bytes = identifier.bytes(); + let valid_start = bytes + .next() + .is_some_and(|byte| byte.is_ascii_alphabetic() || byte == b'_'); + if !valid_start || !bytes.all(|byte| byte.is_ascii_alphanumeric() || byte == b'_') { + return Err(one(failure( + LawpackValidationFailureKind::InvalidFailureIdentifier, + path, + "a bare Edict identifier", + ))); + } + if is_keyword(identifier) { + return Err(one(failure( + LawpackValidationFailureKind::ReservedFailureIdentifier, + path, + "a non-keyword Edict identifier", + ))); + } + Ok(()) +} + +fn parse_obstruction( + value: &CanonicalValue, + path: &str, +) -> Result> { + let fields = closed_map( + value, + path, + &["coordinate", "authorityClass", "payloadSchema"], + )?; + Ok(LawpackObstruction { + coordinate: required_nonempty_text(&fields, "coordinate", path)?, + authority_class: parse_authority_class( + &required_text(&fields, "authorityClass", path)?, + &format!("{path}.authorityClass"), + )?, + payload_schema: required_nonempty_text(&fields, "payloadSchema", path)?, + }) +} + +fn parse_authority_class( + value: &str, + path: &str, +) -> Result> { + match value { + "domainMappable" => Ok(LawpackAuthorityClass::DomainMappable), + "participantOwned" => Ok(LawpackAuthorityClass::ParticipantOwned), + "integrityFault" => Ok(LawpackAuthorityClass::IntegrityFault), + "resourceFault" => Ok(LawpackAuthorityClass::ResourceFault), + "internalFault" => Ok(LawpackAuthorityClass::InternalFault), + _ => Err(one(failure( + LawpackValidationFailureKind::InvalidDiscriminant, + path, + "a supported authority class", + ))), + } +} + +fn parse_operation_profiles( + value: &CanonicalValue, + path: &str, +) -> Result, Vec> { + let fields = string_keyed_map(value, path)?; + let mut profiles = BTreeMap::new(); + for (coordinate, value) in fields { + if coordinate.is_empty() { + return Err(one(failure( + LawpackValidationFailureKind::EmptyIdentity, + path, + "non-empty operation-profile coordinate", + ))); + } + let profile_path = format!("{path}.{coordinate}"); + let profile = closed_map(value, &profile_path, &["opticTemplate", "effectPredicate"])?; + profiles.insert( + coordinate.to_owned(), + LawpackOperationProfile { + optic_template: parse_optic_template( + required(&profile, "opticTemplate", &profile_path)?, + &format!("{profile_path}.opticTemplate"), + )?, + effect_predicate: required_nonempty_text( + &profile, + "effectPredicate", + &profile_path, + )?, + }, + ); + } + Ok(profiles) +} + +fn parse_optic_template( + value: &CanonicalValue, + path: &str, +) -> Result> { + let fields = closed_map( + value, + path, + &[ + "opticKind", + "boundaryKind", + "supportPolicy", + "lossDisposition", + "basisTemplate", + "apertureRequirement", + ], + )?; + let optic_kind = required_text(&fields, "opticKind", path)?; + if !matches!(optic_kind.as_str(), "revelation" | "affectReintegration") { + return Err(one(failure( + LawpackValidationFailureKind::InvalidDiscriminant, + format!("{path}.opticKind"), + "revelation or affectReintegration", + ))); + } + let boundary_kind = required_text(&fields, "boundaryKind", path)?; + if !matches!(boundary_kind.as_str(), "projection" | "affect") { + return Err(one(failure( + LawpackValidationFailureKind::InvalidDiscriminant, + format!("{path}.boundaryKind"), + "projection or affect", + ))); + } + let basis_template = fields + .get("basisTemplate") + .map(|value| nonempty_text(value, &format!("{path}.basisTemplate"))) + .transpose()?; + let aperture_requirement = fields + .get("apertureRequirement") + .map(|value| parse_aperture_requirement(value, &format!("{path}.apertureRequirement"))) + .transpose()?; + Ok(LawpackOpticTemplate { + optic_kind, + boundary_kind, + support_policy: required_nonempty_text(&fields, "supportPolicy", path)?, + loss_disposition: required_nonempty_text(&fields, "lossDisposition", path)?, + basis_template, + aperture_requirement, + }) +} + +fn parse_aperture_requirement( + value: &CanonicalValue, + path: &str, +) -> Result> { + let fields = closed_map(value, path, &["kind", "ref"])?; + let reference = required_nonempty_text(&fields, "ref", path)?; + match required_text(&fields, "kind", path)?.as_str() { + "footprintCeiling" => Ok(LawpackApertureRequirement::FootprintCeiling { reference }), + "abstractFootprintObligation" => { + Ok(LawpackApertureRequirement::AbstractFootprintObligation { reference }) + } + _ => Err(one(failure( + LawpackValidationFailureKind::InvalidDiscriminant, + format!("{path}.kind"), + "footprintCeiling or abstractFootprintObligation", + ))), + } +} + +fn validate_core_fn_body( + value: &CanonicalValue, + path: &str, +) -> Result<(), Vec> { + let fields = pure_map(value, path, &["params", "body"])?; + let params = pure_array( + required(&fields, "params", path)?, + &format!("{path}.params"), + )?; + for (index, param) in params.iter().enumerate() { + validate_local_ref(param, &format!("{path}.params[{index}]"))?; + } + validate_core_pure_block(required(&fields, "body", path)?, &format!("{path}.body")) +} + +fn validate_core_pure_block( + value: &CanonicalValue, + path: &str, +) -> Result<(), Vec> { + let fields = pure_map(value, path, &["locals", "bindings", "result"])?; + let locals = pure_array( + required(&fields, "locals", path)?, + &format!("{path}.locals"), + )?; + for (index, local) in locals.iter().enumerate() { + validate_local_ref(local, &format!("{path}.locals[{index}]"))?; + } + let bindings = pure_array( + required(&fields, "bindings", path)?, + &format!("{path}.bindings"), + )?; + for (index, binding) in bindings.iter().enumerate() { + let binding_path = format!("{path}.bindings[{index}]"); + let binding_fields = pure_map(binding, &binding_path, &["kind", "binding", "value"])?; + pure_discriminant(&binding_fields, "kind", &binding_path, "let")?; + validate_local_ref( + required(&binding_fields, "binding", &binding_path)?, + &format!("{binding_path}.binding"), + )?; + validate_core_expr( + required(&binding_fields, "value", &binding_path)?, + &format!("{binding_path}.value"), + )?; + } + validate_core_expr( + required(&fields, "result", path)?, + &format!("{path}.result"), + ) +} + +fn validate_local_ref( + value: &CanonicalValue, + path: &str, +) -> Result<(), Vec> { + let fields = pure_map(value, path, &["id", "alphaName", "type"])?; + pure_nonempty_text(&fields, "id", path)?; + pure_nonempty_text(&fields, "alphaName", path)?; + pure_nonempty_text(&fields, "type", path)?; + Ok(()) +} + +fn validate_core_expr( + value: &CanonicalValue, + path: &str, +) -> Result<(), Vec> { + let fields = string_keyed_map(value, path).map_err(as_pure_body_failure)?; + let kind = required_text(&fields, "kind", path).map_err(as_pure_body_failure)?; + validate_core_expr_fields(&fields, &kind, path) +} + +fn validate_core_expr_fields( + fields: &BTreeMap<&str, &CanonicalValue>, + kind: &str, + path: &str, +) -> Result<(), Vec> { + match kind { + "local" => { + ensure_allowed_fields(fields, path, &["kind", "ref"]).map_err(as_pure_body_failure)?; + validate_local_ref( + required(fields, "ref", path).map_err(as_pure_body_failure)?, + &format!("{path}.ref"), + ) + } + "const" => { + ensure_allowed_fields(fields, path, &["kind", "value"]) + .map_err(as_pure_body_failure)?; + validate_core_value( + required(fields, "value", path).map_err(as_pure_body_failure)?, + &format!("{path}.value"), + ) + } + "record" => { + ensure_allowed_fields(fields, path, &["kind", "fields"]) + .map_err(as_pure_body_failure)?; + let values = string_keyed_map( + required(fields, "fields", path).map_err(as_pure_body_failure)?, + &format!("{path}.fields"), + ) + .map_err(as_pure_body_failure)?; + for (field, value) in values { + validate_core_expr(value, &format!("{path}.fields.{field}"))?; + } + Ok(()) + } + "field" => { + ensure_allowed_fields(fields, path, &["kind", "base", "field"]) + .map_err(as_pure_body_failure)?; + validate_core_expr( + required(fields, "base", path).map_err(as_pure_body_failure)?, + &format!("{path}.base"), + )?; + required_nonempty_text(fields, "field", path).map_err(as_pure_body_failure)?; + Ok(()) + } + "variant" => { + ensure_allowed_fields(fields, path, &["kind", "type", "case", "payload"]) + .map_err(as_pure_body_failure)?; + required_nonempty_text(fields, "type", path).map_err(as_pure_body_failure)?; + required_nonempty_text(fields, "case", path).map_err(as_pure_body_failure)?; + if let Some(payload) = fields.get("payload") { + validate_core_expr(payload, &format!("{path}.payload"))?; + } + Ok(()) + } + "match" => validate_match_expr(fields, path), + "call" => { + ensure_allowed_fields(fields, path, &["kind", "callee", "typeArgs", "args"]) + .map_err(as_pure_body_failure)?; + required_nonempty_text(fields, "callee", path).map_err(as_pure_body_failure)?; + validate_text_values( + required(fields, "typeArgs", path).map_err(as_pure_body_failure)?, + &format!("{path}.typeArgs"), + )?; + validate_expr_values( + required(fields, "args", path).map_err(as_pure_body_failure)?, + &format!("{path}.args"), + ) + } + "list" => { + ensure_allowed_fields(fields, path, &["kind", "values"]) + .map_err(as_pure_body_failure)?; + validate_expr_values( + required(fields, "values", path).map_err(as_pure_body_failure)?, + &format!("{path}.values"), + ) + } + "map" => { + ensure_allowed_fields(fields, path, &["kind", "entries"]) + .map_err(as_pure_body_failure)?; + validate_expr_entries( + required(fields, "entries", path).map_err(as_pure_body_failure)?, + &format!("{path}.entries"), + ) + } + "if" => { + ensure_allowed_fields(fields, path, &["kind", "predicate", "then", "else"]) + .map_err(as_pure_body_failure)?; + validate_core_predicate( + required(fields, "predicate", path).map_err(as_pure_body_failure)?, + &format!("{path}.predicate"), + )?; + validate_core_expr( + required(fields, "then", path).map_err(as_pure_body_failure)?, + &format!("{path}.then"), + )?; + validate_core_expr( + required(fields, "else", path).map_err(as_pure_body_failure)?, + &format!("{path}.else"), + ) + } + _ => Err(pure_body_failure( + path, + "a Core pure-expression discriminant", + )), + } +} + +fn validate_match_expr( + fields: &BTreeMap<&str, &CanonicalValue>, + path: &str, +) -> Result<(), Vec> { + ensure_allowed_fields(fields, path, &["kind", "scrutinee", "arms"]) + .map_err(as_pure_body_failure)?; + validate_core_expr( + required(fields, "scrutinee", path).map_err(as_pure_body_failure)?, + &format!("{path}.scrutinee"), + )?; + let arms = pure_array( + required(fields, "arms", path).map_err(as_pure_body_failure)?, + &format!("{path}.arms"), + )?; + if arms.is_empty() { + return Err(pure_body_failure(path, "at least one match arm")); + } + for (index, arm) in arms.iter().enumerate() { + let arm_path = format!("{path}.arms[{index}]"); + let arm_fields = pure_map(arm, &arm_path, &["case", "binder", "body"])?; + pure_nonempty_text(&arm_fields, "case", &arm_path)?; + if let Some(binder) = arm_fields.get("binder") { + validate_local_ref(binder, &format!("{arm_path}.binder"))?; + } + validate_core_expr( + required(&arm_fields, "body", &arm_path).map_err(as_pure_body_failure)?, + &format!("{arm_path}.body"), + )?; + } + Ok(()) +} + +fn validate_core_predicate( + value: &CanonicalValue, + path: &str, +) -> Result<(), Vec> { + let fields = string_keyed_map(value, path).map_err(as_pure_body_failure)?; + let kind = required_text(&fields, "kind", path).map_err(as_pure_body_failure)?; + match kind.as_str() { + "true" | "false" => { + ensure_allowed_fields(&fields, path, &["kind"]).map_err(as_pure_body_failure) + } + "not" => { + ensure_allowed_fields(&fields, path, &["kind", "value"]) + .map_err(as_pure_body_failure)?; + validate_core_predicate( + required(&fields, "value", path).map_err(as_pure_body_failure)?, + &format!("{path}.value"), + ) + } + "all" | "any" => { + ensure_allowed_fields(&fields, path, &["kind", "values"]) + .map_err(as_pure_body_failure)?; + let values = pure_array( + required(&fields, "values", path).map_err(as_pure_body_failure)?, + &format!("{path}.values"), + )?; + if values.is_empty() { + return Err(pure_body_failure(path, "at least one predicate")); + } + for (index, predicate) in values.iter().enumerate() { + validate_core_predicate(predicate, &format!("{path}.values[{index}]"))?; + } + Ok(()) + } + "compare" => { + ensure_allowed_fields(&fields, path, &["kind", "op", "left", "right"]) + .map_err(as_pure_body_failure)?; + let op = required_text(&fields, "op", path).map_err(as_pure_body_failure)?; + if !matches!(op.as_str(), "==" | "!=" | "<" | "<=" | ">" | ">=") { + return Err(pure_body_failure( + &format!("{path}.op"), + "a Core comparison operator", + )); + } + validate_core_expr( + required(&fields, "left", path).map_err(as_pure_body_failure)?, + &format!("{path}.left"), + )?; + validate_core_expr( + required(&fields, "right", path).map_err(as_pure_body_failure)?, + &format!("{path}.right"), + ) + } + "call" => { + ensure_allowed_fields(&fields, path, &["kind", "predicate", "args"]) + .map_err(as_pure_body_failure)?; + required_nonempty_text(&fields, "predicate", path).map_err(as_pure_body_failure)?; + validate_expr_values( + required(&fields, "args", path).map_err(as_pure_body_failure)?, + &format!("{path}.args"), + ) + } + "obstruction" => { + ensure_allowed_fields(&fields, path, &["kind", "coordinate", "payload"]) + .map_err(as_pure_body_failure)?; + let coordinate = required_nonempty_text(&fields, "coordinate", path) + .map_err(as_pure_body_failure)?; + validate_failure_identifier(&coordinate, &format!("{path}.coordinate")) + .map_err(as_pure_body_failure)?; + validate_core_expr( + required(&fields, "payload", path).map_err(as_pure_body_failure)?, + &format!("{path}.payload"), + ) + } + _ => Err(pure_body_failure(path, "a Core predicate discriminant")), + } +} + +fn validate_core_value( + value: &CanonicalValue, + path: &str, +) -> Result<(), Vec> { + let fields = string_keyed_map(value, path).map_err(as_pure_body_failure)?; + let kind = required_text(&fields, "kind", path).map_err(as_pure_body_failure)?; + validate_core_value_fields(&fields, &kind, path) +} + +fn validate_core_value_fields( + fields: &BTreeMap<&str, &CanonicalValue>, + kind: &str, + path: &str, +) -> Result<(), Vec> { + match kind { + "null" | "bool" | "int" | "string" | "bytes" | "capability" => { + validate_core_scalar_value(fields, kind, path) + } + "record" => { + ensure_allowed_fields(fields, path, &["kind", "fields"]) + .map_err(as_pure_body_failure)?; + let values = string_keyed_map( + required(fields, "fields", path).map_err(as_pure_body_failure)?, + &format!("{path}.fields"), + ) + .map_err(as_pure_body_failure)?; + for (field, value) in values { + validate_core_value(value, &format!("{path}.fields.{field}"))?; + } + Ok(()) + } + "variant" => { + ensure_allowed_fields(fields, path, &["kind", "type", "case", "payload"]) + .map_err(as_pure_body_failure)?; + required_nonempty_text(fields, "type", path).map_err(as_pure_body_failure)?; + required_nonempty_text(fields, "case", path).map_err(as_pure_body_failure)?; + if let Some(payload) = fields.get("payload") { + validate_core_value(payload, &format!("{path}.payload"))?; + } + Ok(()) + } + "list" => { + ensure_allowed_fields(fields, path, &["kind", "values"]) + .map_err(as_pure_body_failure)?; + let values = pure_array( + required(fields, "values", path).map_err(as_pure_body_failure)?, + &format!("{path}.values"), + )?; + for (index, value) in values.iter().enumerate() { + validate_core_value(value, &format!("{path}.values[{index}]"))?; + } + Ok(()) + } + "map" => { + ensure_allowed_fields(fields, path, &["kind", "entries"]) + .map_err(as_pure_body_failure)?; + let entries = pure_array( + required(fields, "entries", path).map_err(as_pure_body_failure)?, + &format!("{path}.entries"), + )?; + for (index, entry) in entries.iter().enumerate() { + let entry_path = format!("{path}.entries[{index}]"); + let pair = pure_array(entry, &entry_path)?; + if pair.len() != 2 { + return Err(pure_body_failure(&entry_path, "a key/value pair")); + } + validate_core_value(&pair[0], &format!("{entry_path}[0]"))?; + validate_core_value(&pair[1], &format!("{entry_path}[1]"))?; + } + Ok(()) + } + _ => Err(pure_body_failure( + path, + "a Core canonical-value discriminant", + )), + } +} + +fn validate_core_scalar_value( + fields: &BTreeMap<&str, &CanonicalValue>, + kind: &str, + path: &str, +) -> Result<(), Vec> { + match kind { + "null" => ensure_allowed_fields(fields, path, &["kind"]).map_err(as_pure_body_failure), + "bool" => { + ensure_allowed_fields(fields, path, &["kind", "value"]) + .map_err(as_pure_body_failure)?; + required_bool(fields, "value", path) + .map(|_| ()) + .map_err(as_pure_body_failure) + } + "int" => { + ensure_allowed_fields(fields, path, &["kind", "width", "value"]) + .map_err(as_pure_body_failure)?; + required_nonempty_text(fields, "width", path).map_err(as_pure_body_failure)?; + match required(fields, "value", path).map_err(as_pure_body_failure)? { + CanonicalValue::Integer(_) => Ok(()), + _ => Err(pure_body_failure( + &format!("{path}.value"), + "a Core integer", + )), + } + } + "string" => { + ensure_allowed_fields(fields, path, &["kind", "value"]) + .map_err(as_pure_body_failure)?; + required_text(fields, "value", path) + .map(|_| ()) + .map_err(as_pure_body_failure) + } + "bytes" => { + ensure_allowed_fields(fields, path, &["kind", "value"]) + .map_err(as_pure_body_failure)?; + match required(fields, "value", path).map_err(as_pure_body_failure)? { + CanonicalValue::Bytes(_) => Ok(()), + _ => Err(pure_body_failure( + &format!("{path}.value"), + "Core byte string", + )), + } + } + "capability" => { + ensure_allowed_fields(fields, path, &["kind", "receipt"]) + .map_err(as_pure_body_failure)?; + parse_digest( + required(fields, "receipt", path).map_err(as_pure_body_failure)?, + &format!("{path}.receipt"), + ) + .map(|_| ()) + .map_err(as_pure_body_failure) + } + _ => Err(pure_body_failure(path, "a scalar Core value")), + } +} + +fn validate_text_values( + value: &CanonicalValue, + path: &str, +) -> Result<(), Vec> { + let values = pure_array(value, path)?; + for (index, value) in values.iter().enumerate() { + nonempty_text(value, &format!("{path}[{index}]")).map_err(as_pure_body_failure)?; + } + Ok(()) +} + +fn validate_expr_values( + value: &CanonicalValue, + path: &str, +) -> Result<(), Vec> { + let values = pure_array(value, path)?; + for (index, value) in values.iter().enumerate() { + validate_core_expr(value, &format!("{path}[{index}]"))?; + } + Ok(()) +} + +fn validate_expr_entries( + value: &CanonicalValue, + path: &str, +) -> Result<(), Vec> { + let entries = pure_array(value, path)?; + for (index, entry) in entries.iter().enumerate() { + let entry_path = format!("{path}[{index}]"); + let pair = pure_array(entry, &entry_path)?; + if pair.len() != 2 { + return Err(pure_body_failure(&entry_path, "a key/value pair")); + } + validate_core_expr(&pair[0], &format!("{entry_path}[0]"))?; + validate_core_expr(&pair[1], &format!("{entry_path}[1]"))?; + } + Ok(()) +} + +fn pure_map<'a>( + value: &'a CanonicalValue, + path: &str, + allowed: &[&str], +) -> Result, Vec> { + closed_map(value, path, allowed).map_err(as_pure_body_failure) +} + +fn pure_array<'a>( + value: &'a CanonicalValue, + path: &str, +) -> Result<&'a [CanonicalValue], Vec> { + array(value, path).map_err(as_pure_body_failure) +} + +fn pure_nonempty_text( + fields: &BTreeMap<&str, &CanonicalValue>, + field: &str, + path: &str, +) -> Result> { + required_nonempty_text(fields, field, path).map_err(as_pure_body_failure) +} + +fn pure_discriminant( + fields: &BTreeMap<&str, &CanonicalValue>, + field: &str, + path: &str, + expected: &str, +) -> Result<(), Vec> { + let actual = required_text(fields, field, path).map_err(as_pure_body_failure)?; + if actual == expected { + Ok(()) + } else { + Err(pure_body_failure(&format!("{path}.{field}"), expected)) + } +} + +fn as_pure_body_failure(failures: Vec) -> Vec { + failures + .into_iter() + .map(|failure| LawpackValidationFailure { + kind: LawpackValidationFailureKind::InvalidPureFunctionBody, + path: failure.path, + obligation: failure.obligation, + }) + .collect() +} + +fn pure_body_failure(path: &str, obligation: &str) -> Vec { + one(failure( + LawpackValidationFailureKind::InvalidPureFunctionBody, + path, + obligation, + )) +} + +fn decode_lawpack_value( + bytes: &[u8], + path: &str, +) -> Result> { + decode_canonical_cbor(bytes).map_err(|error| { + let obligation = if error.kind() == CanonicalErrorKind::DuplicateMapKey { + "canonical CBOR with unique map keys" + } else { + "edict.canonical-cbor/v1" + }; + one(failure( + LawpackValidationFailureKind::InvalidCanonicalCbor, + path, + obligation, + )) + }) +} + +fn digest_lawpack_value( + domain: &str, + value: &CanonicalValue, + path: &str, +) -> Result<[u8; 32], Vec> { + digest_canonical_value(domain, value).map_err(|_error| { + one(failure( + LawpackValidationFailureKind::InvalidCanonicalCbor, + path, + "domain-framed canonical SHA-256 digest", + )) + }) +} + +fn parse_resource_ref( + value: &CanonicalValue, + path: &str, +) -> Result> { + let fields = closed_map(value, path, &["id", "digest"])?; + Ok(LawpackResourceRef { + id: required_nonempty_text(&fields, "id", path)?, + digest: parse_digest( + required(&fields, "digest", path)?, + &format!("{path}.digest"), + )?, + }) +} + +fn parse_digest( + value: &CanonicalValue, + path: &str, +) -> Result<[u8; 32], Vec> { + let CanonicalValue::Array(parts) = value else { + return Err(one(failure( + LawpackValidationFailureKind::InvalidDigest, + path, + "['sha256', 32-byte bstr]", + ))); + }; + let [CanonicalValue::Text(algorithm), CanonicalValue::Bytes(bytes)] = parts.as_slice() else { + return Err(one(failure( + LawpackValidationFailureKind::InvalidDigest, + path, + "['sha256', 32-byte bstr]", + ))); + }; + if algorithm != "sha256" || bytes.len() != 32 { + return Err(one(failure( + LawpackValidationFailureKind::InvalidDigest, + path, + "['sha256', 32-byte bstr]", + ))); + } + let mut digest = [0u8; 32]; + digest.copy_from_slice(bytes); + Ok(digest) +} + +fn required<'a>( + fields: &BTreeMap<&str, &'a CanonicalValue>, + field: &str, + path: &str, +) -> Result<&'a CanonicalValue, Vec> { + fields.get(field).copied().ok_or_else(|| { + one(failure( + LawpackValidationFailureKind::MissingField, + format!("{path}.{field}"), + "required field", + )) + }) +} + +fn required_text( + fields: &BTreeMap<&str, &CanonicalValue>, + field: &str, + path: &str, +) -> Result> { + text_value(required(fields, field, path)?, &format!("{path}.{field}")) +} + +fn required_nonempty_text( + fields: &BTreeMap<&str, &CanonicalValue>, + field: &str, + path: &str, +) -> Result> { + nonempty_text(required(fields, field, path)?, &format!("{path}.{field}")) +} + +fn required_bool( + fields: &BTreeMap<&str, &CanonicalValue>, + field: &str, + path: &str, +) -> Result> { + match required(fields, field, path)? { + CanonicalValue::Bool(value) => Ok(*value), + _ => Err(one(failure( + LawpackValidationFailureKind::InvalidShape, + format!("{path}.{field}"), + "boolean", + ))), + } +} + +fn required_text_array( + fields: &BTreeMap<&str, &CanonicalValue>, + field: &str, + path: &str, + nonempty: bool, +) -> Result, Vec> { + let field_path = format!("{path}.{field}"); + let values = array(required(fields, field, path)?, &field_path)?; + if nonempty && values.is_empty() { + return Err(one(failure( + LawpackValidationFailureKind::EmptyRequiredCollection, + field_path, + "at least one value", + ))); + } + let mut result = Vec::with_capacity(values.len()); + let mut unique = BTreeSet::new(); + for (index, value) in values.iter().enumerate() { + let item_path = format!("{field_path}[{index}]"); + let item = nonempty_text(value, &item_path)?; + if !unique.insert(item.clone()) { + return Err(one(failure( + LawpackValidationFailureKind::DuplicateIdentity, + item_path, + "unique text values", + ))); + } + result.push(item); + } + Ok(result) +} + +fn text_value(value: &CanonicalValue, path: &str) -> Result> { + match value { + CanonicalValue::Text(value) => Ok(value.clone()), + _ => Err(one(failure( + LawpackValidationFailureKind::InvalidShape, + path, + "text string", + ))), + } +} + +fn nonempty_text( + value: &CanonicalValue, + path: &str, +) -> Result> { + let text = text_value(value, path)?; + if text.is_empty() { + Err(one(failure( + LawpackValidationFailureKind::EmptyIdentity, + path, + "non-empty text string", + ))) + } else { + Ok(text) + } +} + +fn array<'a>( + value: &'a CanonicalValue, + path: &str, +) -> Result<&'a [CanonicalValue], Vec> { + match value { + CanonicalValue::Array(values) => Ok(values), + _ => Err(one(failure( + LawpackValidationFailureKind::InvalidShape, + path, + "array", + ))), + } +} + +fn closed_map<'a>( + value: &'a CanonicalValue, + path: &str, + allowed: &[&str], +) -> Result, Vec> { + let fields = string_keyed_map(value, path)?; + ensure_allowed_fields(&fields, path, allowed)?; + Ok(fields) +} + +fn string_keyed_map<'a>( + value: &'a CanonicalValue, + path: &str, +) -> Result, Vec> { + let CanonicalValue::Map(entries) = value else { + return Err(one(failure( + LawpackValidationFailureKind::InvalidShape, + path, + "map with text keys", + ))); + }; + let mut fields = BTreeMap::new(); + for (key, value) in entries { + let CanonicalValue::Text(key) = key else { + return Err(one(failure( + LawpackValidationFailureKind::InvalidShape, + path, + "map with text keys", + ))); + }; + if fields.insert(key.as_str(), value).is_some() { + return Err(one(failure( + LawpackValidationFailureKind::DuplicateIdentity, + format!("{path}.{key}"), + "unique map key", + ))); + } + } + Ok(fields) +} + +fn ensure_allowed_fields( + fields: &BTreeMap<&str, &CanonicalValue>, + path: &str, + allowed: &[&str], +) -> Result<(), Vec> { + if let Some(field) = fields + .keys() + .find(|field| !allowed.iter().any(|allowed| field == &allowed)) + { + Err(one(failure( + LawpackValidationFailureKind::UnexpectedField, + format!("{path}.{field}"), + "closed schema with no unknown fields", + ))) + } else { + Ok(()) + } +} + +fn failure( + kind: LawpackValidationFailureKind, + path: impl Into, + obligation: impl Into, +) -> LawpackValidationFailure { + LawpackValidationFailure { + kind, + path: path.into(), + obligation: obligation.into(), + } +} + +fn one(failure: LawpackValidationFailure) -> Vec { + vec![failure] +} diff --git a/crates/edict-syntax/src/lawpack_adapter.rs b/crates/edict-syntax/src/lawpack_adapter.rs new file mode 100644 index 0000000..8e4e6cb --- /dev/null +++ b/crates/edict-syntax/src/lawpack_adapter.rs @@ -0,0 +1,900 @@ +//! Canonical direct lawpack-adapter loading and compiler preparation. +//! +//! A direct adapter is declarative, selected by one exact digest-locked target +//! profile entry in a validated lawpack manifest, and complete over that +//! lawpack's runtime effects. It supplies compiler and Target IR facts; it does +//! not execute a runtime, emit an Echo package, or confer admission authority. + +use std::collections::{BTreeMap, BTreeSet}; + +use crate::ast::{ImportKind, Module}; +use crate::canonical::{ + decode_canonical_cbor, digest_canonical_value, sha256_review_string, CanonicalValue, +}; +use crate::compiler::CompilerContext; +use crate::core_ir::{CoreBudget, ResourceRef}; +use crate::lawpack::{ + LawpackExecutionClass, LawpackResourceRef, LawpackSemanticEffect, LawpackTargetAdapter, + ValidatedLawpackBundle, +}; +use crate::lowerability::WriteClass; +use crate::target_ir::{TargetEffectLowering, TargetIrLoweringFacts}; + +/// Canonical direct lawpack-adapter ABI supported by this crate. +pub const LAWPACK_ADAPTER_API_VERSION: &str = "edict.lawpack-adapter/v1"; + +const ADAPTER_PATH: &str = ""; + +/// Stable direct-adapter failure classifications. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum LawpackAdapterFailureKind { + InvalidCanonicalCbor, + InvalidShape, + MissingField, + UnexpectedField, + InvalidApiVersion, + UnsupportedClass, + MissingTargetAdapter, + AmbiguousTargetAdapter, + AdapterDigestMismatch, + MissingOperationProfile, + UnknownOperationProfile, + MissingEffectImplementation, + UnknownEffectImplementation, + DuplicateReference, + ObligationMismatch, + FailureMappingMismatch, + MissingBudget, + UnknownBudget, + InvalidWriteClass, + InvalidTargetIntrinsic, + InvalidTargetConfiguration, + SourceImportMismatch, +} + +/// One failed direct-adapter validation or preparation obligation. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LawpackAdapterFailure { + pub kind: LawpackAdapterFailureKind, + pub path: String, + pub obligation: String, +} + +/// One operation profile discharged by a direct adapter. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LawpackAdapterOperationProfile { + pub core: String, + pub semantic_effects: Vec, +} + +/// One semantic effect discharged by a direct adapter. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LawpackAdapterEffect { + pub target_intrinsic: String, + pub target_configuration: LawpackResourceRef, + pub write_class: WriteClass, + pub footprint_obligation: String, + pub cost_obligation: String, + pub failure_mappings: BTreeMap, +} + +/// A canonical direct adapter corroborated against its owning lawpack. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ValidatedLawpackAdapter { + descriptor: LawpackTargetAdapter, + operation_profiles: BTreeMap, + effects: BTreeMap, + budgets: BTreeMap, + digest: [u8; 32], +} + +impl ValidatedLawpackAdapter { + /// Exact target profile selected by the owning lawpack manifest. + #[must_use] + pub fn target_profile(&self) -> &LawpackResourceRef { + &self.descriptor.accepted_target_profile + } + + /// Exact Target IR selected by the owning lawpack manifest. + #[must_use] + pub fn target_ir(&self) -> &LawpackResourceRef { + &self.descriptor.accepted_target_ir + } + + /// Canonical operation-profile discharges. + #[must_use] + pub fn operation_profiles(&self) -> &BTreeMap { + &self.operation_profiles + } + + /// Canonical semantic-effect discharges. + #[must_use] + pub fn effects(&self) -> &BTreeMap { + &self.effects + } + + /// Canonical budget discharges. + #[must_use] + pub fn budgets(&self) -> &BTreeMap { + &self.budgets + } + + /// Domain-framed digest of the exact canonical adapter bytes. + #[must_use] + pub fn digest_review_string(&self) -> String { + sha256_review_string(&self.digest) + } +} + +/// Compiler and Target IR facts derived from an exact module/lawpack/adapter +/// closure. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PreparedLawpackCompilation { + compiler_context: CompilerContext, + target_ir_facts: TargetIrLoweringFacts, +} + +impl PreparedLawpackCompilation { + /// Compiler facts projected through the module-local lawpack alias. + #[must_use] + pub fn compiler_context(&self) -> &CompilerContext { + &self.compiler_context + } + + /// Target IR facts projected through the same module-local alias. + #[must_use] + pub fn target_ir_facts(&self) -> &TargetIrLoweringFacts { + &self.target_ir_facts + } +} + +/// Decode one canonical direct adapter selected by a validated lawpack. +/// +/// # Errors +/// +/// Returns stable failures for canonical-shape errors, target selection +/// ambiguity, digest substitution, incomplete coverage, or mismatched +/// footprint, cost, and failure obligations. +pub fn decode_lawpack_adapter( + bundle: &ValidatedLawpackBundle, + target_profile: &str, + bytes: &[u8], +) -> Result> { + let descriptor = select_descriptor(bundle, target_profile)?; + let value = decode_canonical_cbor(bytes).map_err(|_| { + one(failure( + LawpackAdapterFailureKind::InvalidCanonicalCbor, + ADAPTER_PATH, + "exact canonical CBOR", + )) + })?; + let digest = digest_value(&descriptor.adapter.id, &value)?; + if digest != descriptor.adapter.digest { + return Err(one(failure( + LawpackAdapterFailureKind::AdapterDigestMismatch, + "manifest.targetAdapters.adapter.digest", + "digest of the exact canonical adapter bytes", + ))); + } + + let (operation_profiles, effects, budgets) = parse_adapter(&value)?; + validate_adapter_closure(bundle, &descriptor, &operation_profiles, &effects, &budgets)?; + + Ok(ValidatedLawpackAdapter { + descriptor, + operation_profiles, + effects, + budgets, + digest, + }) +} + +/// Derive compiler and target facts for one module from its exact imported +/// lawpack and selected direct adapter. +/// +/// # Errors +/// +/// Returns `SourceImportMismatch` unless the module contains exactly one +/// matching digest-locked lawpack import. +pub fn prepare_lawpack_compilation( + module: &Module, + bundle: &ValidatedLawpackBundle, + adapter: &ValidatedLawpackAdapter, +) -> Result> { + let alias = matching_import_alias(module, bundle)?; + let prefix = format!("{}@{}.", bundle.manifest().id, bundle.manifest().version); + let mut compiler_context = CompilerContext::new(); + let mut operation_profiles = BTreeSet::new(); + let mut obstruction_coordinates = BTreeSet::new(); + let mut effect_lowerings = Vec::new(); + + for (coordinate, profile) in &adapter.operation_profiles { + let local_profile = local_coordinate(&alias, &prefix, coordinate)?; + let mut write_classes = BTreeSet::new(); + for effect_coordinate in &profile.semantic_effects { + let effect = adapter.effects.get(effect_coordinate).ok_or_else(|| { + one(failure( + LawpackAdapterFailureKind::MissingEffectImplementation, + format!("adapter.operationProfiles.{coordinate}.semanticEffects"), + effect_coordinate, + )) + })?; + write_classes.insert(effect.write_class.clone()); + } + compiler_context = compiler_context + .with_operation_profile(local_profile.clone(), profile.core.clone()) + .with_operation_profile_write_classes(local_profile, write_classes); + operation_profiles.insert(profile.core.clone()); + } + + for (coordinate, effect) in &adapter.effects { + let local_effect = local_coordinate(&alias, &prefix, coordinate)?; + compiler_context = compiler_context + .with_effect_write_class(local_effect.clone(), effect.write_class.clone()); + obstruction_coordinates.extend(effect.failure_mappings.values().cloned()); + effect_lowerings.push(TargetEffectLowering { + effect: local_effect, + target_intrinsic: effect.target_intrinsic.clone(), + failure_mappings: effect.failure_mappings.clone(), + }); + } + + for (coordinate, budget) in &adapter.budgets { + let local_budget = local_coordinate(&alias, &prefix, coordinate)?; + compiler_context = compiler_context.with_budget(local_budget, budget.clone()); + } + + Ok(PreparedLawpackCompilation { + compiler_context, + target_ir_facts: TargetIrLoweringFacts { + target_profile: ResourceRef { + coordinate: adapter.target_profile().id.clone(), + digest: Some(adapter.target_profile().digest_review_string()), + }, + target_ir_domain: adapter.target_ir().id.clone(), + operation_profiles: operation_profiles.into_iter().collect(), + obstruction_coordinates: obstruction_coordinates.into_iter().collect(), + effect_lowerings, + }, + }) +} + +type AdapterParts = ( + BTreeMap, + BTreeMap, + BTreeMap, +); + +fn parse_adapter(value: &CanonicalValue) -> Result> { + let fields = closed_map( + value, + "adapter", + &[ + "apiVersion", + "class", + "operationProfiles", + "effectImplementations", + "budgets", + ], + )?; + let api_version = required_text(&fields, "apiVersion", "adapter")?; + if api_version != LAWPACK_ADAPTER_API_VERSION { + return Err(one(failure( + LawpackAdapterFailureKind::InvalidApiVersion, + "adapter.apiVersion", + LAWPACK_ADAPTER_API_VERSION, + ))); + } + if required_text(&fields, "class", "adapter")? != "declarative" { + return Err(one(failure( + LawpackAdapterFailureKind::UnsupportedClass, + "adapter.class", + "declarative", + ))); + } + Ok(( + parse_operation_profiles(required(&fields, "operationProfiles", "adapter")?)?, + parse_effects(required(&fields, "effectImplementations", "adapter")?)?, + parse_budgets(required(&fields, "budgets", "adapter")?)?, + )) +} + +fn parse_operation_profiles( + value: &CanonicalValue, +) -> Result, Vec> { + let values = text_map(value, "adapter.operationProfiles")?; + let mut profiles = BTreeMap::new(); + for (coordinate, value) in values { + let path = format!("adapter.operationProfiles.{coordinate}"); + let fields = closed_map(value, &path, &["core", "semanticEffects"])?; + let core = required_nonempty_text(&fields, "core", &path)?; + let semantic_effects = text_array( + required(&fields, "semanticEffects", &path)?, + &format!("{path}.semanticEffects"), + true, + )?; + profiles.insert( + coordinate, + LawpackAdapterOperationProfile { + core, + semantic_effects, + }, + ); + } + Ok(profiles) +} + +fn parse_effects( + value: &CanonicalValue, +) -> Result, Vec> { + let values = text_map(value, "adapter.effectImplementations")?; + let mut effects = BTreeMap::new(); + for (coordinate, value) in values { + let path = format!("adapter.effectImplementations.{coordinate}"); + let fields = closed_map( + value, + &path, + &[ + "targetIntrinsic", + "targetConfiguration", + "writeClass", + "footprintObligation", + "costObligation", + "failureMappings", + ], + )?; + let failure_values = text_map( + required(&fields, "failureMappings", &path)?, + &format!("{path}.failureMappings"), + )?; + let mut failure_mappings = BTreeMap::new(); + for (failure_coordinate, target_obstruction) in failure_values { + failure_mappings.insert( + failure_coordinate, + nonempty_text(target_obstruction, &format!("{path}.failureMappings"))?, + ); + } + effects.insert( + coordinate, + LawpackAdapterEffect { + target_intrinsic: required_nonempty_text(&fields, "targetIntrinsic", &path)?, + target_configuration: parse_resource_ref( + required(&fields, "targetConfiguration", &path)?, + &format!("{path}.targetConfiguration"), + )?, + write_class: parse_write_class( + &required_text(&fields, "writeClass", &path)?, + &format!("{path}.writeClass"), + )?, + footprint_obligation: required_nonempty_text( + &fields, + "footprintObligation", + &path, + )?, + cost_obligation: required_nonempty_text(&fields, "costObligation", &path)?, + failure_mappings, + }, + ); + } + Ok(effects) +} + +fn parse_budgets( + value: &CanonicalValue, +) -> Result, Vec> { + let values = text_map(value, "adapter.budgets")?; + let mut budgets = BTreeMap::new(); + for (coordinate, value) in values { + let path = format!("adapter.budgets.{coordinate}"); + let fields = closed_map( + value, + &path, + &["maxSteps", "maxAllocatedBytes", "maxOutputBytes"], + )?; + budgets.insert( + coordinate, + CoreBudget { + max_steps: required_u64(&fields, "maxSteps", &path)?, + max_allocated_bytes: required_u64(&fields, "maxAllocatedBytes", &path)?, + max_output_bytes: required_u64(&fields, "maxOutputBytes", &path)?, + }, + ); + } + Ok(budgets) +} + +fn validate_adapter_closure( + bundle: &ValidatedLawpackBundle, + descriptor: &LawpackTargetAdapter, + operation_profiles: &BTreeMap, + effects: &BTreeMap, + budgets: &BTreeMap, +) -> Result<(), Vec> { + let exported_profiles = &bundle.exports().operation_profiles; + exact_keys( + exported_profiles.keys().map(String::as_str), + operation_profiles.keys().map(String::as_str), + LawpackAdapterFailureKind::MissingOperationProfile, + LawpackAdapterFailureKind::UnknownOperationProfile, + "adapter.operationProfiles", + )?; + + let runtime_effects = bundle + .exports() + .effects + .iter() + .filter(|effect| effect.execution_class == LawpackExecutionClass::Runtime) + .map(|effect| (effect.coordinate.as_str(), effect)) + .collect::>(); + exact_keys( + runtime_effects.keys().copied(), + effects.keys().map(String::as_str), + LawpackAdapterFailureKind::MissingEffectImplementation, + LawpackAdapterFailureKind::UnknownEffectImplementation, + "adapter.effectImplementations", + )?; + + let intrinsic_prefix = format!("{}.", descriptor.accepted_target_profile.id); + let mut required_budgets = BTreeSet::new(); + for (coordinate, effect) in effects { + let exported = runtime_effects.get(coordinate.as_str()).ok_or_else(|| { + one(failure( + LawpackAdapterFailureKind::UnknownEffectImplementation, + format!("adapter.effectImplementations.{coordinate}"), + "exported runtime semantic effect", + )) + })?; + validate_effect(coordinate, exported, effect, &intrinsic_prefix)?; + required_budgets.insert(effect.cost_obligation.as_str()); + } + exact_keys( + required_budgets, + budgets.keys().map(String::as_str), + LawpackAdapterFailureKind::MissingBudget, + LawpackAdapterFailureKind::UnknownBudget, + "adapter.budgets", + )?; + + for (coordinate, profile) in operation_profiles { + let mut seen = BTreeSet::new(); + for effect in &profile.semantic_effects { + if !seen.insert(effect) { + return Err(one(failure( + LawpackAdapterFailureKind::DuplicateReference, + format!("adapter.operationProfiles.{coordinate}.semanticEffects"), + "unique semantic effect references", + ))); + } + if !effects.contains_key(effect) { + return Err(one(failure( + LawpackAdapterFailureKind::MissingEffectImplementation, + format!("adapter.operationProfiles.{coordinate}.semanticEffects"), + effect, + ))); + } + } + } + Ok(()) +} + +fn validate_effect( + coordinate: &str, + exported: &LawpackSemanticEffect, + effect: &LawpackAdapterEffect, + intrinsic_prefix: &str, +) -> Result<(), Vec> { + let path = format!("adapter.effectImplementations.{coordinate}"); + if !effect.target_intrinsic.starts_with(intrinsic_prefix) + || effect.target_intrinsic.len() == intrinsic_prefix.len() + { + return Err(one(failure( + LawpackAdapterFailureKind::InvalidTargetIntrinsic, + format!("{path}.targetIntrinsic"), + format!( + "intrinsic below `{}`", + intrinsic_prefix.trim_end_matches('.') + ), + ))); + } + if effect.footprint_obligation != exported.footprint_obligation { + return Err(one(failure( + LawpackAdapterFailureKind::ObligationMismatch, + format!("{path}.footprintObligation"), + &exported.footprint_obligation, + ))); + } + if effect.cost_obligation != exported.cost_obligation { + return Err(one(failure( + LawpackAdapterFailureKind::ObligationMismatch, + format!("{path}.costObligation"), + &exported.cost_obligation, + ))); + } + let exported_failures = exported + .effect_failures + .keys() + .map(String::as_str) + .collect::>(); + let mapped_failures = effect + .failure_mappings + .keys() + .map(String::as_str) + .collect::>(); + if exported_failures != mapped_failures { + return Err(one(failure( + LawpackAdapterFailureKind::FailureMappingMismatch, + format!("{path}.failureMappings"), + "exact exported named-failure set", + ))); + } + let target_failures = effect + .failure_mappings + .values() + .map(String::as_str) + .collect::>(); + if target_failures.len() != effect.failure_mappings.len() { + return Err(one(failure( + LawpackAdapterFailureKind::FailureMappingMismatch, + format!("{path}.failureMappings"), + "unique target obstruction coordinates", + ))); + } + Ok(()) +} + +fn select_descriptor( + bundle: &ValidatedLawpackBundle, + target_profile: &str, +) -> Result> { + let matches = bundle + .manifest() + .target_adapters + .iter() + .filter(|adapter| adapter.accepted_target_profile.id == target_profile) + .collect::>(); + match matches.as_slice() { + [descriptor] => Ok((*descriptor).clone()), + [] => Err(one(failure( + LawpackAdapterFailureKind::MissingTargetAdapter, + "manifest.targetAdapters", + format!("one adapter for `{target_profile}`"), + ))), + _ => Err(one(failure( + LawpackAdapterFailureKind::AmbiguousTargetAdapter, + "manifest.targetAdapters", + format!("one adapter for `{target_profile}`"), + ))), + } +} + +fn matching_import_alias( + module: &Module, + bundle: &ValidatedLawpackBundle, +) -> Result> { + let manifest_coordinate = format!("{}@{}", bundle.manifest().id, bundle.manifest().version); + let manifest_digest = bundle.manifest_digest_review_string(); + let matches = module + .imports + .iter() + .filter(|import| { + import.kind == ImportKind::Lawpack + && import.package.as_ref().is_some_and(|package| { + format!("{}@{}", package.path.join("."), package.version) == manifest_coordinate + }) + }) + .collect::>(); + let [import] = matches.as_slice() else { + return Err(one(failure( + LawpackAdapterFailureKind::SourceImportMismatch, + "module.imports", + format!("exactly one import of `{manifest_coordinate}`"), + ))); + }; + if import.digest.as_deref() != Some(manifest_digest.as_str()) { + return Err(one(failure( + LawpackAdapterFailureKind::SourceImportMismatch, + format!("module.imports.{}.digest", import.alias), + manifest_digest, + ))); + } + Ok(import.alias.clone()) +} + +fn local_coordinate( + alias: &str, + canonical_prefix: &str, + coordinate: &str, +) -> Result> { + let Some(suffix) = coordinate.strip_prefix(canonical_prefix) else { + return Err(one(failure( + LawpackAdapterFailureKind::InvalidShape, + coordinate, + format!( + "coordinate below `{}`", + canonical_prefix.trim_end_matches('.') + ), + ))); + }; + if suffix.is_empty() { + return Err(one(failure( + LawpackAdapterFailureKind::InvalidShape, + coordinate, + "non-empty exported coordinate suffix", + ))); + } + Ok(format!("{alias}.{suffix}")) +} + +fn exact_keys<'a>( + expected: impl IntoIterator, + actual: impl IntoIterator, + missing_kind: LawpackAdapterFailureKind, + unknown_kind: LawpackAdapterFailureKind, + path: &str, +) -> Result<(), Vec> { + let expected = expected.into_iter().collect::>(); + let actual = actual.into_iter().collect::>(); + if let Some(missing) = expected.difference(&actual).next() { + return Err(one(failure( + missing_kind, + path, + format!("include `{missing}`"), + ))); + } + if let Some(unknown) = actual.difference(&expected).next() { + return Err(one(failure( + unknown_kind, + path, + format!("exclude unknown `{unknown}`"), + ))); + } + Ok(()) +} + +fn parse_write_class(value: &str, path: &str) -> Result> { + match value { + "none" => Ok(WriteClass::None), + "read" => Ok(WriteClass::Read), + "create" => Ok(WriteClass::Create), + "ensure" => Ok(WriteClass::Ensure), + "append" => Ok(WriteClass::Append), + "replace" => Ok(WriteClass::Replace), + "delete" => Ok(WriteClass::Delete), + "custom" => Ok(WriteClass::Custom("custom".to_owned())), + _ => Err(one(failure( + LawpackAdapterFailureKind::InvalidWriteClass, + path, + "v1 authority write class", + ))), + } +} + +fn parse_resource_ref( + value: &CanonicalValue, + path: &str, +) -> Result> { + let fields = closed_map(value, path, &["id", "digest"])?; + Ok(LawpackResourceRef { + id: required_nonempty_text(&fields, "id", path)?, + digest: parse_digest( + required(&fields, "digest", path)?, + &format!("{path}.digest"), + )?, + }) +} + +fn parse_digest( + value: &CanonicalValue, + path: &str, +) -> Result<[u8; 32], Vec> { + let CanonicalValue::Array(parts) = value else { + return Err(one(failure( + LawpackAdapterFailureKind::InvalidTargetConfiguration, + path, + "['sha256', 32-byte bstr]", + ))); + }; + let [CanonicalValue::Text(algorithm), CanonicalValue::Bytes(bytes)] = parts.as_slice() else { + return Err(one(failure( + LawpackAdapterFailureKind::InvalidTargetConfiguration, + path, + "['sha256', 32-byte bstr]", + ))); + }; + let Ok(digest) = <[u8; 32]>::try_from(bytes.as_slice()) else { + return Err(one(failure( + LawpackAdapterFailureKind::InvalidTargetConfiguration, + path, + "['sha256', 32-byte bstr]", + ))); + }; + if algorithm != "sha256" { + return Err(one(failure( + LawpackAdapterFailureKind::InvalidTargetConfiguration, + path, + "['sha256', 32-byte bstr]", + ))); + } + Ok(digest) +} + +fn digest_value( + domain: &str, + value: &CanonicalValue, +) -> Result<[u8; 32], Vec> { + digest_canonical_value(domain, value).map_err(|_| { + one(failure( + LawpackAdapterFailureKind::InvalidShape, + ADAPTER_PATH, + "canonically encodable adapter digest frame", + )) + }) +} + +fn closed_map<'a>( + value: &'a CanonicalValue, + path: &str, + allowed: &[&str], +) -> Result, Vec> { + let fields = text_map_ref(value, path)?; + if let Some(unexpected) = fields.keys().find(|key| !allowed.contains(key)) { + return Err(one(failure( + LawpackAdapterFailureKind::UnexpectedField, + format!("{path}.{unexpected}"), + "closed map", + ))); + } + Ok(fields) +} + +fn text_map<'a>( + value: &'a CanonicalValue, + path: &str, +) -> Result, Vec> { + Ok(text_map_ref(value, path)? + .into_iter() + .map(|(key, value)| (key.to_owned(), value)) + .collect()) +} + +fn text_map_ref<'a>( + value: &'a CanonicalValue, + path: &str, +) -> Result, Vec> { + let CanonicalValue::Map(entries) = value else { + return Err(one(failure( + LawpackAdapterFailureKind::InvalidShape, + path, + "map", + ))); + }; + let mut fields = BTreeMap::new(); + for (key, value) in entries { + let CanonicalValue::Text(key) = key else { + return Err(one(failure( + LawpackAdapterFailureKind::InvalidShape, + path, + "text-keyed map", + ))); + }; + fields.insert(key.as_str(), value); + } + Ok(fields) +} + +fn required<'a>( + fields: &BTreeMap<&str, &'a CanonicalValue>, + field: &str, + path: &str, +) -> Result<&'a CanonicalValue, Vec> { + fields.get(field).copied().ok_or_else(|| { + one(failure( + LawpackAdapterFailureKind::MissingField, + format!("{path}.{field}"), + "required field", + )) + }) +} + +fn required_text( + fields: &BTreeMap<&str, &CanonicalValue>, + field: &str, + path: &str, +) -> Result> { + text(required(fields, field, path)?, &format!("{path}.{field}")) +} + +fn required_nonempty_text( + fields: &BTreeMap<&str, &CanonicalValue>, + field: &str, + path: &str, +) -> Result> { + nonempty_text(required(fields, field, path)?, &format!("{path}.{field}")) +} + +fn required_u64( + fields: &BTreeMap<&str, &CanonicalValue>, + field: &str, + path: &str, +) -> Result> { + let value = required(fields, field, path)?; + let CanonicalValue::Integer(value) = value else { + return Err(one(failure( + LawpackAdapterFailureKind::InvalidShape, + format!("{path}.{field}"), + "unsigned integer", + ))); + }; + u64::try_from(*value).map_err(|_| { + one(failure( + LawpackAdapterFailureKind::InvalidShape, + format!("{path}.{field}"), + "u64 value", + )) + }) +} + +fn text(value: &CanonicalValue, path: &str) -> Result> { + let CanonicalValue::Text(value) = value else { + return Err(one(failure( + LawpackAdapterFailureKind::InvalidShape, + path, + "text", + ))); + }; + Ok(value.clone()) +} + +fn nonempty_text(value: &CanonicalValue, path: &str) -> Result> { + let value = text(value, path)?; + if value.is_empty() { + return Err(one(failure( + LawpackAdapterFailureKind::InvalidShape, + path, + "non-empty text", + ))); + } + Ok(value) +} + +fn text_array( + value: &CanonicalValue, + path: &str, + nonempty: bool, +) -> Result, Vec> { + let CanonicalValue::Array(values) = value else { + return Err(one(failure( + LawpackAdapterFailureKind::InvalidShape, + path, + "array", + ))); + }; + if nonempty && values.is_empty() { + return Err(one(failure( + LawpackAdapterFailureKind::InvalidShape, + path, + "non-empty array", + ))); + } + values + .iter() + .enumerate() + .map(|(index, value)| nonempty_text(value, &format!("{path}[{index}]"))) + .collect() +} + +fn failure( + kind: LawpackAdapterFailureKind, + path: impl Into, + obligation: impl Into, +) -> LawpackAdapterFailure { + LawpackAdapterFailure { + kind, + path: path.into(), + obligation: obligation.into(), + } +} + +fn one(failure: LawpackAdapterFailure) -> Vec { + vec![failure] +} diff --git a/crates/edict-syntax/src/lib.rs b/crates/edict-syntax/src/lib.rs index babfe12..ba3d18b 100644 --- a/crates/edict-syntax/src/lib.rs +++ b/crates/edict-syntax/src/lib.rs @@ -76,6 +76,8 @@ pub mod compiler; pub mod contract_bundle; pub mod core_ir; pub mod highlight; +pub mod lawpack; +pub mod lawpack_adapter; pub mod lowerability; pub mod parser; pub mod provider; @@ -105,12 +107,12 @@ pub use authority_facts::{ AUTHORITY_FACTS_CDDL_ROOT, }; pub use canonical::{ - decode_canonical_cbor, digest_bundle_layer, digest_core_module, digest_target_ir_artifact, - encode_canonical_cbor, encode_core_module, encode_target_ir_artifact, BundleDigestDomain, - BundlePreimageComponent, BundleSourceDescriptor, CanonicalError, CanonicalErrorKind, - CanonicalValue, CoreDigest, BUNDLE_RELEASE_DIGEST_DOMAIN, BUNDLE_SEMANTIC_DIGEST_DOMAIN, - CORE_CANONICAL_ENCODING, CORE_DIGEST_FRAME, CORE_MODULE_DIGEST_DOMAIN, - MAX_CANONICAL_NESTING_DEPTH, TARGET_IR_ARTIFACT_DIGEST_DOMAIN, + decode_canonical_cbor, digest_bundle_layer, digest_canonical_artifact, digest_core_module, + digest_target_ir_artifact, encode_canonical_cbor, encode_core_module, + encode_target_ir_artifact, BundleDigestDomain, BundlePreimageComponent, BundleSourceDescriptor, + CanonicalError, CanonicalErrorKind, CanonicalValue, CoreDigest, BUNDLE_RELEASE_DIGEST_DOMAIN, + BUNDLE_SEMANTIC_DIGEST_DOMAIN, CORE_CANONICAL_ENCODING, CORE_DIGEST_FRAME, + CORE_MODULE_DIGEST_DOMAIN, MAX_CANONICAL_NESTING_DEPTH, TARGET_IR_ARTIFACT_DIGEST_DOMAIN, }; pub use compiler::{ compile_to_core, lower_core, resolve_module, type_check, CompilerContext, CompilerError, @@ -134,6 +136,21 @@ pub use core_ir::{ CORE_API_VERSION, }; pub use highlight::{highlight_source, HighlightRole, HighlightToken}; +pub use lawpack::{ + decode_lawpack_bundle, validate_lawpack_dependency_graph, LawpackApertureRequirement, + LawpackAuthorityClass, LawpackDependency, LawpackDeterminismClass, LawpackEffectFailure, + LawpackEffectKind, LawpackExecutableComponent, LawpackExecutionClass, LawpackExportedConstant, + LawpackExportedType, LawpackExports, LawpackManifest, LawpackObstruction, + LawpackOperationProfile, LawpackOpticTemplate, LawpackPureFunction, + LawpackPureFunctionImplementation, LawpackResourceRef, LawpackSemanticEffect, + LawpackTargetAdapter, LawpackValidationFailure, LawpackValidationFailureKind, LawpackVerifier, + LawpackVerifierClass, ValidatedLawpackBundle, LAWPACK_API_VERSION, +}; +pub use lawpack_adapter::{ + decode_lawpack_adapter, prepare_lawpack_compilation, LawpackAdapterEffect, + LawpackAdapterFailure, LawpackAdapterFailureKind, LawpackAdapterOperationProfile, + PreparedLawpackCompilation, ValidatedLawpackAdapter, LAWPACK_ADAPTER_API_VERSION, +}; pub use lowerability::{ check_lowerability, AtomicityRequirement, DirectAdapterSupport, GuardKind, LowerabilityEffectResult, LowerabilityEffectStatus, LowerabilityFailure, diff --git a/crates/edict-syntax/src/parser.rs b/crates/edict-syntax/src/parser.rs index d82face..a5d1b7e 100644 --- a/crates/edict-syntax/src/parser.rs +++ b/crates/edict-syntax/src/parser.rs @@ -118,7 +118,7 @@ pub fn parse_module(src: &str) -> Result { /// identifiers in value position; /// - `record`/`map`/`unit`/`migration`/`projection`, whose productions are not /// yet parsed — they will join this set when their syntax lands. -fn is_keyword(s: &str) -> bool { +pub(crate) fn is_keyword(s: &str) -> bool { matches!( s, "package" diff --git a/crates/edict-syntax/src/provider.rs b/crates/edict-syntax/src/provider.rs index c82d82e..18fa661 100644 --- a/crates/edict-syntax/src/provider.rs +++ b/crates/edict-syntax/src/provider.rs @@ -131,6 +131,7 @@ pub enum ProviderManifestValidationFailureKind { NonDigestLockedProvider, MissingArtifact, MissingRole, + UnsafeArtifactRole, DuplicateArtifactRole, NonDigestLockedArtifact, NonDigestLockedGeneratedSource, @@ -249,6 +250,13 @@ fn check_artifacts( "artifacts.role", "non-empty unique artifact role", ); + } else if !is_safe_artifact_role(&artifact.role) { + push_failure( + failures, + ProviderManifestValidationFailureKind::UnsafeArtifactRole, + "artifacts.role", + "path-neutral artifact role without separators, traversal, drive syntax, or control characters", + ); } else if !roles.insert(artifact.role.as_str()) { push_failure( failures, @@ -331,6 +339,13 @@ fn check_artifacts( } } +fn is_safe_artifact_role(role: &str) -> bool { + !role.contains("..") + && !role + .chars() + .any(|character| matches!(character, '/' | '\\' | ':') || character.is_control()) +} + fn check_schema_bindings( artifacts: &[ProviderArtifactRef], bindings: &[ProviderSchemaBinding], diff --git a/crates/edict-syntax/src/target_ir.rs b/crates/edict-syntax/src/target_ir.rs index ac26b83..417aff8 100644 --- a/crates/edict-syntax/src/target_ir.rs +++ b/crates/edict-syntax/src/target_ir.rs @@ -113,6 +113,7 @@ impl TargetIrLoweringFacts { pub struct TargetEffectLowering { pub effect: String, pub target_intrinsic: String, + pub failure_mappings: BTreeMap, } fn selected_native_effect_lowerings(report: &LowerabilityReport) -> Vec { @@ -126,6 +127,7 @@ fn selected_native_effect_lowerings(report: &LowerabilityReport) -> Vec { - let unsupported_obstructions = node - .obstruction_map + let mut mapped_obstructions = BTreeMap::new(); + for (failure, arm) in node.obstruction_map { + let mapped_failure = lowering + .failure_mappings + .get(failure) + .unwrap_or(failure) + .clone(); + if mapped_obstructions + .insert(mapped_failure.clone(), arm.clone()) + .is_some() + { + failures.push(TargetLoweringFailure { + kind: TargetLoweringFailureKind::AmbiguousObstructionMapping, + intent: Some(intent_name.to_owned()), + node_index: Some(node_index), + detail: mapped_failure, + }); + return; + } + } + let unsupported_obstructions = mapped_obstructions .keys() .filter(|failure| !context.obstruction_coordinates.contains(failure.as_str())) .cloned() @@ -704,8 +726,8 @@ fn lower_effect_node( effect: node.effect.to_owned(), target_intrinsic: lowering.target_intrinsic.clone(), input: node.input.clone(), - obstruction_failures: node.obstruction_map.keys().cloned().collect(), - obstruction_arms: node.obstruction_map.clone(), + obstruction_failures: mapped_obstructions.keys().cloned().collect(), + obstruction_arms: mapped_obstructions, }); } [] => failures.push(TargetLoweringFailure { diff --git a/crates/edict-syntax/src/target_profile.rs b/crates/edict-syntax/src/target_profile.rs index 3b917aa..dd514f7 100644 --- a/crates/edict-syntax/src/target_profile.rs +++ b/crates/edict-syntax/src/target_profile.rs @@ -5,6 +5,7 @@ //! does not load files, lower Core, verify Target IR, or perform admission. use crate::core_ir::{ResourceRef, CORE_API_VERSION}; +use crate::lawpack_adapter::LAWPACK_ADAPTER_API_VERSION; /// Target-profile manifest ABI supported by this crate. pub const TARGET_PROFILE_API_VERSION: &str = "edict.target-profile/v1"; @@ -66,7 +67,7 @@ pub enum TargetProfileConformanceFailureKind { MissingIntrinsicNamespace, NonDigestLockedResource, UnsupportedCanonicalEncoding, - DeferredLawpackAdapterAbiUnsupported, + UnsupportedLawpackAdapterAbi, UnsupportedCompositeProfile, UnsupportedApplicationModel, UnsupportedReadConsistency, @@ -155,12 +156,17 @@ pub fn validate_target_profile_manifest( CANONICAL_CBOR_ABI, ); } - if !manifest.accepted_lawpack_adapter_abi.is_empty() { + let lawpack_adapter_abi_supported = match manifest.accepted_lawpack_adapter_abi.as_slice() { + [] => true, + [abi] => abi == LAWPACK_ADAPTER_API_VERSION, + _ => false, + }; + if !lawpack_adapter_abi_supported { push_failure( &mut failures, - TargetProfileConformanceFailureKind::DeferredLawpackAdapterAbiUnsupported, + TargetProfileConformanceFailureKind::UnsupportedLawpackAdapterAbi, "accepted_lawpack_adapter_abi", - "empty until edict.lawpack-adapter/v1 is specified", + format!("empty or exactly [`{LAWPACK_ADAPTER_API_VERSION}`]"), ); } if manifest.multi_target { diff --git a/crates/edict-syntax/tests/contract_bundle.rs b/crates/edict-syntax/tests/contract_bundle.rs index 34ba917..13f724e 100644 --- a/crates/edict-syntax/tests/contract_bundle.rs +++ b/crates/edict-syntax/tests/contract_bundle.rs @@ -373,7 +373,7 @@ mod contract_bundle_assembly { ContractBundleValidationFailureKind, ContractBundleValidationStatus, CoreBudget, CoreExpr, CoreImport, CoreImportKind, CoreValue, DigestLockedResource, ResourceRef, SuppliedTargetIrResource, TargetEffectLowering, TargetIrArtifact, TargetIrLoweringFacts, - WriteClass, ECHO_DPO_TARGET_PROFILE, ECHO_SPAN_IR_DOMAIN, + TargetIrSemanticClosure, WriteClass, ECHO_DPO_TARGET_PROFILE, ECHO_SPAN_IR_DOMAIN, }; const EFFECTFUL_REPLACE: &str = "package a.b@1;\n\ @@ -474,6 +474,34 @@ mod contract_bundle_assembly { } } + fn assembly_from_provider_closed_target_ir_input() -> ContractBundleAssemblyFromTargetIrInput { + let mut input = assembly_from_target_ir_input(); + input + .core_module + .imports + .retain(|import| import.kind != CoreImportKind::Lawpack); + input.target_ir_artifact = lower_to_target_ir(&input.core_module, &echo_facts()) + .artifact + .expect("closure-free Core still lowers"); + assert!(input.target_ir_artifact.semantic_closure.is_none()); + input.target_ir_artifact.semantic_closure = Some(TargetIrSemanticClosure { + source_core: ResourceRef { + coordinate: input.core_module.coordinate.clone(), + digest: Some( + digest_core_module(&input.core_module) + .expect("Core digest") + .to_review_string(), + ), + }, + lawpacks: input + .lawpacks + .iter() + .map(DigestLockedResource::to_resource_ref) + .collect(), + }); + input + } + fn effectful_core_and_target_ir() -> (edict_syntax::CoreModule, TargetIrArtifact) { let module = edict_syntax::parse_module(EFFECTFUL_REPLACE).expect("effectful source parses"); @@ -512,6 +540,7 @@ mod contract_bundle_assembly { effect_lowerings: vec![TargetEffectLowering { effect: "target.replace".to_owned(), target_intrinsic: "echo.dpo@1.replace".to_owned(), + failure_mappings: std::collections::BTreeMap::new(), }], } } @@ -696,7 +725,80 @@ mod contract_bundle_assembly { } #[test] - fn assembly_from_target_ir_rejects_unexpected_semantic_closure() { + fn assembly_from_target_ir_accepts_exact_provider_supplied_closure() { + let manifest = assemble_contract_bundle_from_target_ir( + assembly_from_provider_closed_target_ir_input(), + ) + .expect("exact provider-supplied semantic closure assembles"); + + assert_eq!( + manifest.lawpacks, + vec![resource("hello.optics@1", '2').to_resource_ref()] + ); + } + + #[test] + fn assembly_from_target_ir_rejects_provider_closure_core_substitution() { + let mut input = assembly_from_provider_closed_target_ir_input(); + input + .target_ir_artifact + .semantic_closure + .as_mut() + .expect("provider-supplied closure") + .source_core + .digest = Some(digest('e')); + + let err = assemble_contract_bundle_from_target_ir(input) + .expect_err("provider closure must bind the exact supplied Core"); + + assert_eq!( + err.kind(), + ContractBundleAssemblyErrorKind::TargetIrSourceMismatch + ); + assert_eq!( + err.field(), + "target_ir_artifact.semantic_closure.source_core" + ); + } + + #[test] + fn assembly_from_target_ir_rejects_provider_closure_lawpack_substitution() { + let mut input = assembly_from_provider_closed_target_ir_input(); + input + .target_ir_artifact + .semantic_closure + .as_mut() + .expect("provider-supplied closure") + .lawpacks[0] + .digest = Some(digest('e')); + + let err = assemble_contract_bundle_from_target_ir(input) + .expect_err("provider closure lawpacks must equal explicit bundle lawpacks"); + + assert_eq!( + err.kind(), + ContractBundleAssemblyErrorKind::TargetIrSourceMismatch + ); + assert_eq!(err.field(), "lawpacks"); + } + + #[test] + fn assembly_from_target_ir_rejects_missing_provider_closure() { + let mut input = assembly_from_provider_closed_target_ir_input(); + input.target_ir_artifact.semantic_closure = None; + + let err = assemble_contract_bundle_from_target_ir(input) + .expect_err("non-empty bundle lawpacks require a corroborating closure"); + + assert_eq!( + err.kind(), + ContractBundleAssemblyErrorKind::TargetIrSourceMismatch + ); + assert_eq!(err.field(), "lawpacks"); + } + + #[test] + fn assembly_from_target_ir_rejects_mismatched_provider_semantic_closure() { let mut input = assembly_from_target_ir_input(); let injected_closure = input .target_ir_artifact @@ -715,13 +817,16 @@ mod contract_bundle_assembly { input.lawpacks.clear(); let err = assemble_contract_bundle_from_target_ir(input) - .expect_err("closure-free Core rejects an injected semantic closure"); + .expect_err("closure-free Core rejects a mismatched provider semantic closure"); assert_eq!( err.kind(), ContractBundleAssemblyErrorKind::TargetIrSourceMismatch ); - assert_eq!(err.field(), "target_ir_artifact.semantic_closure"); + assert_eq!( + err.field(), + "target_ir_artifact.semantic_closure.source_core" + ); } #[test] diff --git a/crates/edict-syntax/tests/lawpack.rs b/crates/edict-syntax/tests/lawpack.rs new file mode 100644 index 0000000..3d54302 --- /dev/null +++ b/crates/edict-syntax/tests/lawpack.rs @@ -0,0 +1,1010 @@ +//! Canonical lawpack loading and dependency validation. +//! +//! These tests enter through exact canonical bytes and assert typed exports or +//! stable failure kinds. They do not construct an already-trusted manifest. + +use edict_syntax::{ + compile_to_core, decode_canonical_cbor, decode_lawpack_adapter, decode_lawpack_bundle, + digest_core_module, digest_target_ir_artifact, encode_canonical_cbor, encode_core_module, + encode_target_ir_artifact, lower_to_target_ir, parse_module, prepare_lawpack_compilation, + validate_lawpack_dependency_graph, CanonicalValue, LawpackAdapterFailureKind, + LawpackExecutionClass, LawpackPureFunctionImplementation, LawpackValidationFailureKind, + LawpackVerifierClass, TargetLoweringStatus, ValidatedLawpackBundle, +}; +use sha2::{Digest, Sha256}; + +const DIGEST_FRAME: &str = "edict.digest/v1"; +const EXPORTS_COORDINATE: &str = "hello.echo.exports/v1"; +const ADAPTER_COORDINATE: &str = "hello.echo.echo-dpo-adapter/v1"; +const MANIFEST_BYTES: &[u8] = include_bytes!("../../../fixtures/lawpack/hello-echo/manifest.cbor"); +const EXPORTS_BYTES: &[u8] = include_bytes!("../../../fixtures/lawpack/hello-echo/exports.cbor"); +const ADAPTER_BYTES: &[u8] = include_bytes!("../../../fixtures/lawpack/hello-echo/adapter.cbor"); +const ADAPTER_DIGEST: &str = include_str!("../../../fixtures/lawpack/hello-echo/adapter.sha256"); +const TARGET_CONFIGURATION_COORDINATE: &str = "hello.echo.echo-operation-configuration/v1"; +const TARGET_CONFIGURATION_BYTES: &[u8] = + include_bytes!("../../../fixtures/lawpack/hello-echo/echo-operation-configuration.cbor"); +const TARGET_CONFIGURATION_DIGEST: &str = + include_str!("../../../fixtures/lawpack/hello-echo/echo-operation-configuration.sha256"); +const MANIFEST_DIGEST: &str = include_str!("../../../fixtures/lawpack/hello-echo/manifest.sha256"); +const CREATE_GREETING_SOURCE: &str = + include_str!("../../../fixtures/lawpack/hello-echo/create-greeting.edict"); +const CREATE_GREETING_CORE_BYTES: &[u8] = + include_bytes!("../../../fixtures/lawpack/hello-echo/create-greeting.core.cbor"); +const CREATE_GREETING_CORE_DIGEST: &str = + include_str!("../../../fixtures/lawpack/hello-echo/create-greeting.core.sha256"); +const CREATE_GREETING_TARGET_IR_BYTES: &[u8] = + include_bytes!("../../../fixtures/lawpack/hello-echo/create-greeting.target-ir.cbor"); +const CREATE_GREETING_TARGET_IR_DIGEST: &str = + include_str!("../../../fixtures/lawpack/hello-echo/create-greeting.target-ir.sha256"); + +#[test] +fn hello_echo_lawpack_bundle_loads_from_exact_canonical_resources() { + let bundle = + decode_lawpack_bundle(MANIFEST_BYTES, EXPORTS_BYTES).expect("load Hello Echo lawpack"); + let source = parse_module(CREATE_GREETING_SOURCE).expect("parse createGreeting source"); + + assert_eq!(bundle.manifest().id, "hello.echo"); + assert_eq!(bundle.manifest().version, "1"); + assert_eq!(bundle.exports().effects.len(), 1); + assert_eq!( + bundle.exports().effects[0].coordinate, + "hello.echo@1.createGreeting" + ); + assert_eq!( + bundle.exports().effects[0].execution_class, + LawpackExecutionClass::Runtime + ); + assert_eq!( + bundle.exports().effects[0] + .effect_failures + .keys() + .map(String::as_str) + .collect::>(), + vec!["alreadyExists"] + ); + assert_eq!( + source.imports[0].digest.as_deref(), + Some(MANIFEST_DIGEST.trim()) + ); + assert_eq!( + bundle.manifest_digest_review_string(), + MANIFEST_DIGEST.trim() + ); +} + +#[test] +fn hello_echo_source_compiles_to_echo_target_ir_from_exact_lawpack_adapter() { + let bundle = + decode_lawpack_bundle(MANIFEST_BYTES, EXPORTS_BYTES).expect("load Hello Echo lawpack"); + let module = parse_module(CREATE_GREETING_SOURCE).expect("parse createGreeting source"); + let adapter = + decode_lawpack_adapter(&bundle, "echo.dpo@1", ADAPTER_BYTES).expect("load exact adapter"); + assert_eq!(adapter.digest_review_string(), ADAPTER_DIGEST.trim()); + let effect = adapter + .effects() + .get("hello.echo@1.createGreeting") + .expect("createGreeting adapter effect"); + assert_eq!( + effect.target_configuration.id, + TARGET_CONFIGURATION_COORDINATE + ); + assert_eq!( + effect.target_configuration.digest_review_string(), + TARGET_CONFIGURATION_DIGEST.trim() + ); + let target_configuration = + decode_canonical_cbor(TARGET_CONFIGURATION_BYTES).expect("decode target configuration"); + assert_eq!( + encode_canonical_cbor(&target_configuration).expect("re-encode target configuration"), + TARGET_CONFIGURATION_BYTES + ); + assert_eq!( + digest_value(TARGET_CONFIGURATION_COORDINATE, &target_configuration), + effect.target_configuration.digest + ); + let preparation = prepare_lawpack_compilation(&module, &bundle, &adapter) + .expect("derive compiler and target facts"); + let core = compile_to_core(&module, preparation.compiler_context()) + .expect("compile source-derived Core"); + let report = lower_to_target_ir(&core, preparation.target_ir_facts()); + + assert_eq!(report.status, TargetLoweringStatus::Lowered); + let artifact = report.artifact.expect("Echo Target IR artifact"); + assert_eq!(artifact.domain, "echo.span-ir/v1"); + assert_eq!(artifact.target_profile.coordinate, "echo.dpo@1"); + assert_eq!( + artifact.target_profile.digest.as_deref(), + Some("sha256:2e2494121aecf5e6a2d920f5fb85408825d394765fad41484c416397c920fb04") + ); + let semantic_closure = artifact + .semantic_closure + .as_ref() + .expect("lawpack-backed semantic closure"); + assert_eq!(semantic_closure.lawpacks.len(), 1); + assert_eq!(semantic_closure.lawpacks[0].coordinate, "hello.echo@1"); + assert_eq!( + semantic_closure.lawpacks[0].digest.as_deref(), + Some(MANIFEST_DIGEST.trim()) + ); + let intent = artifact + .intents + .get("createGreeting") + .expect("createGreeting intent"); + assert!( + intent.basis.is_some(), + "explicit basis must survive lowering" + ); + assert_eq!( + intent.steps[0].target_intrinsic, + "echo.dpo@1.anchored-node-attachment-create-if-absent" + ); + assert_eq!( + intent.steps[0].obstruction_failures, + vec!["echo.executable-operation/precondition-mismatch/v1"] + ); + assert!( + intent.steps[0] + .obstruction_arms + .contains_key("echo.executable-operation/precondition-mismatch/v1"), + "adapter-mapped target obstruction must survive lowering" + ); + assert_eq!( + encode_core_module(&core).expect("encode createGreeting Core"), + CREATE_GREETING_CORE_BYTES + ); + assert_eq!( + digest_core_module(&core) + .expect("digest createGreeting Core") + .to_review_string(), + CREATE_GREETING_CORE_DIGEST.trim() + ); + assert_eq!( + encode_target_ir_artifact(&artifact).expect("encode createGreeting Target IR"), + CREATE_GREETING_TARGET_IR_BYTES + ); + assert_eq!( + digest_target_ir_artifact(&artifact) + .expect("digest createGreeting Target IR") + .to_review_string(), + CREATE_GREETING_TARGET_IR_DIGEST.trim() + ); +} + +#[test] +fn lawpack_adapter_bytes_must_be_canonical_and_digest_bound() { + let bundle = + decode_lawpack_bundle(MANIFEST_BYTES, EXPORTS_BYTES).expect("load Hello Echo lawpack"); + let noncanonical = decode_lawpack_adapter(&bundle, "echo.dpo@1", &[0x18, 0x00]) + .expect_err("noncanonical adapter must reject"); + assert_eq!( + adapter_failure_kinds(&noncanonical), + vec![LawpackAdapterFailureKind::InvalidCanonicalCbor] + ); + + let mut substituted = + decode_canonical_cbor(ADAPTER_BYTES).expect("decode canonical adapter fixture"); + replace_field( + &mut substituted, + "class", + text("a different canonical adapter"), + ); + let substituted_bytes = + encode_canonical_cbor(&substituted).expect("encode substituted adapter"); + let failures = decode_lawpack_adapter(&bundle, "echo.dpo@1", &substituted_bytes) + .expect_err("adapter digest substitution must reject"); + assert_eq!( + adapter_failure_kinds(&failures), + vec![LawpackAdapterFailureKind::AdapterDigestMismatch] + ); +} + +#[test] +fn lawpack_adapter_requires_a_typed_target_configuration_reference() { + let mut adapter = decode_canonical_cbor(ADAPTER_BYTES).expect("decode canonical adapter"); + let effect = first_map_value_mut(field_mut(&mut adapter, "effectImplementations")); + let target_configuration = field_mut(effect, "targetConfiguration"); + let digest = field_mut(target_configuration, "digest"); + let CanonicalValue::Array(parts) = digest else { + panic!("target configuration digest fixture must be an array"); + }; + let CanonicalValue::Bytes(bytes) = &mut parts[1] else { + panic!("target configuration digest fixture must contain bytes"); + }; + bytes.pop(); + + let bundle = bundle_with_adapter(&adapter); + let bytes = encode_canonical_cbor(&adapter).expect("encode malformed adapter"); + let failures = decode_lawpack_adapter(&bundle, "echo.dpo@1", &bytes) + .expect_err("malformed target configuration reference must reject"); + + assert_eq!( + adapter_failure_kinds(&failures), + vec![LawpackAdapterFailureKind::InvalidTargetConfiguration] + ); +} + +#[test] +fn lawpack_adapter_rejects_an_undeclared_write_class_at_the_effect_path() { + let mut adapter = decode_canonical_cbor(ADAPTER_BYTES).expect("decode canonical adapter"); + let effect = first_map_value_mut(field_mut(&mut adapter, "effectImplementations")); + replace_field(effect, "writeClass", text("modify")); + let bundle = bundle_with_adapter(&adapter); + let bytes = encode_canonical_cbor(&adapter).expect("encode adapter"); + + let failures = decode_lawpack_adapter(&bundle, "echo.dpo@1", &bytes) + .expect_err("undeclared write class must reject"); + + assert_eq!( + adapter_failure_kinds(&failures), + vec![LawpackAdapterFailureKind::InvalidWriteClass] + ); + assert_eq!( + failures[0].path, + "adapter.effectImplementations.hello.echo@1.createGreeting.writeClass" + ); +} + +#[test] +fn lawpack_adapter_selection_requires_one_exact_target_profile() { + let bundle = + decode_lawpack_bundle(MANIFEST_BYTES, EXPORTS_BYTES).expect("load Hello Echo lawpack"); + let failures = decode_lawpack_adapter(&bundle, "echo.dpo@2", ADAPTER_BYTES) + .expect_err("unselected target profile must reject"); + + assert_eq!( + adapter_failure_kinds(&failures), + vec![LawpackAdapterFailureKind::MissingTargetAdapter] + ); +} + +#[test] +fn lawpack_adapter_requires_complete_exported_effect_coverage() { + let mut adapter = decode_canonical_cbor(ADAPTER_BYTES).expect("decode canonical adapter"); + map_mut(field_mut(&mut adapter, "effectImplementations")).clear(); + let bundle = bundle_with_adapter(&adapter); + let bytes = encode_canonical_cbor(&adapter).expect("encode adapter"); + let failures = decode_lawpack_adapter(&bundle, "echo.dpo@1", &bytes) + .expect_err("missing effect implementation must reject"); + + assert_eq!( + adapter_failure_kinds(&failures), + vec![LawpackAdapterFailureKind::MissingEffectImplementation] + ); +} + +#[test] +fn lawpack_adapter_corroborates_footprint_cost_and_failure_obligations() { + for (field, replacement, expected) in [ + ( + "footprintObligation", + text("hello.echo@1.someOtherFootprint"), + LawpackAdapterFailureKind::ObligationMismatch, + ), + ( + "costObligation", + text("hello.echo@1.someOtherBudget"), + LawpackAdapterFailureKind::ObligationMismatch, + ), + ] { + let mut adapter = decode_canonical_cbor(ADAPTER_BYTES).expect("decode canonical adapter"); + let effect = first_map_value_mut(field_mut(&mut adapter, "effectImplementations")); + replace_field(effect, field, replacement); + let bundle = bundle_with_adapter(&adapter); + let bytes = encode_canonical_cbor(&adapter).expect("encode adapter"); + let failures = decode_lawpack_adapter(&bundle, "echo.dpo@1", &bytes) + .expect_err("mismatched obligation must reject"); + assert_eq!(adapter_failure_kinds(&failures), vec![expected]); + } + + let mut adapter = decode_canonical_cbor(ADAPTER_BYTES).expect("decode canonical adapter"); + let effect = first_map_value_mut(field_mut(&mut adapter, "effectImplementations")); + map_mut(field_mut(effect, "failureMappings")).clear(); + let bundle = bundle_with_adapter(&adapter); + let bytes = encode_canonical_cbor(&adapter).expect("encode adapter"); + let failures = decode_lawpack_adapter(&bundle, "echo.dpo@1", &bytes) + .expect_err("incomplete failure mapping must reject"); + assert_eq!( + adapter_failure_kinds(&failures), + vec![LawpackAdapterFailureKind::FailureMappingMismatch] + ); +} + +#[test] +fn lawpack_compilation_requires_the_exact_digest_locked_source_import() { + let bundle = + decode_lawpack_bundle(MANIFEST_BYTES, EXPORTS_BYTES).expect("load Hello Echo lawpack"); + let adapter = + decode_lawpack_adapter(&bundle, "echo.dpo@1", ADAPTER_BYTES).expect("load exact adapter"); + let source = CREATE_GREETING_SOURCE.replace( + MANIFEST_DIGEST.trim(), + &format!("sha256:{}", "0".repeat(64)), + ); + let module = parse_module(&source).expect("parse source with substituted import"); + let failures = prepare_lawpack_compilation(&module, &bundle, &adapter) + .expect_err("substituted source import must reject"); + + assert_eq!( + adapter_failure_kinds(&failures), + vec![LawpackAdapterFailureKind::SourceImportMismatch] + ); +} + +#[test] +fn noncanonical_manifest_bytes_reject_before_shape_validation() { + let failures = decode_lawpack_bundle(&[0x18, 0x00], EXPORTS_BYTES) + .expect_err("noncanonical manifest must reject"); + + assert_eq!( + failure_kinds(&failures), + vec![LawpackValidationFailureKind::InvalidCanonicalCbor] + ); +} + +#[test] +fn manifest_and_export_maps_are_closed() { + let exports = hello_echo_exports(); + let exports_bytes = encode_canonical_cbor(&exports).expect("encode exports"); + let mut manifest = hello_echo_manifest(digest_value(EXPORTS_COORDINATE, &exports)); + insert_field( + &mut manifest, + "displayName", + text("not hash-significant here"), + ); + let manifest_bytes = encode_canonical_cbor(&manifest).expect("encode manifest"); + let failures = decode_lawpack_bundle(&manifest_bytes, &exports_bytes) + .expect_err("unknown manifest field must reject"); + + assert_eq!( + failure_kinds(&failures), + vec![LawpackValidationFailureKind::UnexpectedField] + ); + assert_eq!(failures[0].path, "manifest.displayName"); +} + +#[test] +fn export_digest_substitution_rejects() { + let exports = hello_echo_exports(); + let exports_bytes = encode_canonical_cbor(&exports).expect("encode exports"); + let manifest = hello_echo_manifest([0xff; 32]); + let manifest_bytes = encode_canonical_cbor(&manifest).expect("encode manifest"); + let failures = decode_lawpack_bundle(&manifest_bytes, &exports_bytes) + .expect_err("substituted export digest must reject"); + + assert_eq!( + failure_kinds(&failures), + vec![LawpackValidationFailureKind::ExportsDigestMismatch] + ); +} + +#[test] +fn runtime_effect_requires_at_least_one_target_adapter() { + let exports = hello_echo_exports(); + let exports_bytes = encode_canonical_cbor(&exports).expect("encode exports"); + let mut manifest = hello_echo_manifest(digest_value(EXPORTS_COORDINATE, &exports)); + remove_field(&mut manifest, "targetAdapters"); + let manifest_bytes = encode_canonical_cbor(&manifest).expect("encode manifest"); + let failures = decode_lawpack_bundle(&manifest_bytes, &exports_bytes) + .expect_err("runtime effect without an adapter must reject"); + + assert_eq!( + failure_kinds(&failures), + vec![LawpackValidationFailureKind::RuntimeEffectWithoutTargetAdapter] + ); +} + +#[test] +fn proof_only_effect_does_not_invent_a_runtime_adapter_requirement() { + let mut exports = hello_echo_exports(); + replace_field( + first_array_item_mut(field_mut(&mut exports, "effects")), + "executionClass", + text("proofOnly"), + ); + let exports_bytes = encode_canonical_cbor(&exports).expect("encode exports"); + let mut manifest = hello_echo_manifest(digest_value(EXPORTS_COORDINATE, &exports)); + remove_field(&mut manifest, "targetAdapters"); + let manifest_bytes = encode_canonical_cbor(&manifest).expect("encode manifest"); + + let bundle = decode_lawpack_bundle(&manifest_bytes, &exports_bytes) + .expect("proof-only lawpack does not require an adapter"); + + assert_eq!( + bundle.exports().effects[0].execution_class, + LawpackExecutionClass::ProofOnly + ); +} + +#[test] +fn executable_verifier_must_carry_component_sandbox_and_fuel() { + let exports = hello_echo_exports(); + let exports_bytes = encode_canonical_cbor(&exports).expect("encode exports"); + let mut manifest = hello_echo_manifest(digest_value(EXPORTS_COORDINATE, &exports)); + replace_field( + &mut manifest, + "verifier", + map([ + ("class", text("executable")), + ( + "component", + resource_ref("hello.echo.verifier/v1", [0x77; 32]), + ), + ( + "sandbox", + resource_ref("edict.wasm-component/v1", [0x88; 32]), + ), + ]), + ); + let manifest_bytes = encode_canonical_cbor(&manifest).expect("encode manifest"); + let failures = decode_lawpack_bundle(&manifest_bytes, &exports_bytes) + .expect_err("unbounded executable verifier must reject"); + + assert_eq!( + failure_kinds(&failures), + vec![LawpackValidationFailureKind::MissingField] + ); + assert_eq!(failures[0].path, "manifest.verifier.fuelModel"); +} + +#[test] +fn all_hash_bound_helper_and_verifier_variants_load() { + let mut exports = hello_echo_exports(); + let identity_body = map([ + ( + "params", + CanonicalValue::Array(vec![local_ref( + "arg:0", + "value", + "hello.echo@1.GreetingKey", + )]), + ), + ( + "body", + map([ + ("locals", CanonicalValue::Array(Vec::new())), + ("bindings", CanonicalValue::Array(Vec::new())), + ( + "result", + map([ + ("kind", text("local")), + ( + "ref", + local_ref("arg:0", "value", "hello.echo@1.GreetingKey"), + ), + ]), + ), + ]), + ), + ]); + let pure_functions = array_mut(field_mut(&mut exports, "pureFunctions")); + pure_functions.push(pure_function( + "hello.echo@1.identity", + "edict", + ("body", identity_body), + )); + pure_functions.push(pure_function( + "hello.echo@1.componentIdentity", + "component", + ( + "implementation", + executable_component("hello.echo.component-identity/v1", 0x91), + ), + )); + let exports_bytes = encode_canonical_cbor(&exports).expect("encode exports"); + let mut manifest = hello_echo_manifest(digest_value(EXPORTS_COORDINATE, &exports)); + insert_field( + &mut manifest, + "helperComponent", + executable_component("hello.echo.helpers/v1", 0x92), + ); + replace_field( + &mut manifest, + "verifier", + map([ + ("class", text("executable")), + ( + "component", + resource_ref("hello.echo.verifier/v1", [0x93; 32]), + ), + ( + "sandbox", + resource_ref("edict.wasm-component/v1", [0x94; 32]), + ), + ("fuelModel", resource_ref("edict.fuel/v1", [0x95; 32])), + ]), + ); + let manifest_bytes = encode_canonical_cbor(&manifest).expect("encode manifest"); + + let bundle = + decode_lawpack_bundle(&manifest_bytes, &exports_bytes).expect("full variant bundle"); + + assert_eq!( + bundle.manifest().verifier.class(), + LawpackVerifierClass::Executable + ); + assert!(bundle.manifest().helper_component.is_some()); + assert!(matches!( + bundle.exports().pure_functions[0].implementation, + LawpackPureFunctionImplementation::Edict { .. } + )); + assert!(matches!( + bundle.exports().pure_functions[1].implementation, + LawpackPureFunctionImplementation::Component { .. } + )); +} + +#[test] +fn edict_pure_helpers_reject_effectful_and_unresolved_callees() { + for callee in ["hello.echo@1.createGreeting", "hello.echo@1.notExported"] { + let mut exports = hello_echo_exports(); + let body = map([ + ( + "params", + CanonicalValue::Array(vec![local_ref( + "arg:0", + "value", + "hello.echo@1.GreetingKey", + )]), + ), + ( + "body", + map([ + ("locals", CanonicalValue::Array(Vec::new())), + ("bindings", CanonicalValue::Array(Vec::new())), + ( + "result", + map([ + ("kind", text("call")), + ("callee", text(callee)), + ("typeArgs", CanonicalValue::Array(Vec::new())), + ( + "args", + CanonicalValue::Array(vec![map([ + ("kind", text("local")), + ( + "ref", + local_ref("arg:0", "value", "hello.echo@1.GreetingKey"), + ), + ])]), + ), + ]), + ), + ]), + ), + ]); + array_mut(field_mut(&mut exports, "pureFunctions")).push(pure_function( + "hello.echo@1.invalidCaller", + "edict", + ("body", body), + )); + let exports_bytes = encode_canonical_cbor(&exports).expect("encode exports"); + let manifest = hello_echo_manifest(digest_value(EXPORTS_COORDINATE, &exports)); + let manifest_bytes = encode_canonical_cbor(&manifest).expect("encode manifest"); + + let failures = decode_lawpack_bundle(&manifest_bytes, &exports_bytes) + .expect_err("effectful or unresolved pure callee must reject"); + + assert_eq!( + failure_kinds(&failures), + vec![LawpackValidationFailureKind::InvalidPureFunctionBody], + "callee {callee}" + ); + } +} + +#[test] +fn typed_digests_and_target_adapter_selectors_are_exact() { + let exports = hello_echo_exports(); + let exports_bytes = encode_canonical_cbor(&exports).expect("encode exports"); + let mut malformed = hello_echo_manifest(digest_value(EXPORTS_COORDINATE, &exports)); + let digest = field_mut(field_mut(&mut malformed, "exports"), "digest"); + *digest = CanonicalValue::Array(vec![text("sha256"), CanonicalValue::Bytes(vec![0x01; 31])]); + let failures = decode_lawpack_bundle( + &encode_canonical_cbor(&malformed).expect("encode malformed manifest"), + &exports_bytes, + ) + .expect_err("short digest must reject"); + assert_eq!( + failure_kinds(&failures), + vec![LawpackValidationFailureKind::InvalidDigest] + ); + + let mut duplicate = hello_echo_manifest(digest_value(EXPORTS_COORDINATE, &exports)); + let adapters = array_mut(field_mut(&mut duplicate, "targetAdapters")); + adapters.push(adapters[0].clone()); + let failures = decode_lawpack_bundle( + &encode_canonical_cbor(&duplicate).expect("encode duplicate adapter manifest"), + &exports_bytes, + ) + .expect_err("duplicate exact target selector must reject"); + assert_eq!( + failure_kinds(&failures), + vec![LawpackValidationFailureKind::DuplicateIdentity] + ); +} + +#[test] +fn operation_profile_optic_template_is_a_closed_typed_contract() { + let mut exports = hello_echo_exports(); + let profiles = field_mut(&mut exports, "operationProfiles"); + let profile = first_map_value_mut(profiles); + let optic = field_mut(profile, "opticTemplate"); + replace_field( + field_mut(optic, "apertureRequirement"), + "kind", + text("whatever"), + ); + let exports_bytes = encode_canonical_cbor(&exports).expect("encode exports"); + let manifest = hello_echo_manifest(digest_value(EXPORTS_COORDINATE, &exports)); + let failures = decode_lawpack_bundle( + &encode_canonical_cbor(&manifest).expect("encode manifest"), + &exports_bytes, + ) + .expect_err("unknown aperture kind must reject"); + + assert_eq!( + failure_kinds(&failures), + vec![LawpackValidationFailureKind::InvalidDiscriminant] + ); +} + +#[test] +fn manifest_must_accept_the_supported_core_abi() { + let exports = hello_echo_exports(); + let exports_bytes = encode_canonical_cbor(&exports).expect("encode exports"); + let mut manifest = hello_echo_manifest(digest_value(EXPORTS_COORDINATE, &exports)); + replace_field( + &mut manifest, + "acceptedCoreAbi", + CanonicalValue::Array(vec![text("edict.core/v2")]), + ); + let failures = decode_lawpack_bundle( + &encode_canonical_cbor(&manifest).expect("encode manifest"), + &exports_bytes, + ) + .expect_err("unsupported Core ABI set must reject"); + + assert_eq!( + failure_kinds(&failures), + vec![LawpackValidationFailureKind::MissingAcceptedCoreAbi] + ); +} + +#[test] +fn effect_failure_names_must_be_source_mappable_identifiers() { + for (identifier, expected) in [ + ( + "not-source-mappable", + LawpackValidationFailureKind::InvalidFailureIdentifier, + ), + ( + "else", + LawpackValidationFailureKind::ReservedFailureIdentifier, + ), + ] { + let mut exports = hello_echo_exports(); + let effect = first_array_item_mut(field_mut(&mut exports, "effects")); + let failures = field_mut(effect, "effectFailures"); + rename_only_map_key(failures, identifier); + let exports_bytes = encode_canonical_cbor(&exports).expect("encode exports"); + let manifest = hello_echo_manifest(digest_value(EXPORTS_COORDINATE, &exports)); + let manifest_bytes = encode_canonical_cbor(&manifest).expect("encode manifest"); + let actual = decode_lawpack_bundle(&manifest_bytes, &exports_bytes) + .expect_err("unmappable failure identifier must reject"); + + assert_eq!(failure_kinds(&actual), vec![expected]); + } +} + +#[test] +fn duplicate_export_coordinates_reject_within_their_category() { + let mut exports = hello_echo_exports(); + let effects = array_mut(field_mut(&mut exports, "effects")); + effects.push(effects[0].clone()); + let exports_bytes = encode_canonical_cbor(&exports).expect("encode exports"); + let manifest = hello_echo_manifest(digest_value(EXPORTS_COORDINATE, &exports)); + let manifest_bytes = encode_canonical_cbor(&manifest).expect("encode manifest"); + let failures = decode_lawpack_bundle(&manifest_bytes, &exports_bytes) + .expect_err("duplicate effect coordinate must reject"); + + assert_eq!( + failure_kinds(&failures), + vec![LawpackValidationFailureKind::DuplicateIdentity] + ); +} + +#[test] +fn edict_pure_helper_body_must_match_the_closed_pure_core_schema() { + let mut exports = hello_echo_exports(); + array_mut(field_mut(&mut exports, "pureFunctions")).push(map([ + ("coordinate", text("hello.echo@1.identity")), + ("typeParameters", CanonicalValue::Array(Vec::new())), + ( + "parameterTypes", + CanonicalValue::Array(vec![text("hello.echo@1.GreetingKey")]), + ), + ("returnType", text("hello.echo@1.GreetingKey")), + ("costTemplate", text("hello.echo@1.tiny")), + ("determinismClass", text("total")), + ("source", text("edict")), + ("body", map([("opaque", text("not Core"))])), + ])); + let exports_bytes = encode_canonical_cbor(&exports).expect("encode exports"); + let manifest = hello_echo_manifest(digest_value(EXPORTS_COORDINATE, &exports)); + let manifest_bytes = encode_canonical_cbor(&manifest).expect("encode manifest"); + let failures = decode_lawpack_bundle(&manifest_bytes, &exports_bytes) + .expect_err("opaque pure helper body must reject"); + + assert_eq!( + failure_kinds(&failures), + vec![LawpackValidationFailureKind::InvalidPureFunctionBody] + ); +} + +#[test] +fn dependency_graph_requires_the_complete_exact_set_independent_of_input_order() { + let dependency = bundle("hello.base", "1", &[]); + let dependent = bundle( + "hello.app", + "1", + &[("hello.base", "1", *dependency.manifest_digest())], + ); + + validate_lawpack_dependency_graph(&[dependent.clone(), dependency.clone()]) + .expect("reverse input order validates"); + validate_lawpack_dependency_graph(&[dependency, dependent]).expect("forward order validates"); +} + +#[test] +fn dependency_graph_rejects_missing_and_substituted_manifests() { + let missing = bundle("hello.app", "1", &[("hello.base", "1", [0x99; 32])]); + let failures = + validate_lawpack_dependency_graph(&[missing]).expect_err("missing dependency must reject"); + assert_eq!( + failure_kinds(&failures), + vec![LawpackValidationFailureKind::MissingDependency] + ); + + let dependency = bundle("hello.base", "1", &[]); + let substituted = bundle("hello.app", "1", &[("hello.base", "1", [0xaa; 32])]); + let failures = validate_lawpack_dependency_graph(&[dependency, substituted]) + .expect_err("dependency digest substitution must reject"); + assert_eq!( + failure_kinds(&failures), + vec![LawpackValidationFailureKind::DependencyDigestMismatch] + ); +} + +#[test] +fn dependency_graph_rejects_cycles_before_digest_corroboration() { + let left = bundle("hello.left", "1", &[("hello.right", "1", [0x01; 32])]); + let right = bundle("hello.right", "1", &[("hello.left", "1", [0x02; 32])]); + let failures = + validate_lawpack_dependency_graph(&[right, left]).expect_err("cycle must reject"); + + assert_eq!( + failure_kinds(&failures), + vec![LawpackValidationFailureKind::DependencyCycle] + ); +} + +fn hello_echo_manifest(exports_digest: [u8; 32]) -> CanonicalValue { + manifest("hello.echo", "1", &[], exports_digest) +} + +fn manifest( + id: &str, + version: &str, + dependencies: &[(&str, &str, [u8; 32])], + exports_digest: [u8; 32], +) -> CanonicalValue { + let mut value = decode_canonical_cbor(MANIFEST_BYTES).expect("decode fixture manifest"); + replace_field(&mut value, "id", text(id)); + replace_field(&mut value, "version", text(version)); + replace_field( + &mut value, + "dependencies", + CanonicalValue::Array( + dependencies + .iter() + .map(|(id, version, digest)| { + map([ + ("id", text(id)), + ("version", text(version)), + ( + "digest", + CanonicalValue::Array(vec![ + text("sha256"), + CanonicalValue::Bytes(digest.to_vec()), + ]), + ), + ]) + }) + .collect(), + ), + ); + let exports = field_mut(&mut value, "exports"); + replace_field( + exports, + "digest", + CanonicalValue::Array(vec![ + text("sha256"), + CanonicalValue::Bytes(exports_digest.to_vec()), + ]), + ); + value +} + +fn bundle( + id: &str, + version: &str, + dependencies: &[(&str, &str, [u8; 32])], +) -> ValidatedLawpackBundle { + let exports = hello_echo_exports(); + let exports_bytes = encode_canonical_cbor(&exports).expect("encode exports"); + let manifest = manifest( + id, + version, + dependencies, + digest_value(EXPORTS_COORDINATE, &exports), + ); + let manifest_bytes = encode_canonical_cbor(&manifest).expect("encode manifest"); + decode_lawpack_bundle(&manifest_bytes, &exports_bytes).expect("decode test bundle") +} + +fn hello_echo_exports() -> CanonicalValue { + decode_canonical_cbor(EXPORTS_BYTES).expect("decode fixture exports") +} + +fn resource_ref(id: &str, digest: [u8; 32]) -> CanonicalValue { + map([ + ("id", text(id)), + ( + "digest", + CanonicalValue::Array(vec![text("sha256"), CanonicalValue::Bytes(digest.to_vec())]), + ), + ]) +} + +fn executable_component(id: &str, digest_byte: u8) -> CanonicalValue { + map([ + ("component", resource_ref(id, [digest_byte; 32])), + ( + "sandbox", + resource_ref("edict.wasm-component/v1", [digest_byte.wrapping_add(1); 32]), + ), + ( + "fuelModel", + resource_ref("edict.fuel/v1", [digest_byte.wrapping_add(2); 32]), + ), + ]) +} + +fn pure_function( + coordinate: &str, + source: &str, + implementation: (&str, CanonicalValue), +) -> CanonicalValue { + map([ + ("coordinate", text(coordinate)), + ("typeParameters", CanonicalValue::Array(Vec::new())), + ( + "parameterTypes", + CanonicalValue::Array(vec![text("hello.echo@1.GreetingKey")]), + ), + ("returnType", text("hello.echo@1.GreetingKey")), + ("costTemplate", text("hello.echo@1.tiny")), + ("determinismClass", text("total")), + ("source", text(source)), + implementation, + ]) +} + +fn local_ref(id: &str, alpha_name: &str, ty: &str) -> CanonicalValue { + map([ + ("id", text(id)), + ("alphaName", text(alpha_name)), + ("type", text(ty)), + ]) +} + +fn digest_value(domain: &str, value: &CanonicalValue) -> [u8; 32] { + let framed = CanonicalValue::Array(vec![text(DIGEST_FRAME), text(domain), value.clone()]); + let bytes = encode_canonical_cbor(&framed).expect("encode digest frame"); + Sha256::digest(bytes).into() +} + +fn map(entries: [(&str, CanonicalValue); N]) -> CanonicalValue { + CanonicalValue::Map( + entries + .into_iter() + .map(|(key, value)| (text(key), value)) + .collect(), + ) +} + +fn text(value: &str) -> CanonicalValue { + CanonicalValue::Text(value.to_owned()) +} + +fn failure_kinds( + failures: &[edict_syntax::LawpackValidationFailure], +) -> Vec { + failures.iter().map(|failure| failure.kind).collect() +} + +fn adapter_failure_kinds( + failures: &[edict_syntax::LawpackAdapterFailure], +) -> Vec { + failures.iter().map(|failure| failure.kind).collect() +} + +fn bundle_with_adapter(adapter: &CanonicalValue) -> ValidatedLawpackBundle { + let mut manifest = decode_canonical_cbor(MANIFEST_BYTES).expect("decode canonical manifest"); + let descriptor = first_array_item_mut(field_mut(&mut manifest, "targetAdapters")); + replace_field( + descriptor, + "adapter", + resource_ref( + ADAPTER_COORDINATE, + digest_value(ADAPTER_COORDINATE, adapter), + ), + ); + let manifest_bytes = encode_canonical_cbor(&manifest).expect("encode rebound manifest"); + decode_lawpack_bundle(&manifest_bytes, EXPORTS_BYTES).expect("load rebound lawpack") +} + +fn insert_field(value: &mut CanonicalValue, field: &str, replacement: CanonicalValue) { + map_mut(value).push((text(field), replacement)); +} + +fn replace_field(value: &mut CanonicalValue, field: &str, replacement: CanonicalValue) { + let target = field_mut(value, field); + *target = replacement; +} + +fn remove_field(value: &mut CanonicalValue, field: &str) { + let entries = map_mut(value); + let index = entries + .iter() + .position(|(key, _value)| key == &text(field)) + .expect("fixture field"); + entries.remove(index); +} + +fn field_mut<'a>(value: &'a mut CanonicalValue, field: &str) -> &'a mut CanonicalValue { + map_mut(value) + .iter_mut() + .find_map(|(key, value)| (key == &text(field)).then_some(value)) + .expect("fixture field") +} + +fn rename_only_map_key(value: &mut CanonicalValue, replacement: &str) { + let entries = map_mut(value); + assert_eq!(entries.len(), 1); + entries[0].0 = text(replacement); +} + +fn first_array_item_mut(value: &mut CanonicalValue) -> &mut CanonicalValue { + array_mut(value).first_mut().expect("fixture array item") +} + +fn first_map_value_mut(value: &mut CanonicalValue) -> &mut CanonicalValue { + map_mut(value) + .first_mut() + .map(|(_key, value)| value) + .expect("fixture map entry") +} + +fn array_mut(value: &mut CanonicalValue) -> &mut Vec { + let CanonicalValue::Array(values) = value else { + panic!("fixture array"); + }; + values +} + +fn map_mut(value: &mut CanonicalValue) -> &mut Vec<(CanonicalValue, CanonicalValue)> { + let CanonicalValue::Map(entries) = value else { + panic!("fixture map"); + }; + entries +} diff --git a/crates/edict-syntax/tests/operation_prerequisites.rs b/crates/edict-syntax/tests/operation_prerequisites.rs index f706265..8cad4a9 100644 --- a/crates/edict-syntax/tests/operation_prerequisites.rs +++ b/crates/edict-syntax/tests/operation_prerequisites.rs @@ -49,6 +49,7 @@ fn target_facts() -> TargetIrLoweringFacts { effect_lowerings: vec![TargetEffectLowering { effect: "sequence.splice".to_owned(), target_intrinsic: "echo.dpo@1.splice".to_owned(), + failure_mappings: std::collections::BTreeMap::new(), }], } } diff --git a/crates/edict-syntax/tests/provider.rs b/crates/edict-syntax/tests/provider.rs index dadff50..778c5b4 100644 --- a/crates/edict-syntax/tests/provider.rs +++ b/crates/edict-syntax/tests/provider.rs @@ -250,6 +250,43 @@ fn provider_manifest_rejects_empty_artifact_role() { ); } +#[test] +fn provider_manifest_rejects_path_like_artifact_roles() { + for unsafe_role in [ + "../outside", + "nested/role", + r"nested\role", + "drive:role", + "control\nrole", + ] { + let mut manifest = fixture_manifest(); + let lowerer = manifest + .artifacts + .iter_mut() + .find(|artifact| artifact.artifact_kind == ProviderArtifactKind::Lowerer) + .expect("fixture should declare a lowerer"); + lowerer.role = unsafe_role.to_owned(); + + let report = validate_target_provider_manifest(&manifest); + + assert_eq!( + report.status, + ProviderManifestValidationStatus::Invalid, + "role {unsafe_role}" + ); + assert_eq!(report.failures.len(), 1, "role {unsafe_role}"); + assert_eq!( + report.failures[0].kind, + ProviderManifestValidationFailureKind::UnsafeArtifactRole, + "role {unsafe_role}" + ); + assert_eq!( + report.failures[0].field, "artifacts.role", + "role {unsafe_role}" + ); + } +} + #[test] fn provider_manifest_requires_schema_bindings() { let mut manifest = fixture_manifest(); diff --git a/crates/edict-syntax/tests/provider_lowering.rs b/crates/edict-syntax/tests/provider_lowering.rs index a3ef939..677f0e4 100644 --- a/crates/edict-syntax/tests/provider_lowering.rs +++ b/crates/edict-syntax/tests/provider_lowering.rs @@ -95,6 +95,7 @@ fn echo_fixture() -> LoweringFixture { effect_lowerings: vec![TargetEffectLowering { effect: "target.replace".to_owned(), target_intrinsic: "echo.dpo@1.replace".to_owned(), + failure_mappings: std::collections::BTreeMap::new(), }], }; @@ -133,6 +134,7 @@ fn gitwarp_fixture() -> LoweringFixture { effect_lowerings: vec![TargetEffectLowering { effect: "gitwarp.appendEvent".to_owned(), target_intrinsic: "gitwarp.ref_crdt@1.appendEvent".to_owned(), + failure_mappings: std::collections::BTreeMap::new(), }], }; diff --git a/crates/edict-syntax/tests/target_ir.rs b/crates/edict-syntax/tests/target_ir.rs index 00cbf73..fcce3ac 100644 --- a/crates/edict-syntax/tests/target_ir.rs +++ b/crates/edict-syntax/tests/target_ir.rs @@ -9,13 +9,13 @@ use std::collections::BTreeMap; use edict_syntax::{ check_lowerability, compile_to_core, decode_canonical_cbor, digest_target_ir_artifact, encode_target_ir_artifact, lower_to_target_ir, AtomicityRequirement, CanonicalErrorKind, - CompilerContext, CoreBudget, CoreExpr, CoreImport, CoreImportKind, CorePredicate, CoreValue, - GuardKind, InputConstraint, InputConstraintSource, LowerabilityStatus, LoweringRequirements, - NativeEffectSupport, ResourceRef, SemanticEffectRequirement, TargetEffectLowering, - TargetIrArtifact, TargetIrLoweringFacts, TargetIrRequireFailure, TargetLoweringFailureKind, - TargetLoweringStatus, TargetProfileFacts, WriteClass, ECHO_DPO_TARGET_PROFILE, - ECHO_SPAN_IR_DOMAIN, GITWARP_COMMIT_REDUCER_IR_DOMAIN, GITWARP_REF_CRDT_TARGET_PROFILE, - TARGET_IR_ARTIFACT_DIGEST_DOMAIN, + CompilerContext, CoreBudget, CoreExpr, CoreImport, CoreImportKind, CoreNode, CorePredicate, + CoreValue, GuardKind, InputConstraint, InputConstraintSource, LowerabilityStatus, + LoweringRequirements, NativeEffectSupport, ResourceRef, SemanticEffectRequirement, + TargetEffectLowering, TargetIrArtifact, TargetIrLoweringFacts, TargetIrRequireFailure, + TargetLoweringFailureKind, TargetLoweringStatus, TargetProfileFacts, WriteClass, + ECHO_DPO_TARGET_PROFILE, ECHO_SPAN_IR_DOMAIN, GITWARP_COMMIT_REDUCER_IR_DOMAIN, + GITWARP_REF_CRDT_TARGET_PROFILE, TARGET_IR_ARTIFACT_DIGEST_DOMAIN, }; const EFFECTFUL_REPLACE: &str = "package a.b@1;\n\ @@ -228,6 +228,7 @@ fn echo_facts() -> TargetIrLoweringFacts { effect_lowerings: vec![TargetEffectLowering { effect: "target.replace".to_owned(), target_intrinsic: "echo.dpo@1.replace".to_owned(), + failure_mappings: BTreeMap::new(), }], } } @@ -244,6 +245,7 @@ fn gitwarp_facts() -> TargetIrLoweringFacts { effect_lowerings: vec![TargetEffectLowering { effect: "gitwarp.appendEvent".to_owned(), target_intrinsic: "gitwarp.ref_crdt@1.appendEvent".to_owned(), + failure_mappings: BTreeMap::new(), }], } } @@ -689,10 +691,12 @@ fn unused_duplicate_effect_lowerings_do_not_reject_supported_effect() { facts.effect_lowerings.push(TargetEffectLowering { effect: "target.archive".to_owned(), target_intrinsic: "echo.dpo@1.archive".to_owned(), + failure_mappings: BTreeMap::new(), }); facts.effect_lowerings.push(TargetEffectLowering { effect: "target.archive".to_owned(), target_intrinsic: "echo.dpo@1.archive.v2".to_owned(), + failure_mappings: BTreeMap::new(), }); let report = lower_to_target_ir(&effectful_core(), &facts); @@ -943,6 +947,7 @@ fn ambiguous_effect_lowering_rejects_without_artifact() { facts.effect_lowerings.push(TargetEffectLowering { effect: "target.replace".to_owned(), target_intrinsic: "echo.dpo@1.replace.alternate".to_owned(), + failure_mappings: BTreeMap::new(), }); let report = lower_to_target_ir(&effectful_core(), &facts); @@ -1008,6 +1013,38 @@ fn unsupported_obstruction_key_rejects_without_artifact() { ); } +#[test] +fn colliding_target_obstruction_mappings_reject_without_artifact() { + let mut core = effectful_core(); + let CoreNode::Effect { + obstruction_map, .. + } = &mut core.intents.get_mut("t").expect("intent t").body.nodes[0] + else { + panic!("fixture begins with an effect node"); + }; + obstruction_map.insert( + "alternate".to_owned(), + obstruction_map + .get("rejected") + .expect("fixture declares rejected obstruction") + .clone(), + ); + let mut facts = echo_facts(); + facts.effect_lowerings[0] + .failure_mappings + .insert("alternate".to_owned(), "rejected".to_owned()); + + let report = lower_to_target_ir(&core, &facts); + + assert_eq!(report.status, TargetLoweringStatus::Unsupported); + assert!(report.artifact.is_none()); + assert_eq!( + failure_kinds(&report), + vec![TargetLoweringFailureKind::AmbiguousObstructionMapping] + ); + assert_eq!(report.failures[0].detail, "rejected"); +} + #[test] fn empty_target_step_intents_reject_without_artifact() { let mut core = effectful_core(); diff --git a/crates/edict-syntax/tests/target_profile.rs b/crates/edict-syntax/tests/target_profile.rs index 63cbd1e..b16be07 100644 --- a/crates/edict-syntax/tests/target_profile.rs +++ b/crates/edict-syntax/tests/target_profile.rs @@ -7,6 +7,7 @@ use edict_syntax::{ validate_target_profile_manifest, ResourceRef, TargetProfileConformanceFailureKind, TargetProfileConformanceStatus, TargetProfileManifest, CORE_API_VERSION, + LAWPACK_ADAPTER_API_VERSION, }; fn digest_locked(coordinate: &str) -> ResourceRef { @@ -38,7 +39,7 @@ fn echo_profile() -> TargetProfileManifest { bundle_profile: digest_locked("echo.dpo.bundle/v1"), generated_artifact_profiles: vec![digest_locked("echo.dpo.registration/v1")], canonical_encoding_rules: digest_locked("edict.canonical-cbor/v1"), - accepted_lawpack_adapter_abi: Vec::new(), + accepted_lawpack_adapter_abi: vec![LAWPACK_ADAPTER_API_VERSION.to_owned()], diagnostic_abi: digest_locked("edict.diagnostics/v1"), application_model: "atomic".to_owned(), read_consistency: "application-snapshot".to_owned(), @@ -129,15 +130,26 @@ fn accepted_core_abi_must_include_v1_core() { } #[test] -fn deferred_lawpack_adapter_abi_must_stay_empty_in_v1() { +fn direct_lawpack_adapter_abi_is_supported_in_v1() { + let profile = echo_profile(); + + assert!(failure_kinds(&profile).is_empty()); +} + +#[test] +fn unknown_or_duplicate_lawpack_adapter_abis_are_rejected() { let mut profile = echo_profile(); - profile - .accepted_lawpack_adapter_abi - .push("edict.lawpack-adapter/v1".to_owned()); + profile.accepted_lawpack_adapter_abi = vec!["edict.lawpack-adapter/v2".to_owned()]; + + assert_eq!( + failure_kinds(&profile), + vec![TargetProfileConformanceFailureKind::UnsupportedLawpackAdapterAbi] + ); + profile.accepted_lawpack_adapter_abi = vec![LAWPACK_ADAPTER_API_VERSION.to_owned(); 2]; assert_eq!( failure_kinds(&profile), - vec![TargetProfileConformanceFailureKind::DeferredLawpackAdapterAbiUnsupported] + vec![TargetProfileConformanceFailureKind::UnsupportedLawpackAdapterAbi] ); } diff --git a/docs/REQUIREMENTS.md b/docs/REQUIREMENTS.md index b40f440..a0310e6 100644 --- a/docs/REQUIREMENTS.md +++ b/docs/REQUIREMENTS.md @@ -87,7 +87,7 @@ but owned by a follow-up issue; no fixtures until its dependency lands). | EDICT-TARGET-NEUTRAL-LOWERING-001 | Lowerer compares cost/footprint vs declared target ceiling, never an admitted participant budget | Target | `target/lowering/declared-ceiling` | `target/lowering/admitted-budget` | spec | | EDICT-CORE-GUARD-PAYLOAD-001 | CoreGuard/ObstructionMap carry a typed obstruction payload construct (coordinate + binder + payload expr) | Language | `core/guard/payload-roundtrip` | `core/guard/coordinate-only` | spec | | EDICT-CORE-WHERE-HASH-001 | Core carries typed `where` predicate trees in `inputConstraints`, not a validator coordinate | Language | `core/where/predicate-tree` | `core/where/coordinate-only` | spec | -| EDICT-ABI-LAWPACK-ADAPTER-DEFER-001 | `acceptedLawpackAdapterAbi` is optional/deferred until `edict.lawpack-adapter/v1` exists | Target | `abi/adapter/deferred-empty` | `abi/adapter/required-undefined` | spec | +| EDICT-ABI-LAWPACK-ADAPTER-001 | `acceptedLawpackAdapterAbi` is absent/empty or exactly `["edict.lawpack-adapter/v1"]`; unknown and duplicate ABI claims reject | Target | `abi/adapter/direct-v1` | `abi/adapter/unknown-or-duplicate` | impl | | CONTINUUM-SEMANTIC-OPTIONS-001 | Only semantic compile options enter the semantic digest; diagnostic options excluded | Bundle | `bundle/options/semantic-only` | `bundle/options/diagnostic-in-semantic` | spec | | EDICT-LANG-ENCODEMAX-001 | `CanonicalEncodedMax` is the compiler-derived max canonical-CBOR size of T, composed structurally; rejected for unbounded T | Language | `lang/encodemax/bounded` | `lang/encodemax/unbounded-type` | spec | | EDICT-LANG-BUDGET-UNITS-001 | Core budget units pinned (steps/peak live bytes/output octets); targetBudget = costAlgebra+resolved ceiling | Language | `lang/budget/units` | `lang/budget/undefined-units` | spec | diff --git a/docs/SPEC_edict-lawpack-abi-v1.md b/docs/SPEC_edict-lawpack-abi-v1.md index 2dea741..ea1d801 100644 --- a/docs/SPEC_edict-lawpack-abi-v1.md +++ b/docs/SPEC_edict-lawpack-abi-v1.md @@ -264,6 +264,14 @@ adapter: guard attachment rules ``` +The canonical byte contract for the direct declarative adapter is +[`docs/abi/edict-lawpack-adapter.cddl`](./abi/edict-lawpack-adapter.cddl). +Its operation-profile, effect, and budget maps must exactly cover the owning +lawpack's exported operation profiles, runtime effects, and referenced cost +obligations. The adapter repeats the exported footprint, cost, and named-failure +sets so the loader can corroborate them before deriving compiler or Target IR +facts; a mismatch is a compile-time adapter error. + A portable semantic intent compiles for a target **only** when the lawpack supplies an adapter for that target profile. Absent an adapter, this is a **compiler/lowering error**, not an admission-class error: no valid target diff --git a/docs/SPEC_edict-target-profile-abi-v1.md b/docs/SPEC_edict-target-profile-abi-v1.md index 8f6002b..91687bf 100644 --- a/docs/SPEC_edict-target-profile-abi-v1.md +++ b/docs/SPEC_edict-target-profile-abi-v1.md @@ -98,23 +98,22 @@ including `bundleProfile`, `generatedArtifactProfiles`, `canonicalEncodingRules` and `diagnosticAbi`. This is the **single** authoritative manifest; the Language spec must not duplicate it (`EDICT-ABI-NODUP-001`). -`acceptedLawpackAdapterAbi` is **reserved and deferred**: it will list accepted -lawpack-adapter ABI ids once the byte-level `edict.lawpack-adapter/v1` ABI is -specified. Until that schema exists it is optional and empty, and a target must -not be expected to validate adapter compatibility from it -(`EDICT-ABI-LAWPACK-ADAPTER-DEFER-001`). Lawpack adapters are still digest-locked -resource references in the lawpack manifest; only the cross-ABI compatibility -field is deferred. +`acceptedLawpackAdapterAbi` is optional. A target that consumes the direct +declarative adapter ABI declares exactly `["edict.lawpack-adapter/v1"]`; a +target that does not consume lawpack adapters leaves the field absent or empty. +Unknown and duplicate entries reject +(`EDICT-ABI-LAWPACK-ADAPTER-001`). The adapter itself remains a digest-locked +resource selected by the lawpack manifest. Display metadata is not part of this manifest. Human-facing names, codenames, and marketing copy live in sidecar documents keyed by the target profile digest. The reference `edict_syntax` conformance surface accepts typed manifest values only. It validates the profile API version, identity, accepted Core ABI, -digest-locked component references, canonical encoding rule, deferred -lawpack-adapter ABI emptiness, and v1 application doctrine. The checker is -runtime-neutral: Echo and non-Echo profile shapes are accepted or rejected by the -same obligations. +digest-locked component references, canonical encoding rule, direct +lawpack-adapter ABI compatibility, and v1 application doctrine. The checker is +runtime-neutral: Echo and non-Echo profile shapes are accepted or rejected by +the same obligations. ## Exchange Types And Plugin Boundary diff --git a/docs/abi/edict-lawpack-adapter.cddl b/docs/abi/edict-lawpack-adapter.cddl new file mode 100644 index 0000000..c949bbe --- /dev/null +++ b/docs/abi/edict-lawpack-adapter.cddl @@ -0,0 +1,49 @@ +; edict-lawpack-adapter.cddl +; Canonical schema for one direct declarative lawpack target adapter. +; Authoritative byte encoding: edict.canonical-cbor/v1. +; +; The enclosing lawpack manifest selects the exact target profile, target IR, +; and adapter resource digest. Those identities are not repeated here. + +lawpack-adapter = { + apiVersion: "edict.lawpack-adapter/v1", + class: "declarative", + operationProfiles: { + * tstr => lawpack-adapter-operation-profile + }, + effectImplementations: { + * tstr => lawpack-adapter-effect + }, + budgets: { + * tstr => lawpack-adapter-budget + }, +} + +; Keys are canonical lawpack operation-profile coordinates. +lawpack-adapter-operation-profile = { + core: tstr, + semanticEffects: [+ tstr], +} + +; Keys are canonical lawpack semantic-effect coordinates. Footprint, cost, and +; failure fields must exactly discharge the matching exported effect. +lawpack-adapter-effect = { + targetIntrinsic: tstr, + targetConfiguration: resource-ref, + writeClass: lawpack-adapter-write-class, + footprintObligation: tstr, + costObligation: tstr, + failureMappings: { * failure-ident => tstr }, +} + +; Keys are canonical exported cost-obligation coordinates. +lawpack-adapter-budget = { + maxSteps: uint, + maxAllocatedBytes: uint, + maxOutputBytes: uint, +} + +lawpack-adapter-write-class = "none" / "read" / "create" / "ensure" / + "append" / "replace" / "delete" / "custom" + +; failure-ident is defined in edict-common.cddl. diff --git a/docs/abi/edict-target-profile.cddl b/docs/abi/edict-target-profile.cddl index 6bb35b2..746ae03 100644 --- a/docs/abi/edict-target-profile.cddl +++ b/docs/abi/edict-target-profile.cddl @@ -30,11 +30,10 @@ target-profile-manifest = { 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: [], + ; A profile that accepts the direct declarative lawpack-adapter ABI names it + ; exactly once. Profiles that do not consume lawpack adapters leave this + ; optional slot absent or empty. + ? acceptedLawpackAdapterAbi: [] / ["edict.lawpack-adapter/v1"], diagnosticAbi: resource-ref, ; application doctrine diff --git a/docs/schemas/edict.compiler-settings.v1.schema.json b/docs/schemas/edict.compiler-settings.v1.schema.json index 43da0cc..5b61042 100644 --- a/docs/schemas/edict.compiler-settings.v1.schema.json +++ b/docs/schemas/edict.compiler-settings.v1.schema.json @@ -14,7 +14,12 @@ "const": "compilerSettings" }, "operation": { - "enum": ["check", "project"] + "enum": ["build", "check", "project"] + }, + "application": { + "type": "string", + "minLength": 1, + "description": "Path to an edict.application/v1 manifest for the build operation." }, "emit": { "type": "array", @@ -167,26 +172,54 @@ "allOf": [ { "if": { + "required": ["operation"], "properties": { "operation": { "const": "check" } } }, "then": { "properties": { + "type": { "const": "compilerSettings" }, "emit": false, "compilerContext": false, - "target": false + "target": false, + "application": false } } }, { "if": { + "required": ["operation"], "properties": { "operation": { "const": "project" } } }, "then": { - "required": ["emit"] + "required": ["emit"], + "properties": { + "type": { "const": "compilerSettings" }, + "application": false + } + } + }, + { + "if": { + "required": ["operation"], + "properties": { + "operation": { "const": "build" } + } + }, + "then": { + "required": ["application"], + "properties": { + "type": { "const": "compilerSettings" }, + "emit": false, + "compilerContext": false, + "target": false, + "inputRoot": false, + "followSymlinks": false, + "directoryExtensions": false + } } } ] diff --git a/docs/topics/cli/README.md b/docs/topics/cli/README.md index d7ac81d..da53c03 100644 --- a/docs/topics/cli/README.md +++ b/docs/topics/cli/README.md @@ -21,7 +21,63 @@ accepted request schema identifiers, and the exit-code contract. Any other argument is rejected with an `InvalidArguments` diagnostic and exit `2`. [CLI-REQ-009] -The implemented operations are `check` and `project`. +The implemented operations are `build`, `check`, and `project`. + +A `build` request contains one settings record and no compiler-input records. +Its `application` field points to an `edict.application/v1` JSON manifest. The +manifest names one exact Edict source, its complete lawpack closure, the +selected target profile and provider package, and the output directory. The +current executable-operation route accepts exactly one source and a non-empty +ordered lawpack closure whose first entry is the root. It validates the complete +supplied dependency graph, compiles and lowers the source through the root +lawpack's declarative target adapter, and resolves the selected provider only +from its checked package manifest. + +```json +{"schema":"edict.compiler.settings/v1","type":"compilerSettings","operation":"build","application":"edict.application.json"} +``` + +The application path is resolved from the process working directory. Every +path inside the manifest is resolved from the manifest's parent directory and +must be non-empty, relative, and free of parent traversal. A minimal manifest +has this shape: + +```json +{ + "schema": "edict.application/v1", + "coordinate": "examples.hello_echo@1", + "sources": ["src/hello_echo.edict"], + "lawpacks": [{ + "manifest": "vendor/causal-cell/manifest.cbor", + "exports": "vendor/causal-cell/exports.cbor", + "adapter": "vendor/causal-cell/adapter.cbor", + "targetConfiguration": "vendor/causal-cell/echo-operation-configuration.cbor" + }], + "target": { + "profile": "echo.dpo@1", + "providerPackage": ".build/echo-provider" + }, + "outputDirectory": ".build/application" +} +``` + +The build invokes the provider's checked lowerer component and its structurally +separate verifier component through the capability-denied provider host. Only +an accepted verification result reaches the output directory. The current Echo +target writes the exact provider-emitted bytes as: + +- `executable-operation-package.cbor`; +- `verification-report.cbor`. + +Edict does not re-encode either artifact and does not execute the package. +Concurrent writers are excluded, and replacement preserves the previous pair +if either output cannot be published. + +Provider diagnostics are fail-closed on this first public build route: any +provider-authored diagnostic rejects publication, independent of its severity. +The terminal status `checked` count is `1` after a successful build because the +request processes one application manifest and its complete referenced closure. +[CLI-REQ-015] A `check` request accepts: @@ -64,12 +120,14 @@ within that root, or the CLI rejects the request with `InputPathOutsideRoot` and exit `2`. Inline source records are not filesystem reads. [CLI-REQ-003, CLI-REQ-011] -Successful `check` results are emitted to stdout. `check` compiler diagnostics, -CLI input errors, and failure status records are emitted to stderr. `project` -projection records, including compiler diagnostics and lowering failures, are -emitted to stdout when the request itself is valid. Both streams use one JSON -object per line with no banners, spinners, blank lines, or direct human prose -outside JSON string fields. [CLI-REQ-001, CLI-REQ-014] +Successful `check` results are emitted to stdout. A successful `build` emits +only its terminal status record to stdout after the accepted artifacts have +been written. `check` compiler diagnostics, build failures, CLI input errors, +and failure status records are emitted to stderr. `project` projection records, +including compiler diagnostics and lowering failures, are emitted to stdout +when the request itself is valid. Both streams use one JSON object per line +with no banners, spinners, blank lines, or direct human prose outside JSON +string fields. [CLI-REQ-001, CLI-REQ-014, CLI-REQ-015] When a CLI-input failure happens after the requested operation is known, the diagnostic and terminal status records carry that command. Invalid `project` @@ -99,9 +157,9 @@ the checked-in schemas as the accepted wire shape. ## Exit Codes -- `0`: request completed successfully. For `project`, this can include - compiler diagnostics or Target IR lowering failures emitted as projection - records. +- `0`: request completed successfully. For `build`, the accepted provider + artifacts were written. For `project`, this can include compiler diagnostics + or Target IR lowering failures emitted as projection records. - `1`: compiler or validation diagnostics were produced for at least one source input in the `check` operation. - `2`: CLI input or usage was invalid before compiler validation could run. @@ -119,8 +177,8 @@ expansion paths, including optional root-confinement rejection. [CLI-REQ-008] The following are not implemented by this first CLI slice: -- bundle assembly; -- admission workflow execution; +- general-purpose bundle assembly; +- runtime admission and execution workflows; - human-pretty output mode; - Echo execution; - language-server transport. diff --git a/docs/topics/cli/test-plan.md b/docs/topics/cli/test-plan.md index 01beb50..20563d7 100644 --- a/docs/topics/cli/test-plan.md +++ b/docs/topics/cli/test-plan.md @@ -13,13 +13,15 @@ In scope: patterns; - structured parser and CLI diagnostics; - editor-facing projection JSONL records over dirty source input; +- application builds from an exact source, lawpack, target-adapter, provider, + and verifier closure; - a checked-in golden fixture corpus replayed end-to-end through the binary. Out of scope: - human-pretty output; -- bundle assembly; -- admission workflows; +- general-purpose bundle assembly; +- runtime admission and execution workflows; - language-server transport. ## Requirements @@ -40,6 +42,7 @@ Out of scope: | CLI-REQ-012 | implemented | The checked-in CLI golden corpus can be regenerated by `cargo xtask cli-goldens --write` and checked by `cargo xtask cli-goldens --check`; `cargo xtask verify` runs the check mode. | xtask/src/goldens.rs, xtask/src/main.rs | | CLI-REQ-013 | implemented | The `project` operation accepts dirty editor source from `source` input records and emits structured syntax, diagnostics, Core, Target IR, digest, and status records without requiring a source file on disk. | crates/edict-cli/tests/jsonl_cli.rs | | CLI-REQ-014 | implemented | Compiler-level projection failures are emitted as structured projection records and diagnostics, not process-level CLI failures. | crates/edict-cli/tests/jsonl_cli.rs | +| CLI-REQ-015 | planned | The `build` operation loads one `edict.application/v1` request, validates the exact Edict source and complete lawpack/adapter closure, invokes the selected target provider lowerer and independent verifier through the bounded provider host, and writes only the accepted canonical package and verification-report bytes. | crates/edict-cli/src/application_build.rs | ## Fixtures @@ -47,6 +50,7 @@ Out of scope: | --- | --- | --- | | crates/edict-cli/tests/jsonl_cli.rs | Runtime-created JSONL requests and source files for CLI behavior. | Tests parse every stdout and stderr line as JSON objects and assert stable fields. | | docs/schemas/edict.compiler-settings.v1.schema.json | Stable JSON Schema for compiler settings records. | The schema contract test validates the schema identifier, required fields, supported operation values, deterministic input-expansion settings, and optional root confinement. | +| fixtures/lawpack/hello-echo/README.md | Canonical Hello Echo Target IR witness and digest sidecar corpus. | `cargo xtask lawpack-goldens --check` recompiles the digest-pinned Edict source through the generated causal-cell closure and rejects byte drift. | | docs/schemas/edict.compiler-input.v1.schema.json | Stable JSON Schema for compiler input records. | The schema contract test validates the identifier, required fields, supported input kinds, and per-kind variant fields. | | docs/schemas/edict.cli-check-result.v1.schema.json | Stable JSON Schema for success result records. | The schema contract test validates the identifier, required fields, and pinned `command`, `type`, and `status` values. | | docs/schemas/edict.cli-diagnostic.v1.schema.json | Stable JSON Schema for diagnostic records. | The schema contract test validates the identifier, required fields, supported commands and stages, and optional span, line, and message fields. | @@ -93,6 +97,8 @@ Out of scope: | CLI-TP-024 | implemented | Error handling | CLI-REQ-006, CLI-REQ-007 | Invalid `project` settings fail as CLI-input errors whose diagnostic and terminal status records carry `command: "project"`. | invalid_project_settings_report_project_command, diagnostic_schema_declares_jsonl_contract | crates/edict-cli/tests/jsonl_cli.rs, docs/schemas/edict.cli-diagnostic.v1.schema.json | Keeps editor adapters from misclassifying project request failures as `check` failures. | | CLI-TP-025 | implemented | Error handling | CLI-REQ-003 | Explicit `null` values for object-valued compiler settings are rejected as `InvalidSettings`, including `compilerContext` and `target`. | projection_object_settings_null_reject_as_invalid_settings | crates/edict-cli/tests/jsonl_cli.rs | Runtime request parsing stays aligned with the checked-in settings schema rather than letting serde coerce nulls into absent values. | | CLI-TP-026 | implemented | Schema guard | CLI-REQ-003 | The compiler settings schema rejects empty `project` emit lists. | compiler_settings_schema_declares_jsonl_contract | docs/schemas/edict.compiler-settings.v1.schema.json | The schema and runtime both require at least one requested projection slot. | +| CLI-TP-027 | implemented | Build request dispatch | CLI-REQ-015 | A `build` settings record is accepted without compiler input records and reaches application-config loading; a missing application config fails as a structured `ApplicationConfigReadFailed` build diagnostic rather than as invalid settings or missing compiler input. | build_accepts_application_request_without_compiler_input_records | crates/edict-cli/tests/jsonl_cli.rs | Covers the public request shape and dispatch boundary only. | +| CLI-TP-028 | planned | Verified application build | CLI-REQ-015 | A repository-owned provider-component fixture builds one exact source and complete lawpack closure, independently accepts the package, and publishes the canonical package/report pair. | - | - | The standalone external Hello Echo build is a green integration witness; this planned case makes the full crossing reproducible inside Edict's own automated suite. | ## Determinism Obligations @@ -107,6 +113,10 @@ Out of scope: ## Open Gaps -- No CLI surface for explain, bundle, or admission workflows yet. +- No CLI surface for explain, general-purpose bundle, runtime admission, or + execution workflows yet. - No JSON Schema validation engine is embedded in the CLI; the schema is the stable contract artifact for callers. +- The full successful provider-component application build has an external + Hello Echo integration witness but not yet a repository-owned automated + fixture (CLI-TP-028). diff --git a/docs/topics/compiler-spine/test-plan.md b/docs/topics/compiler-spine/test-plan.md index 7fbbbe1..14d4b88 100644 --- a/docs/topics/compiler-spine/test-plan.md +++ b/docs/topics/compiler-spine/test-plan.md @@ -49,6 +49,7 @@ Out of scope: | CSPINE-REQ-019 | implemented | The compiler spine recognizes the source `I32`, `I64`, `U32`, and `U64` scalar types, preserves an explicitly suffixed literal's exact width and signedness, propagates an unambiguous expected integer width through supported comparisons, annotations, and record returns, and rejects unconstrained, out-of-range, or cross-width values before Core lowering. | docs/SPEC_edict-language-v1.md | | CSPINE-REQ-020 | implemented | An explicit source basis expression is type-checked only in the pure pre-body input environment and is preserved structurally in the lowered Core intent; `basis none` remains the explicit no-basis posture. | docs/SPEC_edict-language-v1.md, EDICT-LANG-BASIS-PURE-001 | | CSPINE-REQ-021 | implemented | The compiler spine accepts a statically bounded `Bytes` field in an operation input and preserves its exact bound in Core; unbounded bytes remain a surface error. | docs/SPEC_edict-language-v1.md | +| CSPINE-REQ-022 | implemented | A validated lawpack plus its exact direct target adapter derive the compiler profile, write-class, effect, and budget facts under the source module's import alias; application compilation must not require a caller-built or handwritten authority-facts substitute. | issue #169, docs/topics/lawpacks/test-plan.md | ## Fixtures @@ -83,6 +84,7 @@ Out of scope: | CSPINE-TP-020 | implemented | Golden path | CSPINE-REQ-003, CSPINE-REQ-004, CSPINE-REQ-020 | The operation prerequisite fixture lowers `basis input.basis` to the exact Core field-expression tree, while a basis that references a body local or a directly constructed resolved module with a missing or duplicate basis clause rejects before Core exists. | operation_prerequisite_fixture_preserves_fixed_width_basis_and_lawpack_closure, body_local_cannot_become_an_intent_basis, direct_type_check_refuses_a_missing_basis_without_panicking, direct_type_check_refuses_duplicate_basis_clauses | fixtures/lang/operations/explicit-basis-u64.edict | Basis authoring remains distinct from runtime basis resolution or admission. | | CSPINE-TP-021 | implemented | Golden path | CSPINE-REQ-003, CSPINE-REQ-004, CSPINE-REQ-021 | The operation prerequisite fixture lowers its replacement field as bounded Core `Bytes` with the authored maximum unchanged. | operation_prerequisite_fixture_preserves_fixed_width_basis_and_lawpack_closure | fixtures/lang/operations/explicit-basis-u64.edict | Adds only the bounded byte payload required by the downstream operation seam. | | CSPINE-TP-022 | implemented | Golden path and boundary guard | CSPINE-REQ-003, CSPINE-REQ-004, CSPINE-REQ-019 | Bare integer literals inherit `U64`/`U32` from a typed comparison operand, explicit annotation, or record return field, while an unconstrained bare integer rejects before Core identity exists. | bare_integer_literals_inherit_unambiguous_fixed_width_context, unconstrained_bare_integer_literal_refuses_before_core | fixtures/lang/operations/explicit-basis-u64.edict | Expected-type propagation is limited to contexts whose width is unambiguous; it never defaults an unconstrained literal to a host or convenience width. | +| CSPINE-TP-023 | implemented | Integration | CSPINE-REQ-003, CSPINE-REQ-004, CSPINE-REQ-022 | The exact Hello Echo source, manifest, exports, and selected adapter derive module-local compiler facts and produce canonical Core without a test-built `CompilerContext`. | hello_echo_source_compiles_to_echo_target_ir_from_exact_lawpack_adapter | fixtures/lawpack/hello-echo/README.md | The same witness continues into Target IR but makes no package or runtime claim. | ## Determinism Obligations diff --git a/docs/topics/contract-bundles/README.md b/docs/topics/contract-bundles/README.md index 86ed993..2347499 100644 --- a/docs/topics/contract-bundles/README.md +++ b/docs/topics/contract-bundles/README.md @@ -70,14 +70,17 @@ The canonical artifact identity rules are named in into `manifest.target_ir.digest`, and uses the same digest in the semantic bundle preimage. The target profile reference is derived from the artifact's digest-locked `target_profile`. Before assembly, this path recomputes the - semantic closure from the supplied Core, requires the artifact closure to - match its exact Core identity and lawpack set, and requires the bundle - lawpacks to equal that set. Equivalent bundle lawpack order and duplicate - identical references normalize to the closure's canonical set before bundle - identity is computed. Malformed Core retains the same canonical-digest error - classification as the direct assembly path. This is cross-artifact - corroboration at the computed assembly boundary, not a runtime admission - claim. [BUNDLE-REQ-008] + semantic closure from the supplied Core when Core declares one and requires + the artifact closure to match its exact Core identity and lawpack set. When + Core declares no closure, a provider-supplied artifact closure is accepted + only when it binds that exact Core and its lawpack set equals the explicit + bundle lawpacks. Missing or substituted bindings remain rejected. Equivalent + bundle lawpack order and duplicate identical references normalize to the + closure's canonical set before bundle identity is computed. Malformed Core + retains the same canonical-digest error classification as the direct + assembly path. This is cross-artifact corroboration at the computed assembly + boundary, not a runtime admission claim. [BUNDLE-REQ-008] + [BUNDLE-REQ-010] - `assemble_contract_bundle` remains available for already-digested external Target IR references. On that supplied-reference path, the same typed Target IR resource supplies both `manifest.target_ir.digest` and the semantic bundle diff --git a/docs/topics/contract-bundles/test-plan.md b/docs/topics/contract-bundles/test-plan.md index 436314e..5c1f093 100644 --- a/docs/topics/contract-bundles/test-plan.md +++ b/docs/topics/contract-bundles/test-plan.md @@ -43,8 +43,9 @@ Out of scope: | BUNDLE-REQ-005 | implemented | HOLMES, Watson, and Moriarty evidence entries are optional in the typed bundle; when present, each entry must bind to the manifest's selected bundle subject digest, target profile digest, and target IR digest. | issue #1, docs/GUIDE_edict-assurance-transparency.md | | BUNDLE-REQ-006 | implemented | Admission artifacts remain out of the participant-neutral contract bundle manifest; non-empty admission references are rejected. | docs/SPEC_continuum-contract-bundle-v1.md | | BUNDLE-REQ-007 | implemented | The typed bundle pins `canonicalization_profile.coordinate` to `edict.canonical-cbor/v1`. | docs/SPEC_continuum-contract-bundle-v1.md | -| BUNDLE-REQ-008 | implemented | The crate can assemble a `ContractBundleManifest`, computing `semanticBundleDigest` and `releaseBundleDigest` per the exact spec preimages from digest-locked references, with `coreIrDigest` computed from a real compiled Core module and `targetIrDigest` either supplied as a typed reference or computed from a real `TargetIrArtifact`; the real-artifact path requires the Target IR semantic closure to equal the closure recomputed from the supplied Core, requires and canonicalizes the bundle lawpack set to match that closure, and preserves malformed-Core canonical failure taxonomy; the assembled manifest validates. | docs/SPEC_continuum-contract-bundle-v1.md, docs/design/contract-bundle-assembly-v0.11.md, docs/design/canonical-target-ir-v0.11.md | +| BUNDLE-REQ-008 | implemented | The crate can assemble a `ContractBundleManifest`, computing `semanticBundleDigest` and `releaseBundleDigest` per the exact spec preimages from digest-locked references, with `coreIrDigest` computed from a real compiled Core module and `targetIrDigest` either supplied as a typed reference or computed from a real `TargetIrArtifact`; the real-artifact path corroborates the Target IR semantic closure against the exact supplied Core and explicit bundle lawpacks, canonicalizes the lawpack set, and preserves malformed-Core canonical failure taxonomy; the assembled manifest validates. | docs/SPEC_continuum-contract-bundle-v1.md, docs/design/contract-bundle-assembly-v0.11.md, docs/design/canonical-target-ir-v0.11.md | | BUNDLE-REQ-009 | implemented | Direct and built-in-lowerer Target IR artifacts produce identical semantic and release bundle identities under identical explicit assembly inputs; lowerer identity remains release-only. | issue #140, docs/design/provider-artifact-pipeline-alpha.md | +| BUNDLE-REQ-010 | implemented | When Core does not declare a semantic closure, the computed-Target-IR assembly path may accept a provider-supplied closure only when it binds the exact supplied Core and its canonical lawpack set equals the explicitly supplied bundle lawpack set; missing or substituted bindings remain rejected. | docs/topics/contract-bundles/README.md | ## Fixtures @@ -67,12 +68,13 @@ Out of scope: | BUNDLE-TP-007 | implemented | Boundary guard | BUNDLE-REQ-001 | Removing the release-only build-provenance digest returns `InvalidArtifactReference` on `build_provenance`. | release_bundle_inputs_must_be_digest_locked | crates/edict-syntax/tests/contract_bundle.rs | Proves release digest preimage inputs are represented by the typed manifest. | | BUNDLE-TP-008 | implemented | Boundary guard | BUNDLE-REQ-007 | Changing `canonicalization_profile.coordinate` returns `UnsupportedCanonicalizationProfile`. | canonicalization_profile_must_be_the_v1_cbor_profile | crates/edict-syntax/tests/contract_bundle.rs | Pins the v1 bundle to the canonical CBOR profile. | | BUNDLE-TP-009 | implemented | Golden path | BUNDLE-REQ-001 | Empty lawpack, generated-artifact, and conformance-corpus lists remain valid because optional lists bind what is present without creating non-empty obligations. | optional_artifact_lists_may_be_empty | crates/edict-syntax/tests/contract_bundle.rs | Source artifacts remain the required artifact set. | -| BUNDLE-TP-010 | implemented | Golden path and substitution boundary | BUNDLE-REQ-008 | Assembling a bundle from a real compiled Core module plus supplied digest-locked references produces a manifest that `validate_contract_bundle_manifest` returns `Valid` for; the real-Target-IR path computes `targetIrDigest`, rejects source-coordinate, source-Core-digest, missing, injected, artifact-lawpack, and bundle-lawpack substitutions, preserves malformed-Core canonical failures, canonicalizes equivalent bundle lawpack order and duplicates, reports invalid embedded target-profile digests with stable fields, and the checked-in bundle digest golden matches regenerated assembler output. | assembled_bundle_from_real_core_validates, assembled_bundle_from_real_target_ir_computes_target_ir_digest, assembly_from_target_ir_rejects_mismatched_core_source, assembly_from_target_ir_rejects_core_digest_substitution, assembly_from_target_ir_rejects_stripped_lawpack_only_closure, assembly_from_target_ir_rejects_unexpected_semantic_closure, assembly_from_target_ir_rejects_artifact_lawpack_substitution, assembly_from_target_ir_rejects_lawpack_set_substitution, assembly_from_target_ir_preserves_canonical_core_failure, assembly_from_target_ir_canonicalizes_bundle_lawpacks, assembly_from_target_ir_rejects_invalid_target_profile_digest_with_stable_field, assembly_rejects_inputs_that_would_not_validate, bundle_digest_goldens_match_assembly | crates/edict-syntax/tests/contract_bundle.rs, fixtures/bundle/assembly/bounded-hello.bundle-digests.txt | The computed-artifact path recomputes semantic closure from Core; standalone canonical Target IR validation remains self-validation and does not reconstruct erased source dependencies. `cargo xtask bundle-goldens --check` guards golden drift. | +| BUNDLE-TP-010 | implemented | Golden path and substitution boundary | BUNDLE-REQ-008 | Assembling a bundle from a real compiled Core module plus supplied digest-locked references produces a manifest that `validate_contract_bundle_manifest` returns `Valid` for; the real-Target-IR path computes `targetIrDigest`, rejects source-coordinate, source-Core-digest, missing, mismatched-provider, artifact-lawpack, and bundle-lawpack substitutions, preserves malformed-Core canonical failures, canonicalizes equivalent bundle lawpack order and duplicates, reports invalid embedded target-profile digests with stable fields, and the checked-in bundle digest golden matches regenerated assembler output. | assembled_bundle_from_real_core_validates, assembled_bundle_from_real_target_ir_computes_target_ir_digest, assembly_from_target_ir_rejects_mismatched_core_source, assembly_from_target_ir_rejects_core_digest_substitution, assembly_from_target_ir_rejects_stripped_lawpack_only_closure, assembly_from_target_ir_rejects_mismatched_provider_semantic_closure, assembly_from_target_ir_rejects_artifact_lawpack_substitution, assembly_from_target_ir_rejects_lawpack_set_substitution, assembly_from_target_ir_preserves_canonical_core_failure, assembly_from_target_ir_canonicalizes_bundle_lawpacks, assembly_from_target_ir_rejects_invalid_target_profile_digest_with_stable_field, assembly_rejects_inputs_that_would_not_validate, bundle_digest_goldens_match_assembly | crates/edict-syntax/tests/contract_bundle.rs, fixtures/bundle/assembly/bounded-hello.bundle-digests.txt | The computed-artifact path recomputes semantic closure from Core when Core declares it and otherwise corroborates an exact provider-supplied closure against explicit bundle lawpacks; standalone canonical Target IR validation remains self-validation and does not reconstruct erased source dependencies. `cargo xtask bundle-goldens --check` guards golden drift. | | BUNDLE-TP-011 | implemented | Boundary guard | BUNDLE-REQ-008 | Changing any semantic-layer preimage component (Core digest, supplied `targetIrDigest` reference, target profile, lawpack, generated artifact, conformance corpus, verifier report, `sourceProfileSemanticFactsDigest`, `canonicalizationProfileDigest`, or semantic compile options) changes both `semanticBundleDigest` and `releaseBundleDigest`. | semantic_preimage_mutations_change_semantic_and_release_digests | crates/edict-syntax/tests/contract_bundle.rs | Semantic-layer mutation propagates to both digests. | | BUNDLE-TP-012 | implemented | Boundary guard | BUNDLE-REQ-008 | Changing any release-only preimage component (provenance-only source digest or logical path, compiler/lowerer/verifier identity with produced artifacts unchanged, nonsemantic compile options, build provenance, or compile explanation) changes `releaseBundleDigest` only, leaving `semanticBundleDigest` unchanged. | release_only_preimage_mutations_leave_semantic_digest_unchanged | crates/edict-syntax/tests/contract_bundle.rs | Honors the semantic/release split; diagnostic and provenance-only changes do not move semantic identity. | | BUNDLE-TP-013 | implemented | Boundary guard | BUNDLE-REQ-008 | The assembly input API distinguishes the computed Core digest from supplied references by type, rejects non-lowercase supplied digest references, and uses the supplied `targetIrDigest` as the single source of truth for both the manifest and semantic preimage. | assembly_rejects_uppercase_supplied_target_ir_digest, assembly_rejects_uppercase_supplied_artifact_digest, target_ir_digest_is_single_source_of_truth | crates/edict-syntax/tests/contract_bundle.rs | The separate computed Target IR path is covered by BUNDLE-TP-010 and TIR-TP-022. | | BUNDLE-TP-014 | implemented | Boundary guard | BUNDLE-REQ-008 | Changing an optional assurance-evidence artifact that is not a top-level preimage component does not change either bundle digest; assurance evidence is governed by subject/target binding, not by top-level digest propagation. | optional_assurance_evidence_is_not_a_top_level_digest_preimage | crates/edict-syntax/tests/contract_bundle.rs | No top-level bundle tamper claim for optional external evidence. | | BUNDLE-TP-015 | implemented | Integration | BUNDLE-REQ-009 | Direct and built-in-lowerer artifacts produce identical semantic and release bundle identities when explicit inputs match; changing only the explicit lowerer identity preserves semantic identity and changes release identity. | builtin_lowerer_bundles_preserve_semantic_and_release_identity, changing_builtin_lowerer_identity_changes_only_release_identity | crates/edict-syntax/tests/provider_lowering.rs | Bundle assembly consumes a `TargetIrArtifact`; it does not invoke the lowerer. | +| BUNDLE-TP-016 | implemented | Integration and substitution boundary | BUNDLE-REQ-010 | Closure-free Core plus an exact provider-supplied Target IR closure and matching explicit bundle lawpacks assembles; a changed source-Core digest, changed Target IR lawpack set, or missing Target IR closure with non-empty bundle lawpacks is rejected with a stable field. | assembly_from_target_ir_accepts_exact_provider_supplied_closure, assembly_from_target_ir_rejects_provider_closure_core_substitution, assembly_from_target_ir_rejects_provider_closure_lawpack_substitution, assembly_from_target_ir_rejects_missing_provider_closure | crates/edict-syntax/tests/contract_bundle.rs | The provider cannot inject ambient semantics: every closure resource remains exact, digest-locked, and corroborated by an explicit bundle input. | ## Determinism Obligations diff --git a/docs/topics/fixtures/test-plan.md b/docs/topics/fixtures/test-plan.md index 02f5a53..624357d 100644 --- a/docs/topics/fixtures/test-plan.md +++ b/docs/topics/fixtures/test-plan.md @@ -11,12 +11,14 @@ In scope: - reviewed Core golden bytes and exact digest artifacts; - reviewed authority-facts canonical bytes and exact digest artifact; - reviewed Edict-owned target-profile contract-resource bytes and digests; +- reviewed lawpack manifest/export canonical bytes, exact digests, and + digest-pinned source; - contract-graph validation of fixture references in topic test plans. Out of scope: -- lawpack, runtime-owned target-profile, contract-bundle, admission, and - conformance fixture families that do not have executable owning behavior yet; +- runtime-owned target-profile, contract-bundle, admission, and conformance + fixture families that do not have executable owning behavior yet; - tests that assert documentation prose about fixtures. ## Requirements @@ -26,10 +28,12 @@ Out of scope: | FIXTURES-REQ-001 | implemented | Source fixtures under `fixtures/lang/` are executable behavior inputs, not illustrative prose. | fixtures/README.md | | FIXTURES-REQ-002 | implemented | Reviewed Core golden artifacts are generated from the executable compiler and canonical encoder, then checked for exact bytes and digest stability. | fixtures/core/canonical/README.md, xtask/src/goldens.rs | | FIXTURES-REQ-003 | implemented | Topic-shelf fixture references resolve to checked-in artifacts through the local contract graph. | xtask/src/contract_check.rs | -| FIXTURES-REQ-004 | gap | Target, lawpack, contract-bundle, admission, and conformance fixture families remain unpopulated until owning behavior lands. | fixtures/README.md, ROADMAP.md | +| FIXTURES-REQ-004 | gap | Runtime-owned target-profile, contract-bundle, admission, and conformance fixture families remain unpopulated until owning behavior lands. | fixtures/README.md, ROADMAP.md | | FIXTURES-REQ-005 | implemented | Reviewed authority-facts golden artifacts are generated from the validated JSON review input and executable canonical codec, then checked for exact bytes and digest stability. | fixtures/authority-facts/canonical/README.md, xtask/src/goldens.rs | | FIXTURES-REQ-006 | implemented | Reviewed Edict-owned target-profile contract resources are generated from the executable semantic model, then checked for exact canonical bytes and coordinate-framed digest stability. | fixtures/target-profile/contract-resources/README.md, xtask/src/goldens.rs | | FIXTURES-REQ-007 | implemented | The reviewed provider contract pack and manifest are generated from Edict-owned ABI schemas and canonical target-profile resources, then checked for exact bytes, root closure, and raw digest stability. | issue #161, docs/topics/providers/test-plan.md | +| FIXTURES-REQ-008 | implemented | The reviewed Hello Echo lawpack manifest and export bytes, exact digests, and digest-pinned source are generated or checked through the executable lawpack loader. | issue #169, docs/topics/lawpacks/test-plan.md | +| FIXTURES-REQ-009 | implemented | The portable causal-cell lawpack closure is generator-owned and checked as exact canonical manifest, exports, adapter, target-configuration, and digest artifacts after an executable compiler witness. | docs/topics/lawpacks/test-plan.md | ## Fixtures @@ -49,6 +53,8 @@ Out of scope: | fixtures/target-profile/contract-resources/README.md | Five reviewed Edict-owned target-profile contract resources and digest fixtures. | Every file matches the executable model and its coordinate-framed identity. | | fixtures/provider-contracts/v1/edict-provider-contracts.cddl | Reviewed self-contained provider CDDL pack. | Exact bytes match deterministic assembly from authoritative ABI sources. | | fixtures/provider-contracts/v1/manifest.json | Reviewed provider contract-pack manifest. | Exact bytes and all embedded schema/resource identities match deterministic assembly. | +| fixtures/lawpack/hello-echo/README.md | First reviewed canonical lawpack fixture and digest-pinned source. | `cargo xtask lawpack-goldens --check` reproduces exact bytes and digests; the public loader corroborates the source import digest. | +| fixtures/lawpack/causal-cell/README.md | Portable capability closure for external Edict applications. | `cargo xtask lawpack-goldens --check` validates, compiles through, and reproduces the exact closure. | ## Cases @@ -57,10 +63,12 @@ Out of scope: | FIXTURES-TP-001 | implemented | Source behavior | FIXTURES-REQ-001 | Source fixtures are consumed through public APIs and produce stable parser, validator, compiler, and highlighter behavior. | bounded_hello_parses, read_greeting_parses, conditional_blob_fixture_parses, palette_fixture_parses, phase1_fixtures_validate_semantically, bounded_hello_compiles_to_initial_core, highlight_source_emits_editor_roles_for_fixture | fixtures/lang/bounds/bounded-hello.edict, fixtures/lang/effects/conditional-blob.edict, fixtures/lang/effects/read-greeting.edict, fixtures/lang/types/color-match.edict, fixtures/lang/tooling/highlight-smoke.edict | Fixtures are behavior inputs, not prose anchors. | | FIXTURES-TP-002 | implemented | Golden artifact | FIXTURES-REQ-002 | Reviewed Core byte and digest fixtures exactly match executable compiler and encoder output. | reviewed_core_golden_bytes_match_executable_encoder, reviewed_core_digest_matches_exact_fixture | xtask/src/goldens.rs, fixtures/lang/bounds/bounded-hello.edict, fixtures/core/canonical/bounded-hello.core.cbor, fixtures/core/canonical/bounded-hello.core.sha256 | `cargo xtask core-goldens --check` covers the same artifact contract. | | FIXTURES-TP-003 | implemented | Contract graph | FIXTURES-REQ-003 | Topic-shelf test plans cannot cite missing fixture paths. | contract_graph_is_valid | xtask/src/contract_check.rs, xtask/src/tests.rs, fixtures/README.md | The checker validates referenced artifacts rather than prose. | -| FIXTURES-TP-004 | gap | Future corpus | FIXTURES-REQ-004 | No target, lawpack, bundle, admission, or conformance fixture corpus is claimed before owning behavior lands. | - | - | Add these families with the implementation slice that first consumes them. | +| FIXTURES-TP-004 | gap | Future corpus | FIXTURES-REQ-004 | No runtime-owned target-profile, contract-bundle, admission, or conformance fixture corpus is claimed before owning behavior lands. | - | - | Add these families with the implementation slice that first consumes them. | | FIXTURES-TP-005 | implemented | Golden artifact | FIXTURES-REQ-005 | Reviewed authority-facts byte and digest fixtures exactly match validated JSON input plus executable canonical codec output. | authority_facts_goldens_match_executable_codec | xtask/src/goldens.rs, fixtures/authority-facts/canonical/example-effectful.authority-facts.json, fixtures/authority-facts/canonical/example-effectful.authority-facts.cbor, fixtures/authority-facts/canonical/example-effectful.authority-facts.sha256 | `cargo xtask authority-facts-goldens --check` covers the same artifact contract. | | FIXTURES-TP-006 | implemented | Golden artifact | FIXTURES-REQ-006 | All five reviewed target-profile contract-resource bytes and digests exactly match the executable semantic model. | target_profile_resource_goldens_match_executable_contract | xtask/src/goldens.rs, fixtures/target-profile/contract-resources/README.md | `cargo xtask target-profile-resource-goldens --check` covers the same artifact contract. | | FIXTURES-TP-007 | implemented | Golden artifact | FIXTURES-REQ-007 | Provider contract-pack CDDL and manifest fixtures exactly match executable assembly, while check mode detects drift without modifying either file. | provider_contract_pack_goldens_match_executable_contract, provider_contract_pack_check_rejects_drift_without_rewriting | xtask/src/provider_contract_pack.rs, xtask/src/tests.rs, fixtures/provider-contracts/v1/edict-provider-contracts.cddl, fixtures/provider-contracts/v1/manifest.json | `cargo xtask provider-contract-pack --check` is the focused drift gate. | +| FIXTURES-TP-008 | implemented | Golden artifact | FIXTURES-REQ-008 | The Hello Echo lawpack loader accepts the exact checked-in manifest/export bytes, reproduces their digests, and proves the `.edict` import pins the manifest identity. | hello_echo_lawpack_bundle_loads_from_exact_canonical_resources | fixtures/lawpack/hello-echo/README.md, crates/edict-syntax/tests/lawpack.rs, xtask/src/lawpack_goldens.rs | `cargo xtask lawpack-goldens --check` is the focused drift gate. | +| FIXTURES-TP-009 | implemented | Golden artifact | FIXTURES-REQ-009 | The causal-cell generator validates the bundle and adapter, compiles and lowers a digest-pinned Edict witness, and reproduces the exact closure bytes and digest sidecars. | lawpack_goldens_match_executable_codec | fixtures/lawpack/causal-cell/README.md, xtask/src/lawpack_goldens.rs, xtask/src/tests.rs | The fixture is a portable capability, not an exact application provider. | ## Determinism Obligations @@ -74,7 +82,7 @@ Out of scope: ## Open Gaps -- Runtime-owned target-profile, lawpack, bundle, admission, and conformance +- Runtime-owned target-profile, contract-bundle, admission, and conformance fixture families remain future work. - Additional reviewed Core golden fixtures should be added as lowerable Core language coverage expands. diff --git a/docs/topics/lawpacks/README.md b/docs/topics/lawpacks/README.md index b5386b3..d5c5325 100644 --- a/docs/topics/lawpacks/README.md +++ b/docs/topics/lawpacks/README.md @@ -5,10 +5,11 @@ Status: current HEAD contract. This shelf describes the lawpack boundary that exists today. A lawpack is an external, digest-locked source of portable Edict semantics. Edict can parse lawpack imports, carry lawpack references through bundle and lowerability -contracts, reject unsupported v1 adapter claims, and load first compiler context -facts from authority-facts documents whose source kind is `lawpack`. It does not -yet validate full lawpack manifest instances or load complete lawpack export -surfaces. +contracts, validate the direct declarative adapter ABI, load first compiler +context facts from authority-facts documents whose source kind is `lawpack`, +and decode exact canonical lawpack manifests and export surfaces into an opaque +validated bundle. Complete supplied dependency sets are checked for exact +digest binding, missing dependencies, and cycles before use. Provider manifests can describe lawpacks as generated provider artifacts with digest-locked semantic-source and generator provenance. This validates only the @@ -30,16 +31,25 @@ The machine-readable lawpack manifest and export surface are specified in [`docs/abi/edict-lawpack.cddl`](../../abi/edict-lawpack.cddl), with explanatory reference material in [`docs/SPEC_edict-lawpack-abi-v1.md`](../../SPEC_edict-lawpack-abi-v1.md). -Those files are current design/reference material, not an executable validator. -[LAWPACKS-REQ-005] +`decode_lawpack_bundle` enforces the closed canonical shape and local semantic +obligations. `validate_lawpack_dependency_graph` validates the complete supplied +dependency set independent of input order. [LAWPACKS-REQ-005] The current executable Rust surfaces touching lawpacks are: - parser support for `ImportKind::Lawpack`; +- canonical manifest/export loading through `ValidatedLawpackBundle`; +- complete dependency-set validation with exact manifest-digest edges; +- canonical direct-adapter loading with exact target selection, adapter digest + corroboration, complete profile/effect/budget coverage, and typed target + configuration resource references; +- compiler and Target IR fact derivation from the exact + module/lawpack/adapter closure; +- reproducible canonical Core and Target IR artifacts for the standalone Hello + Echo crossing; - authority-facts loading for budget and effect write-class facts whose source identity is a digest-locked lawpack reference; -- target-profile validation that keeps the deferred - `accepted_lawpack_adapter_abi` slot empty for v1; +- target-profile validation for the exact `edict.lawpack-adapter/v1` ABI; - lowerability checks for digest-locked, one-hop direct adapter support; - contract-bundle manifest validation that can carry lawpack artifact references as participant-neutral resources. @@ -51,9 +61,29 @@ The current executable Rust surfaces touching lawpacks are: - Lawpack source imports require lexically valid digest review strings when a digest is present. Invalid digest strings reject at the parser boundary. [LAWPACKS-REQ-001] -- v1 target profiles do not yet accept a lawpack adapter ABI declaration. The - field exists for byte-level `edict.lawpack-adapter/v1` ABI ids once that ABI - is specified, and non-empty values reject until then. [LAWPACKS-REQ-003] +- Canonical manifests and export surfaces reject non-canonical bytes, missing + or unknown fields, malformed typed digests, substituted exports, invalid + discriminants, duplicate identities, unmappable failure names, opaque Edict + helper bodies, unbounded executable components, and runtime effects with no + target-adapter descriptor. The successful wrapper exposes typed values but + cannot be fabricated or mutated by callers. [LAWPACKS-REQ-005] +- Dependency validation resolves the complete supplied set by `(id, version)`, + detects cycles independent of input ordering, then corroborates every edge + against the exact resolved manifest digest. [LAWPACKS-REQ-005] +- v1 target profiles accept the exact `edict.lawpack-adapter/v1` identifier. + Unknown and duplicate declarations reject. [LAWPACKS-REQ-003] +- `decode_lawpack_adapter` accepts only canonical adapter bytes selected by one + exact digest-locked target descriptor. It requires exact operation-profile, + runtime-effect, budget, footprint, cost, and named-failure coverage before + returning an opaque validated adapter. Each effect also carries one typed, + digest-locked target-configuration reference. Edict preserves that reference + but does not interpret its target-owned semantics. + `prepare_lawpack_compilation` then derives compiler and Target IR facts + through the source import's exact alias and manifest digest. + [LAWPACKS-REQ-008] +- The Hello Echo golden generator compiles the exact source and lawpack closure, + lowers the resulting Core module, and pins canonical Core and Target IR bytes + under their native domain-framed identities. [LAWPACKS-REQ-009] - Lowerability may classify an operation as adapted when exactly one digest-locked direct adapter satisfies the required semantic effect, write class, and guard facts. Floating, chained, or ambiguous adapter claims reject @@ -74,13 +104,10 @@ The current executable Rust surfaces touching lawpacks are: The following are not implemented: -- full `edict.lawpack/v1` manifest file loading beyond authority-facts - documents; -- full `edict.lawpack/v1` CDDL instance validation; -- export-surface validation for pure functions, semantic effects, - obstructions, and operation profiles; -- dependency DAG validation; -- target adapter ABI validation; +- executable target-adapter component loading; v1 currently specifies and + implements the direct declarative adapter class only; +- target-owned configuration resource loading and interpretation; +- Echo executable-operation package emission from lowered Target IR; - lawpack conformance fixtures and two-lowerer differential trials. The verification matrix is tracked in [test-plan.md](./test-plan.md). diff --git a/docs/topics/lawpacks/test-plan.md b/docs/topics/lawpacks/test-plan.md index 3fbe1a7..5232b41 100644 --- a/docs/topics/lawpacks/test-plan.md +++ b/docs/topics/lawpacks/test-plan.md @@ -7,7 +7,19 @@ Status: current verification design for the lawpack boundary. In scope: - source-level lawpack import parsing and digest literal validation; -- v1 target-profile rejection of deferred lawpack adapter ABI declarations; +- canonical `edict.lawpack/v1` manifest and export-surface loading; +- digest binding between a manifest, its export surface, and source imports; +- closed validation of manifest dependencies, verifier classes, executable + component bounds, target-adapter descriptors, exports, semantic-effect + failures, obstruction schemas, and operation-profile optic templates; +- acyclic validation of a complete digest-locked lawpack dependency set; +- v1 target-profile acceptance of the exact direct lawpack adapter ABI and + rejection of unknown or duplicate declarations; +- canonical direct-adapter loading, digest corroboration, complete semantic + closure, typed target-configuration references, and compiler/Target IR fact + derivation; +- canonical compiler-produced Core and Target IR fixture bytes with their + native domain-framed identities; - lowerability behavior for one-hop digest-locked direct adapters; - contract-bundle handling of lawpack artifact references as external, participant-neutral resources. @@ -18,10 +30,9 @@ In scope: Out of scope: -- full `edict.lawpack/v1` manifest loading; -- lawpack export-surface validation; -- lawpack dependency DAG validation; -- target adapter ABI validation; +- executable target-adapter component loading; +- general or chained target-adapter composition beyond the direct declarative + `edict.lawpack-adapter/v1` ABI; - lawpack conformance fixtures and differential lowerer trials. - generating lawpacks from Wesley or runtime-owned semantic sources. @@ -31,11 +42,14 @@ Out of scope: | --- | --- | --- | --- | | LAWPACKS-REQ-001 | implemented | Source lawpack imports preserve kind, coordinate, version label, alias, and digest review string through the public parser. | docs/SPEC_edict-language-v1.md, crates/edict-syntax/src/parser.rs | | LAWPACKS-REQ-002 | implemented | v1 lowerability supports at most one digest-locked direct adapter per semantic effect and rejects floating, chained, or ambiguous adapter claims. | crates/edict-syntax/src/lowerability.rs | -| LAWPACKS-REQ-003 | implemented | v1 target-profile validation rejects non-empty `accepted_lawpack_adapter_abi` declarations until the adapter ABI is supported. | crates/edict-syntax/src/target_profile.rs | +| LAWPACKS-REQ-003 | implemented | v1 target-profile validation accepts an absent/empty adapter ABI list or exactly `["edict.lawpack-adapter/v1"]` and rejects unknown or duplicate declarations. | crates/edict-syntax/src/target_profile.rs | | LAWPACKS-REQ-004 | implemented | Contract-bundle validation treats lawpacks as external participant-neutral artifact references, not loaded or executed manifests. | crates/edict-syntax/src/contract_bundle.rs | -| LAWPACKS-REQ-005 | gap | The `edict.lawpack/v1` CDDL manifest and export surface have no executable instance validator yet. | docs/abi/edict-lawpack.cddl, docs/SPEC_edict-lawpack-abi-v1.md | +| LAWPACKS-REQ-005 | implemented | Edict loads canonical `edict.lawpack/v1` manifests and export surfaces into typed values, rejects every value outside the closed CDDL shape with stable failure kinds, corroborates the export digest, and validates a complete supplied dependency set as digest-locked and acyclic before exposing any exports to compilation. | issue #169, crates/edict-syntax/src/lawpack.rs, docs/abi/edict-lawpack.cddl, docs/abi/edict-common.cddl, docs/abi/edict-core.cddl | | LAWPACKS-REQ-006 | implemented | Authority-facts loading accepts digest-locked `lawpack` source identity for first compiler budget and effect write-class facts without claiming full manifest validation. | docs/topics/authority-facts/test-plan.md | | LAWPACKS-REQ-007 | implemented | Provider manifests model lawpacks as generated provider artifacts with digest-locked semantic source and generator provenance; Edict validates the reference/provenance envelope without owning runtime lawpack semantics. | issue #139, docs/topics/providers/test-plan.md | +| LAWPACKS-REQ-008 | implemented | Edict validates one exact direct declarative `edict.lawpack-adapter/v1` resource selected by a loaded lawpack manifest, requires complete profile/effect/budget coverage plus one typed target-configuration resource reference per runtime effect, and corroborates every exported footprint, cost, and named-failure obligation before deriving compiler or target facts. Edict preserves but does not interpret target-owned configuration semantics. | issue #169, docs/abi/edict-lawpack-adapter.cddl | +| LAWPACKS-REQ-009 | implemented | The standalone Hello Echo fixture pins exact canonical Core and Target IR bytes produced from the digest-locked source/lawpack/adapter closure and computes each identity with the artifact's native domain. | issue #169, fixtures/lawpack/hello-echo/README.md, xtask/src/lawpack_goldens.rs | +| LAWPACKS-REQ-010 | implemented | The portable `causal.cell@1.createIfAbsent` capability closure is generated through the executable lawpack, adapter, compiler, and Target IR path, with exact canonical manifest, export, adapter, and target-configuration bytes and digests for external application builds. | fixtures/lawpack/causal-cell/README.md, xtask/src/lawpack_goldens.rs | ## Fixtures @@ -43,6 +57,8 @@ Out of scope: | --- | --- | --- | | fixtures/lang/bounds/bounded-hello.edict | Lawpack import source fixture. | Parser preserves the `hello.optics@1` lawpack import and digest review string. | | fixtures/lang/effects/read-greeting.edict | Multi-import source fixture. | Parser preserves shape, lawpack, and target imports for effect-call syntax. | +| fixtures/lawpack/hello-echo/README.md | Standalone capability fixture for the first real Edict-to-Echo crossing. | Canonical manifest, exports, and adapter load with exact digests; exact source compiles to pinned canonical Core and Target IR; `createGreeting` exposes a bounded create effect and typed `AlreadyExists` failure without GraphQL or a handwritten Echo package. | +| fixtures/lawpack/causal-cell/README.md | Portable capability closure for external application builds. | `cargo xtask lawpack-goldens --check` reproduces the exact canonical closure after validating the bundle and adapter and compiling a source witness through Target IR. | ## Cases @@ -50,11 +66,14 @@ Out of scope: | --- | --- | --- | --- | --- | --- | --- | --- | | LAWPACKS-TP-001 | implemented | Source import | LAWPACKS-REQ-001 | Lawpack imports preserve version labels and valid digest strings, and invalid digest strings reject with a stable parser error kind. | bounded_hello_parses, read_greeting_parses, import_versions_preserve_underscore_labels, import_digest_literals_are_validated | fixtures/lang/bounds/bounded-hello.edict, fixtures/lang/effects/read-greeting.edict | Tests use the public parser and AST/error contract. | | LAWPACKS-TP-002 | implemented | Lowerability | LAWPACKS-REQ-002 | Exactly one digest-locked direct adapter can satisfy a v1 semantic effect; floating, chained, and ambiguous adapters reject. | one_direct_adapter_satisfies_v1_lowering_requirements, v1_rejects_floating_direct_adapter_claims, v1_rejects_chained_adapter_claims, v1_rejects_ambiguous_direct_adapters | - | Tests assert lowerability classification and stable failure kinds. | -| LAWPACKS-TP-003 | implemented | Target profile | LAWPACKS-REQ-003 | A non-empty lawpack adapter ABI declaration rejects from v1 target-profile conformance. | deferred_lawpack_adapter_abi_must_stay_empty_in_v1 | - | Keeps the future adapter slot from becoming an implicit claim. | +| LAWPACKS-TP-003 | implemented | Target profile | LAWPACKS-REQ-003 | The exact direct adapter ABI is accepted; unknown and duplicate declarations reject from v1 target-profile conformance. | direct_lawpack_adapter_abi_is_supported_in_v1, unknown_or_duplicate_lawpack_adapter_abis_are_rejected | - | Keeps target compatibility closed over the implemented ABI. | | LAWPACKS-TP-004 | implemented | Contract bundle | LAWPACKS-REQ-004 | Runtime-neutral bundles can carry lawpack artifact references, and lawpacks remain optional artifact-list entries. | echo_and_kv_bundles_validate_with_the_same_runtime_neutral_contract, optional_artifact_lists_may_be_empty | - | Contract-bundle validation does not load lawpack manifests. | -| LAWPACKS-TP-005 | gap | Manifest validation | LAWPACKS-REQ-005 | No executable lawpack manifest instance validator is claimed. | - | - | Add with lawpack loading or schema-validation work. | +| LAWPACKS-TP-005 | implemented | Manifest validation | LAWPACKS-REQ-005 | The Hello Echo bundle loads from canonical bytes; non-canonical or malformed values, unknown or missing fields, digest substitution, invalid identifiers or discriminants, unbounded executable components, runtime effects without target adapters, duplicate identities, missing dependencies, digest conflicts, and dependency cycles reject with stable failure kinds before exports become compiler facts. | hello_echo_lawpack_bundle_loads_from_exact_canonical_resources, all_hash_bound_helper_and_verifier_variants_load, noncanonical_manifest_bytes_reject_before_shape_validation, export_digest_substitution_rejects, runtime_effect_requires_at_least_one_target_adapter, effect_failure_names_must_be_source_mappable_identifiers, dependency_graph_requires_the_complete_exact_set_independent_of_input_order, dependency_graph_rejects_missing_and_substituted_manifests, dependency_graph_rejects_cycles_before_digest_corroboration | fixtures/lawpack/hello-echo/README.md, crates/edict-syntax/tests/lawpack.rs, xtask/src/lawpack_goldens.rs | `cargo xtask lawpack-goldens --check` reproduces exact manifest/export bytes and digests without rewriting them. | | LAWPACKS-TP-006 | implemented | Authority facts | LAWPACKS-REQ-006 | A lawpack-sourced authority-facts file can provide budget and effect write-class facts consumed by the compiler. | file_backed_authority_facts_compile_bounded_hello, file_backed_authority_facts_reject_write_effect_profile_mismatch | crates/edict-syntax/tests/authority_facts.rs | Asserts compiler behavior, not manifest prose. | | LAWPACKS-TP-007 | implemented | Provider provenance | LAWPACKS-REQ-007 | A provider manifest fixture can carry a generated lawpack artifact with digest-locked semantic source and generator provenance, while unlocked artifact/provenance references reject with stable provider validation failures. | generated_provider_manifest_fixture_validates, provider_manifest_rejects_unlocked_generated_artifact, provider_manifest_rejects_unlocked_generated_provenance, provider_manifest_rejects_unlocked_generator_provenance | fixtures/providers/echo-generated/provider-manifest.json, crates/edict-syntax/tests/provider.rs | Provider validation is envelope/provenance validation only; no Echo semantics are interpreted. | +| LAWPACKS-TP-008 | implemented | Direct adapter | LAWPACKS-REQ-008 | The exact Hello Echo adapter selected by the manifest derives all compiler and Echo Target IR facts and exposes the exact target-configuration resource identity, while missing, substituted, non-canonical, incomplete, target-mismatched, import-mismatched, malformed-configuration, undeclared-write-class, or obligation-mismatched adapters fail closed before trusted compiler facts exist. | hello_echo_source_compiles_to_echo_target_ir_from_exact_lawpack_adapter, lawpack_adapter_bytes_must_be_canonical_and_digest_bound, lawpack_adapter_requires_a_typed_target_configuration_reference, lawpack_adapter_rejects_an_undeclared_write_class_at_the_effect_path, lawpack_adapter_selection_requires_one_exact_target_profile, lawpack_adapter_requires_complete_exported_effect_coverage, lawpack_adapter_corroborates_footprint_cost_and_failure_obligations, lawpack_compilation_requires_the_exact_digest_locked_source_import | fixtures/lawpack/hello-echo/README.md, crates/edict-syntax/tests/lawpack.rs | The positive test constructs no `CompilerContext` or `TargetIrLoweringFacts`; Echo-specific configuration interpretation remains outside Edict. | +| LAWPACKS-TP-009 | implemented | Compiler artifacts | LAWPACKS-REQ-009 | Compiling and lowering the exact Hello Echo closure reproduces the reviewed Core and Target IR bytes and their native domain-framed identities. | hello_echo_source_compiles_to_echo_target_ir_from_exact_lawpack_adapter | fixtures/lawpack/hello-echo/create-greeting.core.cbor, fixtures/lawpack/hello-echo/create-greeting.target-ir.cbor, crates/edict-syntax/tests/lawpack.rs, xtask/src/lawpack_goldens.rs | The fixtures are outputs of the real compiler pipeline, not handwritten substitutes; `cargo xtask lawpack-goldens --check` reproduces them. | +| LAWPACKS-TP-010 | implemented | Portable capability | LAWPACKS-REQ-010 | Generating the causal-cell closure validates its canonical lawpack and direct adapter, then compiles and lowers an Edict source witness that imports the exact generated manifest digest. | lawpack_goldens_match_executable_codec | fixtures/lawpack/causal-cell/README.md, xtask/src/lawpack_goldens.rs, xtask/src/tests.rs | The generator fails if the portable capability no longer reaches a compiler-produced Target IR artifact. | ## Determinism Obligations @@ -63,9 +82,15 @@ Out of scope: internal branch choices. - Contract-bundle tests must assert validation behavior, not the text of the lawpack ABI specification. +- Manifest tests must enter through exact canonical bytes and assert typed + exports or stable failure kinds. Constructing a typed manifest directly does + not prove the loader boundary. +- Dependency validation must be input-order invariant and must bind each edge + to the exact supplied manifest digest. ## Open Gaps -- No executable full lawpack manifest validator exists. -- No checked-in lawpack fixture corpus exists. -- No lawpack target adapter ABI is accepted in v1 target-profile manifests. +- No executable target-adapter component is loaded or semantically verified by + Edict; v1 implements only the direct declarative adapter ABI. +- Runtime admission and execution of compiler-emitted packages remain outside + Edict. diff --git a/docs/topics/providers/test-plan.md b/docs/topics/providers/test-plan.md index 3a8ea6f..4e54f2c 100644 --- a/docs/topics/providers/test-plan.md +++ b/docs/topics/providers/test-plan.md @@ -141,7 +141,7 @@ Out of scope: | PROVIDERS-TP-045 | implemented | Drift recovery | PROVIDERS-REQ-030 | A contract-pack drift failure identifies the exact supported regeneration command. | provider_contract_pack_check_rejects_drift_without_rewriting | xtask/src/provider_contract_pack.rs, xtask/src/tests.rs | The diagnostic does not direct users to the unrelated `*-goldens` command family. | | PROVIDERS-TP-046 | implemented | Schema closure | PROVIDERS-REQ-030 | A CDDL control whose nested rule graph cannot be inspected through the validator API rejects with a distinct stable unsupported-control failure rather than masquerading as an unresolved external rule. | assembly_rejects_uninspectable_schema_controls_explicitly | crates/edict-provider-schema/tests/provider_contract_pack.rs | The pack does not claim self-containment when dependency visibility prevents closure inspection. | | PROVIDERS-TP-047 | implemented | Internal invariant | PROVIDERS-REQ-030 | Static contract and domain binding tables must remain strictly sorted and unique by lookup key before they can back binary-search admission. | static_binding_guard_rejects_unsorted_and_duplicate_keys | crates/edict-provider-schema/src/contract_pack.rs | A future table edit fails immediately rather than silently changing lookup behavior. | -| PROVIDERS-TP-048 | implemented | Guarded recursive schemas | PROVIDERS-REQ-020, PROVIDERS-REQ-031 | Productive self and mutual recursion through map keys, map values, or array elements, including the published `core-module` root, constructs and validates finite instances. Required singleton text discriminators select exactly one recursive arm regardless of encoded key order. Missing, unknown, duplicate, same-valued, or overlapping discriminators; recursive variable-member backtracking; unsupported array/map assignments; invalid nested children; and values beyond the exact depth limit fail closed. Exact native `.regexp` map-key semantics survive specialization. | construction_accepts_guarded_recursive_roots, recursive_map_discriminator_dispatch_is_independent_of_encoded_key_order, recursive_map_discriminator_dispatch_rejects_invalid_discriminators, recursive_dispatch_preserves_native_regexp_map_key_semantics, construction_rejects_ambiguous_recursive_choice_shapes, guarded_recursive_validation_is_bounded_and_rejects_invalid_descendants, specialization_selects_one_arm_and_eliminates_rule_reentry, unknown_tag_does_not_specialize_a_recursive_child | crates/edict-provider-schema/src/recursive_dispatch.rs, crates/edict-provider-schema/tests/registry.rs, fixtures/provider-contracts/v1/edict-provider-contracts.cddl | Edict looks up the discriminator before whole-value traversal, proves through a visit counter that unselected deep children are not entered, and specializes a finite rule containing only selected arms. Construction refuses any shape for which specialization could narrow native validity, and the finished rule may contain no rule reference. Canonical duplicate rejection and the exact depth gate still precede native validation. No declaration-order, encoded-map-order, private work-ceiling, or reimplemented-regexp assumption is authority. | +| PROVIDERS-TP-048 | implemented | Guarded recursive schemas | PROVIDERS-REQ-020, PROVIDERS-REQ-031 | Productive self and mutual recursion through map keys, map values, or array elements, including the published `core-module` root, constructs and validates finite instances. Required singleton text discriminators select exactly one recursive arm regardless of encoded key order. A two-arm recursive map choice may instead dispatch on one exact required text key that the other closed arm cannot accept, including the checked closed-versus-legacy `target-ir-artifact` union. Missing, unknown, duplicate, same-valued, optional, wildcard, or otherwise overlapping discriminators; recursive variable-member backtracking; unsupported array/map assignments; invalid nested children; and values beyond the exact depth limit fail closed. Exact native `.regexp` map-key semantics survive specialization. | construction_accepts_guarded_recursive_roots, recursive_map_discriminator_dispatch_is_independent_of_encoded_key_order, recursive_required_key_dispatch_accepts_compatible_untagged_arms, checked_target_ir_root_constructs_through_required_key_dispatch, recursive_map_discriminator_dispatch_rejects_invalid_discriminators, recursive_dispatch_preserves_native_regexp_map_key_semantics, construction_rejects_ambiguous_recursive_choice_shapes, guarded_recursive_validation_is_bounded_and_rejects_invalid_descendants, specialization_selects_one_arm_and_eliminates_rule_reentry, unknown_tag_does_not_specialize_a_recursive_child | crates/edict-provider-schema/src/recursive_dispatch.rs, crates/edict-provider-schema/tests/registry.rs, fixtures/provider-contracts/v1/edict-provider-contracts.cddl | Edict looks up the discriminator before whole-value traversal, proves through a visit counter that unselected deep children are not entered, and specializes a finite rule containing only selected arms. Required-key dispatch is limited to exact text-key maps with no flexible-key ambiguity. Construction refuses any shape for which specialization could narrow native validity, and the finished rule may contain no rule reference. Canonical duplicate rejection and the exact depth gate still precede native validation. No declaration-order, encoded-map-order, private work-ceiling, or reimplemented-regexp assumption is authority. | ## Determinism Obligations diff --git a/docs/topics/target-ir/test-plan.md b/docs/topics/target-ir/test-plan.md index f6e4c24..7b6e8f0 100644 --- a/docs/topics/target-ir/test-plan.md +++ b/docs/topics/target-ir/test-plan.md @@ -52,6 +52,7 @@ Out of scope: | TIR-REQ-013 | implemented | Explicit built-in Echo and git-warp lowerer adapters preserve direct Target IR reports, artifacts, canonical bytes, digests, and structured target-lowering failures. | issue #140, docs/design/provider-artifact-pipeline-alpha.md | | TIR-REQ-014 | implemented | `edict-target-ir.cddl` defines `target-ir-artifact` from the executable canonical value shape and is published in the self-contained provider contract pack. | issue #161, EDICT-ABI-PROVIDER-CONTRACT-PACK-001 | | TIR-REQ-015 | implemented | Target IR preserves an explicit Core basis expression and, when an operation carries an explicit basis or lawpack import, binds a semantic closure containing the exact source Core digest and the sorted digest-locked lawpack resource set. Existing no-basis/no-lawpack Target IR bytes remain unchanged. | docs/SPEC_edict-language-v1.md, docs/design/canonical-target-ir-v0.11.md | +| TIR-REQ-016 | implemented | A validated direct lawpack adapter derives the exact target profile, Target IR domain, operation-profile support, effect intrinsic, and named-failure support used for Target IR lowering; callers do not hand-assemble `TargetIrLoweringFacts`. | issue #169, docs/abi/edict-lawpack-adapter.cddl | ## Fixtures @@ -103,6 +104,7 @@ Out of scope: | TIR-TP-031 | implemented | Integration | TIR-REQ-001, TIR-REQ-015 | Compiling and lowering the operation prerequisite fixture preserves the exact Core basis expression and binds the computed Core digest plus exact lawpack coordinate/digest in Target IR; an empty Core coordinate, unidentifiable or noncanonical resource, coordinate conflict, internally contradictory closure, or missing closure on a basis-bearing artifact refuses without an artifact identity. The canonical encoder and published CDDL root reject an empty source Core coordinate, and the CDDL root independently rejects the externally constructed missing-closure shape. | operation_prerequisite_fixture_preserves_fixed_width_basis_and_lawpack_closure, target_lowering_refuses_an_unidentifiable_semantic_closure, legacy_target_ir_encoder_rejects_an_empty_source_core_coordinate, invalid_or_conflicting_lawpack_resources_refuse_before_target_artifact, semantic_closure_cannot_substitute_a_different_source_core_coordinate, explicit_basis_target_ir_cannot_drop_its_semantic_closure, target_ir_root_matches_reference_encoder | fixtures/lang/operations/explicit-basis-u64.edict | The target artifact binds semantic inputs but remains distinct from package admission or runtime execution. | | TIR-TP-032 | implemented | Mutation sensitivity | TIR-REQ-008, TIR-REQ-015 | Changing the basis expression, source Core meaning, lawpack coordinate, or lawpack digest moves the Target IR digest, while equivalent lawpack construction order does not. | explicit_basis_and_semantic_input_mutations_move_target_identity, semantic_closure_lawpack_set_is_order_invariant | fixtures/lang/operations/explicit-basis-u64.edict | Prevents Target IR identity from floating over operation semantic inputs. | | TIR-TP-033 | implemented | Compatibility | TIR-REQ-009, TIR-REQ-015 | Existing reviewed Echo and git-warp fixtures with `basis none` and no lawpack import retain byte-identical Target IR and digest goldens. | target_ir_goldens_match_executable_encoder | fixtures/target-ir/canonical/echo-effectful.target-ir.cbor, fixtures/target-ir/canonical/gitwarp-append.target-ir.cbor | Provider-v1 compatibility artifacts remain stable alongside the new operation prerequisites. | +| TIR-TP-034 | implemented | Integration | TIR-REQ-001, TIR-REQ-002, TIR-REQ-015, TIR-REQ-016 | Hello Echo lowers from exact source and lawpack resources through the selected direct adapter into `echo.span-ir/v1`, preserving the explicit basis, create effect, create-if-absent intrinsic, typed failure arm, and semantic closure. | hello_echo_source_compiles_to_echo_target_ir_from_exact_lawpack_adapter | fixtures/lawpack/hello-echo/README.md | Target IR remains distinct from the later Echo executable package. | ## Determinism Obligations diff --git a/docs/topics/target-profiles/README.md b/docs/topics/target-profiles/README.md index 96de943..6a88f4d 100644 --- a/docs/topics/target-profiles/README.md +++ b/docs/topics/target-profiles/README.md @@ -55,7 +55,7 @@ The canonical artifact shape for `edict.target-profile/v1` is named in - `TargetProfileManifest` records profile identity, accepted Core ABI, intrinsic namespace, every required digest-locked manifest component, - canonical encoding rules, deferred lawpack-adapter ABI entries, diagnostics, + canonical encoding rules, direct lawpack-adapter ABI entries, diagnostics, v1 application doctrine, deterministic execution, and conformance fixture corpus references. [TPROF-REQ-001] - Conformance is runtime-neutral. `echo.dpo@1` and `kv.transactional@1` shaped @@ -66,8 +66,10 @@ The canonical artifact shape for `edict.target-profile/v1` is named in coordinate and valid `sha256:<64 hex>` digest review rendering. [TPROF-REQ-003] - A conforming profile must accept `edict.core/v1`. [TPROF-REQ-004] -- `acceptedLawpackAdapterAbi` remains empty in v1 until the byte-level - `edict.lawpack-adapter/v1` ABI is specified. [TPROF-REQ-005] +- `acceptedLawpackAdapterAbi` is absent/empty for profiles that do not consume + lawpack adapters or exactly `["edict.lawpack-adapter/v1"]` for profiles that + accept the direct declarative ABI. Unknown and duplicate claims reject. + [TPROF-REQ-005] - `multiTarget` remains false in v1 conformance until composite profile validation exists. [TPROF-REQ-006] - The v1 application doctrine accepted by the checker is atomic application, diff --git a/docs/topics/target-profiles/test-plan.md b/docs/topics/target-profiles/test-plan.md index 77138a8..90084c3 100644 --- a/docs/topics/target-profiles/test-plan.md +++ b/docs/topics/target-profiles/test-plan.md @@ -11,7 +11,7 @@ In scope: - runtime-neutral acceptance of Echo and non-Echo target-profile shapes; - digest-locked manifest component references; - accepted Core ABI requirements; -- deferred lawpack-adapter ABI emptiness; +- exact direct lawpack-adapter ABI compatibility; - v1 atomic application doctrine. - authority-facts documents whose source kind is `targetProfile` for first compiler operation-profile facts. @@ -45,7 +45,7 @@ Out of scope: | TPROF-REQ-002 | implemented | Target-profile conformance is runtime-neutral: Echo-shaped and KV-shaped profiles are checked by the same obligations without requiring graph/runtime-specific nouns. | issue #1 | | TPROF-REQ-003 | implemented | Normative manifest component references must be digest-locked by non-empty coordinate and valid `sha256:<64 hex>` digest review renderings. | docs/abi/edict-target-profile.cddl | | TPROF-REQ-004 | implemented | A conforming v1 target profile must accept `edict.core/v1`. | docs/abi/edict-target-profile.cddl | -| TPROF-REQ-005 | implemented | `acceptedLawpackAdapterAbi` is rejected when non-empty until the byte-level adapter ABI is specified. | EDICT-ABI-LAWPACK-ADAPTER-DEFER-001 | +| TPROF-REQ-005 | implemented | `acceptedLawpackAdapterAbi` is absent/empty or exactly `["edict.lawpack-adapter/v1"]`; unknown and duplicate claims reject. | EDICT-ABI-LAWPACK-ADAPTER-001 | | TPROF-REQ-006 | implemented | `multiTarget: true` is rejected until composite profile validation exists. | ROADMAP.md | | TPROF-REQ-007 | implemented | V1 conformance requires atomic application, application-snapshot reads, precommit-atomic guard evaluation, and no-visible-effects obstruction rollback. | docs/SPEC_edict-target-profile-abi-v1.md | | TPROF-REQ-008 | implemented | Authority-facts loading accepts digest-locked `targetProfile` source identity for first compiler operation-profile facts without claiming full manifest loading. | docs/topics/authority-facts/test-plan.md | @@ -69,7 +69,7 @@ Out of scope: | TPROF-TP-001 | implemented | Golden path | TPROF-REQ-001, TPROF-REQ-002 | Echo-shaped and KV-shaped manifests both return `TargetProfileConformanceStatus::Conformant` with no failures. | echo_and_kv_profiles_conform_to_the_same_runtime_neutral_manifest_contract | crates/edict-syntax/tests/target_profile.rs | Proves the checker is not Echo-specific. | | TPROF-TP-002 | implemented | Boundary guard | TPROF-REQ-003 | Removing or malforming the verifier digest returns `NonConformant` with `NonDigestLockedResource` on the `verifier` field. | missing_digest_on_normative_manifest_slot_is_rejected, malformed_digest_on_normative_manifest_slot_is_rejected | crates/edict-syntax/tests/target_profile.rs | Stable failure kind and field, not prose. | | TPROF-TP-003 | implemented | Boundary guard | TPROF-REQ-004 | Removing `edict.core/v1` from accepted Core ABI returns `MissingAcceptedCoreAbi`. | accepted_core_abi_must_include_v1_core | crates/edict-syntax/tests/target_profile.rs | Ensures target profiles declare the Core contract they accept. | -| TPROF-TP-004 | implemented | Boundary guard | TPROF-REQ-005 | Adding `edict.lawpack-adapter/v1` before that ABI is specified returns `DeferredLawpackAdapterAbiUnsupported`. | deferred_lawpack_adapter_abi_must_stay_empty_in_v1 | crates/edict-syntax/tests/target_profile.rs | Keeps adapter ABI claims out of this release. | +| TPROF-TP-004 | implemented | Compatibility | TPROF-REQ-005 | The exact direct adapter ABI is accepted; unknown and duplicate ABI claims return `UnsupportedLawpackAdapterAbi`. | direct_lawpack_adapter_abi_is_supported_in_v1, unknown_or_duplicate_lawpack_adapter_abis_are_rejected | crates/edict-syntax/tests/target_profile.rs | Keeps target claims closed over the one implemented ABI. | | TPROF-TP-005 | implemented | Boundary guard | TPROF-REQ-006 | Setting `multiTarget` true returns `UnsupportedCompositeProfile`. | multi_target_profiles_are_rejected_until_composite_validation_exists | crates/edict-syntax/tests/target_profile.rs | Prevents unvalidated composite profiles from passing v1 conformance. | | TPROF-TP-006 | implemented | Boundary guard | TPROF-REQ-007 | Non-atomic application doctrine returns stable failure kinds for application model, read consistency, guard evaluation, and rollback. | atomic_application_semantics_are_required_for_v1_conformance | crates/edict-syntax/tests/target_profile.rs | Asserts structured behavior only. | | TPROF-TP-007 | implemented | Authority facts | TPROF-REQ-008 | A target-profile-sourced authority-facts file can provide operation-profile facts consumed by the compiler. | file_backed_authority_facts_compile_bounded_hello, file_backed_authority_facts_reject_write_effect_profile_mismatch | crates/edict-syntax/tests/authority_facts.rs | Asserts compiler behavior, not manifest prose. | diff --git a/fixtures/README.md b/fixtures/README.md index 1bbfc21..470e3e0 100644 --- a/fixtures/README.md +++ b/fixtures/README.md @@ -44,6 +44,8 @@ authority-facts canonical fixture lives in [`authority-facts/canonical/`](./authority-facts/canonical/). Additional Edict-owned target-profile contract resources live in [`target-profile/contract-resources/`](./target-profile/contract-resources/). +The first canonical lawpack manifest/export fixture and its digest-pinned Edict +source live in [`lawpack/hello-echo/`](./lawpack/hello-echo/). The Rust-neutral provider contract pack and its exact manifest live in [`provider-contracts/v1/`](./provider-contracts/v1/). Additional source coverage, relapse-zoo cases, runtime-owned target fixtures, diff --git a/fixtures/lawpack/causal-cell/README.md b/fixtures/lawpack/causal-cell/README.md new file mode 100644 index 0000000..170d778 --- /dev/null +++ b/fixtures/lawpack/causal-cell/README.md @@ -0,0 +1,37 @@ +# Causal Cell Lawpack Fixture + +This generator-owned fixture is the portable capability closure currently used +by standalone Edict applications that target Echo: + +```text +causal.cell@1.createIfAbsent +``` + +The fixture is not a Hello Echo provider. Application coordinates and operation +names remain in external Edict source; this closure owns only the portable +create-if-absent capability and its typed `AlreadyExists` obstruction. + +- `manifest.cbor` and `manifest.sha256` bind the canonical + `edict.lawpack/v1` manifest. +- `exports.cbor` and `exports.sha256` bind the portable capability surface. +- `adapter.cbor` and `adapter.sha256` bind the direct declarative Echo adapter. +- `echo-operation-configuration.cbor` and its digest sidecar bind the generic + Echo operation-lowering configuration. + +Regenerate only through: + +```sh +cargo xtask lawpack-goldens --write +``` + +Check without modifying reviewed bytes: + +```sh +cargo xtask lawpack-goldens --check +``` + +Before emitting these artifacts, the generator validates the lawpack bundle and +adapter, constructs an Edict source witness that imports the exact manifest +digest, compiles it to Core, and requires successful Target IR lowering. The +source witness is deliberately not published as fixture authority; it proves +the generated portable closure remains usable by a real Edict application. diff --git a/fixtures/lawpack/causal-cell/adapter.cbor b/fixtures/lawpack/causal-cell/adapter.cbor new file mode 100644 index 0000000..3b0c8b1 Binary files /dev/null and b/fixtures/lawpack/causal-cell/adapter.cbor differ diff --git a/fixtures/lawpack/causal-cell/adapter.sha256 b/fixtures/lawpack/causal-cell/adapter.sha256 new file mode 100644 index 0000000..f8231bc --- /dev/null +++ b/fixtures/lawpack/causal-cell/adapter.sha256 @@ -0,0 +1 @@ +sha256:0b9f28a9ba9a8417a110896ec91040967dfcb2c928eb90379c2550eefae40537 diff --git a/fixtures/lawpack/causal-cell/echo-operation-configuration.cbor b/fixtures/lawpack/causal-cell/echo-operation-configuration.cbor new file mode 100644 index 0000000..24527d3 Binary files /dev/null and b/fixtures/lawpack/causal-cell/echo-operation-configuration.cbor differ diff --git a/fixtures/lawpack/causal-cell/echo-operation-configuration.sha256 b/fixtures/lawpack/causal-cell/echo-operation-configuration.sha256 new file mode 100644 index 0000000..d293c8a --- /dev/null +++ b/fixtures/lawpack/causal-cell/echo-operation-configuration.sha256 @@ -0,0 +1 @@ +sha256:32cb7ff96fd86ce9f038f29faa02ce079fd0ffd068093c9d0ca75748313aca87 diff --git a/fixtures/lawpack/causal-cell/exports.cbor b/fixtures/lawpack/causal-cell/exports.cbor new file mode 100644 index 0000000..ecf797b --- /dev/null +++ b/fixtures/lawpack/causal-cell/exports.cbor @@ -0,0 +1 @@ +¦etypes€geffectsªiinputTypexcausal.cell@1.CreateInputjcoordinatexcausal.cell@1.createIfAbsentjoutputTypexcausal.cell@1.CreateReceiptlguardSupportõncostObligationxcausal.cell@1.smallCreateBudgetneffectFailures¡malreadyExists¢kpayloadTypexcausal.cell@1.ExistingValuenauthorityClassndomainMappableneffectKindHintfcreatenexecutionClassgruntimentypeParameters€sfootprintObligationxcausal.cell@1.cellKeyFootprinticonstants€lobstructions£jcoordinatexcausal.cell@1.AlreadyExistsmpayloadSchemaxcausal.cell@1.ExistingValuenauthorityClassndomainMappablempureFunctions€qoperationProfiles¡xcausal.cell@1.createIfAbsent¢mopticTemplate¥iopticKindsaffectReintegrationlboundaryKindfaffectmsupportPolicyxcausal.cell@1.directSupportolossDispositionvcausal.cell@1.losslesssapertureRequirement¢crefxcausal.cell@1.cellKeyFootprintdkindxabstractFootprintObligationoeffectPredicatex"causal.cell@1.createIfAbsentEffect \ No newline at end of file diff --git a/fixtures/lawpack/causal-cell/exports.sha256 b/fixtures/lawpack/causal-cell/exports.sha256 new file mode 100644 index 0000000..76f995c --- /dev/null +++ b/fixtures/lawpack/causal-cell/exports.sha256 @@ -0,0 +1 @@ +sha256:d44357cad98852033193b65f1517f0efbdf95cfdfe0ee81261a87b580af98a63 diff --git a/fixtures/lawpack/causal-cell/manifest.cbor b/fixtures/lawpack/causal-cell/manifest.cbor new file mode 100644 index 0000000..62992de Binary files /dev/null and b/fixtures/lawpack/causal-cell/manifest.cbor differ diff --git a/fixtures/lawpack/causal-cell/manifest.sha256 b/fixtures/lawpack/causal-cell/manifest.sha256 new file mode 100644 index 0000000..e9b9a91 --- /dev/null +++ b/fixtures/lawpack/causal-cell/manifest.sha256 @@ -0,0 +1 @@ +sha256:42d96b95089ff634ab104274aa5e269c8088af8e311daa5826bd0436871f2590 diff --git a/fixtures/lawpack/hello-echo/README.md b/fixtures/lawpack/hello-echo/README.md new file mode 100644 index 0000000..ae01977 --- /dev/null +++ b/fixtures/lawpack/hello-echo/README.md @@ -0,0 +1,47 @@ +# Hello Echo Lawpack Fixture + +This is the first reviewed `edict.lawpack/v1` fixture. It exists to expose the +next real Edict-to-Echo crossing without GraphQL, native application callbacks, +a fake transport, or a handwritten Echo executable package. + +- `manifest.cbor` is the canonical lawpack manifest. +- `manifest.sha256` is its `edict.lawpack/v1` domain-framed identity. +- `exports.cbor` is the canonical export surface. +- `exports.sha256` is its `hello.echo.exports/v1` domain-framed identity. +- `adapter.cbor` is the canonical direct declarative Echo target adapter. +- `adapter.sha256` is its manifest-bound domain-framed identity. +- `echo-operation-configuration.cbor` is the target-owned, adapter-bound + program, budget, authority, and invocation-binding configuration that Echo + may interpret. +- `echo-operation-configuration.sha256` is its + `hello.echo.echo-operation-configuration/v1` domain-framed identity. +- `create-greeting.edict` imports the exact manifest digest and declares the + bounded `createGreeting` action with typed `AlreadyExists` mapping. +- `create-greeting.core.cbor` is the canonical Core module compiled from that + exact source and lawpack closure. +- `create-greeting.core.sha256` is its `edict.core.module/v1` + domain-framed identity. +- `create-greeting.target-ir.cbor` is the canonical `echo.span-ir/v1` + artifact lowered from that exact Core module. +- `create-greeting.target-ir.sha256` is its + `edict.target-ir.artifact/v1` domain-framed identity. + +Regenerate only through: + +```sh +cargo xtask lawpack-goldens --write +``` + +Check reviewed artifacts without modifying them: + +```sh +cargo xtask lawpack-goldens --check +``` + +The loader validates the manifest, exports, and exact direct adapter. The +compiler derives Core and `echo.span-ir/v1` facts from that closure without a +handwritten compiler context. The golden command compiles and lowers the source +before reproducing the reviewed Core and Target IR bytes and identities. This +fixture does not yet emit an Echo executable package or execute an Echo Action. +Edict corroborates the target-configuration reference but deliberately leaves +its Echo-specific semantics to the Echo-owned target provider. diff --git a/fixtures/lawpack/hello-echo/adapter.cbor b/fixtures/lawpack/hello-echo/adapter.cbor new file mode 100644 index 0000000..24e2f54 Binary files /dev/null and b/fixtures/lawpack/hello-echo/adapter.cbor differ diff --git a/fixtures/lawpack/hello-echo/adapter.sha256 b/fixtures/lawpack/hello-echo/adapter.sha256 new file mode 100644 index 0000000..91df491 --- /dev/null +++ b/fixtures/lawpack/hello-echo/adapter.sha256 @@ -0,0 +1 @@ +sha256:b01e5b8566fd13adf743c04ddade0d0870877df0fe3c942273d9d48b8f7dab99 diff --git a/fixtures/lawpack/hello-echo/create-greeting.core.cbor b/fixtures/lawpack/hello-echo/create-greeting.core.cbor new file mode 100644 index 0000000..f1cfa45 Binary files /dev/null and b/fixtures/lawpack/hello-echo/create-greeting.core.cbor differ diff --git a/fixtures/lawpack/hello-echo/create-greeting.core.sha256 b/fixtures/lawpack/hello-echo/create-greeting.core.sha256 new file mode 100644 index 0000000..4f5bd60 --- /dev/null +++ b/fixtures/lawpack/hello-echo/create-greeting.core.sha256 @@ -0,0 +1 @@ +sha256:2b6afaf519f92d5fd51ab525bf4ac13e05844d6364a318018da8a9f425a2c895 diff --git a/fixtures/lawpack/hello-echo/create-greeting.edict b/fixtures/lawpack/hello-echo/create-greeting.edict new file mode 100644 index 0000000..81c53b9 --- /dev/null +++ b/fixtures/lawpack/hello-echo/create-greeting.edict @@ -0,0 +1,31 @@ +package examples.hello_echo@1; + +use lawpack hello.echo@1 digest "sha256:21f3643c75764f8179ab725b8a8335178a52efddea50a628e09ee03aba565f41" as hello; + +type CreateGreetingInput = { + basis: String, + key: String, + message: String, +}; + +type GreetingReceipt = { + key: String, +}; + +type GreetingCreated = { + key: String, + message: String, +}; + +intent createGreeting(input: CreateGreetingInput) returns GreetingCreated + profile hello.createGreeting + basis input.basis + budget <= hello.smallCreateBudget +{ + let receipt: GreetingReceipt = hello.createGreeting(input) + else { alreadyExists(existing) => hello.AlreadyExists }; + return { + key: receipt.key, + message: input.message, + }; +} diff --git a/fixtures/lawpack/hello-echo/create-greeting.target-ir.cbor b/fixtures/lawpack/hello-echo/create-greeting.target-ir.cbor new file mode 100644 index 0000000..96e1806 Binary files /dev/null and b/fixtures/lawpack/hello-echo/create-greeting.target-ir.cbor differ diff --git a/fixtures/lawpack/hello-echo/create-greeting.target-ir.sha256 b/fixtures/lawpack/hello-echo/create-greeting.target-ir.sha256 new file mode 100644 index 0000000..4d28b70 --- /dev/null +++ b/fixtures/lawpack/hello-echo/create-greeting.target-ir.sha256 @@ -0,0 +1 @@ +sha256:157b5006707be64e8449313a4cf7bbcb818df87cf5dd83c78c7e95a844c5fafe diff --git a/fixtures/lawpack/hello-echo/echo-operation-configuration.cbor b/fixtures/lawpack/hello-echo/echo-operation-configuration.cbor new file mode 100644 index 0000000..84838ab Binary files /dev/null and b/fixtures/lawpack/hello-echo/echo-operation-configuration.cbor differ diff --git a/fixtures/lawpack/hello-echo/echo-operation-configuration.sha256 b/fixtures/lawpack/hello-echo/echo-operation-configuration.sha256 new file mode 100644 index 0000000..0898688 --- /dev/null +++ b/fixtures/lawpack/hello-echo/echo-operation-configuration.sha256 @@ -0,0 +1 @@ +sha256:629adbf28e55e6856e5065cf3e8100dbdee2345861897f0a7e7e2ffc052c35b6 diff --git a/fixtures/lawpack/hello-echo/exports.cbor b/fixtures/lawpack/hello-echo/exports.cbor new file mode 100644 index 0000000..eca7234 --- /dev/null +++ b/fixtures/lawpack/hello-echo/exports.cbor @@ -0,0 +1 @@ +¦etypes€geffectsªiinputTypex hello.echo@1.CreateGreetingInputjcoordinatexhello.echo@1.createGreetingjoutputTypexhello.echo@1.GreetingReceiptlguardSupportõncostObligationxhello.echo@1.smallCreateBudgetneffectFailures¡malreadyExists¢kpayloadTypexhello.echo@1.ExistingGreetingnauthorityClassndomainMappableneffectKindHintfcreatenexecutionClassgruntimentypeParameters€sfootprintObligationx!hello.echo@1.greetingKeyFootprinticonstants€lobstructions£jcoordinatexhello.echo@1.AlreadyExistsmpayloadSchemaxhello.echo@1.ExistingGreetingnauthorityClassndomainMappablempureFunctions€qoperationProfiles¡xhello.echo@1.createGreeting¢mopticTemplate¥iopticKindsaffectReintegrationlboundaryKindfaffectmsupportPolicyxhello.echo@1.directSupportolossDispositionuhello.echo@1.losslesssapertureRequirement¢crefx!hello.echo@1.greetingKeyFootprintdkindxabstractFootprintObligationoeffectPredicatex!hello.echo@1.createGreetingEffect \ No newline at end of file diff --git a/fixtures/lawpack/hello-echo/exports.sha256 b/fixtures/lawpack/hello-echo/exports.sha256 new file mode 100644 index 0000000..8473ecf --- /dev/null +++ b/fixtures/lawpack/hello-echo/exports.sha256 @@ -0,0 +1 @@ +sha256:3c66c474566f68e9cfab888fcca1db796584e516d4ba259dfa605626e15e657e diff --git a/fixtures/lawpack/hello-echo/manifest.cbor b/fixtures/lawpack/hello-echo/manifest.cbor new file mode 100644 index 0000000..c0c1d55 Binary files /dev/null and b/fixtures/lawpack/hello-echo/manifest.cbor differ diff --git a/fixtures/lawpack/hello-echo/manifest.sha256 b/fixtures/lawpack/hello-echo/manifest.sha256 new file mode 100644 index 0000000..9d5ea89 --- /dev/null +++ b/fixtures/lawpack/hello-echo/manifest.sha256 @@ -0,0 +1 @@ +sha256:21f3643c75764f8179ab725b8a8335178a52efddea50a628e09ee03aba565f41 diff --git a/fixtures/provider-contracts/v1/edict-provider-contracts.cddl b/fixtures/provider-contracts/v1/edict-provider-contracts.cddl index 58337a7..5b6fe74 100644 --- a/fixtures/provider-contracts/v1/edict-provider-contracts.cddl +++ b/fixtures/provider-contracts/v1/edict-provider-contracts.cddl @@ -536,6 +536,57 @@ obstruction-def = { ; 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-lawpack-adapter.cddl --- +; edict-lawpack-adapter.cddl +; Canonical schema for one direct declarative lawpack target adapter. +; Authoritative byte encoding: edict.canonical-cbor/v1. +; +; The enclosing lawpack manifest selects the exact target profile, target IR, +; and adapter resource digest. Those identities are not repeated here. + +lawpack-adapter = { + apiVersion: "edict.lawpack-adapter/v1", + class: "declarative", + operationProfiles: { + * tstr => lawpack-adapter-operation-profile + }, + effectImplementations: { + * tstr => lawpack-adapter-effect + }, + budgets: { + * tstr => lawpack-adapter-budget + }, +} + +; Keys are canonical lawpack operation-profile coordinates. +lawpack-adapter-operation-profile = { + core: tstr, + semanticEffects: [+ tstr], +} + +; Keys are canonical lawpack semantic-effect coordinates. Footprint, cost, and +; failure fields must exactly discharge the matching exported effect. +lawpack-adapter-effect = { + targetIntrinsic: tstr, + targetConfiguration: resource-ref, + writeClass: lawpack-adapter-write-class, + footprintObligation: tstr, + costObligation: tstr, + failureMappings: { * failure-ident => tstr }, +} + +; Keys are canonical exported cost-obligation coordinates. +lawpack-adapter-budget = { + maxSteps: uint, + maxAllocatedBytes: uint, + maxOutputBytes: uint, +} + +lawpack-adapter-write-class = "none" / "read" / "create" / "ensure" / + "append" / "replace" / "delete" / "custom" + +; failure-ident is defined in edict-common.cddl. + ; --- edict-target-profile.cddl --- ; edict-target-profile.cddl ; Canonical schema for the Edict target profile manifest. @@ -569,11 +620,10 @@ target-profile-manifest = { 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: [], + ; A profile that accepts the direct declarative lawpack-adapter ABI names it + ; exactly once. Profiles that do not consume lawpack adapters leave this + ; optional slot absent or empty. + ? acceptedLawpackAdapterAbi: [] / ["edict.lawpack-adapter/v1"], diagnosticAbi: resource-ref, ; application doctrine diff --git a/fixtures/provider-contracts/v1/manifest.json b/fixtures/provider-contracts/v1/manifest.json index a87e139..440dec8 100644 --- a/fixtures/provider-contracts/v1/manifest.json +++ b/fixtures/provider-contracts/v1/manifest.json @@ -3,12 +3,13 @@ "coordinate": "edict.provider-contract-pack.cddl@1", "license": "Apache-2.0", "schema": { - "bytesHex": "3b20535044582d4c6963656e73652d4964656e7469666965723a204170616368652d322e300a3b2065646963742d70726f76696465722d636f6e7472616374732e6364646c0a3b2047656e6572617465642066726f6d2045646963742d6f776e65642041424920667261676d656e74732e20444f204e4f5420454449542e0a0a3b202d2d2d2065646963742d636f6d6d6f6e2e6364646c202d2d2d0a3b2065646963742d636f6d6d6f6e2e6364646c0a3b20536861726564204344444c20747970657320666f722074686520456469637420414249732c20646566696e6564204f4e4345206865726520736f20746865792063616e6e6f742064726966740a3b202845444943542d4142492d4e4f4455502d303031292e2054776f2067726f7570733a0a3b2020202d207265736f757263652d7265662c207368613235362d6469676573742c206566666563742d6661696c7572652d626f64792c206566666563742d6b696e642c20617574686f726974792d636c6173732c0a3b2020202020636f72652d747970652d7265663a20617373656d626c656420776974682065646963742d7461726765742d70726f66696c652e6364646c20616e640a3b202020202065646963742d6c61777061636b2e6364646c20627920746865206275696c643b2074686f736520736368656d617320646f206e6f74207265646566696e65207468656d2e0a3b2020202d206f7065726174696f6e2d70726f66696c652c206f707469632d74656d706c6174652c2061706572747572652d726571756972656d656e7420616e6420746865697220726566733a0a3b2020202020636f6e73756d65642062792074686520436f72652f6f70746963206c61796572202865646963742d636f72652e6364646c2920616e64207265666572656e636564206279207468650a3b20202020206c616e67756167652f7461726765742d70726f66696c652073706563732e0a3b20417574686f7269746174697665206279746520656e636f64696e673a2065646963742e63616e6f6e6963616c2d63626f722f76312e0a0a3b2041206e6f726d617469766520737562636f6d706f6e656e74207265666572656e636564206279206964656e7469747920706c7573206469676573742e204d616e696665737473206e657665720a3b20656d626564207468656972206f776e2073656c662d64696765737420696e20746865697220707265696d616765202845444943542d434f52452d53454c46484153482d303031292e0a7265736f757263652d726566203d207b2069643a20747374722c206469676573743a207368613235362d646967657374207d0a0a3b20446967657374732061726520617574686f726974617469766520617320747970656420627974652076616c7565732c206e657665722068657820737472696e67732e20526576696577204a534f4e0a3b2072656e64657273207468697320617320227368613235363a3c3634206c6f77657263617365206865783e22202845444943542d4449474553542d574952452d303031292e0a7368613235362d646967657374203d205b20616c676f726974686d3a2022736861323536222c2062797465733a2062737472202e73697a65203332205d0a0a3b2041206e616d6564206c6f772d6c6576656c206661696c75726520616e206566666563742063616e2072616973652e2054686520736f75726365206f62737472756374696f6e206d61700a3b2062696e64732069742028627920636f6f7264696e6174652920616e6420636f6e73747275637473206120747970656420646f6d61696e206f62737472756374696f6e2066726f6d206974730a3b207061796c6f6164202845444943542d4142492d4641494c5552452d4e414d45442d303031292e0a3b20416e20656666656374277320606566666563744661696c7572657360206c697374204d555354206861766520756e697175652060636f6f7264696e61746560733a2073696e6365207468650a3b206f62737472756374696f6e206d6170206973206b6579656420627920636f6f7264696e6174652c2074776f206661696c757265732073686172696e67206120636f6f7264696e61746520286576656e0a3b207769746820646966666572656e7420617574686f72697479436c6173732f7061796c6f61645479706529206d616b652065786861757374697665206d617070696e6720616e642062696e6465720a3b20747970696e6720616d626967756f757320616e64206172652072656a6563746564202845444943542d4142492d4641494c5552452d554e495155452d303031292e0a3b0a3b2045666665637473206361727279207468656972206661696c757265732061732061206d617020607b206661696c7572652d6964656e74203d3e206566666563742d6661696c7572652d626f6479207d600a3b202873656520746865207461726765742f6c61777061636b2065666665637420736368656d6173292e20546865206661696c75726520636f6f7264696e61746520697320746865206d61700a3b204b45592c20736f206974206973206e6f7420726570656174656420696e2074686520626f647920616e642063616e6e6f74206469736167726565207769746820746865206b65792e0a6566666563742d6661696c7572652d626f6479203d207b0a2020617574686f72697479436c6173733a20617574686f726974792d636c6173732c0a20207061796c6f6164547970653a20636f72652d747970652d7265662c202020202020202020202020203b2074797065642c20626f756e64656420286d617920626520656d707479207265636f7264290a7d0a0a3b2041206661696c75726520636f6f7264696e617465206d7573742062652061206261726520456469637420606964656e746020286c65747465722f756e64657273636f7265207468656e0a3b206c6574746572732f6469676974732f756e64657273636f7265732920414e44206d757374206e6f742062652061207265736572766564206b6579776f72642028652e672e2060656c7365602c0a3b20606261736973602c20607768657265602c206072657175697265602c2060666f72602c2060696660292e2054686520736f75726365206f62737472756374696f6e2d6d6170204c4853206f6e6c790a3b20616363657074732061206e6f6e2d6b6579776f726420606964656e74602c20736f20612068797068656e2f646f742f6b6579776f726420636f6f7264696e61746520776f756c642062650a3b204142492d76616c69642079657420696d706f737369626c6520746f206d617020657868617573746976656c7920696e20736f757263652e20546865207265676578206361707475726573207468650a3b206c65786963616c2073686170653b206b6579776f7264206578636c7573696f6e20697320616e206164646974696f6e616c2076616c69646174696f6e2072756c650a3b202845444943542d4142492d4641494c5552452d4944454e542d303031292e0a6661696c7572652d6964656e74203d2074737472202e72656765787020225b412d5a612d7a5f5d5b412d5a612d7a302d395f5d2a220a0a6566666563742d6b696e64203d20227265616422202f202263726561746522202f2022656e7375726522202f20227265706c61636522202f202264656c65746522202f0a202020202020202020202020202022617070656e6422202f202272656475636522202f202273656d616e7469632e656d697422202f2022637573746f6d220a0a617574686f726974792d636c617373203d2022646f6d61696e4d61707061626c6522202f20227061727469636970616e744f776e656422202f2022696e746567726974794661756c7422202f0a202020202020202020202020202020202020227265736f757263654661756c7422202f2022696e7465726e616c4661756c74220a0a636f72652d747970652d726566203d20747374722020203b2063616e6f6e6963616c20436f7265207479706520636f6f7264696e6174650a0a3b20416e206f7065726174696f6e2070726f66696c6520737570706c69657320746865206f707469632074656d706c617465206120436f726520696e74656e74207265736f6c766573206974730a3b206f707469634b696e642f626f756e646172794b696e642f737570706f7274506f6c6963792f6c6f7373446973706f736974696f6e2066726f6d2e205461726765742070726f66696c657320616e640a3b206c61777061636b73207075626c6973682074686573652061732061206d617020607b20636f6f7264696e617465203d3e206f7065726174696f6e2d70726f66696c65207d602c20736f207468650a3b20636f6f7264696e61746520697320746865204b45592c206e6f7420612076616c7565206669656c64202845444943542d4f505449432d54454d504c4154452d4f574e45522d3030312c0a3b2045444943542d4142492d4f5050524f46494c452d554e495155452d303031292e0a6f7065726174696f6e2d70726f66696c65203d207b0a20206f7074696354656d706c6174653a206f707469632d74656d706c6174652c0a20206566666563745072656469636174653a20747374722c20202020202020202020203b20636f6f7264696e617465206f6620746865206f7065726174696f6e2d6d6f6465207072656469636174650a7d0a0a6f707469632d74656d706c617465203d207b0a20206f707469634b696e643a2022726576656c6174696f6e22202f20226166666563745265696e746567726174696f6e222c0a2020626f756e646172794b696e643a202270726f6a656374696f6e22202f2022616666656374222c0a2020737570706f7274506f6c6963793a20747374722c202020202020202020202020203b2063616e6f6e6963616c20737570706f72742d706f6c69637920636f6f7264696e6174650a20206c6f7373446973706f736974696f6e3a20747374722c20202020202020202020203b2063616e6f6e6963616c206c6f73732d646973706f736974696f6e20636f6f7264696e6174650a20203f20626173697354656d706c6174653a20747374722c20202020202020202020203b206f7074696f6e616c206469676573742d6c6f636b65642062617369732074656d706c61746520636f6f72640a20203b2074686520617065727475726520726571756972656d656e7420746869732074656d706c61746520737570706c6965732e205265717569726564207768656e207468652074656d706c6174650a20203b2069732074686520736f75726365206f66206120436f7265206f707469632773206170657274757265526571756972656d656e742028692e652e2074686520696e74656e7420686173206e6f0a20203b20736f757263652060666f6f747072696e74203c3d202e2e2e60292c2073696e6365206170657274757265526571756972656d656e74206973206d616e6461746f727920696e20436f72650a20203b202845444943542d4f505449432d41504552545552452d5245462d303031292e0a20203f206170657274757265526571756972656d656e743a2061706572747572652d726571756972656d656e742c0a7d0a0a3b206170657274757265526571756972656d656e742069732061207479706564207265666572656e63652c206e65766572206120667265652d666f726d20737472696e672e2041207265766965770a3b2072656e646572696e67206d61792073686f772069747320636f6f7264696e617465202845444943542d4f505449432d41504552545552452d5245462d303031292e0a61706572747572652d726571756972656d656e74203d20666f6f747072696e742d6365696c696e672d726566202f2061627374726163742d666f6f747072696e742d6f626c69676174696f6e2d7265660a666f6f747072696e742d6365696c696e672d726566203d207b206b696e643a2022666f6f747072696e744365696c696e67222c207265663a2074737472207d0a61627374726163742d666f6f747072696e742d6f626c69676174696f6e2d726566203d207b206b696e643a20226162737472616374466f6f747072696e744f626c69676174696f6e222c207265663a2074737472207d0a0a3b202d2d2d2065646963742d636f72652e6364646c202d2d2d0a3b2065646963742d636f72652e6364646c0a3b204e6f726d617469766520736368656d6120666f722074686520456469637420436f72652076312073656d616e746963206d6f64656c2e0a3b0a3b2053636f706520626f756e646172793a20746869732066696c6520646566696e657320436f7265206d65616e696e6720616e6420736368656d61207368617065206f6e6c792e20497420646f65730a3b206e6f7420646566696e6520612063616e6f6e6963616c20656e636f6465722c20436f7265206d6f64756c652068617368206669656c64732c20686173682066697874757265732c207461726765740a3b206c6f776572696e672c2061646d697373696f6e2062756e646c65732c206f72207461726765742d6f776e65642049522e0a0a636f72652d6d6f64756c65203d207b0a202061706956657273696f6e3a202265646963742e636f72652f7631222c0a2020636f6f7264696e6174653a20747374722c0a2020696d706f7274733a205b2a20636f72652d696d706f72745d2c0a202074797065733a207b202a2074737472203d3e20636f72652d74797065207d2c0a2020696e74656e74733a207b202b2074737472203d3e20636f72652d696e74656e74207d2c0a20207265717569726564436f72654361706162696c69746965733a205b2a20747374725d2c0a7d0a0a636f72652d696d706f7274203d207b0a20206b696e643a20226c61777061636b22202f202274617267657422202f2022636f7265222c0a20207265663a207265736f757263652d7265662c0a20203f20616c6961733a20747374722c0a7d0a0a3b202d2d2d207479706573202d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d0a0a636f72652d74797065203d20636f72652d7363616c61722d74797065202f20636f72652d7265636f72642d74797065202f20636f72652d76617269616e742d74797065202f0a202020202020202020202020636f72652d6f7074696f6e2d74797065202f20636f72652d6c6973742d74797065202f20636f72652d6d61702d74797065202f0a202020202020202020202020636f72652d6361706162696c6974792d7265662d747970650a0a636f72652d7363616c61722d74797065203d20636f72652d626f6f6c2d74797065202f20636f72652d696e742d74797065202f20636f72652d737472696e672d74797065202f0a20202020202020202020202020202020202020636f72652d62797465732d74797065202f20636f72652d756e69742d747970650a0a636f72652d626f6f6c2d74797065203d207b206b696e643a2022426f6f6c22207d0a636f72652d756e69742d74797065203d207b206b696e643a2022556e697422207d0a636f72652d696e742d74797065203d207b0a20206b696e643a202249363422202f202255363422202f202249333222202f202255333222202f202249313622202f202255313622202f2022493822202f20225538222c0a7d0a636f72652d737472696e672d74797065203d207b0a20206b696e643a2022537472696e67222c0a20206d61783a2075696e742c0a202063616e6f6e6963616c3a2022756e69636f64652d7363616c61722d6e666322202f20227261772d75746638222c0a7d0a636f72652d62797465732d74797065203d207b0a20206b696e643a20224279746573222c0a20206d61783a2075696e742c0a7d0a636f72652d7265636f72642d74797065203d207b0a20206b696e643a20225265636f7264222c0a20206669656c64733a207b202a2074737472203d3e20636f72652d747970652d726566207d2c0a7d0a636f72652d76617269616e742d74797065203d207b0a20206b696e643a202256617269616e74222c0a202063617365733a207b202b2074737472203d3e2076617269616e742d636173652d626f6479207d2c0a7d0a76617269616e742d636173652d626f6479203d207b0a20203f207061796c6f61643a20636f72652d747970652d7265662c0a7d0a636f72652d6f7074696f6e2d74797065203d207b0a20206b696e643a20224f7074696f6e222c0a20206974656d3a20636f72652d747970652d7265662c0a7d0a636f72652d6c6973742d74797065203d207b0a20206b696e643a20224c697374222c0a20206974656d3a20636f72652d747970652d7265662c0a20206d61783a2075696e742c0a7d0a636f72652d6d61702d74797065203d207b0a20206b696e643a20224d6170222c0a20206b65793a20636f72652d747970652d7265662c0a202076616c75653a20636f72652d747970652d7265662c0a20206d61783a2075696e742c0a7d0a636f72652d6361706162696c6974792d7265662d74797065203d207b0a20206b696e643a20224361706162696c697479526566222c0a20206974656d3a20636f72652d747970652d7265662c0a7d0a0a3b20636f72652d747970652d72656620697320646566696e656420696e2065646963742d636f6d6d6f6e2e6364646c20616e6420617373656d626c65642077697468207468697320736368656d612e0a0a3b202d2d2d207265666572656e63657320616e642076616c756573202d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d0a0a3b204c6f63616c206964656e7469747920697320616c7068612d737461626c652e20606964602069732074686520636f6d70696c65722d6f776e6564206c6f63616c20636f6f7264696e6174653b0a3b2060616c7068614e616d656020697320746865206e6f726d616c697a65642068756d616e2f6465627567206e616d652e20536f757263652062696e646572207370656c6c696e67206973206e6f740a3b206964656e746974792e0a6c6f63616c2d726566203d207b0a202069643a20747374722c0a2020616c7068614e616d653a20747374722c0a2020747970653a20636f72652d747970652d7265662c0a7d0a0a636f72652d76616c7565203d20636f72652d6e756c6c2d76616c7565202f20636f72652d626f6f6c2d76616c7565202f20636f72652d696e742d76616c7565202f0a20202020202020202020202020636f72652d737472696e672d76616c7565202f20636f72652d62797465732d76616c7565202f20636f72652d7265636f72642d76616c7565202f0a20202020202020202020202020636f72652d76617269616e742d76616c7565202f20636f72652d6c6973742d76616c7565202f20636f72652d6d61702d76616c7565202f0a20202020202020202020202020636f72652d6361706162696c6974792d76616c75650a0a636f72652d6e756c6c2d76616c7565203d207b206b696e643a20226e756c6c22207d0a636f72652d626f6f6c2d76616c7565203d207b206b696e643a2022626f6f6c222c2076616c75653a20626f6f6c207d0a636f72652d696e742d76616c7565203d207b206b696e643a2022696e74222c2077696474683a20747374722c2076616c75653a20696e74207d0a636f72652d737472696e672d76616c7565203d207b206b696e643a2022737472696e67222c2076616c75653a2074737472207d0a636f72652d62797465732d76616c7565203d207b206b696e643a20226279746573222c2076616c75653a2062737472207d0a636f72652d7265636f72642d76616c7565203d207b206b696e643a20227265636f7264222c206669656c64733a207b202a2074737472203d3e20636f72652d76616c7565207d207d0a636f72652d76617269616e742d76616c7565203d207b0a20206b696e643a202276617269616e74222c0a2020747970653a20636f72652d747970652d7265662c0a2020636173653a20747374722c0a20203f207061796c6f61643a20636f72652d76616c75652c0a7d0a636f72652d6c6973742d76616c7565203d207b206b696e643a20226c697374222c2076616c7565733a205b2a20636f72652d76616c75655d207d0a636f72652d6d61702d76616c7565203d207b206b696e643a20226d6170222c20656e74726965733a205b2a205b6b65793a20636f72652d76616c75652c2076616c75653a20636f72652d76616c75655d5d207d0a636f72652d6361706162696c6974792d76616c7565203d207b0a20206b696e643a20226361706162696c697479222c0a2020726563656970743a207368613235362d6469676573742c0a7d0a0a3b2045646963742d617574686f72656420707572652068656c70657273207573652061207075726520436f72652066756e6374696f6e20626f64792e2054686520626f64792063616e2062696e640a3b20707572652065787072657373696f6e7320616e642072657475726e20616e2065787072657373696f6e2c206275742069742063616e6e6f7420636f6e7461696e20436f7265206566666563742c0a3b2067756172642c206272616e63682c206c6f6f702c206d617463682d6e6f64652c206f722070726f6f662d6f626c69676174696f6e206e6f6465732e0a636f72652d666e2d626f6479203d207b0a2020706172616d733a205b2a206c6f63616c2d7265665d2c0a2020626f64793a20636f72652d707572652d626c6f636b2c0a7d0a0a636f72652d707572652d626c6f636b203d207b0a20206c6f63616c733a205b2a206c6f63616c2d7265665d2c0a202062696e64696e67733a205b2a20707572652d6c65742d6e6f64655d2c0a2020726573756c743a20636f72652d657870722c0a7d0a0a707572652d6c65742d6e6f6465203d207b0a20206b696e643a20226c6574222c0a202062696e64696e673a206c6f63616c2d7265662c0a202076616c75653a20636f72652d657870722c0a7d0a0a3b202d2d2d2065787072657373696f6e7320616e642070726564696361746573202d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d0a0a636f72652d65787072203d206c6f63616c2d65787072202f20636f6e73742d65787072202f207265636f72642d65787072202f206669656c642d65787072202f0a20202020202020202020202076617269616e742d65787072202f206d617463682d65787072202f2063616c6c2d65787072202f206c6973742d65787072202f206d61702d65787072202f0a20202020202020202020202069662d657870720a0a6c6f63616c2d65787072203d207b206b696e643a20226c6f63616c222c207265663a206c6f63616c2d726566207d0a636f6e73742d65787072203d207b206b696e643a2022636f6e7374222c2076616c75653a20636f72652d76616c7565207d0a7265636f72642d65787072203d207b206b696e643a20227265636f7264222c206669656c64733a207b202a2074737472203d3e20636f72652d65787072207d207d0a6669656c642d65787072203d207b206b696e643a20226669656c64222c20626173653a20636f72652d657870722c206669656c643a2074737472207d0a76617269616e742d65787072203d207b0a20206b696e643a202276617269616e74222c0a2020747970653a20636f72652d747970652d7265662c0a2020636173653a20747374722c0a20203f207061796c6f61643a20636f72652d657870722c0a7d0a6d617463682d65787072203d207b0a20206b696e643a20226d61746368222c0a20207363727574696e65653a20636f72652d657870722c0a202061726d733a205b2b206d617463682d61726d5d2c0a7d0a6d617463682d61726d203d207b0a2020636173653a20747374722c0a20203f2062696e6465723a206c6f63616c2d7265662c0a2020626f64793a20636f72652d657870722c0a7d0a63616c6c2d65787072203d207b0a20206b696e643a202263616c6c222c0a202063616c6c65653a20747374722c0a202074797065417267733a205b2a20636f72652d747970652d7265665d2c0a2020617267733a205b2a20636f72652d657870725d2c0a7d0a6c6973742d65787072203d207b206b696e643a20226c697374222c2076616c7565733a205b2a20636f72652d657870725d207d0a6d61702d65787072203d207b206b696e643a20226d6170222c20656e74726965733a205b2a205b6b65793a20636f72652d657870722c2076616c75653a20636f72652d657870725d5d207d0a69662d65787072203d207b0a20206b696e643a20226966222c0a20207072656469636174653a20636f72652d7072656469636174652c0a20207468656e3a20636f72652d657870722c0a2020656c73653a20636f72652d657870722c0a7d0a0a636f72652d707265646963617465203d20747275652d707265646963617465202f2066616c73652d707265646963617465202f206e6f742d707265646963617465202f0a2020202020202020202020202020202020616c6c2d707265646963617465202f20616e792d707265646963617465202f20636f6d706172652d707265646963617465202f0a202020202020202020202020202020202063616c6c2d707265646963617465202f206f62737472756374696f6e2d7072656469636174650a0a747275652d707265646963617465203d207b206b696e643a20227472756522207d0a66616c73652d707265646963617465203d207b206b696e643a202266616c736522207d0a6e6f742d707265646963617465203d207b206b696e643a20226e6f74222c2076616c75653a20636f72652d707265646963617465207d0a616c6c2d707265646963617465203d207b206b696e643a2022616c6c222c2076616c7565733a205b2b20636f72652d7072656469636174655d207d0a616e792d707265646963617465203d207b206b696e643a2022616e79222c2076616c7565733a205b2b20636f72652d7072656469636174655d207d0a636f6d706172652d707265646963617465203d207b0a20206b696e643a2022636f6d70617265222c0a20206f703a20223d3d22202f2022213d22202f20223c22202f20223c3d22202f20223e22202f20223e3d222c0a20206c6566743a20636f72652d657870722c0a202072696768743a20636f72652d657870722c0a7d0a63616c6c2d707265646963617465203d207b0a20206b696e643a202263616c6c222c0a20207072656469636174653a20747374722c0a2020617267733a205b2a20636f72652d657870725d2c0a7d0a6f62737472756374696f6e2d707265646963617465203d207b0a20206b696e643a20226f62737472756374696f6e222c0a2020636f6f7264696e6174653a206661696c7572652d6964656e742c0a20207061796c6f61643a20636f72652d657870722c0a7d0a0a696e7075742d636f6e73747261696e74203d207b0a2020636f6f7264696e6174653a20747374722c0a2020736f757263653a2022776865726522202f2022636f6d70696c6572222c0a20207072656469636174653a20636f72652d7072656469636174652c0a7d0a0a3b202d2d2d20696e74656e74732c20626c6f636b732c20616e64206e6f646573202d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d0a0a636f72652d696e74656e74203d207b0a2020696e7075743a20636f72652d747970652d7265662c0a20206f75747075743a20636f72652d747970652d7265662c0a202072657175697265644f7065726174696f6e50726f66696c653a20747374722c0a20203f2062617369733a20636f72652d657870722c0a2020696e707574436f6e73747261696e74733a205b2a20696e7075742d636f6e73747261696e745d2c0a2020636f72654576616c756174696f6e4275646765743a20636f72652d6275646765742c0a2020626f64793a20636f72652d626c6f636b2c0a20203f206f707469633a20636f72652d6f707469632c0a7d0a0a636f72652d627564676574203d207b0a20206d617853746570733a2075696e742c0a20206d6178416c6c6f636174656442797465733a2075696e742c0a20206d61784f757470757442797465733a2075696e742c0a7d0a0a636f72652d6f70746963203d207b0a20206f707469634b696e643a2022726576656c6174696f6e22202f20226166666563745265696e746567726174696f6e222c0a2020626f756e646172794b696e643a202270726f6a656374696f6e22202f2022616666656374222c0a20206170657274757265526571756972656d656e743a2061706572747572652d726571756972656d656e742c0a2020737570706f7274506f6c6963793a20747374722c0a20206c6f7373446973706f736974696f6e3a20747374722c0a7d0a0a636f72652d626c6f636b203d207b0a20206c6f63616c733a205b2a206c6f63616c2d7265665d2c0a20206e6f6465733a205b2a20636f72652d6e6f64655d2c0a2020726573756c743a20636f72652d657870722c0a7d0a0a636f72652d6e6f6465203d206c65742d6e6f6465202f20726571756972652d6e6f6465202f206566666563742d6e6f6465202f2067756172642d6e6f6465202f206272616e63682d6e6f6465202f0a202020202020202020202020666f722d6e6f6465202f206d617463682d6e6f6465202f2070726f6f662d6f626c69676174696f6e2d6e6f64650a0a6c65742d6e6f6465203d207b0a20206b696e643a20226c6574222c0a202062696e64696e673a206c6f63616c2d7265662c0a202076616c75653a20636f72652d657870722c0a7d0a726571756972652d6e6f6465203d207b0a20206b696e643a202272657175697265222c0a20207072656469636174653a20636f72652d7072656469636174652c0a20206f6e4661696c7572653a20726571756972652d6661696c7572652d61726d2c0a7d0a726571756972652d6661696c7572652d61726d203d207465726d696e616c2d726571756972652d6661696c757265202f0a20202020202020202020202020202020202020202020636f6e74696e75652d6f6273747275637465642d726571756972652d6661696c7572650a7465726d696e616c2d726571756972652d6661696c757265203d207b0a20206b696e643a20227465726d696e616c222c0a2020726561736f6e3a206f62737472756374696f6e2d726561736f6e2c0a7d0a636f6e74696e75652d6f6273747275637465642d726571756972652d6661696c757265203d207b0a20206b696e643a2022636f6e74696e75654f627374727563746564222c0a2020726561736f6e3a206f62737472756374696f6e2d726561736f6e2c0a7d0a6f62737472756374696f6e2d726561736f6e203d207b0a2020726561736f6e4b696e643a20747374722c0a20207061796c6f61643a207b202a2074737472203d3e20636f72652d65787072207d2c0a7d0a6566666563742d6e6f6465203d207b0a20206b696e643a2022656666656374222c0a202062696e64696e673a206c6f63616c2d7265662c0a20206566666563743a20747374722c0a2020696e7075743a20636f72652d657870722c0a20206f62737472756374696f6e4d61703a207b202a206661696c7572652d6964656e74203d3e206f62737472756374696f6e2d61726d207d2c0a7d0a6f62737472756374696f6e2d61726d203d207b0a202062696e6465723a206c6f63616c2d7265662c0a202076616c75653a20636f72652d657870722c0a7d0a67756172642d6e6f6465203d207b0a20206b696e643a20226775617264222c0a20207072656469636174653a20636f72652d7072656469636174652c0a20206f62737472756374696f6e3a20636f72652d657870722c0a7d0a6272616e63682d6e6f6465203d207b0a20206b696e643a20226272616e6368222c0a20207072656469636174653a20636f72652d7072656469636174652c0a20207468656e3a20636f72652d626c6f636b2c0a2020656c73653a20636f72652d626c6f636b2c0a7d0a666f722d6e6f6465203d207b0a20206b696e643a2022666f72222c0a202062696e6465723a206c6f63616c2d7265662c0a2020697465723a20636f72652d657870722c0a2020626f756e643a20636f72652d626f756e642c0a2020626f64793a20636f72652d626c6f636b2c0a7d0a6d617463682d6e6f6465203d207b0a20206b696e643a20226d61746368222c0a20207363727574696e65653a20636f72652d657870722c0a202061726d733a205b2b206d617463682d626c6f636b2d61726d5d2c0a7d0a6d617463682d626c6f636b2d61726d203d207b0a2020636173653a20747374722c0a20203f2062696e6465723a206c6f63616c2d7265662c0a2020626f64793a20636f72652d626c6f636b2c0a7d0a70726f6f662d6f626c69676174696f6e2d6e6f6465203d207b0a20206b696e643a202270726f6f66222c0a2020636f6f7264696e6174653a20747374722c0a20207072656469636174653a20636f72652d7072656469636174652c0a7d0a0a636f72652d626f756e64203d206c69746572616c2d626f756e64202f20636f6f7264696e6174652d626f756e640a6c69746572616c2d626f756e64203d207b206b696e643a20226c69746572616c222c2076616c75653a2075696e74207d0a636f6f7264696e6174652d626f756e64203d207b206b696e643a2022636f6f7264696e617465222c207265663a2074737472207d0a0a3b20536861726564207265736f757263652d7265662c207368613235362d6469676573742c206661696c7572652d6964656e742c2061706572747572652d726571756972656d656e742c20616e640a3b20636f72652d747970652d7265662061726520646566696e6564206f6e636520696e2065646963742d636f6d6d6f6e2e6364646c2e0a0a3b202d2d2d2065646963742d6c61777061636b2e6364646c202d2d2d0a3b2065646963742d6c61777061636b2e6364646c0a3b2043616e6f6e6963616c20736368656d6120666f7220746865204564696374206c61777061636b206d616e696665737420616e64206578706f727420737572666163652e0a3b20417574686f7269746174697665206279746520656e636f64696e673a2065646963742e63616e6f6e6963616c2d63626f722f76312e204a534f4e20696e207468652070726f73652073706563730a3b2069732061207265766965772072656e646572696e672067656e6572617465642066726f6d207468697320736368656d61202845444943542d4142492d4e4f4455502d303031292e0a0a6c61777061636b2d6d616e6966657374203d207b0a202061706956657273696f6e3a202265646963742e6c61777061636b2f7631222c0a202069643a20747374722c0a202076657273696f6e3a20747374722c0a20206163636570746564436f72654162693a205b2b20747374725d2c0a2020646570656e64656e636965733a205b2a206c61777061636b2d6465705d2c202020202020202020203b20616379636c69632c206469676573742d6c6f636b6564202845444943542d4c41575041434b2d4441472d303031290a20206578706f7274733a207265736f757263652d7265662c0a20203f2074617267657441646170746572733a205b2b207461726765742d616461707465725d2c2020203b207265717569726564206f6e6c7920696620616e792072756e74696d6520656666656374206578697374730a20203f2068656c706572436f6d706f6e656e743a2065786563757461626c652d636f6d706f6e656e742c203b2065786563757461626c652068656c70657273206361727279207468656972206f776e2073616e64626f782b6675656c0a202076657269666965723a2076657269666965722c202020202020202020202020202020202020202020203b20636c61737369666965643a206465636c61726174697665206f722065786563757461626c650a2020636f6d7061746962696c6974793a207265736f757263652d7265662c0a2020636f6e666f726d616e636546697874757265436f727075733a207265736f757263652d7265662c0a7d0a0a3b2041207665726966696572206973206569746865722061206465636c617261746976652072756c6573657420286e6f2072756e74696d6529206f7220616e2065786563757461626c650a3b20636f6d706f6e656e742e20416e2065786563757461626c65207665726966696572204d55535420636172727920697473206f776e2073616e64626f7820616e64206675656c206d6f64656c2c0a3b20736f2074686520736368656d6120656e666f726365732074686174206e6f2065786563757461626c6520636f6d706f6e656e74206973206c65667420756e626f756e6465640a3b202845444943542d4142492d56455249464945522d424f554e442d303031292e0a7665726966696572203d206465636c617261746976652d7665726966696572202f2065786563757461626c652d76657269666965720a6465636c617261746976652d7665726966696572203d207b20636c6173733a20226465636c61726174697665222c2072756c657365743a207265736f757263652d726566207d0a65786563757461626c652d7665726966696572203d207b0a2020636c6173733a202265786563757461626c65222c0a2020636f6d706f6e656e743a207265736f757263652d7265662c0a202073616e64626f783a207265736f757263652d7265662c0a20206675656c4d6f64656c3a207265736f757263652d7265662c0a7d0a0a3b20416e792065786563757461626c6520636f6d706f6e656e7420697320626f756e64656420627920697473206f776e2073616e64626f78202b206675656c206d6f64656c2e0a65786563757461626c652d636f6d706f6e656e74203d207b0a2020636f6d706f6e656e743a207265736f757263652d7265662c0a202073616e64626f783a207265736f757263652d7265662c0a20206675656c4d6f64656c3a207265736f757263652d7265662c0a7d0a0a6c61777061636b2d646570203d207b2069643a20747374722c2076657273696f6e3a20747374722c206469676573743a207368613235362d646967657374207d0a0a3b20416461707465722073656c656374696f6e206b65797320534f4c454c59206f666620746865206469676573742d6c6f636b65642060616363657074656454617267657450726f66696c65600a3b20286974732060696460206973207468652070726f66696c652069643b206974732060646967657374602070696e73207468652065786163742070726f66696c652f76657273696f6e292e2054686572650a3b20617265206e6f20696e646570656e64656e7420646973706c617920737472696e6773207468617420636f756c64206469736167726565207769746820746865206c6f636b2c20736f20610a3b207265736f6c7665722063616e6e6f742062696e6420616e206164617074657220746f206f6e6520746172676574207768696c6520746865206c6f636b2070726f76657320616e6f746865720a3b202845444943542d4c41575041434b2d414441505445522d54415247455449522d303031292e0a7461726765742d61646170746572203d207b0a2020616363657074656454617267657450726f66696c653a207265736f757263652d7265662c202020203b206469676573742d6c6f636b65642c20617574686f72697461746976652073656c6563746f720a2020616363657074656454617267657449723a207265736f757263652d7265662c2020202020202020203b206469676573742d6c6f636b65640a2020616461707465723a207265736f757263652d7265662c0a7d0a0a3b20536861726564207479706573207265736f757263652d72656620616e64207368613235362d6469676573742061726520646566696e6564206f6e636520696e0a3b2065646963742d636f6d6d6f6e2e6364646c20616e6420617373656d626c65642077697468207468697320736368656d61202845444943542d4142492d4e4f4455502d303031292e0a0a3b202d2d2d206578706f72742073757266616365202d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d0a0a6c61777061636b2d6578706f727473203d207b0a202074797065733a205b2a206578706f727465642d747970655d2c0a2020636f6e7374616e74733a205b2a206578706f727465642d636f6e7374616e745d2c0a20207075726546756e6374696f6e733a205b2a20707572652d66756e6374696f6e5d2c0a2020656666656374733a205b2a2073656d616e7469632d6566666563745d2c0a20206f62737472756374696f6e733a205b2a206f62737472756374696f6e2d6465665d2c0a20203b206b65796564206279206f7065726174696f6e2d70726f66696c6520636f6f7264696e61746520e2869220756e697175656e65737320656e666f726365640a20203b202845444943542d4142492d4f5050524f46494c452d554e495155452d303031290a20203b206f7065726174696f6e2d70726f66696c65207265636f7264732074686973206c61777061636b206578706f72747320286f707469632074656d706c6174657320746861740a20203b2060696d706c656d656e7473602f6070726f66696c656020636c6175736573207265736f6c766520616761696e7374292e206f7065726174696f6e2d70726f66696c652069730a20203b20646566696e656420696e2065646963742d636f6d6d6f6e2e6364646c202845444943542d4142492d4f5050524f46494c452d534c4f542d303031292e0a20206f7065726174696f6e50726f66696c65733a207b202a2074737472203d3e206f7065726174696f6e2d70726f66696c65207d2c20203b206b6579656420627920636f6f7264696e6174650a7d0a0a6578706f727465642d7479706520202020203d207b20636f6f7264696e6174653a20747374722c20646566696e6974696f6e3a20636f72652d747970652d726566207d0a6578706f727465642d636f6e7374616e74203d207b20636f6f7264696e6174653a20747374722c20747970653a20636f72652d747970652d7265662c2076616c75653a20616e79207d0a0a3b204120707572652068656c7065722069732061206469736372696d696e6174656420756e696f6e2062792060736f75726365602c20736f2074686520736368656d6120697473656c660a3b2067756172616e7465657320616e20696d706c656d656e746174696f6e20657869737473202845444943542d4c41575041434b2d505552452d494d504c2d303031293a0a3b2020202d20226564696374223a20617574686f72656420696e2045646963742f436f72653b2074686520436f726520626f6479206973206361727269656420696e6c696e6520286861736865640a3b20202020207769746820746865206578706f72742073757266616365292e2054686520736368656d61207265717569726573207468652060626f647960206669656c642e0a3b2020202d2022636f6d706f6e656e74223a20696d706c656d656e746564206f7574736964652045646963743b2063617272696573206e6f20696e6c696e6520626f647920616e6420696e73746561640a3b20202020206361727269657320697473206f776e206469676573742d6c6f636b65642060696d706c656d656e746174696f6e60202873616e64626f78202b206675656c292e20497420646f65730a3b20202020206e6f7420646570656e64206f6e20746865206f7074696f6e616c206d616e69666573742d6c6576656c2068656c706572436f6d706f6e656e742e0a707572652d66756e6374696f6e203d2065646963742d707572652d66756e6374696f6e202f20636f6d706f6e656e742d707572652d66756e6374696f6e0a0a707572652d66756e6374696f6e2d636f6d6d6f6e203d20280a2020636f6f7264696e6174653a20747374722c0a202074797065506172616d65746572733a205b2a20747374725d2c0a2020706172616d6574657254797065733a205b2a20636f72652d747970652d7265665d2c2020202020203b20616c6c20626f756e6465640a202072657475726e547970653a20636f72652d747970652d7265662c20202020202020202020202020203b20626f756e6465640a2020636f737454656d706c6174653a20747374722c0a202064657465726d696e69736d436c6173733a2022746f74616c22202f2022746f74616c2d776974682d74797065642d646961676e6f73746963222c0a290a0a65646963742d707572652d66756e6374696f6e203d207b0a2020707572652d66756e6374696f6e2d636f6d6d6f6e2c0a2020736f757263653a20226564696374222c0a2020626f64793a20636f72652d666e2d626f64792c2020202020202020202020202020202020202020203b20696e6c696e652c20686173682d7369676e69666963616e740a7d0a0a636f6d706f6e656e742d707572652d66756e6374696f6e203d207b0a2020707572652d66756e6374696f6e2d636f6d6d6f6e2c0a2020736f757263653a2022636f6d706f6e656e74222c0a20203b20746865206469676573742d6c6f636b656420636f6d706f6e656e7420696d706c656d656e74696e6720746869732068656c7065722e2052657175697265642061742074686520736368656d610a20203b206c6576656c20736f206120636f6d706f6e656e742068656c7065722063616e206e657665722076616c696461746520776974686f7574206120686173682d626f756e642c0a20203b2073616e64626f782b6675656c2d64657363726962656420696d706c656d656e746174696f6e202845444943542d4c41575041434b2d505552452d494d504c2d303031292e0a2020696d706c656d656e746174696f6e3a2065786563757461626c652d636f6d706f6e656e742c0a7d0a0a3b20636f72652d666e2d626f647920697320646566696e65642062792065646963742d636f72652e6364646c20616e6420617373656d626c656420776974682074686973206c61777061636b0a3b20736368656d612e2049742069732061207075726520436f72652066756e6374696f6e20626f64792c206e6f7420616e206566666563742d63617061626c6520636f72652d626c6f636b2e0a0a73656d616e7469632d656666656374203d207b0a2020636f6f7264696e6174653a20747374722c0a202074797065506172616d65746572733a205b2a20747374725d2c0a2020696e707574547970653a20636f72652d747970652d7265662c2020202020202020202020202020203b20626f756e6465640a20206f7574707574547970653a20636f72652d747970652d7265662c20202020202020202020202020203b20626f756e6465640a2020657865637574696f6e436c6173733a202270726f6f664f6e6c7922202f202272756e74696d65222c2020203b206f7274686f676f6e616c20746f207772697465436c6173730a20206566666563744b696e6448696e743a206566666563742d6b696e642c0a2020666f6f747072696e744f626c69676174696f6e3a20747374722c0a2020636f73744f626c69676174696f6e3a20747374722c0a20206566666563744661696c757265733a207b202a206661696c7572652d6964656e74203d3e206566666563742d6661696c7572652d626f6479207d2c20203b206b6579656420627920636f6f7264696e6174653b20756e697175650a20206775617264537570706f72743a20626f6f6c2c0a7d0a0a6f62737472756374696f6e2d646566203d207b0a2020636f6f7264696e6174653a20747374722c0a2020617574686f72697479436c6173733a20617574686f726974792d636c6173732c0a20207061796c6f6164536368656d613a20636f72652d747970652d7265662c20202020202020202020203b2074797065642c20626f756e64656420286d617920626520656d707479207265636f7264290a7d0a0a3b206566666563742d6661696c7572652d626f64792c206566666563742d6b696e642c20617574686f726974792d636c6173732c20616e6420636f72652d747970652d7265662061726520646566696e65640a3b206f6e636520696e2065646963742d636f6d6d6f6e2e6364646c20616e6420617373656d626c65642077697468207468697320736368656d61202845444943542d4142492d4e4f4455502d303031292e0a0a3b202d2d2d2065646963742d7461726765742d70726f66696c652e6364646c202d2d2d0a3b2065646963742d7461726765742d70726f66696c652e6364646c0a3b2043616e6f6e6963616c20736368656d6120666f7220746865204564696374207461726765742070726f66696c65206d616e69666573742e0a3b20417574686f7269746174697665206279746520656e636f64696e673a2065646963742e63616e6f6e6963616c2d63626f722f76310a3b202873656520535045435f636f6e74696e75756d2d636f6e74726163742d62756e646c652d76312e6d64292e204a534f4e20696e207468652070726f736520737065637320697320610a3b207265766965772072656e646572696e672067656e6572617465642066726f6d207468697320736368656d613b2074686973204344444c206973207468652073696e676c6520736f757263650a3b206f66207472757468202845444943542d4142492d4e4f4455502d303031292e0a0a7461726765742d70726f66696c652d6d616e6966657374203d207b0a202061706956657273696f6e3a202265646963742e7461726765742d70726f66696c652f7631222c0a202069643a20747374722c202020202020202020202020202020202020202020202020203b20652e672e20226563686f2e64706f220a202076657273696f6e3a20747374722c20202020202020202020202020202020202020203b20652e672e202231220a20206163636570746564436f72654162693a205b2b20747374725d2c20202020202020203b20652e672e205b2265646963742e636f72652f7631225d0a0a2020696e7472696e736963733a207265736f757263652d7265662c0a2020696e7472696e7369634e616d6573706163653a20747374722c0a20203b207075626c697368657320746869732070726f66696c652773206f7065726174696f6e2d70726f66696c65207265636f72647320286f707469632074656d706c6174657320746861740a20203b206070726f66696c65602f60696d706c656d656e74736020636c6175736573207265736f6c766520616761696e7374292e205265666572656e63657320616e0a20203b206f7065726174696f6e2d70726f66696c65732d646f63756d656e74202845444943542d4142492d4f5050524f46494c452d534c4f542d303031292e0a20206f7065726174696f6e50726f66696c65733a207265736f757263652d7265662c0a2020666f6f747072696e74416c67656272613a207265736f757263652d7265662c0a2020636f7374416c67656272613a207265736f757263652d7265662c0a202074617267657449723a207265736f757263652d7265662c0a20206f62737472756374696f6e5461786f6e6f6d793a207265736f757263652d7265662c0a202076657269666965723a207265736f757263652d7265662c0a20206c6f77657265723a207265736f757263652d7265662c0a202073616e64626f783a207265736f757263652d7265662c0a20206675656c4d6f64656c3a207265736f757263652d7265662c0a0a20203b206669656c647320746865206c616e67756167652073706563207265717569726573206f662065766572792070726f66696c650a202062756e646c6550726f66696c653a207265736f757263652d7265662c0a202067656e657261746564417274696661637450726f66696c65733a205b2a207265736f757263652d7265665d2c0a202063616e6f6e6963616c456e636f64696e6752756c65733a207265736f757263652d7265662c0a20203b2052657365727665642f64656665727265643a2077696c6c206c697374206163636570746564206c61777061636b2d616461707465722041424920696473206f6e636520746861740a20203b20627974652d6c6576656c20414249202865646963742e6c61777061636b2d616461707465722f763129206973207370656369666965642e2054686520736368656d6120656e666f726365730a20203b20656d7074696e657373206e6f7720e280942074686520747970652069732074686520656d7074792061727261792c20736f206e6f6e2d656d7074792076616c756573206172650a20203b2072656a65637465642c206e6f74206d6572656c7920646973636f757261676564202845444943542d4142492d4c41575041434b2d414441505445522d44454645522d303031292e0a20203f2061636365707465644c61777061636b416461707465724162693a205b5d2c0a2020646961676e6f737469634162693a207265736f757263652d7265662c0a0a20203b206170706c69636174696f6e20646f637472696e650a20206170706c69636174696f6e4d6f64656c3a202261746f6d6963222c0a202072656164436f6e73697374656e63793a20226170706c69636174696f6e2d736e617073686f7422202f20747374722c0a202067756172644576616c756174696f6e3a2022707265636f6d6d69742d61746f6d696322202f20747374722c0a20206f62737472756374696f6e526f6c6c6261636b3a20226e6f2d76697369626c652d6566666563747322202f20747374722c0a20206d756c74695461726765743a20626f6f6c2c0a20203b207768657468657220746865207461726765742063616e206576616c7561746520707265636f6d6d697420706f7374636f6e646974696f6e20286067756172616e746565602920636865636b730a20203b20696e73696465207468652061746f6d6963206170706c69636174696f6e20756e6974202845444943542d5441524745542d504f5354434f4e442d303031290a2020706f7374636f6e646974696f6e537570706f72743a20626f6f6c2c0a0a202064657465726d696e6973746963457865637574696f6e3a207265736f757263652d7265662c0a2020636f6e666f726d616e636546697874757265436f727075733a207265736f757263652d7265662c0a7d0a0a3b20536861726564207479706573207265736f757263652d72656620616e64207368613235362d6469676573742061726520646566696e6564206f6e636520696e0a3b2065646963742d636f6d6d6f6e2e6364646c20616e6420617373656d626c65642077697468207468697320736368656d6120627920746865206275696c640a3b202845444943542d4142492d4e4f4455502d303031292e205468657920617265206e6f74207265646566696e656420686572652e0a0a3b202d2d2d20696e7472696e736963207369676e6174757265202d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d0a3b20546865206172746966616374207265666572656e63656420627920746865206d616e696665737427732060696e7472696e7369637360207265736f757263652d726566206973207468650a3b20696e7472696e7369632d7369676e617475726520636f7270757320646f63756d656e742062656c6f772e20497473206c61796f757420697320666978656420736f2074776f0a3b20696e646570656e64656e742070726f66696c65732076616c69646174652f686173682074686520636f72707573206964656e746963616c6c790a3b202845444943542d4142492d494e5452494e534943532d444f432d303031292e0a0a3b20696e7472696e736963732069732061204d4150206b6579656420627920636f6f7264696e6174652c20736f2074686520736368656d6120697473656c6620656e666f726365730a3b20636f6f7264696e61746520756e697175656e6573732e20412070726f766964657220726563656976657320746865207265736f6c76656420636f7270757320617320610a3b206469676573742d626f756e642073656d616e74696320696e70757420616e64207265736f6c76657320636f6f7264696e617465732077697468696e20746861742061727469666163742e0a3b2045616368206d6170206b6579204d55535420657175616c20697473207265636f726427732060636f6f7264696e61746560206669656c640a3b202845444943542d4142492d494e5452494e5349432d554e495155452d303031292e0a696e7472696e736963732d646f63756d656e74203d207b0a202061706956657273696f6e3a202265646963742e7461726765742d70726f66696c652e696e7472696e736963732f7631222c0a2020696e7472696e736963733a207b202a2074737472203d3e20696e7472696e736963207d2c0a7d0a0a3b20546865206172746966616374207265666572656e63656420627920746865206d616e6966657374277320606f7065726174696f6e50726f66696c657360207265736f757263652d7265662e0a3b206f7065726174696f6e2d70726f66696c65202f206f707469632d74656d706c6174652061726520646566696e656420696e2065646963742d636f6d6d6f6e2e6364646c2e204b657965642062790a3b20636f6f7264696e61746520736f207265736f6c7574696f6e2063616e2774207069636b206265747765656e2074776f2073616d652d636f6f7264696e6174652070726f66696c65730a3b202845444943542d4142492d4f5050524f46494c452d534c4f542d3030312c2045444943542d4142492d4f5050524f46494c452d554e495155452d303031292e0a6f7065726174696f6e2d70726f66696c65732d646f63756d656e74203d207b0a202061706956657273696f6e3a202265646963742e7461726765742d70726f66696c652e6f7065726174696f6e2d70726f66696c65732f7631222c0a202070726f66696c65733a207b202a2074737472203d3e206f7065726174696f6e2d70726f66696c65207d2c0a7d0a0a3b2041207479706564207072652d6c6f776572696e67207175657374696f6e20746861742063616e2062652070726f706f73656420627920576174736f6e206f7220616e206167656e7420616e640a3b20636865636b65642062792074686520636f6d70696c65722e2049742069732063616e6f6e6963616c2d43424f5220656e636f64656420756e6465720a3b206065646963742e6c6f776572696e672d726571756972656d656e74732f7631603b2074686520636f6d70696c657220636865636b7320746869732061727469666163742c206e6f74207468650a3b2070726f736520746861742070726f64756365642069742e0a6c6f776572696e672d726571756972656d656e7473203d207b0a202061706956657273696f6e3a202265646963742e6c6f776572696e672d726571756972656d656e74732f7631222c0a20206f7065726174696f6e50726f66696c653a20747374722c0a202073656d616e746963456666656374733a205b2a2073656d616e7469632d6566666563742d726571756972656d656e745d2c0a202072657175697265645772697465436c61737365733a205b2a2077726974652d636c6173735d2c0a202067756172644b696e64733a205b2a2067756172642d6b696e645d2c0a202061746f6d69636974793a2061746f6d69636974792d726571756972656d656e742c0a2020706f7374636f6e646974696f6e537570706f72743a20626f6f6c2c0a20206f62737472756374696f6e436f6f7264696e617465733a205b2a20747374725d2c0a2020666f6f747072696e744f626c69676174696f6e733a205b2a20747374725d2c0a2020636f73744f626c69676174696f6e733a205b2a20747374725d2c0a20206f70746963436f6e74726163743a20747374722c0a7d0a0a73656d616e7469632d6566666563742d726571756972656d656e74203d207b0a2020636f6f7264696e6174653a20747374722c0a20207772697465436c6173733a2077726974652d636c6173732c0a202067756172644b696e64733a205b2a2067756172642d6b696e645d2c0a20206f62737472756374696f6e436f6f7264696e617465733a205b2a20747374725d2c0a2020666f6f747072696e744f626c69676174696f6e733a205b2a20747374725d2c0a2020636f73744f626c69676174696f6e733a205b2a20747374725d2c0a7d0a0a77726974652d636c617373203d20226e6f6e6522202f20227265616422202f202263726561746522202f2022656e7375726522202f2022617070656e6422202f0a2020202020202020202020202020227265706c61636522202f202264656c65746522202f20747374720a67756172642d6b696e64203d2022707265636f6d6d69742d61746f6d696322202f20747374720a61746f6d69636974792d726571756972656d656e74203d202261746f6d696322202f20747374720a0a3b20412067656e75696e6520756e696f6e3a207075726520636f6e7374727563746f7273206361727279206e6f20656666656374206b696e64206f72206661696c757265733b206566666563740a3b20696e7472696e73696373206d757374202845444943542d5441524745542d494e5452494e5349432d434c4153532d303031292e2054686520736368656d6120656e666f7263657320746869732c0a3b206e6f74206120636f6d6d656e742e0a0a3b2054686520696e7472696e736963277320636f6f7264696e6174652069732074686520696e7472696e73696373206d6170204b45592c206e6f7420612076616c7565206669656c642c20736f207468650a3b206b657920616e6420636f6f7264696e6174652063616e206e65766572206469736167726565202845444943542d4142492d494e5452494e5349432d554e495155452d303031292e0a696e7472696e736963203d20707572652d696e7472696e736963202f206566666563742d696e7472696e7369630a0a707572652d696e7472696e736963203d207b0a2020696e7472696e736963436c6173733a202270757265222c0a202074797065506172616d65746572733a205b2a20747374725d2c0a2020617267756d656e7454797065733a205b2a20636f72652d747970652d7265665d2c0a202072657475726e547970653a20636f72652d747970652d7265662c0a20206775617264537570706f72743a2066616c73652c0a2020666f6f747072696e7454656d706c6174653a20747374722c0a2020636f737454656d706c6174653a20747374722c0a20207772697465436c6173733a20226e6f6e65222c0a7d0a0a6566666563742d696e7472696e736963203d207b0a2020696e7472696e736963436c6173733a2022656666656374222c0a202074797065506172616d65746572733a205b2a20747374725d2c0a2020617267756d656e7454797065733a205b2a20636f72652d747970652d7265665d2c0a202072657475726e547970653a20636f72652d747970652d7265662c0a20206566666563744b696e643a206566666563742d6b696e642c0a20203b206d6170206b65796564206279206661696c75726520636f6f7264696e61746520286661696c7572652d6964656e74293b20746865206661696c75726520636f6f7264696e6174652069730a20203b20746865206b65792c206e6f7420612076616c7565206669656c642c20736f20756e697175656e657373206973207374727563747572616c0a20203b202845444943542d4142492d4641494c5552452d554e495155452d303031292e0a20206566666563744661696c757265733a207b202a206661696c7572652d6964656e74203d3e206566666563742d6661696c7572652d626f6479207d2c0a20206775617264537570706f72743a20626f6f6c2c0a2020666f6f747072696e7454656d706c6174653a20747374722c0a2020636f737454656d706c6174653a20747374722c0a20207772697465436c6173733a20227265616422202f202263726561746522202f2022656e7375726522202f2022617070656e6422202f20227265706c61636522202f0a20202020202020202020202020202264656c65746522202f2022637573746f6d222c0a202063616e5061727469636970617465496e41746f6d696347756172643a20626f6f6c2c0a7d0a0a3b206566666563742d6661696c7572652d626f64792c206566666563742d6b696e642c20617574686f726974792d636c6173732c20616e6420636f72652d747970652d7265662061726520646566696e65640a3b206f6e636520696e2065646963742d636f6d6d6f6e2e6364646c20616e6420617373656d626c65642077697468207468697320736368656d61202845444943542d4142492d4e4f4455502d303031292e0a0a3b202d2d2d2065646963742d617574686f726974792d66616374732e6364646c202d2d2d0a3b2065646963742d617574686f726974792d66616374732e6364646c0a3b2043616e6f6e6963616c20736368656d6120666f722074686520666972737420636f6d70696c65722d636f6e7465787420617574686f726974792d666163747320646f63756d656e742e0a3b20417574686f7269746174697665206279746520656e636f64696e673a2065646963742e63616e6f6e6963616c2d63626f722f76312e0a3b0a3b205468697320736368656d6120697320617373656d626c656420776974682065646963742d636f6d6d6f6e2e6364646c20736f20736f757263652e6469676573742075736573207468650a3b20736861726564207368613235362d6469676573742074797065642076616c75652e204a534f4e2069732061207265766965772f696e7075742072656e646572696e673a206974730a3b20607368613235363a3c3634206865783e6020736f75726365206469676573742069732070726f6a656374656420746f205b60736861323536602c203332207261772062797465735d206f6e0a3b2074686520776972652c20616e64206974732066616374206172726179732070726f6a65637420746f2074686520636f6f7264696e6174652d6b65796564206d6170732062656c6f772e0a0a617574686f726974792d6661637473203d207b0a202061706956657273696f6e3a202265646963742e617574686f726974792d66616374732f7631222c0a2020736f757263653a20617574686f726974792d666163742d736f757263652c0a20206f7065726174696f6e50726f66696c65733a207b202a2074737472203d3e20617574686f726974792d6f7065726174696f6e2d70726f66696c652d66616374207d2c0a20206566666563745772697465436c61737365733a207b202a2074737472203d3e20617574686f726974792d77726974652d636c617373207d2c0a2020627564676574733a207b202a2074737472203d3e20617574686f726974792d6275646765742d66616374207d2c0a7d0a0a617574686f726974792d666163742d736f75726365203d207b0a20206b696e643a20226c61777061636b22202f202274617267657450726f66696c65222c0a2020636f6f7264696e6174653a20747374722c0a20206469676573743a207368613235362d6469676573742c0a7d0a0a3b20546865206d6170206b65792069732074686520736f75726365206f7065726174696f6e2d70726f66696c6520636f6f7264696e6174652e204974206973206e6f7420726570656174656420696e0a3b207468652076616c75652c20736f2061206b657920616e6420656d62656464656420636f6f7264696e6174652063616e6e6f742064697361677265652e20416c6c6f7765642077726974650a3b20636c61737365732061726520612063616e6f6e6963616c206d61702d7365743a2074686520636c6173732069732074686520756e69717565206b657920616e64206e756c6c206973207468650a3b20756e6974206d61726b65722e2043616e6f6e6963616c2043424f52206669786573206b6579206f7264657220776974686f75742061207365636f6e64206f72646572696e672072756c652e0a617574686f726974792d6f7065726174696f6e2d70726f66696c652d66616374203d207b0a2020636f72653a20747374722c0a2020616c6c6f7765645772697465436c61737365733a207b202a20617574686f726974792d77726974652d636c617373203d3e206e756c6c207d2c0a7d0a0a3b20546865206566666563745772697465436c6173736573206d6170206b6579206973207468652073656d616e7469632065666665637420636f6f7264696e6174652e2054686520627564676574730a3b206d6170206b65792069732074686520736f757263652062756467657420636f6f7264696e6174652e2043616e6f6e6963616c2043424f52206d61702d6b657920756e697175656e6573730a3b206d616b6573206475706c6963617465206661637420636f6f7264696e61746573207374727563747572616c6c7920756e726570726573656e7461626c652e0a617574686f726974792d6275646765742d66616374203d207b0a20206d617853746570733a2075696e742c0a20206d6178416c6c6f636174656442797465733a2075696e742c0a20206d61784f757470757442797465733a2075696e742c0a7d0a0a3b20417574686f726974794661637473446f63756d656e7420763120696e74656e74696f6e616c6c792061636365707473206f6e6c792074686520777269746520636c6173736573207468650a3b2063757272656e7420636f6d70696c6572206d6f64656c2063616e20636f6e73756d652e2060637573746f6d602069732074686520736f6c6520763120637573746f6d207370656c6c696e673b0a3b20617262697472617279207461726765742d70726f66696c6520657874656e73696f6e20737472696e677320646f206e6f7420656e746572207468697320636f6d70696c657220706174682e0a617574686f726974792d77726974652d636c617373203d20226e6f6e6522202f20227265616422202f202263726561746522202f2022656e7375726522202f2022617070656e6422202f0a202020202020202020202020202020202020202020202020227265706c61636522202f202264656c65746522202f2022637573746f6d220a0a3b202d2d2d2065646963742d7461726765742d69722e6364646c202d2d2d0a3b20535044582d4c6963656e73652d4964656e7469666965723a204170616368652d322e300a3b2065646963742d7461726765742d69722e6364646c0a3b2043616e6f6e6963616c20736368656d6120666f72207468652045646963742d6f776e65642054617267657420495220617274696661637420656e76656c6f70652e0a3b20417574686f7269746174697665206279746520656e636f64696e673a2065646963742e63616e6f6e6963616c2d63626f722f76312e0a3b0a3b205468697320736368656d6120697320617373656d626c656420776974682065646963742d636f6d6d6f6e2e6364646c20616e642065646963742d636f72652e6364646c2e2049740a3b2064656c696265726174656c792072657573657320436f72652065787072657373696f6e732c20707265646963617465732c20627564676574732c206c6f63616c207265666572656e6365732c0a3b206f62737472756374696f6e20726561736f6e732c20616e64206f62737472756374696f6e2061726d7320736f2074686520736368656d61206d617463686573207468652076616c75650a3b20656d6974746564206279207468652063616e6f6e6963616c2054617267657420495220656e636f64657220726174686572207468616e20726573746174696e672074686f73652074797065732e0a3b2049742064657363726962657320746865207374727563747572616c207368617065206f662076616c6964206c6f776572696e672d70726f6475636564206172746966616374732e205468650a3b206c6f776572696e6720616e6420656e636f64657220636f6e7472616374732073657061726174656c7920656e666f7263652073656d616e746963206964656e7469666965722072756c65730a3b20616e642063616e6f6e6963616c206f72646572696e672f64656475706c69636174696f6e20666f72207365742d6c696b652076616c7565732e0a0a3b2054617267657420495220656e636f64696e672072656a6563747320616e20656d707479207461726765742d70726f66696c6520636f6f7264696e617465206265666f72652062797465730a3b2065786973742c20736f207468697320726f6f74207469676874656e732074686520736861726564207374727563747572616c207265736f757263652d726566206163636f7264696e676c792e0a7461726765742d69722d7265736f757263652d726566203d207b0a202069643a2074737472202e7265676578702022283f73292e2b222c0a20206469676573743a207368613235362d6469676573742c0a7d0a0a7461726765742d69722d6172746966616374203d207461726765742d69722d636c6f7365642d6172746966616374202f207461726765742d69722d6c65676163792d61727469666163740a0a7461726765742d69722d61727469666163742d636f6d6d6f6e203d20280a20206b696e643a202274617267657449724172746966616374222c0a2020646f6d61696e3a20747374722c0a202074617267657450726f66696c653a207461726765742d69722d7265736f757263652d7265662c0a2020736f75726365436f7265436f6f7264696e6174653a2074737472202e7265676578702022283f73292e2b222c0a290a0a7461726765742d69722d636c6f7365642d6172746966616374203d207b0a20207461726765742d69722d61727469666163742d636f6d6d6f6e2c0a202073656d616e746963436c6f737572653a207461726765742d69722d73656d616e7469632d636c6f737572652c0a2020696e74656e74733a207b202a2074737472203d3e207461726765742d69722d696e74656e74207d2c0a7d0a0a7461726765742d69722d6c65676163792d6172746966616374203d207b0a20207461726765742d69722d61727469666163742d636f6d6d6f6e2c0a2020696e74656e74733a207b202a2074737472203d3e207461726765742d69722d6c65676163792d696e74656e74207d2c0a7d0a0a7461726765742d69722d73656d616e7469632d636c6f73757265203d207b0a2020736f75726365436f72653a207461726765742d69722d7265736f757263652d7265662c0a20206c61777061636b733a205b2a207461726765742d69722d7265736f757263652d7265665d2c0a7d0a0a7461726765742d69722d696e74656e74203d207b0a20207461726765742d69722d696e74656e742d636f6d6d6f6e2c0a20203f2062617369733a20636f72652d657870722c0a7d0a0a7461726765742d69722d6c65676163792d696e74656e74203d207b0a20207461726765742d69722d696e74656e742d636f6d6d6f6e2c0a7d0a0a7461726765742d69722d696e74656e742d636f6d6d6f6e203d20280a20206f7065726174696f6e50726f66696c653a20747374722c0a2020696e707574436f6e73747261696e74733a205b2a20696e7075742d636f6e73747261696e745d2c0a2020636f72654576616c756174696f6e4275646765743a20636f72652d6275646765742c0a2020726571756972656d656e74733a205b2a207461726765742d69722d726571756972656d656e745d2c0a202073746570733a205b2a207461726765742d69722d737465705d2c0a2020726573756c743a20636f72652d657870722c0a290a0a7461726765742d69722d726571756972656d656e74203d207b0a202069643a20747374722c0a20207072656469636174653a20636f72652d7072656469636174652c0a20206f6e4661696c7572653a20726571756972652d6661696c7572652d61726d2c0a7d0a0a7461726765742d69722d73746570203d207b0a202069643a20747374722c0a202062696e64696e673a206c6f63616c2d7265662c0a20206566666563743a20747374722c0a2020746172676574496e7472696e7369633a20747374722c0a2020696e7075743a20636f72652d657870722c0a20206f62737472756374696f6e4661696c757265733a205b2a206661696c7572652d6964656e745d2c0a20206f62737472756374696f6e41726d733a207b202a206661696c7572652d6964656e74203d3e206f62737472756374696f6e2d61726d207d2c0a7d0a", - "rawSha256": "c98618a4f1f1037f9c61720039a04f8a54498b934141ae3c2ebc03e8f79de5c1" + "bytesHex": "3b20535044582d4c6963656e73652d4964656e7469666965723a204170616368652d322e300a3b2065646963742d70726f76696465722d636f6e7472616374732e6364646c0a3b2047656e6572617465642066726f6d2045646963742d6f776e65642041424920667261676d656e74732e20444f204e4f5420454449542e0a0a3b202d2d2d2065646963742d636f6d6d6f6e2e6364646c202d2d2d0a3b2065646963742d636f6d6d6f6e2e6364646c0a3b20536861726564204344444c20747970657320666f722074686520456469637420414249732c20646566696e6564204f4e4345206865726520736f20746865792063616e6e6f742064726966740a3b202845444943542d4142492d4e4f4455502d303031292e2054776f2067726f7570733a0a3b2020202d207265736f757263652d7265662c207368613235362d6469676573742c206566666563742d6661696c7572652d626f64792c206566666563742d6b696e642c20617574686f726974792d636c6173732c0a3b2020202020636f72652d747970652d7265663a20617373656d626c656420776974682065646963742d7461726765742d70726f66696c652e6364646c20616e640a3b202020202065646963742d6c61777061636b2e6364646c20627920746865206275696c643b2074686f736520736368656d617320646f206e6f74207265646566696e65207468656d2e0a3b2020202d206f7065726174696f6e2d70726f66696c652c206f707469632d74656d706c6174652c2061706572747572652d726571756972656d656e7420616e6420746865697220726566733a0a3b2020202020636f6e73756d65642062792074686520436f72652f6f70746963206c61796572202865646963742d636f72652e6364646c2920616e64207265666572656e636564206279207468650a3b20202020206c616e67756167652f7461726765742d70726f66696c652073706563732e0a3b20417574686f7269746174697665206279746520656e636f64696e673a2065646963742e63616e6f6e6963616c2d63626f722f76312e0a0a3b2041206e6f726d617469766520737562636f6d706f6e656e74207265666572656e636564206279206964656e7469747920706c7573206469676573742e204d616e696665737473206e657665720a3b20656d626564207468656972206f776e2073656c662d64696765737420696e20746865697220707265696d616765202845444943542d434f52452d53454c46484153482d303031292e0a7265736f757263652d726566203d207b2069643a20747374722c206469676573743a207368613235362d646967657374207d0a0a3b20446967657374732061726520617574686f726974617469766520617320747970656420627974652076616c7565732c206e657665722068657820737472696e67732e20526576696577204a534f4e0a3b2072656e64657273207468697320617320227368613235363a3c3634206c6f77657263617365206865783e22202845444943542d4449474553542d574952452d303031292e0a7368613235362d646967657374203d205b20616c676f726974686d3a2022736861323536222c2062797465733a2062737472202e73697a65203332205d0a0a3b2041206e616d6564206c6f772d6c6576656c206661696c75726520616e206566666563742063616e2072616973652e2054686520736f75726365206f62737472756374696f6e206d61700a3b2062696e64732069742028627920636f6f7264696e6174652920616e6420636f6e73747275637473206120747970656420646f6d61696e206f62737472756374696f6e2066726f6d206974730a3b207061796c6f6164202845444943542d4142492d4641494c5552452d4e414d45442d303031292e0a3b20416e20656666656374277320606566666563744661696c7572657360206c697374204d555354206861766520756e697175652060636f6f7264696e61746560733a2073696e6365207468650a3b206f62737472756374696f6e206d6170206973206b6579656420627920636f6f7264696e6174652c2074776f206661696c757265732073686172696e67206120636f6f7264696e61746520286576656e0a3b207769746820646966666572656e7420617574686f72697479436c6173732f7061796c6f61645479706529206d616b652065786861757374697665206d617070696e6720616e642062696e6465720a3b20747970696e6720616d626967756f757320616e64206172652072656a6563746564202845444943542d4142492d4641494c5552452d554e495155452d303031292e0a3b0a3b2045666665637473206361727279207468656972206661696c757265732061732061206d617020607b206661696c7572652d6964656e74203d3e206566666563742d6661696c7572652d626f6479207d600a3b202873656520746865207461726765742f6c61777061636b2065666665637420736368656d6173292e20546865206661696c75726520636f6f7264696e61746520697320746865206d61700a3b204b45592c20736f206974206973206e6f7420726570656174656420696e2074686520626f647920616e642063616e6e6f74206469736167726565207769746820746865206b65792e0a6566666563742d6661696c7572652d626f6479203d207b0a2020617574686f72697479436c6173733a20617574686f726974792d636c6173732c0a20207061796c6f6164547970653a20636f72652d747970652d7265662c202020202020202020202020203b2074797065642c20626f756e64656420286d617920626520656d707479207265636f7264290a7d0a0a3b2041206661696c75726520636f6f7264696e617465206d7573742062652061206261726520456469637420606964656e746020286c65747465722f756e64657273636f7265207468656e0a3b206c6574746572732f6469676974732f756e64657273636f7265732920414e44206d757374206e6f742062652061207265736572766564206b6579776f72642028652e672e2060656c7365602c0a3b20606261736973602c20607768657265602c206072657175697265602c2060666f72602c2060696660292e2054686520736f75726365206f62737472756374696f6e2d6d6170204c4853206f6e6c790a3b20616363657074732061206e6f6e2d6b6579776f726420606964656e74602c20736f20612068797068656e2f646f742f6b6579776f726420636f6f7264696e61746520776f756c642062650a3b204142492d76616c69642079657420696d706f737369626c6520746f206d617020657868617573746976656c7920696e20736f757263652e20546865207265676578206361707475726573207468650a3b206c65786963616c2073686170653b206b6579776f7264206578636c7573696f6e20697320616e206164646974696f6e616c2076616c69646174696f6e2072756c650a3b202845444943542d4142492d4641494c5552452d4944454e542d303031292e0a6661696c7572652d6964656e74203d2074737472202e72656765787020225b412d5a612d7a5f5d5b412d5a612d7a302d395f5d2a220a0a6566666563742d6b696e64203d20227265616422202f202263726561746522202f2022656e7375726522202f20227265706c61636522202f202264656c65746522202f0a202020202020202020202020202022617070656e6422202f202272656475636522202f202273656d616e7469632e656d697422202f2022637573746f6d220a0a617574686f726974792d636c617373203d2022646f6d61696e4d61707061626c6522202f20227061727469636970616e744f776e656422202f2022696e746567726974794661756c7422202f0a202020202020202020202020202020202020227265736f757263654661756c7422202f2022696e7465726e616c4661756c74220a0a636f72652d747970652d726566203d20747374722020203b2063616e6f6e6963616c20436f7265207479706520636f6f7264696e6174650a0a3b20416e206f7065726174696f6e2070726f66696c6520737570706c69657320746865206f707469632074656d706c617465206120436f726520696e74656e74207265736f6c766573206974730a3b206f707469634b696e642f626f756e646172794b696e642f737570706f7274506f6c6963792f6c6f7373446973706f736974696f6e2066726f6d2e205461726765742070726f66696c657320616e640a3b206c61777061636b73207075626c6973682074686573652061732061206d617020607b20636f6f7264696e617465203d3e206f7065726174696f6e2d70726f66696c65207d602c20736f207468650a3b20636f6f7264696e61746520697320746865204b45592c206e6f7420612076616c7565206669656c64202845444943542d4f505449432d54454d504c4154452d4f574e45522d3030312c0a3b2045444943542d4142492d4f5050524f46494c452d554e495155452d303031292e0a6f7065726174696f6e2d70726f66696c65203d207b0a20206f7074696354656d706c6174653a206f707469632d74656d706c6174652c0a20206566666563745072656469636174653a20747374722c20202020202020202020203b20636f6f7264696e617465206f6620746865206f7065726174696f6e2d6d6f6465207072656469636174650a7d0a0a6f707469632d74656d706c617465203d207b0a20206f707469634b696e643a2022726576656c6174696f6e22202f20226166666563745265696e746567726174696f6e222c0a2020626f756e646172794b696e643a202270726f6a656374696f6e22202f2022616666656374222c0a2020737570706f7274506f6c6963793a20747374722c202020202020202020202020203b2063616e6f6e6963616c20737570706f72742d706f6c69637920636f6f7264696e6174650a20206c6f7373446973706f736974696f6e3a20747374722c20202020202020202020203b2063616e6f6e6963616c206c6f73732d646973706f736974696f6e20636f6f7264696e6174650a20203f20626173697354656d706c6174653a20747374722c20202020202020202020203b206f7074696f6e616c206469676573742d6c6f636b65642062617369732074656d706c61746520636f6f72640a20203b2074686520617065727475726520726571756972656d656e7420746869732074656d706c61746520737570706c6965732e205265717569726564207768656e207468652074656d706c6174650a20203b2069732074686520736f75726365206f66206120436f7265206f707469632773206170657274757265526571756972656d656e742028692e652e2074686520696e74656e7420686173206e6f0a20203b20736f757263652060666f6f747072696e74203c3d202e2e2e60292c2073696e6365206170657274757265526571756972656d656e74206973206d616e6461746f727920696e20436f72650a20203b202845444943542d4f505449432d41504552545552452d5245462d303031292e0a20203f206170657274757265526571756972656d656e743a2061706572747572652d726571756972656d656e742c0a7d0a0a3b206170657274757265526571756972656d656e742069732061207479706564207265666572656e63652c206e65766572206120667265652d666f726d20737472696e672e2041207265766965770a3b2072656e646572696e67206d61792073686f772069747320636f6f7264696e617465202845444943542d4f505449432d41504552545552452d5245462d303031292e0a61706572747572652d726571756972656d656e74203d20666f6f747072696e742d6365696c696e672d726566202f2061627374726163742d666f6f747072696e742d6f626c69676174696f6e2d7265660a666f6f747072696e742d6365696c696e672d726566203d207b206b696e643a2022666f6f747072696e744365696c696e67222c207265663a2074737472207d0a61627374726163742d666f6f747072696e742d6f626c69676174696f6e2d726566203d207b206b696e643a20226162737472616374466f6f747072696e744f626c69676174696f6e222c207265663a2074737472207d0a0a3b202d2d2d2065646963742d636f72652e6364646c202d2d2d0a3b2065646963742d636f72652e6364646c0a3b204e6f726d617469766520736368656d6120666f722074686520456469637420436f72652076312073656d616e746963206d6f64656c2e0a3b0a3b2053636f706520626f756e646172793a20746869732066696c6520646566696e657320436f7265206d65616e696e6720616e6420736368656d61207368617065206f6e6c792e20497420646f65730a3b206e6f7420646566696e6520612063616e6f6e6963616c20656e636f6465722c20436f7265206d6f64756c652068617368206669656c64732c20686173682066697874757265732c207461726765740a3b206c6f776572696e672c2061646d697373696f6e2062756e646c65732c206f72207461726765742d6f776e65642049522e0a0a636f72652d6d6f64756c65203d207b0a202061706956657273696f6e3a202265646963742e636f72652f7631222c0a2020636f6f7264696e6174653a20747374722c0a2020696d706f7274733a205b2a20636f72652d696d706f72745d2c0a202074797065733a207b202a2074737472203d3e20636f72652d74797065207d2c0a2020696e74656e74733a207b202b2074737472203d3e20636f72652d696e74656e74207d2c0a20207265717569726564436f72654361706162696c69746965733a205b2a20747374725d2c0a7d0a0a636f72652d696d706f7274203d207b0a20206b696e643a20226c61777061636b22202f202274617267657422202f2022636f7265222c0a20207265663a207265736f757263652d7265662c0a20203f20616c6961733a20747374722c0a7d0a0a3b202d2d2d207479706573202d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d0a0a636f72652d74797065203d20636f72652d7363616c61722d74797065202f20636f72652d7265636f72642d74797065202f20636f72652d76617269616e742d74797065202f0a202020202020202020202020636f72652d6f7074696f6e2d74797065202f20636f72652d6c6973742d74797065202f20636f72652d6d61702d74797065202f0a202020202020202020202020636f72652d6361706162696c6974792d7265662d747970650a0a636f72652d7363616c61722d74797065203d20636f72652d626f6f6c2d74797065202f20636f72652d696e742d74797065202f20636f72652d737472696e672d74797065202f0a20202020202020202020202020202020202020636f72652d62797465732d74797065202f20636f72652d756e69742d747970650a0a636f72652d626f6f6c2d74797065203d207b206b696e643a2022426f6f6c22207d0a636f72652d756e69742d74797065203d207b206b696e643a2022556e697422207d0a636f72652d696e742d74797065203d207b0a20206b696e643a202249363422202f202255363422202f202249333222202f202255333222202f202249313622202f202255313622202f2022493822202f20225538222c0a7d0a636f72652d737472696e672d74797065203d207b0a20206b696e643a2022537472696e67222c0a20206d61783a2075696e742c0a202063616e6f6e6963616c3a2022756e69636f64652d7363616c61722d6e666322202f20227261772d75746638222c0a7d0a636f72652d62797465732d74797065203d207b0a20206b696e643a20224279746573222c0a20206d61783a2075696e742c0a7d0a636f72652d7265636f72642d74797065203d207b0a20206b696e643a20225265636f7264222c0a20206669656c64733a207b202a2074737472203d3e20636f72652d747970652d726566207d2c0a7d0a636f72652d76617269616e742d74797065203d207b0a20206b696e643a202256617269616e74222c0a202063617365733a207b202b2074737472203d3e2076617269616e742d636173652d626f6479207d2c0a7d0a76617269616e742d636173652d626f6479203d207b0a20203f207061796c6f61643a20636f72652d747970652d7265662c0a7d0a636f72652d6f7074696f6e2d74797065203d207b0a20206b696e643a20224f7074696f6e222c0a20206974656d3a20636f72652d747970652d7265662c0a7d0a636f72652d6c6973742d74797065203d207b0a20206b696e643a20224c697374222c0a20206974656d3a20636f72652d747970652d7265662c0a20206d61783a2075696e742c0a7d0a636f72652d6d61702d74797065203d207b0a20206b696e643a20224d6170222c0a20206b65793a20636f72652d747970652d7265662c0a202076616c75653a20636f72652d747970652d7265662c0a20206d61783a2075696e742c0a7d0a636f72652d6361706162696c6974792d7265662d74797065203d207b0a20206b696e643a20224361706162696c697479526566222c0a20206974656d3a20636f72652d747970652d7265662c0a7d0a0a3b20636f72652d747970652d72656620697320646566696e656420696e2065646963742d636f6d6d6f6e2e6364646c20616e6420617373656d626c65642077697468207468697320736368656d612e0a0a3b202d2d2d207265666572656e63657320616e642076616c756573202d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d0a0a3b204c6f63616c206964656e7469747920697320616c7068612d737461626c652e20606964602069732074686520636f6d70696c65722d6f776e6564206c6f63616c20636f6f7264696e6174653b0a3b2060616c7068614e616d656020697320746865206e6f726d616c697a65642068756d616e2f6465627567206e616d652e20536f757263652062696e646572207370656c6c696e67206973206e6f740a3b206964656e746974792e0a6c6f63616c2d726566203d207b0a202069643a20747374722c0a2020616c7068614e616d653a20747374722c0a2020747970653a20636f72652d747970652d7265662c0a7d0a0a636f72652d76616c7565203d20636f72652d6e756c6c2d76616c7565202f20636f72652d626f6f6c2d76616c7565202f20636f72652d696e742d76616c7565202f0a20202020202020202020202020636f72652d737472696e672d76616c7565202f20636f72652d62797465732d76616c7565202f20636f72652d7265636f72642d76616c7565202f0a20202020202020202020202020636f72652d76617269616e742d76616c7565202f20636f72652d6c6973742d76616c7565202f20636f72652d6d61702d76616c7565202f0a20202020202020202020202020636f72652d6361706162696c6974792d76616c75650a0a636f72652d6e756c6c2d76616c7565203d207b206b696e643a20226e756c6c22207d0a636f72652d626f6f6c2d76616c7565203d207b206b696e643a2022626f6f6c222c2076616c75653a20626f6f6c207d0a636f72652d696e742d76616c7565203d207b206b696e643a2022696e74222c2077696474683a20747374722c2076616c75653a20696e74207d0a636f72652d737472696e672d76616c7565203d207b206b696e643a2022737472696e67222c2076616c75653a2074737472207d0a636f72652d62797465732d76616c7565203d207b206b696e643a20226279746573222c2076616c75653a2062737472207d0a636f72652d7265636f72642d76616c7565203d207b206b696e643a20227265636f7264222c206669656c64733a207b202a2074737472203d3e20636f72652d76616c7565207d207d0a636f72652d76617269616e742d76616c7565203d207b0a20206b696e643a202276617269616e74222c0a2020747970653a20636f72652d747970652d7265662c0a2020636173653a20747374722c0a20203f207061796c6f61643a20636f72652d76616c75652c0a7d0a636f72652d6c6973742d76616c7565203d207b206b696e643a20226c697374222c2076616c7565733a205b2a20636f72652d76616c75655d207d0a636f72652d6d61702d76616c7565203d207b206b696e643a20226d6170222c20656e74726965733a205b2a205b6b65793a20636f72652d76616c75652c2076616c75653a20636f72652d76616c75655d5d207d0a636f72652d6361706162696c6974792d76616c7565203d207b0a20206b696e643a20226361706162696c697479222c0a2020726563656970743a207368613235362d6469676573742c0a7d0a0a3b2045646963742d617574686f72656420707572652068656c70657273207573652061207075726520436f72652066756e6374696f6e20626f64792e2054686520626f64792063616e2062696e640a3b20707572652065787072657373696f6e7320616e642072657475726e20616e2065787072657373696f6e2c206275742069742063616e6e6f7420636f6e7461696e20436f7265206566666563742c0a3b2067756172642c206272616e63682c206c6f6f702c206d617463682d6e6f64652c206f722070726f6f662d6f626c69676174696f6e206e6f6465732e0a636f72652d666e2d626f6479203d207b0a2020706172616d733a205b2a206c6f63616c2d7265665d2c0a2020626f64793a20636f72652d707572652d626c6f636b2c0a7d0a0a636f72652d707572652d626c6f636b203d207b0a20206c6f63616c733a205b2a206c6f63616c2d7265665d2c0a202062696e64696e67733a205b2a20707572652d6c65742d6e6f64655d2c0a2020726573756c743a20636f72652d657870722c0a7d0a0a707572652d6c65742d6e6f6465203d207b0a20206b696e643a20226c6574222c0a202062696e64696e673a206c6f63616c2d7265662c0a202076616c75653a20636f72652d657870722c0a7d0a0a3b202d2d2d2065787072657373696f6e7320616e642070726564696361746573202d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d0a0a636f72652d65787072203d206c6f63616c2d65787072202f20636f6e73742d65787072202f207265636f72642d65787072202f206669656c642d65787072202f0a20202020202020202020202076617269616e742d65787072202f206d617463682d65787072202f2063616c6c2d65787072202f206c6973742d65787072202f206d61702d65787072202f0a20202020202020202020202069662d657870720a0a6c6f63616c2d65787072203d207b206b696e643a20226c6f63616c222c207265663a206c6f63616c2d726566207d0a636f6e73742d65787072203d207b206b696e643a2022636f6e7374222c2076616c75653a20636f72652d76616c7565207d0a7265636f72642d65787072203d207b206b696e643a20227265636f7264222c206669656c64733a207b202a2074737472203d3e20636f72652d65787072207d207d0a6669656c642d65787072203d207b206b696e643a20226669656c64222c20626173653a20636f72652d657870722c206669656c643a2074737472207d0a76617269616e742d65787072203d207b0a20206b696e643a202276617269616e74222c0a2020747970653a20636f72652d747970652d7265662c0a2020636173653a20747374722c0a20203f207061796c6f61643a20636f72652d657870722c0a7d0a6d617463682d65787072203d207b0a20206b696e643a20226d61746368222c0a20207363727574696e65653a20636f72652d657870722c0a202061726d733a205b2b206d617463682d61726d5d2c0a7d0a6d617463682d61726d203d207b0a2020636173653a20747374722c0a20203f2062696e6465723a206c6f63616c2d7265662c0a2020626f64793a20636f72652d657870722c0a7d0a63616c6c2d65787072203d207b0a20206b696e643a202263616c6c222c0a202063616c6c65653a20747374722c0a202074797065417267733a205b2a20636f72652d747970652d7265665d2c0a2020617267733a205b2a20636f72652d657870725d2c0a7d0a6c6973742d65787072203d207b206b696e643a20226c697374222c2076616c7565733a205b2a20636f72652d657870725d207d0a6d61702d65787072203d207b206b696e643a20226d6170222c20656e74726965733a205b2a205b6b65793a20636f72652d657870722c2076616c75653a20636f72652d657870725d5d207d0a69662d65787072203d207b0a20206b696e643a20226966222c0a20207072656469636174653a20636f72652d7072656469636174652c0a20207468656e3a20636f72652d657870722c0a2020656c73653a20636f72652d657870722c0a7d0a0a636f72652d707265646963617465203d20747275652d707265646963617465202f2066616c73652d707265646963617465202f206e6f742d707265646963617465202f0a2020202020202020202020202020202020616c6c2d707265646963617465202f20616e792d707265646963617465202f20636f6d706172652d707265646963617465202f0a202020202020202020202020202020202063616c6c2d707265646963617465202f206f62737472756374696f6e2d7072656469636174650a0a747275652d707265646963617465203d207b206b696e643a20227472756522207d0a66616c73652d707265646963617465203d207b206b696e643a202266616c736522207d0a6e6f742d707265646963617465203d207b206b696e643a20226e6f74222c2076616c75653a20636f72652d707265646963617465207d0a616c6c2d707265646963617465203d207b206b696e643a2022616c6c222c2076616c7565733a205b2b20636f72652d7072656469636174655d207d0a616e792d707265646963617465203d207b206b696e643a2022616e79222c2076616c7565733a205b2b20636f72652d7072656469636174655d207d0a636f6d706172652d707265646963617465203d207b0a20206b696e643a2022636f6d70617265222c0a20206f703a20223d3d22202f2022213d22202f20223c22202f20223c3d22202f20223e22202f20223e3d222c0a20206c6566743a20636f72652d657870722c0a202072696768743a20636f72652d657870722c0a7d0a63616c6c2d707265646963617465203d207b0a20206b696e643a202263616c6c222c0a20207072656469636174653a20747374722c0a2020617267733a205b2a20636f72652d657870725d2c0a7d0a6f62737472756374696f6e2d707265646963617465203d207b0a20206b696e643a20226f62737472756374696f6e222c0a2020636f6f7264696e6174653a206661696c7572652d6964656e742c0a20207061796c6f61643a20636f72652d657870722c0a7d0a0a696e7075742d636f6e73747261696e74203d207b0a2020636f6f7264696e6174653a20747374722c0a2020736f757263653a2022776865726522202f2022636f6d70696c6572222c0a20207072656469636174653a20636f72652d7072656469636174652c0a7d0a0a3b202d2d2d20696e74656e74732c20626c6f636b732c20616e64206e6f646573202d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d0a0a636f72652d696e74656e74203d207b0a2020696e7075743a20636f72652d747970652d7265662c0a20206f75747075743a20636f72652d747970652d7265662c0a202072657175697265644f7065726174696f6e50726f66696c653a20747374722c0a20203f2062617369733a20636f72652d657870722c0a2020696e707574436f6e73747261696e74733a205b2a20696e7075742d636f6e73747261696e745d2c0a2020636f72654576616c756174696f6e4275646765743a20636f72652d6275646765742c0a2020626f64793a20636f72652d626c6f636b2c0a20203f206f707469633a20636f72652d6f707469632c0a7d0a0a636f72652d627564676574203d207b0a20206d617853746570733a2075696e742c0a20206d6178416c6c6f636174656442797465733a2075696e742c0a20206d61784f757470757442797465733a2075696e742c0a7d0a0a636f72652d6f70746963203d207b0a20206f707469634b696e643a2022726576656c6174696f6e22202f20226166666563745265696e746567726174696f6e222c0a2020626f756e646172794b696e643a202270726f6a656374696f6e22202f2022616666656374222c0a20206170657274757265526571756972656d656e743a2061706572747572652d726571756972656d656e742c0a2020737570706f7274506f6c6963793a20747374722c0a20206c6f7373446973706f736974696f6e3a20747374722c0a7d0a0a636f72652d626c6f636b203d207b0a20206c6f63616c733a205b2a206c6f63616c2d7265665d2c0a20206e6f6465733a205b2a20636f72652d6e6f64655d2c0a2020726573756c743a20636f72652d657870722c0a7d0a0a636f72652d6e6f6465203d206c65742d6e6f6465202f20726571756972652d6e6f6465202f206566666563742d6e6f6465202f2067756172642d6e6f6465202f206272616e63682d6e6f6465202f0a202020202020202020202020666f722d6e6f6465202f206d617463682d6e6f6465202f2070726f6f662d6f626c69676174696f6e2d6e6f64650a0a6c65742d6e6f6465203d207b0a20206b696e643a20226c6574222c0a202062696e64696e673a206c6f63616c2d7265662c0a202076616c75653a20636f72652d657870722c0a7d0a726571756972652d6e6f6465203d207b0a20206b696e643a202272657175697265222c0a20207072656469636174653a20636f72652d7072656469636174652c0a20206f6e4661696c7572653a20726571756972652d6661696c7572652d61726d2c0a7d0a726571756972652d6661696c7572652d61726d203d207465726d696e616c2d726571756972652d6661696c757265202f0a20202020202020202020202020202020202020202020636f6e74696e75652d6f6273747275637465642d726571756972652d6661696c7572650a7465726d696e616c2d726571756972652d6661696c757265203d207b0a20206b696e643a20227465726d696e616c222c0a2020726561736f6e3a206f62737472756374696f6e2d726561736f6e2c0a7d0a636f6e74696e75652d6f6273747275637465642d726571756972652d6661696c757265203d207b0a20206b696e643a2022636f6e74696e75654f627374727563746564222c0a2020726561736f6e3a206f62737472756374696f6e2d726561736f6e2c0a7d0a6f62737472756374696f6e2d726561736f6e203d207b0a2020726561736f6e4b696e643a20747374722c0a20207061796c6f61643a207b202a2074737472203d3e20636f72652d65787072207d2c0a7d0a6566666563742d6e6f6465203d207b0a20206b696e643a2022656666656374222c0a202062696e64696e673a206c6f63616c2d7265662c0a20206566666563743a20747374722c0a2020696e7075743a20636f72652d657870722c0a20206f62737472756374696f6e4d61703a207b202a206661696c7572652d6964656e74203d3e206f62737472756374696f6e2d61726d207d2c0a7d0a6f62737472756374696f6e2d61726d203d207b0a202062696e6465723a206c6f63616c2d7265662c0a202076616c75653a20636f72652d657870722c0a7d0a67756172642d6e6f6465203d207b0a20206b696e643a20226775617264222c0a20207072656469636174653a20636f72652d7072656469636174652c0a20206f62737472756374696f6e3a20636f72652d657870722c0a7d0a6272616e63682d6e6f6465203d207b0a20206b696e643a20226272616e6368222c0a20207072656469636174653a20636f72652d7072656469636174652c0a20207468656e3a20636f72652d626c6f636b2c0a2020656c73653a20636f72652d626c6f636b2c0a7d0a666f722d6e6f6465203d207b0a20206b696e643a2022666f72222c0a202062696e6465723a206c6f63616c2d7265662c0a2020697465723a20636f72652d657870722c0a2020626f756e643a20636f72652d626f756e642c0a2020626f64793a20636f72652d626c6f636b2c0a7d0a6d617463682d6e6f6465203d207b0a20206b696e643a20226d61746368222c0a20207363727574696e65653a20636f72652d657870722c0a202061726d733a205b2b206d617463682d626c6f636b2d61726d5d2c0a7d0a6d617463682d626c6f636b2d61726d203d207b0a2020636173653a20747374722c0a20203f2062696e6465723a206c6f63616c2d7265662c0a2020626f64793a20636f72652d626c6f636b2c0a7d0a70726f6f662d6f626c69676174696f6e2d6e6f6465203d207b0a20206b696e643a202270726f6f66222c0a2020636f6f7264696e6174653a20747374722c0a20207072656469636174653a20636f72652d7072656469636174652c0a7d0a0a636f72652d626f756e64203d206c69746572616c2d626f756e64202f20636f6f7264696e6174652d626f756e640a6c69746572616c2d626f756e64203d207b206b696e643a20226c69746572616c222c2076616c75653a2075696e74207d0a636f6f7264696e6174652d626f756e64203d207b206b696e643a2022636f6f7264696e617465222c207265663a2074737472207d0a0a3b20536861726564207265736f757263652d7265662c207368613235362d6469676573742c206661696c7572652d6964656e742c2061706572747572652d726571756972656d656e742c20616e640a3b20636f72652d747970652d7265662061726520646566696e6564206f6e636520696e2065646963742d636f6d6d6f6e2e6364646c2e0a0a3b202d2d2d2065646963742d6c61777061636b2e6364646c202d2d2d0a3b2065646963742d6c61777061636b2e6364646c0a3b2043616e6f6e6963616c20736368656d6120666f7220746865204564696374206c61777061636b206d616e696665737420616e64206578706f727420737572666163652e0a3b20417574686f7269746174697665206279746520656e636f64696e673a2065646963742e63616e6f6e6963616c2d63626f722f76312e204a534f4e20696e207468652070726f73652073706563730a3b2069732061207265766965772072656e646572696e672067656e6572617465642066726f6d207468697320736368656d61202845444943542d4142492d4e4f4455502d303031292e0a0a6c61777061636b2d6d616e6966657374203d207b0a202061706956657273696f6e3a202265646963742e6c61777061636b2f7631222c0a202069643a20747374722c0a202076657273696f6e3a20747374722c0a20206163636570746564436f72654162693a205b2b20747374725d2c0a2020646570656e64656e636965733a205b2a206c61777061636b2d6465705d2c202020202020202020203b20616379636c69632c206469676573742d6c6f636b6564202845444943542d4c41575041434b2d4441472d303031290a20206578706f7274733a207265736f757263652d7265662c0a20203f2074617267657441646170746572733a205b2b207461726765742d616461707465725d2c2020203b207265717569726564206f6e6c7920696620616e792072756e74696d6520656666656374206578697374730a20203f2068656c706572436f6d706f6e656e743a2065786563757461626c652d636f6d706f6e656e742c203b2065786563757461626c652068656c70657273206361727279207468656972206f776e2073616e64626f782b6675656c0a202076657269666965723a2076657269666965722c202020202020202020202020202020202020202020203b20636c61737369666965643a206465636c61726174697665206f722065786563757461626c650a2020636f6d7061746962696c6974793a207265736f757263652d7265662c0a2020636f6e666f726d616e636546697874757265436f727075733a207265736f757263652d7265662c0a7d0a0a3b2041207665726966696572206973206569746865722061206465636c617261746976652072756c6573657420286e6f2072756e74696d6529206f7220616e2065786563757461626c650a3b20636f6d706f6e656e742e20416e2065786563757461626c65207665726966696572204d55535420636172727920697473206f776e2073616e64626f7820616e64206675656c206d6f64656c2c0a3b20736f2074686520736368656d6120656e666f726365732074686174206e6f2065786563757461626c6520636f6d706f6e656e74206973206c65667420756e626f756e6465640a3b202845444943542d4142492d56455249464945522d424f554e442d303031292e0a7665726966696572203d206465636c617261746976652d7665726966696572202f2065786563757461626c652d76657269666965720a6465636c617261746976652d7665726966696572203d207b20636c6173733a20226465636c61726174697665222c2072756c657365743a207265736f757263652d726566207d0a65786563757461626c652d7665726966696572203d207b0a2020636c6173733a202265786563757461626c65222c0a2020636f6d706f6e656e743a207265736f757263652d7265662c0a202073616e64626f783a207265736f757263652d7265662c0a20206675656c4d6f64656c3a207265736f757263652d7265662c0a7d0a0a3b20416e792065786563757461626c6520636f6d706f6e656e7420697320626f756e64656420627920697473206f776e2073616e64626f78202b206675656c206d6f64656c2e0a65786563757461626c652d636f6d706f6e656e74203d207b0a2020636f6d706f6e656e743a207265736f757263652d7265662c0a202073616e64626f783a207265736f757263652d7265662c0a20206675656c4d6f64656c3a207265736f757263652d7265662c0a7d0a0a6c61777061636b2d646570203d207b2069643a20747374722c2076657273696f6e3a20747374722c206469676573743a207368613235362d646967657374207d0a0a3b20416461707465722073656c656374696f6e206b65797320534f4c454c59206f666620746865206469676573742d6c6f636b65642060616363657074656454617267657450726f66696c65600a3b20286974732060696460206973207468652070726f66696c652069643b206974732060646967657374602070696e73207468652065786163742070726f66696c652f76657273696f6e292e2054686572650a3b20617265206e6f20696e646570656e64656e7420646973706c617920737472696e6773207468617420636f756c64206469736167726565207769746820746865206c6f636b2c20736f20610a3b207265736f6c7665722063616e6e6f742062696e6420616e206164617074657220746f206f6e6520746172676574207768696c6520746865206c6f636b2070726f76657320616e6f746865720a3b202845444943542d4c41575041434b2d414441505445522d54415247455449522d303031292e0a7461726765742d61646170746572203d207b0a2020616363657074656454617267657450726f66696c653a207265736f757263652d7265662c202020203b206469676573742d6c6f636b65642c20617574686f72697461746976652073656c6563746f720a2020616363657074656454617267657449723a207265736f757263652d7265662c2020202020202020203b206469676573742d6c6f636b65640a2020616461707465723a207265736f757263652d7265662c0a7d0a0a3b20536861726564207479706573207265736f757263652d72656620616e64207368613235362d6469676573742061726520646566696e6564206f6e636520696e0a3b2065646963742d636f6d6d6f6e2e6364646c20616e6420617373656d626c65642077697468207468697320736368656d61202845444943542d4142492d4e4f4455502d303031292e0a0a3b202d2d2d206578706f72742073757266616365202d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d0a0a6c61777061636b2d6578706f727473203d207b0a202074797065733a205b2a206578706f727465642d747970655d2c0a2020636f6e7374616e74733a205b2a206578706f727465642d636f6e7374616e745d2c0a20207075726546756e6374696f6e733a205b2a20707572652d66756e6374696f6e5d2c0a2020656666656374733a205b2a2073656d616e7469632d6566666563745d2c0a20206f62737472756374696f6e733a205b2a206f62737472756374696f6e2d6465665d2c0a20203b206b65796564206279206f7065726174696f6e2d70726f66696c6520636f6f7264696e61746520e2869220756e697175656e65737320656e666f726365640a20203b202845444943542d4142492d4f5050524f46494c452d554e495155452d303031290a20203b206f7065726174696f6e2d70726f66696c65207265636f7264732074686973206c61777061636b206578706f72747320286f707469632074656d706c6174657320746861740a20203b2060696d706c656d656e7473602f6070726f66696c656020636c6175736573207265736f6c766520616761696e7374292e206f7065726174696f6e2d70726f66696c652069730a20203b20646566696e656420696e2065646963742d636f6d6d6f6e2e6364646c202845444943542d4142492d4f5050524f46494c452d534c4f542d303031292e0a20206f7065726174696f6e50726f66696c65733a207b202a2074737472203d3e206f7065726174696f6e2d70726f66696c65207d2c20203b206b6579656420627920636f6f7264696e6174650a7d0a0a6578706f727465642d7479706520202020203d207b20636f6f7264696e6174653a20747374722c20646566696e6974696f6e3a20636f72652d747970652d726566207d0a6578706f727465642d636f6e7374616e74203d207b20636f6f7264696e6174653a20747374722c20747970653a20636f72652d747970652d7265662c2076616c75653a20616e79207d0a0a3b204120707572652068656c7065722069732061206469736372696d696e6174656420756e696f6e2062792060736f75726365602c20736f2074686520736368656d6120697473656c660a3b2067756172616e7465657320616e20696d706c656d656e746174696f6e20657869737473202845444943542d4c41575041434b2d505552452d494d504c2d303031293a0a3b2020202d20226564696374223a20617574686f72656420696e2045646963742f436f72653b2074686520436f726520626f6479206973206361727269656420696e6c696e6520286861736865640a3b20202020207769746820746865206578706f72742073757266616365292e2054686520736368656d61207265717569726573207468652060626f647960206669656c642e0a3b2020202d2022636f6d706f6e656e74223a20696d706c656d656e746564206f7574736964652045646963743b2063617272696573206e6f20696e6c696e6520626f647920616e6420696e73746561640a3b20202020206361727269657320697473206f776e206469676573742d6c6f636b65642060696d706c656d656e746174696f6e60202873616e64626f78202b206675656c292e20497420646f65730a3b20202020206e6f7420646570656e64206f6e20746865206f7074696f6e616c206d616e69666573742d6c6576656c2068656c706572436f6d706f6e656e742e0a707572652d66756e6374696f6e203d2065646963742d707572652d66756e6374696f6e202f20636f6d706f6e656e742d707572652d66756e6374696f6e0a0a707572652d66756e6374696f6e2d636f6d6d6f6e203d20280a2020636f6f7264696e6174653a20747374722c0a202074797065506172616d65746572733a205b2a20747374725d2c0a2020706172616d6574657254797065733a205b2a20636f72652d747970652d7265665d2c2020202020203b20616c6c20626f756e6465640a202072657475726e547970653a20636f72652d747970652d7265662c20202020202020202020202020203b20626f756e6465640a2020636f737454656d706c6174653a20747374722c0a202064657465726d696e69736d436c6173733a2022746f74616c22202f2022746f74616c2d776974682d74797065642d646961676e6f73746963222c0a290a0a65646963742d707572652d66756e6374696f6e203d207b0a2020707572652d66756e6374696f6e2d636f6d6d6f6e2c0a2020736f757263653a20226564696374222c0a2020626f64793a20636f72652d666e2d626f64792c2020202020202020202020202020202020202020203b20696e6c696e652c20686173682d7369676e69666963616e740a7d0a0a636f6d706f6e656e742d707572652d66756e6374696f6e203d207b0a2020707572652d66756e6374696f6e2d636f6d6d6f6e2c0a2020736f757263653a2022636f6d706f6e656e74222c0a20203b20746865206469676573742d6c6f636b656420636f6d706f6e656e7420696d706c656d656e74696e6720746869732068656c7065722e2052657175697265642061742074686520736368656d610a20203b206c6576656c20736f206120636f6d706f6e656e742068656c7065722063616e206e657665722076616c696461746520776974686f7574206120686173682d626f756e642c0a20203b2073616e64626f782b6675656c2d64657363726962656420696d706c656d656e746174696f6e202845444943542d4c41575041434b2d505552452d494d504c2d303031292e0a2020696d706c656d656e746174696f6e3a2065786563757461626c652d636f6d706f6e656e742c0a7d0a0a3b20636f72652d666e2d626f647920697320646566696e65642062792065646963742d636f72652e6364646c20616e6420617373656d626c656420776974682074686973206c61777061636b0a3b20736368656d612e2049742069732061207075726520436f72652066756e6374696f6e20626f64792c206e6f7420616e206566666563742d63617061626c6520636f72652d626c6f636b2e0a0a73656d616e7469632d656666656374203d207b0a2020636f6f7264696e6174653a20747374722c0a202074797065506172616d65746572733a205b2a20747374725d2c0a2020696e707574547970653a20636f72652d747970652d7265662c2020202020202020202020202020203b20626f756e6465640a20206f7574707574547970653a20636f72652d747970652d7265662c20202020202020202020202020203b20626f756e6465640a2020657865637574696f6e436c6173733a202270726f6f664f6e6c7922202f202272756e74696d65222c2020203b206f7274686f676f6e616c20746f207772697465436c6173730a20206566666563744b696e6448696e743a206566666563742d6b696e642c0a2020666f6f747072696e744f626c69676174696f6e3a20747374722c0a2020636f73744f626c69676174696f6e3a20747374722c0a20206566666563744661696c757265733a207b202a206661696c7572652d6964656e74203d3e206566666563742d6661696c7572652d626f6479207d2c20203b206b6579656420627920636f6f7264696e6174653b20756e697175650a20206775617264537570706f72743a20626f6f6c2c0a7d0a0a6f62737472756374696f6e2d646566203d207b0a2020636f6f7264696e6174653a20747374722c0a2020617574686f72697479436c6173733a20617574686f726974792d636c6173732c0a20207061796c6f6164536368656d613a20636f72652d747970652d7265662c20202020202020202020203b2074797065642c20626f756e64656420286d617920626520656d707479207265636f7264290a7d0a0a3b206566666563742d6661696c7572652d626f64792c206566666563742d6b696e642c20617574686f726974792d636c6173732c20616e6420636f72652d747970652d7265662061726520646566696e65640a3b206f6e636520696e2065646963742d636f6d6d6f6e2e6364646c20616e6420617373656d626c65642077697468207468697320736368656d61202845444943542d4142492d4e4f4455502d303031292e0a0a3b202d2d2d2065646963742d6c61777061636b2d616461707465722e6364646c202d2d2d0a3b2065646963742d6c61777061636b2d616461707465722e6364646c0a3b2043616e6f6e6963616c20736368656d6120666f72206f6e6520646972656374206465636c61726174697665206c61777061636b2074617267657420616461707465722e0a3b20417574686f7269746174697665206279746520656e636f64696e673a2065646963742e63616e6f6e6963616c2d63626f722f76312e0a3b0a3b2054686520656e636c6f73696e67206c61777061636b206d616e69666573742073656c6563747320746865206578616374207461726765742070726f66696c652c207461726765742049522c0a3b20616e642061646170746572207265736f75726365206469676573742e2054686f7365206964656e74697469657320617265206e6f7420726570656174656420686572652e0a0a6c61777061636b2d61646170746572203d207b0a202061706956657273696f6e3a202265646963742e6c61777061636b2d616461707465722f7631222c0a2020636c6173733a20226465636c61726174697665222c0a20206f7065726174696f6e50726f66696c65733a207b0a202020202a2074737472203d3e206c61777061636b2d616461707465722d6f7065726174696f6e2d70726f66696c650a20207d2c0a2020656666656374496d706c656d656e746174696f6e733a207b0a202020202a2074737472203d3e206c61777061636b2d616461707465722d6566666563740a20207d2c0a2020627564676574733a207b0a202020202a2074737472203d3e206c61777061636b2d616461707465722d6275646765740a20207d2c0a7d0a0a3b204b657973206172652063616e6f6e6963616c206c61777061636b206f7065726174696f6e2d70726f66696c6520636f6f7264696e617465732e0a6c61777061636b2d616461707465722d6f7065726174696f6e2d70726f66696c65203d207b0a2020636f72653a20747374722c0a202073656d616e746963456666656374733a205b2b20747374725d2c0a7d0a0a3b204b657973206172652063616e6f6e6963616c206c61777061636b2073656d616e7469632d65666665637420636f6f7264696e617465732e20466f6f747072696e742c20636f73742c20616e640a3b206661696c757265206669656c6473206d7573742065786163746c792064697363686172676520746865206d61746368696e67206578706f72746564206566666563742e0a6c61777061636b2d616461707465722d656666656374203d207b0a2020746172676574496e7472696e7369633a20747374722c0a2020746172676574436f6e66696775726174696f6e3a207265736f757263652d7265662c0a20207772697465436c6173733a206c61777061636b2d616461707465722d77726974652d636c6173732c0a2020666f6f747072696e744f626c69676174696f6e3a20747374722c0a2020636f73744f626c69676174696f6e3a20747374722c0a20206661696c7572654d617070696e67733a207b202a206661696c7572652d6964656e74203d3e2074737472207d2c0a7d0a0a3b204b657973206172652063616e6f6e6963616c206578706f7274656420636f73742d6f626c69676174696f6e20636f6f7264696e617465732e0a6c61777061636b2d616461707465722d627564676574203d207b0a20206d617853746570733a2075696e742c0a20206d6178416c6c6f636174656442797465733a2075696e742c0a20206d61784f757470757442797465733a2075696e742c0a7d0a0a6c61777061636b2d616461707465722d77726974652d636c617373203d20226e6f6e6522202f20227265616422202f202263726561746522202f2022656e7375726522202f0a20202020202020202020202020202020202020202020202020202020202022617070656e6422202f20227265706c61636522202f202264656c65746522202f2022637573746f6d220a0a3b206661696c7572652d6964656e7420697320646566696e656420696e2065646963742d636f6d6d6f6e2e6364646c2e0a0a3b202d2d2d2065646963742d7461726765742d70726f66696c652e6364646c202d2d2d0a3b2065646963742d7461726765742d70726f66696c652e6364646c0a3b2043616e6f6e6963616c20736368656d6120666f7220746865204564696374207461726765742070726f66696c65206d616e69666573742e0a3b20417574686f7269746174697665206279746520656e636f64696e673a2065646963742e63616e6f6e6963616c2d63626f722f76310a3b202873656520535045435f636f6e74696e75756d2d636f6e74726163742d62756e646c652d76312e6d64292e204a534f4e20696e207468652070726f736520737065637320697320610a3b207265766965772072656e646572696e672067656e6572617465642066726f6d207468697320736368656d613b2074686973204344444c206973207468652073696e676c6520736f757263650a3b206f66207472757468202845444943542d4142492d4e4f4455502d303031292e0a0a7461726765742d70726f66696c652d6d616e6966657374203d207b0a202061706956657273696f6e3a202265646963742e7461726765742d70726f66696c652f7631222c0a202069643a20747374722c202020202020202020202020202020202020202020202020203b20652e672e20226563686f2e64706f220a202076657273696f6e3a20747374722c20202020202020202020202020202020202020203b20652e672e202231220a20206163636570746564436f72654162693a205b2b20747374725d2c20202020202020203b20652e672e205b2265646963742e636f72652f7631225d0a0a2020696e7472696e736963733a207265736f757263652d7265662c0a2020696e7472696e7369634e616d6573706163653a20747374722c0a20203b207075626c697368657320746869732070726f66696c652773206f7065726174696f6e2d70726f66696c65207265636f72647320286f707469632074656d706c6174657320746861740a20203b206070726f66696c65602f60696d706c656d656e74736020636c6175736573207265736f6c766520616761696e7374292e205265666572656e63657320616e0a20203b206f7065726174696f6e2d70726f66696c65732d646f63756d656e74202845444943542d4142492d4f5050524f46494c452d534c4f542d303031292e0a20206f7065726174696f6e50726f66696c65733a207265736f757263652d7265662c0a2020666f6f747072696e74416c67656272613a207265736f757263652d7265662c0a2020636f7374416c67656272613a207265736f757263652d7265662c0a202074617267657449723a207265736f757263652d7265662c0a20206f62737472756374696f6e5461786f6e6f6d793a207265736f757263652d7265662c0a202076657269666965723a207265736f757263652d7265662c0a20206c6f77657265723a207265736f757263652d7265662c0a202073616e64626f783a207265736f757263652d7265662c0a20206675656c4d6f64656c3a207265736f757263652d7265662c0a0a20203b206669656c647320746865206c616e67756167652073706563207265717569726573206f662065766572792070726f66696c650a202062756e646c6550726f66696c653a207265736f757263652d7265662c0a202067656e657261746564417274696661637450726f66696c65733a205b2a207265736f757263652d7265665d2c0a202063616e6f6e6963616c456e636f64696e6752756c65733a207265736f757263652d7265662c0a20203b20412070726f66696c65207468617420616363657074732074686520646972656374206465636c61726174697665206c61777061636b2d6164617074657220414249206e616d65732069740a20203b2065786163746c79206f6e63652e2050726f66696c6573207468617420646f206e6f7420636f6e73756d65206c61777061636b206164617074657273206c6561766520746869730a20203b206f7074696f6e616c20736c6f7420616273656e74206f7220656d7074792e0a20203f2061636365707465644c61777061636b416461707465724162693a205b5d202f205b2265646963742e6c61777061636b2d616461707465722f7631225d2c0a2020646961676e6f737469634162693a207265736f757263652d7265662c0a0a20203b206170706c69636174696f6e20646f637472696e650a20206170706c69636174696f6e4d6f64656c3a202261746f6d6963222c0a202072656164436f6e73697374656e63793a20226170706c69636174696f6e2d736e617073686f7422202f20747374722c0a202067756172644576616c756174696f6e3a2022707265636f6d6d69742d61746f6d696322202f20747374722c0a20206f62737472756374696f6e526f6c6c6261636b3a20226e6f2d76697369626c652d6566666563747322202f20747374722c0a20206d756c74695461726765743a20626f6f6c2c0a20203b207768657468657220746865207461726765742063616e206576616c7561746520707265636f6d6d697420706f7374636f6e646974696f6e20286067756172616e746565602920636865636b730a20203b20696e73696465207468652061746f6d6963206170706c69636174696f6e20756e6974202845444943542d5441524745542d504f5354434f4e442d303031290a2020706f7374636f6e646974696f6e537570706f72743a20626f6f6c2c0a0a202064657465726d696e6973746963457865637574696f6e3a207265736f757263652d7265662c0a2020636f6e666f726d616e636546697874757265436f727075733a207265736f757263652d7265662c0a7d0a0a3b20536861726564207479706573207265736f757263652d72656620616e64207368613235362d6469676573742061726520646566696e6564206f6e636520696e0a3b2065646963742d636f6d6d6f6e2e6364646c20616e6420617373656d626c65642077697468207468697320736368656d6120627920746865206275696c640a3b202845444943542d4142492d4e4f4455502d303031292e205468657920617265206e6f74207265646566696e656420686572652e0a0a3b202d2d2d20696e7472696e736963207369676e6174757265202d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d0a3b20546865206172746966616374207265666572656e63656420627920746865206d616e696665737427732060696e7472696e7369637360207265736f757263652d726566206973207468650a3b20696e7472696e7369632d7369676e617475726520636f7270757320646f63756d656e742062656c6f772e20497473206c61796f757420697320666978656420736f2074776f0a3b20696e646570656e64656e742070726f66696c65732076616c69646174652f686173682074686520636f72707573206964656e746963616c6c790a3b202845444943542d4142492d494e5452494e534943532d444f432d303031292e0a0a3b20696e7472696e736963732069732061204d4150206b6579656420627920636f6f7264696e6174652c20736f2074686520736368656d6120697473656c6620656e666f726365730a3b20636f6f7264696e61746520756e697175656e6573732e20412070726f766964657220726563656976657320746865207265736f6c76656420636f7270757320617320610a3b206469676573742d626f756e642073656d616e74696320696e70757420616e64207265736f6c76657320636f6f7264696e617465732077697468696e20746861742061727469666163742e0a3b2045616368206d6170206b6579204d55535420657175616c20697473207265636f726427732060636f6f7264696e61746560206669656c640a3b202845444943542d4142492d494e5452494e5349432d554e495155452d303031292e0a696e7472696e736963732d646f63756d656e74203d207b0a202061706956657273696f6e3a202265646963742e7461726765742d70726f66696c652e696e7472696e736963732f7631222c0a2020696e7472696e736963733a207b202a2074737472203d3e20696e7472696e736963207d2c0a7d0a0a3b20546865206172746966616374207265666572656e63656420627920746865206d616e6966657374277320606f7065726174696f6e50726f66696c657360207265736f757263652d7265662e0a3b206f7065726174696f6e2d70726f66696c65202f206f707469632d74656d706c6174652061726520646566696e656420696e2065646963742d636f6d6d6f6e2e6364646c2e204b657965642062790a3b20636f6f7264696e61746520736f207265736f6c7574696f6e2063616e2774207069636b206265747765656e2074776f2073616d652d636f6f7264696e6174652070726f66696c65730a3b202845444943542d4142492d4f5050524f46494c452d534c4f542d3030312c2045444943542d4142492d4f5050524f46494c452d554e495155452d303031292e0a6f7065726174696f6e2d70726f66696c65732d646f63756d656e74203d207b0a202061706956657273696f6e3a202265646963742e7461726765742d70726f66696c652e6f7065726174696f6e2d70726f66696c65732f7631222c0a202070726f66696c65733a207b202a2074737472203d3e206f7065726174696f6e2d70726f66696c65207d2c0a7d0a0a3b2041207479706564207072652d6c6f776572696e67207175657374696f6e20746861742063616e2062652070726f706f73656420627920576174736f6e206f7220616e206167656e7420616e640a3b20636865636b65642062792074686520636f6d70696c65722e2049742069732063616e6f6e6963616c2d43424f5220656e636f64656420756e6465720a3b206065646963742e6c6f776572696e672d726571756972656d656e74732f7631603b2074686520636f6d70696c657220636865636b7320746869732061727469666163742c206e6f74207468650a3b2070726f736520746861742070726f64756365642069742e0a6c6f776572696e672d726571756972656d656e7473203d207b0a202061706956657273696f6e3a202265646963742e6c6f776572696e672d726571756972656d656e74732f7631222c0a20206f7065726174696f6e50726f66696c653a20747374722c0a202073656d616e746963456666656374733a205b2a2073656d616e7469632d6566666563742d726571756972656d656e745d2c0a202072657175697265645772697465436c61737365733a205b2a2077726974652d636c6173735d2c0a202067756172644b696e64733a205b2a2067756172642d6b696e645d2c0a202061746f6d69636974793a2061746f6d69636974792d726571756972656d656e742c0a2020706f7374636f6e646974696f6e537570706f72743a20626f6f6c2c0a20206f62737472756374696f6e436f6f7264696e617465733a205b2a20747374725d2c0a2020666f6f747072696e744f626c69676174696f6e733a205b2a20747374725d2c0a2020636f73744f626c69676174696f6e733a205b2a20747374725d2c0a20206f70746963436f6e74726163743a20747374722c0a7d0a0a73656d616e7469632d6566666563742d726571756972656d656e74203d207b0a2020636f6f7264696e6174653a20747374722c0a20207772697465436c6173733a2077726974652d636c6173732c0a202067756172644b696e64733a205b2a2067756172642d6b696e645d2c0a20206f62737472756374696f6e436f6f7264696e617465733a205b2a20747374725d2c0a2020666f6f747072696e744f626c69676174696f6e733a205b2a20747374725d2c0a2020636f73744f626c69676174696f6e733a205b2a20747374725d2c0a7d0a0a77726974652d636c617373203d20226e6f6e6522202f20227265616422202f202263726561746522202f2022656e7375726522202f2022617070656e6422202f0a2020202020202020202020202020227265706c61636522202f202264656c65746522202f20747374720a67756172642d6b696e64203d2022707265636f6d6d69742d61746f6d696322202f20747374720a61746f6d69636974792d726571756972656d656e74203d202261746f6d696322202f20747374720a0a3b20412067656e75696e6520756e696f6e3a207075726520636f6e7374727563746f7273206361727279206e6f20656666656374206b696e64206f72206661696c757265733b206566666563740a3b20696e7472696e73696373206d757374202845444943542d5441524745542d494e5452494e5349432d434c4153532d303031292e2054686520736368656d6120656e666f7263657320746869732c0a3b206e6f74206120636f6d6d656e742e0a0a3b2054686520696e7472696e736963277320636f6f7264696e6174652069732074686520696e7472696e73696373206d6170204b45592c206e6f7420612076616c7565206669656c642c20736f207468650a3b206b657920616e6420636f6f7264696e6174652063616e206e65766572206469736167726565202845444943542d4142492d494e5452494e5349432d554e495155452d303031292e0a696e7472696e736963203d20707572652d696e7472696e736963202f206566666563742d696e7472696e7369630a0a707572652d696e7472696e736963203d207b0a2020696e7472696e736963436c6173733a202270757265222c0a202074797065506172616d65746572733a205b2a20747374725d2c0a2020617267756d656e7454797065733a205b2a20636f72652d747970652d7265665d2c0a202072657475726e547970653a20636f72652d747970652d7265662c0a20206775617264537570706f72743a2066616c73652c0a2020666f6f747072696e7454656d706c6174653a20747374722c0a2020636f737454656d706c6174653a20747374722c0a20207772697465436c6173733a20226e6f6e65222c0a7d0a0a6566666563742d696e7472696e736963203d207b0a2020696e7472696e736963436c6173733a2022656666656374222c0a202074797065506172616d65746572733a205b2a20747374725d2c0a2020617267756d656e7454797065733a205b2a20636f72652d747970652d7265665d2c0a202072657475726e547970653a20636f72652d747970652d7265662c0a20206566666563744b696e643a206566666563742d6b696e642c0a20203b206d6170206b65796564206279206661696c75726520636f6f7264696e61746520286661696c7572652d6964656e74293b20746865206661696c75726520636f6f7264696e6174652069730a20203b20746865206b65792c206e6f7420612076616c7565206669656c642c20736f20756e697175656e657373206973207374727563747572616c0a20203b202845444943542d4142492d4641494c5552452d554e495155452d303031292e0a20206566666563744661696c757265733a207b202a206661696c7572652d6964656e74203d3e206566666563742d6661696c7572652d626f6479207d2c0a20206775617264537570706f72743a20626f6f6c2c0a2020666f6f747072696e7454656d706c6174653a20747374722c0a2020636f737454656d706c6174653a20747374722c0a20207772697465436c6173733a20227265616422202f202263726561746522202f2022656e7375726522202f2022617070656e6422202f20227265706c61636522202f0a20202020202020202020202020202264656c65746522202f2022637573746f6d222c0a202063616e5061727469636970617465496e41746f6d696347756172643a20626f6f6c2c0a7d0a0a3b206566666563742d6661696c7572652d626f64792c206566666563742d6b696e642c20617574686f726974792d636c6173732c20616e6420636f72652d747970652d7265662061726520646566696e65640a3b206f6e636520696e2065646963742d636f6d6d6f6e2e6364646c20616e6420617373656d626c65642077697468207468697320736368656d61202845444943542d4142492d4e4f4455502d303031292e0a0a3b202d2d2d2065646963742d617574686f726974792d66616374732e6364646c202d2d2d0a3b2065646963742d617574686f726974792d66616374732e6364646c0a3b2043616e6f6e6963616c20736368656d6120666f722074686520666972737420636f6d70696c65722d636f6e7465787420617574686f726974792d666163747320646f63756d656e742e0a3b20417574686f7269746174697665206279746520656e636f64696e673a2065646963742e63616e6f6e6963616c2d63626f722f76312e0a3b0a3b205468697320736368656d6120697320617373656d626c656420776974682065646963742d636f6d6d6f6e2e6364646c20736f20736f757263652e6469676573742075736573207468650a3b20736861726564207368613235362d6469676573742074797065642076616c75652e204a534f4e2069732061207265766965772f696e7075742072656e646572696e673a206974730a3b20607368613235363a3c3634206865783e6020736f75726365206469676573742069732070726f6a656374656420746f205b60736861323536602c203332207261772062797465735d206f6e0a3b2074686520776972652c20616e64206974732066616374206172726179732070726f6a65637420746f2074686520636f6f7264696e6174652d6b65796564206d6170732062656c6f772e0a0a617574686f726974792d6661637473203d207b0a202061706956657273696f6e3a202265646963742e617574686f726974792d66616374732f7631222c0a2020736f757263653a20617574686f726974792d666163742d736f757263652c0a20206f7065726174696f6e50726f66696c65733a207b202a2074737472203d3e20617574686f726974792d6f7065726174696f6e2d70726f66696c652d66616374207d2c0a20206566666563745772697465436c61737365733a207b202a2074737472203d3e20617574686f726974792d77726974652d636c617373207d2c0a2020627564676574733a207b202a2074737472203d3e20617574686f726974792d6275646765742d66616374207d2c0a7d0a0a617574686f726974792d666163742d736f75726365203d207b0a20206b696e643a20226c61777061636b22202f202274617267657450726f66696c65222c0a2020636f6f7264696e6174653a20747374722c0a20206469676573743a207368613235362d6469676573742c0a7d0a0a3b20546865206d6170206b65792069732074686520736f75726365206f7065726174696f6e2d70726f66696c6520636f6f7264696e6174652e204974206973206e6f7420726570656174656420696e0a3b207468652076616c75652c20736f2061206b657920616e6420656d62656464656420636f6f7264696e6174652063616e6e6f742064697361677265652e20416c6c6f7765642077726974650a3b20636c61737365732061726520612063616e6f6e6963616c206d61702d7365743a2074686520636c6173732069732074686520756e69717565206b657920616e64206e756c6c206973207468650a3b20756e6974206d61726b65722e2043616e6f6e6963616c2043424f52206669786573206b6579206f7264657220776974686f75742061207365636f6e64206f72646572696e672072756c652e0a617574686f726974792d6f7065726174696f6e2d70726f66696c652d66616374203d207b0a2020636f72653a20747374722c0a2020616c6c6f7765645772697465436c61737365733a207b202a20617574686f726974792d77726974652d636c617373203d3e206e756c6c207d2c0a7d0a0a3b20546865206566666563745772697465436c6173736573206d6170206b6579206973207468652073656d616e7469632065666665637420636f6f7264696e6174652e2054686520627564676574730a3b206d6170206b65792069732074686520736f757263652062756467657420636f6f7264696e6174652e2043616e6f6e6963616c2043424f52206d61702d6b657920756e697175656e6573730a3b206d616b6573206475706c6963617465206661637420636f6f7264696e61746573207374727563747572616c6c7920756e726570726573656e7461626c652e0a617574686f726974792d6275646765742d66616374203d207b0a20206d617853746570733a2075696e742c0a20206d6178416c6c6f636174656442797465733a2075696e742c0a20206d61784f757470757442797465733a2075696e742c0a7d0a0a3b20417574686f726974794661637473446f63756d656e7420763120696e74656e74696f6e616c6c792061636365707473206f6e6c792074686520777269746520636c6173736573207468650a3b2063757272656e7420636f6d70696c6572206d6f64656c2063616e20636f6e73756d652e2060637573746f6d602069732074686520736f6c6520763120637573746f6d207370656c6c696e673b0a3b20617262697472617279207461726765742d70726f66696c6520657874656e73696f6e20737472696e677320646f206e6f7420656e746572207468697320636f6d70696c657220706174682e0a617574686f726974792d77726974652d636c617373203d20226e6f6e6522202f20227265616422202f202263726561746522202f2022656e7375726522202f2022617070656e6422202f0a202020202020202020202020202020202020202020202020227265706c61636522202f202264656c65746522202f2022637573746f6d220a0a3b202d2d2d2065646963742d7461726765742d69722e6364646c202d2d2d0a3b20535044582d4c6963656e73652d4964656e7469666965723a204170616368652d322e300a3b2065646963742d7461726765742d69722e6364646c0a3b2043616e6f6e6963616c20736368656d6120666f72207468652045646963742d6f776e65642054617267657420495220617274696661637420656e76656c6f70652e0a3b20417574686f7269746174697665206279746520656e636f64696e673a2065646963742e63616e6f6e6963616c2d63626f722f76312e0a3b0a3b205468697320736368656d6120697320617373656d626c656420776974682065646963742d636f6d6d6f6e2e6364646c20616e642065646963742d636f72652e6364646c2e2049740a3b2064656c696265726174656c792072657573657320436f72652065787072657373696f6e732c20707265646963617465732c20627564676574732c206c6f63616c207265666572656e6365732c0a3b206f62737472756374696f6e20726561736f6e732c20616e64206f62737472756374696f6e2061726d7320736f2074686520736368656d61206d617463686573207468652076616c75650a3b20656d6974746564206279207468652063616e6f6e6963616c2054617267657420495220656e636f64657220726174686572207468616e20726573746174696e672074686f73652074797065732e0a3b2049742064657363726962657320746865207374727563747572616c207368617065206f662076616c6964206c6f776572696e672d70726f6475636564206172746966616374732e205468650a3b206c6f776572696e6720616e6420656e636f64657220636f6e7472616374732073657061726174656c7920656e666f7263652073656d616e746963206964656e7469666965722072756c65730a3b20616e642063616e6f6e6963616c206f72646572696e672f64656475706c69636174696f6e20666f72207365742d6c696b652076616c7565732e0a0a3b2054617267657420495220656e636f64696e672072656a6563747320616e20656d707479207461726765742d70726f66696c6520636f6f7264696e617465206265666f72652062797465730a3b2065786973742c20736f207468697320726f6f74207469676874656e732074686520736861726564207374727563747572616c207265736f757263652d726566206163636f7264696e676c792e0a7461726765742d69722d7265736f757263652d726566203d207b0a202069643a2074737472202e7265676578702022283f73292e2b222c0a20206469676573743a207368613235362d6469676573742c0a7d0a0a7461726765742d69722d6172746966616374203d207461726765742d69722d636c6f7365642d6172746966616374202f207461726765742d69722d6c65676163792d61727469666163740a0a7461726765742d69722d61727469666163742d636f6d6d6f6e203d20280a20206b696e643a202274617267657449724172746966616374222c0a2020646f6d61696e3a20747374722c0a202074617267657450726f66696c653a207461726765742d69722d7265736f757263652d7265662c0a2020736f75726365436f7265436f6f7264696e6174653a2074737472202e7265676578702022283f73292e2b222c0a290a0a7461726765742d69722d636c6f7365642d6172746966616374203d207b0a20207461726765742d69722d61727469666163742d636f6d6d6f6e2c0a202073656d616e746963436c6f737572653a207461726765742d69722d73656d616e7469632d636c6f737572652c0a2020696e74656e74733a207b202a2074737472203d3e207461726765742d69722d696e74656e74207d2c0a7d0a0a7461726765742d69722d6c65676163792d6172746966616374203d207b0a20207461726765742d69722d61727469666163742d636f6d6d6f6e2c0a2020696e74656e74733a207b202a2074737472203d3e207461726765742d69722d6c65676163792d696e74656e74207d2c0a7d0a0a7461726765742d69722d73656d616e7469632d636c6f73757265203d207b0a2020736f75726365436f72653a207461726765742d69722d7265736f757263652d7265662c0a20206c61777061636b733a205b2a207461726765742d69722d7265736f757263652d7265665d2c0a7d0a0a7461726765742d69722d696e74656e74203d207b0a20207461726765742d69722d696e74656e742d636f6d6d6f6e2c0a20203f2062617369733a20636f72652d657870722c0a7d0a0a7461726765742d69722d6c65676163792d696e74656e74203d207b0a20207461726765742d69722d696e74656e742d636f6d6d6f6e2c0a7d0a0a7461726765742d69722d696e74656e742d636f6d6d6f6e203d20280a20206f7065726174696f6e50726f66696c653a20747374722c0a2020696e707574436f6e73747261696e74733a205b2a20696e7075742d636f6e73747261696e745d2c0a2020636f72654576616c756174696f6e4275646765743a20636f72652d6275646765742c0a2020726571756972656d656e74733a205b2a207461726765742d69722d726571756972656d656e745d2c0a202073746570733a205b2a207461726765742d69722d737465705d2c0a2020726573756c743a20636f72652d657870722c0a290a0a7461726765742d69722d726571756972656d656e74203d207b0a202069643a20747374722c0a20207072656469636174653a20636f72652d7072656469636174652c0a20206f6e4661696c7572653a20726571756972652d6661696c7572652d61726d2c0a7d0a0a7461726765742d69722d73746570203d207b0a202069643a20747374722c0a202062696e64696e673a206c6f63616c2d7265662c0a20206566666563743a20747374722c0a2020746172676574496e7472696e7369633a20747374722c0a2020696e7075743a20636f72652d657870722c0a20206f62737472756374696f6e4661696c757265733a205b2a206661696c7572652d6964656e745d2c0a20206f62737472756374696f6e41726d733a207b202a206661696c7572652d6964656e74203d3e206f62737472756374696f6e2d61726d207d2c0a7d0a", + "rawSha256": "b01990008a21dfe34e24c5306c562d71cd9afb83d1355a5677a7b44d8efe5ea5" }, "contracts": [ {"contract": "authority-facts", "rootRule": "authority-facts"}, {"contract": "core-module", "rootRule": "core-module"}, + {"contract": "lawpack-adapter", "rootRule": "lawpack-adapter"}, {"contract": "lawpack-exports", "rootRule": "lawpack-exports"}, {"contract": "lawpack-manifest", "rootRule": "lawpack-manifest"}, {"contract": "lowering-requirements", "rootRule": "lowering-requirements"}, diff --git a/fixtures/providers/components/inventory.json b/fixtures/providers/components/inventory.json index 5446659..3420f39 100644 --- a/fixtures/providers/components/inventory.json +++ b/fixtures/providers/components/inventory.json @@ -7,5 +7,5 @@ "malformed-lowerer": "sha256:dfcd171918373d18b9dff16778e98b7618eeb4ac85976dd7134b9e201562f41b", "verifier": "sha256:9fa8e16ed7735075d559e3094685ce846d06425b4bb479be31f7498417bf87e4" }, - "sourceDigest": "sha256:247da4c15b0b174cca5fef9720d55914345d4b674fdd9eb6febe097425ac047a" + "sourceDigest": "sha256:0f4d719b50a0ec4073693905bf254ef7bf59d43b2ca2f2b4d2bfd8bea08b1992" } diff --git a/xtask/src/goldens.rs b/xtask/src/goldens.rs index b9c0644..038870c 100644 --- a/xtask/src/goldens.rs +++ b/xtask/src/goldens.rs @@ -358,6 +358,7 @@ fn target_ir_echo_facts() -> TargetIrLoweringFacts { effect_lowerings: vec![TargetEffectLowering { effect: "target.replace".to_owned(), target_intrinsic: "echo.dpo@1.replace".to_owned(), + failure_mappings: std::collections::BTreeMap::new(), }], } } @@ -374,6 +375,7 @@ fn target_ir_gitwarp_facts() -> TargetIrLoweringFacts { effect_lowerings: vec![TargetEffectLowering { effect: "gitwarp.appendEvent".to_owned(), target_intrinsic: "gitwarp.ref_crdt@1.appendEvent".to_owned(), + failure_mappings: std::collections::BTreeMap::new(), }], } } diff --git a/xtask/src/lawpack_goldens.rs b/xtask/src/lawpack_goldens.rs new file mode 100644 index 0000000..483dbdc --- /dev/null +++ b/xtask/src/lawpack_goldens.rs @@ -0,0 +1,751 @@ +use std::fmt::Write as _; +use std::fs; +use std::path::Path; + +use edict_syntax::{ + compile_to_core, decode_lawpack_adapter, decode_lawpack_bundle, digest_core_module, + digest_target_ir_artifact, encode_canonical_cbor, encode_core_module, + encode_target_ir_artifact, lower_to_target_ir, parse_module, prepare_lawpack_compilation, + CanonicalValue, TargetLoweringStatus, +}; +use sha2::{Digest, Sha256}; + +use crate::goldens::{check_golden_file_with_command, write_golden_file}; + +const DIGEST_FRAME: &str = "edict.digest/v1"; +const EXPORTS_COORDINATE: &str = "hello.echo.exports/v1"; +const FIXTURE_ROOT: &str = "fixtures/lawpack/hello-echo"; +const MANIFEST_CBOR: &str = "fixtures/lawpack/hello-echo/manifest.cbor"; +const MANIFEST_DIGEST: &str = "fixtures/lawpack/hello-echo/manifest.sha256"; +const EXPORTS_CBOR: &str = "fixtures/lawpack/hello-echo/exports.cbor"; +const EXPORTS_DIGEST: &str = "fixtures/lawpack/hello-echo/exports.sha256"; +const ADAPTER_CBOR: &str = "fixtures/lawpack/hello-echo/adapter.cbor"; +const ADAPTER_DIGEST: &str = "fixtures/lawpack/hello-echo/adapter.sha256"; +const TARGET_CONFIGURATION_CBOR: &str = + "fixtures/lawpack/hello-echo/echo-operation-configuration.cbor"; +const TARGET_CONFIGURATION_DIGEST: &str = + "fixtures/lawpack/hello-echo/echo-operation-configuration.sha256"; +const CREATE_GREETING_SOURCE: &str = "fixtures/lawpack/hello-echo/create-greeting.edict"; +const CREATE_GREETING_CORE_CBOR: &str = "fixtures/lawpack/hello-echo/create-greeting.core.cbor"; +const CREATE_GREETING_CORE_DIGEST: &str = "fixtures/lawpack/hello-echo/create-greeting.core.sha256"; +const CREATE_GREETING_TARGET_IR_CBOR: &str = + "fixtures/lawpack/hello-echo/create-greeting.target-ir.cbor"; +const CREATE_GREETING_TARGET_IR_DIGEST: &str = + "fixtures/lawpack/hello-echo/create-greeting.target-ir.sha256"; +const ADAPTER_COORDINATE: &str = "hello.echo.echo-dpo-adapter/v1"; +const TARGET_CONFIGURATION_COORDINATE: &str = "hello.echo.echo-operation-configuration/v1"; +const ECHO_TARGET_PROFILE_DIGEST: [u8; 32] = [ + 0x2e, 0x24, 0x94, 0x12, 0x1a, 0xec, 0xf5, 0xe6, 0xa2, 0xd9, 0x20, 0xf5, 0xfb, 0x85, 0x40, 0x88, + 0x25, 0xd3, 0x94, 0x76, 0x5f, 0xad, 0x41, 0x48, 0x4c, 0x41, 0x63, 0x97, 0xc9, 0x20, 0xfb, 0x04, +]; +const ECHO_TARGET_IR_DIGEST: [u8; 32] = [ + 0x00, 0x57, 0x16, 0x7e, 0x68, 0xf5, 0x0c, 0x99, 0xdc, 0xce, 0x08, 0x7b, 0x3e, 0x1c, 0xd6, 0x77, + 0xd1, 0x7c, 0x5d, 0x1d, 0xc2, 0x38, 0xbd, 0xb5, 0x2d, 0x89, 0x46, 0x9e, 0x14, 0x72, 0xfc, 0x2f, +]; +const WRITE_COMMAND: &str = "cargo xtask lawpack-goldens --write"; +const CAUSAL_CELL_FIXTURE_ROOT: &str = "fixtures/lawpack/causal-cell"; +const CAUSAL_CELL_MANIFEST_CBOR: &str = "fixtures/lawpack/causal-cell/manifest.cbor"; +const CAUSAL_CELL_MANIFEST_DIGEST: &str = "fixtures/lawpack/causal-cell/manifest.sha256"; +const CAUSAL_CELL_EXPORTS_CBOR: &str = "fixtures/lawpack/causal-cell/exports.cbor"; +const CAUSAL_CELL_EXPORTS_DIGEST: &str = "fixtures/lawpack/causal-cell/exports.sha256"; +const CAUSAL_CELL_ADAPTER_CBOR: &str = "fixtures/lawpack/causal-cell/adapter.cbor"; +const CAUSAL_CELL_ADAPTER_DIGEST: &str = "fixtures/lawpack/causal-cell/adapter.sha256"; +const CAUSAL_CELL_CONFIGURATION_CBOR: &str = + "fixtures/lawpack/causal-cell/echo-operation-configuration.cbor"; +const CAUSAL_CELL_CONFIGURATION_DIGEST: &str = + "fixtures/lawpack/causal-cell/echo-operation-configuration.sha256"; +const CAUSAL_CELL_EXPORTS_COORDINATE: &str = "causal.cell.exports/v1"; +const CAUSAL_CELL_ADAPTER_COORDINATE: &str = "causal.cell.echo-adapter/v1"; +const CAUSAL_CELL_CONFIGURATION_COORDINATE: &str = "echo.operation-lowering-configuration/v1"; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum LawpackGoldenMode { + Check, + Write, +} + +pub(crate) fn lawpack_goldens(root: &Path, mode: LawpackGoldenMode) -> Result<(), String> { + let artifacts = hello_echo_golden_artifacts(root)? + .into_iter() + .chain(causal_cell_golden_artifacts()?); + for (path, bytes) in artifacts { + match mode { + LawpackGoldenMode::Check => { + check_golden_file_with_command(root, path, &bytes, WRITE_COMMAND)?; + } + LawpackGoldenMode::Write => write_golden_file(&root.join(path), &bytes)?, + } + } + + println!( + "lawpack-goldens: {FIXTURE_ROOT} and {CAUSAL_CELL_FIXTURE_ROOT} {}", + match mode { + LawpackGoldenMode::Check => "checked", + LawpackGoldenMode::Write => "written", + } + ); + Ok(()) +} + +fn causal_cell_golden_artifacts() -> Result)>, String> { + let exports_value = causal_cell_exports(); + let exports_bytes = encode_canonical_cbor(&exports_value) + .map_err(|error| format!("encode causal.cell exports: {error}"))?; + let exports_digest = digest_value(CAUSAL_CELL_EXPORTS_COORDINATE, &exports_value)?; + + let configuration_value = causal_cell_target_configuration(); + let configuration_bytes = encode_canonical_cbor(&configuration_value) + .map_err(|error| format!("encode causal.cell target configuration: {error}"))?; + let configuration_digest = + digest_value(CAUSAL_CELL_CONFIGURATION_COORDINATE, &configuration_value)?; + + let adapter_value = causal_cell_adapter(configuration_digest); + let adapter_bytes = encode_canonical_cbor(&adapter_value) + .map_err(|error| format!("encode causal.cell adapter: {error}"))?; + let adapter_digest = digest_value(CAUSAL_CELL_ADAPTER_COORDINATE, &adapter_value)?; + + let manifest_value = causal_cell_manifest(exports_digest, adapter_digest); + let manifest_bytes = encode_canonical_cbor(&manifest_value) + .map_err(|error| format!("encode causal.cell manifest: {error}"))?; + let bundle = decode_lawpack_bundle(&manifest_bytes, &exports_bytes) + .map_err(|failures| format!("validate causal.cell lawpack: {failures:?}"))?; + let adapter = decode_lawpack_adapter(&bundle, "echo.dpo@1", &adapter_bytes) + .map_err(|failures| format!("validate causal.cell adapter: {failures:?}"))?; + + let source = causal_cell_application_source(&bundle.manifest_digest_review_string()); + let module = parse_module(&source) + .map_err(|error| format!("parse causal.cell application witness: {error:?}"))?; + let preparation = prepare_lawpack_compilation(&module, &bundle, &adapter) + .map_err(|failures| format!("prepare causal.cell application witness: {failures:?}"))?; + let core = compile_to_core(&module, preparation.compiler_context()) + .map_err(|error| format!("compile causal.cell application witness: {error:?}"))?; + let target_ir = lower_to_target_ir(&core, preparation.target_ir_facts()); + if target_ir.status != TargetLoweringStatus::Lowered || target_ir.artifact.is_none() { + return Err(format!( + "lower causal.cell application witness: expected artifact, got {:?}", + target_ir.failures + )); + } + + Ok(vec![ + (CAUSAL_CELL_MANIFEST_CBOR, manifest_bytes), + ( + CAUSAL_CELL_MANIFEST_DIGEST, + format!("{}\n", bundle.manifest_digest_review_string()).into_bytes(), + ), + (CAUSAL_CELL_EXPORTS_CBOR, exports_bytes), + ( + CAUSAL_CELL_EXPORTS_DIGEST, + format!("{}\n", bundle.manifest().exports.digest_review_string()).into_bytes(), + ), + (CAUSAL_CELL_ADAPTER_CBOR, adapter_bytes), + ( + CAUSAL_CELL_ADAPTER_DIGEST, + format!("{}\n", sha256_review_string(&adapter_digest)).into_bytes(), + ), + (CAUSAL_CELL_CONFIGURATION_CBOR, configuration_bytes), + ( + CAUSAL_CELL_CONFIGURATION_DIGEST, + format!("{}\n", sha256_review_string(&configuration_digest)).into_bytes(), + ), + ]) +} + +fn causal_cell_manifest(exports_digest: [u8; 32], adapter_digest: [u8; 32]) -> CanonicalValue { + map([ + ("apiVersion", text("edict.lawpack/v1")), + ("id", text("causal.cell")), + ("version", text("1")), + ( + "acceptedCoreAbi", + CanonicalValue::Array(vec![text("edict.core/v1")]), + ), + ("dependencies", CanonicalValue::Array(Vec::new())), + ( + "exports", + resource_ref(CAUSAL_CELL_EXPORTS_COORDINATE, exports_digest), + ), + ( + "targetAdapters", + CanonicalValue::Array(vec![map([ + ( + "acceptedTargetProfile", + resource_ref("echo.dpo@1", ECHO_TARGET_PROFILE_DIGEST), + ), + ( + "acceptedTargetIr", + resource_ref("echo.span-ir/v1", ECHO_TARGET_IR_DIGEST), + ), + ( + "adapter", + resource_ref(CAUSAL_CELL_ADAPTER_COORDINATE, adapter_digest), + ), + ])]), + ), + ( + "verifier", + map([ + ("class", text("declarative")), + ( + "ruleset", + resource_ref("causal.cell.verifier-rules/v1", [0x74; 32]), + ), + ]), + ), + ( + "compatibility", + resource_ref("causal.cell.compatibility/v1", [0x75; 32]), + ), + ( + "conformanceFixtureCorpus", + resource_ref("causal.cell.fixtures/v1", [0x76; 32]), + ), + ]) +} + +fn causal_cell_adapter(configuration_digest: [u8; 32]) -> CanonicalValue { + map([ + ("apiVersion", text("edict.lawpack-adapter/v1")), + ("class", text("declarative")), + ( + "operationProfiles", + map([( + "causal.cell@1.createIfAbsent", + map([ + ("core", text("continuum.profile.create/v1")), + ( + "semanticEffects", + CanonicalValue::Array(vec![text("causal.cell@1.createIfAbsent")]), + ), + ]), + )]), + ), + ( + "effectImplementations", + map([( + "causal.cell@1.createIfAbsent", + map([ + ( + "targetIntrinsic", + text("echo.dpo@1.anchored-node-attachment-create-if-absent"), + ), + ( + "targetConfiguration", + resource_ref(CAUSAL_CELL_CONFIGURATION_COORDINATE, configuration_digest), + ), + ("writeClass", text("create")), + ( + "footprintObligation", + text("causal.cell@1.cellKeyFootprint"), + ), + ("costObligation", text("causal.cell@1.smallCreateBudget")), + ( + "failureMappings", + map([( + "alreadyExists", + text("echo.executable-operation/precondition-mismatch/v1"), + )]), + ), + ]), + )]), + ), + ( + "budgets", + map([( + "causal.cell@1.smallCreateBudget", + map([ + ("maxSteps", CanonicalValue::Integer(16)), + ("maxAllocatedBytes", CanonicalValue::Integer(2_048)), + ("maxOutputBytes", CanonicalValue::Integer(512)), + ]), + )]), + ), + ]) +} + +fn causal_cell_target_configuration() -> CanonicalValue { + map([ + ( + "apiVersion", + text("echo.operation-lowering-configuration/v1"), + ), + ( + "programKind", + text("anchored-node-attachment-create-if-absent/v1"), + ), + ("requiredNodeTypeProfile", text("causal.cell.node/value/v1")), + ( + "requiredAttachmentTypeProfile", + text("causal.cell.attachment/value/v1"), + ), + ("maxReplacementBytes", CanonicalValue::Integer(256)), + ( + "authorityProfile", + text("causal.cell.authority.application/v1"), + ), + ( + "budgetCeiling", + map([ + ("steps", CanonicalValue::Integer(16)), + ("readBytes", CanonicalValue::Integer(64)), + ("writeBytes", CanonicalValue::Integer(320)), + ]), + ), + ( + "invocationBinding", + map([ + ("nodeKeyField", text("key")), + ("replacementField", text("value")), + ("nodeIdDerivation", text("sha256-utf8/v1")), + ("warpIdSource", text("action-lane/v1")), + ]), + ), + ]) +} + +fn causal_cell_exports() -> CanonicalValue { + map([ + ("types", CanonicalValue::Array(Vec::new())), + ("constants", CanonicalValue::Array(Vec::new())), + ("pureFunctions", CanonicalValue::Array(Vec::new())), + ( + "effects", + CanonicalValue::Array(vec![map([ + ("coordinate", text("causal.cell@1.createIfAbsent")), + ("typeParameters", CanonicalValue::Array(Vec::new())), + ("inputType", text("causal.cell@1.CreateInput")), + ("outputType", text("causal.cell@1.CreateReceipt")), + ("executionClass", text("runtime")), + ("effectKindHint", text("create")), + ( + "footprintObligation", + text("causal.cell@1.cellKeyFootprint"), + ), + ("costObligation", text("causal.cell@1.smallCreateBudget")), + ( + "effectFailures", + map([( + "alreadyExists", + map([ + ("authorityClass", text("domainMappable")), + ("payloadType", text("causal.cell@1.ExistingValue")), + ]), + )]), + ), + ("guardSupport", CanonicalValue::Bool(true)), + ])]), + ), + ( + "obstructions", + CanonicalValue::Array(vec![map([ + ("coordinate", text("causal.cell@1.AlreadyExists")), + ("authorityClass", text("domainMappable")), + ("payloadSchema", text("causal.cell@1.ExistingValue")), + ])]), + ), + ( + "operationProfiles", + map([( + "causal.cell@1.createIfAbsent", + map([ + ( + "opticTemplate", + map([ + ("opticKind", text("affectReintegration")), + ("boundaryKind", text("affect")), + ("supportPolicy", text("causal.cell@1.directSupport")), + ("lossDisposition", text("causal.cell@1.lossless")), + ( + "apertureRequirement", + map([ + ("kind", text("abstractFootprintObligation")), + ("ref", text("causal.cell@1.cellKeyFootprint")), + ]), + ), + ]), + ), + ( + "effectPredicate", + text("causal.cell@1.createIfAbsentEffect"), + ), + ]), + )]), + ), + ]) +} + +fn causal_cell_application_source(manifest_digest: &str) -> String { + format!( + r#"package examples.hello_echo@1; + +use lawpack causal.cell@1 digest "{manifest_digest}" as cell; + +type CreateGreetingInput = {{ + basis: String, + key: String, + value: String, +}}; + +type CellCreateReceipt = {{ + key: String, +}}; + +type GreetingCreated = {{ + key: String, + message: String, +}}; + +intent createGreeting(input: CreateGreetingInput) returns GreetingCreated + profile cell.createIfAbsent + basis input.basis + budget <= cell.smallCreateBudget +{{ + let receipt: CellCreateReceipt = cell.createIfAbsent(input) + else {{ alreadyExists(existing) => cell.AlreadyExists }}; + return {{ + key: receipt.key, + message: input.value, + }}; +}} +"# + ) +} + +fn hello_echo_golden_artifacts(root: &Path) -> Result)>, String> { + let exports_value = hello_echo_exports(); + let exports_bytes = encode_canonical_cbor(&exports_value) + .map_err(|error| format!("encode Hello Echo exports: {error}"))?; + let exports_digest = digest_value(EXPORTS_COORDINATE, &exports_value)?; + let target_configuration_value = hello_echo_target_configuration(); + let target_configuration_bytes = encode_canonical_cbor(&target_configuration_value) + .map_err(|error| format!("encode Hello Echo target configuration: {error}"))?; + let target_configuration_digest = + digest_value(TARGET_CONFIGURATION_COORDINATE, &target_configuration_value)?; + let adapter_value = hello_echo_adapter(target_configuration_digest); + let adapter_bytes = encode_canonical_cbor(&adapter_value) + .map_err(|error| format!("encode Hello Echo adapter: {error}"))?; + let adapter_digest = digest_value(ADAPTER_COORDINATE, &adapter_value)?; + let manifest_value = hello_echo_manifest(exports_digest, adapter_digest); + let manifest_bytes = encode_canonical_cbor(&manifest_value) + .map_err(|error| format!("encode Hello Echo manifest: {error}"))?; + let bundle = decode_lawpack_bundle(&manifest_bytes, &exports_bytes) + .map_err(|failures| format!("validate Hello Echo lawpack: {failures:?}"))?; + let adapter = decode_lawpack_adapter(&bundle, "echo.dpo@1", &adapter_bytes) + .map_err(|failures| format!("validate Hello Echo adapter: {failures:?}"))?; + let source = fs::read_to_string(root.join(CREATE_GREETING_SOURCE)) + .map_err(|error| format!("read {CREATE_GREETING_SOURCE}: {error}"))?; + let module = parse_module(&source) + .map_err(|error| format!("parse {CREATE_GREETING_SOURCE}: {error:?}"))?; + let preparation = prepare_lawpack_compilation(&module, &bundle, &adapter) + .map_err(|failures| format!("prepare Hello Echo compilation: {failures:?}"))?; + let core = compile_to_core(&module, preparation.compiler_context()) + .map_err(|error| format!("compile Hello Echo Core: {error:?}"))?; + let core_bytes = + encode_core_module(&core).map_err(|error| format!("encode Hello Echo Core: {error}"))?; + let core_digest = format!( + "{}\n", + digest_core_module(&core) + .map_err(|error| format!("digest Hello Echo Core: {error}"))? + .to_review_string() + ); + let target_ir_report = lower_to_target_ir(&core, preparation.target_ir_facts()); + if target_ir_report.status != TargetLoweringStatus::Lowered { + return Err(format!( + "lower Hello Echo Target IR: expected lowered status, got {:?}", + target_ir_report.status + )); + } + let target_ir = target_ir_report + .artifact + .ok_or_else(|| "lower Hello Echo Target IR: lowered report omitted artifact".to_owned())?; + let target_ir_bytes = encode_target_ir_artifact(&target_ir) + .map_err(|error| format!("encode Hello Echo Target IR: {error}"))?; + let target_ir_digest = format!( + "{}\n", + digest_target_ir_artifact(&target_ir) + .map_err(|error| format!("digest Hello Echo Target IR: {error}"))? + .to_review_string() + ); + let manifest_digest = format!("{}\n", bundle.manifest_digest_review_string()); + let exports_digest = format!("{}\n", bundle.manifest().exports.digest_review_string()); + let adapter_digest = format!("{}\n", sha256_review_string(&adapter_digest)); + let target_configuration_digest = + format!("{}\n", sha256_review_string(&target_configuration_digest)); + + Ok(vec![ + (MANIFEST_CBOR, manifest_bytes), + (MANIFEST_DIGEST, manifest_digest.into_bytes()), + (EXPORTS_CBOR, exports_bytes), + (EXPORTS_DIGEST, exports_digest.into_bytes()), + (ADAPTER_CBOR, adapter_bytes), + (ADAPTER_DIGEST, adapter_digest.into_bytes()), + (TARGET_CONFIGURATION_CBOR, target_configuration_bytes), + ( + TARGET_CONFIGURATION_DIGEST, + target_configuration_digest.into_bytes(), + ), + (CREATE_GREETING_CORE_CBOR, core_bytes), + (CREATE_GREETING_CORE_DIGEST, core_digest.into_bytes()), + (CREATE_GREETING_TARGET_IR_CBOR, target_ir_bytes), + ( + CREATE_GREETING_TARGET_IR_DIGEST, + target_ir_digest.into_bytes(), + ), + ]) +} + +fn hello_echo_manifest(exports_digest: [u8; 32], adapter_digest: [u8; 32]) -> CanonicalValue { + map([ + ("apiVersion", text("edict.lawpack/v1")), + ("id", text("hello.echo")), + ("version", text("1")), + ( + "acceptedCoreAbi", + CanonicalValue::Array(vec![text("edict.core/v1")]), + ), + ("dependencies", CanonicalValue::Array(Vec::new())), + ("exports", resource_ref(EXPORTS_COORDINATE, exports_digest)), + ( + "targetAdapters", + CanonicalValue::Array(vec![map([ + ( + "acceptedTargetProfile", + resource_ref("echo.dpo@1", ECHO_TARGET_PROFILE_DIGEST), + ), + ( + "acceptedTargetIr", + resource_ref("echo.span-ir/v1", ECHO_TARGET_IR_DIGEST), + ), + ("adapter", resource_ref(ADAPTER_COORDINATE, adapter_digest)), + ])]), + ), + ( + "verifier", + map([ + ("class", text("declarative")), + ( + "ruleset", + resource_ref("hello.echo.verifier-rules/v1", [0x44; 32]), + ), + ]), + ), + ( + "compatibility", + resource_ref("hello.echo.compatibility/v1", [0x55; 32]), + ), + ( + "conformanceFixtureCorpus", + resource_ref("hello.echo.fixtures/v1", [0x66; 32]), + ), + ]) +} + +fn hello_echo_adapter(target_configuration_digest: [u8; 32]) -> CanonicalValue { + map([ + ("apiVersion", text("edict.lawpack-adapter/v1")), + ("class", text("declarative")), + ( + "operationProfiles", + map([( + "hello.echo@1.createGreeting", + map([ + ("core", text("continuum.profile.create/v1")), + ( + "semanticEffects", + CanonicalValue::Array(vec![text("hello.echo@1.createGreeting")]), + ), + ]), + )]), + ), + ( + "effectImplementations", + map([( + "hello.echo@1.createGreeting", + map([ + ( + "targetIntrinsic", + text("echo.dpo@1.anchored-node-attachment-create-if-absent"), + ), + ( + "targetConfiguration", + resource_ref(TARGET_CONFIGURATION_COORDINATE, target_configuration_digest), + ), + ("writeClass", text("create")), + ( + "footprintObligation", + text("hello.echo@1.greetingKeyFootprint"), + ), + ("costObligation", text("hello.echo@1.smallCreateBudget")), + ( + "failureMappings", + map([( + "alreadyExists", + text("echo.executable-operation/precondition-mismatch/v1"), + )]), + ), + ]), + )]), + ), + ( + "budgets", + map([( + "hello.echo@1.smallCreateBudget", + map([ + ("maxSteps", CanonicalValue::Integer(16)), + ("maxAllocatedBytes", CanonicalValue::Integer(2_048)), + ("maxOutputBytes", CanonicalValue::Integer(512)), + ]), + )]), + ), + ]) +} + +fn hello_echo_target_configuration() -> CanonicalValue { + map([ + ( + "apiVersion", + text("echo.operation-lowering-configuration/v1"), + ), + ( + "programKind", + text("anchored-node-attachment-create-if-absent/v1"), + ), + ( + "requiredNodeTypeProfile", + text("hello.echo.node.greeting/v1"), + ), + ( + "requiredAttachmentTypeProfile", + text("hello.echo.attachment.greeting-message/v1"), + ), + ("maxReplacementBytes", CanonicalValue::Integer(256)), + ( + "authorityProfile", + text("hello.echo.authority.local-demo/v1"), + ), + ( + "budgetCeiling", + map([ + ("steps", CanonicalValue::Integer(16)), + ("readBytes", CanonicalValue::Integer(64)), + ("writeBytes", CanonicalValue::Integer(320)), + ]), + ), + ( + "invocationBinding", + map([ + ("nodeKeyField", text("key")), + ("replacementField", text("message")), + ("nodeIdDerivation", text("sha256-utf8/v1")), + ("warpIdSource", text("action-lane/v1")), + ]), + ), + ]) +} + +fn hello_echo_exports() -> CanonicalValue { + map([ + ("types", CanonicalValue::Array(Vec::new())), + ("constants", CanonicalValue::Array(Vec::new())), + ("pureFunctions", CanonicalValue::Array(Vec::new())), + ( + "effects", + CanonicalValue::Array(vec![map([ + ("coordinate", text("hello.echo@1.createGreeting")), + ("typeParameters", CanonicalValue::Array(Vec::new())), + ("inputType", text("hello.echo@1.CreateGreetingInput")), + ("outputType", text("hello.echo@1.GreetingReceipt")), + ("executionClass", text("runtime")), + ("effectKindHint", text("create")), + ( + "footprintObligation", + text("hello.echo@1.greetingKeyFootprint"), + ), + ("costObligation", text("hello.echo@1.smallCreateBudget")), + ( + "effectFailures", + map([( + "alreadyExists", + map([ + ("authorityClass", text("domainMappable")), + ("payloadType", text("hello.echo@1.ExistingGreeting")), + ]), + )]), + ), + ("guardSupport", CanonicalValue::Bool(true)), + ])]), + ), + ( + "obstructions", + CanonicalValue::Array(vec![map([ + ("coordinate", text("hello.echo@1.AlreadyExists")), + ("authorityClass", text("domainMappable")), + ("payloadSchema", text("hello.echo@1.ExistingGreeting")), + ])]), + ), + ( + "operationProfiles", + map([( + "hello.echo@1.createGreeting", + map([ + ( + "opticTemplate", + map([ + ("opticKind", text("affectReintegration")), + ("boundaryKind", text("affect")), + ("supportPolicy", text("hello.echo@1.directSupport")), + ("lossDisposition", text("hello.echo@1.lossless")), + ( + "apertureRequirement", + map([ + ("kind", text("abstractFootprintObligation")), + ("ref", text("hello.echo@1.greetingKeyFootprint")), + ]), + ), + ]), + ), + ("effectPredicate", text("hello.echo@1.createGreetingEffect")), + ]), + )]), + ), + ]) +} + +fn digest_value(domain: &str, value: &CanonicalValue) -> Result<[u8; 32], String> { + let framed = CanonicalValue::Array(vec![text(DIGEST_FRAME), text(domain), value.clone()]); + let bytes = encode_canonical_cbor(&framed) + .map_err(|error| format!("encode lawpack digest frame: {error}"))?; + Ok(Sha256::digest(bytes).into()) +} + +fn sha256_review_string(digest: &[u8; 32]) -> String { + let mut review = String::with_capacity(71); + review.push_str("sha256:"); + for byte in digest { + write!(&mut review, "{byte:02x}").expect("writing to a String cannot fail"); + } + review +} + +fn resource_ref(id: &str, digest: [u8; 32]) -> CanonicalValue { + map([ + ("id", text(id)), + ( + "digest", + CanonicalValue::Array(vec![text("sha256"), CanonicalValue::Bytes(digest.to_vec())]), + ), + ]) +} + +fn map(entries: [(&str, CanonicalValue); N]) -> CanonicalValue { + CanonicalValue::Map( + entries + .into_iter() + .map(|(key, value)| (text(key), value)) + .collect(), + ) +} + +fn text(value: &str) -> CanonicalValue { + CanonicalValue::Text(value.to_owned()) +} diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 833eec6..0355c31 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -3,6 +3,7 @@ mod contract_check; mod goldens; +mod lawpack_goldens; mod provider_components; mod provider_contract_pack; mod provider_dependencies; @@ -22,6 +23,7 @@ use goldens::{ target_profile_resource_goldens, AuthorityFactsGoldenMode, BundleGoldenMode, CliGoldenMode, CoreGoldenMode, TargetIrGoldenMode, TargetProfileResourceGoldenMode, }; +use lawpack_goldens::{lawpack_goldens, LawpackGoldenMode}; use provider_components::{provider_component_fixtures, ProviderComponentFixtureMode}; use provider_contract_pack::{provider_contract_pack, ProviderContractPackMode}; use provider_dependencies::provider_runtime_dependencies; @@ -90,6 +92,7 @@ fn run() -> Result<(), String> { } target_ir_goldens(&repo_root()?, mode) } + Some("lawpack-goldens") => run_lawpack_goldens(&mut args), Some("release-prep") => { let version = args .next() @@ -128,7 +131,7 @@ fn run() -> Result<(), String> { Some("verify") => verify(&repo_root()?), Some(cmd) => Err(format!("unknown xtask command `{cmd}`")), None => Err( - "usage: cargo xtask " + "usage: cargo xtask " .into(), ), } @@ -148,6 +151,18 @@ fn run_authority_facts_goldens(args: &mut impl Iterator) -> Resul authority_facts_goldens(&repo_root()?, mode) } +fn run_lawpack_goldens(args: &mut impl Iterator) -> Result<(), String> { + let mode = match args.next().as_deref() { + Some("--write") => LawpackGoldenMode::Write, + Some("--check") | None => LawpackGoldenMode::Check, + Some(flag) => return Err(format!("unknown lawpack-goldens flag `{flag}`")), + }; + if let Some(extra) = args.next() { + return Err(format!("unexpected lawpack-goldens argument `{extra}`")); + } + lawpack_goldens(&repo_root()?, mode) +} + fn run_target_profile_resource_goldens( args: &mut impl Iterator, ) -> Result<(), String> { @@ -188,6 +203,7 @@ fn verify(root: &Path) -> Result<(), String> { target_profile_resource_goldens(root, TargetProfileResourceGoldenMode::Check)?; core_goldens(root, CoreGoldenMode::Check)?; target_ir_goldens(root, TargetIrGoldenMode::Check)?; + lawpack_goldens(root, LawpackGoldenMode::Check)?; bundle_goldens(root, BundleGoldenMode::Check)?; cli_goldens(root, CliGoldenMode::Check)?; provider_component_fixtures(root, ProviderComponentFixtureMode::Check)?; diff --git a/xtask/src/provider_contract_pack.rs b/xtask/src/provider_contract_pack.rs index a26f8ea..74be7b9 100644 --- a/xtask/src/provider_contract_pack.rs +++ b/xtask/src/provider_contract_pack.rs @@ -9,6 +9,7 @@ use crate::goldens::{check_golden_file_with_command, write_golden_file}; const COMMON_CDDL: &str = "docs/abi/edict-common.cddl"; const CORE_CDDL: &str = "docs/abi/edict-core.cddl"; const LAWPACK_CDDL: &str = "docs/abi/edict-lawpack.cddl"; +const LAWPACK_ADAPTER_CDDL: &str = "docs/abi/edict-lawpack-adapter.cddl"; const TARGET_PROFILE_CDDL: &str = "docs/abi/edict-target-profile.cddl"; const AUTHORITY_FACTS_CDDL: &str = "docs/abi/edict-authority-facts.cddl"; const TARGET_IR_CDDL: &str = "docs/abi/edict-target-ir.cddl"; @@ -30,6 +31,7 @@ pub(crate) fn provider_contract_pack( let common_cddl = read(root, COMMON_CDDL)?; let core_cddl = read(root, CORE_CDDL)?; let lawpack_cddl = read(root, LAWPACK_CDDL)?; + let lawpack_adapter_cddl = read(root, LAWPACK_ADAPTER_CDDL)?; let target_profile_cddl = read(root, TARGET_PROFILE_CDDL)?; let authority_facts_cddl = read(root, AUTHORITY_FACTS_CDDL)?; let target_ir_cddl = read(root, TARGET_IR_CDDL)?; @@ -37,6 +39,7 @@ pub(crate) fn provider_contract_pack( common_cddl: &common_cddl, core_cddl: &core_cddl, lawpack_cddl: &lawpack_cddl, + lawpack_adapter_cddl: &lawpack_adapter_cddl, target_profile_cddl: &target_profile_cddl, authority_facts_cddl: &authority_facts_cddl, target_ir_cddl: &target_ir_cddl, diff --git a/xtask/src/tests.rs b/xtask/src/tests.rs index 858648c..0343a80 100644 --- a/xtask/src/tests.rs +++ b/xtask/src/tests.rs @@ -26,6 +26,7 @@ use super::goldens::{ target_ir_goldens, target_profile_resource_goldens, AuthorityFactsGoldenMode, BundleGoldenMode, TargetIrGoldenMode, TargetProfileResourceGoldenMode, }; +use super::lawpack_goldens::{lawpack_goldens, LawpackGoldenMode}; use super::provider_contract_pack::{ provider_contract_pack, ProviderContractPackMode, CONTRACT_PACK_CDDL, CONTRACT_PACK_MANIFEST, }; @@ -71,6 +72,12 @@ fn authority_facts_goldens_match_executable_codec() { .expect("authority-facts goldens match executable codec output"); } +#[test] +fn lawpack_goldens_match_executable_codec() { + lawpack_goldens(&repo_root().expect("repo root"), LawpackGoldenMode::Check) + .expect("lawpack goldens match executable codec output"); +} + #[test] fn target_profile_resource_goldens_match_executable_contract() { target_profile_resource_goldens( @@ -130,6 +137,7 @@ fn assert_provider_contract_manifest_metadata(manifest: &Value) { [ ("authority-facts", "authority-facts"), ("core-module", "core-module"), + ("lawpack-adapter", "lawpack-adapter"), ("lawpack-exports", "lawpack-exports"), ("lawpack-manifest", "lawpack-manifest"), ("lowering-requirements", "lowering-requirements"), @@ -264,6 +272,7 @@ fn provider_contract_pack_check_rejects_drift_without_rewriting() { "docs/abi/edict-common.cddl", "docs/abi/edict-core.cddl", "docs/abi/edict-lawpack.cddl", + "docs/abi/edict-lawpack-adapter.cddl", "docs/abi/edict-target-profile.cddl", "docs/abi/edict-authority-facts.cddl", "docs/abi/edict-target-ir.cddl", @@ -1972,6 +1981,32 @@ fn provider_runtime_dependency_boundary_is_narrow() { .expect("provider runtime dependency boundary remains narrow"); } +fn assert_compiler_settings_projection_properties(properties: &serde_json::Map) { + for field in ["emit", "compilerContext", "target"] { + assert!( + properties.contains_key(field), + "compiler settings schema missing projection field `{field}`" + ); + } + let emit = properties + .get("emit") + .unwrap_or_else(|| panic!("compiler settings schema missing `emit` property")); + assert_eq!( + emit.get("minItems").and_then(Value::as_u64), + Some(1), + "compiler settings projection emit list must reject empty arrays" + ); + let expected_emit_values = ["syntax", "diagnostics", "core", "targetIr", "digests"] + .into_iter() + .map(Value::from) + .collect::>(); + assert_eq!( + emit.pointer("/items/enum").and_then(Value::as_array), + Some(&expected_emit_values), + "compiler settings projection emit enum is an exact ordered contract" + ); +} + #[test] fn compiler_settings_schema_declares_jsonl_contract() { let root = repo_root().expect("repo root"); @@ -2010,41 +2045,35 @@ fn compiler_settings_schema_declares_jsonl_contract() { .unwrap_or_else(|| panic!("compiler settings schema missing `type` property")); assert_eq!( record_type.get("const").and_then(Value::as_str), - Some("compilerSettings") + Some("compilerSettings"), + "compiler settings schema must expose one record type" ); let operation = properties .get("operation") .unwrap_or_else(|| panic!("compiler settings schema missing `operation` property")); - assert!( - json_string_array_contains(operation, "enum", "check"), - "compiler settings schema must declare the `check` operation" + assert_eq!( + json_array(operation, "enum"), + &vec![ + Value::String("build".to_owned()), + Value::String("check".to_owned()), + Value::String("project".to_owned()), + ], + "compiler settings schema must expose exactly the supported operations" ); - assert!( - json_string_array_contains(operation, "enum", "project"), - "compiler settings schema must declare the `project` operation" + let application = properties + .get("application") + .unwrap_or_else(|| panic!("compiler settings schema missing `application` property")); + assert_eq!( + application.get("type").and_then(Value::as_str), + Some("string"), + "application path must be a string" ); - for field in ["emit", "compilerContext", "target"] { - assert!( - properties.contains_key(field), - "compiler settings schema missing projection field `{field}`" - ); - } - let emit = properties - .get("emit") - .unwrap_or_else(|| panic!("compiler settings schema missing `emit` property")); assert_eq!( - emit.get("minItems").and_then(Value::as_u64), + application.get("minLength").and_then(Value::as_u64), Some(1), - "compiler settings projection emit list must reject empty arrays" + "application path must be non-empty" ); - for value in ["syntax", "diagnostics", "core", "targetIr", "digests"] { - assert!( - emit.pointer("/items/enum") - .and_then(Value::as_array) - .is_some_and(|values| values.iter().any(|item| item.as_str() == Some(value))), - "compiler settings projection emit list must declare `{value}`" - ); - } + assert_compiler_settings_projection_properties(properties); assert_eq!( schema .pointer("/properties/target/properties/profileDigest/pattern")