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
446pub 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
672pub 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
709pub 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
722pub 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
749pub 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
778pub 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
790pub 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
850pub 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 #[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 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 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 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 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
942pub 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 #[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 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 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 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}