axon-frontend 3.4.0

AXON compiler frontend - lexer, parser, AST, epistemic type system, type checker, IR generator. Zero runtime dependencies. Ships the two judgments the runtime shares verbatim: `stability` (the `mandate` gain band D < |Kp+Ki+Kd| < 1/L, both endpoints exclusive, verified at compile time against declared bounds) and `substrate` (the `fabric` provider/region/jurisdiction catalog behind axon-E041 region mismatch and axon-E042 compliance-jurisdiction). It also accepts the step-body statement positions the language reference has always published: mandate/shield/ots/lambda applications scoped to the step they govern, the PIX verbs with a braceless field list, and `hibernate until <event>`. axon-T957 RegulatedBoundaryCoverage and axon-T1215 channel κ-coverage guard every regulated boundary. See https://www.ricardovelit.com/axon-docs
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
//! v4.0.0 — **the closed regulatory vocabulary Κ.**
//!
//! # Why this module exists in the FRONTEND
//!
//! The ESK paper (section 6.1, *Regulatory Type Theory*) states the rule plainly:
//!
//! > *"κ es un subconjunto del registro canónico Κ = {HIPAA, PCI_DSS, GDPR,
//! > SOX, FINRA, ISO27001, SOC2, FISMA, GxP, CCPA, NIST_800_53}. Cualquier
//! > etiqueta fuera de Κ es **compile-time error** (typos como "HIPPA"
//! > rechazados)."*
//!
//! That promise was true — in Python. The paper cites
//! `TestComplianceCoverage.test_unknown_regulatory_class_rejected`, a test from
//! the retired interpreter, and the rule was lost in the Rust rewrite. Not by a
//! decision: the canonical registry landed in `axon-rs::esk::compliance`, and
//! `axon-frontend` depends on `serde` and nothing else. **The catalog ended up
//! downstream of the type checker that needed it**, so the law could not be
//! written where it belonged, and `compliance:` quietly became a free-string
//! field while `effects:` beside it stayed a closed catalog.
//!
//! Measured 2026-08-14, and recorded in `advertised.rs` before this cycle
//! existed: `compliance: [NOT_A_FRAMEWORK]` on an `axonendpoint` compiled
//! clean. The values an adopter writes there are `PCI_DSS`, `SOX`, `HIPAA` —
//! read by a regulated reader as an assertion.
//!
//! # What lives here and what does not
//!
//! Only the **membership question**: which labels exist. That is all a compiler
//! needs, and it is pure data.
//!
//! The rich metadata for each class — title, jurisdiction, sector, description
//! — stays in `axon-rs::esk::compliance`, because it exists to build audit
//! dossiers, which is a runtime concern. That module now derives its keys from
//! [`REGULATORY_CLASSES`](crate::compliance::REGULATORY_CLASSES) and a test
//! pins the two in agreement, so there is one
//! source of truth for *what exists* and one place for *what it means*.
//!
//! This is deliberately NOT a second copy of the catalog. v2.89.0 paid for a law
//! written three times: two copies were updated, the third was missed, and the
//! workspace suite caught it — after the first two attempts had already shipped
//! the drift.

use std::collections::BTreeSet;

