Skip to main content

faf_wasm_sdk/
lib.rs

1//! FAF WASM SDK — the kernel for the edge.
2//!
3//! A thin `wasm-bindgen` shell over the workspace kernel: scoring is
4//! [`faf-kernel`](https://docs.rs/faf-kernel), the binary form is
5//! [`faf-fafb`](https://docs.rs/faf-fafb). No scoring or format logic lives
6//! here — the same engine that runs in the CLI and the MCP server runs in the
7//! browser and at the edge, so there is nothing to drift.
8//!
9//! 7 pure-function exports. No classes. JSON / bytes in, JSON / bytes out.
10//!
11//! # Usage (JavaScript)
12//! ```js
13//! import init, { sdk_version, score_faf, validate_faf,
14//!     compile_fafb, decompile_fafb, score_fafb, fafb_info } from 'faf-wasm-sdk';
15//!
16//! await init();
17//! const result = score_faf(yamlContent);   // JSON string (always-33 Mk4)
18//! const bytes  = compile_fafb(yamlContent); // Uint8Array (FAFb v2)
19//! const json   = decompile_fafb(bytes);     // JSON string
20//! ```
21
22mod fafb_json;
23
24use wasm_bindgen::prelude::*;
25
26/// Get SDK version.
27#[wasm_bindgen]
28pub fn sdk_version() -> String {
29    env!("CARGO_PKG_VERSION").to_string()
30}
31
32/// Score FAF YAML content with the Mk4 kernel (always-33) — returns JSON.
33#[wasm_bindgen]
34pub fn score_faf(yaml: String) -> Result<String, JsValue> {
35    faf_kernel::score(&yaml)
36        .map(|r| r.to_json())
37        .map_err(|e| JsValue::from_str(&e))
38}
39
40/// Validate FAF YAML content — true if it parses as a YAML mapping.
41#[wasm_bindgen]
42pub fn validate_faf(yaml: String) -> bool {
43    use serde_yaml_ng::Value;
44    matches!(
45        serde_yaml_ng::from_str::<Value>(&yaml),
46        Ok(Value::Mapping(_))
47    )
48}
49
50/// Compile YAML to a FAFb v2 binary — returns Uint8Array.
51#[wasm_bindgen]
52pub fn compile_fafb(yaml: String) -> Result<Vec<u8>, JsValue> {
53    fafb_json::compile_fafb(&yaml).map_err(|e| JsValue::from_str(&e))
54}
55
56/// Decompile a FAFb binary to JSON (full content) — returns JSON string.
57#[wasm_bindgen]
58pub fn decompile_fafb(bytes: &[u8]) -> Result<String, JsValue> {
59    fafb_json::decompile_fafb(bytes).map_err(|e| JsValue::from_str(&e))
60}
61
62/// Score a FAFb binary — returns JSON string (same shape as `score_faf`).
63#[wasm_bindgen]
64pub fn score_fafb(bytes: &[u8]) -> Result<String, JsValue> {
65    fafb_json::score_fafb(bytes).map_err(|e| JsValue::from_str(&e))
66}
67
68/// Get FAFb file info (header + section metadata, no content) — returns JSON.
69#[wasm_bindgen]
70pub fn fafb_info(bytes: &[u8]) -> Result<String, JsValue> {
71    fafb_json::fafb_info(bytes).map_err(|e| JsValue::from_str(&e))
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    #[test]
79    fn test_sdk_version_is_3x() {
80        assert!(sdk_version().starts_with("3."));
81    }
82
83    #[test]
84    fn test_validate_faf_accepts_mapping() {
85        assert!(validate_faf("project:\n  name: test".to_string()));
86    }
87
88    #[test]
89    fn test_validate_faf_rejects_non_mapping() {
90        assert!(!validate_faf("just a string".to_string()));
91        assert!(!validate_faf("- list\n- items".to_string()));
92        assert!(!validate_faf("42".to_string()));
93    }
94
95    #[test]
96    fn test_validate_faf_rejects_broken_yaml() {
97        assert!(!validate_faf("[invalid: yaml: {{{".to_string()));
98    }
99
100    #[test]
101    fn test_score_faf_is_always_33() {
102        // v3: always-33 model — no Base/21. The score comes from faf-kernel.
103        let result = score_faf("project:\n  name: test".to_string()).unwrap();
104        assert!(result.contains("\"score\":"));
105        assert!(result.contains("\"tier\":"));
106        assert!(result.contains("\"total\":33"));
107    }
108
109    #[test]
110    fn test_canonical_tiers_not_medals() {
111        // Trophy is the only emoji; sub-Trophy tiers are canonical names.
112        let result = score_faf("project:\n  name: test".to_string()).unwrap();
113        assert!(!result.contains("🥇") && !result.contains("🥈") && !result.contains("🥉"));
114    }
115
116    #[test]
117    fn test_compile_decompile_fafb_roundtrip_v2() {
118        let yaml = "faf_version: 2.5.0\nproject:\n  name: test\n".to_string();
119        let bytes = compile_fafb(yaml).unwrap();
120        assert_eq!(&bytes[0..4], b"FAFB");
121        assert_eq!(bytes[4], 2); // FAFb v2
122        let json = decompile_fafb(&bytes).unwrap();
123        assert!(json.contains("\"sections\":"));
124        assert!(json.contains("\"version\":\"2.0\""));
125    }
126
127    #[test]
128    fn test_score_fafb_from_compiled() {
129        let yaml = "faf_version: 2.5.0\nproject:\n  name: test\n".to_string();
130        let bytes = compile_fafb(yaml).unwrap();
131        let score = score_fafb(&bytes).unwrap();
132        assert!(score.contains("\"score\":"));
133        assert!(score.contains("\"total\":33"));
134    }
135
136    #[test]
137    fn test_fafb_info_from_compiled() {
138        let yaml = "faf_version: 2.5.0\nproject:\n  name: test\n".to_string();
139        let bytes = compile_fafb(yaml).unwrap();
140        let info = fafb_info(&bytes).unwrap();
141        assert!(info.contains("\"section_count\":"));
142        assert!(!info.contains("\"content\":"));
143    }
144}