Skip to main content

faf_fafb/
canon.rs

1//! The canonical chunk table — FAFb v2 is CLOSED CANONICAL.
2//!
3//! The writer emits exactly the chunks in this table, in this order.
4//! Non-canonical top-level YAML keys are folded into the `context` chunk —
5//! nothing is lost, and nothing grows a new section name. The reader keeps
6//! the IFF rule (skip unknown names gracefully) so future minor versions can
7//! add a chunk without breaking deployed readers: writer closed, reader
8//! graceful.
9//!
10//! Closed canonical is what makes the brick deterministic: identical content
11//! produces identical bytes regardless of input key order, so a `.fafb` is
12//! content-addressable — same project context, same hash, everywhere.
13
14/// Chunk classification, stored in bits 0–1 of `SectionEntry.flags`.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum ChunkClassification {
17    /// Core project identity (project, stack, human_context, …).
18    Dna = 0b00,
19    /// Runtime/supplementary context.
20    Context = 0b01,
21    /// Documentation references (docs).
22    Pointer = 0b10,
23    /// Reserved for future use.
24    Reserved = 0b11,
25}
26
27impl ChunkClassification {
28    /// The 2-bit value for encoding into flags.
29    pub const fn bits(&self) -> u32 {
30        *self as u32
31    }
32
33    /// Decode from the low 2 bits of a flags value.
34    pub fn from_bits(bits: u32) -> Self {
35        match bits & 0b11 {
36            0b00 => Self::Dna,
37            0b01 => Self::Context,
38            0b10 => Self::Pointer,
39            _ => Self::Reserved,
40        }
41    }
42
43    /// Human-readable name.
44    pub const fn name(&self) -> &'static str {
45        match self {
46            Self::Dna => "DNA",
47            Self::Context => "Context",
48            Self::Pointer => "Pointer",
49            Self::Reserved => "Reserved",
50        }
51    }
52}
53
54/// Classification mask for the low 2 bits of section flags.
55pub const CLASSIFICATION_MASK: u32 = 0b11;
56
57/// One canonical chunk: name, classification, default priority.
58#[derive(Debug, Clone, Copy)]
59pub struct CanonicalChunk {
60    pub name: &'static str,
61    pub classification: ChunkClassification,
62    pub priority: u8,
63}
64
65/// The canonical chunk table — the complete, closed set of FAFb v2 section
66/// names, in serialization order. **This IS the format: there is no chunk 14.**
67///
68/// The set mirrors `faf-cli`'s `FafData` (the single source of truth for the
69/// `.faf` structure — `src/core/types.ts`): 11 DNA chunks + 2 Context. The
70/// metastamp is NOT a chunk — it's the FAFb header (`created_timestamp`),
71/// which is why `.faf`'s `generated:` key lives there, not here. Non-canonical
72/// top-level keys (anything tools add) fold losslessly into `context`.
73pub const CANONICAL_CHUNKS: &[CanonicalChunk] = &[
74    // ── DNA — core identity (11) ──
75    CanonicalChunk {
76        name: "faf_version",
77        classification: ChunkClassification::Dna,
78        priority: 255,
79    },
80    CanonicalChunk {
81        name: "project",
82        classification: ChunkClassification::Dna,
83        priority: 255,
84    },
85    CanonicalChunk {
86        name: "app_type",
87        classification: ChunkClassification::Dna,
88        priority: 200,
89    },
90    CanonicalChunk {
91        name: "about",
92        classification: ChunkClassification::Dna,
93        priority: 150,
94    },
95    CanonicalChunk {
96        name: "stack",
97        classification: ChunkClassification::Dna,
98        priority: 200,
99    },
100    CanonicalChunk {
101        name: "human_context",
102        classification: ChunkClassification::Dna,
103        priority: 200,
104    },
105    CanonicalChunk {
106        name: "tech_stack",
107        classification: ChunkClassification::Dna,
108        priority: 200,
109    },
110    CanonicalChunk {
111        name: "key_files",
112        classification: ChunkClassification::Dna,
113        priority: 200,
114    },
115    CanonicalChunk {
116        name: "commands",
117        classification: ChunkClassification::Dna,
118        priority: 180,
119    },
120    CanonicalChunk {
121        name: "monorepo",
122        classification: ChunkClassification::Dna,
123        priority: 150,
124    },
125    CanonicalChunk {
126        name: "architecture",
127        classification: ChunkClassification::Dna,
128        priority: 128,
129    },
130    // ── Context (2) — derived output + the fold target ──
131    CanonicalChunk {
132        name: "scores",
133        classification: ChunkClassification::Context,
134        priority: 64,
135    },
136    CanonicalChunk {
137        name: "context",
138        classification: ChunkClassification::Context,
139        priority: 64,
140    },
141];
142
143/// Look up a canonical chunk by name.
144pub fn canonical_chunk(name: &str) -> Option<&'static CanonicalChunk> {
145    CANONICAL_CHUNKS.iter().find(|c| c.name == name)
146}
147
148/// Is this top-level key part of the canonical chunk set?
149pub fn is_canonical(name: &str) -> bool {
150    canonical_chunk(name).is_some()
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156
157    #[test]
158    fn table_is_closed_at_13() {
159        // Mirrors faf-cli FafData: 11 DNA + 2 Context. The one-way door.
160        assert_eq!(CANONICAL_CHUNKS.len(), 13);
161        let dna = CANONICAL_CHUNKS
162            .iter()
163            .filter(|c| c.classification == ChunkClassification::Dna)
164            .count();
165        let ctx = CANONICAL_CHUNKS
166            .iter()
167            .filter(|c| c.classification == ChunkClassification::Context)
168            .count();
169        assert_eq!(dna, 11);
170        assert_eq!(ctx, 2);
171    }
172
173    #[test]
174    fn mirrors_faf_cli_fafdata() {
175        // The canonical set IS faf-cli's FafData top-level keys (the truth).
176        for key in [
177            "faf_version",
178            "project",
179            "app_type",
180            "about",
181            "stack",
182            "human_context",
183            "monorepo",
184            "scores",
185            "tech_stack",
186            "key_files",
187            "commands",
188            "architecture",
189            "context",
190        ] {
191            assert!(is_canonical(key), "FafData key '{}' must be canonical", key);
192        }
193        // Keys that diverged from the old faf-rust-sdk model are NOT canonical.
194        for key in [
195            "instant_context",
196            "ai_score",
197            "ai_confidence",
198            "ai_tldr",
199            "context_quality",
200            "preferences",
201            "state",
202            "tags",
203            "meta",
204            "bi_sync",
205            "docs",
206            "generated",
207        ] {
208            assert!(!is_canonical(key), "'{}' must fold, not be a chunk", key);
209        }
210    }
211
212    #[test]
213    fn names_are_unique() {
214        for (i, a) in CANONICAL_CHUNKS.iter().enumerate() {
215            for b in &CANONICAL_CHUNKS[i + 1..] {
216                assert_ne!(a.name, b.name);
217            }
218        }
219    }
220
221    #[test]
222    fn identity_chunks_are_critical() {
223        assert_eq!(canonical_chunk("faf_version").unwrap().priority, 255);
224        assert_eq!(canonical_chunk("project").unwrap().priority, 255);
225    }
226
227    #[test]
228    fn context_is_the_fold_target() {
229        // `context` is canonical (Context class) and is where non-canonical
230        // keys fold. No Pointer-class chunk exists in the FafData truth.
231        assert_eq!(
232            canonical_chunk("context").unwrap().classification,
233            ChunkClassification::Context
234        );
235        assert!(
236            !CANONICAL_CHUNKS
237                .iter()
238                .any(|c| c.classification == ChunkClassification::Pointer)
239        );
240    }
241
242    #[test]
243    fn unknown_keys_are_not_canonical() {
244        assert!(!is_canonical("custom_field"));
245        assert!(!is_canonical("my_exotic_field"));
246        // Case-sensitive (YAML convention)
247        assert!(!is_canonical("Project"));
248        assert!(is_canonical("project"));
249    }
250
251    #[test]
252    fn classification_bits_roundtrip() {
253        for class in &[
254            ChunkClassification::Dna,
255            ChunkClassification::Context,
256            ChunkClassification::Pointer,
257            ChunkClassification::Reserved,
258        ] {
259            assert_eq!(ChunkClassification::from_bits(class.bits()), *class);
260        }
261        // Higher bits ignored
262        assert_eq!(
263            ChunkClassification::from_bits(0xFF00_0001),
264            ChunkClassification::Context
265        );
266    }
267}