/// The canonical regulatory classes — Κ.
///
/// Reflexive by construction: a class covers itself and nothing else.
/// Cross-framework overlap (does SOC2 imply ISO27001?) is an explicit policy
/// decision a regulator makes, not something a compiler may infer.
///
/// **Adding an entry is a deliberate act.** A new framework here becomes a
/// label every adopter can assert, and the assertion is what a regulated
/// reader trusts — so it belongs in a cycle with a paper behind it, not in a
/// convenience commit.
pub const REGULATORY_CLASSES: &[&str] = &[
    "HIPAA",
    "PCI_DSS",
    "GDPR",
    "SOX",
    "FINRA",
    "ISO27001",
    "SOC2",
    "FISMA",
    "GxP",
    "CCPA",
    "NIST_800_53",
    // v4.0.0 — the four LATAM jurisdictions the product serves.
    //
    // They enter Κ on the same footing as every class above: declarable, and a
    // participant in `axon-T957`'s coverage difference. That is the whole
    // criterion, and it is worth stating precisely because the first draft of
    // this decision justified the set by saying the excluded jurisdictions
    // "impose no real restriction" — which does not hold. Ley 25.326
    // (Argentina) and Ley 81 (Panamá) would impose exactly the same restriction
    // as these four.
    //
    // The real criterion is PRODUCT PRIORITY: each of these four has a named
    // vertical in the README and a mechanism the language already provides
    // (NOM-151 → `axonstore` sealing; LFPDPPP/LGPD/Ley 1581 → `shield`
    // redaction). That is a good reason. Writing the other one down would have
    // been a justification that does not survive a reading, which is the
    // species of claim this whole line of work exists to remove.
    "NOM151",
    "LFPDPPP",
    "LGPD",
    "LEY1581",
];

/// Is `label` a member of Κ?
///
/// **Case-SENSITIVE, deliberately.** `hipaa` is not `HIPAA`. The canonical
/// spelling is the one that appears in the regulation, and a compliance
/// annotation that renders differently from the framework it names is a
/// different string to every downstream consumer that groups by it — the audit
/// dossier, the SBOM filter, the evidence packager. Accepting case variants
/// would make `[HIPAA]` and `[hipaa]` two classes that look like one.
pub fn is_known(label: &str) -> bool {
    REGULATORY_CLASSES.contains(&label)
}

/// The classes in `declared` that are NOT in Κ, in declaration order.
///
/// Returns the offenders rather than a bool so a diagnostic can name what the
/// author actually wrote — `axon-T1214` quotes the typo back, which is the
/// difference between "invalid compliance class" and "`HIPPA` is not a
/// regulatory class; did you mean `HIPAA`?".
pub fn unknown_classes<'a>(declared: &'a [String]) -> Vec<&'a str> {
    declared
        .iter()
        .map(|s| s.as_str())
        .filter(|s| !is_known(s))
        .collect()
}

/// The member of Κ within edit distance 1 of `label`, if exactly one exists.
///
/// A typo in a regulatory class is the failure this catalog exists to catch,
/// and the two that matter — `HIPPA` for `HIPAA`, `PCI-DSS` for `PCI_DSS` —
/// are both one edit away. Suggesting is cheap; suggesting the WRONG framework
/// is not, so an ambiguous match suggests nothing.
pub fn nearest_class(label: &str) -> Option<&'static str> {
    let mut hit: Option<&'static str> = None;
    for candidate in REGULATORY_CLASSES {
        if edit_distance_at_most_1(label, candidate) {
            if hit.is_some() {
                return None; // ambiguous — say nothing rather than guess
            }
            hit = Some(candidate);
        }
    }
    hit
}

/// True when `a` and `b` differ by at most one insertion, deletion or
/// substitution. Bounded on purpose — a full Levenshtein over an 11-entry
/// catalog would suggest `SOX` for `SOC2`.
fn edit_distance_at_most_1(a: &str, b: &str) -> bool {
    let (a, b) = (a.as_bytes(), b.as_bytes());
    if a == b {
        return true;
    }
    let (long, short) = if a.len() >= b.len() { (a, b) } else { (b, a) };
    if long.len() - short.len() > 1 {
        return false;
    }
    let mut i = 0;
    let mut j = 0;
    let mut edited = false;
    while i < long.len() && j < short.len() {
        if long[i] == short[j] {
            i += 1;
            j += 1;
            continue;
        }
        if edited {
            return false;
        }
        edited = true;
        if long.len() == short.len() {
            i += 1;
            j += 1;
        } else {
            i += 1;
        }
    }
    true
}

