Skip to main content

runtime_core/
lib.rs

1use serde::{Deserialize, Serialize};
2use std::fmt;
3
4pub mod landscape;
5
6#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
7#[serde(transparent)]
8pub struct DiagnosticCode(pub String);
9
10impl DiagnosticCode {
11    pub fn new(value: impl Into<String>) -> Self {
12        Self(value.into())
13    }
14
15    pub fn as_str(&self) -> &str {
16        &self.0
17    }
18}
19
20impl From<&str> for DiagnosticCode {
21    fn from(value: &str) -> Self {
22        Self(value.to_string())
23    }
24}
25
26impl From<String> for DiagnosticCode {
27    fn from(value: String) -> Self {
28        Self(value)
29    }
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
33#[serde(rename_all = "camelCase")]
34pub struct Diagnostic {
35    pub severity: DiagnosticSeverity,
36    pub code: DiagnosticCode,
37    pub message: String,
38    pub source: Option<String>,
39    pub help: Option<String>,
40}
41
42impl Diagnostic {
43    pub fn new(
44        severity: DiagnosticSeverity,
45        code: impl Into<DiagnosticCode>,
46        message: impl Into<String>,
47    ) -> Self {
48        Self {
49            severity,
50            code: code.into(),
51            message: message.into(),
52            source: None,
53            help: None,
54        }
55    }
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
59#[serde(rename_all = "camelCase")]
60pub enum DiagnosticSeverity {
61    Info,
62    Warning,
63    Error,
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
67#[serde(rename_all = "camelCase")]
68pub struct RuntimeCapabilities {
69    pub native: bool,
70    pub server: bool,
71    pub wasm: bool,
72    pub mobile: MobileCapability,
73    pub requirements: Vec<RuntimeRequirement>,
74    pub max_recommended_input_bytes: Option<u64>,
75}
76
77impl RuntimeCapabilities {
78    pub fn pure_rust() -> Self {
79        Self {
80            native: true,
81            server: true,
82            wasm: true,
83            mobile: MobileCapability::Wasm,
84            requirements: Vec::new(),
85            max_recommended_input_bytes: None,
86        }
87    }
88
89    pub fn with_max_recommended_input_bytes(mut self, bytes: u64) -> Self {
90        self.max_recommended_input_bytes = Some(bytes);
91        self
92    }
93
94    pub fn with_requirement(
95        mut self,
96        name: impl Into<String>,
97        description: impl Into<String>,
98        required: bool,
99    ) -> Self {
100        self.requirements.push(RuntimeRequirement {
101            name: name.into(),
102            description: Some(description.into()),
103            required,
104        });
105        self
106    }
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
110#[serde(rename_all = "camelCase")]
111pub enum MobileCapability {
112    Native,
113    Wasm,
114    ApiOnly,
115    Unsupported,
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
119#[serde(rename_all = "camelCase")]
120pub struct RuntimeRequirement {
121    pub name: String,
122    pub description: Option<String>,
123    pub required: bool,
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
127#[serde(transparent)]
128pub struct OperationId(pub String);
129
130impl OperationId {
131    pub fn new(value: impl Into<String>) -> Self {
132        Self(value.into())
133    }
134
135    pub fn as_str(&self) -> &str {
136        &self.0
137    }
138}
139
140impl From<&str> for OperationId {
141    fn from(value: &str) -> Self {
142        Self(value.to_string())
143    }
144}
145
146impl From<String> for OperationId {
147    fn from(value: String) -> Self {
148        Self(value)
149    }
150}
151
152#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
153#[serde(rename_all = "camelCase")]
154pub struct OperationMetadata {
155    pub id: OperationId,
156    pub name: String,
157    pub description: Option<String>,
158    pub version: String,
159    pub capabilities: RuntimeCapabilities,
160}
161
162#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
163#[serde(rename_all = "camelCase")]
164pub struct PackageSurface {
165    pub library: String,
166    pub version: String,
167    pub operations: Vec<SurfaceOperation>,
168    pub capabilities: RuntimeCapabilities,
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
172#[serde(rename_all = "camelCase")]
173pub struct SurfaceOperation {
174    pub id: OperationId,
175    pub name: String,
176    pub description: Option<String>,
177    pub input_schema: serde_json::Value,
178    pub output_schema: serde_json::Value,
179    pub example_request: serde_json::Value,
180    pub wasm_supported: bool,
181    pub server_supported: bool,
182}
183
184#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
185#[serde(rename_all = "camelCase")]
186pub enum SurfaceExecutionMode {
187    InMemory,
188    PlannedJob,
189    BackgroundJob,
190    ExternalCommand,
191}
192
193#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
194#[serde(rename_all = "camelCase")]
195pub enum SurfaceSideEffect {
196    None,
197    ReadsFiles,
198    WritesFiles,
199    Network,
200    ExternalProcess,
201}
202
203#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
204#[serde(rename_all = "camelCase")]
205pub struct SurfaceArtifactExpectation {
206    pub id: String,
207    pub kind: String,
208    pub media_type: String,
209    pub required: bool,
210    pub description: Option<String>,
211}
212
213#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
214#[serde(rename_all = "camelCase")]
215pub struct SurfaceExecutionPlan {
216    pub operation: OperationId,
217    pub mode: SurfaceExecutionMode,
218    pub side_effects: Vec<SurfaceSideEffect>,
219    pub cancellable: bool,
220    pub progress_unit: Option<String>,
221    pub expected_artifacts: Vec<SurfaceArtifactExpectation>,
222    pub requirements: Vec<RuntimeRequirement>,
223    pub max_recommended_input_bytes: Option<u64>,
224}
225
226#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
227#[serde(rename_all = "camelCase")]
228pub struct SurfaceRequest {
229    pub operation: OperationId,
230    pub input: serde_json::Value,
231}
232
233#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
234#[serde(rename_all = "camelCase")]
235pub struct SurfaceResponse {
236    pub operation: OperationId,
237    pub value: serde_json::Value,
238    pub diagnostics: Vec<Diagnostic>,
239    pub artifacts: Vec<serde_json::Value>,
240}
241
242#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
243#[serde(rename_all = "camelCase")]
244pub struct SurfaceError {
245    pub code: String,
246    pub message: String,
247    pub operation: Option<OperationId>,
248    pub details: serde_json::Value,
249}
250
251impl SurfaceError {
252    pub fn invalid_request(
253        operation: Option<impl Into<OperationId>>,
254        message: impl Into<String>,
255    ) -> Self {
256        Self::new("invalid_request", operation, message, serde_json::json!({}))
257    }
258
259    pub fn unsupported_operation(
260        operation: impl Into<OperationId>,
261        package: impl Into<String>,
262    ) -> Self {
263        let operation = operation.into();
264        let package = package.into();
265        Self::new(
266            "unsupported_operation",
267            Some(operation.clone()),
268            format!(
269                "unsupported operation `{}` for {}",
270                operation.as_str(),
271                package
272            ),
273            serde_json::json!({"package": package}),
274        )
275    }
276
277    pub fn unsupported_value(
278        operation: Option<impl Into<OperationId>>,
279        field: impl Into<String>,
280        value: impl Into<String>,
281        allowed: &[&str],
282    ) -> Self {
283        let field = field.into();
284        let value = value.into();
285        Self::new(
286            "unsupported_value",
287            operation,
288            format!("unsupported value `{value}` for `{field}`"),
289            serde_json::json!({
290                "field": field,
291                "value": value,
292                "allowed": allowed
293            }),
294        )
295    }
296
297    pub fn resource_limit(
298        operation: Option<impl Into<OperationId>>,
299        field: impl Into<String>,
300        limit: usize,
301        actual: usize,
302    ) -> Self {
303        let field = field.into();
304        Self::new(
305            "resource_limit",
306            operation,
307            format!("`{field}` exceeds the maximum supported size of {limit}"),
308            serde_json::json!({
309                "field": field,
310                "limit": limit,
311                "actual": actual
312            }),
313        )
314    }
315
316    pub fn cancelled(operation: impl Into<OperationId>, message: impl Into<String>) -> Self {
317        Self::new("cancelled", Some(operation), message, serde_json::json!({}))
318    }
319
320    pub fn execution_failed(
321        operation: impl Into<OperationId>,
322        message: impl Into<String>,
323        details: serde_json::Value,
324    ) -> Self {
325        Self::new("execution_failed", Some(operation), message, details)
326    }
327
328    pub fn artifact_error(
329        operation: impl Into<OperationId>,
330        message: impl Into<String>,
331        details: serde_json::Value,
332    ) -> Self {
333        Self::new("artifact_error", Some(operation), message, details)
334    }
335
336    pub fn missing_dependency(
337        operation: Option<impl Into<OperationId>>,
338        dependency: impl Into<String>,
339        setup: impl Into<String>,
340    ) -> Self {
341        let dependency = dependency.into();
342        let setup = setup.into();
343        Self::new(
344            "missing_dependency",
345            operation,
346            format!("missing required dependency `{dependency}`"),
347            serde_json::json!({
348                "dependency": dependency,
349                "setup": setup
350            }),
351        )
352    }
353
354    pub fn new(
355        code: impl Into<String>,
356        operation: Option<impl Into<OperationId>>,
357        message: impl Into<String>,
358        details: serde_json::Value,
359    ) -> Self {
360        Self {
361            code: code.into(),
362            message: message.into(),
363            operation: operation.map(Into::into),
364            details,
365        }
366    }
367
368    pub fn to_error_string(&self) -> String {
369        serde_json::to_string(self).unwrap_or_else(|_| self.message.clone())
370    }
371}
372
373impl fmt::Display for SurfaceError {
374    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
375        f.write_str(&self.message)
376    }
377}
378
379impl std::error::Error for SurfaceError {}
380
381pub fn parse_surface_error(error: &str) -> Option<SurfaceError> {
382    serde_json::from_str(error).ok()
383}
384
385pub fn parse_surface_input<T: for<'de> Deserialize<'de>>(
386    operation: Option<&str>,
387    input: serde_json::Value,
388) -> Result<T, String> {
389    serde_json::from_value(input).map_err(|error| {
390        SurfaceError::invalid_request(
391            operation.map(OperationId::new),
392            format!("invalid request: {error}"),
393        )
394        .to_error_string()
395    })
396}
397
398pub fn require_non_empty<T>(operation: &str, field: &str, values: &[T]) -> Result<(), String> {
399    if values.is_empty() {
400        Err(SurfaceError::invalid_request(
401            Some(OperationId::new(operation)),
402            format!("invalid request: {field} must not be empty"),
403        )
404        .to_error_string())
405    } else {
406        Ok(())
407    }
408}
409
410pub fn validate_max_items(
411    operation: &str,
412    field: &str,
413    actual: usize,
414    limit: usize,
415) -> Result<(), String> {
416    if actual > limit {
417        Err(
418            SurfaceError::resource_limit(Some(OperationId::new(operation)), field, limit, actual)
419                .to_error_string(),
420        )
421    } else {
422        Ok(())
423    }
424}
425
426pub fn validate_matching_lengths(
427    operation: &str,
428    left_field: &str,
429    left_len: usize,
430    right_field: &str,
431    right_len: usize,
432) -> Result<(), String> {
433    if left_len != right_len {
434        Err(SurfaceError::invalid_request(
435            Some(OperationId::new(operation)),
436            format!(
437                "invalid request: `{left_field}` length {left_len} must match `{right_field}` length {right_len}"
438            ),
439        )
440        .to_error_string())
441    } else {
442        Ok(())
443    }
444}
445
446/// Builds the standard package-surface operation metadata used by library
447/// crates and transport adapters.
448pub fn surface_operation(
449    id: impl Into<String>,
450    name: impl Into<String>,
451    description: impl Into<String>,
452    example_request: serde_json::Value,
453) -> SurfaceOperation {
454    let id = id.into();
455    SurfaceOperation {
456        id: OperationId::new(id.clone()),
457        name: name.into(),
458        description: Some(description.into()),
459        input_schema: surface_input_schema(&id, &example_request),
460        output_schema: surface_output_schema(&id),
461        example_request,
462        wasm_supported: true,
463        server_supported: true,
464    }
465}
466
467pub fn surface_operation_with_execution_plan(
468    id: impl Into<String>,
469    name: impl Into<String>,
470    description: impl Into<String>,
471    example_request: serde_json::Value,
472    execution_plan: SurfaceExecutionPlan,
473) -> SurfaceOperation {
474    let mut operation = surface_operation(id, name, description, example_request);
475    let execution_plan = surface_execution_plan_value(&execution_plan);
476    insert_schema_extension(
477        &mut operation.input_schema,
478        "xExecutionPlan",
479        execution_plan.clone(),
480    );
481    insert_schema_extension(
482        &mut operation.output_schema,
483        "xExecutionPlan",
484        execution_plan,
485    );
486    operation
487}
488
489pub fn landscape_operation_contract_value(
490    contract: &landscape::LandscapeOperationContract,
491) -> serde_json::Value {
492    serde_json::to_value(contract).unwrap_or_else(|_| serde_json::json!({}))
493}
494
495pub fn surface_operation_with_landscape(
496    id: impl Into<String>,
497    name: impl Into<String>,
498    description: impl Into<String>,
499    example_request: serde_json::Value,
500    contract: landscape::LandscapeOperationContract,
501) -> SurfaceOperation {
502    let mut operation = surface_operation(id, name, description, example_request);
503    attach_landscape_contract(&mut operation, contract);
504    operation
505}
506
507pub fn attach_landscape_contract(
508    operation: &mut SurfaceOperation,
509    contract: landscape::LandscapeOperationContract,
510) {
511    let landscape = landscape_operation_contract_value(&contract);
512    insert_schema_extension(&mut operation.input_schema, "xLandscape", landscape.clone());
513    insert_schema_extension(&mut operation.output_schema, "xLandscape", landscape);
514}
515
516pub fn surface_execution_plan_value(plan: &SurfaceExecutionPlan) -> serde_json::Value {
517    serde_json::to_value(plan).unwrap_or_else(|_| serde_json::json!({}))
518}
519
520fn insert_schema_extension(schema: &mut serde_json::Value, key: &str, value: serde_json::Value) {
521    if let serde_json::Value::Object(object) = schema {
522        object.insert(key.to_string(), value);
523    }
524}
525
526pub fn surface_input_schema(
527    operation: &str,
528    example_request: &serde_json::Value,
529) -> serde_json::Value {
530    let properties = match example_request {
531        serde_json::Value::Object(object) => object
532            .iter()
533            .map(|(key, value)| (key.clone(), infer_schema_for_value(key, value)))
534            .collect::<serde_json::Map<_, _>>(),
535        _ => serde_json::Map::new(),
536    };
537    let required = required_fields_for_operation(operation, example_request);
538    serde_json::json!({
539        "type": "object",
540        "additionalProperties": false,
541        "properties": properties,
542        "required": required,
543        "xOperationCategory": operation_category(operation),
544        "xReleaseStability": "stable",
545        "xContractPolicy": "additiveOnly",
546        "xErrorShape": {
547            "code": "string",
548            "message": "string",
549            "operation": "string|null",
550            "details": "object"
551        },
552        "xResourceLimits": {
553            "maxRecommendedInputBytes": 1048576,
554            "largePayloadBehavior": "reject or deterministically truncate by operation-specific limit"
555        }
556    })
557}
558
559pub fn surface_output_schema(operation: &str) -> serde_json::Value {
560    serde_json::json!({
561        "type": "object",
562        "required": ["operation", "title", "message", "summary", "result"],
563        "properties": {
564            "operation": {"type": "string", "const": operation},
565            "title": {"type": "string", "minLength": 1},
566            "message": {"type": "string", "minLength": 1},
567            "summary": {"type": "object"},
568            "result": {}
569        },
570        "additionalProperties": true
571    })
572}
573
574pub fn operation_category(operation: &str) -> &'static str {
575    match operation {
576        "describe"
577        | "analysis.describe"
578        | "classification.models"
579        | "classification.schema"
580        | "embeddings.backends"
581        | "qa.models" => "debug",
582        "runtime.softmax" => "support",
583        _ => "workflow",
584    }
585}
586
587fn required_fields_for_operation(
588    operation: &str,
589    example_request: &serde_json::Value,
590) -> Vec<String> {
591    if operation == "describe" || operation.ends_with(".models") || operation.ends_with(".describe")
592    {
593        return Vec::new();
594    }
595    let optional = [
596        "dimensions",
597        "embedding",
598        "id",
599        "includeNearDuplicates",
600        "includePunctuation",
601        "includeSemanticNeighbors",
602        "keywordLimit",
603        "linguistics",
604        "lowercase",
605        "maxAlternatives",
606        "maxTokens",
607        "minTokensForDecision",
608        "mode",
609        "model",
610        "n",
611        "ngramSizes",
612        "normalizeWhitespace",
613        "options",
614        "order",
615        "profile",
616        "previewLimit",
617        "seed",
618        "sentenceLevel",
619        "shingleSizes",
620        "streamId",
621        "summarySentences",
622        "topK",
623        "truncation",
624    ];
625    match example_request {
626        serde_json::Value::Object(object) => object
627            .keys()
628            .filter(|key| !optional.contains(&key.as_str()))
629            .cloned()
630            .collect(),
631        _ => Vec::new(),
632    }
633}
634
635fn infer_schema_for_value(key: &str, value: &serde_json::Value) -> serde_json::Value {
636    let mut schema = match value {
637        serde_json::Value::Bool(_) => serde_json::json!({"type": "boolean"}),
638        serde_json::Value::Number(number) if number.is_i64() || number.is_u64() => {
639            serde_json::json!({"type": "integer", "minimum": 0})
640        }
641        serde_json::Value::Number(_) => serde_json::json!({"type": "number"}),
642        serde_json::Value::String(_) => serde_json::json!({"type": "string", "minLength": 1}),
643        serde_json::Value::Array(values) => {
644            let item_schema = values
645                .first()
646                .map(|value| infer_schema_for_value("item", value))
647                .unwrap_or_else(|| serde_json::json!({}));
648            serde_json::json!({"type": "array", "items": item_schema, "minItems": 1})
649        }
650        serde_json::Value::Object(object) => serde_json::json!({
651            "type": "object",
652            "additionalProperties": true,
653            "properties": object
654                .iter()
655                .map(|(key, value)| (key.clone(), infer_schema_for_value(key, value)))
656                .collect::<serde_json::Map<_, _>>()
657        }),
658        serde_json::Value::Null => serde_json::json!({}),
659    };
660    if matches!(
661        key,
662        "topK" | "top_k" | "maxTokens" | "max_tokens" | "order" | "dimensions" | "n"
663    ) {
664        if let serde_json::Value::Object(object) = &mut schema {
665            object.insert("minimum".to_string(), serde_json::json!(1));
666            object.insert("maximum".to_string(), serde_json::json!(4096));
667        }
668    }
669    schema
670}
671
672/// Builds the standard `describe` response without changing the shared
673/// `SurfaceResponse` JSON shape.
674pub fn describe_surface_response(
675    surface: &PackageSurface,
676    request: SurfaceRequest,
677) -> SurfaceResponse {
678    let result = serde_json::json!({
679        "library": &surface.library,
680        "version": &surface.version,
681        "operationCount": surface.operations.len(),
682        "operations": surface
683            .operations
684            .iter()
685            .map(|operation| operation.id.as_str())
686            .collect::<Vec<_>>(),
687        "input": request.input
688    });
689    structured_surface_response(
690        request.operation,
691        "Package surface metadata",
692        format!(
693            "{} exposes {} package-surface operations.",
694            surface.library,
695            surface.operations.len()
696        ),
697        serde_json::json!({
698            "operationCount": surface.operations.len(),
699            "runtime": {
700                "wasm": surface.capabilities.wasm,
701                "server": surface.capabilities.server,
702                "native": surface.capabilities.native
703            }
704        }),
705        result,
706    )
707}
708
709/// Builds a successful surface response with empty diagnostics and artifacts.
710pub fn surface_response(operation: OperationId, value: serde_json::Value) -> SurfaceResponse {
711    let title = operation.as_str().to_string();
712    let message = format!("Ran package-surface operation `{}`.", operation.as_str());
713    let value = ensure_structured_surface_value(&operation, title, message, value);
714    SurfaceResponse {
715        operation,
716        value,
717        diagnostics: Vec::new(),
718        artifacts: Vec::new(),
719    }
720}
721
722/// Builds a package-surface value with standard human-readable metadata while
723/// preserving object fields from the concrete operation result at the top level.
724pub fn structured_surface_value(
725    operation: &OperationId,
726    title: impl Into<String>,
727    message: impl Into<String>,
728    summary: serde_json::Value,
729    result: serde_json::Value,
730) -> serde_json::Value {
731    let mut object = match &result {
732        serde_json::Value::Object(map) => map.clone(),
733        _ => serde_json::Map::new(),
734    };
735    object.insert("title".to_string(), serde_json::Value::String(title.into()));
736    object.insert(
737        "operation".to_string(),
738        serde_json::Value::String(operation.as_str().to_string()),
739    );
740    object.insert(
741        "message".to_string(),
742        serde_json::Value::String(message.into()),
743    );
744    object.insert("summary".to_string(), summary);
745    object.insert("result".to_string(), result);
746    serde_json::Value::Object(object)
747}
748
749/// Adds the common package-surface UI fields to a result value when they are
750/// missing, while preserving every existing top-level domain field.
751pub fn ensure_structured_surface_value(
752    operation: &OperationId,
753    title: impl Into<String>,
754    message: impl Into<String>,
755    value: serde_json::Value,
756) -> serde_json::Value {
757    let result = value.clone();
758    let mut object = match value {
759        serde_json::Value::Object(map) => map,
760        _ => serde_json::Map::new(),
761    };
762    object
763        .entry("operation".to_string())
764        .or_insert_with(|| serde_json::Value::String(operation.as_str().to_string()));
765    object
766        .entry("title".to_string())
767        .or_insert_with(|| serde_json::Value::String(title.into()));
768    object
769        .entry("message".to_string())
770        .or_insert_with(|| serde_json::Value::String(message.into()));
771    object
772        .entry("summary".to_string())
773        .or_insert_with(|| operation_summary(&result));
774    object.entry("result".to_string()).or_insert(result);
775    serde_json::Value::Object(object)
776}
777
778/// Builds a successful package-surface response using `structured_surface_value`.
779pub fn structured_surface_response(
780    operation: OperationId,
781    title: impl Into<String>,
782    message: impl Into<String>,
783    summary: serde_json::Value,
784    result: serde_json::Value,
785) -> SurfaceResponse {
786    let value = structured_surface_value(&operation, title, message, summary, result);
787    surface_response(operation, value)
788}
789
790/// Builds a structured response for an operation listed in a package surface.
791///
792/// This keeps the concrete operation result at the top level for compatibility,
793/// while adding the common `title`, `message`, `summary`, and `result` fields
794/// expected by package-surface UIs.
795pub fn structured_operation_response(
796    surface: &PackageSurface,
797    operation: OperationId,
798    result: serde_json::Value,
799) -> SurfaceResponse {
800    let metadata = surface
801        .operations
802        .iter()
803        .find(|candidate| candidate.id.as_str() == operation.as_str());
804    let title = metadata
805        .map(|operation| operation.name.clone())
806        .unwrap_or_else(|| operation.as_str().to_string());
807    let message = metadata
808        .and_then(|operation| operation.description.clone())
809        .unwrap_or_else(|| format!("Ran package-surface operation `{}`.", operation.as_str()));
810    let summary = operation_summary(&result);
811    structured_surface_response(operation, title, message, summary, result)
812}
813
814fn operation_summary(result: &serde_json::Value) -> serde_json::Value {
815    match result {
816        serde_json::Value::Object(object) => {
817            let mut summary = serde_json::Map::new();
818            summary.insert("status".to_string(), serde_json::json!("ok"));
819            for key in [
820                "count",
821                "width",
822                "height",
823                "format",
824                "pixelFormat",
825                "dimensions",
826                "operationCount",
827            ] {
828                if let Some(value) = object.get(key) {
829                    summary.insert(key.to_string(), value.clone());
830                }
831            }
832            if let Some((key, value)) = object
833                .iter()
834                .find(|(_, value)| matches!(value, serde_json::Value::Array(_)))
835            {
836                summary.insert(
837                    format!("{key}Count"),
838                    serde_json::json!(value.as_array().map(Vec::len).unwrap_or(0)),
839                );
840            }
841            serde_json::Value::Object(summary)
842        }
843        serde_json::Value::Array(values) => {
844            serde_json::json!({"status": "ok", "count": values.len()})
845        }
846        _ => serde_json::json!({"status": "ok"}),
847    }
848}
849
850/// Shared helpers for thin package-surface CLI adapters.
851pub mod cli {
852    use std::fs;
853    use std::io::{self, Read};
854
855    use super::{
856        ensure_structured_surface_value, OperationId, PackageSurface, SurfaceRequest,
857        SurfaceResponse,
858    };
859
860    /// Static package metadata for one CLI adapter.
861    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
862    pub struct CliAdapterMetadata {
863        pub library_crate: &'static str,
864        pub surface_kind: &'static str,
865        pub library_import: &'static str,
866        pub server_package: &'static str,
867        pub app_package: &'static str,
868        pub wasm_package: &'static str,
869    }
870
871    /// Builds the standard CLI adapter metadata payload.
872    pub fn package_metadata_json(metadata: CliAdapterMetadata, surface: PackageSurface) -> String {
873        serde_json::json!({
874            "package": format!("{}-cli", metadata.library_crate),
875            "surface": metadata.surface_kind,
876            "library": metadata.library_crate,
877            "libraryImport": metadata.library_import,
878            "serverPackage": metadata.server_package,
879            "appPackage": metadata.app_package,
880            "wasmPackage": metadata.wasm_package,
881            "operations": surface.operations
882        })
883        .to_string()
884    }
885
886    /// Builds the standard CLI command schema payload.
887    pub fn command_schema_json() -> String {
888        serde_json::json!({
889            "commands": [
890                {"name": "info", "description": "Print package and adapter metadata."},
891                {"name": "schema", "description": "Print the CLI command schema."},
892                {"name": "operations", "description": "Print library operations."},
893                {"name": "run", "description": "Run one library-owned operation."}
894            ]
895        })
896        .to_string()
897    }
898
899    /// Reads a JSON request from `--json`, `--file`, or stdin.
900    pub fn read_json_input(
901        json: Option<String>,
902        file: Option<String>,
903    ) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
904        let input = if let Some(json) = json {
905            json
906        } else if let Some(file) = file {
907            fs::read_to_string(file)?
908        } else {
909            let mut buffer = String::new();
910            io::stdin().read_to_string(&mut buffer)?;
911            if buffer.trim().is_empty() {
912                "{}".to_string()
913            } else {
914                buffer
915            }
916        };
917        Ok(serde_json::from_str(&input)?)
918    }
919
920    /// Runs an operation through a library-owned surface and adds standard
921    /// package-surface value fields if an older surface omitted them.
922    pub fn run_wrapped_operation(
923        operation: &str,
924        input: serde_json::Value,
925        runner: fn(SurfaceRequest) -> Result<SurfaceResponse, String>,
926    ) -> Result<SurfaceResponse, String> {
927        let mut response = runner(SurfaceRequest {
928            operation: OperationId::new(operation),
929            input,
930        })?;
931        let value = std::mem::take(&mut response.value);
932        response.value = ensure_structured_surface_value(
933            &response.operation,
934            operation.to_string(),
935            format!("Ran package-surface operation `{}`.", operation),
936            value,
937        );
938        Ok(response)
939    }
940}
941
942/// Shared helpers for local package-surface HTTP adapters.
943pub mod server {
944    use std::io::{self, BufRead, BufReader, Read, Write};
945    use std::net::{TcpListener, TcpStream};
946
947    use super::{
948        parse_surface_error, Diagnostic, DiagnosticSeverity, OperationId, PackageSurface,
949        SurfaceRequest, SurfaceResponse,
950    };
951
952    /// Static package metadata for one server adapter.
953    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
954    pub struct ServerAdapterMetadata {
955        pub library_crate: &'static str,
956        pub surface_kind: &'static str,
957        pub library_import: &'static str,
958        pub cli_package: &'static str,
959        pub app_package: &'static str,
960        pub wasm_package: &'static str,
961    }
962
963    #[derive(Debug, Clone, PartialEq, Eq)]
964    pub struct HttpResponse {
965        pub status_code: u16,
966        pub reason: &'static str,
967        pub content_type: &'static str,
968        pub body: String,
969    }
970
971    /// Serves the standard local package-surface HTTP API.
972    pub fn serve(
973        addr: &str,
974        metadata: ServerAdapterMetadata,
975        surface_provider: fn() -> PackageSurface,
976        runner: fn(SurfaceRequest) -> Result<SurfaceResponse, String>,
977    ) -> io::Result<()> {
978        let listener = TcpListener::bind(addr)?;
979        for stream in listener.incoming() {
980            handle_stream(stream?, metadata, surface_provider, runner)?;
981        }
982        Ok(())
983    }
984
985    /// Returns the standard response for one HTTP request.
986    pub fn response_for(
987        method: &str,
988        path: &str,
989        body: &str,
990        metadata: ServerAdapterMetadata,
991        surface_provider: fn() -> PackageSurface,
992        runner: fn(SurfaceRequest) -> Result<SurfaceResponse, String>,
993    ) -> HttpResponse {
994        match (method, path) {
995            ("OPTIONS", _) => HttpResponse {
996                status_code: 204,
997                reason: "No Content",
998                content_type: "application/json",
999                body: String::new(),
1000            },
1001            ("GET", "/health") => json_response(
1002                200,
1003                "OK",
1004                serde_json::json!({
1005                    "ok": true,
1006                    "package": format!("{}-server", metadata.library_crate),
1007                    "library": metadata.library_crate
1008                }),
1009            ),
1010            ("GET", "/api/package") => json_response(
1011                200,
1012                "OK",
1013                package_metadata_value(metadata, surface_provider()),
1014            ),
1015            ("GET", "/api/schema") => {
1016                json_response(200, "OK", schema_value(metadata, surface_provider()))
1017            }
1018            ("GET", "/api/operations") => {
1019                json_response(200, "OK", serde_json::json!(surface_provider().operations))
1020            }
1021            ("POST", "/api/run") => run_response(body, metadata, runner),
1022            ("POST", path) if path.starts_with("/api/") => {
1023                let operation = path.trim_start_matches("/api/");
1024                run_request(
1025                    SurfaceRequest {
1026                        operation: OperationId::new(operation),
1027                        input: parse_json_or_empty(body),
1028                    },
1029                    metadata,
1030                    runner,
1031                )
1032            }
1033            _ => json_response(
1034                404,
1035                "Not Found",
1036                serde_json::json!({
1037                    "error": "not found",
1038                    "path": path
1039                }),
1040            ),
1041        }
1042    }
1043
1044    /// Builds the standard server adapter metadata payload.
1045    pub fn package_metadata_json(
1046        metadata: ServerAdapterMetadata,
1047        surface: PackageSurface,
1048    ) -> String {
1049        package_metadata_value(metadata, surface).to_string()
1050    }
1051
1052    fn package_metadata_value(
1053        metadata: ServerAdapterMetadata,
1054        surface: PackageSurface,
1055    ) -> serde_json::Value {
1056        serde_json::json!({
1057            "package": format!("{}-server", metadata.library_crate),
1058            "surface": metadata.surface_kind,
1059            "library": metadata.library_crate,
1060            "libraryImport": metadata.library_import,
1061            "cliPackage": metadata.cli_package,
1062            "appPackage": metadata.app_package,
1063            "wasmPackage": metadata.wasm_package,
1064            "endpoints": [
1065                "GET /health",
1066                "GET /api/package",
1067                "GET /api/schema",
1068                "GET /api/operations",
1069                "POST /api/run",
1070                "POST /api/<operation-id>"
1071            ],
1072            "runtimeMetadata": {
1073                "candleDevice": serde_json::Value::Null
1074            },
1075            "operations": surface.operations
1076        })
1077    }
1078
1079    fn schema_value(metadata: ServerAdapterMetadata, surface: PackageSurface) -> serde_json::Value {
1080        let operations = surface
1081            .operations
1082            .into_iter()
1083            .map(|operation| {
1084                let path = format!("/api/{}", operation.id.as_str());
1085                (
1086                    path,
1087                    serde_json::json!({
1088                        "post": {
1089                            "summary": operation.name,
1090                            "description": operation.description,
1091                            "requestBody": operation.input_schema,
1092                            "responses": {"200": operation.output_schema}
1093                        }
1094                    }),
1095                )
1096            })
1097            .collect::<serde_json::Map<_, _>>();
1098
1099        serde_json::json!({
1100            "openapi": "3.1.0",
1101            "info": {
1102                "title": format!("{} API", metadata.library_crate),
1103                "version": env!("CARGO_PKG_VERSION")
1104            },
1105            "paths": operations
1106        })
1107    }
1108
1109    fn run_response(
1110        body: &str,
1111        metadata: ServerAdapterMetadata,
1112        runner: fn(SurfaceRequest) -> Result<SurfaceResponse, String>,
1113    ) -> HttpResponse {
1114        let payload = match serde_json::from_str::<serde_json::Value>(body) {
1115            Ok(value) => value,
1116            Err(error) => {
1117                return diagnostic_response(
1118                    400,
1119                    "Bad Request",
1120                    "invalid_request",
1121                    &format!("invalid JSON: {error}"),
1122                    metadata,
1123                );
1124            }
1125        };
1126        let operation = payload
1127            .get("operation")
1128            .and_then(serde_json::Value::as_str)
1129            .unwrap_or("describe")
1130            .to_string();
1131        let input = payload
1132            .get("input")
1133            .cloned()
1134            .unwrap_or_else(|| payload.clone());
1135        run_request(
1136            SurfaceRequest {
1137                operation: OperationId::new(operation),
1138                input,
1139            },
1140            metadata,
1141            runner,
1142        )
1143    }
1144
1145    fn run_request(
1146        request: SurfaceRequest,
1147        metadata: ServerAdapterMetadata,
1148        runner: fn(SurfaceRequest) -> Result<SurfaceResponse, String>,
1149    ) -> HttpResponse {
1150        match runner(request) {
1151            Ok(response) => json_response(200, "OK", serde_json::json!(response)),
1152            Err(error) => {
1153                diagnostic_response(400, "Bad Request", "operation_failed", &error, metadata)
1154            }
1155        }
1156    }
1157
1158    fn diagnostic_response(
1159        status_code: u16,
1160        reason: &'static str,
1161        code: &str,
1162        message: &str,
1163        metadata: ServerAdapterMetadata,
1164    ) -> HttpResponse {
1165        let parsed = parse_surface_error(message);
1166        let diagnostic_code = parsed
1167            .as_ref()
1168            .map(|error| error.code.as_str())
1169            .unwrap_or(code);
1170        let diagnostic_message = parsed
1171            .as_ref()
1172            .map(|error| error.message.as_str())
1173            .unwrap_or(message);
1174        let details = parsed
1175            .as_ref()
1176            .map(|error| error.details.clone())
1177            .unwrap_or_else(|| serde_json::json!({}));
1178        json_response(
1179            status_code,
1180            reason,
1181            serde_json::json!({
1182                "diagnostics": [Diagnostic {
1183                    severity: DiagnosticSeverity::Error,
1184                    code: diagnostic_code.into(),
1185                    message: diagnostic_message.to_string(),
1186                    source: Some(format!("{}-server", metadata.library_crate)),
1187                    help: None,
1188                }],
1189                "error": {
1190                    "code": diagnostic_code,
1191                    "message": diagnostic_message,
1192                    "details": details
1193                }
1194            }),
1195        )
1196    }
1197
1198    fn parse_json_or_empty(body: &str) -> serde_json::Value {
1199        if body.trim().is_empty() {
1200            serde_json::json!({})
1201        } else {
1202            serde_json::from_str(body).unwrap_or_else(|_| serde_json::json!({"raw": body}))
1203        }
1204    }
1205
1206    fn handle_stream(
1207        mut stream: TcpStream,
1208        metadata: ServerAdapterMetadata,
1209        surface_provider: fn() -> PackageSurface,
1210        runner: fn(SurfaceRequest) -> Result<SurfaceResponse, String>,
1211    ) -> io::Result<()> {
1212        let mut reader = BufReader::new(stream.try_clone()?);
1213        let mut request_line = String::new();
1214        reader.read_line(&mut request_line)?;
1215
1216        let mut content_length = 0usize;
1217        loop {
1218            let mut header = String::new();
1219            reader.read_line(&mut header)?;
1220            let trimmed = header.trim_end();
1221            if trimmed.is_empty() {
1222                break;
1223            }
1224            if let Some((name, value)) = trimmed.split_once(':') {
1225                if name.eq_ignore_ascii_case("content-length") {
1226                    content_length = value.trim().parse().unwrap_or(0);
1227                }
1228            }
1229        }
1230
1231        let mut body = vec![0; content_length];
1232        if content_length > 0 {
1233            reader.read_exact(&mut body)?;
1234        }
1235        let body = String::from_utf8_lossy(&body);
1236
1237        let mut parts = request_line.split_whitespace();
1238        let method = parts.next().unwrap_or("GET");
1239        let path = parts.next().unwrap_or("/");
1240        let response = response_for(method, path, &body, metadata, surface_provider, runner);
1241        write_response(&mut stream, response)
1242    }
1243
1244    fn json_response(
1245        status_code: u16,
1246        reason: &'static str,
1247        value: serde_json::Value,
1248    ) -> HttpResponse {
1249        HttpResponse {
1250            status_code,
1251            reason,
1252            content_type: "application/json",
1253            body: value.to_string(),
1254        }
1255    }
1256
1257    fn write_response(stream: &mut TcpStream, response: HttpResponse) -> io::Result<()> {
1258        write!(
1259            stream,
1260            "HTTP/1.1 {} {}\r\nContent-Type: {}\r\nContent-Length: {}\r\nAccess-Control-Allow-Origin: *\r\nAccess-Control-Allow-Headers: content-type\r\nAccess-Control-Allow-Methods: GET, POST, OPTIONS\r\nConnection: close\r\n\r\n{}",
1261            response.status_code,
1262            response.reason,
1263            response.content_type,
1264            response.body.len(),
1265            response.body
1266        )
1267    }
1268}
1269
1270#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
1271#[serde(transparent)]
1272pub struct JobId(pub String);
1273
1274impl JobId {
1275    pub fn new(value: impl Into<String>) -> Self {
1276        Self(value.into())
1277    }
1278
1279    pub fn as_str(&self) -> &str {
1280        &self.0
1281    }
1282}
1283
1284impl From<&str> for JobId {
1285    fn from(value: &str) -> Self {
1286        Self(value.to_string())
1287    }
1288}
1289
1290impl From<String> for JobId {
1291    fn from(value: String) -> Self {
1292        Self(value)
1293    }
1294}
1295
1296#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
1297#[serde(transparent)]
1298pub struct ArtifactId(pub String);
1299
1300impl ArtifactId {
1301    pub fn new(value: impl Into<String>) -> Self {
1302        Self(value.into())
1303    }
1304
1305    pub fn as_str(&self) -> &str {
1306        &self.0
1307    }
1308}
1309
1310impl From<&str> for ArtifactId {
1311    fn from(value: &str) -> Self {
1312        Self(value.to_string())
1313    }
1314}
1315
1316impl From<String> for ArtifactId {
1317    fn from(value: String) -> Self {
1318        Self(value)
1319    }
1320}
1321
1322#[cfg(test)]
1323mod tests {
1324    use super::*;
1325
1326    #[test]
1327    fn diagnostic_uses_camel_case_json() {
1328        let diagnostic = Diagnostic::new(DiagnosticSeverity::Warning, "demo.warning", "check");
1329        let json = serde_json::to_string(&diagnostic).expect("serialize diagnostic");
1330
1331        assert!(json.contains("\"severity\":\"warning\""));
1332        assert!(json.contains("\"code\":\"demo.warning\""));
1333    }
1334
1335    #[test]
1336    fn pure_rust_capabilities_allow_wasm_and_server() {
1337        let capabilities = RuntimeCapabilities::pure_rust();
1338
1339        assert!(capabilities.native);
1340        assert!(capabilities.server);
1341        assert!(capabilities.wasm);
1342        assert_eq!(capabilities.mobile, MobileCapability::Wasm);
1343    }
1344
1345    #[test]
1346    fn capability_builders_preserve_pure_rust_defaults() {
1347        let capabilities = RuntimeCapabilities::pure_rust()
1348            .with_max_recommended_input_bytes(1024)
1349            .with_requirement("fixture", "test fixture input", false);
1350
1351        assert!(capabilities.native);
1352        assert!(capabilities.server);
1353        assert!(capabilities.wasm);
1354        assert_eq!(capabilities.max_recommended_input_bytes, Some(1024));
1355        assert_eq!(capabilities.requirements[0].name, "fixture");
1356        assert!(!capabilities.requirements[0].required);
1357    }
1358
1359    #[test]
1360    fn package_surface_uses_camel_case_json() {
1361        let surface = PackageSurface {
1362            library: "demo-core".to_string(),
1363            version: "0.1.0".to_string(),
1364            capabilities: RuntimeCapabilities::pure_rust(),
1365            operations: vec![SurfaceOperation {
1366                id: OperationId::new("describe"),
1367                name: "Describe".to_string(),
1368                description: Some("Describe package surface".to_string()),
1369                input_schema: serde_json::json!({"type": "object"}),
1370                output_schema: serde_json::json!({"type": "object"}),
1371                example_request: serde_json::json!({}),
1372                wasm_supported: true,
1373                server_supported: true,
1374            }],
1375        };
1376
1377        let json = serde_json::to_string(&surface).expect("serialize surface");
1378
1379        assert!(json.contains("\"inputSchema\""));
1380        assert!(json.contains("\"exampleRequest\""));
1381        assert!(json.contains("\"wasmSupported\":true"));
1382    }
1383
1384    #[test]
1385    fn surface_helpers_preserve_standard_response_shape() {
1386        let surface = PackageSurface {
1387            library: "demo".to_string(),
1388            version: "0.1.0".to_string(),
1389            capabilities: RuntimeCapabilities::pure_rust(),
1390            operations: vec![surface_operation(
1391                "describe",
1392                "Describe",
1393                "Describe demo package",
1394                serde_json::json!({"includeOperations": true}),
1395            )],
1396        };
1397        let response = describe_surface_response(
1398            &surface,
1399            SurfaceRequest {
1400                operation: OperationId::new("describe"),
1401                input: serde_json::json!({"includeOperations": true}),
1402            },
1403        );
1404
1405        assert_eq!(response.operation.as_str(), "describe");
1406        assert_eq!(response.value["library"], "demo");
1407        assert_eq!(response.value["operationCount"], 1);
1408        assert_eq!(response.diagnostics, Vec::new());
1409        assert_eq!(response.artifacts, Vec::<serde_json::Value>::new());
1410    }
1411
1412    #[test]
1413    fn surface_operation_declares_release_contract_schema() {
1414        let operation = surface_operation(
1415            "demo.run",
1416            "Run demo",
1417            "Run a demo workflow",
1418            serde_json::json!({"text": "hello", "topK": 3}),
1419        );
1420
1421        assert_eq!(operation.input_schema["additionalProperties"], false);
1422        assert_eq!(operation.input_schema["xOperationCategory"], "workflow");
1423        assert_eq!(operation.input_schema["xReleaseStability"], "stable");
1424        assert_eq!(
1425            operation.input_schema["required"],
1426            serde_json::json!(["text"])
1427        );
1428        assert_eq!(operation.input_schema["properties"]["topK"]["minimum"], 1);
1429        assert_eq!(operation.output_schema["required"][0], "operation");
1430    }
1431
1432    #[test]
1433    fn typed_surface_errors_roundtrip_for_transport_adapters() {
1434        let error = SurfaceError::unsupported_operation("demo.missing", "demo-package");
1435        let serialized = error.to_error_string();
1436        let parsed = parse_surface_error(&serialized).expect("typed surface error");
1437
1438        assert_eq!(parsed.code, "unsupported_operation");
1439        assert_eq!(parsed.operation.unwrap().as_str(), "demo.missing");
1440        assert!(parsed.message.contains("unsupported operation"));
1441    }
1442
1443    #[test]
1444    fn surface_error_is_standard_error_type() {
1445        let error = SurfaceError::invalid_request(Some("demo.run"), "invalid request: missing id");
1446        let boxed: Box<dyn std::error::Error> = Box::new(error.clone());
1447
1448        assert_eq!(boxed.to_string(), "invalid request: missing id");
1449        assert_eq!(error.to_string(), "invalid request: missing id");
1450    }
1451
1452    #[test]
1453    fn validation_helpers_return_typed_errors() {
1454        let limit = validate_max_items("demo.run", "values", 3, 2).expect_err("limit error");
1455        let parsed = parse_surface_error(&limit).expect("typed resource error");
1456        assert_eq!(parsed.code, "resource_limit");
1457        assert_eq!(parsed.details["field"], "values");
1458        assert_eq!(parsed.details["actual"], 3);
1459
1460        let length =
1461            validate_matching_lengths("demo.run", "left", 2, "right", 3).expect_err("length");
1462        let parsed = parse_surface_error(&length).expect("typed length error");
1463        assert_eq!(parsed.code, "invalid_request");
1464        assert!(parsed.message.contains("left"));
1465        assert!(parsed.message.contains("right"));
1466    }
1467
1468    #[test]
1469    fn execution_plan_serializes_to_schema_extension() {
1470        let plan = SurfaceExecutionPlan {
1471            operation: OperationId::new("demo.run"),
1472            mode: SurfaceExecutionMode::PlannedJob,
1473            side_effects: vec![SurfaceSideEffect::None],
1474            cancellable: true,
1475            progress_unit: Some("items".to_string()),
1476            expected_artifacts: vec![SurfaceArtifactExpectation {
1477                id: "report".to_string(),
1478                kind: "json".to_string(),
1479                media_type: "application/json".to_string(),
1480                required: true,
1481                description: Some("Structured report".to_string()),
1482            }],
1483            requirements: vec![RuntimeRequirement {
1484                name: "runtime-core".to_string(),
1485                description: Some("Pure Rust planner".to_string()),
1486                required: true,
1487            }],
1488            max_recommended_input_bytes: Some(1024),
1489        };
1490
1491        let operation = surface_operation_with_execution_plan(
1492            "demo.run",
1493            "Run demo",
1494            "Build a deterministic demo plan",
1495            serde_json::json!({"items": [1]}),
1496            plan,
1497        );
1498
1499        assert_eq!(
1500            operation.input_schema["xExecutionPlan"]["mode"],
1501            serde_json::json!("plannedJob")
1502        );
1503        assert_eq!(
1504            operation.output_schema["xExecutionPlan"]["expectedArtifacts"][0]["id"],
1505            serde_json::json!("report")
1506        );
1507        assert_eq!(
1508            operation.input_schema["xExecutionPlan"]["sideEffects"],
1509            serde_json::json!(["none"])
1510        );
1511    }
1512
1513    #[test]
1514    fn landscape_contract_serializes_to_schema_extension() {
1515        let contract = landscape::LandscapeOperationContract::new(
1516            landscape::LandscapeFunction::new("demo.curated", "moritzbrantner-runtime-core")
1517                .input(landscape::LandscapePort::new(
1518                    "request",
1519                    landscape::well_known::runtime_surface_request(),
1520                ))
1521                .output(landscape::LandscapePort::new(
1522                    "response",
1523                    landscape::well_known::runtime_surface_response(),
1524                )),
1525        );
1526
1527        let operation = surface_operation_with_landscape(
1528            "demo.run",
1529            "Run demo",
1530            "Run a curated demo workflow",
1531            serde_json::json!({"request": {"operation": "demo.run", "input": {}}}),
1532            contract,
1533        );
1534
1535        assert_eq!(operation.id.as_str(), "demo.run");
1536        assert_eq!(
1537            operation.example_request["request"]["operation"],
1538            "demo.run"
1539        );
1540        assert!(operation.wasm_supported);
1541        assert!(operation.server_supported);
1542        assert_eq!(
1543            operation.input_schema["xLandscape"]["function"]["id"],
1544            serde_json::json!("demo.curated")
1545        );
1546        assert_eq!(
1547            operation.input_schema["xLandscape"]["function"]["inputs"][0]["typeRef"]["id"],
1548            serde_json::json!("runtime.surfaceRequest")
1549        );
1550        assert_eq!(
1551            operation.output_schema["xLandscape"]["function"]["outputs"][0]["typeRef"]["rustType"],
1552            serde_json::json!("runtime_core::SurfaceResponse")
1553        );
1554    }
1555
1556    #[test]
1557    fn landscape_contract_value_uses_camel_case_json() {
1558        let contract = landscape::LandscapeOperationContract::new(
1559            landscape::LandscapeFunction::new("demo.curated", "moritzbrantner-runtime-core")
1560                .input(
1561                    landscape::LandscapePort::new(
1562                        "optionalRequest",
1563                        landscape::well_known::runtime_surface_request(),
1564                    )
1565                    .optional(),
1566                )
1567                .output(
1568                    landscape::LandscapePort::new(
1569                        "responses",
1570                        landscape::well_known::runtime_surface_response(),
1571                    )
1572                    .many(),
1573                ),
1574        );
1575        let value = landscape_operation_contract_value(&contract);
1576
1577        assert_eq!(value["function"]["stability"], "stable");
1578        assert_eq!(
1579            value["function"]["inputs"][0]["typeRef"]["schemaRef"],
1580            serde_json::Value::Null
1581        );
1582        assert_eq!(value["function"]["inputs"][0]["required"], false);
1583        assert_eq!(value["function"]["inputs"][0]["cardinality"], "optional");
1584        assert_eq!(value["function"]["outputs"][0]["cardinality"], "many");
1585    }
1586
1587    #[test]
1588    fn new_surface_error_constructors_are_typed_json() {
1589        let cancelled = SurfaceError::cancelled("demo.run", "cancelled by request");
1590        assert_eq!(cancelled.code, "cancelled");
1591        assert_eq!(cancelled.operation.unwrap().as_str(), "demo.run");
1592
1593        let execution = SurfaceError::execution_failed(
1594            "demo.run",
1595            "execution failed",
1596            serde_json::json!({
1597                "stage": "prepare"
1598            }),
1599        );
1600        assert_eq!(execution.code, "execution_failed");
1601        assert_eq!(execution.details["stage"], "prepare");
1602
1603        let artifact = SurfaceError::artifact_error(
1604            "demo.run",
1605            "artifact invalid",
1606            serde_json::json!({
1607                "artifact": "report"
1608            }),
1609        );
1610        assert_eq!(artifact.code, "artifact_error");
1611        assert_eq!(artifact.details["artifact"], "report");
1612    }
1613
1614    #[test]
1615    fn structured_operation_response_preserves_result_fields() {
1616        let surface = PackageSurface {
1617            library: "demo".to_string(),
1618            version: "0.1.0".to_string(),
1619            capabilities: RuntimeCapabilities::pure_rust(),
1620            operations: vec![surface_operation(
1621                "demo.run",
1622                "Run demo",
1623                "Run a demo workflow",
1624                serde_json::json!({"values": [1, 2]}),
1625            )],
1626        };
1627        let response = structured_operation_response(
1628            &surface,
1629            OperationId::new("demo.run"),
1630            serde_json::json!({"count": 2, "values": [1, 2]}),
1631        );
1632
1633        assert_eq!(response.value["count"], 2);
1634        assert_eq!(response.value["operation"], "demo.run");
1635        assert_eq!(response.value["title"], "Run demo");
1636        assert_eq!(response.value["summary"]["count"], 2);
1637        assert_eq!(
1638            response.value["result"]["values"],
1639            serde_json::json!([1, 2])
1640        );
1641    }
1642}