Skip to main content

wesley_core/domain/
optic.rs

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