Skip to main content

wesley_core/domain/
operation_artifact.rs

1//! Operation artifact models.
2//!
3//! These types are intentionally domain-empty. They describe the GraphQL
4//! operation shape, declared bounds, and law claims that an external target can
5//! import, evaluate, witness, or replay.
6
7use crate::domain::ir::TypeReference;
8use crate::domain::operation::OperationType;
9use serde::{Deserialize, Serialize};
10use std::collections::BTreeMap;
11use std::fmt;
12
13/// Canonical JSON codec for Wesley-owned operation artifact contract requirements.
14pub const OPERATION_REQUIREMENTS_ARTIFACT_CODEC: &str = "wesley.requirements.canonical-json.v0";
15
16/// Compiled contract for one GraphQL operation.
17#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
18#[serde(rename_all = "camelCase")]
19pub struct OperationArtifact {
20    /// Stable artifact identity derived from the schema and operation identity.
21    pub artifact_id: String,
22    /// Stable content hash for the full compiled artifact.
23    pub artifact_hash: String,
24    /// Stable schema identity derived from the lowered Wesley IR.
25    pub schema_id: String,
26    /// Stable digest of contract requirements and law claim templates.
27    pub requirements_digest: String,
28    /// Wesley-owned canonical byte artifact for contract requirements.
29    pub requirements_artifact: OperationRequirementsArtifact,
30    /// The selected GraphQL operation compiled into an inspectable contract.
31    pub operation: CompiledOperation,
32    /// Compiler-described requirements an external target may evaluate.
33    pub requirements: OperationRequirements,
34    /// Descriptor an application can present when registering this artifact.
35    pub registration: OperationRegistrationDescriptor,
36}
37
38/// Wesley-produced descriptor for registering a compiled operation artifact.
39///
40/// This is not a runtime handle and it is not an authority grant. It is a small
41/// descriptor an external target can compare with the full artifact before it
42/// stores or imports compiler-produced evidence.
43#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
44#[serde(rename_all = "camelCase")]
45pub struct OperationRegistrationDescriptor {
46    /// Stable artifact identity this descriptor refers to.
47    pub artifact_id: String,
48    /// Stable content hash for the full compiled artifact.
49    pub artifact_hash: String,
50    /// Stable schema identity for the referenced artifact.
51    pub schema_id: String,
52    /// Stable operation identity for the referenced artifact.
53    pub operation_id: String,
54    /// Stable digest of contract requirements and law claim templates.
55    pub requirements_digest: String,
56}
57
58/// Contract requirements for an operation artifact.
59#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
60#[serde(rename_all = "camelCase")]
61pub struct OperationRequirements {
62    /// Identity binding described by the compiler.
63    pub identity: IdentityRequirement,
64    /// Permission requirements inferred from the declared operation bounds.
65    pub required_permissions: Vec<PermissionRequirement>,
66    /// Resource labels that must remain inaccessible to the operation.
67    pub forbidden_resources: Vec<String>,
68}
69
70/// Wesley-owned canonical contract requirements byte artifact.
71///
72/// Downstream runtimes import these bytes directly. They should verify the
73/// digest and codec, but they should not serialize Wesley structs to create
74/// target-owned truth.
75#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
76#[serde(rename_all = "camelCase")]
77pub struct OperationRequirementsArtifact {
78    /// Stable digest computed from `bytes` exactly.
79    pub digest: String,
80    /// Explicit codec describing how `bytes` were generated.
81    pub codec: String,
82    /// Canonical requirements bytes generated by Wesley.
83    pub bytes: Vec<u8>,
84}
85
86/// Artifact resolver input accepted by Wesley-side registries.
87pub type OperationArtifactRef = OperationRegistrationDescriptor;
88
89/// Resolves compiled operation artifacts from registration descriptors.
90pub trait OperationArtifactResolver {
91    /// Resolves a registration descriptor to its full compiled artifact and
92    /// verifies the descriptor still matches artifact identity and requirements.
93    fn resolve_operation_artifact(
94        &self,
95        registration: &OperationRegistrationDescriptor,
96    ) -> Result<OperationArtifact, ResolveError>;
97}
98
99/// In-memory artifact registry for tests and single-process hosts.
100#[derive(Debug, Default, Clone)]
101pub struct InMemoryOperationArtifactRegistry {
102    artifacts: BTreeMap<String, OperationArtifact>,
103}
104
105impl InMemoryOperationArtifactRegistry {
106    /// Creates an empty registry.
107    pub fn new() -> Self {
108        Self::default()
109    }
110
111    /// Stores an artifact and returns its registration descriptor.
112    pub fn insert(&mut self, mut artifact: OperationArtifact) -> OperationRegistrationDescriptor {
113        let registration = registration_descriptor_for_artifact(&artifact);
114        artifact.registration = registration.clone();
115        self.artifacts
116            .insert(artifact.artifact_id.clone(), artifact);
117        registration
118    }
119
120    /// Returns the number of stored artifacts.
121    pub fn len(&self) -> usize {
122        self.artifacts.len()
123    }
124
125    /// Returns true when no artifacts are stored.
126    pub fn is_empty(&self) -> bool {
127        self.artifacts.is_empty()
128    }
129}
130
131fn registration_descriptor_for_artifact(
132    artifact: &OperationArtifact,
133) -> OperationRegistrationDescriptor {
134    OperationRegistrationDescriptor {
135        artifact_id: artifact.artifact_id.clone(),
136        artifact_hash: artifact.artifact_hash.clone(),
137        schema_id: artifact.schema_id.clone(),
138        operation_id: artifact.operation.operation_id.clone(),
139        requirements_digest: artifact.requirements_digest.clone(),
140    }
141}
142
143impl OperationArtifactResolver for InMemoryOperationArtifactRegistry {
144    fn resolve_operation_artifact(
145        &self,
146        registration: &OperationRegistrationDescriptor,
147    ) -> Result<OperationArtifact, ResolveError> {
148        let artifact = self
149            .artifacts
150            .get(&registration.artifact_id)
151            .ok_or_else(|| ResolveError::ArtifactNotFound {
152                artifact_id: registration.artifact_id.clone(),
153            })?;
154        verify_registration_matches_artifact(registration, artifact)?;
155        Ok(artifact.clone())
156    }
157}
158
159fn verify_registration_matches_artifact(
160    registration: &OperationRegistrationDescriptor,
161    artifact: &OperationArtifact,
162) -> Result<(), ResolveError> {
163    if registration.artifact_hash != artifact.artifact_hash {
164        return Err(ResolveError::ArtifactHashMismatch {
165            expected: artifact.artifact_hash.clone(),
166            actual: registration.artifact_hash.clone(),
167        });
168    }
169
170    if registration.schema_id != artifact.schema_id {
171        return Err(ResolveError::SchemaIdMismatch {
172            expected: artifact.schema_id.clone(),
173            actual: registration.schema_id.clone(),
174        });
175    }
176
177    if registration.operation_id != artifact.operation.operation_id {
178        return Err(ResolveError::OperationIdMismatch {
179            expected: artifact.operation.operation_id.clone(),
180            actual: registration.operation_id.clone(),
181        });
182    }
183
184    if registration.requirements_digest != artifact.requirements_digest {
185        return Err(ResolveError::RequirementsDigestMismatch {
186            expected: artifact.requirements_digest.clone(),
187            actual: registration.requirements_digest.clone(),
188        });
189    }
190
191    Ok(())
192}
193
194/// Error raised when an operation artifact reference cannot resolve cleanly.
195#[derive(Debug, Clone, PartialEq, Eq)]
196pub enum ResolveError {
197    /// No artifact exists for the referenced artifact identity.
198    ArtifactNotFound {
199        /// Artifact identity requested by the descriptor.
200        artifact_id: String,
201    },
202    /// The supplied artifact hash does not match the stored artifact.
203    ArtifactHashMismatch {
204        /// Artifact hash expected by the stored artifact.
205        expected: String,
206        /// Artifact hash supplied by the caller.
207        actual: String,
208    },
209    /// The supplied schema id does not match the stored artifact.
210    SchemaIdMismatch {
211        /// Schema id expected by the stored artifact.
212        expected: String,
213        /// Schema id supplied by the caller.
214        actual: String,
215    },
216    /// The supplied operation id does not match the stored artifact.
217    OperationIdMismatch {
218        /// Operation id expected by the stored artifact.
219        expected: String,
220        /// Operation id supplied by the caller.
221        actual: String,
222    },
223    /// The supplied requirements digest does not match the stored artifact.
224    RequirementsDigestMismatch {
225        /// Requirements digest expected by the stored artifact.
226        expected: String,
227        /// Requirements digest supplied by the caller.
228        actual: String,
229    },
230}
231
232impl fmt::Display for ResolveError {
233    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
234        match self {
235            ResolveError::ArtifactNotFound { artifact_id } => {
236                write!(
237                    formatter,
238                    "operation artifact '{artifact_id}' was not found"
239                )
240            }
241            ResolveError::ArtifactHashMismatch { expected, actual } => write!(
242                formatter,
243                "operation artifact hash mismatch: expected '{expected}', got '{actual}'"
244            ),
245            ResolveError::SchemaIdMismatch { expected, actual } => write!(
246                formatter,
247                "operation artifact schema id mismatch: expected '{expected}', got '{actual}'"
248            ),
249            ResolveError::OperationIdMismatch { expected, actual } => write!(
250                formatter,
251                "operation artifact id mismatch: expected '{expected}', got '{actual}'"
252            ),
253            ResolveError::RequirementsDigestMismatch { expected, actual } => write!(
254                formatter,
255                "operation requirements digest mismatch: expected '{expected}', got '{actual}'"
256            ),
257        }
258    }
259}
260
261impl std::error::Error for ResolveError {}
262
263/// Identity requirement described by compiler output.
264#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
265#[serde(rename_all = "camelCase")]
266pub struct IdentityRequirement {
267    /// Whether an external target is expected to bind a principal.
268    pub required: bool,
269    /// Accepted principal kinds, or empty when target policy owns the vocabulary.
270    pub accepted_principal_kinds: Vec<String>,
271}
272
273/// Permission requirement inferred from an operation declaration.
274#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
275#[serde(rename_all = "camelCase")]
276pub struct PermissionRequirement {
277    /// Required action.
278    pub action: PermissionAction,
279    /// Resource label the action applies to.
280    pub resource: String,
281    /// Compiler source of the requirement.
282    pub source: String,
283}
284
285/// Permission action required for an operation resource label.
286#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
287#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
288pub enum PermissionAction {
289    /// Read access is required.
290    Read,
291    /// Write access is required.
292    Write,
293}
294
295/// Inspectable contract for a selected GraphQL operation.
296#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
297#[serde(rename_all = "camelCase")]
298pub struct CompiledOperation {
299    /// Stable operation identity derived from the selected operation shape.
300    pub operation_id: String,
301    /// Optional GraphQL operation name.
302    #[serde(skip_serializing_if = "Option::is_none")]
303    pub name: Option<String>,
304    /// GraphQL operation kind.
305    pub kind: OperationKind,
306    /// Root schema field selected by the operation.
307    pub root_field: String,
308    /// Canonical bindings supplied to the selected root field.
309    pub root_arguments: Vec<RootArgumentBinding>,
310    /// Canonical argument bindings supplied to selected payload fields.
311    #[serde(default, skip_serializing_if = "Vec::is_empty")]
312    pub selection_arguments: Vec<SelectionArgumentBinding>,
313    /// Codec shape for the operation variables or root field arguments.
314    pub variable_shape: CodecShape,
315    /// Codec shape for the selected response payload.
316    pub payload_shape: CodecShape,
317    /// Directives preserved from the executable operation.
318    pub directives: Vec<DirectiveRecord>,
319    /// Declared resource footprint, when the operation supplies one.
320    #[serde(skip_serializing_if = "Option::is_none")]
321    pub declared_footprint: Option<Footprint>,
322    /// Compiler-produced templates for laws relevant to this operation.
323    pub law_claims: Vec<LawClaimTemplate>,
324}
325
326/// Canonical binding for one argument supplied to an operation root field.
327#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
328#[serde(rename_all = "camelCase")]
329pub struct RootArgumentBinding {
330    /// Root argument name.
331    pub name: String,
332    /// Schema-declared argument type.
333    pub type_ref: TypeReference,
334    /// Canonical JSON representation of the supplied GraphQL input value.
335    pub value_canonical_json: String,
336}
337
338/// Canonical binding for one argument supplied to a selected payload field.
339#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
340#[serde(rename_all = "camelCase")]
341pub struct SelectionArgumentBinding {
342    /// Response-path field that received the argument.
343    pub path: String,
344    /// Field argument name.
345    pub name: String,
346    /// Schema-declared argument type.
347    pub type_ref: TypeReference,
348    /// Canonical JSON representation of the supplied GraphQL input value.
349    pub value_canonical_json: String,
350}
351
352/// GraphQL executable operation kind.
353#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
354#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
355pub enum OperationKind {
356    /// GraphQL query operation.
357    Query,
358    /// GraphQL mutation operation.
359    Mutation,
360    /// GraphQL subscription operation.
361    Subscription,
362}
363
364impl From<OperationType> for OperationKind {
365    fn from(value: OperationType) -> Self {
366        match value {
367            OperationType::Query => OperationKind::Query,
368            OperationType::Mutation => OperationKind::Mutation,
369            OperationType::Subscription => OperationKind::Subscription,
370        }
371    }
372}
373
374impl From<OperationKind> for OperationType {
375    fn from(value: OperationKind) -> Self {
376        match value {
377            OperationKind::Query => OperationType::Query,
378            OperationKind::Mutation => OperationType::Mutation,
379            OperationKind::Subscription => OperationType::Subscription,
380        }
381    }
382}
383
384/// Named codec view for variables or payload data.
385#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
386#[serde(rename_all = "camelCase")]
387pub struct CodecShape {
388    /// Logical shape name.
389    pub type_name: String,
390    /// Fields visible inside the shape.
391    pub fields: Vec<CodecField>,
392}
393
394/// One field inside a codec shape.
395#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
396#[serde(rename_all = "camelCase")]
397pub struct CodecField {
398    /// Field name or selected response path.
399    pub name: String,
400    /// GraphQL type reference for the field.
401    pub type_ref: TypeReference,
402    /// Whether the field is non-null in the GraphQL type system.
403    pub required: bool,
404    /// Whether the field has an outer or nested list wrapper.
405    pub list: bool,
406}
407
408/// Directive preserved from a compiled executable operation.
409#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
410#[serde(rename_all = "camelCase")]
411pub struct DirectiveRecord {
412    /// Schema coordinate or operation coordinate where the directive was found.
413    pub coordinate: String,
414    /// Directive name without the `@` prefix.
415    pub name: String,
416    /// Canonical JSON object containing the directive arguments.
417    pub arguments_canonical_json: String,
418}
419
420/// Declared resource families an operation may read, write, or must not touch.
421#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
422#[serde(rename_all = "camelCase")]
423pub struct Footprint {
424    /// Resource labels the operation declares it may read.
425    pub reads: Vec<String>,
426    /// Resource labels the operation declares it may write.
427    pub writes: Vec<String>,
428    /// Resource labels the operation declares forbidden.
429    pub forbids: Vec<String>,
430}
431
432/// Compiler-produced declaration that a law is relevant to an operation.
433#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
434#[serde(rename_all = "camelCase")]
435pub struct LawClaimTemplate {
436    /// Stable law identifier.
437    pub law_id: String,
438    /// Stable claim identity for this operation and law pairing.
439    pub claim_id: String,
440    /// Operation identity this claim applies to.
441    pub operation_id: String,
442    /// Evidence categories a runtime or verifier should produce.
443    pub required_evidence: Vec<EvidenceKind>,
444}
445
446/// Evidence category requested by a law claim template.
447#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
448#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
449pub enum EvidenceKind {
450    /// Evidence produced by the Wesley compiler.
451    Compiler,
452    /// Evidence produced by codec inspection or fixture vectors.
453    Codec,
454    /// Evidence produced by host/runtime policy.
455    HostPolicy,
456    /// Evidence produced from runtime trace data.
457    RuntimeTrace,
458    /// Evidence produced by a domain verifier outside Wesley core.
459    DomainVerifier,
460}
461
462/// Runtime or verifier-produced verdict for one law claim.
463#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
464#[serde(rename_all = "camelCase")]
465pub struct LawWitness {
466    /// Stable law identifier.
467    pub law_id: String,
468    /// Claim identity this witness evaluates.
469    pub claim_id: String,
470    /// Optional state basis reference evaluated by the checker.
471    #[serde(skip_serializing_if = "Option::is_none")]
472    pub basis_ref: Option<String>,
473    /// Identifier for the checker that produced the verdict.
474    pub checker_id: String,
475    /// Optional hash of the checker artifact.
476    #[serde(skip_serializing_if = "Option::is_none")]
477    pub checker_artifact_hash: Option<String>,
478    /// Verdict for the law claim.
479    pub verdict: LawVerdict,
480    /// Digests of evidence artifacts considered by the checker.
481    pub evidence_digests: Vec<String>,
482    /// Optional digest of the runtime trace considered by the checker.
483    #[serde(skip_serializing_if = "Option::is_none")]
484    pub runtime_trace_digest: Option<String>,
485    /// Optional reason for an obstructed verdict.
486    #[serde(skip_serializing_if = "Option::is_none")]
487    pub obstruction_reason: Option<String>,
488    /// Replay hints supplied by the runtime or verifier.
489    pub replay_hints: Vec<ReplayHint>,
490}
491
492/// Verdict produced for a law claim.
493#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
494#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
495pub enum LawVerdict {
496    /// The checker found the law satisfied.
497    Satisfied,
498    /// The checker found a concrete obstruction.
499    Obstructed,
500    /// The checker cannot establish satisfaction or obstruction.
501    Unknown,
502}
503
504/// Hint that helps a runtime replay or inspect a witnessed interaction.
505#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
506#[serde(rename_all = "camelCase")]
507pub struct ReplayHint {
508    /// Hint kind, such as `trace`, `basis`, or `artifact`.
509    pub kind: String,
510    /// Hint value, intentionally opaque to Wesley core.
511    pub value: String,
512}