/// Peel the type constructors that are transparent to κ:
/// `FlowEnvelope<T>` / `List<T>` / `Stream<T>` carry the κ of `T`, and `?`
/// (optionality) is orthogonal to what the data IS.
///
/// v4.0.0 — hoisted here from `type_checker`'s private helper because a
/// THIRD consumer arrived (the audit engine's coverage rule, joining T957 and
/// the typed-bus predicate) and three private copies of "what wraps a type
/// without changing its κ" is how the copies drift — v2.89.0 paid for exactly
/// that with a law written three times.
pub fn peel_type_constructors(type_ref: &str) -> &str {
    let mut t = type_ref.trim();
    t = t.strip_suffix('?').unwrap_or(t).trim();
    loop {
        let peeled = ["FlowEnvelope<", "List<", "Stream<"].iter().find_map(|ctor| {
            t.strip_prefix(*ctor)
                .and_then(|rest| rest.strip_suffix('>'))
                .map(|inner| inner.trim())
        });
        match peeled {
            Some(inner) => t = inner.strip_suffix('?').unwrap_or(inner).trim(),
            None => return t,
        }
    }
}

/// How a given exit's κ is covered — or why it cannot be.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Coverage {
    /// κ is read from DECLARED TYPES at this exit and covered by a named
    /// `shield:`. `kappa_from` names the fields the rule reads; `code` is the
    /// diagnostic that refuses an uncovered crossing.
    Shielded {
        kappa_from: &'static str,
        code: &'static str,
    },
    /// This exit has NO static κ. Not an oversight — a property of how it binds
    /// its data, recorded so that "no rule here" can never be confused with
    /// "nobody looked here". `governed_by` is the diagnostic that governs it on
    /// the epistemic axis instead, or `None` where nothing does.
    NoStaticKappa {
        governed_by: Option<&'static str>,
        why: &'static str,
    },
}

/// One way data leaves an AXON program.
#[derive(Debug, Clone, Copy)]
pub struct Egress {
    pub primitive: &'static str,
    pub coverage: Coverage,
}

/// **The closed catalogue of exits.**
///
/// A compliance guarantee is worth exactly what its list of exits is worth. A
/// list somebody maintains by hand develops a hole the first time primitive
/// number eight arrives, and the hole is invisible: every existing rule still
/// passes. So this catalogue is closed, it is gated
/// (`tests/the_egress_catalogue_is_closed.rs`), and every entry says how that
/// exit is covered **or why it cannot be**.
///
/// Recording the uncoverable ones is the point. An exit missing from this list
/// is indistinguishable from an exit nobody examined; an exit present with
/// `NoStaticKappa` is a stated limit an auditor can read and price.
///
/// # What was measured to build it (v4.3.0)
///
/// κ originates in exactly one place: a `type` declaration's `compliance:`
/// list. So an exit has a static κ precisely when it binds a DECLARED TYPE.
/// Three do. Four do not, for two different reasons, and both reasons are
/// structural rather than incidental:
///
/// - `document` / `deliver` / `notify` bind **bare value references**
///   (`DocScalar::Ref`) and have no typed binding site in the grammar —
///   `render` is a runtime concept, not a declaration. This is also exactly why
///   their epistemic barriers work: "is this value attributed?" is answerable
///   about a bare reference, and "what classes does it carry?" is not.
/// - `axonstore` binds a **closed catalogue of primitive SQL column types**. A
///   regulated value is decomposed into columns before it lands, and κ does not
///   survive that decomposition — the same intra-expression limit this cycle
///   names in its own trade-off list, met at the storage boundary.
pub const EGRESS_PRIMITIVES: &[Egress] = &[
    Egress {
        primitive: "axonendpoint",
        coverage: Coverage::Shielded {
            kappa_from: "body: and output:",
            code: "axon-T957",
        },
    },
    Egress {
        primitive: "channel",
        coverage: Coverage::Shielded {
            kappa_from: "message:",
            code: "axon-T1215",
        },
    },
    // v4.3.0 — the widest exit, and the last one to get a rule. A whole
    // regulated record passed to a tool with `effects: <network, web>`
    // compiled clean until this entry existed.
    Egress {
        primitive: "tool",
        coverage: Coverage::Shielded {
            kappa_from: "parameters: and output_type:",
            code: "axon-T1221",
        },
    },
    Egress {
        primitive: "document",
        coverage: Coverage::Shielded {
            kappa_from: "payload:",
            code: "axon-T1222",
        },
    },
    Egress {
        primitive: "deliver",
        coverage: Coverage::Shielded {
            kappa_from: "payload:",
            code: "axon-T1223",
        },
    },
    Egress {
        primitive: "notify",
        coverage: Coverage::Shielded {
            kappa_from: "payload:",
            code: "axon-T1224",
        },
    },
    Egress {
        primitive: "axonstore",
        coverage: Coverage::NoStaticKappa {
            governed_by: None,
            why: "a store schema is a closed catalogue of primitive SQL column types, so a \
                  regulated value is decomposed into columns before it lands and carries no \
                  declared type to read a class from",
        },
    },
];

