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