diff --git a/.continuum/release.yml b/.continuum/release.yml index 27231c17..c0efee2d 100644 --- a/.continuum/release.yml +++ b/.continuum/release.yml @@ -45,12 +45,6 @@ version_sources: field: package.version required: true published: true - - path: crates/wesley-holmes/Cargo.toml - name: wesley-holmes - type: cargo-manifest - field: package.version - required: true - published: false - path: package.json name: root-private-package type: json diff --git a/.github/workflows/rust-native.yml b/.github/workflows/rust-native.yml index 4d7d0e2a..537044bd 100644 --- a/.github/workflows/rust-native.yml +++ b/.github/workflows/rust-native.yml @@ -21,7 +21,6 @@ on: - 'schemas/**' - 'test/fixtures/extension-generation/**' - 'test/fixtures/ir-parity/**' - - 'test/fixtures/weslaw/**' - 'xtask/**' push: branches: @@ -42,7 +41,6 @@ on: - 'schemas/**' - 'test/fixtures/extension-generation/**' - 'test/fixtures/ir-parity/**' - - 'test/fixtures/weslaw/**' - 'xtask/**' jobs: diff --git a/CHANGELOG.md b/CHANGELOG.md index 36964133..a6339e83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,21 @@ The format is based on Keep a Changelog, and this project adheres to Semantic Ve ## [Unreleased] +### Changed + +- Versioned the domain-neutral extension-generation input, provenance, and + review contracts to v2. The canonical input now contains structural IR, + operations, owner declarations, settings, and projection roles without an + embedded semantic-law document. + +### Removed + +- Removed Weslaw authoring, Law IR, semantic-law hashes and diffs, `wesley law` + and `wesley init-law`, the emitter `--law` option and generated validators, + the Weslaw-specific Rust Holmes foundation, and their schemas and fixtures. + Executable semantics belong to Edict; target and runtime semantics belong to + their owning adapters rather than generic Wesley. + ## [0.3.0-alpha.1] - 2026-07-15 ### Added diff --git a/Cargo.lock b/Cargo.lock index d5a3d651..45af0c02 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1582,14 +1582,6 @@ dependencies = [ "wesley-emit-codec", ] -[[package]] -name = "wesley-holmes" -version = "0.3.0-alpha.1" -dependencies = [ - "serde", - "serde_json", -] - [[package]] name = "windows-core" version = "0.62.2" diff --git a/Cargo.toml b/Cargo.toml index d2f032e9..e91bf37c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,6 @@ members = [ "crates/wesley-emit-codec", "crates/wesley-emit-typescript", "crates/wesley-emit-rust", - "crates/wesley-holmes", "crates/wesley-cli", "xtask", ] diff --git a/README.md b/README.md index 550933eb..a9d1b3a4 100644 --- a/README.md +++ b/README.md @@ -70,12 +70,12 @@ Generate target-neutral Rust or TypeScript projections: ```bash cargo run --bin wesley -- emit rust \ - --schema test/fixtures/weslaw/contract-bundle-shape.graphql \ + --schema test/fixtures/extension-generation/schema.graphql \ --out generated/model.rs \ --metadata-out generated/model.metadata.json cargo run --bin wesley -- emit typescript \ - --schema test/fixtures/weslaw/contract-bundle-shape.graphql \ + --schema test/fixtures/extension-generation/schema.graphql \ --out generated/types.ts \ --metadata-out generated/types.metadata.json ``` @@ -116,7 +116,6 @@ Wesley owns domain-free compiler facts: - operation selection and directive-argument extraction - Rust and TypeScript model and operation bindings - TypeScript and Rust LE-binary codec projections -- `weslaw/v1` authoring, hashing, diffing, rebinding, and coverage metadata - deterministic evidence inputs and release checks around those generic compiler contracts diff --git a/crates/wesley-cli/src/main.rs b/crates/wesley-cli/src/main.rs index ecf45f45..3f3cf215 100644 --- a/crates/wesley-cli/src/main.rs +++ b/crates/wesley-cli/src/main.rs @@ -1,26 +1,20 @@ //! Native Wesley CLI entry point. -use std::collections::BTreeSet; use std::env; use std::fs; use std::path::{Path, PathBuf}; use std::process::{Command, ExitCode}; use wesley_core::{ - build_contract_bundle_manifest_v1, compute_content_hash, compute_law_hash_v1, - compute_registry_hash, diff_law_ir_v1, diff_schema_sdl, extract_operation_directive_args, - format_law_diff_markdown_v1, list_schema_operations_sdl, load_project_manifest, - load_weslaw_yaml, lower_schema_sdl, lower_wes_channel_directives_to_law_ir_v1, - normalize_schema_sdl, record_law_binding_error_v1, resolve_operation_selections, - resolve_operation_selections_with_schema, select_changed_schema_paths, - ContractBundleManifestV1, DetailedSchemaPathConfig, FootprintLawV1, LawDiffReportV1, - LawEntryBodyV1, LawIrV1, OperationType, ProjectManifest, ProjectManifestError, - ResolvedSchemaPath, ScalarSemanticsLawV1, SchemaDelta, SchemaPathConfig, SelectedSchemaPath, - TypeKind, WeslawError, WesleyError, WesleyIR, + compute_content_hash, compute_registry_hash, diff_schema_sdl, extract_operation_directive_args, + list_schema_operations_sdl, load_project_manifest, lower_schema_sdl, normalize_schema_sdl, + resolve_operation_selections, resolve_operation_selections_with_schema, + select_changed_schema_paths, DetailedSchemaPathConfig, ProjectManifest, ProjectManifestError, + ResolvedSchemaPath, SchemaDelta, SchemaPathConfig, SelectedSchemaPath, WesleyError, WesleyIR, }; use wesley_emit_rust::{ - emit_le_binary_rust, emit_rust_with_operations, emit_rust_with_operations_and_law, - GENERATOR_NAME as RUST_GENERATOR_NAME, GENERATOR_VERSION as RUST_GENERATOR_VERSION, - LE_BINARY_RUST_DEFAULT_CODEC_IMPORT, LE_BINARY_RUST_GENERATOR_NAME, + emit_le_binary_rust, emit_rust_with_operations, GENERATOR_NAME as RUST_GENERATOR_NAME, + GENERATOR_VERSION as RUST_GENERATOR_VERSION, LE_BINARY_RUST_DEFAULT_CODEC_IMPORT, + LE_BINARY_RUST_GENERATOR_NAME, }; use wesley_emit_typescript::{ emit_le_binary_typescript, emit_typescript_with_operations, DEFAULT_CODEC_IMPORT, @@ -68,13 +62,7 @@ fn run(args: Vec) -> Result { Some("doctor") => run_doctor_command(&args[1..]), Some("config") => run_config_command(&args[1..]), Some("target") => run_target_command(&args[1..]), - Some("init-law") if wants_help(&args[1..]) => { - print_init_law_help(); - Ok(EXIT_OK) - } - Some("init-law") => run_init_law_command(&args[1..]), Some("schema") => run_schema_command(&args[1..]), - Some("law") => run_law_command(&args[1..]), Some("emit") => run_emit_command(&args[1..]), Some("operation") => run_operation_command(&args[1..]), Some("version") | Some("--version") | Some("-V") => { @@ -85,292 +73,6 @@ fn run(args: Vec) -> Result { } } -fn run_law_command(args: &[String]) -> Result { - match args.first().map(String::as_str) { - None | Some("--help") | Some("-h") => { - print_law_help(); - Ok(EXIT_OK) - } - Some("validate") if wants_help(&args[1..]) => { - print_law_help(); - Ok(EXIT_OK) - } - Some("validate") => { - let options = parse_options(&args[1..], "law validate")?; - let schema_path = options.required_schema("law validate")?; - let law_path = options.required_law("law validate")?; - let schema_sdl = read_file(&schema_path, "schema")?; - let law_source = read_file(&law_path, "law")?; - let ir = lower_schema_sdl(&schema_sdl)?; - let operations = list_schema_operations_sdl(&schema_sdl)?; - let law_ir = load_weslaw_yaml(&law_source)?; - let manifest = build_contract_bundle_manifest_v1(&law_ir, &ir, &operations)?; - - if options.json { - print_json(&LawValidateReport { - schema_hash: manifest.schema_hash.clone(), - law_hash: manifest.law_hash.clone(), - law_document_hash: manifest.law_document_hash.clone(), - profile_hash: manifest.profile_hash.clone(), - bundle_hash: manifest.bundle_hash.clone(), - bound_entry_count: manifest.law_entry_count, - manifest, - })?; - } else { - println!( - "Law validation passed: {} active entries bound to {} (lawHash {}, bundleHash {})", - manifest.law_entry_count, - manifest.schema_hash, - manifest.law_hash, - manifest.bundle_hash - ); - } - - Ok(EXIT_OK) - } - Some("lint") if wants_help(&args[1..]) => { - print_law_help(); - Ok(EXIT_OK) - } - Some("lint") => run_law_lint_command(&args[1..]), - Some("diff") if wants_help(&args[1..]) => { - print_law_help(); - Ok(EXIT_OK) - } - Some("diff") => run_law_diff_command(&args[1..]), - Some("explain") if wants_help(&args[1..]) => { - print_law_help(); - Ok(EXIT_OK) - } - Some("explain") => run_law_explain_command(&args[1..]), - Some("rebind") if wants_help(&args[1..]) => { - print_law_help(); - Ok(EXIT_OK) - } - Some("rebind") => run_law_rebind_command(&args[1..]), - Some("capabilities") if wants_help(&args[1..]) => { - print_law_help(); - Ok(EXIT_OK) - } - Some("capabilities") => run_law_capabilities_command(&args[1..]), - Some("coverage") if wants_help(&args[1..]) => { - print_law_help(); - Ok(EXIT_OK) - } - Some("coverage") => run_law_coverage_command(&args[1..]), - Some(command) => Err(CliError::usage(format!("unknown law command '{command}'"))), - } -} - -fn run_law_lint_command(args: &[String]) -> Result { - let options = parse_options(args, "law lint")?; - let law_path = options.required_law("law lint")?; - let law_source = read_file(&law_path, "law")?; - let law_ir = load_weslaw_yaml(&law_source)?; - let report = LawLintReport { - api_version: law_ir.api_version.clone(), - family: law_ir.family.clone(), - schema_hash: law_ir.schema_hash.clone(), - active_entry_count: law_ir.entries.len(), - law_hash: compute_law_hash_v1(&law_ir)?, - }; - - if options.json { - print_json(&report)?; - } else { - println!( - "Law lint passed: {} active entries in {} (lawHash {})", - report.active_entry_count, report.family, report.law_hash - ); - } - - Ok(EXIT_OK) -} - -fn run_law_diff_command(args: &[String]) -> Result { - let options = parse_options(args, "law diff")?; - let old_path = options.required_old("law diff")?; - let new_path = options.required_new("law diff")?; - let old_law_source = read_file(&old_path, "old law")?; - let new_law_source = read_file(&new_path, "new law")?; - let old_law_ir = load_weslaw_yaml(&old_law_source)?; - let new_law_ir = load_weslaw_yaml(&new_law_source)?; - let mut report = diff_law_ir_v1(&old_law_ir, &new_law_ir)?; - let mut exit_code = EXIT_OK; - - if let Some(schema_path) = options.schema.as_ref() { - let schema_sdl = read_file(schema_path, "schema")?; - let ir = lower_schema_sdl(&schema_sdl)?; - let operations = list_schema_operations_sdl(&schema_sdl)?; - if let Err(error) = build_contract_bundle_manifest_v1(&new_law_ir, &ir, &operations) { - record_law_binding_error_v1(&mut report, &error); - exit_code = EXIT_FAILURE; - } - } - - print_law_diff(&report, options.output_format()?)?; - Ok(exit_code) -} - -fn run_init_law_command(args: &[String]) -> Result { - let options = parse_options(args, "init-law")?; - let schema_path = options.required_schema("init-law")?; - let family = options.required_family("init-law")?; - let schema_sdl = read_file(&schema_path, "schema")?; - let ir = lower_schema_sdl(&schema_sdl)?; - let schema_hash = format!("sha256:{}", compute_registry_hash(&ir)?); - let law_ir = lower_wes_channel_directives_to_law_ir_v1( - &ir, - &family, - &schema_hash, - Some(schema_path.display().to_string()), - )?; - let drafts = draft_suggestions_from_descriptions(&ir); - let output = render_weslaw_yaml(&law_ir, &drafts); - - if let Some(out) = options.out.as_ref() { - write_file(out, &output, "weslaw scaffold")?; - } else { - print!("{output}"); - } - - Ok(EXIT_OK) -} - -fn run_law_explain_command(args: &[String]) -> Result { - let options = parse_options(args, "law explain")?; - let law_path = options.required_law("law explain")?; - let subject = options.required_subject("law explain")?; - let law_source = read_file(&law_path, "law")?; - let law_ir = load_weslaw_yaml(&law_source)?; - let explanation = explain_law_subject(&law_ir, &subject)?; - - if options.json { - print_json(&explanation)?; - } else { - print!("{}", explanation.to_text()); - } - - Ok(EXIT_OK) -} - -fn run_law_rebind_command(args: &[String]) -> Result { - let options = parse_options(args, "law rebind")?; - let schema_path = options.required_schema("law rebind")?; - let law_path = options.required_law("law rebind")?; - let schema_sdl = read_file(&schema_path, "schema")?; - let law_source = read_file(&law_path, "law")?; - let ir = lower_schema_sdl(&schema_sdl)?; - let operations = list_schema_operations_sdl(&schema_sdl)?; - let old_law_ir = load_weslaw_yaml(&law_source)?; - let new_schema_hash = format!("sha256:{}", compute_registry_hash(&ir)?); - let mut report = LawRebindReport { - api_version: "wesley.law-rebind/v1", - old_schema_hash: old_law_ir.schema_hash.clone(), - new_schema_hash: new_schema_hash.clone(), - changed: old_law_ir.schema_hash != new_schema_hash, - accepted: false, - output: None, - }; - - if options.accept { - let out = options.required_out("law rebind --accept")?; - let rebound = replace_schema_hash_anchor( - &law_source, - &old_law_ir.schema_hash, - &new_schema_hash, - &law_path, - )?; - let rebound_law_ir = load_weslaw_yaml(&rebound)?; - build_contract_bundle_manifest_v1(&rebound_law_ir, &ir, &operations)?; - write_file(&out, &rebound, "rebound law")?; - report.accepted = true; - report.output = Some(out.display().to_string()); - } - - if options.json { - print_json(&report)?; - } else if report.accepted { - println!( - "Law rebind accepted: {} -> {} ({})", - report.old_schema_hash, - report.new_schema_hash, - report.output.as_deref().unwrap_or("-") - ); - } else if report.changed { - println!( - "Law rebind required: {} -> {}", - report.old_schema_hash, report.new_schema_hash - ); - } else { - println!("Law rebind not required: {}", report.new_schema_hash); - } - - Ok(EXIT_OK) -} - -fn run_law_capabilities_command(args: &[String]) -> Result { - let options = parse_options(args, "law capabilities")?; - let law_path = options.required_law("law capabilities")?; - let law_source = read_file(&law_path, "law")?; - let law_ir = load_weslaw_yaml(&law_source)?; - let report = capability_report_from_law(&law_ir); - - if options.json { - print_json(&report)?; - } else { - println!( - "Capability report: {} footprint(s), reportOnly={}, runtimeEnforcement={}", - report.footprints.len(), - report.report_only, - report.runtime_enforcement - ); - for footprint in &report.footprints { - println!("{} {}", footprint.subject, footprint.law_id); - print_nonempty_list(" reads", &footprint.reads); - print_nonempty_list(" writes", &footprint.writes); - print_nonempty_list(" creates", &footprint.creates); - print_nonempty_list(" forbids", &footprint.forbids); - } - } - - Ok(EXIT_OK) -} - -fn run_law_coverage_command(args: &[String]) -> Result { - let options = parse_options(args, "law coverage")?; - let schema_path = options.required_schema("law coverage")?; - let law_path = options.required_law("law coverage")?; - let schema_sdl = read_file(&schema_path, "schema")?; - let law_source = read_file(&law_path, "law")?; - let ir = lower_schema_sdl(&schema_sdl)?; - let operations = list_schema_operations_sdl(&schema_sdl)?; - let law_ir = load_weslaw_yaml(&law_source)?; - build_contract_bundle_manifest_v1(&law_ir, &ir, &operations)?; - let profile = coverage_profile_or_default(options.profile.as_deref())?.to_string(); - let report = law_coverage_report(&ir, &operations, &law_ir, &profile); - - if options.json { - print_json(&report)?; - } else { - println!( - "Law coverage profile {}: {}/{} required subjects covered ({:.1}%)", - report.profile, report.required_covered, report.required_total, report.required_percent - ); - for category in &report.categories { - println!( - "{}: {}/{} covered{}", - category.id, - category.covered, - category.total, - if category.required { " required" } else { "" } - ); - } - } - - Ok(EXIT_OK) -} - fn run_normalize_sdl_command(args: &[String]) -> Result { let options = parse_options(args, "normalize-sdl")?; let schema_path = options.required_schema("normalize-sdl")?; @@ -644,26 +346,12 @@ fn run_emit_command(args: &[String]) -> Result { let sdl = read_file(&schema_path, "schema")?; let ir = lower_schema_sdl(&sdl)?; let operations = list_schema_operations_sdl(&sdl)?; - let bundle = - load_contract_bundle_if_requested(options.law.as_deref(), &ir, &operations)?; - let manifest = bundle.as_ref().map(|bundle| &bundle.manifest); - let rust = if let Some(bundle) = &bundle { - emit_rust_with_operations_and_law( - &ir, - &operations, - &bundle.manifest.schema_hash, - &bundle.manifest.law_hash, - &bundle.law_ir, - ) - } else { - emit_rust_with_operations(&ir, &operations) - }; + let rust = emit_rust_with_operations(&ir, &operations); write_file(&out_path, &rust, "Rust output")?; write_emit_metadata_if_requested( options.metadata_out.as_deref(), &ir, - manifest, RUST_GENERATOR_NAME, RUST_GENERATOR_VERSION, )?; @@ -680,16 +368,12 @@ fn run_emit_command(args: &[String]) -> Result { let sdl = read_file(&schema_path, "schema")?; let ir = lower_schema_sdl(&sdl)?; let operations = list_schema_operations_sdl(&sdl)?; - let bundle = - load_contract_bundle_if_requested(options.law.as_deref(), &ir, &operations)?; - let manifest = bundle.as_ref().map(|bundle| &bundle.manifest); let typescript = emit_typescript_with_operations(&ir, &operations); write_file(&out_path, &typescript, "TypeScript output")?; write_emit_metadata_if_requested( options.metadata_out.as_deref(), &ir, - manifest, TYPESCRIPT_GENERATOR_NAME, TYPESCRIPT_GENERATOR_VERSION, )?; @@ -706,9 +390,6 @@ fn run_emit_command(args: &[String]) -> Result { let sdl = read_file(&schema_path, "schema")?; let ir = lower_schema_sdl(&sdl)?; let operations = list_schema_operations_sdl(&sdl)?; - let bundle = - load_contract_bundle_if_requested(options.law.as_deref(), &ir, &operations)?; - let manifest = bundle.as_ref().map(|bundle| &bundle.manifest); let codec_import = options .codec_import .as_deref() @@ -719,7 +400,6 @@ fn run_emit_command(args: &[String]) -> Result { write_emit_metadata_if_requested( options.metadata_out.as_deref(), &ir, - manifest, LE_BINARY_TYPESCRIPT_GENERATOR_NAME, TYPESCRIPT_GENERATOR_VERSION, )?; @@ -736,9 +416,6 @@ fn run_emit_command(args: &[String]) -> Result { let sdl = read_file(&schema_path, "schema")?; let ir = lower_schema_sdl(&sdl)?; let operations = list_schema_operations_sdl(&sdl)?; - let bundle = - load_contract_bundle_if_requested(options.law.as_deref(), &ir, &operations)?; - let manifest = bundle.as_ref().map(|bundle| &bundle.manifest); let codec_import = options .codec_import .as_deref() @@ -749,7 +426,6 @@ fn run_emit_command(args: &[String]) -> Result { write_emit_metadata_if_requested( options.metadata_out.as_deref(), &ir, - manifest, LE_BINARY_RUST_GENERATOR_NAME, RUST_GENERATOR_VERSION, )?; @@ -986,7 +662,6 @@ fn print_doctor_text(report: &DoctorReport) { struct ParsedOptions { schema: Option, config: Option, - law: Option, old_schema: Option, new_schema: Option, revision: Option, @@ -995,9 +670,6 @@ struct ParsedOptions { metadata_out: Option, codec_import: Option, directive: Option, - family: Option, - profile: Option, - subject: Option, changed: Vec, changed_file: Option, format: Option, @@ -1005,7 +677,6 @@ struct ParsedOptions { exit_code: bool, json: bool, hash: bool, - accept: bool, } impl ParsedOptions { @@ -1021,24 +692,6 @@ impl ParsedOptions { .ok_or_else(|| CliError::usage(format!("missing --operation for `{command}`"))) } - fn required_law(&self, command: &str) -> Result { - self.law - .clone() - .ok_or_else(|| CliError::usage(format!("missing --law for `{command}`"))) - } - - fn required_old(&self, command: &str) -> Result { - self.old_schema - .clone() - .ok_or_else(|| CliError::usage(format!("missing --old for `{command}`"))) - } - - fn required_new(&self, command: &str) -> Result { - self.new_schema - .clone() - .ok_or_else(|| CliError::usage(format!("missing --new for `{command}`"))) - } - fn required_out(&self, command: &str) -> Result { self.out .clone() @@ -1051,18 +704,6 @@ impl ParsedOptions { .ok_or_else(|| CliError::usage(format!("missing --directive for `{command}`"))) } - fn required_family(&self, command: &str) -> Result { - self.family - .clone() - .ok_or_else(|| CliError::usage(format!("missing --family for `{command}`"))) - } - - fn required_subject(&self, command: &str) -> Result { - self.subject - .clone() - .ok_or_else(|| CliError::usage(format!("missing subject for `{command}`"))) - } - fn output_format(&self) -> Result { if self.json { return Ok(OutputFormat::Json); @@ -1126,20 +767,6 @@ fn parse_options(args: &[String], command: &str) -> Result - { - index += 1; - options.law = Some(PathBuf::from(required_value(args, index, "--law")?)); - } "--law" => { return Err(CliError::usage(format!( "unknown option '--law' for `{command}`" @@ -1168,27 +795,16 @@ fn parse_options(args: &[String], command: &str) -> Result { - index += 1; - options.family = Some(required_value(args, index, "--family")?); - } "--family" => { return Err(CliError::usage(format!( "unknown option '--family' for `{command}`" ))); } - "--profile" if command == "law coverage" => { - index += 1; - options.profile = Some(required_value(args, index, "--profile")?); - } "--profile" => { return Err(CliError::usage(format!( "unknown option '--profile' for `{command}`" ))); } - "--accept" if command == "law rebind" => { - options.accept = true; - } "--accept" => { return Err(CliError::usage(format!( "unknown option '--accept' for `{command}`" @@ -1258,11 +874,6 @@ fn parse_options(args: &[String], command: &str) -> Result { - if options.subject.replace(value.to_string()).is_some() { - return Err(CliError::usage("pass exactly one subject to `law explain`")); - } - } value => { return Err(CliError::usage(format!( "unexpected argument '{value}' for `{command}`" @@ -1696,7 +1307,6 @@ fn changed_files_from_options(options: &ParsedOptions) -> Result, Cl fn write_emit_metadata_if_requested( path: Option<&Path>, ir: &WesleyIR, - manifest: Option<&ContractBundleManifestV1>, generator: &'static str, generator_version: &'static str, ) -> Result<(), CliError> { @@ -1704,22 +1314,12 @@ fn write_emit_metadata_if_requested( return Ok(()); }; - let schema_hash = manifest - .map(|manifest| unqualified_sha256(&manifest.schema_hash)) - .transpose()? - .unwrap_or(compute_registry_hash(ir)?); - let schema_hash_qualified = manifest - .map(|manifest| manifest.schema_hash.clone()) - .unwrap_or_else(|| format!("sha256:{schema_hash}")); + let schema_hash = compute_registry_hash(ir)?; + let schema_hash_qualified = format!("sha256:{schema_hash}"); let metadata = EmitMetadata { schema_hash, schema_hash_qualified, - law_hash: manifest.map(|manifest| manifest.law_hash.clone()), - law_document_hash: manifest.and_then(|manifest| manifest.law_document_hash.clone()), - profile_hash: manifest.map(|manifest| manifest.profile_hash.clone()), - bundle_hash: manifest.map(|manifest| manifest.bundle_hash.clone()), - law_ir_codec: manifest.map(|manifest| manifest.law_ir_codec.clone()), generator, generator_version, execution_mode: RUST_NATIVE_EXECUTION_MODE, @@ -1730,31 +1330,6 @@ fn write_emit_metadata_if_requested( write_file(path, &json, "emit metadata") } -struct LoadedContractBundle { - law_ir: LawIrV1, - manifest: ContractBundleManifestV1, -} - -fn load_contract_bundle_if_requested( - law_path: Option<&Path>, - ir: &WesleyIR, - operations: &[wesley_core::SchemaOperation], -) -> Result, CliError> { - let Some(law_path) = law_path else { - return Ok(None); - }; - - let law_source = fs::read_to_string(law_path).map_err(|source| CliError::Io { - label: "law".to_string(), - path: law_path.to_path_buf(), - source: source.to_string(), - })?; - let law_ir = load_weslaw_yaml(&law_source)?; - let manifest = build_contract_bundle_manifest_v1(&law_ir, ir, operations)?; - - Ok(Some(LoadedContractBundle { law_ir, manifest })) -} - fn read_schema_diff_inputs(options: &ParsedOptions) -> Result<(String, String), CliError> { let explicit_mode = options.old_schema.is_some() || options.new_schema.is_some(); let git_mode = options.schema.is_some() || options.revision.is_some(); @@ -1978,125 +1553,11 @@ struct FlatChange { description: String, } -#[derive(serde::Serialize)] -#[serde(rename_all = "camelCase")] -struct LawValidateReport { - schema_hash: String, - law_hash: String, - law_document_hash: Option, - profile_hash: String, - bundle_hash: String, - bound_entry_count: usize, - manifest: ContractBundleManifestV1, -} - -#[derive(serde::Serialize)] -#[serde(rename_all = "camelCase")] -struct LawLintReport { - api_version: String, - family: String, - schema_hash: String, - active_entry_count: usize, - law_hash: String, -} - -#[derive(serde::Serialize)] -#[serde(rename_all = "camelCase")] -struct LawExplanation { - subject: String, - law_count: usize, - lines: Vec, -} - -impl LawExplanation { - fn to_text(&self) -> String { - let mut output = format!("Subject: {}\n", self.subject); - output.push_str(&format!("Bound laws: {}\n", self.law_count)); - for line in &self.lines { - output.push_str(line); - output.push('\n'); - } - output - } -} - -#[derive(serde::Serialize)] -#[serde(rename_all = "camelCase")] -struct LawRebindReport { - api_version: &'static str, - old_schema_hash: String, - new_schema_hash: String, - changed: bool, - accepted: bool, - #[serde(skip_serializing_if = "Option::is_none")] - output: Option, -} - -#[derive(serde::Serialize)] -#[serde(rename_all = "camelCase")] -struct CapabilityReport { - api_version: &'static str, - report_only: bool, - runtime_enforcement: bool, - note: &'static str, - footprints: Vec, -} - -#[derive(serde::Serialize)] -#[serde(rename_all = "camelCase")] -struct CapabilityFootprintReport { - law_id: String, - subject: String, - reads: Vec, - writes: Vec, - creates: Vec, - forbids: Vec, -} - -#[derive(serde::Serialize)] -#[serde(rename_all = "camelCase")] -struct LawCoverageReport { - api_version: &'static str, - profile: String, - required_total: usize, - required_covered: usize, - required_percent: f64, - categories: Vec, -} - -#[derive(serde::Serialize)] -#[serde(rename_all = "camelCase")] -struct LawCoverageCategory { - id: &'static str, - label: &'static str, - required: bool, - total: usize, - covered: usize, - missing_subjects: Vec, -} - -struct DraftSuggestion { - id: String, - subject: String, - source: String, - text: String, -} - #[derive(serde::Serialize)] #[serde(rename_all = "camelCase")] struct EmitMetadata { schema_hash: String, schema_hash_qualified: String, - #[serde(skip_serializing_if = "Option::is_none")] - law_hash: Option, - #[serde(skip_serializing_if = "Option::is_none")] - law_document_hash: Option, - #[serde(skip_serializing_if = "Option::is_none")] - profile_hash: Option, - #[serde(skip_serializing_if = "Option::is_none")] - bundle_hash: Option, - #[serde(skip_serializing_if = "Option::is_none")] - law_ir_codec: Option, generator: &'static str, generator_version: &'static str, execution_mode: &'static str, @@ -2173,442 +1634,6 @@ fn print_schema_delta( Ok(()) } -fn print_law_diff(report: &LawDiffReportV1, output_format: OutputFormat) -> Result<(), CliError> { - match output_format { - OutputFormat::Json => print_json(report)?, - OutputFormat::Text => println!("{}", format_law_diff_markdown_v1(report)), - OutputFormat::Summary => println!("{}", format_law_diff_summary(report)), - } - - Ok(()) -} - -fn format_law_diff_summary(report: &LawDiffReportV1) -> String { - if report.changes.is_empty() { - return "No semantic law changes detected.".to_string(); - } - - format!( - "{} semantic law change(s), oldLawHash {}, newLawHash {}", - report.changes.len(), - report.old_law_hash, - report.new_law_hash - ) -} - -fn capability_report_from_law(law_ir: &LawIrV1) -> CapabilityReport { - let footprints = law_ir - .entries - .iter() - .filter_map(|entry| { - let LawEntryBodyV1::FootprintLaw(body) = &entry.body else { - return None; - }; - Some(CapabilityFootprintReport { - law_id: entry.id.clone(), - subject: entry.subject.clone(), - reads: body.reads.clone(), - writes: body.writes.clone(), - creates: body.creates.clone(), - forbids: body.forbids.clone(), - }) - }) - .collect(); - - CapabilityReport { - api_version: "wesley.law-capabilities/v1", - report_only: true, - runtime_enforcement: false, - note: "Footprint capabilities are report-only in weslaw v1; no runtime enforcement is claimed.", - footprints, - } -} - -fn law_coverage_report( - ir: &WesleyIR, - operations: &[wesley_core::SchemaOperation], - law_ir: &LawIrV1, - profile: &str, -) -> LawCoverageReport { - let release_required = matches!(profile, "release" | "ci-release"); - let categories = vec![ - law_coverage_category( - "customScalarSemantics", - "Custom scalar semantic law", - release_required, - custom_scalar_subjects(ir), - law_ir, - |body| matches!(body, LawEntryBodyV1::ScalarSemantics(_)), - ), - law_coverage_category( - "variantInputLaw", - "Input variant law", - release_required, - variant_input_subjects(ir), - law_ir, - |body| matches!(body, LawEntryBodyV1::VariantLaw(_)), - ), - law_coverage_category( - "mutationFootprintLaw", - "Mutation footprint law", - release_required, - mutation_operation_subjects(operations), - law_ir, - |body| matches!(body, LawEntryBodyV1::FootprintLaw(_)), - ), - law_coverage_category( - "channelLaw", - "Channel law", - release_required, - schema_channel_subjects(ir), - law_ir, - |body| matches!(body, LawEntryBodyV1::ChannelLaw(_)), - ), - ]; - let required_total = categories - .iter() - .filter(|category| category.required) - .map(|category| category.total) - .sum(); - let required_covered = categories - .iter() - .filter(|category| category.required) - .map(|category| category.covered) - .sum(); - let required_percent = percentage(required_covered, required_total); - - LawCoverageReport { - api_version: "wesley.law-coverage/v1", - profile: profile.to_string(), - required_total, - required_covered, - required_percent, - categories, - } -} - -fn law_coverage_category( - id: &'static str, - label: &'static str, - required: bool, - subjects: Vec, - law_ir: &LawIrV1, - predicate: impl Fn(&LawEntryBodyV1) -> bool, -) -> LawCoverageCategory { - let subjects = subjects.into_iter().collect::>(); - let missing_subjects = subjects - .iter() - .filter(|subject| !has_law_for_subject(law_ir, subject, &predicate)) - .cloned() - .collect::>(); - let total = subjects.len(); - let covered = total - missing_subjects.len(); - - LawCoverageCategory { - id, - label, - required, - total, - covered, - missing_subjects, - } -} - -fn has_law_for_subject( - law_ir: &LawIrV1, - subject: &str, - predicate: impl Fn(&LawEntryBodyV1) -> bool, -) -> bool { - law_ir - .entries - .iter() - .any(|entry| entry.subject == subject && predicate(&entry.body)) -} - -fn custom_scalar_subjects(ir: &WesleyIR) -> Vec { - ir.types - .iter() - .filter(|definition| { - definition.kind == TypeKind::Scalar && !is_builtin_graphql_scalar(&definition.name) - }) - .map(|definition| format!("scalar:{}", definition.name)) - .collect() -} - -fn variant_input_subjects(ir: &WesleyIR) -> Vec { - ir.types - .iter() - .filter(|definition| { - definition.kind == TypeKind::InputObject - && definition.fields.iter().any(|field| field.name == "kind") - }) - .map(|definition| format!("input:{}", definition.name)) - .collect() -} - -fn mutation_operation_subjects(operations: &[wesley_core::SchemaOperation]) -> Vec { - operations - .iter() - .filter(|operation| matches!(operation.operation_type, OperationType::Mutation)) - .map(|operation| format!("operation:Mutation.{}", operation.field_name)) - .collect() -} - -fn schema_channel_subjects(ir: &WesleyIR) -> Vec { - ir.types - .iter() - .filter_map(|definition| { - let directive = definition.directives.get("wes_channel")?; - let name = directive.get("name").and_then(serde_json::Value::as_str)?; - let version = directive - .get("version") - .and_then(serde_json::Value::as_u64)?; - Some(format!("channel:{name}@{version}")) - }) - .collect() -} - -fn is_builtin_graphql_scalar(name: &str) -> bool { - matches!(name, "ID" | "String" | "Int" | "Float" | "Boolean") -} - -fn percentage(covered: usize, total: usize) -> f64 { - if total == 0 { - 100.0 - } else { - ((covered as f64 / total as f64) * 1000.0).round() / 10.0 - } -} - -fn draft_suggestions_from_descriptions(ir: &WesleyIR) -> Vec { - ir.types - .iter() - .filter_map(|definition| { - let description = definition.description.as_ref()?; - let (id_kind, subject_kind) = match definition.kind { - TypeKind::Object | TypeKind::Interface | TypeKind::Union => ("type", "type"), - TypeKind::Enum => ("enum", "enum"), - TypeKind::Scalar => ("scalar", "scalar"), - TypeKind::InputObject => ("input", "input"), - }; - Some(DraftSuggestion { - id: format!("draft.description.{id_kind}.{}", definition.name), - subject: format!("{subject_kind}:{}", definition.name), - source: "description".to_string(), - text: description.clone(), - }) - }) - .collect() -} - -fn render_weslaw_yaml(law_ir: &LawIrV1, drafts: &[DraftSuggestion]) -> String { - let mut output = String::new(); - output.push_str("apiVersion: weslaw/v1\n"); - output.push_str("schema:\n"); - output.push_str(&format!(" family: {}\n", yaml_quote(&law_ir.family))); - output.push_str(&format!(" hash: {}\n", law_ir.schema_hash)); - if let Some(source) = &law_ir.schema_source { - output.push_str(&format!(" source: {}\n", yaml_quote(source))); - } - if law_ir.entries.is_empty() && drafts.is_empty() { - output.push_str("laws: []\n"); - return output; - } - - output.push_str("laws:\n"); - - for entry in &law_ir.entries { - if let LawEntryBodyV1::ChannelLaw(body) = &entry.body { - output.push_str(&format!(" - id: {}\n", yaml_quote(&entry.id))); - output.push_str(" status: active\n"); - output.push_str(" kind: channelLaw\n"); - output.push_str(&format!(" subject: {}\n", yaml_quote(&entry.subject))); - output.push_str(&format!(" ordered: {}\n", body.ordered)); - output.push_str(&format!(" version: {}\n", body.version)); - output.push_str(" messages:\n"); - for message in &body.messages { - output.push_str(&format!(" - field: {}\n", yaml_quote(&message.field))); - output.push_str(&format!(" type: {}\n", yaml_quote(&message.r#type))); - } - } - } - - for draft in drafts { - output.push_str(&format!(" - id: {}\n", yaml_quote(&draft.id))); - output.push_str(" status: draft\n"); - output.push_str(&format!(" subject: {}\n", yaml_quote(&draft.subject))); - output.push_str(&format!(" source: {}\n", yaml_quote(&draft.source))); - output.push_str(&format!(" suggestion: {}\n", yaml_quote(&draft.text))); - } - - output -} - -fn yaml_quote(value: &str) -> String { - serde_json::to_string(value).expect("string serialization should not fail") -} - -fn explain_law_subject(law_ir: &LawIrV1, subject: &str) -> Result { - let mut lines = Vec::new(); - let mut law_count = 0; - for entry in law_ir - .entries - .iter() - .filter(|entry| entry.subject == subject) - { - law_count += 1; - lines.push(format!("Law: {}", entry.id)); - match &entry.body { - LawEntryBodyV1::ScalarSemantics(body) => explain_scalar_semantics(body, &mut lines), - LawEntryBodyV1::FootprintLaw(body) => explain_footprint(body, &mut lines), - LawEntryBodyV1::VariantLaw(_) => lines.push("Kind: variant law".to_string()), - LawEntryBodyV1::ChannelLaw(_) => lines.push("Kind: channel law".to_string()), - LawEntryBodyV1::InvariantLaw(_) => lines.push("Kind: invariant law".to_string()), - } - } - - if law_count == 0 { - return Err(CliError::usage(format!( - "no active law entries found for subject `{subject}`" - ))); - } - - Ok(LawExplanation { - subject: subject.to_string(), - law_count, - lines, - }) -} - -fn explain_scalar_semantics(body: &ScalarSemanticsLawV1, lines: &mut Vec) { - lines.push(format!( - "Kind: scalar semantics ({:?})", - body.representation - )); - if let Some(min) = body.min_inclusive { - lines.push(format!("Minimum: {min}")); - } - if let Some(max) = body.max_inclusive { - lines.push(format!("Maximum: {max}")); - } - if let Some(ordering) = body.ordering { - lines.push(format!("Ordering: {ordering:?}")); - } - if let Some(scope) = &body.scope { - lines.push(format!("Scope: {scope}")); - } - if !body.forbids.is_empty() { - lines.push(format!("Forbids: {}", serde_list(&body.forbids))); - } -} - -fn explain_footprint(body: &FootprintLawV1, lines: &mut Vec) { - lines.push("Kind: operation footprint".to_string()); - push_named_list(lines, "Reads", &body.reads); - push_named_list(lines, "Writes", &body.writes); - push_named_list(lines, "Creates", &body.creates); - push_named_list(lines, "Forbids", &body.forbids); -} - -fn push_named_list(lines: &mut Vec, label: &str, values: &[String]) { - if !values.is_empty() { - lines.push(format!("{label}: {}", values.join(", "))); - } -} - -fn print_nonempty_list(label: &str, values: &[String]) { - if !values.is_empty() { - println!("{label}: {}", values.join(", ")); - } -} - -fn coverage_profile_or_default(profile: Option<&str>) -> Result<&str, CliError> { - let profile = profile.unwrap_or("release"); - match profile { - "release" | "ci-release" | "local" => Ok(profile), - value => Err(CliError::usage(format!( - "unknown law coverage profile `{value}`; expected release, ci-release, or local" - ))), - } -} - -fn unqualified_sha256(value: &str) -> Result { - let Some(hash) = value.strip_prefix("sha256:") else { - return Err(CliError::usage(format!( - "expected sha256-qualified hash, got `{value}`" - ))); - }; - Ok(hash.to_string()) -} - -fn serde_list(values: &T) -> String { - match serde_json::to_value(values) { - Ok(serde_json::Value::Array(items)) => items - .iter() - .filter_map(serde_json::Value::as_str) - .collect::>() - .join(", "), - _ => "-".to_string(), - } -} - -fn replace_schema_hash_anchor( - source: &str, - old_schema_hash: &str, - new_schema_hash: &str, - path: &Path, -) -> Result { - let mut replacements = 0usize; - let mut in_schema = false; - let mut schema_indent = 0usize; - let trailing_newline = source.ends_with('\n'); - let mut lines = Vec::new(); - - for line in source.lines() { - let trimmed = line.trim_start(); - let indent = line.len() - trimmed.len(); - if !trimmed.is_empty() && !trimmed.starts_with('#') { - if trimmed == "schema:" { - in_schema = true; - schema_indent = indent; - } else if in_schema && indent <= schema_indent { - in_schema = false; - } - } - - if in_schema { - if let Some(hash_value) = trimmed.strip_prefix("hash:") { - let hash_without_comment = hash_value - .split_once('#') - .map(|(value, _)| value) - .unwrap_or(hash_value) - .trim(); - let unquoted_hash = hash_without_comment.trim_matches('"').trim_matches('\''); - if unquoted_hash == old_schema_hash { - replacements += 1; - lines.push(line.replacen(old_schema_hash, new_schema_hash, 1)); - continue; - } - } - } - - lines.push(line.to_string()); - } - - if replacements != 1 { - return Err(CliError::usage(format!( - "expected exactly one schema.hash anchor `{old_schema_hash}` in `{}`; found {replacements}", - path.display() - ))); - } - - let mut output = lines.join("\n"); - if trailing_newline { - output.push('\n'); - } - Ok(output) -} - fn flattened_schema_changes(delta: &SchemaDelta, breaking_only: bool) -> Vec { let mut changes = Vec::new(); @@ -2705,7 +1730,6 @@ Usage: Commands: normalize-sdl Print the Rust-core normalized SDL view doctor Run Rust-native health checks - init-law Scaffold weslaw/v1 from known SDL law directives config validate Validate a Wesley project manifest config inspect Print resolved manifest schema paths and targets config changed-schemas Select schema sets affected by changed files @@ -2714,13 +1738,6 @@ Commands: schema hash Print the Wesley L1 registry hash for GraphQL SDL schema operations List Query/Mutation/Subscription root operations schema diff Compare GraphQL SDL states as Wesley L1 IR - law validate Validate weslaw against active GraphQL SDL - law lint Validate weslaw structure without schema binding - law diff Compare weslaw semantic Law IR states - law explain Explain active laws bound to one subject - law rebind Re-anchor weslaw to an active schema hash - law capabilities Emit report-only footprint capability summaries - law coverage Report profile/category-aware law coverage emit rust Emit Rust models and operation bindings from GraphQL SDL emit typescript Emit TypeScript declarations and operation bindings from GraphQL SDL emit le-binary-typescript Emit TypeScript LE binary codecs from GraphQL SDL @@ -2735,24 +1752,6 @@ Options: ); } -fn print_init_law_help() { - println!( - "\ -Wesley init-law - -Scaffolds weslaw/v1 from formally known SDL law directives and draft -description-derived suggestions. Draft suggestions are not active law. - -Usage: - wesley init-law --schema --family [--out ] - -Options: - -s, --schema GraphQL SDL file - --family Contract family id for the generated law document - --out Optional output path; stdout when omitted" - ); -} - fn print_doctor_help() { println!( "\ @@ -2845,33 +1844,6 @@ Options: ); } -fn print_law_help() { - println!( - "\ -Wesley law commands - -Usage: - wesley law lint --law [--json] - wesley law validate --schema --law [--json] - wesley law diff --old --new [--schema ] [--format markdown|json|summary] - wesley law explain --law [--json] - wesley law rebind --schema --law [--accept --out ] [--json] - wesley law capabilities --law [--json] - wesley law coverage --schema --law [--profile release|ci-release|local] [--json] - -Options: - -s, --schema GraphQL SDL file used to validate the new law document - --law weslaw/v1 authoring file - --old Old/base weslaw/v1 authoring file for diff - --new New/target weslaw/v1 authoring file for diff - --accept Write an explicitly accepted rebind output - --out Rebind output path - --profile Coverage profile, default: release - --json Emit JSON output - --format Output format: markdown, json, or summary" - ); -} - fn print_emit_help() { println!( "\ @@ -2881,14 +1853,13 @@ Emits model declarations and root operation bindings when the schema declares Query, Mutation, or Subscription fields. Usage: - wesley emit rust --schema --out [--law ] [--metadata-out ] - wesley emit typescript --schema --out [--law ] [--metadata-out ] - wesley emit le-binary-typescript --schema --out [--law ] [--metadata-out ] [--codec-import ] - wesley emit le-binary-rust --schema --out [--law ] [--metadata-out ] [--codec-import ] + wesley emit rust --schema --out [--metadata-out ] + wesley emit typescript --schema --out [--metadata-out ] + wesley emit le-binary-typescript --schema --out [--metadata-out ] [--codec-import ] + wesley emit le-binary-rust --schema --out [--metadata-out ] [--codec-import ] Options: -s, --schema GraphQL SDL file - --law Optional weslaw/v1 file for bundle hashes --out Output file --metadata-out Deterministic metadata JSON sidecar --codec-import Module specifier for Writer/Reader/CodecError (le-binary-* only)" @@ -2920,7 +1891,6 @@ enum CliError { source: String, }, Core(WesleyError), - Law(WeslawError), Config(ProjectManifestError), Git(String), Json(String), @@ -2941,7 +1911,6 @@ impl CliError { Self::Usage(_) => EXIT_USAGE, Self::Io { .. } | Self::Core(_) - | Self::Law(_) | Self::Config(_) | Self::Git(_) | Self::Json(_) @@ -2967,13 +1936,6 @@ impl std::fmt::Display for CliError { path.display() ), Self::Core(error) => write!(formatter, "{error}"), - Self::Law(error) => { - write!(formatter, "{}: {}", error.code.as_str(), error.message)?; - if let Some(path) = &error.path { - write!(formatter, " ({path})")?; - } - Ok(()) - } Self::Config(error) => write!(formatter, "{error}"), Self::Git(error) => write!(formatter, "git error: {error}"), Self::Json(error) => write!(formatter, "failed to serialize JSON output: {error}"), @@ -2996,12 +1958,6 @@ impl From for CliError { } } -impl From for CliError { - fn from(error: WeslawError) -> Self { - Self::Law(error) - } -} - impl From for CliError { fn from(error: ProjectManifestError) -> Self { Self::Config(error) diff --git a/crates/wesley-cli/tests/cli.rs b/crates/wesley-cli/tests/cli.rs index 3bc9c970..5694caf3 100644 --- a/crates/wesley-cli/tests/cli.rs +++ b/crates/wesley-cli/tests/cli.rs @@ -9,26 +9,20 @@ fn help_exits_zero_without_footprint_command() { let stdout = String::from_utf8(output.stdout).expect("stdout should be utf8"); assert!(stdout.contains("Wesley native CLI")); assert!(stdout.contains("normalize-sdl")); - assert!(stdout.contains("init-law")); assert!(stdout.contains("schema lower")); assert!(stdout.contains("schema operations")); assert!(stdout.contains("schema diff")); assert!(stdout.contains("config validate")); assert!(stdout.contains("config changed-schemas")); assert!(stdout.contains("target verify")); - assert!(stdout.contains("law validate")); - assert!(stdout.contains("law lint")); - assert!(stdout.contains("law diff")); - assert!(stdout.contains("law explain")); - assert!(stdout.contains("law rebind")); - assert!(stdout.contains("law capabilities")); - assert!(stdout.contains("law coverage")); assert!(stdout.contains("doctor")); assert!(stdout.contains("emit rust")); assert!(stdout.contains("emit typescript")); assert!(stdout.contains("emit le-binary-typescript")); assert!(stdout.contains("operation selections")); assert!(!stdout.contains("check-footprint")); + assert!(!stdout.contains("init-law")); + assert!(!stdout.contains("law validate")); } #[test] @@ -536,528 +530,28 @@ fn removed_footprint_checker_is_not_a_wesley_command() { } #[test] -fn law_diff_json_emits_structured_semantic_events() { - let output = wesley() - .args(["law", "diff", "--old"]) - .arg(fixture("test/fixtures/weslaw/diff/old.weslaw.yaml")) - .arg("--new") - .arg(fixture("test/fixtures/weslaw/diff/new.weslaw.yaml")) - .arg("--json") - .output() - .expect("wesley should run"); - - assert_success(&output); - let stdout = String::from_utf8(output.stdout).expect("stdout should be utf8"); - let stderr = String::from_utf8(output.stderr).expect("stderr should be utf8"); - let report: serde_json::Value = serde_json::from_str(&stdout).expect("stdout should be json"); - - assert_eq!(report["apiVersion"], "wesley.law-diff/v1"); - assert!(report["oldLawHash"] - .as_str() - .expect("old law hash should be a string") - .starts_with("sha256:")); - assert_eq!( - report["changes"] - .as_array() - .expect("changes should be an array") - .iter() - .map(|change| change["kind"].as_str().expect("kind should be a string")) - .collect::>(), - vec!["LAW_WEAKENED", "LAW_WEAKENED", "FOOTPRINT_EXPANDED"] - ); - assert_eq!( - stdout, - std::fs::read_to_string(fixture("test/fixtures/weslaw/diff/ci-semantic-diff.json",)) - .expect("CI semantic diff fixture should read") - ); - assert!(stderr.is_empty()); -} - -#[test] -fn law_diff_markdown_summarizes_structured_events() { - let output = wesley() - .args(["law", "diff", "--old"]) - .arg(fixture("test/fixtures/weslaw/diff/old.weslaw.yaml")) - .arg("--new") - .arg(fixture("test/fixtures/weslaw/diff/new.weslaw.yaml")) - .arg("--format") - .arg("markdown") - .output() - .expect("wesley should run"); - - assert_success(&output); - let stdout = String::from_utf8(output.stdout).expect("stdout should be utf8"); - let stderr = String::from_utf8(output.stderr).expect("stderr should be utf8"); - - assert!(stdout.contains("# Wesley Law Diff")); - assert!(stdout.contains("| `LAW_WEAKENED` | `echo.scalar.positiveInt.u32-positive` |")); - assert!(stdout.contains("| `FOOTPRINT_EXPANDED` | `jedit.op.replaceRangeAsTick.footprint` |")); - assert_eq!( - stdout, - std::fs::read_to_string(fixture("test/fixtures/weslaw/diff/ci-semantic-diff.md",)) - .expect("CI semantic diff Markdown fixture should read") - ); - assert!(stderr.is_empty()); -} - -#[test] -fn law_diff_reports_binding_breaks_against_active_schema() { - let output = wesley() - .args(["law", "diff", "--old"]) - .arg(fixture("test/fixtures/weslaw/diff/old.weslaw.yaml")) - .arg("--new") - .arg(fixture( - "test/fixtures/weslaw/diff/binding-broken.weslaw.yaml", - )) - .arg("--schema") - .arg(fixture( - "test/fixtures/weslaw/contract-bundle-shape.graphql", - )) - .arg("--json") - .output() - .expect("wesley should run"); - - assert_eq!(output.status.code(), Some(1)); - let stdout = String::from_utf8(output.stdout).expect("stdout should be utf8"); - let stderr = String::from_utf8(output.stderr).expect("stderr should be utf8"); - let report: serde_json::Value = serde_json::from_str(&stdout).expect("stdout should be json"); - let changes = report["changes"] - .as_array() - .expect("changes should be an array"); - - assert!(changes - .iter() - .any(|change| change["kind"] == "BINDING_BROKEN")); - assert!(stdout.contains("WESLAW_UNRESOLVED_SUBJECT")); - assert_eq!( - stdout, - std::fs::read_to_string(fixture( - "test/fixtures/weslaw/diff/holmes-blade-binding-broken.json", - )) - .expect("Holmes/BLADE binding fixture should read") - ); - assert!(stderr.is_empty()); -} - -#[test] -fn law_validate_accepts_schema_bound_weslaw() { - let output = wesley() - .args(["law", "validate", "--schema"]) - .arg(fixture( - "test/fixtures/weslaw/contract-bundle-shape.graphql", - )) - .arg("--law") - .arg(fixture( - "test/fixtures/weslaw/accepted/footprint-replace-range.weslaw.yaml", - )) - .output() - .expect("wesley should run"); - - assert_success(&output); - let stdout = String::from_utf8(output.stdout).expect("stdout should be utf8"); - let stderr = String::from_utf8(output.stderr).expect("stderr should be utf8"); - - assert!(stdout.contains("Law validation passed: 1 active entries bound to sha256:")); - assert!(stderr.is_empty()); -} - -#[test] -fn law_validate_json_emits_bundle_manifest_hashes() { - let output = wesley() - .args(["law", "validate", "--schema"]) - .arg(fixture( - "test/fixtures/weslaw/contract-bundle-shape.graphql", - )) - .arg("--law") - .arg(fixture( - "test/fixtures/weslaw/accepted/footprint-replace-range.weslaw.yaml", - )) - .arg("--json") - .output() - .expect("wesley should run"); - - assert_success(&output); - let stdout = String::from_utf8(output.stdout).expect("stdout should be utf8"); - let stderr = String::from_utf8(output.stderr).expect("stderr should be utf8"); - let report: serde_json::Value = serde_json::from_str(&stdout).expect("stdout should be json"); - - assert_eq!(report["boundEntryCount"], 1); - assert!(report["schemaHash"] - .as_str() - .expect("schema hash should be a string") - .starts_with("sha256:")); - assert!(report["lawHash"] - .as_str() - .expect("law hash should be a string") - .starts_with("sha256:")); - assert!(report["lawDocumentHash"] - .as_str() - .expect("law document hash should be a string") - .starts_with("sha256:")); - assert!(report["profileHash"] - .as_str() - .expect("profile hash should be a string") - .starts_with("sha256:")); - assert_eq!(report["bundleHash"], report["manifest"]["bundleHash"]); - assert_eq!( - report["manifest"]["apiVersion"], - "wesley.contract-bundle-manifest/v1" - ); - assert!(stderr.is_empty()); -} - -#[test] -fn law_lint_accepts_structure_without_schema_binding() { - let output = wesley() - .args(["law", "lint", "--law"]) - .arg(fixture( - "test/fixtures/weslaw/rejected/schema-hash-mismatch.weslaw.yaml", - )) - .arg("--json") - .output() - .expect("wesley should run"); - - assert_success(&output); - let stdout = String::from_utf8(output.stdout).expect("stdout should be utf8"); - let stderr = String::from_utf8(output.stderr).expect("stderr should be utf8"); - let report: serde_json::Value = serde_json::from_str(&stdout).expect("stdout should be json"); - - assert_eq!(report["apiVersion"], "wesley.law-ir/v1"); - assert_eq!(report["activeEntryCount"], 1); - assert_eq!( - report["schemaHash"], - "sha256:0000000000000000000000000000000000000000000000000000000000000000" - ); - assert!(report["lawHash"] - .as_str() - .expect("law hash should be a string") - .starts_with("sha256:")); - assert!(stderr.is_empty()); -} - -#[test] -fn init_law_scaffolds_known_directive_law_and_draft_suggestions() { - let dir = temp_dir("init-law"); - let schema = dir.join("schema.graphql"); - let out = dir.join("scaffold.weslaw.yaml"); - - std::fs::write( - &schema, - r#" - directive @wes_channel(name: String!, version: Int!, ordered: Boolean!) on OBJECT - - """ - Positive integer values must preserve their intended domain. - """ - scalar PositiveInt - - type ReadyMessage { - ok: Boolean! - } - - type DemoChannel - @wes_channel(name: "demo.channel", version: 1, ordered: true) - { - ready: ReadyMessage! - } - - type Query { - ready: ReadyMessage! - } - "#, - ) - .expect("schema should write"); - - let output = wesley() - .args(["init-law", "--schema"]) - .arg(&schema) - .args(["--family", "adoption-test", "--out"]) - .arg(&out) - .output() - .expect("wesley should run"); - - assert_success(&output); - let stdout = String::from_utf8(output.stdout).expect("stdout should be utf8"); - let stderr = String::from_utf8(output.stderr).expect("stderr should be utf8"); - let scaffold = std::fs::read_to_string(&out).expect("scaffold should read"); - - assert!(stdout.is_empty()); - assert!(stderr.is_empty()); - assert!(scaffold.contains("family: \"adoption-test\"")); - assert!(scaffold.contains("kind: channelLaw")); - assert!(scaffold.contains("subject: \"channel:demo.channel@1\"")); - assert!(scaffold.contains("field: \"ready\"")); - assert!(scaffold.contains("type: \"ReadyMessage\"")); - assert!(scaffold.contains("status: draft")); - assert!(scaffold.contains("draft.description.scalar.PositiveInt")); - - let lint = wesley() - .args(["law", "lint", "--law"]) - .arg(&out) - .arg("--json") - .output() - .expect("wesley should run"); - assert_success(&lint); - let lint_stdout = String::from_utf8(lint.stdout).expect("stdout should be utf8"); - let lint_report: serde_json::Value = - serde_json::from_str(&lint_stdout).expect("stdout should be json"); - assert_eq!(lint_report["activeEntryCount"], 1); - - let _ = std::fs::remove_dir_all(dir); -} - -#[test] -fn law_explain_reports_scalar_semantics() { - let output = wesley() - .args(["law", "explain", "--law"]) - .arg(fixture( - "test/fixtures/weslaw/accepted/scalar-semantics.weslaw.yaml", - )) - .arg("scalar:PositiveInt") - .output() - .expect("wesley should run"); - - assert_success(&output); - let stdout = String::from_utf8(output.stdout).expect("stdout should be utf8"); - let stderr = String::from_utf8(output.stderr).expect("stderr should be utf8"); - - assert!(stdout.contains("Subject: scalar:PositiveInt")); - assert!(stdout.contains("Bound laws: 1")); - assert!(stdout.contains("Law: echo.scalar.positiveInt.u32-positive")); - assert!(stdout.contains("Kind: scalar semantics")); - assert!(stdout.contains("Minimum: 1")); - assert!(stdout.contains("Maximum: 4294967295")); - assert!(stdout.contains("Forbids: silentGraphQLIntNarrowing")); - assert!(stderr.is_empty()); -} - -#[test] -fn law_explain_reports_operation_footprint() { - let output = wesley() - .args(["law", "explain", "--law"]) - .arg(fixture( - "test/fixtures/weslaw/accepted/footprint-replace-range.weslaw.yaml", - )) - .arg("operation:Mutation.replaceRangeAsTick") - .output() - .expect("wesley should run"); - - assert_success(&output); - let stdout = String::from_utf8(output.stdout).expect("stdout should be utf8"); - let stderr = String::from_utf8(output.stderr).expect("stderr should be utf8"); - - assert!(stdout.contains("Subject: operation:Mutation.replaceRangeAsTick")); - assert!(stdout.contains("Law: jedit.op.replaceRangeAsTick.footprint")); - assert!(stdout.contains("Kind: operation footprint")); - assert!(stdout.contains("Reads: Anchor, BufferWorldline")); - assert!(stdout.contains("Writes: BufferWorldline")); - assert!(stdout.contains("Creates: RopeBranch, RopeHead")); - assert!(stdout.contains("Forbids: AstState, Diagnostics, GitWitness, UiState")); - assert!(stderr.is_empty()); -} - -#[test] -fn law_rebind_reports_and_accepts_schema_hash_updates() { - let dir = temp_dir("law-rebind"); - let out = dir.join("rebound.weslaw.yaml"); - let law_with_extra_hash = dir.join("law-with-extra-hash.weslaw.yaml"); - let schema = fixture("test/fixtures/weslaw/contract-bundle-shape.graphql"); - let law = fixture("test/fixtures/weslaw/rejected/schema-hash-mismatch.weslaw.yaml"); - let old_hash = "sha256:0000000000000000000000000000000000000000000000000000000000000000"; - let law_source = std::fs::read_to_string(&law).expect("law fixture should read"); - std::fs::write( - &law_with_extra_hash, - law_source.replace( - " semantics:\n", - &format!(" rationale: \"Historical mention {old_hash} must not be rewritten.\"\n semantics:\n"), - ), - ) - .expect("law fixture copy should write"); - - let report_output = wesley() - .args(["law", "rebind", "--schema"]) - .arg(&schema) - .arg("--law") - .arg(&law) - .arg("--json") - .output() - .expect("wesley should run"); - - assert_success(&report_output); - let report_stdout = String::from_utf8(report_output.stdout).expect("stdout should be utf8"); - let report: serde_json::Value = - serde_json::from_str(&report_stdout).expect("stdout should be json"); - assert_eq!(report["apiVersion"], "wesley.law-rebind/v1"); - assert_eq!(report["changed"], true); - assert_eq!(report["accepted"], false); - assert!(report["newSchemaHash"] - .as_str() - .expect("new schema hash should be a string") - .starts_with("sha256:")); - - let accept_output = wesley() - .args(["law", "rebind", "--schema"]) - .arg(&schema) - .arg("--law") - .arg(&law_with_extra_hash) - .args(["--accept", "--out"]) - .arg(&out) - .arg("--json") - .output() - .expect("wesley should run"); - - assert_success(&accept_output); - let accept_stdout = String::from_utf8(accept_output.stdout).expect("stdout should be utf8"); - let accept_report: serde_json::Value = - serde_json::from_str(&accept_stdout).expect("stdout should be json"); - assert_eq!(accept_report["accepted"], true); - assert_eq!(accept_report["output"], out.display().to_string()); - - let validate_output = wesley() - .args(["law", "validate", "--schema"]) - .arg(&schema) - .arg("--law") - .arg(&out) - .output() - .expect("wesley should run"); - assert_success(&validate_output); - let rebound = std::fs::read_to_string(&out).expect("rebound law should read"); - assert!(rebound.contains(&format!("Historical mention {old_hash}"))); - assert_eq!(rebound.matches(old_hash).count(), 1); - - let _ = std::fs::remove_dir_all(dir); -} - -#[test] -fn law_capabilities_reports_footprints_without_runtime_enforcement() { - let output = wesley() - .args(["law", "capabilities", "--law"]) - .arg(fixture( - "test/fixtures/weslaw/accepted/footprint-replace-range.weslaw.yaml", - )) - .arg("--json") - .output() - .expect("wesley should run"); - - assert_success(&output); - let stdout = String::from_utf8(output.stdout).expect("stdout should be utf8"); - let stderr = String::from_utf8(output.stderr).expect("stderr should be utf8"); - let report: serde_json::Value = serde_json::from_str(&stdout).expect("stdout should be json"); - - assert_eq!(report["apiVersion"], "wesley.law-capabilities/v1"); - assert_eq!(report["reportOnly"], true); - assert_eq!(report["runtimeEnforcement"], false); - assert_eq!( - report["footprints"][0]["lawId"], - "jedit.op.replaceRangeAsTick.footprint" - ); - assert_eq!( - report["footprints"][0]["subject"], - "operation:Mutation.replaceRangeAsTick" - ); - assert!(report["footprints"][0]["reads"] - .as_array() - .expect("reads should be an array") - .iter() - .any(|value| value == "BufferWorldline")); - assert!(report["footprints"][0]["forbids"] - .as_array() - .expect("forbids should be an array") - .iter() - .any(|value| value == "Diagnostics")); - assert!(stderr.is_empty()); -} +fn removed_weslaw_commands_are_not_wesley_commands() { + for command in ["law", "init-law"] { + let output = wesley().arg(command).output().expect("wesley should run"); -#[test] -fn law_coverage_reports_profile_categories() { - let output = wesley() - .args(["law", "coverage", "--schema"]) - .arg(fixture( - "test/fixtures/weslaw/contract-bundle-shape.graphql", - )) - .arg("--law") - .arg(fixture( - "test/fixtures/weslaw/accepted/rust-validator-payoff.weslaw.yaml", - )) - .args(["--profile", "release", "--json"]) - .output() - .expect("wesley should run"); - - assert_success(&output); - let stdout = String::from_utf8(output.stdout).expect("stdout should be utf8"); - let stderr = String::from_utf8(output.stderr).expect("stderr should be utf8"); - let report: serde_json::Value = serde_json::from_str(&stdout).expect("stdout should be json"); - - assert_eq!(report["apiVersion"], "wesley.law-coverage/v1"); - assert_eq!(report["profile"], "release"); - assert_eq!(report["requiredTotal"], 6); - assert_eq!(report["requiredCovered"], 2); - assert_eq!(report["requiredPercent"], 33.3); - - let categories = report["categories"] - .as_array() - .expect("categories should be an array"); - let scalar = categories - .iter() - .find(|category| category["id"] == "customScalarSemantics") - .expect("scalar coverage should exist"); - assert_eq!(scalar["total"], 3); - assert_eq!(scalar["covered"], 1); - assert!(scalar["missingSubjects"] - .as_array() - .expect("missing subjects should be an array") - .iter() - .any(|value| value == "scalar:WorldlineTick")); - - let variant = categories - .iter() - .find(|category| category["id"] == "variantInputLaw") - .expect("variant coverage should exist"); - assert_eq!(variant["covered"], 1); - assert!(stderr.is_empty()); -} - -#[test] -fn law_coverage_rejects_unknown_profiles() { - let output = wesley() - .args(["law", "coverage", "--schema"]) - .arg(fixture( - "test/fixtures/weslaw/contract-bundle-shape.graphql", - )) - .arg("--law") - .arg(fixture( - "test/fixtures/weslaw/accepted/rust-validator-payoff.weslaw.yaml", - )) - .args(["--profile", "prod", "--json"]) - .output() - .expect("wesley should run"); - - assert!(!output.status.success()); - let stderr = String::from_utf8(output.stderr).expect("stderr should be utf8"); - assert!(stderr.contains("unknown law coverage profile `prod`")); -} + assert_eq!( + output.status.code(), + Some(2), + "{command} must be rejected as an unsupported command" + ); + let stdout = String::from_utf8(output.stdout).expect("stdout should be utf8"); + let stderr = String::from_utf8(output.stderr).expect("stderr should be utf8"); + assert!(stdout.is_empty()); + assert!(stderr.contains(&format!("unknown command '{command}'"))); + } -#[test] -fn law_validate_reports_schema_hash_mismatch() { let output = wesley() - .args(["law", "validate", "--schema"]) - .arg(fixture( - "test/fixtures/weslaw/contract-bundle-shape.graphql", - )) - .arg("--law") - .arg(fixture( - "test/fixtures/weslaw/rejected/schema-hash-mismatch.weslaw.yaml", - )) + .args(["emit", "rust", "--law", "retired.weslaw.yaml"]) .output() .expect("wesley should run"); - - assert_eq!(output.status.code(), Some(1)); - let stdout = String::from_utf8(output.stdout).expect("stdout should be utf8"); + assert_eq!(output.status.code(), Some(2)); let stderr = String::from_utf8(output.stderr).expect("stderr should be utf8"); - - assert!(stdout.is_empty()); - assert!(stderr.contains("WESLAW_SCHEMA_HASH_MISMATCH")); - assert!(stderr.contains("$.schema.hash")); + assert!(stderr.contains("unknown option '--law' for `emit rust`")); } #[test] @@ -1683,100 +1177,22 @@ fn emit_commands_write_deterministic_metadata_sidecars() { ); assert_eq!(rust_json["executionMode"], "rust-native"); assert_eq!(typescript_json["executionMode"], "rust-native"); - - let _ = std::fs::remove_dir_all(dir); -} - -#[test] -fn emit_rust_with_law_embeds_schema_and_law_hash_constants() { - let dir = temp_dir("emit-rust-law-hashes"); - let out = dir.join("generated").join("model.rs"); - let metadata = dir.join("generated").join("model.metadata.json"); - - let output = wesley() - .args(["emit", "rust", "--schema"]) - .arg(fixture( - "test/fixtures/weslaw/contract-bundle-shape.graphql", - )) - .arg("--law") - .arg(fixture( - "test/fixtures/weslaw/accepted/footprint-replace-range.weslaw.yaml", - )) - .arg("--out") - .arg(&out) - .arg("--metadata-out") - .arg(&metadata) - .output() - .expect("wesley should run"); - - assert_success(&output); - let generated = std::fs::read_to_string(&out).expect("Rust output should read"); - let metadata_json: serde_json::Value = - serde_json::from_str(&std::fs::read_to_string(&metadata).expect("metadata should read")) - .expect("metadata should be JSON"); - - assert!(generated.contains("pub const WESLEY_SCHEMA_HASH: &'static str = \"sha256:")); - assert!(generated.contains("pub const WESLAW_HASH: &'static str = \"sha256:")); - assert_eq!( - metadata_json["lawHash"], - generated_hash_literal(&generated, "WESLAW_HASH") - ); - assert_eq!( - metadata_json["schemaHashQualified"], - generated_hash_literal(&generated, "WESLEY_SCHEMA_HASH") - ); - assert_eq!( - metadata_json["schemaHashQualified"], - format!( - "sha256:{}", - metadata_json["schemaHash"] - .as_str() - .expect("schema hash should be a string") - ) - ); - assert!(metadata_json["bundleHash"] - .as_str() - .expect("bundle hash should be a string") - .starts_with("sha256:")); - assert!(metadata_json["profileHash"] - .as_str() - .expect("profile hash should be a string") - .starts_with("sha256:")); - - let _ = std::fs::remove_dir_all(dir); -} - -#[test] -fn emit_rust_with_law_generates_scalar_and_variant_validators() { - let dir = temp_dir("emit-rust-law-validators"); - let out = dir.join("generated").join("model.rs"); - - let output = wesley() - .args(["emit", "rust", "--schema"]) - .arg(fixture( - "test/fixtures/weslaw/contract-bundle-shape.graphql", - )) - .arg("--law") - .arg(fixture( - "test/fixtures/weslaw/accepted/rust-validator-payoff.weslaw.yaml", - )) - .arg("--out") - .arg(&out) - .output() - .expect("wesley should run"); - - assert_success(&output); - let generated = std::fs::read_to_string(&out).expect("Rust output should read"); - assert!(generated.contains("pub fn validate_positive_int(value: u64)")); - assert!(generated.contains("if value < 1")); - assert!(generated.contains("if value > 4294967295")); - assert!(generated - .contains("pub fn validate_playback_mode_input_variant(value: &PlaybackModeInput)")); - assert!(generated.contains("PlaybackModeKind::Seek => {")); - assert!(generated.contains("if value.target.is_none()")); - assert!(generated.contains("if value.then.is_none()")); - assert!(generated.contains("PlaybackModeKind::Paused => {")); - assert!(generated.contains("if value.target.is_some()")); + for retired_field in [ + "lawHash", + "lawDocumentHash", + "profileHash", + "bundleHash", + "lawIrCodec", + ] { + assert!( + rust_json.get(retired_field).is_none(), + "Rust metadata must not carry retired field {retired_field}" + ); + assert!( + typescript_json.get(retired_field).is_none(), + "TypeScript metadata must not carry retired field {retired_field}" + ); + } let _ = std::fs::remove_dir_all(dir); } @@ -2078,14 +1494,6 @@ fn valid_target_descriptor_json(program: &str) -> String { ) } -fn generated_hash_literal(generated: &str, constant: &str) -> serde_json::Value { - let marker = format!("pub const {constant}: &'static str = \""); - let start = generated.find(&marker).expect("hash constant should exist") + marker.len(); - let rest = &generated[start..]; - let end = rest.find("\";").expect("hash constant should terminate"); - serde_json::Value::String(rest[..end].to_string()) -} - fn run_git(repo: &std::path::Path, args: [&str; N]) { let mut command = Command::new("git"); command.current_dir(repo).args(args); diff --git a/crates/wesley-core/README.md b/crates/wesley-core/README.md index daab6136..c5cd47a1 100644 --- a/crates/wesley-core/README.md +++ b/crates/wesley-core/README.md @@ -7,10 +7,10 @@ operation-selection and directive-argument analysis primitives. This crate is intended to be embedded by native Wesley tools and by downstream systems that need Wesley's GraphQL semantics without the CLI. -External semantic generators can consume `ExtensionGenerationInputV1` directly -from Rust. The input combines canonical Shape IR, normalized operations, -optional bound Law IR, explicit owner-declaration references, a settings digest, -and requested projection roles. `GenerationProvenanceManifestV1` then binds the +External generators can consume `ExtensionGenerationInputV2` directly from +Rust. The input combines canonical Shape IR, normalized operations, explicit +owner-declaration references, a settings digest, and requested projection +roles. `GenerationProvenanceManifestV2` then binds the exact generator, sources, input, settings, schema/ABI versions, and outputs. Verification recomputes every supplied digest without filesystem, registry, network, clock, process, or environment access. Target semantics and generated diff --git a/crates/wesley-core/src/domain/extension_generation.rs b/crates/wesley-core/src/domain/extension_generation.rs index 5c3e6992..07f2e1df 100644 --- a/crates/wesley-core/src/domain/extension_generation.rs +++ b/crates/wesley-core/src/domain/extension_generation.rs @@ -1,4 +1,4 @@ -//! Canonical input and provenance contracts for external semantic generators. +//! Canonical input and provenance contracts for external generators. //! //! The types in this module are deliberately domain-empty. They bind Wesley's //! canonical compiler facts to explicit owner declarations and generated @@ -11,39 +11,35 @@ use sha2::{Digest, Sha256}; use thiserror::Error; use super::ir::{compute_content_hash_bytes, to_canonical_json, WesleyIR}; -use super::law::{ - compute_law_hash_v1, to_canonical_law_ir_json, validate_law_ir_v1_bindings, - FootprintCardinalityV1, LawEntryBodyV1, LawIrV1, LawStatusV1, WESLEY_LAW_IR_API_VERSION, -}; use super::operation::{OperationType, SchemaOperation}; -/// API version for [`ExtensionGenerationInputV1`]. +/// API version for [`ExtensionGenerationInputV2`]. pub const WESLEY_EXTENSION_GENERATION_INPUT_API_VERSION: &str = - "wesley.extension-generation-input/v1"; + "wesley.extension-generation-input/v2"; -/// Canonical JSON codec for [`ExtensionGenerationInputV1`]. +/// Canonical JSON codec for [`ExtensionGenerationInputV2`]. pub const WESLEY_EXTENSION_GENERATION_INPUT_CODEC: &str = - "wesley.extension-generation-input.canonical-json.v1"; + "wesley.extension-generation-input.canonical-json.v2"; -/// API version for [`GenerationProvenanceManifestV1`]. +/// API version for [`GenerationProvenanceManifestV2`]. pub const WESLEY_GENERATION_PROVENANCE_MANIFEST_API_VERSION: &str = - "wesley.generation-provenance-manifest/v1"; + "wesley.generation-provenance-manifest/v2"; -/// Canonical JSON codec for [`GenerationProvenanceManifestV1`]. +/// Canonical JSON codec for [`GenerationProvenanceManifestV2`]. pub const WESLEY_GENERATION_PROVENANCE_MANIFEST_CODEC: &str = - "wesley.generation-provenance-manifest.canonical-json.v1"; + "wesley.generation-provenance-manifest.canonical-json.v2"; -/// API version for the derived [`GenerationReviewV1`] projection. -pub const WESLEY_GENERATION_REVIEW_API_VERSION: &str = "wesley.generation-review/v1"; +/// API version for the derived [`GenerationReviewV2`] projection. +pub const WESLEY_GENERATION_REVIEW_API_VERSION: &str = "wesley.generation-review/v2"; -/// Canonical JSON codec for [`GenerationReviewV1`]. -pub const WESLEY_GENERATION_REVIEW_CODEC: &str = "wesley.generation-review.canonical-json.v1"; +/// Canonical JSON codec for [`GenerationReviewV2`]. +pub const WESLEY_GENERATION_REVIEW_CODEC: &str = "wesley.generation-review.canonical-json.v2"; -/// ABI version implemented by external semantic generators consuming this contract. -pub const WESLEY_EXTENSION_GENERATOR_ABI_VERSION: &str = "wesley.extension-generator/v1"; +/// ABI version implemented by external generators consuming this contract. +pub const WESLEY_EXTENSION_GENERATOR_ABI_VERSION: &str = "wesley.extension-generator/v2"; -const INPUT_HASH_DOMAIN: &str = "wesley.extension-generation-input.digest.v1"; -const PROVENANCE_HASH_DOMAIN: &str = "wesley.generation-provenance-manifest.digest.v1"; +const INPUT_HASH_DOMAIN: &str = "wesley.extension-generation-input.digest.v2"; +const PROVENANCE_HASH_DOMAIN: &str = "wesley.generation-provenance-manifest.digest.v2"; /// One content-addressed source or generated artifact. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] @@ -97,22 +93,10 @@ impl GenerationArtifactContentV1 { } } -/// Canonical, validated Law IR carried into extension generation. +/// Pure structural input consumed by an external generator. #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct GenerationLawInputV1 { - /// Path-free, prose-free, normalized active Law IR. - pub law_ir: LawIrV1, - /// Exact semantic Law IR digest. - pub semantic_digest: String, - /// Exact canonical digest of the normalized typed Law IR. - pub canonical_digest: String, -} - -/// Pure semantic input consumed by an external generator. -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct ExtensionGenerationInputV1 { +pub struct ExtensionGenerationInputV2 { /// Exact input contract version. pub api_version: String, /// Canonical path-free Wesley Shape IR. @@ -121,10 +105,7 @@ pub struct ExtensionGenerationInputV1 { pub shape_digest: String, /// Normalized root operation catalog. pub operations: Vec, - /// Optional validated semantic Law IR and its exact digests. - #[serde(skip_serializing_if = "Option::is_none")] - pub law: Option, - /// Exact owner-declaration artifacts outside Wesley Shape and Law IR. + /// Exact target-owned declarations outside Wesley Shape IR. pub owner_declarations: Vec, /// Digest of explicit generator settings, computed by the caller. pub settings_digest: String, @@ -132,7 +113,7 @@ pub struct ExtensionGenerationInputV1 { pub projection_roles: Vec, } -impl ExtensionGenerationInputV1 { +impl ExtensionGenerationInputV2 { /// Builds a canonical generation input from explicit in-memory facts. /// /// This function performs no filesystem, registry, network, clock, process, @@ -140,7 +121,6 @@ impl ExtensionGenerationInputV1 { pub fn new( shape_ir: WesleyIR, operations: Vec, - law_ir: Option, owner_declarations: Vec, settings_digest: String, projection_roles: Vec, @@ -150,16 +130,12 @@ impl ExtensionGenerationInputV1 { let operations = normalize_operations(operations)?; let shape_json = canonical_json_bytes(&shape_ir)?; let shape_digest = compute_generation_artifact_digest_v1(&shape_json); - let law = law_ir - .map(|law_ir| normalize_law(law_ir, &shape_ir, &operations, &shape_digest)) - .transpose()?; Ok(Self { api_version: WESLEY_EXTENSION_GENERATION_INPUT_API_VERSION.to_owned(), shape_ir, shape_digest, operations, - law, owner_declarations: normalize_references(owner_declarations)?, settings_digest, projection_roles: normalize_strings(projection_roles, "projectionRoles")?, @@ -188,7 +164,6 @@ impl ExtensionGenerationInputV1 { let normalized = Self::new( self.shape_ir.clone(), self.operations.clone(), - self.law.as_ref().map(|law| law.law_ir.clone()), self.owner_declarations.clone(), self.settings_digest.clone(), self.projection_roles.clone(), @@ -201,33 +176,6 @@ impl ExtensionGenerationInputV1 { &self.shape_digest, )); } - match (&self.law, &normalized.law) { - (Some(actual), Some(expected)) => { - if actual.semantic_digest != expected.semantic_digest { - return Err(GenerationContractError::mismatch( - GenerationContractErrorKind::LawDigestMismatch, - "law.semanticDigest", - &expected.semantic_digest, - &actual.semantic_digest, - )); - } - if actual.canonical_digest != expected.canonical_digest { - return Err(GenerationContractError::mismatch( - GenerationContractErrorKind::LawDigestMismatch, - "law.canonicalDigest", - &expected.canonical_digest, - &actual.canonical_digest, - )); - } - } - (None, None) => {} - _ => { - return Err(GenerationContractError::new( - GenerationContractErrorKind::LawDigestMismatch, - "law", - )); - } - } Ok(normalized) } } @@ -270,16 +218,16 @@ impl GeneratorIdentityV1 { /// Frozen schema and ABI identities used by one provenance manifest. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct GenerationContractVersionsV1 { +pub struct GenerationContractVersionsV2 { /// Extension-generation input schema identity. pub input_schema: String, /// Generation-provenance manifest schema identity. pub provenance_schema: String, - /// External semantic-generator ABI identity. + /// External generator ABI identity. pub generator_abi: String, } -impl Default for GenerationContractVersionsV1 { +impl Default for GenerationContractVersionsV2 { fn default() -> Self { Self { input_schema: WESLEY_EXTENSION_GENERATION_INPUT_API_VERSION.to_owned(), @@ -292,27 +240,27 @@ impl Default for GenerationContractVersionsV1 { /// Provenance for one deterministic external generation invocation. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct GenerationProvenanceManifestV1 { +pub struct GenerationProvenanceManifestV2 { /// Exact manifest contract version. pub api_version: String, /// Exact generator identity. pub generator: GeneratorIdentityV1, - /// Digest of the canonical [`ExtensionGenerationInputV1`]. + /// Digest of the canonical [`ExtensionGenerationInputV2`]. pub generation_input_digest: String, /// Digest of the explicit settings carried by the generation input. pub settings_digest: String, /// Frozen Wesley schema and generator ABI identities. - pub contract_versions: GenerationContractVersionsV1, + pub contract_versions: GenerationContractVersionsV2, /// Exact owner-declaration source artifacts. pub source_artifacts: Vec, /// Exact emitted artifact references. pub emitted_artifacts: Vec, } -impl GenerationProvenanceManifestV1 { +impl GenerationProvenanceManifestV2 { /// Builds a provenance manifest from canonical input and explicit outputs. pub fn new( - input: &ExtensionGenerationInputV1, + input: &ExtensionGenerationInputV2, generator: GeneratorIdentityV1, emitted_artifacts: Vec, ) -> Result { @@ -329,7 +277,7 @@ impl GenerationProvenanceManifestV1 { generator, generation_input_digest: input.digest()?, settings_digest: input.settings_digest.clone(), - contract_versions: GenerationContractVersionsV1::default(), + contract_versions: GenerationContractVersionsV2::default(), source_artifacts: input.owner_declarations.clone(), emitted_artifacts, }) @@ -351,11 +299,11 @@ impl GenerationProvenanceManifestV1 { /// Verifies the generator and every referenced source and emitted artifact. pub fn verify( &self, - input: &ExtensionGenerationInputV1, + input: &ExtensionGenerationInputV2, generator_bytes: &[u8], source_artifacts: &[GenerationArtifactContentV1], emitted_artifacts: &[GenerationArtifactContentV1], - ) -> Result { + ) -> Result { let manifest = self.normalized()?; let input = input.normalized()?; let input_digest = input.digest()?; @@ -389,7 +337,7 @@ impl GenerationProvenanceManifestV1 { verify_materials(&manifest.source_artifacts, source_artifacts)?; verify_materials(&manifest.emitted_artifacts, emitted_artifacts)?; - Ok(GenerationProvenanceVerificationV1 { + Ok(GenerationProvenanceVerificationV2 { generation_input_digest: input_digest, verified_source_count: manifest.source_artifacts.len(), verified_output_count: manifest.emitted_artifacts.len(), @@ -408,7 +356,7 @@ impl GenerationProvenanceManifestV1 { self.generator.validate()?; validate_digest(&self.generation_input_digest, "generationInputDigest")?; validate_digest(&self.settings_digest, "settingsDigest")?; - let expected_versions = GenerationContractVersionsV1::default(); + let expected_versions = GenerationContractVersionsV2::default(); if self.contract_versions != expected_versions { return Err(GenerationContractError::new( GenerationContractErrorKind::ContractVersionMismatch, @@ -437,7 +385,7 @@ impl GenerationProvenanceManifestV1 { /// Receipt proving exact provenance materials were recomputed successfully. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct GenerationProvenanceVerificationV1 { +pub struct GenerationProvenanceVerificationV2 { /// Verified canonical generation-input digest. pub generation_input_digest: String, /// Number of exact source artifacts verified. @@ -449,7 +397,7 @@ pub struct GenerationProvenanceVerificationV1 { /// Deterministic, derived, non-authoritative review projection. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct GenerationReviewV1 { +pub struct GenerationReviewV2 { /// Exact review projection version. pub api_version: String, /// Always false: this projection is never an authority artifact. @@ -481,7 +429,7 @@ where Ok(false) } -impl GenerationReviewV1 { +impl GenerationReviewV2 { /// Returns false because review projections cannot claim authority. pub const fn authoritative(&self) -> bool { self.authoritative @@ -489,8 +437,8 @@ impl GenerationReviewV1 { /// Derives a non-authoritative review projection from validated input and provenance. pub fn from_manifest( - input: &ExtensionGenerationInputV1, - manifest: &GenerationProvenanceManifestV1, + input: &ExtensionGenerationInputV2, + manifest: &GenerationProvenanceManifestV2, ) -> Result { let input = input.normalized()?; let manifest = manifest.normalized()?; @@ -585,10 +533,6 @@ pub enum GenerationContractErrorKind { CoordinateDigestConflict, /// The claimed Shape IR digest did not match canonical bytes. ShapeDigestMismatch, - /// Law IR failed strict binding against Shape IR and operations. - LawBindingFailed, - /// A claimed Law IR digest did not match canonical bytes. - LawDigestMismatch, /// Frozen input-schema, provenance-schema, or generator-ABI identity changed. ContractVersionMismatch, /// A provenance manifest selected a different generation input. @@ -620,8 +564,6 @@ impl GenerationContractErrorKind { Self::DuplicateCoordinate => "WESLEY_GENERATION_DUPLICATE_COORDINATE", Self::CoordinateDigestConflict => "WESLEY_GENERATION_COORDINATE_DIGEST_CONFLICT", Self::ShapeDigestMismatch => "WESLEY_GENERATION_SHAPE_DIGEST_MISMATCH", - Self::LawBindingFailed => "WESLEY_GENERATION_LAW_BINDING_FAILED", - Self::LawDigestMismatch => "WESLEY_GENERATION_LAW_DIGEST_MISMATCH", Self::ContractVersionMismatch => "WESLEY_GENERATION_CONTRACT_VERSION_MISMATCH", Self::GenerationInputDigestMismatch => "WESLEY_GENERATION_INPUT_DIGEST_MISMATCH", Self::SettingsDigestMismatch => "WESLEY_GENERATION_SETTINGS_DIGEST_MISMATCH", @@ -749,198 +691,6 @@ fn normalize_operations( Ok(operations) } -fn normalize_law( - mut law_ir: LawIrV1, - shape_ir: &WesleyIR, - operations: &[SchemaOperation], - shape_digest: &str, -) -> Result { - if law_ir.api_version != WESLEY_LAW_IR_API_VERSION { - return Err(GenerationContractError::mismatch( - GenerationContractErrorKind::UnsupportedApiVersion, - "law.apiVersion", - WESLEY_LAW_IR_API_VERSION, - &law_ir.api_version, - )); - } - law_ir.schema_source = None; - for resource in &mut law_ir.registries.resources { - resource.notes = None; - } - law_ir - .registries - .resources - .sort_by(|left, right| left.id.cmp(&right.id)); - reject_duplicate_keys( - &law_ir.registries.resources, - |resource| resource.id.as_str(), - "law.registries.resources", - )?; - for verifier in &mut law_ir.registries.verifiers { - verifier.input_contracts = normalize_strings( - std::mem::take(&mut verifier.input_contracts), - &format!("law.verifier:{}.inputContracts", verifier.id), - )?; - } - law_ir - .registries - .verifiers - .sort_by(|left, right| left.id.cmp(&right.id)); - reject_duplicate_keys( - &law_ir.registries.verifiers, - |verifier| verifier.id.as_str(), - "law.registries.verifiers", - )?; - law_ir.registries.channels.sort_by(|left, right| { - (left.name.as_str(), left.version).cmp(&(right.name.as_str(), right.version)) - }); - for window in law_ir.registries.channels.windows(2) { - if window[0].name == window[1].name && window[0].version == window[1].version { - return Err(GenerationContractError::new( - GenerationContractErrorKind::DuplicateCoordinate, - format!("channel:{}@{}", window[0].name, window[0].version), - )); - } - } - for entry in &mut law_ir.entries { - if entry.status != LawStatusV1::Active { - return Err(GenerationContractError::new( - GenerationContractErrorKind::LawBindingFailed, - &entry.id, - )); - } - entry.rationale = None; - entry.source_index = None; - entry.tags = normalize_strings( - std::mem::take(&mut entry.tags), - &format!("law.entry:{}.tags", entry.id), - )?; - normalize_law_body(&entry.id, &mut entry.body)?; - } - law_ir.entries.sort_by(|left, right| left.id.cmp(&right.id)); - reject_duplicate_keys(&law_ir.entries, |entry| entry.id.as_str(), "law.entries")?; - - validate_law_ir_v1_bindings(&law_ir, shape_ir, operations, shape_digest).map_err(|error| { - GenerationContractError::mismatch( - GenerationContractErrorKind::LawBindingFailed, - error.path.unwrap_or_else(|| "law".to_owned()), - "valid bound Law IR", - error.code.as_str(), - ) - })?; - let semantic_digest = compute_law_hash_v1(&law_ir).map_err(|error| { - GenerationContractError::mismatch( - GenerationContractErrorKind::LawBindingFailed, - error.path.unwrap_or_else(|| "law".to_owned()), - "canonical semantic Law IR", - error.code.as_str(), - ) - })?; - let canonical_json = to_canonical_law_ir_json(&law_ir).map_err(canonicalization_error)?; - let canonical_digest = compute_generation_artifact_digest_v1(canonical_json.as_bytes()); - Ok(GenerationLawInputV1 { - law_ir, - semantic_digest, - canonical_digest, - }) -} - -fn normalize_law_body( - entry_id: &str, - body: &mut LawEntryBodyV1, -) -> Result<(), GenerationContractError> { - match body { - LawEntryBodyV1::ScalarSemantics(body) => body.forbids.sort(), - LawEntryBodyV1::VariantLaw(body) => { - for case in &mut body.cases { - case.requires = normalize_strings( - std::mem::take(&mut case.requires), - &format!("law.entry:{entry_id}.case:{}.requires", case.value), - )?; - case.forbids = normalize_strings( - std::mem::take(&mut case.forbids), - &format!("law.entry:{entry_id}.case:{}.forbids", case.value), - )?; - } - body.cases - .sort_by(|left, right| left.value.cmp(&right.value)); - reject_duplicate_keys( - &body.cases, - |case| case.value.as_str(), - &format!("law.entry:{entry_id}.cases"), - )?; - } - LawEntryBodyV1::FootprintLaw(body) => { - body.reads = normalize_strings( - std::mem::take(&mut body.reads), - &format!("law.entry:{entry_id}.reads"), - )?; - body.writes = normalize_strings( - std::mem::take(&mut body.writes), - &format!("law.entry:{entry_id}.writes"), - )?; - body.creates = normalize_strings( - std::mem::take(&mut body.creates), - &format!("law.entry:{entry_id}.creates"), - )?; - body.forbids = normalize_strings( - std::mem::take(&mut body.forbids), - &format!("law.entry:{entry_id}.forbids"), - )?; - for slot in &mut body.slots { - slot.access = normalize_strings( - std::mem::take(&mut slot.access), - &format!("law.entry:{entry_id}.slot:{}.access", slot.name), - )?; - } - body.slots.sort_by(|left, right| left.name.cmp(&right.name)); - reject_duplicate_keys( - &body.slots, - |slot| slot.name.as_str(), - &format!("law.entry:{entry_id}.slots"), - )?; - for closure in &mut body.closures { - closure.reads = normalize_strings( - std::mem::take(&mut closure.reads), - &format!("law.entry:{entry_id}.closure:{}.reads", closure.name), - )?; - } - body.closures - .sort_by(|left, right| left.name.cmp(&right.name)); - reject_duplicate_keys( - &body.closures, - |closure| closure.name.as_str(), - &format!("law.entry:{entry_id}.closures"), - )?; - for slot in &mut body.create_slots { - slot.cardinality = Some(slot.cardinality.unwrap_or(FootprintCardinalityV1::One)); - } - body.create_slots - .sort_by(|left, right| left.name.cmp(&right.name)); - reject_duplicate_keys( - &body.create_slots, - |slot| slot.name.as_str(), - &format!("law.entry:{entry_id}.createSlots"), - )?; - for update in &mut body.updates { - update.fields = normalize_strings( - std::mem::take(&mut update.fields), - &format!("law.entry:{entry_id}.update:{}.fields", update.slot), - )?; - } - body.updates - .sort_by(|left, right| left.slot.cmp(&right.slot)); - reject_duplicate_keys( - &body.updates, - |update| update.slot.as_str(), - &format!("law.entry:{entry_id}.updates"), - )?; - } - LawEntryBodyV1::ChannelLaw(_) | LawEntryBodyV1::InvariantLaw(_) => {} - } - Ok(()) -} - fn normalize_references( references: Vec, ) -> Result, GenerationContractError> { diff --git a/crates/wesley-core/src/domain/law.rs b/crates/wesley-core/src/domain/law.rs deleted file mode 100644 index 8a09024e..00000000 --- a/crates/wesley-core/src/domain/law.rs +++ /dev/null @@ -1,4049 +0,0 @@ -//! `weslaw` semantic Law IR v1. -//! -//! This module owns the first typed Rust substrate for `weslaw/v1` authoring -//! documents and the normalized `wesley.law-ir/v1` representation. - -use std::collections::{BTreeMap, BTreeSet, HashSet}; - -use serde::{Deserialize, Serialize}; -use thiserror::Error; -use yaml_rust2::yaml::Hash as Mapping; -use yaml_rust2::{Yaml, YamlLoader}; - -use super::ir::{ - compute_content_hash, compute_registry_hash, to_canonical_json, Field, TypeDefinition, - TypeKind, WesleyIR, -}; -use super::operation::{OperationType, SchemaOperation}; - -/// Authored `weslaw` document API version accepted by the v1 loader. -pub const WESLAW_API_VERSION: &str = "weslaw/v1"; - -/// Normalized Wesley Law IR API version emitted by the v1 loader. -pub const WESLEY_LAW_IR_API_VERSION: &str = "wesley.law-ir/v1"; - -/// Canonical JSON codec name for future Law IR hashing. -pub const WESLEY_LAW_IR_CANONICAL_JSON_CODEC: &str = "wesley.law-ir.canonical-json.v1"; - -/// Contract bundle manifest API version emitted by the v1 hash path. -pub const WESLEY_CONTRACT_BUNDLE_MANIFEST_API_VERSION: &str = "wesley.contract-bundle-manifest/v1"; - -/// Contract bundle canonical hash input codec. -pub const WESLEY_CONTRACT_BUNDLE_HASH_INPUT_CODEC: &str = - "wesley.contract-bundle.hash-input.canonical-json.v1"; - -/// Empty policy/profile API version used until Policy IR exists. -pub const WESLEY_EMPTY_PROFILE_API_VERSION: &str = "wesley.policy-profile.empty/v1"; - -/// Law diff report API version emitted by the first semantic diff model. -pub const WESLEY_LAW_DIFF_API_VERSION: &str = "wesley.law-diff/v1"; - -/// Diagnostic codes emitted by the `weslaw/v1` structure loader. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum WeslawDiagnosticCode { - /// The document could not be parsed as YAML. - ParseError, - /// The document used an unsupported `apiVersion`. - UnsupportedApiVersion, - /// The document shape was invalid for `weslaw/v1`. - InvalidDocument, - /// More than one active law entry used the same id. - DuplicateId, - /// An invariant used a raw string expression instead of a typed predicate. - RawExprRejected, - /// A law entry used a kind outside the closed v1 sum type. - UnknownKind, - /// A document object contained a field outside the v1 schema. - UnknownField, - /// A schema hash anchor did not match the active Shape IR hash. - SchemaHashMismatch, - /// A subject coordinate did not use the accepted v1 grammar. - InvalidCoordinate, - /// A subject coordinate did not resolve against Shape IR or law registries. - UnresolvedSubject, - /// A referenced field, enum value, argument path, resource, or verifier did not bind. - UnresolvedReference, - /// A law entry kind was not valid for the resolved subject kind. - WrongSubjectKind, - /// Active laws or their internal clauses contradicted each other. - Conflict, -} - -impl WeslawDiagnosticCode { - /// Returns the stable external diagnostic code. - pub fn as_str(self) -> &'static str { - match self { - Self::ParseError => "WESLAW_PARSE_ERROR", - Self::UnsupportedApiVersion => "WESLAW_UNSUPPORTED_API_VERSION", - Self::InvalidDocument => "WESLAW_INVALID_DOCUMENT", - Self::DuplicateId => "WESLAW_DUPLICATE_ID", - Self::RawExprRejected => "WESLAW_RAW_EXPR_REJECTED", - Self::UnknownKind => "WESLAW_UNKNOWN_KIND", - Self::UnknownField => "WESLAW_UNKNOWN_FIELD", - Self::SchemaHashMismatch => "WESLAW_SCHEMA_HASH_MISMATCH", - Self::InvalidCoordinate => "WESLAW_INVALID_COORDINATE", - Self::UnresolvedSubject => "WESLAW_UNRESOLVED_SUBJECT", - Self::UnresolvedReference => "WESLAW_UNRESOLVED_REFERENCE", - Self::WrongSubjectKind => "WESLAW_WRONG_SUBJECT_KIND", - Self::Conflict => "WESLAW_CONFLICT", - } - } -} - -/// Error returned by `weslaw/v1` structure loading. -#[derive(Debug, Error, Clone, PartialEq, Eq)] -#[error("{code:?}: {message}")] -pub struct WeslawError { - /// Stable diagnostic code. - pub code: WeslawDiagnosticCode, - /// Human-readable diagnostic summary. - pub message: String, - /// Dot path to the invalid field when known. - pub path: Option, -} - -impl WeslawError { - fn new(code: WeslawDiagnosticCode, message: impl Into) -> Self { - Self { - code, - message: message.into(), - path: None, - } - } - - fn at_path( - code: WeslawDiagnosticCode, - path: impl Into, - message: impl Into, - ) -> Self { - Self { - code, - message: message.into(), - path: Some(path.into()), - } - } -} - -/// Normalized Law IR v1 document. -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -#[serde(rename_all = "camelCase")] -pub struct LawIrV1 { - /// Law IR API version. Always `wesley.law-ir/v1` for this struct. - pub api_version: String, - /// Contract family identity from the authored schema anchor. - pub family: String, - /// Canonical schema hash anchor supplied by the authored document. - pub schema_hash: String, - /// Optional authored schema source path. - #[serde(skip_serializing_if = "Option::is_none")] - pub schema_source: Option, - /// Non-shape registries visible to Law IR entries. - pub registries: LawRegistrySetV1, - /// Normalized active law entries. - pub entries: Vec, -} - -/// Non-shape registries declared by a `weslaw/v1` document. -#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub struct LawRegistrySetV1 { - /// Declared runtime/resource/evidence domain ids. - pub resources: Vec, - /// Declared external verifier ids. - pub verifiers: Vec, - /// Declared non-shape channel ids. - pub channels: Vec, -} - -/// Resource registry entry for non-shape footprint and evidence symbols. -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub struct ResourceRegistryEntryV1 { - /// Stable resource id. - pub id: String, - /// Owning module, product, or family. - pub owner: String, - /// Resource category. - pub kind: String, - /// Optional notes retained outside semantic hashes. - #[serde(skip_serializing_if = "Option::is_none")] - pub notes: Option, -} - -/// Verifier registry entry for externally checked predicates. -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub struct VerifierRegistryEntryV1 { - /// Stable verifier id. - pub id: String, - /// Owning module, product, or family. - pub owner: String, - /// Accepted input contract ids. - pub input_contracts: Vec, -} - -/// Channel registry entry for non-shape protocol subjects. -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub struct ChannelRegistryEntryV1 { - /// Stable channel name. - pub name: String, - /// Channel version. - pub version: u64, - /// Carrier or transport family. - pub carrier: String, -} - -/// Common normalized Law IR entry. -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -#[serde(rename_all = "camelCase")] -pub struct LawEntryV1 { - /// Stable law id. - pub id: String, - /// Active or draft law status. - pub status: LawStatusV1, - /// Closed v1 law kind. - pub kind: LawKindV1, - /// Subject coordinate governed by this law. - pub subject: String, - /// Optional classifier tags. - pub tags: Vec, - /// Optional prose rationale excluded from semantic law hashing. - #[serde(skip_serializing_if = "Option::is_none")] - pub rationale: Option, - /// Kind-specific law body. - pub body: LawEntryBodyV1, - /// Authored zero-based `laws` sequence index, retained only for diagnostics. - #[serde(skip)] - pub source_index: Option, -} - -/// Law entry lifecycle state. -#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub enum LawStatusV1 { - /// Authoritative law included in active compilation. - #[default] - Active, - /// Draft law retained for review but not active compilation. - Draft, -} - -/// Closed Law IR v1 kind set. -#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub enum LawKindV1 { - /// Scalar representation and interpretation law. - ScalarSemantics, - /// Discriminated input/envelope variant law. - VariantLaw, - /// Operation footprint and resource effect law. - FootprintLaw, - /// Protocol/channel law. - ChannelLaw, - /// Typed invariant law. - InvariantLaw, -} - -/// Kind-specific normalized Law IR v1 body. -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -#[serde(untagged)] -pub enum LawEntryBodyV1 { - /// Scalar semantics body. - ScalarSemantics(ScalarSemanticsLawV1), - /// Variant law body. - VariantLaw(VariantLawV1), - /// Footprint law body. - FootprintLaw(FootprintLawV1), - /// Channel law body. - ChannelLaw(ChannelLawV1), - /// Invariant law body. - InvariantLaw(InvariantLawV1), -} - -/// Scalar semantics law body. -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub struct ScalarSemanticsLawV1 { - /// Scalar representation family. - pub representation: ScalarRepresentationV1, - /// Inclusive minimum value when the representation is numeric. - #[serde(skip_serializing_if = "Option::is_none")] - pub min_inclusive: Option, - /// Inclusive maximum value when the representation is numeric. - #[serde(skip_serializing_if = "Option::is_none")] - pub max_inclusive: Option, - /// Ordering semantics, when present. - #[serde(skip_serializing_if = "Option::is_none")] - pub ordering: Option, - /// Scope semantics, when present. - #[serde(skip_serializing_if = "Option::is_none")] - pub scope: Option, - /// Forbidden interpretations for this scalar. - pub forbids: Vec, -} - -/// Scalar representation families accepted by Law IR v1. -#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub enum ScalarRepresentationV1 { - /// Integer representation. - Integer, - /// Opaque identifier representation. - OpaqueIdentifier, - /// String representation. - String, -} - -/// Closed scalar ordering vocabulary accepted by Law IR v1. -#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub enum ScalarOrderingV1 { - /// No ordering semantics are implied. - #[serde(rename = "none")] - None, - /// Lamport-style logical ordering. - Lamport, - /// Total ordering semantics. - Total, - /// Partial ordering semantics. - Partial, -} - -/// Closed forbidden scalar interpretation enum. -#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -#[serde(rename_all = "camelCase")] -pub enum ScalarForbiddenInterpretationV1 { - /// Generated code must not silently narrow the value to GraphQL signed int. - #[serde(rename = "silentGraphQLIntNarrowing")] - SilentGraphqlIntNarrowing, -} - -/// Discriminated variant law body. -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub struct VariantLawV1 { - /// Discriminator field and enum. - pub discriminator: VariantDiscriminatorV1, - /// Per-case requirements. - pub cases: Vec, -} - -/// Variant discriminator descriptor. -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub struct VariantDiscriminatorV1 { - /// Discriminator field name. - pub field: String, - /// Discriminator enum name. - pub r#enum: String, -} - -/// Variant case law. -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub struct VariantCaseV1 { - /// Enum value for this case. - pub value: String, - /// Required fields for this case. - pub requires: Vec, - /// Forbidden fields for this case. - pub forbids: Vec, -} - -/// Operation footprint law body. -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub struct FootprintLawV1 { - /// Resource types read by the operation. - pub reads: Vec, - /// Resource types written by the operation. - pub writes: Vec, - /// Resource types created by the operation. - pub creates: Vec, - /// Resource domains forbidden to the operation. - pub forbids: Vec, - /// Bound input/resource slots. - pub slots: Vec, - /// Closure-derived resource windows. - pub closures: Vec, - /// Named create slots. - pub create_slots: Vec, - /// Field update surfaces. - pub updates: Vec, -} - -/// Footprint slot descriptor. -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub struct FootprintSlotV1 { - /// Slot name. - pub name: String, - /// Resource kind bound to the slot. - pub kind: String, - /// Argument path that binds the slot. - pub bind_from_arg: String, - /// Access modes granted for this slot. - pub access: Vec, -} - -/// Footprint closure descriptor. -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub struct FootprintClosureV1 { - /// Closure slot name. - pub name: String, - /// Source slot. - pub from_slot: String, - /// Closure operator id. - pub operator: String, - /// Argument bindings passed to the operator. - pub arg_bindings: Vec, - /// Resource kinds read by the closure. - pub reads: Vec, - /// Cardinality label. - pub cardinality: FootprintCardinalityV1, -} - -/// Footprint create-slot descriptor. -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub struct CreateSlotV1 { - /// Create slot name. - pub name: String, - /// Resource kind created for the slot. - pub kind: String, - /// Optional cardinality label. - #[serde(skip_serializing_if = "Option::is_none")] - pub cardinality: Option, -} - -/// Closed footprint cardinality vocabulary accepted by Law IR v1. -#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub enum FootprintCardinalityV1 { - /// Exactly one resource. - #[serde(rename = "one")] - One, - /// Zero or one resource. - Optional, - /// Zero or more resources. - Many, -} - -/// Footprint update descriptor. -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub struct FootprintUpdateV1 { - /// Slot being updated. - pub slot: String, - /// Fields updated on that slot. - pub fields: Vec, -} - -/// Channel law body. -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub struct ChannelLawV1 { - /// Whether the channel is ordered. - pub ordered: bool, - /// Channel version. - pub version: u64, - /// Optional compatibility posture. - #[serde(skip_serializing_if = "Option::is_none")] - pub compatibility: Option, - /// Channel message fields. - pub messages: Vec, -} - -/// Channel compatibility descriptor. -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub struct ChannelCompatibilityV1 { - /// Versioning family. - pub versioning: String, - /// Whether the channel version is coupled to semver. - pub semver_coupled: bool, -} - -/// Channel message descriptor. -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub struct ChannelMessageV1 { - /// GraphQL field name carrying the message. - pub field: String, - /// GraphQL type name for the message payload. - pub r#type: String, -} - -/// Typed invariant law body. -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -#[serde(rename_all = "camelCase")] -pub struct InvariantLawV1 { - /// Typed predicate for the invariant. - pub predicate: PredicateV1, -} - -/// Closed typed predicate set for Law IR v1 invariants. -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -#[serde(rename_all = "camelCase", tag = "op")] -pub enum PredicateV1 { - /// Checks a field against an exact JSON value. - FieldEquals { - /// Field name. - field: String, - /// Expected JSON value. - value: serde_json::Value, - }, - /// Delegates evaluation to a declared external verifier. - External { - /// Verifier id. - verifier: String, - /// External predicate reference. - r#ref: String, - /// Optional input contract id. - #[serde(skip_serializing_if = "Option::is_none")] - input_contract: Option, - }, -} - -/// Report emitted when active Law IR entries bind successfully. -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub struct LawBindingReportV1 { - /// Active Shape IR hash used for this validation pass. - pub schema_hash: String, - /// Number of active entries bound against the Shape IR. - pub bound_entry_count: usize, -} - -/// Contract bundle manifest emitted after schema-bound Law IR validation. -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub struct ContractBundleManifestV1 { - /// Manifest API version. - pub api_version: String, - /// Canonical Shape IR hash, prefixed with `sha256:`. - pub schema_hash: String, - /// Canonical active semantic Law IR hash. - pub law_hash: String, - /// Optional provenance-bearing law document hash. - #[serde(skip_serializing_if = "Option::is_none")] - pub law_document_hash: Option, - /// Canonical policy/profile hash. v1 uses the known empty profile. - pub profile_hash: String, - /// Hash over schema, law, profile, compiler, and codec identities. - pub bundle_hash: String, - /// Law IR semantic byte codec. - pub law_ir_codec: String, - /// Bundle hash input codec. - pub bundle_hash_codec: String, - /// Compiler crate identity. - pub compiler: String, - /// Compiler crate version. - pub compiler_version: String, - /// Bound active Law IR entry count. - pub law_entry_count: usize, -} - -/// Hashes computed from a bound active Law IR document. -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub struct LawHashSetV1 { - /// Canonical active semantic Law IR hash. - pub law_hash: String, - /// Provenance-bearing law document hash. - pub law_document_hash: String, -} - -/// Machine-readable semantic diff report for two Law IR v1 documents. -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -#[serde(rename_all = "camelCase")] -pub struct LawDiffReportV1 { - /// Report API version. - pub api_version: String, - /// Old document schema hash anchor. - pub old_schema_hash: String, - /// New document schema hash anchor. - pub new_schema_hash: String, - /// Old semantic Law IR hash. - pub old_law_hash: String, - /// New semantic Law IR hash. - pub new_law_hash: String, - /// Semantic change events, sorted by law id and event kind. - pub changes: Vec, -} - -/// Single semantic law diff event. -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -#[serde(rename_all = "camelCase")] -pub struct LawDiffEventV1 { - /// Event classification. - pub kind: LawDiffEventKindV1, - /// Stable law id affected by the event. - #[serde(skip_serializing_if = "Option::is_none")] - pub law_id: Option, - /// Subject coordinate affected by the event. - #[serde(skip_serializing_if = "Option::is_none")] - pub subject: Option, - /// Law kind affected by the event. - #[serde(skip_serializing_if = "Option::is_none")] - pub law_kind: Option, - /// Review posture for this initial diff event. - pub review_posture: LawDiffReviewPostureV1, - /// Field-level changes when a law body changed. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub field_changes: Vec, - /// Footprint resources newly read. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub added_reads: Vec, - /// Footprint resources no longer read. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub removed_reads: Vec, - /// Footprint resources newly written. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub added_writes: Vec, - /// Footprint resources no longer written. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub removed_writes: Vec, - /// Footprint resources newly created. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub added_creates: Vec, - /// Footprint resources no longer created. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub removed_creates: Vec, - /// Footprint resources newly forbidden. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub added_forbids: Vec, - /// Footprint resources no longer forbidden. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub removed_forbids: Vec, -} - -/// Law diff event classification. -#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -#[serde(rename_all = "SCREAMING_SNAKE_CASE")] -pub enum LawDiffEventKindV1 { - /// Bundle-level semantic fields changed. - LawBundleChanged, - /// Semantic registry facts changed. - RegistryChanged, - /// New active law entry. - LawAdded, - /// Active law entry removed. - LawRemoved, - /// Existing law tags changed. - LawTagsChanged, - /// Existing law was monotonically strengthened. - LawStrengthened, - /// Existing law was monotonically weakened. - LawWeakened, - /// Existing law changed outside a narrower v1 event class. - LawChanged, - /// Scalar semantic body changed. - ScalarSemanticsChanged, - /// Variant law body changed. - VariantLawChanged, - /// Footprint reach expanded. - FootprintExpanded, - /// Footprint reach contracted. - FootprintContracted, - /// Footprint changed in mixed or structural ways. - FootprintChanged, - /// Channel version changed. - ChannelVersionChanged, - /// Channel law changed without a channel-version change. - ChannelLawChanged, - /// Typed invariant predicate changed. - PredicateChanged, - /// A law no longer binds to the active schema or law registry. - BindingBroken, - /// Schema hash anchor changed. - SchemaHashRebound, -} - -/// Review posture for an initial semantic diff event. -#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)] -#[serde(rename_all = "kebab-case")] -pub enum LawDiffReviewPostureV1 { - /// The change requires human review. - RequiresReview, -} - -/// Field-level semantic law diff. -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -#[serde(rename_all = "camelCase")] -pub struct LawDiffFieldChangeV1 { - /// Law body path that changed. - pub path: String, - /// Previous canonical value. - pub old: serde_json::Value, - /// New canonical value. - pub new: serde_json::Value, -} - -/// Loads an authored `weslaw/v1` YAML document into normalized Law IR v1. -pub fn load_weslaw_yaml(source: &str) -> Result { - let documents = YamlLoader::load_from_str(source) - .map_err(|err| WeslawError::new(WeslawDiagnosticCode::ParseError, err.to_string()))?; - if documents.len() != 1 { - return Err(WeslawError::new( - WeslawDiagnosticCode::InvalidDocument, - "weslaw/v1 documents must contain exactly one YAML document", - )); - } - let document = &documents[0]; - let root = expect_mapping(document, "$")?; - reject_unknown_fields(root, "$", &["apiVersion", "schema", "registries", "laws"])?; - - let api_version = required_string(root, "apiVersion", "$.apiVersion")?; - if api_version != WESLAW_API_VERSION { - return Err(WeslawError::at_path( - WeslawDiagnosticCode::UnsupportedApiVersion, - "$.apiVersion", - format!("unsupported weslaw apiVersion {api_version}"), - )); - } - - let schema = required_mapping(root, "schema", "$.schema")?; - reject_unknown_fields(schema, "$.schema", &["family", "hash", "source"])?; - let family = required_string(schema, "family", "$.schema.family")?; - let schema_hash = required_string(schema, "hash", "$.schema.hash")?; - validate_schema_hash_anchor(&schema_hash, "$.schema.hash")?; - let schema_source = optional_string(schema, "source", "$.schema.source")?; - - let registries = match mapping_get(root, "registries") { - Some(value) => parse_registries(expect_mapping(value, "$.registries")?)?, - None => LawRegistrySetV1::default(), - }; - - let laws = required_sequence(root, "laws", "$.laws")?; - let mut active_ids = HashSet::new(); - let mut entries = Vec::with_capacity(laws.len()); - for (index, law_value) in laws.iter().enumerate() { - let path = format!("$.laws[{index}]"); - if let Some(mut entry) = parse_law_entry(law_value, &path)? { - entry.source_index = Some(index); - if !active_ids.insert(entry.id.clone()) { - return Err(WeslawError::at_path( - WeslawDiagnosticCode::DuplicateId, - format!("{path}.id"), - format!("duplicate active law id {}", entry.id), - )); - } - entries.push(entry); - } - } - sort_law_ir_entries(&mut entries); - - Ok(LawIrV1 { - api_version: WESLEY_LAW_IR_API_VERSION.to_string(), - family, - schema_hash, - schema_source, - registries, - entries, - }) -} - -/// Lowers formally known `@wes_channel` SDL directives into Law IR v1. -/// -/// This is the first directive-authored law bridge: SDL remains the source of -/// structural shape, while the directive facts lower into the same canonical -/// Law IR used by authored `weslaw/v1` documents. -pub fn lower_wes_channel_directives_to_law_ir_v1( - schema_ir: &WesleyIR, - family: &str, - schema_hash: &str, - schema_source: Option, -) -> Result { - validate_schema_hash_anchor(schema_hash, "schemaHash")?; - let mut entries = Vec::new(); - - for definition in &schema_ir.types { - let Some(directive) = definition.directives.get("wes_channel") else { - continue; - }; - if definition.kind != TypeKind::Object { - return Err(WeslawError::at_path( - WeslawDiagnosticCode::WrongSubjectKind, - format!("type:{}", definition.name), - "@wes_channel may only lower from object types", - )); - } - let name = directive_string_field( - directive, - "name", - &format!("type:{}.@wes_channel.name", definition.name), - )?; - let version = directive_u64_field( - directive, - "version", - &format!("type:{}.@wes_channel.version", definition.name), - )?; - let ordered = directive_bool_field( - directive, - "ordered", - &format!("type:{}.@wes_channel.ordered", definition.name), - )?; - let subject = format!("channel:{name}@{version}"); - parse_subject_coordinate(&subject, &format!("type:{}.@wes_channel", definition.name))?; - - entries.push(LawEntryV1 { - id: format!("directive.wes_channel.{name}.v{version}"), - status: LawStatusV1::Active, - kind: LawKindV1::ChannelLaw, - subject, - tags: Vec::new(), - rationale: None, - source_index: None, - body: LawEntryBodyV1::ChannelLaw(ChannelLawV1 { - ordered, - version, - compatibility: None, - messages: definition - .fields - .iter() - .map(|field| ChannelMessageV1 { - field: field.name.clone(), - r#type: field.r#type.base.clone(), - }) - .collect(), - }), - }); - } - - sort_law_ir_entries(&mut entries); - - Ok(LawIrV1 { - api_version: WESLEY_LAW_IR_API_VERSION.to_string(), - family: family.to_string(), - schema_hash: schema_hash.to_string(), - schema_source, - registries: LawRegistrySetV1::default(), - entries, - }) -} - -/// Serializes Law IR v1 as canonical JSON for the public v1 representation. -/// -/// This helper provides deterministic JSON bytes for `wesley.law-ir/v1` -/// exchange and fixture assertions. Semantic `lawHash` computation is stricter -/// and will be introduced separately because it excludes rationale and other -/// provenance-only fields. -pub fn to_canonical_law_ir_json(value: &LawIrV1) -> Result { - let mut normalized = value.clone(); - normalized - .entries - .retain(|entry| entry.status == LawStatusV1::Active); - sort_law_ir_entries(&mut normalized.entries); - to_canonical_json(&normalized) -} - -/// Serializes canonical active semantic Law IR v1 bytes for `lawHash`. -/// -/// The semantic form excludes authoring provenance such as `schemaSource`, -/// resource notes, entry status, and rationale prose. It sorts set-like arrays, -/// materializes v1 defaults, and preserves order-sensitive arrays such as -/// channel messages. -pub fn to_semantic_law_ir_json(value: &LawIrV1) -> Result { - let semantic = semantic_law_ir_value(value)?; - to_canonical_json(&semantic).map_err(canonicalization_error) -} - -/// Computes the `sha256:` hash over canonical active semantic Law IR v1. -pub fn compute_law_hash_v1(value: &LawIrV1) -> Result { - Ok(prefixed_sha256(&to_semantic_law_ir_json(value)?)) -} - -/// Computes law hashes from active semantic and provenance-bearing Law IR bytes. -pub fn compute_law_hash_set_v1(value: &LawIrV1) -> Result { - let law_hash = compute_law_hash_v1(value)?; - let document_json = to_canonical_law_ir_json(value).map_err(canonicalization_error)?; - let law_document_hash = prefixed_sha256(&document_json); - - Ok(LawHashSetV1 { - law_hash, - law_document_hash, - }) -} - -/// Builds the first contract bundle manifest after strict law binding succeeds. -pub fn build_contract_bundle_manifest_v1( - law_ir: &LawIrV1, - schema_ir: &WesleyIR, - operations: &[SchemaOperation], -) -> Result { - let schema_hash = - prefixed_sha256_hex(&compute_registry_hash(schema_ir).map_err(canonicalization_error)?); - let binding = validate_law_ir_v1_bindings(law_ir, schema_ir, operations, &schema_hash)?; - let law_hashes = compute_law_hash_set_v1(law_ir)?; - let profile_hash = empty_profile_hash_v1()?; - let bundle_hash = compute_bundle_hash_v1( - &schema_hash, - &law_hashes.law_hash, - &profile_hash, - WESLEY_LAW_IR_CANONICAL_JSON_CODEC, - WESLEY_CONTRACT_BUNDLE_HASH_INPUT_CODEC, - "wesley-core", - env!("CARGO_PKG_VERSION"), - )?; - - Ok(ContractBundleManifestV1 { - api_version: WESLEY_CONTRACT_BUNDLE_MANIFEST_API_VERSION.to_string(), - schema_hash, - law_hash: law_hashes.law_hash, - law_document_hash: Some(law_hashes.law_document_hash), - profile_hash, - bundle_hash, - law_ir_codec: WESLEY_LAW_IR_CANONICAL_JSON_CODEC.to_string(), - bundle_hash_codec: WESLEY_CONTRACT_BUNDLE_HASH_INPUT_CODEC.to_string(), - compiler: "wesley-core".to_string(), - compiler_version: env!("CARGO_PKG_VERSION").to_string(), - law_entry_count: binding.bound_entry_count, - }) -} - -/// Computes the first machine-readable semantic diff between two Law IR docs. -/// -/// This function compares active semantic law, not authoring prose. Rationale, -/// source paths, resource notes, and draft law entries do not create diff -/// events because they do not affect `lawHash`. -pub fn diff_law_ir_v1( - old_law_ir: &LawIrV1, - new_law_ir: &LawIrV1, -) -> Result { - let old_entries = law_entry_index(&old_law_ir.entries, "$.old.entries")?; - let new_entries = law_entry_index(&new_law_ir.entries, "$.new.entries")?; - let old_law_hash = compute_law_hash_v1(old_law_ir)?; - let new_law_hash = compute_law_hash_v1(new_law_ir)?; - let mut changes = Vec::new(); - - push_bundle_level_diff_events(old_law_ir, new_law_ir, &mut changes)?; - - for old_id in old_entries.keys() { - if !new_entries.contains_key(old_id) { - changes.push(law_lifecycle_event( - LawDiffEventKindV1::LawRemoved, - old_entries[old_id], - )); - } - } - - for new_id in new_entries.keys() { - if !old_entries.contains_key(new_id) { - changes.push(law_lifecycle_event( - LawDiffEventKindV1::LawAdded, - new_entries[new_id], - )); - } - } - - for law_id in old_entries - .keys() - .filter(|law_id| new_entries.contains_key(*law_id)) - { - let old_entry = old_entries[law_id]; - let new_entry = new_entries[law_id]; - if old_entry.kind != new_entry.kind || old_entry.subject != new_entry.subject { - changes.push(law_lifecycle_event( - LawDiffEventKindV1::LawRemoved, - old_entry, - )); - changes.push(law_lifecycle_event(LawDiffEventKindV1::LawAdded, new_entry)); - continue; - } - - if let Some(tags_event) = diff_law_tags(old_entry, new_entry)? { - changes.push(tags_event); - } - if semantic_body_value(old_entry)? == semantic_body_value(new_entry)? { - continue; - } - if let Some(event) = diff_law_entry_bodies(old_entry, new_entry)? { - changes.push(event); - } - } - - if changes.is_empty() && old_law_hash != new_law_hash { - changes.push(bundle_diff_event( - LawDiffEventKindV1::LawBundleChanged, - vec![LawDiffFieldChangeV1 { - path: "lawHash".to_string(), - old: old_law_hash.clone().into(), - new: new_law_hash.clone().into(), - }], - )); - } - - sort_law_diff_events(&mut changes); - - Ok(LawDiffReportV1 { - api_version: WESLEY_LAW_DIFF_API_VERSION.to_string(), - old_schema_hash: old_law_ir.schema_hash.clone(), - new_schema_hash: new_law_ir.schema_hash.clone(), - old_law_hash, - new_law_hash, - changes, - }) -} - -/// Records a schema-bound validation failure as a structured law diff event. -/// -/// This lets CI and assurance consumers receive a machine-readable diff report -/// even when the target `weslaw` document no longer binds to the active schema. -pub fn record_law_binding_error_v1(report: &mut LawDiffReportV1, error: &WeslawError) { - let mut error_value = serde_json::json!({ - "code": error.code.as_str(), - "message": error.message, - }); - if let Some(path) = &error.path { - error_value["path"] = path.clone().into(); - } - report.changes.push(bundle_diff_event( - LawDiffEventKindV1::BindingBroken, - vec![LawDiffFieldChangeV1 { - path: "binding".to_string(), - old: serde_json::Value::Null, - new: error_value, - }], - )); - sort_law_diff_events(&mut report.changes); -} - -/// Formats a `wesley.law-diff/v1` report as a Markdown review summary. -pub fn format_law_diff_markdown_v1(report: &LawDiffReportV1) -> String { - let mut output = String::new(); - output.push_str("# Wesley Law Diff\n\n"); - output.push_str("| Field | Value |\n"); - output.push_str("| --- | --- |\n"); - output.push_str(&format!("| API version | `{}` |\n", report.api_version)); - output.push_str(&format!( - "| Old schema hash | `{}` |\n", - report.old_schema_hash - )); - output.push_str(&format!( - "| New schema hash | `{}` |\n", - report.new_schema_hash - )); - output.push_str(&format!("| Old law hash | `{}` |\n", report.old_law_hash)); - output.push_str(&format!("| New law hash | `{}` |\n", report.new_law_hash)); - output.push('\n'); - - if report.changes.is_empty() { - output.push_str("No semantic law changes detected.\n"); - return output; - } - - output.push_str("## Changes\n\n"); - output.push_str("| Kind | Law | Subject | Summary |\n"); - output.push_str("| --- | --- | --- | --- |\n"); - for change in &report.changes { - output.push_str(&format!( - "| `{}` | {} | {} | {} |\n", - law_diff_kind_text(change.kind), - markdown_code_or_dash(change.law_id.as_deref()), - markdown_code_or_dash(change.subject.as_deref()), - markdown_escape(&law_diff_event_summary(change)), - )); - } - - output -} - -/// Validates active Law IR v1 entries against Shape IR and root operations. -/// -/// This is the strict binding gate for `WLAW-021` through `WLAW-035`: the law -/// document must target the active schema hash, each active subject must parse, -/// schema-backed subjects must resolve, kind-specific references must bind, and -/// the bundle must expose contradictory active law instead of silently -/// accepting it. -pub fn validate_law_ir_v1_bindings( - law_ir: &LawIrV1, - schema_ir: &WesleyIR, - operations: &[SchemaOperation], - active_schema_hash: &str, -) -> Result { - validate_schema_hash_anchor(active_schema_hash, "activeSchemaHash")?; - if law_ir.schema_hash != active_schema_hash { - return Err(WeslawError::at_path( - WeslawDiagnosticCode::SchemaHashMismatch, - "$.schema.hash", - format!( - "law document expects schema hash {}; active schema hash is {}", - law_ir.schema_hash, active_schema_hash - ), - )); - } - - let context = BindingContext { - schema_ir, - operations, - law_ir, - }; - - let mut unique_subject_entries = HashSet::new(); - for (index, entry) in law_ir.entries.iter().enumerate() { - let authored_index = entry.source_index.unwrap_or(index); - let law_path = format!("$.laws[{authored_index}]"); - let subject_path = format!("{law_path}.subject"); - let coordinate = parse_subject_coordinate(&entry.subject, &subject_path)?; - if law_kind_has_unique_subject(entry.kind) - && !unique_subject_entries.insert(format!("{:?}:{}", entry.kind, entry.subject)) - { - return Err(conflict( - entry, - &subject_path, - format!( - "more than one active {:?} entry targets {}", - entry.kind, entry.subject - ), - )); - } - context.bind_entry(entry, coordinate, &law_path)?; - } - - Ok(LawBindingReportV1 { - schema_hash: active_schema_hash.to_string(), - bound_entry_count: law_ir.entries.len(), - }) -} - -fn sort_law_ir_entries(entries: &mut [LawEntryV1]) { - entries.sort_by(|left, right| left.id.cmp(&right.id)); -} - -fn law_entry_index<'entry>( - entries: &'entry [LawEntryV1], - path: &str, -) -> Result, WeslawError> { - let mut index = BTreeMap::new(); - for (position, entry) in entries.iter().enumerate() { - if entry.status != LawStatusV1::Active { - continue; - } - if index.insert(entry.id.as_str(), entry).is_some() { - return Err(WeslawError::at_path( - WeslawDiagnosticCode::DuplicateId, - format!("{path}[{position}].id"), - format!("duplicate active law id {}", entry.id), - )); - } - } - Ok(index) -} - -fn sort_law_diff_events(changes: &mut [LawDiffEventV1]) { - changes.sort_by(|left, right| { - left.law_id - .as_deref() - .unwrap_or("") - .cmp(right.law_id.as_deref().unwrap_or("")) - .then(left.kind.cmp(&right.kind)) - .then( - left.subject - .as_deref() - .unwrap_or("") - .cmp(right.subject.as_deref().unwrap_or("")), - ) - }); -} - -fn law_diff_kind_text(kind: LawDiffEventKindV1) -> &'static str { - match kind { - LawDiffEventKindV1::LawBundleChanged => "LAW_BUNDLE_CHANGED", - LawDiffEventKindV1::RegistryChanged => "REGISTRY_CHANGED", - LawDiffEventKindV1::LawAdded => "LAW_ADDED", - LawDiffEventKindV1::LawRemoved => "LAW_REMOVED", - LawDiffEventKindV1::LawTagsChanged => "LAW_TAGS_CHANGED", - LawDiffEventKindV1::LawStrengthened => "LAW_STRENGTHENED", - LawDiffEventKindV1::LawWeakened => "LAW_WEAKENED", - LawDiffEventKindV1::LawChanged => "LAW_CHANGED", - LawDiffEventKindV1::ScalarSemanticsChanged => "SCALAR_SEMANTICS_CHANGED", - LawDiffEventKindV1::VariantLawChanged => "VARIANT_LAW_CHANGED", - LawDiffEventKindV1::FootprintExpanded => "FOOTPRINT_EXPANDED", - LawDiffEventKindV1::FootprintContracted => "FOOTPRINT_CONTRACTED", - LawDiffEventKindV1::FootprintChanged => "FOOTPRINT_CHANGED", - LawDiffEventKindV1::ChannelVersionChanged => "CHANNEL_VERSION_CHANGED", - LawDiffEventKindV1::ChannelLawChanged => "CHANNEL_LAW_CHANGED", - LawDiffEventKindV1::PredicateChanged => "PREDICATE_CHANGED", - LawDiffEventKindV1::BindingBroken => "BINDING_BROKEN", - LawDiffEventKindV1::SchemaHashRebound => "SCHEMA_HASH_REBOUND", - } -} - -fn markdown_code_or_dash(value: Option<&str>) -> String { - value - .map(|value| format!("`{}`", markdown_escape(value))) - .unwrap_or_else(|| "-".to_string()) -} - -fn markdown_escape(value: &str) -> String { - value.replace('|', "\\|").replace('\n', "
") -} - -fn law_diff_event_summary(event: &LawDiffEventV1) -> String { - match event.kind { - LawDiffEventKindV1::LawStrengthened => { - format!("law strengthened: {}", summarize_field_changes(event)) - } - LawDiffEventKindV1::LawWeakened => { - format!("law weakened: {}", summarize_field_changes(event)) - } - LawDiffEventKindV1::BindingBroken => { - format!("binding broken: {}", summarize_field_changes(event)) - } - LawDiffEventKindV1::FootprintExpanded => { - let mut parts = Vec::new(); - push_array_summary(&mut parts, "added reads", &event.added_reads); - push_array_summary(&mut parts, "added writes", &event.added_writes); - push_array_summary(&mut parts, "added creates", &event.added_creates); - push_array_summary(&mut parts, "removed forbids", &event.removed_forbids); - join_or_field_changes(parts, event) - } - LawDiffEventKindV1::FootprintContracted => { - let mut parts = Vec::new(); - push_array_summary(&mut parts, "removed reads", &event.removed_reads); - push_array_summary(&mut parts, "removed writes", &event.removed_writes); - push_array_summary(&mut parts, "removed creates", &event.removed_creates); - push_array_summary(&mut parts, "added forbids", &event.added_forbids); - join_or_field_changes(parts, event) - } - _ => summarize_field_changes(event), - } -} - -fn push_array_summary(parts: &mut Vec, label: &str, values: &[String]) { - if !values.is_empty() { - parts.push(format!("{label}: {}", values.join(", "))); - } -} - -fn join_or_field_changes(parts: Vec, event: &LawDiffEventV1) -> String { - if parts.is_empty() { - summarize_field_changes(event) - } else { - parts.join("; ") - } -} - -fn summarize_field_changes(event: &LawDiffEventV1) -> String { - if event.field_changes.is_empty() { - return "no field-level detail".to_string(); - } - event - .field_changes - .iter() - .map(|change| change.path.as_str()) - .collect::>() - .join(", ") -} - -fn push_bundle_level_diff_events( - old_law_ir: &LawIrV1, - new_law_ir: &LawIrV1, - changes: &mut Vec, -) -> Result<(), WeslawError> { - let mut bundle_changes = Vec::new(); - push_value_change( - &mut bundle_changes, - "family", - &old_law_ir.family, - &new_law_ir.family, - )?; - if !bundle_changes.is_empty() { - changes.push(bundle_diff_event( - LawDiffEventKindV1::LawBundleChanged, - bundle_changes, - )); - } - - let mut schema_changes = Vec::new(); - push_value_change( - &mut schema_changes, - "schemaHash", - &old_law_ir.schema_hash, - &new_law_ir.schema_hash, - )?; - if !schema_changes.is_empty() { - changes.push(bundle_diff_event( - LawDiffEventKindV1::SchemaHashRebound, - schema_changes, - )); - } - - let mut registry_changes = Vec::new(); - push_json_change( - &mut registry_changes, - "registries", - semantic_registry_value(&old_law_ir.registries)?, - semantic_registry_value(&new_law_ir.registries)?, - ); - if !registry_changes.is_empty() { - changes.push(bundle_diff_event( - LawDiffEventKindV1::RegistryChanged, - registry_changes, - )); - } - - Ok(()) -} - -fn diff_law_tags( - old_entry: &LawEntryV1, - new_entry: &LawEntryV1, -) -> Result, WeslawError> { - let mut field_changes = Vec::new(); - push_json_change( - &mut field_changes, - "tags", - string_vec_value(&old_entry.tags, "$.old.entries.tags")?, - string_vec_value(&new_entry.tags, "$.new.entries.tags")?, - ); - if field_changes.is_empty() { - return Ok(None); - } - - let mut event = base_diff_event(LawDiffEventKindV1::LawTagsChanged, new_entry); - event.field_changes = field_changes; - Ok(Some(event)) -} - -fn diff_law_entry_bodies( - old_entry: &LawEntryV1, - new_entry: &LawEntryV1, -) -> Result, WeslawError> { - match (&old_entry.body, &new_entry.body) { - (LawEntryBodyV1::ScalarSemantics(old_body), LawEntryBodyV1::ScalarSemantics(new_body)) => { - diff_scalar_semantics(old_entry, new_entry, old_body, new_body) - } - (LawEntryBodyV1::VariantLaw(old_body), LawEntryBodyV1::VariantLaw(new_body)) => { - diff_variant_law(old_entry, new_entry, old_body, new_body) - } - (LawEntryBodyV1::FootprintLaw(old_body), LawEntryBodyV1::FootprintLaw(new_body)) => { - diff_footprint_law(old_entry, new_entry, old_body, new_body) - } - (LawEntryBodyV1::ChannelLaw(old_body), LawEntryBodyV1::ChannelLaw(new_body)) => { - diff_channel_law(old_entry, new_entry, old_body, new_body) - } - (LawEntryBodyV1::InvariantLaw(old_body), LawEntryBodyV1::InvariantLaw(new_body)) => { - diff_invariant_law(old_entry, new_entry, old_body, new_body) - } - _ => { - let mut event = base_diff_event(LawDiffEventKindV1::LawChanged, new_entry); - push_json_change( - &mut event.field_changes, - "body", - semantic_body_value(old_entry)?, - semantic_body_value(new_entry)?, - ); - Ok(Some(event)) - } - } -} - -#[derive(Default)] -struct SemanticDirection { - strengthened: bool, - weakened: bool, - structural: bool, -} - -impl SemanticDirection { - fn mark_strengthened(&mut self) { - self.strengthened = true; - } - - fn mark_weakened(&mut self) { - self.weakened = true; - } - - fn mark_structural(&mut self) { - self.structural = true; - } - - fn note_min_change(&mut self, old: Option, new: Option) { - match (old, new) { - (None, Some(_)) => self.mark_strengthened(), - (Some(_), None) => self.mark_weakened(), - (Some(old), Some(new)) if new > old => self.mark_strengthened(), - (Some(old), Some(new)) if new < old => self.mark_weakened(), - _ => {} - } - } - - fn note_max_change(&mut self, old: Option, new: Option) { - match (old, new) { - (None, Some(_)) => self.mark_strengthened(), - (Some(_), None) => self.mark_weakened(), - (Some(old), Some(new)) if new < old => self.mark_strengthened(), - (Some(old), Some(new)) if new > old => self.mark_weakened(), - _ => {} - } - } - - fn note_scalar_forbids_change( - &mut self, - old: &[ScalarForbiddenInterpretationV1], - new: &[ScalarForbiddenInterpretationV1], - ) { - let old_values = old.iter().copied().collect::>(); - let new_values = new.iter().copied().collect::>(); - if new_values.difference(&old_values).next().is_some() { - self.mark_strengthened(); - } - if old_values.difference(&new_values).next().is_some() { - self.mark_weakened(); - } - } - - fn note_required_or_forbidden_change( - &mut self, - old: &[String], - new: &[String], - path: &str, - ) -> Result<(), WeslawError> { - if !set_added(old, new, path)?.is_empty() { - self.mark_strengthened(); - } - if !set_removed(old, new, path)?.is_empty() { - self.mark_weakened(); - } - Ok(()) - } - - fn event_kind(&self, fallback: LawDiffEventKindV1) -> LawDiffEventKindV1 { - match (self.structural, self.strengthened, self.weakened) { - (false, true, false) => LawDiffEventKindV1::LawStrengthened, - (false, false, true) => LawDiffEventKindV1::LawWeakened, - _ => fallback, - } - } -} - -fn diff_scalar_semantics( - _old_entry: &LawEntryV1, - new_entry: &LawEntryV1, - old_body: &ScalarSemanticsLawV1, - new_body: &ScalarSemanticsLawV1, -) -> Result, WeslawError> { - let mut field_changes = Vec::new(); - let mut direction = SemanticDirection::default(); - if old_body.representation != new_body.representation { - direction.mark_structural(); - } - push_value_change( - &mut field_changes, - "body.representation", - old_body.representation, - new_body.representation, - )?; - direction.note_min_change(old_body.min_inclusive, new_body.min_inclusive); - push_value_change( - &mut field_changes, - "body.minInclusive", - old_body.min_inclusive, - new_body.min_inclusive, - )?; - direction.note_max_change(old_body.max_inclusive, new_body.max_inclusive); - push_value_change( - &mut field_changes, - "body.maxInclusive", - old_body.max_inclusive, - new_body.max_inclusive, - )?; - if old_body.ordering != new_body.ordering { - direction.mark_structural(); - } - push_value_change( - &mut field_changes, - "body.ordering", - old_body.ordering, - new_body.ordering, - )?; - if old_body.scope != new_body.scope { - direction.mark_structural(); - } - push_value_change( - &mut field_changes, - "body.scope", - &old_body.scope, - &new_body.scope, - )?; - direction.note_scalar_forbids_change(&old_body.forbids, &new_body.forbids); - push_json_change( - &mut field_changes, - "body.forbids", - scalar_forbids_value(old_body)?, - scalar_forbids_value(new_body)?, - ); - - if field_changes.is_empty() { - return Ok(None); - } - - let mut event = base_diff_event( - direction.event_kind(LawDiffEventKindV1::ScalarSemanticsChanged), - new_entry, - ); - event.field_changes = field_changes; - Ok(Some(event)) -} - -fn diff_variant_law( - _old_entry: &LawEntryV1, - new_entry: &LawEntryV1, - old_body: &VariantLawV1, - new_body: &VariantLawV1, -) -> Result, WeslawError> { - let mut field_changes = Vec::new(); - let mut direction = SemanticDirection::default(); - if old_body.discriminator.field != new_body.discriminator.field { - direction.mark_structural(); - } - push_value_change( - &mut field_changes, - "body.discriminator.field", - &old_body.discriminator.field, - &new_body.discriminator.field, - )?; - if old_body.discriminator.r#enum != new_body.discriminator.r#enum { - direction.mark_structural(); - } - push_value_change( - &mut field_changes, - "body.discriminator.enum", - &old_body.discriminator.r#enum, - &new_body.discriminator.r#enum, - )?; - - let old_cases = variant_case_index(&old_body.cases, "$.old.entries.body.cases")?; - let new_cases = variant_case_index(&new_body.cases, "$.new.entries.body.cases")?; - for old_case in old_cases.keys() { - if !new_cases.contains_key(old_case) { - direction.mark_structural(); - push_json_change( - &mut field_changes, - &format!("body.cases.{old_case}"), - variant_case_value(old_cases[old_case])?, - serde_json::Value::Null, - ); - } - } - for new_case in new_cases.keys() { - if !old_cases.contains_key(new_case) { - direction.mark_structural(); - push_json_change( - &mut field_changes, - &format!("body.cases.{new_case}"), - serde_json::Value::Null, - variant_case_value(new_cases[new_case])?, - ); - } - } - for case_value in old_cases - .keys() - .filter(|case_value| new_cases.contains_key(*case_value)) - { - let old_case = old_cases[case_value]; - let new_case = new_cases[case_value]; - direction.note_required_or_forbidden_change( - &old_case.requires, - &new_case.requires, - "$.entries.body.cases.requires", - )?; - push_json_change( - &mut field_changes, - &format!("body.cases.{case_value}.requires"), - string_vec_value(&old_case.requires, "$.old.entries.body.cases.requires")?, - string_vec_value(&new_case.requires, "$.new.entries.body.cases.requires")?, - ); - direction.note_required_or_forbidden_change( - &old_case.forbids, - &new_case.forbids, - "$.entries.body.cases.forbids", - )?; - push_json_change( - &mut field_changes, - &format!("body.cases.{case_value}.forbids"), - string_vec_value(&old_case.forbids, "$.old.entries.body.cases.forbids")?, - string_vec_value(&new_case.forbids, "$.new.entries.body.cases.forbids")?, - ); - } - - if field_changes.is_empty() { - return Ok(None); - } - - let mut event = base_diff_event( - direction.event_kind(LawDiffEventKindV1::VariantLawChanged), - new_entry, - ); - event.field_changes = field_changes; - Ok(Some(event)) -} - -fn diff_footprint_law( - _old_entry: &LawEntryV1, - new_entry: &LawEntryV1, - old_body: &FootprintLawV1, - new_body: &FootprintLawV1, -) -> Result, WeslawError> { - let added_reads = set_added(&old_body.reads, &new_body.reads, "$.entries.body.reads")?; - let removed_reads = set_removed(&old_body.reads, &new_body.reads, "$.entries.body.reads")?; - let added_writes = set_added(&old_body.writes, &new_body.writes, "$.entries.body.writes")?; - let removed_writes = set_removed(&old_body.writes, &new_body.writes, "$.entries.body.writes")?; - let added_creates = set_added( - &old_body.creates, - &new_body.creates, - "$.entries.body.creates", - )?; - let removed_creates = set_removed( - &old_body.creates, - &new_body.creates, - "$.entries.body.creates", - )?; - let added_forbids = set_added( - &old_body.forbids, - &new_body.forbids, - "$.entries.body.forbids", - )?; - let removed_forbids = set_removed( - &old_body.forbids, - &new_body.forbids, - "$.entries.body.forbids", - )?; - - let old_value = semantic_footprint_body_value(old_body)?; - let new_value = semantic_footprint_body_value(new_body)?; - let mut field_changes = Vec::new(); - for path in ["slots", "closures", "createSlots", "updates"] { - push_json_change( - &mut field_changes, - &format!("body.{path}"), - old_value[path].clone(), - new_value[path].clone(), - ); - } - - let footprint_expanded = !added_reads.is_empty() - || !added_writes.is_empty() - || !added_creates.is_empty() - || !removed_forbids.is_empty(); - let footprint_contracted = !removed_reads.is_empty() - || !removed_writes.is_empty() - || !removed_creates.is_empty() - || !added_forbids.is_empty(); - let structural_change = !field_changes.is_empty(); - - if !footprint_expanded && !footprint_contracted && !structural_change { - return Ok(None); - } - - let event_kind = match (footprint_expanded, footprint_contracted, structural_change) { - (true, false, false) => LawDiffEventKindV1::FootprintExpanded, - (false, true, false) => LawDiffEventKindV1::FootprintContracted, - _ => LawDiffEventKindV1::FootprintChanged, - }; - let mut event = base_diff_event(event_kind, new_entry); - event.field_changes = field_changes; - event.added_reads = added_reads; - event.removed_reads = removed_reads; - event.added_writes = added_writes; - event.removed_writes = removed_writes; - event.added_creates = added_creates; - event.removed_creates = removed_creates; - event.added_forbids = added_forbids; - event.removed_forbids = removed_forbids; - Ok(Some(event)) -} - -fn diff_channel_law( - _old_entry: &LawEntryV1, - new_entry: &LawEntryV1, - old_body: &ChannelLawV1, - new_body: &ChannelLawV1, -) -> Result, WeslawError> { - let mut field_changes = Vec::new(); - push_value_change( - &mut field_changes, - "body.ordered", - old_body.ordered, - new_body.ordered, - )?; - push_value_change( - &mut field_changes, - "body.version", - old_body.version, - new_body.version, - )?; - push_json_change( - &mut field_changes, - "body.compatibility", - serde_json::to_value(&old_body.compatibility).map_err(canonicalization_error)?, - serde_json::to_value(&new_body.compatibility).map_err(canonicalization_error)?, - ); - push_json_change( - &mut field_changes, - "body.messages", - serde_json::to_value(&old_body.messages).map_err(canonicalization_error)?, - serde_json::to_value(&new_body.messages).map_err(canonicalization_error)?, - ); - if field_changes.is_empty() { - return Ok(None); - } - - let kind = if old_body.version != new_body.version { - LawDiffEventKindV1::ChannelVersionChanged - } else { - LawDiffEventKindV1::ChannelLawChanged - }; - let mut event = base_diff_event(kind, new_entry); - event.field_changes = field_changes; - Ok(Some(event)) -} - -fn diff_invariant_law( - _old_entry: &LawEntryV1, - new_entry: &LawEntryV1, - old_body: &InvariantLawV1, - new_body: &InvariantLawV1, -) -> Result, WeslawError> { - let mut field_changes = Vec::new(); - push_json_change( - &mut field_changes, - "body.predicate", - serde_json::to_value(&old_body.predicate).map_err(canonicalization_error)?, - serde_json::to_value(&new_body.predicate).map_err(canonicalization_error)?, - ); - if field_changes.is_empty() { - return Ok(None); - } - - let mut event = base_diff_event(LawDiffEventKindV1::PredicateChanged, new_entry); - event.field_changes = field_changes; - Ok(Some(event)) -} - -fn law_lifecycle_event(kind: LawDiffEventKindV1, entry: &LawEntryV1) -> LawDiffEventV1 { - base_diff_event(kind, entry) -} - -fn bundle_diff_event( - kind: LawDiffEventKindV1, - field_changes: Vec, -) -> LawDiffEventV1 { - LawDiffEventV1 { - kind, - law_id: None, - subject: None, - law_kind: None, - review_posture: LawDiffReviewPostureV1::RequiresReview, - field_changes, - added_reads: Vec::new(), - removed_reads: Vec::new(), - added_writes: Vec::new(), - removed_writes: Vec::new(), - added_creates: Vec::new(), - removed_creates: Vec::new(), - added_forbids: Vec::new(), - removed_forbids: Vec::new(), - } -} - -fn base_diff_event(kind: LawDiffEventKindV1, entry: &LawEntryV1) -> LawDiffEventV1 { - LawDiffEventV1 { - kind, - law_id: Some(entry.id.clone()), - subject: Some(entry.subject.clone()), - law_kind: Some(entry.kind), - review_posture: LawDiffReviewPostureV1::RequiresReview, - field_changes: Vec::new(), - added_reads: Vec::new(), - removed_reads: Vec::new(), - added_writes: Vec::new(), - removed_writes: Vec::new(), - added_creates: Vec::new(), - removed_creates: Vec::new(), - added_forbids: Vec::new(), - removed_forbids: Vec::new(), - } -} - -fn push_value_change( - changes: &mut Vec, - path: &str, - old: impl Serialize, - new: impl Serialize, -) -> Result<(), WeslawError> { - let old_value = serde_json::to_value(old).map_err(canonicalization_error)?; - let new_value = serde_json::to_value(new).map_err(canonicalization_error)?; - push_json_change(changes, path, old_value, new_value); - Ok(()) -} - -fn push_json_change( - changes: &mut Vec, - path: &str, - old: serde_json::Value, - new: serde_json::Value, -) { - if old == new { - return; - } - changes.push(LawDiffFieldChangeV1 { - path: path.to_string(), - old, - new, - }); -} - -fn directive_string_field( - value: &serde_json::Value, - field: &str, - path: &str, -) -> Result { - value - .get(field) - .and_then(serde_json::Value::as_str) - .map(ToString::to_string) - .ok_or_else(|| { - WeslawError::at_path( - WeslawDiagnosticCode::InvalidDocument, - path, - format!("directive field {field} must be a string"), - ) - }) -} - -fn directive_u64_field( - value: &serde_json::Value, - field: &str, - path: &str, -) -> Result { - value - .get(field) - .and_then(serde_json::Value::as_u64) - .ok_or_else(|| { - WeslawError::at_path( - WeslawDiagnosticCode::InvalidDocument, - path, - format!("directive field {field} must be an unsigned integer"), - ) - }) -} - -fn directive_bool_field( - value: &serde_json::Value, - field: &str, - path: &str, -) -> Result { - value - .get(field) - .and_then(serde_json::Value::as_bool) - .ok_or_else(|| { - WeslawError::at_path( - WeslawDiagnosticCode::InvalidDocument, - path, - format!("directive field {field} must be a boolean"), - ) - }) -} - -fn scalar_forbids_value(body: &ScalarSemanticsLawV1) -> Result { - let forbids = body - .forbids - .iter() - .map(|item| serde_json::to_value(item).map_err(canonicalization_error)) - .collect::, _>>()? - .into_iter() - .map(|value| { - value - .as_str() - .expect("forbidden interpretation should serialize as a string") - .to_string() - }) - .collect::>(); - Ok(sorted_unique_strings(&forbids, "$.entries.body.forbids")?.into()) -} - -fn variant_case_index<'case>( - cases: &'case [VariantCaseV1], - path: &str, -) -> Result, WeslawError> { - let mut index = BTreeMap::new(); - for (position, case) in cases.iter().enumerate() { - if index.insert(case.value.as_str(), case).is_some() { - return Err(WeslawError::at_path( - WeslawDiagnosticCode::Conflict, - format!("{path}[{position}].value"), - format!("duplicate variant case value {}", case.value), - )); - } - } - Ok(index) -} - -fn variant_case_value(case: &VariantCaseV1) -> Result { - Ok(serde_json::json!({ - "value": case.value, - "requires": sorted_unique_strings(&case.requires, "$.entries.body.cases.requires")?, - "forbids": sorted_unique_strings(&case.forbids, "$.entries.body.cases.forbids")?, - })) -} - -fn string_vec_value(values: &[String], path: &str) -> Result { - Ok(sorted_unique_strings(values, path)?.into()) -} - -fn set_added(old: &[String], new: &[String], path: &str) -> Result, WeslawError> { - let old_values = sorted_string_set(old, path)?; - let new_values = sorted_string_set(new, path)?; - Ok(new_values.difference(&old_values).cloned().collect()) -} - -fn set_removed(old: &[String], new: &[String], path: &str) -> Result, WeslawError> { - let old_values = sorted_string_set(old, path)?; - let new_values = sorted_string_set(new, path)?; - Ok(old_values.difference(&new_values).cloned().collect()) -} - -fn sorted_string_set(values: &[String], path: &str) -> Result, WeslawError> { - Ok(sorted_unique_strings(values, path)?.into_iter().collect()) -} - -fn semantic_law_ir_value(value: &LawIrV1) -> Result { - let entries = value - .entries - .iter() - .filter(|entry| entry.status == LawStatusV1::Active) - .map(semantic_entry_value) - .collect::, _>>()?; - - Ok(serde_json::json!({ - "apiVersion": value.api_version, - "family": value.family, - "schemaHash": value.schema_hash, - "registries": semantic_registry_value(&value.registries)?, - "entries": entries, - })) -} - -fn semantic_registry_value( - registries: &LawRegistrySetV1, -) -> Result { - let mut resources = registries - .resources - .iter() - .map(|resource| { - Ok(serde_json::json!({ - "id": resource.id, - "owner": resource.owner, - "kind": resource.kind, - })) - }) - .collect::, WeslawError>>()?; - sort_values_by_string_field(&mut resources, "id", "$.registries.resources")?; - - let mut verifiers = registries - .verifiers - .iter() - .map(|verifier| { - Ok(serde_json::json!({ - "id": verifier.id, - "owner": verifier.owner, - "inputContracts": sorted_unique_strings( - &verifier.input_contracts, - "$.registries.verifiers.inputContracts", - )?, - })) - }) - .collect::, WeslawError>>()?; - sort_values_by_string_field(&mut verifiers, "id", "$.registries.verifiers")?; - - let mut channels = registries - .channels - .iter() - .map(|channel| { - Ok(serde_json::json!({ - "name": channel.name, - "version": channel.version, - "carrier": channel.carrier, - })) - }) - .collect::, WeslawError>>()?; - channels.sort_by(|left, right| { - let left_key = ( - string_field(left, "name").unwrap_or_default(), - u64_field(left, "version").unwrap_or_default(), - ); - let right_key = ( - string_field(right, "name").unwrap_or_default(), - u64_field(right, "version").unwrap_or_default(), - ); - left_key.cmp(&right_key) - }); - - Ok(serde_json::json!({ - "resources": resources, - "verifiers": verifiers, - "channels": channels, - })) -} - -fn semantic_entry_value(entry: &LawEntryV1) -> Result { - Ok(serde_json::json!({ - "id": entry.id, - "kind": law_kind_text(entry.kind), - "subject": entry.subject, - "tags": sorted_unique_strings(&entry.tags, "$.entries.tags")?, - "body": semantic_body_value(entry)?, - })) -} - -fn semantic_body_value(entry: &LawEntryV1) -> Result { - match &entry.body { - LawEntryBodyV1::ScalarSemantics(body) => semantic_scalar_body_value(body), - LawEntryBodyV1::VariantLaw(body) => semantic_variant_body_value(body), - LawEntryBodyV1::FootprintLaw(body) => semantic_footprint_body_value(body), - LawEntryBodyV1::ChannelLaw(body) => semantic_channel_body_value(body), - LawEntryBodyV1::InvariantLaw(body) => { - serde_json::to_value(body).map_err(canonicalization_error) - } - } -} - -fn semantic_scalar_body_value( - body: &ScalarSemanticsLawV1, -) -> Result { - let mut object = serde_json::Map::new(); - object.insert( - "representation".to_string(), - serde_json::to_value(body.representation).map_err(canonicalization_error)?, - ); - if let Some(min) = body.min_inclusive { - object.insert("minInclusive".to_string(), min.into()); - } - if let Some(max) = body.max_inclusive { - object.insert("maxInclusive".to_string(), max.into()); - } - if let Some(ordering) = body.ordering { - object.insert( - "ordering".to_string(), - serde_json::to_value(ordering).map_err(canonicalization_error)?, - ); - } - if let Some(scope) = &body.scope { - object.insert("scope".to_string(), scope.clone().into()); - } - let forbids = body - .forbids - .iter() - .map(|item| serde_json::to_value(item).map_err(canonicalization_error)) - .collect::, _>>()? - .into_iter() - .map(|value| { - value - .as_str() - .expect("forbidden interpretation should serialize as a string") - .to_string() - }) - .collect::>(); - object.insert( - "forbids".to_string(), - sorted_unique_strings(&forbids, "$.entries.body.forbids")?.into(), - ); - Ok(serde_json::Value::Object(object)) -} - -fn semantic_variant_body_value(body: &VariantLawV1) -> Result { - let mut cases = body - .cases - .iter() - .map(|case| { - Ok(serde_json::json!({ - "value": case.value, - "requires": sorted_unique_strings(&case.requires, "$.entries.body.cases.requires")?, - "forbids": sorted_unique_strings(&case.forbids, "$.entries.body.cases.forbids")?, - })) - }) - .collect::, WeslawError>>()?; - sort_values_by_string_field(&mut cases, "value", "$.entries.body.cases")?; - - Ok(serde_json::json!({ - "discriminator": { - "field": body.discriminator.field, - "enum": body.discriminator.r#enum, - }, - "cases": cases, - })) -} - -fn semantic_footprint_body_value(body: &FootprintLawV1) -> Result { - let mut slots = body - .slots - .iter() - .map(|slot| { - Ok(serde_json::json!({ - "name": slot.name, - "kind": slot.kind, - "bindFromArg": slot.bind_from_arg, - "access": sorted_unique_strings(&slot.access, "$.entries.body.slots.access")?, - })) - }) - .collect::, WeslawError>>()?; - sort_values_by_string_field(&mut slots, "name", "$.entries.body.slots")?; - - let mut closures = body - .closures - .iter() - .map(|closure| { - Ok(serde_json::json!({ - "name": closure.name, - "fromSlot": closure.from_slot, - "operator": closure.operator, - "argBindings": closure.arg_bindings, - "reads": sorted_unique_strings(&closure.reads, "$.entries.body.closures.reads")?, - "cardinality": closure.cardinality, - })) - }) - .collect::, WeslawError>>()?; - sort_values_by_string_field(&mut closures, "name", "$.entries.body.closures")?; - - let mut create_slots = body - .create_slots - .iter() - .map(|slot| { - Ok(serde_json::json!({ - "name": slot.name, - "kind": slot.kind, - "cardinality": slot.cardinality.unwrap_or(FootprintCardinalityV1::One), - })) - }) - .collect::, WeslawError>>()?; - sort_values_by_string_field(&mut create_slots, "name", "$.entries.body.createSlots")?; - - let mut updates = body - .updates - .iter() - .map(|update| { - Ok(serde_json::json!({ - "slot": update.slot, - "fields": sorted_unique_strings(&update.fields, "$.entries.body.updates.fields")?, - })) - }) - .collect::, WeslawError>>()?; - sort_values_by_string_field(&mut updates, "slot", "$.entries.body.updates")?; - - Ok(serde_json::json!({ - "reads": sorted_unique_strings(&body.reads, "$.entries.body.reads")?, - "writes": sorted_unique_strings(&body.writes, "$.entries.body.writes")?, - "creates": sorted_unique_strings(&body.creates, "$.entries.body.creates")?, - "forbids": sorted_unique_strings(&body.forbids, "$.entries.body.forbids")?, - "slots": slots, - "closures": closures, - "createSlots": create_slots, - "updates": updates, - })) -} - -fn semantic_channel_body_value(body: &ChannelLawV1) -> Result { - let mut object = serde_json::Map::new(); - object.insert("ordered".to_string(), body.ordered.into()); - object.insert("version".to_string(), body.version.into()); - if let Some(compatibility) = &body.compatibility { - object.insert( - "compatibility".to_string(), - serde_json::to_value(compatibility).map_err(canonicalization_error)?, - ); - } - object.insert( - "messages".to_string(), - serde_json::to_value(&body.messages).map_err(canonicalization_error)?, - ); - Ok(serde_json::Value::Object(object)) -} - -fn compute_bundle_hash_v1( - schema_hash: &str, - law_hash: &str, - profile_hash: &str, - law_ir_codec: &str, - bundle_hash_codec: &str, - compiler: &str, - compiler_version: &str, -) -> Result { - let input = serde_json::json!({ - "schemaHash": schema_hash, - "lawHash": law_hash, - "profileHash": profile_hash, - "lawIrCodec": law_ir_codec, - "bundleHashCodec": bundle_hash_codec, - "compiler": compiler, - "compilerVersion": compiler_version, - }); - let bytes = to_canonical_json(&input).map_err(canonicalization_error)?; - Ok(prefixed_sha256(&bytes)) -} - -fn empty_profile_hash_v1() -> Result { - let input = serde_json::json!({ - "apiVersion": WESLEY_EMPTY_PROFILE_API_VERSION, - "entries": [], - }); - let bytes = to_canonical_json(&input).map_err(canonicalization_error)?; - Ok(prefixed_sha256(&bytes)) -} - -fn sorted_unique_strings(values: &[String], path: &str) -> Result, WeslawError> { - let mut sorted = values.to_vec(); - sorted.sort(); - if let Some((duplicate, _)) = sorted - .windows(2) - .find_map(|window| (window[0] == window[1]).then(|| (&window[0], &window[1]))) - { - return Err(WeslawError::at_path( - WeslawDiagnosticCode::Conflict, - path, - format!("duplicate set-like value {duplicate}"), - )); - } - Ok(sorted) -} - -fn sort_values_by_string_field( - values: &mut [serde_json::Value], - field: &str, - path: &str, -) -> Result<(), WeslawError> { - values.sort_by(|left, right| { - string_field(left, field) - .unwrap_or_default() - .cmp(string_field(right, field).unwrap_or_default()) - }); - if let Some(duplicate) = values.windows(2).find_map(|window| { - let left = string_field(&window[0], field)?; - let right = string_field(&window[1], field)?; - (left == right).then_some(left) - }) { - return Err(WeslawError::at_path( - WeslawDiagnosticCode::Conflict, - path, - format!("duplicate set-like key {duplicate}"), - )); - } - Ok(()) -} - -fn string_field<'a>(value: &'a serde_json::Value, field: &str) -> Option<&'a str> { - value.get(field).and_then(serde_json::Value::as_str) -} - -fn u64_field(value: &serde_json::Value, field: &str) -> Option { - value.get(field).and_then(serde_json::Value::as_u64) -} - -fn law_kind_text(kind: LawKindV1) -> &'static str { - match kind { - LawKindV1::ScalarSemantics => "scalarSemantics", - LawKindV1::VariantLaw => "variantLaw", - LawKindV1::FootprintLaw => "footprintLaw", - LawKindV1::ChannelLaw => "channelLaw", - LawKindV1::InvariantLaw => "invariantLaw", - } -} - -fn prefixed_sha256(value: &str) -> String { - prefixed_sha256_hex(&compute_content_hash(value)) -} - -fn prefixed_sha256_hex(value: &str) -> String { - format!("sha256:{value}") -} - -fn canonicalization_error(error: impl std::fmt::Display) -> WeslawError { - WeslawError::new( - WeslawDiagnosticCode::InvalidDocument, - format!("Law IR canonicalization failed: {error}"), - ) -} - -struct BindingContext<'a> { - schema_ir: &'a WesleyIR, - operations: &'a [SchemaOperation], - law_ir: &'a LawIrV1, -} - -impl BindingContext<'_> { - fn bind_entry( - &self, - entry: &LawEntryV1, - coordinate: SubjectCoordinate<'_>, - path: &str, - ) -> Result<(), WeslawError> { - match entry.kind { - LawKindV1::ScalarSemantics => self.bind_scalar_semantics(entry, coordinate, path), - LawKindV1::VariantLaw => self.bind_variant_law(entry, coordinate, path), - LawKindV1::FootprintLaw => self.bind_footprint_law(entry, coordinate, path), - LawKindV1::ChannelLaw => self.bind_channel_law(entry, coordinate, path), - LawKindV1::InvariantLaw => self.bind_invariant_law(entry, coordinate, path), - } - } - - fn bind_scalar_semantics( - &self, - entry: &LawEntryV1, - coordinate: SubjectCoordinate<'_>, - path: &str, - ) -> Result<(), WeslawError> { - let SubjectCoordinate::Scalar(name) = coordinate else { - return Err(wrong_subject_kind(entry, path, "scalar:")); - }; - self.require_type(name, TypeKind::Scalar, entry, path)?; - Ok(()) - } - - fn bind_variant_law( - &self, - entry: &LawEntryV1, - coordinate: SubjectCoordinate<'_>, - path: &str, - ) -> Result<(), WeslawError> { - let SubjectCoordinate::Input(name) = coordinate else { - return Err(wrong_subject_kind(entry, path, "input:")); - }; - let input = self.require_type(name, TypeKind::InputObject, entry, path)?; - let LawEntryBodyV1::VariantLaw(body) = &entry.body else { - return Err(wrong_subject_kind(entry, path, "variant law body")); - }; - - let discriminator = self.require_input_field( - input, - &body.discriminator.field, - entry, - format!("{path}.discriminator.field"), - )?; - if discriminator.r#type.base != body.discriminator.r#enum { - return Err(unresolved_reference( - entry, - format!("{path}.discriminator.enum"), - format!( - "discriminator field {} has type {}, not enum {}", - body.discriminator.field, discriminator.r#type.base, body.discriminator.r#enum - ), - )); - } - let enum_type = self.require_type_reference( - &body.discriminator.r#enum, - TypeKind::Enum, - entry, - format!("{path}.discriminator.enum"), - )?; - - for (case_index, case) in body.cases.iter().enumerate() { - let case_path = format!("{path}.cases[{case_index}]"); - if !enum_type - .enum_values - .iter() - .any(|candidate| candidate == &case.value) - { - return Err(unresolved_reference( - entry, - format!("{case_path}.value"), - format!( - "enum {} has no value {}", - body.discriminator.r#enum, case.value - ), - )); - } - for field in &case.requires { - self.require_input_field(input, field, entry, format!("{case_path}.requires"))?; - } - for field in &case.forbids { - self.require_input_field(input, field, entry, format!("{case_path}.forbids"))?; - } - if let Some(field) = first_overlap(&case.requires, &case.forbids) { - return Err(conflict( - entry, - case_path, - format!( - "variant case {} both requires and forbids {field}", - case.value - ), - )); - } - } - Ok(()) - } - - fn bind_footprint_law( - &self, - entry: &LawEntryV1, - coordinate: SubjectCoordinate<'_>, - path: &str, - ) -> Result<(), WeslawError> { - let SubjectCoordinate::Operation { - operation_type, - field, - } = coordinate - else { - return Err(wrong_subject_kind( - entry, - path, - "operation:Query., operation:Mutation., or operation:Subscription.", - )); - }; - let operation = self.require_operation(operation_type, field, entry, path)?; - let LawEntryBodyV1::FootprintLaw(body) = &entry.body else { - return Err(wrong_subject_kind(entry, path, "footprint law body")); - }; - - for resource in body - .reads - .iter() - .chain(body.writes.iter()) - .chain(body.creates.iter()) - .chain(body.forbids.iter()) - .chain(body.slots.iter().map(|slot| &slot.kind)) - .chain( - body.closures - .iter() - .flat_map(|closure| closure.reads.iter()), - ) - .chain(body.create_slots.iter().map(|slot| &slot.kind)) - { - self.require_resource(resource, entry, format!("{path}.resources"))?; - } - - let mut slot_names = HashSet::new(); - for (slot_index, slot) in body.slots.iter().enumerate() { - let slot_path = format!("{path}.slots[{slot_index}]"); - if !slot_names.insert(slot.name.as_str()) { - return Err(conflict( - entry, - format!("{slot_path}.name"), - format!("duplicate footprint slot {}", slot.name), - )); - } - self.require_arg_path( - operation, - &slot.bind_from_arg, - entry, - format!("{slot_path}.bindFromArg"), - )?; - } - - let mut create_slot_names = HashSet::new(); - for (slot_index, slot) in body.create_slots.iter().enumerate() { - if !create_slot_names.insert(slot.name.as_str()) { - return Err(conflict( - entry, - format!("{path}.createSlots[{slot_index}].name"), - format!("duplicate create slot {}", slot.name), - )); - } - } - - for (closure_index, closure) in body.closures.iter().enumerate() { - let closure_path = format!("{path}.closures[{closure_index}]"); - if !slot_names.contains(closure.from_slot.as_str()) { - return Err(unresolved_reference( - entry, - format!("{closure_path}.fromSlot"), - format!("closure source slot {} is not declared", closure.from_slot), - )); - } - for binding in &closure.arg_bindings { - if !slot_names.contains(binding.as_str()) { - self.require_arg_path( - operation, - binding, - entry, - format!("{closure_path}.argBindings"), - )?; - } - } - } - - for (update_index, update) in body.updates.iter().enumerate() { - if !slot_names.contains(update.slot.as_str()) { - return Err(unresolved_reference( - entry, - format!("{path}.updates[{update_index}].slot"), - format!("update slot {} is not declared", update.slot), - )); - } - } - - let touched_resources = body - .reads - .iter() - .chain(body.writes.iter()) - .chain(body.creates.iter()) - .chain(body.slots.iter().map(|slot| &slot.kind)) - .chain( - body.closures - .iter() - .flat_map(|closure| closure.reads.iter()), - ) - .chain(body.create_slots.iter().map(|slot| &slot.kind)) - .cloned() - .collect::>(); - if let Some(resource) = first_overlap(&body.forbids, &touched_resources) { - return Err(conflict( - entry, - format!("{path}.forbids"), - format!("resource {resource} is both forbidden and used"), - )); - } - Ok(()) - } - - fn bind_channel_law( - &self, - entry: &LawEntryV1, - coordinate: SubjectCoordinate<'_>, - path: &str, - ) -> Result<(), WeslawError> { - let SubjectCoordinate::Channel { name, version } = coordinate else { - return Err(wrong_subject_kind(entry, path, "channel:@")); - }; - let LawEntryBodyV1::ChannelLaw(body) = &entry.body else { - return Err(wrong_subject_kind(entry, path, "channel law body")); - }; - if body.version != version { - return Err(WeslawError::at_path( - WeslawDiagnosticCode::InvalidDocument, - path, - format!( - "channel subject {} expects version {version}, but law body declares version {}", - entry.subject, body.version - ), - )); - } - let carrier = self - .channel_carrier(name, version) - .ok_or_else(|| unresolved_subject(entry, path, Vec::new()))?; - let carrier_type = self.require_type_reference( - carrier, - TypeKind::Object, - entry, - format!("{path}.carrier"), - )?; - for (message_index, message) in body.messages.iter().enumerate() { - let message_path = format!("{path}.messages[{message_index}]"); - let field = self.require_field_on_type( - carrier_type, - &message.field, - entry, - format!("{message_path}.field"), - )?; - if field.r#type.base != message.r#type { - return Err(unresolved_reference( - entry, - format!("{message_path}.type"), - format!( - "channel field {} has type {}, not {}", - message.field, field.r#type.base, message.r#type - ), - )); - } - self.require_type_reference( - &message.r#type, - TypeKind::Object, - entry, - format!("{message_path}.type"), - )?; - } - Ok(()) - } - - fn bind_invariant_law( - &self, - entry: &LawEntryV1, - coordinate: SubjectCoordinate<'_>, - path: &str, - ) -> Result<(), WeslawError> { - match coordinate { - SubjectCoordinate::Type(name) => { - self.require_type(name, TypeKind::Object, entry, path)?; - } - SubjectCoordinate::Input(name) => { - self.require_type(name, TypeKind::InputObject, entry, path)?; - } - SubjectCoordinate::Enum(name) => { - self.require_type(name, TypeKind::Enum, entry, path)?; - } - SubjectCoordinate::Field { owner, field } => { - self.require_field(owner, field, entry, path)?; - } - SubjectCoordinate::Operation { - operation_type, - field, - } => { - self.require_operation(operation_type, field, entry, path)?; - } - SubjectCoordinate::Family(name) if name == self.law_ir.family => {} - _ => { - return Err(wrong_subject_kind( - entry, - path, - "type:, input:, enum:, field:., operation:., or family:", - )); - } - } - let LawEntryBodyV1::InvariantLaw(body) = &entry.body else { - return Err(wrong_subject_kind(entry, path, "invariant law body")); - }; - match &body.predicate { - PredicateV1::FieldEquals { field, .. } => { - self.require_predicate_field( - coordinate, - field, - entry, - format!("{path}.predicate.field"), - )?; - } - PredicateV1::External { verifier, .. } => { - if !self - .law_ir - .registries - .verifiers - .iter() - .any(|candidate| candidate.id == *verifier) - { - return Err(unresolved_reference( - entry, - format!("{path}.predicate.verifier"), - format!("verifier {verifier} is not declared"), - )); - } - } - } - Ok(()) - } - - fn require_type( - &self, - name: &str, - expected_kind: TypeKind, - entry: &LawEntryV1, - path: &str, - ) -> Result<&TypeDefinition, WeslawError> { - match self.find_type(name) { - Some(definition) if definition.kind == expected_kind => Ok(definition), - Some(_) => Err(wrong_subject_kind( - entry, - path, - expected_kind_label(expected_kind), - )), - None => Err(unresolved_subject( - entry, - path, - self.closest_type_subjects(name), - )), - } - } - - fn require_type_reference( - &self, - name: &str, - expected_kind: TypeKind, - entry: &LawEntryV1, - path: impl Into, - ) -> Result<&TypeDefinition, WeslawError> { - match self.find_type(name) { - Some(definition) if definition.kind == expected_kind => Ok(definition), - Some(definition) => Err(unresolved_reference( - entry, - path, - format!( - "type {name} has kind {:?}, expected {:?}", - definition.kind, expected_kind - ), - )), - None => Err(unresolved_reference( - entry, - path, - format!("type {name} is not declared"), - )), - } - } - - fn require_input_field<'a>( - &self, - input: &'a TypeDefinition, - field: &str, - entry: &LawEntryV1, - path: impl Into, - ) -> Result<&'a Field, WeslawError> { - if input.kind != TypeKind::InputObject { - return Err(unresolved_reference( - entry, - path, - format!("{} is not an input object", input.name), - )); - } - self.require_field_on_type(input, field, entry, path) - } - - fn require_field_on_type<'a>( - &self, - definition: &'a TypeDefinition, - field: &str, - entry: &LawEntryV1, - path: impl Into, - ) -> Result<&'a Field, WeslawError> { - let path = path.into(); - definition - .fields - .iter() - .find(|candidate| candidate.name == field) - .ok_or_else(|| { - unresolved_reference( - entry, - path, - format!("{} has no field {field}", definition.name), - ) - }) - } - - fn require_field( - &self, - owner: &str, - field: &str, - entry: &LawEntryV1, - path: &str, - ) -> Result<(), WeslawError> { - let Some(definition) = self.find_type(owner) else { - return Err(unresolved_subject( - entry, - path, - self.closest_type_subjects(owner), - )); - }; - if !matches!( - definition.kind, - TypeKind::Object | TypeKind::Interface | TypeKind::InputObject - ) { - return Err(wrong_subject_kind( - entry, - path, - "field:.", - )); - } - if definition - .fields - .iter() - .any(|candidate| candidate.name == field) - { - Ok(()) - } else { - Err(unresolved_subject( - entry, - path, - definition - .fields - .iter() - .map(|candidate| format!("field:{owner}.{}", candidate.name)) - .collect(), - )) - } - } - - fn require_operation( - &self, - operation_type: OperationType, - field: &str, - entry: &LawEntryV1, - path: &str, - ) -> Result<&SchemaOperation, WeslawError> { - if let Some(operation) = self.operations.iter().find(|candidate| { - candidate.operation_type == operation_type && candidate.field_name == field - }) { - Ok(operation) - } else { - Err(unresolved_subject( - entry, - path, - self.closest_operation_subjects(operation_type), - )) - } - } - - fn require_resource( - &self, - resource: &str, - entry: &LawEntryV1, - path: impl Into, - ) -> Result<(), WeslawError> { - let registry_resource_exists = self - .law_ir - .registries - .resources - .iter() - .any(|candidate| candidate.id == resource); - if let Some(definition) = self.find_type(resource) { - if definition.kind == TypeKind::Object || registry_resource_exists { - return Ok(()); - } - return Err(WeslawError::at_path( - WeslawDiagnosticCode::WrongSubjectKind, - path.into(), - format!( - "law {} has footprint resource {resource}, but schema-backed resources must be object types or explicit registry entries; got {:?}", - entry.id, definition.kind - ), - )); - } - if registry_resource_exists { - Ok(()) - } else { - Err(unresolved_reference( - entry, - path, - format!("resource {resource} is neither a GraphQL type nor registry entry"), - )) - } - } - - fn require_arg_path( - &self, - operation: &SchemaOperation, - arg_path: &str, - entry: &LawEntryV1, - path: impl Into, - ) -> Result<(), WeslawError> { - let path = path.into(); - let mut segments = arg_path.split('.'); - let Some(arg_name) = segments.next() else { - return Err(unresolved_reference(entry, path, "empty argument path")); - }; - let Some(argument) = operation - .arguments - .iter() - .find(|candidate| candidate.name == arg_name) - else { - return Err(unresolved_reference( - entry, - path, - format!( - "operation {}.{} has no argument {arg_name}", - operation_type_coordinate(operation.operation_type), - operation.field_name - ), - )); - }; - - let mut current_type = argument.r#type.base.as_str(); - for segment in segments { - let definition = self.require_type_reference( - current_type, - TypeKind::InputObject, - entry, - path.clone(), - )?; - let field = self.require_input_field(definition, segment, entry, path.clone())?; - current_type = field.r#type.base.as_str(); - } - Ok(()) - } - - fn require_predicate_field( - &self, - coordinate: SubjectCoordinate<'_>, - field_path: &str, - entry: &LawEntryV1, - path: impl Into, - ) -> Result<(), WeslawError> { - let path = path.into(); - let root = match coordinate { - SubjectCoordinate::Type(name) | SubjectCoordinate::Input(name) => name, - SubjectCoordinate::Field { owner, .. } => owner, - _ => { - return Err(unresolved_reference( - entry, - path.clone(), - "fieldEquals predicates require a type, input, or field subject", - )); - } - }; - let mut current_type = root; - for segment in field_path.split('.') { - let definition = self.find_type(current_type).ok_or_else(|| { - unresolved_reference( - entry, - path.clone(), - format!("type {current_type} is not declared"), - ) - })?; - let field = self.require_field_on_type(definition, segment, entry, path.clone())?; - current_type = field.r#type.base.as_str(); - } - Ok(()) - } - - fn find_type(&self, name: &str) -> Option<&TypeDefinition> { - self.schema_ir - .types - .iter() - .find(|candidate| candidate.name == name) - } - - fn closest_type_subjects(&self, name: &str) -> Vec { - let mut candidates = self - .schema_ir - .types - .iter() - .filter(|candidate| shares_prefix(&candidate.name, name)) - .map(|candidate| { - format!( - "{}:{}", - coordinate_prefix_for_type(candidate.kind), - candidate.name - ) - }) - .collect::>(); - candidates.sort(); - candidates - } - - fn closest_operation_subjects(&self, operation_type: OperationType) -> Vec { - let mut candidates = self - .operations - .iter() - .filter(|candidate| candidate.operation_type == operation_type) - .map(|candidate| { - format!( - "operation:{}.{}", - operation_type_coordinate(candidate.operation_type), - candidate.field_name - ) - }) - .collect::>(); - candidates.sort(); - candidates - } - - fn channel_carrier(&self, name: &str, version: u64) -> Option<&str> { - if let Some(channel) = self - .law_ir - .registries - .channels - .iter() - .find(|channel| channel.name == name && channel.version == version) - { - return Some(channel.carrier.as_str()); - } - - self.schema_ir - .types - .iter() - .find(|definition| { - definition - .directives - .get("wes_channel") - .is_some_and(|value| { - value.get("name").and_then(serde_json::Value::as_str) == Some(name) - && value.get("version").and_then(serde_json::Value::as_u64) - == Some(version) - }) - }) - .map(|definition| definition.name.as_str()) - } -} - -#[derive(Clone, Copy)] -enum SubjectCoordinate<'a> { - Scalar(&'a str), - Type(&'a str), - Input(&'a str), - Enum(&'a str), - Field { - owner: &'a str, - field: &'a str, - }, - Operation { - operation_type: OperationType, - field: &'a str, - }, - Channel { - name: &'a str, - version: u64, - }, - Family(&'a str), -} - -fn parse_subject_coordinate<'a>( - subject: &'a str, - path: &str, -) -> Result, WeslawError> { - if let Some(name) = subject.strip_prefix("scalar:") { - require_graphql_name(name, subject, path)?; - return Ok(SubjectCoordinate::Scalar(name)); - } - if let Some(name) = subject.strip_prefix("type:") { - require_graphql_name(name, subject, path)?; - return Ok(SubjectCoordinate::Type(name)); - } - if let Some(name) = subject.strip_prefix("input:") { - require_graphql_name(name, subject, path)?; - return Ok(SubjectCoordinate::Input(name)); - } - if let Some(name) = subject.strip_prefix("enum:") { - require_graphql_name(name, subject, path)?; - return Ok(SubjectCoordinate::Enum(name)); - } - if let Some(rest) = subject.strip_prefix("field:") { - let (owner, field) = split_once(rest, '.', subject, path)?; - require_graphql_name(owner, subject, path)?; - require_graphql_name(field, subject, path)?; - return Ok(SubjectCoordinate::Field { owner, field }); - } - if let Some(rest) = subject.strip_prefix("operation:") { - let (root, field) = split_once(rest, '.', subject, path)?; - let operation_type = parse_operation_type(root, subject, path)?; - require_graphql_name(field, subject, path)?; - return Ok(SubjectCoordinate::Operation { - operation_type, - field, - }); - } - if let Some(rest) = subject.strip_prefix("channel:") { - let (name, version_text) = split_once(rest, '@', subject, path)?; - require_dotted_name(name, subject, path)?; - let version = version_text.parse::().map_err(|_| { - invalid_coordinate(subject, path, "channel version must be an unsigned integer") - })?; - return Ok(SubjectCoordinate::Channel { name, version }); - } - if let Some(name) = subject.strip_prefix("family:") { - require_dotted_name(name, subject, path)?; - return Ok(SubjectCoordinate::Family(name)); - } - - Err(invalid_coordinate( - subject, - path, - "unknown subject coordinate prefix", - )) -} - -fn validate_schema_hash_anchor(schema_hash: &str, path: &str) -> Result<(), WeslawError> { - let Some(hex) = schema_hash.strip_prefix("sha256:") else { - return Err(WeslawError::at_path( - WeslawDiagnosticCode::InvalidDocument, - path, - "schema hash must use sha256:<64 lowercase hex>", - )); - }; - if hex.len() == 64 - && hex - .chars() - .all(|character| character.is_ascii_hexdigit() && !character.is_ascii_uppercase()) - { - Ok(()) - } else { - Err(WeslawError::at_path( - WeslawDiagnosticCode::InvalidDocument, - path, - "schema hash must use sha256:<64 lowercase hex>", - )) - } -} - -fn split_once<'a>( - value: &'a str, - delimiter: char, - subject: &str, - path: &str, -) -> Result<(&'a str, &'a str), WeslawError> { - let Some((left, right)) = value.split_once(delimiter) else { - return Err(invalid_coordinate( - subject, - path, - "missing coordinate delimiter", - )); - }; - if left.is_empty() || right.is_empty() || right.contains(delimiter) { - return Err(invalid_coordinate( - subject, - path, - "invalid coordinate segments", - )); - } - Ok((left, right)) -} - -fn parse_operation_type( - value: &str, - subject: &str, - path: &str, -) -> Result { - match value { - "Query" => Ok(OperationType::Query), - "Mutation" => Ok(OperationType::Mutation), - "Subscription" => Ok(OperationType::Subscription), - _ => Err(invalid_coordinate( - subject, - path, - "operation root must be Query, Mutation, or Subscription", - )), - } -} - -fn require_graphql_name(name: &str, subject: &str, path: &str) -> Result<(), WeslawError> { - let mut chars = name.chars(); - let Some(first) = chars.next() else { - return Err(invalid_coordinate(subject, path, "empty GraphQL name")); - }; - if !(first == '_' || first.is_ascii_alphabetic()) { - return Err(invalid_coordinate(subject, path, "invalid GraphQL name")); - } - if chars.all(|character| character == '_' || character.is_ascii_alphanumeric()) { - Ok(()) - } else { - Err(invalid_coordinate(subject, path, "invalid GraphQL name")) - } -} - -fn require_dotted_name(value: &str, subject: &str, path: &str) -> Result<(), WeslawError> { - if value.split('.').all(is_dotted_token) { - Ok(()) - } else { - Err(invalid_coordinate(subject, path, "invalid dotted name")) - } -} - -fn is_dotted_token(value: &str) -> bool { - let mut chars = value.chars(); - let Some(first) = chars.next() else { - return false; - }; - first.is_ascii_lowercase() - && chars.all(|character| { - character.is_ascii_lowercase() || character.is_ascii_digit() || character == '-' - }) -} - -fn invalid_coordinate(subject: &str, path: &str, detail: &str) -> WeslawError { - WeslawError::at_path( - WeslawDiagnosticCode::InvalidCoordinate, - path, - format!("{detail}: {subject}"), - ) -} - -fn unresolved_subject(entry: &LawEntryV1, path: &str, closest_matches: Vec) -> WeslawError { - let mut message = format!( - "unresolved subject coordinate {} for law {}", - entry.subject, entry.id - ); - if !closest_matches.is_empty() { - message.push_str("; closest matches: "); - message.push_str(&closest_matches.join(", ")); - } - WeslawError::at_path( - WeslawDiagnosticCode::UnresolvedSubject, - subject_path(path), - message, - ) -} - -fn wrong_subject_kind(entry: &LawEntryV1, path: &str, expected: &str) -> WeslawError { - WeslawError::at_path( - WeslawDiagnosticCode::WrongSubjectKind, - subject_path(path), - format!( - "law {} with kind {:?} requires subject kind {expected}, got {}", - entry.id, entry.kind, entry.subject - ), - ) -} - -fn unresolved_reference( - entry: &LawEntryV1, - path: impl Into, - detail: impl Into, -) -> WeslawError { - WeslawError::at_path( - WeslawDiagnosticCode::UnresolvedReference, - path, - format!( - "law {} has unresolved reference: {}", - entry.id, - detail.into() - ), - ) -} - -fn conflict(entry: &LawEntryV1, path: impl Into, detail: impl Into) -> WeslawError { - WeslawError::at_path( - WeslawDiagnosticCode::Conflict, - path, - format!("law {} is contradictory: {}", entry.id, detail.into()), - ) -} - -fn subject_path(path: &str) -> String { - if path.ends_with(".subject") { - path.to_string() - } else { - format!("{path}.subject") - } -} - -fn expected_kind_label(kind: TypeKind) -> &'static str { - match kind { - TypeKind::Object => "type:", - TypeKind::Interface => "interface:", - TypeKind::Union => "union:", - TypeKind::Enum => "enum:", - TypeKind::Scalar => "scalar:", - TypeKind::InputObject => "input:", - } -} - -fn coordinate_prefix_for_type(kind: TypeKind) -> &'static str { - match kind { - TypeKind::Object | TypeKind::Interface | TypeKind::Union => "type", - TypeKind::Enum => "enum", - TypeKind::Scalar => "scalar", - TypeKind::InputObject => "input", - } -} - -fn operation_type_coordinate(operation_type: OperationType) -> &'static str { - match operation_type { - OperationType::Query => "Query", - OperationType::Mutation => "Mutation", - OperationType::Subscription => "Subscription", - } -} - -fn law_kind_has_unique_subject(kind: LawKindV1) -> bool { - !matches!(kind, LawKindV1::InvariantLaw) -} - -fn first_overlap(left: &[String], right: &[String]) -> Option { - let right = right.iter().collect::>(); - left.iter() - .filter(|candidate| right.contains(candidate)) - .min() - .cloned() -} - -fn shares_prefix(left: &str, right: &str) -> bool { - left.starts_with(right) || right.starts_with(left) -} - -fn parse_registries(map: &Mapping) -> Result { - reject_unknown_fields(map, "$.registries", &["resources", "verifiers", "channels"])?; - Ok(LawRegistrySetV1 { - resources: optional_sequence(map, "resources", "$.registries.resources")? - .unwrap_or_default() - .iter() - .enumerate() - .map(|(index, value)| { - parse_resource_registry_entry(value, &format!("$.registries.resources[{index}]")) - }) - .collect::, _>>()?, - verifiers: optional_sequence(map, "verifiers", "$.registries.verifiers")? - .unwrap_or_default() - .iter() - .enumerate() - .map(|(index, value)| { - parse_verifier_registry_entry(value, &format!("$.registries.verifiers[{index}]")) - }) - .collect::, _>>()?, - channels: optional_sequence(map, "channels", "$.registries.channels")? - .unwrap_or_default() - .iter() - .enumerate() - .map(|(index, value)| { - parse_channel_registry_entry(value, &format!("$.registries.channels[{index}]")) - }) - .collect::, _>>()?, - }) -} - -fn parse_resource_registry_entry( - value: &Yaml, - path: &str, -) -> Result { - let map = expect_mapping(value, path)?; - reject_unknown_fields(map, path, &["id", "owner", "kind", "notes"])?; - Ok(ResourceRegistryEntryV1 { - id: required_string(map, "id", &format!("{path}.id"))?, - owner: required_string(map, "owner", &format!("{path}.owner"))?, - kind: required_string(map, "kind", &format!("{path}.kind"))?, - notes: optional_string(map, "notes", &format!("{path}.notes"))?, - }) -} - -fn parse_verifier_registry_entry( - value: &Yaml, - path: &str, -) -> Result { - let map = expect_mapping(value, path)?; - reject_unknown_fields(map, path, &["id", "owner", "inputContracts"])?; - Ok(VerifierRegistryEntryV1 { - id: required_string(map, "id", &format!("{path}.id"))?, - owner: required_string(map, "owner", &format!("{path}.owner"))?, - input_contracts: optional_string_list( - map, - "inputContracts", - &format!("{path}.inputContracts"), - )? - .unwrap_or_default(), - }) -} - -fn parse_channel_registry_entry( - value: &Yaml, - path: &str, -) -> Result { - let map = expect_mapping(value, path)?; - reject_unknown_fields(map, path, &["name", "version", "carrier"])?; - Ok(ChannelRegistryEntryV1 { - name: required_string(map, "name", &format!("{path}.name"))?, - version: required_u64(map, "version", &format!("{path}.version"))?, - carrier: required_string(map, "carrier", &format!("{path}.carrier"))?, - }) -} - -fn parse_law_entry(value: &Yaml, path: &str) -> Result, WeslawError> { - let map = expect_mapping(value, path)?; - let status = parse_status( - optional_string(map, "status", &format!("{path}.status"))? - .unwrap_or_else(|| "active".to_string()), - &format!("{path}.status"), - )?; - if status == LawStatusV1::Draft { - return Ok(None); - } - - let kind_text = required_string(map, "kind", &format!("{path}.kind"))?; - let kind = parse_kind(&kind_text, &format!("{path}.kind"))?; - reject_unknown_fields(map, path, allowed_law_fields(kind))?; - - let tags = optional_string_list(map, "tags", &format!("{path}.tags"))?.unwrap_or_default(); - let rationale = optional_string(map, "rationale", &format!("{path}.rationale"))?; - let body = match kind { - LawKindV1::ScalarSemantics => { - LawEntryBodyV1::ScalarSemantics(parse_scalar_semantics(map, path)?) - } - LawKindV1::VariantLaw => LawEntryBodyV1::VariantLaw(parse_variant_law(map, path)?), - LawKindV1::FootprintLaw => LawEntryBodyV1::FootprintLaw(parse_footprint_law(map, path)?), - LawKindV1::ChannelLaw => LawEntryBodyV1::ChannelLaw(parse_channel_law(map, path)?), - LawKindV1::InvariantLaw => LawEntryBodyV1::InvariantLaw(parse_invariant_law(map, path)?), - }; - - Ok(Some(LawEntryV1 { - id: required_string(map, "id", &format!("{path}.id"))?, - status, - kind, - subject: required_string(map, "subject", &format!("{path}.subject"))?, - tags, - rationale, - body, - source_index: None, - })) -} - -fn parse_scalar_semantics(map: &Mapping, path: &str) -> Result { - let semantics = required_mapping(map, "semantics", &format!("{path}.semantics"))?; - reject_unknown_fields( - semantics, - &format!("{path}.semantics"), - &[ - "representation", - "minInclusive", - "maxInclusive", - "ordering", - "scope", - "forbids", - ], - )?; - let representation = parse_scalar_representation( - required_string( - semantics, - "representation", - &format!("{path}.semantics.representation"), - )?, - &format!("{path}.semantics.representation"), - )?; - let min_inclusive = optional_u64( - semantics, - "minInclusive", - &format!("{path}.semantics.minInclusive"), - )?; - let max_inclusive = optional_u64( - semantics, - "maxInclusive", - &format!("{path}.semantics.maxInclusive"), - )?; - let forbids = optional_string_list(semantics, "forbids", &format!("{path}.semantics.forbids"))? - .unwrap_or_default() - .into_iter() - .map(|item| parse_scalar_forbidden(item, &format!("{path}.semantics.forbids"))) - .collect::, _>>()?; - let ordering = optional_string(semantics, "ordering", &format!("{path}.semantics.ordering"))? - .map(|value| parse_scalar_ordering(value, &format!("{path}.semantics.ordering"))) - .transpose()?; - validate_scalar_semantics(representation, min_inclusive, max_inclusive, &forbids, path)?; - Ok(ScalarSemanticsLawV1 { - representation, - min_inclusive, - max_inclusive, - ordering, - scope: optional_string(semantics, "scope", &format!("{path}.semantics.scope"))?, - forbids, - }) -} - -fn validate_scalar_semantics( - representation: ScalarRepresentationV1, - min_inclusive: Option, - max_inclusive: Option, - forbids: &[ScalarForbiddenInterpretationV1], - path: &str, -) -> Result<(), WeslawError> { - let is_integer = representation == ScalarRepresentationV1::Integer; - if !is_integer { - if min_inclusive.is_some() || max_inclusive.is_some() { - return Err(WeslawError::at_path( - WeslawDiagnosticCode::InvalidDocument, - format!("{path}.semantics.representation"), - "integer ranges require representation: integer", - )); - } - if forbids.contains(&ScalarForbiddenInterpretationV1::SilentGraphqlIntNarrowing) { - return Err(WeslawError::at_path( - WeslawDiagnosticCode::InvalidDocument, - format!("{path}.semantics.forbids"), - "silentGraphQLIntNarrowing is meaningful only for integer-like scalars", - )); - } - } - if let (Some(min), Some(max)) = (min_inclusive, max_inclusive) { - if min > max { - return Err(WeslawError::at_path( - WeslawDiagnosticCode::InvalidDocument, - format!("{path}.semantics.maxInclusive"), - "minInclusive must not exceed maxInclusive", - )); - } - } - Ok(()) -} - -fn parse_variant_law(map: &Mapping, path: &str) -> Result { - let discriminator = required_mapping(map, "discriminator", &format!("{path}.discriminator"))?; - reject_unknown_fields( - discriminator, - &format!("{path}.discriminator"), - &["field", "enum"], - )?; - let cases = required_sequence(map, "cases", &format!("{path}.cases"))? - .iter() - .enumerate() - .map(|(index, value)| parse_variant_case(value, &format!("{path}.cases[{index}]"))) - .collect::, _>>()?; - Ok(VariantLawV1 { - discriminator: VariantDiscriminatorV1 { - field: required_string( - discriminator, - "field", - &format!("{path}.discriminator.field"), - )?, - r#enum: required_string(discriminator, "enum", &format!("{path}.discriminator.enum"))?, - }, - cases, - }) -} - -fn parse_variant_case(value: &Yaml, path: &str) -> Result { - let map = expect_mapping(value, path)?; - reject_unknown_fields(map, path, &["value", "requires", "forbids"])?; - Ok(VariantCaseV1 { - value: required_string(map, "value", &format!("{path}.value"))?, - requires: optional_string_list(map, "requires", &format!("{path}.requires"))? - .unwrap_or_default(), - forbids: optional_string_list(map, "forbids", &format!("{path}.forbids"))? - .unwrap_or_default(), - }) -} - -fn parse_footprint_law(map: &Mapping, path: &str) -> Result { - Ok(FootprintLawV1 { - reads: optional_string_list(map, "reads", &format!("{path}.reads"))?.unwrap_or_default(), - writes: optional_string_list(map, "writes", &format!("{path}.writes"))?.unwrap_or_default(), - creates: optional_string_list(map, "creates", &format!("{path}.creates"))? - .unwrap_or_default(), - forbids: optional_string_list(map, "forbids", &format!("{path}.forbids"))? - .unwrap_or_default(), - slots: optional_sequence(map, "slots", &format!("{path}.slots"))? - .unwrap_or_default() - .iter() - .enumerate() - .map(|(index, value)| parse_footprint_slot(value, &format!("{path}.slots[{index}]"))) - .collect::, _>>()?, - closures: optional_sequence(map, "closures", &format!("{path}.closures"))? - .unwrap_or_default() - .iter() - .enumerate() - .map(|(index, value)| { - parse_footprint_closure(value, &format!("{path}.closures[{index}]")) - }) - .collect::, _>>()?, - create_slots: optional_sequence(map, "createSlots", &format!("{path}.createSlots"))? - .unwrap_or_default() - .iter() - .enumerate() - .map(|(index, value)| parse_create_slot(value, &format!("{path}.createSlots[{index}]"))) - .collect::, _>>()?, - updates: optional_sequence(map, "updates", &format!("{path}.updates"))? - .unwrap_or_default() - .iter() - .enumerate() - .map(|(index, value)| { - parse_footprint_update(value, &format!("{path}.updates[{index}]")) - }) - .collect::, _>>()?, - }) -} - -fn parse_footprint_slot(value: &Yaml, path: &str) -> Result { - let map = expect_mapping(value, path)?; - reject_unknown_fields(map, path, &["name", "kind", "bindFromArg", "access"])?; - Ok(FootprintSlotV1 { - name: required_string(map, "name", &format!("{path}.name"))?, - kind: required_string(map, "kind", &format!("{path}.kind"))?, - bind_from_arg: required_string(map, "bindFromArg", &format!("{path}.bindFromArg"))?, - access: optional_string_list(map, "access", &format!("{path}.access"))?.unwrap_or_default(), - }) -} - -fn parse_footprint_closure(value: &Yaml, path: &str) -> Result { - let map = expect_mapping(value, path)?; - reject_unknown_fields( - map, - path, - &[ - "name", - "fromSlot", - "operator", - "argBindings", - "reads", - "cardinality", - ], - )?; - Ok(FootprintClosureV1 { - name: required_string(map, "name", &format!("{path}.name"))?, - from_slot: required_string(map, "fromSlot", &format!("{path}.fromSlot"))?, - operator: required_string(map, "operator", &format!("{path}.operator"))?, - arg_bindings: optional_string_list(map, "argBindings", &format!("{path}.argBindings"))? - .unwrap_or_default(), - reads: optional_string_list(map, "reads", &format!("{path}.reads"))?.unwrap_or_default(), - cardinality: optional_string(map, "cardinality", &format!("{path}.cardinality"))? - .map(|value| parse_footprint_cardinality(value, &format!("{path}.cardinality"))) - .transpose()? - .unwrap_or(FootprintCardinalityV1::One), - }) -} - -fn parse_create_slot(value: &Yaml, path: &str) -> Result { - let map = expect_mapping(value, path)?; - reject_unknown_fields(map, path, &["name", "kind", "cardinality"])?; - Ok(CreateSlotV1 { - name: required_string(map, "name", &format!("{path}.name"))?, - kind: required_string(map, "kind", &format!("{path}.kind"))?, - cardinality: optional_string(map, "cardinality", &format!("{path}.cardinality"))? - .map(|value| parse_footprint_cardinality(value, &format!("{path}.cardinality"))) - .transpose()?, - }) -} - -fn parse_footprint_update(value: &Yaml, path: &str) -> Result { - let map = expect_mapping(value, path)?; - reject_unknown_fields(map, path, &["slot", "fields"])?; - Ok(FootprintUpdateV1 { - slot: required_string(map, "slot", &format!("{path}.slot"))?, - fields: optional_string_list(map, "fields", &format!("{path}.fields"))?.unwrap_or_default(), - }) -} - -fn parse_channel_law(map: &Mapping, path: &str) -> Result { - Ok(ChannelLawV1 { - ordered: required_bool(map, "ordered", &format!("{path}.ordered"))?, - version: required_u64(map, "version", &format!("{path}.version"))?, - compatibility: match mapping_get(map, "compatibility") { - Some(value) => Some(parse_channel_compatibility( - value, - &format!("{path}.compatibility"), - )?), - None => None, - }, - messages: optional_sequence(map, "messages", &format!("{path}.messages"))? - .unwrap_or_default() - .iter() - .enumerate() - .map(|(index, value)| { - parse_channel_message(value, &format!("{path}.messages[{index}]")) - }) - .collect::, _>>()?, - }) -} - -fn parse_channel_compatibility( - value: &Yaml, - path: &str, -) -> Result { - let map = expect_mapping(value, path)?; - reject_unknown_fields(map, path, &["versioning", "semverCoupled"])?; - Ok(ChannelCompatibilityV1 { - versioning: required_string(map, "versioning", &format!("{path}.versioning"))?, - semver_coupled: required_bool(map, "semverCoupled", &format!("{path}.semverCoupled"))?, - }) -} - -fn parse_channel_message(value: &Yaml, path: &str) -> Result { - let map = expect_mapping(value, path)?; - reject_unknown_fields(map, path, &["field", "type"])?; - Ok(ChannelMessageV1 { - field: required_string(map, "field", &format!("{path}.field"))?, - r#type: required_string(map, "type", &format!("{path}.type"))?, - }) -} - -fn parse_invariant_law(map: &Mapping, path: &str) -> Result { - if mapping_get(map, "expr").is_some() { - return Err(WeslawError::at_path( - WeslawDiagnosticCode::RawExprRejected, - format!("{path}.expr"), - "raw invariant expressions are not accepted in weslaw/v1", - )); - } - let predicate = required_mapping(map, "predicate", &format!("{path}.predicate"))?; - let predicate_path = format!("{path}.predicate"); - match required_string(predicate, "op", &format!("{predicate_path}.op"))?.as_str() { - "fieldEquals" => { - reject_unknown_fields(predicate, &predicate_path, &["op", "field", "value"])?; - Ok(InvariantLawV1 { - predicate: PredicateV1::FieldEquals { - field: required_string(predicate, "field", &format!("{predicate_path}.field"))?, - value: yaml_to_json_value( - required_value(predicate, "value", &format!("{predicate_path}.value"))?, - &format!("{predicate_path}.value"), - )?, - }, - }) - } - "external" => { - reject_unknown_fields( - predicate, - &predicate_path, - &["op", "verifier", "ref", "inputContract"], - )?; - Ok(InvariantLawV1 { - predicate: PredicateV1::External { - verifier: required_string( - predicate, - "verifier", - &format!("{predicate_path}.verifier"), - )?, - r#ref: required_string(predicate, "ref", &format!("{predicate_path}.ref"))?, - input_contract: optional_string( - predicate, - "inputContract", - &format!("{predicate_path}.inputContract"), - )?, - }, - }) - } - other => Err(WeslawError::at_path( - WeslawDiagnosticCode::InvalidDocument, - format!("{predicate_path}.op"), - format!("unknown predicate op {other}"), - )), - } -} - -fn parse_kind(kind: &str, path: &str) -> Result { - match kind { - "scalarSemantics" => Ok(LawKindV1::ScalarSemantics), - "variantLaw" => Ok(LawKindV1::VariantLaw), - "footprintLaw" => Ok(LawKindV1::FootprintLaw), - "channelLaw" => Ok(LawKindV1::ChannelLaw), - "invariantLaw" => Ok(LawKindV1::InvariantLaw), - _ => Err(WeslawError::at_path( - WeslawDiagnosticCode::UnknownKind, - path, - format!("unknown law kind {kind}"), - )), - } -} - -fn parse_status(status: String, path: &str) -> Result { - match status.as_str() { - "active" => Ok(LawStatusV1::Active), - "draft" => Ok(LawStatusV1::Draft), - _ => Err(WeslawError::at_path( - WeslawDiagnosticCode::InvalidDocument, - path, - format!("unknown law status {status}"), - )), - } -} - -fn parse_scalar_representation( - representation: String, - path: &str, -) -> Result { - match representation.as_str() { - "integer" => Ok(ScalarRepresentationV1::Integer), - "opaqueIdentifier" => Ok(ScalarRepresentationV1::OpaqueIdentifier), - "string" => Ok(ScalarRepresentationV1::String), - _ => Err(WeslawError::at_path( - WeslawDiagnosticCode::InvalidDocument, - path, - format!("unknown scalar representation {representation}"), - )), - } -} - -fn parse_scalar_forbidden( - value: String, - path: &str, -) -> Result { - match value.as_str() { - "silentGraphQLIntNarrowing" => { - Ok(ScalarForbiddenInterpretationV1::SilentGraphqlIntNarrowing) - } - _ => Err(WeslawError::at_path( - WeslawDiagnosticCode::InvalidDocument, - path, - format!("unknown scalar forbidden interpretation {value}"), - )), - } -} - -fn parse_scalar_ordering(value: String, path: &str) -> Result { - match value.as_str() { - "none" => Ok(ScalarOrderingV1::None), - "lamport" => Ok(ScalarOrderingV1::Lamport), - "total" => Ok(ScalarOrderingV1::Total), - "partial" => Ok(ScalarOrderingV1::Partial), - _ => Err(WeslawError::at_path( - WeslawDiagnosticCode::InvalidDocument, - path, - format!("unknown scalar ordering {value}"), - )), - } -} - -fn parse_footprint_cardinality( - value: String, - path: &str, -) -> Result { - match value.as_str() { - "one" => Ok(FootprintCardinalityV1::One), - "optional" => Ok(FootprintCardinalityV1::Optional), - "many" => Ok(FootprintCardinalityV1::Many), - _ => Err(WeslawError::at_path( - WeslawDiagnosticCode::InvalidDocument, - path, - format!("unknown footprint cardinality {value}"), - )), - } -} - -fn allowed_law_fields(kind: LawKindV1) -> &'static [&'static str] { - match kind { - LawKindV1::ScalarSemantics => &[ - "id", - "status", - "kind", - "subject", - "tags", - "rationale", - "semantics", - ], - LawKindV1::VariantLaw => &[ - "id", - "status", - "kind", - "subject", - "tags", - "rationale", - "discriminator", - "cases", - ], - LawKindV1::FootprintLaw => &[ - "id", - "status", - "kind", - "subject", - "tags", - "rationale", - "reads", - "writes", - "creates", - "forbids", - "slots", - "closures", - "createSlots", - "updates", - ], - LawKindV1::ChannelLaw => &[ - "id", - "status", - "kind", - "subject", - "tags", - "rationale", - "ordered", - "version", - "compatibility", - "messages", - ], - LawKindV1::InvariantLaw => &[ - "id", - "status", - "kind", - "subject", - "tags", - "rationale", - "predicate", - "expr", - ], - } -} - -fn expect_mapping<'a>(value: &'a Yaml, path: &str) -> Result<&'a Mapping, WeslawError> { - value.as_hash().ok_or_else(|| { - WeslawError::at_path( - WeslawDiagnosticCode::InvalidDocument, - path, - "expected object", - ) - }) -} - -fn required_mapping<'a>( - map: &'a Mapping, - key: &str, - path: &str, -) -> Result<&'a Mapping, WeslawError> { - let value = required_value(map, key, path)?; - expect_mapping(value, path) -} - -fn required_sequence<'a>( - map: &'a Mapping, - key: &str, - path: &str, -) -> Result<&'a [Yaml], WeslawError> { - let value = required_value(map, key, path)?; - value.as_vec().map(Vec::as_slice).ok_or_else(|| { - WeslawError::at_path( - WeslawDiagnosticCode::InvalidDocument, - path, - "expected array", - ) - }) -} - -fn optional_sequence<'a>( - map: &'a Mapping, - key: &str, - path: &str, -) -> Result, WeslawError> { - match mapping_get(map, key) { - Some(value) => value.as_vec().map(Vec::as_slice).map(Some).ok_or_else(|| { - WeslawError::at_path( - WeslawDiagnosticCode::InvalidDocument, - path, - "expected array", - ) - }), - None => Ok(None), - } -} - -fn required_value<'a>(map: &'a Mapping, key: &str, path: &str) -> Result<&'a Yaml, WeslawError> { - mapping_get(map, key).ok_or_else(|| { - WeslawError::at_path( - WeslawDiagnosticCode::InvalidDocument, - path, - format!("missing required field {key}"), - ) - }) -} - -fn required_string(map: &Mapping, key: &str, path: &str) -> Result { - required_value(map, key, path)? - .as_str() - .map(str::to_string) - .ok_or_else(|| { - WeslawError::at_path( - WeslawDiagnosticCode::InvalidDocument, - path, - "expected string", - ) - }) -} - -fn optional_string(map: &Mapping, key: &str, path: &str) -> Result, WeslawError> { - match mapping_get(map, key) { - Some(value) => value.as_str().map(str::to_string).map(Some).ok_or_else(|| { - WeslawError::at_path( - WeslawDiagnosticCode::InvalidDocument, - path, - "expected string", - ) - }), - None => Ok(None), - } -} - -fn required_u64(map: &Mapping, key: &str, path: &str) -> Result { - yaml_u64(required_value(map, key, path)?, path) -} - -fn optional_u64(map: &Mapping, key: &str, path: &str) -> Result, WeslawError> { - match mapping_get(map, key) { - Some(value) => yaml_u64(value, path).map(Some), - None => Ok(None), - } -} - -fn yaml_u64(value: &Yaml, path: &str) -> Result { - let Some(integer) = value.as_i64() else { - return Err(WeslawError::at_path( - WeslawDiagnosticCode::InvalidDocument, - path, - "expected unsigned integer", - )); - }; - u64::try_from(integer).map_err(|_| { - WeslawError::at_path( - WeslawDiagnosticCode::InvalidDocument, - path, - "expected unsigned integer", - ) - }) -} - -fn required_bool(map: &Mapping, key: &str, path: &str) -> Result { - required_value(map, key, path)?.as_bool().ok_or_else(|| { - WeslawError::at_path( - WeslawDiagnosticCode::InvalidDocument, - path, - "expected boolean", - ) - }) -} - -fn optional_string_list( - map: &Mapping, - key: &str, - path: &str, -) -> Result>, WeslawError> { - match mapping_get(map, key) { - Some(value) => { - let sequence = value.as_vec().ok_or_else(|| { - WeslawError::at_path( - WeslawDiagnosticCode::InvalidDocument, - path, - "expected string array", - ) - })?; - sequence - .iter() - .enumerate() - .map(|(index, item)| { - item.as_str().map(str::to_string).ok_or_else(|| { - WeslawError::at_path( - WeslawDiagnosticCode::InvalidDocument, - format!("{path}[{index}]"), - "expected string", - ) - }) - }) - .collect::, _>>() - .map(Some) - } - None => Ok(None), - } -} - -fn reject_unknown_fields(map: &Mapping, path: &str, allowed: &[&str]) -> Result<(), WeslawError> { - for key in map.keys() { - let key_text = key.as_str().ok_or_else(|| { - WeslawError::at_path( - WeslawDiagnosticCode::UnknownField, - path, - "object keys must be strings", - ) - })?; - if !allowed.contains(&key_text) { - return Err(WeslawError::at_path( - WeslawDiagnosticCode::UnknownField, - format!("{path}.{key_text}"), - format!("unknown field {key_text}"), - )); - } - } - Ok(()) -} - -fn mapping_get<'a>(map: &'a Mapping, key: &str) -> Option<&'a Yaml> { - map.get(&Yaml::String(key.to_string())) -} - -fn yaml_to_json_value(value: &Yaml, path: &str) -> Result { - match value { - Yaml::Real(text) => { - let number = text - .parse::() - .ok() - .and_then(serde_json::Number::from_f64); - number - .map(serde_json::Value::Number) - .ok_or_else(|| invalid_json_value(path, "unsupported YAML real value")) - } - Yaml::Integer(integer) => Ok(serde_json::Value::Number((*integer).into())), - Yaml::String(text) => Ok(serde_json::Value::String(text.clone())), - Yaml::Boolean(value) => Ok(serde_json::Value::Bool(*value)), - Yaml::Array(items) => items - .iter() - .enumerate() - .map(|(index, item)| yaml_to_json_value(item, &format!("{path}[{index}]"))) - .collect::, _>>() - .map(serde_json::Value::Array), - Yaml::Hash(map) => { - let mut object = serde_json::Map::new(); - for (key, value) in map { - let Some(key_text) = key.as_str() else { - return Err(invalid_json_value(path, "YAML object keys must be strings")); - }; - object.insert( - key_text.to_string(), - yaml_to_json_value(value, &format!("{path}.{key_text}"))?, - ); - } - Ok(serde_json::Value::Object(object)) - } - Yaml::Null => Ok(serde_json::Value::Null), - Yaml::Alias(_) | Yaml::BadValue => Err(invalid_json_value( - path, - "unsupported YAML value for JSON conversion", - )), - } -} - -fn invalid_json_value(path: &str, message: &str) -> WeslawError { - WeslawError::at_path(WeslawDiagnosticCode::InvalidDocument, path, message) -} diff --git a/crates/wesley-core/src/domain/mod.rs b/crates/wesley-core/src/domain/mod.rs index 4d1fc41d..aa255f61 100644 --- a/crates/wesley-core/src/domain/mod.rs +++ b/crates/wesley-core/src/domain/mod.rs @@ -4,7 +4,6 @@ pub mod capability; pub mod error; pub mod extension_generation; pub mod ir; -pub mod law; pub(crate) mod normalized_sdl; pub mod operation; pub mod operation_artifact; diff --git a/crates/wesley-core/src/lib.rs b/crates/wesley-core/src/lib.rs index 9701dfab..9295ae0d 100644 --- a/crates/wesley-core/src/lib.rs +++ b/crates/wesley-core/src/lib.rs @@ -26,7 +26,6 @@ pub use domain::capability::*; pub use domain::error::*; pub use domain::extension_generation::*; pub use domain::ir::*; -pub use domain::law::*; pub use domain::operation::*; pub use domain::operation_artifact::*; pub use domain::project_manifest::*; diff --git a/crates/wesley-core/tests/extension_generation.rs b/crates/wesley-core/tests/extension_generation.rs index 1c279eb7..20eaad39 100644 --- a/crates/wesley-core/tests/extension_generation.rs +++ b/crates/wesley-core/tests/extension_generation.rs @@ -3,10 +3,10 @@ use std::path::PathBuf; use serde_json::Value; use wesley_core::{ - compute_generation_artifact_digest_v1, list_schema_operations_sdl, load_weslaw_yaml, - lower_schema_sdl, ExtensionGenerationInputV1, GenerationArtifactContentV1, - GenerationArtifactReferenceV1, GenerationContractErrorKind, GenerationProvenanceManifestV1, - GenerationReviewV1, GeneratorIdentityV1, Metadata, UnitMeta, + compute_generation_artifact_digest_v1, list_schema_operations_sdl, lower_schema_sdl, + ExtensionGenerationInputV2, GenerationArtifactContentV1, GenerationArtifactReferenceV1, + GenerationContractErrorKind, GenerationProvenanceManifestV2, GenerationReviewV2, + GeneratorIdentityV1, Metadata, UnitMeta, }; fn repo_path(path: &str) -> PathBuf { @@ -20,34 +20,16 @@ fn read(path: &str) -> String { } fn generation_schema_validator(schema: &Value) -> jsonschema::Validator { - let registry = jsonschema::Registry::new() - .add( - "https://wesley.dev/schemas/wesley-law-ir-v1.schema.json", - serde_json::from_str::(&read("schemas/wesley-law-ir-v1.schema.json")).unwrap(), - ) - .expect("Law IR schema URI should be valid") - .prepare() - .expect("generation schema registry should prepare"); - jsonschema::options() - .with_registry(®istry) - .build(schema) - .expect("generation schema should compile") + jsonschema::validator_for(schema).expect("generation schema should compile") } fn shape_fixture() -> (wesley_core::WesleyIR, Vec) { - let sdl = read("test/fixtures/weslaw/contract-bundle-shape.graphql"); + let sdl = read("test/fixtures/extension-generation/schema.graphql"); let shape_ir = lower_schema_sdl(&sdl).expect("fixture schema should lower"); let operations = list_schema_operations_sdl(&sdl).expect("fixture operations should list"); (shape_ir, operations) } -fn law_fixture() -> wesley_core::LawIrV1 { - load_weslaw_yaml(&read( - "test/fixtures/weslaw/accepted/scalar-semantics.weslaw.yaml", - )) - .expect("fixture law should lower") -} - fn artifact_ref(coordinate: &str, bytes: &[u8]) -> GenerationArtifactReferenceV1 { GenerationArtifactReferenceV1::for_bytes(coordinate, bytes) .expect("fixture artifact reference should be valid") @@ -56,15 +38,13 @@ fn artifact_ref(coordinate: &str, bytes: &[u8]) -> GenerationArtifactReferenceV1 fn input_with( shape_ir: wesley_core::WesleyIR, operations: Vec, - law_ir: Option, owner_declarations: Vec, settings: &[u8], projection_roles: Vec<&str>, -) -> ExtensionGenerationInputV1 { - ExtensionGenerationInputV1::new( +) -> ExtensionGenerationInputV2 { + ExtensionGenerationInputV2::new( shape_ir, operations, - law_ir, owner_declarations, compute_generation_artifact_digest_v1(settings), projection_roles.into_iter().map(str::to_owned).collect(), @@ -106,20 +86,16 @@ fn canonical_generation_input_ignores_ambient_metadata_and_set_order() { let first = input_with( first_shape, first_operations, - Some(law_fixture()), vec![owner_a.clone(), owner_b.clone()], b"settings", - vec!["profile", "lawpack"], + vec!["profile", "target-artifact"], ); - let mut reordered_law = law_fixture(); - reordered_law.entries[0].tags.reverse(); let second = input_with( second_shape, second_operations, - Some(reordered_law), vec![owner_b, owner_a], b"settings", - vec!["lawpack", "profile"], + vec!["target-artifact", "profile"], ); assert_eq!( @@ -136,70 +112,55 @@ fn canonical_generation_input_ignores_ambient_metadata_and_set_order() { } #[test] -fn every_semantic_input_class_moves_the_generation_digest() { +fn every_generation_input_class_moves_the_generation_digest() { let (shape_ir, operations) = shape_fixture(); let owner = artifact_ref("fixture:owner@1", b"owner-v1"); let base = input_with( shape_ir.clone(), operations.clone(), - None, vec![owner.clone()], b"settings-v1", - vec!["lawpack"], + vec!["target-artifact"], ); let base_digest = base.digest().expect("base input should hash"); let mut changed_shape = shape_ir.clone(); - changed_shape.types[0].description = Some("semantic shape revision".to_owned()); + changed_shape.types[0].description = Some("structural shape revision".to_owned()); let shape_change = input_with( changed_shape, operations.clone(), - None, vec![owner.clone()], b"settings-v1", - vec!["lawpack"], + vec!["target-artifact"], ); assert_ne!(base_digest, shape_change.digest().unwrap()); - let law_change = input_with( - shape_ir.clone(), - operations.clone(), - Some(law_fixture()), - vec![owner.clone()], - b"settings-v1", - vec!["lawpack"], - ); - assert_ne!(base_digest, law_change.digest().unwrap()); - let mut changed_operations = operations.clone(); changed_operations[0].result_type.nullable = !changed_operations[0].result_type.nullable; let operation_change = input_with( shape_ir.clone(), changed_operations, - None, vec![owner.clone()], b"settings-v1", - vec!["lawpack"], + vec!["target-artifact"], ); assert_ne!(base_digest, operation_change.digest().unwrap()); let owner_change = input_with( shape_ir.clone(), operations.clone(), - None, vec![artifact_ref("fixture:owner@1", b"owner-v2")], b"settings-v1", - vec!["lawpack"], + vec!["target-artifact"], ); assert_ne!(base_digest, owner_change.digest().unwrap()); let settings_change = input_with( shape_ir, operations, - None, vec![owner], b"settings-v2", - vec!["lawpack"], + vec!["target-artifact"], ); assert_ne!(base_digest, settings_change.digest().unwrap()); } @@ -208,10 +169,9 @@ fn every_semantic_input_class_moves_the_generation_digest() { fn generation_input_rejects_malformed_operation_coordinates() { let (shape_ir, operations) = shape_fixture(); let assert_invalid = |operations, expected_subject: &str| { - let error = ExtensionGenerationInputV1::new( + let error = ExtensionGenerationInputV2::new( shape_ir.clone(), operations, - None, Vec::new(), compute_generation_artifact_digest_v1(b"settings"), Vec::new(), @@ -237,10 +197,9 @@ fn generation_input_rejects_malformed_operation_coordinates() { #[test] fn generation_input_classifies_invalid_projection_roles_as_tokens() { let (shape_ir, operations) = shape_fixture(); - let error = ExtensionGenerationInputV1::new( + let error = ExtensionGenerationInputV2::new( shape_ir, operations, - None, Vec::new(), compute_generation_artifact_digest_v1(b"settings"), vec![" padded-role ".to_owned()], @@ -255,16 +214,15 @@ fn generation_input_classifies_invalid_projection_roles_as_tokens() { #[test] fn conflicting_digests_for_one_coordinate_are_rejected_structurally() { let (shape_ir, operations) = shape_fixture(); - let error = ExtensionGenerationInputV1::new( + let error = ExtensionGenerationInputV2::new( shape_ir, operations, - None, vec![ artifact_ref("fixture:owner@1", b"first"), artifact_ref("fixture:owner@1", b"second"), ], compute_generation_artifact_digest_v1(b"settings"), - vec!["lawpack".to_owned()], + vec!["target-artifact".to_owned()], ) .expect_err("conflicting coordinate digests must fail"); @@ -282,14 +240,13 @@ fn conflicting_digests_for_one_coordinate_are_rejected_structurally() { let input = input_with( shape_ir, operations, - None, vec![artifact_ref("fixture:shared@1", b"source")], b"settings", - vec!["lawpack"], + vec!["target-artifact"], ); let generator = GeneratorIdentityV1::for_bytes("fixture:generator@1", "1.0.0", b"generator").unwrap(); - let error = GenerationProvenanceManifestV1::new( + let error = GenerationProvenanceManifestV2::new( &input, generator, vec![artifact_ref("fixture:shared@1", b"different output")], @@ -310,24 +267,23 @@ fn provenance_recomputes_generator_source_and_output_digests() { GenerationArtifactContentV1::new("fixture:owner-b@1", b"owner-b".to_vec()), ]; let outputs = vec![ - GenerationArtifactContentV1::new("fixture:lawpack@1", b"lawpack".to_vec()), + GenerationArtifactContentV1::new("fixture:target-artifact@1", b"target".to_vec()), GenerationArtifactContentV1::new("fixture:profile@1", b"profile".to_vec()), ]; let input = input_with( shape_ir, operations, - Some(law_fixture()), sources .iter() .map(GenerationArtifactContentV1::reference) .collect(), b"settings", - vec!["profile", "lawpack"], + vec!["profile", "target-artifact"], ); let generator_bytes = b"fixture generator component"; let generator = GeneratorIdentityV1::for_bytes("fixture:generator@1", "1.0.0", generator_bytes) .expect("fixture generator identity should validate"); - let manifest = GenerationProvenanceManifestV1::new( + let manifest = GenerationProvenanceManifestV2::new( &input, generator, outputs @@ -388,26 +344,25 @@ fn review_json_is_deterministic_and_explicitly_non_authoritative() { let input = input_with( shape_ir, operations, - None, vec![source.reference()], b"settings", - vec!["profile", "lawpack"], + vec!["profile", "target-artifact"], ); let generator = GeneratorIdentityV1::for_bytes("fixture:generator@1", "1.0.0", b"generator") .expect("generator identity should validate"); - let output_a = artifact_ref("fixture:lawpack@1", b"lawpack"); + let output_a = artifact_ref("fixture:target-artifact@1", b"target"); let output_b = artifact_ref("fixture:profile@1", b"profile"); - let first_manifest = GenerationProvenanceManifestV1::new( + let first_manifest = GenerationProvenanceManifestV2::new( &input, generator.clone(), vec![output_a.clone(), output_b.clone()], ) .unwrap(); let second_manifest = - GenerationProvenanceManifestV1::new(&input, generator, vec![output_b, output_a]).unwrap(); + GenerationProvenanceManifestV2::new(&input, generator, vec![output_b, output_a]).unwrap(); - let first = GenerationReviewV1::from_manifest(&input, &first_manifest).unwrap(); - let second = GenerationReviewV1::from_manifest(&input, &second_manifest).unwrap(); + let first = GenerationReviewV2::from_manifest(&input, &first_manifest).unwrap(); + let second = GenerationReviewV2::from_manifest(&input, &second_manifest).unwrap(); assert!(!first.authoritative()); assert_eq!( first.canonical_bytes().unwrap(), @@ -421,7 +376,7 @@ fn generation_review_deserialization_rejects_authority_claims() { serde_json::from_str(&read("test/fixtures/extension-generation/review.json")).unwrap(); fixture["authoritative"] = Value::Bool(true); - let error = serde_json::from_value::(fixture) + let error = serde_json::from_value::(fixture) .expect_err("authoritative review projection must not deserialize"); assert!( error @@ -449,36 +404,35 @@ fn checked_generation_fixtures_match_public_api_and_published_schemas() { let input = input_with( shape_ir, operations, - None, vec![source.reference()], b"fixture-settings-v1", vec!["generated-profile"], ); let generator_bytes = b"fixture-generator-v1"; - let manifest = GenerationProvenanceManifestV1::new( + let manifest = GenerationProvenanceManifestV2::new( &input, GeneratorIdentityV1::for_bytes("fixture:semantic-generator@1", "1.0.0", generator_bytes) .unwrap(), vec![output.reference()], ) .unwrap(); - let review = GenerationReviewV1::from_manifest(&input, &manifest).unwrap(); + let review = GenerationReviewV2::from_manifest(&input, &manifest).unwrap(); let artifacts = [ ( - "schemas/wesley-extension-generation-input-v1.schema.json", + "schemas/wesley-extension-generation-input-v2.schema.json", "input.json", serde_json::to_value(&input).unwrap(), input.canonical_bytes().unwrap(), ), ( - "schemas/wesley-generation-provenance-manifest-v1.schema.json", + "schemas/wesley-generation-provenance-manifest-v2.schema.json", "provenance.json", serde_json::to_value(&manifest).unwrap(), manifest.canonical_bytes().unwrap(), ), ( - "schemas/wesley-generation-review-v1.schema.json", + "schemas/wesley-generation-review-v2.schema.json", "review.json", serde_json::to_value(&review).unwrap(), review.canonical_bytes().unwrap(), @@ -503,7 +457,7 @@ fn checked_generation_fixtures_match_public_api_and_published_schemas() { } let input_fixture = read(&format!("{fixture_root}/input.json")); - let decoded_input: ExtensionGenerationInputV1 = + let decoded_input: ExtensionGenerationInputV2 = serde_json::from_str(&input_fixture).expect("input fixture should deserialize"); assert_eq!( decoded_input.canonical_bytes().unwrap(), @@ -511,7 +465,7 @@ fn checked_generation_fixtures_match_public_api_and_published_schemas() { ); let provenance_fixture = read(&format!("{fixture_root}/provenance.json")); - let decoded_manifest: GenerationProvenanceManifestV1 = + let decoded_manifest: GenerationProvenanceManifestV2 = serde_json::from_str(&provenance_fixture).expect("provenance fixture should deserialize"); assert_eq!( decoded_manifest.canonical_bytes().unwrap(), @@ -519,7 +473,7 @@ fn checked_generation_fixtures_match_public_api_and_published_schemas() { ); let review_fixture = read(&format!("{fixture_root}/review.json")); - let decoded_review: GenerationReviewV1 = + let decoded_review: GenerationReviewV2 = serde_json::from_str(&review_fixture).expect("review fixture should deserialize"); assert_eq!( decoded_review.canonical_bytes().unwrap(), @@ -534,7 +488,7 @@ fn checked_generation_fixtures_match_public_api_and_published_schemas() { #[test] fn published_input_schema_rejects_malformed_nested_contracts() { let schema: Value = serde_json::from_str(&read( - "schemas/wesley-extension-generation-input-v1.schema.json", + "schemas/wesley-extension-generation-input-v2.schema.json", )) .unwrap(); let validator = generation_schema_validator(&schema); @@ -547,17 +501,13 @@ fn published_input_schema_rejects_malformed_nested_contracts() { let mut empty_shape_type = fixture.clone(); empty_shape_type["shapeIr"]["types"][0] = serde_json::json!({}); - let mut incomplete_law = fixture; - incomplete_law["law"] = serde_json::json!({ - "lawIr": { "apiVersion": "wesley.law-ir/v1" }, - "semanticDigest": compute_generation_artifact_digest_v1(b"semantic-law"), - "canonicalDigest": compute_generation_artifact_digest_v1(b"canonical-law") - }); + let mut unexpected_semantics = fixture; + unexpected_semantics["law"] = serde_json::json!({"ignored": true}); for (name, invalid) in [ ("empty operation", empty_operation), ("empty Shape IR type", empty_shape_type), - ("incomplete Law IR", incomplete_law), + ("unexpected semantic law", unexpected_semantics), ] { assert!( !validator.is_valid(&invalid), @@ -566,23 +516,34 @@ fn published_input_schema_rejects_malformed_nested_contracts() { } } +#[test] +fn generation_input_deserialization_rejects_semantic_law_fields() { + let mut fixture: Value = + serde_json::from_str(&read("test/fixtures/extension-generation/input.json")).unwrap(); + fixture["law"] = serde_json::json!({"ignored": true}); + + let error = serde_json::from_value::(fixture) + .expect_err("v2 must not accept or silently ignore semantic law"); + assert!(error.to_string().contains("unknown field `law`")); +} + #[test] fn published_generation_schemas_reject_malformed_tokens() { let cases = [ ( - "schemas/wesley-extension-generation-input-v1.schema.json", + "schemas/wesley-extension-generation-input-v2.schema.json", "test/fixtures/extension-generation/input.json", "/ownerDeclarations/0/coordinate", serde_json::json!(" fixture:semantic-source@1 "), ), ( - "schemas/wesley-generation-provenance-manifest-v1.schema.json", + "schemas/wesley-generation-provenance-manifest-v2.schema.json", "test/fixtures/extension-generation/provenance.json", "/generator/version", serde_json::json!("1.0.0\n"), ), ( - "schemas/wesley-generation-review-v1.schema.json", + "schemas/wesley-generation-review-v2.schema.json", "test/fixtures/extension-generation/review.json", "/projectionRoles/0", serde_json::json!("\tgenerated-profile"), @@ -617,13 +578,12 @@ fn published_input_schema_accepts_documented_shape_arguments() { let input = input_with( lower_schema_sdl(schema_sdl).expect("documented schema should lower"), list_schema_operations_sdl(schema_sdl).expect("documented operations should list"), - None, Vec::new(), b"settings", vec!["projection"], ); let schema: Value = serde_json::from_str(&read( - "schemas/wesley-extension-generation-input-v1.schema.json", + "schemas/wesley-extension-generation-input-v2.schema.json", )) .unwrap(); let validator = generation_schema_validator(&schema); diff --git a/crates/wesley-core/tests/generated_json_artifacts.rs b/crates/wesley-core/tests/generated_json_artifacts.rs index 3a29862d..bd69efb4 100644 --- a/crates/wesley-core/tests/generated_json_artifacts.rs +++ b/crates/wesley-core/tests/generated_json_artifacts.rs @@ -4,12 +4,8 @@ use std::path::{Path, PathBuf}; use serde_json::json; use sha2::{Digest, Sha256}; -use yaml_rust2::{Yaml, YamlLoader}; -use wesley_core::{ - build_contract_bundle_manifest_v1, compute_registry_hash, diff_law_ir_v1, - list_schema_operations_sdl, load_weslaw_yaml, lower_schema_sdl, to_canonical_law_ir_json, -}; +use wesley_core::{compute_registry_hash, list_schema_operations_sdl, lower_schema_sdl}; const JSON_SCHEMA_DRAFT_07: &str = "http://json-schema.org/draft-07/schema#"; const JSON_SCHEMA_DRAFT_2020_12: &str = "https://json-schema.org/draft/2020-12/schema"; @@ -183,48 +179,6 @@ fn assert_schema_invalid(schema_path: &str, artifact_name: &str, artifact: &serd ); } -fn yaml_fixture_to_json(source: &str) -> serde_json::Value { - let documents = YamlLoader::load_from_str(source).expect("fixture should parse as YAML"); - assert_eq!( - documents.len(), - 1, - "fixture should contain one YAML document" - ); - yaml_to_json_value(&documents[0]) -} - -fn yaml_to_json_value(value: &Yaml) -> serde_json::Value { - match value { - Yaml::Real(text) => { - serde_json::Number::from_f64(text.parse::().expect("real should parse")) - .map(serde_json::Value::Number) - .expect("real should be finite") - } - Yaml::Integer(integer) => serde_json::Value::Number((*integer).into()), - Yaml::String(text) => serde_json::Value::String(text.clone()), - Yaml::Boolean(value) => serde_json::Value::Bool(*value), - Yaml::Array(items) => { - serde_json::Value::Array(items.iter().map(yaml_to_json_value).collect()) - } - Yaml::Hash(map) => { - let object = map - .iter() - .map(|(key, value)| { - ( - key.as_str() - .expect("fixture object keys should be strings") - .to_string(), - yaml_to_json_value(value), - ) - }) - .collect(); - serde_json::Value::Object(object) - } - Yaml::Null => serde_json::Value::Null, - Yaml::Alias(_) | Yaml::BadValue => panic!("unsupported YAML value in fixture"), - } -} - #[test] fn schema_family_declares_supported_draft_boundary() { let readme = read_text("schemas/README.md"); @@ -267,22 +221,6 @@ fn schema_family_declares_supported_draft_boundary() { } } -fn contract_bundle_shape() -> ( - wesley_core::WesleyIR, - Vec, - String, -) { - let sdl = read_text("test/fixtures/weslaw/contract-bundle-shape.graphql"); - let ir = lower_schema_sdl(&sdl).expect("fixture schema should lower"); - let operations = list_schema_operations_sdl(&sdl).expect("fixture operations should list"); - let schema_hash = format!( - "sha256:{}", - compute_registry_hash(&ir).expect("schema hash should compute") - ); - - (ir, operations, schema_hash) -} - #[test] fn l1_ir_fixtures_satisfy_declared_schema() { let fixture_dir = repo_path("test/fixtures/ir-parity"); @@ -320,63 +258,6 @@ fn l1_ir_fixtures_satisfy_declared_schema() { } } -#[test] -fn weslaw_artifact_families_satisfy_declared_schemas() { - let law_fixture = "test/fixtures/weslaw/accepted/footprint-replace-range.weslaw.yaml"; - let law_source = read_text(law_fixture); - let authoring_json = yaml_fixture_to_json(&law_source); - assert_schema_valid( - "schemas/weslaw-v1.schema.json", - law_fixture, - &authoring_json, - ); - - let law_ir = load_weslaw_yaml(&law_source).expect("law fixture should lower"); - let law_ir_json = - serde_json::from_str(&to_canonical_law_ir_json(&law_ir).expect("Law IR should serialize")) - .expect("Law IR JSON should parse"); - assert_schema_valid( - "schemas/wesley-law-ir-v1.schema.json", - "generated Law IR", - &law_ir_json, - ); - - let (ir, operations, _) = contract_bundle_shape(); - let manifest = build_contract_bundle_manifest_v1(&law_ir, &ir, &operations) - .expect("contract bundle manifest should build"); - let manifest_json = - serde_json::to_value(&manifest).expect("contract bundle manifest should serialize"); - assert_schema_valid( - "schemas/wesley-contract-bundle-manifest-v1.schema.json", - "generated contract bundle manifest", - &manifest_json, - ); - - for fixture in [ - "test/fixtures/weslaw/diff/ci-semantic-diff.json", - "test/fixtures/weslaw/diff/holmes-blade-binding-broken.json", - ] { - assert_schema_valid( - "schemas/wesley-law-diff-v1.schema.json", - fixture, - &read_json(fixture), - ); - } - - let old_law = load_weslaw_yaml(&read_text("test/fixtures/weslaw/diff/old.weslaw.yaml")) - .expect("old law should lower"); - let new_law = load_weslaw_yaml(&read_text("test/fixtures/weslaw/diff/new.weslaw.yaml")) - .expect("new law should lower"); - let generated_diff = - serde_json::to_value(diff_law_ir_v1(&old_law, &new_law).expect("diff should compute")) - .expect("diff should serialize"); - assert_schema_valid( - "schemas/wesley-law-diff-v1.schema.json", - "generated law diff", - &generated_diff, - ); -} - #[test] fn holmes_and_shipme_artifacts_satisfy_declared_schemas() { let sha = "abcdef1234567890abcdef1234567890abcdef12"; diff --git a/crates/wesley-core/tests/law_ir.rs b/crates/wesley-core/tests/law_ir.rs deleted file mode 100644 index 781a5980..00000000 --- a/crates/wesley-core/tests/law_ir.rs +++ /dev/null @@ -1,1779 +0,0 @@ -use std::fs; -use std::path::{Path, PathBuf}; - -use yaml_rust2::{Yaml, YamlLoader}; - -use wesley_core::{ - build_contract_bundle_manifest_v1, compute_law_hash_set_v1, compute_law_hash_v1, - compute_registry_hash, diff_law_ir_v1, list_schema_operations_sdl, load_weslaw_yaml, - lower_schema_sdl, lower_wes_channel_directives_to_law_ir_v1, to_canonical_law_ir_json, - to_semantic_law_ir_json, validate_law_ir_v1_bindings, FootprintCardinalityV1, - LawDiffEventKindV1, LawEntryBodyV1, LawKindV1, LawStatusV1, PredicateV1, - ScalarForbiddenInterpretationV1, ScalarRepresentationV1, WeslawDiagnosticCode, - WESLEY_CONTRACT_BUNDLE_MANIFEST_API_VERSION, WESLEY_LAW_DIFF_API_VERSION, - WESLEY_LAW_IR_API_VERSION, WESLEY_LAW_IR_CANONICAL_JSON_CODEC, -}; - -fn repo_path(path: &str) -> PathBuf { - Path::new(env!("CARGO_MANIFEST_DIR")) - .join("../..") - .join(path) -} - -fn read_fixture(path: &str) -> String { - fs::read_to_string(repo_path(path)).expect("fixture should be readable") -} - -fn contract_bundle_shape() -> ( - wesley_core::WesleyIR, - Vec, - String, -) { - let sdl = read_fixture("test/fixtures/weslaw/contract-bundle-shape.graphql"); - let ir = lower_schema_sdl(&sdl).expect("fixture schema should lower"); - let operations = list_schema_operations_sdl(&sdl).expect("fixture operations should list"); - let schema_hash = format!( - "sha256:{}", - compute_registry_hash(&ir).expect("schema hash should compute") - ); - - (ir, operations, schema_hash) -} - -fn bind_law_source( - source: &str, -) -> Result { - let (ir, operations, schema_hash) = contract_bundle_shape(); - let law_ir = load_weslaw_yaml(source)?; - - validate_law_ir_v1_bindings(&law_ir, &ir, &operations, &schema_hash) -} - -fn yaml_fixture_to_json(source: &str) -> serde_json::Value { - let documents = YamlLoader::load_from_str(source).expect("fixture should parse as YAML"); - assert_eq!( - documents.len(), - 1, - "fixture should contain one YAML document" - ); - yaml_to_json_value(&documents[0]) -} - -fn yaml_to_json_value(value: &Yaml) -> serde_json::Value { - match value { - Yaml::Real(text) => { - serde_json::Number::from_f64(text.parse::().expect("fixture real should parse")) - .map(serde_json::Value::Number) - .expect("fixture real should be finite") - } - Yaml::Integer(integer) => serde_json::Value::Number((*integer).into()), - Yaml::String(text) => serde_json::Value::String(text.clone()), - Yaml::Boolean(value) => serde_json::Value::Bool(*value), - Yaml::Array(items) => { - serde_json::Value::Array(items.iter().map(yaml_to_json_value).collect()) - } - Yaml::Hash(map) => { - let object = map - .iter() - .map(|(key, value)| { - ( - key.as_str() - .expect("fixture object keys should be strings") - .to_string(), - yaml_to_json_value(value), - ) - }) - .collect(); - serde_json::Value::Object(object) - } - Yaml::Null => serde_json::Value::Null, - Yaml::Alias(_) | Yaml::BadValue => panic!("fixture contains unsupported YAML value"), - } -} - -#[test] -fn accepted_weslaw_fixtures_satisfy_authoring_json_schema() { - let schema: serde_json::Value = - serde_json::from_str(&read_fixture("schemas/weslaw-v1.schema.json")) - .expect("schema should parse"); - let validator = jsonschema::validator_for(&schema).expect("schema should compile"); - - let fixtures = [ - "test/fixtures/weslaw/accepted/scalar-semantics.weslaw.yaml", - "test/fixtures/weslaw/accepted/variant-playback-mode.weslaw.yaml", - "test/fixtures/weslaw/accepted/footprint-replace-range.weslaw.yaml", - "test/fixtures/weslaw/accepted/channel-ttd-protocol.weslaw.yaml", - "test/fixtures/weslaw/accepted/channel-ttd-protocol-from-directive.weslaw.yaml", - "test/fixtures/weslaw/accepted/invariant-translated-evidence.weslaw.yaml", - "test/fixtures/weslaw/accepted/rust-validator-payoff.weslaw.yaml", - ]; - - for fixture in fixtures { - let json = yaml_fixture_to_json(&read_fixture(fixture)); - let errors = validator - .iter_errors(&json) - .map(|error| error.to_string()) - .collect::>(); - assert!(errors.is_empty(), "{fixture}: {errors:#?}"); - } - - let draft_scaffolding = r#"apiVersion: weslaw/v1 -schema: - family: weslaw-fixture-contract-bundle - hash: sha256:ee681e8c2c99acb5db74f09b2eb06cca2e9379fc7d69627d3287cba6177ac4b6 -laws: - - id: draft.future-law - status: draft - kind: futureLaw - subject: law:future - expr: "forall x in Future: x.ready == true" - futureOnlyField: preserved-for-review -"#; - let draft_json = yaml_fixture_to_json(draft_scaffolding); - let draft_errors = validator - .iter_errors(&draft_json) - .map(|error| error.to_string()) - .collect::>(); - assert!( - draft_errors.is_empty(), - "draft scaffolding should satisfy the authoring schema: {draft_errors:#?}" - ); - - let invalid_cardinality = r#"apiVersion: weslaw/v1 -schema: - family: weslaw-fixture-contract-bundle - hash: sha256:ee681e8c2c99acb5db74f09b2eb06cca2e9379fc7d69627d3287cba6177ac4b6 -laws: - - id: jedit.op.replaceRangeAsTick.bad-cardinality - status: active - kind: footprintLaw - subject: operation:Mutation.replaceRangeAsTick - closures: - - name: touchedRope - fromSlot: baseHead - operator: ropeRangeClosure - cardinality: several -"#; - let invalid_cardinality_json = yaml_fixture_to_json(invalid_cardinality); - let cardinality_errors = validator - .iter_errors(&invalid_cardinality_json) - .map(|error| error.to_string()) - .collect::>(); - assert!( - !cardinality_errors.is_empty(), - "authoring schema must reject unknown cardinality values" - ); -} - -#[test] -fn accepted_weslaw_fixtures_lower_into_typed_law_ir() { - let scalar = load_weslaw_yaml(&read_fixture( - "test/fixtures/weslaw/accepted/scalar-semantics.weslaw.yaml", - )) - .expect("scalar fixture should lower"); - - assert_eq!(scalar.api_version, WESLEY_LAW_IR_API_VERSION); - assert_eq!(scalar.family, "weslaw-fixture-contract-bundle"); - assert_eq!( - scalar.schema_hash, - "sha256:ee681e8c2c99acb5db74f09b2eb06cca2e9379fc7d69627d3287cba6177ac4b6" - ); - assert_eq!(scalar.entries.len(), 1); - - let scalar_entry = &scalar.entries[0]; - assert_eq!(scalar_entry.id, "echo.scalar.positiveInt.u32-positive"); - assert_eq!(scalar_entry.kind, LawKindV1::ScalarSemantics); - assert_eq!(scalar_entry.subject, "scalar:PositiveInt"); - assert_eq!(scalar_entry.tags, vec!["echo", "scalar"]); - let LawEntryBodyV1::ScalarSemantics(body) = &scalar_entry.body else { - panic!("expected scalar semantics body"); - }; - assert_eq!(body.representation, ScalarRepresentationV1::Integer); - assert_eq!(body.min_inclusive, Some(1)); - assert_eq!(body.max_inclusive, Some(4_294_967_295)); - assert_eq!( - body.forbids, - vec![ScalarForbiddenInterpretationV1::SilentGraphqlIntNarrowing] - ); - - let variant = load_weslaw_yaml(&read_fixture( - "test/fixtures/weslaw/accepted/variant-playback-mode.weslaw.yaml", - )) - .expect("variant fixture should lower"); - let LawEntryBodyV1::VariantLaw(body) = &variant.entries[0].body else { - panic!("expected variant body"); - }; - assert_eq!(body.discriminator.field, "kind"); - assert_eq!(body.discriminator.r#enum, "PlaybackModeKind"); - assert_eq!(body.cases.len(), 5); - assert_eq!(body.cases[4].value, "SEEK"); - assert_eq!(body.cases[4].requires, vec!["target", "then"]); - - let footprint = load_weslaw_yaml(&read_fixture( - "test/fixtures/weslaw/accepted/footprint-replace-range.weslaw.yaml", - )) - .expect("footprint fixture should lower"); - let LawEntryBodyV1::FootprintLaw(body) = &footprint.entries[0].body else { - panic!("expected footprint body"); - }; - assert_eq!(body.reads.len(), 6); - assert_eq!(body.slots[0].name, "worldline"); - assert_eq!(body.closures[1].name, "affectedAnchors"); - assert_eq!( - body.create_slots[0].cardinality, - Some(FootprintCardinalityV1::Optional) - ); - assert_eq!(body.updates[0].slot, "worldline"); - - let channel = load_weslaw_yaml(&read_fixture( - "test/fixtures/weslaw/accepted/channel-ttd-protocol.weslaw.yaml", - )) - .expect("channel fixture should lower"); - let LawEntryBodyV1::ChannelLaw(body) = &channel.entries[0].body else { - panic!("expected channel body"); - }; - assert!(body.ordered); - assert_eq!(body.version, 4); - assert_eq!(body.messages.len(), 8); - - let invariant = load_weslaw_yaml(&read_fixture( - "test/fixtures/weslaw/accepted/invariant-translated-evidence.weslaw.yaml", - )) - .expect("invariant fixture should lower"); - let LawEntryBodyV1::InvariantLaw(body) = &invariant.entries[0].body else { - panic!("expected invariant body"); - }; - assert_eq!( - body.predicate, - PredicateV1::FieldEquals { - field: "nativeContinuumWitness".to_string(), - value: serde_json::Value::Bool(false), - } - ); -} - -#[test] -fn law_ir_v1_serializes_as_versioned_canonical_json() { - let law_ir = load_weslaw_yaml(&read_fixture( - "test/fixtures/weslaw/accepted/scalar-semantics.weslaw.yaml", - )) - .expect("fixture should lower"); - - let json = to_canonical_law_ir_json(&law_ir).expect("Law IR should serialize"); - let parsed: serde_json::Value = serde_json::from_str(&json).expect("JSON should parse"); - assert!(!json.contains('\n')); - assert!(!json.contains(" ")); - assert_eq!(parsed["apiVersion"], WESLEY_LAW_IR_API_VERSION); - assert!( - json.contains("silentGraphQLIntNarrowing"), - "closed enum spelling must preserve the GraphQL acronym" - ); - assert_eq!( - json, - wesley_core::to_canonical_json(&parsed).expect("parsed value should re-canonicalize") - ); -} - -#[test] -fn law_hash_uses_semantic_canonical_json_and_excludes_rationale() { - let common_header = r#"apiVersion: weslaw/v1 -schema: - family: weslaw-fixture-contract-bundle - hash: sha256:ee681e8c2c99acb5db74f09b2eb06cca2e9379fc7d69627d3287cba6177ac4b6 -"#; - let first = format!( - r#"{common_header} source: ../first.graphql -registries: - resources: - - id: Diagnostics - owner: jedit - kind: forbidden-runtime-domain - notes: first note -laws: - - id: z.echo.scalar.positiveInt.u32-positive - status: active - kind: scalarSemantics - subject: scalar:PositiveInt - tags: [scalar, echo] - rationale: First rationale. - semantics: - representation: integer - forbids: [silentGraphQLIntNarrowing] - - id: a.jedit.op.replaceRangeAsTick.footprint - status: active - kind: footprintLaw - subject: operation:Mutation.replaceRangeAsTick - reads: [BufferWorldline, TextBlob] -"# - ); - let second = format!( - r#"{common_header} source: ../second.graphql -registries: - resources: - - kind: forbidden-runtime-domain - owner: jedit - id: Diagnostics - notes: second note -laws: - - id: a.jedit.op.replaceRangeAsTick.footprint - kind: footprintLaw - status: active - subject: operation:Mutation.replaceRangeAsTick - reads: [TextBlob, BufferWorldline] - - id: z.echo.scalar.positiveInt.u32-positive - kind: scalarSemantics - status: active - subject: scalar:PositiveInt - rationale: Different prose. - tags: [echo, scalar] - semantics: - forbids: [silentGraphQLIntNarrowing] - representation: integer -"# - ); - - let first_ir = load_weslaw_yaml(&first).expect("first law should lower"); - let second_ir = load_weslaw_yaml(&second).expect("second law should lower"); - - assert_eq!( - compute_law_hash_v1(&first_ir).expect("first law hash should compute"), - compute_law_hash_v1(&second_ir).expect("second law hash should compute") - ); - assert_ne!( - compute_law_hash_set_v1(&first_ir) - .expect("first law document hash should compute") - .law_document_hash, - compute_law_hash_set_v1(&second_ir) - .expect("second law document hash should compute") - .law_document_hash - ); - - let semantic_json = to_semantic_law_ir_json(&first_ir).expect("semantic JSON should compute"); - assert!(!semantic_json.contains("rationale")); - assert!(!semantic_json.contains("schemaSource")); - assert!(!semantic_json.contains("notes")); - assert!(!semantic_json.contains("status")); -} - -#[test] -fn law_hash_materializes_defaults_and_preserves_channel_message_order() { - let omitted_defaults = load_weslaw_yaml( - r#"apiVersion: weslaw/v1 -schema: - family: weslaw-fixture-contract-bundle - hash: sha256:ee681e8c2c99acb5db74f09b2eb06cca2e9379fc7d69627d3287cba6177ac4b6 -laws: - - id: jedit.op.replaceRangeAsTick.footprint - status: active - kind: footprintLaw - subject: operation:Mutation.replaceRangeAsTick - closures: - - name: touchedRope - fromSlot: baseHead - operator: ropeRangeClosure - createSlots: - - name: nextHead - kind: RopeHead -"#, - ) - .expect("omitted defaults should lower"); - let explicit_defaults = load_weslaw_yaml( - r#"apiVersion: weslaw/v1 -schema: - family: weslaw-fixture-contract-bundle - hash: sha256:ee681e8c2c99acb5db74f09b2eb06cca2e9379fc7d69627d3287cba6177ac4b6 -laws: - - id: jedit.op.replaceRangeAsTick.footprint - status: active - kind: footprintLaw - subject: operation:Mutation.replaceRangeAsTick - createSlots: - - name: nextHead - kind: RopeHead - cardinality: one - closures: - - name: touchedRope - fromSlot: baseHead - operator: ropeRangeClosure - cardinality: one -"#, - ) - .expect("explicit defaults should lower"); - - assert_eq!( - compute_law_hash_v1(&omitted_defaults).expect("omitted hash should compute"), - compute_law_hash_v1(&explicit_defaults).expect("explicit hash should compute") - ); - - let ordered_channel = load_weslaw_yaml( - r#"apiVersion: weslaw/v1 -schema: - family: weslaw-fixture-contract-bundle - hash: sha256:ee681e8c2c99acb5db74f09b2eb06cca2e9379fc7d69627d3287cba6177ac4b6 -laws: - - id: warp-ttd.channel.protocol - status: active - kind: channelLaw - subject: channel:ttd.protocol@4 - ordered: true - version: 4 - messages: - - field: hostHello - type: HostHello - - field: laneCatalog - type: LaneCatalog -"#, - ) - .expect("ordered channel should lower"); - let reversed_channel = load_weslaw_yaml( - r#"apiVersion: weslaw/v1 -schema: - family: weslaw-fixture-contract-bundle - hash: sha256:ee681e8c2c99acb5db74f09b2eb06cca2e9379fc7d69627d3287cba6177ac4b6 -laws: - - id: warp-ttd.channel.protocol - status: active - kind: channelLaw - subject: channel:ttd.protocol@4 - ordered: true - version: 4 - messages: - - field: laneCatalog - type: LaneCatalog - - field: hostHello - type: HostHello -"#, - ) - .expect("reversed channel should lower"); - - assert_ne!( - compute_law_hash_v1(&ordered_channel).expect("ordered hash should compute"), - compute_law_hash_v1(&reversed_channel).expect("reversed hash should compute") - ); -} - -#[test] -fn contract_bundle_manifest_records_schema_law_profile_and_bundle_hashes() { - let (ir, operations, schema_hash) = contract_bundle_shape(); - let law_ir = load_weslaw_yaml(&read_fixture( - "test/fixtures/weslaw/accepted/footprint-replace-range.weslaw.yaml", - )) - .expect("fixture should lower"); - - let manifest = build_contract_bundle_manifest_v1(&law_ir, &ir, &operations) - .expect("bundle manifest should build"); - - assert_eq!( - manifest.api_version, - WESLEY_CONTRACT_BUNDLE_MANIFEST_API_VERSION - ); - assert_eq!(manifest.schema_hash, schema_hash); - assert!(manifest.law_hash.starts_with("sha256:")); - assert!(manifest.profile_hash.starts_with("sha256:")); - assert!(manifest.bundle_hash.starts_with("sha256:")); - assert_eq!(manifest.law_ir_codec, WESLEY_LAW_IR_CANONICAL_JSON_CODEC); - assert_eq!(manifest.law_entry_count, 1); - assert!(manifest.law_document_hash.is_some()); - - let schema: serde_json::Value = serde_json::from_str(&read_fixture( - "schemas/wesley-contract-bundle-manifest-v1.schema.json", - )) - .expect("contract bundle manifest schema should parse"); - let validator = - jsonschema::validator_for(&schema).expect("contract bundle manifest schema should compile"); - let manifest_json = - serde_json::to_value(&manifest).expect("contract bundle manifest should serialize"); - let errors = validator - .iter_errors(&manifest_json) - .map(|error| error.to_string()) - .collect::>(); - assert!(errors.is_empty(), "{errors:#?}"); -} - -#[test] -fn law_diff_v1_reports_added_and_removed_entries_and_satisfies_schema() { - let (_, _, schema_hash) = contract_bundle_shape(); - let old_ir = load_weslaw_yaml(&format!( - r#"apiVersion: weslaw/v1 -schema: - family: weslaw-fixture-contract-bundle - hash: {schema_hash} -laws: - - id: b.echo.scalar.positiveInt.u32-positive - status: active - kind: scalarSemantics - subject: scalar:PositiveInt - semantics: - representation: integer -"# - )) - .expect("old law should lower"); - let new_ir = load_weslaw_yaml(&format!( - r#"apiVersion: weslaw/v1 -schema: - family: weslaw-fixture-contract-bundle - hash: {schema_hash} -laws: - - id: a.echo.variant.playback-mode - status: active - kind: variantLaw - subject: input:PlaybackModeInput - discriminator: - field: kind - enum: PlaybackModeKind - cases: - - value: PAUSED - forbids: [target, then] -"# - )) - .expect("new law should lower"); - - let report = diff_law_ir_v1(&old_ir, &new_ir).expect("law diff should compute"); - assert_eq!(report.api_version, WESLEY_LAW_DIFF_API_VERSION); - assert_eq!(report.old_schema_hash, schema_hash); - assert_eq!(report.new_schema_hash, schema_hash); - assert!(report.old_law_hash.starts_with("sha256:")); - assert!(report.new_law_hash.starts_with("sha256:")); - assert_eq!( - report - .changes - .iter() - .map(|change| change.kind) - .collect::>(), - vec![LawDiffEventKindV1::LawAdded, LawDiffEventKindV1::LawRemoved] - ); - - let schema: serde_json::Value = - serde_json::from_str(&read_fixture("schemas/wesley-law-diff-v1.schema.json")) - .expect("law diff schema should parse"); - let validator = jsonschema::validator_for(&schema).expect("law diff schema should compile"); - let report_json = serde_json::to_value(&report).expect("law diff report should serialize"); - let errors = validator - .iter_errors(&report_json) - .map(|error| error.to_string()) - .collect::>(); - assert!(errors.is_empty(), "{errors:#?}"); -} - -#[test] -fn law_diff_v1_reports_scalar_semantic_changes_without_rationale_noise() { - let (_, _, schema_hash) = contract_bundle_shape(); - let old_ir = load_weslaw_yaml(&format!( - r#"apiVersion: weslaw/v1 -schema: - family: weslaw-fixture-contract-bundle - hash: {schema_hash} -laws: - - id: echo.scalar.positiveInt.u32-positive - status: active - kind: scalarSemantics - subject: scalar:PositiveInt - rationale: old prose - semantics: - representation: integer - minInclusive: 1 - maxInclusive: 100 -"# - )) - .expect("old scalar law should lower"); - let new_ir = load_weslaw_yaml(&format!( - r#"apiVersion: weslaw/v1 -schema: - family: weslaw-fixture-contract-bundle - hash: {schema_hash} -laws: - - id: echo.scalar.positiveInt.u32-positive - status: active - kind: scalarSemantics - subject: scalar:PositiveInt - rationale: new prose - semantics: - representation: integer - minInclusive: 2 - maxInclusive: 90 - forbids: [silentGraphQLIntNarrowing] -"# - )) - .expect("new scalar law should lower"); - - let report = diff_law_ir_v1(&old_ir, &new_ir).expect("law diff should compute"); - assert_eq!(report.changes.len(), 1); - let change = &report.changes[0]; - assert_eq!(change.kind, LawDiffEventKindV1::LawStrengthened); - assert_eq!(change.law_kind, Some(LawKindV1::ScalarSemantics)); - assert_eq!( - change - .field_changes - .iter() - .map(|field| field.path.as_str()) - .collect::>(), - vec!["body.minInclusive", "body.maxInclusive", "body.forbids"] - ); - assert!( - !serde_json::to_string(change) - .expect("change should serialize") - .contains("rationale"), - "semantic diff must not include rationale prose" - ); -} - -#[test] -fn law_diff_v1_reports_weakening_scalar_changes() { - let (_, _, schema_hash) = contract_bundle_shape(); - let old_ir = load_weslaw_yaml(&format!( - r#"apiVersion: weslaw/v1 -schema: - family: weslaw-fixture-contract-bundle - hash: {schema_hash} -laws: - - id: echo.scalar.positiveInt.u32-positive - status: active - kind: scalarSemantics - subject: scalar:PositiveInt - semantics: - representation: integer - minInclusive: 10 - maxInclusive: 90 - forbids: [silentGraphQLIntNarrowing] -"# - )) - .expect("old scalar law should lower"); - let new_ir = load_weslaw_yaml(&format!( - r#"apiVersion: weslaw/v1 -schema: - family: weslaw-fixture-contract-bundle - hash: {schema_hash} -laws: - - id: echo.scalar.positiveInt.u32-positive - status: active - kind: scalarSemantics - subject: scalar:PositiveInt - semantics: - representation: integer - minInclusive: 1 - maxInclusive: 100 -"# - )) - .expect("new scalar law should lower"); - - let report = diff_law_ir_v1(&old_ir, &new_ir).expect("law diff should compute"); - assert_eq!(report.changes.len(), 1); - assert_eq!(report.changes[0].kind, LawDiffEventKindV1::LawWeakened); -} - -#[test] -fn law_diff_v1_reports_variant_case_changes() { - let (_, _, schema_hash) = contract_bundle_shape(); - let old_ir = load_weslaw_yaml(&format!( - r#"apiVersion: weslaw/v1 -schema: - family: weslaw-fixture-contract-bundle - hash: {schema_hash} -laws: - - id: echo.variant.playback-mode - status: active - kind: variantLaw - subject: input:PlaybackModeInput - discriminator: - field: kind - enum: PlaybackModeKind - cases: - - value: PAUSED - forbids: [target, then] - - value: SEEK - requires: [target] -"# - )) - .expect("old variant law should lower"); - let new_ir = load_weslaw_yaml(&format!( - r#"apiVersion: weslaw/v1 -schema: - family: weslaw-fixture-contract-bundle - hash: {schema_hash} -laws: - - id: echo.variant.playback-mode - status: active - kind: variantLaw - subject: input:PlaybackModeInput - discriminator: - field: kind - enum: PlaybackModeKind - cases: - - value: PAUSED - forbids: [target] - - value: SEEK - requires: [target, then] -"# - )) - .expect("new variant law should lower"); - - let report = diff_law_ir_v1(&old_ir, &new_ir).expect("law diff should compute"); - assert_eq!(report.changes.len(), 1); - let change = &report.changes[0]; - assert_eq!(change.kind, LawDiffEventKindV1::VariantLawChanged); - assert_eq!( - change - .field_changes - .iter() - .map(|field| field.path.as_str()) - .collect::>(), - vec!["body.cases.PAUSED.forbids", "body.cases.SEEK.requires"] - ); -} - -#[test] -fn law_diff_v1_reports_variant_strengthening_and_weakening() { - let (_, _, schema_hash) = contract_bundle_shape(); - let baseline = load_weslaw_yaml(&format!( - r#"apiVersion: weslaw/v1 -schema: - family: weslaw-fixture-contract-bundle - hash: {schema_hash} -laws: - - id: echo.variant.playback-mode - status: active - kind: variantLaw - subject: input:PlaybackModeInput - discriminator: - field: kind - enum: PlaybackModeKind - cases: - - value: PAUSED - forbids: [target] - - value: SEEK - requires: [target] -"# - )) - .expect("baseline variant law should lower"); - let strengthened = load_weslaw_yaml(&format!( - r#"apiVersion: weslaw/v1 -schema: - family: weslaw-fixture-contract-bundle - hash: {schema_hash} -laws: - - id: echo.variant.playback-mode - status: active - kind: variantLaw - subject: input:PlaybackModeInput - discriminator: - field: kind - enum: PlaybackModeKind - cases: - - value: PAUSED - forbids: [target, then] - - value: SEEK - requires: [target, then] -"# - )) - .expect("strengthened variant law should lower"); - let weakened = load_weslaw_yaml(&format!( - r#"apiVersion: weslaw/v1 -schema: - family: weslaw-fixture-contract-bundle - hash: {schema_hash} -laws: - - id: echo.variant.playback-mode - status: active - kind: variantLaw - subject: input:PlaybackModeInput - discriminator: - field: kind - enum: PlaybackModeKind - cases: - - value: PAUSED - - value: SEEK -"# - )) - .expect("weakened variant law should lower"); - - let strengthened_report = - diff_law_ir_v1(&baseline, &strengthened).expect("law diff should compute"); - assert_eq!(strengthened_report.changes.len(), 1); - assert_eq!( - strengthened_report.changes[0].kind, - LawDiffEventKindV1::LawStrengthened - ); - - let weakened_report = diff_law_ir_v1(&baseline, &weakened).expect("law diff should compute"); - assert_eq!(weakened_report.changes.len(), 1); - assert_eq!( - weakened_report.changes[0].kind, - LawDiffEventKindV1::LawWeakened - ); -} - -#[test] -fn law_diff_v1_reports_footprint_expansion_contraction_and_mixed_changes() { - let (_, _, schema_hash) = contract_bundle_shape(); - let baseline = load_weslaw_yaml(&format!( - r#"apiVersion: weslaw/v1 -schema: - family: weslaw-fixture-contract-bundle - hash: {schema_hash} -laws: - - id: jedit.op.replaceRangeAsTick.footprint - status: active - kind: footprintLaw - subject: operation:Mutation.replaceRangeAsTick - reads: [BufferWorldline] - writes: [BufferWorldline] - creates: [Tick] - forbids: [Diagnostics, UiState] -"# - )) - .expect("baseline footprint law should lower"); - let expanded = load_weslaw_yaml(&format!( - r#"apiVersion: weslaw/v1 -schema: - family: weslaw-fixture-contract-bundle - hash: {schema_hash} -laws: - - id: jedit.op.replaceRangeAsTick.footprint - status: active - kind: footprintLaw - subject: operation:Mutation.replaceRangeAsTick - reads: [BufferWorldline, TextBlob] - writes: [BufferWorldline] - creates: [Tick, TickReceipt] - forbids: [UiState] -"# - )) - .expect("expanded footprint law should lower"); - let contracted = load_weslaw_yaml(&format!( - r#"apiVersion: weslaw/v1 -schema: - family: weslaw-fixture-contract-bundle - hash: {schema_hash} -laws: - - id: jedit.op.replaceRangeAsTick.footprint - status: active - kind: footprintLaw - subject: operation:Mutation.replaceRangeAsTick - reads: [BufferWorldline] - writes: [] - creates: [Tick] - forbids: [Diagnostics, GitWitness, UiState] -"# - )) - .expect("contracted footprint law should lower"); - let mixed_change = load_weslaw_yaml(&format!( - r#"apiVersion: weslaw/v1 -schema: - family: weslaw-fixture-contract-bundle - hash: {schema_hash} -laws: - - id: jedit.op.replaceRangeAsTick.footprint - status: active - kind: footprintLaw - subject: operation:Mutation.replaceRangeAsTick - reads: [BufferWorldline, TextBlob] - writes: [BufferWorldline] - creates: [Tick] - forbids: [Diagnostics, GitWitness, UiState] -"# - )) - .expect("mixed footprint law should lower"); - - let expansion = diff_law_ir_v1(&baseline, &expanded).expect("expansion diff should compute"); - assert_eq!(expansion.changes.len(), 1); - assert_eq!( - expansion.changes[0].kind, - LawDiffEventKindV1::FootprintExpanded - ); - assert_eq!(expansion.changes[0].added_reads, vec!["TextBlob"]); - assert_eq!(expansion.changes[0].added_creates, vec!["TickReceipt"]); - assert_eq!(expansion.changes[0].removed_forbids, vec!["Diagnostics"]); - - let contraction = - diff_law_ir_v1(&baseline, &contracted).expect("contraction diff should compute"); - assert_eq!(contraction.changes.len(), 1); - assert_eq!( - contraction.changes[0].kind, - LawDiffEventKindV1::FootprintContracted - ); - assert_eq!( - contraction.changes[0].removed_writes, - vec!["BufferWorldline"] - ); - assert_eq!(contraction.changes[0].added_forbids, vec!["GitWitness"]); - - let mixed = diff_law_ir_v1(&baseline, &mixed_change).expect("mixed diff should compute"); - assert_eq!(mixed.changes.len(), 1); - assert_eq!(mixed.changes[0].kind, LawDiffEventKindV1::FootprintChanged); -} - -#[test] -fn law_diff_v1_reports_channel_and_invariant_changes_as_modifications() { - let (_, _, schema_hash) = contract_bundle_shape(); - let old_ir = load_weslaw_yaml(&format!( - r#"apiVersion: weslaw/v1 -schema: - family: weslaw-fixture-contract-bundle - hash: {schema_hash} -laws: - - id: warp-ttd.channel.protocol - status: active - kind: channelLaw - subject: channel:ttd.protocol@4 - ordered: true - version: 4 - messages: - - field: hostHello - type: HostHello - - id: continuum.invariant.translated-evidence - status: active - kind: invariantLaw - subject: type:TranslatedSubstrateEvidence - predicate: - op: fieldEquals - field: nativeContinuumWitness - value: false -"# - )) - .expect("old law should lower"); - let new_ir = load_weslaw_yaml(&format!( - r#"apiVersion: weslaw/v1 -schema: - family: weslaw-fixture-contract-bundle - hash: {schema_hash} -laws: - - id: warp-ttd.channel.protocol - status: active - kind: channelLaw - subject: channel:ttd.protocol@4 - ordered: true - version: 5 - messages: - - field: hostHello - type: HostHello - - field: laneCatalog - type: LaneCatalog - - id: continuum.invariant.translated-evidence - status: active - kind: invariantLaw - subject: type:TranslatedSubstrateEvidence - predicate: - op: fieldEquals - field: nativeContinuumWitness - value: true -"# - )) - .expect("new law should lower"); - - let report = diff_law_ir_v1(&old_ir, &new_ir).expect("law diff should compute"); - assert_eq!( - report - .changes - .iter() - .map(|change| change.kind) - .collect::>(), - vec![ - LawDiffEventKindV1::PredicateChanged, - LawDiffEventKindV1::ChannelVersionChanged, - ] - ); - assert!( - report - .changes - .iter() - .all(|change| change.kind != LawDiffEventKindV1::LawAdded), - "existing law modifications must not be reported as additions" - ); -} - -#[test] -fn law_diff_v1_reports_registry_and_tag_hash_deltas() { - let (_, _, schema_hash) = contract_bundle_shape(); - let old_ir = load_weslaw_yaml(&format!( - r#"apiVersion: weslaw/v1 -schema: - family: weslaw-fixture-contract-bundle - hash: {schema_hash} -registries: - resources: - - id: Diagnostics - owner: jedit - kind: forbidden-runtime-domain -laws: - - id: echo.scalar.positiveInt.u32-positive - status: active - kind: scalarSemantics - subject: scalar:PositiveInt - tags: [echo] - semantics: - representation: integer -"# - )) - .expect("old law should lower"); - let new_ir = load_weslaw_yaml(&format!( - r#"apiVersion: weslaw/v1 -schema: - family: weslaw-fixture-contract-bundle - hash: {schema_hash} -registries: - resources: - - id: Diagnostics - owner: echo - kind: forbidden-runtime-domain -laws: - - id: echo.scalar.positiveInt.u32-positive - status: active - kind: scalarSemantics - subject: scalar:PositiveInt - tags: [echo, scalar] - semantics: - representation: integer -"# - )) - .expect("new law should lower"); - - let report = diff_law_ir_v1(&old_ir, &new_ir).expect("law diff should compute"); - assert_ne!(report.old_law_hash, report.new_law_hash); - assert_eq!( - report - .changes - .iter() - .map(|change| change.kind) - .collect::>(), - vec![ - LawDiffEventKindV1::RegistryChanged, - LawDiffEventKindV1::LawTagsChanged, - ] - ); -} - -#[test] -fn law_diff_v1_fixture_outputs_satisfy_published_schema() { - let schema: serde_json::Value = - serde_json::from_str(&read_fixture("schemas/wesley-law-diff-v1.schema.json")) - .expect("law diff schema should parse"); - let validator = jsonschema::validator_for(&schema).expect("law diff schema should compile"); - for fixture_path in [ - "test/fixtures/weslaw/diff/ci-semantic-diff.json", - "test/fixtures/weslaw/diff/holmes-blade-binding-broken.json", - ] { - let report_json: serde_json::Value = - serde_json::from_str(&read_fixture(fixture_path)).expect("fixture should parse"); - let errors = validator - .iter_errors(&report_json) - .map(|error| error.to_string()) - .collect::>(); - assert!(errors.is_empty(), "{fixture_path}: {errors:#?}"); - } -} - -#[test] -fn semantic_law_hash_ignores_programmatic_draft_entries() { - let mut law_ir = load_weslaw_yaml(&read_fixture( - "test/fixtures/weslaw/accepted/scalar-semantics.weslaw.yaml", - )) - .expect("fixture should lower"); - let baseline_hash = compute_law_hash_v1(&law_ir).expect("baseline hash should compute"); - let mut draft = law_ir.entries[0].clone(); - draft.id = "draft.echo.scalar.positiveInt.other".to_string(); - draft.status = LawStatusV1::Draft; - draft.rationale = Some("Draft prose must not affect semantic hashes.".to_string()); - if let LawEntryBodyV1::ScalarSemantics(body) = &mut draft.body { - body.min_inclusive = Some(999); - } - law_ir.entries.push(draft); - - assert_eq!( - baseline_hash, - compute_law_hash_v1(&law_ir).expect("draft-bearing hash should compute") - ); -} - -#[test] -fn law_ir_v1_excludes_drafts_and_sorts_active_entries_by_id() { - let common_header = r#"apiVersion: weslaw/v1 -schema: - family: weslaw-fixture-contract-bundle - hash: sha256:ee681e8c2c99acb5db74f09b2eb06cca2e9379fc7d69627d3287cba6177ac4b6 - source: ../contract-bundle-shape.graphql -laws: -"#; - let scalar_law = r#" - id: z.echo.scalar.positiveInt.u32-positive - status: active - kind: scalarSemantics - subject: scalar:PositiveInt - semantics: - representation: integer - minInclusive: 1 - maxInclusive: 4294967295 - forbids: [silentGraphQLIntNarrowing] -"#; - let invariant_law = r#" - id: a.continuum.invariant.translated-evidence - status: active - kind: invariantLaw - subject: type:TranslatedSubstrateEvidence - predicate: - op: fieldEquals - field: nativeContinuumWitness - value: false -"#; - let draft_law = r#" - id: m.draft.scalar.example - status: draft - kind: scalarSemantics - subject: scalar:PositiveInt - semantics: - representation: integer - minInclusive: 1 -"#; - - let authored_order = format!("{common_header}{scalar_law}{draft_law}{invariant_law}"); - let reversed_order = format!("{common_header}{invariant_law}{draft_law}{scalar_law}"); - - let law_ir = load_weslaw_yaml(&authored_order).expect("fixture should lower"); - let ids = law_ir - .entries - .iter() - .map(|entry| entry.id.as_str()) - .collect::>(); - assert_eq!( - ids, - vec![ - "a.continuum.invariant.translated-evidence", - "z.echo.scalar.positiveInt.u32-positive" - ] - ); - - let authored_json = to_canonical_law_ir_json(&law_ir).expect("Law IR should serialize"); - let reversed_json = to_canonical_law_ir_json( - &load_weslaw_yaml(&reversed_order).expect("reversed fixture should lower"), - ) - .expect("Law IR should serialize"); - assert_eq!(authored_json, reversed_json); - assert!(!authored_json.contains("m.draft.scalar.example")); -} - -#[test] -fn law_ir_v1_ignores_draft_entries_before_kind_body_validation() { - let source = r#"apiVersion: weslaw/v1 -schema: - family: weslaw-fixture-contract-bundle - hash: sha256:ee681e8c2c99acb5db74f09b2eb06cca2e9379fc7d69627d3287cba6177ac4b6 - source: ../contract-bundle-shape.graphql -laws: - - id: a.continuum.invariant.translated-evidence - status: active - kind: invariantLaw - subject: type:TranslatedSubstrateEvidence - predicate: - op: fieldEquals - field: nativeContinuumWitness - value: false - - id: z.draft.future-law - status: draft - kind: futureLaw - subject: law:future - expr: "forall x in Future: x.ready == true" - futureOnlyField: preserved-for-review -"#; - - let law_ir = - load_weslaw_yaml(source).expect("draft scaffolding should not fail active lowering"); - assert_eq!(law_ir.entries.len(), 1); - assert_eq!( - law_ir.entries[0].id, - "a.continuum.invariant.translated-evidence" - ); -} - -#[test] -fn footprint_closure_cardinality_defaults_to_one_when_omitted() { - let source = r#"apiVersion: weslaw/v1 -schema: - family: weslaw-fixture-contract-bundle - hash: sha256:ee681e8c2c99acb5db74f09b2eb06cca2e9379fc7d69627d3287cba6177ac4b6 -laws: - - id: jedit.op.replaceRangeAsTick.footprint - status: active - kind: footprintLaw - subject: operation:Mutation.replaceRangeAsTick - closures: - - name: touchedRope - fromSlot: baseHead - operator: ropeRangeClosure -"#; - - let law_ir = load_weslaw_yaml(source).expect("schema-valid closure should lower"); - let LawEntryBodyV1::FootprintLaw(body) = &law_ir.entries[0].body else { - panic!("expected footprint law body"); - }; - assert_eq!(body.closures[0].cardinality, FootprintCardinalityV1::One); -} - -#[test] -fn law_ir_v1_json_schema_accepts_ir_and_rejects_kind_body_mismatch() { - let schema: serde_json::Value = - serde_json::from_str(&read_fixture("schemas/wesley-law-ir-v1.schema.json")) - .expect("Law IR schema should parse"); - let validator = jsonschema::validator_for(&schema).expect("Law IR schema should compile"); - - let law_ir = load_weslaw_yaml(&read_fixture( - "test/fixtures/weslaw/accepted/scalar-semantics.weslaw.yaml", - )) - .expect("fixture should lower"); - let json = to_canonical_law_ir_json(&law_ir).expect("Law IR should serialize"); - let valid_ir: serde_json::Value = serde_json::from_str(&json).expect("JSON should parse"); - let valid_errors = validator - .iter_errors(&valid_ir) - .map(|error| error.to_string()) - .collect::>(); - assert!(valid_errors.is_empty(), "{valid_errors:#?}"); - - let mut mismatched_ir = valid_ir.clone(); - mismatched_ir["entries"][0]["kind"] = serde_json::Value::String("footprintLaw".to_string()); - let mismatch_errors = validator - .iter_errors(&mismatched_ir) - .map(|error| error.to_string()) - .collect::>(); - assert!( - !mismatch_errors.is_empty(), - "Law IR schema must reject kind/body mismatches" - ); - - let mut draft_ir = valid_ir; - draft_ir["entries"][0]["status"] = serde_json::Value::String("draft".to_string()); - let draft_errors = validator - .iter_errors(&draft_ir) - .map(|error| error.to_string()) - .collect::>(); - assert!( - !draft_errors.is_empty(), - "Law IR schema must reject draft entries" - ); - - let mut invalid_ordering_ir = draft_ir; - invalid_ordering_ir["entries"][0]["status"] = serde_json::Value::String("active".to_string()); - invalid_ordering_ir["entries"][0]["body"]["ordering"] = - serde_json::Value::String("lamprot".to_string()); - let ordering_errors = validator - .iter_errors(&invalid_ordering_ir) - .map(|error| error.to_string()) - .collect::>(); - assert!( - !ordering_errors.is_empty(), - "Law IR schema must reject unknown scalar ordering values" - ); -} - -#[test] -fn law_ir_v1_loader_rejects_malformed_schema_hash_anchors() { - let source = r#"apiVersion: weslaw/v1 -schema: - family: weslaw-fixture-contract-bundle - hash: ee681e8c2c99acb5db74f09b2eb06cca2e9379fc7d69627d3287cba6177ac4b6 -laws: - - id: echo.scalar.positiveInt.u32-positive - status: active - kind: scalarSemantics - subject: scalar:PositiveInt - semantics: - representation: integer -"#; - - let error = load_weslaw_yaml(source).expect_err("malformed schema hash should fail"); - assert_eq!(error.code, WeslawDiagnosticCode::InvalidDocument); - assert_eq!(error.path.as_deref(), Some("$.schema.hash")); -} - -#[test] -fn law_ir_v1_binding_rejects_schema_hash_mismatch() { - let (ir, operations, schema_hash) = contract_bundle_shape(); - let law_ir = load_weslaw_yaml(&read_fixture( - "test/fixtures/weslaw/rejected/schema-hash-mismatch.weslaw.yaml", - )) - .expect("structure-valid law should lower"); - - let error = validate_law_ir_v1_bindings(&law_ir, &ir, &operations, &schema_hash) - .expect_err("schema hash mismatch must fail before subject binding"); - assert_eq!(error.code, WeslawDiagnosticCode::SchemaHashMismatch); - assert_eq!(error.code.as_str(), "WESLAW_SCHEMA_HASH_MISMATCH"); - assert_eq!(error.path.as_deref(), Some("$.schema.hash")); -} - -#[test] -fn law_ir_v1_binding_rejects_unresolved_subjects() { - let (ir, operations, schema_hash) = contract_bundle_shape(); - let law_ir = load_weslaw_yaml(&read_fixture( - "test/fixtures/weslaw/rejected/unresolved-subject.weslaw.yaml", - )) - .expect("structure-valid law should lower"); - - let error = validate_law_ir_v1_bindings(&law_ir, &ir, &operations, &schema_hash) - .expect_err("unresolved operation subject must fail"); - assert_eq!(error.code, WeslawDiagnosticCode::UnresolvedSubject); - assert_eq!(error.code.as_str(), "WESLAW_UNRESOLVED_SUBJECT"); - assert_eq!(error.path.as_deref(), Some("$.laws[0].subject")); - assert!(error.message.contains("operation:Mutation.replaceRange")); - assert!(error - .message - .contains("operation:Mutation.replaceRangeAsTick")); -} - -#[test] -fn law_ir_v1_binding_reports_authored_law_indices_after_normalization() { - let (ir, operations, schema_hash) = contract_bundle_shape(); - let source = format!( - r#"apiVersion: weslaw/v1 -schema: - family: weslaw-fixture-contract-bundle - hash: {schema_hash} - source: ../contract-bundle-shape.graphql -laws: - - id: z.scalar.positiveInt.u32-positive - status: active - kind: scalarSemantics - subject: scalar:PositiveInt - semantics: - representation: integer - minInclusive: 1 - maxInclusive: 4294967295 - forbids: [silentGraphQLIntNarrowing] - - id: a.scalar.missing - status: active - kind: scalarSemantics - subject: scalar:MissingScalar - semantics: - representation: string - forbids: [] -"# - ); - let law_ir = load_weslaw_yaml(&source).expect("law should lower"); - - let error = validate_law_ir_v1_bindings(&law_ir, &ir, &operations, &schema_hash) - .expect_err("missing scalar should fail"); - assert_eq!(error.code, WeslawDiagnosticCode::UnresolvedSubject); - assert_eq!(error.path.as_deref(), Some("$.laws[1].subject")); -} - -#[test] -fn law_ir_v1_binding_rejects_non_object_schema_footprint_resources() { - let (ir, operations, schema_hash) = contract_bundle_shape(); - let source = format!( - r#"apiVersion: weslaw/v1 -schema: - family: weslaw-fixture-contract-bundle - hash: {schema_hash} - source: ../contract-bundle-shape.graphql -laws: - - id: jedit.op.replaceRangeAsTick.bad-resource - status: active - kind: footprintLaw - subject: operation:Mutation.replaceRangeAsTick - reads: [PositiveInt] - writes: [] - creates: [] - forbids: [] - slots: [] - closures: [] - createSlots: [] - updates: [] -"# - ); - let law_ir = load_weslaw_yaml(&source).expect("law should lower"); - - let error = validate_law_ir_v1_bindings(&law_ir, &ir, &operations, &schema_hash) - .expect_err("scalar footprint resource should fail"); - assert_eq!(error.code, WeslawDiagnosticCode::WrongSubjectKind); - assert_eq!(error.path.as_deref(), Some("$.laws[0].resources")); - assert!(error - .message - .contains("schema-backed resources must be object types")); -} - -#[test] -fn formal_wes_channel_directives_lower_into_canonical_law_ir() { - let (ir, operations, schema_hash) = contract_bundle_shape(); - let lowered = lower_wes_channel_directives_to_law_ir_v1( - &ir, - "weslaw-fixture-contract-bundle", - &schema_hash, - Some("../contract-bundle-shape.graphql".to_string()), - ) - .expect("@wes_channel directives should lower"); - let authored = load_weslaw_yaml(&read_fixture( - "test/fixtures/weslaw/accepted/channel-ttd-protocol-from-directive.weslaw.yaml", - )) - .expect("authored equivalent should lower"); - - assert_eq!( - to_semantic_law_ir_json(&lowered).expect("lowered semantic JSON should compute"), - to_semantic_law_ir_json(&authored).expect("authored semantic JSON should compute") - ); - assert_eq!( - compute_law_hash_v1(&lowered).expect("lowered hash should compute"), - compute_law_hash_v1(&authored).expect("authored hash should compute") - ); - - let binding = validate_law_ir_v1_bindings(&lowered, &ir, &operations, &schema_hash) - .expect("directive-lowered channel law should bind"); - assert_eq!(binding.bound_entry_count, 1); -} - -#[test] -fn law_ir_v1_binding_accepts_schema_and_operation_subjects() { - let (ir, operations, schema_hash) = contract_bundle_shape(); - let law_source = format!( - r#"apiVersion: weslaw/v1 -schema: - family: weslaw-fixture-contract-bundle - hash: {schema_hash} -registries: - verifiers: - - id: continuum-law-checker - owner: continuum -laws: - - id: echo.scalar.positiveInt.u32-positive - status: active - kind: scalarSemantics - subject: scalar:PositiveInt - semantics: - representation: integer - minInclusive: 1 - - id: echo.variant.playback-mode - status: active - kind: variantLaw - subject: input:PlaybackModeInput - discriminator: - field: kind - enum: PlaybackModeKind - cases: - - value: PAUSED - forbids: [target, then] - - id: continuum.invariant.translated-evidence - status: active - kind: invariantLaw - subject: type:TranslatedSubstrateEvidence - predicate: - op: fieldEquals - field: nativeContinuumWitness - value: false - - id: continuum.invariant.playback-mode-kind - status: active - kind: invariantLaw - subject: enum:PlaybackModeKind - predicate: - op: external - verifier: continuum-law-checker - ref: continuum.invariants.playbackModeKind - - id: continuum.invariant.buffer-worldline-id - status: active - kind: invariantLaw - subject: field:BufferWorldline.worldlineId - predicate: - op: external - verifier: continuum-law-checker - ref: continuum.invariants.bufferWorldlineId - - id: jedit.op.replaceRangeAsTick.footprint - status: active - kind: footprintLaw - subject: operation:Mutation.replaceRangeAsTick - reads: [BufferWorldline] -"# - ); - let law_ir = load_weslaw_yaml(&law_source).expect("law should lower"); - - let report = validate_law_ir_v1_bindings(&law_ir, &ir, &operations, &schema_hash) - .expect("accepted subject coordinates should bind"); - - assert_eq!(report.schema_hash, schema_hash); - assert_eq!(report.bound_entry_count, 6); -} - -#[test] -fn accepted_weslaw_fixtures_bind_to_contract_bundle_shape() { - let fixtures = [ - "test/fixtures/weslaw/accepted/scalar-semantics.weslaw.yaml", - "test/fixtures/weslaw/accepted/variant-playback-mode.weslaw.yaml", - "test/fixtures/weslaw/accepted/footprint-replace-range.weslaw.yaml", - "test/fixtures/weslaw/accepted/channel-ttd-protocol.weslaw.yaml", - "test/fixtures/weslaw/accepted/invariant-translated-evidence.weslaw.yaml", - "test/fixtures/weslaw/accepted/rust-validator-payoff.weslaw.yaml", - ]; - - for fixture in fixtures { - let report = bind_law_source(&read_fixture(fixture)).expect(fixture); - let expected_count = if fixture.ends_with("rust-validator-payoff.weslaw.yaml") { - 2 - } else { - 1 - }; - assert_eq!(report.bound_entry_count, expected_count, "{fixture}"); - } -} - -#[test] -fn law_ir_v1_binding_rejects_wrong_subject_kind_for_law_kind() { - let (ir, operations, schema_hash) = contract_bundle_shape(); - let source = format!( - r#"apiVersion: weslaw/v1 -schema: - family: weslaw-fixture-contract-bundle - hash: {schema_hash} -laws: - - id: echo.scalar.positiveInt.u32-positive - status: active - kind: scalarSemantics - subject: type:BufferWorldline - semantics: - representation: integer -"# - ); - let law_ir = load_weslaw_yaml(&source).expect("law should lower"); - - let error = validate_law_ir_v1_bindings(&law_ir, &ir, &operations, &schema_hash) - .expect_err("scalar semantics must target a scalar subject"); - - assert_eq!(error.code, WeslawDiagnosticCode::WrongSubjectKind); - assert_eq!(error.code.as_str(), "WESLAW_WRONG_SUBJECT_KIND"); - assert_eq!(error.path.as_deref(), Some("$.laws[0].subject")); -} - -#[test] -fn law_ir_v1_binding_rejects_unbound_variant_references() { - let (_, _, schema_hash) = contract_bundle_shape(); - let source = format!( - r#"apiVersion: weslaw/v1 -schema: - family: weslaw-fixture-contract-bundle - hash: {schema_hash} -laws: - - id: echo.variant.playback-mode - status: active - kind: variantLaw - subject: input:PlaybackModeInput - discriminator: - field: missingKind - enum: PlaybackModeKind - cases: - - value: PAUSED -"# - ); - - let error = bind_law_source(&source).expect_err("missing discriminator should fail"); - assert_eq!(error.code, WeslawDiagnosticCode::UnresolvedReference); - assert_eq!(error.code.as_str(), "WESLAW_UNRESOLVED_REFERENCE"); - assert_eq!(error.path.as_deref(), Some("$.laws[0].discriminator.field")); -} - -#[test] -fn law_ir_v1_binding_rejects_unbound_footprint_arg_paths() { - let (_, _, schema_hash) = contract_bundle_shape(); - let source = format!( - r#"apiVersion: weslaw/v1 -schema: - family: weslaw-fixture-contract-bundle - hash: {schema_hash} -laws: - - id: jedit.op.replaceRangeAsTick.footprint - status: active - kind: footprintLaw - subject: operation:Mutation.replaceRangeAsTick - reads: [BufferWorldline] - slots: - - name: worldline - kind: BufferWorldline - bindFromArg: input.missingWorldlineId -"# - ); - - let error = bind_law_source(&source).expect_err("missing argument path should fail"); - assert_eq!(error.code, WeslawDiagnosticCode::UnresolvedReference); - assert_eq!( - error.path.as_deref(), - Some("$.laws[0].slots[0].bindFromArg") - ); -} - -#[test] -fn law_ir_v1_binding_rejects_unbound_footprint_resources() { - let (_, _, schema_hash) = contract_bundle_shape(); - let source = format!( - r#"apiVersion: weslaw/v1 -schema: - family: weslaw-fixture-contract-bundle - hash: {schema_hash} -laws: - - id: jedit.op.replaceRangeAsTick.footprint - status: active - kind: footprintLaw - subject: operation:Mutation.replaceRangeAsTick - reads: [MissingResource] -"# - ); - - let error = bind_law_source(&source).expect_err("missing resource should fail"); - assert_eq!(error.code, WeslawDiagnosticCode::UnresolvedReference); -} - -#[test] -fn law_ir_v1_binding_rejects_contradictory_variant_and_footprint_law() { - let (_, _, schema_hash) = contract_bundle_shape(); - let variant = format!( - r#"apiVersion: weslaw/v1 -schema: - family: weslaw-fixture-contract-bundle - hash: {schema_hash} -laws: - - id: echo.variant.playback-mode - status: active - kind: variantLaw - subject: input:PlaybackModeInput - discriminator: - field: kind - enum: PlaybackModeKind - cases: - - value: SEEK - requires: [target] - forbids: [target] -"# - ); - let variant_error = bind_law_source(&variant).expect_err("contradictory variant should fail"); - assert_eq!(variant_error.code, WeslawDiagnosticCode::Conflict); - - let footprint = format!( - r#"apiVersion: weslaw/v1 -schema: - family: weslaw-fixture-contract-bundle - hash: {schema_hash} -laws: - - id: jedit.op.replaceRangeAsTick.footprint - status: active - kind: footprintLaw - subject: operation:Mutation.replaceRangeAsTick - reads: [BufferWorldline] - forbids: [BufferWorldline] -"# - ); - let footprint_error = - bind_law_source(&footprint).expect_err("contradictory footprint should fail"); - assert_eq!(footprint_error.code, WeslawDiagnosticCode::Conflict); - - let duplicate_subject = format!( - r#"apiVersion: weslaw/v1 -schema: - family: weslaw-fixture-contract-bundle - hash: {schema_hash} -laws: - - id: echo.scalar.positiveInt.u32-positive - status: active - kind: scalarSemantics - subject: scalar:PositiveInt - semantics: - representation: integer - - id: echo.scalar.positiveInt.other - status: active - kind: scalarSemantics - subject: scalar:PositiveInt - semantics: - representation: string -"# - ); - let duplicate_error = - bind_law_source(&duplicate_subject).expect_err("duplicate scalar law subject should fail"); - assert_eq!(duplicate_error.code, WeslawDiagnosticCode::Conflict); -} - -#[test] -fn law_ir_v1_binding_rejects_unbound_invariant_predicate_fields() { - let (_, _, schema_hash) = contract_bundle_shape(); - let source = format!( - r#"apiVersion: weslaw/v1 -schema: - family: weslaw-fixture-contract-bundle - hash: {schema_hash} -laws: - - id: continuum.invariant.translated-evidence - status: active - kind: invariantLaw - subject: type:TranslatedSubstrateEvidence - predicate: - op: fieldEquals - field: missingWitness - value: false -"# - ); - - let error = bind_law_source(&source).expect_err("missing predicate field should fail"); - assert_eq!(error.code, WeslawDiagnosticCode::UnresolvedReference); - assert_eq!(error.path.as_deref(), Some("$.laws[0].predicate.field")); -} - -#[test] -fn rejected_weslaw_fixtures_emit_stable_diagnostic_codes() { - let cases = [ - ( - "test/fixtures/weslaw/rejected/duplicate-id.weslaw.yaml", - "test/fixtures/weslaw/rejected/duplicate-id.expected.txt", - WeslawDiagnosticCode::DuplicateId, - ), - ( - "test/fixtures/weslaw/rejected/raw-expression-invariant.weslaw.yaml", - "test/fixtures/weslaw/rejected/raw-expression-invariant.expected.txt", - WeslawDiagnosticCode::RawExprRejected, - ), - ( - "test/fixtures/weslaw/rejected/unknown-kind.weslaw.yaml", - "test/fixtures/weslaw/rejected/unknown-kind.expected.txt", - WeslawDiagnosticCode::UnknownKind, - ), - ( - "test/fixtures/weslaw/rejected/unknown-field.weslaw.yaml", - "test/fixtures/weslaw/rejected/unknown-field.expected.txt", - WeslawDiagnosticCode::UnknownField, - ), - ( - "test/fixtures/weslaw/rejected/wrong-type-optional-sequence.weslaw.yaml", - "test/fixtures/weslaw/rejected/wrong-type-optional-sequence.expected.txt", - WeslawDiagnosticCode::InvalidDocument, - ), - ( - "test/fixtures/weslaw/rejected/field-equals-extra-predicate-field.weslaw.yaml", - "test/fixtures/weslaw/rejected/field-equals-extra-predicate-field.expected.txt", - WeslawDiagnosticCode::UnknownField, - ), - ( - "test/fixtures/weslaw/rejected/external-extra-predicate-field.weslaw.yaml", - "test/fixtures/weslaw/rejected/external-extra-predicate-field.expected.txt", - WeslawDiagnosticCode::UnknownField, - ), - ( - "test/fixtures/weslaw/rejected/scalar-range-on-non-integer.weslaw.yaml", - "test/fixtures/weslaw/rejected/scalar-range-on-non-integer.expected.txt", - WeslawDiagnosticCode::InvalidDocument, - ), - ( - "test/fixtures/weslaw/rejected/scalar-min-greater-than-max.weslaw.yaml", - "test/fixtures/weslaw/rejected/scalar-min-greater-than-max.expected.txt", - WeslawDiagnosticCode::InvalidDocument, - ), - ( - "test/fixtures/weslaw/rejected/scalar-forbid-non-integer.weslaw.yaml", - "test/fixtures/weslaw/rejected/scalar-forbid-non-integer.expected.txt", - WeslawDiagnosticCode::InvalidDocument, - ), - ( - "test/fixtures/weslaw/rejected/scalar-unknown-ordering.weslaw.yaml", - "test/fixtures/weslaw/rejected/scalar-unknown-ordering.expected.txt", - WeslawDiagnosticCode::InvalidDocument, - ), - ( - "test/fixtures/weslaw/rejected/footprint-unknown-cardinality.weslaw.yaml", - "test/fixtures/weslaw/rejected/footprint-unknown-cardinality.expected.txt", - WeslawDiagnosticCode::InvalidDocument, - ), - ]; - - for (path, expected_path, code) in cases { - let error = load_weslaw_yaml(&read_fixture(path)).expect_err(path); - assert_eq!(error.code, code, "{path}"); - assert_eq!( - error.code.as_str(), - read_fixture(expected_path).trim(), - "{path}" - ); - } -} diff --git a/crates/wesley-emit-rust/src/lib.rs b/crates/wesley-emit-rust/src/lib.rs index 51126c84..f0efb8e0 100644 --- a/crates/wesley-emit-rust/src/lib.rs +++ b/crates/wesley-emit-rust/src/lib.rs @@ -6,8 +6,8 @@ use std::collections::BTreeSet; use std::fmt::Write; use wesley_core::{ - Field, LawEntryBodyV1, LawIrV1, OperationArgument, OperationType, ScalarRepresentationV1, - SchemaOperation, TypeDefinition, TypeKind, TypeReference, WesleyIR, + Field, OperationArgument, OperationType, SchemaOperation, TypeDefinition, TypeKind, + TypeReference, WesleyIR, }; mod le_binary; @@ -36,45 +36,9 @@ pub fn emit_rust_with_operations(ir: &WesleyIR, operations: &[SchemaOperation]) print_file(&file) } -/// Emits Rust declarations with schema and law hash traceability constants. -pub fn emit_rust_with_operations_and_hashes( - ir: &WesleyIR, - operations: &[SchemaOperation], - schema_hash: &str, - law_hash: &str, -) -> String { - let mut file = RustFile::from_ir_and_operations(ir, operations); - file.provenance = Some(RustProvenanceConstants { - schema_hash: schema_hash.to_string(), - law_hash: law_hash.to_string(), - }); - - print_file(&file) -} - -/// Emits Rust declarations with provenance constants and law-backed helper validators. -pub fn emit_rust_with_operations_and_law( - ir: &WesleyIR, - operations: &[SchemaOperation], - schema_hash: &str, - law_hash: &str, - law_ir: &LawIrV1, -) -> String { - let mut file = RustFile::from_ir_and_operations(ir, operations); - file.provenance = Some(RustProvenanceConstants { - schema_hash: schema_hash.to_string(), - law_hash: law_hash.to_string(), - }); - file.law_items = rust_law_items_from_ir(ir, law_ir); - - print_file(&file) -} - #[derive(Debug, Clone, PartialEq, Eq)] struct RustFile { - provenance: Option, items: Vec, - law_items: Vec, } impl RustFile { @@ -114,20 +78,10 @@ impl RustFile { .map(|operation| RustItem::Operation(operation_binding_from_schema(operation))), ); - Self { - provenance: None, - items, - law_items: Vec::new(), - } + Self { items } } } -#[derive(Debug, Clone, PartialEq, Eq)] -struct RustProvenanceConstants { - schema_hash: String, - law_hash: String, -} - #[derive(Debug, Clone, PartialEq, Eq)] enum RustItem { TypeAlias(RustTypeAlias), @@ -136,12 +90,6 @@ enum RustItem { Operation(RustOperationBinding), } -#[derive(Debug, Clone, PartialEq, Eq)] -enum RustLawItem { - ScalarValidator(RustScalarValidator), - VariantValidator(RustVariantValidator), -} - #[derive(Debug, Clone, PartialEq, Eq)] struct RustTypeAlias { name: String, @@ -184,31 +132,6 @@ struct RustVariant { payload: Option, } -#[derive(Debug, Clone, PartialEq, Eq)] -struct RustScalarValidator { - function_name: String, - scalar_name: String, - min_inclusive: Option, - max_inclusive: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -struct RustVariantValidator { - function_name: String, - input_type: String, - discriminator_field: String, - enum_type: String, - cases: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -struct RustVariantValidatorCase { - enum_variant: String, - case_value: String, - requires: Vec, - forbids: Vec, -} - #[derive(Debug, Clone, PartialEq, Eq)] enum RustType { String, @@ -220,91 +143,6 @@ enum RustType { Option(Box), } -fn rust_law_items_from_ir(ir: &WesleyIR, law_ir: &LawIrV1) -> Vec { - law_ir - .entries - .iter() - .filter_map(|entry| match &entry.body { - LawEntryBodyV1::ScalarSemantics(body) => { - scalar_validator_from_law(&entry.subject, body) - } - LawEntryBodyV1::VariantLaw(body) => { - variant_validator_from_law(ir, &entry.subject, body) - } - LawEntryBodyV1::FootprintLaw(_) - | LawEntryBodyV1::ChannelLaw(_) - | LawEntryBodyV1::InvariantLaw(_) => None, - }) - .collect() -} - -fn scalar_validator_from_law( - subject: &str, - body: &wesley_core::ScalarSemanticsLawV1, -) -> Option { - if body.representation != ScalarRepresentationV1::Integer { - return None; - } - let scalar_name = subject.strip_prefix("scalar:")?; - - Some(RustLawItem::ScalarValidator(RustScalarValidator { - function_name: format!("validate_{}", rust_field_name(scalar_name)), - scalar_name: rust_type_name(scalar_name), - min_inclusive: body.min_inclusive, - max_inclusive: body.max_inclusive, - })) -} - -fn variant_validator_from_law( - ir: &WesleyIR, - subject: &str, - body: &wesley_core::VariantLawV1, -) -> Option { - let input_name = subject.strip_prefix("input:")?; - let input = ir.types.iter().find(|definition| { - definition.kind == TypeKind::InputObject && definition.name == input_name - })?; - let discriminator = input - .fields - .iter() - .find(|field| field.name == body.discriminator.field)?; - let cases = body - .cases - .iter() - .map(|case| RustVariantValidatorCase { - enum_variant: rust_variant_name(&case.value), - case_value: case.value.clone(), - requires: dynamic_variant_fields(input, &case.requires), - forbids: dynamic_variant_fields(input, &case.forbids), - }) - .collect(); - - Some(RustLawItem::VariantValidator(RustVariantValidator { - function_name: format!("validate_{}_variant", rust_field_name(input_name)), - input_type: rust_type_name(input_name), - discriminator_field: rust_field_name(&discriminator.name), - enum_type: rust_type_name(&body.discriminator.r#enum), - cases, - })) -} - -fn dynamic_variant_fields(input: &TypeDefinition, fields: &[String]) -> Vec { - fields - .iter() - .filter_map(|field_name| { - let field = input - .fields - .iter() - .find(|field| field.name == *field_name)?; - if matches!(rust_type_from_reference(&field.r#type), RustType::Option(_)) { - Some(rust_field_name(field_name)) - } else { - None - } - }) - .collect() -} - fn struct_from_type(type_def: &TypeDefinition) -> RustStruct { RustStruct { name: rust_type_name(&type_def.name), @@ -435,28 +273,11 @@ fn rust_type_from_reference(type_ref: &TypeReference) -> RustType { fn print_file(file: &RustFile) -> String { let mut out = String::from("// @generated by Wesley. Do not edit.\n"); - if let Some(provenance) = &file.provenance { - out.push('\n'); - write!(out, "pub const WESLEY_SCHEMA_HASH: &'static str = ") - .expect("writing to string should not fail"); - print_rust_string_literal(&mut out, &provenance.schema_hash); - out.push_str(";\n"); - write!(out, "pub const WESLAW_HASH: &'static str = ") - .expect("writing to string should not fail"); - print_rust_string_literal(&mut out, &provenance.law_hash); - out.push_str(";\n"); - } - for item in &file.items { out.push('\n'); print_item(&mut out, item); } - for item in &file.law_items { - out.push('\n'); - print_law_item(&mut out, item); - } - out } @@ -469,79 +290,6 @@ fn print_item(out: &mut String, item: &RustItem) { } } -fn print_law_item(out: &mut String, item: &RustLawItem) { - match item { - RustLawItem::ScalarValidator(validator) => print_scalar_validator(out, validator), - RustLawItem::VariantValidator(validator) => print_variant_validator(out, validator), - } -} - -fn print_scalar_validator(out: &mut String, validator: &RustScalarValidator) { - writeln!( - out, - "pub fn {}(value: u64) -> Result {{", - validator.function_name - ) - .expect("writing to string should not fail"); - if let Some(min) = validator.min_inclusive { - writeln!( - out, - " if value < {min} {{ return Err(\"{} below minInclusive {min}\"); }}", - validator.scalar_name - ) - .expect("writing to string should not fail"); - } - if let Some(max) = validator.max_inclusive { - writeln!( - out, - " if value > {max} {{ return Err(\"{} above maxInclusive {max}\"); }}", - validator.scalar_name - ) - .expect("writing to string should not fail"); - } - out.push_str(" Ok(value)\n"); - out.push_str("}\n"); -} - -fn print_variant_validator(out: &mut String, validator: &RustVariantValidator) { - writeln!( - out, - "pub fn {}(value: &{}) -> Result<(), &'static str> {{", - validator.function_name, validator.input_type - ) - .expect("writing to string should not fail"); - writeln!( - out, - " match value.{}.clone() {{", - validator.discriminator_field - ) - .expect("writing to string should not fail"); - for case in &validator.cases { - writeln!( - out, - " {}::{} => {{", - validator.enum_type, case.enum_variant - ) - .expect("writing to string should not fail"); - for field in &case.requires { - write!(out, " if value.{field}.is_none() {{ return Err(") - .expect("writing to string should not fail"); - print_rust_string_literal(out, &format!("{} requires {field}", case.case_value)); - out.push_str("); }\n"); - } - for field in &case.forbids { - write!(out, " if value.{field}.is_some() {{ return Err(") - .expect("writing to string should not fail"); - print_rust_string_literal(out, &format!("{} forbids {field}", case.case_value)); - out.push_str("); }\n"); - } - out.push_str(" }\n"); - } - out.push_str(" }\n"); - out.push_str(" Ok(())\n"); - out.push_str("}\n"); -} - fn print_type_alias(out: &mut String, alias: &RustTypeAlias) { write!(out, "pub type {} = ", alias.name).expect("writing to string should not fail"); print_type(out, &alias.target); @@ -878,13 +626,7 @@ fn print_rust_string_literal(out: &mut String, value: &str) { mod tests { use super::*; use pretty_assertions::assert_eq; - use std::{ - fs, - path::PathBuf, - process::Command, - time::{SystemTime, UNIX_EPOCH}, - }; - use wesley_core::{list_schema_operations_sdl, load_weslaw_yaml, lower_schema_sdl}; + use wesley_core::{list_schema_operations_sdl, lower_schema_sdl}; #[test] fn emits_rust_models_from_l1_ir() { @@ -1031,102 +773,6 @@ pub struct UserFilter { assert!(actual.contains("\\\"wes_footprint\\\"")); } - #[test] - fn emits_law_backed_scalar_and_variant_validators() { - let sdl = include_str!("../../../test/fixtures/weslaw/contract-bundle-shape.graphql"); - let law_ir = load_weslaw_yaml(include_str!( - "../../../test/fixtures/weslaw/accepted/rust-validator-payoff.weslaw.yaml" - )) - .expect("law fixture should lower"); - let ir = lower_schema_sdl(sdl).expect("schema should lower"); - let operations = list_schema_operations_sdl(sdl).expect("operations should resolve"); - - let actual = emit_rust_with_operations_and_law( - &ir, - &operations, - "sha256:schema", - "sha256:law", - &law_ir, - ); - - syn::parse_file(&actual).expect("generated Rust should parse"); - assert!(actual.contains("pub fn validate_positive_int(value: u64)")); - assert!(actual.contains("if value < 1")); - assert!(actual.contains("if value > 4294967295")); - assert!(actual - .contains("pub fn validate_playback_mode_input_variant(value: &PlaybackModeInput)")); - assert!(actual.contains("PlaybackModeKind::Seek => {")); - assert!(actual.contains("if value.target.is_none()")); - assert!(actual.contains("if value.then.is_none()")); - assert!(actual.contains("PlaybackModeKind::Paused => {")); - assert!(actual.contains("if value.target.is_some()")); - } - - #[test] - fn escapes_law_variant_case_values_in_validator_messages() { - let actual = hostile_variant_validator_source(); - - syn::parse_file(&actual).expect("generated Rust validator should parse"); - assert!(actual.contains( - "return Err(\"CASE \\\"quote\\\" \\\\slash\\nline\\rreturn\\t tab requires target\");" - )); - assert!(actual.contains( - "return Err(\"CASE \\\"quote\\\" \\\\slash\\nline\\rreturn\\t tab forbids then\");" - )); - } - - #[test] - fn escaped_law_variant_validator_compiles() { - let source = format!( - r#" -#[derive(Clone)] -enum PlaybackModeKind {{ - Seek, -}} - -struct PlaybackModeInput {{ - kind: PlaybackModeKind, - target: Option, - then: Option, -}} - -{validator} -"#, - validator = hostile_variant_validator_source() - ); - - compile_rust_source("escaped_law_variant_validator", &source); - } - - #[test] - fn law_backed_generated_rust_compiles_as_crate() { - let sdl = include_str!("../../../test/fixtures/weslaw/contract-bundle-shape.graphql"); - let law_ir = load_weslaw_yaml(include_str!( - "../../../test/fixtures/weslaw/accepted/rust-validator-payoff.weslaw.yaml" - )) - .expect("law fixture should lower"); - let ir = lower_schema_sdl(sdl).expect("fixture should lower"); - let operations = list_schema_operations_sdl(sdl).expect("operations should list"); - let actual = - emit_rust_with_operations_and_law(&ir, &operations, "schema-hash", "law-hash", &law_ir); - - cargo_check_generated_rust_crate("law_backed_generated_rust", &actual); - } - - #[test] - fn variant_validator_printer_does_not_directly_interpolate_case_values() { - let source = include_str!("lib.rs"); - for pattern in [ - concat!("Err(", "\\", "\"", "{} requires"), - concat!("Err(", "\\", "\"", "{} forbids"), - ] { - assert!( - !source.contains(pattern), - "variant validator printer must route case values through the Rust string literal printer" - ); - } - } - #[test] fn rust_string_literal_escapes_remaining_control_characters() { let mut actual = String::new(); @@ -1284,97 +930,4 @@ struct PlaybackModeInput {{ + "\n}\n".len(); &tail[..end] } - - fn hostile_variant_validator_source() -> String { - let validator = RustVariantValidator { - function_name: "validate_playback_mode_input_variant".to_string(), - input_type: "PlaybackModeInput".to_string(), - discriminator_field: "kind".to_string(), - enum_type: "PlaybackModeKind".to_string(), - cases: vec![RustVariantValidatorCase { - enum_variant: "Seek".to_string(), - case_value: "CASE \"quote\" \\slash\nline\rreturn\t tab".to_string(), - requires: vec!["target".to_string()], - forbids: vec!["then".to_string()], - }], - }; - let mut actual = String::new(); - print_variant_validator(&mut actual, &validator); - actual - } - - fn compile_rust_source(test_name: &str, source: &str) { - let dir = temporary_compile_dir(test_name); - fs::create_dir_all(&dir).expect("temporary compile directory should create"); - let source_path = dir.join("lib.rs"); - fs::write(&source_path, source).expect("temporary Rust source should write"); - - let output = Command::new("rustc") - .arg("--edition=2021") - .arg("--crate-type=lib") - .arg(&source_path) - .arg("--out-dir") - .arg(&dir) - .output() - .expect("rustc should run"); - - let _ = fs::remove_dir_all(&dir); - - assert!( - output.status.success(), - "generated Rust should compile\nstdout:\n{}\nstderr:\n{}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - ); - } - - fn cargo_check_generated_rust_crate(test_name: &str, source: &str) { - let dir = temporary_compile_dir(test_name); - let src_dir = dir.join("src"); - fs::create_dir_all(&src_dir).expect("temporary crate source directory should create"); - fs::write( - dir.join("Cargo.toml"), - r#"[package] -name = "wesley-generated-rust-check" -version = "0.0.0" -edition = "2021" -publish = false - -[dependencies] -serde = { version = "1", features = ["derive"] } -"#, - ) - .expect("temporary crate manifest should write"); - fs::write(src_dir.join("lib.rs"), source).expect("temporary generated Rust should write"); - - let cargo = std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into()); - let output = Command::new(cargo) - .arg("check") - .arg("--quiet") - .arg("--manifest-path") - .arg(dir.join("Cargo.toml")) - .env("CARGO_TARGET_DIR", dir.join("target")) - .output() - .expect("cargo check should run"); - - let _ = fs::remove_dir_all(&dir); - - assert!( - output.status.success(), - "generated Rust crate should pass cargo check\nstdout:\n{}\nstderr:\n{}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - ); - } - - fn temporary_compile_dir(test_name: &str) -> PathBuf { - let unique = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("system clock should be after Unix epoch") - .as_nanos(); - std::env::temp_dir().join(format!( - "wesley-emit-rust-{test_name}-{}-{unique}", - std::process::id() - )) - } } diff --git a/crates/wesley-holmes/Cargo.toml b/crates/wesley-holmes/Cargo.toml deleted file mode 100644 index 96f9e8bf..00000000 --- a/crates/wesley-holmes/Cargo.toml +++ /dev/null @@ -1,14 +0,0 @@ -[package] -name = "wesley-holmes" -version = "0.3.0-alpha.1" -edition = "2021" -description = "Rust Holmes law assurance foundation for Wesley semantic evidence" -license = "MIT" -repository = "https://github.com/flyingrobots/wesley" -homepage = "https://github.com/flyingrobots/wesley" -readme = "README.md" -publish = false - -[dependencies] -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" diff --git a/crates/wesley-holmes/README.md b/crates/wesley-holmes/README.md deleted file mode 100644 index 0b9db234..00000000 --- a/crates/wesley-holmes/README.md +++ /dev/null @@ -1,48 +0,0 @@ -# wesley-holmes - -`wesley-holmes` is the Rust foundation for Holmes law assurance work inside -Wesley. It consumes Wesley-published law evidence, policy, witness, MCP, and -GitHub payload artifacts; validates their envelope shape, provenance, artifact -availability, and version posture; and prepares deterministic diagnostics and -reporting surfaces for later CLI, API, and MCP interfaces. - -This crate is intentionally not published yet. It is a workspace implementation -crate for the Holmes redesign described in the Wesley design packet: - -- [Holmes `weslaw` Assurance PRD/Test Plan](https://github.com/flyingrobots/wesley/blob/main/docs/design/0020-holmes-weslaw-assurance-prd-test-plan/holmes-weslaw-assurance-prd-test-plan.md) -- [Holmes Assurance Hexagon](https://github.com/flyingrobots/wesley/blob/main/docs/design/0018-holmes-assurance-hexagon/holmes-assurance-hexagon.md) - -## Boundary - -The crate follows the planned hexagonal boundary: - -- `domain`: pure law-assurance data, diagnostics, evidence models, and version - rules. Domain code must not import filesystem, network, process, GitHub, MCP, - or wall-clock dependencies. -- `application`: deterministic orchestration utilities that bind domain facts - to ports without owning external side effects. -- `ports`: abstract clock, artifact, policy, reporting, GitHub, MCP, and command - I/O traits plus deterministic fakes for tests. -- `adapters`: future concrete integrations for filesystem, GitHub, MCP, and CLI - surfaces. -- `reporting`: future renderer-facing DTOs and report assembly helpers. - -The current implementation includes the first local law evidence validation -gate, `wesley.law-diff/v1` ingest with stable normalized event records, -`wesley.law-coverage/v1` ingest with normalized profile/category counts and -omitted missing-subject accounting, report-only `wesley.law-capabilities/v1` -ingest, contract bundle manifest ingest with evidence-bundle provenance -cross-checks, semantic change findings with stable ids, and profile/category -law coverage gate decisions. It also includes the first domain substrate for -bundle traceability decisions, provenance reporting, aggregate law assurance -assessment outcomes, bounded finding summaries, typed -`holmes.law-assurance-policy/v1` normalization, severity mappings, materialized -coverage threshold policy, and narrow suppression records. No public Holmes CLI -command is exposed from Wesley yet. - -## Law Capability Ingest - -Holmes accepts `wesley.law-capabilities/v1` as the only law capability artifact -API version. Pre-canonical capability-report aliases are rejected before -assessment so fixtures and future public surfaces cannot depend on a retired -artifact name. diff --git a/crates/wesley-holmes/src/adapters/mod.rs b/crates/wesley-holmes/src/adapters/mod.rs deleted file mode 100644 index bd3d2c70..00000000 --- a/crates/wesley-holmes/src/adapters/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -//! Concrete adapter boundary for future filesystem, GitHub, MCP, and CLI integrations. -//! -//! The first Holmes implementation slice deliberately leaves this namespace -//! empty. Concrete adapters will land only after the domain and port contracts -//! have proven stable. diff --git a/crates/wesley-holmes/src/application/artifact_locator.rs b/crates/wesley-holmes/src/application/artifact_locator.rs deleted file mode 100644 index 039f7f52..00000000 --- a/crates/wesley-holmes/src/application/artifact_locator.rs +++ /dev/null @@ -1,108 +0,0 @@ -//! Workspace-relative artifact path resolution. - -use std::path::{Component, Path}; - -use crate::domain::{HolmesDiagnostic, HolmesDiagnosticCode, HolmesResult, HolmesSeverity}; - -/// A normalized artifact path that stays inside the configured workspace root. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ResolvedArtifactPath { - /// Workspace-relative path using `/` separators. - pub workspace_relative: String, -} - -/// Resolves `weslaw` artifact references without touching the filesystem. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct WeslawArtifactLocator { - workspace_root: String, -} - -impl WeslawArtifactLocator { - /// Create a locator for a workspace root label. - pub fn new(workspace_root: impl Into) -> Self { - Self { - workspace_root: workspace_root.into(), - } - } - - /// Return the configured workspace root label. - pub fn workspace_root(&self) -> &str { - &self.workspace_root - } - - /// Normalize a user-authored artifact path relative to the workspace. - /// - /// The resolver is lexical by design. It rejects absolute paths, Windows - /// prefixes, empty paths, and `..` components that would escape the - /// workspace root. It does not perform symlink or filesystem - /// canonicalization. - pub fn resolve(&self, path: &str) -> HolmesResult { - if path.trim().is_empty() { - return Err(invalid_path("artifact path must not be empty")); - } - - if path.contains('\\') { - return Err(path_escape("artifact path must use `/` separators")); - } - - if looks_like_windows_drive_path(path) { - return Err(path_escape("artifact path must be workspace-relative")); - } - - let path = Path::new(path); - if path.is_absolute() { - return Err(path_escape("artifact path must be workspace-relative")); - } - - let mut normalized = Vec::new(); - for component in path.components() { - match component { - Component::CurDir => {} - Component::Normal(segment) => { - normalized.push(segment.to_string_lossy().into_owned()) - } - Component::ParentDir => { - if normalized.pop().is_none() { - return Err(path_escape( - "artifact path must not escape the workspace root", - )); - } - } - Component::Prefix(_) | Component::RootDir => { - return Err(path_escape("artifact path must be workspace-relative")); - } - } - } - - if normalized.is_empty() { - return Err(invalid_path("artifact path must reference a file")); - } - - Ok(ResolvedArtifactPath { - workspace_relative: normalized.join("/"), - }) - } -} - -fn invalid_path(message: impl Into) -> HolmesDiagnostic { - HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawArtifactPathInvalid, - HolmesSeverity::Error, - message, - ) - .at_field("path") -} - -fn path_escape(message: impl Into) -> HolmesDiagnostic { - HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawArtifactPathEscape, - HolmesSeverity::Error, - message, - ) - .at_field("path") -} - -fn looks_like_windows_drive_path(path: &str) -> bool { - let bytes = path.as_bytes(); - bytes.len() >= 2 && bytes[0].is_ascii_alphabetic() && bytes[1] == b':' -} diff --git a/crates/wesley-holmes/src/application/contract_manifest_ingest.rs b/crates/wesley-holmes/src/application/contract_manifest_ingest.rs deleted file mode 100644 index a876bca6..00000000 --- a/crates/wesley-holmes/src/application/contract_manifest_ingest.rs +++ /dev/null @@ -1,374 +0,0 @@ -//! JSON ingest boundary for Wesley contract bundle manifest artifacts. - -use serde::Deserialize; - -use crate::domain::{ - BundleProvenance, ContractBundleManifest, HolmesDiagnostic, HolmesDiagnosticCode, - HolmesSeverity, WESLEY_CONTRACT_BUNDLE_HASH_INPUT_CODEC, - WESLEY_CONTRACT_BUNDLE_MANIFEST_API_VERSION, WESLEY_LAW_IR_CANONICAL_JSON_CODEC, -}; - -/// Validation status for contract bundle manifest ingest. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ContractBundleManifestIngestStatus { - /// Contract bundle manifest JSON was accepted and normalized into a typed report. - Valid, - /// Contract bundle manifest JSON was rejected before Holmes assessment. - Invalid, -} - -/// Result of ingesting a Wesley contract bundle manifest artifact. -#[derive(Debug, Clone, PartialEq)] -pub struct ContractBundleManifestIngestResult { - /// Ingest status. - pub status: ContractBundleManifestIngestStatus, - /// Deterministically ordered ingest diagnostics. - pub diagnostics: Vec, - /// Parsed contract bundle manifest when ingest succeeded. - pub manifest: Option, -} - -impl ContractBundleManifestIngestResult { - fn valid(manifest: ContractBundleManifest) -> Self { - Self { - status: ContractBundleManifestIngestStatus::Valid, - diagnostics: Vec::new(), - manifest: Some(manifest), - } - } - - fn invalid(diagnostics: Vec) -> Self { - Self { - status: ContractBundleManifestIngestStatus::Invalid, - diagnostics, - manifest: None, - } - } -} - -/// Input port for `wesley.contract-bundle-manifest/v1` JSON artifacts. -pub trait ContractBundleManifestIngestPort { - /// Ingest raw manifest bytes and optionally cross-check evidence-bundle provenance. - fn ingest_contract_bundle_manifest( - &self, - bytes: &[u8], - bundle_provenance: Option<&BundleProvenance>, - ) -> ContractBundleManifestIngestResult; -} - -/// JSON implementation of the contract bundle manifest ingest port. -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] -pub struct JsonContractBundleManifestIngestPort; - -impl ContractBundleManifestIngestPort for JsonContractBundleManifestIngestPort { - fn ingest_contract_bundle_manifest( - &self, - bytes: &[u8], - bundle_provenance: Option<&BundleProvenance>, - ) -> ContractBundleManifestIngestResult { - let raw = match serde_json::from_slice::(bytes) { - Ok(raw) => raw, - Err(err) => { - return ContractBundleManifestIngestResult::invalid(vec![ - HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawManifestMalformedJson, - HolmesSeverity::Error, - format!( - "contract bundle manifest is not valid wesley.contract-bundle-manifest/v1 JSON: {err}" - ), - ) - .for_family("contract-bundle-manifest"), - ]); - } - }; - - let mut diagnostics = Vec::new(); - validate_api_version(&raw, &mut diagnostics); - validate_hash_field("schemaHash", raw.schema_hash.as_deref(), &mut diagnostics); - validate_hash_field("lawHash", raw.law_hash.as_deref(), &mut diagnostics); - validate_optional_hash_field( - "lawDocumentHash", - raw.law_document_hash.as_deref(), - &mut diagnostics, - ); - validate_hash_field("profileHash", raw.profile_hash.as_deref(), &mut diagnostics); - validate_hash_field("bundleHash", raw.bundle_hash.as_deref(), &mut diagnostics); - validate_required_text("compiler", raw.compiler.as_deref(), &mut diagnostics); - validate_required_text( - "compilerVersion", - raw.compiler_version.as_deref(), - &mut diagnostics, - ); - validate_codec( - "lawIrCodec", - raw.law_ir_codec.as_deref(), - WESLEY_LAW_IR_CANONICAL_JSON_CODEC, - &mut diagnostics, - ); - validate_codec( - "bundleHashCodec", - raw.bundle_hash_codec.as_deref(), - WESLEY_CONTRACT_BUNDLE_HASH_INPUT_CODEC, - &mut diagnostics, - ); - if raw.law_entry_count.is_none() { - diagnostics.push( - HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawManifestMissingRequiredField, - HolmesSeverity::Error, - "contract bundle manifest is missing lawEntryCount", - ) - .for_family("contract-bundle-manifest") - .at_field("lawEntryCount"), - ); - } - - if let Some(provenance) = bundle_provenance { - cross_check_bundle_provenance(&raw, provenance, &mut diagnostics); - } - - if !diagnostics.is_empty() { - return ContractBundleManifestIngestResult::invalid(diagnostics); - } - - ContractBundleManifestIngestResult::valid(ContractBundleManifest { - api_version: raw.api_version.expect("apiVersion was validated"), - schema_hash: raw.schema_hash.expect("schemaHash was validated"), - law_hash: raw.law_hash.expect("lawHash was validated"), - law_document_hash: raw.law_document_hash, - profile_hash: raw.profile_hash.expect("profileHash was validated"), - bundle_hash: raw.bundle_hash.expect("bundleHash was validated"), - law_ir_codec: raw.law_ir_codec.expect("lawIrCodec was validated"), - bundle_hash_codec: raw - .bundle_hash_codec - .expect("bundleHashCodec was validated"), - compiler: raw.compiler.expect("compiler was validated"), - compiler_version: raw.compiler_version.expect("compilerVersion was validated"), - law_entry_count: raw.law_entry_count.expect("lawEntryCount was validated"), - }) - } -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -struct RawContractBundleManifest { - #[serde(default)] - api_version: Option, - #[serde(default)] - schema_hash: Option, - #[serde(default)] - law_hash: Option, - #[serde(default)] - law_document_hash: Option, - #[serde(default)] - profile_hash: Option, - #[serde(default)] - bundle_hash: Option, - #[serde(default)] - law_ir_codec: Option, - #[serde(default)] - bundle_hash_codec: Option, - #[serde(default)] - compiler: Option, - #[serde(default)] - compiler_version: Option, - #[serde(default)] - law_entry_count: Option, -} - -fn validate_api_version(raw: &RawContractBundleManifest, diagnostics: &mut Vec) { - match raw.api_version.as_deref() { - Some(WESLEY_CONTRACT_BUNDLE_MANIFEST_API_VERSION) => {} - Some(version) => diagnostics.push( - HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawManifestUnsupportedVersion, - HolmesSeverity::Error, - format!( - "unsupported contract bundle manifest apiVersion {version}; expected {WESLEY_CONTRACT_BUNDLE_MANIFEST_API_VERSION}" - ), - ) - .for_family("contract-bundle-manifest") - .at_field("apiVersion"), - ), - None => diagnostics.push( - HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawManifestMissingRequiredField, - HolmesSeverity::Error, - "contract bundle manifest is missing apiVersion", - ) - .for_family("contract-bundle-manifest") - .at_field("apiVersion"), - ), - } -} - -fn validate_hash_field( - field_path: &'static str, - value: Option<&str>, - diagnostics: &mut Vec, -) { - match value { - Some(value) if is_canonical_sha256(value) => {} - Some(value) if value.trim().is_empty() => diagnostics.push(missing_hash(field_path)), - Some(_) => diagnostics.push(invalid_hash(field_path)), - None => diagnostics.push(missing_hash(field_path)), - } -} - -fn validate_optional_hash_field( - field_path: &'static str, - value: Option<&str>, - diagnostics: &mut Vec, -) { - if let Some(value) = value { - if !is_canonical_sha256(value) { - diagnostics.push(invalid_hash(field_path)); - } - } -} - -fn validate_required_text( - field_path: &'static str, - value: Option<&str>, - diagnostics: &mut Vec, -) { - match value { - Some(value) if !value.trim().is_empty() => {} - _ => diagnostics.push( - HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawManifestMissingRequiredField, - HolmesSeverity::Error, - "contract bundle manifest is missing required provenance metadata", - ) - .for_family("contract-bundle-manifest") - .at_field(field_path), - ), - } -} - -fn validate_codec( - field_path: &'static str, - value: Option<&str>, - expected: &'static str, - diagnostics: &mut Vec, -) { - match value { - Some(value) if value == expected => {} - Some(value) if value.trim().is_empty() => diagnostics.push( - HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawManifestMissingRequiredField, - HolmesSeverity::Error, - "contract bundle manifest is missing required codec metadata", - ) - .for_family("contract-bundle-manifest") - .at_field(field_path), - ), - Some(value) => diagnostics.push( - HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawManifestUnsupportedCodec, - HolmesSeverity::Error, - format!("unsupported contract bundle manifest codec {value}; expected {expected}"), - ) - .for_family("contract-bundle-manifest") - .at_field(field_path), - ), - None => diagnostics.push( - HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawManifestMissingRequiredField, - HolmesSeverity::Error, - "contract bundle manifest is missing required codec metadata", - ) - .for_family("contract-bundle-manifest") - .at_field(field_path), - ), - } -} - -fn cross_check_bundle_provenance( - raw: &RawContractBundleManifest, - provenance: &BundleProvenance, - diagnostics: &mut Vec, -) { - compare_hash( - "schemaHash", - raw.schema_hash.as_deref(), - &provenance.schema_hash, - diagnostics, - ); - compare_hash( - "lawHash", - raw.law_hash.as_deref(), - &provenance.law_hash, - diagnostics, - ); - if let Some(policy_hash) = provenance.policy_hash.as_deref() { - compare_hash( - "profileHash", - raw.profile_hash.as_deref(), - policy_hash, - diagnostics, - ); - } - compare_hash( - "bundleHash", - raw.bundle_hash.as_deref(), - &provenance.bundle_hash, - diagnostics, - ); -} - -fn compare_hash( - field_path: &'static str, - manifest_hash: Option<&str>, - expected_hash: &str, - diagnostics: &mut Vec, -) { - if let Some(manifest_hash) = manifest_hash { - if !is_canonical_sha256(manifest_hash) { - return; - } - if manifest_hash != expected_hash { - diagnostics.push( - HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawManifestHashMismatch, - HolmesSeverity::Error, - format!( - "contract bundle manifest {field_path} does not match evidence bundle provenance" - ), - ) - .for_family("contract-bundle-manifest") - .at_field(field_path), - ); - } - } -} - -fn missing_hash(field_path: &'static str) -> HolmesDiagnostic { - HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawManifestMissingRequiredHash, - HolmesSeverity::Error, - "contract bundle manifest is missing a required hash", - ) - .for_family("contract-bundle-manifest") - .at_field(field_path) -} - -fn invalid_hash(field_path: &'static str) -> HolmesDiagnostic { - HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawManifestInvalidHash, - HolmesSeverity::Error, - "contract bundle manifest hash must use sha256:<64 lowercase hex>", - ) - .for_family("contract-bundle-manifest") - .at_field(field_path) -} - -fn is_canonical_sha256(value: &str) -> bool { - let Some(hex) = value.strip_prefix("sha256:") else { - return false; - }; - hex.len() == 64 - && hex - .bytes() - .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) -} diff --git a/crates/wesley-holmes/src/application/evidence_validation.rs b/crates/wesley-holmes/src/application/evidence_validation.rs deleted file mode 100644 index ca219f72..00000000 --- a/crates/wesley-holmes/src/application/evidence_validation.rs +++ /dev/null @@ -1,122 +0,0 @@ -//! Evidence-bundle validation application service. - -use std::collections::BTreeMap; - -use crate::application::WeslawArtifactLocator; -use crate::domain::{ - ArtifactRef, HolmesDiagnostic, HolmesDiagnosticCode, HolmesLawEvidenceBundle, HolmesSeverity, - LawEvidenceValidationResult, LoadedArtifactMetadata, VersionRegistry, -}; -use crate::ports::ArtifactLoadPort; - -/// Validates Holmes law evidence bundles without performing assurance judgment. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct LawEvidenceValidator { - locator: WeslawArtifactLocator, - max_bytes: usize, -} - -impl LawEvidenceValidator { - /// Create a validator with a workspace artifact locator. - pub fn new(locator: WeslawArtifactLocator) -> Self { - Self { - locator, - max_bytes: 8 * 1024 * 1024, - } - } - - /// Return a validator with a deterministic artifact byte limit. - pub fn with_max_bytes(mut self, max_bytes: usize) -> Self { - self.max_bytes = max_bytes; - self - } - - /// Validate structure, provenance, and referenced artifact availability. - pub fn validate( - &self, - bundle: &HolmesLawEvidenceBundle, - artifact_loader: &impl ArtifactLoadPort, - version_registry: &VersionRegistry, - ) -> LawEvidenceValidationResult { - let structural = bundle.validate_structure(version_registry); - if structural - .diagnostics - .iter() - .any(|diagnostic| diagnostic.severity == HolmesSeverity::Error) - { - return structural; - } - - let mut diagnostics = structural.diagnostics; - let mut loaded_artifacts = Vec::new(); - let mut normalized_paths = BTreeMap::new(); - - for bundle_artifact in bundle.artifact_refs() { - let artifact = bundle_artifact.artifact; - let resolved = match self.locator.resolve(&artifact.path) { - Ok(resolved) => resolved, - Err(diagnostic) => { - diagnostics.push(diagnostic.at_field(bundle_artifact.field_path)); - continue; - } - }; - - if let Some(first_field_path) = normalized_paths.insert( - resolved.workspace_relative.clone(), - bundle_artifact.field_path, - ) { - diagnostics.push( - HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawEvidenceBundleInvalid, - HolmesSeverity::Error, - format!( - "artifact path normalizes to the same path as {first_field_path}; each artifact role must point at distinct evidence" - ), - ) - .for_family(bundle_artifact.family.id()) - .at_field(bundle_artifact.field_path), - ); - continue; - } - - let normalized = ArtifactRef { - path: resolved.workspace_relative.clone(), - schema_version: artifact.schema_version.clone(), - sha256: artifact.sha256.clone(), - }; - - match artifact_loader.read_artifact(&normalized) { - Ok(bytes) if bytes.len() <= self.max_bytes => { - loaded_artifacts.push(LoadedArtifactMetadata { - field_path: bundle_artifact.field_path.to_owned(), - artifact_family: bundle_artifact.family.id().to_owned(), - path: normalized.path, - byte_len: bytes.len(), - }); - } - Ok(bytes) => diagnostics.push( - HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawArtifactOversized, - HolmesSeverity::Error, - format!( - "artifact {:?} is {} bytes, above the configured {} byte limit", - normalized.path, - bytes.len(), - self.max_bytes - ), - ) - .for_family(bundle_artifact.family.id()) - .at_field(bundle_artifact.field_path), - ), - Err(diagnostic) => diagnostics.push( - diagnostic - .for_family(bundle_artifact.family.id()) - .at_field(bundle_artifact.field_path), - ), - } - } - - LawEvidenceValidationResult::from_diagnostics(diagnostics) - .with_loaded_artifacts(loaded_artifacts) - } -} diff --git a/crates/wesley-holmes/src/application/law_capability_ingest.rs b/crates/wesley-holmes/src/application/law_capability_ingest.rs deleted file mode 100644 index f2d41fc5..00000000 --- a/crates/wesley-holmes/src/application/law_capability_ingest.rs +++ /dev/null @@ -1,313 +0,0 @@ -//! JSON ingest boundary for Wesley law capability artifacts. - -use std::collections::BTreeSet; - -use serde::Deserialize; - -use crate::domain::{ - HolmesDiagnostic, HolmesDiagnosticCode, HolmesSeverity, LawCapabilityClosure, - LawCapabilityFootprint, LawCapabilityReport, LawCapabilitySlot, - WESLEY_LAW_CAPABILITIES_API_VERSION, -}; - -/// Validation status for law capability ingest. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum LawCapabilityIngestStatus { - /// Law capability JSON was accepted and normalized into a typed report. - Valid, - /// Law capability JSON was rejected before Holmes assessment. - Invalid, -} - -/// Result of ingesting a Wesley law capability artifact. -#[derive(Debug, Clone, PartialEq)] -pub struct LawCapabilityIngestResult { - /// Ingest status. - pub status: LawCapabilityIngestStatus, - /// Deterministically ordered ingest diagnostics. - pub diagnostics: Vec, - /// Parsed law capability report when ingest succeeded. - pub report: Option, -} - -impl LawCapabilityIngestResult { - fn valid(report: LawCapabilityReport) -> Self { - Self { - status: LawCapabilityIngestStatus::Valid, - diagnostics: Vec::new(), - report: Some(report), - } - } - - fn invalid(diagnostics: Vec) -> Self { - Self { - status: LawCapabilityIngestStatus::Invalid, - diagnostics, - report: None, - } - } -} - -/// Input port for Wesley law capability JSON artifacts. -pub trait LawCapabilityIngestPort { - /// Ingest raw law capability bytes into a typed Holmes report boundary. - fn ingest_law_capabilities(&self, bytes: &[u8]) -> LawCapabilityIngestResult; -} - -/// JSON implementation of the law capability ingest port. -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] -pub struct JsonLawCapabilityIngestPort; - -impl LawCapabilityIngestPort for JsonLawCapabilityIngestPort { - fn ingest_law_capabilities(&self, bytes: &[u8]) -> LawCapabilityIngestResult { - let raw = match serde_json::from_slice::(bytes) { - Ok(raw) => raw, - Err(err) => { - return LawCapabilityIngestResult::invalid(vec![HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawCapabilityMalformedJson, - HolmesSeverity::Error, - format!("law capability artifact is not valid JSON: {err}"), - ) - .for_family("law-capabilities")]); - } - }; - - let mut diagnostics = Vec::new(); - if raw.api_version != WESLEY_LAW_CAPABILITIES_API_VERSION { - diagnostics.push( - HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawCapabilityUnsupportedVersion, - HolmesSeverity::Error, - format!( - "unsupported law capability apiVersion {}; expected {}", - raw.api_version, WESLEY_LAW_CAPABILITIES_API_VERSION - ), - ) - .for_family("law-capabilities") - .at_field("apiVersion"), - ); - } - - let Some(report_only) = raw.report_only else { - diagnostics.push( - HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawCapabilityMissingPosture, - HolmesSeverity::Error, - "law capability artifact must explicitly set reportOnly", - ) - .for_family("law-capabilities") - .at_field("reportOnly"), - ); - return LawCapabilityIngestResult::invalid(diagnostics); - }; - let Some(runtime_enforcement) = raw.runtime_enforcement else { - diagnostics.push( - HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawCapabilityMissingPosture, - HolmesSeverity::Error, - "law capability artifact must explicitly set runtimeEnforcement", - ) - .for_family("law-capabilities") - .at_field("runtimeEnforcement"), - ); - return LawCapabilityIngestResult::invalid(diagnostics); - }; - - if report_only && runtime_enforcement { - diagnostics.push( - HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawCapabilityMissingPosture, - HolmesSeverity::Error, - "law capability artifact cannot claim reportOnly and runtimeEnforcement without separate enforcement evidence", - ) - .for_family("law-capabilities") - .at_field("runtimeEnforcement"), - ); - } - - let footprints = raw - .footprints - .into_iter() - .enumerate() - .map(|(index, footprint)| { - validate_footprint(index, &footprint, &mut diagnostics); - LawCapabilityFootprint { - law_id: footprint.law_id, - subject: footprint.subject, - reads: footprint.reads, - writes: footprint.writes, - creates: footprint.creates, - forbids: footprint.forbids, - slots: footprint.slots, - closures: footprint.closures, - intentionally_empty: footprint.intentionally_empty, - } - }) - .collect::>(); - - if diagnostics.is_empty() { - LawCapabilityIngestResult::valid(LawCapabilityReport { - api_version: WESLEY_LAW_CAPABILITIES_API_VERSION.to_owned(), - report_only, - runtime_enforcement, - note: raw.note.unwrap_or_default(), - footprints, - }) - } else { - LawCapabilityIngestResult::invalid(diagnostics) - } - } -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -struct RawLawCapabilityReport { - api_version: String, - #[serde(default)] - report_only: Option, - #[serde(default)] - runtime_enforcement: Option, - #[serde(default)] - note: Option, - #[serde(default)] - footprints: Vec, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -struct RawLawCapabilityFootprint { - law_id: String, - subject: String, - #[serde(default)] - reads: Vec, - #[serde(default)] - writes: Vec, - #[serde(default)] - creates: Vec, - #[serde(default)] - forbids: Vec, - #[serde(default)] - slots: Vec, - #[serde(default)] - closures: Vec, - #[serde(default)] - intentionally_empty: bool, -} - -fn validate_footprint( - index: usize, - footprint: &RawLawCapabilityFootprint, - diagnostics: &mut Vec, -) { - if footprint.law_id.trim().is_empty() { - diagnostics.push( - HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawCapabilityMalformedJson, - HolmesSeverity::Error, - "law capability footprint lawId must not be blank", - ) - .for_family("law-capabilities") - .at_field(format!("footprints[{index}].lawId")), - ); - } - if footprint.subject.trim().is_empty() { - diagnostics.push( - HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawCapabilityMalformedJson, - HolmesSeverity::Error, - "law capability footprint subject must not be blank", - ) - .for_family("law-capabilities") - .at_field(format!("footprints[{index}].subject")), - ); - } - - let forbids = footprint - .forbids - .iter() - .map(String::as_str) - .collect::>(); - for (field, resources) in [ - ("reads", &footprint.reads), - ("writes", &footprint.writes), - ("creates", &footprint.creates), - ] { - reject_forbidden_resource_overlaps( - field, - resources, - &forbids, - diagnostics, - format!("footprints[{index}].{field}"), - ); - } - - for (slot_index, slot) in footprint.slots.iter().enumerate() { - if forbids.contains(slot.kind.as_str()) { - diagnostics.push(contradictory_resource_diagnostic( - "slot kind", - &slot.kind, - format!("footprints[{index}].slots[{slot_index}].kind"), - )); - } - } - - for (closure_index, closure) in footprint.closures.iter().enumerate() { - reject_forbidden_resource_overlaps( - "closure reads", - &closure.reads, - &forbids, - diagnostics, - format!("footprints[{index}].closures[{closure_index}].reads"), - ); - } - - let has_resource_posture = !footprint.reads.is_empty() - || !footprint.writes.is_empty() - || !footprint.creates.is_empty() - || !footprint.forbids.is_empty() - || !footprint.slots.is_empty() - || !footprint.closures.is_empty(); - if !has_resource_posture && !footprint.intentionally_empty { - diagnostics.push( - HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawCapabilityImplicitEmptyFootprint, - HolmesSeverity::Error, - "law capability footprint with no resources must set intentionallyEmpty", - ) - .for_family("law-capabilities") - .at_field(format!("footprints[{index}]")), - ); - } -} - -fn reject_forbidden_resource_overlaps( - field: &str, - resources: &[String], - forbids: &BTreeSet<&str>, - diagnostics: &mut Vec, - field_path: String, -) { - for resource in resources { - if forbids.contains(resource.as_str()) { - diagnostics.push(contradictory_resource_diagnostic( - field, - resource, - field_path.clone(), - )); - } - } -} - -fn contradictory_resource_diagnostic( - field: &str, - resource: &str, - field_path: String, -) -> HolmesDiagnostic { - HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawCapabilityContradictoryResourcePosture, - HolmesSeverity::Error, - format!("law capability resource {resource:?} appears in both {field} and forbids"), - ) - .for_family("law-capabilities") - .at_field(field_path) -} diff --git a/crates/wesley-holmes/src/application/law_coverage_ingest.rs b/crates/wesley-holmes/src/application/law_coverage_ingest.rs deleted file mode 100644 index b7a27caa..00000000 --- a/crates/wesley-holmes/src/application/law_coverage_ingest.rs +++ /dev/null @@ -1,190 +0,0 @@ -//! JSON ingest boundary for Wesley law coverage artifacts. - -use crate::domain::{ - percentage, HolmesDiagnostic, HolmesDiagnosticCode, HolmesSeverity, LawCoverageReport, - WESLEY_LAW_COVERAGE_API_VERSION, -}; - -/// Validation status for law coverage ingest. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum LawCoverageIngestStatus { - /// Law coverage JSON was accepted and normalized into a typed report. - Valid, - /// Law coverage JSON was rejected before Holmes assessment. - Invalid, -} - -/// Result of ingesting a Wesley law coverage artifact. -#[derive(Debug, Clone, PartialEq)] -pub struct LawCoverageIngestResult { - /// Ingest status. - pub status: LawCoverageIngestStatus, - /// Deterministically ordered ingest diagnostics. - pub diagnostics: Vec, - /// Parsed law coverage report when ingest succeeded. - pub report: Option, -} - -impl LawCoverageIngestResult { - fn valid(report: LawCoverageReport) -> Self { - Self { - status: LawCoverageIngestStatus::Valid, - diagnostics: Vec::new(), - report: Some(report), - } - } - - fn invalid(diagnostics: Vec) -> Self { - Self { - status: LawCoverageIngestStatus::Invalid, - diagnostics, - report: None, - } - } -} - -/// Input port for `wesley.law-coverage/v1` JSON artifacts. -pub trait LawCoverageIngestPort { - /// Ingest raw law coverage bytes into a typed Holmes report boundary. - fn ingest_law_coverage(&self, bytes: &[u8]) -> LawCoverageIngestResult; -} - -/// JSON implementation of the law coverage ingest port. -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] -pub struct JsonLawCoverageIngestPort; - -impl LawCoverageIngestPort for JsonLawCoverageIngestPort { - fn ingest_law_coverage(&self, bytes: &[u8]) -> LawCoverageIngestResult { - let report = match serde_json::from_slice::(bytes) { - Ok(report) => report, - Err(err) => { - return LawCoverageIngestResult::invalid(vec![HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawCoverageMalformedJson, - HolmesSeverity::Error, - format!( - "law coverage artifact is not valid wesley.law-coverage/v1 JSON: {err}" - ), - ) - .for_family("law-coverage")]); - } - }; - - if report.api_version != WESLEY_LAW_COVERAGE_API_VERSION { - return LawCoverageIngestResult::invalid(vec![HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawCoverageUnsupportedVersion, - HolmesSeverity::Error, - format!( - "unsupported law coverage apiVersion {}; expected {}", - report.api_version, WESLEY_LAW_COVERAGE_API_VERSION - ), - ) - .for_family("law-coverage") - .at_field("apiVersion")]); - } - - let diagnostics = coverage_count_diagnostics(&report); - if diagnostics.is_empty() { - LawCoverageIngestResult::valid(report) - } else { - LawCoverageIngestResult::invalid(diagnostics) - } - } -} - -fn coverage_count_diagnostics(report: &LawCoverageReport) -> Vec { - let mut diagnostics = Vec::new(); - - if report.required_covered > report.required_total { - diagnostics.push( - HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawCoverageInconsistentCounts, - HolmesSeverity::Error, - "law coverage requiredCovered must not exceed requiredTotal", - ) - .for_family("law-coverage") - .at_field("requiredCovered"), - ); - } - - let required_total = report - .categories - .iter() - .filter(|category| category.required) - .map(|category| category.total) - .sum::(); - if report.required_total != required_total { - diagnostics.push( - HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawCoverageInconsistentCounts, - HolmesSeverity::Error, - "law coverage requiredTotal must equal the sum of required category totals", - ) - .for_family("law-coverage") - .at_field("requiredTotal"), - ); - } - - let required_covered = report - .categories - .iter() - .filter(|category| category.required) - .map(|category| category.covered) - .sum::(); - if report.required_covered != required_covered { - diagnostics.push( - HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawCoverageInconsistentCounts, - HolmesSeverity::Error, - "law coverage requiredCovered must equal the sum of required category covered counts", - ) - .for_family("law-coverage") - .at_field("requiredCovered"), - ); - } - - let expected_required_percent = percentage(report.required_covered, report.required_total); - if (report.required_percent - expected_required_percent).abs() > 0.000_001 { - diagnostics.push( - HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawCoverageInconsistentCounts, - HolmesSeverity::Error, - format!( - "law coverage requiredPercent must be {expected_required_percent:.1} for the supplied required counts" - ), - ) - .for_family("law-coverage") - .at_field("requiredPercent"), - ); - } - - for (index, category) in report.categories.iter().enumerate() { - if category.covered > category.total { - diagnostics.push( - HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawCoverageInconsistentCounts, - HolmesSeverity::Error, - "law coverage category covered count must not exceed total count", - ) - .for_family("law-coverage") - .at_field(format!("categories[{index}].covered")), - ); - } - - let expected_missing = category.total.saturating_sub(category.covered); - if category.missing_subjects.len() != expected_missing { - diagnostics.push( - HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawCoverageMissingCountMismatch, - HolmesSeverity::Error, - format!( - "law coverage category missingSubjects length must be {expected_missing} for supplied counts" - ), - ) - .for_family("law-coverage") - .at_field(format!("categories[{index}].missingSubjects")), - ); - } - } - - diagnostics -} diff --git a/crates/wesley-holmes/src/application/law_diff_ingest.rs b/crates/wesley-holmes/src/application/law_diff_ingest.rs deleted file mode 100644 index a6472d0b..00000000 --- a/crates/wesley-holmes/src/application/law_diff_ingest.rs +++ /dev/null @@ -1,228 +0,0 @@ -//! JSON ingest boundary for Wesley law diff artifacts. - -use std::collections::BTreeMap; - -use serde::Deserialize; - -use crate::domain::{ - HolmesDiagnostic, HolmesDiagnosticCode, HolmesSeverity, LawDiffEvent, LawDiffEventKind, - LawDiffFieldChange, LawDiffLawKind, LawDiffReport, LawDiffReviewPosture, - WESLEY_LAW_DIFF_API_VERSION, -}; - -/// Validation status for law diff ingest. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum LawDiffIngestStatus { - /// Law diff JSON was accepted and normalized into a typed report. - Valid, - /// Law diff JSON was rejected before Holmes assessment. - Invalid, -} - -/// Result of ingesting a Wesley law diff artifact. -#[derive(Debug, Clone, PartialEq)] -pub struct LawDiffIngestResult { - /// Ingest status. - pub status: LawDiffIngestStatus, - /// Deterministically ordered ingest diagnostics. - pub diagnostics: Vec, - /// Parsed law diff report when ingest succeeded. - pub report: Option, -} - -impl LawDiffIngestResult { - fn valid(report: LawDiffReport) -> Self { - Self { - status: LawDiffIngestStatus::Valid, - diagnostics: Vec::new(), - report: Some(report), - } - } - - fn invalid(diagnostics: Vec) -> Self { - Self { - status: LawDiffIngestStatus::Invalid, - diagnostics, - report: None, - } - } -} - -/// Input port for `wesley.law-diff/v1` JSON artifacts. -pub trait LawDiffIngestPort { - /// Ingest raw law diff bytes into a typed Holmes report boundary. - fn ingest_law_diff(&self, bytes: &[u8]) -> LawDiffIngestResult; -} - -/// JSON implementation of the law diff ingest port. -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] -pub struct JsonLawDiffIngestPort; - -impl LawDiffIngestPort for JsonLawDiffIngestPort { - fn ingest_law_diff(&self, bytes: &[u8]) -> LawDiffIngestResult { - let raw = match serde_json::from_slice::(bytes) { - Ok(raw) => raw, - Err(err) => { - return LawDiffIngestResult::invalid(vec![HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawDiffMalformedJson, - HolmesSeverity::Error, - format!("law diff artifact is not valid wesley.law-diff/v1 JSON: {err}"), - ) - .for_family("law-diff")]); - } - }; - - let mut diagnostics = Vec::new(); - if raw.api_version != WESLEY_LAW_DIFF_API_VERSION { - diagnostics.push( - HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawDiffUnsupportedVersion, - HolmesSeverity::Error, - format!( - "unsupported law diff apiVersion {}; expected {}", - raw.api_version, WESLEY_LAW_DIFF_API_VERSION - ), - ) - .for_family("law-diff") - .at_field("apiVersion"), - ); - } - - for (field_path, value) in [ - ("oldSchemaHash", raw.old_schema_hash.as_str()), - ("newSchemaHash", raw.new_schema_hash.as_str()), - ("oldLawHash", raw.old_law_hash.as_str()), - ("newLawHash", raw.new_law_hash.as_str()), - ] { - if !is_canonical_sha256(value) { - diagnostics.push( - HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawDiffHashMalformed, - HolmesSeverity::Error, - "law diff hash must use sha256:<64 lowercase hex>", - ) - .for_family("law-diff") - .at_field(field_path), - ); - } - } - - let mut changes = Vec::with_capacity(raw.changes.len()); - let mut seen_law_events = BTreeMap::new(); - for (index, raw_event) in raw.changes.into_iter().enumerate() { - let Some(kind) = LawDiffEventKind::parse(&raw_event.kind) else { - diagnostics.push( - HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawDiffUnknownEventKind, - HolmesSeverity::Error, - format!("unknown law diff event kind {:?}", raw_event.kind), - ) - .for_family("law-diff") - .at_field(format!("changes[{index}].kind")), - ); - continue; - }; - - if let Some(law_id) = raw_event.law_id.as_deref() { - let event_key = (kind, law_id.to_owned()); - if let Some(first_index) = seen_law_events.insert(event_key, index) { - diagnostics.push( - HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawDiffDuplicateEvent, - HolmesSeverity::Error, - format!( - "law diff repeats law id {law_id:?} for event kind {:?}; first occurrence was changes[{first_index}]", - kind - ), - ) - .for_family("law-diff") - .at_field(format!("changes[{index}].lawId")), - ); - continue; - } - } - - changes.push(LawDiffEvent { - kind, - law_id: raw_event.law_id, - subject: raw_event.subject, - law_kind: raw_event.law_kind, - review_posture: raw_event.review_posture, - field_changes: raw_event.field_changes, - added_reads: raw_event.added_reads, - removed_reads: raw_event.removed_reads, - added_writes: raw_event.added_writes, - removed_writes: raw_event.removed_writes, - added_creates: raw_event.added_creates, - removed_creates: raw_event.removed_creates, - added_forbids: raw_event.added_forbids, - removed_forbids: raw_event.removed_forbids, - }); - } - - if diagnostics.is_empty() { - LawDiffIngestResult::valid(LawDiffReport { - api_version: raw.api_version, - old_schema_hash: raw.old_schema_hash, - new_schema_hash: raw.new_schema_hash, - old_law_hash: raw.old_law_hash, - new_law_hash: raw.new_law_hash, - changes, - }) - } else { - LawDiffIngestResult::invalid(diagnostics) - } - } -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -struct RawLawDiffReport { - api_version: String, - old_schema_hash: String, - new_schema_hash: String, - old_law_hash: String, - new_law_hash: String, - changes: Vec, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -struct RawLawDiffEvent { - kind: String, - #[serde(default)] - law_id: Option, - #[serde(default)] - subject: Option, - #[serde(default)] - law_kind: Option, - review_posture: LawDiffReviewPosture, - #[serde(default)] - field_changes: Vec, - #[serde(default)] - added_reads: Vec, - #[serde(default)] - removed_reads: Vec, - #[serde(default)] - added_writes: Vec, - #[serde(default)] - removed_writes: Vec, - #[serde(default)] - added_creates: Vec, - #[serde(default)] - removed_creates: Vec, - #[serde(default)] - added_forbids: Vec, - #[serde(default)] - removed_forbids: Vec, -} - -fn is_canonical_sha256(value: &str) -> bool { - let Some(hex) = value.strip_prefix("sha256:") else { - return false; - }; - hex.len() == 64 - && hex - .bytes() - .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) -} diff --git a/crates/wesley-holmes/src/application/mod.rs b/crates/wesley-holmes/src/application/mod.rs deleted file mode 100644 index f15d09c0..00000000 --- a/crates/wesley-holmes/src/application/mod.rs +++ /dev/null @@ -1,26 +0,0 @@ -//! Application services for deterministic Holmes law-assurance orchestration. - -mod artifact_locator; -mod contract_manifest_ingest; -mod evidence_validation; -mod law_capability_ingest; -mod law_coverage_ingest; -mod law_diff_ingest; - -pub use artifact_locator::{ResolvedArtifactPath, WeslawArtifactLocator}; -pub use contract_manifest_ingest::{ - ContractBundleManifestIngestPort, ContractBundleManifestIngestResult, - ContractBundleManifestIngestStatus, JsonContractBundleManifestIngestPort, -}; -pub use evidence_validation::LawEvidenceValidator; -pub use law_capability_ingest::{ - JsonLawCapabilityIngestPort, LawCapabilityIngestPort, LawCapabilityIngestResult, - LawCapabilityIngestStatus, -}; -pub use law_coverage_ingest::{ - JsonLawCoverageIngestPort, LawCoverageIngestPort, LawCoverageIngestResult, - LawCoverageIngestStatus, -}; -pub use law_diff_ingest::{ - JsonLawDiffIngestPort, LawDiffIngestPort, LawDiffIngestResult, LawDiffIngestStatus, -}; diff --git a/crates/wesley-holmes/src/domain/assessment.rs b/crates/wesley-holmes/src/domain/assessment.rs deleted file mode 100644 index 37ebd252..00000000 --- a/crates/wesley-holmes/src/domain/assessment.rs +++ /dev/null @@ -1,478 +0,0 @@ -//! Domain-level Holmes law assurance assessment and traceability models. - -use serde::{Deserialize, Serialize}; - -use super::contract_manifest::{ContractBundleManifest, NormalizedContractBundleProvenance}; -use super::evidence::{ - HolmesLawEvidenceBundle, LawEvidenceValidationResult, LawEvidenceValidationStatus, -}; -use super::finding::{sort_semantic_change_findings, LawFindingSeverity, SemanticChangeFinding}; -use super::law_coverage_gate::{LawCoverageGateDecision, LawCoverageGateState}; - -/// State for one bundle traceability check or the aggregate traceability gate. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub enum BundleTraceabilityGateState { - /// All compared values agree. - Pass, - /// At least one compared value disagrees. - Fail, - /// Required traceability evidence is absent. - Unavailable, -} - -impl BundleTraceabilityGateState { - /// Stable lowercase state label. - pub fn label(self) -> &'static str { - match self { - Self::Pass => "pass", - Self::Fail => "fail", - Self::Unavailable => "unavailable", - } - } -} - -/// One deterministic traceability comparison. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct BundleTraceabilityCheck { - /// Stable field path for this comparison. - pub field_path: String, - /// Expected value from the evidence bundle when available. - #[serde(skip_serializing_if = "Option::is_none")] - pub expected: Option, - /// Actual value from the compared artifact when available. - #[serde(skip_serializing_if = "Option::is_none")] - pub actual: Option, - /// Check state. - pub state: BundleTraceabilityGateState, - /// Stable lowercase state label. - pub state_label: String, - /// Renderer-neutral explanation. - pub rationale: String, -} - -/// Aggregate traceability gate for one law evidence bundle. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct BundleTraceabilityGateDecision { - /// Stable gate id. - pub gate_id: String, - /// Aggregate gate state. - pub state: BundleTraceabilityGateState, - /// Stable lowercase state label. - pub state_label: String, - /// Individual deterministic checks included in the gate. - pub checks: Vec, - /// Renderer-neutral explanation. - pub rationale: String, -} - -/// Build the bundle traceability gate without recomputing hashes or loading artifacts. -pub fn evaluate_bundle_traceability( - bundle: &HolmesLawEvidenceBundle, - manifest: Option<&ContractBundleManifest>, -) -> BundleTraceabilityGateDecision { - let mut checks = Vec::new(); - - match manifest { - Some(manifest) => { - checks.push(hash_check( - "manifest.schemaHash", - Some(bundle.provenance.schema_hash.as_str()), - Some(manifest.schema_hash.as_str()), - )); - checks.push(hash_check( - "manifest.lawHash", - Some(bundle.provenance.law_hash.as_str()), - Some(manifest.law_hash.as_str()), - )); - checks.push(hash_check( - "manifest.profileHash", - bundle.provenance.policy_hash.as_deref(), - Some(manifest.profile_hash.as_str()), - )); - checks.push(hash_check( - "manifest.bundleHash", - Some(bundle.provenance.bundle_hash.as_str()), - Some(manifest.bundle_hash.as_str()), - )); - } - None => checks.push(unavailable_check( - "manifest", - "contract bundle manifest evidence is unavailable", - )), - } - - for artifact in bundle.artifact_refs() { - let field_path = format!("{}.sha256", artifact.field_path); - let sha256 = artifact.artifact.sha256.as_deref(); - checks.push(hash_check(&field_path, sha256, sha256)); - } - - let state = aggregate_traceability_state(&checks); - BundleTraceabilityGateDecision { - gate_id: "bundle-traceability".to_owned(), - state, - state_label: state.label().to_owned(), - checks, - rationale: traceability_rationale(state).to_owned(), - } -} - -/// Provenance details retained for one evidence artifact. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct LawAssuranceArtifactProvenance { - /// Stable evidence bundle field path. - pub field_path: String, - /// Artifact family identifier. - pub artifact_family: String, - /// Workspace-relative path. - pub path: String, - /// Artifact-local schema version when declared. - #[serde(skip_serializing_if = "Option::is_none")] - pub schema_version: Option, - /// Artifact digest when declared. - #[serde(skip_serializing_if = "Option::is_none")] - pub sha256: Option, - /// Renderer-facing evidence reference. - pub evidence_ref: String, -} - -/// Deterministic provenance report substrate for later renderer sections. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct LawAssuranceProvenanceReport { - /// Stable evidence bundle identifier. - pub bundle_id: String, - /// Human-readable bundle source. - pub bundle_source: String, - /// Evidence bundle schema hash. - pub schema_hash: String, - /// Evidence bundle law hash. - pub law_hash: String, - /// Evidence bundle policy/profile hash when present. - #[serde(skip_serializing_if = "Option::is_none")] - pub policy_hash: Option, - /// Evidence bundle contract bundle hash. - pub bundle_hash: String, - /// Normalized contract bundle manifest provenance when present. - #[serde(skip_serializing_if = "Option::is_none")] - pub manifest: Option, - /// Provenance details for each present artifact reference. - pub artifacts: Vec, -} - -/// Build deterministic provenance report data from bundle and manifest evidence. -pub fn law_assurance_provenance_report( - bundle: &HolmesLawEvidenceBundle, - manifest: Option<&ContractBundleManifest>, -) -> LawAssuranceProvenanceReport { - LawAssuranceProvenanceReport { - bundle_id: bundle.bundle_id.clone(), - bundle_source: bundle.provenance.source.clone(), - schema_hash: bundle.provenance.schema_hash.clone(), - law_hash: bundle.provenance.law_hash.clone(), - policy_hash: bundle.provenance.policy_hash.clone(), - bundle_hash: bundle.provenance.bundle_hash.clone(), - manifest: manifest.map(ContractBundleManifest::normalized_provenance), - artifacts: bundle - .artifact_refs() - .into_iter() - .map(|artifact| LawAssuranceArtifactProvenance { - field_path: artifact.field_path.to_owned(), - artifact_family: artifact.family.id().to_owned(), - path: artifact.artifact.path.clone(), - schema_version: artifact.artifact.schema_version.clone(), - sha256: artifact.artifact.sha256.clone(), - evidence_ref: artifact.artifact.path.clone(), - }) - .collect(), - } -} - -/// Aggregate outcome for a Holmes law assurance assessment. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub enum LawAssuranceAssessmentOutcome { - /// All validation, finding, coverage, and traceability gates passed. - Pass, - /// Assessment is usable but carries warning-level evidence. - Warn, - /// Assessment found blocking semantic or gate evidence. - Fail, - /// Required evidence was unavailable. - Unavailable, - /// Input evidence was invalid and assessment must not continue. - Invalid, -} - -impl LawAssuranceAssessmentOutcome { - /// Stable lowercase outcome label. - pub fn label(self) -> &'static str { - match self { - Self::Pass => "pass", - Self::Warn => "warn", - Self::Fail => "fail", - Self::Unavailable => "unavailable", - Self::Invalid => "invalid", - } - } -} - -/// Deterministic aggregate assessment summary. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct LawAssuranceAssessmentSummary { - /// Aggregate assessment outcome. - pub outcome: LawAssuranceAssessmentOutcome, - /// Stable lowercase outcome label. - pub outcome_label: String, - /// Validation status that fed the aggregate decision. - pub validation_status: LawEvidenceValidationStatus, - /// Stable lowercase validation status label. - pub validation_status_label: String, - /// Total semantic finding count. - pub finding_count: usize, - /// Critical semantic finding count. - pub critical_finding_count: usize, - /// Error semantic finding count. - pub error_finding_count: usize, - /// Warning semantic finding count. - pub warning_finding_count: usize, - /// Total coverage gate count. - pub coverage_gate_count: usize, - /// Coverage gates that failed. - pub coverage_fail_count: usize, - /// Coverage gates that warned. - pub coverage_warn_count: usize, - /// Coverage gates that were unavailable. - pub coverage_unavailable_count: usize, - /// Traceability gate state. - pub traceability_state: BundleTraceabilityGateState, - /// Stable lowercase traceability state label. - pub traceability_state_label: String, - /// Renderer-neutral aggregate rationale. - pub rationale: String, -} - -/// Aggregate validation, findings, coverage, and traceability into one outcome. -pub fn aggregate_law_assurance_assessment( - validation: &LawEvidenceValidationResult, - findings: &[SemanticChangeFinding], - coverage_gates: &[LawCoverageGateDecision], - traceability_gate: &BundleTraceabilityGateDecision, -) -> LawAssuranceAssessmentSummary { - let finding_counts = FindingSeverityCounts::from_findings(findings); - let coverage_fail_count = coverage_gates - .iter() - .filter(|gate| gate.state == LawCoverageGateState::Fail) - .count(); - let coverage_warn_count = coverage_gates - .iter() - .filter(|gate| gate.state == LawCoverageGateState::Warn) - .count(); - let coverage_unavailable_count = coverage_gates - .iter() - .filter(|gate| gate.state == LawCoverageGateState::Unavailable) - .count(); - - let outcome = if validation.status == LawEvidenceValidationStatus::Invalid { - LawAssuranceAssessmentOutcome::Invalid - } else if traceability_gate.state == BundleTraceabilityGateState::Fail - || finding_counts.critical > 0 - || finding_counts.error > 0 - || coverage_fail_count > 0 - { - LawAssuranceAssessmentOutcome::Fail - } else if validation.status == LawEvidenceValidationStatus::InfrastructureError - || traceability_gate.state == BundleTraceabilityGateState::Unavailable - || coverage_unavailable_count > 0 - { - LawAssuranceAssessmentOutcome::Unavailable - } else if validation.status == LawEvidenceValidationStatus::ValidWithWarnings - || finding_counts.warning > 0 - || coverage_warn_count > 0 - { - LawAssuranceAssessmentOutcome::Warn - } else { - LawAssuranceAssessmentOutcome::Pass - }; - - LawAssuranceAssessmentSummary { - outcome, - outcome_label: outcome.label().to_owned(), - validation_status: validation.status, - validation_status_label: validation_status_label(validation.status).to_owned(), - finding_count: findings.len(), - critical_finding_count: finding_counts.critical, - error_finding_count: finding_counts.error, - warning_finding_count: finding_counts.warning, - coverage_gate_count: coverage_gates.len(), - coverage_fail_count, - coverage_warn_count, - coverage_unavailable_count, - traceability_state: traceability_gate.state, - traceability_state_label: traceability_gate.state.label().to_owned(), - rationale: assessment_rationale(outcome).to_owned(), - } -} - -/// Bounded finding display summary with omitted-detail accounting. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct BoundedFindingSummary { - /// Findings retained for inline display after deterministic sorting. - pub displayed_findings: Vec, - /// Findings omitted from inline display. - pub omitted_finding_count: usize, - /// Total finding count before display limiting. - pub total_finding_count: usize, - /// Critical finding count. - pub critical_count: usize, - /// Error finding count. - pub error_count: usize, - /// Warning finding count. - pub warning_count: usize, - /// Advisory finding count. - pub advisory_count: usize, - /// Informational finding count. - pub info_count: usize, -} - -/// Build a bounded deterministic finding summary for renderer and agent surfaces. -pub fn bounded_finding_summary( - findings: &[SemanticChangeFinding], - display_limit: usize, -) -> BoundedFindingSummary { - let mut sorted = findings.to_vec(); - sort_semantic_change_findings(&mut sorted); - let counts = FindingSeverityCounts::from_findings(&sorted); - let displayed_findings = sorted - .iter() - .take(display_limit) - .cloned() - .collect::>(); - BoundedFindingSummary { - omitted_finding_count: sorted.len().saturating_sub(displayed_findings.len()), - total_finding_count: sorted.len(), - displayed_findings, - critical_count: counts.critical, - error_count: counts.error, - warning_count: counts.warning, - advisory_count: counts.advisory, - info_count: counts.info, - } -} - -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] -struct FindingSeverityCounts { - critical: usize, - error: usize, - warning: usize, - advisory: usize, - info: usize, -} - -impl FindingSeverityCounts { - fn from_findings(findings: &[SemanticChangeFinding]) -> Self { - let mut counts = Self::default(); - for finding in findings { - match finding.severity { - LawFindingSeverity::Critical => counts.critical += 1, - LawFindingSeverity::Error => counts.error += 1, - LawFindingSeverity::Warning => counts.warning += 1, - LawFindingSeverity::Advisory => counts.advisory += 1, - LawFindingSeverity::Info => counts.info += 1, - } - } - counts - } -} - -fn hash_check( - field_path: &str, - expected: Option<&str>, - actual: Option<&str>, -) -> BundleTraceabilityCheck { - let state = match (expected, actual) { - (Some(expected), Some(actual)) if expected == actual => BundleTraceabilityGateState::Pass, - (Some(_), Some(_)) => BundleTraceabilityGateState::Fail, - _ => BundleTraceabilityGateState::Unavailable, - }; - BundleTraceabilityCheck { - field_path: field_path.to_owned(), - expected: expected.map(str::to_owned), - actual: actual.map(str::to_owned), - state, - state_label: state.label().to_owned(), - rationale: check_rationale(state).to_owned(), - } -} - -fn unavailable_check(field_path: &str, rationale: &str) -> BundleTraceabilityCheck { - BundleTraceabilityCheck { - field_path: field_path.to_owned(), - expected: None, - actual: None, - state: BundleTraceabilityGateState::Unavailable, - state_label: BundleTraceabilityGateState::Unavailable.label().to_owned(), - rationale: rationale.to_owned(), - } -} - -fn aggregate_traceability_state(checks: &[BundleTraceabilityCheck]) -> BundleTraceabilityGateState { - if checks - .iter() - .any(|check| check.state == BundleTraceabilityGateState::Fail) - { - BundleTraceabilityGateState::Fail - } else if checks - .iter() - .any(|check| check.state == BundleTraceabilityGateState::Unavailable) - { - BundleTraceabilityGateState::Unavailable - } else { - BundleTraceabilityGateState::Pass - } -} - -fn check_rationale(state: BundleTraceabilityGateState) -> &'static str { - match state { - BundleTraceabilityGateState::Pass => "traceability value matches", - BundleTraceabilityGateState::Fail => "traceability value does not match", - BundleTraceabilityGateState::Unavailable => "traceability value is unavailable", - } -} - -fn traceability_rationale(state: BundleTraceabilityGateState) -> &'static str { - match state { - BundleTraceabilityGateState::Pass => "bundle traceability checks passed", - BundleTraceabilityGateState::Fail => "bundle traceability contains mismatched hashes", - BundleTraceabilityGateState::Unavailable => { - "bundle traceability cannot be fully evaluated from available evidence" - } - } -} - -fn validation_status_label(status: LawEvidenceValidationStatus) -> &'static str { - match status { - LawEvidenceValidationStatus::Valid => "valid", - LawEvidenceValidationStatus::ValidWithWarnings => "valid-with-warnings", - LawEvidenceValidationStatus::Invalid => "invalid", - LawEvidenceValidationStatus::InfrastructureError => "infrastructure-error", - } -} - -fn assessment_rationale(outcome: LawAssuranceAssessmentOutcome) -> &'static str { - match outcome { - LawAssuranceAssessmentOutcome::Pass => "all assessment inputs passed", - LawAssuranceAssessmentOutcome::Warn => "assessment contains warning-level evidence", - LawAssuranceAssessmentOutcome::Fail => "assessment contains blocking evidence", - LawAssuranceAssessmentOutcome::Unavailable => { - "assessment cannot be completed from available evidence" - } - LawAssuranceAssessmentOutcome::Invalid => "assessment input evidence is invalid", - } -} diff --git a/crates/wesley-holmes/src/domain/contract_manifest.rs b/crates/wesley-holmes/src/domain/contract_manifest.rs deleted file mode 100644 index 387431d9..00000000 --- a/crates/wesley-holmes/src/domain/contract_manifest.rs +++ /dev/null @@ -1,93 +0,0 @@ -//! Typed Wesley contract bundle manifest evidence accepted by Holmes. - -use serde::{Deserialize, Serialize}; - -/// API version supported by the first Holmes contract bundle manifest ingest port. -pub const WESLEY_CONTRACT_BUNDLE_MANIFEST_API_VERSION: &str = "wesley.contract-bundle-manifest/v1"; - -/// Canonical Law IR codec expected in current Wesley manifests. -pub const WESLEY_LAW_IR_CANONICAL_JSON_CODEC: &str = "wesley.law-ir.canonical-json.v1"; - -/// Contract bundle hash input codec expected in current Wesley manifests. -pub const WESLEY_CONTRACT_BUNDLE_HASH_INPUT_CODEC: &str = - "wesley.contract-bundle.hash-input.canonical-json.v1"; - -/// Contract bundle manifest emitted after schema-bound Law IR validation. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ContractBundleManifest { - /// Manifest API version. - pub api_version: String, - /// Canonical Shape IR hash, prefixed with `sha256:`. - pub schema_hash: String, - /// Canonical active semantic Law IR hash. - pub law_hash: String, - /// Optional provenance-bearing law document hash. - #[serde(skip_serializing_if = "Option::is_none")] - pub law_document_hash: Option, - /// Canonical policy/profile hash. - pub profile_hash: String, - /// Hash over schema, law, profile, compiler, and codec identities. - pub bundle_hash: String, - /// Law IR semantic byte codec. - pub law_ir_codec: String, - /// Bundle hash input codec. - pub bundle_hash_codec: String, - /// Compiler crate identity. - pub compiler: String, - /// Compiler crate version. - pub compiler_version: String, - /// Bound active Law IR entry count. - pub law_entry_count: usize, -} - -impl ContractBundleManifest { - /// Return normalized manifest provenance fields for report construction. - pub fn normalized_provenance(&self) -> NormalizedContractBundleProvenance { - NormalizedContractBundleProvenance { - manifest_ref: "contractBundleManifest".to_owned(), - api_version: self.api_version.clone(), - schema_hash: self.schema_hash.clone(), - law_hash: self.law_hash.clone(), - law_document_hash: self.law_document_hash.clone(), - profile_hash: self.profile_hash.clone(), - bundle_hash: self.bundle_hash.clone(), - law_ir_codec: self.law_ir_codec.clone(), - bundle_hash_codec: self.bundle_hash_codec.clone(), - compiler: self.compiler.clone(), - compiler_version: self.compiler_version.clone(), - law_entry_count: self.law_entry_count, - } - } -} - -/// Normalized manifest provenance fields used by later report sections. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct NormalizedContractBundleProvenance { - /// Stable manifest reference for later diagnostics and report links. - pub manifest_ref: String, - /// Manifest API version. - pub api_version: String, - /// Canonical Shape IR hash, prefixed with `sha256:`. - pub schema_hash: String, - /// Canonical active semantic Law IR hash. - pub law_hash: String, - /// Optional provenance-bearing law document hash. - #[serde(skip_serializing_if = "Option::is_none")] - pub law_document_hash: Option, - /// Canonical policy/profile hash. - pub profile_hash: String, - /// Contract bundle hash. - pub bundle_hash: String, - /// Law IR semantic byte codec. - pub law_ir_codec: String, - /// Bundle hash input codec. - pub bundle_hash_codec: String, - /// Compiler crate identity. - pub compiler: String, - /// Compiler crate version. - pub compiler_version: String, - /// Bound active Law IR entry count. - pub law_entry_count: usize, -} diff --git a/crates/wesley-holmes/src/domain/diagnostic.rs b/crates/wesley-holmes/src/domain/diagnostic.rs deleted file mode 100644 index 786963b3..00000000 --- a/crates/wesley-holmes/src/domain/diagnostic.rs +++ /dev/null @@ -1,186 +0,0 @@ -//! Deterministic diagnostic envelopes for Holmes law assurance. - -use std::error::Error; -use std::fmt; - -use serde::{Deserialize, Serialize}; - -/// Stable diagnostic code emitted by Holmes validation and ingest paths. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] -#[serde(rename_all = "SCREAMING_SNAKE_CASE")] -pub enum HolmesDiagnosticCode { - /// A required `schemaVersion` field was absent or blank. - HlawSchemaVersionMissing, - /// A `schemaVersion` field was not valid semantic version syntax. - HlawSchemaVersionMalformed, - /// A `schemaVersion` is accepted but deprecated. - HlawSchemaVersionDeprecated, - /// A `schemaVersion` major version is not supported by this Holmes build. - HlawSchemaVersionUnsupportedMajor, - /// A `schemaVersion` minor version is newer than this Holmes build accepts. - HlawSchemaVersionUnsupportedMinor, - /// No local version requirement was configured for an artifact family. - HlawSchemaVersionRequirementMissing, - /// An artifact path attempted to escape the workspace root. - HlawArtifactPathEscape, - /// An artifact path was malformed before resolution. - HlawArtifactPathInvalid, - /// A law evidence bundle was missing a required artifact reference. - HlawEvidenceBundleInvalid, - /// An artifact digest field was absent or blank. - HlawArtifactHashMissing, - /// An artifact digest field did not use canonical `sha256:<64 lowercase hex>` syntax. - HlawArtifactHashMalformed, - /// A provenance hash was absent or blank. - HlawProvenanceHashMissing, - /// A provenance hash did not use canonical `sha256:<64 lowercase hex>` syntax. - HlawProvenanceHashMalformed, - /// A provenance source identity was absent or blank. - HlawProvenanceSourceMissing, - /// A requested artifact was unavailable through its port. - HlawArtifactUnavailable, - /// A requested artifact was present but unreadable through its port. - HlawArtifactUnreadable, - /// A requested artifact exceeded the configured byte limit. - HlawArtifactOversized, - /// A law diff artifact could not be parsed as the expected JSON envelope. - HlawDiffMalformedJson, - /// A law diff artifact declared an unsupported API version. - HlawDiffUnsupportedVersion, - /// A law diff artifact carried an unknown event kind. - HlawDiffUnknownEventKind, - /// A law diff artifact repeated the same law-id event identity. - HlawDiffDuplicateEvent, - /// A law diff artifact carried a non-canonical schema or law hash. - HlawDiffHashMalformed, - /// A law coverage artifact could not be parsed as the expected JSON envelope. - HlawCoverageMalformedJson, - /// A law coverage artifact declared an unsupported API version. - HlawCoverageUnsupportedVersion, - /// A law coverage artifact contained covered counts greater than total counts. - HlawCoverageInconsistentCounts, - /// A law coverage artifact's missing-subject list did not match uncovered counts. - HlawCoverageMissingCountMismatch, - /// A law capability artifact could not be parsed as the expected JSON envelope. - HlawCapabilityMalformedJson, - /// A law capability artifact declared an unsupported API version. - HlawCapabilityUnsupportedVersion, - /// A law capability artifact omitted explicit report-only/runtime posture. - HlawCapabilityMissingPosture, - /// A law capability artifact claimed contradictory resource posture. - HlawCapabilityContradictoryResourcePosture, - /// A law capability artifact carried an implicit empty footprint. - HlawCapabilityImplicitEmptyFootprint, - /// A contract bundle manifest could not be parsed as the expected JSON envelope. - HlawManifestMalformedJson, - /// A contract bundle manifest declared an unsupported API version. - HlawManifestUnsupportedVersion, - /// A contract bundle manifest omitted a required hash field. - HlawManifestMissingRequiredHash, - /// A contract bundle manifest carried a non-canonical hash field. - HlawManifestInvalidHash, - /// A contract bundle manifest omitted required provenance metadata. - HlawManifestMissingRequiredField, - /// A contract bundle manifest declared an unsupported codec identity. - HlawManifestUnsupportedCodec, - /// A contract bundle manifest disagreed with evidence-bundle provenance. - HlawManifestHashMismatch, - /// A semantic finding could not be constructed because event identity was missing. - HlawFindingMissingEventIdentity, - /// A law assurance policy artifact could not be parsed as the expected JSON envelope. - HlawPolicyMalformedJson, - /// A law assurance policy artifact declared an unsupported API version. - HlawPolicyUnsupportedVersion, - /// A law assurance policy artifact carried an unknown top-level field. - HlawPolicyUnknownField, - /// A law assurance policy did not provide a selectable profile. - HlawPolicyMissingProfile, - /// A requested law assurance policy profile was not present. - HlawPolicyUnknownProfile, - /// A law assurance policy profile inheritance chain was circular. - HlawPolicyCircularInheritance, - /// A law assurance coverage threshold policy was invalid. - HlawPolicyInvalidThreshold, - /// A law assurance severity mapping referenced an unknown Wesley event kind. - HlawPolicyUnknownEventKind, - /// Exhaustive severity policy did not map a Wesley event kind. - HlawSeverityUnmappedEventKind, - /// A law assurance suppression rule was malformed or too broad. - HlawSuppressionInvalid, - /// A suppression was rejected because evidence validation failed. - HlawSuppressionRejectedInvalidEvidence, - /// A suppression was rejected because it targets a non-overridable gate. - HlawSuppressionRejectedNonOverridable, - /// A suppression rule was present but expired and was not applied. - HlawSuppressionExpired, -} - -/// Severity attached to a Holmes diagnostic. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] -#[serde(rename_all = "SCREAMING_SNAKE_CASE")] -pub enum HolmesSeverity { - /// A hard failure that prevents safe continuation. - Error, - /// A non-blocking issue that should be visible in reports. - Warning, - /// Informational context attached to a report. - Info, -} - -/// Structured diagnostic envelope shared by validation and ingest flows. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct HolmesDiagnostic { - /// Stable diagnostic code. - pub code: HolmesDiagnosticCode, - /// Diagnostic severity. - pub severity: HolmesSeverity, - /// Human-readable explanation. - pub message: String, - /// Optional artifact family associated with this diagnostic. - #[serde(skip_serializing_if = "Option::is_none")] - pub artifact_family: Option, - /// Optional field path associated with this diagnostic. - #[serde(skip_serializing_if = "Option::is_none")] - pub field_path: Option, -} - -impl HolmesDiagnostic { - /// Create a new diagnostic envelope. - pub fn new( - code: HolmesDiagnosticCode, - severity: HolmesSeverity, - message: impl Into, - ) -> Self { - Self { - code, - severity, - message: message.into(), - artifact_family: None, - field_path: None, - } - } - - /// Attach an artifact-family label. - pub fn for_family(mut self, family: impl Into) -> Self { - self.artifact_family = Some(family.into()); - self - } - - /// Attach a field path. - pub fn at_field(mut self, field_path: impl Into) -> Self { - self.field_path = Some(field_path.into()); - self - } -} - -impl fmt::Display for HolmesDiagnostic { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(formatter, "{:?}: {}", self.code, self.message) - } -} - -impl Error for HolmesDiagnostic {} - -/// Result alias for Holmes domain and port operations. -pub type HolmesResult = Result; diff --git a/crates/wesley-holmes/src/domain/evidence.rs b/crates/wesley-holmes/src/domain/evidence.rs deleted file mode 100644 index ba345de8..00000000 --- a/crates/wesley-holmes/src/domain/evidence.rs +++ /dev/null @@ -1,474 +0,0 @@ -//! Law evidence bundle model consumed by Holmes. - -use std::collections::BTreeMap; - -use serde::{Deserialize, Serialize}; - -use super::diagnostic::{HolmesDiagnostic, HolmesDiagnosticCode, HolmesResult, HolmesSeverity}; -use super::versioning::{ArtifactFamily, VersionRegistry}; - -/// Workspace-relative reference to a Wesley-published artifact. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ArtifactRef { - /// Workspace-relative artifact path. - pub path: String, - /// Artifact-local schema version required for present artifact references. - #[serde(skip_serializing_if = "Option::is_none")] - pub schema_version: Option, - /// Optional expected SHA-256 digest. - #[serde(skip_serializing_if = "Option::is_none")] - pub sha256: Option, -} - -impl ArtifactRef { - /// Create an artifact reference with only a path. - pub fn new(path: impl Into) -> Self { - Self { - path: path.into(), - schema_version: None, - sha256: None, - } - } - - /// Attach an artifact-local schema version. - pub fn with_schema_version(mut self, schema_version: impl Into) -> Self { - self.schema_version = Some(schema_version.into()); - self - } - - fn is_blank(&self) -> bool { - self.path.trim().is_empty() - } -} - -/// Whether a bundle artifact reference is required for validation. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub enum ArtifactRequirement { - /// The artifact reference must be present and non-blank. - Required, - /// The artifact reference may be omitted, but must be valid when present. - Optional, -} - -/// A bundle artifact reference with its stable field path and family. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct BundleArtifactRef<'a> { - /// Stable field path in the evidence bundle. - pub field_path: &'static str, - /// Artifact family used for schema-version validation. - pub family: ArtifactFamily, - /// Requirement class for this reference. - pub requirement: ArtifactRequirement, - /// Referenced artifact. - pub artifact: &'a ArtifactRef, -} - -/// Validation status before Holmes performs assurance assessment. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub enum LawEvidenceValidationStatus { - /// Evidence is structurally valid and all required artifacts were readable. - Valid, - /// Evidence is usable, but carries non-fatal diagnostics. - ValidWithWarnings, - /// Evidence is invalid and assessment must not run. - Invalid, - /// A dependency failure prevented validation from completing. - InfrastructureError, -} - -/// Metadata captured for a loaded evidence artifact. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct LoadedArtifactMetadata { - /// Stable evidence-bundle field path for this artifact. - pub field_path: String, - /// Artifact-family identifier. - pub artifact_family: String, - /// Normalized workspace-relative path. - pub path: String, - /// Loaded byte length. - pub byte_len: usize, -} - -/// Collected evidence validation result. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct LawEvidenceValidationResult { - /// Validation status. - pub status: LawEvidenceValidationStatus, - /// Deterministically ordered diagnostics. - pub diagnostics: Vec, - /// Metadata for artifacts successfully loaded by the validation gate. - pub loaded_artifacts: Vec, -} - -impl LawEvidenceValidationResult { - /// Build a validation result from diagnostics. - pub fn from_diagnostics(diagnostics: Vec) -> Self { - let status = validation_status_for(&diagnostics); - Self { - status, - diagnostics, - loaded_artifacts: Vec::new(), - } - } - - /// Add loaded artifact metadata. - pub fn with_loaded_artifacts(mut self, loaded_artifacts: Vec) -> Self { - self.loaded_artifacts = loaded_artifacts; - self - } -} - -/// Required artifact families in a Holmes law evidence bundle. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct LawEvidenceArtifacts { - /// Machine-readable law diff artifact. - pub law_diff: ArtifactRef, - /// Law coverage artifact for the active assurance profile. - pub law_coverage: ArtifactRef, - /// Capability model artifact derived from operation footprint law. - pub law_capabilities: ArtifactRef, - /// Contract bundle manifest artifact. - pub contract_bundle_manifest: ArtifactRef, - /// Optional active policy artifact. - #[serde(skip_serializing_if = "Option::is_none")] - pub policy: Option, - /// Optional rendered report artifact. - #[serde(skip_serializing_if = "Option::is_none")] - pub report: Option, - /// Optional witness artifact. - #[serde(skip_serializing_if = "Option::is_none")] - pub witness: Option, -} - -/// Hash and source provenance for a law evidence bundle. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct BundleProvenance { - /// Canonical schema hash that the evidence was derived from. - pub schema_hash: String, - /// Canonical law hash that the evidence was derived from. - pub law_hash: String, - /// Optional policy hash active during evidence production. - #[serde(skip_serializing_if = "Option::is_none")] - pub policy_hash: Option, - /// Canonical contract bundle hash. - pub bundle_hash: String, - /// Human-readable source label for the bundle. - pub source: String, -} - -/// Top-level Holmes law evidence bundle envelope. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct HolmesLawEvidenceBundle { - /// Evidence bundle schema version. - pub schema_version: String, - /// Stable evidence bundle identifier. - pub bundle_id: String, - /// Required and optional artifacts that make up the bundle. - pub artifacts: LawEvidenceArtifacts, - /// Hash and source provenance. - pub provenance: BundleProvenance, -} - -impl HolmesLawEvidenceBundle { - /// Return all present required and optional artifact references. - pub fn artifact_refs(&self) -> Vec> { - let mut artifacts = vec![ - BundleArtifactRef { - field_path: "artifacts.lawDiff", - family: ArtifactFamily::LawDiff, - requirement: ArtifactRequirement::Required, - artifact: &self.artifacts.law_diff, - }, - BundleArtifactRef { - field_path: "artifacts.lawCoverage", - family: ArtifactFamily::LawCoverage, - requirement: ArtifactRequirement::Required, - artifact: &self.artifacts.law_coverage, - }, - BundleArtifactRef { - field_path: "artifacts.lawCapabilities", - family: ArtifactFamily::LawCapabilities, - requirement: ArtifactRequirement::Required, - artifact: &self.artifacts.law_capabilities, - }, - BundleArtifactRef { - field_path: "artifacts.contractBundleManifest", - family: ArtifactFamily::ContractBundleManifest, - requirement: ArtifactRequirement::Required, - artifact: &self.artifacts.contract_bundle_manifest, - }, - ]; - - if let Some(artifact) = &self.artifacts.policy { - artifacts.push(BundleArtifactRef { - field_path: "artifacts.policy", - family: ArtifactFamily::Policy, - requirement: ArtifactRequirement::Optional, - artifact, - }); - } - if let Some(artifact) = &self.artifacts.report { - artifacts.push(BundleArtifactRef { - field_path: "artifacts.report", - family: ArtifactFamily::Report, - requirement: ArtifactRequirement::Optional, - artifact, - }); - } - if let Some(artifact) = &self.artifacts.witness { - artifacts.push(BundleArtifactRef { - field_path: "artifacts.witness", - family: ArtifactFamily::AuditWitness, - requirement: ArtifactRequirement::Optional, - artifact, - }); - } - - artifacts - } - - /// Validate bundle shape, artifact references, schema versions, and provenance. - pub fn validate_structure( - &self, - version_registry: &VersionRegistry, - ) -> LawEvidenceValidationResult { - let mut diagnostics = Vec::new(); - - match version_registry.classify( - ArtifactFamily::EvidenceBundle, - Some(self.schema_version.as_str()), - ) { - Ok(version_check) => diagnostics.extend(version_check.diagnostics), - Err(diagnostic) => diagnostics.push(diagnostic), - } - - if self.bundle_id.trim().is_empty() { - diagnostics.push( - HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawEvidenceBundleInvalid, - HolmesSeverity::Error, - "law evidence bundle is missing bundleId", - ) - .at_field("bundleId"), - ); - } - - self.validate_artifact_structure(version_registry, &mut diagnostics); - self.validate_provenance(&mut diagnostics); - - LawEvidenceValidationResult::from_diagnostics(diagnostics) - } - - /// Validate that all required artifact references are present. - pub fn validate_required_artifacts(&self) -> HolmesResult<()> { - let required = [ - ("artifacts.lawDiff", &self.artifacts.law_diff), - ("artifacts.lawCoverage", &self.artifacts.law_coverage), - ( - "artifacts.lawCapabilities", - &self.artifacts.law_capabilities, - ), - ( - "artifacts.contractBundleManifest", - &self.artifacts.contract_bundle_manifest, - ), - ]; - - for (field_path, artifact) in required { - if artifact.is_blank() { - return Err(HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawEvidenceBundleInvalid, - HolmesSeverity::Error, - "law evidence bundle is missing a required artifact reference", - ) - .at_field(field_path)); - } - } - - Ok(()) - } - - fn validate_artifact_structure( - &self, - version_registry: &VersionRegistry, - diagnostics: &mut Vec, - ) { - let mut paths = BTreeMap::new(); - - for bundle_artifact in self.artifact_refs() { - let artifact = bundle_artifact.artifact; - if artifact.is_blank() { - diagnostics.push( - HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawEvidenceBundleInvalid, - HolmesSeverity::Error, - match bundle_artifact.requirement { - ArtifactRequirement::Required => { - "law evidence bundle is missing a required artifact reference" - } - ArtifactRequirement::Optional => { - "law evidence bundle contains a blank optional artifact reference" - } - }, - ) - .for_family(bundle_artifact.family.id()) - .at_field(bundle_artifact.field_path), - ); - continue; - } - - let duplicate_of = - paths.insert(artifact.path.trim().to_owned(), bundle_artifact.field_path); - if let Some(first_field_path) = duplicate_of { - diagnostics.push( - HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawEvidenceBundleInvalid, - HolmesSeverity::Error, - format!( - "artifact path duplicates {first_field_path}; each artifact role must point at distinct evidence" - ), - ) - .for_family(bundle_artifact.family.id()) - .at_field(bundle_artifact.field_path), - ); - } - - let schema_field = format!("{}.schemaVersion", bundle_artifact.field_path); - match version_registry - .classify(bundle_artifact.family, artifact.schema_version.as_deref()) - { - Ok(version_check) => diagnostics.extend( - version_check - .diagnostics - .into_iter() - .map(|diagnostic| diagnostic.at_field(schema_field.clone())), - ), - Err(diagnostic) => diagnostics.push(diagnostic.at_field(schema_field)), - } - - if let Some(sha256) = artifact.sha256.as_deref() { - validate_artifact_sha256( - sha256, - format!("{}.sha256", bundle_artifact.field_path), - diagnostics, - ); - } - } - } - - fn validate_provenance(&self, diagnostics: &mut Vec) { - validate_required_sha256( - &self.provenance.schema_hash, - "provenance.schemaHash", - diagnostics, - ); - validate_required_sha256(&self.provenance.law_hash, "provenance.lawHash", diagnostics); - if let Some(policy_hash) = &self.provenance.policy_hash { - validate_required_sha256(policy_hash, "provenance.policyHash", diagnostics); - } - validate_required_sha256( - &self.provenance.bundle_hash, - "provenance.bundleHash", - diagnostics, - ); - - if self.provenance.source.trim().is_empty() { - diagnostics.push( - HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawProvenanceSourceMissing, - HolmesSeverity::Error, - "law evidence bundle provenance source must not be blank", - ) - .at_field("provenance.source"), - ); - } - } -} - -fn validation_status_for(diagnostics: &[HolmesDiagnostic]) -> LawEvidenceValidationStatus { - if diagnostics - .iter() - .any(|diagnostic| diagnostic.severity == HolmesSeverity::Error) - { - LawEvidenceValidationStatus::Invalid - } else if diagnostics - .iter() - .any(|diagnostic| diagnostic.severity == HolmesSeverity::Warning) - { - LawEvidenceValidationStatus::ValidWithWarnings - } else { - LawEvidenceValidationStatus::Valid - } -} - -fn validate_required_sha256( - value: &str, - field_path: impl Into, - diagnostics: &mut Vec, -) { - let field_path = field_path.into(); - if value.trim().is_empty() { - diagnostics.push( - HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawProvenanceHashMissing, - HolmesSeverity::Error, - "law evidence bundle provenance hash must not be blank", - ) - .at_field(field_path), - ); - } else if !is_canonical_sha256(value) { - diagnostics.push( - HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawProvenanceHashMalformed, - HolmesSeverity::Error, - "law evidence bundle provenance hash must use sha256:<64 lowercase hex>", - ) - .at_field(field_path), - ); - } -} - -fn validate_artifact_sha256( - value: &str, - field_path: impl Into, - diagnostics: &mut Vec, -) { - let field_path = field_path.into(); - if value.trim().is_empty() { - diagnostics.push( - HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawArtifactHashMissing, - HolmesSeverity::Error, - "artifact sha256 digest must not be blank", - ) - .at_field(field_path), - ); - } else if !is_canonical_sha256(value) { - diagnostics.push( - HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawArtifactHashMalformed, - HolmesSeverity::Error, - "artifact sha256 digest must use sha256:<64 lowercase hex>", - ) - .at_field(field_path), - ); - } -} - -fn is_canonical_sha256(value: &str) -> bool { - let Some(hex) = value.strip_prefix("sha256:") else { - return false; - }; - hex.len() == 64 - && hex - .bytes() - .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) -} diff --git a/crates/wesley-holmes/src/domain/finding.rs b/crates/wesley-holmes/src/domain/finding.rs deleted file mode 100644 index ffca0c5e..00000000 --- a/crates/wesley-holmes/src/domain/finding.rs +++ /dev/null @@ -1,327 +0,0 @@ -//! Holmes semantic findings derived from Wesley law diff evidence. - -use serde::{Deserialize, Serialize}; - -use super::diagnostic::{HolmesDiagnostic, HolmesDiagnosticCode, HolmesResult, HolmesSeverity}; -use super::law_diff::{ - LawDiffEventKind, LawDiffFieldChange, LawDiffReport, LawDiffReviewPosture, - NormalizedLawDiffEvent, -}; - -/// Reviewer-facing severity assigned to a semantic change finding. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub enum LawFindingSeverity { - /// Informational change. - Info, - /// Advisory change that should be visible but does not imply risk by itself. - Advisory, - /// Warning change that deserves reviewer attention. - Warning, - /// Error-level change under the default Holmes mapping. - Error, - /// Critical change under the default Holmes mapping. - Critical, -} - -impl LawFindingSeverity { - /// Return a lowercase label for renderer-neutral output. - pub fn label(self) -> &'static str { - match self { - Self::Info => "info", - Self::Advisory => "advisory", - Self::Warning => "warning", - Self::Error => "error", - Self::Critical => "critical", - } - } - - fn rank_desc(self) -> u8 { - match self { - Self::Critical => 0, - Self::Error => 1, - Self::Warning => 2, - Self::Advisory => 3, - Self::Info => 4, - } - } -} - -/// Renderer-neutral Holmes finding for one Wesley semantic law change. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SemanticChangeFinding { - /// Stable Holmes finding id. - pub finding_id: String, - /// Stable event reference inside the source law diff report. - pub event_ref: String, - /// Zero-based event index in Wesley's emitted diff order. - pub event_index: usize, - /// Source artifact reference supplied by the caller. - pub source_artifact_ref: String, - /// Manifest or bundle hash family used for finding id derivation. - pub bundle_hash_family: String, - /// Report API version. - pub api_version: String, - /// Old document schema hash anchor. - pub old_schema_hash: String, - /// New document schema hash anchor. - pub new_schema_hash: String, - /// Old semantic Law IR hash. - pub old_law_hash: String, - /// New semantic Law IR hash. - pub new_law_hash: String, - /// Wesley event classification preserved exactly. - pub event_kind: LawDiffEventKind, - /// Wesley review posture preserved exactly. - pub change_posture: LawDiffReviewPosture, - /// Default Holmes severity before later policy overrides. - pub severity: LawFindingSeverity, - /// Stable text severity label. - pub severity_label: String, - /// Stable law id affected by the event when Wesley supplied one. - #[serde(skip_serializing_if = "Option::is_none")] - pub law_id: Option, - /// Subject coordinate affected by the event when Wesley supplied one. - #[serde(skip_serializing_if = "Option::is_none")] - pub subject: Option, - /// Subject kind derived from the subject coordinate. - #[serde(skip_serializing_if = "Option::is_none")] - pub subject_kind: Option, - /// Optional active assessment profile. - #[serde(skip_serializing_if = "Option::is_none")] - pub profile: Option, - /// Optional classifier tags reserved for later policy mapping. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub tags: Vec, - /// Bounded summary suitable for tables and comments. - pub summary: String, - /// Renderer-neutral detail text. - pub details: String, - /// Field-level changes when a law body changed. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub field_changes: Vec, - /// Footprint resources newly read. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub added_reads: Vec, - /// Footprint resources no longer read. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub removed_reads: Vec, - /// Footprint resources newly written. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub added_writes: Vec, - /// Footprint resources no longer written. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub removed_writes: Vec, - /// Footprint resources newly created. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub added_creates: Vec, - /// Footprint resources no longer created. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub removed_creates: Vec, - /// Footprint resources newly forbidden. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub added_forbids: Vec, - /// Footprint resources no longer forbidden. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub removed_forbids: Vec, -} - -impl SemanticChangeFinding { - /// Construct one finding from a normalized Wesley law diff event. - pub fn from_normalized_event( - bundle_hash_family: impl Into, - source_artifact_ref: impl Into, - profile: Option, - tags: Vec, - event: &NormalizedLawDiffEvent, - ) -> HolmesResult { - if event.event_ref.trim().is_empty() { - return Err(HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawFindingMissingEventIdentity, - HolmesSeverity::Error, - "semantic change finding requires a non-empty event reference", - ) - .for_family("law-diff") - .at_field("eventRef")); - } - - let bundle_hash_family = bundle_hash_family.into(); - let source_artifact_ref = source_artifact_ref.into(); - let severity = default_severity_for_event(event.kind); - let subject_kind = event.subject.as_deref().and_then(subject_kind); - let summary = summary_for_event(event); - let details = details_for_event(event); - let finding_id = stable_finding_id(&bundle_hash_family, event); - - Ok(Self { - finding_id, - event_ref: event.event_ref.clone(), - event_index: event.event_index, - source_artifact_ref, - bundle_hash_family, - api_version: event.api_version.clone(), - old_schema_hash: event.old_schema_hash.clone(), - new_schema_hash: event.new_schema_hash.clone(), - old_law_hash: event.old_law_hash.clone(), - new_law_hash: event.new_law_hash.clone(), - event_kind: event.kind, - change_posture: event.review_posture, - severity, - severity_label: severity.label().to_owned(), - law_id: event.law_id.clone(), - subject: event.subject.clone(), - subject_kind, - profile, - tags, - summary, - details, - field_changes: event.field_changes.clone(), - added_reads: sorted_strings(&event.added_reads), - removed_reads: sorted_strings(&event.removed_reads), - added_writes: sorted_strings(&event.added_writes), - removed_writes: sorted_strings(&event.removed_writes), - added_creates: sorted_strings(&event.added_creates), - removed_creates: sorted_strings(&event.removed_creates), - added_forbids: sorted_strings(&event.added_forbids), - removed_forbids: sorted_strings(&event.removed_forbids), - }) - } -} - -/// Build sorted semantic change findings for a parsed law diff report. -pub fn semantic_change_findings_from_law_diff( - report: &LawDiffReport, - bundle_hash_family: impl Into, - source_artifact_ref: impl Into, - profile: Option, -) -> HolmesResult> { - let bundle_hash_family = bundle_hash_family.into(); - let source_artifact_ref = source_artifact_ref.into(); - let mut findings = report - .normalized_events() - .iter() - .map(|event| { - SemanticChangeFinding::from_normalized_event( - bundle_hash_family.clone(), - source_artifact_ref.clone(), - profile.clone(), - Vec::new(), - event, - ) - }) - .collect::>>()?; - sort_semantic_change_findings(&mut findings); - Ok(findings) -} - -/// Sort semantic change findings by the default deterministic review order. -pub fn sort_semantic_change_findings(findings: &mut [SemanticChangeFinding]) { - findings.sort_by(|left, right| { - left.severity - .rank_desc() - .cmp(&right.severity.rank_desc()) - .then_with(|| left.subject_kind.cmp(&right.subject_kind)) - .then_with(|| left.subject.cmp(&right.subject)) - .then_with(|| left.law_id.cmp(&right.law_id)) - .then_with(|| left.event_kind.cmp(&right.event_kind)) - .then_with(|| left.event_index.cmp(&right.event_index)) - }); -} - -/// Return the first default Holmes severity for a Wesley law diff event kind. -pub fn default_severity_for_event(kind: LawDiffEventKind) -> LawFindingSeverity { - match kind { - LawDiffEventKind::BindingBroken - | LawDiffEventKind::LawRemoved - | LawDiffEventKind::LawWeakened => LawFindingSeverity::Critical, - LawDiffEventKind::FootprintExpanded | LawDiffEventKind::SchemaHashRebound => { - LawFindingSeverity::Error - } - LawDiffEventKind::ChannelLawChanged - | LawDiffEventKind::ChannelVersionChanged - | LawDiffEventKind::FootprintChanged - | LawDiffEventKind::LawBundleChanged - | LawDiffEventKind::LawChanged - | LawDiffEventKind::PredicateChanged - | LawDiffEventKind::RegistryChanged - | LawDiffEventKind::ScalarSemanticsChanged - | LawDiffEventKind::VariantLawChanged => LawFindingSeverity::Warning, - LawDiffEventKind::FootprintContracted - | LawDiffEventKind::LawAdded - | LawDiffEventKind::LawStrengthened - | LawDiffEventKind::LawTagsChanged => LawFindingSeverity::Advisory, - } -} - -fn stable_finding_id(bundle_hash_family: &str, event: &NormalizedLawDiffEvent) -> String { - let identity = format!( - "{}\n{}\n{}\n{}\n{}\n{}", - bundle_hash_family, - event.event_ref, - event.event_index, - event.kind.as_str(), - event.law_id.as_deref().unwrap_or(""), - event.subject.as_deref().unwrap_or("") - ); - format!("semantic-change:{:016x}", fnv1a64(identity.as_bytes())) -} - -fn fnv1a64(bytes: &[u8]) -> u64 { - let mut hash = 0xcbf29ce484222325_u64; - for byte in bytes { - hash ^= u64::from(*byte); - hash = hash.wrapping_mul(0x100000001b3); - } - hash -} - -fn summary_for_event(event: &NormalizedLawDiffEvent) -> String { - let target = event - .subject - .as_deref() - .or(event.law_id.as_deref()) - .unwrap_or("law bundle"); - format!("{} requires review for {target}", event.kind.as_str()) -} - -fn details_for_event(event: &NormalizedLawDiffEvent) -> String { - let mut parts = Vec::new(); - if !event.field_changes.is_empty() { - parts.push(format!("{} field change(s)", event.field_changes.len())); - } - push_resource_detail(&mut parts, "added reads", &event.added_reads); - push_resource_detail(&mut parts, "removed reads", &event.removed_reads); - push_resource_detail(&mut parts, "added writes", &event.added_writes); - push_resource_detail(&mut parts, "removed writes", &event.removed_writes); - push_resource_detail(&mut parts, "added creates", &event.added_creates); - push_resource_detail(&mut parts, "removed creates", &event.removed_creates); - push_resource_detail(&mut parts, "added forbids", &event.added_forbids); - push_resource_detail(&mut parts, "removed forbids", &event.removed_forbids); - - if parts.is_empty() { - format!( - "Wesley emitted {} with no additional payload", - event.kind.as_str() - ) - } else { - parts.join("; ") - } -} - -fn push_resource_detail(parts: &mut Vec, label: &'static str, values: &[String]) { - if !values.is_empty() { - parts.push(format!("{label}: {}", sorted_strings(values).join(", "))); - } -} - -fn subject_kind(subject: &str) -> Option { - subject.split_once(':').map(|(kind, _)| kind.to_owned()) -} - -fn sorted_strings(values: &[String]) -> Vec { - let mut values = values.to_vec(); - values.sort(); - values.dedup(); - values -} diff --git a/crates/wesley-holmes/src/domain/law_capability.rs b/crates/wesley-holmes/src/domain/law_capability.rs deleted file mode 100644 index 5cf021da..00000000 --- a/crates/wesley-holmes/src/domain/law_capability.rs +++ /dev/null @@ -1,219 +0,0 @@ -//! Typed Wesley law capability evidence accepted by Holmes. - -use serde::{Deserialize, Serialize}; - -/// API version named by the Holmes PRD for law capability summaries. -pub const WESLEY_LAW_CAPABILITIES_API_VERSION: &str = "wesley.law-capabilities/v1"; - -/// Machine-readable footprint capability summary emitted by Wesley. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct LawCapabilityReport { - /// Report API version. - pub api_version: String, - /// Whether the report is declaration-only and does not claim runtime enforcement. - pub report_only: bool, - /// Whether runtime enforcement evidence supports the reported posture. - pub runtime_enforcement: bool, - /// Human-readable note emitted by Wesley. - pub note: String, - /// Per-operation footprint summaries. - pub footprints: Vec, -} - -impl LawCapabilityReport { - /// Normalize operation capability rows for report sections and gates. - pub fn normalized_operations(&self) -> Vec { - let mut operations = self - .footprints - .iter() - .enumerate() - .map(|(operation_index, footprint)| { - footprint.normalized_operation( - operation_index, - self.report_only, - self.runtime_enforcement, - ) - }) - .collect::>(); - operations.sort_by(|left, right| { - left.subject - .cmp(&right.subject) - .then_with(|| left.law_id.cmp(&right.law_id)) - .then_with(|| left.operation_index.cmp(&right.operation_index)) - }); - operations - } -} - -/// Operation footprint capability summary. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct LawCapabilityFootprint { - /// Stable law id that produced this summary. - pub law_id: String, - /// Operation subject coordinate. - pub subject: String, - /// Resource types read by the operation. - #[serde(default)] - pub reads: Vec, - /// Resource types written by the operation. - #[serde(default)] - pub writes: Vec, - /// Resource types created by the operation. - #[serde(default)] - pub creates: Vec, - /// Resource domains forbidden to the operation. - #[serde(default)] - pub forbids: Vec, - /// Bound input/resource slots when supplied by a richer artifact. - #[serde(default)] - pub slots: Vec, - /// Closure-derived resource windows when supplied by a richer artifact. - #[serde(default)] - pub closures: Vec, - /// Whether an otherwise empty footprint was intentionally authored. - #[serde(default)] - pub intentionally_empty: bool, -} - -impl LawCapabilityFootprint { - fn normalized_operation( - &self, - operation_index: usize, - report_only: bool, - runtime_enforcement: bool, - ) -> NormalizedLawCapabilityOperation { - NormalizedLawCapabilityOperation { - operation_ref: format!("lawCapabilities.footprints[{operation_index}]"), - operation_index, - law_id: self.law_id.clone(), - subject: self.subject.clone(), - report_only, - runtime_enforcement, - wording_hint: wording_hint(report_only, runtime_enforcement).to_owned(), - intentionally_empty: self.intentionally_empty, - reads: sorted_strings(&self.reads), - writes: sorted_strings(&self.writes), - creates: sorted_strings(&self.creates), - forbids: sorted_strings(&self.forbids), - slots: sorted_slots(&self.slots), - closures: sorted_closures(&self.closures), - } - } -} - -/// Bound input/resource slot in a footprint summary. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct LawCapabilitySlot { - /// Slot name. - pub name: String, - /// Resource kind bound to the slot. - pub kind: String, - /// Argument path that binds the slot. - pub bind_from_arg: String, - /// Access modes granted for this slot. - #[serde(default)] - pub access: Vec, -} - -/// Closure-derived resource window in a footprint summary. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct LawCapabilityClosure { - /// Closure slot name. - pub name: String, - /// Source slot. - pub from_slot: String, - /// Closure operator id. - pub operator: String, - /// Argument bindings passed to the operator. - #[serde(default)] - pub arg_bindings: Vec, - /// Resource kinds read by the closure. - #[serde(default)] - pub reads: Vec, - /// Cardinality label. - pub cardinality: String, -} - -/// Normalized per-operation capability posture. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct NormalizedLawCapabilityOperation { - /// Stable operation reference inside the parsed capability report. - pub operation_ref: String, - /// Zero-based operation index in Wesley's emitted capability order. - pub operation_index: usize, - /// Stable law id that produced this summary. - pub law_id: String, - /// Operation subject coordinate. - pub subject: String, - /// Whether the summary is declaration-only. - pub report_only: bool, - /// Whether runtime enforcement evidence supports the reported posture. - pub runtime_enforcement: bool, - /// Renderer-facing wording constraint for the posture. - pub wording_hint: String, - /// Whether all empty resource groups were intentionally authored. - pub intentionally_empty: bool, - /// Resource types read by the operation. - pub reads: Vec, - /// Resource types written by the operation. - pub writes: Vec, - /// Resource types created by the operation. - pub creates: Vec, - /// Resource domains forbidden to the operation. - pub forbids: Vec, - /// Bound input/resource slots. - pub slots: Vec, - /// Closure-derived resource windows. - pub closures: Vec, -} - -fn wording_hint(report_only: bool, runtime_enforcement: bool) -> &'static str { - if runtime_enforcement { - "runtime enforcement evidence present" - } else if report_only { - "report-only footprint declaration; do not imply runtime enforcement" - } else { - "capability posture is explicitly not marked report-only" - } -} - -fn sorted_strings(values: &[String]) -> Vec { - let mut values = values.to_vec(); - values.sort(); - values.dedup(); - values -} - -fn sorted_slots(slots: &[LawCapabilitySlot]) -> Vec { - let mut slots = slots.to_vec(); - slots.sort_by(|left, right| { - left.name - .cmp(&right.name) - .then_with(|| left.kind.cmp(&right.kind)) - .then_with(|| left.bind_from_arg.cmp(&right.bind_from_arg)) - }); - for slot in &mut slots { - slot.access = sorted_strings(&slot.access); - } - slots -} - -fn sorted_closures(closures: &[LawCapabilityClosure]) -> Vec { - let mut closures = closures.to_vec(); - closures.sort_by(|left, right| { - left.name - .cmp(&right.name) - .then_with(|| left.from_slot.cmp(&right.from_slot)) - .then_with(|| left.operator.cmp(&right.operator)) - }); - for closure in &mut closures { - closure.arg_bindings = sorted_strings(&closure.arg_bindings); - closure.reads = sorted_strings(&closure.reads); - } - closures -} diff --git a/crates/wesley-holmes/src/domain/law_coverage.rs b/crates/wesley-holmes/src/domain/law_coverage.rs deleted file mode 100644 index e17a21a3..00000000 --- a/crates/wesley-holmes/src/domain/law_coverage.rs +++ /dev/null @@ -1,162 +0,0 @@ -//! Typed Wesley law coverage evidence accepted by Holmes. - -use serde::{Deserialize, Serialize}; - -/// API version supported by the first Holmes law coverage ingest port. -pub const WESLEY_LAW_COVERAGE_API_VERSION: &str = "wesley.law-coverage/v1"; - -/// Machine-readable category/profile-aware coverage report emitted by Wesley. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct LawCoverageReport { - /// Report API version. - pub api_version: String, - /// Coverage profile identifier. - pub profile: String, - /// Total number of required subjects considered by the profile. - pub required_total: usize, - /// Number of required subjects covered by law. - pub required_covered: usize, - /// Required-subject coverage percentage emitted by Wesley. - pub required_percent: f64, - /// Per-category coverage records. - pub categories: Vec, -} - -impl LawCoverageReport { - /// Normalize coverage evidence for gate and report construction. - pub fn normalized_profile( - &self, - missing_subject_display_limit: usize, - ) -> NormalizedLawCoverageProfile { - NormalizedLawCoverageProfile { - profile: self.profile.clone(), - required_total: self.required_total, - required_covered: self.required_covered, - required_percent: percentage(self.required_covered, self.required_total), - categories: self - .categories - .iter() - .enumerate() - .map(|(category_index, category)| { - category.normalized_category(category_index, missing_subject_display_limit) - }) - .collect(), - } - } -} - -/// Per-category law coverage record. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct LawCoverageCategory { - /// Stable category identifier. - pub id: String, - /// Human-readable category label. - pub label: String, - /// Whether this category is required in the active profile. - pub required: bool, - /// Total subjects considered in this category. - pub total: usize, - /// Covered subjects in this category. - pub covered: usize, - /// Subject coordinates missing required law in this category. - pub missing_subjects: Vec, -} - -impl LawCoverageCategory { - fn normalized_category( - &self, - category_index: usize, - missing_subject_display_limit: usize, - ) -> NormalizedLawCoverageCategory { - let mut missing_subjects = self.missing_subjects.clone(); - missing_subjects.sort(); - let missing_count = missing_subjects.len(); - let displayed_missing_subjects = missing_subjects - .iter() - .take(missing_subject_display_limit) - .cloned() - .collect::>(); - - NormalizedLawCoverageCategory { - category_ref: format!("lawCoverage.categories[{category_index}]"), - category_index, - id: self.id.clone(), - label: self.label.clone(), - required: self.required, - total: self.total, - covered: self.covered, - percent: percentage(self.covered, self.total), - missing_count, - missing_subjects, - displayed_missing_subjects, - omitted_missing_subject_count: missing_count - .saturating_sub(missing_subject_display_limit), - } - } -} - -/// Normalized coverage evidence for one profile. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct NormalizedLawCoverageProfile { - /// Coverage profile identifier. - pub profile: String, - /// Total number of required subjects considered by the profile. - pub required_total: usize, - /// Number of required subjects covered by law. - pub required_covered: usize, - /// Required-subject coverage percentage rounded like Wesley CLI output. - pub required_percent: f64, - /// Deterministically normalized category records. - pub categories: Vec, -} - -impl NormalizedLawCoverageProfile { - /// Return a normalized category by stable id. - pub fn category(&self, category_id: &str) -> Option<&NormalizedLawCoverageCategory> { - self.categories - .iter() - .find(|category| category.id == category_id) - } -} - -/// Normalized per-category coverage evidence. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct NormalizedLawCoverageCategory { - /// Stable category reference inside the parsed law coverage report. - pub category_ref: String, - /// Zero-based category index in Wesley's emitted coverage order. - pub category_index: usize, - /// Stable category identifier. - pub id: String, - /// Human-readable category label. - pub label: String, - /// Whether this category is required in the active profile. - pub required: bool, - /// Total subjects considered in this category. - pub total: usize, - /// Covered subjects in this category. - pub covered: usize, - /// Category coverage percentage rounded like Wesley CLI output. - pub percent: f64, - /// Total missing subject count. - pub missing_count: usize, - /// All missing subject coordinates, sorted for deterministic reporting. - pub missing_subjects: Vec, - /// Missing subject coordinates retained for inline display. - pub displayed_missing_subjects: Vec, - /// Missing subject count omitted from inline display. - pub omitted_missing_subject_count: usize, -} - -/// Calculate Wesley's one-decimal coverage percentage. -pub fn percentage(covered: usize, total: usize) -> f64 { - if total == 0 { - 100.0 - } else { - ((covered as f64 / total as f64) * 1000.0).round() / 10.0 - } -} diff --git a/crates/wesley-holmes/src/domain/law_coverage_gate.rs b/crates/wesley-holmes/src/domain/law_coverage_gate.rs deleted file mode 100644 index f07010b6..00000000 --- a/crates/wesley-holmes/src/domain/law_coverage_gate.rs +++ /dev/null @@ -1,286 +0,0 @@ -//! Coverage gate decisions derived from normalized Wesley law coverage evidence. - -use serde::{Deserialize, Serialize}; - -use super::law_coverage::NormalizedLawCoverageProfile; - -/// Coverage gate state. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub enum LawCoverageGateState { - /// Coverage satisfies policy. - Pass, - /// Coverage is below an advisory threshold. - Warn, - /// Coverage is below a required threshold. - Fail, - /// Coverage evidence or category evidence is unavailable. - Unavailable, -} - -impl LawCoverageGateState { - /// Return the stable text label for renderer-neutral output. - pub fn label(self) -> &'static str { - match self { - Self::Pass => "pass", - Self::Warn => "warn", - Self::Fail => "fail", - Self::Unavailable => "unavailable", - } - } -} - -/// Policy behavior when the coverage artifact is unavailable. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub enum CoverageUnavailableBehavior { - /// Emit an unavailable gate decision. - Unavailable, - /// Treat unavailable coverage as a failed gate. - Fail, -} - -/// Policy behavior when a category is absent from available coverage evidence. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub enum CoverageAbsentCategoryBehavior { - /// Emit an unavailable gate decision. - Unavailable, - /// Treat the absent category as a failed gate. - Fail, -} - -/// Minimal coverage threshold policy input for one profile. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct LawCoverageGatePolicy { - /// Profile evaluated by this policy. - pub profile: String, - /// Category thresholds evaluated for the profile. - pub categories: Vec, - /// Number of missing subjects to include in each decision. - pub missing_subject_display_limit: usize, - /// Behavior when coverage evidence is unavailable. - pub unavailable_behavior: CoverageUnavailableBehavior, - /// Behavior when a category is absent from available coverage evidence. - pub absent_category_behavior: CoverageAbsentCategoryBehavior, - /// Optional evidence artifact reference. - #[serde(skip_serializing_if = "Option::is_none")] - pub evidence_ref: Option, -} - -/// Minimal threshold policy for one coverage category. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct LawCoverageCategoryThreshold { - /// Stable category identifier. - pub category_id: String, - /// Whether this category is required by policy. - pub required: bool, - /// Advisory threshold that produces a warning below this percentage. - #[serde(skip_serializing_if = "Option::is_none")] - pub warning_threshold: Option, - /// Required threshold that produces a failure below this percentage. - #[serde(skip_serializing_if = "Option::is_none")] - pub failure_threshold: Option, -} - -/// Coverage gate decision for one profile/category pair. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct LawCoverageGateDecision { - /// Stable gate id. - pub gate_id: String, - /// Evaluated profile. - pub profile: String, - /// Evaluated category id. - pub category_id: String, - /// Human-readable category label when evidence supplied it. - #[serde(skip_serializing_if = "Option::is_none")] - pub category_label: Option, - /// Gate state. - pub state: LawCoverageGateState, - /// Stable text state label. - pub state_label: String, - /// Whether this category is required by policy or evidence. - pub required: bool, - /// Covered subject count when evidence is available. - #[serde(skip_serializing_if = "Option::is_none")] - pub covered: Option, - /// Total subject count when evidence is available. - #[serde(skip_serializing_if = "Option::is_none")] - pub total: Option, - /// Actual one-decimal coverage percentage when evidence is available. - #[serde(skip_serializing_if = "Option::is_none")] - pub actual_percent: Option, - /// Advisory threshold from policy. - #[serde(skip_serializing_if = "Option::is_none")] - pub warning_threshold: Option, - /// Required threshold from policy. - #[serde(skip_serializing_if = "Option::is_none")] - pub failure_threshold: Option, - /// Missing subject coordinates retained for inline display. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub missing_subjects: Vec, - /// Missing subject count omitted from inline display. - pub omitted_missing_subject_count: usize, - /// Optional evidence artifact reference. - #[serde(skip_serializing_if = "Option::is_none")] - pub evidence_ref: Option, - /// Renderer-neutral rationale for the decision. - pub rationale: String, -} - -/// Evaluate coverage evidence against a minimal threshold policy. -pub fn evaluate_law_coverage_gates( - coverage: Option<&NormalizedLawCoverageProfile>, - policy: &LawCoverageGatePolicy, -) -> Vec { - policy - .categories - .iter() - .map(|threshold| evaluate_category(coverage, policy, threshold)) - .collect() -} - -fn evaluate_category( - coverage: Option<&NormalizedLawCoverageProfile>, - policy: &LawCoverageGatePolicy, - threshold: &LawCoverageCategoryThreshold, -) -> LawCoverageGateDecision { - let gate_id = format!("law-coverage:{}:{}", policy.profile, threshold.category_id); - let Some(coverage) = coverage else { - let state = match policy.unavailable_behavior { - CoverageUnavailableBehavior::Unavailable => LawCoverageGateState::Unavailable, - CoverageUnavailableBehavior::Fail => LawCoverageGateState::Fail, - }; - return unavailable_decision( - gate_id, - policy, - threshold, - state, - "coverage evidence is unavailable", - ); - }; - - if coverage.profile != policy.profile { - let state = match policy.unavailable_behavior { - CoverageUnavailableBehavior::Unavailable => LawCoverageGateState::Unavailable, - CoverageUnavailableBehavior::Fail => LawCoverageGateState::Fail, - }; - return unavailable_decision( - gate_id, - policy, - threshold, - state, - "coverage evidence profile does not match policy profile", - ); - } - - let Some(category) = coverage.category(&threshold.category_id) else { - let state = match policy.absent_category_behavior { - CoverageAbsentCategoryBehavior::Unavailable => LawCoverageGateState::Unavailable, - CoverageAbsentCategoryBehavior::Fail => LawCoverageGateState::Fail, - }; - return unavailable_decision( - gate_id, - policy, - threshold, - state, - "coverage category is absent from evidence", - ); - }; - - let state = if threshold - .failure_threshold - .is_some_and(|failure_threshold| category.percent < failure_threshold) - { - LawCoverageGateState::Fail - } else if threshold - .warning_threshold - .is_some_and(|warning_threshold| category.percent < warning_threshold) - { - LawCoverageGateState::Warn - } else { - LawCoverageGateState::Pass - }; - - let displayed_missing_subjects = category - .missing_subjects - .iter() - .take(policy.missing_subject_display_limit) - .cloned() - .collect::>(); - let omitted_missing_subject_count = category - .missing_count - .saturating_sub(displayed_missing_subjects.len()); - - LawCoverageGateDecision { - gate_id, - profile: policy.profile.clone(), - category_id: threshold.category_id.clone(), - category_label: Some(category.label.clone()), - state, - state_label: state.label().to_owned(), - required: threshold.required || category.required, - covered: Some(category.covered), - total: Some(category.total), - actual_percent: Some(category.percent), - warning_threshold: threshold.warning_threshold, - failure_threshold: threshold.failure_threshold, - missing_subjects: displayed_missing_subjects, - omitted_missing_subject_count, - evidence_ref: policy.evidence_ref.clone(), - rationale: rationale_for_state(state, category.percent, threshold), - } -} - -fn unavailable_decision( - gate_id: String, - policy: &LawCoverageGatePolicy, - threshold: &LawCoverageCategoryThreshold, - state: LawCoverageGateState, - rationale: &'static str, -) -> LawCoverageGateDecision { - LawCoverageGateDecision { - gate_id, - profile: policy.profile.clone(), - category_id: threshold.category_id.clone(), - category_label: None, - state, - state_label: state.label().to_owned(), - required: threshold.required, - covered: None, - total: None, - actual_percent: None, - warning_threshold: threshold.warning_threshold, - failure_threshold: threshold.failure_threshold, - missing_subjects: Vec::new(), - omitted_missing_subject_count: 0, - evidence_ref: policy.evidence_ref.clone(), - rationale: rationale.to_owned(), - } -} - -fn rationale_for_state( - state: LawCoverageGateState, - actual_percent: f64, - threshold: &LawCoverageCategoryThreshold, -) -> String { - match state { - LawCoverageGateState::Pass => "coverage satisfies configured thresholds".to_owned(), - LawCoverageGateState::Warn => format!( - "coverage {actual_percent:.1}% is below warning threshold {:.1}%", - threshold - .warning_threshold - .expect("warning state requires warning threshold") - ), - LawCoverageGateState::Fail => format!( - "coverage {actual_percent:.1}% is below failure threshold {:.1}%", - threshold - .failure_threshold - .expect("failure state requires failure threshold") - ), - LawCoverageGateState::Unavailable => "coverage evidence is unavailable".to_owned(), - } -} diff --git a/crates/wesley-holmes/src/domain/law_diff.rs b/crates/wesley-holmes/src/domain/law_diff.rs deleted file mode 100644 index 46b8a1b8..00000000 --- a/crates/wesley-holmes/src/domain/law_diff.rs +++ /dev/null @@ -1,301 +0,0 @@ -//! Typed Wesley law diff evidence accepted by Holmes. - -use serde::{Deserialize, Serialize}; - -/// API version supported by the first Holmes law diff ingest port. -pub const WESLEY_LAW_DIFF_API_VERSION: &str = "wesley.law-diff/v1"; - -/// Machine-readable semantic diff report emitted by `wesley law diff --json`. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct LawDiffReport { - /// Report API version. - pub api_version: String, - /// Old document schema hash anchor. - pub old_schema_hash: String, - /// New document schema hash anchor. - pub new_schema_hash: String, - /// Old semantic Law IR hash. - pub old_law_hash: String, - /// New semantic Law IR hash. - pub new_law_hash: String, - /// Semantic change events in Wesley's emitted review order. - pub changes: Vec, -} - -impl LawDiffReport { - /// Normalize emitted diff events into stable Holmes event records. - pub fn normalized_events(&self) -> Vec { - self.changes - .iter() - .enumerate() - .map(|(event_index, event)| NormalizedLawDiffEvent { - event_ref: format!("lawDiff.changes[{event_index}]"), - event_index, - api_version: self.api_version.clone(), - old_schema_hash: self.old_schema_hash.clone(), - new_schema_hash: self.new_schema_hash.clone(), - old_law_hash: self.old_law_hash.clone(), - new_law_hash: self.new_law_hash.clone(), - kind: event.kind, - law_id: event.law_id.clone(), - subject: event.subject.clone(), - law_kind: event.law_kind, - review_posture: event.review_posture, - field_changes: event.field_changes.clone(), - added_reads: event.added_reads.clone(), - removed_reads: event.removed_reads.clone(), - added_writes: event.added_writes.clone(), - removed_writes: event.removed_writes.clone(), - added_creates: event.added_creates.clone(), - removed_creates: event.removed_creates.clone(), - added_forbids: event.added_forbids.clone(), - removed_forbids: event.removed_forbids.clone(), - }) - .collect() - } -} - -/// Stable internal Holmes record for one Wesley law diff event. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct NormalizedLawDiffEvent { - /// Stable event reference inside the parsed law diff report. - pub event_ref: String, - /// Zero-based event index in Wesley's emitted diff order. - pub event_index: usize, - /// Report API version. - pub api_version: String, - /// Old document schema hash anchor. - pub old_schema_hash: String, - /// New document schema hash anchor. - pub new_schema_hash: String, - /// Old semantic Law IR hash. - pub old_law_hash: String, - /// New semantic Law IR hash. - pub new_law_hash: String, - /// Event classification supplied by Wesley. - pub kind: LawDiffEventKind, - /// Stable law id affected by the event. - #[serde(skip_serializing_if = "Option::is_none")] - pub law_id: Option, - /// Subject coordinate affected by the event. - #[serde(skip_serializing_if = "Option::is_none")] - pub subject: Option, - /// Law kind affected by the event. - #[serde(skip_serializing_if = "Option::is_none")] - pub law_kind: Option, - /// Review posture emitted by Wesley. - pub review_posture: LawDiffReviewPosture, - /// Field-level changes when a law body changed. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub field_changes: Vec, - /// Footprint resources newly read. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub added_reads: Vec, - /// Footprint resources no longer read. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub removed_reads: Vec, - /// Footprint resources newly written. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub added_writes: Vec, - /// Footprint resources no longer written. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub removed_writes: Vec, - /// Footprint resources newly created. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub added_creates: Vec, - /// Footprint resources no longer created. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub removed_creates: Vec, - /// Footprint resources newly forbidden. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub added_forbids: Vec, - /// Footprint resources no longer forbidden. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub removed_forbids: Vec, -} - -/// Single semantic law diff event preserved from Wesley output. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct LawDiffEvent { - /// Event classification supplied by Wesley. - pub kind: LawDiffEventKind, - /// Stable law id affected by the event. - #[serde(skip_serializing_if = "Option::is_none")] - pub law_id: Option, - /// Subject coordinate affected by the event. - #[serde(skip_serializing_if = "Option::is_none")] - pub subject: Option, - /// Law kind affected by the event. - #[serde(skip_serializing_if = "Option::is_none")] - pub law_kind: Option, - /// Review posture emitted by Wesley. - pub review_posture: LawDiffReviewPosture, - /// Field-level changes when a law body changed. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub field_changes: Vec, - /// Footprint resources newly read. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub added_reads: Vec, - /// Footprint resources no longer read. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub removed_reads: Vec, - /// Footprint resources newly written. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub added_writes: Vec, - /// Footprint resources no longer written. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub removed_writes: Vec, - /// Footprint resources newly created. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub added_creates: Vec, - /// Footprint resources no longer created. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub removed_creates: Vec, - /// Footprint resources newly forbidden. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub added_forbids: Vec, - /// Footprint resources no longer forbidden. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub removed_forbids: Vec, -} - -/// Law diff event classifications emitted by Wesley. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] -#[serde(rename_all = "SCREAMING_SNAKE_CASE")] -pub enum LawDiffEventKind { - /// Bundle-level semantic fields changed. - LawBundleChanged, - /// Semantic registry facts changed. - RegistryChanged, - /// New active law entry. - LawAdded, - /// Active law entry removed. - LawRemoved, - /// Existing law tags changed. - LawTagsChanged, - /// Existing law was monotonically strengthened. - LawStrengthened, - /// Existing law was monotonically weakened. - LawWeakened, - /// Existing law changed outside a narrower v1 event class. - LawChanged, - /// Scalar semantic body changed. - ScalarSemanticsChanged, - /// Variant law body changed. - VariantLawChanged, - /// Footprint reach expanded. - FootprintExpanded, - /// Footprint reach contracted. - FootprintContracted, - /// Footprint changed in mixed or structural ways. - FootprintChanged, - /// Channel version changed. - ChannelVersionChanged, - /// Channel law changed without a channel-version change. - ChannelLawChanged, - /// Typed invariant predicate changed. - PredicateChanged, - /// A law no longer binds to the active schema or law registry. - BindingBroken, - /// Schema hash anchor changed. - SchemaHashRebound, -} - -impl LawDiffEventKind { - /// Return Wesley's stable event-kind string. - pub fn as_str(self) -> &'static str { - match self { - Self::LawBundleChanged => "LAW_BUNDLE_CHANGED", - Self::RegistryChanged => "REGISTRY_CHANGED", - Self::LawAdded => "LAW_ADDED", - Self::LawRemoved => "LAW_REMOVED", - Self::LawTagsChanged => "LAW_TAGS_CHANGED", - Self::LawStrengthened => "LAW_STRENGTHENED", - Self::LawWeakened => "LAW_WEAKENED", - Self::LawChanged => "LAW_CHANGED", - Self::ScalarSemanticsChanged => "SCALAR_SEMANTICS_CHANGED", - Self::VariantLawChanged => "VARIANT_LAW_CHANGED", - Self::FootprintExpanded => "FOOTPRINT_EXPANDED", - Self::FootprintContracted => "FOOTPRINT_CONTRACTED", - Self::FootprintChanged => "FOOTPRINT_CHANGED", - Self::ChannelVersionChanged => "CHANNEL_VERSION_CHANGED", - Self::ChannelLawChanged => "CHANNEL_LAW_CHANGED", - Self::PredicateChanged => "PREDICATE_CHANGED", - Self::BindingBroken => "BINDING_BROKEN", - Self::SchemaHashRebound => "SCHEMA_HASH_REBOUND", - } - } - - /// Parse Wesley's stable event-kind string. - pub fn parse(value: &str) -> Option { - match value { - "LAW_BUNDLE_CHANGED" => Some(Self::LawBundleChanged), - "REGISTRY_CHANGED" => Some(Self::RegistryChanged), - "LAW_ADDED" => Some(Self::LawAdded), - "LAW_REMOVED" => Some(Self::LawRemoved), - "LAW_TAGS_CHANGED" => Some(Self::LawTagsChanged), - "LAW_STRENGTHENED" => Some(Self::LawStrengthened), - "LAW_WEAKENED" => Some(Self::LawWeakened), - "LAW_CHANGED" => Some(Self::LawChanged), - "SCALAR_SEMANTICS_CHANGED" => Some(Self::ScalarSemanticsChanged), - "VARIANT_LAW_CHANGED" => Some(Self::VariantLawChanged), - "FOOTPRINT_EXPANDED" => Some(Self::FootprintExpanded), - "FOOTPRINT_CONTRACTED" => Some(Self::FootprintContracted), - "FOOTPRINT_CHANGED" => Some(Self::FootprintChanged), - "CHANNEL_VERSION_CHANGED" => Some(Self::ChannelVersionChanged), - "CHANNEL_LAW_CHANGED" => Some(Self::ChannelLawChanged), - "PREDICATE_CHANGED" => Some(Self::PredicateChanged), - "BINDING_BROKEN" => Some(Self::BindingBroken), - "SCHEMA_HASH_REBOUND" => Some(Self::SchemaHashRebound), - _ => None, - } - } -} - -/// Law body kind affected by a semantic diff event. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub enum LawDiffLawKind { - /// Scalar semantic law. - ScalarSemantics, - /// Variant or discriminated-input law. - VariantLaw, - /// Operation footprint law. - FootprintLaw, - /// Channel or protocol law. - ChannelLaw, - /// Typed invariant law. - InvariantLaw, -} - -/// Review posture supplied by Wesley for a diff event. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub enum LawDiffReviewPosture { - /// The semantic change requires review. - RequiresReview, -} - -impl LawDiffReviewPosture { - /// Return Wesley's stable review-posture string. - pub fn as_str(self) -> &'static str { - match self { - Self::RequiresReview => "requires-review", - } - } -} - -/// Field-level semantic law diff. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct LawDiffFieldChange { - /// Law body path that changed. - pub path: String, - /// Previous canonical value. - pub old: serde_json::Value, - /// New canonical value. - pub new: serde_json::Value, -} diff --git a/crates/wesley-holmes/src/domain/mod.rs b/crates/wesley-holmes/src/domain/mod.rs deleted file mode 100644 index 0a09f237..00000000 --- a/crates/wesley-holmes/src/domain/mod.rs +++ /dev/null @@ -1,68 +0,0 @@ -//! Pure Holmes law-assurance domain model. -//! -//! Domain code owns data, deterministic validation, and diagnostics. It must -//! not import ambient filesystem, network, process, GitHub, MCP, or wall-clock -//! dependencies. - -mod assessment; -mod contract_manifest; -mod diagnostic; -mod evidence; -mod finding; -mod law_capability; -mod law_coverage; -mod law_coverage_gate; -mod law_diff; -mod policy; -mod versioning; - -pub use assessment::{ - aggregate_law_assurance_assessment, bounded_finding_summary, evaluate_bundle_traceability, - law_assurance_provenance_report, BoundedFindingSummary, BundleTraceabilityCheck, - BundleTraceabilityGateDecision, BundleTraceabilityGateState, LawAssuranceArtifactProvenance, - LawAssuranceAssessmentOutcome, LawAssuranceAssessmentSummary, LawAssuranceProvenanceReport, -}; -pub use contract_manifest::{ - ContractBundleManifest, NormalizedContractBundleProvenance, - WESLEY_CONTRACT_BUNDLE_HASH_INPUT_CODEC, WESLEY_CONTRACT_BUNDLE_MANIFEST_API_VERSION, - WESLEY_LAW_IR_CANONICAL_JSON_CODEC, -}; -pub use diagnostic::{HolmesDiagnostic, HolmesDiagnosticCode, HolmesResult, HolmesSeverity}; -pub use evidence::{ - ArtifactRef, ArtifactRequirement, BundleArtifactRef, BundleProvenance, HolmesLawEvidenceBundle, - LawEvidenceArtifacts, LawEvidenceValidationResult, LawEvidenceValidationStatus, - LoadedArtifactMetadata, -}; -pub use finding::{ - default_severity_for_event, semantic_change_findings_from_law_diff, - sort_semantic_change_findings, LawFindingSeverity, SemanticChangeFinding, -}; -pub use law_capability::{ - LawCapabilityClosure, LawCapabilityFootprint, LawCapabilityReport, LawCapabilitySlot, - NormalizedLawCapabilityOperation, WESLEY_LAW_CAPABILITIES_API_VERSION, -}; -pub use law_coverage::{ - percentage, LawCoverageCategory, LawCoverageReport, NormalizedLawCoverageCategory, - NormalizedLawCoverageProfile, WESLEY_LAW_COVERAGE_API_VERSION, -}; -pub use law_coverage_gate::{ - evaluate_law_coverage_gates, CoverageAbsentCategoryBehavior, CoverageUnavailableBehavior, - LawCoverageCategoryThreshold, LawCoverageGateDecision, LawCoverageGatePolicy, - LawCoverageGateState, -}; -pub use law_diff::{ - LawDiffEvent, LawDiffEventKind, LawDiffFieldChange, LawDiffLawKind, LawDiffReport, - LawDiffReviewPosture, NormalizedLawDiffEvent, WESLEY_LAW_DIFF_API_VERSION, -}; -pub use policy::{ - apply_suppression_policy, map_semantic_finding_severities, matching_suppressions_for_finding, - normalize_law_assurance_policy, parse_law_assurance_policy, AnnotatedFinding, - LawAssuranceCoverageThresholdPolicy, LawAssurancePolicyProfile, LawAssurancePolicySchema, - LawAssuranceSuppressionMatch, LawAssuranceSuppressionRule, LawAssuranceSuppressionTarget, - LawAssuranceSuppressionTargetKind, NormalizedLawAssurancePolicy, SuppressionApplicationRecord, - SuppressionPolicyOutcome, SuppressionRejectionReason, SuppressionRejectionRecord, - HOLMES_LAW_ASSURANCE_POLICY_API_VERSION, -}; -pub use versioning::{ - ArtifactFamily, ParsedSchemaVersion, VersionCheck, VersionRegistry, VersionRequirement, -}; diff --git a/crates/wesley-holmes/src/domain/policy.rs b/crates/wesley-holmes/src/domain/policy.rs deleted file mode 100644 index 5c8246f3..00000000 --- a/crates/wesley-holmes/src/domain/policy.rs +++ /dev/null @@ -1,1133 +0,0 @@ -//! Domain-level law assurance policy parsing, normalization, and matching. - -use std::collections::{BTreeMap, BTreeSet}; - -use serde::{Deserialize, Serialize}; - -use super::diagnostic::{HolmesDiagnostic, HolmesDiagnosticCode, HolmesResult, HolmesSeverity}; -use super::finding::{LawFindingSeverity, SemanticChangeFinding}; -use super::law_coverage_gate::{ - CoverageAbsentCategoryBehavior, CoverageUnavailableBehavior, LawCoverageCategoryThreshold, - LawCoverageGatePolicy, LawCoverageGateState, -}; -use super::law_diff::LawDiffEventKind; - -/// Supported law assurance policy API version. -pub const HOLMES_LAW_ASSURANCE_POLICY_API_VERSION: &str = "holmes.law-assurance-policy/v1"; - -const DEFAULT_MISSING_SUBJECT_DISPLAY_LIMIT: usize = 25; - -/// Versioned Holmes law assurance policy envelope. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct LawAssurancePolicySchema { - /// Policy artifact API version. - pub api_version: String, - /// Default profile selected when the caller does not provide one. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub default_profile: Option, - /// Top-level severity mappings inherited by profiles. - #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] - pub severity_mappings: BTreeMap, - /// Top-level coverage gate state severity mappings inherited by profiles. - #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] - pub coverage_severity_mappings: BTreeMap, - /// Top-level severity fallback inherited by profiles. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub default_severity: Option, - /// Whether inherited severity mappings must cover every event kind. - #[serde(default)] - pub severity_mapping_exhaustive: bool, - /// Top-level coverage thresholds inherited by profiles. - #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] - pub coverage_thresholds: BTreeMap, - /// Missing-subject display limit inherited by profiles. - #[serde(default = "default_missing_subject_display_limit")] - pub missing_subject_display_limit: usize, - /// Unavailable-evidence behavior inherited by profiles. - #[serde(default = "default_unavailable_behavior")] - pub unavailable_behavior: CoverageUnavailableBehavior, - /// Absent-category behavior inherited by profiles. - #[serde(default = "default_absent_category_behavior")] - pub absent_category_behavior: CoverageAbsentCategoryBehavior, - /// Required evidence labels retained for later application services. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub required_evidence: Vec, - /// Fail-on labels retained for later application services. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub fail_on: Vec, - /// Named policy profiles. - #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] - pub profiles: BTreeMap, - /// Optional schema metadata retained for deterministic policy snapshots. - #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] - pub schema_metadata: BTreeMap, -} - -/// Profile-specific policy overlay. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct LawAssurancePolicyProfile { - /// Optional parent profile identifier. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub inherits: Option, - /// Profile-local severity mapping overrides. - #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] - pub severity_mappings: BTreeMap, - /// Profile-local coverage gate state severity overrides. - #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] - pub coverage_severity_mappings: BTreeMap, - /// Profile-local severity fallback. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub default_severity: Option, - /// Profile-local exhaustive severity setting. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub severity_mapping_exhaustive: Option, - /// Profile-local coverage threshold overrides. - #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] - pub coverage_thresholds: BTreeMap, - /// Profile-local missing-subject display limit. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub missing_subject_display_limit: Option, - /// Profile-local unavailable-evidence behavior. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub unavailable_behavior: Option, - /// Profile-local absent-category behavior. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub absent_category_behavior: Option, - /// Profile-local suppression rules. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub suppressions: Vec, - /// Gate ids that suppression and override handling must not bypass. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub non_overridable_gates: Vec, - /// Whether broad wildcard suppressions are accepted for this profile. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub allow_broad_suppressions: Option, -} - -/// Coverage threshold policy for one category. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct LawAssuranceCoverageThresholdPolicy { - /// Whether this category is required by policy. - #[serde(default)] - pub required: bool, - /// Warning threshold percentage. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub warning_threshold: Option, - /// Failure threshold percentage. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub failure_threshold: Option, -} - -/// Suppression target kind. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub enum LawAssuranceSuppressionTargetKind { - /// Match a semantic finding id exactly. - FindingId, - /// Match a gate id exactly. - GateId, - /// Match a law id exactly. - LawId, - /// Match a subject coordinate exactly. - Subject, - /// Match a coverage category id exactly. - Category, -} - -/// Target selector for one suppression rule. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct LawAssuranceSuppressionTarget { - /// Target kind. - pub kind: LawAssuranceSuppressionTargetKind, - /// Exact selector value. - pub selector: String, -} - -/// Policy-bound suppression rule. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct LawAssuranceSuppressionRule { - /// Stable suppression id. - pub id: String, - /// Target selector. - pub target: LawAssuranceSuppressionTarget, - /// Optional profile restriction. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub profile: Option, - /// Human-authored reason text. - pub reason: String, - /// Owning person or team. - pub owner: String, - /// Creation date in `YYYY-MM-DD` format. - pub created_on: String, - /// Expiration date in `YYYY-MM-DD` format. - pub expires_on: String, - /// Severities this suppression may suppress. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub allowed_severities: Vec, - /// Audit tags retained in suppression summaries. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub audit_tags: Vec, -} - -/// Matched suppression summary for one finding. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct LawAssuranceSuppressionMatch { - /// Matched suppression id. - pub suppression_id: String, - /// Owning person or team. - pub owner: String, - /// Human-authored reason text. - pub reason: String, - /// Expiration date in `YYYY-MM-DD` format. - pub expires_on: String, - /// Audit tags retained from the matched suppression. - pub audit_tags: Vec, -} - -/// Normalized profile materialized from a law assurance policy. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct NormalizedLawAssurancePolicy { - /// Policy artifact API version. - pub api_version: String, - /// Selected profile id. - pub profile: String, - /// Canonical event-kind to severity mapping. - pub severity_mappings: BTreeMap, - /// Coverage gate state to severity mapping. - pub coverage_gate_severity_mappings: BTreeMap, - /// Severity fallback used when an event kind is not explicitly mapped. - #[serde(skip_serializing_if = "Option::is_none")] - pub default_severity: Option, - /// Whether every event kind must be explicitly mapped. - pub severity_mapping_exhaustive: bool, - /// Materialized coverage gate policy for the selected profile. - pub coverage_gate_policy: LawCoverageGatePolicy, - /// Active suppression rules for the selected profile. - pub suppressions: Vec, - /// Gate ids that suppression and override handling must not bypass. - pub non_overridable_gates: Vec, - /// Whether broad wildcard suppressions are accepted for this profile. - pub allow_broad_suppressions: bool, -} - -impl NormalizedLawAssurancePolicy { - /// Return the configured severity for an event kind. - pub fn severity_for_event_kind( - &self, - event_kind: LawDiffEventKind, - ) -> HolmesResult> { - if let Some(severity) = self.severity_mappings.get(event_kind.as_str()).copied() { - return Ok(Some(severity)); - } - - if let Some(default_severity) = self.default_severity { - return Ok(Some(default_severity)); - } - - if self.severity_mapping_exhaustive { - Err(HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawSeverityUnmappedEventKind, - HolmesSeverity::Error, - format!( - "policy profile {:?} does not map event kind {}", - self.profile, - event_kind.as_str() - ), - ) - .for_family("policy") - .at_field(format!("severityMappings.{}", event_kind.as_str()))) - } else { - Ok(None) - } - } - - /// Return the configured severity for a coverage gate state. - pub fn severity_for_coverage_gate_state( - &self, - gate_state: LawCoverageGateState, - ) -> Option { - self.coverage_gate_severity_mappings - .get(gate_state.label()) - .copied() - } -} - -/// Parse a law assurance policy JSON artifact. -pub fn parse_law_assurance_policy(bytes: &[u8]) -> HolmesResult { - let value = serde_json::from_slice::(bytes).map_err(|error| { - HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawPolicyMalformedJson, - HolmesSeverity::Error, - format!("law assurance policy is not valid JSON: {error}"), - ) - .for_family("policy") - })?; - - let object = value.as_object().ok_or_else(|| { - HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawPolicyMalformedJson, - HolmesSeverity::Error, - "law assurance policy must be a JSON object", - ) - .for_family("policy") - })?; - - for key in object.keys() { - if !known_policy_fields().contains(key.as_str()) { - return Err(HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawPolicyUnknownField, - HolmesSeverity::Error, - format!("law assurance policy contains unknown top-level field {key:?}"), - ) - .for_family("policy") - .at_field(key)); - } - } - - let policy = serde_json::from_value::(value).map_err(|error| { - HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawPolicyMalformedJson, - HolmesSeverity::Error, - format!("law assurance policy does not match v1 schema: {error}"), - ) - .for_family("policy") - })?; - - if policy.api_version != HOLMES_LAW_ASSURANCE_POLICY_API_VERSION { - return Err(HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawPolicyUnsupportedVersion, - HolmesSeverity::Error, - format!( - "unsupported law assurance policy apiVersion {:?}", - policy.api_version - ), - ) - .for_family("policy") - .at_field("apiVersion")); - } - - if policy.profiles.is_empty() { - return Err(HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawPolicyMissingProfile, - HolmesSeverity::Error, - "law assurance policy requires at least one profile", - ) - .for_family("policy") - .at_field("profiles")); - } - - Ok(policy) -} - -/// Normalize a policy for one profile. -pub fn normalize_law_assurance_policy( - policy: &LawAssurancePolicySchema, - profile: Option<&str>, -) -> HolmesResult { - if policy.api_version != HOLMES_LAW_ASSURANCE_POLICY_API_VERSION { - return Err(HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawPolicyUnsupportedVersion, - HolmesSeverity::Error, - format!( - "unsupported law assurance policy apiVersion {:?}", - policy.api_version - ), - ) - .for_family("policy") - .at_field("apiVersion")); - } - - let profile_id = profile - .or(policy.default_profile.as_deref()) - .ok_or_else(|| { - HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawPolicyMissingProfile, - HolmesSeverity::Error, - "law assurance policy requires an explicit or default profile", - ) - .for_family("policy") - .at_field("defaultProfile") - })?; - - let mut visiting = BTreeSet::new(); - let materialized = materialize_profile(policy, profile_id, &mut visiting)?; - let mut categories = Vec::new(); - for (category_id, threshold) in &materialized.coverage_thresholds { - validate_threshold(category_id, threshold)?; - categories.push(LawCoverageCategoryThreshold { - category_id: category_id.clone(), - required: threshold.required, - warning_threshold: threshold.warning_threshold, - failure_threshold: threshold.failure_threshold, - }); - } - - let mut suppressions = materialized - .suppressions - .into_iter() - .filter(|rule| match rule.profile.as_deref() { - Some(rule_profile) => rule_profile == profile_id, - None => true, - }) - .collect::>(); - suppressions.sort_by(|left, right| left.id.cmp(&right.id)); - for suppression in &suppressions { - validate_suppression( - suppression, - materialized.allow_broad_suppressions, - profile_id, - )?; - } - - let mut non_overridable_gates = materialized.non_overridable_gates; - non_overridable_gates.sort(); - non_overridable_gates.dedup(); - - Ok(NormalizedLawAssurancePolicy { - api_version: policy.api_version.clone(), - profile: profile_id.to_owned(), - severity_mappings: materialized.severity_mappings, - coverage_gate_severity_mappings: materialized.coverage_gate_severity_mappings, - default_severity: materialized.default_severity, - severity_mapping_exhaustive: materialized.severity_mapping_exhaustive, - coverage_gate_policy: LawCoverageGatePolicy { - profile: profile_id.to_owned(), - categories, - missing_subject_display_limit: materialized.missing_subject_display_limit, - unavailable_behavior: materialized.unavailable_behavior, - absent_category_behavior: materialized.absent_category_behavior, - evidence_ref: None, - }, - suppressions, - non_overridable_gates, - allow_broad_suppressions: materialized.allow_broad_suppressions, - }) -} - -/// Apply policy severity mappings without changing Wesley event identity. -pub fn map_semantic_finding_severities( - findings: &[SemanticChangeFinding], - policy: &NormalizedLawAssurancePolicy, -) -> HolmesResult> { - findings - .iter() - .map(|finding| { - let mut mapped = finding.clone(); - if let Some(severity) = policy.severity_for_event_kind(finding.event_kind)? { - mapped.severity = severity; - mapped.severity_label = severity.label().to_owned(); - } - Ok(mapped) - }) - .collect() -} - -/// A semantic finding annotated with its suppression state. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct AnnotatedFinding { - /// The underlying semantic change finding. - pub finding: SemanticChangeFinding, - /// The suppression that muted this finding, if any. - #[serde(skip_serializing_if = "Option::is_none")] - pub suppressed_by: Option, -} - -impl AnnotatedFinding { - /// Return whether this finding was suppressed. - pub fn is_suppressed(&self) -> bool { - self.suppressed_by.is_some() - } -} - -/// Record of one successfully applied suppression. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SuppressionApplicationRecord { - /// Matched suppression id. - pub suppression_id: String, - /// Finding id that was muted. - pub finding_id: String, - /// Target selector that matched — determines the suppression scope. - pub target: LawAssuranceSuppressionTarget, - /// Owning person or team. - pub owner: String, - /// Human-authored reason text. - pub reason: String, - /// Creation date in `YYYY-MM-DD` format. - pub created_on: String, - /// Expiration date in `YYYY-MM-DD` format. - pub expires_on: String, - /// Audit tags retained from the suppression rule. - pub audit_tags: Vec, -} - -/// Why a suppression rule was rejected. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase", tag = "kind")] -pub enum SuppressionRejectionReason { - /// Evidence was invalid; no suppression may override an invalid bundle. - InvalidEvidence, - /// The suppression targeted a gate that policy marks non-overridable. - NonOverridableGate { - /// The protected gate id. - gate_id: String, - }, -} - -/// Record of one rejected suppression attempt. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SuppressionRejectionRecord { - /// Rejected suppression id. - pub suppression_id: String, - /// Owning person or team. - pub owner: String, - /// Target of the rejected rule. - pub target: LawAssuranceSuppressionTarget, - /// Reason this suppression was rejected. - pub rejection_reason: SuppressionRejectionReason, -} - -/// Outcome of applying suppression policy to a set of semantic findings. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SuppressionPolicyOutcome { - /// All findings annotated with their suppression state. - pub annotated_findings: Vec, - /// Suppression rules that were successfully applied to a finding. - pub applied: Vec, - /// Suppression rules that were rejected by abuse-prevention policy. - pub rejected: Vec, - /// Ids of suppression rules that were present but expired. - pub expired: Vec, - /// Diagnostics emitted for rejected and expired suppressions. - pub diagnostics: Vec, -} - -impl SuppressionPolicyOutcome { - /// Return only the findings that were not suppressed. - pub fn active_findings(&self) -> Vec<&SemanticChangeFinding> { - self.annotated_findings - .iter() - .filter(|annotated| !annotated.is_suppressed()) - .map(|annotated| &annotated.finding) - .collect() - } -} - -/// Apply suppression policy to a set of semantic findings. -/// -/// Enforces three abuse-prevention rules in order: -/// 1. Invalid evidence blocks all suppressions. -/// 2. Suppressions targeting a non-overridable gate are rejected. -/// 3. Expired suppressions are reported as diagnostics but not applied. -/// -/// # Parameters -/// -/// - `evaluation_date` — current date in **`YYYY-MM-DD`** format. Malformed input -/// (wrong separators, timestamps with a time component, etc.) returns a single -/// `HlawSuppressionInvalid` diagnostic and leaves all findings unsuppressed. -/// -/// # Matching semantics -/// -/// A suppression silences **every** finding whose `target` (kind + selector) matches, -/// not just the first. The selector kind defines the blast radius: use `finding-id` to -/// suppress a single specific finding, `law-id` or `subject` to suppress an entire class. -/// Each finding is suppressed by **at most one rule**: the first matching suppression in -/// policy declaration order wins per finding. -/// -/// # Expiry boundary -/// -/// Expiry uses strict less-than (`expires_on < evaluation_date`), so a suppression -/// is still active on its `expiresOn` date (last valid day inclusive). -pub fn apply_suppression_policy( - findings: &[SemanticChangeFinding], - validation_result: &super::evidence::LawEvidenceValidationResult, - policy: &NormalizedLawAssurancePolicy, - evaluation_date: &str, -) -> SuppressionPolicyOutcome { - use super::diagnostic::{HolmesDiagnosticCode, HolmesSeverity}; - use super::evidence::LawEvidenceValidationStatus; - - if !is_iso_date(evaluation_date) { - return SuppressionPolicyOutcome { - annotated_findings: findings - .iter() - .map(|f| AnnotatedFinding { - finding: f.clone(), - suppressed_by: None, - }) - .collect(), - applied: Vec::new(), - rejected: Vec::new(), - expired: Vec::new(), - diagnostics: vec![HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawSuppressionInvalid, - HolmesSeverity::Error, - format!( - "evaluation_date {evaluation_date:?} is not in YYYY-MM-DD format; \ - no suppressions were applied" - ), - ) - .for_family("policy") - .at_field("evaluation_date")], - }; - } - - let evidence_invalid = matches!( - validation_result.status, - LawEvidenceValidationStatus::Invalid | LawEvidenceValidationStatus::InfrastructureError, - ); - - let mut annotated_findings: Vec = findings - .iter() - .map(|finding| AnnotatedFinding { - finding: finding.clone(), - suppressed_by: None, - }) - .collect(); - - let mut applied = Vec::new(); - let mut rejected = Vec::new(); - let mut expired = Vec::new(); - let mut diagnostics = Vec::new(); - - for suppression in &policy.suppressions { - // Rule 1: invalid evidence blocks all suppressions. - if evidence_invalid { - diagnostics.push( - HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawSuppressionRejectedInvalidEvidence, - HolmesSeverity::Error, - format!( - "suppression {} was rejected: evidence validation failed \ - and suppressions cannot override invalid evidence", - suppression.id - ), - ) - .for_family("policy") - .at_field("suppressions"), - ); - rejected.push(SuppressionRejectionRecord { - suppression_id: suppression.id.clone(), - owner: suppression.owner.clone(), - target: suppression.target.clone(), - rejection_reason: SuppressionRejectionReason::InvalidEvidence, - }); - continue; - } - - // Rule 2: non-overridable gate protection. - if suppression.target.kind == LawAssuranceSuppressionTargetKind::GateId - && policy - .non_overridable_gates - .contains(&suppression.target.selector) - { - let gate_id = suppression.target.selector.clone(); - diagnostics.push( - HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawSuppressionRejectedNonOverridable, - HolmesSeverity::Error, - format!( - "suppression {} was rejected: gate {} is non-overridable", - suppression.id, gate_id, - ), - ) - .for_family("policy") - .at_field("suppressions"), - ); - rejected.push(SuppressionRejectionRecord { - suppression_id: suppression.id.clone(), - owner: suppression.owner.clone(), - target: suppression.target.clone(), - rejection_reason: SuppressionRejectionReason::NonOverridableGate { gate_id }, - }); - continue; - } - - // Rule 3: expired suppression — report but do not apply. - if suppression.expires_on.as_str() < evaluation_date { - diagnostics.push( - HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawSuppressionExpired, - HolmesSeverity::Warning, - format!( - "suppression {} expired on {} and was not applied", - suppression.id, suppression.expires_on, - ), - ) - .for_family("policy") - .at_field("suppressions"), - ); - expired.push(suppression.id.clone()); - continue; - } - - // Each suppression silences all findings it matches; each finding is suppressed by at - // most one rule (first suppression in policy order wins per finding). - for annotated in &mut annotated_findings { - if annotated.suppressed_by.is_none() - && suppression_matches_finding(suppression, &annotated.finding, evaluation_date) - { - applied.push(SuppressionApplicationRecord { - suppression_id: suppression.id.clone(), - finding_id: annotated.finding.finding_id.clone(), - target: suppression.target.clone(), - owner: suppression.owner.clone(), - reason: suppression.reason.clone(), - created_on: suppression.created_on.clone(), - expires_on: suppression.expires_on.clone(), - audit_tags: suppression.audit_tags.clone(), - }); - annotated.suppressed_by = Some(LawAssuranceSuppressionMatch { - suppression_id: suppression.id.clone(), - owner: suppression.owner.clone(), - reason: suppression.reason.clone(), - expires_on: suppression.expires_on.clone(), - audit_tags: suppression.audit_tags.clone(), - }); - } - } - } - - SuppressionPolicyOutcome { - annotated_findings, - applied, - rejected, - expired, - diagnostics, - } -} - -/// Return active suppression matches for one semantic finding. -pub fn matching_suppressions_for_finding( - finding: &SemanticChangeFinding, - policy: &NormalizedLawAssurancePolicy, - now_date: &str, -) -> Vec { - policy - .suppressions - .iter() - .filter(|suppression| suppression_matches_finding(suppression, finding, now_date)) - .map(|suppression| LawAssuranceSuppressionMatch { - suppression_id: suppression.id.clone(), - owner: suppression.owner.clone(), - reason: suppression.reason.clone(), - expires_on: suppression.expires_on.clone(), - audit_tags: suppression.audit_tags.clone(), - }) - .collect() -} - -#[derive(Debug, Clone, PartialEq)] -struct MaterializedPolicyProfile { - severity_mappings: BTreeMap, - coverage_gate_severity_mappings: BTreeMap, - default_severity: Option, - severity_mapping_exhaustive: bool, - coverage_thresholds: BTreeMap, - missing_subject_display_limit: usize, - unavailable_behavior: CoverageUnavailableBehavior, - absent_category_behavior: CoverageAbsentCategoryBehavior, - suppressions: Vec, - non_overridable_gates: Vec, - allow_broad_suppressions: bool, -} - -impl MaterializedPolicyProfile { - fn from_schema(policy: &LawAssurancePolicySchema) -> HolmesResult { - let mut severity_mappings = BTreeMap::new(); - insert_severity_mappings( - &mut severity_mappings, - &policy.severity_mappings, - "severityMappings", - )?; - let mut coverage_gate_severity_mappings = BTreeMap::new(); - insert_coverage_severity_mappings( - &mut coverage_gate_severity_mappings, - &policy.coverage_severity_mappings, - "coverageSeverityMappings", - )?; - - Ok(Self { - severity_mappings, - coverage_gate_severity_mappings, - default_severity: policy.default_severity, - severity_mapping_exhaustive: policy.severity_mapping_exhaustive, - coverage_thresholds: policy.coverage_thresholds.clone(), - missing_subject_display_limit: policy.missing_subject_display_limit, - unavailable_behavior: policy.unavailable_behavior, - absent_category_behavior: policy.absent_category_behavior, - suppressions: Vec::new(), - non_overridable_gates: Vec::new(), - allow_broad_suppressions: false, - }) - } - - fn apply_profile( - &mut self, - profile_id: &str, - profile: &LawAssurancePolicyProfile, - ) -> HolmesResult<()> { - insert_severity_mappings( - &mut self.severity_mappings, - &profile.severity_mappings, - &format!("profiles.{profile_id}.severityMappings"), - )?; - insert_coverage_severity_mappings( - &mut self.coverage_gate_severity_mappings, - &profile.coverage_severity_mappings, - &format!("profiles.{profile_id}.coverageSeverityMappings"), - )?; - - if let Some(default_severity) = profile.default_severity { - self.default_severity = Some(default_severity); - } - if let Some(exhaustive) = profile.severity_mapping_exhaustive { - self.severity_mapping_exhaustive = exhaustive; - } - self.coverage_thresholds - .extend(profile.coverage_thresholds.clone()); - if let Some(display_limit) = profile.missing_subject_display_limit { - self.missing_subject_display_limit = display_limit; - } - if let Some(unavailable_behavior) = profile.unavailable_behavior { - self.unavailable_behavior = unavailable_behavior; - } - if let Some(absent_category_behavior) = profile.absent_category_behavior { - self.absent_category_behavior = absent_category_behavior; - } - self.suppressions.extend(profile.suppressions.clone()); - self.non_overridable_gates - .extend(profile.non_overridable_gates.clone()); - if let Some(allow_broad_suppressions) = profile.allow_broad_suppressions { - self.allow_broad_suppressions = allow_broad_suppressions; - } - - Ok(()) - } -} - -fn materialize_profile( - policy: &LawAssurancePolicySchema, - profile_id: &str, - visiting: &mut BTreeSet, -) -> HolmesResult { - if !visiting.insert(profile_id.to_owned()) { - return Err(HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawPolicyCircularInheritance, - HolmesSeverity::Error, - format!("law assurance policy profile {profile_id:?} inherits circularly"), - ) - .for_family("policy") - .at_field(format!("profiles.{profile_id}.inherits"))); - } - - let profile = policy.profiles.get(profile_id).ok_or_else(|| { - HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawPolicyUnknownProfile, - HolmesSeverity::Error, - format!("law assurance policy profile {profile_id:?} is not defined"), - ) - .for_family("policy") - .at_field(format!("profiles.{profile_id}")) - })?; - - let mut materialized = if let Some(parent_id) = profile.inherits.as_deref() { - materialize_profile(policy, parent_id, visiting)? - } else { - MaterializedPolicyProfile::from_schema(policy)? - }; - - materialized.apply_profile(profile_id, profile)?; - visiting.remove(profile_id); - Ok(materialized) -} - -fn insert_severity_mappings( - target: &mut BTreeMap, - mappings: &BTreeMap, - field_prefix: &str, -) -> HolmesResult<()> { - for (raw_kind, severity) in mappings { - let event_kind = parse_policy_event_kind(raw_kind).ok_or_else(|| { - HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawPolicyUnknownEventKind, - HolmesSeverity::Error, - format!("policy severity mapping references unknown event kind {raw_kind:?}"), - ) - .for_family("policy") - .at_field(format!("{field_prefix}.{raw_kind}")) - })?; - target.insert(event_kind.as_str().to_owned(), *severity); - } - Ok(()) -} - -fn insert_coverage_severity_mappings( - target: &mut BTreeMap, - mappings: &BTreeMap, - field_prefix: &str, -) -> HolmesResult<()> { - for (raw_state, severity) in mappings { - let state = parse_coverage_gate_state(raw_state).ok_or_else(|| { - HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawPolicyUnknownEventKind, - HolmesSeverity::Error, - format!( - "policy coverage severity mapping references unknown gate state {raw_state:?}" - ), - ) - .for_family("policy") - .at_field(format!("{field_prefix}.{raw_state}")) - })?; - target.insert(state.label().to_owned(), *severity); - } - Ok(()) -} - -fn validate_threshold( - category_id: &str, - threshold: &LawAssuranceCoverageThresholdPolicy, -) -> HolmesResult<()> { - if category_id.trim().is_empty() { - return invalid_threshold( - "coverageThresholds", - "coverage category id must not be blank", - ); - } - - if let Some(warning_threshold) = threshold.warning_threshold { - validate_percent( - warning_threshold, - &format!("coverageThresholds.{category_id}.warningThreshold"), - )?; - } - if let Some(failure_threshold) = threshold.failure_threshold { - validate_percent( - failure_threshold, - &format!("coverageThresholds.{category_id}.failureThreshold"), - )?; - } - if let (Some(warning_threshold), Some(failure_threshold)) = - (threshold.warning_threshold, threshold.failure_threshold) - { - if warning_threshold < failure_threshold { - return invalid_threshold( - &format!("coverageThresholds.{category_id}.warningThreshold"), - "warning threshold must be greater than or equal to failure threshold", - ); - } - } - - Ok(()) -} - -fn validate_percent(percent: f64, field_path: &str) -> HolmesResult<()> { - if percent.is_finite() && (0.0..=100.0).contains(&percent) { - Ok(()) - } else { - invalid_threshold(field_path, "threshold percentage must be between 0 and 100") - } -} - -fn invalid_threshold(field_path: &str, message: &str) -> HolmesResult<()> { - Err(HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawPolicyInvalidThreshold, - HolmesSeverity::Error, - message, - ) - .for_family("policy") - .at_field(field_path)) -} - -fn validate_suppression( - suppression: &LawAssuranceSuppressionRule, - allow_broad_suppressions: bool, - profile_id: &str, -) -> HolmesResult<()> { - let field_prefix = format!("profiles.{profile_id}.suppressions.{}", suppression.id); - if suppression.id.trim().is_empty() { - return invalid_suppression(&field_prefix, "suppression id must not be blank"); - } - if suppression.owner.trim().is_empty() { - return invalid_suppression( - &format!("{field_prefix}.owner"), - "suppression owner must not be blank", - ); - } - if suppression.reason.trim().is_empty() { - return invalid_suppression( - &format!("{field_prefix}.reason"), - "suppression reason must not be blank", - ); - } - if suppression.target.selector.trim().is_empty() { - return invalid_suppression( - &format!("{field_prefix}.target.selector"), - "suppression selector must not be blank", - ); - } - if suppression.target.selector == "*" && !allow_broad_suppressions { - return invalid_suppression( - &format!("{field_prefix}.target.selector"), - "broad wildcard suppression is disabled for this profile", - ); - } - if suppression.allowed_severities.is_empty() { - return invalid_suppression( - &format!("{field_prefix}.allowedSeverities"), - "suppression must list allowed severities", - ); - } - if !is_iso_date(&suppression.created_on) { - return invalid_suppression( - &format!("{field_prefix}.createdOn"), - "suppression createdOn must use YYYY-MM-DD", - ); - } - if !is_iso_date(&suppression.expires_on) { - return invalid_suppression( - &format!("{field_prefix}.expiresOn"), - "suppression expiresOn must use YYYY-MM-DD", - ); - } - if suppression.expires_on < suppression.created_on { - return invalid_suppression( - &format!("{field_prefix}.expiresOn"), - "suppression expiresOn must not precede createdOn", - ); - } - - Ok(()) -} - -fn invalid_suppression(field_path: &str, message: &str) -> HolmesResult<()> { - Err(HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawSuppressionInvalid, - HolmesSeverity::Error, - message, - ) - .for_family("policy") - .at_field(field_path)) -} - -fn suppression_matches_finding( - suppression: &LawAssuranceSuppressionRule, - finding: &SemanticChangeFinding, - now_date: &str, -) -> bool { - if suppression.expires_on.as_str() < now_date { - return false; - } - if !suppression.allowed_severities.contains(&finding.severity) { - return false; - } - - match suppression.target.kind { - LawAssuranceSuppressionTargetKind::FindingId => { - finding.finding_id == suppression.target.selector - } - LawAssuranceSuppressionTargetKind::LawId => { - finding.law_id.as_deref() == Some(suppression.target.selector.as_str()) - } - LawAssuranceSuppressionTargetKind::Subject => { - finding.subject.as_deref() == Some(suppression.target.selector.as_str()) - } - LawAssuranceSuppressionTargetKind::Category | LawAssuranceSuppressionTargetKind::GateId => { - false - } - } -} - -fn parse_policy_event_kind(raw_kind: &str) -> Option { - LawDiffEventKind::parse(raw_kind).or_else(|| { - let canonical = canonical_event_kind_key(raw_kind); - LawDiffEventKind::parse(&canonical) - }) -} - -fn parse_coverage_gate_state(raw_state: &str) -> Option { - match raw_state { - "pass" | "PASS" => Some(LawCoverageGateState::Pass), - "warn" | "warning" | "WARN" | "WARNING" => Some(LawCoverageGateState::Warn), - "fail" | "failure" | "FAIL" | "FAILURE" => Some(LawCoverageGateState::Fail), - "unavailable" | "UNAVAILABLE" => Some(LawCoverageGateState::Unavailable), - _ => None, - } -} - -fn canonical_event_kind_key(raw_kind: &str) -> String { - let mut canonical = String::new(); - let mut previous_was_separator = true; - for character in raw_kind.chars() { - if matches!(character, '-' | '_' | ' ') { - if !canonical.ends_with('_') { - canonical.push('_'); - } - previous_was_separator = true; - } else if character.is_ascii_uppercase() { - if !previous_was_separator && !canonical.ends_with('_') { - canonical.push('_'); - } - canonical.push(character); - previous_was_separator = false; - } else { - canonical.push(character.to_ascii_uppercase()); - previous_was_separator = false; - } - } - canonical.trim_matches('_').to_owned() -} - -fn is_iso_date(value: &str) -> bool { - let bytes = value.as_bytes(); - bytes.len() == 10 - && bytes[4] == b'-' - && bytes[7] == b'-' - && bytes - .iter() - .enumerate() - .all(|(index, byte)| index == 4 || index == 7 || byte.is_ascii_digit()) -} - -fn known_policy_fields() -> BTreeSet<&'static str> { - [ - "apiVersion", - "defaultProfile", - "severityMappings", - "coverageSeverityMappings", - "defaultSeverity", - "severityMappingExhaustive", - "coverageThresholds", - "missingSubjectDisplayLimit", - "unavailableBehavior", - "absentCategoryBehavior", - "requiredEvidence", - "failOn", - "profiles", - "schemaMetadata", - ] - .into_iter() - .collect() -} - -fn default_missing_subject_display_limit() -> usize { - DEFAULT_MISSING_SUBJECT_DISPLAY_LIMIT -} - -fn default_unavailable_behavior() -> CoverageUnavailableBehavior { - CoverageUnavailableBehavior::Unavailable -} - -fn default_absent_category_behavior() -> CoverageAbsentCategoryBehavior { - CoverageAbsentCategoryBehavior::Unavailable -} diff --git a/crates/wesley-holmes/src/domain/versioning.rs b/crates/wesley-holmes/src/domain/versioning.rs deleted file mode 100644 index 94dcc2d4..00000000 --- a/crates/wesley-holmes/src/domain/versioning.rs +++ /dev/null @@ -1,309 +0,0 @@ -//! Schema-version registry for Holmes artifact families. - -use std::collections::BTreeMap; - -use serde::{Deserialize, Serialize}; - -use super::diagnostic::{HolmesDiagnostic, HolmesDiagnosticCode, HolmesResult, HolmesSeverity}; - -/// Artifact families that Holmes accepts at its ingest boundary. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub enum ArtifactFamily { - /// Law evidence bundle envelope. - EvidenceBundle, - /// Machine-readable law diff artifact. - LawDiff, - /// Law coverage artifact. - LawCoverage, - /// Law capability summary artifact. - LawCapabilities, - /// Contract bundle manifest artifact. - ContractBundleManifest, - /// Assurance policy artifact. - Policy, - /// Rendered or structured assurance report artifact. - Report, - /// Audit witness artifact. - AuditWitness, - /// MCP response payload artifact. - McpResponse, - /// Agent summary payload artifact. - AgentSummary, - /// GitHub PR comment or review payload artifact. - GithubPayload, -} - -impl ArtifactFamily { - /// Return the stable artifact-family identifier. - pub fn id(self) -> &'static str { - match self { - ArtifactFamily::EvidenceBundle => "evidence-bundle", - ArtifactFamily::LawDiff => "law-diff", - ArtifactFamily::LawCoverage => "law-coverage", - ArtifactFamily::LawCapabilities => "law-capabilities", - ArtifactFamily::ContractBundleManifest => "contract-bundle-manifest", - ArtifactFamily::Policy => "policy", - ArtifactFamily::Report => "report", - ArtifactFamily::AuditWitness => "audit-witness", - ArtifactFamily::McpResponse => "mcp-response", - ArtifactFamily::AgentSummary => "agent-summary", - ArtifactFamily::GithubPayload => "github-payload", - } - } - - fn all() -> [ArtifactFamily; 11] { - [ - ArtifactFamily::EvidenceBundle, - ArtifactFamily::LawDiff, - ArtifactFamily::LawCoverage, - ArtifactFamily::LawCapabilities, - ArtifactFamily::ContractBundleManifest, - ArtifactFamily::Policy, - ArtifactFamily::Report, - ArtifactFamily::AuditWitness, - ArtifactFamily::McpResponse, - ArtifactFamily::AgentSummary, - ArtifactFamily::GithubPayload, - ] - } -} - -/// Parsed three-part semantic schema version. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ParsedSchemaVersion { - /// Major version. - pub major: u64, - /// Minor version. - pub minor: u64, - /// Patch version. - pub patch: u64, -} - -impl ParsedSchemaVersion { - /// Parse a strict `MAJOR.MINOR.PATCH` schema version. - pub fn parse(value: &str) -> HolmesResult { - let parts = value.split('.').collect::>(); - if parts.len() != 3 || parts.iter().any(|part| part.is_empty()) { - return Err(malformed_version(value)); - } - - let parse_part = |part: &str| { - if !part.bytes().all(|byte| byte.is_ascii_digit()) { - return Err(malformed_version(value)); - } - if part.len() > 1 && part.starts_with('0') { - return Err(malformed_version(value)); - } - part.parse::().map_err(|_| malformed_version(value)) - }; - - Ok(Self { - major: parse_part(parts[0])?, - minor: parse_part(parts[1])?, - patch: parse_part(parts[2])?, - }) - } -} - -/// Accepted version requirement for one artifact family. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct VersionRequirement { - /// Artifact family covered by the requirement. - pub family: ArtifactFamily, - /// Accepted major version. - pub major: u64, - /// Highest accepted minor version for the accepted major. - pub max_minor: u64, - /// Highest accepted minor version that should be reported as deprecated. - #[serde(skip_serializing_if = "Option::is_none")] - pub deprecated_minor_through: Option, -} - -impl VersionRequirement { - /// Create a version requirement. - pub fn new(family: ArtifactFamily, major: u64, max_minor: u64) -> Self { - Self { - family, - major, - max_minor, - deprecated_minor_through: None, - } - } - - /// Mark accepted versions at or below a minor version as deprecated. - pub fn with_deprecated_minor_through(mut self, minor: u64) -> Self { - self.deprecated_minor_through = Some(minor); - self - } -} - -/// Schema-version validation details for an accepted artifact version. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct VersionCheck { - /// Parsed schema version. - pub parsed: ParsedSchemaVersion, - /// Non-fatal diagnostics, such as deprecation warnings. - pub diagnostics: Vec, -} - -/// Local registry of accepted artifact-family schema versions. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct VersionRegistry { - requirements: BTreeMap, -} - -impl VersionRegistry { - /// Create a registry from explicit requirements. - pub fn new(requirements: impl IntoIterator) -> Self { - Self { - requirements: requirements - .into_iter() - .map(|requirement| (requirement.family, requirement)) - .collect(), - } - } - - /// Return a registry with one requirement inserted or replaced. - pub fn with_requirement(mut self, requirement: VersionRequirement) -> Self { - self.requirements.insert(requirement.family, requirement); - self - } - - /// Return the requirement for an artifact family. - pub fn requirement(&self, family: ArtifactFamily) -> Option { - self.requirements.get(&family).copied() - } - - /// Validate a schema version for an artifact family. - pub fn validate( - &self, - family: ArtifactFamily, - schema_version: Option<&str>, - ) -> HolmesResult { - Ok(self.classify(family, schema_version)?.parsed) - } - - /// Validate and classify a schema version for an artifact family. - pub fn classify( - &self, - family: ArtifactFamily, - schema_version: Option<&str>, - ) -> HolmesResult { - let Some(raw_version) = schema_version else { - return Err(missing_version(family)); - }; - - if raw_version.trim().is_empty() { - return Err(missing_version(family)); - } - - let parsed = ParsedSchemaVersion::parse(raw_version) - .map_err(|diagnostic| diagnostic.for_family(family.id()))?; - let requirement = self - .requirement(family) - .ok_or_else(|| missing_requirement(family))?; - - if parsed.major != requirement.major { - return Err(HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawSchemaVersionUnsupportedMajor, - HolmesSeverity::Error, - format!( - "unsupported {} schemaVersion major {}; expected {}", - family.id(), - parsed.major, - requirement.major - ), - ) - .for_family(family.id()) - .at_field("schemaVersion")); - } - - if parsed.minor > requirement.max_minor { - return Err(HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawSchemaVersionUnsupportedMinor, - HolmesSeverity::Error, - format!( - "unsupported {} schemaVersion minor {}; maximum accepted minor is {}", - family.id(), - parsed.minor, - requirement.max_minor - ), - ) - .for_family(family.id()) - .at_field("schemaVersion")); - } - - let mut diagnostics = Vec::new(); - if requirement - .deprecated_minor_through - .is_some_and(|minor| parsed.minor <= minor) - { - diagnostics.push( - HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawSchemaVersionDeprecated, - HolmesSeverity::Warning, - format!( - "{} schemaVersion {}.{}.{} is accepted but deprecated", - family.id(), - parsed.major, - parsed.minor, - parsed.patch - ), - ) - .for_family(family.id()) - .at_field("schemaVersion"), - ); - } - - Ok(VersionCheck { - parsed, - diagnostics, - }) - } -} - -impl Default for VersionRegistry { - fn default() -> Self { - Self::new( - ArtifactFamily::all() - .into_iter() - .map(|family| VersionRequirement::new(family, 1, 0)), - ) - } -} - -fn missing_version(family: ArtifactFamily) -> HolmesDiagnostic { - HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawSchemaVersionMissing, - HolmesSeverity::Error, - format!("{} artifact is missing schemaVersion", family.id()), - ) - .for_family(family.id()) - .at_field("schemaVersion") -} - -fn malformed_version(value: &str) -> HolmesDiagnostic { - HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawSchemaVersionMalformed, - HolmesSeverity::Error, - format!("schemaVersion must use MAJOR.MINOR.PATCH digits, got {value:?}"), - ) - .at_field("schemaVersion") -} - -fn missing_requirement(family: ArtifactFamily) -> HolmesDiagnostic { - HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawSchemaVersionRequirementMissing, - HolmesSeverity::Error, - format!( - "no schemaVersion requirement is configured for {}", - family.id() - ), - ) - .for_family(family.id()) - .at_field("schemaVersion") -} diff --git a/crates/wesley-holmes/src/lib.rs b/crates/wesley-holmes/src/lib.rs deleted file mode 100644 index 59f0f61d..00000000 --- a/crates/wesley-holmes/src/lib.rs +++ /dev/null @@ -1,60 +0,0 @@ -#![deny(warnings)] -#![deny(missing_docs)] - -//! Rust Holmes law assurance foundation for Wesley. -//! -//! This crate hosts the new Holmes boundary that consumes Wesley-published law -//! evidence. The first implementation slice keeps the domain pure, exposes -//! deterministic ports, and validates artifact-family version envelopes without -//! adding public CLI commands. - -pub mod adapters; -pub mod application; -pub mod domain; -pub mod ports; -pub mod reporting; - -pub use application::{ - ContractBundleManifestIngestPort, ContractBundleManifestIngestResult, - ContractBundleManifestIngestStatus, JsonContractBundleManifestIngestPort, - JsonLawCapabilityIngestPort, JsonLawCoverageIngestPort, JsonLawDiffIngestPort, - LawCapabilityIngestPort, LawCapabilityIngestResult, LawCapabilityIngestStatus, - LawCoverageIngestPort, LawCoverageIngestResult, LawCoverageIngestStatus, LawDiffIngestPort, - LawDiffIngestResult, LawDiffIngestStatus, LawEvidenceValidator, ResolvedArtifactPath, - WeslawArtifactLocator, -}; -pub use domain::{ - aggregate_law_assurance_assessment, apply_suppression_policy, bounded_finding_summary, - default_severity_for_event, evaluate_bundle_traceability, evaluate_law_coverage_gates, - law_assurance_provenance_report, map_semantic_finding_severities, - matching_suppressions_for_finding, normalize_law_assurance_policy, parse_law_assurance_policy, - percentage, semantic_change_findings_from_law_diff, sort_semantic_change_findings, - AnnotatedFinding, ArtifactFamily, ArtifactRef, ArtifactRequirement, BoundedFindingSummary, - BundleArtifactRef, BundleProvenance, BundleTraceabilityCheck, BundleTraceabilityGateDecision, - BundleTraceabilityGateState, ContractBundleManifest, CoverageAbsentCategoryBehavior, - CoverageUnavailableBehavior, HolmesDiagnostic, HolmesDiagnosticCode, HolmesLawEvidenceBundle, - HolmesResult, HolmesSeverity, LawAssuranceArtifactProvenance, LawAssuranceAssessmentOutcome, - LawAssuranceAssessmentSummary, LawAssuranceCoverageThresholdPolicy, LawAssurancePolicyProfile, - LawAssurancePolicySchema, LawAssuranceProvenanceReport, LawAssuranceSuppressionMatch, - LawAssuranceSuppressionRule, LawAssuranceSuppressionTarget, LawAssuranceSuppressionTargetKind, - LawCapabilityClosure, LawCapabilityFootprint, LawCapabilityReport, LawCapabilitySlot, - LawCoverageCategory, LawCoverageCategoryThreshold, LawCoverageGateDecision, - LawCoverageGatePolicy, LawCoverageGateState, LawCoverageReport, LawDiffEvent, LawDiffEventKind, - LawDiffFieldChange, LawDiffLawKind, LawDiffReport, LawDiffReviewPosture, LawEvidenceArtifacts, - LawEvidenceValidationResult, LawEvidenceValidationStatus, LawFindingSeverity, - LoadedArtifactMetadata, NormalizedContractBundleProvenance, NormalizedLawAssurancePolicy, - NormalizedLawCapabilityOperation, NormalizedLawCoverageCategory, NormalizedLawCoverageProfile, - NormalizedLawDiffEvent, ParsedSchemaVersion, SemanticChangeFinding, - SuppressionApplicationRecord, SuppressionPolicyOutcome, SuppressionRejectionReason, - SuppressionRejectionRecord, VersionCheck, VersionRegistry, VersionRequirement, - HOLMES_LAW_ASSURANCE_POLICY_API_VERSION, WESLEY_CONTRACT_BUNDLE_HASH_INPUT_CODEC, - WESLEY_CONTRACT_BUNDLE_MANIFEST_API_VERSION, WESLEY_LAW_CAPABILITIES_API_VERSION, - WESLEY_LAW_COVERAGE_API_VERSION, WESLEY_LAW_DIFF_API_VERSION, - WESLEY_LAW_IR_CANONICAL_JSON_CODEC, -}; -pub use ports::{ - ArtifactLoadPort, ArtifactWritePort, ClockPort, CommandIoPort, EchoReportRenderer, - FilesystemPort, FixedClock, GithubPublishPort, InMemoryArtifactStore, - InMemoryMcpResourceRegistry, McpResourcePort, PolicyLoadPort, RecordingCommandIo, - RecordingGithubPublisher, ReportRenderPort, StaticPolicyLoader, Timestamp, -}; diff --git a/crates/wesley-holmes/src/ports/mod.rs b/crates/wesley-holmes/src/ports/mod.rs deleted file mode 100644 index 778f7447..00000000 --- a/crates/wesley-holmes/src/ports/mod.rs +++ /dev/null @@ -1,314 +0,0 @@ -//! Abstract Holmes side-effect ports and deterministic fakes. - -use std::collections::BTreeMap; - -use crate::domain::{ - ArtifactRef, HolmesDiagnostic, HolmesDiagnosticCode, HolmesResult, HolmesSeverity, -}; - -/// Deterministic timestamp value supplied through a clock port. -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct Timestamp { - /// Stable timestamp text. - pub value: String, -} - -impl Timestamp { - /// Create a timestamp from stable text. - pub fn new(value: impl Into) -> Self { - Self { - value: value.into(), - } - } -} - -/// Port for deterministic time access. -pub trait ClockPort { - /// Return the current timestamp according to this clock. - fn now(&self) -> Timestamp; -} - -/// Clock implementation that always returns the same timestamp. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct FixedClock { - now: Timestamp, -} - -impl FixedClock { - /// Create a fixed clock. - pub fn new(now: Timestamp) -> Self { - Self { now } - } -} - -impl ClockPort for FixedClock { - fn now(&self) -> Timestamp { - self.now.clone() - } -} - -/// Port for loading artifact bytes. -pub trait ArtifactLoadPort { - /// Load an artifact reference. - fn read_artifact(&self, artifact: &ArtifactRef) -> HolmesResult>; -} - -/// Port for writing artifact bytes. -pub trait ArtifactWritePort { - /// Write artifact bytes to a workspace-relative path. - fn write_artifact(&mut self, path: &str, bytes: &[u8]) -> HolmesResult<()>; -} - -/// Port for workspace-local byte-oriented filesystem access. -pub trait FilesystemPort { - /// Read bytes from a workspace-relative file. - fn read_workspace_file(&self, path: &str) -> HolmesResult>; - - /// Write bytes to a workspace-relative file. - fn write_workspace_file(&mut self, path: &str, bytes: &[u8]) -> HolmesResult<()>; -} - -/// In-memory artifact store for deterministic tests. -#[derive(Debug, Default, Clone, PartialEq, Eq)] -pub struct InMemoryArtifactStore { - artifacts: BTreeMap>, - writes: BTreeMap>, - unreadable: BTreeMap, -} - -impl InMemoryArtifactStore { - /// Insert a readable artifact. - pub fn insert(&mut self, path: impl Into, bytes: impl Into>) { - let path = path.into(); - self.unreadable.remove(&path); - self.artifacts.insert(path, bytes.into()); - } - - /// Mark an artifact path as present but unreadable. - pub fn mark_unreadable(&mut self, path: impl Into, reason: impl Into) { - self.unreadable.insert(path.into(), reason.into()); - } - - /// Return bytes written to a path. - pub fn written(&self, path: &str) -> Option<&[u8]> { - self.writes.get(path).map(Vec::as_slice) - } -} - -impl ArtifactLoadPort for InMemoryArtifactStore { - fn read_artifact(&self, artifact: &ArtifactRef) -> HolmesResult> { - if let Some(reason) = self.unreadable.get(&artifact.path) { - return Err(HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawArtifactUnreadable, - HolmesSeverity::Error, - format!("artifact {:?} is unreadable: {reason}", artifact.path), - ) - .at_field("path")); - } - - self.artifacts.get(&artifact.path).cloned().ok_or_else(|| { - HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawArtifactUnavailable, - HolmesSeverity::Error, - format!("artifact {:?} is unavailable", artifact.path), - ) - .at_field("path") - }) - } -} - -impl ArtifactWritePort for InMemoryArtifactStore { - fn write_artifact(&mut self, path: &str, bytes: &[u8]) -> HolmesResult<()> { - let data = bytes.to_vec(); - self.unreadable.remove(path); - self.writes.insert(path.to_owned(), data.clone()); - self.artifacts.insert(path.to_owned(), data); - Ok(()) - } -} - -impl FilesystemPort for InMemoryArtifactStore { - fn read_workspace_file(&self, path: &str) -> HolmesResult> { - if let Some(reason) = self.unreadable.get(path) { - return Err(HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawArtifactUnreadable, - HolmesSeverity::Error, - format!("workspace file {path:?} is unreadable: {reason}"), - ) - .at_field("path")); - } - - self.artifacts.get(path).cloned().ok_or_else(|| { - HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawArtifactUnavailable, - HolmesSeverity::Error, - format!("workspace file {path:?} is unavailable"), - ) - .at_field("path") - }) - } - - fn write_workspace_file(&mut self, path: &str, bytes: &[u8]) -> HolmesResult<()> { - let data = bytes.to_vec(); - self.unreadable.remove(path); - self.writes.insert(path.to_owned(), data.clone()); - self.artifacts.insert(path.to_owned(), data); - Ok(()) - } -} - -/// Port for publishing GitHub PR comments or review summaries. -pub trait GithubPublishPort { - /// Publish a PR-facing comment body. - fn publish_pr_comment(&mut self, body: &str) -> HolmesResult<()>; -} - -/// Recording GitHub publisher fake. -#[derive(Debug, Default, Clone, PartialEq, Eq)] -pub struct RecordingGithubPublisher { - comments: Vec, -} - -impl RecordingGithubPublisher { - /// Return recorded comment bodies. - pub fn comments(&self) -> &[String] { - &self.comments - } -} - -impl GithubPublishPort for RecordingGithubPublisher { - fn publish_pr_comment(&mut self, body: &str) -> HolmesResult<()> { - self.comments.push(body.to_owned()); - Ok(()) - } -} - -/// Port for registering MCP resources. -pub trait McpResourcePort { - /// Register a named MCP resource payload. - fn put_resource(&mut self, resource_id: &str, bytes: &[u8]) -> HolmesResult<()>; -} - -/// In-memory MCP resource registry fake. -#[derive(Debug, Default, Clone, PartialEq, Eq)] -pub struct InMemoryMcpResourceRegistry { - resources: BTreeMap>, -} - -impl InMemoryMcpResourceRegistry { - /// Return bytes for a registered resource. - pub fn resource(&self, resource_id: &str) -> Option<&[u8]> { - self.resources.get(resource_id).map(Vec::as_slice) - } -} - -impl McpResourcePort for InMemoryMcpResourceRegistry { - fn put_resource(&mut self, resource_id: &str, bytes: &[u8]) -> HolmesResult<()> { - self.resources - .insert(resource_id.to_owned(), bytes.to_vec()); - Ok(()) - } -} - -/// Port for loading active policy bytes. -pub trait PolicyLoadPort { - /// Load policy bytes from a workspace-relative path. - fn load_policy(&self, path: &str) -> HolmesResult>; -} - -/// Static policy loader fake. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct StaticPolicyLoader { - path: String, - bytes: Vec, -} - -impl StaticPolicyLoader { - /// Create a static policy loader. - pub fn new(path: impl Into, bytes: impl Into>) -> Self { - Self { - path: path.into(), - bytes: bytes.into(), - } - } -} - -impl PolicyLoadPort for StaticPolicyLoader { - fn load_policy(&self, path: &str) -> HolmesResult> { - if path == self.path { - Ok(self.bytes.clone()) - } else { - Err(HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawArtifactUnavailable, - HolmesSeverity::Error, - format!("policy artifact {path:?} is unavailable"), - ) - .at_field("path")) - } - } -} - -/// Port for rendering report payloads. -pub trait ReportRenderPort { - /// Render a report payload. - fn render_report(&self, body: &str) -> HolmesResult; -} - -/// Deterministic report renderer fake that prefixes report bodies. -#[derive(Debug, Default, Clone, PartialEq, Eq)] -pub struct EchoReportRenderer { - prefix: String, -} - -impl EchoReportRenderer { - /// Create a report renderer with a static prefix. - pub fn new(prefix: impl Into) -> Self { - Self { - prefix: prefix.into(), - } - } -} - -impl ReportRenderPort for EchoReportRenderer { - fn render_report(&self, body: &str) -> HolmesResult { - Ok(format!("{}{body}", self.prefix)) - } -} - -/// Port for command standard output and error streams. -pub trait CommandIoPort { - /// Write standard output text. - fn stdout(&mut self, text: &str); - - /// Write standard error text. - fn stderr(&mut self, text: &str); -} - -/// Command I/O fake that records output streams. -#[derive(Debug, Default, Clone, PartialEq, Eq)] -pub struct RecordingCommandIo { - stdout: Vec, - stderr: Vec, -} - -impl RecordingCommandIo { - /// Return recorded standard output writes. - pub fn stdout_lines(&self) -> &[String] { - &self.stdout - } - - /// Return recorded standard error writes. - pub fn stderr_lines(&self) -> &[String] { - &self.stderr - } -} - -impl CommandIoPort for RecordingCommandIo { - fn stdout(&mut self, text: &str) { - self.stdout.push(text.to_owned()); - } - - fn stderr(&mut self, text: &str) { - self.stderr.push(text.to_owned()); - } -} diff --git a/crates/wesley-holmes/src/reporting/mod.rs b/crates/wesley-holmes/src/reporting/mod.rs deleted file mode 100644 index b855b09c..00000000 --- a/crates/wesley-holmes/src/reporting/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -//! Reporting DTO and renderer boundary for future Holmes outputs. -//! -//! This namespace intentionally starts empty while the first implementation -//! slice establishes diagnostic, evidence, and versioning primitives. diff --git a/crates/wesley-holmes/tests/architecture.rs b/crates/wesley-holmes/tests/architecture.rs deleted file mode 100644 index c4708ee0..00000000 --- a/crates/wesley-holmes/tests/architecture.rs +++ /dev/null @@ -1,49 +0,0 @@ -use std::fs; -use std::path::{Path, PathBuf}; - -#[test] -fn domain_sources_do_not_import_ambient_adapters() { - let domain_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("src/domain"); - let mut files = Vec::new(); - collect_rs_files(&domain_dir, &mut files); - - let forbidden = [ - ("use std::fs", "filesystem imports"), - ("std::fs::", "filesystem references"), - ("use std::net", "network imports"), - ("std::net::", "network references"), - ("use std::process", "process imports"), - ("std::process::", "process references"), - ("SystemTime", "wall-clock access"), - ("Instant::now", "wall-clock access"), - ("chrono::Utc::now", "wall-clock access"), - ("reqwest", "HTTP client dependency"), - ("octocrab", "GitHub client dependency"), - ]; - - for file in files { - let source = fs::read_to_string(&file) - .unwrap_or_else(|error| panic!("failed to read {}: {error}", file.display())); - for (token, reason) in forbidden { - assert!( - !source.contains(token), - "domain file {} contains forbidden {reason}: {token}", - file.display() - ); - } - } -} - -fn collect_rs_files(directory: &Path, files: &mut Vec) { - for entry in fs::read_dir(directory) - .unwrap_or_else(|error| panic!("failed to read {}: {error}", directory.display())) - { - let entry = entry.expect("failed to read directory entry"); - let path = entry.path(); - if path.is_dir() { - collect_rs_files(&path, files); - } else if path.extension().and_then(|extension| extension.to_str()) == Some("rs") { - files.push(path); - } - } -} diff --git a/crates/wesley-holmes/tests/assessment_core.rs b/crates/wesley-holmes/tests/assessment_core.rs deleted file mode 100644 index 38d994c2..00000000 --- a/crates/wesley-holmes/tests/assessment_core.rs +++ /dev/null @@ -1,271 +0,0 @@ -use wesley_holmes::{ - aggregate_law_assurance_assessment, bounded_finding_summary, evaluate_bundle_traceability, - law_assurance_provenance_report, ArtifactRef, BundleProvenance, BundleTraceabilityGateState, - ContractBundleManifest, HolmesLawEvidenceBundle, JsonLawDiffIngestPort, - LawAssuranceArtifactProvenance, LawAssuranceAssessmentOutcome, LawCoverageGateDecision, - LawCoverageGateState, LawDiffIngestPort, LawEvidenceArtifacts, LawEvidenceValidationResult, - SemanticChangeFinding, -}; - -const HASH_A: &str = "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; -const HASH_B: &str = "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; -const HASH_C: &str = "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; -const HASH_D: &str = "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"; -const HASH_E: &str = "sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"; -const CI_SEMANTIC_DIFF: &str = - include_str!("../../../test/fixtures/weslaw/diff/ci-semantic-diff.json"); - -#[test] -fn bundle_traceability_gate_passes_matching_manifest_and_artifact_hashes() { - let bundle = evidence_bundle(); - let manifest = contract_manifest(HASH_B); - - let decision = evaluate_bundle_traceability(&bundle, Some(&manifest)); - - assert_eq!(decision.state, BundleTraceabilityGateState::Pass); - assert_eq!(decision.state_label, "pass"); - assert_eq!(decision.checks.len(), 8); - assert!(decision - .checks - .iter() - .all(|check| check.state == BundleTraceabilityGateState::Pass)); -} - -#[test] -fn bundle_traceability_gate_fails_manifest_hash_mismatch() { - let bundle = evidence_bundle(); - let stale_manifest = contract_manifest(HASH_E); - - let decision = evaluate_bundle_traceability(&bundle, Some(&stale_manifest)); - - assert_eq!(decision.state, BundleTraceabilityGateState::Fail); - let law_hash = decision - .checks - .iter() - .find(|check| check.field_path == "manifest.lawHash") - .expect("lawHash check should exist"); - assert_eq!(law_hash.state, BundleTraceabilityGateState::Fail); - assert_eq!(law_hash.expected.as_deref(), Some(HASH_B)); - assert_eq!(law_hash.actual.as_deref(), Some(HASH_E)); -} - -#[test] -fn assessment_aggregates_validation_findings_coverage_and_traceability() { - let bundle = evidence_bundle(); - let manifest = contract_manifest(HASH_B); - let traceability = evaluate_bundle_traceability(&bundle, Some(&manifest)); - let findings = semantic_findings(); - let validation = LawEvidenceValidationResult::from_diagnostics(Vec::new()); - let coverage_gates = vec![coverage_decision(LawCoverageGateState::Pass)]; - - let assessment = - aggregate_law_assurance_assessment(&validation, &findings, &coverage_gates, &traceability); - - assert_eq!(assessment.outcome, LawAssuranceAssessmentOutcome::Fail); - assert_eq!(assessment.outcome_label, "fail"); - assert_eq!(assessment.finding_count, 3); - assert_eq!(assessment.critical_finding_count, 2); - assert_eq!(assessment.error_finding_count, 1); - assert_eq!(assessment.coverage_fail_count, 0); - assert_eq!( - assessment.traceability_state, - BundleTraceabilityGateState::Pass - ); -} - -#[test] -fn assessment_failures_dominate_unavailable_evidence() { - let bundle = evidence_bundle(); - let traceability = evaluate_bundle_traceability(&bundle, None); - let findings = semantic_findings(); - let validation = LawEvidenceValidationResult::from_diagnostics(Vec::new()); - let coverage_gates = vec![coverage_decision(LawCoverageGateState::Pass)]; - - let assessment = - aggregate_law_assurance_assessment(&validation, &findings, &coverage_gates, &traceability); - - assert_eq!(traceability.state, BundleTraceabilityGateState::Unavailable); - assert_eq!(assessment.outcome, LawAssuranceAssessmentOutcome::Fail); - assert_eq!(assessment.outcome_label, "fail"); - assert_eq!( - assessment.traceability_state, - BundleTraceabilityGateState::Unavailable - ); -} - -#[test] -fn bounded_finding_summary_tracks_omitted_details_by_severity() { - let findings = semantic_findings(); - - let summary = bounded_finding_summary(&findings, 2); - - assert_eq!(summary.total_finding_count, 3); - assert_eq!(summary.displayed_findings.len(), 2); - assert_eq!(summary.omitted_finding_count, 1); - assert_eq!(summary.critical_count, 2); - assert_eq!(summary.error_count, 1); -} - -#[test] -fn provenance_report_snapshot_is_stable() { - let bundle = evidence_bundle(); - let manifest = contract_manifest(HASH_B); - - let report = law_assurance_provenance_report(&bundle, Some(&manifest)); - - assert_eq!( - report.artifacts[0], - LawAssuranceArtifactProvenance { - field_path: "artifacts.lawDiff".to_owned(), - artifact_family: "law-diff".to_owned(), - path: "evidence/law-diff.json".to_owned(), - schema_version: Some("1.0.0".to_owned()), - sha256: Some(HASH_A.to_owned()), - evidence_ref: "evidence/law-diff.json".to_owned(), - } - ); - - let json = serde_json::to_string_pretty(&report).expect("report should serialize"); - assert_eq!( - json, - r#"{ - "bundleId": "bundle-release", - "bundleSource": "ci", - "schemaHash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "lawHash": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - "policyHash": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", - "bundleHash": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", - "manifest": { - "manifestRef": "contractBundleManifest", - "apiVersion": "wesley.contract-bundle-manifest/v1", - "schemaHash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "lawHash": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - "lawDocumentHash": "sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", - "profileHash": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", - "bundleHash": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", - "lawIrCodec": "wesley.law-ir.canonical-json.v1", - "bundleHashCodec": "wesley.contract-bundle.hash-input.canonical-json.v1", - "compiler": "wesley-core", - "compilerVersion": "0.0.5", - "lawEntryCount": 4 - }, - "artifacts": [ - { - "fieldPath": "artifacts.lawDiff", - "artifactFamily": "law-diff", - "path": "evidence/law-diff.json", - "schemaVersion": "1.0.0", - "sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "evidenceRef": "evidence/law-diff.json" - }, - { - "fieldPath": "artifacts.lawCoverage", - "artifactFamily": "law-coverage", - "path": "evidence/law-coverage.json", - "schemaVersion": "1.0.0", - "sha256": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - "evidenceRef": "evidence/law-coverage.json" - }, - { - "fieldPath": "artifacts.lawCapabilities", - "artifactFamily": "law-capabilities", - "path": "evidence/law-capabilities.json", - "schemaVersion": "1.0.0", - "sha256": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", - "evidenceRef": "evidence/law-capabilities.json" - }, - { - "fieldPath": "artifacts.contractBundleManifest", - "artifactFamily": "contract-bundle-manifest", - "path": "evidence/manifest.json", - "schemaVersion": "1.0.0", - "sha256": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", - "evidenceRef": "evidence/manifest.json" - } - ] -}"# - ); -} - -fn evidence_bundle() -> HolmesLawEvidenceBundle { - HolmesLawEvidenceBundle { - schema_version: "1.0.0".to_owned(), - bundle_id: "bundle-release".to_owned(), - artifacts: LawEvidenceArtifacts { - law_diff: artifact("evidence/law-diff.json", HASH_A), - law_coverage: artifact("evidence/law-coverage.json", HASH_B), - law_capabilities: artifact("evidence/law-capabilities.json", HASH_C), - contract_bundle_manifest: artifact("evidence/manifest.json", HASH_D), - policy: None, - report: None, - witness: None, - }, - provenance: BundleProvenance { - schema_hash: HASH_A.to_owned(), - law_hash: HASH_B.to_owned(), - policy_hash: Some(HASH_C.to_owned()), - bundle_hash: HASH_D.to_owned(), - source: "ci".to_owned(), - }, - } -} - -fn artifact(path: &str, sha256: &str) -> ArtifactRef { - ArtifactRef { - path: path.to_owned(), - schema_version: Some("1.0.0".to_owned()), - sha256: Some(sha256.to_owned()), - } -} - -fn contract_manifest(law_hash: &str) -> ContractBundleManifest { - ContractBundleManifest { - api_version: "wesley.contract-bundle-manifest/v1".to_owned(), - schema_hash: HASH_A.to_owned(), - law_hash: law_hash.to_owned(), - law_document_hash: Some(HASH_E.to_owned()), - profile_hash: HASH_C.to_owned(), - bundle_hash: HASH_D.to_owned(), - law_ir_codec: "wesley.law-ir.canonical-json.v1".to_owned(), - bundle_hash_codec: "wesley.contract-bundle.hash-input.canonical-json.v1".to_owned(), - compiler: "wesley-core".to_owned(), - compiler_version: "0.0.5".to_owned(), - law_entry_count: 4, - } -} - -fn semantic_findings() -> Vec { - let report = JsonLawDiffIngestPort - .ingest_law_diff(CI_SEMANTIC_DIFF.as_bytes()) - .report - .expect("fixture should parse"); - - wesley_holmes::semantic_change_findings_from_law_diff( - &report, - HASH_D, - "evidence/law-diff.json", - Some("release".to_owned()), - ) - .expect("findings should construct") -} - -fn coverage_decision(state: LawCoverageGateState) -> LawCoverageGateDecision { - LawCoverageGateDecision { - gate_id: "law-coverage:release:mutationFootprintLaw".to_owned(), - profile: "release".to_owned(), - category_id: "mutationFootprintLaw".to_owned(), - category_label: Some("Mutation footprint law".to_owned()), - state, - state_label: state.label().to_owned(), - required: true, - covered: Some(3), - total: Some(3), - actual_percent: Some(100.0), - warning_threshold: Some(90.0), - failure_threshold: Some(80.0), - missing_subjects: Vec::new(), - omitted_missing_subject_count: 0, - evidence_ref: Some("evidence/law-coverage.json".to_owned()), - rationale: "coverage satisfies configured thresholds".to_owned(), - } -} diff --git a/crates/wesley-holmes/tests/contract_manifest_ingest.rs b/crates/wesley-holmes/tests/contract_manifest_ingest.rs deleted file mode 100644 index 4c24a837..00000000 --- a/crates/wesley-holmes/tests/contract_manifest_ingest.rs +++ /dev/null @@ -1,170 +0,0 @@ -use wesley_holmes::{ - BundleProvenance, ContractBundleManifestIngestPort, ContractBundleManifestIngestStatus, - HolmesDiagnosticCode, JsonContractBundleManifestIngestPort, -}; - -const HASH_A: &str = "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; -const HASH_B: &str = "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; -const HASH_C: &str = "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; -const HASH_D: &str = "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"; -const HASH_E: &str = "sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"; - -fn release_manifest() -> String { - format!( - r#"{{ - "apiVersion": "wesley.contract-bundle-manifest/v1", - "schemaHash": "{HASH_A}", - "lawHash": "{HASH_B}", - "lawDocumentHash": "{HASH_E}", - "profileHash": "{HASH_C}", - "bundleHash": "{HASH_D}", - "lawIrCodec": "wesley.law-ir.canonical-json.v1", - "bundleHashCodec": "wesley.contract-bundle.hash-input.canonical-json.v1", - "compiler": "wesley-core", - "compilerVersion": "0.0.5", - "lawEntryCount": 4 -}}"# - ) -} - -#[test] -fn contract_manifest_ingest_accepts_release_manifest_and_normalizes_provenance() { - let result = JsonContractBundleManifestIngestPort - .ingest_contract_bundle_manifest(release_manifest().as_bytes(), None); - - assert_eq!(result.status, ContractBundleManifestIngestStatus::Valid); - assert!(result.diagnostics.is_empty()); - - let manifest = result - .manifest - .expect("valid manifest should produce typed provenance"); - assert_eq!(manifest.api_version, "wesley.contract-bundle-manifest/v1"); - assert_eq!(manifest.schema_hash, HASH_A); - assert_eq!(manifest.law_hash, HASH_B); - assert_eq!(manifest.profile_hash, HASH_C); - assert_eq!(manifest.bundle_hash, HASH_D); - assert_eq!(manifest.law_ir_codec, "wesley.law-ir.canonical-json.v1"); - assert_eq!(manifest.compiler, "wesley-core"); - assert_eq!(manifest.law_entry_count, 4); - - let provenance = manifest.normalized_provenance(); - assert_eq!(provenance.manifest_ref, "contractBundleManifest"); - assert_eq!(provenance.law_document_hash.as_deref(), Some(HASH_E)); -} - -#[test] -fn contract_manifest_ingest_cross_checks_bundle_provenance_hashes() { - let provenance = BundleProvenance { - schema_hash: HASH_A.to_owned(), - law_hash: HASH_B.to_owned(), - policy_hash: Some(HASH_C.to_owned()), - bundle_hash: HASH_D.to_owned(), - source: "ci".to_owned(), - }; - - let result = JsonContractBundleManifestIngestPort - .ingest_contract_bundle_manifest(release_manifest().as_bytes(), Some(&provenance)); - - assert_eq!(result.status, ContractBundleManifestIngestStatus::Valid); - - let stale_provenance = BundleProvenance { - schema_hash: HASH_A.to_owned(), - law_hash: HASH_E.to_owned(), - policy_hash: Some(HASH_C.to_owned()), - bundle_hash: HASH_D.to_owned(), - source: "ci".to_owned(), - }; - let result = JsonContractBundleManifestIngestPort - .ingest_contract_bundle_manifest(release_manifest().as_bytes(), Some(&stale_provenance)); - - assert_eq!(result.status, ContractBundleManifestIngestStatus::Invalid); - assert_diagnostic( - &result.diagnostics, - HolmesDiagnosticCode::HlawManifestHashMismatch, - ); - assert_diagnostic_field(&result.diagnostics, "lawHash"); -} - -#[test] -fn contract_manifest_ingest_rejects_invalid_hash_syntax() { - let invalid = release_manifest().replace(HASH_A, "abc"); - - let result = JsonContractBundleManifestIngestPort - .ingest_contract_bundle_manifest(invalid.as_bytes(), None); - - assert_eq!(result.status, ContractBundleManifestIngestStatus::Invalid); - assert!(result.manifest.is_none()); - assert_diagnostic( - &result.diagnostics, - HolmesDiagnosticCode::HlawManifestInvalidHash, - ); - assert_diagnostic_field(&result.diagnostics, "schemaHash"); -} - -#[test] -fn contract_manifest_ingest_rejects_missing_required_hash() { - let missing = release_manifest().replace(&format!(" \"bundleHash\": \"{HASH_D}\",\n"), ""); - - let result = JsonContractBundleManifestIngestPort - .ingest_contract_bundle_manifest(missing.as_bytes(), None); - - assert_eq!(result.status, ContractBundleManifestIngestStatus::Invalid); - assert!(result.manifest.is_none()); - assert_diagnostic( - &result.diagnostics, - HolmesDiagnosticCode::HlawManifestMissingRequiredHash, - ); - assert_diagnostic_field(&result.diagnostics, "bundleHash"); -} - -#[test] -fn contract_manifest_ingest_rejects_unsupported_version_and_codec() { - let unsupported = release_manifest() - .replace( - "wesley.contract-bundle-manifest/v1", - "wesley.contract-bundle-manifest/v2", - ) - .replace("wesley.law-ir.canonical-json.v1", "custom-law-ir/v9"); - - let result = JsonContractBundleManifestIngestPort - .ingest_contract_bundle_manifest(unsupported.as_bytes(), None); - - assert_eq!(result.status, ContractBundleManifestIngestStatus::Invalid); - assert_diagnostic( - &result.diagnostics, - HolmesDiagnosticCode::HlawManifestUnsupportedVersion, - ); - assert_diagnostic( - &result.diagnostics, - HolmesDiagnosticCode::HlawManifestUnsupportedCodec, - ); -} - -#[test] -fn contract_manifest_ingest_rejects_malformed_json() { - let result = - JsonContractBundleManifestIngestPort.ingest_contract_bundle_manifest(b"{broken", None); - - assert_eq!(result.status, ContractBundleManifestIngestStatus::Invalid); - assert!(result.manifest.is_none()); - assert_diagnostic( - &result.diagnostics, - HolmesDiagnosticCode::HlawManifestMalformedJson, - ); -} - -fn assert_diagnostic(diagnostics: &[wesley_holmes::HolmesDiagnostic], code: HolmesDiagnosticCode) { - assert!( - diagnostics.iter().any(|diagnostic| diagnostic.code == code), - "expected {code:?} in {diagnostics:#?}" - ); -} - -fn assert_diagnostic_field(diagnostics: &[wesley_holmes::HolmesDiagnostic], field_path: &str) { - assert!( - diagnostics - .iter() - .any(|diagnostic| diagnostic.field_path.as_deref() == Some(field_path)), - "expected field {field_path:?} in {diagnostics:#?}" - ); -} diff --git a/crates/wesley-holmes/tests/foundation.rs b/crates/wesley-holmes/tests/foundation.rs deleted file mode 100644 index 770cefaf..00000000 --- a/crates/wesley-holmes/tests/foundation.rs +++ /dev/null @@ -1,573 +0,0 @@ -use wesley_holmes::{ - ArtifactFamily, ArtifactLoadPort, ArtifactRef, ArtifactWritePort, BundleProvenance, ClockPort, - CommandIoPort, EchoReportRenderer, FilesystemPort, FixedClock, HolmesDiagnosticCode, - HolmesLawEvidenceBundle, InMemoryArtifactStore, LawEvidenceArtifacts, - LawEvidenceValidationStatus, LawEvidenceValidator, McpResourcePort, RecordingCommandIo, - ReportRenderPort, Timestamp, VersionRegistry, VersionRequirement, WeslawArtifactLocator, -}; - -#[test] -fn fixed_clock_returns_deterministic_timestamp() { - let clock = FixedClock::new(Timestamp::new("2026-05-26T00:00:00Z")); - - assert_eq!(clock.now(), Timestamp::new("2026-05-26T00:00:00Z")); -} - -#[test] -fn in_memory_artifact_store_reads_and_writes() { - let mut store = InMemoryArtifactStore::default(); - store.insert("evidence/law-diff.json", b"diff".to_vec()); - - let bytes = store - .read_artifact(&ArtifactRef::new("evidence/law-diff.json")) - .expect("artifact should be readable"); - store - .write_artifact("reports/summary.md", b"summary") - .expect("artifact should be writable"); - - assert_eq!(bytes, b"diff"); - assert_eq!(store.written("reports/summary.md"), Some(&b"summary"[..])); - assert_eq!( - store - .read_artifact(&ArtifactRef::new("reports/summary.md")) - .expect("written artifact should be readable"), - b"summary" - ); -} - -#[test] -fn filesystem_port_uses_workspace_relative_bytes_without_real_filesystem() { - let mut store = InMemoryArtifactStore::default(); - store.insert("policy/release.json", b"policy".to_vec()); - - let bytes = store - .read_workspace_file("policy/release.json") - .expect("workspace file should be readable"); - store - .write_workspace_file("reports/holmes.md", b"report") - .expect("workspace file should be writable"); - - assert_eq!(bytes, b"policy"); - assert_eq!(store.written("reports/holmes.md"), Some(&b"report"[..])); - assert_eq!( - store - .read_workspace_file("reports/holmes.md") - .expect("written workspace file should be readable"), - b"report" - ); -} - -#[test] -fn evidence_bundle_requires_core_artifacts() { - let bundle = evidence_bundle_with_law_diff_path(""); - - let diagnostic = bundle - .validate_required_artifacts() - .expect_err("blank law diff path should fail validation"); - - assert_eq!( - diagnostic.code, - HolmesDiagnosticCode::HlawEvidenceBundleInvalid - ); - assert_eq!(diagnostic.field_path.as_deref(), Some("artifacts.lawDiff")); -} - -#[test] -fn evidence_bundle_structure_validation_collects_required_optional_and_duplicate_errors() { - let mut bundle = valid_evidence_bundle(); - bundle.bundle_id = " ".to_owned(); - bundle.artifacts.law_diff.path = "evidence/shared.json".to_owned(); - bundle.artifacts.law_coverage.path = "evidence/shared.json".to_owned(); - bundle.artifacts.law_capabilities.path = "".to_owned(); - bundle.artifacts.policy = Some(ArtifactRef::new(" ")); - - let result = bundle.validate_structure(&VersionRegistry::default()); - - assert_eq!(result.status, LawEvidenceValidationStatus::Invalid); - assert_diagnostic_field(&result.diagnostics, "bundleId"); - assert_diagnostic_field(&result.diagnostics, "artifacts.lawCapabilities"); - assert_diagnostic_field(&result.diagnostics, "artifacts.policy"); - assert_diagnostic_field(&result.diagnostics, "artifacts.lawCoverage"); -} - -#[test] -fn evidence_bundle_provenance_validation_requires_canonical_hashes_and_source() { - let mut bundle = valid_evidence_bundle(); - bundle.provenance.schema_hash = "schema".to_owned(); - bundle.provenance.law_hash = format!("sha256:{}z", "a".repeat(63)); - bundle.provenance.policy_hash = Some(" ".to_owned()); - bundle.provenance.bundle_hash = format!("sha1:{}", "b".repeat(64)); - bundle.provenance.source = "\t".to_owned(); - - let result = bundle.validate_structure(&VersionRegistry::default()); - - assert_eq!(result.status, LawEvidenceValidationStatus::Invalid); - assert_diagnostic( - &result.diagnostics, - HolmesDiagnosticCode::HlawProvenanceHashMalformed, - ); - assert_diagnostic( - &result.diagnostics, - HolmesDiagnosticCode::HlawProvenanceHashMissing, - ); - assert_diagnostic( - &result.diagnostics, - HolmesDiagnosticCode::HlawProvenanceSourceMissing, - ); - assert_diagnostic_field(&result.diagnostics, "provenance.schemaHash"); - assert_diagnostic_field(&result.diagnostics, "provenance.lawHash"); - assert_diagnostic_field(&result.diagnostics, "provenance.policyHash"); - assert_diagnostic_field(&result.diagnostics, "provenance.bundleHash"); - assert_diagnostic_field(&result.diagnostics, "provenance.source"); -} - -#[test] -fn version_fixture_matrix_covers_current_deprecated_malformed_unsupported_and_mixed_artifacts() { - let registry = VersionRegistry::default().with_requirement( - VersionRequirement::new(ArtifactFamily::EvidenceBundle, 1, 1) - .with_deprecated_minor_through(0), - ); - - let deprecated = valid_evidence_bundle(); - let deprecated_result = deprecated.validate_structure(®istry); - assert_eq!( - deprecated_result.status, - LawEvidenceValidationStatus::ValidWithWarnings - ); - assert_diagnostic( - &deprecated_result.diagnostics, - HolmesDiagnosticCode::HlawSchemaVersionDeprecated, - ); - - let mut current = valid_evidence_bundle(); - current.schema_version = "1.1.0".to_owned(); - assert_eq!( - current.validate_structure(®istry).status, - LawEvidenceValidationStatus::Valid - ); - - let mut malformed = valid_evidence_bundle(); - malformed.schema_version = "v1.0.0".to_owned(); - assert_diagnostic( - &malformed.validate_structure(®istry).diagnostics, - HolmesDiagnosticCode::HlawSchemaVersionMalformed, - ); - - let mut unsupported = valid_evidence_bundle(); - unsupported.schema_version = "1.2.0".to_owned(); - assert_diagnostic( - &unsupported.validate_structure(®istry).diagnostics, - HolmesDiagnosticCode::HlawSchemaVersionUnsupportedMinor, - ); - - let mut mixed_generation = valid_evidence_bundle(); - mixed_generation.artifacts.law_diff.schema_version = Some("2.0.0".to_owned()); - let mixed_result = mixed_generation.validate_structure(®istry); - assert_diagnostic( - &mixed_result.diagnostics, - HolmesDiagnosticCode::HlawSchemaVersionUnsupportedMajor, - ); - assert_diagnostic_field(&mixed_result.diagnostics, "artifacts.lawDiff.schemaVersion"); -} - -#[test] -fn artifact_sha256_fields_must_be_canonical_when_present() { - let mut bundle = valid_evidence_bundle(); - - for (sha256, field_path) in [ - (" ", "artifacts.lawDiff.sha256"), - ( - "sha1:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "artifacts.lawCoverage.sha256", - ), - ( - "sha256:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", - "artifacts.lawCapabilities.sha256", - ), - ("sha256:aaaaaaaa", "artifacts.contractBundleManifest.sha256"), - ] { - match field_path { - "artifacts.lawDiff.sha256" => { - bundle.artifacts.law_diff.sha256 = Some(sha256.to_owned()) - } - "artifacts.lawCoverage.sha256" => { - bundle.artifacts.law_coverage.sha256 = Some(sha256.to_owned()); - } - "artifacts.lawCapabilities.sha256" => { - bundle.artifacts.law_capabilities.sha256 = Some(sha256.to_owned()); - } - "artifacts.contractBundleManifest.sha256" => { - bundle.artifacts.contract_bundle_manifest.sha256 = Some(sha256.to_owned()); - } - _ => unreachable!("test fixture uses known fields"), - } - } - - let result = bundle.validate_structure(&VersionRegistry::default()); - - assert_eq!(result.status, LawEvidenceValidationStatus::Invalid); - assert_diagnostic( - &result.diagnostics, - HolmesDiagnosticCode::HlawArtifactHashMissing, - ); - assert_diagnostic( - &result.diagnostics, - HolmesDiagnosticCode::HlawArtifactHashMalformed, - ); - assert_diagnostic_field(&result.diagnostics, "artifacts.lawDiff.sha256"); - assert_diagnostic_field(&result.diagnostics, "artifacts.lawCoverage.sha256"); - assert_diagnostic_field(&result.diagnostics, "artifacts.lawCapabilities.sha256"); - assert_diagnostic_field( - &result.diagnostics, - "artifacts.contractBundleManifest.sha256", - ); -} - -#[test] -fn artifact_schema_versions_are_required_for_every_present_artifact_reference() { - let mut bundle = valid_evidence_bundle(); - bundle.artifacts.law_diff.schema_version = None; - bundle.artifacts.law_coverage.schema_version = None; - bundle.artifacts.law_capabilities.schema_version = None; - bundle.artifacts.contract_bundle_manifest.schema_version = None; - bundle.artifacts.policy = Some(ArtifactRef::new("evidence/policy.json")); - - let result = bundle.validate_structure(&VersionRegistry::default()); - - assert_eq!(result.status, LawEvidenceValidationStatus::Invalid); - assert_diagnostic( - &result.diagnostics, - HolmesDiagnosticCode::HlawSchemaVersionMissing, - ); - assert_diagnostic_field(&result.diagnostics, "artifacts.lawDiff.schemaVersion"); - assert_diagnostic_field(&result.diagnostics, "artifacts.lawCoverage.schemaVersion"); - assert_diagnostic_field( - &result.diagnostics, - "artifacts.lawCapabilities.schemaVersion", - ); - assert_diagnostic_field( - &result.diagnostics, - "artifacts.contractBundleManifest.schemaVersion", - ); - assert_diagnostic_field(&result.diagnostics, "artifacts.policy.schemaVersion"); -} - -#[test] -fn law_evidence_validator_reports_artifact_availability_size_and_read_errors() { - let bundle = valid_evidence_bundle(); - let mut store = InMemoryArtifactStore::default(); - store.insert("evidence/law-diff.json", b"oversized".to_vec()); - store.mark_unreadable("evidence/law-capabilities.json", "permission denied"); - store.insert("evidence/bundle-manifest.json", b"manifest".to_vec()); - - let validator = - LawEvidenceValidator::new(WeslawArtifactLocator::new("/workspace")).with_max_bytes(8); - let result = validator.validate(&bundle, &store, &VersionRegistry::default()); - - assert_eq!(result.status, LawEvidenceValidationStatus::Invalid); - assert_diagnostic( - &result.diagnostics, - HolmesDiagnosticCode::HlawArtifactOversized, - ); - assert_diagnostic( - &result.diagnostics, - HolmesDiagnosticCode::HlawArtifactUnavailable, - ); - assert_diagnostic( - &result.diagnostics, - HolmesDiagnosticCode::HlawArtifactUnreadable, - ); - assert_eq!(result.loaded_artifacts.len(), 1); - assert_eq!( - result.loaded_artifacts[0].field_path, - "artifacts.contractBundleManifest" - ); -} - -#[test] -fn law_evidence_validator_continues_after_structure_warnings_to_find_artifact_errors() { - let bundle = valid_evidence_bundle(); - let registry = VersionRegistry::default().with_requirement( - VersionRequirement::new(ArtifactFamily::EvidenceBundle, 1, 1) - .with_deprecated_minor_through(0), - ); - let store = InMemoryArtifactStore::default(); - - let result = LawEvidenceValidator::new(WeslawArtifactLocator::new("/workspace")) - .validate(&bundle, &store, ®istry); - - assert_eq!(result.status, LawEvidenceValidationStatus::Invalid); - assert_diagnostic( - &result.diagnostics, - HolmesDiagnosticCode::HlawSchemaVersionDeprecated, - ); - assert_diagnostic( - &result.diagnostics, - HolmesDiagnosticCode::HlawArtifactUnavailable, - ); -} - -#[test] -fn law_evidence_validator_rejects_duplicate_normalized_artifact_paths() { - let mut bundle = valid_evidence_bundle(); - bundle.artifacts.law_diff.path = "./evidence/shared.json".to_owned(); - bundle.artifacts.law_coverage.path = "evidence/shared.json".to_owned(); - let mut store = InMemoryArtifactStore::default(); - store.insert("evidence/shared.json", b"shared".to_vec()); - store.insert("evidence/law-capabilities.json", b"capabilities".to_vec()); - store.insert("evidence/bundle-manifest.json", b"manifest".to_vec()); - - let result = LawEvidenceValidator::new(WeslawArtifactLocator::new("/workspace")).validate( - &bundle, - &store, - &VersionRegistry::default(), - ); - - assert_eq!(result.status, LawEvidenceValidationStatus::Invalid); - assert_diagnostic_field(&result.diagnostics, "artifacts.lawCoverage"); - assert_diagnostic( - &result.diagnostics, - HolmesDiagnosticCode::HlawEvidenceBundleInvalid, - ); -} - -#[test] -fn first_law_evidence_validation_gate_accepts_clean_bundle_and_artifacts() { - let bundle = valid_evidence_bundle(); - let mut store = InMemoryArtifactStore::default(); - for artifact in [ - &bundle.artifacts.law_diff, - &bundle.artifacts.law_coverage, - &bundle.artifacts.law_capabilities, - &bundle.artifacts.contract_bundle_manifest, - ] { - store.insert(&artifact.path, b"{}".to_vec()); - } - - let validator = - LawEvidenceValidator::new(WeslawArtifactLocator::new("/workspace")).with_max_bytes(1024); - let result = validator.validate(&bundle, &store, &VersionRegistry::default()); - - assert_eq!(result.status, LawEvidenceValidationStatus::Valid); - assert!(result.diagnostics.is_empty()); - assert_eq!(result.loaded_artifacts.len(), 4); - assert_eq!(result.loaded_artifacts[0].field_path, "artifacts.lawDiff"); -} - -#[test] -fn artifact_locator_normalizes_workspace_relative_paths() { - let locator = WeslawArtifactLocator::new("/workspace"); - - let resolved = locator - .resolve("./evidence/../evidence/law-diff.json") - .expect("path should normalize inside workspace"); - - assert_eq!(resolved.workspace_relative, "evidence/law-diff.json"); -} - -#[test] -fn artifact_locator_rejects_escape_and_absolute_paths() { - let locator = WeslawArtifactLocator::new("/workspace"); - - let escape = locator - .resolve("../outside.json") - .expect_err("parent traversal should be rejected"); - let absolute = locator - .resolve("/tmp/outside.json") - .expect_err("absolute paths should be rejected"); - - assert_eq!(escape.code, HolmesDiagnosticCode::HlawArtifactPathEscape); - assert_eq!(absolute.code, HolmesDiagnosticCode::HlawArtifactPathEscape); -} - -#[test] -fn artifact_locator_rejects_backslash_and_windows_drive_paths() { - let locator = WeslawArtifactLocator::new("/workspace"); - - for path in [ - "..\\outside.json", - "C:\\tmp\\outside.json", - "\\\\server\\share\\outside.json", - "C:/tmp/outside.json", - ] { - let diagnostic = locator - .resolve(path) - .expect_err("platform-specific path syntax should be rejected"); - assert_eq!( - diagnostic.code, - HolmesDiagnosticCode::HlawArtifactPathEscape - ); - } -} - -#[test] -fn version_registry_accepts_current_versions() { - let registry = VersionRegistry::default(); - - for family in [ - ArtifactFamily::EvidenceBundle, - ArtifactFamily::LawDiff, - ArtifactFamily::LawCoverage, - ArtifactFamily::LawCapabilities, - ArtifactFamily::ContractBundleManifest, - ArtifactFamily::Policy, - ArtifactFamily::Report, - ArtifactFamily::AuditWitness, - ArtifactFamily::McpResponse, - ArtifactFamily::AgentSummary, - ArtifactFamily::GithubPayload, - ] { - let parsed = registry - .validate(family, Some("1.0.0")) - .expect("current schema version should be accepted"); - assert_eq!(parsed.major, 1); - assert_eq!(parsed.minor, 0); - assert_eq!(parsed.patch, 0); - } -} - -#[test] -fn version_registry_rejects_missing_malformed_and_unsupported_versions() { - let registry = VersionRegistry::default(); - - let missing = registry - .validate(ArtifactFamily::EvidenceBundle, None) - .expect_err("missing schema version should fail"); - let malformed = registry - .validate(ArtifactFamily::EvidenceBundle, Some("v1.0.0")) - .expect_err("malformed schema version should fail"); - let unsupported_major = registry - .validate(ArtifactFamily::EvidenceBundle, Some("2.0.0")) - .expect_err("unsupported major should fail"); - let unsupported_minor = registry - .validate(ArtifactFamily::EvidenceBundle, Some("1.1.0")) - .expect_err("unsupported minor should fail"); - let leading_zero = registry - .validate(ArtifactFamily::EvidenceBundle, Some("01.0.0")) - .expect_err("leading-zero schema version should fail"); - - assert_eq!(missing.code, HolmesDiagnosticCode::HlawSchemaVersionMissing); - assert_eq!( - malformed.code, - HolmesDiagnosticCode::HlawSchemaVersionMalformed - ); - assert_eq!( - unsupported_major.code, - HolmesDiagnosticCode::HlawSchemaVersionUnsupportedMajor - ); - assert_eq!( - unsupported_minor.code, - HolmesDiagnosticCode::HlawSchemaVersionUnsupportedMinor - ); - assert_eq!( - leading_zero.code, - HolmesDiagnosticCode::HlawSchemaVersionMalformed - ); -} - -#[test] -fn version_registry_fails_closed_when_requirement_is_missing() { - let registry = VersionRegistry::new([]); - - let diagnostic = registry - .validate(ArtifactFamily::EvidenceBundle, Some("999.999.0")) - .expect_err("missing registry entry should fail closed"); - - assert_eq!( - diagnostic.code, - HolmesDiagnosticCode::HlawSchemaVersionRequirementMissing - ); - assert_eq!( - diagnostic.artifact_family.as_deref(), - Some("evidence-bundle") - ); -} - -#[test] -fn schema_version_parser_rejects_leading_zero_identifiers() { - for version in ["01.0.0", "1.01.0", "1.0.01"] { - let diagnostic = VersionRegistry::default() - .validate(ArtifactFamily::EvidenceBundle, Some(version)) - .expect_err("leading-zero semver identifiers should be malformed"); - assert_eq!( - diagnostic.code, - HolmesDiagnosticCode::HlawSchemaVersionMalformed - ); - } -} - -#[test] -fn recording_ports_capture_outputs() { - let mut io = RecordingCommandIo::default(); - let mut mcp = wesley_holmes::InMemoryMcpResourceRegistry::default(); - - io.stdout("ready"); - io.stderr("diagnostic"); - mcp.put_resource("holmes://summary", b"payload") - .expect("MCP resource should record"); - - assert_eq!(io.stdout_lines(), &["ready".to_owned()]); - assert_eq!(io.stderr_lines(), &["diagnostic".to_owned()]); - assert_eq!(mcp.resource("holmes://summary"), Some(&b"payload"[..])); -} - -#[test] -fn report_renderer_fake_is_deterministic() { - let renderer = EchoReportRenderer::new("prefix:"); - - assert_eq!( - renderer - .render_report("body") - .expect("report rendering should be deterministic"), - "prefix:body" - ); -} - -fn evidence_bundle_with_law_diff_path(path: &str) -> HolmesLawEvidenceBundle { - let mut bundle = valid_evidence_bundle(); - bundle.artifacts.law_diff.path = path.to_owned(); - bundle -} - -fn valid_evidence_bundle() -> HolmesLawEvidenceBundle { - HolmesLawEvidenceBundle { - schema_version: "1.0.0".to_owned(), - bundle_id: "bundle-001".to_owned(), - artifacts: LawEvidenceArtifacts { - law_diff: artifact_ref("evidence/law-diff.json"), - law_coverage: artifact_ref("evidence/law-coverage.json"), - law_capabilities: artifact_ref("evidence/law-capabilities.json"), - contract_bundle_manifest: artifact_ref("evidence/bundle-manifest.json"), - policy: None, - report: None, - witness: None, - }, - provenance: BundleProvenance { - schema_hash: format!("sha256:{}", "a".repeat(64)), - law_hash: format!("sha256:{}", "b".repeat(64)), - policy_hash: None, - bundle_hash: format!("sha256:{}", "c".repeat(64)), - source: "test".to_owned(), - }, - } -} - -fn artifact_ref(path: &str) -> ArtifactRef { - ArtifactRef::new(path).with_schema_version("1.0.0") -} - -fn assert_diagnostic(diagnostics: &[wesley_holmes::HolmesDiagnostic], code: HolmesDiagnosticCode) { - assert!( - diagnostics.iter().any(|diagnostic| diagnostic.code == code), - "expected {code:?} in {diagnostics:#?}" - ); -} - -fn assert_diagnostic_field(diagnostics: &[wesley_holmes::HolmesDiagnostic], field_path: &str) { - assert!( - diagnostics - .iter() - .any(|diagnostic| diagnostic.field_path.as_deref() == Some(field_path)), - "expected field {field_path:?} in {diagnostics:#?}" - ); -} diff --git a/crates/wesley-holmes/tests/law_capability_ingest.rs b/crates/wesley-holmes/tests/law_capability_ingest.rs deleted file mode 100644 index b2b4c4b4..00000000 --- a/crates/wesley-holmes/tests/law_capability_ingest.rs +++ /dev/null @@ -1,261 +0,0 @@ -use wesley_holmes::{ - HolmesDiagnosticCode, JsonLawCapabilityIngestPort, LawCapabilityIngestPort, - LawCapabilityIngestStatus, -}; - -const CAPABILITY_REPORT: &str = r#"{ - "apiVersion": "wesley.law-capabilities/v1", - "reportOnly": true, - "runtimeEnforcement": false, - "note": "Footprint capabilities are report-only in weslaw v1; no runtime enforcement is claimed.", - "footprints": [ - { - "lawId": "operation.replaceRange.footprint", - "subject": "operation:Mutation.replaceRange", - "reads": [ - "TextBlob", - "Selection" - ], - "writes": [ - "TextBlob" - ], - "creates": [ - "TickReceipt" - ], - "forbids": [ - "Diagnostics" - ] - }, - { - "lawId": "operation.snapshot.footprint", - "subject": "operation:Query.snapshot", - "reads": [ - "TextBlob" - ], - "writes": [], - "creates": [], - "forbids": [] - } - ] -}"#; - -#[test] -fn law_capability_ingest_accepts_current_wesley_capability_report_json() { - let result = JsonLawCapabilityIngestPort.ingest_law_capabilities(CAPABILITY_REPORT.as_bytes()); - - assert_eq!(result.status, LawCapabilityIngestStatus::Valid); - assert!(result.diagnostics.is_empty()); - - let report = result - .report - .expect("valid law capability JSON should produce a report"); - assert_eq!(report.api_version, "wesley.law-capabilities/v1"); - assert!(report.report_only); - assert!(!report.runtime_enforcement); - assert_eq!(report.footprints.len(), 2); - assert_eq!(report.footprints[0].reads, ["TextBlob", "Selection"]); -} - -#[test] -fn law_capability_report_normalizes_operations_and_posture() { - let result = JsonLawCapabilityIngestPort.ingest_law_capabilities(CAPABILITY_REPORT.as_bytes()); - let report = result.report.expect("valid report should parse"); - - let operations = report.normalized_operations(); - - assert_eq!(operations.len(), 2); - assert_eq!(operations[0].subject, "operation:Mutation.replaceRange"); - assert_eq!(operations[0].operation_ref, "lawCapabilities.footprints[0]"); - assert!(operations[0].report_only); - assert!(!operations[0].runtime_enforcement); - assert_eq!( - operations[0].wording_hint, - "report-only footprint declaration; do not imply runtime enforcement" - ); - assert_eq!(operations[0].reads, ["Selection", "TextBlob"]); - assert_eq!(operations[0].writes, ["TextBlob"]); - assert_eq!(operations[0].creates, ["TickReceipt"]); - assert_eq!(operations[0].forbids, ["Diagnostics"]); -} - -#[test] -fn law_capability_ingest_rejects_missing_posture_fields() { - let missing_posture = CAPABILITY_REPORT.replace(" \"reportOnly\": true,\n", ""); - - let result = JsonLawCapabilityIngestPort.ingest_law_capabilities(missing_posture.as_bytes()); - - assert_eq!(result.status, LawCapabilityIngestStatus::Invalid); - assert!(result.report.is_none()); - assert_diagnostic( - &result.diagnostics, - HolmesDiagnosticCode::HlawCapabilityMissingPosture, - ); - assert_diagnostic_field(&result.diagnostics, "reportOnly"); -} - -#[test] -fn law_capability_ingest_rejects_legacy_capability_report_alias() { - let legacy = - CAPABILITY_REPORT.replace("wesley.law-capabilities/v1", "wesley.capability-report/v1"); - - let result = JsonLawCapabilityIngestPort.ingest_law_capabilities(legacy.as_bytes()); - - assert_eq!(result.status, LawCapabilityIngestStatus::Invalid); - assert!(result.report.is_none()); - assert_diagnostic( - &result.diagnostics, - HolmesDiagnosticCode::HlawCapabilityUnsupportedVersion, - ); - assert_diagnostic_field(&result.diagnostics, "apiVersion"); -} - -#[test] -fn law_capability_ingest_rejects_contradictory_resource_posture() { - let contradictory = CAPABILITY_REPORT.replace( - r#""forbids": [ - "Diagnostics" - ]"#, - r#""forbids": [ - "Diagnostics", - "TextBlob" - ]"#, - ); - - let result = JsonLawCapabilityIngestPort.ingest_law_capabilities(contradictory.as_bytes()); - - assert_eq!(result.status, LawCapabilityIngestStatus::Invalid); - assert!(result.report.is_none()); - assert_diagnostic( - &result.diagnostics, - HolmesDiagnosticCode::HlawCapabilityContradictoryResourcePosture, - ); - assert_diagnostic_field(&result.diagnostics, "footprints[0].writes"); -} - -#[test] -fn law_capability_ingest_rejects_forbids_overlapping_all_touched_resources() { - let contradictory = r#"{ - "apiVersion": "wesley.law-capabilities/v1", - "reportOnly": true, - "runtimeEnforcement": false, - "footprints": [ - { - "lawId": "operation.replaceRange.footprint", - "subject": "operation:Mutation.replaceRange", - "reads": [ - "TextBlob" - ], - "writes": [], - "creates": [], - "forbids": [ - "Cursor", - "DerivedRead", - "TextBlob" - ], - "slots": [ - { - "name": "cursor", - "kind": "Cursor", - "bindFromArg": "input.cursor", - "access": [ - "read" - ] - } - ], - "closures": [ - { - "name": "visibleRange", - "fromSlot": "cursor", - "operator": "window", - "argBindings": [ - "cursor" - ], - "reads": [ - "DerivedRead" - ], - "cardinality": "many" - } - ] - } - ] -}"#; - - let result = JsonLawCapabilityIngestPort.ingest_law_capabilities(contradictory.as_bytes()); - - assert_eq!(result.status, LawCapabilityIngestStatus::Invalid); - assert!(result.report.is_none()); - assert_diagnostic( - &result.diagnostics, - HolmesDiagnosticCode::HlawCapabilityContradictoryResourcePosture, - ); - assert_diagnostic_field(&result.diagnostics, "footprints[0].reads"); - assert_diagnostic_field(&result.diagnostics, "footprints[0].slots[0].kind"); - assert_diagnostic_field(&result.diagnostics, "footprints[0].closures[0].reads"); -} - -#[test] -fn law_capability_ingest_requires_explicit_empty_footprint() { - let empty = r#"{ - "apiVersion": "wesley.law-capabilities/v1", - "reportOnly": true, - "runtimeEnforcement": false, - "footprints": [ - { - "lawId": "operation.noop.footprint", - "subject": "operation:Mutation.noop", - "reads": [], - "writes": [], - "creates": [], - "forbids": [] - } - ] -}"#; - - let result = JsonLawCapabilityIngestPort.ingest_law_capabilities(empty.as_bytes()); - - assert_eq!(result.status, LawCapabilityIngestStatus::Invalid); - assert_diagnostic( - &result.diagnostics, - HolmesDiagnosticCode::HlawCapabilityImplicitEmptyFootprint, - ); - - let explicit_empty = empty.replace( - " \"forbids\": []", - " \"forbids\": [],\n \"intentionallyEmpty\": true", - ); - let result = JsonLawCapabilityIngestPort.ingest_law_capabilities(explicit_empty.as_bytes()); - - assert_eq!(result.status, LawCapabilityIngestStatus::Valid); -} - -#[test] -fn law_capability_ingest_rejects_unsupported_api_version() { - let unsupported = - CAPABILITY_REPORT.replace("wesley.law-capabilities/v1", "wesley.law-capabilities/v2"); - - let result = JsonLawCapabilityIngestPort.ingest_law_capabilities(unsupported.as_bytes()); - - assert_eq!(result.status, LawCapabilityIngestStatus::Invalid); - assert!(result.report.is_none()); - assert_diagnostic( - &result.diagnostics, - HolmesDiagnosticCode::HlawCapabilityUnsupportedVersion, - ); - assert_diagnostic_field(&result.diagnostics, "apiVersion"); -} - -fn assert_diagnostic(diagnostics: &[wesley_holmes::HolmesDiagnostic], code: HolmesDiagnosticCode) { - assert!( - diagnostics.iter().any(|diagnostic| diagnostic.code == code), - "expected {code:?} in {diagnostics:#?}" - ); -} - -fn assert_diagnostic_field(diagnostics: &[wesley_holmes::HolmesDiagnostic], field_path: &str) { - assert!( - diagnostics - .iter() - .any(|diagnostic| diagnostic.field_path.as_deref() == Some(field_path)), - "expected field {field_path:?} in {diagnostics:#?}" - ); -} diff --git a/crates/wesley-holmes/tests/law_coverage_gate.rs b/crates/wesley-holmes/tests/law_coverage_gate.rs deleted file mode 100644 index 2e403c2a..00000000 --- a/crates/wesley-holmes/tests/law_coverage_gate.rs +++ /dev/null @@ -1,138 +0,0 @@ -use wesley_holmes::{ - evaluate_law_coverage_gates, CoverageAbsentCategoryBehavior, CoverageUnavailableBehavior, - JsonLawCoverageIngestPort, LawCoverageCategoryThreshold, LawCoverageGatePolicy, - LawCoverageGateState, LawCoverageIngestPort, -}; - -const RELEASE_COVERAGE: &str = r#"{ - "apiVersion": "wesley.law-coverage/v1", - "profile": "release", - "requiredTotal": 3, - "requiredCovered": 1, - "requiredPercent": 33.3, - "categories": [ - { - "id": "mutationFootprintLaw", - "label": "Mutation footprint law", - "required": true, - "total": 3, - "covered": 1, - "missingSubjects": [ - "operation:Mutation.archive", - "operation:Mutation.replaceRange" - ] - } - ] -}"#; - -#[test] -fn coverage_gate_fails_required_category_below_failure_threshold() { - let coverage = normalized_release_coverage(); - let policy = policy(vec![LawCoverageCategoryThreshold { - category_id: "mutationFootprintLaw".to_owned(), - required: true, - warning_threshold: None, - failure_threshold: Some(100.0), - }]); - - let decisions = evaluate_law_coverage_gates(Some(&coverage), &policy); - - assert_eq!(decisions.len(), 1); - assert_eq!(decisions[0].state, LawCoverageGateState::Fail); - assert_eq!(decisions[0].state_label, "fail"); - assert_eq!(decisions[0].actual_percent, Some(33.3)); - assert_eq!(decisions[0].covered, Some(1)); - assert_eq!(decisions[0].total, Some(3)); - assert_eq!( - decisions[0].missing_subjects, - ["operation:Mutation.archive"] - ); - assert_eq!(decisions[0].omitted_missing_subject_count, 1); -} - -#[test] -fn coverage_gate_warns_for_advisory_gap_without_failure_threshold() { - let coverage = normalized_release_coverage(); - let policy = policy(vec![LawCoverageCategoryThreshold { - category_id: "mutationFootprintLaw".to_owned(), - required: false, - warning_threshold: Some(80.0), - failure_threshold: None, - }]); - - let decisions = evaluate_law_coverage_gates(Some(&coverage), &policy); - - assert_eq!(decisions[0].state, LawCoverageGateState::Warn); - assert_eq!(decisions[0].state_label, "warn"); - assert_eq!(decisions[0].warning_threshold, Some(80.0)); - assert!(decisions[0].rationale.contains("warning threshold")); -} - -#[test] -fn coverage_gate_passes_exact_boundary_threshold() { - let coverage = normalized_release_coverage(); - let policy = policy(vec![LawCoverageCategoryThreshold { - category_id: "mutationFootprintLaw".to_owned(), - required: true, - warning_threshold: None, - failure_threshold: Some(33.3), - }]); - - let decisions = evaluate_law_coverage_gates(Some(&coverage), &policy); - - assert_eq!(decisions[0].state, LawCoverageGateState::Pass); - assert_eq!(decisions[0].actual_percent, Some(33.3)); -} - -#[test] -fn coverage_gate_marks_unavailable_evidence_without_false_pass() { - let policy = policy(vec![LawCoverageCategoryThreshold { - category_id: "mutationFootprintLaw".to_owned(), - required: true, - warning_threshold: None, - failure_threshold: Some(100.0), - }]); - - let decisions = evaluate_law_coverage_gates(None, &policy); - - assert_eq!(decisions[0].state, LawCoverageGateState::Unavailable); - assert_eq!(decisions[0].state_label, "unavailable"); - assert_eq!(decisions[0].actual_percent, None); -} - -#[test] -fn coverage_gate_follows_absent_category_policy() { - let coverage = normalized_release_coverage(); - let mut policy = policy(vec![LawCoverageCategoryThreshold { - category_id: "customScalarSemantics".to_owned(), - required: true, - warning_threshold: None, - failure_threshold: Some(100.0), - }]); - policy.absent_category_behavior = CoverageAbsentCategoryBehavior::Fail; - - let decisions = evaluate_law_coverage_gates(Some(&coverage), &policy); - - assert_eq!(decisions[0].state, LawCoverageGateState::Fail); - assert_eq!(decisions[0].actual_percent, None); - assert!(decisions[0].rationale.contains("absent")); -} - -fn normalized_release_coverage() -> wesley_holmes::NormalizedLawCoverageProfile { - JsonLawCoverageIngestPort - .ingest_law_coverage(RELEASE_COVERAGE.as_bytes()) - .report - .expect("coverage fixture should parse") - .normalized_profile(10) -} - -fn policy(categories: Vec) -> LawCoverageGatePolicy { - LawCoverageGatePolicy { - profile: "release".to_owned(), - categories, - missing_subject_display_limit: 1, - unavailable_behavior: CoverageUnavailableBehavior::Unavailable, - absent_category_behavior: CoverageAbsentCategoryBehavior::Unavailable, - evidence_ref: Some("artifacts/law-coverage.json".to_owned()), - } -} diff --git a/crates/wesley-holmes/tests/law_coverage_ingest.rs b/crates/wesley-holmes/tests/law_coverage_ingest.rs deleted file mode 100644 index 842eb000..00000000 --- a/crates/wesley-holmes/tests/law_coverage_ingest.rs +++ /dev/null @@ -1,193 +0,0 @@ -use wesley_holmes::{ - HolmesDiagnosticCode, JsonLawCoverageIngestPort, LawCoverageIngestPort, LawCoverageIngestStatus, -}; - -const RELEASE_COVERAGE: &str = r#"{ - "apiVersion": "wesley.law-coverage/v1", - "profile": "release", - "requiredTotal": 6, - "requiredCovered": 1, - "requiredPercent": 16.7, - "categories": [ - { - "id": "customScalarSemantics", - "label": "Custom scalar semantic law", - "required": true, - "total": 3, - "covered": 0, - "missingSubjects": [ - "scalar:Hash", - "scalar:PositiveInt", - "scalar:WorldlineTick" - ] - }, - { - "id": "mutationFootprintLaw", - "label": "Mutation footprint law", - "required": true, - "total": 1, - "covered": 1, - "missingSubjects": [] - }, - { - "id": "variantInputLaw", - "label": "Input variant law", - "required": true, - "total": 1, - "covered": 0, - "missingSubjects": [ - "input:PlaybackModeInput" - ] - }, - { - "id": "channelLaw", - "label": "Channel law", - "required": true, - "total": 1, - "covered": 0, - "missingSubjects": [ - "channel:TTD@1" - ] - } - ] -}"#; - -#[test] -fn law_coverage_ingest_accepts_wesley_law_coverage_v1_json() { - let result = JsonLawCoverageIngestPort.ingest_law_coverage(RELEASE_COVERAGE.as_bytes()); - - assert_eq!(result.status, LawCoverageIngestStatus::Valid); - assert!(result.diagnostics.is_empty()); - - let report = result - .report - .expect("valid law coverage should produce a report"); - assert_eq!(report.api_version, "wesley.law-coverage/v1"); - assert_eq!(report.profile, "release"); - assert_eq!(report.required_total, 6); - assert_eq!(report.required_covered, 1); - assert_eq!(report.categories.len(), 4); - assert_eq!(report.categories[0].id, "customScalarSemantics"); - assert!(report.categories[0].required); - assert_eq!(report.categories[0].missing_subjects.len(), 3); - assert_eq!(report.categories[1].id, "mutationFootprintLaw"); - assert!(report.categories[1].missing_subjects.is_empty()); -} - -#[test] -fn law_coverage_report_normalizes_percentages_subjects_and_omitted_counts() { - let result = JsonLawCoverageIngestPort.ingest_law_coverage(RELEASE_COVERAGE.as_bytes()); - let report = result - .report - .expect("valid law coverage should produce a report"); - - let normalized = report.normalized_profile(2); - - assert_eq!(normalized.profile, "release"); - assert_eq!(normalized.required_total, 6); - assert_eq!(normalized.required_covered, 1); - assert_eq!(normalized.required_percent, 16.7); - let scalar = normalized - .category("customScalarSemantics") - .expect("scalar category should normalize"); - assert_eq!(scalar.category_ref, "lawCoverage.categories[0]"); - assert_eq!(scalar.percent, 0.0); - assert_eq!(scalar.missing_count, 3); - assert_eq!( - scalar.missing_subjects, - ["scalar:Hash", "scalar:PositiveInt", "scalar:WorldlineTick"] - ); - assert_eq!( - scalar.displayed_missing_subjects, - ["scalar:Hash", "scalar:PositiveInt"] - ); - assert_eq!(scalar.omitted_missing_subject_count, 1); - let mutation = normalized - .category("mutationFootprintLaw") - .expect("mutation category should normalize"); - assert_eq!(mutation.percent, 100.0); - assert_eq!(mutation.omitted_missing_subject_count, 0); -} - -#[test] -fn law_coverage_ingest_rejects_covered_counts_above_totals() { - let inconsistent = RELEASE_COVERAGE.replace("\"covered\": 1", "\"covered\": 2"); - - let result = JsonLawCoverageIngestPort.ingest_law_coverage(inconsistent.as_bytes()); - - assert_eq!(result.status, LawCoverageIngestStatus::Invalid); - assert!(result.report.is_none()); - assert_diagnostic( - &result.diagnostics, - HolmesDiagnosticCode::HlawCoverageInconsistentCounts, - ); - assert_diagnostic_field(&result.diagnostics, "categories[1].covered"); -} - -#[test] -fn law_coverage_ingest_rejects_missing_subject_count_mismatch() { - let mismatch = RELEASE_COVERAGE.replace( - r#""missingSubjects": [ - "scalar:Hash", - "scalar:PositiveInt", - "scalar:WorldlineTick" - ]"#, - r#""missingSubjects": [ - "scalar:Hash", - "scalar:PositiveInt" - ]"#, - ); - - let result = JsonLawCoverageIngestPort.ingest_law_coverage(mismatch.as_bytes()); - - assert_eq!(result.status, LawCoverageIngestStatus::Invalid); - assert!(result.report.is_none()); - assert_diagnostic( - &result.diagnostics, - HolmesDiagnosticCode::HlawCoverageMissingCountMismatch, - ); - assert_diagnostic_field(&result.diagnostics, "categories[0].missingSubjects"); -} - -#[test] -fn law_coverage_ingest_rejects_unsupported_api_version() { - let unsupported = RELEASE_COVERAGE.replace("wesley.law-coverage/v1", "wesley.law-coverage/v2"); - - let result = JsonLawCoverageIngestPort.ingest_law_coverage(unsupported.as_bytes()); - - assert_eq!(result.status, LawCoverageIngestStatus::Invalid); - assert!(result.report.is_none()); - assert_diagnostic( - &result.diagnostics, - HolmesDiagnosticCode::HlawCoverageUnsupportedVersion, - ); - assert_diagnostic_field(&result.diagnostics, "apiVersion"); -} - -#[test] -fn law_coverage_ingest_rejects_malformed_json() { - let result = JsonLawCoverageIngestPort.ingest_law_coverage(b"{broken"); - - assert_eq!(result.status, LawCoverageIngestStatus::Invalid); - assert!(result.report.is_none()); - assert_diagnostic( - &result.diagnostics, - HolmesDiagnosticCode::HlawCoverageMalformedJson, - ); -} - -fn assert_diagnostic(diagnostics: &[wesley_holmes::HolmesDiagnostic], code: HolmesDiagnosticCode) { - assert!( - diagnostics.iter().any(|diagnostic| diagnostic.code == code), - "expected {code:?} in {diagnostics:#?}" - ); -} - -fn assert_diagnostic_field(diagnostics: &[wesley_holmes::HolmesDiagnostic], field_path: &str) { - assert!( - diagnostics - .iter() - .any(|diagnostic| diagnostic.field_path.as_deref() == Some(field_path)), - "expected field {field_path:?} in {diagnostics:#?}" - ); -} diff --git a/crates/wesley-holmes/tests/law_diff_ingest.rs b/crates/wesley-holmes/tests/law_diff_ingest.rs deleted file mode 100644 index ca15967c..00000000 --- a/crates/wesley-holmes/tests/law_diff_ingest.rs +++ /dev/null @@ -1,224 +0,0 @@ -use wesley_holmes::{ - HolmesDiagnosticCode, JsonLawDiffIngestPort, LawDiffEventKind, LawDiffIngestPort, - LawDiffIngestStatus, -}; - -const CI_SEMANTIC_DIFF: &str = - include_str!("../../../test/fixtures/weslaw/diff/ci-semantic-diff.json"); - -#[test] -fn law_diff_ingest_accepts_wesley_law_diff_v1_json() { - let result = JsonLawDiffIngestPort.ingest_law_diff(CI_SEMANTIC_DIFF.as_bytes()); - - assert_eq!(result.status, LawDiffIngestStatus::Valid); - assert!(result.diagnostics.is_empty()); - - let report = result - .report - .expect("valid law diff should produce a report"); - assert_eq!(report.api_version, "wesley.law-diff/v1"); - assert_eq!( - report.old_schema_hash, - "sha256:ee681e8c2c99acb5db74f09b2eb06cca2e9379fc7d69627d3287cba6177ac4b6" - ); - assert_eq!( - report.new_law_hash, - "sha256:ba4a878e94a961bbbe68b421aa2829f39e9e464a4d3e4647dc8d4ccb0c55eab7" - ); - assert_eq!(report.changes.len(), 3); - - let positive_int = &report.changes[0]; - assert_eq!(positive_int.kind, LawDiffEventKind::LawWeakened); - assert_eq!( - positive_int.law_id.as_deref(), - Some("echo.scalar.positiveInt.u32-positive") - ); - assert_eq!(positive_int.subject.as_deref(), Some("scalar:PositiveInt")); - assert_eq!(positive_int.field_changes.len(), 3); - assert_eq!(positive_int.field_changes[0].path, "body.minInclusive"); - - let footprint = &report.changes[2]; - assert_eq!(footprint.kind, LawDiffEventKind::FootprintExpanded); - assert_eq!( - footprint.law_id.as_deref(), - Some("jedit.op.replaceRangeAsTick.footprint") - ); - assert_eq!(footprint.added_reads, ["TextBlob"]); - assert_eq!(footprint.added_creates, ["TickReceipt"]); - assert_eq!(footprint.removed_forbids, ["Diagnostics"]); -} - -#[test] -fn law_diff_report_normalizes_events_without_reclassifying_wesley_kind() { - let result = JsonLawDiffIngestPort.ingest_law_diff(CI_SEMANTIC_DIFF.as_bytes()); - let report = result.report.expect("valid law diff should parse"); - - let records = report.normalized_events(); - - assert_eq!(records.len(), 3); - assert_eq!(records[0].event_ref, "lawDiff.changes[0]"); - assert_eq!(records[0].event_index, 0); - assert_eq!(records[0].kind, LawDiffEventKind::LawWeakened); - assert_eq!( - records[0].law_id.as_deref(), - Some("echo.scalar.positiveInt.u32-positive") - ); - assert_eq!(records[0].subject.as_deref(), Some("scalar:PositiveInt")); - assert_eq!( - records[0].old_law_hash, - "sha256:88fcbb7fb07cc0bb5dfa30252ec61badb3a8dff1be71c0f20ae21031e4e80f51" - ); - assert_eq!( - records[0].new_law_hash, - "sha256:ba4a878e94a961bbbe68b421aa2829f39e9e464a4d3e4647dc8d4ccb0c55eab7" - ); - assert_eq!(records[2].event_ref, "lawDiff.changes[2]"); - assert_eq!(records[2].kind, LawDiffEventKind::FootprintExpanded); - assert_eq!(records[2].added_reads, ["TextBlob"]); -} - -#[test] -fn law_diff_normalized_events_preserve_repeated_law_ids_as_distinct_records() { - let repeated_law_id = format!( - r#"{{ - "apiVersion": "wesley.law-diff/v1", - "oldSchemaHash": "sha256:{hash_a}", - "newSchemaHash": "sha256:{hash_a}", - "oldLawHash": "sha256:{hash_b}", - "newLawHash": "sha256:{hash_c}", - "changes": [ - {{ - "kind": "LAW_WEAKENED", - "lawId": "echo.scalar.positiveInt.u32-positive", - "subject": "scalar:PositiveInt", - "lawKind": "scalarSemantics", - "reviewPosture": "requires-review" - }}, - {{ - "kind": "LAW_TAGS_CHANGED", - "lawId": "echo.scalar.positiveInt.u32-positive", - "subject": "scalar:PositiveInt", - "lawKind": "scalarSemantics", - "reviewPosture": "requires-review" - }} - ] -}}"#, - hash_a = "a".repeat(64), - hash_b = "b".repeat(64), - hash_c = "c".repeat(64) - ); - - let result = JsonLawDiffIngestPort.ingest_law_diff(repeated_law_id.as_bytes()); - let records = result - .report - .expect("valid repeated law id fixture should parse") - .normalized_events(); - - assert_eq!(records.len(), 2); - assert_eq!(records[0].event_ref, "lawDiff.changes[0]"); - assert_eq!(records[1].event_ref, "lawDiff.changes[1]"); - assert_eq!(records[0].law_id, records[1].law_id); - assert_ne!(records[0].event_ref, records[1].event_ref); - assert_eq!(records[0].kind, LawDiffEventKind::LawWeakened); - assert_eq!(records[1].kind, LawDiffEventKind::LawTagsChanged); -} - -#[test] -fn law_diff_ingest_rejects_unsupported_api_version() { - let unsupported = CI_SEMANTIC_DIFF.replace("wesley.law-diff/v1", "wesley.law-diff/v2"); - - let result = JsonLawDiffIngestPort.ingest_law_diff(unsupported.as_bytes()); - - assert_eq!(result.status, LawDiffIngestStatus::Invalid); - assert!(result.report.is_none()); - assert_diagnostic( - &result.diagnostics, - HolmesDiagnosticCode::HlawDiffUnsupportedVersion, - ); - assert_diagnostic_field(&result.diagnostics, "apiVersion"); -} - -#[test] -fn law_diff_ingest_rejects_malformed_json_without_findings() { - let result = JsonLawDiffIngestPort.ingest_law_diff(b"{not-json"); - - assert_eq!(result.status, LawDiffIngestStatus::Invalid); - assert!(result.report.is_none()); - assert_diagnostic( - &result.diagnostics, - HolmesDiagnosticCode::HlawDiffMalformedJson, - ); -} - -#[test] -fn law_diff_ingest_rejects_unknown_event_kind() { - let unknown_kind = CI_SEMANTIC_DIFF.replace("LAW_WEAKENED", "QUANTUM_LAW_SHIFT"); - - let result = JsonLawDiffIngestPort.ingest_law_diff(unknown_kind.as_bytes()); - - assert_eq!(result.status, LawDiffIngestStatus::Invalid); - assert!(result.report.is_none()); - assert_diagnostic( - &result.diagnostics, - HolmesDiagnosticCode::HlawDiffUnknownEventKind, - ); - assert_diagnostic_field(&result.diagnostics, "changes[0].kind"); -} - -#[test] -fn law_diff_ingest_rejects_duplicate_law_id_event_identity() { - let duplicate = format!( - r#"{{ - "apiVersion": "wesley.law-diff/v1", - "oldSchemaHash": "sha256:{hash_a}", - "newSchemaHash": "sha256:{hash_a}", - "oldLawHash": "sha256:{hash_b}", - "newLawHash": "sha256:{hash_c}", - "changes": [ - {{ - "kind": "LAW_WEAKENED", - "lawId": "echo.scalar.positiveInt.u32-positive", - "subject": "scalar:PositiveInt", - "lawKind": "scalarSemantics", - "reviewPosture": "requires-review" - }}, - {{ - "kind": "LAW_WEAKENED", - "lawId": "echo.scalar.positiveInt.u32-positive", - "subject": "scalar:PositiveInt", - "lawKind": "scalarSemantics", - "reviewPosture": "requires-review" - }} - ] -}}"#, - hash_a = "a".repeat(64), - hash_b = "b".repeat(64), - hash_c = "c".repeat(64) - ); - - let result = JsonLawDiffIngestPort.ingest_law_diff(duplicate.as_bytes()); - - assert_eq!(result.status, LawDiffIngestStatus::Invalid); - assert!(result.report.is_none()); - assert_diagnostic( - &result.diagnostics, - HolmesDiagnosticCode::HlawDiffDuplicateEvent, - ); - assert_diagnostic_field(&result.diagnostics, "changes[1].lawId"); -} - -fn assert_diagnostic(diagnostics: &[wesley_holmes::HolmesDiagnostic], code: HolmesDiagnosticCode) { - assert!( - diagnostics.iter().any(|diagnostic| diagnostic.code == code), - "expected {code:?} in {diagnostics:#?}" - ); -} - -fn assert_diagnostic_field(diagnostics: &[wesley_holmes::HolmesDiagnostic], field_path: &str) { - assert!( - diagnostics - .iter() - .any(|diagnostic| diagnostic.field_path.as_deref() == Some(field_path)), - "expected field {field_path:?} in {diagnostics:#?}" - ); -} diff --git a/crates/wesley-holmes/tests/policy_core.rs b/crates/wesley-holmes/tests/policy_core.rs deleted file mode 100644 index 618272ae..00000000 --- a/crates/wesley-holmes/tests/policy_core.rs +++ /dev/null @@ -1,217 +0,0 @@ -use wesley_holmes::{ - map_semantic_finding_severities, matching_suppressions_for_finding, - normalize_law_assurance_policy, parse_law_assurance_policy, HolmesDiagnosticCode, - JsonLawDiffIngestPort, LawCoverageGateState, LawDiffEventKind, LawDiffIngestPort, - LawFindingSeverity, SemanticChangeFinding, HOLMES_LAW_ASSURANCE_POLICY_API_VERSION, -}; - -const CI_SEMANTIC_DIFF: &str = - include_str!("../../../test/fixtures/weslaw/diff/ci-semantic-diff.json"); - -const RELEASE_POLICY: &str = r#"{ - "apiVersion": "holmes.law-assurance-policy/v1", - "defaultProfile": "release", - "defaultSeverity": "advisory", - "severityMappings": { - "lawWeakened": "critical" - }, - "coverageSeverityMappings": { - "fail": "error", - "warn": "warning", - "unavailable": "error" - }, - "profiles": { - "base": { - "missingSubjectDisplayLimit": 1, - "unavailableBehavior": "unavailable", - "absentCategoryBehavior": "fail", - "coverageThresholds": { - "mutationFootprintLaw": { - "required": true, - "warningThreshold": 100.0, - "failureThreshold": 100.0 - } - } - }, - "release": { - "inherits": "base", - "severityMappings": { - "footprintExpanded": "error" - }, - "coverageThresholds": { - "scalarSemantics": { - "required": false, - "warningThreshold": 90.0 - } - }, - "nonOverridableGates": [ - "bundle-traceability" - ], - "suppressions": [ - { - "id": "known-scalar-window", - "target": { - "kind": "law-id", - "selector": "echo.scalar.positiveInt.u32-positive" - }, - "reason": "temporary scalar migration window", - "owner": "release-team", - "createdOn": "2026-06-01", - "expiresOn": "2026-07-01", - "allowedSeverities": [ - "critical" - ], - "auditTags": [ - "migration" - ] - } - ] - }, - "local": { - "inherits": "base", - "severityMappings": { - "lawWeakened": "warning" - } - } - } -}"#; - -#[test] -fn policy_schema_normalizes_default_profile_with_inherited_thresholds() { - let schema = - parse_law_assurance_policy(RELEASE_POLICY.as_bytes()).expect("policy should parse"); - - let policy = - normalize_law_assurance_policy(&schema, None).expect("default profile should normalize"); - - assert_eq!(schema.api_version, HOLMES_LAW_ASSURANCE_POLICY_API_VERSION); - assert_eq!(policy.profile, "release"); - assert_eq!(policy.default_severity, Some(LawFindingSeverity::Advisory)); - assert_eq!( - policy.severity_mappings.get("LAW_WEAKENED"), - Some(&LawFindingSeverity::Critical) - ); - assert_eq!( - policy.severity_mappings.get("FOOTPRINT_EXPANDED"), - Some(&LawFindingSeverity::Error) - ); - assert_eq!( - policy.severity_for_coverage_gate_state(LawCoverageGateState::Fail), - Some(LawFindingSeverity::Error) - ); - assert_eq!(policy.coverage_gate_policy.profile, "release"); - assert_eq!(policy.coverage_gate_policy.missing_subject_display_limit, 1); - assert_eq!(policy.coverage_gate_policy.categories.len(), 2); - assert_eq!( - policy.coverage_gate_policy.categories[0].category_id, - "mutationFootprintLaw" - ); - assert_eq!( - policy.coverage_gate_policy.categories[0].failure_threshold, - Some(100.0) - ); - assert_eq!( - policy.coverage_gate_policy.categories[1].category_id, - "scalarSemantics" - ); - assert_eq!( - policy.coverage_gate_policy.categories[1].warning_threshold, - Some(90.0) - ); - assert_eq!(policy.suppressions.len(), 1); - assert_eq!(policy.non_overridable_gates, ["bundle-traceability"]); -} - -#[test] -fn policy_schema_rejects_unknown_top_level_field() { - let err = parse_law_assurance_policy( - br#"{ - "apiVersion": "holmes.law-assurance-policy/v1", - "profiles": { "release": {} }, - "implicitEnvironmentProfile": true - }"#, - ) - .expect_err("unknown top-level fields should be rejected"); - - assert_eq!(err.code, HolmesDiagnosticCode::HlawPolicyUnknownField); - assert_eq!( - err.field_path.as_deref(), - Some("implicitEnvironmentProfile") - ); -} - -#[test] -fn policy_normalization_rejects_unknown_profile() { - let schema = - parse_law_assurance_policy(RELEASE_POLICY.as_bytes()).expect("policy should parse"); - - let err = normalize_law_assurance_policy(&schema, Some("staging")) - .expect_err("unknown profile should be rejected"); - - assert_eq!(err.code, HolmesDiagnosticCode::HlawPolicyUnknownProfile); - assert_eq!(err.field_path.as_deref(), Some("profiles.staging")); -} - -#[test] -fn severity_policy_preserves_wesley_event_identity() { - let schema = - parse_law_assurance_policy(RELEASE_POLICY.as_bytes()).expect("policy should parse"); - let policy = normalize_law_assurance_policy(&schema, Some("local")) - .expect("local profile should normalize"); - let findings = semantic_findings(); - - let mapped = - map_semantic_finding_severities(&findings, &policy).expect("severity mapping should apply"); - let original = findings - .iter() - .find(|finding| finding.event_kind == LawDiffEventKind::LawWeakened) - .expect("fixture should contain weakened law"); - let remapped = mapped - .iter() - .find(|finding| finding.finding_id == original.finding_id) - .expect("mapped finding should retain identity"); - - assert_eq!(remapped.severity, LawFindingSeverity::Warning); - assert_eq!(remapped.severity_label, "warning"); - assert_eq!(remapped.event_kind, original.event_kind); - assert_eq!(remapped.change_posture, original.change_posture); - assert_eq!(remapped.law_id, original.law_id); - assert_eq!(remapped.subject, original.subject); -} - -#[test] -fn suppression_policy_matches_narrow_unexpired_findings_only() { - let schema = - parse_law_assurance_policy(RELEASE_POLICY.as_bytes()).expect("policy should parse"); - let policy = normalize_law_assurance_policy(&schema, Some("release")) - .expect("release profile should normalize"); - let findings = semantic_findings(); - let suppressed = findings - .iter() - .find(|finding| finding.law_id.as_deref() == Some("echo.scalar.positiveInt.u32-positive")) - .expect("fixture should contain scalar finding"); - - let active = matching_suppressions_for_finding(suppressed, &policy, "2026-06-04"); - let expired = matching_suppressions_for_finding(suppressed, &policy, "2026-07-02"); - - assert_eq!(active.len(), 1); - assert_eq!(active[0].suppression_id, "known-scalar-window"); - assert_eq!(active[0].owner, "release-team"); - assert_eq!(active[0].audit_tags, ["migration"]); - assert!(expired.is_empty()); -} - -fn semantic_findings() -> Vec { - let report = JsonLawDiffIngestPort - .ingest_law_diff(CI_SEMANTIC_DIFF.as_bytes()) - .report - .expect("fixture should parse"); - - wesley_holmes::semantic_change_findings_from_law_diff( - &report, - "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", - "evidence/law-diff.json", - Some("policy-test".to_owned()), - ) - .expect("findings should construct") -} diff --git a/crates/wesley-holmes/tests/semantic_findings.rs b/crates/wesley-holmes/tests/semantic_findings.rs deleted file mode 100644 index a1645021..00000000 --- a/crates/wesley-holmes/tests/semantic_findings.rs +++ /dev/null @@ -1,149 +0,0 @@ -use wesley_holmes::{ - semantic_change_findings_from_law_diff, HolmesDiagnosticCode, JsonLawDiffIngestPort, - LawDiffEventKind, LawDiffIngestPort, LawDiffReviewPosture, LawFindingSeverity, - NormalizedLawDiffEvent, SemanticChangeFinding, -}; - -const CI_SEMANTIC_DIFF: &str = - include_str!("../../../test/fixtures/weslaw/diff/ci-semantic-diff.json"); -const BUNDLE_HASH: &str = "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"; - -#[test] -fn semantic_findings_preserve_wesley_event_classification_and_traceability() { - let report = JsonLawDiffIngestPort - .ingest_law_diff(CI_SEMANTIC_DIFF.as_bytes()) - .report - .expect("fixture should parse"); - - let findings = semantic_change_findings_from_law_diff( - &report, - BUNDLE_HASH, - "artifacts/law-diff.json", - Some("release".to_owned()), - ) - .expect("findings should construct"); - - assert_eq!(findings.len(), 3); - let weakened = findings - .iter() - .find(|finding| finding.law_id.as_deref() == Some("echo.scalar.positiveInt.u32-positive")) - .expect("weakened law finding should exist"); - assert_eq!(weakened.source_artifact_ref, "artifacts/law-diff.json"); - assert_eq!(weakened.bundle_hash_family, BUNDLE_HASH); - assert_eq!(weakened.event_ref, "lawDiff.changes[0]"); - assert_eq!( - weakened.law_id.as_deref(), - Some("echo.scalar.positiveInt.u32-positive") - ); - assert_eq!(weakened.subject.as_deref(), Some("scalar:PositiveInt")); - assert_eq!(weakened.subject_kind.as_deref(), Some("scalar")); - assert_eq!( - weakened.change_posture, - LawDiffReviewPosture::RequiresReview - ); - assert_eq!(weakened.severity, LawFindingSeverity::Critical); - assert_eq!(weakened.severity_label, "critical"); - assert!(weakened.summary.contains("LAW_WEAKENED")); - assert_eq!(weakened.field_changes.len(), 3); - - let footprint = findings - .iter() - .find(|finding| finding.event_kind == LawDiffEventKind::FootprintExpanded) - .expect("footprint finding should exist"); - assert_eq!(footprint.severity, LawFindingSeverity::Error); - assert_eq!(footprint.added_reads, ["TextBlob"]); - assert_eq!(footprint.added_creates, ["TickReceipt"]); - assert_eq!(footprint.removed_forbids, ["Diagnostics"]); -} - -#[test] -fn semantic_finding_ids_are_stable_and_distinguish_distinct_events() { - let report = JsonLawDiffIngestPort - .ingest_law_diff(CI_SEMANTIC_DIFF.as_bytes()) - .report - .expect("fixture should parse"); - - let first = semantic_change_findings_from_law_diff( - &report, - BUNDLE_HASH, - "artifacts/law-diff.json", - Some("release".to_owned()), - ) - .expect("findings should construct"); - let second = semantic_change_findings_from_law_diff( - &report, - BUNDLE_HASH, - "artifacts/law-diff.json", - Some("release".to_owned()), - ) - .expect("findings should construct"); - - assert_eq!(first[0].finding_id, second[0].finding_id); - assert_ne!(first[0].finding_id, first[1].finding_id); - assert!(first[0].finding_id.starts_with("semantic-change:")); -} - -#[test] -fn semantic_findings_are_sorted_by_severity_and_subject() { - let report = JsonLawDiffIngestPort - .ingest_law_diff(CI_SEMANTIC_DIFF.as_bytes()) - .report - .expect("fixture should parse"); - - let findings = semantic_change_findings_from_law_diff( - &report, - BUNDLE_HASH, - "artifacts/law-diff.json", - None, - ) - .expect("findings should construct"); - - assert_eq!(findings[0].severity, LawFindingSeverity::Critical); - assert_eq!(findings[0].event_kind, LawDiffEventKind::LawWeakened); - assert_eq!(findings[1].severity, LawFindingSeverity::Critical); - assert_eq!(findings[1].event_kind, LawDiffEventKind::LawWeakened); - assert_eq!(findings[2].severity, LawFindingSeverity::Error); - assert_eq!(findings[2].event_kind, LawDiffEventKind::FootprintExpanded); -} - -#[test] -fn semantic_finding_constructor_rejects_missing_event_identity() { - let event = NormalizedLawDiffEvent { - event_ref: " ".to_owned(), - event_index: 0, - api_version: "wesley.law-diff/v1".to_owned(), - old_schema_hash: BUNDLE_HASH.to_owned(), - new_schema_hash: BUNDLE_HASH.to_owned(), - old_law_hash: BUNDLE_HASH.to_owned(), - new_law_hash: BUNDLE_HASH.to_owned(), - kind: LawDiffEventKind::LawChanged, - law_id: Some("law.example".to_owned()), - subject: Some("scalar:Example".to_owned()), - law_kind: None, - review_posture: LawDiffReviewPosture::RequiresReview, - field_changes: Vec::new(), - added_reads: Vec::new(), - removed_reads: Vec::new(), - added_writes: Vec::new(), - removed_writes: Vec::new(), - added_creates: Vec::new(), - removed_creates: Vec::new(), - added_forbids: Vec::new(), - removed_forbids: Vec::new(), - }; - - let diagnostic = SemanticChangeFinding::from_normalized_event( - BUNDLE_HASH, - "artifacts/law-diff.json", - None, - Vec::new(), - &event, - ) - .expect_err("blank event ref should fail"); - - assert_eq!( - diagnostic.code, - HolmesDiagnosticCode::HlawFindingMissingEventIdentity - ); - assert_eq!(diagnostic.field_path.as_deref(), Some("eventRef")); -} diff --git a/crates/wesley-holmes/tests/suppression_abuse.rs b/crates/wesley-holmes/tests/suppression_abuse.rs deleted file mode 100644 index 5192b694..00000000 --- a/crates/wesley-holmes/tests/suppression_abuse.rs +++ /dev/null @@ -1,518 +0,0 @@ -use wesley_holmes::{ - apply_suppression_policy, normalize_law_assurance_policy, parse_law_assurance_policy, - HolmesDiagnosticCode, HolmesSeverity, JsonLawDiffIngestPort, LawAssuranceSuppressionTargetKind, - LawDiffIngestPort, LawEvidenceValidationResult, SuppressionRejectionReason, -}; - -const CI_SEMANTIC_DIFF: &str = - include_str!("../../../test/fixtures/weslaw/diff/ci-semantic-diff.json"); - -const BUNDLE_HASH: &str = "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"; - -const POLICY_WITH_ACTIVE_SUPPRESSION: &str = r#"{ - "apiVersion": "holmes.law-assurance-policy/v1", - "defaultProfile": "release", - "defaultSeverity": "advisory", - "profiles": { - "release": { - "suppressions": [ - { - "id": "known-scalar-window", - "target": { "kind": "law-id", "selector": "echo.scalar.positiveInt.u32-positive" }, - "reason": "temporary scalar migration window", - "owner": "release-team", - "createdOn": "2026-06-01", - "expiresOn": "2026-07-01", - "allowedSeverities": ["critical", "error", "warning", "advisory"], - "auditTags": ["migration"] - } - ] - } - } -}"#; - -const POLICY_WITH_EXPIRED_SUPPRESSION: &str = r#"{ - "apiVersion": "holmes.law-assurance-policy/v1", - "defaultProfile": "release", - "defaultSeverity": "advisory", - "profiles": { - "release": { - "suppressions": [ - { - "id": "expired-scalar-window", - "target": { "kind": "law-id", "selector": "echo.scalar.positiveInt.u32-positive" }, - "reason": "expired migration window", - "owner": "release-team", - "createdOn": "2026-05-01", - "expiresOn": "2026-05-31", - "allowedSeverities": ["critical", "error", "warning", "advisory"], - "auditTags": [] - } - ] - } - } -}"#; - -const POLICY_WITH_NON_OVERRIDABLE_GATE_SUPPRESSION: &str = r#"{ - "apiVersion": "holmes.law-assurance-policy/v1", - "defaultProfile": "release", - "defaultSeverity": "advisory", - "profiles": { - "release": { - "nonOverridableGates": ["bundle-traceability"], - "suppressions": [ - { - "id": "attempted-traceability-bypass", - "target": { "kind": "gate-id", "selector": "bundle-traceability" }, - "reason": "trying to bypass traceability check", - "owner": "bad-actor", - "createdOn": "2026-06-01", - "expiresOn": "2026-12-31", - "allowedSeverities": ["critical", "error", "warning", "advisory"], - "auditTags": [] - } - ] - } - } -}"#; - -fn semantic_findings() -> Vec { - let report = JsonLawDiffIngestPort - .ingest_law_diff(CI_SEMANTIC_DIFF.as_bytes()) - .report - .expect("fixture should parse"); - - wesley_holmes::semantic_change_findings_from_law_diff( - &report, - BUNDLE_HASH, - "evidence/law-diff.json", - Some("release".to_owned()), - ) - .expect("findings should construct") -} - -fn valid_evidence() -> LawEvidenceValidationResult { - LawEvidenceValidationResult::from_diagnostics(Vec::new()) -} - -fn invalid_evidence() -> LawEvidenceValidationResult { - LawEvidenceValidationResult::from_diagnostics(vec![wesley_holmes::HolmesDiagnostic::new( - HolmesDiagnosticCode::HlawEvidenceBundleInvalid, - HolmesSeverity::Error, - "required artifact reference is missing", - )]) -} - -#[test] -fn active_suppression_is_applied_to_matching_finding() { - let schema = parse_law_assurance_policy(POLICY_WITH_ACTIVE_SUPPRESSION.as_bytes()) - .expect("should parse"); - let policy = normalize_law_assurance_policy(&schema, None).expect("should normalize"); - let findings = semantic_findings(); - - let outcome = apply_suppression_policy(&findings, &valid_evidence(), &policy, "2026-06-05"); - - let suppressed = outcome - .annotated_findings - .iter() - .filter(|annotated| annotated.is_suppressed()) - .collect::>(); - assert!( - !suppressed.is_empty(), - "at least one finding should be suppressed" - ); - - let record = suppressed[0].suppressed_by.as_ref().unwrap(); - assert_eq!(record.suppression_id, "known-scalar-window"); - assert_eq!(record.owner, "release-team"); - assert_eq!(record.audit_tags, ["migration"]); - - assert_eq!(outcome.applied.len(), suppressed.len()); - assert_eq!(outcome.applied[0].created_on, "2026-06-01"); - assert_eq!( - outcome.applied[0].finding_id, suppressed[0].finding.finding_id, - "applied record must reference the suppressed finding" - ); - assert_eq!( - outcome.applied[0].target.kind, - LawAssuranceSuppressionTargetKind::LawId, - "applied record must carry the suppression target kind" - ); - assert_eq!( - outcome.applied[0].target.selector, "echo.scalar.positiveInt.u32-positive", - "applied record must carry the suppression target selector" - ); - assert!(outcome.rejected.is_empty()); - assert!(outcome.expired.is_empty()); - assert!(outcome.diagnostics.is_empty()); -} - -#[test] -fn invalid_evidence_rejects_all_suppressions_with_diagnostic() { - let schema = parse_law_assurance_policy(POLICY_WITH_ACTIVE_SUPPRESSION.as_bytes()) - .expect("should parse"); - let policy = normalize_law_assurance_policy(&schema, None).expect("should normalize"); - let findings = semantic_findings(); - - let outcome = apply_suppression_policy(&findings, &invalid_evidence(), &policy, "2026-06-05"); - - assert!( - outcome - .annotated_findings - .iter() - .all(|f| !f.is_suppressed()), - "no finding should be suppressed when evidence is invalid" - ); - assert_eq!(outcome.rejected.len(), 1); - assert_eq!(outcome.rejected[0].suppression_id, "known-scalar-window"); - assert_eq!( - outcome.rejected[0].rejection_reason, - SuppressionRejectionReason::InvalidEvidence - ); - assert!(outcome.applied.is_empty()); - - let diagnostic = &outcome.diagnostics[0]; - assert_eq!( - diagnostic.code, - HolmesDiagnosticCode::HlawSuppressionRejectedInvalidEvidence - ); - assert_eq!(diagnostic.severity, HolmesSeverity::Error); -} - -#[test] -fn non_overridable_gate_suppression_is_rejected_with_diagnostic() { - let schema = - parse_law_assurance_policy(POLICY_WITH_NON_OVERRIDABLE_GATE_SUPPRESSION.as_bytes()) - .expect("should parse"); - let policy = normalize_law_assurance_policy(&schema, None).expect("should normalize"); - let findings = semantic_findings(); - - let outcome = apply_suppression_policy(&findings, &valid_evidence(), &policy, "2026-06-05"); - - assert_eq!(outcome.rejected.len(), 1); - assert_eq!( - outcome.rejected[0].suppression_id, - "attempted-traceability-bypass" - ); - assert_eq!( - outcome.rejected[0].rejection_reason, - SuppressionRejectionReason::NonOverridableGate { - gate_id: "bundle-traceability".to_owned() - } - ); - assert!(outcome.applied.is_empty()); - assert!( - outcome - .annotated_findings - .iter() - .all(|f| !f.is_suppressed()), - "no finding should be suppressed" - ); - - let diagnostic = &outcome.diagnostics[0]; - assert_eq!( - diagnostic.code, - HolmesDiagnosticCode::HlawSuppressionRejectedNonOverridable - ); - assert_eq!(diagnostic.severity, HolmesSeverity::Error); -} - -#[test] -fn expired_suppression_emits_warning_diagnostic_and_is_not_applied() { - let schema = parse_law_assurance_policy(POLICY_WITH_EXPIRED_SUPPRESSION.as_bytes()) - .expect("should parse"); - let policy = normalize_law_assurance_policy(&schema, None).expect("should normalize"); - let findings = semantic_findings(); - - let outcome = apply_suppression_policy(&findings, &valid_evidence(), &policy, "2026-06-05"); - - assert!( - outcome - .annotated_findings - .iter() - .all(|f| !f.is_suppressed()), - "expired suppression must not mute any finding" - ); - assert_eq!(outcome.expired, ["expired-scalar-window"]); - assert!(outcome.applied.is_empty()); - assert!(outcome.rejected.is_empty()); - - let diagnostic = &outcome.diagnostics[0]; - assert_eq!( - diagnostic.code, - HolmesDiagnosticCode::HlawSuppressionExpired - ); - assert_eq!(diagnostic.severity, HolmesSeverity::Warning); -} - -#[test] -fn suppression_with_no_matching_finding_produces_no_applied_records() { - let policy_json = r#"{ - "apiVersion": "holmes.law-assurance-policy/v1", - "defaultProfile": "release", - "defaultSeverity": "advisory", - "profiles": { - "release": { - "suppressions": [ - { - "id": "unmatched-suppression", - "target": { "kind": "law-id", "selector": "nonexistent.law.id" }, - "reason": "targets a law id not present in the diff", - "owner": "test", - "createdOn": "2026-06-01", - "expiresOn": "2026-12-31", - "allowedSeverities": ["critical", "error", "warning", "advisory"], - "auditTags": [] - } - ] - } - } - }"#; - let schema = parse_law_assurance_policy(policy_json.as_bytes()).expect("should parse"); - let policy = normalize_law_assurance_policy(&schema, None).expect("should normalize"); - let findings = semantic_findings(); - - let outcome = apply_suppression_policy(&findings, &valid_evidence(), &policy, "2026-06-05"); - - assert!(outcome.applied.is_empty()); - assert!(outcome.rejected.is_empty()); - assert!(outcome.expired.is_empty()); - assert!(outcome.diagnostics.is_empty()); - assert!(outcome - .annotated_findings - .iter() - .all(|f| !f.is_suppressed())); -} - -#[test] -fn rejection_diagnostic_messages_use_display_format() { - // Rule 1: invalid-evidence rejection must not debug-quote the suppression id. - { - let schema = parse_law_assurance_policy(POLICY_WITH_ACTIVE_SUPPRESSION.as_bytes()) - .expect("should parse"); - let policy = normalize_law_assurance_policy(&schema, None).expect("should normalize"); - let msg = &apply_suppression_policy( - &semantic_findings(), - &invalid_evidence(), - &policy, - "2026-06-05", - ) - .diagnostics[0] - .message; - assert!( - msg.contains("known-scalar-window"), - "message should contain suppression id" - ); - assert!( - !msg.contains("\"known-scalar-window\""), - "id must not be debug-quoted; got: {msg:?}" - ); - } - // Rule 2: non-overridable gate rejection must not debug-quote id or gate id. - { - let schema = - parse_law_assurance_policy(POLICY_WITH_NON_OVERRIDABLE_GATE_SUPPRESSION.as_bytes()) - .expect("should parse"); - let policy = normalize_law_assurance_policy(&schema, None).expect("should normalize"); - let msg = &apply_suppression_policy( - &semantic_findings(), - &valid_evidence(), - &policy, - "2026-06-05", - ) - .diagnostics[0] - .message; - assert!( - !msg.contains("\"attempted-traceability-bypass\""), - "suppression id must not be debug-quoted; got: {msg:?}" - ); - assert!( - !msg.contains("\"bundle-traceability\""), - "gate id must not be debug-quoted; got: {msg:?}" - ); - } - // Rule 3: expiry message must not debug-quote the suppression id. - { - let schema = parse_law_assurance_policy(POLICY_WITH_EXPIRED_SUPPRESSION.as_bytes()) - .expect("should parse"); - let policy = normalize_law_assurance_policy(&schema, None).expect("should normalize"); - let msg = &apply_suppression_policy( - &semantic_findings(), - &valid_evidence(), - &policy, - "2026-06-05", - ) - .diagnostics[0] - .message; - assert!( - !msg.contains("\"expired-scalar-window\""), - "suppression id must not be debug-quoted; got: {msg:?}" - ); - } -} - -#[test] -fn invalid_evaluation_date_returns_diagnostic_and_leaves_findings_unsuppressed() { - let schema = parse_law_assurance_policy(POLICY_WITH_ACTIVE_SUPPRESSION.as_bytes()) - .expect("should parse"); - let policy = normalize_law_assurance_policy(&schema, None).expect("should normalize"); - let findings = semantic_findings(); - - let outcome = apply_suppression_policy(&findings, &valid_evidence(), &policy, "06/05/2026"); - - assert!( - outcome - .annotated_findings - .iter() - .all(|f| !f.is_suppressed()), - "invalid evaluation date must not suppress any findings" - ); - assert!(outcome.applied.is_empty()); - assert_eq!(outcome.diagnostics.len(), 1); - assert_eq!( - outcome.diagnostics[0].code, - HolmesDiagnosticCode::HlawSuppressionInvalid - ); - assert_eq!(outcome.diagnostics[0].severity, HolmesSeverity::Error); -} - -#[test] -fn suppression_on_exact_expiry_date_is_still_active() { - // expires_on == evaluation_date uses strict `<`, so the suppression is still valid on - // its expiry date (last valid day inclusive). - const POLICY_EXPIRES_TODAY: &str = r#"{ - "apiVersion": "holmes.law-assurance-policy/v1", - "defaultProfile": "release", - "defaultSeverity": "advisory", - "profiles": { - "release": { - "suppressions": [ - { - "id": "expires-today", - "target": { "kind": "law-id", "selector": "echo.scalar.positiveInt.u32-positive" }, - "reason": "valid through the expiry date itself", - "owner": "release-team", - "createdOn": "2026-06-01", - "expiresOn": "2026-06-05", - "allowedSeverities": ["critical", "error", "warning", "advisory"], - "auditTags": [] - } - ] - } - } - }"#; - let schema = parse_law_assurance_policy(POLICY_EXPIRES_TODAY.as_bytes()).expect("should parse"); - let policy = normalize_law_assurance_policy(&schema, None).expect("should normalize"); - let findings = semantic_findings(); - - let outcome = apply_suppression_policy(&findings, &valid_evidence(), &policy, "2026-06-05"); - - assert!( - outcome.expired.is_empty(), - "suppression expiring today must not be marked expired" - ); - assert!( - outcome.annotated_findings.iter().any(|f| f.is_suppressed()), - "suppression expiring today must still be applied" - ); -} - -#[test] -fn one_suppression_matches_multiple_findings() { - // Two findings share the same law-id; one suppression should silence both. - // Two events with different kinds but the same lawId — the ingest deduplicates on - // (kind, lawId), so this is valid. A law-id suppression must match both findings. - const MULTI_EVENT_DIFF: &str = r#"{ - "apiVersion": "wesley.law-diff/v1", - "oldSchemaHash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "newSchemaHash": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - "oldLawHash": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", - "newLawHash": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", - "changes": [ - { - "kind": "LAW_WEAKENED", - "lawId": "shared.scalar.law", - "subject": "scalar:SharedType", - "lawKind": "scalarSemantics", - "reviewPosture": "requires-review", - "fieldChanges": [{ "path": "body.minInclusive", "old": 1, "new": 0 }] - }, - { - "kind": "LAW_STRENGTHENED", - "lawId": "shared.scalar.law", - "subject": "scalar:SharedType", - "lawKind": "scalarSemantics", - "reviewPosture": "requires-review", - "fieldChanges": [{ "path": "body.maxInclusive", "old": 100, "new": 50 }] - } - ] - }"#; - const POLICY_TARGETS_SHARED_LAW: &str = r#"{ - "apiVersion": "holmes.law-assurance-policy/v1", - "defaultProfile": "release", - "defaultSeverity": "advisory", - "profiles": { - "release": { - "suppressions": [ - { - "id": "multi-match", - "target": { "kind": "law-id", "selector": "shared.scalar.law" }, - "reason": "covers all usages of shared law", - "owner": "release-team", - "createdOn": "2026-06-01", - "expiresOn": "2026-12-31", - "allowedSeverities": ["critical", "error", "warning", "advisory"], - "auditTags": [] - } - ] - } - } - }"#; - - use wesley_holmes::{ - semantic_change_findings_from_law_diff, JsonLawDiffIngestPort, LawDiffIngestPort, - }; - let report = JsonLawDiffIngestPort - .ingest_law_diff(MULTI_EVENT_DIFF.as_bytes()) - .report - .expect("multi-event fixture should parse"); - let findings = semantic_change_findings_from_law_diff( - &report, - BUNDLE_HASH, - "evidence/law-diff.json", - Some("release".to_owned()), - ) - .expect("findings should construct"); - assert_eq!( - findings.len(), - 2, - "fixture must produce exactly two findings" - ); - - let schema = - parse_law_assurance_policy(POLICY_TARGETS_SHARED_LAW.as_bytes()).expect("should parse"); - let policy = normalize_law_assurance_policy(&schema, None).expect("should normalize"); - - let outcome = apply_suppression_policy(&findings, &valid_evidence(), &policy, "2026-06-05"); - - assert_eq!( - outcome - .annotated_findings - .iter() - .filter(|f| f.is_suppressed()) - .count(), - 2, - "both findings sharing the law-id must be suppressed" - ); - assert_eq!(outcome.applied.len(), 2); - assert!( - outcome - .applied - .iter() - .all(|r| r.suppression_id == "multi-match"), - "both applied records must reference the same suppression" - ); - assert!(outcome.rejected.is_empty()); - assert!(outcome.expired.is_empty()); - assert!(outcome.diagnostics.is_empty()); -} diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 8ad522f5..e6c0e98e 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -40,14 +40,9 @@ The repo is now split into three practical layers: structure, lists schema root operations, and exposes generic operation facts. 2. **Native CLI / body**: `crates/wesley-cli` is the Rust product command. It exposes schema lowering, schema hashing, schema operation listing, schema - diffing, Rust/TypeScript emission, LE-binary codec emission, `weslaw` - validation/diff/coverage commands, operation selection analysis, and - directive argument extraction from Rust crates. -3. **Rust Holmes assurance foundation**: `crates/wesley-holmes` is the new - law-assurance foundation for Holmes evidence, versioning, ports, and future - reports. It consumes Wesley compiler artifacts and does not expose product - CLI commands yet. -4. **Non-compiler JavaScript surfaces**: `packages/` now contains the retained + diffing, Rust/TypeScript emission, LE-binary codec emission, operation + selection analysis, and directive argument extraction from Rust crates. +3. **Non-compiler JavaScript surfaces**: `packages/` now contains the retained Holmes assurance package only. Website/docs tooling and repository scripts remain JavaScript support surfaces; browser/Bun/Deno host experiments are retired from the Wesley release surface. @@ -70,7 +65,6 @@ flowchart LR CodecPlan[wesley-emit-codec] RustEmitter[wesley-emit-rust] TsEmitter[wesley-emit-typescript] - RustHolmes[wesley-holmes] NativeCli[wesley-cli] Xtask[xtask] end @@ -131,9 +125,8 @@ semantics. | `crates/wesley-emit-codec/` | Shared LE-binary codec planning crate consumed by Rust and TypeScript codec emitters. | | `crates/wesley-emit-rust/` | Rust projection crate. Builds a Rust item/type AST from L1 IR and `SchemaOperation` data, then prints deterministic model and operation declarations. | | `crates/wesley-emit-typescript/` | Rust TypeScript projection crate. Builds a TypeScript declaration AST from L1 IR and `SchemaOperation` data, then prints deterministic model and operation declarations. | -| `crates/wesley-holmes/` | Rust Holmes law-assurance foundation. Defines pure domain models, deterministic ports/fakes, evidence bundle validation, artifact path resolution, and version diagnostics without exposing public CLI commands yet. | | `xtask/` | Rust repository automation: docs checks, tests, native preflight, release check, and package hygiene bridge. | -| `packages/wesley-holmes/` | Existing JavaScript Holmes surface outside compiler authority while the Rust assurance foundation grows behind it. | +| `packages/wesley-holmes/` | Existing JavaScript Holmes evidence and reporting surface outside compiler authority. | | `schemas/` | JSON schemas and generic directive/schema assets used by tooling and tests. | | `test/fixtures/` | GraphQL fixtures, Rust L1 goldens, package examples, and reference schemas. | | `scripts/` | Preflight, docs truth, docs link, fixture generation, smoke, and CI helper scripts. | @@ -606,8 +599,6 @@ Wesley currently does these things in this repo: - Extracts operation directive arguments by directive name. - Provides a native Rust workspace preflight and release check. - Emits Rust and TypeScript models, operation bindings, and LE-binary codecs. -- Validates, lints, diffs, explains, rebinds, and reports coverage for - `weslaw/v1` documents. - Maintains non-compiler JavaScript tooling only where it has an explicit owner. - Maintains docs, schemas, fixtures, CI scripts, and design packets around the broader compiler-and-assurance system. diff --git a/docs/BEARING.md b/docs/BEARING.md index fdbf8ff8..79b62c91 100644 --- a/docs/BEARING.md +++ b/docs/BEARING.md @@ -74,23 +74,21 @@ release surface. - Keep Wesley core CI independent of external product and database repos by exercising hermetic fixture modules. -### 5. Holmes And `weslaw` Assurance +### 5. Evidence And Assurance Boundaries -`weslaw` is Wesley's semantic law layer for contract bundles. GraphQL SDL remains -sovereign over structural shape. `weslaw` becomes sovereign over semantic law. -The combined, bound, canonical contract bundle is the unit Wesley hashes, diffs, -emits, explains, validates, and hands to assurance tools. +GraphQL SDL is Wesley's structural source. Executable semantics belong to Edict, +and target behavior belongs to the owning target or runtime. Wesley may emit +content-addressed compiler evidence about exact sources, settings, projections, +and outputs, but it must not invent a second semantic language. -Holmes should mature as an assurance layer over Wesley-published evidence. It -may validate, judge, report, publish, and audit that evidence. It must not -reinterpret GraphQL shape, mutate law, rebind law, invent semantic diffs, or -become the source of truth for contract bundles. +Holmes may validate, judge, report, publish, and audit independently owned +evidence. It must not reinterpret GraphQL shape, become an application-language +compiler, or become the source of truth for target semantics. Durable design evidence: - [0018 Holmes Assurance Hexagon](./design/0018-holmes-assurance-hexagon/holmes-assurance-hexagon.md) -- [0019 `weslaw` Semantic Law IR](./design/0019-weslaw-semantic-law-ir/weslaw-semantic-law-ir.md) -- [0020 Holmes `weslaw` Assurance PRD And Test Plan](./design/0020-holmes-weslaw-assurance-prd-test-plan/holmes-weslaw-assurance-prd-test-plan.md) +- [0023 Remove Weslaw](./design/0023-remove-weslaw/SOURCE_remove-weslaw.md) ### 6. Sibling Repo Boundaries @@ -99,8 +97,8 @@ Durable design evidence: - Edict owns Edict language/Core IR/canonicalization/target-profile ABI. - Echo owns Echo target semantics. - Continuum owns participant protocol and admission. -- Wesley owns GraphQL and `weslaw` source-profile adapters plus compiler - evidence integration. +- Wesley owns GraphQL structural compilation plus generic compiler evidence + integration. Wesley should coordinate through generic module seams, contract artifacts, hashes, and evidence, not by absorbing sibling runtime semantics. @@ -139,9 +137,8 @@ issues. The release policy and checklist remain the operational source: - **External Module Gap**: The domain-empty boundary is named; the module seam still needs hermetic target-dispatch fixtures, runtime boundary evidence, and artifact evidence before external modules can consume it cleanly. -- **Law Versus Runtime Meaning**: `weslaw` lets Wesley preserve and reason about - semantic law. Target meaning and runtime behavior still belong to owning - modules and sibling repos. +- **Semantic Ownership**: Edict owns executable language semantics. Target + meaning and runtime behavior belong to owning modules and sibling repos. ## Durable Closeouts @@ -159,7 +156,8 @@ and GitHub milestones for exact ordering. Product gravity remains: 1. keep the Rust-native compiler and project-manifest platform boring and reproducible, -2. harden the `v0.3.0` evidence truth around Holmes and `weslaw`, +2. harden the `v0.3.0` evidence truth without coupling compiler evidence to + application semantics, 3. preserve the domain-empty module boundary while external targets consume Wesley artifacts, and 4. cut future releases only from tagged `main` once release-gate issues are diff --git a/docs/END_TO_END.md b/docs/END_TO_END.md index 7229566b..df07a71f 100644 --- a/docs/END_TO_END.md +++ b/docs/END_TO_END.md @@ -2,1202 +2,238 @@ -This document explains Wesley from first principles. +Wesley is a domain-empty GraphQL compiler kernel. It turns authored GraphQL +structure into deterministic intermediate representation, operation facts, +generated declarations, and exact generation evidence. -Assume you have never heard of Wesley. The shortest honest description is: +Wesley is not an application language, runtime, database migration engine, or +policy authority. Edict owns executable application semantics. External targets +and sibling runtimes own the behavior of the artifacts they consume. -> Wesley is a schema-first compiler kernel and assurance toolchain. - -That sentence has a lot packed into it. - -- **Schema-first** means the authored GraphQL Schema Definition Language file is - the source of truth. -- **Compiler kernel** means Wesley parses that source, lowers it into semantic - facts, computes hashes and deltas, and emits target artifacts. -- **Assurance toolchain** means Wesley also carries tools for packaging, - witnessing, checking, and judging whether derived artifacts still trace back - to the authored source. - -Wesley is not an application runtime. It is not a database migration engine. It -is not Echo, Continuum, jedit, WARPspace, or PostgreSQL. Wesley owns compiler -truth. External modules and sibling projects own target meaning. - -```mermaid -flowchart TD - Problem[Problem: many artifacts drift] --> Decision[Decision: one authored GraphQL contract] - Decision --> Compiler[Wesley compiler kernel] - Compiler --> Facts[Deterministic compiler facts] - Compiler --> Artifacts[Generated artifacts] - Facts --> Evidence[Evidence and witnesses] - Artifacts --> Evidence - Evidence --> Judgment[Bounded judgment] - Artifacts --> External[External modules and projects] - - External --> Runtime[Runtime, database, product, or deployment behavior] - - classDef source fill:#eff6ff,stroke:#1d4ed8,stroke-width:2px - classDef wesley fill:#f8fafc,stroke:#334155,stroke-width:2px - classDef outside fill:#fef3c7,stroke:#d97706,stroke-width:2px - - class Decision source - class Compiler,Facts,Artifacts,Evidence,Judgment wesley - class External,Runtime outside -``` - -## The Problem Wesley Solves - -Most systems have several copies of the same truth. - -A database has tables. A backend has models. A frontend has TypeScript types. A -runtime has handlers. Tests have fixtures. Documentation has examples. A -deployment pipeline has policy. Over time those copies drift. One field is -renamed in the API but not in the test. One migration changes a shape without -updating generated bindings. One runtime learns a domain rule that nobody wrote -down in the contract. - -Wesley attacks that drift by insisting on an authored contract and a repeatable -compile path: - -```text -authored GraphQL SDL -> compiler facts -> generated artifacts -> bounded evidence -``` - -The design goal is not "more code generation." Code generation is a tactic. The -goal is trustworthy change: when a contract changes, every derived artifact and -every proof surface should be regenerated, checked, and explained from the same -source. +## The Boundary ```mermaid -flowchart TD - Drift[Traditional drift pattern] --> ModelA[Backend model] - Drift --> ModelB[Frontend type] - Drift --> ModelC[Database shape] - Drift --> ModelD[Test fixture] - Drift --> ModelE[Runtime policy] - - ModelA -. manual sync .- ModelB - ModelB -. manual sync .- ModelC - ModelC -. manual sync .- ModelD - ModelD -. manual sync .- ModelE - - Source[Wesley pattern: authored SDL] --> Lower[Lower once] - Lower --> IR[L1 IR] - IR --> Rust[Rust artifacts] - IR --> TypeScript[TypeScript artifacts] - IR --> Hash[Hashes and deltas] - IR --> Evidence[Witness inputs] - IR --> ModuleTargets[Module-owned targets] - - classDef weak fill:#fee2e2,stroke:#b91c1c,stroke-width:2px - classDef strong fill:#dcfce7,stroke:#15803d,stroke-width:2px - class Drift,ModelA,ModelB,ModelC,ModelD,ModelE weak - class Source,Lower,IR,Rust,TypeScript,Hash,Evidence,ModuleTargets strong +flowchart LR + SDL["GraphQL SDL"] --> Core["Wesley structural compiler"] + Core --> IR["Canonical L1 IR"] + Core --> Ops["Root operation facts"] + IR --> Emit["Rust and TypeScript emitters"] + Ops --> Emit + IR --> Extension["External target generator"] + Ops --> Extension + Extension --> Evidence["Exact provenance and review"] + Emit --> Consumer["Downstream consumer"] + Evidence --> Consumer ``` -## Why GraphQL SDL - -Wesley starts with GraphQL SDL because SDL is a compact, inspectable contract -language. It describes shape without forcing a storage engine, transport, or -runtime architecture. - -GraphQL gives Wesley several useful compiler properties: - -- Types, fields, arguments, interfaces, unions, enums, input objects, nullability, - and lists are all explicit. -- Root operations name the public operation boundary. -- Directives attach law-shaped metadata at known schema coordinates. -- The language is additive enough to support long-lived contract evolution. -- The same source can feed many targets without making one target the authority. +The arrows have deliberately different authority: -This distinction matters: +- GraphQL SDL is authoritative for the structure Wesley compiles. +- L1 IR and operation catalogs are derived compiler facts. +- generated Rust and TypeScript are projections, not peer authorities. +- target generators own target-specific declarations and semantics. +- provenance binds exact inputs and outputs; it does not prove runtime behavior. -| Layer | Question | Wesley posture | -| ------------------ | ------------------------------------------------------ | ----------------------------------------------- | -| Shape | What exists? | Wesley core can lower, hash, diff, and emit it. | -| Law-shaped data | What is declared as required, permitted, or forbidden? | Wesley core can preserve it. | -| Law interpretation | Is the declaration honest or admissible? | A domain module or runtime must decide. | +## Stage 1: Author GraphQL Structure -For example, a GraphQL field can carry a footprint directive: +An authored SDL file declares types, fields, arguments, root operations, +nullability, lists, and directives: ```graphql -type Query { - textWindow(input: TextWindowInput!): TextWindowReading! - @wes_op(name: "textWindow") - @wes_footprint( - reads: ["Buffer", "Tick", "Receipt"] - writes: [] - forbids: ["AstState", "Diagnostics", "GitWitness", "UiState"] - ) +type Greeting { + id: ID! + message: String! } -``` - -Generic Wesley can preserve the operation name, argument type, result type, -directive name, and directive arguments. It should not decide whether that -footprint is honest for Echo, correct for jedit, or meaningful for a database. -Those are domain decisions. - -```mermaid -flowchart TD - SDL[GraphQL SDL] --> Shape[Shape facts] - SDL --> DirectiveData[Directive data] - Shape --> Wesley[Wesley core] - DirectiveData --> Wesley - - Wesley --> GenericFacts[Generic compiler facts] - GenericFacts --> Emitters[Rust and TypeScript emitters] - GenericFacts --> Hashing[Canonical bytes and hashes] - GenericFacts --> Diffs[Schema deltas] - - DirectiveData -. preserved, not interpreted .-> Module[External module] - Module --> DomainLaw[Domain law interpretation] - DomainLaw --> RuntimeDecision[Runtime, database, or product decision] -``` - -The "why" is simple: if generic Wesley interprets Echo law, PostgreSQL law, or -Continuum law, it stops being a reusable compiler and becomes a hidden product -runtime. The current architecture keeps Wesley narrow so it can remain useful -to many target worlds. - -## The End-To-End Shape - -At the highest level, a Wesley cycle looks like this: - -```mermaid -flowchart TD - Author[Human or agent author] --> SDL[Authored GraphQL SDL] - Author --> Operations[Optional GraphQL operations] - Author --> Config[Compile config and module selection] - - SDL --> Parse[Parse] - Parse --> Lower[Lower to L1 IR] - Lower --> Canonical[Canonical JSON] - Canonical --> Hash[Registry or content hash] - - Lower --> OperationFacts[Operation catalog and selections] - Operations --> OperationFacts - - Lower --> EmitRust[Emit Rust] - Lower --> EmitTS[Emit TypeScript] - Lower --> ModuleCompile[Module-owned compile targets] - - EmitRust --> Artifacts[Generated artifacts] - EmitTS --> Artifacts - ModuleCompile --> Artifacts - - SDL --> Evidence[Evidence and witness surfaces] - Lower --> Evidence - Artifacts --> Evidence - Hash --> Evidence - Evidence --> Judgment[Holmes, Watson, Moriarty, BLADE] - Artifacts --> Consumers[Project or external consumers] - Judgment --> Operator[Operator or CI decision] -``` - -That picture used to hide an important migration detail: Wesley once had two -implementation surfaces. The retired surface is now gone from compiler -authority. - -1. The **Rust workspace** is the current compiler center. New compiler truth - belongs in `crates/wesley-core` and the native `wesley` CLI. -2. The **JavaScript workspace** is explicitly non-compiler: Holmes assurance, - docs tooling, and repository automation. The old product website/playground - surface and the browser/Bun/Deno host experiments are retired from the - Wesley release surface. - -```mermaid -flowchart LR - subgraph CurrentCenter["Current center of gravity"] - Core[crates/wesley-core] - Cli[crates/wesley-cli] - EmitRust[crates/wesley-emit-rust] - EmitTS[crates/wesley-emit-typescript] - Xtask[xtask] - end - - subgraph NonCompilerJS["Non-compiler JavaScript surfaces"] - Holmes[packages/wesley-holmes] - Tooling[scripts] - end - - Core --> Cli - Core --> EmitRust - Core --> EmitTS - Xtask --> Core - Xtask --> Cli - Holmes --> Tooling - - NonCompilerJS -. evidence and docs only .-> CurrentCenter -``` - -The current post-retirement work is about keeping that boundary honest: expand Rust IR -fixtures, compare selected JS and Rust lowering projections, and preserve module -boundaries before retiring legacy behavior. - -## Stage 1: Author The Contract - -The starting point is an authored GraphQL schema. - -The schema is not treated as an API afterthought. It is the sovereign source -for contract shape. A project, module, or product may own the schema, but -Wesley treats the file as the thing to compile. - -```mermaid -erDiagram - SCHEMA ||--o{ TYPE_DEFINITION : declares - SCHEMA ||--o{ ROOT_OPERATION : exposes - TYPE_DEFINITION ||--o{ FIELD : has - FIELD ||--|| TYPE_REFERENCE : returns - ROOT_OPERATION ||--o{ ARGUMENT : accepts - ROOT_OPERATION ||--|| TYPE_REFERENCE : returns - TYPE_DEFINITION ||--o{ DIRECTIVE_VALUE : annotates - FIELD ||--o{ DIRECTIVE_VALUE : annotates - ROOT_OPERATION ||--o{ DIRECTIVE_VALUE : annotates - ARGUMENT ||--o{ DIRECTIVE_VALUE : annotates - - SCHEMA { - string path - string source_hash - } - TYPE_DEFINITION { - string name - string kind - } - FIELD { - string name - } - ROOT_OPERATION { - string operation_type - string field_name - } - ARGUMENT { - string name - } - TYPE_REFERENCE { - string base - bool nullable - list wrappers - } - DIRECTIVE_VALUE { - string name - json arguments - } -``` - -The main design choice is that authored SDL stays above every emitted artifact. -Generated Rust, TypeScript, SQL, codecs, manifests, reports, and tests are not -peer authorities. If they drift, regenerate them or fail the check. - -## Stage 2: Parse And Lower To L1 IR - -The Rust compiler kernel lives in `crates/wesley-core`. - -Its central job is to lower GraphQL SDL into Wesley's L1 IR. L1 IR is -domain-empty. It captures GraphQL facts: definitions, fields, directives, -interfaces, unions, enum values, input objects, nullability, list wrappers, root -operations, and hashes. It does not capture database migrations, Echo -scheduling, Continuum product policy, or deployment rules. - -```mermaid -sequenceDiagram - actor User - participant CLI as wesley CLI - participant Core as wesley-core - participant Parser as Apollo parser adapter - participant IR as WesleyIR - participant Hash as Canonical JSON and hash - - User->>CLI: wesley schema lower --schema schema.graphql --json - CLI->>Core: lower_schema_sdl(sdl) - Core->>Parser: parse GraphQL SDL - Parser-->>Core: parsed document or parser diagnostic - Core->>Core: fold extensions and normalize facts - Core->>IR: build L1 semantic graph - Core->>Hash: canonicalize IR when requested - Hash-->>CLI: stable bytes and digest - IR-->>CLI: JSON compiler facts - CLI-->>User: L1 IR or structured error -``` - -The lowering path is deliberately deterministic. Determinism is what makes -hashes, golden fixtures, parity checks, and release evidence meaningful. - -```mermaid -classDiagram - class WesleyIR { - +String version - +Option metadata - +Vec~TypeDefinition~ types - } - - class TypeDefinition { - +String name - +TypeKind kind - +Option~String~ description - +Map directives - +Vec~String~ implements - +Vec~Field~ fields - +Vec~String~ enum_values - +Vec~String~ union_members - } - - class Field { - +String name - +Option~String~ description - +TypeReference type - +Map directives - } - - class TypeReference { - +String base - +bool nullable - +bool is_list - +Option~bool~ list_item_nullable - +nested list wrapper facts - } - - class SchemaOperation { - +OperationType operation_type - +String root_type_name - +String field_name - +Vec~OperationArgument~ arguments - +TypeReference result_type - +Map directives - } - - class OperationArgument { - +String name - +TypeReference type - +Option default_value - +Map directives - } - - class SchemaDelta { - +Vec added_types - +Vec removed_types - +Vec modified_types - } - - WesleyIR "1" --> "*" TypeDefinition - TypeDefinition "1" --> "*" Field - Field "1" --> "1" TypeReference - SchemaOperation "1" --> "*" OperationArgument - SchemaOperation "1" --> "1" TypeReference - OperationArgument "1" --> "1" TypeReference - SchemaDelta --> TypeDefinition +type Query { + greeting(id: ID!): Greeting +} ``` -The "why" behind L1 is portability. Once a schema is lowered into a stable, -domain-empty graph, many target worlds can consume it without each target -re-parsing GraphQL and inventing its own meaning for basic shape. - -## Stage 3: Compute Hashes, Deltas, And Operation Facts +GraphQL is a useful structural source because it is compact, inspectable, and +portable. It is not expected to encode application laws that the language +cannot express honestly. -After lowering, Wesley can compute facts that help humans and tools reason -about change. +## Stage 2: Lower And Identify -The native CLI exposes commands such as: +The native CLI parses and lowers SDL through `wesley-core`: -```bash -wesley schema lower --schema --json -wesley schema hash --schema -wesley schema operations --schema --json -wesley schema diff --old --new --format summary --exit-code -wesley init-law --schema --family --out -wesley law lint --law --json -wesley law validate --schema --law --json -wesley law diff --old --new --json -wesley law diff --old --new --format markdown -wesley law explain --law scalar:PositiveInt -wesley law explain --law operation:Mutation.replaceRangeAsTick -wesley law rebind --schema --law --accept --out -wesley law capabilities --law --json -wesley law coverage --schema --law --profile release --json -wesley operation selections --operation --schema --json -wesley operation directive-args --operation --directive --json +```text +wesley schema lower --schema schema.graphql --json +wesley schema hash --schema schema.graphql +wesley schema operations --schema schema.graphql --json ``` -These commands are boring on purpose. They answer compiler questions: - -- What does this schema lower to? -- What is its stable identity? -- Which root operations exist? -- What changed between two schema versions? -- Which semantic law entries bind to this schema, and which `lawHash`, - `lawDocumentHash`, `profileHash`, and `bundleHash` identify the bound - contract bundle? -- Which known formal directives can be scaffolded into active `weslaw/v1`, and - which comments are only draft suggestions that require human promotion? -- Which semantic law changed between two law versions, and is the change a - strengthening, weakening, footprint expansion/contraction, channel version - change, predicate change, schema-hash rebound, or binding break? -- Which laws govern a particular scalar, operation, or other subject? -- Does a law file need an explicit schema-hash rebind before it can validate? -- Which footprint laws can be rendered as report-only capability summaries? -- Which profile/category coverage gaps remain before release? -- Which response paths or schema-coordinate selections does an operation use? -- Which directive arguments are present on an operation? - -```mermaid -flowchart TD - IR[L1 IR] --> Canonical[Canonical JSON] - Canonical --> Hash[Schema hash] - IR --> Delta[Schema diff] - IR --> RootOps[Root operation catalog] - IR --> InitLaw[init-law directive lowering] - IR --> LawBind[Strict law binding] - InitLaw --> LawDrafts[Active law plus draft suggestions] - LawDoc[weslaw/v1 document] --> LawBind - LawDoc --> LawLint[Structure-only law lint] - LawDoc --> LawExplain[Law explain] - LawDoc --> LawRebind[Explicit schema-hash rebind] - LawDoc --> CapabilityReport[Report-only capability summary] - LawDoc --> CoverageReport[Profile/category coverage] - LawBind --> LawManifest[Contract bundle manifest] - LawDoc --> LawDiff[Semantic law diff] - LawDiff --> LawDiffJson[JSON diff report] - LawDiff --> LawDiffMarkdown[Markdown review summary] +Lowering produces canonical L1 IR. Registry hashes are computed from canonical +semantic structure rather than source paths, timestamps, or formatting. - Operation[GraphQL operation document] --> OpParse[Operation parser] - OpParse --> ResponsePaths[Response-path selections] - IR --> SchemaIndex[Schema index] - SchemaIndex --> SchemaCoords[Schema-coordinate selections] - OpParse --> SchemaCoords - OpParse --> DirectiveArgs[Directive argument extraction] +Structural schema changes can be compared directly or against a Git revision: - Hash --> CI[CI and fixture evidence] - LawLint --> CI - LawManifest --> CI - LawExplain --> Operator[Operator inspection] - LawRebind --> Operator - CapabilityReport --> Operator - CoverageReport --> CI - CoverageReport --> Operator - LawDiffJson --> CI - LawDiffJson --> Assurance[Holmes/BLADE] - LawDiffMarkdown --> Review - Delta --> Review[Human review] - RootOps --> Emitters[Operation binding emitters] - ResponsePaths --> Witness[Witness inputs] - SchemaCoords --> Witness - DirectiveArgs --> Module[Module or runtime interpretation] +```text +wesley schema diff --schema schema.graphql --against origin/main --format json ``` -The design choice here is to keep facts separately inspectable. A schema hash, -a diff, an operation catalog, and a generated file should not be a single -opaque blob. They should each be visible so a reviewer can understand where a -claim came from. +## Stage 3: Emit Generic Projections -## Stage 4: Emit Generic Artifacts +Wesley can emit deterministic Rust and TypeScript declarations: -Wesley can emit artifacts from compiler facts. - -The current Rust-native projection crates are: - -- `crates/wesley-emit-rust` -- `crates/wesley-emit-typescript` - -They take `WesleyIR` plus operation data and build language-specific ASTs before -printing deterministic files. - -```mermaid -sequenceDiagram - actor User - participant CLI as wesley CLI - participant Core as wesley-core - participant Law as weslaw Law IR - participant Ops as schema operations - participant RustEmitter as wesley-emit-rust - participant TSEmitter as wesley-emit-typescript - participant Out as filesystem - participant Metadata as metadata sidecar - - User->>CLI: wesley emit rust --schema schema.graphql --law law.weslaw.yaml --out model.rs --metadata-out model.metadata.json - CLI->>Core: lower_schema_sdl(sdl) - CLI->>Core: list_schema_operations_sdl(sdl) - Core-->>CLI: WesleyIR and SchemaOperation facts - CLI->>Law: load, strictly bind, and hash law - Law-->>CLI: contract bundle manifest - CLI->>RustEmitter: build Rust file AST - RustEmitter-->>CLI: deterministic Rust source - CLI->>Out: write model.rs with schema/law hash constants - CLI->>Metadata: write schema, law, profile, bundle, generator, and mode metadata +```text +wesley emit rust \ + --schema schema.graphql \ + --out generated/model.rs \ + --metadata-out generated/model.metadata.json - User->>CLI: wesley emit typescript --schema schema.graphql --law law.weslaw.yaml --out types.ts --metadata-out types.metadata.json - CLI->>Core: lower_schema_sdl(sdl) - CLI->>Ops: root operation facts - CLI->>Law: load, strictly bind, and hash law - CLI->>TSEmitter: build TypeScript declaration AST - TSEmitter-->>CLI: deterministic TypeScript source - CLI->>Out: write types.ts - CLI->>Metadata: write schema, law, profile, bundle, generator, and mode metadata +wesley emit typescript \ + --schema schema.graphql \ + --out generated/model.ts \ + --metadata-out generated/model.metadata.json ``` -The important design choice is that pure generation does not need to inspect or -mutate existing source files. Wesley owns an emitter AST, then prints. Tools -such as tree-sitter, SWC, or oxc become relevant only when Wesley needs to -understand existing code and edit it safely. - -When `--law ` is supplied, emitters first build the same validated -contract bundle manifest that `wesley law validate --json` reports. Rust output -embeds `WESLEY_SCHEMA_HASH` and `WESLAW_HASH` constants so a generated artifact -can identify the exact shape and semantic law that produced it. Metadata -sidecars also include `schemaHashQualified`, `lawHash`, `lawDocumentHash`, -`profileHash`, `bundleHash`, and the Law IR codec. The legacy bare -`schemaHash` remains in metadata for compatibility. TypeScript currently -records the law facts in metadata only; executable or declaration-level -TypeScript hash constants can be added when that artifact path needs them. - -Rust is also the first retained emitter to consume active scalar and variant -law for helper generation. Integer scalar semantics produce standalone -`validate_` helpers, and discriminated input variant law produces -`validate__variant` helpers. These helpers are generated evidence and -developer affordances; they do not mutate GraphQL shape, replace strict law -binding, or claim runtime enforcement for footprints. - -```mermaid -classDiagram - class WesleyIR - class SchemaOperation - - class RustFile { - +Vec~RustItem~ items - } - class RustItem { - <> - Struct - Enum - TypeAlias - Operation - } - class RustType { - <> - String - I32 - F64 - Bool - Vec - Option - Reference - } +The emitters consume structural IR and normalized operation facts. Metadata +sidecars record the schema identity, generator identity, version, and emission +mode. They do not assert that a runtime enforces application behavior. - class TsProgram { - +Vec~TsDeclaration~ declarations - } - class TsDeclaration { - <> - Interface - TypeAlias - Operation - MetadataConstant - } - class TsTypeExpr { - <> - String - Number - Boolean - Null - Reference - Object - Array - Union - } +Little-endian binary codec projections are available for supported operation +variable shapes: - WesleyIR --> RustFile - SchemaOperation --> RustFile - RustFile "1" --> "*" RustItem - RustItem --> RustType - - WesleyIR --> TsProgram - SchemaOperation --> TsProgram - TsProgram "1" --> "*" TsDeclaration - TsDeclaration --> TsTypeExpr +```text +wesley emit le-binary-rust --schema schema.graphql --out generated/codec.rs +wesley emit le-binary-typescript --schema schema.graphql --out generated/codec.ts ``` -Generic emitters stay domain-empty. They can emit types and operation bindings. -They should not decide what an Echo footprint means, how a PostgreSQL migration -should be locked, or how Continuum should publish a family. - -## Stage 5: Load External Modules For Target Meaning - -Wesley is intentionally a `GraphQL -> whatever` compiler. +## Stage 4: Cross An External Target Boundary -Wesley brings the `GraphQL ->` part. External modules bring `whatever`. +Target-specific generation belongs outside generic Wesley. The retained Rust +contract makes that crossing explicit: -That module boundary is one of the most important design choices in the system. -Without it, every useful target would pressure the base compiler to absorb -product meaning. With it, Wesley can stay small and generic while modules supply -domain-specific directives, generators, witness scopes, release profiles, and -commands. - -```mermaid -flowchart TD - subgraph Base["Wesley base platform"] - Compiler[Compiler core] - GenericCLI[Generic CLI] - Hosts[Generic hosts] - Evidence[Generic evidence plumbing] - Registry[Module capability registry] - end - - subgraph ModuleLayer["External module layer"] - Language[Language generator module] - Database[Database module such as wesley-postgres] - Product[Product module such as Echo or Continuum integration] - ProjectLocal[Project-local module] - end - - subgraph Project["Project workspace"] - Schemas[Authored schemas] - Config[wesley.config.mjs or env module list] - Tests[Project tests] - Runtime[Runtime and deployment] - end - - Schemas --> Compiler - Config --> Registry - Registry --> Language - Registry --> Database - Registry --> Product - Registry --> ProjectLocal - - Compiler --> Language - Compiler --> Database - Compiler --> Product - Compiler --> ProjectLocal - - Language --> Project - Database --> Project - Product --> Project - ProjectLocal --> Project +```text +ExtensionGenerationInputV2 + | + v +external owner generator + | + v +GenerationProvenanceManifestV2 + | + v +GenerationReviewV2 ``` -The module capability model can represent several capability areas: +`ExtensionGenerationInputV2` contains: -```mermaid -erDiagram - MODULE ||--o{ MODULE_SUMMARY : records - MODULE ||--o{ CAPABILITY_ENTRY : contributes - CAPABILITY_REGISTRY ||--o{ MODULE_SUMMARY : lists - CAPABILITY_REGISTRY ||--o{ CAPABILITY_AREA : contains - CAPABILITY_AREA ||--o{ CAPABILITY_COLLECTION : contains - CAPABILITY_COLLECTION ||--o{ CAPABILITY_ENTRY : stores +- canonical Shape IR; +- normalized root operations; +- exact owner-declaration references; +- an exact settings digest; and +- requested projection roles. - MODULE { - string apiVersion - string name - } - MODULE_SUMMARY { - string name - string apiVersion - } - CAPABILITY_REGISTRY { - object modules - object capabilities - } - CAPABILITY_AREA { - string name - } - CAPABILITY_COLLECTION { - string name - } - CAPABILITY_ENTRY { - string moduleName - string capabilityName - object value - } -``` +The external generator interprets its own declarations and produces its own +artifacts. Wesley records exact source, generator, input, settings, and output +digests without claiming semantic authority over them. -Current JavaScript-side module capability areas include `wesley`, `holmes`, -`watson`, `moriarty`, `blade`, and `cli`. A module can contribute target -descriptors, commands, witness scopes, verification profiles, judgment -profiles, or certification hooks. The sequence below is historical legacy -compatibility, not the product front door Wesley is moving toward. +See [Extension Generation Contract](./reference/extension-generation.md). -The Rust-side capability model now records ABI compatibility and runtime state -before execution. A target declares its capability ABI range, execution mode, -portability floor, host imports, and resource-handle needs. The host evaluates -that metadata first; unsupported ABI ranges produce typed diagnostics such as -`WASM_ABI_UNSUPPORTED`, denied host functions are rejected before execution, and -the default runtime model is stateless unless a future policy explicitly grants -resource handles. +## Stage 5: Verify Evidence -```mermaid -flowchart TD - Target[Module target descriptor] - Contract[Capability ABI requirement] - Runtime[Runtime state model] - Imports[Requested host imports] - Resources[Requested resource handles] - Host[Host policy] - Diagnostics[Typed pre-execution diagnostics] - Execute[Execution hook] +Generation verification is deterministic and closed over supplied bytes. It +recomputes: - Target --> Contract - Target --> Runtime - Target --> Imports - Target --> Resources - Contract --> Host - Runtime --> Host - Imports --> Host - Resources --> Host - Host -->|accepted| Execute - Host -->|rejected| Diagnostics -``` +- the generator digest; +- every declared source digest; +- the canonical generation-input digest; +- every emitted artifact digest; and +- the provenance-manifest digest used by the review projection. -```mermaid -sequenceDiagram - actor User - participant CLI as Rust wesley CLI - participant Registry as Capability descriptor registry - participant Module as External target module - participant Core as Wesley compiler facts - participant Out as Generated artifacts +Verification does not read the filesystem, clock, environment, network, +registry, or process state. Missing, unexpected, or mismatched bytes fail with +structured diagnostics. - User->>CLI: wesley module command with schema and target - CLI->>Registry: resolve target descriptor - Registry-->>CLI: target metadata and capability requirements - CLI->>Module: invoke module-owned target boundary - Module->>Core: consume schema facts as needed - Module->>Out: emit target-owned artifacts - CLI-->>User: summary, dry-run output, or structured error -``` +`GenerationReviewV2` is explicitly non-authoritative. It is useful for review, +but it cannot become a replacement source. -The "why" is ownership clarity. PostgreSQL semantics belong in -`wesley-postgres`. Echo runtime semantics belong in Echo-owned tooling. -Continuum product policy belongs in Continuum-owned modules. Wesley should make -those modules easier to load, check, and witness. It should not become them. +## Stage 6: Consume Outside Wesley -## Stage 6: Package, Witness, And Judge +A sibling project may compile or load generated artifacts only after its own +target-specific verification. That project remains responsible for: -Wesley is a compiler, but the repository also contains a wider assurance -toolchain. +- application-language semantics; +- runtime capabilities and admission; +- data-model and migration behavior; +- target ABI and package validation; +- generated-output schemas; and +- live execution, durability, and recovery. -The core compile act answers: +For the Graft-on-Echo mission, this means: -> What artifacts follow from this authored contract and selected targets? +- Graft application behavior is authored in Edict; +- Edict compiles executable semantics; +- Echo owns Echo-specific capability and execution semantics; and +- Wesley is involved only where GraphQL structural projection remains useful. -The toolchain asks further questions: +## What Wesley Ships -- Are the generated artifacts traceable to the source? -- Are realization manifests coherent? -- What bounded property did a witness actually check? -- Is the evidence cited and mathematically coherent? -- Given evidence plus history plus policy, should this be considered ready? +The current Rust product surface includes: -```mermaid -flowchart TD - Source[Authored source] --> IR[Lowered IR] - IR --> Artifact[Generated artifact family] - Artifact --> Shell[Realization shell or manifest] - - Source --> Witness[Witness or evidence] - IR --> Witness - Artifact --> Witness - Shell --> Witness +- GraphQL SDL parsing and normalized output; +- deterministic L1 lowering and registry hashing; +- structural schema diffs; +- root operation catalogs and selection facts; +- generic directive-argument extraction; +- Rust and TypeScript declaration emission; +- supported little-endian codec emission; +- project-manifest inspection and changed-schema selection; +- external target-descriptor verification without target execution; and +- canonical extension-generation provenance and review contracts. - Witness --> Holmes[Holmes: investigate structure and evidence] - Witness --> Watson[Watson: verify citations and reasoning] - Holmes --> Moriarty[Moriarty: judge, predict, and gate] - Watson --> Moriarty - Artifact --> Blade[BLADE: certify release readiness] - Holmes --> Blade - Watson --> Blade - Moriarty --> Blade +The retained JavaScript Holmes package is a non-compiler evidence and reporting +surface. It does not extend the compiler's semantic authority. - Blade --> Bundle[Certified or failed readiness bundle] - Bundle --> Operator[Project or operator decision] -``` +## What Wesley Does Not Ship -Read that picture carefully. Wesley does not prove everything by compiling. -Compilation proves a derived artifact path. Witnesses prove bounded properties. -Holmes investigates evidence. Watson audits the evidence chain. Moriarty judges -with policy and context. BLADE certifies readiness. Deployment remains a project -or operator job. +Wesley does not: -This is architectural vocabulary, not a list of shipped native commands. Use -the [Assurance Capability Matrix](./reference/assurance-capability-matrix.md) -for the current shipped, transitional, internal-foundation, and concept-only -status of each assurance surface. +- execute application code; +- define Edict semantics; +- interpret target or runtime capabilities; +- enforce Echo footprints; +- perform database migrations; +- authorize runtime actions; +- certify deployments merely because an artifact is schema-valid; or +- turn GraphQL directives into a second application language. -```mermaid -stateDiagram-v2 - [*] --> Authored: schema is written - Authored --> Lowered: compiler lowers SDL - Lowered --> Emitted: targets emit artifacts - Emitted --> Witnessed: bounded checks run - Witnessed --> Investigated: Holmes report - Investigated --> Verified: Watson review - Verified --> Judged: Moriarty judgment - Judged --> Certified: BLADE passes gates - Judged --> Blocked: evidence or policy fails - Certified --> [*] - Blocked --> Authored: revise contract or target -``` +## Verification -The design choice is honesty. A generated file is not automatically true just -because it exists. The stronger invariant is: +The repository-level gate is: ```text -generated files are derived from named authored source through a recorded tool path -``` - -## Stage 7: Consume Artifacts Outside Wesley - -The end of Wesley's compile path is the beginning of another system's path. - -A project or external module may consume Wesley outputs to register runtime -contracts, load generated types, run database migrations, publish projection -bundles, or stage tests. Those consumers can rely on the fact that Wesley gave -them compiler facts and artifacts. They still own what happens at runtime. - -```mermaid -sequenceDiagram - actor Author - participant Wesley as Wesley compiler - participant Module as External module - participant Project as Project workspace - participant Runtime as Runtime or database - participant Witness as Runtime or project witness - - Author->>Wesley: provide authored SDL - Wesley->>Wesley: lower, hash, diff, emit - Wesley-->>Module: compiler facts and generated artifacts - Module-->>Project: module-owned projection bundle - Project->>Runtime: register or deploy using project policy - Runtime->>Runtime: execute under runtime law - Runtime-->>Witness: emit trace, receipt, or evidence - Witness-->>Project: bounded runtime claim -``` - -This is why Wesley's domain-empty boundary matters. A reusable compiler can -serve many consumers. A compiler that owns every consumer's runtime becomes -unreviewable and brittle. - -## Operation Artifact Boundary - -The long-term direction is bounded, lawful autonomy. - -In that target shape, an application declares a GraphQL operation that names -the structure it needs. Wesley compiles the operation into a typed, inspectable -contract artifact. A host or runtime may consume that artifact, but authority, -support, budget, law, admission, scheduling, and witness semantics remain -target-owned. - -```mermaid -flowchart TD - Agent[Agent or application] --> Operation[GraphQL operation] - Operation --> Wesley[Wesley compiles operation artifact] - Wesley --> Artifact[Operation artifact] - Wesley --> Requirements[Requirements bytes and digest] - Artifact --> Target[External target] - Requirements --> Target - Target --> Policy[Target-owned policy] - Policy --> Evidence[Target-owned evidence] -``` - -Wesley's role in that story is powerful but bounded: - -- compile the operation shape -- preserve law-shaped declarations -- emit canonical bytes and digests for requirements -- produce artifacts a runtime can register and verify - -Wesley still does not grant authority, execute the world, or decide every -domain law. The runtime and host policy own admission and enforcement. - -## Current Repository Map - -If you open the repository today, the important paths are: - -| Path | Role | -| -------------------------------- | ----------------------------------------------------------------------------- | -| `crates/wesley-core/` | Rust compiler kernel: parse, lower, diff, hash, and analyze operations. | -| `crates/wesley-cli/` | Native `wesley` command surface. | -| `crates/wesley-emit-rust/` | Rust projection crate. | -| `crates/wesley-emit-typescript/` | TypeScript projection crate. | -| `xtask/` | Rust repository automation, docs checks, preflight, release checks. | -| `packages/wesley-holmes/` | Self-contained assurance, evidence, verification, and judgment tooling. | -| `docs/` | Architecture, method, design packets, release packets, and current direction. | -| `test/fixtures/` | GraphQL fixtures, Rust L1 golden files, and parity inputs. | -| `scripts/` | Fixture, docs, CI, and repository support scripts. | - -```mermaid -flowchart TB - subgraph Repo["Wesley repository"] - Docs[docs] - Fixtures[test/fixtures] - Scripts[scripts] - - subgraph Rust["Rust workspace"] - Core[wesley-core] - NativeCli[wesley-cli] - EmitRust[wesley-emit-rust] - EmitTS[wesley-emit-typescript] - Xtask[xtask] - end - - subgraph JS["Non-compiler JavaScript"] - Holmes["@wesley/holmes"] - end - end - - subgraph External["External owners"] - Echo[Echo-owned tooling] - Postgres[wesley-postgres] - Continuum[Continuum-owned module] - Apps[Project workspaces] - end - - Docs --> Rust - Fixtures --> Core - Scripts --> Rust - Scripts --> JS - NativeCli --> Core - NativeCli --> EmitRust - NativeCli --> EmitTS - Holmes --> Docs - - Core -. compiler facts .-> Echo - Core -. L1 IR .-> Postgres - Core -. module facts .-> Continuum - EmitRust -. generated artifacts .-> Apps - EmitTS -. generated artifacts .-> Apps +cargo xtask preflight ``` -## Validation And Release Evidence - -Wesley treats tests and generated evidence as part of the product. - -The current validation surface includes Rust tests, native CLI checks, docs -truth checks, docs link checks, retained package checks, fixture generation, -and retirement guards. +That Rust-owned gate composes the compiler, documentation, and retirement +evidence: ```mermaid flowchart TD Change[Proposed change] --> RustPreflight[cargo xtask preflight] - Change --> LegacyPreflight[cargo xtask legacy-preflight] - Change --> Fixtures[pnpm fixtures:ir] - RustPreflight --> RustTests[cargo test --workspace] RustPreflight --> NativeHelp[native CLI help smoke] - LegacyPreflight --> PnpmLegacy[pnpm run legacy-preflight] - PnpmLegacy --> DocsLinks[legacy docs links] - PnpmLegacy --> DocsTruth[legacy docs truth manifest] + RustPreflight --> DocsTruth[documentation truth and link checks] RustPreflight --> NodeRetirement[Node retirement ledger guard] - PnpmLegacy --> Lint[lint and format] - PnpmLegacy --> PackageTests[package tests] - - Fixtures --> Golden[L1 golden files] - - RustTests --> PR[Pull request] - NativeHelp --> PR - DocsLinks --> PR - DocsTruth --> PR - NodeRetirement --> PR - Lint --> PR - PackageTests --> PR - Golden --> PR -``` - -The v0.0.6 release lane tightened Rust IR parity before deletion. That evidence -is now historical. The release oracle is Rust fixture truth and Rust-native -preflight. - -The closed Node retirement campaign leaves another proof surface: a -machine-readable ledger and drift guard that fails if retired package manifests -or imports return. - -## What Wesley Does Today - -Today, Wesley can: - -- lower GraphQL SDL into Rust L1 IR -- preserve generic directive data -- fold schema extensions into consolidated type facts -- print normalized SDL and normalized SDL hashes from Rust compiler facts -- compute canonical JSON and hashes -- compute structural schema deltas -- list root operations with argument and result types -- resolve operation selections with or without schema coordinates -- extract operation directive arguments -- emit Rust models and operation bindings -- emit TypeScript declarations and operation bindings -- write deterministic native emit metadata sidecars -- keep JavaScript outside compiler authority except for Holmes assurance, - docs tooling, and repository automation -- model external module targets through Rust capability descriptors, ABI - compatibility reports, stateless runtime policy, and hermetic fixture checks -- run docs, lint, package, Rust, fixture, and preflight checks -- maintain evidence and design packets around releases and architectural - boundaries - -```mermaid -mindmap - root((Wesley today)) - Rust core - Lower SDL - Hash IR - Diff schemas - List operations - Analyze operations - Native CLI - normalize-sdl - schema lower - schema hash - schema diff - emit rust - emit typescript - law validate - Contract bundle law - Strict binding - lawHash - lawDocumentHash - profileHash - bundleHash - Non-compiler JavaScript - Holmes tooling - Host experiments - Website and docs tooling - Evidence - Fixtures - Docs truth - Boundaries - External modules - wesley-postgres - Echo owned runtime law - Continuum owned product policy -``` - -## What Wesley Does Not Do - -Wesley does not own: - -- Echo rewrite scheduling -- Echo footprint honesty enforcement -- PostgreSQL migrations or RLS policy -- Supabase behavior -- Continuum runtime policy -- jedit product behavior -- project deployment -- host authority decisions outside Wesley's own loading guards -- runtime values emitted by products using Wesley artifacts - -This is not a weakness. It is the reason the compiler can stay useful. - -```mermaid -flowchart TD - Wesley[Wesley owns compiler truth] --> Facts[IR, hashes, diffs, artifacts, evidence inputs] - - Echo[Echo owns runtime law] --> EchoRuntime[scheduling, admission, witnesses] - Postgres[wesley-postgres owns database semantics] --> DB[migrations, SQL, RLS, adapters] - Continuum[Continuum owns product policy] --> ContinuumRuntime[workspace and release policy] - Project[Project owns deployment] --> Deploy[production rollout] - Facts -. consumed by .-> Echo - Facts -. consumed by .-> Postgres - Facts -. consumed by .-> Continuum - Facts -. consumed by .-> Project + RustTests --> Ready[Review-ready change] + NativeHelp --> Ready + DocsTruth --> Ready + NodeRetirement --> Ready ``` -## A Full Example In Words - -Imagine a project has a schema that declares a `Buffer`, a `TextWindowInput`, -and a `textWindow` query. - -1. A human or agent edits the GraphQL SDL. -2. Wesley parses the SDL. -3. Wesley lowers the SDL into L1 IR with type definitions, fields, directives, - operation roots, type references, and metadata. -4. Wesley canonicalizes the IR and computes a stable hash. -5. Wesley lists the root query operation and its argument/result types. -6. Wesley emits Rust and TypeScript bindings. -7. A module may emit additional target-specific artifacts. -8. Fixture tests compare the result against expected compiler behavior. -9. Witness or evidence tooling can record what was checked. -10. A project or runtime consumes the generated artifacts under its own policy. - -```mermaid -sequenceDiagram - actor Author - participant SDL as schema.graphql - participant Core as wesley-core - participant Hash as canonical hash - participant Rust as Rust emitter - participant TS as TypeScript emitter - participant Module as External module - participant Tests as Fixtures and Rust checks - participant Project as Project runtime - - Author->>SDL: edit contract - SDL->>Core: parse and lower - Core-->>Hash: canonical bytes - Core-->>Rust: L1 IR and operation facts - Core-->>TS: L1 IR and operation facts - Core-->>Module: generic compiler facts - Rust-->>Tests: generated Rust artifact - TS-->>Tests: generated TypeScript artifact - Module-->>Tests: module-owned artifacts - Tests-->>Author: pass, fail, or explain drift - Tests-->>Project: artifacts are ready to consume - Project->>Project: runtime behavior remains project-owned -``` - -The key thing to notice is that the source of truth never moves. The schema is -authored. Everything else is derived, witnessed, or consumed. - -## Design Choices That Matter Most - -### One Source, Many Projections - -Wesley rejects the idea that every layer should maintain its own contract copy. -One GraphQL schema can drive Rust, TypeScript, hashes, operation facts, witness -inputs, and module-owned targets. - -### Domain-Empty Core - -The compiler core stays generic so it can serve many domains. Product, -database, and runtime meanings come through external modules or sibling repos. - -### Deterministic Bytes - -Hashes and parity checks only matter if the bytes are deterministic. That is -why canonical JSON, fixture goldens, sorted projections, explicit metadata -rules, and stable diagnostic codes matter. - -### Evidence Before Deletion - -Legacy Node behavior was retired carefully. The v0.0.6 and 0017 lanes built -fixture, parity, and migration evidence before deleting the remaining package -surfaces. That avoided replacing one unproved truth with another. - -### Witnesses Are Bounded Claims - -A witness should say exactly what it checked. A compiler witness is not runtime -observation. A generated artifact is not deployment proof. A release-readiness -bundle is not production health. - -### Explicit External Ownership - -`wesley-postgres` should own PostgreSQL semantics. Echo should own Echo runtime -law. Continuum should own Continuum product policy. Wesley should preserve the -facts those owners need without absorbing their worlds. - -## The Short Version - -If you only remember one end-to-end path, remember this: +Focused compiler and external-generation evidence can be checked with: ```text -Author GraphQL SDL. -Wesley lowers it into deterministic compiler facts. -Wesley emits generic artifacts or hands facts to external modules. -Evidence tools prove bounded properties about the source and artifacts. -External systems consume the outputs and own their runtime meaning. +cargo test -p wesley-core +cargo test -p wesley-cli +cargo test -p wesley-emit-rust ``` -And if you only remember one boundary, remember this: - -```text -Wesley owns compiler truth. -Modules and projects own target meaning. -``` +Use [ARCHITECTURE.md](./ARCHITECTURE.md) for the current component map, +[ENTRYPOINTS.md](./ENTRYPOINTS.md) for command discovery, and +[BEARING.md](./BEARING.md) for current direction. diff --git a/docs/ENTRYPOINTS.md b/docs/ENTRYPOINTS.md index 39360dda..85851f68 100644 --- a/docs/ENTRYPOINTS.md +++ b/docs/ENTRYPOINTS.md @@ -67,8 +67,6 @@ It can: identity, generator version, execution mode, and optional law bundle hashes - run narrow Rust-native health checks without inspecting legacy Node config, plugins, or package state -- scaffold, lint, validate, diff, explain, rebind, and report coverage for - `weslaw/v1` documents - resolve GraphQL operation selection paths - resolve schema-coordinate selections when schema SDL is available - extract arbitrary operation directive arguments as data @@ -85,16 +83,8 @@ wesley schema hash --schema wesley schema operations --schema --json wesley schema diff --old --new [--format text|json|summary] [--exit-code] wesley schema diff --schema --against [--format text|json|summary] [--exit-code] -wesley init-law --schema --family [--out ] -wesley law lint --law [--json] -wesley law validate --schema --law [--json] -wesley law diff --old --new [--schema ] [--format markdown|json|summary] -wesley law explain --law [--json] -wesley law rebind --schema --law [--accept --out ] [--json] -wesley law capabilities --law [--json] -wesley law coverage --schema --law [--profile release|ci-release|local] [--json] -wesley emit rust --schema --out [--law ] [--metadata-out ] -wesley emit typescript --schema --out [--law ] [--metadata-out ] +wesley emit rust --schema --out [--metadata-out ] +wesley emit typescript --schema --out [--metadata-out ] wesley emit le-binary-rust --schema --out [--law ] [--metadata-out ] [--codec-import ] wesley emit le-binary-typescript --schema --out [--law ] [--metadata-out ] [--codec-import ] wesley operation selections --operation [--schema ] [--json] diff --git a/docs/GUIDE.md b/docs/GUIDE.md index ad7d7ca3..1e0781d0 100644 --- a/docs/GUIDE.md +++ b/docs/GUIDE.md @@ -90,9 +90,7 @@ The direct replacements are `wesley schema lower`, `wesley schema hash`, `wesley law rebind`, `wesley law capabilities`, `wesley law coverage`, `wesley doctor`, and `wesley emit typescript` or `wesley emit rust`. Zod and certificate commands are no longer generic compiler-front-door work. -Holmes-family commands still live under `@wesley/holmes`; the Rust foundation -for future Holmes law-assurance ingestion lives in `crates/wesley-holmes` and -does not expose public CLI commands yet. Use the +Holmes-family commands still live under `@wesley/holmes`. Use the [Assurance Capability Matrix](./reference/assurance-capability-matrix.md) for the shipped/transitional/foundation/concept status of each assurance surface. diff --git a/docs/JEDIT_CAPABILITY_EVIDENCE.md b/docs/JEDIT_CAPABILITY_EVIDENCE.md index 1b4067ff..1304e941 100644 --- a/docs/JEDIT_CAPABILITY_EVIDENCE.md +++ b/docs/JEDIT_CAPABILITY_EVIDENCE.md @@ -58,12 +58,7 @@ The invariant remains Wesley's normal compiler boundary: bindings when the schema contains root `Query`, `Mutation`, or `Subscription` fields. - `--metadata-out ` on emit commands writes a deterministic JSON sidecar - recording schema, law, profile, bundle, generator, generator-version, and - execution-mode identity. -- `emit_rust_with_operations_and_hashes` embeds `WESLEY_SCHEMA_HASH` and - `WESLAW_HASH` provenance constants directly in generated Rust output. -- `emit_rust_with_operations_and_law` generates law-backed scalar and variant - validator types from `LawIrV1` into Rust output. + recording schema, generator, generator-version, and execution-mode identity. - `emit_le_binary_typescript` generates a little-endian binary encode/decode codec for operation variable types, exposed as `wesley emit le-binary-typescript`. @@ -78,8 +73,7 @@ The real jedit contract is more than a data model. Its root `Query` and types, and generic directive data. Wesley has a generic operation catalog for those root fields and projects that -catalog into Rust and TypeScript operation bindings. Law-backed validators can -be co-emitted when a `weslaw/v1` file is supplied. +catalog into Rust and TypeScript operation bindings. Echo-specific footprint honesty remains external to Wesley core. Wesley preserves `@wes_footprint` as generic directive JSON for Echo-owned tooling to @@ -102,5 +96,3 @@ The generated bindings connect: - operation kind, field name, argument object type, result type - preserved directive metadata for downstream Echo-owned tooling - LE binary encode/decode for operation variable wire serialization -- `WESLEY_SCHEMA_HASH` and `WESLAW_HASH` provenance anchors in generated Rust -- law-backed scalar and variant validators when a `weslaw/v1` file is supplied diff --git a/docs/NORTHSTAR.md b/docs/NORTHSTAR.md index 0a8b81fd..7fc81834 100644 --- a/docs/NORTHSTAR.md +++ b/docs/NORTHSTAR.md @@ -25,7 +25,6 @@ Wesley owns: - Preserved directive data. - Domain-empty Rust and TypeScript projections. - Shared codec planning for Wesley-owned emitted artifacts. -- `weslaw` authoring, Law IR, coverage, and report-only capability summaries. - Generic operation artifacts that describe operation shape, requirements, and law claims without executing anything. diff --git a/docs/README.md b/docs/README.md index b8751d35..8f23ce42 100644 --- a/docs/README.md +++ b/docs/README.md @@ -70,8 +70,7 @@ not active Wesley release commitments. The active design direction is now: -- [Holmes Assurance Hexagon](./design/0018-holmes-assurance-hexagon/holmes-assurance-hexagon.md) -- [`weslaw` Semantic Law IR](./design/0019-weslaw-semantic-law-ir/weslaw-semantic-law-ir.md) +- [Remove Weslaw](./design/0023-remove-weslaw/SOURCE_remove-weslaw.md) The repo already has the important generic building blocks around that direction: diff --git a/docs/SDL.md b/docs/SDL.md index a2a98d83..e6bb47c6 100644 --- a/docs/SDL.md +++ b/docs/SDL.md @@ -1,15 +1,14 @@ -# SDL, Shape, And Law +# SDL, Shape, And Semantic Ownership This note supports the README. It explains why Wesley starts from GraphQL SDL -and where the line sits between generic compiler facts and domain-owned law. +and where the line sits between generic compiler facts and semantics owned by +other languages and targets. For runnable commands, use [ENTRYPOINTS.md](./ENTRYPOINTS.md). For the domain-free invariant, use [NORTHSTAR.md](./NORTHSTAR.md). For the current -direction and active tensions, use [BEARING.md](./BEARING.md). For the formal -semantic-law design, use -[`weslaw` Semantic Law IR](./design/0019-weslaw-semantic-law-ir/weslaw-semantic-law-ir.md). +direction and active tensions, use [BEARING.md](./BEARING.md). ## Contract Substrate @@ -54,12 +53,12 @@ Generic Wesley can lower this shape into L1 IR, compute hashes, compare schema structure, and emit Rust or TypeScript bindings without knowing anything about editor runtimes, databases, replication, scheduling, or storage. -## Law-Shaped Data +## Directive Data -Law answers: what is permitted, required, or forbidden? - -GraphQL directives can carry law-shaped data at inspectable schema locations. -Wesley preserves that data. It does not interpret domain law in generic core. +GraphQL directives can carry target-owned metadata at inspectable schema +locations. Wesley preserves directive spelling and arguments as structural +input. Generic core does not interpret that data as application or runtime +semantics. ```graphql type Query { @@ -75,12 +74,9 @@ type Query { The generic claim is bounded: Wesley can preserve the root operation, argument type, result type, directive names, and directive arguments. An Echo-owned -extension can then decide whether `@wes_footprint` is honest, sufficient, or -acceptable for Echo runtime law. - -The `weslaw` design promotes this preserved law-shaped data into a future -typed Law IR so semantic laws can be bound, hashed, diffed, and explained -without making generic Wesley the owner of target runtime meaning. +target adapter may decide whether `@wes_footprint` has meaning for Echo. Edict +owns executable language semantics; Wesley does not promote arbitrary +directives into a second semantic language. For operation artifacts, `@wes_footprint` is target-evaluation v0 metadata. It is legal only on the selected root field, must declare `reads` and `writes` diff --git a/docs/TECHNICAL_TEARDOWN.md b/docs/TECHNICAL_TEARDOWN.md index b1d182b1..f5d1dd5e 100644 --- a/docs/TECHNICAL_TEARDOWN.md +++ b/docs/TECHNICAL_TEARDOWN.md @@ -1,18 +1,20 @@ - + # Wesley Technical Teardown -> Status: release-scoped orientation snapshot. +> Status: historical `v0.2.0` release snapshot. > -> This document explains the repository state for a release/readiness review. -> It is not the authoritative architecture map and must not become a roadmap. -> Use [ARCHITECTURE.md](./ARCHITECTURE.md) for current structure and -> [BEARING.md](./BEARING.md) for current direction and active tensions. +> The Weslaw language, Law IR, and Rust Holmes foundation described below were +> removed by +> [design packet 0023](./design/0023-remove-weslaw/SOURCE_remove-weslaw.md). +> They are retained here only as release archaeology. Use +> [ARCHITECTURE.md](./ARCHITECTURE.md) for current structure, +> [END_TO_END.md](./END_TO_END.md) for the current execution story, and +> [BEARING.md](./BEARING.md) for current direction. This document is an end-to-end technical explanation of the Wesley repository -as prepared for the `v0.2.0` release on June 26, 2026. The `v0.3.0-alpha.1` -pre-release carries the same architecture; it is the first 0.3.0-line -pre-release, cut to unblock downstream consumers. +as prepared for the `v0.2.0` release on June 26, 2026. It does not describe the +current product surface. It assumes no prior knowledge of Wesley, its domain, or its implementation. The explanation starts with the business and domain concepts, then follows the diff --git a/docs/architecture/holmes-integration.md b/docs/architecture/holmes-integration.md index 596860a0..f3468a17 100644 --- a/docs/architecture/holmes-integration.md +++ b/docs/architecture/holmes-integration.md @@ -4,7 +4,7 @@ HOLMES is Wesley's assurance sidecar. It consumes explicit evidence artifacts and reports on their quality. It does not become the source of truth for -GraphQL shape, `weslaw`, target semantics, runtime behavior, or product policy. +GraphQL shape, application semantics, target behavior, or product policy. The current CI integration is intentionally narrow: diff --git a/docs/design/0019-weslaw-semantic-law-ir/weslaw-semantic-law-ir.md b/docs/design/0019-weslaw-semantic-law-ir/weslaw-semantic-law-ir.md index 1958a289..92564da9 100644 --- a/docs/design/0019-weslaw-semantic-law-ir/weslaw-semantic-law-ir.md +++ b/docs/design/0019-weslaw-semantic-law-ir/weslaw-semantic-law-ir.md @@ -2,22 +2,26 @@ title: weslaw Semantic Law IR legend: OWN packet: 0019-weslaw-semantic-law-ir -status: active +status: superseded release: v0.0.8 --- # weslaw Semantic Law IR +> [!IMPORTANT] +> This packet is historical. Design packet +> [0023](../0023-remove-weslaw/SOURCE_remove-weslaw.md) removed Weslaw and its +> Law IR from Wesley. Nothing below describes a supported product surface. + ## Status -Active design packet. +Superseded design packet. Decision posture: ```text -ACCEPT: weslaw as Wesley's semantic law layer. -ENHANCE: the v1 specification before implementation. -DEFER: Wesley SDL+ until Law IR is stable, boring, and useful. +RETIRED: weslaw as Wesley's semantic law layer. +RETAIN: this packet as architecture archaeology. ``` ## Question @@ -136,7 +140,7 @@ specs and fixtures: - [Canonicalization And Diagnostics](./CANONICALIZATION_AND_DIAGNOSTICS.md) defines canonical byte rules, hash inputs, active versus draft semantics, and the v1 diagnostic catalog. -- [weslaw fixtures](../../../test/fixtures/weslaw/README.md) define accepted +- the now-removed historical fixture corpus defined accepted and rejected substrate examples for scalar, variant, footprint, channel, and invariant law. diff --git a/docs/design/0020-holmes-weslaw-assurance-prd-test-plan/holmes-weslaw-assurance-prd-test-plan.md b/docs/design/0020-holmes-weslaw-assurance-prd-test-plan/holmes-weslaw-assurance-prd-test-plan.md index 21003394..9513b10b 100644 --- a/docs/design/0020-holmes-weslaw-assurance-prd-test-plan/holmes-weslaw-assurance-prd-test-plan.md +++ b/docs/design/0020-holmes-weslaw-assurance-prd-test-plan/holmes-weslaw-assurance-prd-test-plan.md @@ -2,13 +2,19 @@ title: Holmes weslaw Assurance PRD And Test Plan Campaign legend: SPEC packet: 0020-holmes-weslaw-assurance-prd-test-plan -status: complete +status: superseded release: v0.0.8 --- # Holmes `weslaw` Assurance PRD And Test Plan Campaign -This packet is closed planning evidence. It is not a live implementation +> [!IMPORTANT] +> This packet is historical. Design packet +> [0023](../0023-remove-weslaw/SOURCE_remove-weslaw.md) removed Weslaw and its +> Rust assurance foundation. Nothing below describes supported or planned +> product work. + +This packet is superseded planning evidence. It is not a live implementation tracker. Live Holmes and `weslaw` implementation work belongs in GitHub Issues, diff --git a/docs/design/0022-justification-graph-assurance-kernel/justification-graph-assurance-kernel.md b/docs/design/0022-justification-graph-assurance-kernel/justification-graph-assurance-kernel.md index 55a1385a..aee39637 100644 --- a/docs/design/0022-justification-graph-assurance-kernel/justification-graph-assurance-kernel.md +++ b/docs/design/0022-justification-graph-assurance-kernel/justification-graph-assurance-kernel.md @@ -7,9 +7,8 @@ status: 'active' supersedes: - '0008-holmes-counterfactual-provider-capability' - '0018-holmes-assurance-hexagon' -reconciles: - - '0019-weslaw-semantic-law-ir' - - '0020-holmes-weslaw-assurance-prd-test-plan' +amended-by: + - '0023-remove-weslaw' issues: - 'https://github.com/flyingrobots/wesley/issues/448' - 'https://github.com/flyingrobots/wesley/issues/542' @@ -18,7 +17,7 @@ issues: owners: - '@flyingrobots' created: '2026-07-15' -updated: '2026-07-15' +updated: '2026-07-25' --- @@ -58,11 +57,11 @@ lines of JavaScript (`packages/wesley-holmes`) that: `GIT_DIR` hook-isolation defect both cost release engineering time in practice). -The Rust `crates/wesley-holmes` crate is a hexagonal *domain foundation* with no -CLI, no report rendering, and no prediction. The four open `v0.3.0` issues are -already, in effect, the first slices of a Rust rewrite of the evidence-review -half. This packet defines the whole target so the rewrite builds the extensible -engine we want rather than reproducing the coupling. +The former Rust Holmes foundation was removed with design packet `0023` +because it was coupled to the retired semantic-law subsystem. This packet +defines a future domain-free evidence-review target. Its implementation must +begin from these contracts rather than treating the deleted crate as a +foundation. ### 1.2 The reframe @@ -125,12 +124,11 @@ stays module-owned and untouched by the kernel. ### 2.1 Placement -The kernel is a Rust library crate (`wesley-holmes`), a thin CLI surface -(`wesley holmes …` in `wesley-cli`, or a dedicated `wesley-holmes` bin), and a -set of adapters. It is pure hexagonal: the domain and engines depend only on -ports (traits + data contracts); every I/O, clock, subprocess, and domain -behavior enters through an adapter. This is the `0018` hexagon, made concrete -for Rust and generalized past Holmes-only. +The kernel is a future Rust library crate, a thin CLI surface, and a set of +adapters. Its final crate and command names are intentionally deferred until an +implementation cycle. It is pure hexagonal: the domain and engines depend only +on ports (traits + data contracts); every I/O, clock, subprocess, and domain +behavior enters through an adapter. The kernel links **no domain crate**. `wesley-postgres`, Echo, Continuum, and Edict reach it exclusively as registered providers, discovered through the same @@ -139,9 +137,9 @@ capability/registration protocol Wesley already uses for `target verify` ### 2.2 Components -- **Assembler** — merges compiler evidence (`.wesley-cache/bundle.json`, weslaw - artifacts) and module-contributed evidence into one `justification-graph/v1`. - Pure, deterministic, conflict-detecting (Section 5.4). +- **Assembler** — merges compiler evidence and module-contributed evidence into + one `justification-graph/v1`. Pure, deterministic, conflict-detecting + (Section 5.4). - **Admissibility Filter** — the kernel floor on which evidence may be *considered at all* (distinct from weight). Fail-closed. - **HOLMES engine** — establishment via grounded extension (Section 3.4). @@ -837,9 +835,10 @@ path only at the deletion checkpoint. - **`0018` (Holmes Assurance Hexagon, active).** Superseded: the hexagon's ports are made concrete as the Section 4.5 traits + out-of-process protocol, and the domain-free boundary is stated normatively. -- **`0019` / `0020` (weslaw law IR + assurance PRD).** Reconciled: weslaw law - claims become first-class `Claim`s in the Justification Graph; the PRD's test - intent maps onto the fixture-and-content-hash gates in Sections 7–8. +- **`0019` / `0020` (retired semantic-law experiment).** Superseded by `0023`. + No language types, hashes, or assurance artifacts from those packets carry + into this kernel. Reusable deterministic test intent must be restated against + domain-free evidence contracts. ## Appendix B — Why grounded semantics diff --git a/docs/design/0023-remove-weslaw/SOURCE_remove-weslaw.md b/docs/design/0023-remove-weslaw/SOURCE_remove-weslaw.md new file mode 100644 index 00000000..8a26294b --- /dev/null +++ b/docs/design/0023-remove-weslaw/SOURCE_remove-weslaw.md @@ -0,0 +1,372 @@ +--- +title: 'SOURCE - Remove Weslaw' +legend: 'SPEC|SOURCE|TRANSMUTE|EVIDENCE|OWN' +packet: '0023-remove-weslaw' +issue: 'https://github.com/flyingrobots/wesley/issues/768' +status: 'complete' +supersedes: + - '0019-weslaw-semantic-law-ir' + - '0020-holmes-weslaw-assurance-prd-test-plan' +owners: + - '@flyingrobots' +created: '2026-07-25' +updated: '2026-07-25' +--- + + + +# SOURCE - Remove Weslaw + +## Linked Issue + +- [Remove Weslaw from Wesley](https://github.com/flyingrobots/wesley/issues/768) + +## GitHub Work + +- Issue: `https://github.com/flyingrobots/wesley/issues/768` +- Goalpost milestone: `Goalpost: Make It Truthful` +- Project: `https://github.com/users/flyingrobots/projects/18` + +The issue is assigned to `@flyingrobots`, carries the `v0.3.0` scheduling +label, and is marked `work-in-progress`. The current GitHub token could not add +the issue to the project because it lacks the `read:project` scope. + +## Cycle Preparation + +The cycle branch `james/remove-weslaw` was created from fetched +`origin/main` at `4891a631f888c5b2f70e117e3704538dd1362c2f` in an isolated +worktree. The existing Wesley `main` checkout and the dirty Wesley-Postgres +checkout remain untouched. + +## Decision Summary + +Wesley will stop accepting, binding, hashing, diffing, emitting, or assuring +Weslaw artifacts. GraphQL SDL remains Wesley's structural source. Semantic +programs belong to Edict; runtime capabilities belong to their owning runtime +or target adapter; target-specific declarations may cross Wesley's generic +extension boundary only as opaque, content-addressed owner artifacts. + +## Sponsored Human + +A compiler maintainer wants Wesley to mean `GraphQL -> whatever` so that target +and application semantics have one honest owner, without maintaining a second +language disguised as YAML beside GraphQL. + +## Sponsored Agent + +An agent needs the exported Rust API, CLI help, schemas, fixtures, and current +documentation to agree that Weslaw is unsupported, without inferring ownership +from historical design packets or dead compatibility shims. + +## Hill + +By the end of this cycle, an operator can use every supported Wesley compiler +and extension-generation path without a Weslaw artifact, and the repository +proves that no production, CLI, emitter, assurance, schema, fixture, or current +documentation surface still depends on Weslaw. + +## Current Truth + +At the branch basis: + +- `crates/wesley-core/src/domain/law.rs` implements the Weslaw YAML loader, + typed Law IR, schema binding, canonicalization, hashing, semantic diffing, + and contract-bundle manifest construction. +- `crates/wesley-cli/src/main.rs` exposes `init-law` and the `law lint`, + `validate`, `diff`, `explain`, `rebind`, `capabilities`, and `coverage` + commands. Rust emission accepts an optional `--law` input. +- `crates/wesley-emit-rust/src/lib.rs` emits Weslaw hash constants and + Weslaw-derived scalar and variant validators. +- `crates/wesley-holmes` is a Weslaw-specific assurance foundation over law + diffs, law coverage, law capabilities, and law contract-bundle manifests. +- `ExtensionGenerationInputV1` embeds optional `LawIrV1`. Its surrounding + provenance and content-addressed artifact machinery is otherwise generic. +- `docs/BEARING.md` incorrectly makes Weslaw a current architectural and + `v0.3.0` release objective. + +## Problem + +Weslaw assigns unrelated meanings to one generic GraphQL compiler: + +- scalar semantics that belong to a source profile, target mapping, or + programming language; +- variant constraints that should be represented as actual sum types; +- Echo-specific operation footprints; +- protocol/channel contracts; +- target and application invariants. + +The YAML sidecar therefore creates a second, weak semantic language while +claiming generic compiler ownership. Keeping it would duplicate Edict, +misplace runtime capabilities, and force every downstream GraphQL target to +pass through concepts it does not own. + +## Scope + +This cycle includes: + +- deleting Weslaw source parsing, Law IR, binding, canonicalization, hashes, + semantic diffs, and contract-bundle manifests; +- deleting Weslaw CLI commands and the `--law` emitter option; +- deleting Weslaw-derived Rust validators and provenance constants; +- deleting the current Weslaw-specific Holmes crate; +- deleting Weslaw schemas, fixtures, workflow triggers, and configuration; +- replacing extension-generation v1 with a law-free v2 contract; +- correcting current architecture, guide, CLI, schema, and release-direction + documentation; +- marking historical Weslaw packets as superseded while retaining them as + evidence. + +## Non-Goals + +This cycle does not include: + +- moving Weslaw syntax or types into Edict; +- defining Edict modules, capability packages, or an Echo target adapter; +- implementing the justification-graph assurance kernel; +- designing PostgreSQL semantics; +- rewriting historical audits to pretend Weslaw never existed; +- maintaining a deprecated Weslaw compatibility parser or CLI alias. + +## Compiler / CLI Contract + +The following public surfaces are removed: + +- all `wesley_core` Law IR and Weslaw APIs; +- `wesley init-law`; +- the complete `wesley law ...` command family; +- `wesley emit ... --law`; +- `emit_rust_with_operations_and_hashes`; +- `emit_rust_with_operations_and_law`; +- the Weslaw-specific `wesley-holmes` crate; +- the `weslaw/v1`, `wesley.law-ir/v1`, `wesley.law-diff/v1`, and + `wesley.contract-bundle-manifest/v1` schemas. + +Removed commands and flags receive the ordinary typed CLI usage failure. No +compatibility alias accepts or ignores a semantic input. + +The generic extension-generation contract becomes: + +```rust +ExtensionGenerationInputV2::new( + shape_ir, + operations, + owner_declarations, + settings_digest, + projection_roles, +) +``` + +`ExtensionGenerationInputV2` contains no interpreted semantic field. External +modules may identify target-owned semantic declarations through +`owner_declarations`; Wesley validates only their coordinates and exact +digests. Generation provenance, verification, and review types move to v2 +because their exact contract-version closure selects the v2 input. + +## Data / State / Schema Model + +GraphQL Shape IR and normalized operations remain compiler-owned inputs. +Target-owned declarations remain opaque bytes outside Wesley and enter +generation only as content-addressed references. + +```mermaid +flowchart LR + SDL["GraphQL SDL"] --> IR["Wesley Shape IR"] + IR --> INPUT["ExtensionGenerationInputV2"] + DECL["Owner declarations"] --> HASH["Coordinate + SHA-256"] + HASH --> INPUT + INPUT --> TARGET["External target generator"] + TARGET --> PROV["GenerationProvenanceManifestV2"] +``` + +The removed v1 extension artifacts are not reinterpreted as v2. Producers must +regenerate them from the authoritative SDL and explicit owner declarations. + +## Security / Trust Boundary + +Removing Weslaw eliminates a YAML input parser and a false generic authority +boundary. Owner declarations remain untrusted external bytes. Wesley binds +their exact coordinates and SHA-256 digests but does not parse or execute +their meaning. External target execution and capability enforcement remain +governed by the existing target/module boundary. + +## Agent Inspectability + +An agent can inspect: + +- CLI help, which contains no Weslaw command or option; +- the exported Rust API and v2 JSON Schemas; +- the extension-generation v2 fixtures; +- a repository-wide residual-reference audit that distinguishes superseded + historical evidence from supported surfaces. + +## Accessibility Posture + +| Surface | Requirement | +| --------------------------------- | ------------------------------------------------ | +| Exit codes and error envelopes | Removed commands fail through normal CLI usage. | +| Structured JSON output | v2 generation artifacts remain deterministic. | +| Human-readable Markdown summaries | Current docs name the removal and new ownership. | +| Agent-safe summary fields | Versioned API identities select exact v2 shapes. | + +## Localization / Directionality Posture + +| String or surface | Requirement | +| ------------------------- | --------------------------------------------------- | +| Diagnostic messages | No new domain-specific diagnostic vocabulary. | +| CLI help text | Remove Weslaw entries; preserve existing CLI style. | +| Report or summary strings | Remove Weslaw-specific report language. | + +## Linked Invariants + +- `schema-source-of-truth` — GraphQL SDL remains sovereign over structural + shape. +- `domain-empty-core` — target and runtime semantics do not live in Wesley. +- `evidence-truth` — versioned artifacts describe only implemented behavior. +- `docs-runtime-honesty` — current docs match the supported CLI and crates. +- `deterministic-ir` — canonical Shape IR and content hashes remain stable. + +## Alternatives Considered + +### Option A: Deprecate Weslaw + +Pros: + +- minimizes immediate downstream compilation failures. + +Cons: + +- preserves two sources of semantic authority; +- leaves Echo footprints and language-specific rules in generic Wesley; +- makes dead architecture look supported; +- creates a migration path to nowhere. + +### Option B: Delete Weslaw and version the generic seam + +Pros: + +- restores one owner per semantic concern; +- keeps generic generation provenance without interpreting target meaning; +- makes unsupported inputs fail closed; +- gives changed exact artifacts new identities. + +Cons: + +- intentionally breaks Weslaw consumers; +- removes the current Rust Holmes implementation foundation; +- requires regeneration of extension-generation artifacts. + +## Decision + +Choose Option B. Weslaw is removed without a deprecation shim. Historical +packets remain available with supersession notices. Generic generation +provenance is retained through exact v2 contracts. + +## GitHub Slice Plan + +| Slice | GitHub issue | Required proof | +| -------------- | --------------------------------------------------------- | ----------------------------------------------- | +| Remove Weslaw | [#768](https://github.com/flyingrobots/wesley/issues/768) | CLI, crate, schema, fixture, and preflight proof | + +## Tests To Write First + +Behavior tests required: + +- top-level help does not advertise `init-law` or `law`; +- removed Weslaw commands and `--law` fail as unsupported CLI input; +- extension-generation v2 canonical bytes have no semantic-law field; +- v2 deserialization rejects a `law` field rather than ignoring it; +- generic extension provenance still verifies exact source and output bytes; +- Rust emission still produces structural types and operation bindings. + +Documentation or process tests: + +- the native workflow no longer watches deleted Weslaw fixtures; +- generated schema tests select the v2 extension-generation artifacts. + +## Proof Matrix + +| Claim | Required proof | +| ----------------------------------------- | --------------------------------------------------- | +| Weslaw CLI is gone | `crates/wesley-cli/tests/cli.rs` | +| Law IR is gone from the public core | Rust workspace compilation and residual audit | +| Generic provenance survives independently | `crates/wesley-core/tests/extension_generation.rs` | +| Weslaw validators are gone | `crates/wesley-emit-rust` tests | +| Current docs are truthful | docs lint and targeted residual audit | +| Repository remains releasable | `cargo xtask preflight` | + +## Acceptance Criteria + +The work is done when: + +- no production crate parses or exports Weslaw or Law IR; +- no supported CLI command or flag accepts Weslaw; +- no emitter generates behavior from Weslaw; +- no current assurance crate or schema names Weslaw artifacts; +- extension-generation v2 preserves generic exact provenance without Law IR; +- historical packets are visibly superseded and are the only intentional + Weslaw documentation references; +- `CHANGELOG.md` records the breaking removal; +- focused tests and `cargo xtask preflight` pass. + +## Validation Plan + +```bash +cargo test -p wesley-core +cargo test -p wesley-cli +cargo test -p wesley-emit-rust +cargo xtask preflight +rg -n -i 'weslaw|LawIr|LawEntry|LawKind|law_ir' \ + crates schemas test .github README.md docs +``` + +The residual search must be reviewed manually so superseded historical design +evidence is not confused with supported product surfaces. + +## Playback / Witness + +A reviewer can inspect the CLI help and v2 fixtures, then run the commands in +the validation plan. The v2 input fixture must carry Shape IR, operations, +owner declarations, settings, and projection roles without any Weslaw field. + +## Open Questions + +None. Semantic ownership is explicit: + +- Edict owns executable language semantics. +- Echo and other runtimes own their capability semantics. +- target modules own target-specific generation declarations. +- Wesley owns GraphQL structural compilation and generic generation evidence. + +## Follow-On Issues + +Future justification-graph assurance implementation remains governed by packet +`0022`. It must start from domain-free evidence rather than the removed Weslaw +artifact family. + +## Retrospective + +The removal confirmed that Weslaw was a semantic subsystem rather than a small +sidecar format. Deleting it required one coordinated break across the public +core API, CLI, Rust emitter, assurance foundation, schemas, fixtures, release +version sources, workflow triggers, and current documentation. + +The useful generic seam survived cleanly. Extension generation is now v2 and +binds canonical Shape IR, normalized operations, owner declarations, settings, +projection roles, generator identity, exact sources, and exact outputs without +embedding a semantic-language document. Its canonical fixtures and independent +verification tests remain green. + +The initial RED proved the retired commands and option were still accepted. +The completed GREEN proves they are rejected, the removed paths are absent, +the historical packets are visibly superseded, and the full repository +preflight passes: + +```text +cargo test -p wesley-core -p wesley-cli -p wesley-emit-rust +cargo xtask preflight +git diff --check +``` + +No compatibility parser or alias remains. The retained JavaScript Holmes +package is a separate evidence/reporting surface and contains no Weslaw +implementation. diff --git a/docs/design/README.md b/docs/design/README.md index 2e990525..4090b65a 100644 --- a/docs/design/README.md +++ b/docs/design/README.md @@ -55,24 +55,25 @@ Current packets: ending with the [final closeout](./0017-rust-native-front-door-and-node-retirement/FINAL_CLOSEOUT.md) - [`0018`](./0018-holmes-assurance-hexagon/holmes-assurance-hexagon.md): - Holmes assurance hexagon redesign with CLI, API, MCP, and reporting adapters, - now backed by the initial `crates/wesley-holmes` Rust foundation + historical Holmes assurance hexagon redesign with CLI, API, MCP, and + reporting adapters - [`0019`](./0019-weslaw-semantic-law-ir/weslaw-semantic-law-ir.md): `weslaw` - semantic Law IR for contract bundles, strict binding, canonical law hashes, - semantic diffs, and deferred SDL+ syntax, including the v1 + semantic Law IR experiment, superseded by + [`0023`](./0023-remove-weslaw/SOURCE_remove-weslaw.md), including the retired v1 [Law IR](./0019-weslaw-semantic-law-ir/LAW_IR_V1.md), [coordinate and registry](./0019-weslaw-semantic-law-ir/COORDINATES_AND_REGISTRIES.md), and [canonicalization and diagnostic](./0019-weslaw-semantic-law-ir/CANONICALIZATION_AND_DIAGNOSTICS.md) substrate notes - [`0020`](./0020-holmes-weslaw-assurance-prd-test-plan/holmes-weslaw-assurance-prd-test-plan.md): - completed Holmes `weslaw` assurance PRD and test-plan campaign. Rust Holmes - implementation work is tracked by GitHub Issues and goalpost milestones, not - packet-local status docs + historical Holmes `weslaw` assurance campaign, superseded by + [`0023`](./0023-remove-weslaw/SOURCE_remove-weslaw.md) - [`0021`](./0021-continuum-yolo-runtime-neutral-edict-sha-lock-assurance/continuum-yolo-runtime-neutral-edict-sha-lock-assurance.md): extracted Continuum lawful-autonomous lane and runtime-neutral Edict packet; the canonical specs now live in [flyingrobots/edict](https://github.com/flyingrobots/edict) +- [`0023`](./0023-remove-weslaw/SOURCE_remove-weslaw.md): removes the retired + Weslaw language, its Rust assurance foundation, and its product surfaces - [Module Contract](./wesley-module-contract.md): Generic core boundary versus external module-owned domain surfaces - [Module Capability Contract](./wesley-module-capability-contract.md): The capability surfaces external modules should implement - [Contract / Artifact / Runtime Boundary](./wesley-contract-family-artifact-runtime-value.md): GraphQL-authored families, Wesley-emitted artifacts, and later runtime values diff --git a/docs/design/TEMPLATE.md b/docs/design/TEMPLATE.md index e29f9dc2..44b6d20c 100644 --- a/docs/design/TEMPLATE.md +++ b/docs/design/TEMPLATE.md @@ -108,7 +108,7 @@ workaround. Good: -- "`holmes weslaw assess` exits 0 when an expired suppression overrides a +- "`holmes assess` exits 0 when an expired suppression overrides a non-overridable gate, silently passing a required check." Bad: @@ -131,14 +131,14 @@ Non-goals prevent the design from silently expanding while the PR is in flight. ## Compiler / CLI Contract -Required for new or changed CLI commands, emitter outputs, `weslaw` artifact -formats, or Wesley crate APIs. +Required for new or changed CLI commands, emitter outputs, artifact formats, +or Wesley crate APIs. Name the software contract and include only relevant subsections: - exported Rust types, traits, or enums - CLI subcommands, flags, and exit codes -- artifact schema fields (`weslaw/v1`, `wesley.law-diff/v1`, etc.) +- artifact schema fields and versioned envelopes - emitted metadata fields - state transitions or validation gate outcomes - error behavior and diagnostic codes @@ -165,8 +165,8 @@ flows. ## Security / Trust Boundary -Required for module WASM execution, `weslaw` or contract bundle ingestion, -filesystem or network surfaces, or user-provided content. +Required for module WASM execution, artifact ingestion, filesystem or network +surfaces, or user-provided content. Describe: @@ -325,8 +325,8 @@ Describe what a reviewer can run or inspect. Examples: ```bash -cargo test -p wesley-holmes -- law_assurance -wesley law validate --bundle fixtures/clean-bundle.json --json +cargo test -p wesley-core -- extension_generation +wesley schema validate --schema fixtures/schema.graphql --json ``` If there is a structured JSON output, include the expected shape or a fixture diff --git a/docs/governance/RELEASE_POLICY.md b/docs/governance/RELEASE_POLICY.md index e669c491..324635e3 100644 --- a/docs/governance/RELEASE_POLICY.md +++ b/docs/governance/RELEASE_POLICY.md @@ -101,9 +101,8 @@ artifacts. All release version sources declared in `.continuum/release.yml` must declare the same version as the release tag. Today that means every published crate -`Cargo.toml` manifest, the unpublished `crates/wesley-holmes/Cargo.toml` -manifest, and the private root `package.json`. Workspace members are not -permitted to drift independently. +`Cargo.toml` manifest and the private root `package.json`. Workspace members +are not permitted to drift independently. ### Check 6: Changelog diff --git a/docs/reference/assurance-capability-matrix.md b/docs/reference/assurance-capability-matrix.md index ac6305ce..8563c32a 100644 --- a/docs/reference/assurance-capability-matrix.md +++ b/docs/reference/assurance-capability-matrix.md @@ -6,9 +6,8 @@ This page separates shipped Wesley assurance surfaces from transitional, foundation, and concept-only surfaces. Wesley emits deterministic evidence inputs: schema hashes, metadata sidecars, -law hashes, bundle manifests, coverage reports, and machine-readable -diagnostics. Assurance tooling is experimental unless a command or workflow is -listed here as shipped. +generation provenance, and machine-readable diagnostics. Assurance tooling is +experimental unless a command or workflow is listed here as shipped. ## Status States @@ -26,14 +25,9 @@ listed here as shipped. | Schema lowering and schema hash | Shipped native CLI | `wesley schema lower`, `wesley schema hash` | Deterministic compiler view of GraphQL structure and its hash. | Runtime behavior, database correctness, deployment safety. | | Operation catalog and selection facts | Shipped native CLI | `wesley schema operations`, `wesley operation selections` | Root operation facts and selected response paths from GraphQL input. | Application authorization, runtime footprints, live query execution. | | Directive argument extraction | Shipped native CLI | `wesley operation directive-args` | Generic directive payload data from operation documents. | The meaning or safety of those directive payloads. | -| `weslaw/v1` validation and hashes | Shipped native CLI | `wesley law validate`, `wesley law lint`, `wesley law rebind` | Law document structure, schema binding, hashes, and diagnostics. | That a downstream runtime obeys the law. | -| `weslaw/v1` diff and explanation | Shipped native CLI | `wesley law diff`, `wesley law explain` | Semantic law deltas and subject-oriented explanations. | Product policy approval or deployment readiness. | -| Law capability and coverage reports | Shipped native CLI | `wesley law capabilities`, `wesley law coverage` | Report-only footprint summaries and profile/category coverage facts. | Runtime enforcement, admission control, or authorization guarantees. | -| Emitter metadata sidecars | Shipped native CLI | `wesley emit ... --metadata-out ` | Schema/law hashes, generator identity, and emission mode metadata. | That generated source compiles in every downstream project. | +| Emitter metadata sidecars | Shipped native CLI | `wesley emit ... --metadata-out ` | Schema hash, generator identity, and emission mode metadata. | That generated source compiles in every downstream project. | | HOLMES PR reports | Shipped JS/transitional | `packages/wesley-holmes/`, `.github/workflows/wesley-holmes.yml` | Pull-request evidence reporting from retained JavaScript tooling. | Native Rust Holmes CLI parity or broad external hosting guarantees. | | SHIPME certificate workflow | Shipped JS/transitional | `.github/workflows/cert-shipme.yml` | Post-merge certificate artifacts for matching path-filtered `main` pushes. | That every landed `main` SHA receives a SHIPME artifact. | -| Rust Holmes law-assurance foundation | Internal foundation | `crates/wesley-holmes/` Rust APIs and tests | Artifact-family version envelopes, evidence bundle models, gates, diagnostics, and deterministic ports. | A public `wesley holmes ...` command. | -| Rust Holmes MVP CLI | Concept/design only | Not shipped | Deferred candidate: validate contract bundle and law evidence files, then emit JSON diagnostics. | GitHub integration, policy approval, or release certification. | | Watson verification | Shipped JS/transitional | `holmes verify`, `.github/workflows/wesley-holmes.yml` | Independent verification reports for Holmes evidence artifacts and citations. | Native Rust Watson CLI parity, runtime enforcement, or broad assurance guarantees. | | Moriarty prediction | Shipped JS/transitional | `moriarty`, `holmes predict`, `.github/workflows/wesley-holmes.yml` | Advisory readiness and trend forecasts from retained JavaScript tooling. | Native Rust Moriarty CLI parity, policy approval, or release certification. | | BLADE | Concept/design only | Not shipped as a repo-local command or workflow | Design vocabulary for release-readiness certification. | A native release-certification command in this repository. | @@ -50,19 +44,6 @@ Avoid broad claims that Wesley provides cryptographic assurance by itself. The compiler provides deterministic inputs. Assurance commands and workflows prove bounded properties named by their command, policy, artifact, or workflow. -## Rust Holmes MVP Status - -The Rust Holmes MVP command is explicitly deferred. The likely first native -command should stay narrow: - -```text -validate a contract bundle manifest and law evidence bundle from files, -emit JSON diagnostics, and avoid GitHub integration -``` - -Until that command exists with examples, tests, and release evidence, Rust -Holmes remains an internal foundation rather than a shipped user-facing CLI. - ## End-To-End Evidence Examples Schema evidence: @@ -78,10 +59,3 @@ Emitter metadata: emission mode metadata. - Does not prove: the generated artifact was integrated, compiled, or deployed by a downstream project. - -Law coverage: - -- Command: `wesley law coverage --schema schema.graphql --law contract.weslaw --profile release --json` -- Proves: the authored law bundle has the reported category/profile coverage - against the active schema. -- Does not prove: a runtime enforced those laws. diff --git a/docs/reference/cli.md b/docs/reference/cli.md index f5b16df5..bcfaaff8 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -28,7 +28,6 @@ Commands: | --------------------------- | ------------------------------------------------------------ | | `normalize-sdl` | Print the Rust-core normalized SDL view | | `doctor` | Run Rust-native health checks | -| `init-law` | Scaffold `weslaw/v1` from known SDL law directives | | `config validate` | Validate a Wesley project manifest | | `config inspect` | Print resolved manifest schema paths and targets | | `config changed-schemas` | Select schema sets affected by changed files | @@ -37,13 +36,6 @@ Commands: | `schema hash` | Print the Wesley L1 registry hash for GraphQL SDL | | `schema operations` | List Query/Mutation/Subscription root operations | | `schema diff` | Compare GraphQL SDL states as Wesley L1 IR | -| `law validate` | Validate `weslaw` against active GraphQL SDL | -| `law lint` | Validate `weslaw` structure without schema binding | -| `law diff` | Compare `weslaw` semantic Law IR states | -| `law explain` | Explain active laws bound to one subject | -| `law rebind` | Re-anchor `weslaw` to an active schema hash | -| `law capabilities` | Emit report-only footprint capability summaries | -| `law coverage` | Report profile/category-aware law coverage | | `emit rust` | Emit Rust models and operation bindings from GraphQL SDL | | `emit typescript` | Emit TypeScript declarations and operation bindings from SDL | | `emit le-binary-typescript` | Emit TypeScript LE-binary codecs from GraphQL SDL | @@ -82,23 +74,6 @@ Options: | `--json` | Emit JSON output | | `--format text\|json` | Output format | -## Init Law - -```text -wesley init-law --schema --family [--out ] -``` - -`init-law` scaffolds `weslaw/v1` from formally known SDL law directives and -description-derived suggestions. Draft suggestions are not active law. - -Options: - -| Option | Meaning | -| ----------------------- | ----------------------------------------- | -| `-s`, `--schema ` | GraphQL SDL file | -| `--family ` | Contract family id for the generated law | -| `--out ` | Optional output path; stdout when omitted | - ## Config ```text @@ -169,39 +144,13 @@ Options: `schema lower`, `schema hash`, and `schema operations` may omit `--schema` only when the discovered project manifest contains exactly one schema path. -## Law - -```text -wesley law lint --law [--json] -wesley law validate --schema --law [--json] -wesley law diff --old --new [--schema ] [--format markdown|json|summary] -wesley law explain --law [--json] -wesley law rebind --schema --law [--accept --out ] [--json] -wesley law capabilities --law [--json] -wesley law coverage --schema --law [--profile release|ci-release|local] [--json] -``` - -Options: - -| Option | Meaning | -| ----------------------- | -------------------------------------------- | -| `-s`, `--schema ` | GraphQL SDL file used to validate new law | -| `--law ` | `weslaw/v1` authoring file | -| `--old ` | Old/base `weslaw/v1` authoring file for diff | -| `--new ` | New/target `weslaw/v1` authoring file | -| `--accept` | Write an explicitly accepted rebind output | -| `--out ` | Rebind output path | -| `--profile ` | Coverage profile, default: `release` | -| `--json` | Emit JSON output | -| `--format ` | Output format: `markdown`, `json`, `summary` | - ## Emit ```text -wesley emit rust --schema --out [--law ] [--metadata-out ] -wesley emit typescript --schema --out [--law ] [--metadata-out ] -wesley emit le-binary-typescript --schema --out [--law ] [--metadata-out ] [--codec-import ] -wesley emit le-binary-rust --schema --out [--law ] [--metadata-out ] [--codec-import ] +wesley emit rust --schema --out [--metadata-out ] +wesley emit typescript --schema --out [--metadata-out ] +wesley emit le-binary-typescript --schema --out [--metadata-out ] [--codec-import ] +wesley emit le-binary-rust --schema --out [--metadata-out ] [--codec-import ] ``` Emit commands write model declarations and root operation bindings when the @@ -212,7 +161,6 @@ Options: | Option | Meaning | | ----------------------- | ------------------------------------------------------- | | `-s`, `--schema ` | GraphQL SDL file | -| `--law ` | Optional `weslaw/v1` file for bundle hashes | | `--out ` | Output file | | `--metadata-out ` | Deterministic metadata JSON sidecar | | `--codec-import ` | Writer/Reader/CodecError module specifier for LE-binary | diff --git a/docs/reference/extension-generation.md b/docs/reference/extension-generation.md index 0a2ebb7a..31d0fb40 100644 --- a/docs/reference/extension-generation.md +++ b/docs/reference/extension-generation.md @@ -2,7 +2,7 @@ -Use this reference when a Rust crate outside Wesley needs canonical Shape/Law +Use this reference when a Rust crate outside Wesley needs canonical structural facts for deterministic code or metadata generation without invoking the `wesley` CLI. @@ -18,17 +18,17 @@ The public contract lives in `wesley-core`: | Rust API | Role | | -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | -| `ExtensionGenerationInputV1` | Canonical Shape IR, normalized root operations, optional bound Law IR, owner declarations, settings digest, and requested roles. | +| `ExtensionGenerationInputV2` | Canonical Shape IR, normalized root operations, owner declarations, settings digest, and requested roles. | | `GenerationArtifactReferenceV1` | Owner-defined coordinate plus the exact SHA-256 digest of referenced bytes. | | `GeneratorIdentityV1` | Generator coordinate, release version, and exact executable or component digest. | -| `GenerationProvenanceManifestV1` | Binding from generator and canonical input to exact source and emitted artifacts. | -| `GenerationReviewV1` | Derived deterministic JSON for review; `authoritative` is always `false`. | +| `GenerationProvenanceManifestV2` | Binding from generator and canonical input to exact source and emitted artifacts. | +| `GenerationReviewV2` | Derived deterministic JSON for review; `authoritative` is always `false`. | The corresponding JSON Schemas are: -- `schemas/wesley-extension-generation-input-v1.schema.json` -- `schemas/wesley-generation-provenance-manifest-v1.schema.json` -- `schemas/wesley-generation-review-v1.schema.json` +- `schemas/wesley-extension-generation-input-v2.schema.json` +- `schemas/wesley-generation-provenance-manifest-v2.schema.json` +- `schemas/wesley-generation-review-v2.schema.json` ## Data Flow @@ -37,12 +37,11 @@ are supplied again to provenance verification rather than rediscovered. ```text canonical Wesley Shape IR + normalized operations - + optional validated Law IR + owner artifact references + settings digest + requested roles | v - ExtensionGenerationInputV1 + ExtensionGenerationInputV2 | external owner generator | @@ -50,7 +49,7 @@ canonical Wesley Shape IR + normalized operations emitted artifacts + exact digests | v - GenerationProvenanceManifestV1 + GenerationProvenanceManifestV2 | recompute every exact byte | @@ -59,10 +58,9 @@ canonical Wesley Shape IR + normalized operations ## Canonicalization And Identity -`ExtensionGenerationInputV1::new` strips `WesleyIR.metadata`, including source +`ExtensionGenerationInputV2::new` strips `WesleyIR.metadata`, including source paths and generation timestamps. It preserves Wesley's established canonical L1 -ordering, normalizes the operation catalog, removes Law IR authoring paths and -prose, normalizes Law IR set-like values, and sorts owner references and +ordering, normalizes the operation catalog, and sorts owner references and projection roles. Conflicting digests for one coordinate fail with `WESLEY_GENERATION_COORDINATE_DIGEST_CONFLICT`. @@ -74,7 +72,7 @@ missing, unexpected, or mismatched material with structured error kinds. ## Non-Authority Rules -- `GenerationReviewV1` is derived from canonical input and provenance. Its +- `GenerationReviewV2` is derived from canonical input and provenance. Its schema requires `authoritative: false`. - A decoded or schema-valid target artifact is not thereby semantically valid. The external owner remains responsible for its output schema and semantic diff --git a/docs/topics/README.md b/docs/topics/README.md index da032055..b95952cd 100644 --- a/docs/topics/README.md +++ b/docs/topics/README.md @@ -19,7 +19,6 @@ short path to the authoritative surface. | Inspect GraphQL lowering, hashes, or IR. | [Schema And IR](./schema-ir.md) | `docs/reference/cli.md#schema` | | Work with operations and directive args. | [Operations](./operations.md) | `docs/reference/cli.md#operation` | | Use or classify GraphQL directives. | [Directives](./directives.md) | `docs/reference/directives.md` | -| Author or validate `weslaw/v1`. | [Weslaw](./weslaw.md) | `docs/design/0019-weslaw-semantic-law-ir/` | | Emit Rust, TypeScript, or LE-binary code. | [Emitters](./emitters.md) | `docs/reference/cli.md#emit` | | Understand generated files and caches. | [Artifacts And Cache](./artifacts-and-cache.md) | `docs/build-artifacts.md` | diff --git a/docs/topics/assurance-evidence.md b/docs/topics/assurance-evidence.md index d46538a7..fc171c8b 100644 --- a/docs/topics/assurance-evidence.md +++ b/docs/topics/assurance-evidence.md @@ -9,16 +9,14 @@ Assurance tooling judges explicit evidence. It does not replace the compiler, and it does not create product semantics for GraphQL. Use the [Assurance Capability Matrix](../reference/assurance-capability-matrix.md) -to distinguish shipped native CLI capabilities, transitional JavaScript tooling, -Rust foundation code, and concept/design-only vocabulary. +to distinguish shipped native CLI capabilities, transitional JavaScript +tooling, and concept/design-only vocabulary. ## Current Surfaces | Surface | State | Use For | | ------------------------------------- | ----------------------- | ----------------------------------------------------------------------------------------------- | -| Native `wesley law ...` commands | Shipped native CLI | Law validation, diffing, explanation, capability, and coverage facts. | | `packages/wesley-holmes/` | Shipped JS/transitional | Retained JavaScript assurance reporting tools. | -| `crates/wesley-holmes/` | Internal foundation | Rust assurance data models, validation, ports, and diagnostics without public CLI commands yet. | | `docs/holmes-policy/` | Documentation | Policy documentation. | | `docs/templates/holmes-policy/` | Documentation | Policy templates for host contexts. | | `docs/architecture/holmes-*` | Design/reference | Architecture and integration notes. | @@ -41,7 +39,6 @@ Rust foundation code, and concept/design-only vocabulary. ```bash node --test packages/wesley-holmes/test/pr-comment.test.mjs BATS_LIB_PATH=test/vendor bats -t test/ci-workflows.bats -cargo test -p wesley-holmes ``` ## Related Authority diff --git a/docs/topics/native-cli.md b/docs/topics/native-cli.md index d45927b0..3c2e2de9 100644 --- a/docs/topics/native-cli.md +++ b/docs/topics/native-cli.md @@ -39,11 +39,9 @@ validating this checkout before a PR or release. | ----------- | -------------------------------------------------------------- | | `schema` | Lowering, hashing, operation catalogs, and schema diffs. | | `operation` | Resolving operation selections and directive arguments. | -| `law` | `weslaw/v1` lint, validation, diff, explain, rebind, coverage. | | `emit` | Rust, TypeScript, and LE-binary codec projections. | | `config` | Project manifest validation, inspection, and changed schemas. | | `target` | Verify external target descriptors without executing target code. | -| `init-law` | Scaffold a `weslaw/v1` authoring file. | | `normalize-sdl` | Emit canonical normalized GraphQL SDL. | | `doctor` | Rust-native health checks for the compiler spine. | diff --git a/docs/topics/weslaw.md b/docs/topics/weslaw.md deleted file mode 100644 index b9b46806..00000000 --- a/docs/topics/weslaw.md +++ /dev/null @@ -1,70 +0,0 @@ -# Weslaw - - - -Use this topic when working with `weslaw/v1` authoring files or law-derived -compiler evidence. - -`weslaw` lets authors attach structured law metadata to GraphQL structure. -Wesley validates and reports that structure; downstream owners decide how those -laws are used in a domain. - -## Common Tasks - -Scaffold law from known SDL directives: - -```bash -cargo run --bin wesley -- init-law \ - --schema schema.graphql \ - --family example.family \ - --out example.weslaw.yaml -``` - -Lint law shape without binding it to a schema: - -```bash -cargo run --bin wesley -- law lint --law example.weslaw.yaml --json -``` - -Validate law against active SDL: - -```bash -cargo run --bin wesley -- law validate \ - --schema schema.graphql \ - --law example.weslaw.yaml \ - --json -``` - -Compare law states: - -```bash -cargo run --bin wesley -- law diff \ - --old old.weslaw.yaml \ - --new new.weslaw.yaml \ - --schema schema.graphql \ - --format summary -``` - -Report release coverage: - -```bash -cargo run --bin wesley -- law coverage \ - --schema schema.graphql \ - --law example.weslaw.yaml \ - --profile release \ - --json -``` - -## Rules Of Thumb - -- Draft suggestions are not active law. -- Binding law to SDL must use current schema hashes and resolvable subjects. -- Coverage profiles are evidence and release inputs, not product policy. -- Domain-specific enforcement belongs outside Wesley core. - -## Related Authority - -- [CLI Reference](../reference/cli.md#law) -- [Weslaw Semantic Law IR](../design/0019-weslaw-semantic-law-ir/weslaw-semantic-law-ir.md) -- [HOLMES CI](./holmes-ci.md) -- [Compiler Boundary](./compiler-boundary.md) diff --git a/docs/truth-manifest.json b/docs/truth-manifest.json index 95a02ca7..33e78fce 100644 --- a/docs/truth-manifest.json +++ b/docs/truth-manifest.json @@ -236,11 +236,6 @@ "status": "current", "owner": "@flyingrobots" }, - { - "path": "docs/topics/weslaw.md", - "status": "current", - "owner": "@flyingrobots" - }, { "path": "docs/topics/contributing/triage.md", "status": "current", diff --git a/schemas/README.md b/schemas/README.md index b62edc6c..0596df55 100644 --- a/schemas/README.md +++ b/schemas/README.md @@ -4,21 +4,13 @@ This directory hosts machine-readable schemas that underpin Wesley’s generator - `directives.graphql` – The GraphQL SDL that defines Wesley's generic custom directives. Keep this in sync with the core directive registry. Product-specific directive families, including TTD protocol directives, belong in their owning modules. - `ir.schema.json` – JSON Schema describing the Wesley IR representation. -- `weslaw-v1.schema.json` – Versioned, canonical JSON Schema describing the - `weslaw/v1` authoring document shape. -- `wesley-law-ir-v1.schema.json` – Versioned, canonical JSON Schema describing - the normalized `wesley.law-ir/v1` representation. -- `wesley-contract-bundle-manifest-v1.schema.json` – Versioned, canonical JSON - Schema describing the emitted `wesley.contract-bundle-manifest/v1` shape. -- `wesley-law-diff-v1.schema.json` – Versioned, canonical JSON Schema - describing machine-readable `wesley.law-diff/v1` semantic law diff reports. -- `wesley-extension-generation-input-v1.schema.json` – Versioned, canonical - JSON Schema for the domain-neutral semantic input supplied to an external +- `wesley-extension-generation-input-v2.schema.json` – Versioned, canonical + JSON Schema for the domain-neutral structural input supplied to an external Rust generator. -- `wesley-generation-provenance-manifest-v1.schema.json` – Versioned, +- `wesley-generation-provenance-manifest-v2.schema.json` – Versioned, canonical JSON Schema binding one generator invocation to its exact input, sources, settings, contract versions, and outputs. -- `wesley-generation-review-v1.schema.json` – Versioned, canonical JSON Schema +- `wesley-generation-review-v2.schema.json` – Versioned, canonical JSON Schema for the explicitly non-authoritative review projection derived from one generation invocation. - `wesley-target-descriptor-v1.schema.json` – JSON Schema for @@ -38,8 +30,8 @@ This directory hosts machine-readable schemas that underpin Wesley’s generator Wesley supports two JSON Schema draft families in `schemas/`: -- **Draft 2020-12** for newer canonical evidence and `weslaw` artifacts whose - schemas are generated or published as canonical JSON. +- **Draft 2020-12** for newer canonical evidence artifacts whose schemas are + generated or published as canonical JSON. - **Draft-07** for retained runtime, SHIPME, L1 IR, and external-target protocol artifacts that already have stable consumers. @@ -56,16 +48,9 @@ Run `cargo test -p wesley-core --test generated_json_artifacts` to validate the representative generated JSON artifact families against the schemas in this directory. The full `cargo xtask preflight` gate runs those tests through `cargo test --workspace`, and the Rust product CI workflow watches `schemas/`, -`test/fixtures/ir-parity/`, and `test/fixtures/weslaw/` for schema/fixture -drift. +`test/fixtures/ir-parity/`, and `test/fixtures/extension-generation/` for +schema/fixture drift. -The `weslaw` and contract bundle schema artifacts are checked in as canonical -JSON: object keys are lexicographically sorted and the files contain no -formatting whitespace. That byte form is for deterministic publication and -review only; semantic law hashes must still be computed from normalized Law IR, -not schema-file bytes. - -The `weslaw/v1` authoring schema accepts explicitly marked draft scaffolding -for review queues, including future draft law shapes. The normalized Law IR -schema is self-contained, rejects draft entries, and discriminates each active -entry `kind` against the matching normalized `body` shape. +Canonical evidence schemas are checked in with lexicographically sorted object +keys and no formatting whitespace. That byte form supports deterministic +publication and review. diff --git a/schemas/weslaw-v1.schema.json b/schemas/weslaw-v1.schema.json deleted file mode 100644 index 3bbffbaa..00000000 --- a/schemas/weslaw-v1.schema.json +++ /dev/null @@ -1 +0,0 @@ -{"$defs":{"activeLawStatus":{"const":"active"},"channelCompatibility":{"additionalProperties":false,"properties":{"semverCoupled":{"type":"boolean"},"versioning":{"minLength":1,"type":"string"}},"required":["versioning","semverCoupled"],"type":"object"},"channelLaw":{"additionalProperties":false,"properties":{"compatibility":{"$ref":"#/$defs/channelCompatibility"},"id":{"$ref":"#/$defs/lawId"},"kind":{"const":"channelLaw"},"messages":{"items":{"$ref":"#/$defs/channelMessage"},"type":"array"},"ordered":{"type":"boolean"},"rationale":{"$ref":"#/$defs/rationale"},"status":{"$ref":"#/$defs/activeLawStatus"},"subject":{"$ref":"#/$defs/subject"},"tags":{"$ref":"#/$defs/tags"},"version":{"minimum":0,"type":"integer"}},"required":["id","kind","subject","ordered","version"],"type":"object"},"channelMessage":{"additionalProperties":false,"properties":{"field":{"minLength":1,"type":"string"},"type":{"minLength":1,"type":"string"}},"required":["field","type"],"type":"object"},"channelRegistryEntry":{"additionalProperties":false,"properties":{"carrier":{"minLength":1,"type":"string"},"name":{"minLength":1,"type":"string"},"version":{"minimum":0,"type":"integer"}},"required":["name","version","carrier"],"type":"object"},"createSlot":{"additionalProperties":false,"properties":{"cardinality":{"enum":["one","optional","many"]},"kind":{"minLength":1,"type":"string"},"name":{"minLength":1,"type":"string"}},"required":["name","kind"],"type":"object"},"discriminator":{"additionalProperties":false,"properties":{"enum":{"minLength":1,"type":"string"},"field":{"minLength":1,"type":"string"}},"required":["field","enum"],"type":"object"},"draftLaw":{"additionalProperties":true,"properties":{"id":{"$ref":"#/$defs/lawId"},"status":{"const":"draft"}},"required":["id","status"],"type":"object"},"externalPredicate":{"additionalProperties":false,"properties":{"inputContract":{"minLength":1,"type":"string"},"op":{"const":"external"},"ref":{"minLength":1,"type":"string"},"verifier":{"minLength":1,"type":"string"}},"required":["op","verifier","ref"],"type":"object"},"fieldEqualsPredicate":{"additionalProperties":false,"properties":{"field":{"minLength":1,"type":"string"},"op":{"const":"fieldEquals"},"value":true},"required":["op","field","value"],"type":"object"},"footprintClosure":{"additionalProperties":false,"properties":{"argBindings":{"$ref":"#/$defs/stringArray"},"cardinality":{"enum":["one","optional","many"]},"fromSlot":{"minLength":1,"type":"string"},"name":{"minLength":1,"type":"string"},"operator":{"minLength":1,"type":"string"},"reads":{"$ref":"#/$defs/stringArray"}},"required":["name","fromSlot","operator"],"type":"object"},"footprintLaw":{"additionalProperties":false,"properties":{"closures":{"items":{"$ref":"#/$defs/footprintClosure"},"type":"array"},"createSlots":{"items":{"$ref":"#/$defs/createSlot"},"type":"array"},"creates":{"$ref":"#/$defs/stringArray"},"forbids":{"$ref":"#/$defs/stringArray"},"id":{"$ref":"#/$defs/lawId"},"kind":{"const":"footprintLaw"},"rationale":{"$ref":"#/$defs/rationale"},"reads":{"$ref":"#/$defs/stringArray"},"slots":{"items":{"$ref":"#/$defs/footprintSlot"},"type":"array"},"status":{"$ref":"#/$defs/activeLawStatus"},"subject":{"$ref":"#/$defs/subject"},"tags":{"$ref":"#/$defs/tags"},"updates":{"items":{"$ref":"#/$defs/footprintUpdate"},"type":"array"},"writes":{"$ref":"#/$defs/stringArray"}},"required":["id","kind","subject"],"type":"object"},"footprintSlot":{"additionalProperties":false,"properties":{"access":{"$ref":"#/$defs/stringArray"},"bindFromArg":{"minLength":1,"type":"string"},"kind":{"minLength":1,"type":"string"},"name":{"minLength":1,"type":"string"}},"required":["name","kind","bindFromArg"],"type":"object"},"footprintUpdate":{"additionalProperties":false,"properties":{"fields":{"$ref":"#/$defs/stringArray"},"slot":{"minLength":1,"type":"string"}},"required":["slot"],"type":"object"},"invariantLaw":{"additionalProperties":false,"properties":{"id":{"$ref":"#/$defs/lawId"},"kind":{"const":"invariantLaw"},"predicate":{"$ref":"#/$defs/predicate"},"rationale":{"$ref":"#/$defs/rationale"},"status":{"$ref":"#/$defs/activeLawStatus"},"subject":{"$ref":"#/$defs/subject"},"tags":{"$ref":"#/$defs/tags"}},"required":["id","kind","subject","predicate"],"type":"object"},"law":{"oneOf":[{"$ref":"#/$defs/scalarSemanticsLaw"},{"$ref":"#/$defs/variantLaw"},{"$ref":"#/$defs/footprintLaw"},{"$ref":"#/$defs/channelLaw"},{"$ref":"#/$defs/invariantLaw"},{"$ref":"#/$defs/draftLaw"}]},"lawId":{"minLength":1,"type":"string"},"lawStatus":{"enum":["active","draft"]},"predicate":{"oneOf":[{"$ref":"#/$defs/fieldEqualsPredicate"},{"$ref":"#/$defs/externalPredicate"}]},"rationale":{"type":"string"},"registries":{"additionalProperties":false,"properties":{"channels":{"items":{"$ref":"#/$defs/channelRegistryEntry"},"type":"array"},"resources":{"items":{"$ref":"#/$defs/resourceRegistryEntry"},"type":"array"},"verifiers":{"items":{"$ref":"#/$defs/verifierRegistryEntry"},"type":"array"}},"type":"object"},"resourceRegistryEntry":{"additionalProperties":false,"properties":{"id":{"minLength":1,"type":"string"},"kind":{"minLength":1,"type":"string"},"notes":{"type":"string"},"owner":{"minLength":1,"type":"string"}},"required":["id","owner","kind"],"type":"object"},"scalarSemantics":{"additionalProperties":false,"properties":{"forbids":{"items":{"enum":["silentGraphQLIntNarrowing"]},"type":"array"},"maxInclusive":{"minimum":0,"type":"integer"},"minInclusive":{"minimum":0,"type":"integer"},"ordering":{"enum":["none","lamport","total","partial"]},"representation":{"enum":["integer","opaqueIdentifier","string"]},"scope":{"type":"string"}},"required":["representation"],"type":"object"},"scalarSemanticsLaw":{"additionalProperties":false,"properties":{"id":{"$ref":"#/$defs/lawId"},"kind":{"const":"scalarSemantics"},"rationale":{"$ref":"#/$defs/rationale"},"semantics":{"$ref":"#/$defs/scalarSemantics"},"status":{"$ref":"#/$defs/activeLawStatus"},"subject":{"$ref":"#/$defs/subject"},"tags":{"$ref":"#/$defs/tags"}},"required":["id","kind","subject","semantics"],"type":"object"},"schemaAnchor":{"additionalProperties":false,"properties":{"family":{"minLength":1,"type":"string"},"hash":{"pattern":"^sha256:[0-9a-f]{64}$","type":"string"},"source":{"minLength":1,"type":"string"}},"required":["family","hash"],"type":"object"},"stringArray":{"items":{"type":"string"},"type":"array"},"subject":{"minLength":1,"type":"string"},"tags":{"$ref":"#/$defs/stringArray"},"variantCase":{"additionalProperties":false,"properties":{"forbids":{"$ref":"#/$defs/stringArray"},"requires":{"$ref":"#/$defs/stringArray"},"value":{"minLength":1,"type":"string"}},"required":["value"],"type":"object"},"variantLaw":{"additionalProperties":false,"properties":{"cases":{"items":{"$ref":"#/$defs/variantCase"},"type":"array"},"discriminator":{"$ref":"#/$defs/discriminator"},"id":{"$ref":"#/$defs/lawId"},"kind":{"const":"variantLaw"},"rationale":{"$ref":"#/$defs/rationale"},"status":{"$ref":"#/$defs/activeLawStatus"},"subject":{"$ref":"#/$defs/subject"},"tags":{"$ref":"#/$defs/tags"}},"required":["id","kind","subject","discriminator","cases"],"type":"object"},"verifierRegistryEntry":{"additionalProperties":false,"properties":{"id":{"minLength":1,"type":"string"},"inputContracts":{"$ref":"#/$defs/stringArray"},"owner":{"minLength":1,"type":"string"}},"required":["id","owner"],"type":"object"}},"$id":"https://wesley.dev/schemas/weslaw-v1.schema.json","$schema":"https://json-schema.org/draft/2020-12/schema","additionalProperties":false,"properties":{"apiVersion":{"const":"weslaw/v1"},"laws":{"items":{"$ref":"#/$defs/law"},"type":"array"},"registries":{"$ref":"#/$defs/registries"},"schema":{"$ref":"#/$defs/schemaAnchor"}},"required":["apiVersion","schema","laws"],"title":"weslaw/v1 Authoring Document","type":"object"} \ No newline at end of file diff --git a/schemas/wesley-contract-bundle-manifest-v1.schema.json b/schemas/wesley-contract-bundle-manifest-v1.schema.json deleted file mode 100644 index 93d8192e..00000000 --- a/schemas/wesley-contract-bundle-manifest-v1.schema.json +++ /dev/null @@ -1 +0,0 @@ -{"$defs":{"sha256":{"pattern":"^sha256:[0-9a-f]{64}$","type":"string"}},"$id":"https://wesley.dev/schemas/wesley-contract-bundle-manifest-v1.schema.json","$schema":"https://json-schema.org/draft/2020-12/schema","additionalProperties":false,"properties":{"apiVersion":{"const":"wesley.contract-bundle-manifest/v1"},"bundleHash":{"$ref":"#/$defs/sha256"},"bundleHashCodec":{"const":"wesley.contract-bundle.hash-input.canonical-json.v1"},"compiler":{"minLength":1,"type":"string"},"compilerVersion":{"minLength":1,"type":"string"},"lawDocumentHash":{"$ref":"#/$defs/sha256"},"lawEntryCount":{"minimum":0,"type":"integer"},"lawHash":{"$ref":"#/$defs/sha256"},"lawIrCodec":{"const":"wesley.law-ir.canonical-json.v1"},"profileHash":{"$ref":"#/$defs/sha256"},"schemaHash":{"$ref":"#/$defs/sha256"}},"required":["apiVersion","schemaHash","lawHash","profileHash","bundleHash","lawIrCodec","bundleHashCodec","compiler","compilerVersion","lawEntryCount"],"title":"wesley.contract-bundle-manifest/v1","type":"object"} \ No newline at end of file diff --git a/schemas/wesley-extension-generation-input-v1.schema.json b/schemas/wesley-extension-generation-input-v1.schema.json deleted file mode 100644 index 64b56582..00000000 --- a/schemas/wesley-extension-generation-input-v1.schema.json +++ /dev/null @@ -1 +0,0 @@ -{"$defs":{"artifactReference":{"additionalProperties":false,"properties":{"coordinate":{"$ref":"#/$defs/token"},"digest":{"$ref":"#/$defs/digest"}},"required":["coordinate","digest"],"type":"object"},"digest":{"pattern":"^sha256:[0-9a-f]{64}$","type":"string"},"directiveMap":{"type":"object"},"field":{"additionalProperties":false,"properties":{"arguments":{"items":{"$ref":"#/$defs/fieldArgument"},"type":"array"},"defaultValue":true,"description":{"type":"string"},"directives":{"$ref":"#/$defs/directiveMap"},"name":{"minLength":1,"type":"string"},"type":{"$ref":"#/$defs/typeReference"}},"required":["name","type","directives"],"type":"object"},"fieldArgument":{"additionalProperties":false,"properties":{"defaultValue":true,"description":{"type":"string"},"directives":{"$ref":"#/$defs/directiveMap"},"name":{"minLength":1,"type":"string"},"type":{"$ref":"#/$defs/typeReference"}},"required":["name","type","directives"],"type":"object"},"law":{"additionalProperties":false,"properties":{"canonicalDigest":{"$ref":"#/$defs/digest"},"lawIr":{"$ref":"wesley-law-ir-v1.schema.json"},"semanticDigest":{"$ref":"#/$defs/digest"}},"required":["lawIr","semanticDigest","canonicalDigest"],"type":"object"},"operation":{"additionalProperties":false,"properties":{"arguments":{"items":{"$ref":"#/$defs/operationArgument"},"type":"array"},"directives":{"$ref":"#/$defs/directiveMap"},"fieldName":{"$ref":"#/$defs/token"},"operationType":{"enum":["QUERY","MUTATION","SUBSCRIPTION"]},"resultType":{"$ref":"#/$defs/typeReference"},"rootTypeName":{"$ref":"#/$defs/token"}},"required":["operationType","rootTypeName","fieldName","arguments","resultType","directives"],"type":"object"},"operationArgument":{"additionalProperties":false,"properties":{"defaultValue":true,"directives":{"$ref":"#/$defs/directiveMap"},"name":{"$ref":"#/$defs/token"},"type":{"$ref":"#/$defs/typeReference"}},"required":["name","type","directives"],"type":"object"},"shapeIr":{"additionalProperties":false,"properties":{"types":{"items":{"$ref":"#/$defs/typeDefinition"},"type":"array"},"version":{"minLength":1,"type":"string"}},"required":["version","types"],"type":"object"},"token":{"minLength":1,"pattern":"^(?!\\s)(?!.*\\s$)[^\\u0000-\\u001f\\u007f-\\u009f]+$","type":"string"},"typeDefinition":{"additionalProperties":false,"properties":{"description":{"type":"string"},"directives":{"$ref":"#/$defs/directiveMap"},"enumValues":{"items":{"type":"string"},"type":"array"},"fields":{"items":{"$ref":"#/$defs/field"},"type":"array"},"implements":{"items":{"type":"string"},"type":"array"},"kind":{"enum":["OBJECT","INTERFACE","UNION","ENUM","SCALAR","INPUT_OBJECT"]},"name":{"minLength":1,"type":"string"},"unionMembers":{"items":{"type":"string"},"type":"array"}},"required":["name","kind","directives"],"type":"object"},"typeReference":{"additionalProperties":false,"properties":{"base":{"minLength":1,"type":"string"},"isList":{"type":"boolean"},"leafNullable":{"type":"boolean"},"listItemNullable":{"type":"boolean"},"listWrappers":{"items":{"additionalProperties":false,"properties":{"nullable":{"type":"boolean"}},"required":["nullable"],"type":"object"},"type":"array"},"nullable":{"type":"boolean"}},"required":["base","nullable","isList"],"type":"object"}},"$id":"https://wesley.dev/schemas/wesley-extension-generation-input-v1.schema.json","$schema":"https://json-schema.org/draft/2020-12/schema","additionalProperties":false,"properties":{"apiVersion":{"const":"wesley.extension-generation-input/v1"},"law":{"$ref":"#/$defs/law"},"operations":{"items":{"$ref":"#/$defs/operation"},"type":"array"},"ownerDeclarations":{"items":{"$ref":"#/$defs/artifactReference"},"type":"array"},"projectionRoles":{"items":{"$ref":"#/$defs/token"},"type":"array","uniqueItems":true},"settingsDigest":{"$ref":"#/$defs/digest"},"shapeDigest":{"$ref":"#/$defs/digest"},"shapeIr":{"$ref":"#/$defs/shapeIr"}},"required":["apiVersion","shapeIr","shapeDigest","operations","ownerDeclarations","settingsDigest","projectionRoles"],"title":"Wesley Extension Generation Input v1","type":"object"} diff --git a/schemas/wesley-extension-generation-input-v2.schema.json b/schemas/wesley-extension-generation-input-v2.schema.json new file mode 100644 index 00000000..5ca134a0 --- /dev/null +++ b/schemas/wesley-extension-generation-input-v2.schema.json @@ -0,0 +1 @@ +{"$defs":{"artifactReference":{"additionalProperties":false,"properties":{"coordinate":{"$ref":"#/$defs/token"},"digest":{"$ref":"#/$defs/digest"}},"required":["coordinate","digest"],"type":"object"},"digest":{"pattern":"^sha256:[0-9a-f]{64}$","type":"string"},"directiveMap":{"type":"object"},"field":{"additionalProperties":false,"properties":{"arguments":{"items":{"$ref":"#/$defs/fieldArgument"},"type":"array"},"defaultValue":true,"description":{"type":"string"},"directives":{"$ref":"#/$defs/directiveMap"},"name":{"minLength":1,"type":"string"},"type":{"$ref":"#/$defs/typeReference"}},"required":["name","type","directives"],"type":"object"},"fieldArgument":{"additionalProperties":false,"properties":{"defaultValue":true,"description":{"type":"string"},"directives":{"$ref":"#/$defs/directiveMap"},"name":{"minLength":1,"type":"string"},"type":{"$ref":"#/$defs/typeReference"}},"required":["name","type","directives"],"type":"object"},"operation":{"additionalProperties":false,"properties":{"arguments":{"items":{"$ref":"#/$defs/operationArgument"},"type":"array"},"directives":{"$ref":"#/$defs/directiveMap"},"fieldName":{"$ref":"#/$defs/token"},"operationType":{"enum":["QUERY","MUTATION","SUBSCRIPTION"]},"resultType":{"$ref":"#/$defs/typeReference"},"rootTypeName":{"$ref":"#/$defs/token"}},"required":["operationType","rootTypeName","fieldName","arguments","resultType","directives"],"type":"object"},"operationArgument":{"additionalProperties":false,"properties":{"defaultValue":true,"directives":{"$ref":"#/$defs/directiveMap"},"name":{"$ref":"#/$defs/token"},"type":{"$ref":"#/$defs/typeReference"}},"required":["name","type","directives"],"type":"object"},"shapeIr":{"additionalProperties":false,"properties":{"types":{"items":{"$ref":"#/$defs/typeDefinition"},"type":"array"},"version":{"minLength":1,"type":"string"}},"required":["version","types"],"type":"object"},"token":{"minLength":1,"pattern":"^(?!\\s)(?!.*\\s$)[^\\u0000-\\u001f\\u007f-\\u009f]+$","type":"string"},"typeDefinition":{"additionalProperties":false,"properties":{"description":{"type":"string"},"directives":{"$ref":"#/$defs/directiveMap"},"enumValues":{"items":{"type":"string"},"type":"array"},"fields":{"items":{"$ref":"#/$defs/field"},"type":"array"},"implements":{"items":{"type":"string"},"type":"array"},"kind":{"enum":["OBJECT","INTERFACE","UNION","ENUM","SCALAR","INPUT_OBJECT"]},"name":{"minLength":1,"type":"string"},"unionMembers":{"items":{"type":"string"},"type":"array"}},"required":["name","kind","directives"],"type":"object"},"typeReference":{"additionalProperties":false,"properties":{"base":{"minLength":1,"type":"string"},"isList":{"type":"boolean"},"leafNullable":{"type":"boolean"},"listItemNullable":{"type":"boolean"},"listWrappers":{"items":{"additionalProperties":false,"properties":{"nullable":{"type":"boolean"}},"required":["nullable"],"type":"object"},"type":"array"},"nullable":{"type":"boolean"}},"required":["base","nullable","isList"],"type":"object"}},"$id":"https://wesley.dev/schemas/wesley-extension-generation-input-v2.schema.json","$schema":"https://json-schema.org/draft/2020-12/schema","additionalProperties":false,"properties":{"apiVersion":{"const":"wesley.extension-generation-input/v2"},"operations":{"items":{"$ref":"#/$defs/operation"},"type":"array"},"ownerDeclarations":{"items":{"$ref":"#/$defs/artifactReference"},"type":"array"},"projectionRoles":{"items":{"$ref":"#/$defs/token"},"type":"array","uniqueItems":true},"settingsDigest":{"$ref":"#/$defs/digest"},"shapeDigest":{"$ref":"#/$defs/digest"},"shapeIr":{"$ref":"#/$defs/shapeIr"}},"required":["apiVersion","shapeIr","shapeDigest","operations","ownerDeclarations","settingsDigest","projectionRoles"],"title":"Wesley Extension Generation Input v2","type":"object"} diff --git a/schemas/wesley-generation-provenance-manifest-v1.schema.json b/schemas/wesley-generation-provenance-manifest-v2.schema.json similarity index 54% rename from schemas/wesley-generation-provenance-manifest-v1.schema.json rename to schemas/wesley-generation-provenance-manifest-v2.schema.json index 4fa848b1..50f7fc04 100644 --- a/schemas/wesley-generation-provenance-manifest-v1.schema.json +++ b/schemas/wesley-generation-provenance-manifest-v2.schema.json @@ -1 +1 @@ -{"$defs":{"artifactReference":{"additionalProperties":false,"properties":{"coordinate":{"$ref":"#/$defs/token"},"digest":{"$ref":"#/$defs/digest"}},"required":["coordinate","digest"],"type":"object"},"digest":{"pattern":"^sha256:[0-9a-f]{64}$","type":"string"},"token":{"minLength":1,"pattern":"^(?!\\s)(?!.*\\s$)[^\\u0000-\\u001f\\u007f-\\u009f]+$","type":"string"},"generator":{"additionalProperties":false,"properties":{"coordinate":{"$ref":"#/$defs/token"},"digest":{"$ref":"#/$defs/digest"},"version":{"$ref":"#/$defs/token"}},"required":["coordinate","version","digest"],"type":"object"}},"$id":"https://wesley.dev/schemas/wesley-generation-provenance-manifest-v1.schema.json","$schema":"https://json-schema.org/draft/2020-12/schema","additionalProperties":false,"properties":{"apiVersion":{"const":"wesley.generation-provenance-manifest/v1"},"contractVersions":{"additionalProperties":false,"properties":{"generatorAbi":{"const":"wesley.extension-generator/v1"},"inputSchema":{"const":"wesley.extension-generation-input/v1"},"provenanceSchema":{"const":"wesley.generation-provenance-manifest/v1"}},"required":["inputSchema","provenanceSchema","generatorAbi"],"type":"object"},"emittedArtifacts":{"items":{"$ref":"#/$defs/artifactReference"},"type":"array"},"generationInputDigest":{"$ref":"#/$defs/digest"},"generator":{"$ref":"#/$defs/generator"},"settingsDigest":{"$ref":"#/$defs/digest"},"sourceArtifacts":{"items":{"$ref":"#/$defs/artifactReference"},"type":"array"}},"required":["apiVersion","generator","generationInputDigest","settingsDigest","contractVersions","sourceArtifacts","emittedArtifacts"],"title":"Wesley Generation Provenance Manifest v1","type":"object"} +{"$defs":{"artifactReference":{"additionalProperties":false,"properties":{"coordinate":{"$ref":"#/$defs/token"},"digest":{"$ref":"#/$defs/digest"}},"required":["coordinate","digest"],"type":"object"},"digest":{"pattern":"^sha256:[0-9a-f]{64}$","type":"string"},"generator":{"additionalProperties":false,"properties":{"coordinate":{"$ref":"#/$defs/token"},"digest":{"$ref":"#/$defs/digest"},"version":{"$ref":"#/$defs/token"}},"required":["coordinate","version","digest"],"type":"object"},"token":{"minLength":1,"pattern":"^(?!\\s)(?!.*\\s$)[^\\u0000-\\u001f\\u007f-\\u009f]+$","type":"string"}},"$id":"https://wesley.dev/schemas/wesley-generation-provenance-manifest-v2.schema.json","$schema":"https://json-schema.org/draft/2020-12/schema","additionalProperties":false,"properties":{"apiVersion":{"const":"wesley.generation-provenance-manifest/v2"},"contractVersions":{"additionalProperties":false,"properties":{"generatorAbi":{"const":"wesley.extension-generator/v2"},"inputSchema":{"const":"wesley.extension-generation-input/v2"},"provenanceSchema":{"const":"wesley.generation-provenance-manifest/v2"}},"required":["inputSchema","provenanceSchema","generatorAbi"],"type":"object"},"emittedArtifacts":{"items":{"$ref":"#/$defs/artifactReference"},"type":"array"},"generationInputDigest":{"$ref":"#/$defs/digest"},"generator":{"$ref":"#/$defs/generator"},"settingsDigest":{"$ref":"#/$defs/digest"},"sourceArtifacts":{"items":{"$ref":"#/$defs/artifactReference"},"type":"array"}},"required":["apiVersion","generator","generationInputDigest","settingsDigest","contractVersions","sourceArtifacts","emittedArtifacts"],"title":"Wesley Generation Provenance Manifest v2","type":"object"} diff --git a/schemas/wesley-generation-review-v1.schema.json b/schemas/wesley-generation-review-v2.schema.json similarity index 61% rename from schemas/wesley-generation-review-v1.schema.json rename to schemas/wesley-generation-review-v2.schema.json index 6d7a39aa..8488c264 100644 --- a/schemas/wesley-generation-review-v1.schema.json +++ b/schemas/wesley-generation-review-v2.schema.json @@ -1 +1 @@ -{"$defs":{"artifactReference":{"additionalProperties":false,"properties":{"coordinate":{"$ref":"#/$defs/token"},"digest":{"$ref":"#/$defs/digest"}},"required":["coordinate","digest"],"type":"object"},"digest":{"pattern":"^sha256:[0-9a-f]{64}$","type":"string"},"token":{"minLength":1,"pattern":"^(?!\\s)(?!.*\\s$)[^\\u0000-\\u001f\\u007f-\\u009f]+$","type":"string"},"generator":{"additionalProperties":false,"properties":{"coordinate":{"$ref":"#/$defs/token"},"digest":{"$ref":"#/$defs/digest"},"version":{"$ref":"#/$defs/token"}},"required":["coordinate","version","digest"],"type":"object"}},"$id":"https://wesley.dev/schemas/wesley-generation-review-v1.schema.json","$schema":"https://json-schema.org/draft/2020-12/schema","additionalProperties":false,"properties":{"apiVersion":{"const":"wesley.generation-review/v1"},"authoritative":{"const":false},"emittedArtifacts":{"items":{"$ref":"#/$defs/artifactReference"},"type":"array"},"generationInputDigest":{"$ref":"#/$defs/digest"},"generator":{"$ref":"#/$defs/generator"},"projectionRoles":{"items":{"$ref":"#/$defs/token"},"type":"array","uniqueItems":true},"provenanceManifestDigest":{"$ref":"#/$defs/digest"},"sourceArtifacts":{"items":{"$ref":"#/$defs/artifactReference"},"type":"array"}},"required":["apiVersion","authoritative","generationInputDigest","provenanceManifestDigest","generator","projectionRoles","sourceArtifacts","emittedArtifacts"],"title":"Wesley Generation Review v1","type":"object"} +{"$defs":{"artifactReference":{"additionalProperties":false,"properties":{"coordinate":{"$ref":"#/$defs/token"},"digest":{"$ref":"#/$defs/digest"}},"required":["coordinate","digest"],"type":"object"},"digest":{"pattern":"^sha256:[0-9a-f]{64}$","type":"string"},"generator":{"additionalProperties":false,"properties":{"coordinate":{"$ref":"#/$defs/token"},"digest":{"$ref":"#/$defs/digest"},"version":{"$ref":"#/$defs/token"}},"required":["coordinate","version","digest"],"type":"object"},"token":{"minLength":1,"pattern":"^(?!\\s)(?!.*\\s$)[^\\u0000-\\u001f\\u007f-\\u009f]+$","type":"string"}},"$id":"https://wesley.dev/schemas/wesley-generation-review-v2.schema.json","$schema":"https://json-schema.org/draft/2020-12/schema","additionalProperties":false,"properties":{"apiVersion":{"const":"wesley.generation-review/v2"},"authoritative":{"const":false},"emittedArtifacts":{"items":{"$ref":"#/$defs/artifactReference"},"type":"array"},"generationInputDigest":{"$ref":"#/$defs/digest"},"generator":{"$ref":"#/$defs/generator"},"projectionRoles":{"items":{"$ref":"#/$defs/token"},"type":"array","uniqueItems":true},"provenanceManifestDigest":{"$ref":"#/$defs/digest"},"sourceArtifacts":{"items":{"$ref":"#/$defs/artifactReference"},"type":"array"}},"required":["apiVersion","authoritative","generationInputDigest","provenanceManifestDigest","generator","projectionRoles","sourceArtifacts","emittedArtifacts"],"title":"Wesley Generation Review v2","type":"object"} diff --git a/schemas/wesley-law-diff-v1.schema.json b/schemas/wesley-law-diff-v1.schema.json deleted file mode 100644 index e8effe90..00000000 --- a/schemas/wesley-law-diff-v1.schema.json +++ /dev/null @@ -1 +0,0 @@ -{"$defs":{"diffValue":true,"fieldChange":{"additionalProperties":false,"properties":{"new":{"$ref":"#/$defs/diffValue"},"old":{"$ref":"#/$defs/diffValue"},"path":{"minLength":1,"type":"string"}},"required":["path","old","new"],"type":"object"},"lawDiffEvent":{"additionalProperties":false,"properties":{"addedCreates":{"$ref":"#/$defs/stringArray"},"addedForbids":{"$ref":"#/$defs/stringArray"},"addedReads":{"$ref":"#/$defs/stringArray"},"addedWrites":{"$ref":"#/$defs/stringArray"},"fieldChanges":{"items":{"$ref":"#/$defs/fieldChange"},"type":"array"},"kind":{"enum":["BINDING_BROKEN","CHANNEL_LAW_CHANGED","CHANNEL_VERSION_CHANGED","FOOTPRINT_CHANGED","FOOTPRINT_CONTRACTED","FOOTPRINT_EXPANDED","LAW_ADDED","LAW_BUNDLE_CHANGED","LAW_CHANGED","LAW_REMOVED","LAW_STRENGTHENED","LAW_TAGS_CHANGED","LAW_WEAKENED","PREDICATE_CHANGED","REGISTRY_CHANGED","SCALAR_SEMANTICS_CHANGED","SCHEMA_HASH_REBOUND","VARIANT_LAW_CHANGED"]},"lawId":{"minLength":1,"type":"string"},"lawKind":{"enum":["scalarSemantics","variantLaw","footprintLaw","channelLaw","invariantLaw"]},"removedCreates":{"$ref":"#/$defs/stringArray"},"removedForbids":{"$ref":"#/$defs/stringArray"},"removedReads":{"$ref":"#/$defs/stringArray"},"removedWrites":{"$ref":"#/$defs/stringArray"},"reviewPosture":{"const":"requires-review"},"subject":{"minLength":1,"type":"string"}},"required":["kind","reviewPosture"],"type":"object"},"sha256":{"pattern":"^sha256:[0-9a-f]{64}$","type":"string"},"stringArray":{"items":{"type":"string"},"type":"array"}},"$id":"https://wesley.dev/schemas/wesley-law-diff-v1.schema.json","$schema":"https://json-schema.org/draft/2020-12/schema","additionalProperties":false,"properties":{"apiVersion":{"const":"wesley.law-diff/v1"},"changes":{"items":{"$ref":"#/$defs/lawDiffEvent"},"type":"array"},"newLawHash":{"$ref":"#/$defs/sha256"},"newSchemaHash":{"$ref":"#/$defs/sha256"},"oldLawHash":{"$ref":"#/$defs/sha256"},"oldSchemaHash":{"$ref":"#/$defs/sha256"}},"required":["apiVersion","oldSchemaHash","newSchemaHash","oldLawHash","newLawHash","changes"],"title":"wesley.law-diff/v1","type":"object"} \ No newline at end of file diff --git a/schemas/wesley-law-ir-v1.schema.json b/schemas/wesley-law-ir-v1.schema.json deleted file mode 100644 index c81366b4..00000000 --- a/schemas/wesley-law-ir-v1.schema.json +++ /dev/null @@ -1 +0,0 @@ -{"$defs":{"channelCompatibility":{"additionalProperties":false,"properties":{"semverCoupled":{"type":"boolean"},"versioning":{"minLength":1,"type":"string"}},"required":["versioning","semverCoupled"],"type":"object"},"channelLawBody":{"additionalProperties":false,"properties":{"compatibility":{"$ref":"#/$defs/channelCompatibility"},"messages":{"items":{"$ref":"#/$defs/channelMessage"},"type":"array"},"ordered":{"type":"boolean"},"version":{"minimum":0,"type":"integer"}},"required":["ordered","version","messages"],"type":"object"},"channelLawEntry":{"additionalProperties":false,"properties":{"body":{"$ref":"#/$defs/channelLawBody"},"id":{"$ref":"#/$defs/lawId"},"kind":{"const":"channelLaw"},"rationale":{"$ref":"#/$defs/rationale"},"status":{"$ref":"#/$defs/lawStatus"},"subject":{"$ref":"#/$defs/subject"},"tags":{"$ref":"#/$defs/tags"}},"required":["id","status","kind","subject","tags","body"],"type":"object"},"channelMessage":{"additionalProperties":false,"properties":{"field":{"minLength":1,"type":"string"},"type":{"minLength":1,"type":"string"}},"required":["field","type"],"type":"object"},"channelRegistryEntry":{"additionalProperties":false,"properties":{"carrier":{"minLength":1,"type":"string"},"name":{"minLength":1,"type":"string"},"version":{"minimum":0,"type":"integer"}},"required":["name","version","carrier"],"type":"object"},"createSlot":{"additionalProperties":false,"properties":{"cardinality":{"enum":["one","optional","many"]},"kind":{"minLength":1,"type":"string"},"name":{"minLength":1,"type":"string"}},"required":["name","kind"],"type":"object"},"discriminator":{"additionalProperties":false,"properties":{"enum":{"minLength":1,"type":"string"},"field":{"minLength":1,"type":"string"}},"required":["field","enum"],"type":"object"},"externalPredicate":{"additionalProperties":false,"properties":{"inputContract":{"minLength":1,"type":"string"},"op":{"const":"external"},"ref":{"minLength":1,"type":"string"},"verifier":{"minLength":1,"type":"string"}},"required":["op","verifier","ref"],"type":"object"},"fieldEqualsPredicate":{"additionalProperties":false,"properties":{"field":{"minLength":1,"type":"string"},"op":{"const":"fieldEquals"},"value":true},"required":["op","field","value"],"type":"object"},"footprintClosure":{"additionalProperties":false,"properties":{"argBindings":{"items":{"type":"string"},"type":"array"},"cardinality":{"enum":["one","optional","many"]},"fromSlot":{"minLength":1,"type":"string"},"name":{"minLength":1,"type":"string"},"operator":{"minLength":1,"type":"string"},"reads":{"items":{"type":"string"},"type":"array"}},"required":["name","fromSlot","operator","argBindings","reads","cardinality"],"type":"object"},"footprintLawBody":{"additionalProperties":false,"properties":{"closures":{"items":{"$ref":"#/$defs/footprintClosure"},"type":"array"},"createSlots":{"items":{"$ref":"#/$defs/createSlot"},"type":"array"},"creates":{"$ref":"#/$defs/stringArray"},"forbids":{"$ref":"#/$defs/stringArray"},"reads":{"$ref":"#/$defs/stringArray"},"slots":{"items":{"$ref":"#/$defs/footprintSlot"},"type":"array"},"updates":{"items":{"$ref":"#/$defs/footprintUpdate"},"type":"array"},"writes":{"$ref":"#/$defs/stringArray"}},"required":["reads","writes","creates","forbids","slots","closures","createSlots","updates"],"type":"object"},"footprintLawEntry":{"additionalProperties":false,"properties":{"body":{"$ref":"#/$defs/footprintLawBody"},"id":{"$ref":"#/$defs/lawId"},"kind":{"const":"footprintLaw"},"rationale":{"$ref":"#/$defs/rationale"},"status":{"$ref":"#/$defs/lawStatus"},"subject":{"$ref":"#/$defs/subject"},"tags":{"$ref":"#/$defs/tags"}},"required":["id","status","kind","subject","tags","body"],"type":"object"},"footprintSlot":{"additionalProperties":false,"properties":{"access":{"items":{"type":"string"},"type":"array"},"bindFromArg":{"minLength":1,"type":"string"},"kind":{"minLength":1,"type":"string"},"name":{"minLength":1,"type":"string"}},"required":["name","kind","bindFromArg","access"],"type":"object"},"footprintUpdate":{"additionalProperties":false,"properties":{"fields":{"items":{"type":"string"},"type":"array"},"slot":{"minLength":1,"type":"string"}},"required":["slot","fields"],"type":"object"},"invariantLawBody":{"additionalProperties":false,"properties":{"predicate":{"$ref":"#/$defs/predicate"}},"required":["predicate"],"type":"object"},"invariantLawEntry":{"additionalProperties":false,"properties":{"body":{"$ref":"#/$defs/invariantLawBody"},"id":{"$ref":"#/$defs/lawId"},"kind":{"const":"invariantLaw"},"rationale":{"$ref":"#/$defs/rationale"},"status":{"$ref":"#/$defs/lawStatus"},"subject":{"$ref":"#/$defs/subject"},"tags":{"$ref":"#/$defs/tags"}},"required":["id","status","kind","subject","tags","body"],"type":"object"},"lawEntry":{"oneOf":[{"$ref":"#/$defs/scalarSemanticsEntry"},{"$ref":"#/$defs/variantLawEntry"},{"$ref":"#/$defs/footprintLawEntry"},{"$ref":"#/$defs/channelLawEntry"},{"$ref":"#/$defs/invariantLawEntry"}]},"lawId":{"minLength":1,"type":"string"},"lawStatus":{"const":"active"},"predicate":{"oneOf":[{"$ref":"#/$defs/fieldEqualsPredicate"},{"$ref":"#/$defs/externalPredicate"}]},"rationale":{"type":"string"},"registrySet":{"additionalProperties":false,"properties":{"channels":{"items":{"$ref":"#/$defs/channelRegistryEntry"},"type":"array"},"resources":{"items":{"$ref":"#/$defs/resourceRegistryEntry"},"type":"array"},"verifiers":{"items":{"$ref":"#/$defs/verifierRegistryEntry"},"type":"array"}},"required":["resources","verifiers","channels"],"type":"object"},"resourceRegistryEntry":{"additionalProperties":false,"properties":{"id":{"minLength":1,"type":"string"},"kind":{"minLength":1,"type":"string"},"notes":{"type":"string"},"owner":{"minLength":1,"type":"string"}},"required":["id","owner","kind"],"type":"object"},"scalarSemanticsBody":{"additionalProperties":false,"properties":{"forbids":{"items":{"enum":["silentGraphQLIntNarrowing"]},"type":"array"},"maxInclusive":{"minimum":0,"type":"integer"},"minInclusive":{"minimum":0,"type":"integer"},"ordering":{"enum":["none","lamport","total","partial"]},"representation":{"enum":["integer","opaqueIdentifier","string"]},"scope":{"type":"string"}},"required":["representation","forbids"],"type":"object"},"scalarSemanticsEntry":{"additionalProperties":false,"properties":{"body":{"$ref":"#/$defs/scalarSemanticsBody"},"id":{"$ref":"#/$defs/lawId"},"kind":{"const":"scalarSemantics"},"rationale":{"$ref":"#/$defs/rationale"},"status":{"$ref":"#/$defs/lawStatus"},"subject":{"$ref":"#/$defs/subject"},"tags":{"$ref":"#/$defs/tags"}},"required":["id","status","kind","subject","tags","body"],"type":"object"},"stringArray":{"items":{"type":"string"},"type":"array"},"subject":{"minLength":1,"type":"string"},"tags":{"$ref":"#/$defs/stringArray"},"variantCaseBody":{"additionalProperties":false,"properties":{"forbids":{"items":{"type":"string"},"type":"array"},"requires":{"items":{"type":"string"},"type":"array"},"value":{"minLength":1,"type":"string"}},"required":["value","requires","forbids"],"type":"object"},"variantLawBody":{"additionalProperties":false,"properties":{"cases":{"items":{"$ref":"#/$defs/variantCaseBody"},"type":"array"},"discriminator":{"$ref":"#/$defs/discriminator"}},"required":["discriminator","cases"],"type":"object"},"variantLawEntry":{"additionalProperties":false,"properties":{"body":{"$ref":"#/$defs/variantLawBody"},"id":{"$ref":"#/$defs/lawId"},"kind":{"const":"variantLaw"},"rationale":{"$ref":"#/$defs/rationale"},"status":{"$ref":"#/$defs/lawStatus"},"subject":{"$ref":"#/$defs/subject"},"tags":{"$ref":"#/$defs/tags"}},"required":["id","status","kind","subject","tags","body"],"type":"object"},"verifierRegistryEntry":{"additionalProperties":false,"properties":{"id":{"minLength":1,"type":"string"},"inputContracts":{"$ref":"#/$defs/stringArray"},"owner":{"minLength":1,"type":"string"}},"required":["id","owner"],"type":"object"}},"$id":"https://wesley.dev/schemas/wesley-law-ir-v1.schema.json","$schema":"https://json-schema.org/draft/2020-12/schema","additionalProperties":false,"properties":{"apiVersion":{"const":"wesley.law-ir/v1"},"entries":{"items":{"$ref":"#/$defs/lawEntry"},"type":"array"},"family":{"minLength":1,"type":"string"},"registries":{"$ref":"#/$defs/registrySet"},"schemaHash":{"pattern":"^sha256:[0-9a-f]{64}$","type":"string"},"schemaSource":{"minLength":1,"type":"string"}},"required":["apiVersion","family","schemaHash","registries","entries"],"title":"wesley.law-ir/v1 Canonical Law IR","type":"object"} \ No newline at end of file diff --git a/test/ci-workflows.bats b/test/ci-workflows.bats index 72c20264..419a39f4 100644 --- a/test/ci-workflows.bats +++ b/test/ci-workflows.bats @@ -52,10 +52,6 @@ load 'vendor/bats-plugins/bats-assert/load' assert_success [ "$output" -eq 2 ] - run bash -lc "grep -F \"'test/fixtures/weslaw/**'\" .github/workflows/rust-native.yml | wc -l" - assert_success - [ "$output" -eq 2 ] - run bash -lc "grep -F \"'test/fixtures/extension-generation/**'\" .github/workflows/rust-native.yml | wc -l" assert_success [ "$output" -eq 2 ] diff --git a/test/fixtures/extension-generation/input.json b/test/fixtures/extension-generation/input.json index f7372833..10e04453 100644 --- a/test/fixtures/extension-generation/input.json +++ b/test/fixtures/extension-generation/input.json @@ -1 +1 @@ -{"apiVersion":"wesley.extension-generation-input/v1","operations":[{"arguments":[{"directives":{},"name":"id","type":{"base":"ID","isList":false,"nullable":false}}],"directives":{},"fieldName":"projection","operationType":"QUERY","resultType":{"base":"Projection","isList":false,"nullable":false},"rootTypeName":"Query"}],"ownerDeclarations":[{"coordinate":"fixture:semantic-source@1","digest":"sha256:90b878cbd69d5eb0c1673e4ed9652da70d97ff0a6ffbe689a4ec82241a5927ac"}],"projectionRoles":["generated-profile"],"settingsDigest":"sha256:e4a9ccba950218de669ddbf657479be7f173cd1806fddf8df434b1c4b402ed7c","shapeDigest":"sha256:88f778a53dfca78fe602b9e0b174c020d844700deb346d410b4fda29e374374f","shapeIr":{"types":[{"directives":{},"fields":[{"directives":{},"name":"coordinate","type":{"base":"ID","isList":false,"nullable":false}},{"directives":{},"name":"digest","type":{"base":"String","isList":false,"nullable":false}}],"kind":"OBJECT","name":"Projection"},{"directives":{},"fields":[{"arguments":[{"directives":{},"name":"id","type":{"base":"ID","isList":false,"nullable":false}}],"directives":{},"name":"projection","type":{"base":"Projection","isList":false,"nullable":false}}],"kind":"OBJECT","name":"Query"}],"version":"1.0.0"}} +{"apiVersion":"wesley.extension-generation-input/v2","operations":[{"arguments":[{"directives":{},"name":"id","type":{"base":"ID","isList":false,"nullable":false}}],"directives":{},"fieldName":"projection","operationType":"QUERY","resultType":{"base":"Projection","isList":false,"nullable":false},"rootTypeName":"Query"}],"ownerDeclarations":[{"coordinate":"fixture:semantic-source@1","digest":"sha256:90b878cbd69d5eb0c1673e4ed9652da70d97ff0a6ffbe689a4ec82241a5927ac"}],"projectionRoles":["generated-profile"],"settingsDigest":"sha256:e4a9ccba950218de669ddbf657479be7f173cd1806fddf8df434b1c4b402ed7c","shapeDigest":"sha256:88f778a53dfca78fe602b9e0b174c020d844700deb346d410b4fda29e374374f","shapeIr":{"types":[{"directives":{},"fields":[{"directives":{},"name":"coordinate","type":{"base":"ID","isList":false,"nullable":false}},{"directives":{},"name":"digest","type":{"base":"String","isList":false,"nullable":false}}],"kind":"OBJECT","name":"Projection"},{"directives":{},"fields":[{"arguments":[{"directives":{},"name":"id","type":{"base":"ID","isList":false,"nullable":false}}],"directives":{},"name":"projection","type":{"base":"Projection","isList":false,"nullable":false}}],"kind":"OBJECT","name":"Query"}],"version":"1.0.0"}} diff --git a/test/fixtures/extension-generation/provenance.json b/test/fixtures/extension-generation/provenance.json index 6d39835c..670314e1 100644 --- a/test/fixtures/extension-generation/provenance.json +++ b/test/fixtures/extension-generation/provenance.json @@ -1 +1 @@ -{"apiVersion":"wesley.generation-provenance-manifest/v1","contractVersions":{"generatorAbi":"wesley.extension-generator/v1","inputSchema":"wesley.extension-generation-input/v1","provenanceSchema":"wesley.generation-provenance-manifest/v1"},"emittedArtifacts":[{"coordinate":"fixture:generated-profile@1","digest":"sha256:702f5bb0c6d4fbeba4d44b1eab6317a827a915c6e28398ae7742a229592c7899"}],"generationInputDigest":"sha256:41a870386b59c9bb67658e2a30d964f7a5788eb507a787105d53e1d895c8c4f6","generator":{"coordinate":"fixture:semantic-generator@1","digest":"sha256:ed1c605dc5769e8bf31d0e4cf62e4b590b481d6e4b980c1360a246f43a0343f1","version":"1.0.0"},"settingsDigest":"sha256:e4a9ccba950218de669ddbf657479be7f173cd1806fddf8df434b1c4b402ed7c","sourceArtifacts":[{"coordinate":"fixture:semantic-source@1","digest":"sha256:90b878cbd69d5eb0c1673e4ed9652da70d97ff0a6ffbe689a4ec82241a5927ac"}]} +{"apiVersion":"wesley.generation-provenance-manifest/v2","contractVersions":{"generatorAbi":"wesley.extension-generator/v2","inputSchema":"wesley.extension-generation-input/v2","provenanceSchema":"wesley.generation-provenance-manifest/v2"},"emittedArtifacts":[{"coordinate":"fixture:generated-profile@1","digest":"sha256:702f5bb0c6d4fbeba4d44b1eab6317a827a915c6e28398ae7742a229592c7899"}],"generationInputDigest":"sha256:17eb58becf030f316d16bf943952cd735a461b21beda58b3641e05c6f60efa8c","generator":{"coordinate":"fixture:semantic-generator@1","digest":"sha256:ed1c605dc5769e8bf31d0e4cf62e4b590b481d6e4b980c1360a246f43a0343f1","version":"1.0.0"},"settingsDigest":"sha256:e4a9ccba950218de669ddbf657479be7f173cd1806fddf8df434b1c4b402ed7c","sourceArtifacts":[{"coordinate":"fixture:semantic-source@1","digest":"sha256:90b878cbd69d5eb0c1673e4ed9652da70d97ff0a6ffbe689a4ec82241a5927ac"}]} diff --git a/test/fixtures/extension-generation/review.json b/test/fixtures/extension-generation/review.json index d38feb6c..74535f52 100644 --- a/test/fixtures/extension-generation/review.json +++ b/test/fixtures/extension-generation/review.json @@ -1 +1 @@ -{"apiVersion":"wesley.generation-review/v1","authoritative":false,"emittedArtifacts":[{"coordinate":"fixture:generated-profile@1","digest":"sha256:702f5bb0c6d4fbeba4d44b1eab6317a827a915c6e28398ae7742a229592c7899"}],"generationInputDigest":"sha256:41a870386b59c9bb67658e2a30d964f7a5788eb507a787105d53e1d895c8c4f6","generator":{"coordinate":"fixture:semantic-generator@1","digest":"sha256:ed1c605dc5769e8bf31d0e4cf62e4b590b481d6e4b980c1360a246f43a0343f1","version":"1.0.0"},"projectionRoles":["generated-profile"],"provenanceManifestDigest":"sha256:c5d42078690ac227b8e03f644007de6d4134942257ce95c12acf6602dbb1d1ab","sourceArtifacts":[{"coordinate":"fixture:semantic-source@1","digest":"sha256:90b878cbd69d5eb0c1673e4ed9652da70d97ff0a6ffbe689a4ec82241a5927ac"}]} +{"apiVersion":"wesley.generation-review/v2","authoritative":false,"emittedArtifacts":[{"coordinate":"fixture:generated-profile@1","digest":"sha256:702f5bb0c6d4fbeba4d44b1eab6317a827a915c6e28398ae7742a229592c7899"}],"generationInputDigest":"sha256:17eb58becf030f316d16bf943952cd735a461b21beda58b3641e05c6f60efa8c","generator":{"coordinate":"fixture:semantic-generator@1","digest":"sha256:ed1c605dc5769e8bf31d0e4cf62e4b590b481d6e4b980c1360a246f43a0343f1","version":"1.0.0"},"projectionRoles":["generated-profile"],"provenanceManifestDigest":"sha256:957ee08ab8d27a052bc7f0b537ae4b8eb32ebe14696c42fd2b6427363ba00adc","sourceArtifacts":[{"coordinate":"fixture:semantic-source@1","digest":"sha256:90b878cbd69d5eb0c1673e4ed9652da70d97ff0a6ffbe689a4ec82241a5927ac"}]} diff --git a/test/fixtures/weslaw/README.md b/test/fixtures/weslaw/README.md deleted file mode 100644 index 51611800..00000000 --- a/test/fixtures/weslaw/README.md +++ /dev/null @@ -1,51 +0,0 @@ -# weslaw Fixture Corpus - -This corpus supports design packet -`docs/design/0019-weslaw-semantic-law-ir/`. - -The files define the first v1 substrate target for `WLAW-008` and `WLAW-009`, -then serve as Rust parser and published-schema fixtures for `WLAW-011` through -`WLAW-020`, strict binding fixtures for `WLAW-021` through `WLAW-035`, and -canonical law hash fixtures for `WLAW-036` through `WLAW-045`. The `diff/` -fixtures pin the public semantic diff command output for `WLAW-053` through -`WLAW-059`. The directive-equivalence fixture supports `WLAW-060` and -`WLAW-061` by proving known formal SDL directives and authored YAML lower into -the same canonical Law IR. The Rust-validator payoff fixture supports -`WLAW-070` and `WLAW-071`. - -## Files - -| Path | Purpose | -| --- | --- | -| `contract-bundle-shape.graphql` | Minimal GraphQL shape used by accepted and rejected law fixtures. | -| `accepted/*.weslaw.yaml` | Law documents that `wesley law validate` must accept. | -| `accepted/channel-ttd-protocol-from-directive.weslaw.yaml` | YAML equivalent for the schema's `@wes_channel` directive-lowered law. | -| `accepted/rust-validator-payoff.weslaw.yaml` | Combined scalar and variant law fixture for Rust helper generation. | -| `rejected/*.weslaw.yaml` | Law documents that validation must reject. | -| `rejected/*.expected.txt` | Stable diagnostic code expected for each rejected fixture. | -| `diff/*.weslaw.yaml` | Old/new law documents used by semantic diff fixtures. | -| `diff/ci-semantic-diff.json` | CI-ready `wesley.law-diff/v1` output. | -| `diff/ci-semantic-diff.md` | PR-ready Markdown generated from structured diff events. | -| `diff/holmes-blade-binding-broken.json` | Holmes/BLADE-facing binding-break report. | - -The schema hash anchors use the native Wesley `schema hash` output for -`contract-bundle-shape.graphql`. - -```text -ee681e8c2c99acb5db74f09b2eb06cca2e9379fc7d69627d3287cba6177ac4b6 -``` - -## Fixture Policy - -- Fixtures are compiler fixtures, not product ownership claims. -- Product names such as Echo, jedit, Continuum, and warp-ttd appear only to - preserve the real semantic pressure that motivated `weslaw`. -- External repos remain the owners of their runtime and protocol meaning. -- Accepted fixtures must lower into typed Law IR and satisfy - `schemas/weslaw-v1.schema.json`. -- Rejected fixtures pin stable structure and binding diagnostic codes for the - strict schema-bound validation pass. -- Accepted fixtures also feed canonical Law IR hash, contract bundle manifest, - and generated-artifact provenance tests. -- Diff fixtures must remain generated from `wesley law diff` and satisfy - `schemas/wesley-law-diff-v1.schema.json` when JSON. diff --git a/test/fixtures/weslaw/accepted/channel-ttd-protocol-from-directive.weslaw.yaml b/test/fixtures/weslaw/accepted/channel-ttd-protocol-from-directive.weslaw.yaml deleted file mode 100644 index 12981f77..00000000 --- a/test/fixtures/weslaw/accepted/channel-ttd-protocol-from-directive.weslaw.yaml +++ /dev/null @@ -1,29 +0,0 @@ -apiVersion: weslaw/v1 -schema: - family: weslaw-fixture-contract-bundle - hash: sha256:ee681e8c2c99acb5db74f09b2eb06cca2e9379fc7d69627d3287cba6177ac4b6 - source: ../contract-bundle-shape.graphql -laws: - - id: directive.wes_channel.ttd.protocol.v4 - status: active - kind: channelLaw - subject: channel:ttd.protocol@4 - ordered: true - version: 4 - messages: - - field: hostHello - type: HostHello - - field: laneCatalog - type: LaneCatalog - - field: playbackHeadSnapshot - type: PlaybackHeadSnapshot - - field: playbackFrame - type: PlaybackFrame - - field: receiptSummary - type: ReceiptSummary - - field: effectEmissionSummary - type: EffectEmissionSummary - - field: deliveryObservationSummary - type: DeliveryObservationSummary - - field: executionContext - type: ExecutionContext diff --git a/test/fixtures/weslaw/accepted/channel-ttd-protocol.weslaw.yaml b/test/fixtures/weslaw/accepted/channel-ttd-protocol.weslaw.yaml deleted file mode 100644 index 7ade1beb..00000000 --- a/test/fixtures/weslaw/accepted/channel-ttd-protocol.weslaw.yaml +++ /dev/null @@ -1,33 +0,0 @@ -apiVersion: weslaw/v1 -schema: - family: weslaw-fixture-contract-bundle - hash: sha256:ee681e8c2c99acb5db74f09b2eb06cca2e9379fc7d69627d3287cba6177ac4b6 - source: ../contract-bundle-shape.graphql -laws: - - id: warp-ttd.channel.protocol.v4 - status: active - kind: channelLaw - subject: channel:ttd.protocol@4 - tags: [warp-ttd, channel] - ordered: true - version: 4 - compatibility: - versioning: channel - semverCoupled: false - messages: - - field: hostHello - type: HostHello - - field: laneCatalog - type: LaneCatalog - - field: playbackHeadSnapshot - type: PlaybackHeadSnapshot - - field: playbackFrame - type: PlaybackFrame - - field: receiptSummary - type: ReceiptSummary - - field: effectEmissionSummary - type: EffectEmissionSummary - - field: deliveryObservationSummary - type: DeliveryObservationSummary - - field: executionContext - type: ExecutionContext diff --git a/test/fixtures/weslaw/accepted/footprint-replace-range.weslaw.yaml b/test/fixtures/weslaw/accepted/footprint-replace-range.weslaw.yaml deleted file mode 100644 index 9e9e1909..00000000 --- a/test/fixtures/weslaw/accepted/footprint-replace-range.weslaw.yaml +++ /dev/null @@ -1,64 +0,0 @@ -apiVersion: weslaw/v1 -schema: - family: weslaw-fixture-contract-bundle - hash: sha256:ee681e8c2c99acb5db74f09b2eb06cca2e9379fc7d69627d3287cba6177ac4b6 - source: ../contract-bundle-shape.graphql -registries: - resources: - - id: AstState - owner: jedit - kind: forbidden-runtime-domain - - id: Diagnostics - owner: jedit - kind: forbidden-runtime-domain - - id: GitWitness - owner: jedit - kind: forbidden-runtime-domain - - id: UiState - owner: jedit - kind: forbidden-runtime-domain -laws: - - id: jedit.op.replaceRangeAsTick.footprint - status: active - kind: footprintLaw - subject: operation:Mutation.replaceRangeAsTick - tags: [jedit, footprint] - reads: [Anchor, BufferWorldline, RopeBranch, RopeHead, RopeLeaf, TextBlob] - writes: [BufferWorldline] - creates: [RopeBranch, RopeHead, RopeLeaf, TextBlob, Tick, TickReceipt] - forbids: [AstState, Diagnostics, GitWitness, UiState] - slots: - - name: worldline - kind: BufferWorldline - bindFromArg: input.worldlineId - access: [read, write] - - name: baseHead - kind: RopeHead - bindFromArg: input.baseHeadId - access: [read] - closures: - - name: touchedRope - fromSlot: baseHead - operator: ropeRangeClosure - argBindings: [input.startByte, input.endByte] - reads: [RopeBranch, RopeLeaf, TextBlob] - cardinality: many - - name: affectedAnchors - fromSlot: worldline - operator: anchorsIntersectingEditWindow - argBindings: [baseHead, input.startByte, input.endByte] - reads: [Anchor] - cardinality: many - createSlots: - - name: newBlob - kind: TextBlob - cardinality: optional - - name: nextHead - kind: RopeHead - - name: tick - kind: Tick - - name: receipt - kind: TickReceipt - updates: - - slot: worldline - fields: [canonicalHeadId] diff --git a/test/fixtures/weslaw/accepted/invariant-translated-evidence.weslaw.yaml b/test/fixtures/weslaw/accepted/invariant-translated-evidence.weslaw.yaml deleted file mode 100644 index f2debf31..00000000 --- a/test/fixtures/weslaw/accepted/invariant-translated-evidence.weslaw.yaml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: weslaw/v1 -schema: - family: weslaw-fixture-contract-bundle - hash: sha256:ee681e8c2c99acb5db74f09b2eb06cca2e9379fc7d69627d3287cba6177ac4b6 - source: ../contract-bundle-shape.graphql -laws: - - id: continuum.invariant.translated-evidence-not-native - status: active - kind: invariantLaw - subject: type:TranslatedSubstrateEvidence - tags: [continuum, invariant] - predicate: - op: fieldEquals - field: nativeContinuumWitness - value: false diff --git a/test/fixtures/weslaw/accepted/rust-validator-payoff.weslaw.yaml b/test/fixtures/weslaw/accepted/rust-validator-payoff.weslaw.yaml deleted file mode 100644 index e0aa7541..00000000 --- a/test/fixtures/weslaw/accepted/rust-validator-payoff.weslaw.yaml +++ /dev/null @@ -1,36 +0,0 @@ -apiVersion: weslaw/v1 -schema: - family: weslaw-fixture-contract-bundle - hash: sha256:ee681e8c2c99acb5db74f09b2eb06cca2e9379fc7d69627d3287cba6177ac4b6 - source: ../contract-bundle-shape.graphql -laws: - - id: echo.scalar.positiveInt.u32-positive - status: active - kind: scalarSemantics - subject: scalar:PositiveInt - tags: [echo, scalar] - semantics: - representation: integer - minInclusive: 1 - maxInclusive: 4294967295 - forbids: [silentGraphQLIntNarrowing] - rationale: PositiveInt preserves a positive u32 domain across generated artifacts. - - id: echo.variant.playback-mode - status: active - kind: variantLaw - subject: input:PlaybackModeInput - tags: [echo, variant] - discriminator: - field: kind - enum: PlaybackModeKind - cases: - - value: PAUSED - forbids: [target, then] - - value: PLAY - forbids: [target, then] - - value: STEP_FORWARD - forbids: [target, then] - - value: STEP_BACK - forbids: [target, then] - - value: SEEK - requires: [target, then] diff --git a/test/fixtures/weslaw/accepted/scalar-semantics.weslaw.yaml b/test/fixtures/weslaw/accepted/scalar-semantics.weslaw.yaml deleted file mode 100644 index ff60a411..00000000 --- a/test/fixtures/weslaw/accepted/scalar-semantics.weslaw.yaml +++ /dev/null @@ -1,17 +0,0 @@ -apiVersion: weslaw/v1 -schema: - family: weslaw-fixture-contract-bundle - hash: sha256:ee681e8c2c99acb5db74f09b2eb06cca2e9379fc7d69627d3287cba6177ac4b6 - source: ../contract-bundle-shape.graphql -laws: - - id: echo.scalar.positiveInt.u32-positive - status: active - kind: scalarSemantics - subject: scalar:PositiveInt - tags: [echo, scalar] - semantics: - representation: integer - minInclusive: 1 - maxInclusive: 4294967295 - forbids: [silentGraphQLIntNarrowing] - rationale: PositiveInt preserves a positive u32 domain across generated artifacts. diff --git a/test/fixtures/weslaw/accepted/variant-playback-mode.weslaw.yaml b/test/fixtures/weslaw/accepted/variant-playback-mode.weslaw.yaml deleted file mode 100644 index 4ca16695..00000000 --- a/test/fixtures/weslaw/accepted/variant-playback-mode.weslaw.yaml +++ /dev/null @@ -1,25 +0,0 @@ -apiVersion: weslaw/v1 -schema: - family: weslaw-fixture-contract-bundle - hash: sha256:ee681e8c2c99acb5db74f09b2eb06cca2e9379fc7d69627d3287cba6177ac4b6 - source: ../contract-bundle-shape.graphql -laws: - - id: echo.input.playbackMode.variant-rules - status: active - kind: variantLaw - subject: input:PlaybackModeInput - tags: [echo, variant] - discriminator: - field: kind - enum: PlaybackModeKind - cases: - - value: PAUSED - forbids: [target, then] - - value: PLAY - forbids: [target, then] - - value: STEP_FORWARD - forbids: [target, then] - - value: STEP_BACK - forbids: [target, then] - - value: SEEK - requires: [target, then] diff --git a/test/fixtures/weslaw/contract-bundle-shape.graphql b/test/fixtures/weslaw/contract-bundle-shape.graphql deleted file mode 100644 index 6954a0c7..00000000 --- a/test/fixtures/weslaw/contract-bundle-shape.graphql +++ /dev/null @@ -1,150 +0,0 @@ -""" -Minimal schema used by the weslaw v1 substrate fixtures. - -This is not a product schema. It combines small Echo, jedit, Continuum, and -warp-ttd-shaped surfaces so law fixtures can exercise scalar, variant, -footprint, channel, and invariant subjects against one stable hash. -""" - -directive @wes_channel(name: String!, version: Int!, ordered: Boolean!) on OBJECT -directive @wes_op(name: String, readonly: Boolean, idempotent: Boolean) on FIELD_DEFINITION -directive @wes_footprint( - reads: [String!] - writes: [String!] - creates: [String!] - forbids: [String!] -) on FIELD_DEFINITION - -scalar Hash -scalar PositiveInt -scalar WorldlineTick - -enum SeekThen { - PAUSE - PLAY -} - -enum PlaybackModeKind { - PAUSED - PLAY - STEP_FORWARD - STEP_BACK - SEEK -} - -input PlaybackModeInput { - kind: PlaybackModeKind! - target: WorldlineTick - then: SeekThen -} - -type BufferWorldline { - worldlineId: ID! - canonicalHeadId: ID! -} - -type RopeHead { - headId: ID! - worldlineId: ID! -} - -type RopeBranch { - branchId: ID! -} - -type RopeLeaf { - leafId: ID! -} - -type TextBlob { - blobId: ID! -} - -type Anchor { - anchorId: ID! -} - -type Tick { - tickId: ID! -} - -type TickReceipt { - receiptId: ID! -} - -input ReplaceRangeAsTickInput { - worldlineId: ID! - baseHeadId: ID! - startByte: Int! - endByte: Int! - insertText: String! -} - -type ReplaceRangeAsTickResult { - worldline: BufferWorldline! - nextHead: RopeHead! - tick: Tick! - receipt: TickReceipt! -} - -type TranslatedSubstrateEvidence { - evidenceId: ID! - nativeContinuumWitness: Boolean! - evidenceDigest: Hash! -} - -type HostHello { - hostVersion: String! -} - -type LaneCatalog { - laneCount: Int! -} - -type PlaybackHeadSnapshot { - headId: String! -} - -type PlaybackFrame { - frameIndex: Int! -} - -type ReceiptSummary { - receiptId: String! -} - -type EffectEmissionSummary { - emissionId: String! -} - -type DeliveryObservationSummary { - observationId: String! - emissionId: String! -} - -type ExecutionContext { - mode: String! -} - -type TtdProtocolChannel - @wes_channel(name: "ttd.protocol", version: 4, ordered: true) -{ - hostHello: HostHello! - laneCatalog: LaneCatalog! - playbackHeadSnapshot: PlaybackHeadSnapshot! - playbackFrame: PlaybackFrame! - receiptSummary: ReceiptSummary! - effectEmissionSummary: EffectEmissionSummary! - deliveryObservationSummary: DeliveryObservationSummary! - executionContext: ExecutionContext! -} - -type Query { - playbackHead(headId: String!): PlaybackHeadSnapshot! - @wes_op(name: "playbackHead", readonly: true) -} - -type Mutation { - replaceRangeAsTick(input: ReplaceRangeAsTickInput!): ReplaceRangeAsTickResult! - @wes_op(name: "replaceRangeAsTick") -} diff --git a/test/fixtures/weslaw/diff/binding-broken.weslaw.yaml b/test/fixtures/weslaw/diff/binding-broken.weslaw.yaml deleted file mode 100644 index fb2bfef3..00000000 --- a/test/fixtures/weslaw/diff/binding-broken.weslaw.yaml +++ /dev/null @@ -1,12 +0,0 @@ -apiVersion: weslaw/v1 -schema: - family: weslaw-fixture-contract-bundle - hash: sha256:ee681e8c2c99acb5db74f09b2eb06cca2e9379fc7d69627d3287cba6177ac4b6 - source: ../contract-bundle-shape.graphql -laws: - - id: broken.scalar.missing-subject - status: active - kind: scalarSemantics - subject: scalar:MissingScalar - semantics: - representation: integer diff --git a/test/fixtures/weslaw/diff/ci-semantic-diff.json b/test/fixtures/weslaw/diff/ci-semantic-diff.json deleted file mode 100644 index 02540c04..00000000 --- a/test/fixtures/weslaw/diff/ci-semantic-diff.json +++ /dev/null @@ -1,74 +0,0 @@ -{ - "apiVersion": "wesley.law-diff/v1", - "oldSchemaHash": "sha256:ee681e8c2c99acb5db74f09b2eb06cca2e9379fc7d69627d3287cba6177ac4b6", - "newSchemaHash": "sha256:ee681e8c2c99acb5db74f09b2eb06cca2e9379fc7d69627d3287cba6177ac4b6", - "oldLawHash": "sha256:88fcbb7fb07cc0bb5dfa30252ec61badb3a8dff1be71c0f20ae21031e4e80f51", - "newLawHash": "sha256:ba4a878e94a961bbbe68b421aa2829f39e9e464a4d3e4647dc8d4ccb0c55eab7", - "changes": [ - { - "kind": "LAW_WEAKENED", - "lawId": "echo.scalar.positiveInt.u32-positive", - "subject": "scalar:PositiveInt", - "lawKind": "scalarSemantics", - "reviewPosture": "requires-review", - "fieldChanges": [ - { - "path": "body.minInclusive", - "old": 10, - "new": 1 - }, - { - "path": "body.maxInclusive", - "old": 90, - "new": 100 - }, - { - "path": "body.forbids", - "old": [ - "silentGraphQLIntNarrowing" - ], - "new": [] - } - ] - }, - { - "kind": "LAW_WEAKENED", - "lawId": "echo.variant.playback-mode", - "subject": "input:PlaybackModeInput", - "lawKind": "variantLaw", - "reviewPosture": "requires-review", - "fieldChanges": [ - { - "path": "body.cases.PAUSED.forbids", - "old": [ - "target" - ], - "new": [] - }, - { - "path": "body.cases.SEEK.requires", - "old": [ - "target" - ], - "new": [] - } - ] - }, - { - "kind": "FOOTPRINT_EXPANDED", - "lawId": "jedit.op.replaceRangeAsTick.footprint", - "subject": "operation:Mutation.replaceRangeAsTick", - "lawKind": "footprintLaw", - "reviewPosture": "requires-review", - "addedReads": [ - "TextBlob" - ], - "addedCreates": [ - "TickReceipt" - ], - "removedForbids": [ - "Diagnostics" - ] - } - ] -} diff --git a/test/fixtures/weslaw/diff/ci-semantic-diff.md b/test/fixtures/weslaw/diff/ci-semantic-diff.md deleted file mode 100644 index 3d00e6cc..00000000 --- a/test/fixtures/weslaw/diff/ci-semantic-diff.md +++ /dev/null @@ -1,18 +0,0 @@ -# Wesley Law Diff - -| Field | Value | -| --- | --- | -| API version | `wesley.law-diff/v1` | -| Old schema hash | `sha256:ee681e8c2c99acb5db74f09b2eb06cca2e9379fc7d69627d3287cba6177ac4b6` | -| New schema hash | `sha256:ee681e8c2c99acb5db74f09b2eb06cca2e9379fc7d69627d3287cba6177ac4b6` | -| Old law hash | `sha256:88fcbb7fb07cc0bb5dfa30252ec61badb3a8dff1be71c0f20ae21031e4e80f51` | -| New law hash | `sha256:ba4a878e94a961bbbe68b421aa2829f39e9e464a4d3e4647dc8d4ccb0c55eab7` | - -## Changes - -| Kind | Law | Subject | Summary | -| --- | --- | --- | --- | -| `LAW_WEAKENED` | `echo.scalar.positiveInt.u32-positive` | `scalar:PositiveInt` | law weakened: body.minInclusive, body.maxInclusive, body.forbids | -| `LAW_WEAKENED` | `echo.variant.playback-mode` | `input:PlaybackModeInput` | law weakened: body.cases.PAUSED.forbids, body.cases.SEEK.requires | -| `FOOTPRINT_EXPANDED` | `jedit.op.replaceRangeAsTick.footprint` | `operation:Mutation.replaceRangeAsTick` | added reads: TextBlob; added creates: TickReceipt; removed forbids: Diagnostics | - diff --git a/test/fixtures/weslaw/diff/holmes-blade-binding-broken.json b/test/fixtures/weslaw/diff/holmes-blade-binding-broken.json deleted file mode 100644 index eb0f852c..00000000 --- a/test/fixtures/weslaw/diff/holmes-blade-binding-broken.json +++ /dev/null @@ -1,82 +0,0 @@ -{ - "apiVersion": "wesley.law-diff/v1", - "oldSchemaHash": "sha256:ee681e8c2c99acb5db74f09b2eb06cca2e9379fc7d69627d3287cba6177ac4b6", - "newSchemaHash": "sha256:ee681e8c2c99acb5db74f09b2eb06cca2e9379fc7d69627d3287cba6177ac4b6", - "oldLawHash": "sha256:88fcbb7fb07cc0bb5dfa30252ec61badb3a8dff1be71c0f20ae21031e4e80f51", - "newLawHash": "sha256:86217c36a77d1f2ed27ad40f3baa9613e94e1262689b27ca812c403c850fcda3", - "changes": [ - { - "kind": "REGISTRY_CHANGED", - "reviewPosture": "requires-review", - "fieldChanges": [ - { - "path": "registries", - "old": { - "resources": [ - { - "id": "Diagnostics", - "owner": "jedit", - "kind": "forbidden-runtime-domain" - }, - { - "id": "UiState", - "owner": "jedit", - "kind": "forbidden-runtime-domain" - } - ], - "verifiers": [], - "channels": [] - }, - "new": { - "resources": [], - "verifiers": [], - "channels": [] - } - } - ] - }, - { - "kind": "BINDING_BROKEN", - "reviewPosture": "requires-review", - "fieldChanges": [ - { - "path": "binding", - "old": null, - "new": { - "code": "WESLAW_UNRESOLVED_SUBJECT", - "message": "unresolved subject coordinate scalar:MissingScalar for law broken.scalar.missing-subject", - "path": "$.laws[0].subject" - } - } - ] - }, - { - "kind": "LAW_ADDED", - "lawId": "broken.scalar.missing-subject", - "subject": "scalar:MissingScalar", - "lawKind": "scalarSemantics", - "reviewPosture": "requires-review" - }, - { - "kind": "LAW_REMOVED", - "lawId": "echo.scalar.positiveInt.u32-positive", - "subject": "scalar:PositiveInt", - "lawKind": "scalarSemantics", - "reviewPosture": "requires-review" - }, - { - "kind": "LAW_REMOVED", - "lawId": "echo.variant.playback-mode", - "subject": "input:PlaybackModeInput", - "lawKind": "variantLaw", - "reviewPosture": "requires-review" - }, - { - "kind": "LAW_REMOVED", - "lawId": "jedit.op.replaceRangeAsTick.footprint", - "subject": "operation:Mutation.replaceRangeAsTick", - "lawKind": "footprintLaw", - "reviewPosture": "requires-review" - } - ] -} diff --git a/test/fixtures/weslaw/diff/new.weslaw.yaml b/test/fixtures/weslaw/diff/new.weslaw.yaml deleted file mode 100644 index 12982330..00000000 --- a/test/fixtures/weslaw/diff/new.weslaw.yaml +++ /dev/null @@ -1,43 +0,0 @@ -apiVersion: weslaw/v1 -schema: - family: weslaw-fixture-contract-bundle - hash: sha256:ee681e8c2c99acb5db74f09b2eb06cca2e9379fc7d69627d3287cba6177ac4b6 - source: ../contract-bundle-shape.graphql -registries: - resources: - - id: Diagnostics - owner: jedit - kind: forbidden-runtime-domain - - id: UiState - owner: jedit - kind: forbidden-runtime-domain -laws: - - id: echo.scalar.positiveInt.u32-positive - status: active - kind: scalarSemantics - subject: scalar:PositiveInt - tags: [echo, scalar] - semantics: - representation: integer - minInclusive: 1 - maxInclusive: 100 - - id: echo.variant.playback-mode - status: active - kind: variantLaw - subject: input:PlaybackModeInput - tags: [echo, variant] - discriminator: - field: kind - enum: PlaybackModeKind - cases: - - value: PAUSED - - value: SEEK - - id: jedit.op.replaceRangeAsTick.footprint - status: active - kind: footprintLaw - subject: operation:Mutation.replaceRangeAsTick - tags: [jedit, footprint] - reads: [BufferWorldline, TextBlob] - writes: [BufferWorldline] - creates: [Tick, TickReceipt] - forbids: [UiState] diff --git a/test/fixtures/weslaw/diff/old.weslaw.yaml b/test/fixtures/weslaw/diff/old.weslaw.yaml deleted file mode 100644 index aaae5375..00000000 --- a/test/fixtures/weslaw/diff/old.weslaw.yaml +++ /dev/null @@ -1,46 +0,0 @@ -apiVersion: weslaw/v1 -schema: - family: weslaw-fixture-contract-bundle - hash: sha256:ee681e8c2c99acb5db74f09b2eb06cca2e9379fc7d69627d3287cba6177ac4b6 - source: ../contract-bundle-shape.graphql -registries: - resources: - - id: Diagnostics - owner: jedit - kind: forbidden-runtime-domain - - id: UiState - owner: jedit - kind: forbidden-runtime-domain -laws: - - id: echo.scalar.positiveInt.u32-positive - status: active - kind: scalarSemantics - subject: scalar:PositiveInt - tags: [echo, scalar] - semantics: - representation: integer - minInclusive: 10 - maxInclusive: 90 - forbids: [silentGraphQLIntNarrowing] - - id: echo.variant.playback-mode - status: active - kind: variantLaw - subject: input:PlaybackModeInput - tags: [echo, variant] - discriminator: - field: kind - enum: PlaybackModeKind - cases: - - value: PAUSED - forbids: [target] - - value: SEEK - requires: [target] - - id: jedit.op.replaceRangeAsTick.footprint - status: active - kind: footprintLaw - subject: operation:Mutation.replaceRangeAsTick - tags: [jedit, footprint] - reads: [BufferWorldline] - writes: [BufferWorldline] - creates: [Tick] - forbids: [Diagnostics, UiState] diff --git a/test/fixtures/weslaw/rejected/duplicate-id.expected.txt b/test/fixtures/weslaw/rejected/duplicate-id.expected.txt deleted file mode 100644 index e62248ea..00000000 --- a/test/fixtures/weslaw/rejected/duplicate-id.expected.txt +++ /dev/null @@ -1 +0,0 @@ -WESLAW_DUPLICATE_ID diff --git a/test/fixtures/weslaw/rejected/duplicate-id.weslaw.yaml b/test/fixtures/weslaw/rejected/duplicate-id.weslaw.yaml deleted file mode 100644 index 99ec82ab..00000000 --- a/test/fixtures/weslaw/rejected/duplicate-id.weslaw.yaml +++ /dev/null @@ -1,20 +0,0 @@ -apiVersion: weslaw/v1 -schema: - family: weslaw-fixture-contract-bundle - hash: sha256:ee681e8c2c99acb5db74f09b2eb06cca2e9379fc7d69627d3287cba6177ac4b6 - source: ../contract-bundle-shape.graphql -laws: - - id: duplicate.law - status: active - kind: scalarSemantics - subject: scalar:PositiveInt - semantics: - representation: integer - minInclusive: 1 - - id: duplicate.law - status: active - kind: scalarSemantics - subject: scalar:WorldlineTick - semantics: - representation: integer - ordering: lamport diff --git a/test/fixtures/weslaw/rejected/external-extra-predicate-field.expected.txt b/test/fixtures/weslaw/rejected/external-extra-predicate-field.expected.txt deleted file mode 100644 index 59d17311..00000000 --- a/test/fixtures/weslaw/rejected/external-extra-predicate-field.expected.txt +++ /dev/null @@ -1 +0,0 @@ -WESLAW_UNKNOWN_FIELD diff --git a/test/fixtures/weslaw/rejected/external-extra-predicate-field.weslaw.yaml b/test/fixtures/weslaw/rejected/external-extra-predicate-field.weslaw.yaml deleted file mode 100644 index 39e510a9..00000000 --- a/test/fixtures/weslaw/rejected/external-extra-predicate-field.weslaw.yaml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: weslaw/v1 -schema: - family: weslaw-fixture-contract-bundle - hash: sha256:ee681e8c2c99acb5db74f09b2eb06cca2e9379fc7d69627d3287cba6177ac4b6 - source: ../contract-bundle-shape.graphql -laws: - - id: continuum.invariant.external-extra - status: active - kind: invariantLaw - subject: type:TranslatedSubstrateEvidence - predicate: - op: external - verifier: continuum-law-checker - ref: continuum.invariants.translatedEvidenceCannotClaimNativeWitness - field: nativeContinuumWitness diff --git a/test/fixtures/weslaw/rejected/field-equals-extra-predicate-field.expected.txt b/test/fixtures/weslaw/rejected/field-equals-extra-predicate-field.expected.txt deleted file mode 100644 index 59d17311..00000000 --- a/test/fixtures/weslaw/rejected/field-equals-extra-predicate-field.expected.txt +++ /dev/null @@ -1 +0,0 @@ -WESLAW_UNKNOWN_FIELD diff --git a/test/fixtures/weslaw/rejected/field-equals-extra-predicate-field.weslaw.yaml b/test/fixtures/weslaw/rejected/field-equals-extra-predicate-field.weslaw.yaml deleted file mode 100644 index 8aa86354..00000000 --- a/test/fixtures/weslaw/rejected/field-equals-extra-predicate-field.weslaw.yaml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: weslaw/v1 -schema: - family: weslaw-fixture-contract-bundle - hash: sha256:ee681e8c2c99acb5db74f09b2eb06cca2e9379fc7d69627d3287cba6177ac4b6 - source: ../contract-bundle-shape.graphql -laws: - - id: continuum.invariant.field-equals-extra - status: active - kind: invariantLaw - subject: type:TranslatedSubstrateEvidence - predicate: - op: fieldEquals - field: nativeContinuumWitness - value: false - verifier: continuum-law-checker diff --git a/test/fixtures/weslaw/rejected/footprint-unknown-cardinality.expected.txt b/test/fixtures/weslaw/rejected/footprint-unknown-cardinality.expected.txt deleted file mode 100644 index ee671066..00000000 --- a/test/fixtures/weslaw/rejected/footprint-unknown-cardinality.expected.txt +++ /dev/null @@ -1 +0,0 @@ -WESLAW_INVALID_DOCUMENT diff --git a/test/fixtures/weslaw/rejected/footprint-unknown-cardinality.weslaw.yaml b/test/fixtures/weslaw/rejected/footprint-unknown-cardinality.weslaw.yaml deleted file mode 100644 index 872df25b..00000000 --- a/test/fixtures/weslaw/rejected/footprint-unknown-cardinality.weslaw.yaml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: weslaw/v1 -schema: - family: weslaw-fixture-contract-bundle - hash: sha256:ee681e8c2c99acb5db74f09b2eb06cca2e9379fc7d69627d3287cba6177ac4b6 - source: ../contract-bundle-shape.graphql -laws: - - id: jedit.op.replaceRangeAsTick.bad-cardinality - status: active - kind: footprintLaw - subject: operation:Mutation.replaceRangeAsTick - closures: - - name: touchedRope - fromSlot: baseHead - operator: ropeRangeClosure - cardinality: several diff --git a/test/fixtures/weslaw/rejected/raw-expression-invariant.expected.txt b/test/fixtures/weslaw/rejected/raw-expression-invariant.expected.txt deleted file mode 100644 index ef65500e..00000000 --- a/test/fixtures/weslaw/rejected/raw-expression-invariant.expected.txt +++ /dev/null @@ -1 +0,0 @@ -WESLAW_RAW_EXPR_REJECTED diff --git a/test/fixtures/weslaw/rejected/raw-expression-invariant.weslaw.yaml b/test/fixtures/weslaw/rejected/raw-expression-invariant.weslaw.yaml deleted file mode 100644 index f4c06ac4..00000000 --- a/test/fixtures/weslaw/rejected/raw-expression-invariant.weslaw.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: weslaw/v1 -schema: - family: weslaw-fixture-contract-bundle - hash: sha256:ee681e8c2c99acb5db74f09b2eb06cca2e9379fc7d69627d3287cba6177ac4b6 - source: ../contract-bundle-shape.graphql -laws: - - id: continuum.invariant.translated-evidence-not-native - status: active - kind: invariantLaw - subject: type:TranslatedSubstrateEvidence - expr: "forall e in TranslatedSubstrateEvidence: e.nativeContinuumWitness == false" diff --git a/test/fixtures/weslaw/rejected/scalar-forbid-non-integer.expected.txt b/test/fixtures/weslaw/rejected/scalar-forbid-non-integer.expected.txt deleted file mode 100644 index ee671066..00000000 --- a/test/fixtures/weslaw/rejected/scalar-forbid-non-integer.expected.txt +++ /dev/null @@ -1 +0,0 @@ -WESLAW_INVALID_DOCUMENT diff --git a/test/fixtures/weslaw/rejected/scalar-forbid-non-integer.weslaw.yaml b/test/fixtures/weslaw/rejected/scalar-forbid-non-integer.weslaw.yaml deleted file mode 100644 index 0ed12953..00000000 --- a/test/fixtures/weslaw/rejected/scalar-forbid-non-integer.weslaw.yaml +++ /dev/null @@ -1,13 +0,0 @@ -apiVersion: weslaw/v1 -schema: - family: weslaw-fixture-contract-bundle - hash: sha256:ee681e8c2c99acb5db74f09b2eb06cca2e9379fc7d69627d3287cba6177ac4b6 - source: ../contract-bundle-shape.graphql -laws: - - id: echo.scalar.exampleId.invalid-forbid - status: active - kind: scalarSemantics - subject: scalar:PositiveInt - semantics: - representation: opaqueIdentifier - forbids: [silentGraphQLIntNarrowing] diff --git a/test/fixtures/weslaw/rejected/scalar-min-greater-than-max.expected.txt b/test/fixtures/weslaw/rejected/scalar-min-greater-than-max.expected.txt deleted file mode 100644 index ee671066..00000000 --- a/test/fixtures/weslaw/rejected/scalar-min-greater-than-max.expected.txt +++ /dev/null @@ -1 +0,0 @@ -WESLAW_INVALID_DOCUMENT diff --git a/test/fixtures/weslaw/rejected/scalar-min-greater-than-max.weslaw.yaml b/test/fixtures/weslaw/rejected/scalar-min-greater-than-max.weslaw.yaml deleted file mode 100644 index 7cf28116..00000000 --- a/test/fixtures/weslaw/rejected/scalar-min-greater-than-max.weslaw.yaml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: weslaw/v1 -schema: - family: weslaw-fixture-contract-bundle - hash: sha256:ee681e8c2c99acb5db74f09b2eb06cca2e9379fc7d69627d3287cba6177ac4b6 - source: ../contract-bundle-shape.graphql -laws: - - id: echo.scalar.positiveInt.invalid-range - status: active - kind: scalarSemantics - subject: scalar:PositiveInt - semantics: - representation: integer - minInclusive: 10 - maxInclusive: 1 - forbids: [silentGraphQLIntNarrowing] diff --git a/test/fixtures/weslaw/rejected/scalar-range-on-non-integer.expected.txt b/test/fixtures/weslaw/rejected/scalar-range-on-non-integer.expected.txt deleted file mode 100644 index ee671066..00000000 --- a/test/fixtures/weslaw/rejected/scalar-range-on-non-integer.expected.txt +++ /dev/null @@ -1 +0,0 @@ -WESLAW_INVALID_DOCUMENT diff --git a/test/fixtures/weslaw/rejected/scalar-range-on-non-integer.weslaw.yaml b/test/fixtures/weslaw/rejected/scalar-range-on-non-integer.weslaw.yaml deleted file mode 100644 index 9b10831c..00000000 --- a/test/fixtures/weslaw/rejected/scalar-range-on-non-integer.weslaw.yaml +++ /dev/null @@ -1,13 +0,0 @@ -apiVersion: weslaw/v1 -schema: - family: weslaw-fixture-contract-bundle - hash: sha256:ee681e8c2c99acb5db74f09b2eb06cca2e9379fc7d69627d3287cba6177ac4b6 - source: ../contract-bundle-shape.graphql -laws: - - id: echo.scalar.string.range - status: active - kind: scalarSemantics - subject: scalar:PositiveInt - semantics: - representation: string - minInclusive: 1 diff --git a/test/fixtures/weslaw/rejected/scalar-unknown-ordering.expected.txt b/test/fixtures/weslaw/rejected/scalar-unknown-ordering.expected.txt deleted file mode 100644 index ee671066..00000000 --- a/test/fixtures/weslaw/rejected/scalar-unknown-ordering.expected.txt +++ /dev/null @@ -1 +0,0 @@ -WESLAW_INVALID_DOCUMENT diff --git a/test/fixtures/weslaw/rejected/scalar-unknown-ordering.weslaw.yaml b/test/fixtures/weslaw/rejected/scalar-unknown-ordering.weslaw.yaml deleted file mode 100644 index b80b3415..00000000 --- a/test/fixtures/weslaw/rejected/scalar-unknown-ordering.weslaw.yaml +++ /dev/null @@ -1,13 +0,0 @@ -apiVersion: weslaw/v1 -schema: - family: weslaw-fixture-contract-bundle - hash: sha256:ee681e8c2c99acb5db74f09b2eb06cca2e9379fc7d69627d3287cba6177ac4b6 - source: ../contract-bundle-shape.graphql -laws: - - id: echo.scalar.worldlineTick.bad-ordering - status: active - kind: scalarSemantics - subject: scalar:WorldlineTick - semantics: - representation: integer - ordering: lamprot diff --git a/test/fixtures/weslaw/rejected/schema-hash-mismatch.expected.txt b/test/fixtures/weslaw/rejected/schema-hash-mismatch.expected.txt deleted file mode 100644 index f7b323f6..00000000 --- a/test/fixtures/weslaw/rejected/schema-hash-mismatch.expected.txt +++ /dev/null @@ -1 +0,0 @@ -WESLAW_SCHEMA_HASH_MISMATCH diff --git a/test/fixtures/weslaw/rejected/schema-hash-mismatch.weslaw.yaml b/test/fixtures/weslaw/rejected/schema-hash-mismatch.weslaw.yaml deleted file mode 100644 index 358587f0..00000000 --- a/test/fixtures/weslaw/rejected/schema-hash-mismatch.weslaw.yaml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: weslaw/v1 -schema: - family: weslaw-fixture-contract-bundle - hash: sha256:0000000000000000000000000000000000000000000000000000000000000000 - source: ../contract-bundle-shape.graphql -laws: - - id: echo.scalar.positiveInt.u32-positive - status: active - kind: scalarSemantics - subject: scalar:PositiveInt - semantics: - representation: integer - minInclusive: 1 - maxInclusive: 4294967295 - forbids: [silentGraphQLIntNarrowing] diff --git a/test/fixtures/weslaw/rejected/unknown-field.expected.txt b/test/fixtures/weslaw/rejected/unknown-field.expected.txt deleted file mode 100644 index 59d17311..00000000 --- a/test/fixtures/weslaw/rejected/unknown-field.expected.txt +++ /dev/null @@ -1 +0,0 @@ -WESLAW_UNKNOWN_FIELD diff --git a/test/fixtures/weslaw/rejected/unknown-field.weslaw.yaml b/test/fixtures/weslaw/rejected/unknown-field.weslaw.yaml deleted file mode 100644 index 4b062895..00000000 --- a/test/fixtures/weslaw/rejected/unknown-field.weslaw.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: weslaw/v1 -schema: - family: weslaw-fixture-contract-bundle - hash: sha256:ee681e8c2c99acb5db74f09b2eb06cca2e9379fc7d69627d3287cba6177ac4b6 - source: ../contract-bundle-shape.graphql -laws: - - id: echo.scalar.positiveInt.unknown-field - status: active - kind: scalarSemantics - subject: scalar:PositiveInt - semantics: - representation: integer - minInclusive: 1 - surprise: true diff --git a/test/fixtures/weslaw/rejected/unknown-kind.expected.txt b/test/fixtures/weslaw/rejected/unknown-kind.expected.txt deleted file mode 100644 index 060adcff..00000000 --- a/test/fixtures/weslaw/rejected/unknown-kind.expected.txt +++ /dev/null @@ -1 +0,0 @@ -WESLAW_UNKNOWN_KIND diff --git a/test/fixtures/weslaw/rejected/unknown-kind.weslaw.yaml b/test/fixtures/weslaw/rejected/unknown-kind.weslaw.yaml deleted file mode 100644 index 826cf7a7..00000000 --- a/test/fixtures/weslaw/rejected/unknown-kind.weslaw.yaml +++ /dev/null @@ -1,12 +0,0 @@ -apiVersion: weslaw/v1 -schema: - family: weslaw-fixture-contract-bundle - hash: sha256:ee681e8c2c99acb5db74f09b2eb06cca2e9379fc7d69627d3287cba6177ac4b6 - source: ../contract-bundle-shape.graphql -laws: - - id: echo.scalar.positiveInt.magic-law - status: active - kind: magicLaw - subject: scalar:PositiveInt - semantics: - representation: integer diff --git a/test/fixtures/weslaw/rejected/unresolved-subject.expected.txt b/test/fixtures/weslaw/rejected/unresolved-subject.expected.txt deleted file mode 100644 index a56ab836..00000000 --- a/test/fixtures/weslaw/rejected/unresolved-subject.expected.txt +++ /dev/null @@ -1 +0,0 @@ -WESLAW_UNRESOLVED_SUBJECT diff --git a/test/fixtures/weslaw/rejected/unresolved-subject.weslaw.yaml b/test/fixtures/weslaw/rejected/unresolved-subject.weslaw.yaml deleted file mode 100644 index f3b5e09b..00000000 --- a/test/fixtures/weslaw/rejected/unresolved-subject.weslaw.yaml +++ /dev/null @@ -1,12 +0,0 @@ -apiVersion: weslaw/v1 -schema: - family: weslaw-fixture-contract-bundle - hash: sha256:ee681e8c2c99acb5db74f09b2eb06cca2e9379fc7d69627d3287cba6177ac4b6 - source: ../contract-bundle-shape.graphql -laws: - - id: jedit.op.replaceRange.footprint - status: active - kind: footprintLaw - subject: operation:Mutation.replaceRange - reads: [BufferWorldline] - writes: [BufferWorldline] diff --git a/test/fixtures/weslaw/rejected/wrong-type-optional-sequence.expected.txt b/test/fixtures/weslaw/rejected/wrong-type-optional-sequence.expected.txt deleted file mode 100644 index ee671066..00000000 --- a/test/fixtures/weslaw/rejected/wrong-type-optional-sequence.expected.txt +++ /dev/null @@ -1 +0,0 @@ -WESLAW_INVALID_DOCUMENT diff --git a/test/fixtures/weslaw/rejected/wrong-type-optional-sequence.weslaw.yaml b/test/fixtures/weslaw/rejected/wrong-type-optional-sequence.weslaw.yaml deleted file mode 100644 index e6f748eb..00000000 --- a/test/fixtures/weslaw/rejected/wrong-type-optional-sequence.weslaw.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: weslaw/v1 -schema: - family: weslaw-fixture-contract-bundle - hash: sha256:ee681e8c2c99acb5db74f09b2eb06cca2e9379fc7d69627d3287cba6177ac4b6 - source: ../contract-bundle-shape.graphql -laws: - - id: jedit.op.replaceRangeAsTick.bad-slots - status: active - kind: footprintLaw - subject: operation:Mutation.replaceRangeAsTick - slots: not-an-array diff --git a/test/release-governance.bats b/test/release-governance.bats index 7e42347e..9daff2f0 100644 --- a/test/release-governance.bats +++ b/test/release-governance.bats @@ -61,34 +61,9 @@ load 'vendor/bats-plugins/bats-assert/load' done } -@test "release profile names unpublished Holmes as a required version source" { - run grep -F "path: crates/wesley-holmes/Cargo.toml" .continuum/release.yml - assert_success - - run grep -F "name: wesley-holmes" .continuum/release.yml - assert_success - - run bash -lc "awk ' - /path: crates\\/wesley-holmes\\/Cargo.toml/ { in_block=1 } - in_block && /^[[:space:]]*required:[[:space:]]*true$/ { required=1 } - in_block && /^[[:space:]]*published:[[:space:]]*false$/ { published=1 } - in_block && /^[[:space:]]*-[[:space:]]*path:/ && \$0 !~ /wesley-holmes/ { in_block=0 } - END { exit !(required && published) } - ' .continuum/release.yml" - assert_success -} - -@test "release policy names unpublished Holmes as a version source" { - run grep -F "crates/wesley-holmes/Cargo.toml" docs/governance/RELEASE_POLICY.md - assert_success -} - @test "release profile assertions are YAML spacing tolerant" { run bash -lc "awk '/@test \"release profile names every published Wesley crate\"/{in_test=1} in_test && /^@test / && !/release profile names every published Wesley crate/{exit} in_test {print}' test/release-governance.bats | grep -F 'grep -Eq'" assert_success - - run bash -lc "awk '/@test \"release profile names unpublished Holmes as a required version source\"/{in_test=1} in_test && /^@test / && !/release profile names unpublished Holmes as a required version source/{exit} in_test {print}' test/release-governance.bats | grep -F 'required=1'" - assert_success } @test "release profile declares Rust advisory audit validation" { diff --git a/test/weslaw-fixtures.bats b/test/weslaw-fixtures.bats deleted file mode 100644 index f64bb294..00000000 --- a/test/weslaw-fixtures.bats +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env bats - -load 'vendor/bats-plugins/bats-support/load' -load 'vendor/bats-plugins/bats-assert/load' - -@test "weslaw substrate schema does not shadow accepted footprint law with inline directive law" { - run grep -n '^[[:space:]]*@wes_footprint(' test/fixtures/weslaw/contract-bundle-shape.graphql - assert_failure -} - -@test "weslaw Law IR spec defines every referenced footprint nested shape" { - run grep -F "Create-slot fields:" docs/design/0019-weslaw-semantic-law-ir/LAW_IR_V1.md - assert_success - - run grep -F "Update fields:" docs/design/0019-weslaw-semantic-law-ir/LAW_IR_V1.md - assert_success -} - -@test "weslaw scalar examples use the closed forbidden-interpretation enum" { - run grep -R "forbidSilentNarrowingToGraphQLInt" docs/design/0019-weslaw-semantic-law-ir test/fixtures/weslaw - assert_failure -} - -@test "weslaw explain command prose uses the planned CLI spelling" { - run grep -R "explain-law" docs/design/0019-weslaw-semantic-law-ir - assert_failure -} - -@test "weslaw schema artifacts are versioned canonical JSON" { - run node --input-type=module -e ' - import { readFileSync } from "node:fs"; - const files = [ - ["schemas/weslaw-v1.schema.json", "weslaw/v1"], - ["schemas/wesley-law-ir-v1.schema.json", "wesley.law-ir/v1"], - [ - "schemas/wesley-contract-bundle-manifest-v1.schema.json", - "wesley.contract-bundle-manifest/v1", - ], - ["schemas/wesley-law-diff-v1.schema.json", "wesley.law-diff/v1"], - ]; - function sortJson(value) { - if (Array.isArray(value)) return value.map(sortJson); - if (value && typeof value === "object") { - return Object.fromEntries( - Object.keys(value).sort().map((key) => [key, sortJson(value[key])]), - ); - } - return value; - } - for (const [file, apiVersion] of files) { - const raw = readFileSync(file, "utf8"); - const parsed = JSON.parse(raw); - if (raw !== JSON.stringify(sortJson(parsed))) { - throw new Error(`${file} is not canonical JSON`); - } - if (parsed.properties?.apiVersion?.const !== apiVersion) { - throw new Error(`${file} does not pin ${apiVersion}`); - } - } - ' - assert_success -} diff --git a/wesley.config.json b/wesley.config.json index b51c6363..0ae72d8c 100644 --- a/wesley.config.json +++ b/wesley.config.json @@ -6,7 +6,6 @@ "path": "test/fixtures/examples/ecommerce.graphql", "rebuildOnGlobs": [ "test/fixtures/examples/ecommerce.graphql", - "test/fixtures/weslaw/**", "schemas/**" ] }, diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 43706659..c52bb9d5 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -53,12 +53,6 @@ const PUBLISH_CRATES: &[PublishCrate] = &[ dependencies: &["wesley-core", "wesley-emit-rust", "wesley-emit-typescript"], }, ]; -const UNPUBLISHED_CARGO_VERSION_SOURCES: &[CargoVersionSource] = &[CargoVersionSource { - name: "wesley-holmes", - path: "crates/wesley-holmes", - publish: false, -}]; - fn main() -> ExitCode { match run(env::args_os().skip(1).collect()) { Ok(()) => ExitCode::from(EXIT_OK), @@ -1055,10 +1049,6 @@ fn check_publish_manifest_versions_at(root: &Path, version: &str) -> Result<(), check_root_package_version(root, version, &mut failures); - for source in UNPUBLISHED_CARGO_VERSION_SOURCES { - check_cargo_version_source(root, source, version, &mut failures); - } - for publish_crate in PUBLISH_CRATES { let manifest_path = root.join(publish_crate.path).join("Cargo.toml"); let manifest = match read_toml_manifest(&manifest_path) { @@ -1116,61 +1106,6 @@ fn check_publish_manifest_versions_at(root: &Path, version: &str) -> Result<(), finish_check("release manifest versions", failures) } -fn check_cargo_version_source( - root: &Path, - source: &CargoVersionSource, - version: &str, - failures: &mut Vec, -) { - let manifest_path = root.join(source.path).join("Cargo.toml"); - let manifest = match read_toml_manifest(&manifest_path) { - Ok(manifest) => manifest, - Err(failure) => { - failures.push(failure); - return; - } - }; - - let Some(package) = manifest.get("package").and_then(toml::Value::as_table) else { - failures.push(format!("{} is missing [package]", source.path)); - return; - }; - - let name = package - .get("name") - .and_then(toml::Value::as_str) - .unwrap_or_default(); - if name != source.name { - failures.push(format!( - "{} package.name is `{name}`, expected `{}`", - source.path, source.name - )); - } - - let manifest_version = package - .get("version") - .and_then(toml::Value::as_str) - .unwrap_or_default(); - if manifest_version != version { - failures.push(format!( - "{} version is `{manifest_version}`, expected `{version}`", - source.path - )); - } - - let publish = package.get("publish").and_then(toml::Value::as_bool); - if publish != Some(source.publish) { - failures.push(format!( - "{} publish is `{}`, expected `{}`", - source.path, - publish - .map(|value| value.to_string()) - .unwrap_or_else(|| "unset".to_string()), - source.publish - )); - } -} - fn check_root_package_version(root: &Path, version: &str, failures: &mut Vec) { let path = root.join("package.json"); let content = match fs::read_to_string(&path) { @@ -3185,12 +3120,6 @@ struct PublishCrate { dependencies: &'static [&'static str], } -struct CargoVersionSource { - name: &'static str, - path: &'static str, - publish: bool, -} - #[derive(Debug, PartialEq, Eq)] struct BenchIrOptions { iterations: usize, @@ -4320,14 +4249,6 @@ mod tests { .expect("crate manifest should be written"); } - let holmes_root = root.join("crates/wesley-holmes"); - fs::create_dir_all(holmes_root.join("src")).expect("holmes src should be created"); - fs::write( - holmes_root.join("Cargo.toml"), - "[package]\nname = \"wesley-holmes\"\nversion = \"1.2.3\"\nedition = \"2021\"\npublish = false\n", - ) - .expect("holmes manifest should be written"); - let result = check_publish_manifest_versions_at(&root, "1.2.3"); match result { @@ -4344,74 +4265,6 @@ mod tests { fs::remove_dir_all(root).expect("temp root should be removed"); } - #[test] - fn unpublished_holmes_version_mismatch_blocks_release_manifest_check() { - let root = env::temp_dir().join(format!( - "wesley-xtask-release-holmes-version-{}", - std::process::id() - )); - if root.exists() { - fs::remove_dir_all(&root).expect("stale temp root should be removed"); - } - fs::create_dir_all(&root).expect("temp root should be created"); - fs::write( - root.join("package.json"), - serde_json::json!({ - "name": "wesley", - "version": "1.2.3", - "private": true - }) - .to_string(), - ) - .expect("root package json should be written"); - - for publish_crate in PUBLISH_CRATES { - let crate_root = root.join(publish_crate.path); - fs::create_dir_all(crate_root.join("src")).expect("crate src should be created"); - fs::write(crate_root.join("README.md"), "# Test crate\n") - .expect("crate readme should be written"); - fs::write(crate_root.join("src/lib.rs"), "").expect("crate source should be written"); - - let mut dependency_lines = String::new(); - for dependency in publish_crate.dependencies { - dependency_lines.push_str(&format!( - "{dependency} = {{ path = \"../{dependency}\", version = \"1.2.3\" }}\n" - )); - } - fs::write( - crate_root.join("Cargo.toml"), - format!( - "[package]\nname = \"{}\"\nversion = \"1.2.3\"\nedition = \"2021\"\nreadme = \"README.md\"\n\n[dependencies]\n{}", - publish_crate.name, dependency_lines - ), - ) - .expect("crate manifest should be written"); - } - - let holmes_root = root.join("crates/wesley-holmes"); - fs::create_dir_all(holmes_root.join("src")).expect("holmes src should be created"); - fs::write( - holmes_root.join("Cargo.toml"), - "[package]\nname = \"wesley-holmes\"\nversion = \"9.9.9\"\nedition = \"2021\"\npublish = false\n", - ) - .expect("holmes manifest should be written"); - - let result = check_publish_manifest_versions_at(&root, "1.2.3"); - - match result { - Err(Error::CheckFailed { check, failures }) => { - assert_eq!(check, "release manifest versions"); - assert_eq!( - failures, - vec!["crates/wesley-holmes version is `9.9.9`, expected `1.2.3`"] - ); - } - other => panic!("expected Holmes package version failure, got {other:?}"), - } - - fs::remove_dir_all(root).expect("temp root should be removed"); - } - // --- looks_like_file_path --- #[test]