1use std::collections::{BTreeMap, BTreeSet, HashSet};
7
8use serde::{Deserialize, Serialize};
9use thiserror::Error;
10use yaml_rust2::yaml::Hash as Mapping;
11use yaml_rust2::{Yaml, YamlLoader};
12
13use super::ir::{
14 compute_content_hash, compute_registry_hash, to_canonical_json, Field, TypeDefinition,
15 TypeKind, WesleyIR,
16};
17use super::operation::{OperationType, SchemaOperation};
18
19pub const WESLAW_API_VERSION: &str = "weslaw/v1";
21
22pub const WESLEY_LAW_IR_API_VERSION: &str = "wesley.law-ir/v1";
24
25pub const WESLEY_LAW_IR_CANONICAL_JSON_CODEC: &str = "wesley.law-ir.canonical-json.v1";
27
28pub const WESLEY_CONTRACT_BUNDLE_MANIFEST_API_VERSION: &str = "wesley.contract-bundle-manifest/v1";
30
31pub const WESLEY_CONTRACT_BUNDLE_HASH_INPUT_CODEC: &str =
33 "wesley.contract-bundle.hash-input.canonical-json.v1";
34
35pub const WESLEY_EMPTY_PROFILE_API_VERSION: &str = "wesley.policy-profile.empty/v1";
37
38pub const WESLEY_LAW_DIFF_API_VERSION: &str = "wesley.law-diff/v1";
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum WeslawDiagnosticCode {
44 ParseError,
46 UnsupportedApiVersion,
48 InvalidDocument,
50 DuplicateId,
52 RawExprRejected,
54 UnknownKind,
56 UnknownField,
58 SchemaHashMismatch,
60 InvalidCoordinate,
62 UnresolvedSubject,
64 UnresolvedReference,
66 WrongSubjectKind,
68 Conflict,
70}
71
72impl WeslawDiagnosticCode {
73 pub fn as_str(self) -> &'static str {
75 match self {
76 Self::ParseError => "WESLAW_PARSE_ERROR",
77 Self::UnsupportedApiVersion => "WESLAW_UNSUPPORTED_API_VERSION",
78 Self::InvalidDocument => "WESLAW_INVALID_DOCUMENT",
79 Self::DuplicateId => "WESLAW_DUPLICATE_ID",
80 Self::RawExprRejected => "WESLAW_RAW_EXPR_REJECTED",
81 Self::UnknownKind => "WESLAW_UNKNOWN_KIND",
82 Self::UnknownField => "WESLAW_UNKNOWN_FIELD",
83 Self::SchemaHashMismatch => "WESLAW_SCHEMA_HASH_MISMATCH",
84 Self::InvalidCoordinate => "WESLAW_INVALID_COORDINATE",
85 Self::UnresolvedSubject => "WESLAW_UNRESOLVED_SUBJECT",
86 Self::UnresolvedReference => "WESLAW_UNRESOLVED_REFERENCE",
87 Self::WrongSubjectKind => "WESLAW_WRONG_SUBJECT_KIND",
88 Self::Conflict => "WESLAW_CONFLICT",
89 }
90 }
91}
92
93#[derive(Debug, Error, Clone, PartialEq, Eq)]
95#[error("{code:?}: {message}")]
96pub struct WeslawError {
97 pub code: WeslawDiagnosticCode,
99 pub message: String,
101 pub path: Option<String>,
103}
104
105impl WeslawError {
106 fn new(code: WeslawDiagnosticCode, message: impl Into<String>) -> Self {
107 Self {
108 code,
109 message: message.into(),
110 path: None,
111 }
112 }
113
114 fn at_path(
115 code: WeslawDiagnosticCode,
116 path: impl Into<String>,
117 message: impl Into<String>,
118 ) -> Self {
119 Self {
120 code,
121 message: message.into(),
122 path: Some(path.into()),
123 }
124 }
125}
126
127#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
129#[serde(rename_all = "camelCase")]
130pub struct LawIrV1 {
131 pub api_version: String,
133 pub family: String,
135 pub schema_hash: String,
137 #[serde(skip_serializing_if = "Option::is_none")]
139 pub schema_source: Option<String>,
140 pub registries: LawRegistrySetV1,
142 pub entries: Vec<LawEntryV1>,
144}
145
146#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq)]
148#[serde(rename_all = "camelCase")]
149pub struct LawRegistrySetV1 {
150 pub resources: Vec<ResourceRegistryEntryV1>,
152 pub verifiers: Vec<VerifierRegistryEntryV1>,
154 pub channels: Vec<ChannelRegistryEntryV1>,
156}
157
158#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
160#[serde(rename_all = "camelCase")]
161pub struct ResourceRegistryEntryV1 {
162 pub id: String,
164 pub owner: String,
166 pub kind: String,
168 #[serde(skip_serializing_if = "Option::is_none")]
170 pub notes: Option<String>,
171}
172
173#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
175#[serde(rename_all = "camelCase")]
176pub struct VerifierRegistryEntryV1 {
177 pub id: String,
179 pub owner: String,
181 pub input_contracts: Vec<String>,
183}
184
185#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
187#[serde(rename_all = "camelCase")]
188pub struct ChannelRegistryEntryV1 {
189 pub name: String,
191 pub version: u64,
193 pub carrier: String,
195}
196
197#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
199#[serde(rename_all = "camelCase")]
200pub struct LawEntryV1 {
201 pub id: String,
203 pub status: LawStatusV1,
205 pub kind: LawKindV1,
207 pub subject: String,
209 pub tags: Vec<String>,
211 #[serde(skip_serializing_if = "Option::is_none")]
213 pub rationale: Option<String>,
214 pub body: LawEntryBodyV1,
216 #[serde(skip)]
218 pub source_index: Option<usize>,
219}
220
221#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq)]
223#[serde(rename_all = "camelCase")]
224pub enum LawStatusV1 {
225 #[default]
227 Active,
228 Draft,
230}
231
232#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
234#[serde(rename_all = "camelCase")]
235pub enum LawKindV1 {
236 ScalarSemantics,
238 VariantLaw,
240 FootprintLaw,
242 ChannelLaw,
244 InvariantLaw,
246}
247
248#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
250#[serde(untagged)]
251pub enum LawEntryBodyV1 {
252 ScalarSemantics(ScalarSemanticsLawV1),
254 VariantLaw(VariantLawV1),
256 FootprintLaw(FootprintLawV1),
258 ChannelLaw(ChannelLawV1),
260 InvariantLaw(InvariantLawV1),
262}
263
264#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
266#[serde(rename_all = "camelCase")]
267pub struct ScalarSemanticsLawV1 {
268 pub representation: ScalarRepresentationV1,
270 #[serde(skip_serializing_if = "Option::is_none")]
272 pub min_inclusive: Option<u64>,
273 #[serde(skip_serializing_if = "Option::is_none")]
275 pub max_inclusive: Option<u64>,
276 #[serde(skip_serializing_if = "Option::is_none")]
278 pub ordering: Option<ScalarOrderingV1>,
279 #[serde(skip_serializing_if = "Option::is_none")]
281 pub scope: Option<String>,
282 pub forbids: Vec<ScalarForbiddenInterpretationV1>,
284}
285
286#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
288#[serde(rename_all = "camelCase")]
289pub enum ScalarRepresentationV1 {
290 Integer,
292 OpaqueIdentifier,
294 String,
296}
297
298#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
300#[serde(rename_all = "camelCase")]
301pub enum ScalarOrderingV1 {
302 #[serde(rename = "none")]
304 None,
305 Lamport,
307 Total,
309 Partial,
311}
312
313#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
315#[serde(rename_all = "camelCase")]
316pub enum ScalarForbiddenInterpretationV1 {
317 #[serde(rename = "silentGraphQLIntNarrowing")]
319 SilentGraphqlIntNarrowing,
320}
321
322#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
324#[serde(rename_all = "camelCase")]
325pub struct VariantLawV1 {
326 pub discriminator: VariantDiscriminatorV1,
328 pub cases: Vec<VariantCaseV1>,
330}
331
332#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
334#[serde(rename_all = "camelCase")]
335pub struct VariantDiscriminatorV1 {
336 pub field: String,
338 pub r#enum: String,
340}
341
342#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
344#[serde(rename_all = "camelCase")]
345pub struct VariantCaseV1 {
346 pub value: String,
348 pub requires: Vec<String>,
350 pub forbids: Vec<String>,
352}
353
354#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
356#[serde(rename_all = "camelCase")]
357pub struct FootprintLawV1 {
358 pub reads: Vec<String>,
360 pub writes: Vec<String>,
362 pub creates: Vec<String>,
364 pub forbids: Vec<String>,
366 pub slots: Vec<FootprintSlotV1>,
368 pub closures: Vec<FootprintClosureV1>,
370 pub create_slots: Vec<CreateSlotV1>,
372 pub updates: Vec<FootprintUpdateV1>,
374}
375
376#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
378#[serde(rename_all = "camelCase")]
379pub struct FootprintSlotV1 {
380 pub name: String,
382 pub kind: String,
384 pub bind_from_arg: String,
386 pub access: Vec<String>,
388}
389
390#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
392#[serde(rename_all = "camelCase")]
393pub struct FootprintClosureV1 {
394 pub name: String,
396 pub from_slot: String,
398 pub operator: String,
400 pub arg_bindings: Vec<String>,
402 pub reads: Vec<String>,
404 pub cardinality: FootprintCardinalityV1,
406}
407
408#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
410#[serde(rename_all = "camelCase")]
411pub struct CreateSlotV1 {
412 pub name: String,
414 pub kind: String,
416 #[serde(skip_serializing_if = "Option::is_none")]
418 pub cardinality: Option<FootprintCardinalityV1>,
419}
420
421#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
423#[serde(rename_all = "camelCase")]
424pub enum FootprintCardinalityV1 {
425 #[serde(rename = "one")]
427 One,
428 Optional,
430 Many,
432}
433
434#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
436#[serde(rename_all = "camelCase")]
437pub struct FootprintUpdateV1 {
438 pub slot: String,
440 pub fields: Vec<String>,
442}
443
444#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
446#[serde(rename_all = "camelCase")]
447pub struct ChannelLawV1 {
448 pub ordered: bool,
450 pub version: u64,
452 #[serde(skip_serializing_if = "Option::is_none")]
454 pub compatibility: Option<ChannelCompatibilityV1>,
455 pub messages: Vec<ChannelMessageV1>,
457}
458
459#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
461#[serde(rename_all = "camelCase")]
462pub struct ChannelCompatibilityV1 {
463 pub versioning: String,
465 pub semver_coupled: bool,
467}
468
469#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
471#[serde(rename_all = "camelCase")]
472pub struct ChannelMessageV1 {
473 pub field: String,
475 pub r#type: String,
477}
478
479#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
481#[serde(rename_all = "camelCase")]
482pub struct InvariantLawV1 {
483 pub predicate: PredicateV1,
485}
486
487#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
489#[serde(rename_all = "camelCase", tag = "op")]
490pub enum PredicateV1 {
491 FieldEquals {
493 field: String,
495 value: serde_json::Value,
497 },
498 External {
500 verifier: String,
502 r#ref: String,
504 #[serde(skip_serializing_if = "Option::is_none")]
506 input_contract: Option<String>,
507 },
508}
509
510#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
512#[serde(rename_all = "camelCase")]
513pub struct LawBindingReportV1 {
514 pub schema_hash: String,
516 pub bound_entry_count: usize,
518}
519
520#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
522#[serde(rename_all = "camelCase")]
523pub struct ContractBundleManifestV1 {
524 pub api_version: String,
526 pub schema_hash: String,
528 pub law_hash: String,
530 #[serde(skip_serializing_if = "Option::is_none")]
532 pub law_document_hash: Option<String>,
533 pub profile_hash: String,
535 pub bundle_hash: String,
537 pub law_ir_codec: String,
539 pub bundle_hash_codec: String,
541 pub compiler: String,
543 pub compiler_version: String,
545 pub law_entry_count: usize,
547}
548
549#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
551#[serde(rename_all = "camelCase")]
552pub struct LawHashSetV1 {
553 pub law_hash: String,
555 pub law_document_hash: String,
557}
558
559#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
561#[serde(rename_all = "camelCase")]
562pub struct LawDiffReportV1 {
563 pub api_version: String,
565 pub old_schema_hash: String,
567 pub new_schema_hash: String,
569 pub old_law_hash: String,
571 pub new_law_hash: String,
573 pub changes: Vec<LawDiffEventV1>,
575}
576
577#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
579#[serde(rename_all = "camelCase")]
580pub struct LawDiffEventV1 {
581 pub kind: LawDiffEventKindV1,
583 #[serde(skip_serializing_if = "Option::is_none")]
585 pub law_id: Option<String>,
586 #[serde(skip_serializing_if = "Option::is_none")]
588 pub subject: Option<String>,
589 #[serde(skip_serializing_if = "Option::is_none")]
591 pub law_kind: Option<LawKindV1>,
592 pub review_posture: LawDiffReviewPostureV1,
594 #[serde(default, skip_serializing_if = "Vec::is_empty")]
596 pub field_changes: Vec<LawDiffFieldChangeV1>,
597 #[serde(default, skip_serializing_if = "Vec::is_empty")]
599 pub added_reads: Vec<String>,
600 #[serde(default, skip_serializing_if = "Vec::is_empty")]
602 pub removed_reads: Vec<String>,
603 #[serde(default, skip_serializing_if = "Vec::is_empty")]
605 pub added_writes: Vec<String>,
606 #[serde(default, skip_serializing_if = "Vec::is_empty")]
608 pub removed_writes: Vec<String>,
609 #[serde(default, skip_serializing_if = "Vec::is_empty")]
611 pub added_creates: Vec<String>,
612 #[serde(default, skip_serializing_if = "Vec::is_empty")]
614 pub removed_creates: Vec<String>,
615 #[serde(default, skip_serializing_if = "Vec::is_empty")]
617 pub added_forbids: Vec<String>,
618 #[serde(default, skip_serializing_if = "Vec::is_empty")]
620 pub removed_forbids: Vec<String>,
621}
622
623#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
625#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
626pub enum LawDiffEventKindV1 {
627 LawBundleChanged,
629 RegistryChanged,
631 LawAdded,
633 LawRemoved,
635 LawTagsChanged,
637 LawStrengthened,
639 LawWeakened,
641 LawChanged,
643 ScalarSemanticsChanged,
645 VariantLawChanged,
647 FootprintExpanded,
649 FootprintContracted,
651 FootprintChanged,
653 ChannelVersionChanged,
655 ChannelLawChanged,
657 PredicateChanged,
659 BindingBroken,
661 SchemaHashRebound,
663}
664
665#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
667#[serde(rename_all = "kebab-case")]
668pub enum LawDiffReviewPostureV1 {
669 RequiresReview,
671}
672
673#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
675#[serde(rename_all = "camelCase")]
676pub struct LawDiffFieldChangeV1 {
677 pub path: String,
679 pub old: serde_json::Value,
681 pub new: serde_json::Value,
683}
684
685pub fn load_weslaw_yaml(source: &str) -> Result<LawIrV1, WeslawError> {
687 let documents = YamlLoader::load_from_str(source)
688 .map_err(|err| WeslawError::new(WeslawDiagnosticCode::ParseError, err.to_string()))?;
689 if documents.len() != 1 {
690 return Err(WeslawError::new(
691 WeslawDiagnosticCode::InvalidDocument,
692 "weslaw/v1 documents must contain exactly one YAML document",
693 ));
694 }
695 let document = &documents[0];
696 let root = expect_mapping(document, "$")?;
697 reject_unknown_fields(root, "$", &["apiVersion", "schema", "registries", "laws"])?;
698
699 let api_version = required_string(root, "apiVersion", "$.apiVersion")?;
700 if api_version != WESLAW_API_VERSION {
701 return Err(WeslawError::at_path(
702 WeslawDiagnosticCode::UnsupportedApiVersion,
703 "$.apiVersion",
704 format!("unsupported weslaw apiVersion {api_version}"),
705 ));
706 }
707
708 let schema = required_mapping(root, "schema", "$.schema")?;
709 reject_unknown_fields(schema, "$.schema", &["family", "hash", "source"])?;
710 let family = required_string(schema, "family", "$.schema.family")?;
711 let schema_hash = required_string(schema, "hash", "$.schema.hash")?;
712 validate_schema_hash_anchor(&schema_hash, "$.schema.hash")?;
713 let schema_source = optional_string(schema, "source", "$.schema.source")?;
714
715 let registries = match mapping_get(root, "registries") {
716 Some(value) => parse_registries(expect_mapping(value, "$.registries")?)?,
717 None => LawRegistrySetV1::default(),
718 };
719
720 let laws = required_sequence(root, "laws", "$.laws")?;
721 let mut active_ids = HashSet::new();
722 let mut entries = Vec::with_capacity(laws.len());
723 for (index, law_value) in laws.iter().enumerate() {
724 let path = format!("$.laws[{index}]");
725 if let Some(mut entry) = parse_law_entry(law_value, &path)? {
726 entry.source_index = Some(index);
727 if !active_ids.insert(entry.id.clone()) {
728 return Err(WeslawError::at_path(
729 WeslawDiagnosticCode::DuplicateId,
730 format!("{path}.id"),
731 format!("duplicate active law id {}", entry.id),
732 ));
733 }
734 entries.push(entry);
735 }
736 }
737 sort_law_ir_entries(&mut entries);
738
739 Ok(LawIrV1 {
740 api_version: WESLEY_LAW_IR_API_VERSION.to_string(),
741 family,
742 schema_hash,
743 schema_source,
744 registries,
745 entries,
746 })
747}
748
749pub fn lower_wes_channel_directives_to_law_ir_v1(
755 schema_ir: &WesleyIR,
756 family: &str,
757 schema_hash: &str,
758 schema_source: Option<String>,
759) -> Result<LawIrV1, WeslawError> {
760 validate_schema_hash_anchor(schema_hash, "schemaHash")?;
761 let mut entries = Vec::new();
762
763 for definition in &schema_ir.types {
764 let Some(directive) = definition.directives.get("wes_channel") else {
765 continue;
766 };
767 if definition.kind != TypeKind::Object {
768 return Err(WeslawError::at_path(
769 WeslawDiagnosticCode::WrongSubjectKind,
770 format!("type:{}", definition.name),
771 "@wes_channel may only lower from object types",
772 ));
773 }
774 let name = directive_string_field(
775 directive,
776 "name",
777 &format!("type:{}.@wes_channel.name", definition.name),
778 )?;
779 let version = directive_u64_field(
780 directive,
781 "version",
782 &format!("type:{}.@wes_channel.version", definition.name),
783 )?;
784 let ordered = directive_bool_field(
785 directive,
786 "ordered",
787 &format!("type:{}.@wes_channel.ordered", definition.name),
788 )?;
789 let subject = format!("channel:{name}@{version}");
790 parse_subject_coordinate(&subject, &format!("type:{}.@wes_channel", definition.name))?;
791
792 entries.push(LawEntryV1 {
793 id: format!("directive.wes_channel.{name}.v{version}"),
794 status: LawStatusV1::Active,
795 kind: LawKindV1::ChannelLaw,
796 subject,
797 tags: Vec::new(),
798 rationale: None,
799 source_index: None,
800 body: LawEntryBodyV1::ChannelLaw(ChannelLawV1 {
801 ordered,
802 version,
803 compatibility: None,
804 messages: definition
805 .fields
806 .iter()
807 .map(|field| ChannelMessageV1 {
808 field: field.name.clone(),
809 r#type: field.r#type.base.clone(),
810 })
811 .collect(),
812 }),
813 });
814 }
815
816 sort_law_ir_entries(&mut entries);
817
818 Ok(LawIrV1 {
819 api_version: WESLEY_LAW_IR_API_VERSION.to_string(),
820 family: family.to_string(),
821 schema_hash: schema_hash.to_string(),
822 schema_source,
823 registries: LawRegistrySetV1::default(),
824 entries,
825 })
826}
827
828pub fn to_canonical_law_ir_json(value: &LawIrV1) -> Result<String, serde_json::Error> {
835 let mut normalized = value.clone();
836 normalized
837 .entries
838 .retain(|entry| entry.status == LawStatusV1::Active);
839 sort_law_ir_entries(&mut normalized.entries);
840 to_canonical_json(&normalized)
841}
842
843pub fn to_semantic_law_ir_json(value: &LawIrV1) -> Result<String, WeslawError> {
850 let semantic = semantic_law_ir_value(value)?;
851 to_canonical_json(&semantic).map_err(canonicalization_error)
852}
853
854pub fn compute_law_hash_v1(value: &LawIrV1) -> Result<String, WeslawError> {
856 Ok(prefixed_sha256(&to_semantic_law_ir_json(value)?))
857}
858
859pub fn compute_law_hash_set_v1(value: &LawIrV1) -> Result<LawHashSetV1, WeslawError> {
861 let law_hash = compute_law_hash_v1(value)?;
862 let document_json = to_canonical_law_ir_json(value).map_err(canonicalization_error)?;
863 let law_document_hash = prefixed_sha256(&document_json);
864
865 Ok(LawHashSetV1 {
866 law_hash,
867 law_document_hash,
868 })
869}
870
871pub fn build_contract_bundle_manifest_v1(
873 law_ir: &LawIrV1,
874 schema_ir: &WesleyIR,
875 operations: &[SchemaOperation],
876) -> Result<ContractBundleManifestV1, WeslawError> {
877 let schema_hash =
878 prefixed_sha256_hex(&compute_registry_hash(schema_ir).map_err(canonicalization_error)?);
879 let binding = validate_law_ir_v1_bindings(law_ir, schema_ir, operations, &schema_hash)?;
880 let law_hashes = compute_law_hash_set_v1(law_ir)?;
881 let profile_hash = empty_profile_hash_v1()?;
882 let bundle_hash = compute_bundle_hash_v1(
883 &schema_hash,
884 &law_hashes.law_hash,
885 &profile_hash,
886 WESLEY_LAW_IR_CANONICAL_JSON_CODEC,
887 WESLEY_CONTRACT_BUNDLE_HASH_INPUT_CODEC,
888 "wesley-core",
889 env!("CARGO_PKG_VERSION"),
890 )?;
891
892 Ok(ContractBundleManifestV1 {
893 api_version: WESLEY_CONTRACT_BUNDLE_MANIFEST_API_VERSION.to_string(),
894 schema_hash,
895 law_hash: law_hashes.law_hash,
896 law_document_hash: Some(law_hashes.law_document_hash),
897 profile_hash,
898 bundle_hash,
899 law_ir_codec: WESLEY_LAW_IR_CANONICAL_JSON_CODEC.to_string(),
900 bundle_hash_codec: WESLEY_CONTRACT_BUNDLE_HASH_INPUT_CODEC.to_string(),
901 compiler: "wesley-core".to_string(),
902 compiler_version: env!("CARGO_PKG_VERSION").to_string(),
903 law_entry_count: binding.bound_entry_count,
904 })
905}
906
907pub fn diff_law_ir_v1(
913 old_law_ir: &LawIrV1,
914 new_law_ir: &LawIrV1,
915) -> Result<LawDiffReportV1, WeslawError> {
916 let old_entries = law_entry_index(&old_law_ir.entries, "$.old.entries")?;
917 let new_entries = law_entry_index(&new_law_ir.entries, "$.new.entries")?;
918 let old_law_hash = compute_law_hash_v1(old_law_ir)?;
919 let new_law_hash = compute_law_hash_v1(new_law_ir)?;
920 let mut changes = Vec::new();
921
922 push_bundle_level_diff_events(old_law_ir, new_law_ir, &mut changes)?;
923
924 for old_id in old_entries.keys() {
925 if !new_entries.contains_key(old_id) {
926 changes.push(law_lifecycle_event(
927 LawDiffEventKindV1::LawRemoved,
928 old_entries[old_id],
929 ));
930 }
931 }
932
933 for new_id in new_entries.keys() {
934 if !old_entries.contains_key(new_id) {
935 changes.push(law_lifecycle_event(
936 LawDiffEventKindV1::LawAdded,
937 new_entries[new_id],
938 ));
939 }
940 }
941
942 for law_id in old_entries
943 .keys()
944 .filter(|law_id| new_entries.contains_key(*law_id))
945 {
946 let old_entry = old_entries[law_id];
947 let new_entry = new_entries[law_id];
948 if old_entry.kind != new_entry.kind || old_entry.subject != new_entry.subject {
949 changes.push(law_lifecycle_event(
950 LawDiffEventKindV1::LawRemoved,
951 old_entry,
952 ));
953 changes.push(law_lifecycle_event(LawDiffEventKindV1::LawAdded, new_entry));
954 continue;
955 }
956
957 if let Some(tags_event) = diff_law_tags(old_entry, new_entry)? {
958 changes.push(tags_event);
959 }
960 if semantic_body_value(old_entry)? == semantic_body_value(new_entry)? {
961 continue;
962 }
963 if let Some(event) = diff_law_entry_bodies(old_entry, new_entry)? {
964 changes.push(event);
965 }
966 }
967
968 if changes.is_empty() && old_law_hash != new_law_hash {
969 changes.push(bundle_diff_event(
970 LawDiffEventKindV1::LawBundleChanged,
971 vec![LawDiffFieldChangeV1 {
972 path: "lawHash".to_string(),
973 old: old_law_hash.clone().into(),
974 new: new_law_hash.clone().into(),
975 }],
976 ));
977 }
978
979 sort_law_diff_events(&mut changes);
980
981 Ok(LawDiffReportV1 {
982 api_version: WESLEY_LAW_DIFF_API_VERSION.to_string(),
983 old_schema_hash: old_law_ir.schema_hash.clone(),
984 new_schema_hash: new_law_ir.schema_hash.clone(),
985 old_law_hash,
986 new_law_hash,
987 changes,
988 })
989}
990
991pub fn record_law_binding_error_v1(report: &mut LawDiffReportV1, error: &WeslawError) {
996 let mut error_value = serde_json::json!({
997 "code": error.code.as_str(),
998 "message": error.message,
999 });
1000 if let Some(path) = &error.path {
1001 error_value["path"] = path.clone().into();
1002 }
1003 report.changes.push(bundle_diff_event(
1004 LawDiffEventKindV1::BindingBroken,
1005 vec![LawDiffFieldChangeV1 {
1006 path: "binding".to_string(),
1007 old: serde_json::Value::Null,
1008 new: error_value,
1009 }],
1010 ));
1011 sort_law_diff_events(&mut report.changes);
1012}
1013
1014pub fn format_law_diff_markdown_v1(report: &LawDiffReportV1) -> String {
1016 let mut output = String::new();
1017 output.push_str("# Wesley Law Diff\n\n");
1018 output.push_str("| Field | Value |\n");
1019 output.push_str("| --- | --- |\n");
1020 output.push_str(&format!("| API version | `{}` |\n", report.api_version));
1021 output.push_str(&format!(
1022 "| Old schema hash | `{}` |\n",
1023 report.old_schema_hash
1024 ));
1025 output.push_str(&format!(
1026 "| New schema hash | `{}` |\n",
1027 report.new_schema_hash
1028 ));
1029 output.push_str(&format!("| Old law hash | `{}` |\n", report.old_law_hash));
1030 output.push_str(&format!("| New law hash | `{}` |\n", report.new_law_hash));
1031 output.push('\n');
1032
1033 if report.changes.is_empty() {
1034 output.push_str("No semantic law changes detected.\n");
1035 return output;
1036 }
1037
1038 output.push_str("## Changes\n\n");
1039 output.push_str("| Kind | Law | Subject | Summary |\n");
1040 output.push_str("| --- | --- | --- | --- |\n");
1041 for change in &report.changes {
1042 output.push_str(&format!(
1043 "| `{}` | {} | {} | {} |\n",
1044 law_diff_kind_text(change.kind),
1045 markdown_code_or_dash(change.law_id.as_deref()),
1046 markdown_code_or_dash(change.subject.as_deref()),
1047 markdown_escape(&law_diff_event_summary(change)),
1048 ));
1049 }
1050
1051 output
1052}
1053
1054pub fn validate_law_ir_v1_bindings(
1062 law_ir: &LawIrV1,
1063 schema_ir: &WesleyIR,
1064 operations: &[SchemaOperation],
1065 active_schema_hash: &str,
1066) -> Result<LawBindingReportV1, WeslawError> {
1067 validate_schema_hash_anchor(active_schema_hash, "activeSchemaHash")?;
1068 if law_ir.schema_hash != active_schema_hash {
1069 return Err(WeslawError::at_path(
1070 WeslawDiagnosticCode::SchemaHashMismatch,
1071 "$.schema.hash",
1072 format!(
1073 "law document expects schema hash {}; active schema hash is {}",
1074 law_ir.schema_hash, active_schema_hash
1075 ),
1076 ));
1077 }
1078
1079 let context = BindingContext {
1080 schema_ir,
1081 operations,
1082 law_ir,
1083 };
1084
1085 let mut unique_subject_entries = HashSet::new();
1086 for (index, entry) in law_ir.entries.iter().enumerate() {
1087 let authored_index = entry.source_index.unwrap_or(index);
1088 let law_path = format!("$.laws[{authored_index}]");
1089 let subject_path = format!("{law_path}.subject");
1090 let coordinate = parse_subject_coordinate(&entry.subject, &subject_path)?;
1091 if law_kind_has_unique_subject(entry.kind)
1092 && !unique_subject_entries.insert(format!("{:?}:{}", entry.kind, entry.subject))
1093 {
1094 return Err(conflict(
1095 entry,
1096 &subject_path,
1097 format!(
1098 "more than one active {:?} entry targets {}",
1099 entry.kind, entry.subject
1100 ),
1101 ));
1102 }
1103 context.bind_entry(entry, coordinate, &law_path)?;
1104 }
1105
1106 Ok(LawBindingReportV1 {
1107 schema_hash: active_schema_hash.to_string(),
1108 bound_entry_count: law_ir.entries.len(),
1109 })
1110}
1111
1112fn sort_law_ir_entries(entries: &mut [LawEntryV1]) {
1113 entries.sort_by(|left, right| left.id.cmp(&right.id));
1114}
1115
1116fn law_entry_index<'entry>(
1117 entries: &'entry [LawEntryV1],
1118 path: &str,
1119) -> Result<BTreeMap<&'entry str, &'entry LawEntryV1>, WeslawError> {
1120 let mut index = BTreeMap::new();
1121 for (position, entry) in entries.iter().enumerate() {
1122 if entry.status != LawStatusV1::Active {
1123 continue;
1124 }
1125 if index.insert(entry.id.as_str(), entry).is_some() {
1126 return Err(WeslawError::at_path(
1127 WeslawDiagnosticCode::DuplicateId,
1128 format!("{path}[{position}].id"),
1129 format!("duplicate active law id {}", entry.id),
1130 ));
1131 }
1132 }
1133 Ok(index)
1134}
1135
1136fn sort_law_diff_events(changes: &mut [LawDiffEventV1]) {
1137 changes.sort_by(|left, right| {
1138 left.law_id
1139 .as_deref()
1140 .unwrap_or("")
1141 .cmp(right.law_id.as_deref().unwrap_or(""))
1142 .then(left.kind.cmp(&right.kind))
1143 .then(
1144 left.subject
1145 .as_deref()
1146 .unwrap_or("")
1147 .cmp(right.subject.as_deref().unwrap_or("")),
1148 )
1149 });
1150}
1151
1152fn law_diff_kind_text(kind: LawDiffEventKindV1) -> &'static str {
1153 match kind {
1154 LawDiffEventKindV1::LawBundleChanged => "LAW_BUNDLE_CHANGED",
1155 LawDiffEventKindV1::RegistryChanged => "REGISTRY_CHANGED",
1156 LawDiffEventKindV1::LawAdded => "LAW_ADDED",
1157 LawDiffEventKindV1::LawRemoved => "LAW_REMOVED",
1158 LawDiffEventKindV1::LawTagsChanged => "LAW_TAGS_CHANGED",
1159 LawDiffEventKindV1::LawStrengthened => "LAW_STRENGTHENED",
1160 LawDiffEventKindV1::LawWeakened => "LAW_WEAKENED",
1161 LawDiffEventKindV1::LawChanged => "LAW_CHANGED",
1162 LawDiffEventKindV1::ScalarSemanticsChanged => "SCALAR_SEMANTICS_CHANGED",
1163 LawDiffEventKindV1::VariantLawChanged => "VARIANT_LAW_CHANGED",
1164 LawDiffEventKindV1::FootprintExpanded => "FOOTPRINT_EXPANDED",
1165 LawDiffEventKindV1::FootprintContracted => "FOOTPRINT_CONTRACTED",
1166 LawDiffEventKindV1::FootprintChanged => "FOOTPRINT_CHANGED",
1167 LawDiffEventKindV1::ChannelVersionChanged => "CHANNEL_VERSION_CHANGED",
1168 LawDiffEventKindV1::ChannelLawChanged => "CHANNEL_LAW_CHANGED",
1169 LawDiffEventKindV1::PredicateChanged => "PREDICATE_CHANGED",
1170 LawDiffEventKindV1::BindingBroken => "BINDING_BROKEN",
1171 LawDiffEventKindV1::SchemaHashRebound => "SCHEMA_HASH_REBOUND",
1172 }
1173}
1174
1175fn markdown_code_or_dash(value: Option<&str>) -> String {
1176 value
1177 .map(|value| format!("`{}`", markdown_escape(value)))
1178 .unwrap_or_else(|| "-".to_string())
1179}
1180
1181fn markdown_escape(value: &str) -> String {
1182 value.replace('|', "\\|").replace('\n', "<br>")
1183}
1184
1185fn law_diff_event_summary(event: &LawDiffEventV1) -> String {
1186 match event.kind {
1187 LawDiffEventKindV1::LawStrengthened => {
1188 format!("law strengthened: {}", summarize_field_changes(event))
1189 }
1190 LawDiffEventKindV1::LawWeakened => {
1191 format!("law weakened: {}", summarize_field_changes(event))
1192 }
1193 LawDiffEventKindV1::BindingBroken => {
1194 format!("binding broken: {}", summarize_field_changes(event))
1195 }
1196 LawDiffEventKindV1::FootprintExpanded => {
1197 let mut parts = Vec::new();
1198 push_array_summary(&mut parts, "added reads", &event.added_reads);
1199 push_array_summary(&mut parts, "added writes", &event.added_writes);
1200 push_array_summary(&mut parts, "added creates", &event.added_creates);
1201 push_array_summary(&mut parts, "removed forbids", &event.removed_forbids);
1202 join_or_field_changes(parts, event)
1203 }
1204 LawDiffEventKindV1::FootprintContracted => {
1205 let mut parts = Vec::new();
1206 push_array_summary(&mut parts, "removed reads", &event.removed_reads);
1207 push_array_summary(&mut parts, "removed writes", &event.removed_writes);
1208 push_array_summary(&mut parts, "removed creates", &event.removed_creates);
1209 push_array_summary(&mut parts, "added forbids", &event.added_forbids);
1210 join_or_field_changes(parts, event)
1211 }
1212 _ => summarize_field_changes(event),
1213 }
1214}
1215
1216fn push_array_summary(parts: &mut Vec<String>, label: &str, values: &[String]) {
1217 if !values.is_empty() {
1218 parts.push(format!("{label}: {}", values.join(", ")));
1219 }
1220}
1221
1222fn join_or_field_changes(parts: Vec<String>, event: &LawDiffEventV1) -> String {
1223 if parts.is_empty() {
1224 summarize_field_changes(event)
1225 } else {
1226 parts.join("; ")
1227 }
1228}
1229
1230fn summarize_field_changes(event: &LawDiffEventV1) -> String {
1231 if event.field_changes.is_empty() {
1232 return "no field-level detail".to_string();
1233 }
1234 event
1235 .field_changes
1236 .iter()
1237 .map(|change| change.path.as_str())
1238 .collect::<Vec<_>>()
1239 .join(", ")
1240}
1241
1242fn push_bundle_level_diff_events(
1243 old_law_ir: &LawIrV1,
1244 new_law_ir: &LawIrV1,
1245 changes: &mut Vec<LawDiffEventV1>,
1246) -> Result<(), WeslawError> {
1247 let mut bundle_changes = Vec::new();
1248 push_value_change(
1249 &mut bundle_changes,
1250 "family",
1251 &old_law_ir.family,
1252 &new_law_ir.family,
1253 )?;
1254 if !bundle_changes.is_empty() {
1255 changes.push(bundle_diff_event(
1256 LawDiffEventKindV1::LawBundleChanged,
1257 bundle_changes,
1258 ));
1259 }
1260
1261 let mut schema_changes = Vec::new();
1262 push_value_change(
1263 &mut schema_changes,
1264 "schemaHash",
1265 &old_law_ir.schema_hash,
1266 &new_law_ir.schema_hash,
1267 )?;
1268 if !schema_changes.is_empty() {
1269 changes.push(bundle_diff_event(
1270 LawDiffEventKindV1::SchemaHashRebound,
1271 schema_changes,
1272 ));
1273 }
1274
1275 let mut registry_changes = Vec::new();
1276 push_json_change(
1277 &mut registry_changes,
1278 "registries",
1279 semantic_registry_value(&old_law_ir.registries)?,
1280 semantic_registry_value(&new_law_ir.registries)?,
1281 );
1282 if !registry_changes.is_empty() {
1283 changes.push(bundle_diff_event(
1284 LawDiffEventKindV1::RegistryChanged,
1285 registry_changes,
1286 ));
1287 }
1288
1289 Ok(())
1290}
1291
1292fn diff_law_tags(
1293 old_entry: &LawEntryV1,
1294 new_entry: &LawEntryV1,
1295) -> Result<Option<LawDiffEventV1>, WeslawError> {
1296 let mut field_changes = Vec::new();
1297 push_json_change(
1298 &mut field_changes,
1299 "tags",
1300 string_vec_value(&old_entry.tags, "$.old.entries.tags")?,
1301 string_vec_value(&new_entry.tags, "$.new.entries.tags")?,
1302 );
1303 if field_changes.is_empty() {
1304 return Ok(None);
1305 }
1306
1307 let mut event = base_diff_event(LawDiffEventKindV1::LawTagsChanged, new_entry);
1308 event.field_changes = field_changes;
1309 Ok(Some(event))
1310}
1311
1312fn diff_law_entry_bodies(
1313 old_entry: &LawEntryV1,
1314 new_entry: &LawEntryV1,
1315) -> Result<Option<LawDiffEventV1>, WeslawError> {
1316 match (&old_entry.body, &new_entry.body) {
1317 (LawEntryBodyV1::ScalarSemantics(old_body), LawEntryBodyV1::ScalarSemantics(new_body)) => {
1318 diff_scalar_semantics(old_entry, new_entry, old_body, new_body)
1319 }
1320 (LawEntryBodyV1::VariantLaw(old_body), LawEntryBodyV1::VariantLaw(new_body)) => {
1321 diff_variant_law(old_entry, new_entry, old_body, new_body)
1322 }
1323 (LawEntryBodyV1::FootprintLaw(old_body), LawEntryBodyV1::FootprintLaw(new_body)) => {
1324 diff_footprint_law(old_entry, new_entry, old_body, new_body)
1325 }
1326 (LawEntryBodyV1::ChannelLaw(old_body), LawEntryBodyV1::ChannelLaw(new_body)) => {
1327 diff_channel_law(old_entry, new_entry, old_body, new_body)
1328 }
1329 (LawEntryBodyV1::InvariantLaw(old_body), LawEntryBodyV1::InvariantLaw(new_body)) => {
1330 diff_invariant_law(old_entry, new_entry, old_body, new_body)
1331 }
1332 _ => {
1333 let mut event = base_diff_event(LawDiffEventKindV1::LawChanged, new_entry);
1334 push_json_change(
1335 &mut event.field_changes,
1336 "body",
1337 semantic_body_value(old_entry)?,
1338 semantic_body_value(new_entry)?,
1339 );
1340 Ok(Some(event))
1341 }
1342 }
1343}
1344
1345#[derive(Default)]
1346struct SemanticDirection {
1347 strengthened: bool,
1348 weakened: bool,
1349 structural: bool,
1350}
1351
1352impl SemanticDirection {
1353 fn mark_strengthened(&mut self) {
1354 self.strengthened = true;
1355 }
1356
1357 fn mark_weakened(&mut self) {
1358 self.weakened = true;
1359 }
1360
1361 fn mark_structural(&mut self) {
1362 self.structural = true;
1363 }
1364
1365 fn note_min_change(&mut self, old: Option<u64>, new: Option<u64>) {
1366 match (old, new) {
1367 (None, Some(_)) => self.mark_strengthened(),
1368 (Some(_), None) => self.mark_weakened(),
1369 (Some(old), Some(new)) if new > old => self.mark_strengthened(),
1370 (Some(old), Some(new)) if new < old => self.mark_weakened(),
1371 _ => {}
1372 }
1373 }
1374
1375 fn note_max_change(&mut self, old: Option<u64>, new: Option<u64>) {
1376 match (old, new) {
1377 (None, Some(_)) => self.mark_strengthened(),
1378 (Some(_), None) => self.mark_weakened(),
1379 (Some(old), Some(new)) if new < old => self.mark_strengthened(),
1380 (Some(old), Some(new)) if new > old => self.mark_weakened(),
1381 _ => {}
1382 }
1383 }
1384
1385 fn note_scalar_forbids_change(
1386 &mut self,
1387 old: &[ScalarForbiddenInterpretationV1],
1388 new: &[ScalarForbiddenInterpretationV1],
1389 ) {
1390 let old_values = old.iter().copied().collect::<BTreeSet<_>>();
1391 let new_values = new.iter().copied().collect::<BTreeSet<_>>();
1392 if new_values.difference(&old_values).next().is_some() {
1393 self.mark_strengthened();
1394 }
1395 if old_values.difference(&new_values).next().is_some() {
1396 self.mark_weakened();
1397 }
1398 }
1399
1400 fn note_required_or_forbidden_change(
1401 &mut self,
1402 old: &[String],
1403 new: &[String],
1404 path: &str,
1405 ) -> Result<(), WeslawError> {
1406 if !set_added(old, new, path)?.is_empty() {
1407 self.mark_strengthened();
1408 }
1409 if !set_removed(old, new, path)?.is_empty() {
1410 self.mark_weakened();
1411 }
1412 Ok(())
1413 }
1414
1415 fn event_kind(&self, fallback: LawDiffEventKindV1) -> LawDiffEventKindV1 {
1416 match (self.structural, self.strengthened, self.weakened) {
1417 (false, true, false) => LawDiffEventKindV1::LawStrengthened,
1418 (false, false, true) => LawDiffEventKindV1::LawWeakened,
1419 _ => fallback,
1420 }
1421 }
1422}
1423
1424fn diff_scalar_semantics(
1425 _old_entry: &LawEntryV1,
1426 new_entry: &LawEntryV1,
1427 old_body: &ScalarSemanticsLawV1,
1428 new_body: &ScalarSemanticsLawV1,
1429) -> Result<Option<LawDiffEventV1>, WeslawError> {
1430 let mut field_changes = Vec::new();
1431 let mut direction = SemanticDirection::default();
1432 if old_body.representation != new_body.representation {
1433 direction.mark_structural();
1434 }
1435 push_value_change(
1436 &mut field_changes,
1437 "body.representation",
1438 old_body.representation,
1439 new_body.representation,
1440 )?;
1441 direction.note_min_change(old_body.min_inclusive, new_body.min_inclusive);
1442 push_value_change(
1443 &mut field_changes,
1444 "body.minInclusive",
1445 old_body.min_inclusive,
1446 new_body.min_inclusive,
1447 )?;
1448 direction.note_max_change(old_body.max_inclusive, new_body.max_inclusive);
1449 push_value_change(
1450 &mut field_changes,
1451 "body.maxInclusive",
1452 old_body.max_inclusive,
1453 new_body.max_inclusive,
1454 )?;
1455 if old_body.ordering != new_body.ordering {
1456 direction.mark_structural();
1457 }
1458 push_value_change(
1459 &mut field_changes,
1460 "body.ordering",
1461 old_body.ordering,
1462 new_body.ordering,
1463 )?;
1464 if old_body.scope != new_body.scope {
1465 direction.mark_structural();
1466 }
1467 push_value_change(
1468 &mut field_changes,
1469 "body.scope",
1470 &old_body.scope,
1471 &new_body.scope,
1472 )?;
1473 direction.note_scalar_forbids_change(&old_body.forbids, &new_body.forbids);
1474 push_json_change(
1475 &mut field_changes,
1476 "body.forbids",
1477 scalar_forbids_value(old_body)?,
1478 scalar_forbids_value(new_body)?,
1479 );
1480
1481 if field_changes.is_empty() {
1482 return Ok(None);
1483 }
1484
1485 let mut event = base_diff_event(
1486 direction.event_kind(LawDiffEventKindV1::ScalarSemanticsChanged),
1487 new_entry,
1488 );
1489 event.field_changes = field_changes;
1490 Ok(Some(event))
1491}
1492
1493fn diff_variant_law(
1494 _old_entry: &LawEntryV1,
1495 new_entry: &LawEntryV1,
1496 old_body: &VariantLawV1,
1497 new_body: &VariantLawV1,
1498) -> Result<Option<LawDiffEventV1>, WeslawError> {
1499 let mut field_changes = Vec::new();
1500 let mut direction = SemanticDirection::default();
1501 if old_body.discriminator.field != new_body.discriminator.field {
1502 direction.mark_structural();
1503 }
1504 push_value_change(
1505 &mut field_changes,
1506 "body.discriminator.field",
1507 &old_body.discriminator.field,
1508 &new_body.discriminator.field,
1509 )?;
1510 if old_body.discriminator.r#enum != new_body.discriminator.r#enum {
1511 direction.mark_structural();
1512 }
1513 push_value_change(
1514 &mut field_changes,
1515 "body.discriminator.enum",
1516 &old_body.discriminator.r#enum,
1517 &new_body.discriminator.r#enum,
1518 )?;
1519
1520 let old_cases = variant_case_index(&old_body.cases, "$.old.entries.body.cases")?;
1521 let new_cases = variant_case_index(&new_body.cases, "$.new.entries.body.cases")?;
1522 for old_case in old_cases.keys() {
1523 if !new_cases.contains_key(old_case) {
1524 direction.mark_structural();
1525 push_json_change(
1526 &mut field_changes,
1527 &format!("body.cases.{old_case}"),
1528 variant_case_value(old_cases[old_case])?,
1529 serde_json::Value::Null,
1530 );
1531 }
1532 }
1533 for new_case in new_cases.keys() {
1534 if !old_cases.contains_key(new_case) {
1535 direction.mark_structural();
1536 push_json_change(
1537 &mut field_changes,
1538 &format!("body.cases.{new_case}"),
1539 serde_json::Value::Null,
1540 variant_case_value(new_cases[new_case])?,
1541 );
1542 }
1543 }
1544 for case_value in old_cases
1545 .keys()
1546 .filter(|case_value| new_cases.contains_key(*case_value))
1547 {
1548 let old_case = old_cases[case_value];
1549 let new_case = new_cases[case_value];
1550 direction.note_required_or_forbidden_change(
1551 &old_case.requires,
1552 &new_case.requires,
1553 "$.entries.body.cases.requires",
1554 )?;
1555 push_json_change(
1556 &mut field_changes,
1557 &format!("body.cases.{case_value}.requires"),
1558 string_vec_value(&old_case.requires, "$.old.entries.body.cases.requires")?,
1559 string_vec_value(&new_case.requires, "$.new.entries.body.cases.requires")?,
1560 );
1561 direction.note_required_or_forbidden_change(
1562 &old_case.forbids,
1563 &new_case.forbids,
1564 "$.entries.body.cases.forbids",
1565 )?;
1566 push_json_change(
1567 &mut field_changes,
1568 &format!("body.cases.{case_value}.forbids"),
1569 string_vec_value(&old_case.forbids, "$.old.entries.body.cases.forbids")?,
1570 string_vec_value(&new_case.forbids, "$.new.entries.body.cases.forbids")?,
1571 );
1572 }
1573
1574 if field_changes.is_empty() {
1575 return Ok(None);
1576 }
1577
1578 let mut event = base_diff_event(
1579 direction.event_kind(LawDiffEventKindV1::VariantLawChanged),
1580 new_entry,
1581 );
1582 event.field_changes = field_changes;
1583 Ok(Some(event))
1584}
1585
1586fn diff_footprint_law(
1587 _old_entry: &LawEntryV1,
1588 new_entry: &LawEntryV1,
1589 old_body: &FootprintLawV1,
1590 new_body: &FootprintLawV1,
1591) -> Result<Option<LawDiffEventV1>, WeslawError> {
1592 let added_reads = set_added(&old_body.reads, &new_body.reads, "$.entries.body.reads")?;
1593 let removed_reads = set_removed(&old_body.reads, &new_body.reads, "$.entries.body.reads")?;
1594 let added_writes = set_added(&old_body.writes, &new_body.writes, "$.entries.body.writes")?;
1595 let removed_writes = set_removed(&old_body.writes, &new_body.writes, "$.entries.body.writes")?;
1596 let added_creates = set_added(
1597 &old_body.creates,
1598 &new_body.creates,
1599 "$.entries.body.creates",
1600 )?;
1601 let removed_creates = set_removed(
1602 &old_body.creates,
1603 &new_body.creates,
1604 "$.entries.body.creates",
1605 )?;
1606 let added_forbids = set_added(
1607 &old_body.forbids,
1608 &new_body.forbids,
1609 "$.entries.body.forbids",
1610 )?;
1611 let removed_forbids = set_removed(
1612 &old_body.forbids,
1613 &new_body.forbids,
1614 "$.entries.body.forbids",
1615 )?;
1616
1617 let old_value = semantic_footprint_body_value(old_body)?;
1618 let new_value = semantic_footprint_body_value(new_body)?;
1619 let mut field_changes = Vec::new();
1620 for path in ["slots", "closures", "createSlots", "updates"] {
1621 push_json_change(
1622 &mut field_changes,
1623 &format!("body.{path}"),
1624 old_value[path].clone(),
1625 new_value[path].clone(),
1626 );
1627 }
1628
1629 let footprint_expanded = !added_reads.is_empty()
1630 || !added_writes.is_empty()
1631 || !added_creates.is_empty()
1632 || !removed_forbids.is_empty();
1633 let footprint_contracted = !removed_reads.is_empty()
1634 || !removed_writes.is_empty()
1635 || !removed_creates.is_empty()
1636 || !added_forbids.is_empty();
1637 let structural_change = !field_changes.is_empty();
1638
1639 if !footprint_expanded && !footprint_contracted && !structural_change {
1640 return Ok(None);
1641 }
1642
1643 let event_kind = match (footprint_expanded, footprint_contracted, structural_change) {
1644 (true, false, false) => LawDiffEventKindV1::FootprintExpanded,
1645 (false, true, false) => LawDiffEventKindV1::FootprintContracted,
1646 _ => LawDiffEventKindV1::FootprintChanged,
1647 };
1648 let mut event = base_diff_event(event_kind, new_entry);
1649 event.field_changes = field_changes;
1650 event.added_reads = added_reads;
1651 event.removed_reads = removed_reads;
1652 event.added_writes = added_writes;
1653 event.removed_writes = removed_writes;
1654 event.added_creates = added_creates;
1655 event.removed_creates = removed_creates;
1656 event.added_forbids = added_forbids;
1657 event.removed_forbids = removed_forbids;
1658 Ok(Some(event))
1659}
1660
1661fn diff_channel_law(
1662 _old_entry: &LawEntryV1,
1663 new_entry: &LawEntryV1,
1664 old_body: &ChannelLawV1,
1665 new_body: &ChannelLawV1,
1666) -> Result<Option<LawDiffEventV1>, WeslawError> {
1667 let mut field_changes = Vec::new();
1668 push_value_change(
1669 &mut field_changes,
1670 "body.ordered",
1671 old_body.ordered,
1672 new_body.ordered,
1673 )?;
1674 push_value_change(
1675 &mut field_changes,
1676 "body.version",
1677 old_body.version,
1678 new_body.version,
1679 )?;
1680 push_json_change(
1681 &mut field_changes,
1682 "body.compatibility",
1683 serde_json::to_value(&old_body.compatibility).map_err(canonicalization_error)?,
1684 serde_json::to_value(&new_body.compatibility).map_err(canonicalization_error)?,
1685 );
1686 push_json_change(
1687 &mut field_changes,
1688 "body.messages",
1689 serde_json::to_value(&old_body.messages).map_err(canonicalization_error)?,
1690 serde_json::to_value(&new_body.messages).map_err(canonicalization_error)?,
1691 );
1692 if field_changes.is_empty() {
1693 return Ok(None);
1694 }
1695
1696 let kind = if old_body.version != new_body.version {
1697 LawDiffEventKindV1::ChannelVersionChanged
1698 } else {
1699 LawDiffEventKindV1::ChannelLawChanged
1700 };
1701 let mut event = base_diff_event(kind, new_entry);
1702 event.field_changes = field_changes;
1703 Ok(Some(event))
1704}
1705
1706fn diff_invariant_law(
1707 _old_entry: &LawEntryV1,
1708 new_entry: &LawEntryV1,
1709 old_body: &InvariantLawV1,
1710 new_body: &InvariantLawV1,
1711) -> Result<Option<LawDiffEventV1>, WeslawError> {
1712 let mut field_changes = Vec::new();
1713 push_json_change(
1714 &mut field_changes,
1715 "body.predicate",
1716 serde_json::to_value(&old_body.predicate).map_err(canonicalization_error)?,
1717 serde_json::to_value(&new_body.predicate).map_err(canonicalization_error)?,
1718 );
1719 if field_changes.is_empty() {
1720 return Ok(None);
1721 }
1722
1723 let mut event = base_diff_event(LawDiffEventKindV1::PredicateChanged, new_entry);
1724 event.field_changes = field_changes;
1725 Ok(Some(event))
1726}
1727
1728fn law_lifecycle_event(kind: LawDiffEventKindV1, entry: &LawEntryV1) -> LawDiffEventV1 {
1729 base_diff_event(kind, entry)
1730}
1731
1732fn bundle_diff_event(
1733 kind: LawDiffEventKindV1,
1734 field_changes: Vec<LawDiffFieldChangeV1>,
1735) -> LawDiffEventV1 {
1736 LawDiffEventV1 {
1737 kind,
1738 law_id: None,
1739 subject: None,
1740 law_kind: None,
1741 review_posture: LawDiffReviewPostureV1::RequiresReview,
1742 field_changes,
1743 added_reads: Vec::new(),
1744 removed_reads: Vec::new(),
1745 added_writes: Vec::new(),
1746 removed_writes: Vec::new(),
1747 added_creates: Vec::new(),
1748 removed_creates: Vec::new(),
1749 added_forbids: Vec::new(),
1750 removed_forbids: Vec::new(),
1751 }
1752}
1753
1754fn base_diff_event(kind: LawDiffEventKindV1, entry: &LawEntryV1) -> LawDiffEventV1 {
1755 LawDiffEventV1 {
1756 kind,
1757 law_id: Some(entry.id.clone()),
1758 subject: Some(entry.subject.clone()),
1759 law_kind: Some(entry.kind),
1760 review_posture: LawDiffReviewPostureV1::RequiresReview,
1761 field_changes: Vec::new(),
1762 added_reads: Vec::new(),
1763 removed_reads: Vec::new(),
1764 added_writes: Vec::new(),
1765 removed_writes: Vec::new(),
1766 added_creates: Vec::new(),
1767 removed_creates: Vec::new(),
1768 added_forbids: Vec::new(),
1769 removed_forbids: Vec::new(),
1770 }
1771}
1772
1773fn push_value_change(
1774 changes: &mut Vec<LawDiffFieldChangeV1>,
1775 path: &str,
1776 old: impl Serialize,
1777 new: impl Serialize,
1778) -> Result<(), WeslawError> {
1779 let old_value = serde_json::to_value(old).map_err(canonicalization_error)?;
1780 let new_value = serde_json::to_value(new).map_err(canonicalization_error)?;
1781 push_json_change(changes, path, old_value, new_value);
1782 Ok(())
1783}
1784
1785fn push_json_change(
1786 changes: &mut Vec<LawDiffFieldChangeV1>,
1787 path: &str,
1788 old: serde_json::Value,
1789 new: serde_json::Value,
1790) {
1791 if old == new {
1792 return;
1793 }
1794 changes.push(LawDiffFieldChangeV1 {
1795 path: path.to_string(),
1796 old,
1797 new,
1798 });
1799}
1800
1801fn directive_string_field(
1802 value: &serde_json::Value,
1803 field: &str,
1804 path: &str,
1805) -> Result<String, WeslawError> {
1806 value
1807 .get(field)
1808 .and_then(serde_json::Value::as_str)
1809 .map(ToString::to_string)
1810 .ok_or_else(|| {
1811 WeslawError::at_path(
1812 WeslawDiagnosticCode::InvalidDocument,
1813 path,
1814 format!("directive field {field} must be a string"),
1815 )
1816 })
1817}
1818
1819fn directive_u64_field(
1820 value: &serde_json::Value,
1821 field: &str,
1822 path: &str,
1823) -> Result<u64, WeslawError> {
1824 value
1825 .get(field)
1826 .and_then(serde_json::Value::as_u64)
1827 .ok_or_else(|| {
1828 WeslawError::at_path(
1829 WeslawDiagnosticCode::InvalidDocument,
1830 path,
1831 format!("directive field {field} must be an unsigned integer"),
1832 )
1833 })
1834}
1835
1836fn directive_bool_field(
1837 value: &serde_json::Value,
1838 field: &str,
1839 path: &str,
1840) -> Result<bool, WeslawError> {
1841 value
1842 .get(field)
1843 .and_then(serde_json::Value::as_bool)
1844 .ok_or_else(|| {
1845 WeslawError::at_path(
1846 WeslawDiagnosticCode::InvalidDocument,
1847 path,
1848 format!("directive field {field} must be a boolean"),
1849 )
1850 })
1851}
1852
1853fn scalar_forbids_value(body: &ScalarSemanticsLawV1) -> Result<serde_json::Value, WeslawError> {
1854 let forbids = body
1855 .forbids
1856 .iter()
1857 .map(|item| serde_json::to_value(item).map_err(canonicalization_error))
1858 .collect::<Result<Vec<_>, _>>()?
1859 .into_iter()
1860 .map(|value| {
1861 value
1862 .as_str()
1863 .expect("forbidden interpretation should serialize as a string")
1864 .to_string()
1865 })
1866 .collect::<Vec<_>>();
1867 Ok(sorted_unique_strings(&forbids, "$.entries.body.forbids")?.into())
1868}
1869
1870fn variant_case_index<'case>(
1871 cases: &'case [VariantCaseV1],
1872 path: &str,
1873) -> Result<BTreeMap<&'case str, &'case VariantCaseV1>, WeslawError> {
1874 let mut index = BTreeMap::new();
1875 for (position, case) in cases.iter().enumerate() {
1876 if index.insert(case.value.as_str(), case).is_some() {
1877 return Err(WeslawError::at_path(
1878 WeslawDiagnosticCode::Conflict,
1879 format!("{path}[{position}].value"),
1880 format!("duplicate variant case value {}", case.value),
1881 ));
1882 }
1883 }
1884 Ok(index)
1885}
1886
1887fn variant_case_value(case: &VariantCaseV1) -> Result<serde_json::Value, WeslawError> {
1888 Ok(serde_json::json!({
1889 "value": case.value,
1890 "requires": sorted_unique_strings(&case.requires, "$.entries.body.cases.requires")?,
1891 "forbids": sorted_unique_strings(&case.forbids, "$.entries.body.cases.forbids")?,
1892 }))
1893}
1894
1895fn string_vec_value(values: &[String], path: &str) -> Result<serde_json::Value, WeslawError> {
1896 Ok(sorted_unique_strings(values, path)?.into())
1897}
1898
1899fn set_added(old: &[String], new: &[String], path: &str) -> Result<Vec<String>, WeslawError> {
1900 let old_values = sorted_string_set(old, path)?;
1901 let new_values = sorted_string_set(new, path)?;
1902 Ok(new_values.difference(&old_values).cloned().collect())
1903}
1904
1905fn set_removed(old: &[String], new: &[String], path: &str) -> Result<Vec<String>, WeslawError> {
1906 let old_values = sorted_string_set(old, path)?;
1907 let new_values = sorted_string_set(new, path)?;
1908 Ok(old_values.difference(&new_values).cloned().collect())
1909}
1910
1911fn sorted_string_set(values: &[String], path: &str) -> Result<BTreeSet<String>, WeslawError> {
1912 Ok(sorted_unique_strings(values, path)?.into_iter().collect())
1913}
1914
1915fn semantic_law_ir_value(value: &LawIrV1) -> Result<serde_json::Value, WeslawError> {
1916 let entries = value
1917 .entries
1918 .iter()
1919 .filter(|entry| entry.status == LawStatusV1::Active)
1920 .map(semantic_entry_value)
1921 .collect::<Result<Vec<_>, _>>()?;
1922
1923 Ok(serde_json::json!({
1924 "apiVersion": value.api_version,
1925 "family": value.family,
1926 "schemaHash": value.schema_hash,
1927 "registries": semantic_registry_value(&value.registries)?,
1928 "entries": entries,
1929 }))
1930}
1931
1932fn semantic_registry_value(
1933 registries: &LawRegistrySetV1,
1934) -> Result<serde_json::Value, WeslawError> {
1935 let mut resources = registries
1936 .resources
1937 .iter()
1938 .map(|resource| {
1939 Ok(serde_json::json!({
1940 "id": resource.id,
1941 "owner": resource.owner,
1942 "kind": resource.kind,
1943 }))
1944 })
1945 .collect::<Result<Vec<_>, WeslawError>>()?;
1946 sort_values_by_string_field(&mut resources, "id", "$.registries.resources")?;
1947
1948 let mut verifiers = registries
1949 .verifiers
1950 .iter()
1951 .map(|verifier| {
1952 Ok(serde_json::json!({
1953 "id": verifier.id,
1954 "owner": verifier.owner,
1955 "inputContracts": sorted_unique_strings(
1956 &verifier.input_contracts,
1957 "$.registries.verifiers.inputContracts",
1958 )?,
1959 }))
1960 })
1961 .collect::<Result<Vec<_>, WeslawError>>()?;
1962 sort_values_by_string_field(&mut verifiers, "id", "$.registries.verifiers")?;
1963
1964 let mut channels = registries
1965 .channels
1966 .iter()
1967 .map(|channel| {
1968 Ok(serde_json::json!({
1969 "name": channel.name,
1970 "version": channel.version,
1971 "carrier": channel.carrier,
1972 }))
1973 })
1974 .collect::<Result<Vec<_>, WeslawError>>()?;
1975 channels.sort_by(|left, right| {
1976 let left_key = (
1977 string_field(left, "name").unwrap_or_default(),
1978 u64_field(left, "version").unwrap_or_default(),
1979 );
1980 let right_key = (
1981 string_field(right, "name").unwrap_or_default(),
1982 u64_field(right, "version").unwrap_or_default(),
1983 );
1984 left_key.cmp(&right_key)
1985 });
1986
1987 Ok(serde_json::json!({
1988 "resources": resources,
1989 "verifiers": verifiers,
1990 "channels": channels,
1991 }))
1992}
1993
1994fn semantic_entry_value(entry: &LawEntryV1) -> Result<serde_json::Value, WeslawError> {
1995 Ok(serde_json::json!({
1996 "id": entry.id,
1997 "kind": law_kind_text(entry.kind),
1998 "subject": entry.subject,
1999 "tags": sorted_unique_strings(&entry.tags, "$.entries.tags")?,
2000 "body": semantic_body_value(entry)?,
2001 }))
2002}
2003
2004fn semantic_body_value(entry: &LawEntryV1) -> Result<serde_json::Value, WeslawError> {
2005 match &entry.body {
2006 LawEntryBodyV1::ScalarSemantics(body) => semantic_scalar_body_value(body),
2007 LawEntryBodyV1::VariantLaw(body) => semantic_variant_body_value(body),
2008 LawEntryBodyV1::FootprintLaw(body) => semantic_footprint_body_value(body),
2009 LawEntryBodyV1::ChannelLaw(body) => semantic_channel_body_value(body),
2010 LawEntryBodyV1::InvariantLaw(body) => {
2011 serde_json::to_value(body).map_err(canonicalization_error)
2012 }
2013 }
2014}
2015
2016fn semantic_scalar_body_value(
2017 body: &ScalarSemanticsLawV1,
2018) -> Result<serde_json::Value, WeslawError> {
2019 let mut object = serde_json::Map::new();
2020 object.insert(
2021 "representation".to_string(),
2022 serde_json::to_value(body.representation).map_err(canonicalization_error)?,
2023 );
2024 if let Some(min) = body.min_inclusive {
2025 object.insert("minInclusive".to_string(), min.into());
2026 }
2027 if let Some(max) = body.max_inclusive {
2028 object.insert("maxInclusive".to_string(), max.into());
2029 }
2030 if let Some(ordering) = body.ordering {
2031 object.insert(
2032 "ordering".to_string(),
2033 serde_json::to_value(ordering).map_err(canonicalization_error)?,
2034 );
2035 }
2036 if let Some(scope) = &body.scope {
2037 object.insert("scope".to_string(), scope.clone().into());
2038 }
2039 let forbids = body
2040 .forbids
2041 .iter()
2042 .map(|item| serde_json::to_value(item).map_err(canonicalization_error))
2043 .collect::<Result<Vec<_>, _>>()?
2044 .into_iter()
2045 .map(|value| {
2046 value
2047 .as_str()
2048 .expect("forbidden interpretation should serialize as a string")
2049 .to_string()
2050 })
2051 .collect::<Vec<_>>();
2052 object.insert(
2053 "forbids".to_string(),
2054 sorted_unique_strings(&forbids, "$.entries.body.forbids")?.into(),
2055 );
2056 Ok(serde_json::Value::Object(object))
2057}
2058
2059fn semantic_variant_body_value(body: &VariantLawV1) -> Result<serde_json::Value, WeslawError> {
2060 let mut cases = body
2061 .cases
2062 .iter()
2063 .map(|case| {
2064 Ok(serde_json::json!({
2065 "value": case.value,
2066 "requires": sorted_unique_strings(&case.requires, "$.entries.body.cases.requires")?,
2067 "forbids": sorted_unique_strings(&case.forbids, "$.entries.body.cases.forbids")?,
2068 }))
2069 })
2070 .collect::<Result<Vec<_>, WeslawError>>()?;
2071 sort_values_by_string_field(&mut cases, "value", "$.entries.body.cases")?;
2072
2073 Ok(serde_json::json!({
2074 "discriminator": {
2075 "field": body.discriminator.field,
2076 "enum": body.discriminator.r#enum,
2077 },
2078 "cases": cases,
2079 }))
2080}
2081
2082fn semantic_footprint_body_value(body: &FootprintLawV1) -> Result<serde_json::Value, WeslawError> {
2083 let mut slots = body
2084 .slots
2085 .iter()
2086 .map(|slot| {
2087 Ok(serde_json::json!({
2088 "name": slot.name,
2089 "kind": slot.kind,
2090 "bindFromArg": slot.bind_from_arg,
2091 "access": sorted_unique_strings(&slot.access, "$.entries.body.slots.access")?,
2092 }))
2093 })
2094 .collect::<Result<Vec<_>, WeslawError>>()?;
2095 sort_values_by_string_field(&mut slots, "name", "$.entries.body.slots")?;
2096
2097 let mut closures = body
2098 .closures
2099 .iter()
2100 .map(|closure| {
2101 Ok(serde_json::json!({
2102 "name": closure.name,
2103 "fromSlot": closure.from_slot,
2104 "operator": closure.operator,
2105 "argBindings": closure.arg_bindings,
2106 "reads": sorted_unique_strings(&closure.reads, "$.entries.body.closures.reads")?,
2107 "cardinality": closure.cardinality,
2108 }))
2109 })
2110 .collect::<Result<Vec<_>, WeslawError>>()?;
2111 sort_values_by_string_field(&mut closures, "name", "$.entries.body.closures")?;
2112
2113 let mut create_slots = body
2114 .create_slots
2115 .iter()
2116 .map(|slot| {
2117 Ok(serde_json::json!({
2118 "name": slot.name,
2119 "kind": slot.kind,
2120 "cardinality": slot.cardinality.unwrap_or(FootprintCardinalityV1::One),
2121 }))
2122 })
2123 .collect::<Result<Vec<_>, WeslawError>>()?;
2124 sort_values_by_string_field(&mut create_slots, "name", "$.entries.body.createSlots")?;
2125
2126 let mut updates = body
2127 .updates
2128 .iter()
2129 .map(|update| {
2130 Ok(serde_json::json!({
2131 "slot": update.slot,
2132 "fields": sorted_unique_strings(&update.fields, "$.entries.body.updates.fields")?,
2133 }))
2134 })
2135 .collect::<Result<Vec<_>, WeslawError>>()?;
2136 sort_values_by_string_field(&mut updates, "slot", "$.entries.body.updates")?;
2137
2138 Ok(serde_json::json!({
2139 "reads": sorted_unique_strings(&body.reads, "$.entries.body.reads")?,
2140 "writes": sorted_unique_strings(&body.writes, "$.entries.body.writes")?,
2141 "creates": sorted_unique_strings(&body.creates, "$.entries.body.creates")?,
2142 "forbids": sorted_unique_strings(&body.forbids, "$.entries.body.forbids")?,
2143 "slots": slots,
2144 "closures": closures,
2145 "createSlots": create_slots,
2146 "updates": updates,
2147 }))
2148}
2149
2150fn semantic_channel_body_value(body: &ChannelLawV1) -> Result<serde_json::Value, WeslawError> {
2151 let mut object = serde_json::Map::new();
2152 object.insert("ordered".to_string(), body.ordered.into());
2153 object.insert("version".to_string(), body.version.into());
2154 if let Some(compatibility) = &body.compatibility {
2155 object.insert(
2156 "compatibility".to_string(),
2157 serde_json::to_value(compatibility).map_err(canonicalization_error)?,
2158 );
2159 }
2160 object.insert(
2161 "messages".to_string(),
2162 serde_json::to_value(&body.messages).map_err(canonicalization_error)?,
2163 );
2164 Ok(serde_json::Value::Object(object))
2165}
2166
2167fn compute_bundle_hash_v1(
2168 schema_hash: &str,
2169 law_hash: &str,
2170 profile_hash: &str,
2171 law_ir_codec: &str,
2172 bundle_hash_codec: &str,
2173 compiler: &str,
2174 compiler_version: &str,
2175) -> Result<String, WeslawError> {
2176 let input = serde_json::json!({
2177 "schemaHash": schema_hash,
2178 "lawHash": law_hash,
2179 "profileHash": profile_hash,
2180 "lawIrCodec": law_ir_codec,
2181 "bundleHashCodec": bundle_hash_codec,
2182 "compiler": compiler,
2183 "compilerVersion": compiler_version,
2184 });
2185 let bytes = to_canonical_json(&input).map_err(canonicalization_error)?;
2186 Ok(prefixed_sha256(&bytes))
2187}
2188
2189fn empty_profile_hash_v1() -> Result<String, WeslawError> {
2190 let input = serde_json::json!({
2191 "apiVersion": WESLEY_EMPTY_PROFILE_API_VERSION,
2192 "entries": [],
2193 });
2194 let bytes = to_canonical_json(&input).map_err(canonicalization_error)?;
2195 Ok(prefixed_sha256(&bytes))
2196}
2197
2198fn sorted_unique_strings(values: &[String], path: &str) -> Result<Vec<String>, WeslawError> {
2199 let mut sorted = values.to_vec();
2200 sorted.sort();
2201 if let Some((duplicate, _)) = sorted
2202 .windows(2)
2203 .find_map(|window| (window[0] == window[1]).then(|| (&window[0], &window[1])))
2204 {
2205 return Err(WeslawError::at_path(
2206 WeslawDiagnosticCode::Conflict,
2207 path,
2208 format!("duplicate set-like value {duplicate}"),
2209 ));
2210 }
2211 Ok(sorted)
2212}
2213
2214fn sort_values_by_string_field(
2215 values: &mut [serde_json::Value],
2216 field: &str,
2217 path: &str,
2218) -> Result<(), WeslawError> {
2219 values.sort_by(|left, right| {
2220 string_field(left, field)
2221 .unwrap_or_default()
2222 .cmp(string_field(right, field).unwrap_or_default())
2223 });
2224 if let Some(duplicate) = values.windows(2).find_map(|window| {
2225 let left = string_field(&window[0], field)?;
2226 let right = string_field(&window[1], field)?;
2227 (left == right).then_some(left)
2228 }) {
2229 return Err(WeslawError::at_path(
2230 WeslawDiagnosticCode::Conflict,
2231 path,
2232 format!("duplicate set-like key {duplicate}"),
2233 ));
2234 }
2235 Ok(())
2236}
2237
2238fn string_field<'a>(value: &'a serde_json::Value, field: &str) -> Option<&'a str> {
2239 value.get(field).and_then(serde_json::Value::as_str)
2240}
2241
2242fn u64_field(value: &serde_json::Value, field: &str) -> Option<u64> {
2243 value.get(field).and_then(serde_json::Value::as_u64)
2244}
2245
2246fn law_kind_text(kind: LawKindV1) -> &'static str {
2247 match kind {
2248 LawKindV1::ScalarSemantics => "scalarSemantics",
2249 LawKindV1::VariantLaw => "variantLaw",
2250 LawKindV1::FootprintLaw => "footprintLaw",
2251 LawKindV1::ChannelLaw => "channelLaw",
2252 LawKindV1::InvariantLaw => "invariantLaw",
2253 }
2254}
2255
2256fn prefixed_sha256(value: &str) -> String {
2257 prefixed_sha256_hex(&compute_content_hash(value))
2258}
2259
2260fn prefixed_sha256_hex(value: &str) -> String {
2261 format!("sha256:{value}")
2262}
2263
2264fn canonicalization_error(error: impl std::fmt::Display) -> WeslawError {
2265 WeslawError::new(
2266 WeslawDiagnosticCode::InvalidDocument,
2267 format!("Law IR canonicalization failed: {error}"),
2268 )
2269}
2270
2271struct BindingContext<'a> {
2272 schema_ir: &'a WesleyIR,
2273 operations: &'a [SchemaOperation],
2274 law_ir: &'a LawIrV1,
2275}
2276
2277impl BindingContext<'_> {
2278 fn bind_entry(
2279 &self,
2280 entry: &LawEntryV1,
2281 coordinate: SubjectCoordinate<'_>,
2282 path: &str,
2283 ) -> Result<(), WeslawError> {
2284 match entry.kind {
2285 LawKindV1::ScalarSemantics => self.bind_scalar_semantics(entry, coordinate, path),
2286 LawKindV1::VariantLaw => self.bind_variant_law(entry, coordinate, path),
2287 LawKindV1::FootprintLaw => self.bind_footprint_law(entry, coordinate, path),
2288 LawKindV1::ChannelLaw => self.bind_channel_law(entry, coordinate, path),
2289 LawKindV1::InvariantLaw => self.bind_invariant_law(entry, coordinate, path),
2290 }
2291 }
2292
2293 fn bind_scalar_semantics(
2294 &self,
2295 entry: &LawEntryV1,
2296 coordinate: SubjectCoordinate<'_>,
2297 path: &str,
2298 ) -> Result<(), WeslawError> {
2299 let SubjectCoordinate::Scalar(name) = coordinate else {
2300 return Err(wrong_subject_kind(entry, path, "scalar:<Name>"));
2301 };
2302 self.require_type(name, TypeKind::Scalar, entry, path)?;
2303 Ok(())
2304 }
2305
2306 fn bind_variant_law(
2307 &self,
2308 entry: &LawEntryV1,
2309 coordinate: SubjectCoordinate<'_>,
2310 path: &str,
2311 ) -> Result<(), WeslawError> {
2312 let SubjectCoordinate::Input(name) = coordinate else {
2313 return Err(wrong_subject_kind(entry, path, "input:<Name>"));
2314 };
2315 let input = self.require_type(name, TypeKind::InputObject, entry, path)?;
2316 let LawEntryBodyV1::VariantLaw(body) = &entry.body else {
2317 return Err(wrong_subject_kind(entry, path, "variant law body"));
2318 };
2319
2320 let discriminator = self.require_input_field(
2321 input,
2322 &body.discriminator.field,
2323 entry,
2324 format!("{path}.discriminator.field"),
2325 )?;
2326 if discriminator.r#type.base != body.discriminator.r#enum {
2327 return Err(unresolved_reference(
2328 entry,
2329 format!("{path}.discriminator.enum"),
2330 format!(
2331 "discriminator field {} has type {}, not enum {}",
2332 body.discriminator.field, discriminator.r#type.base, body.discriminator.r#enum
2333 ),
2334 ));
2335 }
2336 let enum_type = self.require_type_reference(
2337 &body.discriminator.r#enum,
2338 TypeKind::Enum,
2339 entry,
2340 format!("{path}.discriminator.enum"),
2341 )?;
2342
2343 for (case_index, case) in body.cases.iter().enumerate() {
2344 let case_path = format!("{path}.cases[{case_index}]");
2345 if !enum_type
2346 .enum_values
2347 .iter()
2348 .any(|candidate| candidate == &case.value)
2349 {
2350 return Err(unresolved_reference(
2351 entry,
2352 format!("{case_path}.value"),
2353 format!(
2354 "enum {} has no value {}",
2355 body.discriminator.r#enum, case.value
2356 ),
2357 ));
2358 }
2359 for field in &case.requires {
2360 self.require_input_field(input, field, entry, format!("{case_path}.requires"))?;
2361 }
2362 for field in &case.forbids {
2363 self.require_input_field(input, field, entry, format!("{case_path}.forbids"))?;
2364 }
2365 if let Some(field) = first_overlap(&case.requires, &case.forbids) {
2366 return Err(conflict(
2367 entry,
2368 case_path,
2369 format!(
2370 "variant case {} both requires and forbids {field}",
2371 case.value
2372 ),
2373 ));
2374 }
2375 }
2376 Ok(())
2377 }
2378
2379 fn bind_footprint_law(
2380 &self,
2381 entry: &LawEntryV1,
2382 coordinate: SubjectCoordinate<'_>,
2383 path: &str,
2384 ) -> Result<(), WeslawError> {
2385 let SubjectCoordinate::Operation {
2386 operation_type,
2387 field,
2388 } = coordinate
2389 else {
2390 return Err(wrong_subject_kind(
2391 entry,
2392 path,
2393 "operation:Query.<field>, operation:Mutation.<field>, or operation:Subscription.<field>",
2394 ));
2395 };
2396 let operation = self.require_operation(operation_type, field, entry, path)?;
2397 let LawEntryBodyV1::FootprintLaw(body) = &entry.body else {
2398 return Err(wrong_subject_kind(entry, path, "footprint law body"));
2399 };
2400
2401 for resource in body
2402 .reads
2403 .iter()
2404 .chain(body.writes.iter())
2405 .chain(body.creates.iter())
2406 .chain(body.forbids.iter())
2407 .chain(body.slots.iter().map(|slot| &slot.kind))
2408 .chain(
2409 body.closures
2410 .iter()
2411 .flat_map(|closure| closure.reads.iter()),
2412 )
2413 .chain(body.create_slots.iter().map(|slot| &slot.kind))
2414 {
2415 self.require_resource(resource, entry, format!("{path}.resources"))?;
2416 }
2417
2418 let mut slot_names = HashSet::new();
2419 for (slot_index, slot) in body.slots.iter().enumerate() {
2420 let slot_path = format!("{path}.slots[{slot_index}]");
2421 if !slot_names.insert(slot.name.as_str()) {
2422 return Err(conflict(
2423 entry,
2424 format!("{slot_path}.name"),
2425 format!("duplicate footprint slot {}", slot.name),
2426 ));
2427 }
2428 self.require_arg_path(
2429 operation,
2430 &slot.bind_from_arg,
2431 entry,
2432 format!("{slot_path}.bindFromArg"),
2433 )?;
2434 }
2435
2436 let mut create_slot_names = HashSet::new();
2437 for (slot_index, slot) in body.create_slots.iter().enumerate() {
2438 if !create_slot_names.insert(slot.name.as_str()) {
2439 return Err(conflict(
2440 entry,
2441 format!("{path}.createSlots[{slot_index}].name"),
2442 format!("duplicate create slot {}", slot.name),
2443 ));
2444 }
2445 }
2446
2447 for (closure_index, closure) in body.closures.iter().enumerate() {
2448 let closure_path = format!("{path}.closures[{closure_index}]");
2449 if !slot_names.contains(closure.from_slot.as_str()) {
2450 return Err(unresolved_reference(
2451 entry,
2452 format!("{closure_path}.fromSlot"),
2453 format!("closure source slot {} is not declared", closure.from_slot),
2454 ));
2455 }
2456 for binding in &closure.arg_bindings {
2457 if !slot_names.contains(binding.as_str()) {
2458 self.require_arg_path(
2459 operation,
2460 binding,
2461 entry,
2462 format!("{closure_path}.argBindings"),
2463 )?;
2464 }
2465 }
2466 }
2467
2468 for (update_index, update) in body.updates.iter().enumerate() {
2469 if !slot_names.contains(update.slot.as_str()) {
2470 return Err(unresolved_reference(
2471 entry,
2472 format!("{path}.updates[{update_index}].slot"),
2473 format!("update slot {} is not declared", update.slot),
2474 ));
2475 }
2476 }
2477
2478 let touched_resources = body
2479 .reads
2480 .iter()
2481 .chain(body.writes.iter())
2482 .chain(body.creates.iter())
2483 .chain(body.slots.iter().map(|slot| &slot.kind))
2484 .chain(
2485 body.closures
2486 .iter()
2487 .flat_map(|closure| closure.reads.iter()),
2488 )
2489 .chain(body.create_slots.iter().map(|slot| &slot.kind))
2490 .cloned()
2491 .collect::<Vec<_>>();
2492 if let Some(resource) = first_overlap(&body.forbids, &touched_resources) {
2493 return Err(conflict(
2494 entry,
2495 format!("{path}.forbids"),
2496 format!("resource {resource} is both forbidden and used"),
2497 ));
2498 }
2499 Ok(())
2500 }
2501
2502 fn bind_channel_law(
2503 &self,
2504 entry: &LawEntryV1,
2505 coordinate: SubjectCoordinate<'_>,
2506 path: &str,
2507 ) -> Result<(), WeslawError> {
2508 let SubjectCoordinate::Channel { name, version } = coordinate else {
2509 return Err(wrong_subject_kind(entry, path, "channel:<name>@<version>"));
2510 };
2511 let LawEntryBodyV1::ChannelLaw(body) = &entry.body else {
2512 return Err(wrong_subject_kind(entry, path, "channel law body"));
2513 };
2514 if body.version != version {
2515 return Err(WeslawError::at_path(
2516 WeslawDiagnosticCode::InvalidDocument,
2517 path,
2518 format!(
2519 "channel subject {} expects version {version}, but law body declares version {}",
2520 entry.subject, body.version
2521 ),
2522 ));
2523 }
2524 let carrier = self
2525 .channel_carrier(name, version)
2526 .ok_or_else(|| unresolved_subject(entry, path, Vec::new()))?;
2527 let carrier_type = self.require_type_reference(
2528 carrier,
2529 TypeKind::Object,
2530 entry,
2531 format!("{path}.carrier"),
2532 )?;
2533 for (message_index, message) in body.messages.iter().enumerate() {
2534 let message_path = format!("{path}.messages[{message_index}]");
2535 let field = self.require_field_on_type(
2536 carrier_type,
2537 &message.field,
2538 entry,
2539 format!("{message_path}.field"),
2540 )?;
2541 if field.r#type.base != message.r#type {
2542 return Err(unresolved_reference(
2543 entry,
2544 format!("{message_path}.type"),
2545 format!(
2546 "channel field {} has type {}, not {}",
2547 message.field, field.r#type.base, message.r#type
2548 ),
2549 ));
2550 }
2551 self.require_type_reference(
2552 &message.r#type,
2553 TypeKind::Object,
2554 entry,
2555 format!("{message_path}.type"),
2556 )?;
2557 }
2558 Ok(())
2559 }
2560
2561 fn bind_invariant_law(
2562 &self,
2563 entry: &LawEntryV1,
2564 coordinate: SubjectCoordinate<'_>,
2565 path: &str,
2566 ) -> Result<(), WeslawError> {
2567 match coordinate {
2568 SubjectCoordinate::Type(name) => {
2569 self.require_type(name, TypeKind::Object, entry, path)?;
2570 }
2571 SubjectCoordinate::Input(name) => {
2572 self.require_type(name, TypeKind::InputObject, entry, path)?;
2573 }
2574 SubjectCoordinate::Enum(name) => {
2575 self.require_type(name, TypeKind::Enum, entry, path)?;
2576 }
2577 SubjectCoordinate::Field { owner, field } => {
2578 self.require_field(owner, field, entry, path)?;
2579 }
2580 SubjectCoordinate::Operation {
2581 operation_type,
2582 field,
2583 } => {
2584 self.require_operation(operation_type, field, entry, path)?;
2585 }
2586 SubjectCoordinate::Family(name) if name == self.law_ir.family => {}
2587 _ => {
2588 return Err(wrong_subject_kind(
2589 entry,
2590 path,
2591 "type:<Name>, input:<Name>, enum:<Name>, field:<Type>.<field>, operation:<Root>.<field>, or family:<name>",
2592 ));
2593 }
2594 }
2595 let LawEntryBodyV1::InvariantLaw(body) = &entry.body else {
2596 return Err(wrong_subject_kind(entry, path, "invariant law body"));
2597 };
2598 match &body.predicate {
2599 PredicateV1::FieldEquals { field, .. } => {
2600 self.require_predicate_field(
2601 coordinate,
2602 field,
2603 entry,
2604 format!("{path}.predicate.field"),
2605 )?;
2606 }
2607 PredicateV1::External { verifier, .. } => {
2608 if !self
2609 .law_ir
2610 .registries
2611 .verifiers
2612 .iter()
2613 .any(|candidate| candidate.id == *verifier)
2614 {
2615 return Err(unresolved_reference(
2616 entry,
2617 format!("{path}.predicate.verifier"),
2618 format!("verifier {verifier} is not declared"),
2619 ));
2620 }
2621 }
2622 }
2623 Ok(())
2624 }
2625
2626 fn require_type(
2627 &self,
2628 name: &str,
2629 expected_kind: TypeKind,
2630 entry: &LawEntryV1,
2631 path: &str,
2632 ) -> Result<&TypeDefinition, WeslawError> {
2633 match self.find_type(name) {
2634 Some(definition) if definition.kind == expected_kind => Ok(definition),
2635 Some(_) => Err(wrong_subject_kind(
2636 entry,
2637 path,
2638 expected_kind_label(expected_kind),
2639 )),
2640 None => Err(unresolved_subject(
2641 entry,
2642 path,
2643 self.closest_type_subjects(name),
2644 )),
2645 }
2646 }
2647
2648 fn require_type_reference(
2649 &self,
2650 name: &str,
2651 expected_kind: TypeKind,
2652 entry: &LawEntryV1,
2653 path: impl Into<String>,
2654 ) -> Result<&TypeDefinition, WeslawError> {
2655 match self.find_type(name) {
2656 Some(definition) if definition.kind == expected_kind => Ok(definition),
2657 Some(definition) => Err(unresolved_reference(
2658 entry,
2659 path,
2660 format!(
2661 "type {name} has kind {:?}, expected {:?}",
2662 definition.kind, expected_kind
2663 ),
2664 )),
2665 None => Err(unresolved_reference(
2666 entry,
2667 path,
2668 format!("type {name} is not declared"),
2669 )),
2670 }
2671 }
2672
2673 fn require_input_field<'a>(
2674 &self,
2675 input: &'a TypeDefinition,
2676 field: &str,
2677 entry: &LawEntryV1,
2678 path: impl Into<String>,
2679 ) -> Result<&'a Field, WeslawError> {
2680 if input.kind != TypeKind::InputObject {
2681 return Err(unresolved_reference(
2682 entry,
2683 path,
2684 format!("{} is not an input object", input.name),
2685 ));
2686 }
2687 self.require_field_on_type(input, field, entry, path)
2688 }
2689
2690 fn require_field_on_type<'a>(
2691 &self,
2692 definition: &'a TypeDefinition,
2693 field: &str,
2694 entry: &LawEntryV1,
2695 path: impl Into<String>,
2696 ) -> Result<&'a Field, WeslawError> {
2697 let path = path.into();
2698 definition
2699 .fields
2700 .iter()
2701 .find(|candidate| candidate.name == field)
2702 .ok_or_else(|| {
2703 unresolved_reference(
2704 entry,
2705 path,
2706 format!("{} has no field {field}", definition.name),
2707 )
2708 })
2709 }
2710
2711 fn require_field(
2712 &self,
2713 owner: &str,
2714 field: &str,
2715 entry: &LawEntryV1,
2716 path: &str,
2717 ) -> Result<(), WeslawError> {
2718 let Some(definition) = self.find_type(owner) else {
2719 return Err(unresolved_subject(
2720 entry,
2721 path,
2722 self.closest_type_subjects(owner),
2723 ));
2724 };
2725 if !matches!(
2726 definition.kind,
2727 TypeKind::Object | TypeKind::Interface | TypeKind::InputObject
2728 ) {
2729 return Err(wrong_subject_kind(
2730 entry,
2731 path,
2732 "field:<ObjectOrInterfaceOrInput>.<field>",
2733 ));
2734 }
2735 if definition
2736 .fields
2737 .iter()
2738 .any(|candidate| candidate.name == field)
2739 {
2740 Ok(())
2741 } else {
2742 Err(unresolved_subject(
2743 entry,
2744 path,
2745 definition
2746 .fields
2747 .iter()
2748 .map(|candidate| format!("field:{owner}.{}", candidate.name))
2749 .collect(),
2750 ))
2751 }
2752 }
2753
2754 fn require_operation(
2755 &self,
2756 operation_type: OperationType,
2757 field: &str,
2758 entry: &LawEntryV1,
2759 path: &str,
2760 ) -> Result<&SchemaOperation, WeslawError> {
2761 if let Some(operation) = self.operations.iter().find(|candidate| {
2762 candidate.operation_type == operation_type && candidate.field_name == field
2763 }) {
2764 Ok(operation)
2765 } else {
2766 Err(unresolved_subject(
2767 entry,
2768 path,
2769 self.closest_operation_subjects(operation_type),
2770 ))
2771 }
2772 }
2773
2774 fn require_resource(
2775 &self,
2776 resource: &str,
2777 entry: &LawEntryV1,
2778 path: impl Into<String>,
2779 ) -> Result<(), WeslawError> {
2780 let registry_resource_exists = self
2781 .law_ir
2782 .registries
2783 .resources
2784 .iter()
2785 .any(|candidate| candidate.id == resource);
2786 if let Some(definition) = self.find_type(resource) {
2787 if definition.kind == TypeKind::Object || registry_resource_exists {
2788 return Ok(());
2789 }
2790 return Err(WeslawError::at_path(
2791 WeslawDiagnosticCode::WrongSubjectKind,
2792 path.into(),
2793 format!(
2794 "law {} has footprint resource {resource}, but schema-backed resources must be object types or explicit registry entries; got {:?}",
2795 entry.id, definition.kind
2796 ),
2797 ));
2798 }
2799 if registry_resource_exists {
2800 Ok(())
2801 } else {
2802 Err(unresolved_reference(
2803 entry,
2804 path,
2805 format!("resource {resource} is neither a GraphQL type nor registry entry"),
2806 ))
2807 }
2808 }
2809
2810 fn require_arg_path(
2811 &self,
2812 operation: &SchemaOperation,
2813 arg_path: &str,
2814 entry: &LawEntryV1,
2815 path: impl Into<String>,
2816 ) -> Result<(), WeslawError> {
2817 let path = path.into();
2818 let mut segments = arg_path.split('.');
2819 let Some(arg_name) = segments.next() else {
2820 return Err(unresolved_reference(entry, path, "empty argument path"));
2821 };
2822 let Some(argument) = operation
2823 .arguments
2824 .iter()
2825 .find(|candidate| candidate.name == arg_name)
2826 else {
2827 return Err(unresolved_reference(
2828 entry,
2829 path,
2830 format!(
2831 "operation {}.{} has no argument {arg_name}",
2832 operation_type_coordinate(operation.operation_type),
2833 operation.field_name
2834 ),
2835 ));
2836 };
2837
2838 let mut current_type = argument.r#type.base.as_str();
2839 for segment in segments {
2840 let definition = self.require_type_reference(
2841 current_type,
2842 TypeKind::InputObject,
2843 entry,
2844 path.clone(),
2845 )?;
2846 let field = self.require_input_field(definition, segment, entry, path.clone())?;
2847 current_type = field.r#type.base.as_str();
2848 }
2849 Ok(())
2850 }
2851
2852 fn require_predicate_field(
2853 &self,
2854 coordinate: SubjectCoordinate<'_>,
2855 field_path: &str,
2856 entry: &LawEntryV1,
2857 path: impl Into<String>,
2858 ) -> Result<(), WeslawError> {
2859 let path = path.into();
2860 let root = match coordinate {
2861 SubjectCoordinate::Type(name) | SubjectCoordinate::Input(name) => name,
2862 SubjectCoordinate::Field { owner, .. } => owner,
2863 _ => {
2864 return Err(unresolved_reference(
2865 entry,
2866 path.clone(),
2867 "fieldEquals predicates require a type, input, or field subject",
2868 ));
2869 }
2870 };
2871 let mut current_type = root;
2872 for segment in field_path.split('.') {
2873 let definition = self.find_type(current_type).ok_or_else(|| {
2874 unresolved_reference(
2875 entry,
2876 path.clone(),
2877 format!("type {current_type} is not declared"),
2878 )
2879 })?;
2880 let field = self.require_field_on_type(definition, segment, entry, path.clone())?;
2881 current_type = field.r#type.base.as_str();
2882 }
2883 Ok(())
2884 }
2885
2886 fn find_type(&self, name: &str) -> Option<&TypeDefinition> {
2887 self.schema_ir
2888 .types
2889 .iter()
2890 .find(|candidate| candidate.name == name)
2891 }
2892
2893 fn closest_type_subjects(&self, name: &str) -> Vec<String> {
2894 let mut candidates = self
2895 .schema_ir
2896 .types
2897 .iter()
2898 .filter(|candidate| shares_prefix(&candidate.name, name))
2899 .map(|candidate| {
2900 format!(
2901 "{}:{}",
2902 coordinate_prefix_for_type(candidate.kind),
2903 candidate.name
2904 )
2905 })
2906 .collect::<Vec<_>>();
2907 candidates.sort();
2908 candidates
2909 }
2910
2911 fn closest_operation_subjects(&self, operation_type: OperationType) -> Vec<String> {
2912 let mut candidates = self
2913 .operations
2914 .iter()
2915 .filter(|candidate| candidate.operation_type == operation_type)
2916 .map(|candidate| {
2917 format!(
2918 "operation:{}.{}",
2919 operation_type_coordinate(candidate.operation_type),
2920 candidate.field_name
2921 )
2922 })
2923 .collect::<Vec<_>>();
2924 candidates.sort();
2925 candidates
2926 }
2927
2928 fn channel_carrier(&self, name: &str, version: u64) -> Option<&str> {
2929 if let Some(channel) = self
2930 .law_ir
2931 .registries
2932 .channels
2933 .iter()
2934 .find(|channel| channel.name == name && channel.version == version)
2935 {
2936 return Some(channel.carrier.as_str());
2937 }
2938
2939 self.schema_ir
2940 .types
2941 .iter()
2942 .find(|definition| {
2943 definition
2944 .directives
2945 .get("wes_channel")
2946 .is_some_and(|value| {
2947 value.get("name").and_then(serde_json::Value::as_str) == Some(name)
2948 && value.get("version").and_then(serde_json::Value::as_u64)
2949 == Some(version)
2950 })
2951 })
2952 .map(|definition| definition.name.as_str())
2953 }
2954}
2955
2956#[derive(Clone, Copy)]
2957enum SubjectCoordinate<'a> {
2958 Scalar(&'a str),
2959 Type(&'a str),
2960 Input(&'a str),
2961 Enum(&'a str),
2962 Field {
2963 owner: &'a str,
2964 field: &'a str,
2965 },
2966 Operation {
2967 operation_type: OperationType,
2968 field: &'a str,
2969 },
2970 Channel {
2971 name: &'a str,
2972 version: u64,
2973 },
2974 Family(&'a str),
2975}
2976
2977fn parse_subject_coordinate<'a>(
2978 subject: &'a str,
2979 path: &str,
2980) -> Result<SubjectCoordinate<'a>, WeslawError> {
2981 if let Some(name) = subject.strip_prefix("scalar:") {
2982 require_graphql_name(name, subject, path)?;
2983 return Ok(SubjectCoordinate::Scalar(name));
2984 }
2985 if let Some(name) = subject.strip_prefix("type:") {
2986 require_graphql_name(name, subject, path)?;
2987 return Ok(SubjectCoordinate::Type(name));
2988 }
2989 if let Some(name) = subject.strip_prefix("input:") {
2990 require_graphql_name(name, subject, path)?;
2991 return Ok(SubjectCoordinate::Input(name));
2992 }
2993 if let Some(name) = subject.strip_prefix("enum:") {
2994 require_graphql_name(name, subject, path)?;
2995 return Ok(SubjectCoordinate::Enum(name));
2996 }
2997 if let Some(rest) = subject.strip_prefix("field:") {
2998 let (owner, field) = split_once(rest, '.', subject, path)?;
2999 require_graphql_name(owner, subject, path)?;
3000 require_graphql_name(field, subject, path)?;
3001 return Ok(SubjectCoordinate::Field { owner, field });
3002 }
3003 if let Some(rest) = subject.strip_prefix("operation:") {
3004 let (root, field) = split_once(rest, '.', subject, path)?;
3005 let operation_type = parse_operation_type(root, subject, path)?;
3006 require_graphql_name(field, subject, path)?;
3007 return Ok(SubjectCoordinate::Operation {
3008 operation_type,
3009 field,
3010 });
3011 }
3012 if let Some(rest) = subject.strip_prefix("channel:") {
3013 let (name, version_text) = split_once(rest, '@', subject, path)?;
3014 require_dotted_name(name, subject, path)?;
3015 let version = version_text.parse::<u64>().map_err(|_| {
3016 invalid_coordinate(subject, path, "channel version must be an unsigned integer")
3017 })?;
3018 return Ok(SubjectCoordinate::Channel { name, version });
3019 }
3020 if let Some(name) = subject.strip_prefix("family:") {
3021 require_dotted_name(name, subject, path)?;
3022 return Ok(SubjectCoordinate::Family(name));
3023 }
3024
3025 Err(invalid_coordinate(
3026 subject,
3027 path,
3028 "unknown subject coordinate prefix",
3029 ))
3030}
3031
3032fn validate_schema_hash_anchor(schema_hash: &str, path: &str) -> Result<(), WeslawError> {
3033 let Some(hex) = schema_hash.strip_prefix("sha256:") else {
3034 return Err(WeslawError::at_path(
3035 WeslawDiagnosticCode::InvalidDocument,
3036 path,
3037 "schema hash must use sha256:<64 lowercase hex>",
3038 ));
3039 };
3040 if hex.len() == 64
3041 && hex
3042 .chars()
3043 .all(|character| character.is_ascii_hexdigit() && !character.is_ascii_uppercase())
3044 {
3045 Ok(())
3046 } else {
3047 Err(WeslawError::at_path(
3048 WeslawDiagnosticCode::InvalidDocument,
3049 path,
3050 "schema hash must use sha256:<64 lowercase hex>",
3051 ))
3052 }
3053}
3054
3055fn split_once<'a>(
3056 value: &'a str,
3057 delimiter: char,
3058 subject: &str,
3059 path: &str,
3060) -> Result<(&'a str, &'a str), WeslawError> {
3061 let Some((left, right)) = value.split_once(delimiter) else {
3062 return Err(invalid_coordinate(
3063 subject,
3064 path,
3065 "missing coordinate delimiter",
3066 ));
3067 };
3068 if left.is_empty() || right.is_empty() || right.contains(delimiter) {
3069 return Err(invalid_coordinate(
3070 subject,
3071 path,
3072 "invalid coordinate segments",
3073 ));
3074 }
3075 Ok((left, right))
3076}
3077
3078fn parse_operation_type(
3079 value: &str,
3080 subject: &str,
3081 path: &str,
3082) -> Result<OperationType, WeslawError> {
3083 match value {
3084 "Query" => Ok(OperationType::Query),
3085 "Mutation" => Ok(OperationType::Mutation),
3086 "Subscription" => Ok(OperationType::Subscription),
3087 _ => Err(invalid_coordinate(
3088 subject,
3089 path,
3090 "operation root must be Query, Mutation, or Subscription",
3091 )),
3092 }
3093}
3094
3095fn require_graphql_name(name: &str, subject: &str, path: &str) -> Result<(), WeslawError> {
3096 let mut chars = name.chars();
3097 let Some(first) = chars.next() else {
3098 return Err(invalid_coordinate(subject, path, "empty GraphQL name"));
3099 };
3100 if !(first == '_' || first.is_ascii_alphabetic()) {
3101 return Err(invalid_coordinate(subject, path, "invalid GraphQL name"));
3102 }
3103 if chars.all(|character| character == '_' || character.is_ascii_alphanumeric()) {
3104 Ok(())
3105 } else {
3106 Err(invalid_coordinate(subject, path, "invalid GraphQL name"))
3107 }
3108}
3109
3110fn require_dotted_name(value: &str, subject: &str, path: &str) -> Result<(), WeslawError> {
3111 if value.split('.').all(is_dotted_token) {
3112 Ok(())
3113 } else {
3114 Err(invalid_coordinate(subject, path, "invalid dotted name"))
3115 }
3116}
3117
3118fn is_dotted_token(value: &str) -> bool {
3119 let mut chars = value.chars();
3120 let Some(first) = chars.next() else {
3121 return false;
3122 };
3123 first.is_ascii_lowercase()
3124 && chars.all(|character| {
3125 character.is_ascii_lowercase() || character.is_ascii_digit() || character == '-'
3126 })
3127}
3128
3129fn invalid_coordinate(subject: &str, path: &str, detail: &str) -> WeslawError {
3130 WeslawError::at_path(
3131 WeslawDiagnosticCode::InvalidCoordinate,
3132 path,
3133 format!("{detail}: {subject}"),
3134 )
3135}
3136
3137fn unresolved_subject(entry: &LawEntryV1, path: &str, closest_matches: Vec<String>) -> WeslawError {
3138 let mut message = format!(
3139 "unresolved subject coordinate {} for law {}",
3140 entry.subject, entry.id
3141 );
3142 if !closest_matches.is_empty() {
3143 message.push_str("; closest matches: ");
3144 message.push_str(&closest_matches.join(", "));
3145 }
3146 WeslawError::at_path(
3147 WeslawDiagnosticCode::UnresolvedSubject,
3148 subject_path(path),
3149 message,
3150 )
3151}
3152
3153fn wrong_subject_kind(entry: &LawEntryV1, path: &str, expected: &str) -> WeslawError {
3154 WeslawError::at_path(
3155 WeslawDiagnosticCode::WrongSubjectKind,
3156 subject_path(path),
3157 format!(
3158 "law {} with kind {:?} requires subject kind {expected}, got {}",
3159 entry.id, entry.kind, entry.subject
3160 ),
3161 )
3162}
3163
3164fn unresolved_reference(
3165 entry: &LawEntryV1,
3166 path: impl Into<String>,
3167 detail: impl Into<String>,
3168) -> WeslawError {
3169 WeslawError::at_path(
3170 WeslawDiagnosticCode::UnresolvedReference,
3171 path,
3172 format!(
3173 "law {} has unresolved reference: {}",
3174 entry.id,
3175 detail.into()
3176 ),
3177 )
3178}
3179
3180fn conflict(entry: &LawEntryV1, path: impl Into<String>, detail: impl Into<String>) -> WeslawError {
3181 WeslawError::at_path(
3182 WeslawDiagnosticCode::Conflict,
3183 path,
3184 format!("law {} is contradictory: {}", entry.id, detail.into()),
3185 )
3186}
3187
3188fn subject_path(path: &str) -> String {
3189 if path.ends_with(".subject") {
3190 path.to_string()
3191 } else {
3192 format!("{path}.subject")
3193 }
3194}
3195
3196fn expected_kind_label(kind: TypeKind) -> &'static str {
3197 match kind {
3198 TypeKind::Object => "type:<Name>",
3199 TypeKind::Interface => "interface:<Name>",
3200 TypeKind::Union => "union:<Name>",
3201 TypeKind::Enum => "enum:<Name>",
3202 TypeKind::Scalar => "scalar:<Name>",
3203 TypeKind::InputObject => "input:<Name>",
3204 }
3205}
3206
3207fn coordinate_prefix_for_type(kind: TypeKind) -> &'static str {
3208 match kind {
3209 TypeKind::Object | TypeKind::Interface | TypeKind::Union => "type",
3210 TypeKind::Enum => "enum",
3211 TypeKind::Scalar => "scalar",
3212 TypeKind::InputObject => "input",
3213 }
3214}
3215
3216fn operation_type_coordinate(operation_type: OperationType) -> &'static str {
3217 match operation_type {
3218 OperationType::Query => "Query",
3219 OperationType::Mutation => "Mutation",
3220 OperationType::Subscription => "Subscription",
3221 }
3222}
3223
3224fn law_kind_has_unique_subject(kind: LawKindV1) -> bool {
3225 !matches!(kind, LawKindV1::InvariantLaw)
3226}
3227
3228fn first_overlap(left: &[String], right: &[String]) -> Option<String> {
3229 let right = right.iter().collect::<HashSet<_>>();
3230 left.iter()
3231 .filter(|candidate| right.contains(candidate))
3232 .min()
3233 .cloned()
3234}
3235
3236fn shares_prefix(left: &str, right: &str) -> bool {
3237 left.starts_with(right) || right.starts_with(left)
3238}
3239
3240fn parse_registries(map: &Mapping) -> Result<LawRegistrySetV1, WeslawError> {
3241 reject_unknown_fields(map, "$.registries", &["resources", "verifiers", "channels"])?;
3242 Ok(LawRegistrySetV1 {
3243 resources: optional_sequence(map, "resources", "$.registries.resources")?
3244 .unwrap_or_default()
3245 .iter()
3246 .enumerate()
3247 .map(|(index, value)| {
3248 parse_resource_registry_entry(value, &format!("$.registries.resources[{index}]"))
3249 })
3250 .collect::<Result<Vec<_>, _>>()?,
3251 verifiers: optional_sequence(map, "verifiers", "$.registries.verifiers")?
3252 .unwrap_or_default()
3253 .iter()
3254 .enumerate()
3255 .map(|(index, value)| {
3256 parse_verifier_registry_entry(value, &format!("$.registries.verifiers[{index}]"))
3257 })
3258 .collect::<Result<Vec<_>, _>>()?,
3259 channels: optional_sequence(map, "channels", "$.registries.channels")?
3260 .unwrap_or_default()
3261 .iter()
3262 .enumerate()
3263 .map(|(index, value)| {
3264 parse_channel_registry_entry(value, &format!("$.registries.channels[{index}]"))
3265 })
3266 .collect::<Result<Vec<_>, _>>()?,
3267 })
3268}
3269
3270fn parse_resource_registry_entry(
3271 value: &Yaml,
3272 path: &str,
3273) -> Result<ResourceRegistryEntryV1, WeslawError> {
3274 let map = expect_mapping(value, path)?;
3275 reject_unknown_fields(map, path, &["id", "owner", "kind", "notes"])?;
3276 Ok(ResourceRegistryEntryV1 {
3277 id: required_string(map, "id", &format!("{path}.id"))?,
3278 owner: required_string(map, "owner", &format!("{path}.owner"))?,
3279 kind: required_string(map, "kind", &format!("{path}.kind"))?,
3280 notes: optional_string(map, "notes", &format!("{path}.notes"))?,
3281 })
3282}
3283
3284fn parse_verifier_registry_entry(
3285 value: &Yaml,
3286 path: &str,
3287) -> Result<VerifierRegistryEntryV1, WeslawError> {
3288 let map = expect_mapping(value, path)?;
3289 reject_unknown_fields(map, path, &["id", "owner", "inputContracts"])?;
3290 Ok(VerifierRegistryEntryV1 {
3291 id: required_string(map, "id", &format!("{path}.id"))?,
3292 owner: required_string(map, "owner", &format!("{path}.owner"))?,
3293 input_contracts: optional_string_list(
3294 map,
3295 "inputContracts",
3296 &format!("{path}.inputContracts"),
3297 )?
3298 .unwrap_or_default(),
3299 })
3300}
3301
3302fn parse_channel_registry_entry(
3303 value: &Yaml,
3304 path: &str,
3305) -> Result<ChannelRegistryEntryV1, WeslawError> {
3306 let map = expect_mapping(value, path)?;
3307 reject_unknown_fields(map, path, &["name", "version", "carrier"])?;
3308 Ok(ChannelRegistryEntryV1 {
3309 name: required_string(map, "name", &format!("{path}.name"))?,
3310 version: required_u64(map, "version", &format!("{path}.version"))?,
3311 carrier: required_string(map, "carrier", &format!("{path}.carrier"))?,
3312 })
3313}
3314
3315fn parse_law_entry(value: &Yaml, path: &str) -> Result<Option<LawEntryV1>, WeslawError> {
3316 let map = expect_mapping(value, path)?;
3317 let status = parse_status(
3318 optional_string(map, "status", &format!("{path}.status"))?
3319 .unwrap_or_else(|| "active".to_string()),
3320 &format!("{path}.status"),
3321 )?;
3322 if status == LawStatusV1::Draft {
3323 return Ok(None);
3324 }
3325
3326 let kind_text = required_string(map, "kind", &format!("{path}.kind"))?;
3327 let kind = parse_kind(&kind_text, &format!("{path}.kind"))?;
3328 reject_unknown_fields(map, path, allowed_law_fields(kind))?;
3329
3330 let tags = optional_string_list(map, "tags", &format!("{path}.tags"))?.unwrap_or_default();
3331 let rationale = optional_string(map, "rationale", &format!("{path}.rationale"))?;
3332 let body = match kind {
3333 LawKindV1::ScalarSemantics => {
3334 LawEntryBodyV1::ScalarSemantics(parse_scalar_semantics(map, path)?)
3335 }
3336 LawKindV1::VariantLaw => LawEntryBodyV1::VariantLaw(parse_variant_law(map, path)?),
3337 LawKindV1::FootprintLaw => LawEntryBodyV1::FootprintLaw(parse_footprint_law(map, path)?),
3338 LawKindV1::ChannelLaw => LawEntryBodyV1::ChannelLaw(parse_channel_law(map, path)?),
3339 LawKindV1::InvariantLaw => LawEntryBodyV1::InvariantLaw(parse_invariant_law(map, path)?),
3340 };
3341
3342 Ok(Some(LawEntryV1 {
3343 id: required_string(map, "id", &format!("{path}.id"))?,
3344 status,
3345 kind,
3346 subject: required_string(map, "subject", &format!("{path}.subject"))?,
3347 tags,
3348 rationale,
3349 body,
3350 source_index: None,
3351 }))
3352}
3353
3354fn parse_scalar_semantics(map: &Mapping, path: &str) -> Result<ScalarSemanticsLawV1, WeslawError> {
3355 let semantics = required_mapping(map, "semantics", &format!("{path}.semantics"))?;
3356 reject_unknown_fields(
3357 semantics,
3358 &format!("{path}.semantics"),
3359 &[
3360 "representation",
3361 "minInclusive",
3362 "maxInclusive",
3363 "ordering",
3364 "scope",
3365 "forbids",
3366 ],
3367 )?;
3368 let representation = parse_scalar_representation(
3369 required_string(
3370 semantics,
3371 "representation",
3372 &format!("{path}.semantics.representation"),
3373 )?,
3374 &format!("{path}.semantics.representation"),
3375 )?;
3376 let min_inclusive = optional_u64(
3377 semantics,
3378 "minInclusive",
3379 &format!("{path}.semantics.minInclusive"),
3380 )?;
3381 let max_inclusive = optional_u64(
3382 semantics,
3383 "maxInclusive",
3384 &format!("{path}.semantics.maxInclusive"),
3385 )?;
3386 let forbids = optional_string_list(semantics, "forbids", &format!("{path}.semantics.forbids"))?
3387 .unwrap_or_default()
3388 .into_iter()
3389 .map(|item| parse_scalar_forbidden(item, &format!("{path}.semantics.forbids")))
3390 .collect::<Result<Vec<_>, _>>()?;
3391 let ordering = optional_string(semantics, "ordering", &format!("{path}.semantics.ordering"))?
3392 .map(|value| parse_scalar_ordering(value, &format!("{path}.semantics.ordering")))
3393 .transpose()?;
3394 validate_scalar_semantics(representation, min_inclusive, max_inclusive, &forbids, path)?;
3395 Ok(ScalarSemanticsLawV1 {
3396 representation,
3397 min_inclusive,
3398 max_inclusive,
3399 ordering,
3400 scope: optional_string(semantics, "scope", &format!("{path}.semantics.scope"))?,
3401 forbids,
3402 })
3403}
3404
3405fn validate_scalar_semantics(
3406 representation: ScalarRepresentationV1,
3407 min_inclusive: Option<u64>,
3408 max_inclusive: Option<u64>,
3409 forbids: &[ScalarForbiddenInterpretationV1],
3410 path: &str,
3411) -> Result<(), WeslawError> {
3412 let is_integer = representation == ScalarRepresentationV1::Integer;
3413 if !is_integer {
3414 if min_inclusive.is_some() || max_inclusive.is_some() {
3415 return Err(WeslawError::at_path(
3416 WeslawDiagnosticCode::InvalidDocument,
3417 format!("{path}.semantics.representation"),
3418 "integer ranges require representation: integer",
3419 ));
3420 }
3421 if forbids.contains(&ScalarForbiddenInterpretationV1::SilentGraphqlIntNarrowing) {
3422 return Err(WeslawError::at_path(
3423 WeslawDiagnosticCode::InvalidDocument,
3424 format!("{path}.semantics.forbids"),
3425 "silentGraphQLIntNarrowing is meaningful only for integer-like scalars",
3426 ));
3427 }
3428 }
3429 if let (Some(min), Some(max)) = (min_inclusive, max_inclusive) {
3430 if min > max {
3431 return Err(WeslawError::at_path(
3432 WeslawDiagnosticCode::InvalidDocument,
3433 format!("{path}.semantics.maxInclusive"),
3434 "minInclusive must not exceed maxInclusive",
3435 ));
3436 }
3437 }
3438 Ok(())
3439}
3440
3441fn parse_variant_law(map: &Mapping, path: &str) -> Result<VariantLawV1, WeslawError> {
3442 let discriminator = required_mapping(map, "discriminator", &format!("{path}.discriminator"))?;
3443 reject_unknown_fields(
3444 discriminator,
3445 &format!("{path}.discriminator"),
3446 &["field", "enum"],
3447 )?;
3448 let cases = required_sequence(map, "cases", &format!("{path}.cases"))?
3449 .iter()
3450 .enumerate()
3451 .map(|(index, value)| parse_variant_case(value, &format!("{path}.cases[{index}]")))
3452 .collect::<Result<Vec<_>, _>>()?;
3453 Ok(VariantLawV1 {
3454 discriminator: VariantDiscriminatorV1 {
3455 field: required_string(
3456 discriminator,
3457 "field",
3458 &format!("{path}.discriminator.field"),
3459 )?,
3460 r#enum: required_string(discriminator, "enum", &format!("{path}.discriminator.enum"))?,
3461 },
3462 cases,
3463 })
3464}
3465
3466fn parse_variant_case(value: &Yaml, path: &str) -> Result<VariantCaseV1, WeslawError> {
3467 let map = expect_mapping(value, path)?;
3468 reject_unknown_fields(map, path, &["value", "requires", "forbids"])?;
3469 Ok(VariantCaseV1 {
3470 value: required_string(map, "value", &format!("{path}.value"))?,
3471 requires: optional_string_list(map, "requires", &format!("{path}.requires"))?
3472 .unwrap_or_default(),
3473 forbids: optional_string_list(map, "forbids", &format!("{path}.forbids"))?
3474 .unwrap_or_default(),
3475 })
3476}
3477
3478fn parse_footprint_law(map: &Mapping, path: &str) -> Result<FootprintLawV1, WeslawError> {
3479 Ok(FootprintLawV1 {
3480 reads: optional_string_list(map, "reads", &format!("{path}.reads"))?.unwrap_or_default(),
3481 writes: optional_string_list(map, "writes", &format!("{path}.writes"))?.unwrap_or_default(),
3482 creates: optional_string_list(map, "creates", &format!("{path}.creates"))?
3483 .unwrap_or_default(),
3484 forbids: optional_string_list(map, "forbids", &format!("{path}.forbids"))?
3485 .unwrap_or_default(),
3486 slots: optional_sequence(map, "slots", &format!("{path}.slots"))?
3487 .unwrap_or_default()
3488 .iter()
3489 .enumerate()
3490 .map(|(index, value)| parse_footprint_slot(value, &format!("{path}.slots[{index}]")))
3491 .collect::<Result<Vec<_>, _>>()?,
3492 closures: optional_sequence(map, "closures", &format!("{path}.closures"))?
3493 .unwrap_or_default()
3494 .iter()
3495 .enumerate()
3496 .map(|(index, value)| {
3497 parse_footprint_closure(value, &format!("{path}.closures[{index}]"))
3498 })
3499 .collect::<Result<Vec<_>, _>>()?,
3500 create_slots: optional_sequence(map, "createSlots", &format!("{path}.createSlots"))?
3501 .unwrap_or_default()
3502 .iter()
3503 .enumerate()
3504 .map(|(index, value)| parse_create_slot(value, &format!("{path}.createSlots[{index}]")))
3505 .collect::<Result<Vec<_>, _>>()?,
3506 updates: optional_sequence(map, "updates", &format!("{path}.updates"))?
3507 .unwrap_or_default()
3508 .iter()
3509 .enumerate()
3510 .map(|(index, value)| {
3511 parse_footprint_update(value, &format!("{path}.updates[{index}]"))
3512 })
3513 .collect::<Result<Vec<_>, _>>()?,
3514 })
3515}
3516
3517fn parse_footprint_slot(value: &Yaml, path: &str) -> Result<FootprintSlotV1, WeslawError> {
3518 let map = expect_mapping(value, path)?;
3519 reject_unknown_fields(map, path, &["name", "kind", "bindFromArg", "access"])?;
3520 Ok(FootprintSlotV1 {
3521 name: required_string(map, "name", &format!("{path}.name"))?,
3522 kind: required_string(map, "kind", &format!("{path}.kind"))?,
3523 bind_from_arg: required_string(map, "bindFromArg", &format!("{path}.bindFromArg"))?,
3524 access: optional_string_list(map, "access", &format!("{path}.access"))?.unwrap_or_default(),
3525 })
3526}
3527
3528fn parse_footprint_closure(value: &Yaml, path: &str) -> Result<FootprintClosureV1, WeslawError> {
3529 let map = expect_mapping(value, path)?;
3530 reject_unknown_fields(
3531 map,
3532 path,
3533 &[
3534 "name",
3535 "fromSlot",
3536 "operator",
3537 "argBindings",
3538 "reads",
3539 "cardinality",
3540 ],
3541 )?;
3542 Ok(FootprintClosureV1 {
3543 name: required_string(map, "name", &format!("{path}.name"))?,
3544 from_slot: required_string(map, "fromSlot", &format!("{path}.fromSlot"))?,
3545 operator: required_string(map, "operator", &format!("{path}.operator"))?,
3546 arg_bindings: optional_string_list(map, "argBindings", &format!("{path}.argBindings"))?
3547 .unwrap_or_default(),
3548 reads: optional_string_list(map, "reads", &format!("{path}.reads"))?.unwrap_or_default(),
3549 cardinality: optional_string(map, "cardinality", &format!("{path}.cardinality"))?
3550 .map(|value| parse_footprint_cardinality(value, &format!("{path}.cardinality")))
3551 .transpose()?
3552 .unwrap_or(FootprintCardinalityV1::One),
3553 })
3554}
3555
3556fn parse_create_slot(value: &Yaml, path: &str) -> Result<CreateSlotV1, WeslawError> {
3557 let map = expect_mapping(value, path)?;
3558 reject_unknown_fields(map, path, &["name", "kind", "cardinality"])?;
3559 Ok(CreateSlotV1 {
3560 name: required_string(map, "name", &format!("{path}.name"))?,
3561 kind: required_string(map, "kind", &format!("{path}.kind"))?,
3562 cardinality: optional_string(map, "cardinality", &format!("{path}.cardinality"))?
3563 .map(|value| parse_footprint_cardinality(value, &format!("{path}.cardinality")))
3564 .transpose()?,
3565 })
3566}
3567
3568fn parse_footprint_update(value: &Yaml, path: &str) -> Result<FootprintUpdateV1, WeslawError> {
3569 let map = expect_mapping(value, path)?;
3570 reject_unknown_fields(map, path, &["slot", "fields"])?;
3571 Ok(FootprintUpdateV1 {
3572 slot: required_string(map, "slot", &format!("{path}.slot"))?,
3573 fields: optional_string_list(map, "fields", &format!("{path}.fields"))?.unwrap_or_default(),
3574 })
3575}
3576
3577fn parse_channel_law(map: &Mapping, path: &str) -> Result<ChannelLawV1, WeslawError> {
3578 Ok(ChannelLawV1 {
3579 ordered: required_bool(map, "ordered", &format!("{path}.ordered"))?,
3580 version: required_u64(map, "version", &format!("{path}.version"))?,
3581 compatibility: match mapping_get(map, "compatibility") {
3582 Some(value) => Some(parse_channel_compatibility(
3583 value,
3584 &format!("{path}.compatibility"),
3585 )?),
3586 None => None,
3587 },
3588 messages: optional_sequence(map, "messages", &format!("{path}.messages"))?
3589 .unwrap_or_default()
3590 .iter()
3591 .enumerate()
3592 .map(|(index, value)| {
3593 parse_channel_message(value, &format!("{path}.messages[{index}]"))
3594 })
3595 .collect::<Result<Vec<_>, _>>()?,
3596 })
3597}
3598
3599fn parse_channel_compatibility(
3600 value: &Yaml,
3601 path: &str,
3602) -> Result<ChannelCompatibilityV1, WeslawError> {
3603 let map = expect_mapping(value, path)?;
3604 reject_unknown_fields(map, path, &["versioning", "semverCoupled"])?;
3605 Ok(ChannelCompatibilityV1 {
3606 versioning: required_string(map, "versioning", &format!("{path}.versioning"))?,
3607 semver_coupled: required_bool(map, "semverCoupled", &format!("{path}.semverCoupled"))?,
3608 })
3609}
3610
3611fn parse_channel_message(value: &Yaml, path: &str) -> Result<ChannelMessageV1, WeslawError> {
3612 let map = expect_mapping(value, path)?;
3613 reject_unknown_fields(map, path, &["field", "type"])?;
3614 Ok(ChannelMessageV1 {
3615 field: required_string(map, "field", &format!("{path}.field"))?,
3616 r#type: required_string(map, "type", &format!("{path}.type"))?,
3617 })
3618}
3619
3620fn parse_invariant_law(map: &Mapping, path: &str) -> Result<InvariantLawV1, WeslawError> {
3621 if mapping_get(map, "expr").is_some() {
3622 return Err(WeslawError::at_path(
3623 WeslawDiagnosticCode::RawExprRejected,
3624 format!("{path}.expr"),
3625 "raw invariant expressions are not accepted in weslaw/v1",
3626 ));
3627 }
3628 let predicate = required_mapping(map, "predicate", &format!("{path}.predicate"))?;
3629 let predicate_path = format!("{path}.predicate");
3630 match required_string(predicate, "op", &format!("{predicate_path}.op"))?.as_str() {
3631 "fieldEquals" => {
3632 reject_unknown_fields(predicate, &predicate_path, &["op", "field", "value"])?;
3633 Ok(InvariantLawV1 {
3634 predicate: PredicateV1::FieldEquals {
3635 field: required_string(predicate, "field", &format!("{predicate_path}.field"))?,
3636 value: yaml_to_json_value(
3637 required_value(predicate, "value", &format!("{predicate_path}.value"))?,
3638 &format!("{predicate_path}.value"),
3639 )?,
3640 },
3641 })
3642 }
3643 "external" => {
3644 reject_unknown_fields(
3645 predicate,
3646 &predicate_path,
3647 &["op", "verifier", "ref", "inputContract"],
3648 )?;
3649 Ok(InvariantLawV1 {
3650 predicate: PredicateV1::External {
3651 verifier: required_string(
3652 predicate,
3653 "verifier",
3654 &format!("{predicate_path}.verifier"),
3655 )?,
3656 r#ref: required_string(predicate, "ref", &format!("{predicate_path}.ref"))?,
3657 input_contract: optional_string(
3658 predicate,
3659 "inputContract",
3660 &format!("{predicate_path}.inputContract"),
3661 )?,
3662 },
3663 })
3664 }
3665 other => Err(WeslawError::at_path(
3666 WeslawDiagnosticCode::InvalidDocument,
3667 format!("{predicate_path}.op"),
3668 format!("unknown predicate op {other}"),
3669 )),
3670 }
3671}
3672
3673fn parse_kind(kind: &str, path: &str) -> Result<LawKindV1, WeslawError> {
3674 match kind {
3675 "scalarSemantics" => Ok(LawKindV1::ScalarSemantics),
3676 "variantLaw" => Ok(LawKindV1::VariantLaw),
3677 "footprintLaw" => Ok(LawKindV1::FootprintLaw),
3678 "channelLaw" => Ok(LawKindV1::ChannelLaw),
3679 "invariantLaw" => Ok(LawKindV1::InvariantLaw),
3680 _ => Err(WeslawError::at_path(
3681 WeslawDiagnosticCode::UnknownKind,
3682 path,
3683 format!("unknown law kind {kind}"),
3684 )),
3685 }
3686}
3687
3688fn parse_status(status: String, path: &str) -> Result<LawStatusV1, WeslawError> {
3689 match status.as_str() {
3690 "active" => Ok(LawStatusV1::Active),
3691 "draft" => Ok(LawStatusV1::Draft),
3692 _ => Err(WeslawError::at_path(
3693 WeslawDiagnosticCode::InvalidDocument,
3694 path,
3695 format!("unknown law status {status}"),
3696 )),
3697 }
3698}
3699
3700fn parse_scalar_representation(
3701 representation: String,
3702 path: &str,
3703) -> Result<ScalarRepresentationV1, WeslawError> {
3704 match representation.as_str() {
3705 "integer" => Ok(ScalarRepresentationV1::Integer),
3706 "opaqueIdentifier" => Ok(ScalarRepresentationV1::OpaqueIdentifier),
3707 "string" => Ok(ScalarRepresentationV1::String),
3708 _ => Err(WeslawError::at_path(
3709 WeslawDiagnosticCode::InvalidDocument,
3710 path,
3711 format!("unknown scalar representation {representation}"),
3712 )),
3713 }
3714}
3715
3716fn parse_scalar_forbidden(
3717 value: String,
3718 path: &str,
3719) -> Result<ScalarForbiddenInterpretationV1, WeslawError> {
3720 match value.as_str() {
3721 "silentGraphQLIntNarrowing" => {
3722 Ok(ScalarForbiddenInterpretationV1::SilentGraphqlIntNarrowing)
3723 }
3724 _ => Err(WeslawError::at_path(
3725 WeslawDiagnosticCode::InvalidDocument,
3726 path,
3727 format!("unknown scalar forbidden interpretation {value}"),
3728 )),
3729 }
3730}
3731
3732fn parse_scalar_ordering(value: String, path: &str) -> Result<ScalarOrderingV1, WeslawError> {
3733 match value.as_str() {
3734 "none" => Ok(ScalarOrderingV1::None),
3735 "lamport" => Ok(ScalarOrderingV1::Lamport),
3736 "total" => Ok(ScalarOrderingV1::Total),
3737 "partial" => Ok(ScalarOrderingV1::Partial),
3738 _ => Err(WeslawError::at_path(
3739 WeslawDiagnosticCode::InvalidDocument,
3740 path,
3741 format!("unknown scalar ordering {value}"),
3742 )),
3743 }
3744}
3745
3746fn parse_footprint_cardinality(
3747 value: String,
3748 path: &str,
3749) -> Result<FootprintCardinalityV1, WeslawError> {
3750 match value.as_str() {
3751 "one" => Ok(FootprintCardinalityV1::One),
3752 "optional" => Ok(FootprintCardinalityV1::Optional),
3753 "many" => Ok(FootprintCardinalityV1::Many),
3754 _ => Err(WeslawError::at_path(
3755 WeslawDiagnosticCode::InvalidDocument,
3756 path,
3757 format!("unknown footprint cardinality {value}"),
3758 )),
3759 }
3760}
3761
3762fn allowed_law_fields(kind: LawKindV1) -> &'static [&'static str] {
3763 match kind {
3764 LawKindV1::ScalarSemantics => &[
3765 "id",
3766 "status",
3767 "kind",
3768 "subject",
3769 "tags",
3770 "rationale",
3771 "semantics",
3772 ],
3773 LawKindV1::VariantLaw => &[
3774 "id",
3775 "status",
3776 "kind",
3777 "subject",
3778 "tags",
3779 "rationale",
3780 "discriminator",
3781 "cases",
3782 ],
3783 LawKindV1::FootprintLaw => &[
3784 "id",
3785 "status",
3786 "kind",
3787 "subject",
3788 "tags",
3789 "rationale",
3790 "reads",
3791 "writes",
3792 "creates",
3793 "forbids",
3794 "slots",
3795 "closures",
3796 "createSlots",
3797 "updates",
3798 ],
3799 LawKindV1::ChannelLaw => &[
3800 "id",
3801 "status",
3802 "kind",
3803 "subject",
3804 "tags",
3805 "rationale",
3806 "ordered",
3807 "version",
3808 "compatibility",
3809 "messages",
3810 ],
3811 LawKindV1::InvariantLaw => &[
3812 "id",
3813 "status",
3814 "kind",
3815 "subject",
3816 "tags",
3817 "rationale",
3818 "predicate",
3819 "expr",
3820 ],
3821 }
3822}
3823
3824fn expect_mapping<'a>(value: &'a Yaml, path: &str) -> Result<&'a Mapping, WeslawError> {
3825 value.as_hash().ok_or_else(|| {
3826 WeslawError::at_path(
3827 WeslawDiagnosticCode::InvalidDocument,
3828 path,
3829 "expected object",
3830 )
3831 })
3832}
3833
3834fn required_mapping<'a>(
3835 map: &'a Mapping,
3836 key: &str,
3837 path: &str,
3838) -> Result<&'a Mapping, WeslawError> {
3839 let value = required_value(map, key, path)?;
3840 expect_mapping(value, path)
3841}
3842
3843fn required_sequence<'a>(
3844 map: &'a Mapping,
3845 key: &str,
3846 path: &str,
3847) -> Result<&'a [Yaml], WeslawError> {
3848 let value = required_value(map, key, path)?;
3849 value.as_vec().map(Vec::as_slice).ok_or_else(|| {
3850 WeslawError::at_path(
3851 WeslawDiagnosticCode::InvalidDocument,
3852 path,
3853 "expected array",
3854 )
3855 })
3856}
3857
3858fn optional_sequence<'a>(
3859 map: &'a Mapping,
3860 key: &str,
3861 path: &str,
3862) -> Result<Option<&'a [Yaml]>, WeslawError> {
3863 match mapping_get(map, key) {
3864 Some(value) => value.as_vec().map(Vec::as_slice).map(Some).ok_or_else(|| {
3865 WeslawError::at_path(
3866 WeslawDiagnosticCode::InvalidDocument,
3867 path,
3868 "expected array",
3869 )
3870 }),
3871 None => Ok(None),
3872 }
3873}
3874
3875fn required_value<'a>(map: &'a Mapping, key: &str, path: &str) -> Result<&'a Yaml, WeslawError> {
3876 mapping_get(map, key).ok_or_else(|| {
3877 WeslawError::at_path(
3878 WeslawDiagnosticCode::InvalidDocument,
3879 path,
3880 format!("missing required field {key}"),
3881 )
3882 })
3883}
3884
3885fn required_string(map: &Mapping, key: &str, path: &str) -> Result<String, WeslawError> {
3886 required_value(map, key, path)?
3887 .as_str()
3888 .map(str::to_string)
3889 .ok_or_else(|| {
3890 WeslawError::at_path(
3891 WeslawDiagnosticCode::InvalidDocument,
3892 path,
3893 "expected string",
3894 )
3895 })
3896}
3897
3898fn optional_string(map: &Mapping, key: &str, path: &str) -> Result<Option<String>, WeslawError> {
3899 match mapping_get(map, key) {
3900 Some(value) => value.as_str().map(str::to_string).map(Some).ok_or_else(|| {
3901 WeslawError::at_path(
3902 WeslawDiagnosticCode::InvalidDocument,
3903 path,
3904 "expected string",
3905 )
3906 }),
3907 None => Ok(None),
3908 }
3909}
3910
3911fn required_u64(map: &Mapping, key: &str, path: &str) -> Result<u64, WeslawError> {
3912 yaml_u64(required_value(map, key, path)?, path)
3913}
3914
3915fn optional_u64(map: &Mapping, key: &str, path: &str) -> Result<Option<u64>, WeslawError> {
3916 match mapping_get(map, key) {
3917 Some(value) => yaml_u64(value, path).map(Some),
3918 None => Ok(None),
3919 }
3920}
3921
3922fn yaml_u64(value: &Yaml, path: &str) -> Result<u64, WeslawError> {
3923 let Some(integer) = value.as_i64() else {
3924 return Err(WeslawError::at_path(
3925 WeslawDiagnosticCode::InvalidDocument,
3926 path,
3927 "expected unsigned integer",
3928 ));
3929 };
3930 u64::try_from(integer).map_err(|_| {
3931 WeslawError::at_path(
3932 WeslawDiagnosticCode::InvalidDocument,
3933 path,
3934 "expected unsigned integer",
3935 )
3936 })
3937}
3938
3939fn required_bool(map: &Mapping, key: &str, path: &str) -> Result<bool, WeslawError> {
3940 required_value(map, key, path)?.as_bool().ok_or_else(|| {
3941 WeslawError::at_path(
3942 WeslawDiagnosticCode::InvalidDocument,
3943 path,
3944 "expected boolean",
3945 )
3946 })
3947}
3948
3949fn optional_string_list(
3950 map: &Mapping,
3951 key: &str,
3952 path: &str,
3953) -> Result<Option<Vec<String>>, WeslawError> {
3954 match mapping_get(map, key) {
3955 Some(value) => {
3956 let sequence = value.as_vec().ok_or_else(|| {
3957 WeslawError::at_path(
3958 WeslawDiagnosticCode::InvalidDocument,
3959 path,
3960 "expected string array",
3961 )
3962 })?;
3963 sequence
3964 .iter()
3965 .enumerate()
3966 .map(|(index, item)| {
3967 item.as_str().map(str::to_string).ok_or_else(|| {
3968 WeslawError::at_path(
3969 WeslawDiagnosticCode::InvalidDocument,
3970 format!("{path}[{index}]"),
3971 "expected string",
3972 )
3973 })
3974 })
3975 .collect::<Result<Vec<_>, _>>()
3976 .map(Some)
3977 }
3978 None => Ok(None),
3979 }
3980}
3981
3982fn reject_unknown_fields(map: &Mapping, path: &str, allowed: &[&str]) -> Result<(), WeslawError> {
3983 for key in map.keys() {
3984 let key_text = key.as_str().ok_or_else(|| {
3985 WeslawError::at_path(
3986 WeslawDiagnosticCode::UnknownField,
3987 path,
3988 "object keys must be strings",
3989 )
3990 })?;
3991 if !allowed.contains(&key_text) {
3992 return Err(WeslawError::at_path(
3993 WeslawDiagnosticCode::UnknownField,
3994 format!("{path}.{key_text}"),
3995 format!("unknown field {key_text}"),
3996 ));
3997 }
3998 }
3999 Ok(())
4000}
4001
4002fn mapping_get<'a>(map: &'a Mapping, key: &str) -> Option<&'a Yaml> {
4003 map.get(&Yaml::String(key.to_string()))
4004}
4005
4006fn yaml_to_json_value(value: &Yaml, path: &str) -> Result<serde_json::Value, WeslawError> {
4007 match value {
4008 Yaml::Real(text) => {
4009 let number = text
4010 .parse::<f64>()
4011 .ok()
4012 .and_then(serde_json::Number::from_f64);
4013 number
4014 .map(serde_json::Value::Number)
4015 .ok_or_else(|| invalid_json_value(path, "unsupported YAML real value"))
4016 }
4017 Yaml::Integer(integer) => Ok(serde_json::Value::Number((*integer).into())),
4018 Yaml::String(text) => Ok(serde_json::Value::String(text.clone())),
4019 Yaml::Boolean(value) => Ok(serde_json::Value::Bool(*value)),
4020 Yaml::Array(items) => items
4021 .iter()
4022 .enumerate()
4023 .map(|(index, item)| yaml_to_json_value(item, &format!("{path}[{index}]")))
4024 .collect::<Result<Vec<_>, _>>()
4025 .map(serde_json::Value::Array),
4026 Yaml::Hash(map) => {
4027 let mut object = serde_json::Map::new();
4028 for (key, value) in map {
4029 let Some(key_text) = key.as_str() else {
4030 return Err(invalid_json_value(path, "YAML object keys must be strings"));
4031 };
4032 object.insert(
4033 key_text.to_string(),
4034 yaml_to_json_value(value, &format!("{path}.{key_text}"))?,
4035 );
4036 }
4037 Ok(serde_json::Value::Object(object))
4038 }
4039 Yaml::Null => Ok(serde_json::Value::Null),
4040 Yaml::Alias(_) | Yaml::BadValue => Err(invalid_json_value(
4041 path,
4042 "unsupported YAML value for JSON conversion",
4043 )),
4044 }
4045}
4046
4047fn invalid_json_value(path: &str, message: &str) -> WeslawError {
4048 WeslawError::at_path(WeslawDiagnosticCode::InvalidDocument, path, message)
4049}