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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
104#[serde(rename_all = "snake_case")]
105pub enum SemanticPackageFormSubmissionExecutionBoundary {
106    Client,
107}
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
110#[serde(rename_all = "snake_case")]
111pub enum SemanticPackageFormSubmissionCancellation {
112    Abort,
113}
114
115#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
116#[serde(rename_all = "snake_case")]
117pub enum SemanticPackageFormSubmissionInput {
118    FormValue,
119}
120
121#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
122#[serde(rename_all = "snake_case")]
123pub enum SemanticPackageFormSubmissionResult {
124    Void,
125}
126
127/// Closed contract for an asynchronous client Form submission capability.
128///
129/// The compiler does not inspect package source. It supplies the canonical
130/// nested Form value and one submission-owned AbortSignal to the named export.
131#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
132#[serde(deny_unknown_fields)]
133pub struct SemanticPackageFormSubmission {
134    pub execution_boundary: SemanticPackageFormSubmissionExecutionBoundary,
135    pub cancellation: SemanticPackageFormSubmissionCancellation,
136    pub input: SemanticPackageFormSubmissionInput,
137    pub result: SemanticPackageFormSubmissionResult,
138}
139
140/// Execution side for the initial opaque terminal package boundary.
141#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
142#[serde(rename_all = "snake_case")]
143pub enum SemanticPackageOpaqueExecutionBoundary {
144    Client,
145}
146
147/// Resume behavior for an opaque terminal package activation.
148#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
149#[serde(rename_all = "snake_case")]
150pub enum SemanticPackageOpaqueResumePolicy {
151    ColdFallback,
152}
153
154/// Closed contract for a no-input, no-output opaque terminal export.
155///
156/// This records how the application may use a package without giving the
157/// compiler authority to inspect its implementation.
158#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
159#[serde(deny_unknown_fields)]
160pub struct SemanticPackageOpaqueTerminal {
161    pub execution_boundary: SemanticPackageOpaqueExecutionBoundary,
162    pub resume: SemanticPackageOpaqueResumePolicy,
163}
164
165/// Closed semantic contract for an executable Resource package export.
166///
167/// The compiler still does not execute or inspect package implementation. This
168/// metadata is the prerequisite for later Resource source lowering.
169#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
170#[serde(deny_unknown_fields)]
171pub struct SemanticPackageResourceEndpoint {
172    pub execution_boundary: SemanticPackageResourceExecutionBoundary,
173    pub cancellation: SemanticPackageResourceCancellation,
174    pub resume: SemanticPackageResourceResumePolicy,
175}
176
177/// Additional contract required before a Resource endpoint may serve as a
178/// route loader. The package implementation remains opaque to the compiler.
179#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
180#[serde(deny_unknown_fields)]
181pub struct SemanticPackageRouteLoader {
182    pub input: SemanticPackageRouteLoaderInput,
183    pub cache: SemanticPackageServerCachePolicy,
184    pub failure: SemanticPackageRouteLoaderFailure,
185}
186
187#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
188#[serde(deny_unknown_fields)]
189pub struct SemanticPackageExport {
190    pub kind: SemanticPackageKind,
191    pub type_signature: String,
192    pub runtime_module: String,
193    pub resume_policy: String,
194    #[serde(default)]
195    pub pure_operation: Option<SemanticPackagePureOperation>,
196    #[serde(default)]
197    pub resource_endpoint: Option<SemanticPackageResourceEndpoint>,
198    #[serde(default)]
199    pub route_loader: Option<SemanticPackageRouteLoader>,
200    #[serde(default)]
201    pub server_action: Option<SemanticPackageServerAction>,
202    #[serde(default)]
203    pub form_submission: Option<SemanticPackageFormSubmission>,
204    #[serde(default)]
205    pub opaque_terminal: Option<SemanticPackageOpaqueTerminal>,
206}
207
208#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
209#[serde(deny_unknown_fields)]
210pub struct SemanticPackageContract {
211    pub schema_version: u32,
212    pub package: String,
213    pub version: String,
214    pub integrity: String,
215    pub exports: BTreeMap<String, SemanticPackageExport>,
216}
217
218#[derive(Debug, Clone, PartialEq, Eq)]
219pub enum SemanticPackageContractError {
220    InvalidJson,
221    UnsupportedSchema,
222    InvalidIntegrity,
223    EmptyPackage,
224    EmptyVersion,
225    EmptyExport,
226    InvalidExportContract,
227    InvalidPureOperation,
228    InvalidResourceEndpoint,
229    InvalidRouteLoader,
230    InvalidServerAction,
231    InvalidFormSubmission,
232    InvalidOpaqueTerminal,
233    DuplicateSpecifier,
234}
235
236pub fn parse_semantic_package_contract(
237    json: &str,
238) -> Result<SemanticPackageContract, SemanticPackageContractError> {
239    let contract = serde_json::from_str::<SemanticPackageContract>(json)
240        .map_err(|_| SemanticPackageContractError::InvalidJson)?;
241    if contract.schema_version != SEMANTIC_PACKAGE_CONTRACT_SCHEMA_VERSION {
242        return Err(SemanticPackageContractError::UnsupportedSchema);
243    }
244    if contract.package.is_empty() {
245        return Err(SemanticPackageContractError::EmptyPackage);
246    }
247    if contract.version.is_empty() {
248        return Err(SemanticPackageContractError::EmptyVersion);
249    }
250    if !contract.integrity.starts_with("sha256:")
251        || contract.integrity.len() != 71
252        || !contract.integrity[7..]
253            .chars()
254            .all(|c| c.is_ascii_hexdigit())
255    {
256        return Err(SemanticPackageContractError::InvalidIntegrity);
257    }
258    if contract.exports.is_empty() || contract.exports.keys().any(|name| name.is_empty()) {
259        return Err(SemanticPackageContractError::EmptyExport);
260    }
261    if contract.exports.values().any(|export| {
262        export.type_signature.is_empty()
263            || export.runtime_module.is_empty()
264            || export.resume_policy.is_empty()
265    }) {
266        return Err(SemanticPackageContractError::InvalidExportContract);
267    }
268    if contract
269        .exports
270        .values()
271        .any(|export| export.pure_operation.is_some() && export.kind != SemanticPackageKind::Pure)
272    {
273        return Err(SemanticPackageContractError::InvalidPureOperation);
274    }
275    if contract.exports.values().any(|export| {
276        (export.kind == SemanticPackageKind::Resource) != export.resource_endpoint.is_some()
277    }) {
278        return Err(SemanticPackageContractError::InvalidResourceEndpoint);
279    }
280    if contract.exports.values().any(|export| {
281        let Some(loader) = &export.route_loader else {
282            return false;
283        };
284        let Some(endpoint) = &export.resource_endpoint else {
285            return true;
286        };
287        !matches!(export.kind, SemanticPackageKind::Resource)
288            || !matches!(
289                endpoint.execution_boundary,
290                SemanticPackageResourceExecutionBoundary::Server
291                    | SemanticPackageResourceExecutionBoundary::Shared
292            )
293            || match loader.cache.scope {
294                SemanticPackageServerCacheScope::Public => {
295                    loader.cache.max_age_seconds.is_none_or(|age| age == 0)
296                }
297                SemanticPackageServerCacheScope::NoStore
298                | SemanticPackageServerCacheScope::Private => {
299                    loader.cache.max_age_seconds.is_some()
300                }
301            }
302    }) {
303        return Err(SemanticPackageContractError::InvalidRouteLoader);
304    }
305    if contract.exports.values().any(|export| {
306        (export.kind == SemanticPackageKind::ServerAction) != export.server_action.is_some()
307            || (export.kind == SemanticPackageKind::ServerAction
308                && (export.type_signature != "FormData -> ServerActionResult"
309                    || export.resume_policy != "cold_fallback"))
310    }) {
311        return Err(SemanticPackageContractError::InvalidServerAction);
312    }
313    if contract.exports.values().any(|export| {
314        (export.kind == SemanticPackageKind::Capability) != export.form_submission.is_some()
315            || (export.kind == SemanticPackageKind::Capability
316                && (export.type_signature != "(FormValue, AbortSignal) -> Promise<void>"
317                    || export.resume_policy != "cold_fallback"))
318    }) {
319        return Err(SemanticPackageContractError::InvalidFormSubmission);
320    }
321    if contract.exports.values().any(|export| {
322        (export.kind == SemanticPackageKind::Opaque) != export.opaque_terminal.is_some()
323            || (export.kind == SemanticPackageKind::Opaque
324                && (export.type_signature != "() -> void"
325                    || export.resume_policy != "cold_fallback"))
326    }) {
327        return Err(SemanticPackageContractError::InvalidOpaqueTerminal);
328    }
329    Ok(contract)
330}
331
332#[derive(Debug, Clone, PartialEq, Eq, Default)]
333pub struct SemanticPackageResolutionTable {
334    contracts: BTreeMap<String, SemanticPackageContract>,
335}
336
337impl SemanticPackageResolutionTable {
338    pub fn insert(
339        &mut self,
340        specifier: String,
341        contract: SemanticPackageContract,
342    ) -> Result<(), SemanticPackageContractError> {
343        if self.contracts.contains_key(&specifier) {
344            return Err(SemanticPackageContractError::DuplicateSpecifier);
345        }
346        self.contracts.insert(specifier, contract);
347        Ok(())
348    }
349
350    #[must_use]
351    pub fn contract(&self, specifier: &str) -> Option<&SemanticPackageContract> {
352        self.contracts.get(specifier)
353    }
354    #[must_use]
355    pub fn resolve(&self, specifier: &str, export: &str) -> Option<&SemanticPackageExport> {
356        self.contracts.get(specifier)?.exports.get(export)
357    }
358}
359
360#[cfg(test)]
361mod tests {
362    use super::*;
363    #[test]
364    fn parses_integrity_checked_contract_and_resolves_an_export() {
365        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();
366        let mut table = SemanticPackageResolutionTable::default();
367        table.insert("date-kit".into(), contract).unwrap();
368        assert_eq!(
369            table.resolve("date-kit", "format").unwrap().kind,
370            SemanticPackageKind::Pure
371        );
372    }
373
374    #[test]
375    fn rejects_incomplete_contracts_without_replacing_existing_resolution() {
376        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();
377        let invalid = parse_semantic_package_contract(
378            r#"{"schema_version":1,"package":"date-kit","version":"","integrity":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","exports":{"format":{"kind":"pure","type_signature":"","runtime_module":"","resume_policy":""}}}"#,
379        );
380        assert_eq!(invalid, Err(SemanticPackageContractError::EmptyVersion));
381
382        let mut table = SemanticPackageResolutionTable::default();
383        table.insert("date-kit".into(), valid.clone()).unwrap();
384        assert_eq!(
385            table.insert("date-kit".into(), valid),
386            Err(SemanticPackageContractError::DuplicateSpecifier)
387        );
388        assert_eq!(table.contract("date-kit").unwrap().version, "1.2.3");
389    }
390
391    #[test]
392    fn resource_exports_require_a_closed_endpoint_contract() {
393        let missing_endpoint = parse_semantic_package_contract(
394            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"}}}"#,
395        );
396        assert_eq!(
397            missing_endpoint,
398            Err(SemanticPackageContractError::InvalidResourceEndpoint)
399        );
400
401        let contract = parse_semantic_package_contract(
402            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"}}}}"#,
403        )
404        .expect("resource endpoint contract");
405        assert_eq!(
406            contract.exports["loadProfile"].resource_endpoint,
407            Some(SemanticPackageResourceEndpoint {
408                execution_boundary: SemanticPackageResourceExecutionBoundary::Shared,
409                cancellation: SemanticPackageResourceCancellation::Abort,
410                resume: SemanticPackageResourceResumePolicy::Snapshot,
411            })
412        );
413    }
414
415    #[test]
416    fn opaque_exports_require_the_closed_terminal_contract() {
417        let missing_terminal = parse_semantic_package_contract(
418            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"}}}"#,
419        );
420        assert_eq!(
421            missing_terminal,
422            Err(SemanticPackageContractError::InvalidOpaqueTerminal)
423        );
424
425        let contract = parse_semantic_package_contract(
426            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"}}}}"#,
427        )
428        .expect("opaque terminal contract");
429        assert_eq!(
430            contract.exports["trackPurchase"].opaque_terminal,
431            Some(SemanticPackageOpaqueTerminal {
432                execution_boundary: SemanticPackageOpaqueExecutionBoundary::Client,
433                resume: SemanticPackageOpaqueResumePolicy::ColdFallback,
434            })
435        );
436    }
437
438    #[test]
439    fn validates_route_loader_capabilities_on_server_or_shared_resource_exports() {
440        let contract = parse_semantic_package_contract(
441            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"}}}}"#,
442        )
443        .expect("closed route loader contract");
444        assert_eq!(
445            contract.exports["loadPost"].route_loader,
446            Some(SemanticPackageRouteLoader {
447                input: SemanticPackageRouteLoaderInput::RouteParameters,
448                cache: SemanticPackageServerCachePolicy {
449                    scope: SemanticPackageServerCacheScope::Public,
450                    max_age_seconds: Some(60),
451                },
452                failure: SemanticPackageRouteLoaderFailure::Typed,
453            })
454        );
455
456        let invalid_client = parse_semantic_package_contract(
457            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"}}}}"#,
458        );
459        assert_eq!(
460            invalid_client,
461            Err(SemanticPackageContractError::InvalidRouteLoader)
462        );
463    }
464
465    #[test]
466    fn validates_closed_server_action_capabilities() {
467        let contract = parse_semantic_package_contract(
468            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"}}}}"#,
469        );
470        assert!(contract.is_ok());
471    }
472
473    #[test]
474    fn form_submission_capabilities_require_the_closed_client_abort_contract() {
475        let contract = parse_semantic_package_contract(
476            r#"{"schema_version":1,"package":"profile-service","version":"1.2.3","integrity":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","exports":{"saveProfile":{"kind":"capability","type_signature":"(FormValue, AbortSignal) -> Promise<void>","runtime_module":"dist/save-profile.js","resume_policy":"cold_fallback","form_submission":{"execution_boundary":"client","cancellation":"abort","input":"form_value","result":"void"}}}}"#,
477        )
478        .expect("closed Form submission contract");
479        assert_eq!(
480            contract.exports["saveProfile"].form_submission,
481            Some(SemanticPackageFormSubmission {
482                execution_boundary: SemanticPackageFormSubmissionExecutionBoundary::Client,
483                cancellation: SemanticPackageFormSubmissionCancellation::Abort,
484                input: SemanticPackageFormSubmissionInput::FormValue,
485                result: SemanticPackageFormSubmissionResult::Void,
486            })
487        );
488
489        let missing = parse_semantic_package_contract(
490            r#"{"schema_version":1,"package":"profile-service","version":"1.2.3","integrity":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","exports":{"saveProfile":{"kind":"capability","type_signature":"(FormValue, AbortSignal) -> Promise<void>","runtime_module":"dist/save-profile.js","resume_policy":"cold_fallback"}}}"#,
491        );
492        assert_eq!(
493            missing,
494            Err(SemanticPackageContractError::InvalidFormSubmission)
495        );
496    }
497}