/// The exits whose κ this compiler can compute and refuse on.
pub fn shielded_exits() -> impl Iterator<Item = &'static Egress> {
    EGRESS_PRIMITIVES
        .iter()
        .filter(|e| matches!(e.coverage, Coverage::Shielded { .. }))
}

/// Every κ class a value of this type carries — **including the classes its
/// fields carry**.
///
/// This is the answer to "what regulated data is in here?", and it is the only
/// definition of that question. [`peel_type_constructors`] answers a smaller
/// one — which wrappers are transparent — and answering the big question with
/// the small one is the defect this function exists to close.
///
/// # Why a walk and not a field read
///
/// The κ of a type used to be `t.compliance`, read after peeling constructors.
/// That reads the DECLARATION, and what crosses a boundary is the VALUE. The
/// two differ the moment anyone writes the most ordinary thing in the language:
///
/// ```text
/// type PatientRecord compliance [HIPAA] { … }
/// type PatientSummaryRequest { rec: PatientRecord }   // ← κ laundered
///
/// axonendpoint PatientSummary {
///     body:   PatientSummaryRequest
///     shield: ClinicalShield          // decorative — T957 saw no κ to cover
/// }
/// ```
///
/// Five of the six regulated-vertical scaffolds this repository ships were
/// written exactly that way, and every one of them compiled with **all** of its
/// `shield:` lines deleted. `peel_type_constructors`' own doc comment says that
/// wrapping a type "in an envelope or a list does not launder its regulatory
/// classes" — and the commonest wrapper of all, a struct field, did.
///
/// # What it walks
///
/// - the transparent constructors, via [`peel_type_constructors`];
/// - **any other generic spelling** `Name<Inner>` — `Inner` is walked whether or
///   not `Name` is a constructor this module recognises. An unknown wrapper must
///   not be a hiding place, which is the same law one level up;
/// - every field of every declared type it reaches, transitively.
///
/// # Totality
///
/// Total by construction. An unresolvable name contributes nothing (the house
/// soft-type discipline: builtins and imported names are not declared here), and
/// a cycle — `type Node { next: Node }` — terminates on the visited set rather
/// than recursing forever.
pub fn transitive_kappa(program: &crate::ast::Program, type_ref: &str) -> BTreeSet<String> {
    let mut found = BTreeSet::new();
    let mut visited: BTreeSet<String> = BTreeSet::new();
    let mut queue: Vec<String> = vec![type_ref.to_string()];

    while let Some(spelling) = queue.pop() {
        let base = peel_type_constructors(&spelling);
        if base.is_empty() {
            continue;
        }

        // A generic this module does not recognise still has an argument, and
        // that argument is data. Walk it before resolving the head.
        if let Some(open) = base.find('<') {
            if let Some(inner) = base.strip_suffix('>').map(|s| &s[open + 1..]) {
                for arg in inner.split(',') {
                    let arg = arg.trim();
                    if !arg.is_empty() && !visited.contains(arg) {
                        queue.push(arg.to_string());
                    }
                }
            }
        }

        if !visited.insert(base.to_string()) {
            continue;
        }

        let Some(decl) = program.declarations.iter().find_map(|d| match d {
            crate::ast::Declaration::Type(t) if t.name == base => Some(t),
            _ => None,
        }) else {
            continue;
        };

        found.extend(decl.compliance.iter().cloned());

        for field in &decl.fields {
            let spelling = if field.type_expr.generic_param.is_empty() {
                field.type_expr.name.clone()
            } else {
                format!("{}<{}>", field.type_expr.name, field.type_expr.generic_param)
            };
            if !spelling.is_empty() {
                queue.push(spelling);
            }
        }
    }

    found
}

