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 proposal;
9pub mod usage_telemetry;
10pub mod violations;
11pub use proposal::{
12    CanonicalProposal, DEFAULT_MAX_CONCURRENT_NODES, DEFAULT_MAX_FAN_OUT, DEFAULT_MAX_JOIN_WIDTH,
13    DEFAULT_MAX_QUEUE_DEPTH, ManifestReference, MappingSource, ParallelSchedule,
14    ParallelScheduleError, ParallelScheduleErrorCode, ParallelScheduleFailure,
15    ParallelScheduleLimits, ProposalEdge, ProposalLimits, ProposalMapping, ProposalNode,
16    ProposalValidationError, ProposalValidationErrorCode, ProposalValidationFailure,
17    SnapshotDigests, WorkflowProposal, canonicalize_proposal, compute_parallel_schedule,
18    proposal_digest, proposal_snapshot_digest,
19};
20pub use usage_telemetry::{NoOpUsageTelemetrySink, UsageEvent, UsageEventKind, UsageTelemetrySink};
21pub use violations::ViolationRecord;
22
23const CAPABILITY_CONTRACT_KIND: &str = "capability_contract";
24const EVENT_CONTRACT_KIND: &str = "event_contract";
25const CONNECTOR_CONTRACT_KIND: &str = "connector_contract";
26const SUPPORTED_SCHEMA_VERSION: &str = "1.0.0";
27const GOVERNED_CONTENT_VERSION: &str = "0.1.0";
28
29#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
30pub struct CapabilityContract {
31    pub kind: String,
32    pub schema_version: String,
33    pub id: String,
34    pub namespace: String,
35    pub name: String,
36    pub version: String,
37    pub lifecycle: Lifecycle,
38    pub owner: Owner,
39    pub summary: String,
40    pub description: String,
41    pub inputs: SchemaContainer,
42    pub outputs: SchemaContainer,
43    pub preconditions: Vec<Condition>,
44    pub postconditions: Vec<Condition>,
45    pub side_effects: Vec<SideEffect>,
46    pub emits: Vec<EventReference>,
47    pub consumes: Vec<EventReference>,
48    pub permissions: Vec<IdReference>,
49    pub execution: Execution,
50    pub policies: Vec<IdReference>,
51    pub dependencies: Vec<DependencyReference>,
52    pub provenance: Provenance,
53    pub evidence: Vec<ValidationEvidence>,
54    /// UMA service type — governs placement and event routing. Defaults to `Stateless`.
55    #[serde(default)]
56    pub service_type: ServiceType,
57    /// Placement targets this capability may run on. Defaults to all targets.
58    #[serde(default = "default_permitted_targets")]
59    pub permitted_targets: Vec<ExecutionTarget>,
60    /// Required for `Subscribable` capabilities: the event type that triggers this capability.
61    #[serde(default)]
62    pub event_trigger: Option<String>,
63    /// External resource connectors required before this capability can be registered or executed.
64    #[serde(default)]
65    pub connector_requirements: Vec<ConnectorRequirement>,
66    /// Typed JSON schema for capability state values written through the runtime `DataStore`.
67    #[serde(default)]
68    pub state_schema: Option<Value>,
69    /// Executable surface examples (spec 102). Preserved through publish; not cleared by validate.
70    #[serde(default)]
71    pub use_cases: Vec<UseCase>,
72    /// Immutable capability risk classification (spec 109 FR-005). Contracts published
73    /// before this field existed default to the most conservative classification so
74    /// they are never silently treated as automatic-eligible.
75    #[serde(default = "default_risk_metadata")]
76    pub risk: RiskMetadata,
77}
78
79/// Portable, immutable capability authority metadata across four independent
80/// dimensions (ADR-0041). A capability's own contract is the only place these
81/// values may be declared; an application manifest may narrow how a capability
82/// is actually wired (for example connector selection) but can never override
83/// or weaken these classifications.
84#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
85pub struct RiskMetadata {
86    pub effect_class: EffectClass,
87    pub determinism_class: DeterminismClass,
88    #[serde(default)]
89    pub data_flow: DataFlowPolicy,
90    pub reliability: ReliabilityMetadata,
91}
92
93/// What kind of effect invoking this capability has on the world.
94#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
95#[serde(rename_all = "snake_case")]
96pub enum EffectClass {
97    /// Reads only; no observable effect on any state.
98    PureRead,
99    /// Writes to Traverse-owned state (`DataStore`, trace).
100    StateWrite,
101    /// Calls an external system that is not owned/reversible by Traverse.
102    ExternalEffect,
103    /// An external effect that cannot be undone or compensated.
104    IrreversibleEffect,
105}
106
107/// Whether repeated invocation with the same inputs is guaranteed to agree.
108#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
109#[serde(rename_all = "snake_case")]
110pub enum DeterminismClass {
111    Deterministic,
112    ExternallyVariable,
113    ModelDerived,
114}
115
116/// Field-level data classification and egress policy for this capability's
117/// declared inputs/outputs (spec 109 FR-005, FR-011). Schema compatibility
118/// alone never authorizes disclosure of a classified field.
119#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
120pub struct DataFlowPolicy {
121    #[serde(default)]
122    pub accepted_data_classifications: Vec<FieldDataClassification>,
123    #[serde(default)]
124    pub produced_data_classifications: Vec<FieldDataClassification>,
125    #[serde(default = "default_egress_policy")]
126    pub egress_policy: EgressPolicy,
127}
128
129impl Default for DataFlowPolicy {
130    fn default() -> Self {
131        Self {
132            accepted_data_classifications: Vec::new(),
133            produced_data_classifications: Vec::new(),
134            egress_policy: default_egress_policy(),
135        }
136    }
137}
138
139/// Declares the classification of one field, addressed by a JSON Pointer
140/// (RFC 6901) into the capability's `inputs.schema` or `outputs.schema`.
141#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
142pub struct FieldDataClassification {
143    pub field_path: String,
144    pub classification: DataClassification,
145}
146
147#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
148#[serde(rename_all = "snake_case")]
149pub enum DataClassification {
150    Public,
151    Internal,
152    Confidential,
153    Restricted,
154}
155
156/// Which connectors classified data produced/accepted by this capability may
157/// legally flow to. `Denied` means no external connector egress is permitted
158/// regardless of what connectors the capability is otherwise wired to.
159#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
160#[serde(rename_all = "snake_case")]
161pub enum EgressPolicy {
162    Denied,
163    AllowedConnectors(Vec<String>),
164}
165
166fn default_egress_policy() -> EgressPolicy {
167    EgressPolicy::Denied
168}
169
170/// Reliability semantics a caller MUST honor when invoking this capability.
171#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
172pub struct ReliabilityMetadata {
173    pub idempotency_required: bool,
174    pub retryable: bool,
175    pub compensation_available: bool,
176}
177
178/// Conservative migration default for contracts published before spec 109:
179/// the most restrictive classification on every dimension, so a capability
180/// never becomes silently automatic-eligible just because it predates risk
181/// metadata.
182#[must_use]
183pub fn default_risk_metadata() -> RiskMetadata {
184    RiskMetadata {
185        effect_class: EffectClass::IrreversibleEffect,
186        determinism_class: DeterminismClass::ModelDerived,
187        data_flow: DataFlowPolicy::default(),
188        reliability: ReliabilityMetadata {
189            idempotency_required: true,
190            retryable: false,
191            compensation_available: false,
192        },
193    }
194}
195
196/// Spec 109 FR-006: whether a proposal using only this capability's declared
197/// risk classes is eligible to run without an authorization token. Every
198/// caller that gates automatic execution MUST consume this single function
199/// rather than re-deriving the rule from individual fields.
200#[must_use]
201pub fn is_automatic_eligible(risk: &RiskMetadata) -> bool {
202    risk.effect_class == EffectClass::PureRead
203        && risk.determinism_class == DeterminismClass::Deterministic
204        && risk.data_flow.egress_policy == EgressPolicy::Denied
205        && !risk.reliability.idempotency_required
206}
207
208/// An application manifest's declared narrowing of a capability's egress
209/// surface (spec 109 FR-005: "a manifest may only tighten these
210/// requirements"). Every other risk dimension is an immutable fact about the
211/// capability's own behavior and has no manifest-side override.
212#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
213pub struct ManifestRiskPolicy {
214    /// When present, the manifest restricts egress to this connector id
215    /// subset. `Some(vec![])` tightens an `AllowedConnectors` capability down
216    /// to no egress for this deployment.
217    #[serde(default)]
218    pub egress_allowed_connectors: Option<Vec<String>>,
219}
220
221/// Validates that a manifest's declared risk policy only narrows the
222/// capability's immutable, contract-declared egress surface — it MUST NOT
223/// permit a connector the contract does not already allow.
224///
225/// # Errors
226///
227/// Returns [`ValidationFailure`] when the manifest policy would widen egress
228/// beyond what the capability's `RiskMetadata` allows.
229pub fn validate_manifest_risk_policy(
230    risk: &RiskMetadata,
231    policy: &ManifestRiskPolicy,
232) -> Result<(), ValidationFailure> {
233    let mut errors = Vec::new();
234
235    if let Some(declared) = &policy.egress_allowed_connectors {
236        match &risk.data_flow.egress_policy {
237            EgressPolicy::Denied => {
238                if !declared.is_empty() {
239                    errors.push(error(
240                        ValidationErrorCode::RiskPolicyWeakened,
241                        "$.risk_policy.egress_allowed_connectors",
242                        "manifest cannot allow egress for a capability whose contract denies all egress",
243                    ));
244                }
245            }
246            EgressPolicy::AllowedConnectors(allowed) => {
247                for connector_id in declared {
248                    if !allowed.contains(connector_id) {
249                        errors.push(error(
250                            ValidationErrorCode::RiskPolicyWeakened,
251                            "$.risk_policy.egress_allowed_connectors",
252                            &format!(
253                                "manifest allows connector '{connector_id}' the contract's \
254                                 egress policy does not permit"
255                            ),
256                        ));
257                    }
258                }
259            }
260        }
261    }
262
263    if errors.is_empty() {
264        Ok(())
265    } else {
266        Err(ValidationFailure { errors })
267    }
268}
269
270/// One authored use case that demonstrates a concrete input/output path for a capability.
271#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
272pub struct UseCase {
273    pub scenario: String,
274    pub input_example: Value,
275    pub output_example: Value,
276    pub happy: bool,
277    #[serde(default, skip_serializing_if = "Option::is_none")]
278    pub persona_ref: Option<String>,
279}
280
281#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
282pub struct ConnectorContract {
283    pub kind: String,
284    pub schema_version: String,
285    pub connector_id: String,
286    pub version: String,
287    pub capabilities_provided: Vec<String>,
288    pub required_config_schema: Value,
289    pub operation_envelopes: Vec<ConnectorOperationEnvelope>,
290    #[serde(default = "default_connector_targets")]
291    pub supported_placement_targets: Vec<ExecutionTarget>,
292}
293
294#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
295pub struct ConnectorOperationEnvelope {
296    pub operation_id: String,
297    pub request_schema: Value,
298    pub success_schema: Value,
299    pub failure_classes: Vec<String>,
300}
301
302#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
303pub struct ConnectorRequirement {
304    pub connector_id: String,
305    pub version: String,
306}
307
308#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
309pub struct ConnectorInvocation {
310    pub capability_id: String,
311    pub connector_id: String,
312    pub config: Value,
313    pub input: Value,
314}
315
316#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
317pub struct ConnectorOutput {
318    pub output: Value,
319}
320
321#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
322pub struct ConnectorError {
323    pub code: String,
324    pub message: String,
325}
326
327pub trait ConnectorPlugin: Send + Sync {
328    fn connector_id(&self) -> &str;
329    fn version(&self) -> &str;
330    fn capabilities_provided(&self) -> &[String];
331    /// Invoke the connector with runtime-injected config and input.
332    ///
333    /// # Errors
334    ///
335    /// Returns [`ConnectorError`] when the connector cannot satisfy the invocation.
336    fn invoke(&self, invocation: ConnectorInvocation) -> Result<ConnectorOutput, ConnectorError>;
337}
338
339#[must_use]
340#[allow(clippy::too_many_lines)]
341pub fn reference_connector_contracts() -> Vec<ConnectorContract> {
342    vec![
343        reference_connector_contract(
344            "traverse.http",
345            vec!["traverse.http.outbound".to_string()],
346            serde_json::json!({
347                "type": "object",
348                "required": ["base_url"],
349                "properties": {
350                    "base_url": {"type": "string"}
351                },
352                "additionalProperties": false
353            }),
354            vec![connector_operation_envelope(
355                "request",
356                serde_json::json!({
357                    "type": "object",
358                    "required": ["method", "resource_ref", "idempotency_key"],
359                    "properties": {
360                        "method": {"type": "string"},
361                        "resource_ref": {"type": "string"},
362                        "headers_ref": {"type": "string"},
363                        "body_ref": {"type": "string"},
364                        "idempotency_key": {"type": "string"}
365                    },
366                    "additionalProperties": false
367                }),
368                serde_json::json!({
369                    "type": "object",
370                    "required": ["status", "body_ref", "result_class"],
371                    "properties": {
372                        "status": {"type": "integer"},
373                        "body_ref": {"type": "string"},
374                        "result_class": {"type": "string"}
375                    },
376                    "additionalProperties": false
377                }),
378                vec!["configuration", "transport", "authorization", "timeout"],
379            )],
380        ),
381        reference_connector_contract(
382            "traverse.fs.read",
383            vec!["traverse.fs.read".to_string()],
384            serde_json::json!({
385                "type": "object",
386                "required": ["root"],
387                "properties": {
388                    "root": {"type": "string"}
389                },
390                "additionalProperties": false
391            }),
392            vec![connector_operation_envelope(
393                "read",
394                serde_json::json!({
395                    "type": "object",
396                    "required": ["resource_ref", "idempotency_key"],
397                    "properties": {
398                        "resource_ref": {"type": "string"},
399                        "max_bytes": {"type": "integer"},
400                        "idempotency_key": {"type": "string"}
401                    },
402                    "additionalProperties": false
403                }),
404                serde_json::json!({
405                    "type": "object",
406                    "required": ["content_ref", "content_digest", "size", "result_class"],
407                    "properties": {
408                        "content_ref": {"type": "string"},
409                        "content_digest": {"type": "string"},
410                        "size": {"type": "integer"},
411                        "result_class": {"type": "string"}
412                    },
413                    "additionalProperties": false
414                }),
415                vec!["configuration", "not_found", "authorization", "too_large"],
416            )],
417        ),
418        reference_connector_contract(
419            "traverse.env",
420            vec!["traverse.env.read".to_string()],
421            serde_json::json!({
422                "type": "object",
423                "required": ["allowed_keys"],
424                "properties": {
425                    "allowed_keys": {
426                        "type": "array",
427                        "items": {"type": "string"}
428                    }
429                },
430                "additionalProperties": false
431            }),
432            vec![connector_operation_envelope(
433                "read",
434                serde_json::json!({
435                    "type": "object",
436                    "required": ["key_ref", "idempotency_key"],
437                    "properties": {
438                        "key_ref": {"type": "string"},
439                        "idempotency_key": {"type": "string"}
440                    },
441                    "additionalProperties": false
442                }),
443                serde_json::json!({
444                    "type": "object",
445                    "required": ["value_ref", "result_class"],
446                    "properties": {
447                        "value_ref": {"type": "string"},
448                        "result_class": {"type": "string"}
449                    },
450                    "additionalProperties": false
451                }),
452                vec!["configuration", "not_found", "authorization"],
453            )],
454        ),
455        reference_connector_contract(
456            "traverse.object-store",
457            vec!["traverse.object_store.put".to_string()],
458            serde_json::json!({
459                "type": "object",
460                "required": ["authority_ref"],
461                "properties": {
462                    "authority_ref": {"type": "string"},
463                    "retention_classes": {
464                        "type": "array",
465                        "items": {"type": "string"}
466                    }
467                },
468                "additionalProperties": false
469            }),
470            vec![connector_operation_envelope(
471                "put_immutable",
472                serde_json::json!({
473                    "type": "object",
474                    "required": ["content_ref", "media_type", "idempotency_key"],
475                    "properties": {
476                        "content_ref": {"type": "string"},
477                        "media_type": {"type": "string"},
478                        "retention_class": {"type": "string"},
479                        "idempotency_key": {"type": "string"}
480                    },
481                    "additionalProperties": false
482                }),
483                serde_json::json!({
484                    "type": "object",
485                    "required": ["asset_ref", "content_digest", "size", "result_class"],
486                    "properties": {
487                        "asset_ref": {"type": "string"},
488                        "content_digest": {"type": "string"},
489                        "size": {"type": "integer"},
490                        "result_class": {"type": "string"}
491                    },
492                    "additionalProperties": false
493                }),
494                vec!["configuration", "authorization", "quota", "integrity"],
495            )],
496        ),
497        reference_connector_contract(
498            "traverse.state-store",
499            vec!["traverse.state_store.append".to_string()],
500            serde_json::json!({
501                "type": "object",
502                "required": ["authority_ref"],
503                "properties": {
504                    "authority_ref": {"type": "string"},
505                    "record_type_namespace": {"type": "string"}
506                },
507                "additionalProperties": false
508            }),
509            vec![connector_operation_envelope(
510                "append_transition",
511                serde_json::json!({
512                    "type": "object",
513                    "required": ["record_refs", "transition", "idempotency_key"],
514                    "properties": {
515                        "record_refs": {
516                            "type": "array",
517                            "items": {"type": "string"}
518                        },
519                        "transition": {"type": "object"},
520                        "expected_version": {"type": "integer"},
521                        "idempotency_key": {"type": "string"}
522                    },
523                    "additionalProperties": false
524                }),
525                serde_json::json!({
526                    "type": "object",
527                    "required": ["result_ref", "version", "replay", "result_class"],
528                    "properties": {
529                        "result_ref": {"type": "string"},
530                        "version": {"type": "integer"},
531                        "replay": {"type": "boolean"},
532                        "result_class": {"type": "string"}
533                    },
534                    "additionalProperties": false
535                }),
536                vec!["configuration", "authorization", "conflict", "integrity"],
537            )],
538        ),
539        reference_connector_contract(
540            "traverse.scheduler",
541            vec!["traverse.scheduler.schedule".to_string()],
542            serde_json::json!({
543                "type": "object",
544                "required": ["authority_ref"],
545                "properties": {
546                    "authority_ref": {"type": "string"},
547                    "allowed_job_kinds": {
548                        "type": "array",
549                        "items": {"type": "string"}
550                    }
551                },
552                "additionalProperties": false
553            }),
554            vec![connector_operation_envelope(
555                "schedule_invocation",
556                serde_json::json!({
557                    "type": "object",
558                    "required": ["job_kind", "calendar_policy_ref", "logical_deadline", "idempotency_key"],
559                    "properties": {
560                        "job_kind": {"type": "string"},
561                        "calendar_policy_ref": {"type": "string"},
562                        "logical_deadline": {"type": "string"},
563                        "cancellation_ref": {"type": "string"},
564                        "idempotency_key": {"type": "string"}
565                    },
566                    "additionalProperties": false
567                }),
568                serde_json::json!({
569                    "type": "object",
570                    "required": ["invocation_ref", "idempotency_key", "result_class"],
571                    "properties": {
572                        "invocation_ref": {"type": "string"},
573                        "idempotency_key": {"type": "string"},
574                        "result_class": {"type": "string"}
575                    },
576                    "additionalProperties": false
577                }),
578                vec!["configuration", "authorization", "deadline", "quota"],
579            )],
580        ),
581    ]
582}
583
584fn reference_connector_contract(
585    connector_id: &str,
586    capabilities_provided: Vec<String>,
587    required_config_schema: Value,
588    operation_envelopes: Vec<ConnectorOperationEnvelope>,
589) -> ConnectorContract {
590    ConnectorContract {
591        kind: CONNECTOR_CONTRACT_KIND.to_string(),
592        schema_version: SUPPORTED_SCHEMA_VERSION.to_string(),
593        connector_id: connector_id.to_string(),
594        version: "1.0.0".to_string(),
595        capabilities_provided,
596        required_config_schema,
597        operation_envelopes,
598        supported_placement_targets: default_connector_targets(),
599    }
600}
601
602fn connector_operation_envelope(
603    operation_id: &str,
604    request_schema: Value,
605    success_schema: Value,
606    failure_classes: Vec<&str>,
607) -> ConnectorOperationEnvelope {
608    ConnectorOperationEnvelope {
609        operation_id: operation_id.to_string(),
610        request_schema,
611        success_schema,
612        failure_classes: failure_classes
613            .into_iter()
614            .map(std::string::ToString::to_string)
615            .collect(),
616    }
617}
618
619#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
620pub struct EventContract {
621    pub kind: String,
622    pub schema_version: String,
623    pub id: String,
624    pub namespace: String,
625    pub name: String,
626    pub version: String,
627    pub lifecycle: Lifecycle,
628    pub owner: Owner,
629    pub summary: String,
630    pub description: String,
631    pub payload: EventPayload,
632    pub classification: EventClassification,
633    pub publishers: Vec<CapabilityReference>,
634    pub subscribers: Vec<CapabilityReference>,
635    pub policies: Vec<IdReference>,
636    pub tags: Vec<String>,
637    pub provenance: EventProvenance,
638    pub evidence: Vec<EventValidationEvidence>,
639}
640
641#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
642pub struct EventPayload {
643    pub schema: Value,
644    pub compatibility: PayloadCompatibility,
645}
646
647#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
648#[serde(rename_all = "kebab-case")]
649pub enum PayloadCompatibility {
650    BackwardCompatible,
651    ForwardCompatible,
652    Breaking,
653}
654
655#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
656pub struct EventClassification {
657    pub domain: String,
658    pub bounded_context: String,
659    pub event_type: EventType,
660    pub tags: Vec<String>,
661}
662
663#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
664#[serde(rename_all = "snake_case")]
665pub enum EventType {
666    Domain,
667    Integration,
668    System,
669}
670
671#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
672pub struct CapabilityReference {
673    pub capability_id: String,
674    pub version: String,
675}
676
677#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
678pub struct EventProvenance {
679    pub source: EventProvenanceSource,
680    pub author: String,
681    pub created_at: String,
682}
683
684#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
685#[serde(rename_all = "kebab-case")]
686pub enum EventProvenanceSource {
687    Greenfield,
688    Brownfield,
689    AiGenerated,
690    Extracted,
691}
692
693#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
694pub struct EventValidationEvidence {
695    pub kind: String,
696    pub r#ref: String,
697}
698
699#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
700#[serde(rename_all = "snake_case")]
701pub enum Lifecycle {
702    Draft,
703    Active,
704    Deprecated,
705    Retired,
706    Archived,
707}
708
709impl Lifecycle {
710    #[must_use]
711    pub fn is_runtime_eligible(&self) -> bool {
712        matches!(self, Self::Active | Self::Deprecated)
713    }
714}
715
716#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
717pub struct Owner {
718    pub team: String,
719    pub contact: String,
720}
721
722#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
723pub struct SchemaContainer {
724    pub schema: Value,
725}
726
727#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
728pub struct Condition {
729    pub id: String,
730    pub description: String,
731}
732
733#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
734pub struct SideEffect {
735    pub kind: SideEffectKind,
736    pub description: String,
737}
738
739#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
740#[serde(rename_all = "snake_case")]
741pub enum SideEffectKind {
742    None,
743    MemoryOnly,
744    EventEmission,
745    ExternalCall,
746    StateChange,
747}
748
749#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
750pub struct EventReference {
751    pub event_id: String,
752    pub version: String,
753}
754
755#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
756pub struct IdReference {
757    pub id: String,
758}
759
760#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
761pub struct Execution {
762    pub binary_format: BinaryFormat,
763    pub entrypoint: Entrypoint,
764    pub preferred_targets: Vec<ExecutionTarget>,
765    pub constraints: ExecutionConstraints,
766}
767
768#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
769#[serde(rename_all = "snake_case")]
770pub enum BinaryFormat {
771    Wasm,
772}
773
774/// UMA service type classification — governs placement routing and event routing.
775#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
776#[serde(rename_all = "snake_case")]
777pub enum ServiceType {
778    /// Runs anywhere; no persistent state required. Default for backward compatibility.
779    #[default]
780    Stateless,
781    /// Activated by an incoming event; requires a non-empty `event_trigger`.
782    Subscribable,
783    /// Requires managed persistence; cannot be placed in Browser environments.
784    Stateful,
785}
786
787fn default_permitted_targets() -> Vec<ExecutionTarget> {
788    vec![
789        ExecutionTarget::Local,
790        ExecutionTarget::Browser,
791        ExecutionTarget::Edge,
792        ExecutionTarget::Cloud,
793        ExecutionTarget::Worker,
794        ExecutionTarget::Device,
795    ]
796}
797
798fn default_connector_targets() -> Vec<ExecutionTarget> {
799    vec![ExecutionTarget::Local, ExecutionTarget::Cloud]
800}
801
802#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
803pub struct Entrypoint {
804    pub kind: EntrypointKind,
805    pub command: String,
806}
807
808#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
809#[serde(rename_all = "kebab-case")]
810pub enum EntrypointKind {
811    WasiCommand,
812}
813
814#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Ord, PartialOrd)]
815#[serde(rename_all = "snake_case")]
816pub enum ExecutionTarget {
817    Local,
818    Browser,
819    Edge,
820    Cloud,
821    Worker,
822    Device,
823}
824
825#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
826pub struct ExecutionConstraints {
827    pub host_api_access: HostApiAccess,
828    pub network_access: NetworkAccess,
829    pub filesystem_access: FilesystemAccess,
830}
831
832#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
833#[serde(rename_all = "snake_case")]
834pub enum HostApiAccess {
835    None,
836    ExceptionRequired,
837}
838
839#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
840#[serde(rename_all = "snake_case")]
841pub enum NetworkAccess {
842    Forbidden,
843    Required,
844}
845
846#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
847#[serde(rename_all = "snake_case")]
848pub enum FilesystemAccess {
849    None,
850    SandboxOnly,
851}
852
853#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
854pub struct DependencyReference {
855    pub artifact_type: DependencyArtifactType,
856    pub id: String,
857    pub version: String,
858}
859
860#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
861#[serde(rename_all = "snake_case")]
862pub enum DependencyArtifactType {
863    Capability,
864    Event,
865    Policy,
866}
867
868#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
869pub struct Provenance {
870    pub source: ProvenanceSource,
871    pub author: String,
872    pub created_at: String,
873    #[serde(default)]
874    pub spec_ref: Option<String>,
875    #[serde(default)]
876    pub adr_refs: Vec<String>,
877    #[serde(default)]
878    pub exception_refs: Vec<String>,
879}
880
881#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
882#[serde(rename_all = "kebab-case")]
883pub enum ProvenanceSource {
884    Greenfield,
885    BrownfieldExtracted,
886    AiGenerated,
887    AiAssisted,
888}
889
890#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
891pub struct ValidationEvidence {
892    pub evidence_id: String,
893    #[serde(rename = "type")]
894    pub evidence_type: EvidenceType,
895    pub status: EvidenceStatus,
896}
897
898#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
899#[serde(rename_all = "snake_case")]
900pub enum EvidenceType {
901    SpecAlignment,
902    ContractValidation,
903    Compatibility,
904}
905
906#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
907#[serde(rename_all = "snake_case")]
908pub enum EvidenceStatus {
909    Passed,
910    Failed,
911    Superseded,
912}
913
914#[derive(Debug, Clone, PartialEq, Eq)]
915pub struct PublishedContractRecord {
916    pub id: String,
917    pub version: String,
918    pub governed_content_digest: String,
919    pub lifecycle: Lifecycle,
920}
921
922#[derive(Debug, Clone, PartialEq, Eq)]
923pub struct PublishedEventRecord {
924    pub id: String,
925    pub version: String,
926    pub governed_content_digest: String,
927    pub lifecycle: Lifecycle,
928}
929
930#[derive(Debug, Clone, PartialEq, Eq)]
931pub struct ValidationContext<'a> {
932    pub governing_spec: &'a str,
933    pub validator_version: &'a str,
934    pub existing_published: Option<&'a PublishedContractRecord>,
935}
936
937#[derive(Debug, Clone, PartialEq, Eq)]
938pub struct EventValidationContext<'a> {
939    pub governing_spec: &'a str,
940    pub validator_version: &'a str,
941    pub existing_published: Option<&'a PublishedEventRecord>,
942}
943
944#[derive(Debug, Clone, PartialEq, Eq)]
945pub struct ValidationResult {
946    pub normalized: CapabilityContract,
947    pub evidence: ProducedValidationEvidence,
948}
949
950#[derive(Debug, Clone, PartialEq, Eq)]
951pub struct EventValidationResult {
952    pub normalized: EventContract,
953    pub evidence: ProducedValidationEvidence,
954}
955
956#[derive(Debug, Clone, PartialEq, Eq)]
957pub struct ProducedValidationEvidence {
958    pub artifact_id: String,
959    pub artifact_version: String,
960    pub governing_spec: String,
961    pub validator_version: String,
962    pub status: EvidenceStatus,
963}
964
965#[derive(Debug, Clone, PartialEq, Eq)]
966pub struct ValidationFailure {
967    pub errors: Vec<ValidationError>,
968}
969
970#[derive(Debug, Clone, PartialEq, Eq)]
971pub struct ValidationError {
972    pub code: ValidationErrorCode,
973    pub message: String,
974    pub path: String,
975    pub severity: ErrorSeverity,
976}
977
978#[derive(Debug, Clone, PartialEq, Eq)]
979pub enum ValidationErrorCode {
980    MissingRequiredField,
981    InvalidLiteral,
982    InvalidFormat,
983    InvalidSemver,
984    InconsistentIdentity,
985    DuplicateItem,
986    InvalidCapabilityBoundary,
987    InvalidEventBoundary,
988    UnsupportedBinaryFormat,
989    UnsupportedEntrypoint,
990    PortabilityExceptionRequired,
991    ImmutableVersionConflict,
992    InvalidDependencyRef,
993    /// `service_type: stateful` combined with `Browser` in `permitted_targets`.
994    InvalidPlacementConstraint,
995    /// `service_type: subscribable` without a non-empty `event_trigger`.
996    MissingEventTrigger,
997    InvalidConnectorContract,
998    InvalidConnectorRequirement,
999    /// A manifest-declared risk policy would widen egress beyond what the
1000    /// capability's immutable `RiskMetadata` allows.
1001    RiskPolicyWeakened,
1002}
1003
1004#[derive(Debug, Clone, PartialEq, Eq)]
1005pub enum ErrorSeverity {
1006    Error,
1007}
1008
1009/// Parses a capability contract from raw JSON text.
1010///
1011/// # Errors
1012///
1013/// Returns [`ValidationFailure`] when the JSON payload cannot be deserialized
1014/// into the capability contract model.
1015pub fn parse_contract(json: &str) -> Result<CapabilityContract, ValidationFailure> {
1016    serde_json::from_str::<CapabilityContract>(json).map_err(|error| ValidationFailure {
1017        errors: vec![ValidationError {
1018            code: ValidationErrorCode::InvalidFormat,
1019            message: error.to_string(),
1020            path: "$".to_string(),
1021            severity: ErrorSeverity::Error,
1022        }],
1023    })
1024}
1025
1026/// Parses an event contract from raw JSON text.
1027///
1028/// # Errors
1029///
1030/// Returns [`ValidationFailure`] when the JSON payload cannot be deserialized
1031/// into the event contract model.
1032pub fn parse_event_contract(json: &str) -> Result<EventContract, ValidationFailure> {
1033    serde_json::from_str::<EventContract>(json).map_err(|error| ValidationFailure {
1034        errors: vec![ValidationError {
1035            code: ValidationErrorCode::InvalidFormat,
1036            message: error.to_string(),
1037            path: "$".to_string(),
1038            severity: ErrorSeverity::Error,
1039        }],
1040    })
1041}
1042
1043/// Parses a connector contract from raw JSON text.
1044///
1045/// # Errors
1046///
1047/// Returns [`ValidationFailure`] when the JSON payload cannot be deserialized
1048/// into the connector contract model.
1049pub fn parse_connector_contract(json: &str) -> Result<ConnectorContract, ValidationFailure> {
1050    serde_json::from_str::<ConnectorContract>(json).map_err(|error| ValidationFailure {
1051        errors: vec![ValidationError {
1052            code: ValidationErrorCode::InvalidFormat,
1053            message: error.to_string(),
1054            path: "$".to_string(),
1055            severity: ErrorSeverity::Error,
1056        }],
1057    })
1058}
1059
1060/// Validates a parsed capability contract against the governed `v0.1` rules.
1061///
1062/// # Errors
1063///
1064/// Returns [`ValidationFailure`] when structural or semantic validation fails.
1065pub fn validate_contract(
1066    mut contract: CapabilityContract,
1067    context: &ValidationContext<'_>,
1068) -> Result<ValidationResult, ValidationFailure> {
1069    let mut errors = Vec::new();
1070
1071    validate_kind(&contract, &mut errors);
1072    validate_schema_version(&contract, &mut errors);
1073    validate_identity(&contract, &mut errors);
1074    validate_semver(&contract.version, "$.version", &mut errors);
1075    validate_owner(&contract.owner, &mut errors);
1076    validate_summary(&contract.summary, "$.summary", &mut errors);
1077    validate_description(&contract.description, "$.description", &mut errors);
1078    validate_schema_container(&contract.inputs, "$.inputs.schema", &mut errors);
1079    validate_schema_container(&contract.outputs, "$.outputs.schema", &mut errors);
1080    validate_conditions(&contract.preconditions, "$.preconditions", &mut errors);
1081    validate_conditions(&contract.postconditions, "$.postconditions", &mut errors);
1082    validate_side_effects(&contract.side_effects, &mut errors);
1083    validate_event_references(&contract.emits, "$.emits", &mut errors);
1084    validate_event_references(&contract.consumes, "$.consumes", &mut errors);
1085    validate_id_references(&contract.permissions, "$.permissions", &mut errors);
1086    validate_execution(&contract.execution, &contract.provenance, &mut errors);
1087    validate_id_references(&contract.policies, "$.policies", &mut errors);
1088    validate_dependencies(&contract.dependencies, &mut errors);
1089    validate_connector_requirements(&contract.connector_requirements, &mut errors);
1090    validate_provenance(&contract.provenance, &mut errors);
1091    validate_evidence(&contract.evidence, &mut errors);
1092    validate_boundary(&contract, &mut errors);
1093    validate_placement_constraints(&contract, &mut errors);
1094    validate_risk_metadata(&contract.risk, &mut errors);
1095    validate_published_record(&contract, context.existing_published, &mut errors);
1096
1097    if !errors.is_empty() {
1098        return Err(ValidationFailure { errors });
1099    }
1100
1101    contract.evidence.clear();
1102
1103    Ok(ValidationResult {
1104        evidence: ProducedValidationEvidence {
1105            artifact_id: contract.id.clone(),
1106            artifact_version: contract.version.clone(),
1107            governing_spec: context.governing_spec.to_string(),
1108            validator_version: context.validator_version.to_string(),
1109            status: EvidenceStatus::Passed,
1110        },
1111        normalized: contract,
1112    })
1113}
1114
1115/// Validates a parsed connector contract.
1116///
1117/// # Errors
1118///
1119/// Returns [`ValidationFailure`] when structural or semantic validation fails.
1120pub fn validate_connector_contract(
1121    contract: ConnectorContract,
1122) -> Result<ConnectorContract, ValidationFailure> {
1123    let mut errors = Vec::new();
1124
1125    if contract.kind != CONNECTOR_CONTRACT_KIND {
1126        errors.push(error(
1127            ValidationErrorCode::InvalidLiteral,
1128            "$.kind",
1129            "kind must equal connector_contract",
1130        ));
1131    }
1132    if contract.schema_version != SUPPORTED_SCHEMA_VERSION {
1133        errors.push(error(
1134            ValidationErrorCode::InvalidLiteral,
1135            "$.schema_version",
1136            "schema_version must equal 1.0.0",
1137        ));
1138    }
1139    validate_non_empty(&contract.connector_id, "$.connector_id", &mut errors);
1140    validate_semver(&contract.version, "$.version", &mut errors);
1141    validate_unique_strings(
1142        &contract.capabilities_provided,
1143        "$.capabilities_provided",
1144        "capabilities_provided must be unique",
1145        &mut errors,
1146    );
1147    if contract.capabilities_provided.is_empty() {
1148        errors.push(error(
1149            ValidationErrorCode::MissingRequiredField,
1150            "$.capabilities_provided",
1151            "capabilities_provided must contain at least one capability id",
1152        ));
1153    }
1154    validate_schema_value(
1155        &contract.required_config_schema,
1156        "$.required_config_schema",
1157        &mut errors,
1158    );
1159    validate_connector_operation_envelopes(&contract.operation_envelopes, &mut errors);
1160    if contract.supported_placement_targets.is_empty() {
1161        errors.push(error(
1162            ValidationErrorCode::MissingRequiredField,
1163            "$.supported_placement_targets",
1164            "supported_placement_targets must contain at least one target",
1165        ));
1166    }
1167    let unique_targets: BTreeSet<_> = contract
1168        .supported_placement_targets
1169        .iter()
1170        .cloned()
1171        .collect();
1172    if unique_targets.len() != contract.supported_placement_targets.len() {
1173        errors.push(error(
1174            ValidationErrorCode::DuplicateItem,
1175            "$.supported_placement_targets",
1176            "supported_placement_targets must be unique",
1177        ));
1178    }
1179
1180    if !errors.is_empty() {
1181        return Err(ValidationFailure { errors });
1182    }
1183
1184    Ok(contract)
1185}
1186
1187fn validate_connector_operation_envelopes(
1188    operation_envelopes: &[ConnectorOperationEnvelope],
1189    errors: &mut Vec<ValidationError>,
1190) {
1191    if operation_envelopes.is_empty() {
1192        errors.push(error(
1193            ValidationErrorCode::MissingRequiredField,
1194            "$.operation_envelopes",
1195            "operation_envelopes must contain at least one operation",
1196        ));
1197        return;
1198    }
1199
1200    let mut seen = HashSet::new();
1201    for (index, envelope) in operation_envelopes.iter().enumerate() {
1202        let operation_id_path = format!("$.operation_envelopes[{index}].operation_id");
1203        validate_non_empty(&envelope.operation_id, &operation_id_path, errors);
1204        if !seen.insert(envelope.operation_id.clone()) {
1205            errors.push(error(
1206                ValidationErrorCode::DuplicateItem,
1207                &operation_id_path,
1208                "operation_id values must be unique",
1209            ));
1210        }
1211
1212        validate_schema_value(
1213            &envelope.request_schema,
1214            &format!("$.operation_envelopes[{index}].request_schema"),
1215            errors,
1216        );
1217        validate_schema_value(
1218            &envelope.success_schema,
1219            &format!("$.operation_envelopes[{index}].success_schema"),
1220            errors,
1221        );
1222
1223        let failure_classes_path = format!("$.operation_envelopes[{index}].failure_classes");
1224        if envelope.failure_classes.is_empty() {
1225            errors.push(error(
1226                ValidationErrorCode::MissingRequiredField,
1227                &failure_classes_path,
1228                "failure_classes must contain at least one stable failure class",
1229            ));
1230        }
1231        validate_unique_strings(
1232            &envelope.failure_classes,
1233            &failure_classes_path,
1234            "failure_classes must be unique",
1235            errors,
1236        );
1237        for (failure_index, failure_class) in envelope.failure_classes.iter().enumerate() {
1238            validate_non_empty(
1239                failure_class,
1240                &format!("$.operation_envelopes[{index}].failure_classes[{failure_index}]"),
1241                errors,
1242            );
1243        }
1244    }
1245}
1246
1247/// Validates a parsed event contract against the governed `v0.1` rules.
1248///
1249/// # Errors
1250///
1251/// Returns [`ValidationFailure`] when structural or semantic validation fails.
1252pub fn validate_event_contract(
1253    mut contract: EventContract,
1254    context: &EventValidationContext<'_>,
1255) -> Result<EventValidationResult, ValidationFailure> {
1256    let mut errors = Vec::new();
1257
1258    validate_event_kind(&contract, &mut errors);
1259    validate_event_schema_version(&contract, &mut errors);
1260    validate_event_identity(&contract, &mut errors);
1261    validate_semver(&contract.version, "$.version", &mut errors);
1262    validate_owner(&contract.owner, &mut errors);
1263    validate_summary(&contract.summary, "$.summary", &mut errors);
1264    validate_description(&contract.description, "$.description", &mut errors);
1265    validate_event_payload(&contract.payload, &mut errors);
1266    validate_event_classification(&contract.classification, &mut errors);
1267    validate_capability_references(&contract.publishers, "$.publishers", true, &mut errors);
1268    validate_capability_references(&contract.subscribers, "$.subscribers", false, &mut errors);
1269    validate_id_references(&contract.policies, "$.policies", &mut errors);
1270    validate_tags(&contract.tags, "$.tags", true, &mut errors);
1271    validate_event_provenance(&contract.provenance, &mut errors);
1272    validate_event_evidence(&contract.evidence, &mut errors);
1273    validate_event_boundary(&contract, &mut errors);
1274    validate_published_event_record(&contract, context.existing_published, &mut errors);
1275
1276    if !errors.is_empty() {
1277        return Err(ValidationFailure { errors });
1278    }
1279
1280    contract.evidence.clear();
1281
1282    Ok(EventValidationResult {
1283        evidence: ProducedValidationEvidence {
1284            artifact_id: contract.id.clone(),
1285            artifact_version: contract.version.clone(),
1286            governing_spec: context.governing_spec.to_string(),
1287            validator_version: context.validator_version.to_string(),
1288            status: EvidenceStatus::Passed,
1289        },
1290        normalized: contract,
1291    })
1292}
1293
1294#[must_use]
1295pub fn governed_content_digest(contract: &CapabilityContract) -> String {
1296    let mut clone = contract.clone();
1297    clone.evidence.clear();
1298    let json = format!("{clone:?}");
1299    let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
1300    for byte in json.as_bytes() {
1301        hash ^= u64::from(*byte);
1302        hash = hash.wrapping_mul(0x0000_0001_0000_01b3);
1303    }
1304    format!("{GOVERNED_CONTENT_VERSION}:{hash:016x}")
1305}
1306
1307#[must_use]
1308pub fn governed_event_content_digest(contract: &EventContract) -> String {
1309    let mut clone = contract.clone();
1310    clone.evidence.clear();
1311    let json = format!("{clone:?}");
1312    let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
1313    for byte in json.as_bytes() {
1314        hash ^= u64::from(*byte);
1315        hash = hash.wrapping_mul(0x0000_0001_0000_01b3);
1316    }
1317    format!("{GOVERNED_CONTENT_VERSION}:{hash:016x}")
1318}
1319
1320fn validate_kind(contract: &CapabilityContract, errors: &mut Vec<ValidationError>) {
1321    if contract.kind != CAPABILITY_CONTRACT_KIND {
1322        errors.push(error(
1323            ValidationErrorCode::InvalidLiteral,
1324            "$.kind",
1325            "kind must equal capability_contract",
1326        ));
1327    }
1328}
1329
1330fn validate_event_kind(contract: &EventContract, errors: &mut Vec<ValidationError>) {
1331    if contract.kind != EVENT_CONTRACT_KIND {
1332        errors.push(error(
1333            ValidationErrorCode::InvalidLiteral,
1334            "$.kind",
1335            "kind must equal event_contract",
1336        ));
1337    }
1338}
1339
1340fn validate_schema_version(contract: &CapabilityContract, errors: &mut Vec<ValidationError>) {
1341    if contract.schema_version != SUPPORTED_SCHEMA_VERSION {
1342        errors.push(error(
1343            ValidationErrorCode::InvalidLiteral,
1344            "$.schema_version",
1345            "schema_version must equal 1.0.0",
1346        ));
1347    }
1348}
1349
1350fn validate_event_schema_version(contract: &EventContract, errors: &mut Vec<ValidationError>) {
1351    if contract.schema_version != SUPPORTED_SCHEMA_VERSION {
1352        errors.push(error(
1353            ValidationErrorCode::InvalidLiteral,
1354            "$.schema_version",
1355            "schema_version must equal 1.0.0",
1356        ));
1357    }
1358}
1359
1360fn validate_identity(contract: &CapabilityContract, errors: &mut Vec<ValidationError>) {
1361    if !is_valid_namespace(&contract.namespace) {
1362        errors.push(error(
1363            ValidationErrorCode::InvalidFormat,
1364            "$.namespace",
1365            "namespace must be dot-separated lowercase kebab-case segments",
1366        ));
1367    }
1368
1369    if !is_valid_name(&contract.name) {
1370        errors.push(error(
1371            ValidationErrorCode::InvalidFormat,
1372            "$.name",
1373            "name must be lowercase kebab-case",
1374        ));
1375    }
1376
1377    let expected_id = format!("{}.{}", contract.namespace, contract.name);
1378    if contract.id != expected_id {
1379        errors.push(error(
1380            ValidationErrorCode::InconsistentIdentity,
1381            "$.id",
1382            "id must equal namespace.name",
1383        ));
1384    }
1385}
1386
1387fn validate_event_identity(contract: &EventContract, errors: &mut Vec<ValidationError>) {
1388    if !is_valid_namespace(&contract.namespace) {
1389        errors.push(error(
1390            ValidationErrorCode::InvalidFormat,
1391            "$.namespace",
1392            "namespace must be dot-separated lowercase kebab-case segments",
1393        ));
1394    }
1395
1396    if !is_valid_name(&contract.name) {
1397        errors.push(error(
1398            ValidationErrorCode::InvalidFormat,
1399            "$.name",
1400            "name must be lowercase kebab-case",
1401        ));
1402    }
1403
1404    let expected_id = format!("{}.{}", contract.namespace, contract.name);
1405    if contract.id != expected_id {
1406        errors.push(error(
1407            ValidationErrorCode::InconsistentIdentity,
1408            "$.id",
1409            "id must equal namespace.name",
1410        ));
1411    }
1412}
1413
1414fn validate_semver(value: &str, path: &str, errors: &mut Vec<ValidationError>) {
1415    if Version::parse(value).is_err() {
1416        errors.push(error(
1417            ValidationErrorCode::InvalidSemver,
1418            path,
1419            "version must match MAJOR.MINOR.PATCH",
1420        ));
1421    }
1422}
1423
1424fn validate_owner(owner: &Owner, errors: &mut Vec<ValidationError>) {
1425    validate_non_empty(&owner.team, "$.owner.team", errors);
1426    validate_non_empty(&owner.contact, "$.owner.contact", errors);
1427}
1428
1429fn validate_summary(summary: &str, path: &str, errors: &mut Vec<ValidationError>) {
1430    if summary.trim().len() < 10 || summary.len() > 200 {
1431        errors.push(error(
1432            ValidationErrorCode::InvalidFormat,
1433            path,
1434            "summary length must be between 10 and 200 characters",
1435        ));
1436    }
1437}
1438
1439fn validate_description(description: &str, path: &str, errors: &mut Vec<ValidationError>) {
1440    if description.trim().len() < 20 {
1441        errors.push(error(
1442            ValidationErrorCode::InvalidFormat,
1443            path,
1444            "description must be at least 20 characters",
1445        ));
1446    }
1447}
1448
1449fn validate_schema_container(
1450    container: &SchemaContainer,
1451    path: &str,
1452    errors: &mut Vec<ValidationError>,
1453) {
1454    validate_schema_value(&container.schema, path, errors);
1455}
1456
1457fn validate_schema_value(schema: &Value, path: &str, errors: &mut Vec<ValidationError>) {
1458    if !schema.is_object() {
1459        errors.push(error(
1460            ValidationErrorCode::InvalidFormat,
1461            path,
1462            "schema must be a JSON object",
1463        ));
1464    }
1465}
1466
1467fn validate_event_payload(payload: &EventPayload, errors: &mut Vec<ValidationError>) {
1468    if !payload.schema.is_object() {
1469        errors.push(error(
1470            ValidationErrorCode::InvalidFormat,
1471            "$.payload.schema",
1472            "schema must be a JSON object",
1473        ));
1474    }
1475}
1476
1477fn validate_event_classification(
1478    classification: &EventClassification,
1479    errors: &mut Vec<ValidationError>,
1480) {
1481    validate_min_length(
1482        &classification.domain,
1483        "$.classification.domain",
1484        2,
1485        "domain must be at least 2 characters",
1486        errors,
1487    );
1488    validate_min_length(
1489        &classification.bounded_context,
1490        "$.classification.bounded_context",
1491        2,
1492        "bounded_context must be at least 2 characters",
1493        errors,
1494    );
1495    validate_tags(&classification.tags, "$.classification.tags", true, errors);
1496}
1497
1498fn validate_capability_references(
1499    references: &[CapabilityReference],
1500    path: &str,
1501    require_one: bool,
1502    errors: &mut Vec<ValidationError>,
1503) {
1504    if require_one && references.is_empty() {
1505        errors.push(error(
1506            ValidationErrorCode::MissingRequiredField,
1507            path,
1508            "array must contain at least one item",
1509        ));
1510    }
1511
1512    let mut seen = HashSet::new();
1513    for (index, item) in references.iter().enumerate() {
1514        let id_path = format!("{path}[{index}].capability_id");
1515        let version_path = format!("{path}[{index}].version");
1516        validate_non_empty(&item.capability_id, &id_path, errors);
1517        validate_semver(&item.version, &version_path, errors);
1518        if !seen.insert((item.capability_id.clone(), item.version.clone())) {
1519            errors.push(error(
1520                ValidationErrorCode::DuplicateItem,
1521                &id_path,
1522                "capability references must be unique by id and version",
1523            ));
1524        }
1525    }
1526}
1527
1528fn validate_tags(
1529    tags: &[String],
1530    path: &str,
1531    require_one: bool,
1532    errors: &mut Vec<ValidationError>,
1533) {
1534    if require_one && tags.is_empty() {
1535        errors.push(error(
1536            ValidationErrorCode::MissingRequiredField,
1537            path,
1538            "array must contain at least one item",
1539        ));
1540    }
1541    for (index, tag) in tags.iter().enumerate() {
1542        validate_non_empty(tag, &format!("{path}[{index}]"), errors);
1543    }
1544    validate_unique_strings(tags, path, "values must be unique", errors);
1545}
1546
1547fn validate_event_provenance(provenance: &EventProvenance, errors: &mut Vec<ValidationError>) {
1548    validate_non_empty(&provenance.author, "$.provenance.author", errors);
1549    validate_non_empty(&provenance.created_at, "$.provenance.created_at", errors);
1550}
1551
1552fn validate_event_evidence(
1553    evidence: &[EventValidationEvidence],
1554    errors: &mut Vec<ValidationError>,
1555) {
1556    let mut seen = HashSet::new();
1557    for (index, item) in evidence.iter().enumerate() {
1558        let kind_path = format!("$.evidence[{index}].kind");
1559        let ref_path = format!("$.evidence[{index}].ref");
1560        validate_non_empty(&item.kind, &kind_path, errors);
1561        validate_non_empty(&item.r#ref, &ref_path, errors);
1562        if !seen.insert((item.kind.clone(), item.r#ref.clone())) {
1563            errors.push(error(
1564                ValidationErrorCode::DuplicateItem,
1565                &kind_path,
1566                "evidence entries must be unique by kind and ref",
1567            ));
1568        }
1569    }
1570}
1571
1572fn validate_conditions(conditions: &[Condition], path: &str, errors: &mut Vec<ValidationError>) {
1573    let mut seen = HashSet::new();
1574    for (index, condition) in conditions.iter().enumerate() {
1575        let id_path = format!("{path}[{index}].id");
1576        let description_path = format!("{path}[{index}].description");
1577        validate_non_empty(&condition.id, &id_path, errors);
1578        validate_non_empty(&condition.description, &description_path, errors);
1579        if !seen.insert(condition.id.clone()) {
1580            errors.push(error(
1581                ValidationErrorCode::DuplicateItem,
1582                &id_path,
1583                "condition ids must be unique",
1584            ));
1585        }
1586    }
1587}
1588
1589fn validate_side_effects(side_effects: &[SideEffect], errors: &mut Vec<ValidationError>) {
1590    if side_effects.is_empty() {
1591        errors.push(error(
1592            ValidationErrorCode::MissingRequiredField,
1593            "$.side_effects",
1594            "side_effects must contain at least one item",
1595        ));
1596    }
1597
1598    for (index, side_effect) in side_effects.iter().enumerate() {
1599        validate_non_empty(
1600            &side_effect.description,
1601            &format!("$.side_effects[{index}].description"),
1602            errors,
1603        );
1604    }
1605}
1606
1607fn validate_event_references(
1608    references: &[EventReference],
1609    path: &str,
1610    errors: &mut Vec<ValidationError>,
1611) {
1612    let mut seen = HashSet::new();
1613    for (index, item) in references.iter().enumerate() {
1614        let event_path = format!("{path}[{index}].event_id");
1615        let version_path = format!("{path}[{index}].version");
1616        validate_non_empty(&item.event_id, &event_path, errors);
1617        validate_semver(&item.version, &version_path, errors);
1618        if !seen.insert((item.event_id.clone(), item.version.clone())) {
1619            errors.push(error(
1620                ValidationErrorCode::DuplicateItem,
1621                &event_path,
1622                "event references must be unique by id and version",
1623            ));
1624        }
1625    }
1626}
1627
1628fn validate_id_references(items: &[IdReference], path: &str, errors: &mut Vec<ValidationError>) {
1629    let mut seen = HashSet::new();
1630    for (index, item) in items.iter().enumerate() {
1631        let item_path = format!("{path}[{index}].id");
1632        validate_non_empty(&item.id, &item_path, errors);
1633        if !seen.insert(item.id.clone()) {
1634            errors.push(error(
1635                ValidationErrorCode::DuplicateItem,
1636                &item_path,
1637                "ids must be unique",
1638            ));
1639        }
1640    }
1641}
1642
1643fn validate_execution(
1644    execution: &Execution,
1645    provenance: &Provenance,
1646    errors: &mut Vec<ValidationError>,
1647) {
1648    match execution.binary_format {
1649        BinaryFormat::Wasm => {}
1650    }
1651
1652    match execution.entrypoint.kind {
1653        EntrypointKind::WasiCommand => {}
1654    }
1655
1656    validate_non_empty(
1657        &execution.entrypoint.command,
1658        "$.execution.entrypoint.command",
1659        errors,
1660    );
1661
1662    if execution.preferred_targets.is_empty() {
1663        errors.push(error(
1664            ValidationErrorCode::MissingRequiredField,
1665            "$.execution.preferred_targets",
1666            "preferred_targets must contain at least one item",
1667        ));
1668    }
1669
1670    let unique_targets: BTreeSet<_> = execution.preferred_targets.iter().cloned().collect();
1671    if unique_targets.len() != execution.preferred_targets.len() {
1672        errors.push(error(
1673            ValidationErrorCode::DuplicateItem,
1674            "$.execution.preferred_targets",
1675            "preferred_targets must be unique",
1676        ));
1677    }
1678
1679    if matches!(
1680        execution.constraints.host_api_access,
1681        HostApiAccess::ExceptionRequired
1682    ) && provenance.exception_refs.is_empty()
1683    {
1684        errors.push(error(
1685            ValidationErrorCode::PortabilityExceptionRequired,
1686            "$.execution.constraints.host_api_access",
1687            "host_api_access=exception_required requires provenance.exception_refs",
1688        ));
1689    }
1690}
1691
1692fn validate_dependencies(dependencies: &[DependencyReference], errors: &mut Vec<ValidationError>) {
1693    let mut seen = HashSet::new();
1694    for (index, dependency) in dependencies.iter().enumerate() {
1695        let id_path = format!("$.dependencies[{index}].id");
1696        let version_path = format!("$.dependencies[{index}].version");
1697        validate_non_empty(&dependency.id, &id_path, errors);
1698        validate_semver(&dependency.version, &version_path, errors);
1699        if !seen.insert((
1700            dependency.artifact_type.clone(),
1701            dependency.id.clone(),
1702            dependency.version.clone(),
1703        )) {
1704            errors.push(error(
1705                ValidationErrorCode::DuplicateItem,
1706                &id_path,
1707                "dependencies must be unique by artifact_type, id, and version",
1708            ));
1709        }
1710    }
1711}
1712
1713fn validate_connector_requirements(
1714    requirements: &[ConnectorRequirement],
1715    errors: &mut Vec<ValidationError>,
1716) {
1717    let mut seen = HashSet::new();
1718    for (index, requirement) in requirements.iter().enumerate() {
1719        let id_path = format!("$.connector_requirements[{index}].connector_id");
1720        let version_path = format!("$.connector_requirements[{index}].version");
1721        validate_non_empty(&requirement.connector_id, &id_path, errors);
1722        if VersionReq::parse(&requirement.version).is_err() {
1723            errors.push(error(
1724                ValidationErrorCode::InvalidConnectorRequirement,
1725                &version_path,
1726                "connector requirement version must be a valid semver range",
1727            ));
1728        }
1729        if !seen.insert((
1730            requirement.connector_id.clone(),
1731            requirement.version.clone(),
1732        )) {
1733            errors.push(error(
1734                ValidationErrorCode::DuplicateItem,
1735                &id_path,
1736                "connector_requirements must be unique by connector_id and version",
1737            ));
1738        }
1739    }
1740}
1741
1742fn validate_provenance(provenance: &Provenance, errors: &mut Vec<ValidationError>) {
1743    validate_non_empty(&provenance.author, "$.provenance.author", errors);
1744    validate_non_empty(&provenance.created_at, "$.provenance.created_at", errors);
1745
1746    if let Some(spec_ref) = &provenance.spec_ref {
1747        validate_non_empty(spec_ref, "$.provenance.spec_ref", errors);
1748    }
1749
1750    validate_unique_strings(
1751        &provenance.adr_refs,
1752        "$.provenance.adr_refs",
1753        "adr_refs must be unique",
1754        errors,
1755    );
1756    validate_unique_strings(
1757        &provenance.exception_refs,
1758        "$.provenance.exception_refs",
1759        "exception_refs must be unique",
1760        errors,
1761    );
1762}
1763
1764fn validate_evidence(evidence: &[ValidationEvidence], errors: &mut Vec<ValidationError>) {
1765    let mut seen = HashSet::new();
1766    for (index, item) in evidence.iter().enumerate() {
1767        let id_path = format!("$.evidence[{index}].evidence_id");
1768        validate_non_empty(&item.evidence_id, &id_path, errors);
1769        if !seen.insert(item.evidence_id.clone()) {
1770            errors.push(error(
1771                ValidationErrorCode::DuplicateItem,
1772                &id_path,
1773                "evidence_id values must be unique",
1774            ));
1775        }
1776    }
1777}
1778
1779fn validate_boundary(contract: &CapabilityContract, errors: &mut Vec<ValidationError>) {
1780    let summary = contract.summary.to_ascii_lowercase();
1781    let description = contract.description.to_ascii_lowercase();
1782    let combined = format!("{summary} {description}");
1783    let banned_terms = [
1784        "utility function",
1785        "helper function",
1786        "crud wrapper",
1787        "transport handler",
1788        "database insert",
1789        "full application",
1790        "subsystem",
1791    ];
1792
1793    if banned_terms.iter().any(|term| combined.contains(term)) {
1794        errors.push(error(
1795            ValidationErrorCode::InvalidCapabilityBoundary,
1796            "$.summary",
1797            "capability must represent one meaningful business action",
1798        ));
1799    }
1800}
1801
1802fn validate_event_boundary(contract: &EventContract, errors: &mut Vec<ValidationError>) {
1803    let summary = contract.summary.to_ascii_lowercase();
1804    let description = contract.description.to_ascii_lowercase();
1805    let combined = format!("{summary} {description}");
1806    let banned_terms = [
1807        "kafka topic",
1808        "transport topic",
1809        "websocket channel",
1810        "queue binding",
1811        "broker partition",
1812        "payload wrapper",
1813    ];
1814
1815    if banned_terms.iter().any(|term| combined.contains(term)) {
1816        errors.push(error(
1817            ValidationErrorCode::InvalidEventBoundary,
1818            "$.summary",
1819            "event must describe one governed business event boundary",
1820        ));
1821    }
1822}
1823
1824fn validate_placement_constraints(
1825    contract: &CapabilityContract,
1826    errors: &mut Vec<ValidationError>,
1827) {
1828    if contract.service_type == ServiceType::Stateful
1829        && contract
1830            .permitted_targets
1831            .contains(&ExecutionTarget::Browser)
1832    {
1833        errors.push(ValidationError {
1834            code: ValidationErrorCode::InvalidPlacementConstraint,
1835            message: "Stateful capabilities cannot target Browser environments; browsers cannot \
1836                      provide managed persistence guarantees."
1837                .to_string(),
1838            path: "$.permitted_targets".to_string(),
1839            severity: ErrorSeverity::Error,
1840        });
1841    }
1842    if contract.service_type == ServiceType::Subscribable
1843        && match contract.event_trigger.as_deref() {
1844            None => true,
1845            Some(event_trigger) => event_trigger.is_empty(),
1846        }
1847    {
1848        errors.push(ValidationError {
1849            code: ValidationErrorCode::MissingEventTrigger,
1850            message: "Subscribable capabilities must declare a non-empty event_trigger field."
1851                .to_string(),
1852            path: "$.event_trigger".to_string(),
1853            severity: ErrorSeverity::Error,
1854        });
1855    }
1856}
1857
1858fn validate_risk_metadata(risk: &RiskMetadata, errors: &mut Vec<ValidationError>) {
1859    validate_field_classifications(
1860        &risk.data_flow.accepted_data_classifications,
1861        "$.risk.data_flow.accepted_data_classifications",
1862        errors,
1863    );
1864    validate_field_classifications(
1865        &risk.data_flow.produced_data_classifications,
1866        "$.risk.data_flow.produced_data_classifications",
1867        errors,
1868    );
1869}
1870
1871fn validate_field_classifications(
1872    classifications: &[FieldDataClassification],
1873    path: &str,
1874    errors: &mut Vec<ValidationError>,
1875) {
1876    let mut seen = HashSet::new();
1877    for (index, item) in classifications.iter().enumerate() {
1878        let field_path = format!("{path}[{index}].field_path");
1879        validate_non_empty(&item.field_path, &field_path, errors);
1880        if !seen.insert(item.field_path.clone()) {
1881            errors.push(error(
1882                ValidationErrorCode::DuplicateItem,
1883                &field_path,
1884                "field_path values must be unique",
1885            ));
1886        }
1887    }
1888}
1889
1890fn validate_published_record(
1891    contract: &CapabilityContract,
1892    published: Option<&PublishedContractRecord>,
1893    errors: &mut Vec<ValidationError>,
1894) {
1895    let Some(published) = published else {
1896        return;
1897    };
1898
1899    if published.id != contract.id || published.version != contract.version {
1900        return;
1901    }
1902
1903    let digest = governed_content_digest(contract);
1904    if published.governed_content_digest != digest {
1905        errors.push(error(
1906            ValidationErrorCode::ImmutableVersionConflict,
1907            "$.version",
1908            "published contract versions are immutable",
1909        ));
1910    }
1911}
1912
1913fn validate_published_event_record(
1914    contract: &EventContract,
1915    published: Option<&PublishedEventRecord>,
1916    errors: &mut Vec<ValidationError>,
1917) {
1918    let Some(published) = published else {
1919        return;
1920    };
1921
1922    if published.id != contract.id || published.version != contract.version {
1923        return;
1924    }
1925
1926    let digest = governed_event_content_digest(contract);
1927    if published.governed_content_digest != digest {
1928        errors.push(error(
1929            ValidationErrorCode::ImmutableVersionConflict,
1930            "$.version",
1931            "published contract versions are immutable",
1932        ));
1933    }
1934}
1935
1936fn validate_non_empty(value: &str, path: &str, errors: &mut Vec<ValidationError>) {
1937    if value.trim().is_empty() {
1938        errors.push(error(
1939            ValidationErrorCode::MissingRequiredField,
1940            path,
1941            "value must be non-empty",
1942        ));
1943    }
1944}
1945
1946fn validate_min_length(
1947    value: &str,
1948    path: &str,
1949    min_length: usize,
1950    message: &str,
1951    errors: &mut Vec<ValidationError>,
1952) {
1953    if value.trim().len() < min_length {
1954        errors.push(error(ValidationErrorCode::InvalidFormat, path, message));
1955    }
1956}
1957
1958fn validate_unique_strings(
1959    values: &[String],
1960    path: &str,
1961    message: &str,
1962    errors: &mut Vec<ValidationError>,
1963) {
1964    let mut seen = HashSet::new();
1965    for value in values {
1966        if !seen.insert(value.clone()) {
1967            errors.push(error(ValidationErrorCode::DuplicateItem, path, message));
1968            break;
1969        }
1970    }
1971}
1972
1973fn error(code: ValidationErrorCode, path: &str, message: &str) -> ValidationError {
1974    ValidationError {
1975        code,
1976        message: message.to_string(),
1977        path: path.to_string(),
1978        severity: ErrorSeverity::Error,
1979    }
1980}
1981
1982fn is_valid_name(name: &str) -> bool {
1983    let mut parts = name.split('-');
1984    let first = parts.next().unwrap_or_default();
1985    is_valid_segment(first) && parts.all(is_valid_segment)
1986}
1987
1988fn is_valid_namespace(namespace: &str) -> bool {
1989    let mut parts = namespace.split('.');
1990    let first = parts.next().unwrap_or_default();
1991    is_valid_name(first) && parts.all(is_valid_name)
1992}
1993
1994fn is_valid_segment(segment: &str) -> bool {
1995    !segment.is_empty()
1996        && segment
1997            .chars()
1998            .all(|character| character.is_ascii_lowercase() || character.is_ascii_digit())
1999}