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, Eq)]
172#[serde(rename_all = "camelCase")]
173pub struct SurfaceOperationCuration {
174    pub role: SurfaceOperationRole,
175    pub primary: bool,
176    pub sort_order: u16,
177}
178
179impl SurfaceOperationCuration {
180    pub fn workflow(sort_order: u16) -> Self {
181        Self {
182            role: SurfaceOperationRole::Workflow,
183            primary: false,
184            sort_order,
185        }
186    }
187
188    pub fn debug(sort_order: u16) -> Self {
189        Self {
190            role: SurfaceOperationRole::Debug,
191            primary: false,
192            sort_order,
193        }
194    }
195
196    pub fn support(sort_order: u16) -> Self {
197        Self {
198            role: SurfaceOperationRole::Support,
199            primary: false,
200            sort_order,
201        }
202    }
203
204    pub fn primary(mut self) -> Self {
205        self.primary = true;
206        self
207    }
208
209    pub fn from_operation_id(operation_id: &str) -> Self {
210        if operation_id == "describe" {
211            Self::debug(900)
212        } else {
213            match operation_category(operation_id) {
214                "debug" => Self::debug(900),
215                "support" => Self::support(500),
216                _ => Self::workflow(100),
217            }
218        }
219    }
220
221    fn legacy_category(&self) -> &'static str {
222        self.role.legacy_category()
223    }
224}
225
226impl Default for SurfaceOperationCuration {
227    fn default() -> Self {
228        Self::workflow(100)
229    }
230}
231
232#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
233#[serde(rename_all = "camelCase")]
234pub enum SurfaceOperationRole {
235    Workflow,
236    Debug,
237    Support,
238}
239
240impl SurfaceOperationRole {
241    fn legacy_category(self) -> &'static str {
242        match self {
243            Self::Workflow => "workflow",
244            Self::Debug => "debug",
245            Self::Support => "support",
246        }
247    }
248
249    fn from_legacy_category(value: &str) -> Option<Self> {
250        match value {
251            "workflow" => Some(Self::Workflow),
252            "debug" => Some(Self::Debug),
253            "support" => Some(Self::Support),
254            _ => None,
255        }
256    }
257}
258
259#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
260#[serde(rename_all = "camelCase")]
261pub struct SurfaceOperation {
262    pub id: OperationId,
263    pub name: String,
264    pub description: Option<String>,
265    #[serde(default)]
266    pub curation: SurfaceOperationCuration,
267    pub input_schema: serde_json::Value,
268    pub output_schema: serde_json::Value,
269    pub example_request: serde_json::Value,
270    pub wasm_supported: bool,
271    pub server_supported: bool,
272}
273
274#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
275#[serde(rename_all = "camelCase")]
276pub enum SurfaceExecutionMode {
277    InMemory,
278    PlannedJob,
279    BackgroundJob,
280    ExternalCommand,
281}
282
283#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
284#[serde(rename_all = "camelCase")]
285pub enum SurfaceSideEffect {
286    None,
287    ReadsFiles,
288    WritesFiles,
289    Network,
290    ExternalProcess,
291}
292
293#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
294#[serde(rename_all = "camelCase")]
295pub struct SurfaceArtifactExpectation {
296    pub id: String,
297    pub kind: String,
298    pub media_type: String,
299    pub required: bool,
300    pub description: Option<String>,
301}
302
303#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
304#[serde(rename_all = "camelCase")]
305pub struct SurfaceExecutionPlan {
306    pub operation: OperationId,
307    pub mode: SurfaceExecutionMode,
308    pub side_effects: Vec<SurfaceSideEffect>,
309    pub cancellable: bool,
310    pub progress_unit: Option<String>,
311    pub expected_artifacts: Vec<SurfaceArtifactExpectation>,
312    pub requirements: Vec<RuntimeRequirement>,
313    pub max_recommended_input_bytes: Option<u64>,
314}
315
316#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
317#[serde(rename_all = "camelCase")]
318pub struct SurfaceRuntimeContext {
319    pub runtime: SurfaceRuntimeKind,
320    pub side_effects: SurfaceSideEffectPolicy,
321    pub storage: SurfaceStorageContext,
322    pub model: SurfaceModelContext,
323}
324
325impl SurfaceRuntimeContext {
326    pub fn no_side_effects(runtime: SurfaceRuntimeKind) -> Self {
327        Self {
328            runtime,
329            side_effects: SurfaceSideEffectPolicy::none(),
330            storage: SurfaceStorageContext::default(),
331            model: SurfaceModelContext::plan_only(),
332        }
333    }
334
335    pub fn compatibility_no_side_effects() -> Self {
336        Self::no_side_effects(SurfaceRuntimeKind::Unknown)
337    }
338
339    pub fn allows_model_auto_setup(&self) -> bool {
340        self.model.auto_setup
341            && self.side_effects.allow_reads
342            && self.side_effects.allow_writes
343            && self.side_effects.allow_network
344            && self.storage.model_root.is_some()
345            && self.runtime.can_setup_models()
346    }
347}
348
349#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
350#[serde(rename_all = "camelCase")]
351pub enum SurfaceRuntimeKind {
352    NativeCli,
353    NativeServer,
354    Wasm,
355    Browser,
356    Mobile,
357    Unknown,
358}
359
360impl SurfaceRuntimeKind {
361    pub fn can_setup_models(&self) -> bool {
362        matches!(self, Self::NativeCli | Self::NativeServer)
363    }
364
365    pub fn can_run_external_processes(&self) -> bool {
366        matches!(self, Self::NativeCli | Self::NativeServer)
367    }
368}
369
370#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
371#[serde(rename_all = "camelCase")]
372pub struct SurfaceSideEffectPolicy {
373    pub allow_reads: bool,
374    pub allow_writes: bool,
375    pub allow_network: bool,
376    pub allow_external_process: bool,
377    pub max_download_bytes: Option<u64>,
378}
379
380impl SurfaceSideEffectPolicy {
381    pub fn none() -> Self {
382        Self {
383            allow_reads: false,
384            allow_writes: false,
385            allow_network: false,
386            allow_external_process: false,
387            max_download_bytes: None,
388        }
389    }
390}
391
392#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
393#[serde(rename_all = "camelCase")]
394pub struct SurfaceStorageContext {
395    pub cache_root: Option<String>,
396    pub artifact_root: Option<String>,
397    pub model_root: Option<String>,
398}
399
400#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
401#[serde(rename_all = "camelCase")]
402pub struct SurfaceModelContext {
403    pub auto_setup: bool,
404    pub preference: SurfaceModelExecutionPreference,
405}
406
407impl SurfaceModelContext {
408    pub fn plan_only() -> Self {
409        Self {
410            auto_setup: false,
411            preference: SurfaceModelExecutionPreference::PlanOnly,
412        }
413    }
414}
415
416#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
417#[serde(rename_all = "camelCase")]
418pub enum SurfaceModelExecutionPreference {
419    ModelFirst,
420    PlanOnly,
421}
422
423#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
424#[serde(rename_all = "camelCase")]
425pub struct SurfaceRequest {
426    pub operation: OperationId,
427    pub input: serde_json::Value,
428}
429
430#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
431#[serde(rename_all = "camelCase")]
432pub struct SurfaceResponse {
433    pub operation: OperationId,
434    pub value: serde_json::Value,
435    pub diagnostics: Vec<Diagnostic>,
436    pub artifacts: Vec<serde_json::Value>,
437}
438
439#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
440#[serde(rename_all = "camelCase")]
441pub struct SurfaceError {
442    pub code: String,
443    pub message: String,
444    pub operation: Option<OperationId>,
445    pub details: serde_json::Value,
446}
447
448impl SurfaceError {
449    pub fn invalid_request(
450        operation: Option<impl Into<OperationId>>,
451        message: impl Into<String>,
452    ) -> Self {
453        Self::new("invalid_request", operation, message, serde_json::json!({}))
454    }
455
456    pub fn unsupported_operation(
457        operation: impl Into<OperationId>,
458        package: impl Into<String>,
459    ) -> Self {
460        let operation = operation.into();
461        let package = package.into();
462        Self::new(
463            "unsupported_operation",
464            Some(operation.clone()),
465            format!(
466                "unsupported operation `{}` for {}",
467                operation.as_str(),
468                package
469            ),
470            serde_json::json!({"package": package}),
471        )
472    }
473
474    pub fn unsupported_value(
475        operation: Option<impl Into<OperationId>>,
476        field: impl Into<String>,
477        value: impl Into<String>,
478        allowed: &[&str],
479    ) -> Self {
480        let field = field.into();
481        let value = value.into();
482        Self::new(
483            "unsupported_value",
484            operation,
485            format!("unsupported value `{value}` for `{field}`"),
486            serde_json::json!({
487                "field": field,
488                "value": value,
489                "allowed": allowed
490            }),
491        )
492    }
493
494    pub fn resource_limit(
495        operation: Option<impl Into<OperationId>>,
496        field: impl Into<String>,
497        limit: usize,
498        actual: usize,
499    ) -> Self {
500        let field = field.into();
501        Self::new(
502            "resource_limit",
503            operation,
504            format!("`{field}` exceeds the maximum supported size of {limit}"),
505            serde_json::json!({
506                "field": field,
507                "limit": limit,
508                "actual": actual
509            }),
510        )
511    }
512
513    pub fn cancelled(operation: impl Into<OperationId>, message: impl Into<String>) -> Self {
514        Self::new("cancelled", Some(operation), message, serde_json::json!({}))
515    }
516
517    pub fn execution_failed(
518        operation: impl Into<OperationId>,
519        message: impl Into<String>,
520        details: serde_json::Value,
521    ) -> Self {
522        Self::new("execution_failed", Some(operation), message, details)
523    }
524
525    pub fn artifact_error(
526        operation: impl Into<OperationId>,
527        message: impl Into<String>,
528        details: serde_json::Value,
529    ) -> Self {
530        Self::new("artifact_error", Some(operation), message, details)
531    }
532
533    pub fn missing_dependency(
534        operation: Option<impl Into<OperationId>>,
535        dependency: impl Into<String>,
536        setup: impl Into<String>,
537    ) -> Self {
538        let dependency = dependency.into();
539        let setup = setup.into();
540        Self::new(
541            "missing_dependency",
542            operation,
543            format!("missing required dependency `{dependency}`"),
544            serde_json::json!({
545                "dependency": dependency,
546                "setup": setup
547            }),
548        )
549    }
550
551    pub fn permission_denied(
552        operation: Option<impl Into<OperationId>>,
553        permission: impl Into<String>,
554        message: impl Into<String>,
555    ) -> Self {
556        let permission = permission.into();
557        Self::new(
558            "permission_denied",
559            operation,
560            message,
561            serde_json::json!({
562                "permission": permission
563            }),
564        )
565    }
566
567    pub fn new(
568        code: impl Into<String>,
569        operation: Option<impl Into<OperationId>>,
570        message: impl Into<String>,
571        details: serde_json::Value,
572    ) -> Self {
573        Self {
574            code: code.into(),
575            message: message.into(),
576            operation: operation.map(Into::into),
577            details,
578        }
579    }
580
581    pub fn to_error_string(&self) -> String {
582        serde_json::to_string(self).unwrap_or_else(|_| self.message.clone())
583    }
584}
585
586impl fmt::Display for SurfaceError {
587    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
588        f.write_str(&self.message)
589    }
590}
591
592impl std::error::Error for SurfaceError {}
593
594pub fn parse_surface_error(error: &str) -> Option<SurfaceError> {
595    serde_json::from_str(error).ok()
596}
597
598pub fn parse_surface_input<T: for<'de> Deserialize<'de>>(
599    operation: Option<&str>,
600    input: serde_json::Value,
601) -> Result<T, String> {
602    serde_json::from_value(input).map_err(|error| {
603        SurfaceError::invalid_request(
604            operation.map(OperationId::new),
605            format!("invalid request: {error}"),
606        )
607        .to_error_string()
608    })
609}
610
611pub fn require_non_empty<T>(operation: &str, field: &str, values: &[T]) -> Result<(), String> {
612    if values.is_empty() {
613        Err(SurfaceError::invalid_request(
614            Some(OperationId::new(operation)),
615            format!("invalid request: {field} must not be empty"),
616        )
617        .to_error_string())
618    } else {
619        Ok(())
620    }
621}
622
623pub fn validate_max_items(
624    operation: &str,
625    field: &str,
626    actual: usize,
627    limit: usize,
628) -> Result<(), String> {
629    if actual > limit {
630        Err(
631            SurfaceError::resource_limit(Some(OperationId::new(operation)), field, limit, actual)
632                .to_error_string(),
633        )
634    } else {
635        Ok(())
636    }
637}
638
639pub fn validate_matching_lengths(
640    operation: &str,
641    left_field: &str,
642    left_len: usize,
643    right_field: &str,
644    right_len: usize,
645) -> Result<(), String> {
646    if left_len != right_len {
647        Err(SurfaceError::invalid_request(
648            Some(OperationId::new(operation)),
649            format!(
650                "invalid request: `{left_field}` length {left_len} must match `{right_field}` length {right_len}"
651            ),
652        )
653        .to_error_string())
654    } else {
655        Ok(())
656    }
657}
658
659/// Builds the standard package-surface operation metadata used by library
660/// crates and transport adapters.
661pub fn surface_operation(
662    id: impl Into<String>,
663    name: impl Into<String>,
664    description: impl Into<String>,
665    example_request: serde_json::Value,
666) -> SurfaceOperation {
667    let id = id.into();
668    let curation = SurfaceOperationCuration::from_operation_id(&id);
669    let mut operation = SurfaceOperation {
670        id: OperationId::new(id.clone()),
671        name: name.into(),
672        description: Some(description.into()),
673        input_schema: surface_input_schema(&id, &example_request),
674        output_schema: surface_output_schema(&id),
675        curation: curation.clone(),
676        example_request,
677        wasm_supported: true,
678        server_supported: true,
679    };
680    set_surface_operation_curation(&mut operation, curation);
681    operation
682}
683
684pub fn surface_operation_with_execution_plan(
685    id: impl Into<String>,
686    name: impl Into<String>,
687    description: impl Into<String>,
688    example_request: serde_json::Value,
689    execution_plan: SurfaceExecutionPlan,
690) -> SurfaceOperation {
691    let mut operation = surface_operation(id, name, description, example_request);
692    let execution_plan = surface_execution_plan_value(&execution_plan);
693    insert_schema_extension(
694        &mut operation.input_schema,
695        "xExecutionPlan",
696        execution_plan.clone(),
697    );
698    insert_schema_extension(
699        &mut operation.output_schema,
700        "xExecutionPlan",
701        execution_plan,
702    );
703    operation
704}
705
706pub fn primary_workflow_operation(
707    id: impl Into<String>,
708    name: impl Into<String>,
709    description: impl Into<String>,
710    example_request: serde_json::Value,
711    execution_plan: Option<SurfaceExecutionPlan>,
712    lower_contracts: &[&str],
713) -> SurfaceOperation {
714    let mut operation = if let Some(plan) = execution_plan {
715        surface_operation_with_execution_plan(id, name, description, example_request, plan)
716    } else {
717        surface_operation(id, name, description, example_request)
718    };
719    set_surface_operation_curation(
720        &mut operation,
721        SurfaceOperationCuration::workflow(0).primary(),
722    );
723    if !lower_contracts.is_empty() {
724        let proof = serde_json::json!({
725            "policy": "primary workflow proves compatibility with lower crate contracts",
726            "crates": lower_contracts
727        });
728        insert_schema_extension(
729            &mut operation.input_schema,
730            "xLowerContractProof",
731            proof.clone(),
732        );
733        insert_schema_extension(&mut operation.output_schema, "xLowerContractProof", proof);
734    }
735    operation
736}
737
738/// Adds a schema extension to both input and output schemas for an operation.
739///
740/// Runtime surfaces use `x*` schema fields for additive metadata such as
741/// operation category, compatibility/deprecation state, or replacement hints.
742pub fn add_surface_operation_schema_extension(
743    operation: &mut SurfaceOperation,
744    key: impl Into<String>,
745    value: serde_json::Value,
746) {
747    let key = key.into();
748    if key == "xOperationCategory" {
749        if let Some(category) = value
750            .as_str()
751            .and_then(SurfaceOperationRole::from_legacy_category)
752        {
753            operation.curation.role = category;
754        }
755    }
756    insert_schema_extension(&mut operation.input_schema, &key, value.clone());
757    insert_schema_extension(&mut operation.output_schema, &key, value);
758}
759
760pub fn set_surface_operation_curation(
761    operation: &mut SurfaceOperation,
762    curation: SurfaceOperationCuration,
763) {
764    let category = serde_json::json!(curation.legacy_category());
765    operation.curation = curation;
766    insert_schema_extension(
767        &mut operation.input_schema,
768        "xOperationCategory",
769        category.clone(),
770    );
771    insert_schema_extension(&mut operation.output_schema, "xOperationCategory", category);
772}
773
774pub fn landscape_operation_contract_value(
775    contract: &landscape::LandscapeOperationContract,
776) -> serde_json::Value {
777    serde_json::to_value(contract).unwrap_or_else(|_| serde_json::json!({}))
778}
779
780pub fn surface_operation_with_landscape(
781    id: impl Into<String>,
782    name: impl Into<String>,
783    description: impl Into<String>,
784    example_request: serde_json::Value,
785    contract: landscape::LandscapeOperationContract,
786) -> SurfaceOperation {
787    let mut operation = surface_operation(id, name, description, example_request);
788    attach_landscape_contract(&mut operation, contract);
789    operation
790}
791
792pub fn attach_landscape_contract(
793    operation: &mut SurfaceOperation,
794    contract: landscape::LandscapeOperationContract,
795) {
796    let landscape = landscape_operation_contract_value(&contract);
797    insert_schema_extension(&mut operation.input_schema, "xLandscape", landscape.clone());
798    insert_schema_extension(&mut operation.output_schema, "xLandscape", landscape);
799}
800
801pub fn surface_execution_plan_value(plan: &SurfaceExecutionPlan) -> serde_json::Value {
802    serde_json::to_value(plan).unwrap_or_else(|_| serde_json::json!({}))
803}
804
805fn insert_schema_extension(schema: &mut serde_json::Value, key: &str, value: serde_json::Value) {
806    if let serde_json::Value::Object(object) = schema {
807        object.insert(key.to_string(), value);
808    }
809}
810
811pub fn surface_input_schema(
812    operation: &str,
813    example_request: &serde_json::Value,
814) -> serde_json::Value {
815    let properties = match example_request {
816        serde_json::Value::Object(object) => object
817            .iter()
818            .map(|(key, value)| (key.clone(), infer_schema_for_value(key, value)))
819            .collect::<serde_json::Map<_, _>>(),
820        _ => serde_json::Map::new(),
821    };
822    let required = required_fields_for_operation(operation, example_request);
823    serde_json::json!({
824        "type": "object",
825        "additionalProperties": false,
826        "properties": properties,
827        "required": required,
828        "xOperationCategory": operation_category(operation),
829        "xReleaseStability": "stable",
830        "xContractPolicy": "additiveOnly",
831        "xErrorShape": {
832            "code": "string",
833            "message": "string",
834            "operation": "string|null",
835            "details": "object"
836        },
837        "xResourceLimits": {
838            "maxRecommendedInputBytes": 1048576,
839            "largePayloadBehavior": "reject or deterministically truncate by operation-specific limit"
840        }
841    })
842}
843
844pub fn surface_output_schema(operation: &str) -> serde_json::Value {
845    serde_json::json!({
846        "type": "object",
847        "required": ["operation", "title", "message", "summary", "result"],
848        "properties": {
849            "operation": {"type": "string", "const": operation},
850            "title": {"type": "string", "minLength": 1},
851            "message": {"type": "string", "minLength": 1},
852            "summary": {"type": "object"},
853            "result": {}
854        },
855        "additionalProperties": true
856    })
857}
858
859pub fn operation_category(operation: &str) -> &'static str {
860    match operation {
861        "describe"
862        | "analysis.describe"
863        | "classification.models"
864        | "classification.schema"
865        | "embeddings.backends"
866        | "index.open"
867        | "index.inspect"
868        | "qa.models" => "debug",
869        "index.removeDocuments" | "runtime.softmax" => "support",
870        _ => "workflow",
871    }
872}
873
874fn required_fields_for_operation(
875    operation: &str,
876    example_request: &serde_json::Value,
877) -> Vec<String> {
878    if operation == "describe" || operation.ends_with(".models") || operation.ends_with(".describe")
879    {
880        return Vec::new();
881    }
882    let optional = [
883        "dimensions",
884        "embedding",
885        "id",
886        "includeNearDuplicates",
887        "includePunctuation",
888        "includeSemanticNeighbors",
889        "keywordLimit",
890        "linguistics",
891        "lowercase",
892        "maxAlternatives",
893        "maxTokens",
894        "minTokensForDecision",
895        "mode",
896        "model",
897        "n",
898        "ngramSizes",
899        "normalizeWhitespace",
900        "options",
901        "order",
902        "profile",
903        "previewLimit",
904        "seed",
905        "sentenceLevel",
906        "shingleSizes",
907        "streamId",
908        "summarySentences",
909        "topK",
910        "truncation",
911    ];
912    match example_request {
913        serde_json::Value::Object(object) => object
914            .keys()
915            .filter(|key| !optional.contains(&key.as_str()))
916            .cloned()
917            .collect(),
918        _ => Vec::new(),
919    }
920}
921
922fn infer_schema_for_value(key: &str, value: &serde_json::Value) -> serde_json::Value {
923    let mut schema = match value {
924        serde_json::Value::Bool(_) => serde_json::json!({"type": "boolean"}),
925        serde_json::Value::Number(number) if number.is_i64() || number.is_u64() => {
926            serde_json::json!({"type": "integer", "minimum": 0})
927        }
928        serde_json::Value::Number(_) => serde_json::json!({"type": "number"}),
929        serde_json::Value::String(_) => serde_json::json!({"type": "string", "minLength": 1}),
930        serde_json::Value::Array(values) => {
931            let item_schema = values
932                .first()
933                .map(|value| infer_schema_for_value("item", value))
934                .unwrap_or_else(|| serde_json::json!({}));
935            serde_json::json!({"type": "array", "items": item_schema, "minItems": 1})
936        }
937        serde_json::Value::Object(object) => serde_json::json!({
938            "type": "object",
939            "additionalProperties": true,
940            "properties": object
941                .iter()
942                .map(|(key, value)| (key.clone(), infer_schema_for_value(key, value)))
943                .collect::<serde_json::Map<_, _>>()
944        }),
945        serde_json::Value::Null => serde_json::json!({}),
946    };
947    if matches!(
948        key,
949        "topK" | "top_k" | "maxTokens" | "max_tokens" | "order" | "dimensions" | "n"
950    ) {
951        if let serde_json::Value::Object(object) = &mut schema {
952            object.insert("minimum".to_string(), serde_json::json!(1));
953            object.insert("maximum".to_string(), serde_json::json!(4096));
954        }
955    }
956    schema
957}
958
959/// Builds the standard `describe` response without changing the shared
960/// `SurfaceResponse` JSON shape.
961pub fn describe_surface_response(
962    surface: &PackageSurface,
963    request: SurfaceRequest,
964) -> SurfaceResponse {
965    let result = serde_json::json!({
966        "library": &surface.library,
967        "version": &surface.version,
968        "operationCount": surface.operations.len(),
969        "operations": surface
970            .operations
971            .iter()
972            .map(|operation| operation.id.as_str())
973            .collect::<Vec<_>>(),
974        "input": request.input
975    });
976    structured_surface_response(
977        request.operation,
978        "Package surface metadata",
979        format!(
980            "{} exposes {} package-surface operations.",
981            surface.library,
982            surface.operations.len()
983        ),
984        serde_json::json!({
985            "operationCount": surface.operations.len(),
986            "runtime": {
987                "wasm": surface.capabilities.wasm,
988                "server": surface.capabilities.server,
989                "native": surface.capabilities.native
990            }
991        }),
992        result,
993    )
994}
995
996/// Builds a successful surface response with empty diagnostics and artifacts.
997pub fn surface_response(operation: OperationId, value: serde_json::Value) -> SurfaceResponse {
998    let title = operation.as_str().to_string();
999    let message = format!("Ran package-surface operation `{}`.", operation.as_str());
1000    let value = ensure_structured_surface_value(&operation, title, message, value);
1001    SurfaceResponse {
1002        operation,
1003        value,
1004        diagnostics: Vec::new(),
1005        artifacts: Vec::new(),
1006    }
1007}
1008
1009/// Builds a package-surface value with standard human-readable metadata while
1010/// preserving object fields from the concrete operation result at the top level.
1011pub fn structured_surface_value(
1012    operation: &OperationId,
1013    title: impl Into<String>,
1014    message: impl Into<String>,
1015    summary: serde_json::Value,
1016    result: serde_json::Value,
1017) -> serde_json::Value {
1018    let mut object = match &result {
1019        serde_json::Value::Object(map) => map.clone(),
1020        _ => serde_json::Map::new(),
1021    };
1022    object.insert("title".to_string(), serde_json::Value::String(title.into()));
1023    object.insert(
1024        "operation".to_string(),
1025        serde_json::Value::String(operation.as_str().to_string()),
1026    );
1027    object.insert(
1028        "message".to_string(),
1029        serde_json::Value::String(message.into()),
1030    );
1031    object.insert("summary".to_string(), summary);
1032    object.insert("result".to_string(), result);
1033    serde_json::Value::Object(object)
1034}
1035
1036/// Adds the common package-surface UI fields to a result value when they are
1037/// missing, while preserving every existing top-level domain field.
1038pub fn ensure_structured_surface_value(
1039    operation: &OperationId,
1040    title: impl Into<String>,
1041    message: impl Into<String>,
1042    value: serde_json::Value,
1043) -> serde_json::Value {
1044    let result = value.clone();
1045    let mut object = match value {
1046        serde_json::Value::Object(map) => map,
1047        _ => serde_json::Map::new(),
1048    };
1049    object
1050        .entry("operation".to_string())
1051        .or_insert_with(|| serde_json::Value::String(operation.as_str().to_string()));
1052    object
1053        .entry("title".to_string())
1054        .or_insert_with(|| serde_json::Value::String(title.into()));
1055    object
1056        .entry("message".to_string())
1057        .or_insert_with(|| serde_json::Value::String(message.into()));
1058    object
1059        .entry("summary".to_string())
1060        .or_insert_with(|| operation_summary(&result));
1061    object.entry("result".to_string()).or_insert(result);
1062    serde_json::Value::Object(object)
1063}
1064
1065/// Builds a successful package-surface response using `structured_surface_value`.
1066pub fn structured_surface_response(
1067    operation: OperationId,
1068    title: impl Into<String>,
1069    message: impl Into<String>,
1070    summary: serde_json::Value,
1071    result: serde_json::Value,
1072) -> SurfaceResponse {
1073    let value = structured_surface_value(&operation, title, message, summary, result);
1074    surface_response(operation, value)
1075}
1076
1077/// Builds a structured response for an operation listed in a package surface.
1078///
1079/// This keeps the concrete operation result at the top level for compatibility,
1080/// while adding the common `title`, `message`, `summary`, and `result` fields
1081/// expected by package-surface UIs.
1082pub fn structured_operation_response(
1083    surface: &PackageSurface,
1084    operation: OperationId,
1085    result: serde_json::Value,
1086) -> SurfaceResponse {
1087    let metadata = surface
1088        .operations
1089        .iter()
1090        .find(|candidate| candidate.id.as_str() == operation.as_str());
1091    let title = metadata
1092        .map(|operation| operation.name.clone())
1093        .unwrap_or_else(|| operation.as_str().to_string());
1094    let message = metadata
1095        .and_then(|operation| operation.description.clone())
1096        .unwrap_or_else(|| format!("Ran package-surface operation `{}`.", operation.as_str()));
1097    let summary = operation_summary(&result);
1098    structured_surface_response(operation, title, message, summary, result)
1099}
1100
1101fn operation_summary(result: &serde_json::Value) -> serde_json::Value {
1102    match result {
1103        serde_json::Value::Object(object) => {
1104            let mut summary = serde_json::Map::new();
1105            summary.insert("status".to_string(), serde_json::json!("ok"));
1106            for key in [
1107                "count",
1108                "width",
1109                "height",
1110                "format",
1111                "pixelFormat",
1112                "dimensions",
1113                "operationCount",
1114            ] {
1115                if let Some(value) = object.get(key) {
1116                    summary.insert(key.to_string(), value.clone());
1117                }
1118            }
1119            if let Some((key, value)) = object
1120                .iter()
1121                .find(|(_, value)| matches!(value, serde_json::Value::Array(_)))
1122            {
1123                summary.insert(
1124                    format!("{key}Count"),
1125                    serde_json::json!(value.as_array().map(Vec::len).unwrap_or(0)),
1126                );
1127            }
1128            serde_json::Value::Object(summary)
1129        }
1130        serde_json::Value::Array(values) => {
1131            serde_json::json!({"status": "ok", "count": values.len()})
1132        }
1133        _ => serde_json::json!({"status": "ok"}),
1134    }
1135}
1136
1137/// Shared helpers for thin package-surface CLI adapters.
1138pub mod cli {
1139    use std::fs;
1140    use std::io::{self, Read};
1141
1142    use super::{
1143        ensure_structured_surface_value, OperationId, PackageSurface, SurfaceRequest,
1144        SurfaceResponse,
1145    };
1146
1147    /// Static package metadata for one CLI adapter.
1148    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1149    pub struct CliAdapterMetadata {
1150        pub library_crate: &'static str,
1151        pub surface_kind: &'static str,
1152        pub library_import: &'static str,
1153        pub server_package: &'static str,
1154        pub app_package: &'static str,
1155        pub wasm_package: &'static str,
1156    }
1157
1158    /// Builds the standard CLI adapter metadata payload.
1159    pub fn package_metadata_json(metadata: CliAdapterMetadata, surface: PackageSurface) -> String {
1160        serde_json::json!({
1161            "package": format!("{}-cli", metadata.library_crate),
1162            "surface": metadata.surface_kind,
1163            "library": metadata.library_crate,
1164            "libraryImport": metadata.library_import,
1165            "serverPackage": metadata.server_package,
1166            "appPackage": metadata.app_package,
1167            "wasmPackage": metadata.wasm_package,
1168            "operations": surface.operations
1169        })
1170        .to_string()
1171    }
1172
1173    /// Builds the standard CLI command schema payload.
1174    pub fn command_schema_json() -> String {
1175        serde_json::json!({
1176            "commands": [
1177                {"name": "info", "description": "Print package and adapter metadata."},
1178                {"name": "schema", "description": "Print the CLI command schema."},
1179                {"name": "operations", "description": "Print library operations."},
1180                {"name": "run", "description": "Run one library-owned operation."}
1181            ]
1182        })
1183        .to_string()
1184    }
1185
1186    /// Reads a JSON request from `--json`, `--file`, or stdin.
1187    pub fn read_json_input(
1188        json: Option<String>,
1189        file: Option<String>,
1190    ) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
1191        let input = if let Some(json) = json {
1192            json
1193        } else if let Some(file) = file {
1194            fs::read_to_string(file)?
1195        } else {
1196            let mut buffer = String::new();
1197            io::stdin().read_to_string(&mut buffer)?;
1198            if buffer.trim().is_empty() {
1199                "{}".to_string()
1200            } else {
1201                buffer
1202            }
1203        };
1204        Ok(serde_json::from_str(&input)?)
1205    }
1206
1207    /// Runs an operation through a library-owned surface and adds standard
1208    /// package-surface value fields if an older surface omitted them.
1209    pub fn run_wrapped_operation(
1210        operation: &str,
1211        input: serde_json::Value,
1212        runner: fn(SurfaceRequest) -> Result<SurfaceResponse, String>,
1213    ) -> Result<SurfaceResponse, String> {
1214        let mut response = runner(SurfaceRequest {
1215            operation: OperationId::new(operation),
1216            input,
1217        })?;
1218        let value = std::mem::take(&mut response.value);
1219        response.value = ensure_structured_surface_value(
1220            &response.operation,
1221            operation.to_string(),
1222            format!("Ran package-surface operation `{}`.", operation),
1223            value,
1224        );
1225        Ok(response)
1226    }
1227}
1228
1229/// Shared helpers for local package-surface HTTP adapters.
1230pub mod server {
1231    use std::io::{self, BufRead, BufReader, Read, Write};
1232    use std::net::{TcpListener, TcpStream};
1233
1234    use super::{
1235        parse_surface_error, Diagnostic, DiagnosticSeverity, OperationId, PackageSurface,
1236        SurfaceRequest, SurfaceResponse,
1237    };
1238
1239    /// Static package metadata for one server adapter.
1240    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1241    pub struct ServerAdapterMetadata {
1242        pub library_crate: &'static str,
1243        pub surface_kind: &'static str,
1244        pub library_import: &'static str,
1245        pub cli_package: &'static str,
1246        pub app_package: &'static str,
1247        pub wasm_package: &'static str,
1248    }
1249
1250    #[derive(Debug, Clone, PartialEq, Eq)]
1251    pub struct HttpResponse {
1252        pub status_code: u16,
1253        pub reason: &'static str,
1254        pub content_type: &'static str,
1255        pub body: String,
1256    }
1257
1258    /// Serves the standard local package-surface HTTP API.
1259    pub fn serve(
1260        addr: &str,
1261        metadata: ServerAdapterMetadata,
1262        surface_provider: fn() -> PackageSurface,
1263        runner: fn(SurfaceRequest) -> Result<SurfaceResponse, String>,
1264    ) -> io::Result<()> {
1265        let listener = TcpListener::bind(addr)?;
1266        for stream in listener.incoming() {
1267            handle_stream(stream?, metadata, surface_provider, runner)?;
1268        }
1269        Ok(())
1270    }
1271
1272    /// Returns the standard response for one HTTP request.
1273    pub fn response_for(
1274        method: &str,
1275        path: &str,
1276        body: &str,
1277        metadata: ServerAdapterMetadata,
1278        surface_provider: fn() -> PackageSurface,
1279        runner: fn(SurfaceRequest) -> Result<SurfaceResponse, String>,
1280    ) -> HttpResponse {
1281        match (method, path) {
1282            ("OPTIONS", _) => HttpResponse {
1283                status_code: 204,
1284                reason: "No Content",
1285                content_type: "application/json",
1286                body: String::new(),
1287            },
1288            ("GET", "/health") => json_response(
1289                200,
1290                "OK",
1291                serde_json::json!({
1292                    "ok": true,
1293                    "package": format!("{}-server", metadata.library_crate),
1294                    "library": metadata.library_crate
1295                }),
1296            ),
1297            ("GET", "/api/package") => json_response(
1298                200,
1299                "OK",
1300                package_metadata_value(metadata, surface_provider()),
1301            ),
1302            ("GET", "/api/schema") => {
1303                json_response(200, "OK", schema_value(metadata, surface_provider()))
1304            }
1305            ("GET", "/api/operations") => {
1306                json_response(200, "OK", serde_json::json!(surface_provider().operations))
1307            }
1308            ("POST", "/api/run") => run_response(body, metadata, runner),
1309            ("POST", path) if path.starts_with("/api/") => {
1310                let operation = path.trim_start_matches("/api/");
1311                run_request(
1312                    SurfaceRequest {
1313                        operation: OperationId::new(operation),
1314                        input: parse_json_or_empty(body),
1315                    },
1316                    metadata,
1317                    runner,
1318                )
1319            }
1320            _ => json_response(
1321                404,
1322                "Not Found",
1323                serde_json::json!({
1324                    "error": "not found",
1325                    "path": path
1326                }),
1327            ),
1328        }
1329    }
1330
1331    /// Builds the standard server adapter metadata payload.
1332    pub fn package_metadata_json(
1333        metadata: ServerAdapterMetadata,
1334        surface: PackageSurface,
1335    ) -> String {
1336        package_metadata_value(metadata, surface).to_string()
1337    }
1338
1339    fn package_metadata_value(
1340        metadata: ServerAdapterMetadata,
1341        surface: PackageSurface,
1342    ) -> serde_json::Value {
1343        serde_json::json!({
1344            "package": format!("{}-server", metadata.library_crate),
1345            "surface": metadata.surface_kind,
1346            "library": metadata.library_crate,
1347            "libraryImport": metadata.library_import,
1348            "cliPackage": metadata.cli_package,
1349            "appPackage": metadata.app_package,
1350            "wasmPackage": metadata.wasm_package,
1351            "endpoints": [
1352                "GET /health",
1353                "GET /api/package",
1354                "GET /api/schema",
1355                "GET /api/operations",
1356                "POST /api/run",
1357                "POST /api/<operation-id>"
1358            ],
1359            "runtimeMetadata": {
1360                "candleDevice": serde_json::Value::Null
1361            },
1362            "operations": surface.operations
1363        })
1364    }
1365
1366    fn schema_value(metadata: ServerAdapterMetadata, surface: PackageSurface) -> serde_json::Value {
1367        let operations = surface
1368            .operations
1369            .into_iter()
1370            .map(|operation| {
1371                let path = format!("/api/{}", operation.id.as_str());
1372                (
1373                    path,
1374                    serde_json::json!({
1375                        "post": {
1376                            "summary": operation.name,
1377                            "description": operation.description,
1378                            "requestBody": operation.input_schema,
1379                            "responses": {"200": operation.output_schema}
1380                        }
1381                    }),
1382                )
1383            })
1384            .collect::<serde_json::Map<_, _>>();
1385
1386        serde_json::json!({
1387            "openapi": "3.1.0",
1388            "info": {
1389                "title": format!("{} API", metadata.library_crate),
1390                "version": env!("CARGO_PKG_VERSION")
1391            },
1392            "paths": operations
1393        })
1394    }
1395
1396    fn run_response(
1397        body: &str,
1398        metadata: ServerAdapterMetadata,
1399        runner: fn(SurfaceRequest) -> Result<SurfaceResponse, String>,
1400    ) -> HttpResponse {
1401        let payload = match serde_json::from_str::<serde_json::Value>(body) {
1402            Ok(value) => value,
1403            Err(error) => {
1404                return diagnostic_response(
1405                    400,
1406                    "Bad Request",
1407                    "invalid_request",
1408                    &format!("invalid JSON: {error}"),
1409                    metadata,
1410                );
1411            }
1412        };
1413        let operation = payload
1414            .get("operation")
1415            .and_then(serde_json::Value::as_str)
1416            .unwrap_or("describe")
1417            .to_string();
1418        let input = payload
1419            .get("input")
1420            .cloned()
1421            .unwrap_or_else(|| payload.clone());
1422        run_request(
1423            SurfaceRequest {
1424                operation: OperationId::new(operation),
1425                input,
1426            },
1427            metadata,
1428            runner,
1429        )
1430    }
1431
1432    fn run_request(
1433        request: SurfaceRequest,
1434        metadata: ServerAdapterMetadata,
1435        runner: fn(SurfaceRequest) -> Result<SurfaceResponse, String>,
1436    ) -> HttpResponse {
1437        match runner(request) {
1438            Ok(response) => json_response(200, "OK", serde_json::json!(response)),
1439            Err(error) => {
1440                diagnostic_response(400, "Bad Request", "operation_failed", &error, metadata)
1441            }
1442        }
1443    }
1444
1445    fn diagnostic_response(
1446        status_code: u16,
1447        reason: &'static str,
1448        code: &str,
1449        message: &str,
1450        metadata: ServerAdapterMetadata,
1451    ) -> HttpResponse {
1452        let parsed = parse_surface_error(message);
1453        let diagnostic_code = parsed
1454            .as_ref()
1455            .map(|error| error.code.as_str())
1456            .unwrap_or(code);
1457        let diagnostic_message = parsed
1458            .as_ref()
1459            .map(|error| error.message.as_str())
1460            .unwrap_or(message);
1461        let details = parsed
1462            .as_ref()
1463            .map(|error| error.details.clone())
1464            .unwrap_or_else(|| serde_json::json!({}));
1465        json_response(
1466            status_code,
1467            reason,
1468            serde_json::json!({
1469                "diagnostics": [Diagnostic {
1470                    severity: DiagnosticSeverity::Error,
1471                    code: diagnostic_code.into(),
1472                    message: diagnostic_message.to_string(),
1473                    source: Some(format!("{}-server", metadata.library_crate)),
1474                    help: None,
1475                }],
1476                "error": {
1477                    "code": diagnostic_code,
1478                    "message": diagnostic_message,
1479                    "details": details
1480                }
1481            }),
1482        )
1483    }
1484
1485    fn parse_json_or_empty(body: &str) -> serde_json::Value {
1486        if body.trim().is_empty() {
1487            serde_json::json!({})
1488        } else {
1489            serde_json::from_str(body).unwrap_or_else(|_| serde_json::json!({"raw": body}))
1490        }
1491    }
1492
1493    fn handle_stream(
1494        mut stream: TcpStream,
1495        metadata: ServerAdapterMetadata,
1496        surface_provider: fn() -> PackageSurface,
1497        runner: fn(SurfaceRequest) -> Result<SurfaceResponse, String>,
1498    ) -> io::Result<()> {
1499        let mut reader = BufReader::new(stream.try_clone()?);
1500        let mut request_line = String::new();
1501        reader.read_line(&mut request_line)?;
1502
1503        let mut content_length = 0usize;
1504        loop {
1505            let mut header = String::new();
1506            reader.read_line(&mut header)?;
1507            let trimmed = header.trim_end();
1508            if trimmed.is_empty() {
1509                break;
1510            }
1511            if let Some((name, value)) = trimmed.split_once(':') {
1512                if name.eq_ignore_ascii_case("content-length") {
1513                    content_length = value.trim().parse().unwrap_or(0);
1514                }
1515            }
1516        }
1517
1518        let mut body = vec![0; content_length];
1519        if content_length > 0 {
1520            reader.read_exact(&mut body)?;
1521        }
1522        let body = String::from_utf8_lossy(&body);
1523
1524        let mut parts = request_line.split_whitespace();
1525        let method = parts.next().unwrap_or("GET");
1526        let path = parts.next().unwrap_or("/");
1527        let response = response_for(method, path, &body, metadata, surface_provider, runner);
1528        write_response(&mut stream, response)
1529    }
1530
1531    fn json_response(
1532        status_code: u16,
1533        reason: &'static str,
1534        value: serde_json::Value,
1535    ) -> HttpResponse {
1536        HttpResponse {
1537            status_code,
1538            reason,
1539            content_type: "application/json",
1540            body: value.to_string(),
1541        }
1542    }
1543
1544    fn write_response(stream: &mut TcpStream, response: HttpResponse) -> io::Result<()> {
1545        write!(
1546            stream,
1547            "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{}",
1548            response.status_code,
1549            response.reason,
1550            response.content_type,
1551            response.body.len(),
1552            response.body
1553        )
1554    }
1555}
1556
1557#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
1558#[serde(transparent)]
1559pub struct JobId(pub String);
1560
1561impl JobId {
1562    pub fn new(value: impl Into<String>) -> Self {
1563        Self(value.into())
1564    }
1565
1566    pub fn as_str(&self) -> &str {
1567        &self.0
1568    }
1569}
1570
1571impl From<&str> for JobId {
1572    fn from(value: &str) -> Self {
1573        Self(value.to_string())
1574    }
1575}
1576
1577impl From<String> for JobId {
1578    fn from(value: String) -> Self {
1579        Self(value)
1580    }
1581}
1582
1583#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
1584#[serde(transparent)]
1585pub struct ArtifactId(pub String);
1586
1587impl ArtifactId {
1588    pub fn new(value: impl Into<String>) -> Self {
1589        Self(value.into())
1590    }
1591
1592    pub fn as_str(&self) -> &str {
1593        &self.0
1594    }
1595}
1596
1597impl From<&str> for ArtifactId {
1598    fn from(value: &str) -> Self {
1599        Self(value.to_string())
1600    }
1601}
1602
1603impl From<String> for ArtifactId {
1604    fn from(value: String) -> Self {
1605        Self(value)
1606    }
1607}
1608
1609#[cfg(test)]
1610mod tests {
1611    use super::*;
1612
1613    #[test]
1614    fn diagnostic_uses_camel_case_json() {
1615        let diagnostic = Diagnostic::new(DiagnosticSeverity::Warning, "demo.warning", "check");
1616        let json = serde_json::to_string(&diagnostic).expect("serialize diagnostic");
1617
1618        assert!(json.contains("\"severity\":\"warning\""));
1619        assert!(json.contains("\"code\":\"demo.warning\""));
1620    }
1621
1622    #[test]
1623    fn pure_rust_capabilities_allow_wasm_and_server() {
1624        let capabilities = RuntimeCapabilities::pure_rust();
1625
1626        assert!(capabilities.native);
1627        assert!(capabilities.server);
1628        assert!(capabilities.wasm);
1629        assert_eq!(capabilities.mobile, MobileCapability::Wasm);
1630    }
1631
1632    #[test]
1633    fn capability_builders_preserve_pure_rust_defaults() {
1634        let capabilities = RuntimeCapabilities::pure_rust()
1635            .with_max_recommended_input_bytes(1024)
1636            .with_requirement("fixture", "test fixture input", false);
1637
1638        assert!(capabilities.native);
1639        assert!(capabilities.server);
1640        assert!(capabilities.wasm);
1641        assert_eq!(capabilities.max_recommended_input_bytes, Some(1024));
1642        assert_eq!(capabilities.requirements[0].name, "fixture");
1643        assert!(!capabilities.requirements[0].required);
1644    }
1645
1646    #[test]
1647    fn package_surface_uses_camel_case_json() {
1648        let surface = PackageSurface {
1649            library: "demo-core".to_string(),
1650            version: "0.1.0".to_string(),
1651            capabilities: RuntimeCapabilities::pure_rust(),
1652            operations: vec![SurfaceOperation {
1653                id: OperationId::new("describe"),
1654                name: "Describe".to_string(),
1655                description: Some("Describe package surface".to_string()),
1656                curation: SurfaceOperationCuration::from_operation_id("describe"),
1657                input_schema: serde_json::json!({"type": "object"}),
1658                output_schema: serde_json::json!({"type": "object"}),
1659                example_request: serde_json::json!({}),
1660                wasm_supported: true,
1661                server_supported: true,
1662            }],
1663        };
1664
1665        let json = serde_json::to_string(&surface).expect("serialize surface");
1666
1667        assert!(json.contains("\"inputSchema\""));
1668        assert!(json.contains("\"curation\""));
1669        assert!(json.contains("\"exampleRequest\""));
1670        assert!(json.contains("\"wasmSupported\":true"));
1671    }
1672
1673    #[test]
1674    fn surface_operation_curation_serializes_and_deserializes() {
1675        let operation = surface_operation(
1676            "demo.run",
1677            "Run demo",
1678            "Run a demo workflow",
1679            serde_json::json!({"text": "hello"}),
1680        );
1681
1682        let value = serde_json::to_value(&operation).expect("serialize operation");
1683        assert_eq!(value["curation"]["role"], "workflow");
1684        assert_eq!(value["curation"]["primary"], false);
1685        assert_eq!(value["curation"]["sortOrder"], 100);
1686
1687        let round_trip: SurfaceOperation =
1688            serde_json::from_value(value).expect("deserialize operation");
1689        assert_eq!(round_trip.curation, SurfaceOperationCuration::workflow(100));
1690    }
1691
1692    #[test]
1693    fn surface_operation_deserializes_without_curation() {
1694        let json = serde_json::json!({
1695            "id": "describe",
1696            "name": "Describe",
1697            "description": "Describe package surface",
1698            "inputSchema": {"type": "object", "xOperationCategory": "debug"},
1699            "outputSchema": {"type": "object", "xOperationCategory": "debug"},
1700            "exampleRequest": {},
1701            "wasmSupported": true,
1702            "serverSupported": true
1703        });
1704
1705        let operation: SurfaceOperation =
1706            serde_json::from_value(json).expect("deserialize old operation JSON");
1707
1708        assert_eq!(operation.id.as_str(), "describe");
1709        assert_eq!(operation.curation, SurfaceOperationCuration::default());
1710    }
1711
1712    #[test]
1713    fn set_surface_operation_curation_syncs_legacy_schema_category() {
1714        let mut operation = surface_operation(
1715            "demo.inspect",
1716            "Inspect demo",
1717            "Inspect demo inputs",
1718            serde_json::json!({}),
1719        );
1720
1721        set_surface_operation_curation(&mut operation, SurfaceOperationCuration::debug(750));
1722
1723        assert_eq!(operation.curation, SurfaceOperationCuration::debug(750));
1724        assert_eq!(operation.input_schema["xOperationCategory"], "debug");
1725        assert_eq!(operation.output_schema["xOperationCategory"], "debug");
1726    }
1727
1728    #[test]
1729    fn surface_helpers_preserve_standard_response_shape() {
1730        let surface = PackageSurface {
1731            library: "demo".to_string(),
1732            version: "0.1.0".to_string(),
1733            capabilities: RuntimeCapabilities::pure_rust(),
1734            operations: vec![surface_operation(
1735                "describe",
1736                "Describe",
1737                "Describe demo package",
1738                serde_json::json!({"includeOperations": true}),
1739            )],
1740        };
1741        let response = describe_surface_response(
1742            &surface,
1743            SurfaceRequest {
1744                operation: OperationId::new("describe"),
1745                input: serde_json::json!({"includeOperations": true}),
1746            },
1747        );
1748
1749        assert_eq!(response.operation.as_str(), "describe");
1750        assert_eq!(response.value["library"], "demo");
1751        assert_eq!(response.value["operationCount"], 1);
1752        assert_eq!(response.diagnostics, Vec::new());
1753        assert_eq!(response.artifacts, Vec::<serde_json::Value>::new());
1754    }
1755
1756    #[test]
1757    fn surface_operation_declares_release_contract_schema() {
1758        let operation = surface_operation(
1759            "demo.run",
1760            "Run demo",
1761            "Run a demo workflow",
1762            serde_json::json!({"text": "hello", "topK": 3}),
1763        );
1764
1765        assert_eq!(operation.input_schema["additionalProperties"], false);
1766        assert_eq!(operation.input_schema["xOperationCategory"], "workflow");
1767        assert_eq!(operation.input_schema["xReleaseStability"], "stable");
1768        assert_eq!(
1769            operation.input_schema["required"],
1770            serde_json::json!(["text"])
1771        );
1772        assert_eq!(operation.input_schema["properties"]["topK"]["minimum"], 1);
1773        assert_eq!(operation.output_schema["required"][0], "operation");
1774    }
1775
1776    #[test]
1777    fn surface_operation_schema_extension_updates_both_schemas() {
1778        let mut operation = surface_operation(
1779            "demo.compat",
1780            "Run compatibility demo",
1781            "Run a compatibility helper",
1782            serde_json::json!({"text": "hello"}),
1783        );
1784
1785        add_surface_operation_schema_extension(
1786            &mut operation,
1787            "xReplacementOperation",
1788            serde_json::json!("demo.run"),
1789        );
1790
1791        assert_eq!(operation.input_schema["xReplacementOperation"], "demo.run");
1792        assert_eq!(operation.output_schema["xReplacementOperation"], "demo.run");
1793    }
1794
1795    #[test]
1796    fn typed_surface_errors_roundtrip_for_transport_adapters() {
1797        let error = SurfaceError::unsupported_operation("demo.missing", "demo-package");
1798        let serialized = error.to_error_string();
1799        let parsed = parse_surface_error(&serialized).expect("typed surface error");
1800
1801        assert_eq!(parsed.code, "unsupported_operation");
1802        assert_eq!(parsed.operation.unwrap().as_str(), "demo.missing");
1803        assert!(parsed.message.contains("unsupported operation"));
1804    }
1805
1806    #[test]
1807    fn surface_error_is_standard_error_type() {
1808        let error = SurfaceError::invalid_request(Some("demo.run"), "invalid request: missing id");
1809        let boxed: Box<dyn std::error::Error> = Box::new(error.clone());
1810
1811        assert_eq!(boxed.to_string(), "invalid request: missing id");
1812        assert_eq!(error.to_string(), "invalid request: missing id");
1813    }
1814
1815    #[test]
1816    fn validation_helpers_return_typed_errors() {
1817        let limit = validate_max_items("demo.run", "values", 3, 2).expect_err("limit error");
1818        let parsed = parse_surface_error(&limit).expect("typed resource error");
1819        assert_eq!(parsed.code, "resource_limit");
1820        assert_eq!(parsed.details["field"], "values");
1821        assert_eq!(parsed.details["actual"], 3);
1822
1823        let length =
1824            validate_matching_lengths("demo.run", "left", 2, "right", 3).expect_err("length");
1825        let parsed = parse_surface_error(&length).expect("typed length error");
1826        assert_eq!(parsed.code, "invalid_request");
1827        assert!(parsed.message.contains("left"));
1828        assert!(parsed.message.contains("right"));
1829    }
1830
1831    #[test]
1832    fn execution_plan_serializes_to_schema_extension() {
1833        let plan = SurfaceExecutionPlan {
1834            operation: OperationId::new("demo.run"),
1835            mode: SurfaceExecutionMode::PlannedJob,
1836            side_effects: vec![SurfaceSideEffect::None],
1837            cancellable: true,
1838            progress_unit: Some("items".to_string()),
1839            expected_artifacts: vec![SurfaceArtifactExpectation {
1840                id: "report".to_string(),
1841                kind: "json".to_string(),
1842                media_type: "application/json".to_string(),
1843                required: true,
1844                description: Some("Structured report".to_string()),
1845            }],
1846            requirements: vec![RuntimeRequirement {
1847                name: "runtime-core".to_string(),
1848                description: Some("Pure Rust planner".to_string()),
1849                required: true,
1850            }],
1851            max_recommended_input_bytes: Some(1024),
1852        };
1853
1854        let operation = surface_operation_with_execution_plan(
1855            "demo.run",
1856            "Run demo",
1857            "Build a deterministic demo plan",
1858            serde_json::json!({"items": [1]}),
1859            plan,
1860        );
1861
1862        assert_eq!(
1863            operation.input_schema["xExecutionPlan"]["mode"],
1864            serde_json::json!("plannedJob")
1865        );
1866        assert_eq!(
1867            operation.output_schema["xExecutionPlan"]["expectedArtifacts"][0]["id"],
1868            serde_json::json!("report")
1869        );
1870        assert_eq!(
1871            operation.input_schema["xExecutionPlan"]["sideEffects"],
1872            serde_json::json!(["none"])
1873        );
1874    }
1875
1876    #[test]
1877    fn runtime_context_serializes_and_tracks_no_side_effect_defaults() {
1878        let context = SurfaceRuntimeContext::no_side_effects(SurfaceRuntimeKind::Wasm);
1879        assert!(!context.side_effects.allow_reads);
1880        assert!(!context.side_effects.allow_writes);
1881        assert!(!context.side_effects.allow_network);
1882        assert_eq!(
1883            context.model.preference,
1884            SurfaceModelExecutionPreference::PlanOnly
1885        );
1886        assert!(!context.allows_model_auto_setup());
1887
1888        let value = serde_json::to_value(&context).expect("serialize context");
1889        assert_eq!(value["runtime"], "wasm");
1890        assert_eq!(value["sideEffects"]["allowWrites"], false);
1891        assert_eq!(value["storage"]["modelRoot"], serde_json::Value::Null);
1892
1893        let round_trip: SurfaceRuntimeContext =
1894            serde_json::from_value(value).expect("deserialize context");
1895        assert_eq!(round_trip, context);
1896    }
1897
1898    #[test]
1899    fn primary_workflow_operation_attaches_strict_metadata_and_lower_contracts() {
1900        let operation = primary_workflow_operation(
1901            "demo.workflow",
1902            "Run workflow",
1903            "Runs the primary workflow.",
1904            serde_json::json!({"input": "value"}),
1905            None,
1906            &[
1907                "moritzbrantner-runtime-core",
1908                "moritzbrantner-video-analysis-core",
1909            ],
1910        );
1911
1912        assert_eq!(
1913            operation.input_schema["additionalProperties"],
1914            serde_json::json!(false)
1915        );
1916        assert_eq!(operation.input_schema["xOperationCategory"], "workflow");
1917        assert_eq!(operation.input_schema["xReleaseStability"], "stable");
1918        assert_eq!(operation.input_schema["xContractPolicy"], "additiveOnly");
1919        assert_eq!(
1920            operation.input_schema["xErrorShape"]["code"],
1921            serde_json::json!("string")
1922        );
1923        assert_eq!(
1924            operation.input_schema["xLowerContractProof"]["crates"][1],
1925            "moritzbrantner-video-analysis-core"
1926        );
1927    }
1928
1929    #[test]
1930    fn landscape_contract_serializes_to_schema_extension() {
1931        let contract = landscape::LandscapeOperationContract::new(
1932            landscape::LandscapeFunction::new("demo.curated", "moritzbrantner-runtime-core")
1933                .input(landscape::LandscapePort::new(
1934                    "request",
1935                    landscape::well_known::runtime_surface_request(),
1936                ))
1937                .output(landscape::LandscapePort::new(
1938                    "response",
1939                    landscape::well_known::runtime_surface_response(),
1940                )),
1941        );
1942
1943        let operation = surface_operation_with_landscape(
1944            "demo.run",
1945            "Run demo",
1946            "Run a curated demo workflow",
1947            serde_json::json!({"request": {"operation": "demo.run", "input": {}}}),
1948            contract,
1949        );
1950
1951        assert_eq!(operation.id.as_str(), "demo.run");
1952        assert_eq!(
1953            operation.example_request["request"]["operation"],
1954            "demo.run"
1955        );
1956        assert!(operation.wasm_supported);
1957        assert!(operation.server_supported);
1958        assert_eq!(
1959            operation.input_schema["xLandscape"]["function"]["id"],
1960            serde_json::json!("demo.curated")
1961        );
1962        assert_eq!(
1963            operation.input_schema["xLandscape"]["function"]["inputs"][0]["typeRef"]["id"],
1964            serde_json::json!("runtime.surfaceRequest")
1965        );
1966        assert_eq!(
1967            operation.output_schema["xLandscape"]["function"]["outputs"][0]["typeRef"]["rustType"],
1968            serde_json::json!("runtime_core::SurfaceResponse")
1969        );
1970    }
1971
1972    #[test]
1973    fn landscape_contract_value_uses_camel_case_json() {
1974        let contract = landscape::LandscapeOperationContract::new(
1975            landscape::LandscapeFunction::new("demo.curated", "moritzbrantner-runtime-core")
1976                .input(
1977                    landscape::LandscapePort::new(
1978                        "optionalRequest",
1979                        landscape::well_known::runtime_surface_request(),
1980                    )
1981                    .optional(),
1982                )
1983                .output(
1984                    landscape::LandscapePort::new(
1985                        "responses",
1986                        landscape::well_known::runtime_surface_response(),
1987                    )
1988                    .many(),
1989                ),
1990        );
1991        let value = landscape_operation_contract_value(&contract);
1992
1993        assert_eq!(value["function"]["stability"], "stable");
1994        assert_eq!(
1995            value["function"]["inputs"][0]["typeRef"]["schemaRef"],
1996            serde_json::Value::Null
1997        );
1998        assert_eq!(value["function"]["inputs"][0]["required"], false);
1999        assert_eq!(value["function"]["inputs"][0]["cardinality"], "optional");
2000        assert_eq!(value["function"]["outputs"][0]["cardinality"], "many");
2001    }
2002
2003    #[test]
2004    fn new_surface_error_constructors_are_typed_json() {
2005        let cancelled = SurfaceError::cancelled("demo.run", "cancelled by request");
2006        assert_eq!(cancelled.code, "cancelled");
2007        assert_eq!(cancelled.operation.unwrap().as_str(), "demo.run");
2008
2009        let execution = SurfaceError::execution_failed(
2010            "demo.run",
2011            "execution failed",
2012            serde_json::json!({
2013                "stage": "prepare"
2014            }),
2015        );
2016        assert_eq!(execution.code, "execution_failed");
2017        assert_eq!(execution.details["stage"], "prepare");
2018
2019        let artifact = SurfaceError::artifact_error(
2020            "demo.run",
2021            "artifact invalid",
2022            serde_json::json!({
2023                "artifact": "report"
2024            }),
2025        );
2026        assert_eq!(artifact.code, "artifact_error");
2027        assert_eq!(artifact.details["artifact"], "report");
2028    }
2029
2030    #[test]
2031    fn structured_operation_response_preserves_result_fields() {
2032        let surface = PackageSurface {
2033            library: "demo".to_string(),
2034            version: "0.1.0".to_string(),
2035            capabilities: RuntimeCapabilities::pure_rust(),
2036            operations: vec![surface_operation(
2037                "demo.run",
2038                "Run demo",
2039                "Run a demo workflow",
2040                serde_json::json!({"values": [1, 2]}),
2041            )],
2042        };
2043        let response = structured_operation_response(
2044            &surface,
2045            OperationId::new("demo.run"),
2046            serde_json::json!({"count": 2, "values": [1, 2]}),
2047        );
2048
2049        assert_eq!(response.value["count"], 2);
2050        assert_eq!(response.value["operation"], "demo.run");
2051        assert_eq!(response.value["title"], "Run demo");
2052        assert_eq!(response.value["summary"]["count"], 2);
2053        assert_eq!(
2054            response.value["result"]["values"],
2055            serde_json::json!([1, 2])
2056        );
2057    }
2058}