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