Skip to main content

presolve_compiler/
semantic_package.rs

1use std::collections::BTreeMap;
2
3use serde::{Deserialize, Serialize};
4
5pub const SEMANTIC_PACKAGE_CONTRACT_SCHEMA_VERSION: u32 = 1;
6
7#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
8#[serde(rename_all = "snake_case")]
9pub enum SemanticPackageKind {
10    Pure,
11    Capability,
12    Resource,
13    Codec,
14    Component,
15    Opaque,
16    ServerAction,
17}
18
19/// A closed compiler-lowered operation that a `pure` package export may declare.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
21#[serde(rename_all = "snake_case")]
22pub enum SemanticPackagePureOperation {
23    Identity,
24}
25
26/// Execution side declared by a third-party Resource endpoint.
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
28#[serde(rename_all = "snake_case")]
29pub enum SemanticPackageResourceExecutionBoundary {
30    Client,
31    Server,
32    Shared,
33}
34
35/// How the generated Resource activation may cancel an endpoint invocation.
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
37#[serde(rename_all = "snake_case")]
38pub enum SemanticPackageResourceCancellation {
39    Abort,
40}
41
42/// How an endpoint's completed result participates in a resumed application.
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
44#[serde(rename_all = "snake_case")]
45pub enum SemanticPackageResourceResumePolicy {
46    Reload,
47    Snapshot,
48}
49
50/// Closed request input admitted for a server-backed route loader.
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
52#[serde(rename_all = "snake_case")]
53pub enum SemanticPackageRouteLoaderInput {
54    RouteParameters,
55}
56
57/// Cache visibility declared by an integrity-bound server capability.
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
59#[serde(rename_all = "snake_case")]
60pub enum SemanticPackageServerCacheScope {
61    NoStore,
62    Private,
63    Public,
64}
65
66/// Immutable cache policy a server adapter may honor but never broaden.
67#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
68#[serde(deny_unknown_fields)]
69pub struct SemanticPackageServerCachePolicy {
70    pub scope: SemanticPackageServerCacheScope,
71    #[serde(default)]
72    pub max_age_seconds: Option<u64>,
73}
74
75/// Closed error transport selected by an integrity-bound route loader.
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
77#[serde(rename_all = "snake_case")]
78pub enum SemanticPackageRouteLoaderFailure {
79    Typed,
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
83#[serde(rename_all = "snake_case")]
84pub enum SemanticPackageServerActionInput {
85    FormData,
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
89#[serde(rename_all = "snake_case")]
90pub enum SemanticPackageServerActionResponse {
91    Json,
92    Redirect,
93}
94
95#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
96#[serde(deny_unknown_fields)]
97pub struct SemanticPackageServerAction {
98    pub input: SemanticPackageServerActionInput,
99    pub response: SemanticPackageServerActionResponse,
100    pub failure: SemanticPackageRouteLoaderFailure,
101}
102
103/// Execution side for the initial opaque terminal package boundary.
104#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
105#[serde(rename_all = "snake_case")]
106pub enum SemanticPackageOpaqueExecutionBoundary {
107    Client,
108}
109
110/// Resume behavior for an opaque terminal package activation.
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
112#[serde(rename_all = "snake_case")]
113pub enum SemanticPackageOpaqueResumePolicy {
114    ColdFallback,
115}
116
117/// Closed contract for a no-input, no-output opaque terminal export.
118///
119/// This records how the application may use a package without giving the
120/// compiler authority to inspect its implementation.
121#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
122#[serde(deny_unknown_fields)]
123pub struct SemanticPackageOpaqueTerminal {
124    pub execution_boundary: SemanticPackageOpaqueExecutionBoundary,
125    pub resume: SemanticPackageOpaqueResumePolicy,
126}
127
128/// Closed semantic contract for an executable Resource package export.
129///
130/// The compiler still does not execute or inspect package implementation. This
131/// metadata is the prerequisite for later Resource source lowering.
132#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
133#[serde(deny_unknown_fields)]
134pub struct SemanticPackageResourceEndpoint {
135    pub execution_boundary: SemanticPackageResourceExecutionBoundary,
136    pub cancellation: SemanticPackageResourceCancellation,
137    pub resume: SemanticPackageResourceResumePolicy,
138}
139
140/// Additional contract required before a Resource endpoint may serve as a
141/// route loader. The package implementation remains opaque to the compiler.
142#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
143#[serde(deny_unknown_fields)]
144pub struct SemanticPackageRouteLoader {
145    pub input: SemanticPackageRouteLoaderInput,
146    pub cache: SemanticPackageServerCachePolicy,
147    pub failure: SemanticPackageRouteLoaderFailure,
148}
149
150#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
151#[serde(deny_unknown_fields)]
152pub struct SemanticPackageExport {
153    pub kind: SemanticPackageKind,
154    pub type_signature: String,
155    pub runtime_module: String,
156    pub resume_policy: String,
157    #[serde(default)]
158    pub pure_operation: Option<SemanticPackagePureOperation>,
159    #[serde(default)]
160    pub resource_endpoint: Option<SemanticPackageResourceEndpoint>,
161    #[serde(default)]
162    pub route_loader: Option<SemanticPackageRouteLoader>,
163    #[serde(default)]
164    pub server_action: Option<SemanticPackageServerAction>,
165    #[serde(default)]
166    pub opaque_terminal: Option<SemanticPackageOpaqueTerminal>,
167}
168
169#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
170#[serde(deny_unknown_fields)]
171pub struct SemanticPackageContract {
172    pub schema_version: u32,
173    pub package: String,
174    pub version: String,
175    pub integrity: String,
176    pub exports: BTreeMap<String, SemanticPackageExport>,
177}
178
179#[derive(Debug, Clone, PartialEq, Eq)]
180pub enum SemanticPackageContractError {
181    InvalidJson,
182    UnsupportedSchema,
183    InvalidIntegrity,
184    EmptyPackage,
185    EmptyVersion,
186    EmptyExport,
187    InvalidExportContract,
188    InvalidPureOperation,
189    InvalidResourceEndpoint,
190    InvalidRouteLoader,
191    InvalidServerAction,
192    InvalidOpaqueTerminal,
193    DuplicateSpecifier,
194}
195
196pub fn parse_semantic_package_contract(
197    json: &str,
198) -> Result<SemanticPackageContract, SemanticPackageContractError> {
199    let contract = serde_json::from_str::<SemanticPackageContract>(json)
200        .map_err(|_| SemanticPackageContractError::InvalidJson)?;
201    if contract.schema_version != SEMANTIC_PACKAGE_CONTRACT_SCHEMA_VERSION {
202        return Err(SemanticPackageContractError::UnsupportedSchema);
203    }
204    if contract.package.is_empty() {
205        return Err(SemanticPackageContractError::EmptyPackage);
206    }
207    if contract.version.is_empty() {
208        return Err(SemanticPackageContractError::EmptyVersion);
209    }
210    if !contract.integrity.starts_with("sha256:")
211        || contract.integrity.len() != 71
212        || !contract.integrity[7..]
213            .chars()
214            .all(|c| c.is_ascii_hexdigit())
215    {
216        return Err(SemanticPackageContractError::InvalidIntegrity);
217    }
218    if contract.exports.is_empty() || contract.exports.keys().any(|name| name.is_empty()) {
219        return Err(SemanticPackageContractError::EmptyExport);
220    }
221    if contract.exports.values().any(|export| {
222        export.type_signature.is_empty()
223            || export.runtime_module.is_empty()
224            || export.resume_policy.is_empty()
225    }) {
226        return Err(SemanticPackageContractError::InvalidExportContract);
227    }
228    if contract
229        .exports
230        .values()
231        .any(|export| export.pure_operation.is_some() && export.kind != SemanticPackageKind::Pure)
232    {
233        return Err(SemanticPackageContractError::InvalidPureOperation);
234    }
235    if contract.exports.values().any(|export| {
236        (export.kind == SemanticPackageKind::Resource) != export.resource_endpoint.is_some()
237    }) {
238        return Err(SemanticPackageContractError::InvalidResourceEndpoint);
239    }
240    if contract.exports.values().any(|export| {
241        let Some(loader) = &export.route_loader else {
242            return false;
243        };
244        let Some(endpoint) = &export.resource_endpoint else {
245            return true;
246        };
247        !matches!(export.kind, SemanticPackageKind::Resource)
248            || !matches!(
249                endpoint.execution_boundary,
250                SemanticPackageResourceExecutionBoundary::Server
251                    | SemanticPackageResourceExecutionBoundary::Shared
252            )
253            || match loader.cache.scope {
254                SemanticPackageServerCacheScope::Public => {
255                    loader.cache.max_age_seconds.is_none_or(|age| age == 0)
256                }
257                SemanticPackageServerCacheScope::NoStore
258                | SemanticPackageServerCacheScope::Private => {
259                    loader.cache.max_age_seconds.is_some()
260                }
261            }
262    }) {
263        return Err(SemanticPackageContractError::InvalidRouteLoader);
264    }
265    if contract.exports.values().any(|export| {
266        (export.kind == SemanticPackageKind::ServerAction) != export.server_action.is_some()
267            || (export.kind == SemanticPackageKind::ServerAction
268                && (export.type_signature != "FormData -> ServerActionResult"
269                    || export.resume_policy != "cold_fallback"))
270    }) {
271        return Err(SemanticPackageContractError::InvalidServerAction);
272    }
273    if contract.exports.values().any(|export| {
274        (export.kind == SemanticPackageKind::Opaque) != export.opaque_terminal.is_some()
275            || (export.kind == SemanticPackageKind::Opaque
276                && (export.type_signature != "() -> void"
277                    || export.resume_policy != "cold_fallback"))
278    }) {
279        return Err(SemanticPackageContractError::InvalidOpaqueTerminal);
280    }
281    Ok(contract)
282}
283
284#[derive(Debug, Clone, PartialEq, Eq, Default)]
285pub struct SemanticPackageResolutionTable {
286    contracts: BTreeMap<String, SemanticPackageContract>,
287}
288
289impl SemanticPackageResolutionTable {
290    pub fn insert(
291        &mut self,
292        specifier: String,
293        contract: SemanticPackageContract,
294    ) -> Result<(), SemanticPackageContractError> {
295        if self.contracts.contains_key(&specifier) {
296            return Err(SemanticPackageContractError::DuplicateSpecifier);
297        }
298        self.contracts.insert(specifier, contract);
299        Ok(())
300    }
301
302    #[must_use]
303    pub fn contract(&self, specifier: &str) -> Option<&SemanticPackageContract> {
304        self.contracts.get(specifier)
305    }
306    #[must_use]
307    pub fn resolve(&self, specifier: &str, export: &str) -> Option<&SemanticPackageExport> {
308        self.contracts.get(specifier)?.exports.get(export)
309    }
310}
311
312#[cfg(test)]
313mod tests {
314    use super::*;
315    #[test]
316    fn parses_integrity_checked_contract_and_resolves_an_export() {
317        let contract = parse_semantic_package_contract(r#"{"schema_version":1,"package":"date-kit","version":"1.2.3","integrity":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","exports":{"format":{"kind":"pure","type_signature":"(Date) -> string","runtime_module":"dist/format.js","resume_policy":"input_only"}}}"#).unwrap();
318        let mut table = SemanticPackageResolutionTable::default();
319        table.insert("date-kit".into(), contract).unwrap();
320        assert_eq!(
321            table.resolve("date-kit", "format").unwrap().kind,
322            SemanticPackageKind::Pure
323        );
324    }
325
326    #[test]
327    fn rejects_incomplete_contracts_without_replacing_existing_resolution() {
328        let valid = parse_semantic_package_contract(r#"{"schema_version":1,"package":"date-kit","version":"1.2.3","integrity":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","exports":{"format":{"kind":"pure","type_signature":"(Date) -> string","runtime_module":"dist/format.js","resume_policy":"input_only"}}}"#).unwrap();
329        let invalid = parse_semantic_package_contract(
330            r#"{"schema_version":1,"package":"date-kit","version":"","integrity":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","exports":{"format":{"kind":"pure","type_signature":"","runtime_module":"","resume_policy":""}}}"#,
331        );
332        assert_eq!(invalid, Err(SemanticPackageContractError::EmptyVersion));
333
334        let mut table = SemanticPackageResolutionTable::default();
335        table.insert("date-kit".into(), valid.clone()).unwrap();
336        assert_eq!(
337            table.insert("date-kit".into(), valid),
338            Err(SemanticPackageContractError::DuplicateSpecifier)
339        );
340        assert_eq!(table.contract("date-kit").unwrap().version, "1.2.3");
341    }
342
343    #[test]
344    fn resource_exports_require_a_closed_endpoint_contract() {
345        let missing_endpoint = parse_semantic_package_contract(
346            r#"{"schema_version":1,"package":"profile-service","version":"1.2.3","integrity":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","exports":{"loadProfile":{"kind":"resource","type_signature":"(ProfileKey) -> Resource<Profile, ProfileError>","runtime_module":"dist/load-profile.js","resume_policy":"snapshot"}}}"#,
347        );
348        assert_eq!(
349            missing_endpoint,
350            Err(SemanticPackageContractError::InvalidResourceEndpoint)
351        );
352
353        let contract = parse_semantic_package_contract(
354            r#"{"schema_version":1,"package":"profile-service","version":"1.2.3","integrity":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","exports":{"loadProfile":{"kind":"resource","type_signature":"(ProfileKey) -> Resource<Profile, ProfileError>","runtime_module":"dist/load-profile.js","resume_policy":"snapshot","resource_endpoint":{"execution_boundary":"shared","cancellation":"abort","resume":"snapshot"}}}}"#,
355        )
356        .expect("resource endpoint contract");
357        assert_eq!(
358            contract.exports["loadProfile"].resource_endpoint,
359            Some(SemanticPackageResourceEndpoint {
360                execution_boundary: SemanticPackageResourceExecutionBoundary::Shared,
361                cancellation: SemanticPackageResourceCancellation::Abort,
362                resume: SemanticPackageResourceResumePolicy::Snapshot,
363            })
364        );
365    }
366
367    #[test]
368    fn opaque_exports_require_the_closed_terminal_contract() {
369        let missing_terminal = parse_semantic_package_contract(
370            r#"{"schema_version":1,"package":"@acme/analytics","version":"1.2.3","integrity":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","exports":{"trackPurchase":{"kind":"opaque","type_signature":"() -> void","runtime_module":"dist/track.js","resume_policy":"cold_fallback"}}}"#,
371        );
372        assert_eq!(
373            missing_terminal,
374            Err(SemanticPackageContractError::InvalidOpaqueTerminal)
375        );
376
377        let contract = parse_semantic_package_contract(
378            r#"{"schema_version":1,"package":"@acme/analytics","version":"1.2.3","integrity":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","exports":{"trackPurchase":{"kind":"opaque","type_signature":"() -> void","runtime_module":"dist/track.js","resume_policy":"cold_fallback","opaque_terminal":{"execution_boundary":"client","resume":"cold_fallback"}}}}"#,
379        )
380        .expect("opaque terminal contract");
381        assert_eq!(
382            contract.exports["trackPurchase"].opaque_terminal,
383            Some(SemanticPackageOpaqueTerminal {
384                execution_boundary: SemanticPackageOpaqueExecutionBoundary::Client,
385                resume: SemanticPackageOpaqueResumePolicy::ColdFallback,
386            })
387        );
388    }
389
390    #[test]
391    fn validates_route_loader_capabilities_on_server_or_shared_resource_exports() {
392        let contract = parse_semantic_package_contract(
393            r#"{"schema_version":1,"package":"post-service","version":"1.2.3","integrity":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","exports":{"loadPost":{"kind":"resource","type_signature":"RouteParameters -> Resource<Post, NotFound>","runtime_module":"dist/load-post.js","resume_policy":"reload","resource_endpoint":{"execution_boundary":"server","cancellation":"abort","resume":"reload"},"route_loader":{"input":"route_parameters","cache":{"scope":"public","max_age_seconds":60},"failure":"typed"}}}}"#,
394        )
395        .expect("closed route loader contract");
396        assert_eq!(
397            contract.exports["loadPost"].route_loader,
398            Some(SemanticPackageRouteLoader {
399                input: SemanticPackageRouteLoaderInput::RouteParameters,
400                cache: SemanticPackageServerCachePolicy {
401                    scope: SemanticPackageServerCacheScope::Public,
402                    max_age_seconds: Some(60),
403                },
404                failure: SemanticPackageRouteLoaderFailure::Typed,
405            })
406        );
407
408        let invalid_client = parse_semantic_package_contract(
409            r#"{"schema_version":1,"package":"post-service","version":"1.2.3","integrity":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","exports":{"loadPost":{"kind":"resource","type_signature":"RouteParameters -> Resource<Post, NotFound>","runtime_module":"dist/load-post.js","resume_policy":"reload","resource_endpoint":{"execution_boundary":"client","cancellation":"abort","resume":"reload"},"route_loader":{"input":"route_parameters","cache":{"scope":"no_store"},"failure":"typed"}}}}"#,
410        );
411        assert_eq!(
412            invalid_client,
413            Err(SemanticPackageContractError::InvalidRouteLoader)
414        );
415    }
416
417    #[test]
418    fn validates_closed_server_action_capabilities() {
419        let contract = parse_semantic_package_contract(
420            r#"{"schema_version":1,"package":"post-service","version":"1.2.3","integrity":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","exports":{"savePost":{"kind":"server_action","type_signature":"FormData -> ServerActionResult","runtime_module":"dist/save-post.js","resume_policy":"cold_fallback","server_action":{"input":"form_data","response":"json","failure":"typed"}}}}"#,
421        );
422        assert!(contract.is_ok());
423    }
424}