/// Peel a channel `message:` spelling to its payload leaf.
///
/// `Channel<…<T>>` peels to `T` — a second-order channel relays the same
/// payload, so it carries the same κ — then the ordinary constructors peel
/// via [`peel_type_constructors`].
pub fn peel_channel_payload(spelling: &str) -> &str {
    let mut leaf = spelling.trim();
    while let Some(inner) = leaf
        .strip_prefix("Channel<")
        .and_then(|rest| rest.strip_suffix('>'))
    {
        leaf = inner.trim();
    }
    peel_type_constructors(leaf)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn the_catalog_is_the_fifteen_the_paper_names() {
        assert_eq!(
            REGULATORY_CLASSES.len(),
            15,
            "Κ is the canonical registry from the ESK paper, extended in v4.0.0 with the \
             four LATAM jurisdictions. Changing its size is a decision about what an adopter \
             may assert, not a refactor — and the paper's own Κ is pinned against this list \
             by the paper-matches-compiler gate."
        );
        for class in [
            "HIPAA", "PCI_DSS", "GDPR", "SOX", "FINRA", "ISO27001", "SOC2", "FISMA", "GxP",
            "CCPA", "NIST_800_53", "NOM151", "LFPDPPP", "LGPD", "LEY1581",
        ] {
            assert!(is_known(class), "{class} must be in Κ");
        }
    }

    #[test]
    fn membership_is_case_sensitive() {
        assert!(is_known("HIPAA"));
        assert!(!is_known("hipaa"), "case variants are different strings to every consumer that groups by this label");
        assert!(!is_known("Hipaa"));
    }

    #[test]
    fn a_typo_is_not_a_class_and_gets_a_suggestion() {
        assert!(!is_known("HIPPA"));
        assert_eq!(nearest_class("HIPPA"), Some("HIPAA"));
        assert_eq!(nearest_class("PCI-DSS"), Some("PCI_DSS"));
    }

    #[test]
    fn a_word_that_is_not_a_framework_suggests_nothing() {
        assert!(!is_known("NOT_A_FRAMEWORK"));
        assert_eq!(
            nearest_class("NOT_A_FRAMEWORK"),
            None,
            "a suggestion must be a near miss, never the closest of eleven unrelated names"
        );
    }

    #[test]
    fn unknown_classes_reports_offenders_in_order() {
        let declared = vec!["HIPAA".to_string(), "HIPPA".to_string(), "SOC2".to_string(), "NOPE".to_string()];
        assert_eq!(unknown_classes(&declared), vec!["HIPPA", "NOPE"]);
    }

    #[test]
    fn every_class_is_known_and_suggests_itself() {
        for class in REGULATORY_CLASSES {
            assert!(is_known(class));
            assert_eq!(
                nearest_class(class),
                Some(*class),
                "a valid class must resolve to itself — if two members of Κ are one edit apart, \
                 `nearest_class` goes ambiguous and the diagnostics silently stop suggesting"
            );
        }
    }
}