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