Skip to main content

wesley_core/domain/
law.rs

1//! `weslaw` semantic Law IR v1.
2//!
3//! This module owns the first typed Rust substrate for `weslaw/v1` authoring
4//! documents and the normalized `wesley.law-ir/v1` representation.
5
6use 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
19/// Authored `weslaw` document API version accepted by the v1 loader.
20pub const WESLAW_API_VERSION: &str = "weslaw/v1";
21
22/// Normalized Wesley Law IR API version emitted by the v1 loader.
23pub const WESLEY_LAW_IR_API_VERSION: &str = "wesley.law-ir/v1";
24
25/// Canonical JSON codec name for future Law IR hashing.
26pub const WESLEY_LAW_IR_CANONICAL_JSON_CODEC: &str = "wesley.law-ir.canonical-json.v1";
27
28/// Contract bundle manifest API version emitted by the v1 hash path.
29pub const WESLEY_CONTRACT_BUNDLE_MANIFEST_API_VERSION: &str = "wesley.contract-bundle-manifest/v1";
30
31/// Contract bundle canonical hash input codec.
32pub const WESLEY_CONTRACT_BUNDLE_HASH_INPUT_CODEC: &str =
33    "wesley.contract-bundle.hash-input.canonical-json.v1";
34
35/// Empty policy/profile API version used until Policy IR exists.
36pub const WESLEY_EMPTY_PROFILE_API_VERSION: &str = "wesley.policy-profile.empty/v1";
37
38/// Law diff report API version emitted by the first semantic diff model.
39pub const WESLEY_LAW_DIFF_API_VERSION: &str = "wesley.law-diff/v1";
40
41/// Diagnostic codes emitted by the `weslaw/v1` structure loader.
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum WeslawDiagnosticCode {
44    /// The document could not be parsed as YAML.
45    ParseError,
46    /// The document used an unsupported `apiVersion`.
47    UnsupportedApiVersion,
48    /// The document shape was invalid for `weslaw/v1`.
49    InvalidDocument,
50    /// More than one active law entry used the same id.
51    DuplicateId,
52    /// An invariant used a raw string expression instead of a typed predicate.
53    RawExprRejected,
54    /// A law entry used a kind outside the closed v1 sum type.
55    UnknownKind,
56    /// A document object contained a field outside the v1 schema.
57    UnknownField,
58    /// A schema hash anchor did not match the active Shape IR hash.
59    SchemaHashMismatch,
60    /// A subject coordinate did not use the accepted v1 grammar.
61    InvalidCoordinate,
62    /// A subject coordinate did not resolve against Shape IR or law registries.
63    UnresolvedSubject,
64    /// A referenced field, enum value, argument path, resource, or verifier did not bind.
65    UnresolvedReference,
66    /// A law entry kind was not valid for the resolved subject kind.
67    WrongSubjectKind,
68    /// Active laws or their internal clauses contradicted each other.
69    Conflict,
70}
71
72impl WeslawDiagnosticCode {
73    /// Returns the stable external diagnostic code.
74    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/// Error returned by `weslaw/v1` structure loading.
94#[derive(Debug, Error, Clone, PartialEq, Eq)]
95#[error("{code:?}: {message}")]
96pub struct WeslawError {
97    /// Stable diagnostic code.
98    pub code: WeslawDiagnosticCode,
99    /// Human-readable diagnostic summary.
100    pub message: String,
101    /// Dot path to the invalid field when known.
102    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/// Normalized Law IR v1 document.
128#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
129#[serde(rename_all = "camelCase")]
130pub struct LawIrV1 {
131    /// Law IR API version. Always `wesley.law-ir/v1` for this struct.
132    pub api_version: String,
133    /// Contract family identity from the authored schema anchor.
134    pub family: String,
135    /// Canonical schema hash anchor supplied by the authored document.
136    pub schema_hash: String,
137    /// Optional authored schema source path.
138    #[serde(skip_serializing_if = "Option::is_none")]
139    pub schema_source: Option<String>,
140    /// Non-shape registries visible to Law IR entries.
141    pub registries: LawRegistrySetV1,
142    /// Normalized active law entries.
143    pub entries: Vec<LawEntryV1>,
144}
145
146/// Non-shape registries declared by a `weslaw/v1` document.
147#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq)]
148#[serde(rename_all = "camelCase")]
149pub struct LawRegistrySetV1 {
150    /// Declared runtime/resource/evidence domain ids.
151    pub resources: Vec<ResourceRegistryEntryV1>,
152    /// Declared external verifier ids.
153    pub verifiers: Vec<VerifierRegistryEntryV1>,
154    /// Declared non-shape channel ids.
155    pub channels: Vec<ChannelRegistryEntryV1>,
156}
157
158/// Resource registry entry for non-shape footprint and evidence symbols.
159#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
160#[serde(rename_all = "camelCase")]
161pub struct ResourceRegistryEntryV1 {
162    /// Stable resource id.
163    pub id: String,
164    /// Owning module, product, or family.
165    pub owner: String,
166    /// Resource category.
167    pub kind: String,
168    /// Optional notes retained outside semantic hashes.
169    #[serde(skip_serializing_if = "Option::is_none")]
170    pub notes: Option<String>,
171}
172
173/// Verifier registry entry for externally checked predicates.
174#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
175#[serde(rename_all = "camelCase")]
176pub struct VerifierRegistryEntryV1 {
177    /// Stable verifier id.
178    pub id: String,
179    /// Owning module, product, or family.
180    pub owner: String,
181    /// Accepted input contract ids.
182    pub input_contracts: Vec<String>,
183}
184
185/// Channel registry entry for non-shape protocol subjects.
186#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
187#[serde(rename_all = "camelCase")]
188pub struct ChannelRegistryEntryV1 {
189    /// Stable channel name.
190    pub name: String,
191    /// Channel version.
192    pub version: u64,
193    /// Carrier or transport family.
194    pub carrier: String,
195}
196
197/// Common normalized Law IR entry.
198#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
199#[serde(rename_all = "camelCase")]
200pub struct LawEntryV1 {
201    /// Stable law id.
202    pub id: String,
203    /// Active or draft law status.
204    pub status: LawStatusV1,
205    /// Closed v1 law kind.
206    pub kind: LawKindV1,
207    /// Subject coordinate governed by this law.
208    pub subject: String,
209    /// Optional classifier tags.
210    pub tags: Vec<String>,
211    /// Optional prose rationale excluded from semantic law hashing.
212    #[serde(skip_serializing_if = "Option::is_none")]
213    pub rationale: Option<String>,
214    /// Kind-specific law body.
215    pub body: LawEntryBodyV1,
216    /// Authored zero-based `laws` sequence index, retained only for diagnostics.
217    #[serde(skip)]
218    pub source_index: Option<usize>,
219}
220
221/// Law entry lifecycle state.
222#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq)]
223#[serde(rename_all = "camelCase")]
224pub enum LawStatusV1 {
225    /// Authoritative law included in active compilation.
226    #[default]
227    Active,
228    /// Draft law retained for review but not active compilation.
229    Draft,
230}
231
232/// Closed Law IR v1 kind set.
233#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
234#[serde(rename_all = "camelCase")]
235pub enum LawKindV1 {
236    /// Scalar representation and interpretation law.
237    ScalarSemantics,
238    /// Discriminated input/envelope variant law.
239    VariantLaw,
240    /// Operation footprint and resource effect law.
241    FootprintLaw,
242    /// Protocol/channel law.
243    ChannelLaw,
244    /// Typed invariant law.
245    InvariantLaw,
246}
247
248/// Kind-specific normalized Law IR v1 body.
249#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
250#[serde(untagged)]
251pub enum LawEntryBodyV1 {
252    /// Scalar semantics body.
253    ScalarSemantics(ScalarSemanticsLawV1),
254    /// Variant law body.
255    VariantLaw(VariantLawV1),
256    /// Footprint law body.
257    FootprintLaw(FootprintLawV1),
258    /// Channel law body.
259    ChannelLaw(ChannelLawV1),
260    /// Invariant law body.
261    InvariantLaw(InvariantLawV1),
262}
263
264/// Scalar semantics law body.
265#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
266#[serde(rename_all = "camelCase")]
267pub struct ScalarSemanticsLawV1 {
268    /// Scalar representation family.
269    pub representation: ScalarRepresentationV1,
270    /// Inclusive minimum value when the representation is numeric.
271    #[serde(skip_serializing_if = "Option::is_none")]
272    pub min_inclusive: Option<u64>,
273    /// Inclusive maximum value when the representation is numeric.
274    #[serde(skip_serializing_if = "Option::is_none")]
275    pub max_inclusive: Option<u64>,
276    /// Ordering semantics, when present.
277    #[serde(skip_serializing_if = "Option::is_none")]
278    pub ordering: Option<ScalarOrderingV1>,
279    /// Scope semantics, when present.
280    #[serde(skip_serializing_if = "Option::is_none")]
281    pub scope: Option<String>,
282    /// Forbidden interpretations for this scalar.
283    pub forbids: Vec<ScalarForbiddenInterpretationV1>,
284}
285
286/// Scalar representation families accepted by Law IR v1.
287#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
288#[serde(rename_all = "camelCase")]
289pub enum ScalarRepresentationV1 {
290    /// Integer representation.
291    Integer,
292    /// Opaque identifier representation.
293    OpaqueIdentifier,
294    /// String representation.
295    String,
296}
297
298/// Closed scalar ordering vocabulary accepted by Law IR v1.
299#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
300#[serde(rename_all = "camelCase")]
301pub enum ScalarOrderingV1 {
302    /// No ordering semantics are implied.
303    #[serde(rename = "none")]
304    None,
305    /// Lamport-style logical ordering.
306    Lamport,
307    /// Total ordering semantics.
308    Total,
309    /// Partial ordering semantics.
310    Partial,
311}
312
313/// Closed forbidden scalar interpretation enum.
314#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
315#[serde(rename_all = "camelCase")]
316pub enum ScalarForbiddenInterpretationV1 {
317    /// Generated code must not silently narrow the value to GraphQL signed int.
318    #[serde(rename = "silentGraphQLIntNarrowing")]
319    SilentGraphqlIntNarrowing,
320}
321
322/// Discriminated variant law body.
323#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
324#[serde(rename_all = "camelCase")]
325pub struct VariantLawV1 {
326    /// Discriminator field and enum.
327    pub discriminator: VariantDiscriminatorV1,
328    /// Per-case requirements.
329    pub cases: Vec<VariantCaseV1>,
330}
331
332/// Variant discriminator descriptor.
333#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
334#[serde(rename_all = "camelCase")]
335pub struct VariantDiscriminatorV1 {
336    /// Discriminator field name.
337    pub field: String,
338    /// Discriminator enum name.
339    pub r#enum: String,
340}
341
342/// Variant case law.
343#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
344#[serde(rename_all = "camelCase")]
345pub struct VariantCaseV1 {
346    /// Enum value for this case.
347    pub value: String,
348    /// Required fields for this case.
349    pub requires: Vec<String>,
350    /// Forbidden fields for this case.
351    pub forbids: Vec<String>,
352}
353
354/// Operation footprint law body.
355#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
356#[serde(rename_all = "camelCase")]
357pub struct FootprintLawV1 {
358    /// Resource types read by the operation.
359    pub reads: Vec<String>,
360    /// Resource types written by the operation.
361    pub writes: Vec<String>,
362    /// Resource types created by the operation.
363    pub creates: Vec<String>,
364    /// Resource domains forbidden to the operation.
365    pub forbids: Vec<String>,
366    /// Bound input/resource slots.
367    pub slots: Vec<FootprintSlotV1>,
368    /// Closure-derived resource windows.
369    pub closures: Vec<FootprintClosureV1>,
370    /// Named create slots.
371    pub create_slots: Vec<CreateSlotV1>,
372    /// Field update surfaces.
373    pub updates: Vec<FootprintUpdateV1>,
374}
375
376/// Footprint slot descriptor.
377#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
378#[serde(rename_all = "camelCase")]
379pub struct FootprintSlotV1 {
380    /// Slot name.
381    pub name: String,
382    /// Resource kind bound to the slot.
383    pub kind: String,
384    /// Argument path that binds the slot.
385    pub bind_from_arg: String,
386    /// Access modes granted for this slot.
387    pub access: Vec<String>,
388}
389
390/// Footprint closure descriptor.
391#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
392#[serde(rename_all = "camelCase")]
393pub struct FootprintClosureV1 {
394    /// Closure slot name.
395    pub name: String,
396    /// Source slot.
397    pub from_slot: String,
398    /// Closure operator id.
399    pub operator: String,
400    /// Argument bindings passed to the operator.
401    pub arg_bindings: Vec<String>,
402    /// Resource kinds read by the closure.
403    pub reads: Vec<String>,
404    /// Cardinality label.
405    pub cardinality: FootprintCardinalityV1,
406}
407
408/// Footprint create-slot descriptor.
409#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
410#[serde(rename_all = "camelCase")]
411pub struct CreateSlotV1 {
412    /// Create slot name.
413    pub name: String,
414    /// Resource kind created for the slot.
415    pub kind: String,
416    /// Optional cardinality label.
417    #[serde(skip_serializing_if = "Option::is_none")]
418    pub cardinality: Option<FootprintCardinalityV1>,
419}
420
421/// Closed footprint cardinality vocabulary accepted by Law IR v1.
422#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
423#[serde(rename_all = "camelCase")]
424pub enum FootprintCardinalityV1 {
425    /// Exactly one resource.
426    #[serde(rename = "one")]
427    One,
428    /// Zero or one resource.
429    Optional,
430    /// Zero or more resources.
431    Many,
432}
433
434/// Footprint update descriptor.
435#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
436#[serde(rename_all = "camelCase")]
437pub struct FootprintUpdateV1 {
438    /// Slot being updated.
439    pub slot: String,
440    /// Fields updated on that slot.
441    pub fields: Vec<String>,
442}
443
444/// Channel law body.
445#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
446#[serde(rename_all = "camelCase")]
447pub struct ChannelLawV1 {
448    /// Whether the channel is ordered.
449    pub ordered: bool,
450    /// Channel version.
451    pub version: u64,
452    /// Optional compatibility posture.
453    #[serde(skip_serializing_if = "Option::is_none")]
454    pub compatibility: Option<ChannelCompatibilityV1>,
455    /// Channel message fields.
456    pub messages: Vec<ChannelMessageV1>,
457}
458
459/// Channel compatibility descriptor.
460#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
461#[serde(rename_all = "camelCase")]
462pub struct ChannelCompatibilityV1 {
463    /// Versioning family.
464    pub versioning: String,
465    /// Whether the channel version is coupled to semver.
466    pub semver_coupled: bool,
467}
468
469/// Channel message descriptor.
470#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
471#[serde(rename_all = "camelCase")]
472pub struct ChannelMessageV1 {
473    /// GraphQL field name carrying the message.
474    pub field: String,
475    /// GraphQL type name for the message payload.
476    pub r#type: String,
477}
478
479/// Typed invariant law body.
480#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
481#[serde(rename_all = "camelCase")]
482pub struct InvariantLawV1 {
483    /// Typed predicate for the invariant.
484    pub predicate: PredicateV1,
485}
486
487/// Closed typed predicate set for Law IR v1 invariants.
488#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
489#[serde(rename_all = "camelCase", tag = "op")]
490pub enum PredicateV1 {
491    /// Checks a field against an exact JSON value.
492    FieldEquals {
493        /// Field name.
494        field: String,
495        /// Expected JSON value.
496        value: serde_json::Value,
497    },
498    /// Delegates evaluation to a declared external verifier.
499    External {
500        /// Verifier id.
501        verifier: String,
502        /// External predicate reference.
503        r#ref: String,
504        /// Optional input contract id.
505        #[serde(skip_serializing_if = "Option::is_none")]
506        input_contract: Option<String>,
507    },
508}
509
510/// Report emitted when active Law IR entries bind successfully.
511#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
512#[serde(rename_all = "camelCase")]
513pub struct LawBindingReportV1 {
514    /// Active Shape IR hash used for this validation pass.
515    pub schema_hash: String,
516    /// Number of active entries bound against the Shape IR.
517    pub bound_entry_count: usize,
518}
519
520/// Contract bundle manifest emitted after schema-bound Law IR validation.
521#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
522#[serde(rename_all = "camelCase")]
523pub struct ContractBundleManifestV1 {
524    /// Manifest API version.
525    pub api_version: String,
526    /// Canonical Shape IR hash, prefixed with `sha256:`.
527    pub schema_hash: String,
528    /// Canonical active semantic Law IR hash.
529    pub law_hash: String,
530    /// Optional provenance-bearing law document hash.
531    #[serde(skip_serializing_if = "Option::is_none")]
532    pub law_document_hash: Option<String>,
533    /// Canonical policy/profile hash. v1 uses the known empty profile.
534    pub profile_hash: String,
535    /// Hash over schema, law, profile, compiler, and codec identities.
536    pub bundle_hash: String,
537    /// Law IR semantic byte codec.
538    pub law_ir_codec: String,
539    /// Bundle hash input codec.
540    pub bundle_hash_codec: String,
541    /// Compiler crate identity.
542    pub compiler: String,
543    /// Compiler crate version.
544    pub compiler_version: String,
545    /// Bound active Law IR entry count.
546    pub law_entry_count: usize,
547}
548
549/// Hashes computed from a bound active Law IR document.
550#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
551#[serde(rename_all = "camelCase")]
552pub struct LawHashSetV1 {
553    /// Canonical active semantic Law IR hash.
554    pub law_hash: String,
555    /// Provenance-bearing law document hash.
556    pub law_document_hash: String,
557}
558
559/// Machine-readable semantic diff report for two Law IR v1 documents.
560#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
561#[serde(rename_all = "camelCase")]
562pub struct LawDiffReportV1 {
563    /// Report API version.
564    pub api_version: String,
565    /// Old document schema hash anchor.
566    pub old_schema_hash: String,
567    /// New document schema hash anchor.
568    pub new_schema_hash: String,
569    /// Old semantic Law IR hash.
570    pub old_law_hash: String,
571    /// New semantic Law IR hash.
572    pub new_law_hash: String,
573    /// Semantic change events, sorted by law id and event kind.
574    pub changes: Vec<LawDiffEventV1>,
575}
576
577/// Single semantic law diff event.
578#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
579#[serde(rename_all = "camelCase")]
580pub struct LawDiffEventV1 {
581    /// Event classification.
582    pub kind: LawDiffEventKindV1,
583    /// Stable law id affected by the event.
584    #[serde(skip_serializing_if = "Option::is_none")]
585    pub law_id: Option<String>,
586    /// Subject coordinate affected by the event.
587    #[serde(skip_serializing_if = "Option::is_none")]
588    pub subject: Option<String>,
589    /// Law kind affected by the event.
590    #[serde(skip_serializing_if = "Option::is_none")]
591    pub law_kind: Option<LawKindV1>,
592    /// Review posture for this initial diff event.
593    pub review_posture: LawDiffReviewPostureV1,
594    /// Field-level changes when a law body changed.
595    #[serde(default, skip_serializing_if = "Vec::is_empty")]
596    pub field_changes: Vec<LawDiffFieldChangeV1>,
597    /// Footprint resources newly read.
598    #[serde(default, skip_serializing_if = "Vec::is_empty")]
599    pub added_reads: Vec<String>,
600    /// Footprint resources no longer read.
601    #[serde(default, skip_serializing_if = "Vec::is_empty")]
602    pub removed_reads: Vec<String>,
603    /// Footprint resources newly written.
604    #[serde(default, skip_serializing_if = "Vec::is_empty")]
605    pub added_writes: Vec<String>,
606    /// Footprint resources no longer written.
607    #[serde(default, skip_serializing_if = "Vec::is_empty")]
608    pub removed_writes: Vec<String>,
609    /// Footprint resources newly created.
610    #[serde(default, skip_serializing_if = "Vec::is_empty")]
611    pub added_creates: Vec<String>,
612    /// Footprint resources no longer created.
613    #[serde(default, skip_serializing_if = "Vec::is_empty")]
614    pub removed_creates: Vec<String>,
615    /// Footprint resources newly forbidden.
616    #[serde(default, skip_serializing_if = "Vec::is_empty")]
617    pub added_forbids: Vec<String>,
618    /// Footprint resources no longer forbidden.
619    #[serde(default, skip_serializing_if = "Vec::is_empty")]
620    pub removed_forbids: Vec<String>,
621}
622
623/// Law diff event classification.
624#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
625#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
626pub enum LawDiffEventKindV1 {
627    /// Bundle-level semantic fields changed.
628    LawBundleChanged,
629    /// Semantic registry facts changed.
630    RegistryChanged,
631    /// New active law entry.
632    LawAdded,
633    /// Active law entry removed.
634    LawRemoved,
635    /// Existing law tags changed.
636    LawTagsChanged,
637    /// Existing law was monotonically strengthened.
638    LawStrengthened,
639    /// Existing law was monotonically weakened.
640    LawWeakened,
641    /// Existing law changed outside a narrower v1 event class.
642    LawChanged,
643    /// Scalar semantic body changed.
644    ScalarSemanticsChanged,
645    /// Variant law body changed.
646    VariantLawChanged,
647    /// Footprint reach expanded.
648    FootprintExpanded,
649    /// Footprint reach contracted.
650    FootprintContracted,
651    /// Footprint changed in mixed or structural ways.
652    FootprintChanged,
653    /// Channel version changed.
654    ChannelVersionChanged,
655    /// Channel law changed without a channel-version change.
656    ChannelLawChanged,
657    /// Typed invariant predicate changed.
658    PredicateChanged,
659    /// A law no longer binds to the active schema or law registry.
660    BindingBroken,
661    /// Schema hash anchor changed.
662    SchemaHashRebound,
663}
664
665/// Review posture for an initial semantic diff event.
666#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
667#[serde(rename_all = "kebab-case")]
668pub enum LawDiffReviewPostureV1 {
669    /// The change requires human review.
670    RequiresReview,
671}
672
673/// Field-level semantic law diff.
674#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
675#[serde(rename_all = "camelCase")]
676pub struct LawDiffFieldChangeV1 {
677    /// Law body path that changed.
678    pub path: String,
679    /// Previous canonical value.
680    pub old: serde_json::Value,
681    /// New canonical value.
682    pub new: serde_json::Value,
683}
684
685/// Loads an authored `weslaw/v1` YAML document into normalized Law IR v1.
686pub 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
749/// Lowers formally known `@wes_channel` SDL directives into Law IR v1.
750///
751/// This is the first directive-authored law bridge: SDL remains the source of
752/// structural shape, while the directive facts lower into the same canonical
753/// Law IR used by authored `weslaw/v1` documents.
754pub 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
828/// Serializes Law IR v1 as canonical JSON for the public v1 representation.
829///
830/// This helper provides deterministic JSON bytes for `wesley.law-ir/v1`
831/// exchange and fixture assertions. Semantic `lawHash` computation is stricter
832/// and will be introduced separately because it excludes rationale and other
833/// provenance-only fields.
834pub 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
843/// Serializes canonical active semantic Law IR v1 bytes for `lawHash`.
844///
845/// The semantic form excludes authoring provenance such as `schemaSource`,
846/// resource notes, entry status, and rationale prose. It sorts set-like arrays,
847/// materializes v1 defaults, and preserves order-sensitive arrays such as
848/// channel messages.
849pub 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
854/// Computes the `sha256:<hex>` hash over canonical active semantic Law IR v1.
855pub fn compute_law_hash_v1(value: &LawIrV1) -> Result<String, WeslawError> {
856    Ok(prefixed_sha256(&to_semantic_law_ir_json(value)?))
857}
858
859/// Computes law hashes from active semantic and provenance-bearing Law IR bytes.
860pub 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
871/// Builds the first contract bundle manifest after strict law binding succeeds.
872pub 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
907/// Computes the first machine-readable semantic diff between two Law IR docs.
908///
909/// This function compares active semantic law, not authoring prose. Rationale,
910/// source paths, resource notes, and draft law entries do not create diff
911/// events because they do not affect `lawHash`.
912pub 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
991/// Records a schema-bound validation failure as a structured law diff event.
992///
993/// This lets CI and assurance consumers receive a machine-readable diff report
994/// even when the target `weslaw` document no longer binds to the active schema.
995pub 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
1014/// Formats a `wesley.law-diff/v1` report as a Markdown review summary.
1015pub 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
1054/// Validates active Law IR v1 entries against Shape IR and root operations.
1055///
1056/// This is the strict binding gate for `WLAW-021` through `WLAW-035`: the law
1057/// document must target the active schema hash, each active subject must parse,
1058/// schema-backed subjects must resolve, kind-specific references must bind, and
1059/// the bundle must expose contradictory active law instead of silently
1060/// accepting it.
1061pub 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}