Skip to main content

traverse_contracts/
lib.rs

1//! Capability contract parsing and validation for Traverse.
2
3use semver::{Version, VersionReq};
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6use std::collections::{BTreeSet, HashSet};
7
8pub mod violations;
9pub use violations::ViolationRecord;
10
11const CAPABILITY_CONTRACT_KIND: &str = "capability_contract";
12const EVENT_CONTRACT_KIND: &str = "event_contract";
13const CONNECTOR_CONTRACT_KIND: &str = "connector_contract";
14const SUPPORTED_SCHEMA_VERSION: &str = "1.0.0";
15const GOVERNED_CONTENT_VERSION: &str = "0.1.0";
16
17#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
18pub struct CapabilityContract {
19    pub kind: String,
20    pub schema_version: String,
21    pub id: String,
22    pub namespace: String,
23    pub name: String,
24    pub version: String,
25    pub lifecycle: Lifecycle,
26    pub owner: Owner,
27    pub summary: String,
28    pub description: String,
29    pub inputs: SchemaContainer,
30    pub outputs: SchemaContainer,
31    pub preconditions: Vec<Condition>,
32    pub postconditions: Vec<Condition>,
33    pub side_effects: Vec<SideEffect>,
34    pub emits: Vec<EventReference>,
35    pub consumes: Vec<EventReference>,
36    pub permissions: Vec<IdReference>,
37    pub execution: Execution,
38    pub policies: Vec<IdReference>,
39    pub dependencies: Vec<DependencyReference>,
40    pub provenance: Provenance,
41    pub evidence: Vec<ValidationEvidence>,
42    /// UMA service type — governs placement and event routing. Defaults to `Stateless`.
43    #[serde(default)]
44    pub service_type: ServiceType,
45    /// Placement targets this capability may run on. Defaults to all targets.
46    #[serde(default = "default_permitted_targets")]
47    pub permitted_targets: Vec<ExecutionTarget>,
48    /// Required for `Subscribable` capabilities: the event type that triggers this capability.
49    #[serde(default)]
50    pub event_trigger: Option<String>,
51    /// External resource connectors required before this capability can be registered or executed.
52    #[serde(default)]
53    pub connector_requirements: Vec<ConnectorRequirement>,
54    /// Typed JSON schema for capability state values written through the runtime `DataStore`.
55    #[serde(default)]
56    pub state_schema: Option<Value>,
57}
58
59#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
60pub struct ConnectorContract {
61    pub kind: String,
62    pub schema_version: String,
63    pub connector_id: String,
64    pub version: String,
65    pub capabilities_provided: Vec<String>,
66    pub required_config_schema: Value,
67    #[serde(default = "default_connector_targets")]
68    pub supported_placement_targets: Vec<ExecutionTarget>,
69}
70
71#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
72pub struct ConnectorRequirement {
73    pub connector_id: String,
74    pub version: String,
75}
76
77#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
78pub struct ConnectorInvocation {
79    pub capability_id: String,
80    pub connector_id: String,
81    pub config: Value,
82    pub input: Value,
83}
84
85#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
86pub struct ConnectorOutput {
87    pub output: Value,
88}
89
90#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
91pub struct ConnectorError {
92    pub code: String,
93    pub message: String,
94}
95
96pub trait ConnectorPlugin: Send + Sync {
97    fn connector_id(&self) -> &str;
98    fn version(&self) -> &str;
99    fn capabilities_provided(&self) -> &[String];
100    /// Invoke the connector with runtime-injected config and input.
101    ///
102    /// # Errors
103    ///
104    /// Returns [`ConnectorError`] when the connector cannot satisfy the invocation.
105    fn invoke(&self, invocation: ConnectorInvocation) -> Result<ConnectorOutput, ConnectorError>;
106}
107
108#[must_use]
109pub fn reference_connector_contracts() -> Vec<ConnectorContract> {
110    vec![
111        reference_connector_contract(
112            "traverse.http",
113            vec!["traverse.http.outbound".to_string()],
114            serde_json::json!({
115                "type": "object",
116                "required": ["base_url"],
117                "properties": {
118                    "base_url": {"type": "string"}
119                },
120                "additionalProperties": false
121            }),
122        ),
123        reference_connector_contract(
124            "traverse.fs.read",
125            vec!["traverse.fs.read".to_string()],
126            serde_json::json!({
127                "type": "object",
128                "required": ["root"],
129                "properties": {
130                    "root": {"type": "string"}
131                },
132                "additionalProperties": false
133            }),
134        ),
135        reference_connector_contract(
136            "traverse.env",
137            vec!["traverse.env.read".to_string()],
138            serde_json::json!({
139                "type": "object",
140                "required": ["allowed_keys"],
141                "properties": {
142                    "allowed_keys": {
143                        "type": "array",
144                        "items": {"type": "string"}
145                    }
146                },
147                "additionalProperties": false
148            }),
149        ),
150    ]
151}
152
153fn reference_connector_contract(
154    connector_id: &str,
155    capabilities_provided: Vec<String>,
156    required_config_schema: Value,
157) -> ConnectorContract {
158    ConnectorContract {
159        kind: CONNECTOR_CONTRACT_KIND.to_string(),
160        schema_version: SUPPORTED_SCHEMA_VERSION.to_string(),
161        connector_id: connector_id.to_string(),
162        version: "1.0.0".to_string(),
163        capabilities_provided,
164        required_config_schema,
165        supported_placement_targets: default_connector_targets(),
166    }
167}
168
169#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
170pub struct EventContract {
171    pub kind: String,
172    pub schema_version: String,
173    pub id: String,
174    pub namespace: String,
175    pub name: String,
176    pub version: String,
177    pub lifecycle: Lifecycle,
178    pub owner: Owner,
179    pub summary: String,
180    pub description: String,
181    pub payload: EventPayload,
182    pub classification: EventClassification,
183    pub publishers: Vec<CapabilityReference>,
184    pub subscribers: Vec<CapabilityReference>,
185    pub policies: Vec<IdReference>,
186    pub tags: Vec<String>,
187    pub provenance: EventProvenance,
188    pub evidence: Vec<EventValidationEvidence>,
189}
190
191#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
192pub struct EventPayload {
193    pub schema: Value,
194    pub compatibility: PayloadCompatibility,
195}
196
197#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
198#[serde(rename_all = "kebab-case")]
199pub enum PayloadCompatibility {
200    BackwardCompatible,
201    ForwardCompatible,
202    Breaking,
203}
204
205#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
206pub struct EventClassification {
207    pub domain: String,
208    pub bounded_context: String,
209    pub event_type: EventType,
210    pub tags: Vec<String>,
211}
212
213#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
214#[serde(rename_all = "snake_case")]
215pub enum EventType {
216    Domain,
217    Integration,
218    System,
219}
220
221#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
222pub struct CapabilityReference {
223    pub capability_id: String,
224    pub version: String,
225}
226
227#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
228pub struct EventProvenance {
229    pub source: EventProvenanceSource,
230    pub author: String,
231    pub created_at: String,
232}
233
234#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
235#[serde(rename_all = "kebab-case")]
236pub enum EventProvenanceSource {
237    Greenfield,
238    Brownfield,
239    AiGenerated,
240    Extracted,
241}
242
243#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
244pub struct EventValidationEvidence {
245    pub kind: String,
246    pub r#ref: String,
247}
248
249#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
250#[serde(rename_all = "snake_case")]
251pub enum Lifecycle {
252    Draft,
253    Active,
254    Deprecated,
255    Retired,
256    Archived,
257}
258
259impl Lifecycle {
260    #[must_use]
261    pub fn is_runtime_eligible(&self) -> bool {
262        matches!(self, Self::Active | Self::Deprecated)
263    }
264}
265
266#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
267pub struct Owner {
268    pub team: String,
269    pub contact: String,
270}
271
272#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
273pub struct SchemaContainer {
274    pub schema: Value,
275}
276
277#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
278pub struct Condition {
279    pub id: String,
280    pub description: String,
281}
282
283#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
284pub struct SideEffect {
285    pub kind: SideEffectKind,
286    pub description: String,
287}
288
289#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
290#[serde(rename_all = "snake_case")]
291pub enum SideEffectKind {
292    None,
293    MemoryOnly,
294    EventEmission,
295    ExternalCall,
296    StateChange,
297}
298
299#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
300pub struct EventReference {
301    pub event_id: String,
302    pub version: String,
303}
304
305#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
306pub struct IdReference {
307    pub id: String,
308}
309
310#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
311pub struct Execution {
312    pub binary_format: BinaryFormat,
313    pub entrypoint: Entrypoint,
314    pub preferred_targets: Vec<ExecutionTarget>,
315    pub constraints: ExecutionConstraints,
316}
317
318#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
319#[serde(rename_all = "snake_case")]
320pub enum BinaryFormat {
321    Wasm,
322}
323
324/// UMA service type classification — governs placement routing and event routing.
325#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
326#[serde(rename_all = "snake_case")]
327pub enum ServiceType {
328    /// Runs anywhere; no persistent state required. Default for backward compatibility.
329    #[default]
330    Stateless,
331    /// Activated by an incoming event; requires a non-empty `event_trigger`.
332    Subscribable,
333    /// Requires managed persistence; cannot be placed in Browser environments.
334    Stateful,
335}
336
337fn default_permitted_targets() -> Vec<ExecutionTarget> {
338    vec![
339        ExecutionTarget::Local,
340        ExecutionTarget::Browser,
341        ExecutionTarget::Edge,
342        ExecutionTarget::Cloud,
343        ExecutionTarget::Worker,
344        ExecutionTarget::Device,
345    ]
346}
347
348fn default_connector_targets() -> Vec<ExecutionTarget> {
349    vec![ExecutionTarget::Local, ExecutionTarget::Cloud]
350}
351
352#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
353pub struct Entrypoint {
354    pub kind: EntrypointKind,
355    pub command: String,
356}
357
358#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
359#[serde(rename_all = "kebab-case")]
360pub enum EntrypointKind {
361    WasiCommand,
362}
363
364#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Ord, PartialOrd)]
365#[serde(rename_all = "snake_case")]
366pub enum ExecutionTarget {
367    Local,
368    Browser,
369    Edge,
370    Cloud,
371    Worker,
372    Device,
373}
374
375#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
376pub struct ExecutionConstraints {
377    pub host_api_access: HostApiAccess,
378    pub network_access: NetworkAccess,
379    pub filesystem_access: FilesystemAccess,
380}
381
382#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
383#[serde(rename_all = "snake_case")]
384pub enum HostApiAccess {
385    None,
386    ExceptionRequired,
387}
388
389#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
390#[serde(rename_all = "snake_case")]
391pub enum NetworkAccess {
392    Forbidden,
393    Required,
394}
395
396#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
397#[serde(rename_all = "snake_case")]
398pub enum FilesystemAccess {
399    None,
400    SandboxOnly,
401}
402
403#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
404pub struct DependencyReference {
405    pub artifact_type: DependencyArtifactType,
406    pub id: String,
407    pub version: String,
408}
409
410#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
411#[serde(rename_all = "snake_case")]
412pub enum DependencyArtifactType {
413    Capability,
414    Event,
415    Policy,
416}
417
418#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
419pub struct Provenance {
420    pub source: ProvenanceSource,
421    pub author: String,
422    pub created_at: String,
423    #[serde(default)]
424    pub spec_ref: Option<String>,
425    #[serde(default)]
426    pub adr_refs: Vec<String>,
427    #[serde(default)]
428    pub exception_refs: Vec<String>,
429}
430
431#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
432#[serde(rename_all = "kebab-case")]
433pub enum ProvenanceSource {
434    Greenfield,
435    BrownfieldExtracted,
436    AiGenerated,
437    AiAssisted,
438}
439
440#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
441pub struct ValidationEvidence {
442    pub evidence_id: String,
443    #[serde(rename = "type")]
444    pub evidence_type: EvidenceType,
445    pub status: EvidenceStatus,
446}
447
448#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
449#[serde(rename_all = "snake_case")]
450pub enum EvidenceType {
451    SpecAlignment,
452    ContractValidation,
453    Compatibility,
454}
455
456#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
457#[serde(rename_all = "snake_case")]
458pub enum EvidenceStatus {
459    Passed,
460    Failed,
461    Superseded,
462}
463
464#[derive(Debug, Clone, PartialEq, Eq)]
465pub struct PublishedContractRecord {
466    pub id: String,
467    pub version: String,
468    pub governed_content_digest: String,
469    pub lifecycle: Lifecycle,
470}
471
472#[derive(Debug, Clone, PartialEq, Eq)]
473pub struct PublishedEventRecord {
474    pub id: String,
475    pub version: String,
476    pub governed_content_digest: String,
477    pub lifecycle: Lifecycle,
478}
479
480#[derive(Debug, Clone, PartialEq, Eq)]
481pub struct ValidationContext<'a> {
482    pub governing_spec: &'a str,
483    pub validator_version: &'a str,
484    pub existing_published: Option<&'a PublishedContractRecord>,
485}
486
487#[derive(Debug, Clone, PartialEq, Eq)]
488pub struct EventValidationContext<'a> {
489    pub governing_spec: &'a str,
490    pub validator_version: &'a str,
491    pub existing_published: Option<&'a PublishedEventRecord>,
492}
493
494#[derive(Debug, Clone, PartialEq, Eq)]
495pub struct ValidationResult {
496    pub normalized: CapabilityContract,
497    pub evidence: ProducedValidationEvidence,
498}
499
500#[derive(Debug, Clone, PartialEq, Eq)]
501pub struct EventValidationResult {
502    pub normalized: EventContract,
503    pub evidence: ProducedValidationEvidence,
504}
505
506#[derive(Debug, Clone, PartialEq, Eq)]
507pub struct ProducedValidationEvidence {
508    pub artifact_id: String,
509    pub artifact_version: String,
510    pub governing_spec: String,
511    pub validator_version: String,
512    pub status: EvidenceStatus,
513}
514
515#[derive(Debug, Clone, PartialEq, Eq)]
516pub struct ValidationFailure {
517    pub errors: Vec<ValidationError>,
518}
519
520#[derive(Debug, Clone, PartialEq, Eq)]
521pub struct ValidationError {
522    pub code: ValidationErrorCode,
523    pub message: String,
524    pub path: String,
525    pub severity: ErrorSeverity,
526}
527
528#[derive(Debug, Clone, PartialEq, Eq)]
529pub enum ValidationErrorCode {
530    MissingRequiredField,
531    InvalidLiteral,
532    InvalidFormat,
533    InvalidSemver,
534    InconsistentIdentity,
535    DuplicateItem,
536    InvalidCapabilityBoundary,
537    InvalidEventBoundary,
538    UnsupportedBinaryFormat,
539    UnsupportedEntrypoint,
540    PortabilityExceptionRequired,
541    ImmutableVersionConflict,
542    InvalidDependencyRef,
543    /// `service_type: stateful` combined with `Browser` in `permitted_targets`.
544    InvalidPlacementConstraint,
545    /// `service_type: subscribable` without a non-empty `event_trigger`.
546    MissingEventTrigger,
547    InvalidConnectorContract,
548    InvalidConnectorRequirement,
549}
550
551#[derive(Debug, Clone, PartialEq, Eq)]
552pub enum ErrorSeverity {
553    Error,
554}
555
556/// Parses a capability contract from raw JSON text.
557///
558/// # Errors
559///
560/// Returns [`ValidationFailure`] when the JSON payload cannot be deserialized
561/// into the capability contract model.
562pub fn parse_contract(json: &str) -> Result<CapabilityContract, ValidationFailure> {
563    serde_json::from_str::<CapabilityContract>(json).map_err(|error| ValidationFailure {
564        errors: vec![ValidationError {
565            code: ValidationErrorCode::InvalidFormat,
566            message: error.to_string(),
567            path: "$".to_string(),
568            severity: ErrorSeverity::Error,
569        }],
570    })
571}
572
573/// Parses an event contract from raw JSON text.
574///
575/// # Errors
576///
577/// Returns [`ValidationFailure`] when the JSON payload cannot be deserialized
578/// into the event contract model.
579pub fn parse_event_contract(json: &str) -> Result<EventContract, ValidationFailure> {
580    serde_json::from_str::<EventContract>(json).map_err(|error| ValidationFailure {
581        errors: vec![ValidationError {
582            code: ValidationErrorCode::InvalidFormat,
583            message: error.to_string(),
584            path: "$".to_string(),
585            severity: ErrorSeverity::Error,
586        }],
587    })
588}
589
590/// Parses a connector contract from raw JSON text.
591///
592/// # Errors
593///
594/// Returns [`ValidationFailure`] when the JSON payload cannot be deserialized
595/// into the connector contract model.
596pub fn parse_connector_contract(json: &str) -> Result<ConnectorContract, ValidationFailure> {
597    serde_json::from_str::<ConnectorContract>(json).map_err(|error| ValidationFailure {
598        errors: vec![ValidationError {
599            code: ValidationErrorCode::InvalidFormat,
600            message: error.to_string(),
601            path: "$".to_string(),
602            severity: ErrorSeverity::Error,
603        }],
604    })
605}
606
607/// Validates a parsed capability contract against the governed `v0.1` rules.
608///
609/// # Errors
610///
611/// Returns [`ValidationFailure`] when structural or semantic validation fails.
612pub fn validate_contract(
613    mut contract: CapabilityContract,
614    context: &ValidationContext<'_>,
615) -> Result<ValidationResult, ValidationFailure> {
616    let mut errors = Vec::new();
617
618    validate_kind(&contract, &mut errors);
619    validate_schema_version(&contract, &mut errors);
620    validate_identity(&contract, &mut errors);
621    validate_semver(&contract.version, "$.version", &mut errors);
622    validate_owner(&contract.owner, &mut errors);
623    validate_summary(&contract.summary, "$.summary", &mut errors);
624    validate_description(&contract.description, "$.description", &mut errors);
625    validate_schema_container(&contract.inputs, "$.inputs.schema", &mut errors);
626    validate_schema_container(&contract.outputs, "$.outputs.schema", &mut errors);
627    validate_conditions(&contract.preconditions, "$.preconditions", &mut errors);
628    validate_conditions(&contract.postconditions, "$.postconditions", &mut errors);
629    validate_side_effects(&contract.side_effects, &mut errors);
630    validate_event_references(&contract.emits, "$.emits", &mut errors);
631    validate_event_references(&contract.consumes, "$.consumes", &mut errors);
632    validate_id_references(&contract.permissions, "$.permissions", &mut errors);
633    validate_execution(&contract.execution, &contract.provenance, &mut errors);
634    validate_id_references(&contract.policies, "$.policies", &mut errors);
635    validate_dependencies(&contract.dependencies, &mut errors);
636    validate_connector_requirements(&contract.connector_requirements, &mut errors);
637    validate_provenance(&contract.provenance, &mut errors);
638    validate_evidence(&contract.evidence, &mut errors);
639    validate_boundary(&contract, &mut errors);
640    validate_placement_constraints(&contract, &mut errors);
641    validate_published_record(&contract, context.existing_published, &mut errors);
642
643    if !errors.is_empty() {
644        return Err(ValidationFailure { errors });
645    }
646
647    contract.evidence.clear();
648
649    Ok(ValidationResult {
650        evidence: ProducedValidationEvidence {
651            artifact_id: contract.id.clone(),
652            artifact_version: contract.version.clone(),
653            governing_spec: context.governing_spec.to_string(),
654            validator_version: context.validator_version.to_string(),
655            status: EvidenceStatus::Passed,
656        },
657        normalized: contract,
658    })
659}
660
661/// Validates a parsed connector contract.
662///
663/// # Errors
664///
665/// Returns [`ValidationFailure`] when structural or semantic validation fails.
666pub fn validate_connector_contract(
667    contract: ConnectorContract,
668) -> Result<ConnectorContract, ValidationFailure> {
669    let mut errors = Vec::new();
670
671    if contract.kind != CONNECTOR_CONTRACT_KIND {
672        errors.push(error(
673            ValidationErrorCode::InvalidLiteral,
674            "$.kind",
675            "kind must equal connector_contract",
676        ));
677    }
678    if contract.schema_version != SUPPORTED_SCHEMA_VERSION {
679        errors.push(error(
680            ValidationErrorCode::InvalidLiteral,
681            "$.schema_version",
682            "schema_version must equal 1.0.0",
683        ));
684    }
685    validate_non_empty(&contract.connector_id, "$.connector_id", &mut errors);
686    validate_semver(&contract.version, "$.version", &mut errors);
687    validate_unique_strings(
688        &contract.capabilities_provided,
689        "$.capabilities_provided",
690        "capabilities_provided must be unique",
691        &mut errors,
692    );
693    if contract.capabilities_provided.is_empty() {
694        errors.push(error(
695            ValidationErrorCode::MissingRequiredField,
696            "$.capabilities_provided",
697            "capabilities_provided must contain at least one capability id",
698        ));
699    }
700    validate_schema_value(
701        &contract.required_config_schema,
702        "$.required_config_schema",
703        &mut errors,
704    );
705    if contract.supported_placement_targets.is_empty() {
706        errors.push(error(
707            ValidationErrorCode::MissingRequiredField,
708            "$.supported_placement_targets",
709            "supported_placement_targets must contain at least one target",
710        ));
711    }
712    let unique_targets: BTreeSet<_> = contract
713        .supported_placement_targets
714        .iter()
715        .cloned()
716        .collect();
717    if unique_targets.len() != contract.supported_placement_targets.len() {
718        errors.push(error(
719            ValidationErrorCode::DuplicateItem,
720            "$.supported_placement_targets",
721            "supported_placement_targets must be unique",
722        ));
723    }
724
725    if !errors.is_empty() {
726        return Err(ValidationFailure { errors });
727    }
728
729    Ok(contract)
730}
731
732/// Validates a parsed event contract against the governed `v0.1` rules.
733///
734/// # Errors
735///
736/// Returns [`ValidationFailure`] when structural or semantic validation fails.
737pub fn validate_event_contract(
738    mut contract: EventContract,
739    context: &EventValidationContext<'_>,
740) -> Result<EventValidationResult, ValidationFailure> {
741    let mut errors = Vec::new();
742
743    validate_event_kind(&contract, &mut errors);
744    validate_event_schema_version(&contract, &mut errors);
745    validate_event_identity(&contract, &mut errors);
746    validate_semver(&contract.version, "$.version", &mut errors);
747    validate_owner(&contract.owner, &mut errors);
748    validate_summary(&contract.summary, "$.summary", &mut errors);
749    validate_description(&contract.description, "$.description", &mut errors);
750    validate_event_payload(&contract.payload, &mut errors);
751    validate_event_classification(&contract.classification, &mut errors);
752    validate_capability_references(&contract.publishers, "$.publishers", true, &mut errors);
753    validate_capability_references(&contract.subscribers, "$.subscribers", false, &mut errors);
754    validate_id_references(&contract.policies, "$.policies", &mut errors);
755    validate_tags(&contract.tags, "$.tags", true, &mut errors);
756    validate_event_provenance(&contract.provenance, &mut errors);
757    validate_event_evidence(&contract.evidence, &mut errors);
758    validate_event_boundary(&contract, &mut errors);
759    validate_published_event_record(&contract, context.existing_published, &mut errors);
760
761    if !errors.is_empty() {
762        return Err(ValidationFailure { errors });
763    }
764
765    contract.evidence.clear();
766
767    Ok(EventValidationResult {
768        evidence: ProducedValidationEvidence {
769            artifact_id: contract.id.clone(),
770            artifact_version: contract.version.clone(),
771            governing_spec: context.governing_spec.to_string(),
772            validator_version: context.validator_version.to_string(),
773            status: EvidenceStatus::Passed,
774        },
775        normalized: contract,
776    })
777}
778
779#[must_use]
780pub fn governed_content_digest(contract: &CapabilityContract) -> String {
781    let mut clone = contract.clone();
782    clone.evidence.clear();
783    let json = format!("{clone:?}");
784    let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
785    for byte in json.as_bytes() {
786        hash ^= u64::from(*byte);
787        hash = hash.wrapping_mul(0x0000_0001_0000_01b3);
788    }
789    format!("{GOVERNED_CONTENT_VERSION}:{hash:016x}")
790}
791
792#[must_use]
793pub fn governed_event_content_digest(contract: &EventContract) -> String {
794    let mut clone = contract.clone();
795    clone.evidence.clear();
796    let json = format!("{clone:?}");
797    let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
798    for byte in json.as_bytes() {
799        hash ^= u64::from(*byte);
800        hash = hash.wrapping_mul(0x0000_0001_0000_01b3);
801    }
802    format!("{GOVERNED_CONTENT_VERSION}:{hash:016x}")
803}
804
805fn validate_kind(contract: &CapabilityContract, errors: &mut Vec<ValidationError>) {
806    if contract.kind != CAPABILITY_CONTRACT_KIND {
807        errors.push(error(
808            ValidationErrorCode::InvalidLiteral,
809            "$.kind",
810            "kind must equal capability_contract",
811        ));
812    }
813}
814
815fn validate_event_kind(contract: &EventContract, errors: &mut Vec<ValidationError>) {
816    if contract.kind != EVENT_CONTRACT_KIND {
817        errors.push(error(
818            ValidationErrorCode::InvalidLiteral,
819            "$.kind",
820            "kind must equal event_contract",
821        ));
822    }
823}
824
825fn validate_schema_version(contract: &CapabilityContract, errors: &mut Vec<ValidationError>) {
826    if contract.schema_version != SUPPORTED_SCHEMA_VERSION {
827        errors.push(error(
828            ValidationErrorCode::InvalidLiteral,
829            "$.schema_version",
830            "schema_version must equal 1.0.0",
831        ));
832    }
833}
834
835fn validate_event_schema_version(contract: &EventContract, errors: &mut Vec<ValidationError>) {
836    if contract.schema_version != SUPPORTED_SCHEMA_VERSION {
837        errors.push(error(
838            ValidationErrorCode::InvalidLiteral,
839            "$.schema_version",
840            "schema_version must equal 1.0.0",
841        ));
842    }
843}
844
845fn validate_identity(contract: &CapabilityContract, errors: &mut Vec<ValidationError>) {
846    if !is_valid_namespace(&contract.namespace) {
847        errors.push(error(
848            ValidationErrorCode::InvalidFormat,
849            "$.namespace",
850            "namespace must be dot-separated lowercase kebab-case segments",
851        ));
852    }
853
854    if !is_valid_name(&contract.name) {
855        errors.push(error(
856            ValidationErrorCode::InvalidFormat,
857            "$.name",
858            "name must be lowercase kebab-case",
859        ));
860    }
861
862    let expected_id = format!("{}.{}", contract.namespace, contract.name);
863    if contract.id != expected_id {
864        errors.push(error(
865            ValidationErrorCode::InconsistentIdentity,
866            "$.id",
867            "id must equal namespace.name",
868        ));
869    }
870}
871
872fn validate_event_identity(contract: &EventContract, errors: &mut Vec<ValidationError>) {
873    if !is_valid_namespace(&contract.namespace) {
874        errors.push(error(
875            ValidationErrorCode::InvalidFormat,
876            "$.namespace",
877            "namespace must be dot-separated lowercase kebab-case segments",
878        ));
879    }
880
881    if !is_valid_name(&contract.name) {
882        errors.push(error(
883            ValidationErrorCode::InvalidFormat,
884            "$.name",
885            "name must be lowercase kebab-case",
886        ));
887    }
888
889    let expected_id = format!("{}.{}", contract.namespace, contract.name);
890    if contract.id != expected_id {
891        errors.push(error(
892            ValidationErrorCode::InconsistentIdentity,
893            "$.id",
894            "id must equal namespace.name",
895        ));
896    }
897}
898
899fn validate_semver(value: &str, path: &str, errors: &mut Vec<ValidationError>) {
900    if Version::parse(value).is_err() {
901        errors.push(error(
902            ValidationErrorCode::InvalidSemver,
903            path,
904            "version must match MAJOR.MINOR.PATCH",
905        ));
906    }
907}
908
909fn validate_owner(owner: &Owner, errors: &mut Vec<ValidationError>) {
910    validate_non_empty(&owner.team, "$.owner.team", errors);
911    validate_non_empty(&owner.contact, "$.owner.contact", errors);
912}
913
914fn validate_summary(summary: &str, path: &str, errors: &mut Vec<ValidationError>) {
915    if summary.trim().len() < 10 || summary.len() > 200 {
916        errors.push(error(
917            ValidationErrorCode::InvalidFormat,
918            path,
919            "summary length must be between 10 and 200 characters",
920        ));
921    }
922}
923
924fn validate_description(description: &str, path: &str, errors: &mut Vec<ValidationError>) {
925    if description.trim().len() < 20 {
926        errors.push(error(
927            ValidationErrorCode::InvalidFormat,
928            path,
929            "description must be at least 20 characters",
930        ));
931    }
932}
933
934fn validate_schema_container(
935    container: &SchemaContainer,
936    path: &str,
937    errors: &mut Vec<ValidationError>,
938) {
939    validate_schema_value(&container.schema, path, errors);
940}
941
942fn validate_schema_value(schema: &Value, path: &str, errors: &mut Vec<ValidationError>) {
943    if !schema.is_object() {
944        errors.push(error(
945            ValidationErrorCode::InvalidFormat,
946            path,
947            "schema must be a JSON object",
948        ));
949    }
950}
951
952fn validate_event_payload(payload: &EventPayload, errors: &mut Vec<ValidationError>) {
953    if !payload.schema.is_object() {
954        errors.push(error(
955            ValidationErrorCode::InvalidFormat,
956            "$.payload.schema",
957            "schema must be a JSON object",
958        ));
959    }
960}
961
962fn validate_event_classification(
963    classification: &EventClassification,
964    errors: &mut Vec<ValidationError>,
965) {
966    validate_min_length(
967        &classification.domain,
968        "$.classification.domain",
969        2,
970        "domain must be at least 2 characters",
971        errors,
972    );
973    validate_min_length(
974        &classification.bounded_context,
975        "$.classification.bounded_context",
976        2,
977        "bounded_context must be at least 2 characters",
978        errors,
979    );
980    validate_tags(&classification.tags, "$.classification.tags", true, errors);
981}
982
983fn validate_capability_references(
984    references: &[CapabilityReference],
985    path: &str,
986    require_one: bool,
987    errors: &mut Vec<ValidationError>,
988) {
989    if require_one && references.is_empty() {
990        errors.push(error(
991            ValidationErrorCode::MissingRequiredField,
992            path,
993            "array must contain at least one item",
994        ));
995    }
996
997    let mut seen = HashSet::new();
998    for (index, item) in references.iter().enumerate() {
999        let id_path = format!("{path}[{index}].capability_id");
1000        let version_path = format!("{path}[{index}].version");
1001        validate_non_empty(&item.capability_id, &id_path, errors);
1002        validate_semver(&item.version, &version_path, errors);
1003        if !seen.insert((item.capability_id.clone(), item.version.clone())) {
1004            errors.push(error(
1005                ValidationErrorCode::DuplicateItem,
1006                &id_path,
1007                "capability references must be unique by id and version",
1008            ));
1009        }
1010    }
1011}
1012
1013fn validate_tags(
1014    tags: &[String],
1015    path: &str,
1016    require_one: bool,
1017    errors: &mut Vec<ValidationError>,
1018) {
1019    if require_one && tags.is_empty() {
1020        errors.push(error(
1021            ValidationErrorCode::MissingRequiredField,
1022            path,
1023            "array must contain at least one item",
1024        ));
1025    }
1026    for (index, tag) in tags.iter().enumerate() {
1027        validate_non_empty(tag, &format!("{path}[{index}]"), errors);
1028    }
1029    validate_unique_strings(tags, path, "values must be unique", errors);
1030}
1031
1032fn validate_event_provenance(provenance: &EventProvenance, errors: &mut Vec<ValidationError>) {
1033    validate_non_empty(&provenance.author, "$.provenance.author", errors);
1034    validate_non_empty(&provenance.created_at, "$.provenance.created_at", errors);
1035}
1036
1037fn validate_event_evidence(
1038    evidence: &[EventValidationEvidence],
1039    errors: &mut Vec<ValidationError>,
1040) {
1041    let mut seen = HashSet::new();
1042    for (index, item) in evidence.iter().enumerate() {
1043        let kind_path = format!("$.evidence[{index}].kind");
1044        let ref_path = format!("$.evidence[{index}].ref");
1045        validate_non_empty(&item.kind, &kind_path, errors);
1046        validate_non_empty(&item.r#ref, &ref_path, errors);
1047        if !seen.insert((item.kind.clone(), item.r#ref.clone())) {
1048            errors.push(error(
1049                ValidationErrorCode::DuplicateItem,
1050                &kind_path,
1051                "evidence entries must be unique by kind and ref",
1052            ));
1053        }
1054    }
1055}
1056
1057fn validate_conditions(conditions: &[Condition], path: &str, errors: &mut Vec<ValidationError>) {
1058    let mut seen = HashSet::new();
1059    for (index, condition) in conditions.iter().enumerate() {
1060        let id_path = format!("{path}[{index}].id");
1061        let description_path = format!("{path}[{index}].description");
1062        validate_non_empty(&condition.id, &id_path, errors);
1063        validate_non_empty(&condition.description, &description_path, errors);
1064        if !seen.insert(condition.id.clone()) {
1065            errors.push(error(
1066                ValidationErrorCode::DuplicateItem,
1067                &id_path,
1068                "condition ids must be unique",
1069            ));
1070        }
1071    }
1072}
1073
1074fn validate_side_effects(side_effects: &[SideEffect], errors: &mut Vec<ValidationError>) {
1075    if side_effects.is_empty() {
1076        errors.push(error(
1077            ValidationErrorCode::MissingRequiredField,
1078            "$.side_effects",
1079            "side_effects must contain at least one item",
1080        ));
1081    }
1082
1083    for (index, side_effect) in side_effects.iter().enumerate() {
1084        validate_non_empty(
1085            &side_effect.description,
1086            &format!("$.side_effects[{index}].description"),
1087            errors,
1088        );
1089    }
1090}
1091
1092fn validate_event_references(
1093    references: &[EventReference],
1094    path: &str,
1095    errors: &mut Vec<ValidationError>,
1096) {
1097    let mut seen = HashSet::new();
1098    for (index, item) in references.iter().enumerate() {
1099        let event_path = format!("{path}[{index}].event_id");
1100        let version_path = format!("{path}[{index}].version");
1101        validate_non_empty(&item.event_id, &event_path, errors);
1102        validate_semver(&item.version, &version_path, errors);
1103        if !seen.insert((item.event_id.clone(), item.version.clone())) {
1104            errors.push(error(
1105                ValidationErrorCode::DuplicateItem,
1106                &event_path,
1107                "event references must be unique by id and version",
1108            ));
1109        }
1110    }
1111}
1112
1113fn validate_id_references(items: &[IdReference], path: &str, errors: &mut Vec<ValidationError>) {
1114    let mut seen = HashSet::new();
1115    for (index, item) in items.iter().enumerate() {
1116        let item_path = format!("{path}[{index}].id");
1117        validate_non_empty(&item.id, &item_path, errors);
1118        if !seen.insert(item.id.clone()) {
1119            errors.push(error(
1120                ValidationErrorCode::DuplicateItem,
1121                &item_path,
1122                "ids must be unique",
1123            ));
1124        }
1125    }
1126}
1127
1128fn validate_execution(
1129    execution: &Execution,
1130    provenance: &Provenance,
1131    errors: &mut Vec<ValidationError>,
1132) {
1133    match execution.binary_format {
1134        BinaryFormat::Wasm => {}
1135    }
1136
1137    match execution.entrypoint.kind {
1138        EntrypointKind::WasiCommand => {}
1139    }
1140
1141    validate_non_empty(
1142        &execution.entrypoint.command,
1143        "$.execution.entrypoint.command",
1144        errors,
1145    );
1146
1147    if execution.preferred_targets.is_empty() {
1148        errors.push(error(
1149            ValidationErrorCode::MissingRequiredField,
1150            "$.execution.preferred_targets",
1151            "preferred_targets must contain at least one item",
1152        ));
1153    }
1154
1155    let unique_targets: BTreeSet<_> = execution.preferred_targets.iter().cloned().collect();
1156    if unique_targets.len() != execution.preferred_targets.len() {
1157        errors.push(error(
1158            ValidationErrorCode::DuplicateItem,
1159            "$.execution.preferred_targets",
1160            "preferred_targets must be unique",
1161        ));
1162    }
1163
1164    if matches!(
1165        execution.constraints.host_api_access,
1166        HostApiAccess::ExceptionRequired
1167    ) && provenance.exception_refs.is_empty()
1168    {
1169        errors.push(error(
1170            ValidationErrorCode::PortabilityExceptionRequired,
1171            "$.execution.constraints.host_api_access",
1172            "host_api_access=exception_required requires provenance.exception_refs",
1173        ));
1174    }
1175}
1176
1177fn validate_dependencies(dependencies: &[DependencyReference], errors: &mut Vec<ValidationError>) {
1178    let mut seen = HashSet::new();
1179    for (index, dependency) in dependencies.iter().enumerate() {
1180        let id_path = format!("$.dependencies[{index}].id");
1181        let version_path = format!("$.dependencies[{index}].version");
1182        validate_non_empty(&dependency.id, &id_path, errors);
1183        validate_semver(&dependency.version, &version_path, errors);
1184        if !seen.insert((
1185            dependency.artifact_type.clone(),
1186            dependency.id.clone(),
1187            dependency.version.clone(),
1188        )) {
1189            errors.push(error(
1190                ValidationErrorCode::DuplicateItem,
1191                &id_path,
1192                "dependencies must be unique by artifact_type, id, and version",
1193            ));
1194        }
1195    }
1196}
1197
1198fn validate_connector_requirements(
1199    requirements: &[ConnectorRequirement],
1200    errors: &mut Vec<ValidationError>,
1201) {
1202    let mut seen = HashSet::new();
1203    for (index, requirement) in requirements.iter().enumerate() {
1204        let id_path = format!("$.connector_requirements[{index}].connector_id");
1205        let version_path = format!("$.connector_requirements[{index}].version");
1206        validate_non_empty(&requirement.connector_id, &id_path, errors);
1207        if VersionReq::parse(&requirement.version).is_err() {
1208            errors.push(error(
1209                ValidationErrorCode::InvalidConnectorRequirement,
1210                &version_path,
1211                "connector requirement version must be a valid semver range",
1212            ));
1213        }
1214        if !seen.insert((
1215            requirement.connector_id.clone(),
1216            requirement.version.clone(),
1217        )) {
1218            errors.push(error(
1219                ValidationErrorCode::DuplicateItem,
1220                &id_path,
1221                "connector_requirements must be unique by connector_id and version",
1222            ));
1223        }
1224    }
1225}
1226
1227fn validate_provenance(provenance: &Provenance, errors: &mut Vec<ValidationError>) {
1228    validate_non_empty(&provenance.author, "$.provenance.author", errors);
1229    validate_non_empty(&provenance.created_at, "$.provenance.created_at", errors);
1230
1231    if let Some(spec_ref) = &provenance.spec_ref {
1232        validate_non_empty(spec_ref, "$.provenance.spec_ref", errors);
1233    }
1234
1235    validate_unique_strings(
1236        &provenance.adr_refs,
1237        "$.provenance.adr_refs",
1238        "adr_refs must be unique",
1239        errors,
1240    );
1241    validate_unique_strings(
1242        &provenance.exception_refs,
1243        "$.provenance.exception_refs",
1244        "exception_refs must be unique",
1245        errors,
1246    );
1247}
1248
1249fn validate_evidence(evidence: &[ValidationEvidence], errors: &mut Vec<ValidationError>) {
1250    let mut seen = HashSet::new();
1251    for (index, item) in evidence.iter().enumerate() {
1252        let id_path = format!("$.evidence[{index}].evidence_id");
1253        validate_non_empty(&item.evidence_id, &id_path, errors);
1254        if !seen.insert(item.evidence_id.clone()) {
1255            errors.push(error(
1256                ValidationErrorCode::DuplicateItem,
1257                &id_path,
1258                "evidence_id values must be unique",
1259            ));
1260        }
1261    }
1262}
1263
1264fn validate_boundary(contract: &CapabilityContract, errors: &mut Vec<ValidationError>) {
1265    let summary = contract.summary.to_ascii_lowercase();
1266    let description = contract.description.to_ascii_lowercase();
1267    let combined = format!("{summary} {description}");
1268    let banned_terms = [
1269        "utility function",
1270        "helper function",
1271        "crud wrapper",
1272        "transport handler",
1273        "database insert",
1274        "full application",
1275        "subsystem",
1276    ];
1277
1278    if banned_terms.iter().any(|term| combined.contains(term)) {
1279        errors.push(error(
1280            ValidationErrorCode::InvalidCapabilityBoundary,
1281            "$.summary",
1282            "capability must represent one meaningful business action",
1283        ));
1284    }
1285}
1286
1287fn validate_event_boundary(contract: &EventContract, errors: &mut Vec<ValidationError>) {
1288    let summary = contract.summary.to_ascii_lowercase();
1289    let description = contract.description.to_ascii_lowercase();
1290    let combined = format!("{summary} {description}");
1291    let banned_terms = [
1292        "kafka topic",
1293        "transport topic",
1294        "websocket channel",
1295        "queue binding",
1296        "broker partition",
1297        "payload wrapper",
1298    ];
1299
1300    if banned_terms.iter().any(|term| combined.contains(term)) {
1301        errors.push(error(
1302            ValidationErrorCode::InvalidEventBoundary,
1303            "$.summary",
1304            "event must describe one governed business event boundary",
1305        ));
1306    }
1307}
1308
1309fn validate_placement_constraints(
1310    contract: &CapabilityContract,
1311    errors: &mut Vec<ValidationError>,
1312) {
1313    if contract.service_type == ServiceType::Stateful
1314        && contract
1315            .permitted_targets
1316            .contains(&ExecutionTarget::Browser)
1317    {
1318        errors.push(ValidationError {
1319            code: ValidationErrorCode::InvalidPlacementConstraint,
1320            message: "Stateful capabilities cannot target Browser environments; browsers cannot \
1321                      provide managed persistence guarantees."
1322                .to_string(),
1323            path: "$.permitted_targets".to_string(),
1324            severity: ErrorSeverity::Error,
1325        });
1326    }
1327    if contract.service_type == ServiceType::Subscribable
1328        && match contract.event_trigger.as_deref() {
1329            None => true,
1330            Some(event_trigger) => event_trigger.is_empty(),
1331        }
1332    {
1333        errors.push(ValidationError {
1334            code: ValidationErrorCode::MissingEventTrigger,
1335            message: "Subscribable capabilities must declare a non-empty event_trigger field."
1336                .to_string(),
1337            path: "$.event_trigger".to_string(),
1338            severity: ErrorSeverity::Error,
1339        });
1340    }
1341}
1342
1343fn validate_published_record(
1344    contract: &CapabilityContract,
1345    published: Option<&PublishedContractRecord>,
1346    errors: &mut Vec<ValidationError>,
1347) {
1348    let Some(published) = published else {
1349        return;
1350    };
1351
1352    if published.id != contract.id || published.version != contract.version {
1353        return;
1354    }
1355
1356    let digest = governed_content_digest(contract);
1357    if published.governed_content_digest != digest {
1358        errors.push(error(
1359            ValidationErrorCode::ImmutableVersionConflict,
1360            "$.version",
1361            "published contract versions are immutable",
1362        ));
1363    }
1364}
1365
1366fn validate_published_event_record(
1367    contract: &EventContract,
1368    published: Option<&PublishedEventRecord>,
1369    errors: &mut Vec<ValidationError>,
1370) {
1371    let Some(published) = published else {
1372        return;
1373    };
1374
1375    if published.id != contract.id || published.version != contract.version {
1376        return;
1377    }
1378
1379    let digest = governed_event_content_digest(contract);
1380    if published.governed_content_digest != digest {
1381        errors.push(error(
1382            ValidationErrorCode::ImmutableVersionConflict,
1383            "$.version",
1384            "published contract versions are immutable",
1385        ));
1386    }
1387}
1388
1389fn validate_non_empty(value: &str, path: &str, errors: &mut Vec<ValidationError>) {
1390    if value.trim().is_empty() {
1391        errors.push(error(
1392            ValidationErrorCode::MissingRequiredField,
1393            path,
1394            "value must be non-empty",
1395        ));
1396    }
1397}
1398
1399fn validate_min_length(
1400    value: &str,
1401    path: &str,
1402    min_length: usize,
1403    message: &str,
1404    errors: &mut Vec<ValidationError>,
1405) {
1406    if value.trim().len() < min_length {
1407        errors.push(error(ValidationErrorCode::InvalidFormat, path, message));
1408    }
1409}
1410
1411fn validate_unique_strings(
1412    values: &[String],
1413    path: &str,
1414    message: &str,
1415    errors: &mut Vec<ValidationError>,
1416) {
1417    let mut seen = HashSet::new();
1418    for value in values {
1419        if !seen.insert(value.clone()) {
1420            errors.push(error(ValidationErrorCode::DuplicateItem, path, message));
1421            break;
1422        }
1423    }
1424}
1425
1426fn error(code: ValidationErrorCode, path: &str, message: &str) -> ValidationError {
1427    ValidationError {
1428        code,
1429        message: message.to_string(),
1430        path: path.to_string(),
1431        severity: ErrorSeverity::Error,
1432    }
1433}
1434
1435fn is_valid_name(name: &str) -> bool {
1436    let mut parts = name.split('-');
1437    let first = parts.next().unwrap_or_default();
1438    is_valid_segment(first) && parts.all(is_valid_segment)
1439}
1440
1441fn is_valid_namespace(namespace: &str) -> bool {
1442    let mut parts = namespace.split('.');
1443    let first = parts.next().unwrap_or_default();
1444    is_valid_name(first) && parts.all(is_valid_name)
1445}
1446
1447fn is_valid_segment(segment: &str) -> bool {
1448    !segment.is_empty()
1449        && segment
1450            .chars()
1451            .all(|character| character.is_ascii_lowercase() || character.is_ascii_digit())
1452}