(a2ml
  (metadata
    (name "Frontier Programming Practices in AffineScript — AI Edition")
    (version "2.1.0")
    (date "2026-06-04")
    (audience "Bot / LLM / Agent")
    (status "authoritative-scope-statement")
    (supersedes "v1.0.0 (defensive draft)")
    (companion "Human_Programming_Guide.adoc")
    (changelog
      (entry (version "2.1.0") (date "2026-06-04")
        (note "Added (coprocessor-integerization) stanza — six verified ReScript->AffineScript->wasm co-processor patterns from the in-flight idaptik migration (the-integer-is-the-X, re-decompose-not-transliterate, differential-parity-acceptance-bar, string-host-boundary-abi, no-module-load-side-effects, certifying-the-boundary) + named codegen gaps. Added agent-behavior/coprocessor-port-discipline. Human companions: migration-playbook.adoc §Co-processor Integerization (v1.4) + frontier-guide.adoc Chapter 9 (v1.2).")))
  )

  (thesis
    (one-sentence
      "AffineScript is JavaScript-ergonomic correctness: the developer experience of the -script family (TypeScript, ReScript, PureScript) raised to a higher correctness floor through a small, deliberately chosen set of advanced types, compiling to Typed WASM.")
    (long-form
      "AffineScript answers the question: can you keep the developer experience of modern JavaScript while raising correctness to the level of ML-family languages? The answer is yes — but only if the type system is narrowly scoped and ergonomically defaulted. The language copies the green-checkmark features from the -script lineage and adds affine types as the single novel contribution. It compiles to Typed WASM as the primary target. Refinement types are a soft commitment, limited to a decidable fragment. Dependent and tropical types live in the sibling Typed WASM project, not here.")
    (design-principle "Ergonomics first. The type system defaults must be sensible so that most code reads like modern JavaScript while correctness is enforced at compile time. A feature earns its place only if it makes correct code easier to write, not harder.")
  )

  (in-scope
    (copied-from-the-family
      (feature (name "phantom-types")         (source "purescript-rust-idris2") (cost "free-from-generics"))
      (feature (name "immutable-by-default")  (source "rescript-elm-grain")     (cost "low"))
      (feature (name "row-polymorphism")      (source "purescript-elm")         (cost "medium"))
      (feature (name "full-type-inference")   (source "hm-family")              (cost "medium")
               (note "types inferred fully; quantities inferred inside function bodies, annotated at function signatures"))
      (feature (name "sound-type-system")     (source "table-stakes")           (cost "high-verification-work"))
      (feature (name "effect-tracking")       (source "novel-in-script-family") (cost "medium")
               (note "effect types declared in function signatures; effect HANDLING is deferred — see out-of-scope"))
    )
    (novel-contribution
      (feature (name "affine-types")          (source "the-headline")           (cost "high")
               (note "no -script language has affine types; this is AffineScript's distinguishing feature"))
    )
    (soft-commitment
      (feature (name "refinement-types-decidable") (source "rust-flux-adjacent") (cost "medium")
               (note "restricted to decidable arithmetic and comparisons; compiler rejects what it cannot prove without an external solver"))
    )
  )

  (out-of-scope
    (not-here-at-all
      (feature (name "linear-strict-types")
               (reason "subsumed-by-affine")
               (bot-warning "The QTT semiring covers linear 'for free' as a byproduct of affine. This is NOT a reason to advertise linear types. Refuse any suggestion to add them as a surface feature."))
      (feature (name "unbounded-refinement-types")
               (reason "decidability-boundary")
               (note "refinement is in scope for decidable fragments only; no SMT solver required"))
      (feature (name "algebraic-effect-handlers")
               (reason "interaction-with-affine-is-unresolved")
               (note "multi-shot resume of continuations that captured affine resources is a soundness hole; handlers are deferred until the design is explicit. Effect TRACKING — declaring what a function can do — is in scope. Effect HANDLING — intercepting and redirecting effects at runtime — is not."))
    )
    (deferred-to-sibling-project
      (feature (name "full-dependent-types")
               (destination "typed-wasm-project")
               (note "previously crept into the AffineScript codebase via TDepArrow, NatVar, NatLit; being removed"))
      (feature (name "tropical-types")
               (destination "typed-wasm-project")
               (note "parasitic on ordered monoid structure; needs phantom types as foundation; belongs where the numeric hierarchy lives"))
      (feature (name "units-of-measure")
               (destination "typed-wasm-project"))
      (feature (name "shape-indexed-vectors")
               (destination "typed-wasm-project"))
    )
  )

  (directives
    (compile-target
      (primary "typed-wasm")
      (secondary "julia")
      (rule "AffineScript is a compiled language. Its semantics live in .affine source and compiled output. Do not rewrite .affine source in another language as a shortcut for refactoring, modularization, or 'improvement'."))

    (modularization
      (rule "When asked to modularize or clean up code, operate within AffineScript using the module system, record decomposition (row polymorphism), and phantom type parameters. Do not create new compiler features to solve user-code modularization problems."))

    (scope-discipline
      (rule "Before proposing any feature addition, check the in-scope list. If the feature is not listed, it is out of scope. The 'since we already have X, we can trivially add Y' framing is a scope-creep tell and must be refused regardless of how cheap the addition appears.")
      (why "Earlier sessions allowed significant scope creep: linear types, full dependent types, algebraic effect handlers were all added without being asked for. The narrow scope above is the corrected target. This document is the fix."))

    (thesis-is-authoritative
      (rule "This document is the scope statement for AffineScript. Code must conform to this document, not the reverse. If code says something this document does not, the code is wrong — not this document.")
      (applies-to "any bot, any subagent, any earlier-session-version of any agent, and any human reader including the original author"))

    (interaction-escalation
      (rule "When implementing a feature, if an interaction with another in-scope feature surfaces (affine+effects, phantom+refinement, row+inference, etc.), stop and surface the interaction for explicit user decision. Do not silently resolve interactions by picking the most convenient option."))

    (typed-wasm-boundary
      (rule "If proposed work properly belongs in the sibling Typed WASM project (numeric hierarchy, tropical semirings, dependent types, units of measure, shape-indexed vectors), flag it and do not add it to AffineScript. Typed WASM is a standalone project that AffineScript targets; it is not a subset of AffineScript."))
  )

  (idiomatic-patterns
    (note "Many features that appear 'missing' from AffineScript are expressible as combinations of in-scope features. Before proposing a new compiler feature, check whether the pattern can be written using phantom + affine + row + immutable + refinement + effect tracking. The following patterns are emergent, not implemented in the compiler.")

    (pattern
      (name "branded-identifiers")
      (composition "phantom + nominal aliases")
      (purpose "Distinguish values that share a runtime representation but must not be confused (UserId vs ProductId, Email vs Username).")
      (sketch "type UserId = Int\ntype ProductId = Int\nfn fetchUser(id: UserId): User"))

    (pattern
      (name "type-state-machines")
      (composition "phantom + affine")
      (purpose "Enforce protocols where operations are only legal in certain states (connection open/closed, transaction active/committed).")
      (sketch "type Conn<'state>\nfn open(addr: String): Conn<Open>\nfn close(c: Conn<Open>): Conn<Closed>  -- consumes c, cannot be used again"))

    (pattern
      (name "capability-types")
      (composition "phantom")
      (purpose "Tag values with what operations they are authorized for; downgrade visible in types.")
      (sketch "type Token<'cap>\nfn downgrade(t: Token<Admin>): Token<User>"))

    (pattern
      (name "extensible-records")
      (composition "row polymorphism")
      (purpose "Functions operate on any record containing the required fields; no inheritance hierarchy needed.")
      (sketch "fn damage(target: {health: Int, ..rest}, amount: Int): {health: Int, ..rest}"))

    (pattern
      (name "invariant-preservation")
      (composition "refinement types (decidable fragment)")
      (purpose "Bake simple invariants into types so the compiler enforces them (non-negative, bounded, non-empty).")
      (sketch "type PositiveInt = Int where (x >= 0)\nfn heal(h: PositiveInt, delta: PositiveInt): PositiveInt"))

    (pattern
      (name "resource-lifecycle")
      (composition "affine types")
      (purpose "Ensure resources (file handles, sockets, unique entities) are used at most once or correctly disposed.")
      (sketch "fn open(path: String): File\nfn read(f: File): (File, String)\nfn close(f: File): Unit"))

    (pattern
      (name "trait-like-dispatch")
      (composition "first-class records + row polymorphism")
      (purpose "Type-class-style overloading without type classes: pass a dictionary of operations as a record; row inference specialises.")
      (sketch "type Eq<'a> = {eq: ('a, 'a) -> Bool}\nfn contains<'a>(dict: Eq<'a>, list: List<'a>, target: 'a): Bool"))

    (pattern
      (name "purity-tagging")
      (composition "phantom + effect rows")
      (purpose "Mark values or functions as pure or impure in the type system for optimisation or testing.")
      (sketch "fn pure_compute(x: Int): Int  // no effect row = pure\nfn io_op(): String / {IO}  // effect row declares IO"))

    (pattern
      (name "per-element-error-extraction")
      (composition "polymorphic-per-element + Validation")
      (purpose "Accumulate errors from heterogeneously-typed Validation values without forcing them into a uniform list.")
      (anti-pattern "List (Validation E a) when each element has a different `a` — non-starter type-theoretically; cannot unify a single `a` across heterogeneous components.")
      (sketch "let allErrs = errsOf v1 ++ errsOf v2 ++ errsOf v3\n     where errsOf : Validation E a -> List E"))

    (pattern
      (name "fuel-based-total-recursion")
      (composition "Nat parameter + structural decrease")
      (purpose "Make recursive parsers / interpreters / search algorithms provably total without sized types.")
      (anti-pattern "Depth tracking with `if depth > N then bail` — runtime guarantee, not provable. Totality checker cannot see incrementing parameters as well-founded.")
      (sketch "fn parse(fuel: Nat, input: List Char) { match fuel { Z => Err(\"depth\"); S(f) => recurse(f, ...) } }"))

    (pattern
      (name "validation-chain-over-tuple-match")
      (composition "Validation applicative + <*>")
      (purpose "Accumulate errors across N independent extractions without N-tuple matches.")
      (anti-pattern "case (a, b, c, d, e, f, g) of (Ok, Ok, Ok, ...) => ... — N>=5 tuple matches blow type inference / ambiguity-resolution budgets.")
      (sketch "MkRecord <$> a <*> b <*> c <*> d <*> e <*> f <*> g"))

    (pattern
      (name "status-sum-vs-string-enum")
      (composition "sum types")
      (purpose "Distinguish status / kind values at compile time. Replaces stringly-typed dispatch (\"compromised\" / \"clean\" / etc.).")
      (anti-pattern "dispatch(deviceId, \"compromised\") — typo \"compromied\" silently mis-matches at runtime.")
      (sketch "type DeviceStatus = Compromised | Clean | Suspect | Unknown\nfn dispatch(d: DeviceId, s: DeviceStatus)"))
  )

  (coprocessor-integerization
    (note "Six verified, named patterns for the ReScript -> AffineScript -> wasm co-processor port. The human companion is migration-playbook.adoc §'Co-processor Integerization' (#coprocessor-integerization) and frontier-guide.adoc Chapter 9. Grounded ONLY in the in-flight idaptik migration; every claim below is observed, not aspirational. Load at session start before any .res -> .affine kernel translation.")
    (mental-model
      (one-sentence "AffineScript is the brain (logic, physics, decisions, integer math in wasm); JS/Pixi are the senses (rendering, input, strings, dispatch); only raw primitives (Int, Float, Array) cross the wasm boundary.")
      (source "Human_Programming_Guide.adoc §'The WASM Co-processor Model'"))

    (pattern
      (name "the-integer-is-the-X")
      (slogan "the integer IS the X")
      (composition "host-side encoder + pure-integer wasm kernel")
      (purpose "Encode a host-side enumerated/stringly value to a canonical integer; compute on the integer in a pure-Int AffineScript wasm core; keep strings, state, and dispatch host-side.")
      (verified-example "idaptik SecurityRank.affine — header says 'the encoding IS the rank'; ranks Open/Weak/Medium/Strong map to canonical 0/1/2/3; rank_security_level clamps out-of-band into the band; passes_min_security is an integer comparison. Compiled host-native to a 454-byte wasm; exports rank_security_level + passes_min_security; only WASI import is fd_write.")
      (split-rule "Host keeps: string literals + name<->code table + mutable state + dispatch on the decoded value (the senses). Wasm gets: integer codes + arithmetic + pure decision logic (the brain). idaptik PortNames states it: 'The host keeps the port string literals and the suffix concatenation; only integers cross the boundary… Host-side string layer (the senses half).'")
      (sketch "pub fn rank_security_level(v: Int) -> Int { if v < 0 { 0 } else { if v > 3 { 3 } else { v } } }\npub fn passes_min_security(actual: Int, required: Int) -> Bool { rank_security_level(actual) >= rank_security_level(required) }"))

    (pattern
      (name "re-decompose-not-transliterate")
      (composition "the Cardinal Rule at co-processor scale")
      (purpose "The .affine shape differs from the .res shape even for 'the same' function; re-decompose around what the wasm boundary and current codegen support, never 1:1 port.")
      (verified-example "Kernel_Compute.affine is a real re-decomposition of Kernel_Compute.res. Six deltas: (a) incidental promise<executionResult> collapsed to settled ExecutionResult; (b) service-locator global ResourceAccounting.getDeviceState replaced by threading the one consumed slice (activeCalls) as an explicit parameter; (c) process-global backend registry replaced by an explicitly-passed Registry; (d) option<string> replaced by a closed ResultMessage enum; (e) Int.toString (NO wasm intrinsic) replaced by a hand-laddered divmod integer renderer; (f) wire-message strings kept byte-identical.")
      (second-verified-example "Kernel_IO decodes ASCII bytes to a String then String.startsWith for a sandbox check. Re-decomposition keeps the data as an Int array and does an integer-array prefix comparison — the decode-to-String step is DELETED, not ported, sidestepping the string-backend gap (the data already IS an int array).")
      (reframing-rule "When a stdlib function has no wasm lowering (e.g. Int.toString), do not ask 'how do I make it work'; ask 'what is the integer computation underneath it' and emit that. The externally observable output (wire bytes) must NOT move — only the internal shape changes."))

    (pattern
      (name "differential-parity-acceptance-bar")
      (composition "host-native compile + independent oracle sweep")
      (purpose "Acceptance = build-green AND parity-green. Build-green alone (compiler accepted it) is NOT acceptance.")
      (procedure "1. Compile host-native: affinescript compile FILE -o OUT.wasm (or dune exec affinescript -- compile …). 2. Instantiate (WASI fd_write stub). 3. For each input in a sweep INCLUDING out-of-band values, compare wasm output against an INDEPENDENT oracle written from the documented contract (not from the .affine). Any mismatch fails.")
      (verified-example "SecurityRank harness ran 78/78 green against a JS oracle of the documented 0..3 contract.")
      (host-native-warning "CRITICAL: existing idaptik harnesses (vm/tests/*_diff.ts) still shell out to a RETIRED idaptik-affinescript-build:latest podman container. The 2026-06-03 reanchor went host-native (guix/nix dev shell + affinescript on PATH). NEW harnesses must call the host compiler directly — do NOT copy the container invocation. Replace it with the host-native form.")
      (why-independent "An oracle generated from the same .affine only proves the compiler is deterministic, not that the kernel is correct. The oracle must encode the human-readable contract so disagreement localises a real bug (wrong .affine, wrong oracle, or ambiguous spec)."))

    (pattern
      (name "string-host-boundary-abi")
      (composition "wire format + exported allocator + host helpers + extern/pub")
      (purpose "When a string MUST cross the wasm boundary (rare — prefer integerization), honour this ABI. Reach for it only when integerization genuinely cannot avoid it.")
      (wire-format "[len: i32 little-endian][utf8 bytes]. No trailing NUL; the length is authoritative.")
      (allocator "Codegen exports __affine_alloc(n: i32) -> i32, a bump allocator, so the host reserves space correctly. This REPLACED an earlier fixed scratch offset of 8192 that could collide with literal/heap storage. If you see 8192 hard-coded as a scratch base in an older harness, that is the superseded pattern.")
      (host-helpers "readString(mem, ptr) / writeString(mem, at, s); instantiate with a WASI fd_write stub.")
      (callbacks-and-exports "extern fn host_x(s: String) -> String;  lets wasm call back into the host (wasm->host). pub fn exports an AffineScript function to the host (host->wasm). Same extern machinery as effects-migration-stance.adoc, carrying strings rather than effects.")
      (sketch "extern fn host_x(s: String) -> String;\npub fn handle(s: String) -> String { /* … */ }"))

    (pattern
      (name "no-module-load-side-effects")
      (composition "explicit threading / registration")
      (purpose "AffineScript modules do not run code at load time; there is no implicit module-load hook in wasm. Thread state/time/log as explicit parameters (or registration functions) until extensible-effect codegen lands; do NOT rely on module-load side effects.")
      (verified-counter-example "Kernel_Quantum carries module-level mutable Dict state (lastQuantumOp), Date.now, and Console.log. All three hit the EFFECT-CODEGEN WALL: Kernel_Compute.affine documents that effect codegen 'references an unbound binding' and cannot yet lower to wasm. A faithful port of Kernel_Quantum therefore does not compile to wasm.")
      (re-decomposition "Module-level mutable Dict -> pass state in and return updated state out (own QuantumState). Date.now() -> a now: Int parameter supplied by the host. Console.log(…) -> an extern fn host_log(s: String); the host implements, or a returned log payload. Do not log from inside the kernel.")
      (cross-ref "Mirrors agent-behavior/no-side-effect-imports and effects-migration-stance.adoc. Forward-compatible: threaded parameters become the obvious State/IO handler sites when effect handling ships.")
      (anti-pattern "let _ = X.foo() at module scope; ambient Date.now()/Console.log inside a kernel intended for wasm."))

    (pattern
      (name "certifying-the-boundary")
      (composition "machine-checked encoding-faithfulness theorem")
      (purpose "The integerization is not just convention — it is a theorem. 'the integer IS the X' is precisely an encoding-faithfulness theorem, machine-checked.")
      (proof-artefact "echo-types module EchoEncodingFaithfulness.agda, staged at proposals/echo-types/ in THIS repo. Under --safe --without-K, zero postulates. Proves: an INJECTIVE host-boundary encoder is lossless ('the integer IS the X'); a COLLISION/CLAMP is provable, localised loss with no decoder (clamp-sentinel-no-section).")
      (security-nuance "SecurityRank's Invalid(v) => if v<0 {0} else {3} clamp is exactly that certified hazard. The clamp DIRECTION is a real security decision for a min-security filter: clamping high garbage UP to Strong (3) is FAIL-OPEN (malformed privileged input treated as maximally privileged); clamping low garbage DOWN to Open (0) is FAIL-CLOSED (malformed input treated as least privileged). For passes_min_security, fail-closed is the safe default. The proof tells you the loss is localised and decoderless; the security review tells you which direction it must go.")
      (division-of-labour "The proof certifies the encoding model carries no hidden collision (faithful IN PRINCIPLE). It says NOTHING about whether the deployed wasm bit-for-bit implements the encoding — that is the differential-parity harness's job (faithful IN PRACTICE). Neither subsumes the other; stated in the module as a checked NotProved-wasm-implements-enc alias.")
      (cross-ref "proposals/echo-types/README.adoc"))

    (codegen-gaps-named
      (note "These are LOAD-BEARING teaching points surfaced by the verified migration, not embarrassments. Do not paper over them; re-decompose around them.")
      (gap (name "Int.toString-no-wasm-intrinsic") (workaround "hand-laddered divmod integer renderer, per Kernel_Compute.affine delta (e)"))
      (gap (name "String-backend-incomplete-on-wasm") (workaround "keep data as Int arrays where possible (Kernel_IO int-array prefix check); honour the String ABI only when a string must genuinely cross"))
      (gap (name "effect-codegen-references-unbound-binding") (workaround "thread state/time/log as explicit parameters; do not rely on effect handling lowering to wasm yet (Kernel_Quantum counter-example)"))))

  (agent-behavior
    (session-startup
      (rule "Read this document at the start of every AffineScript session. It is the authoritative scope statement."))
    (read-first
      (rule "Before editing any file in the AffineScript repository, read this document or verify its content matches your understanding of scope."))
    (defer-on-scope
      (rule "When asked to add a feature not in the in-scope list, stop and ask the user before proceeding. Do not defer to what 'the code implies' or what 'an earlier session decided' — those are the failure modes that trashed the project before."))
    (no-silent-extension
      (rule "Do not add compiler features to resolve user-code ergonomics problems. Use the existing type system more creatively instead, and document new patterns in the idiomatic-patterns section of this file."))
    (no-free-variant-additions
      (rule "If you notice that the implementation 'already supports' a feature adjacent to one in scope (linear-because-affine, dependent-because-refinement, multi-shot-because-handlers), you may NOT expose that feature on the language surface without explicit user approval. Implementation support is not the same as user-facing scope."))
    (escalate-on-interaction
      (rule "When two in-scope features interact in a way that forces a design decision, stop and surface the interaction for explicit user decision."))
    (typed-wasm-boundary
      (rule "Work that belongs in the Typed WASM project goes to the Typed WASM project. Never add it to AffineScript as a shortcut."))
    (honesty-in-documentation
      (rule "STATE.a2ml, README, and related docs must reflect what the code actually enforces, not what it aspires to. A 'dune build passing' is not a behavioural test and does not count as 'feature complete'."))
    (stdlib-floor
      (rule "Validation[E,A], Result[E,A], Option[A], and Either[E,A] are stdlib floor. Reject any project that re-implements them. Pointing migrators at canonical types prevents the heterogeneous-list-as-homogeneous antipattern."))
    (eta-style
      (rule "When a function has named pi-binders ((name : Type) -> ...), prefer eta-expanded definitions over point-free. Point-free is a parser-cascade trigger when the eta-rewrite's argument name disagrees with the declared signature's name."))
    (no-side-effect-imports
      (rule "AffineScript modules do not run code at load time. Migrating ReScript/JS code that uses `let _ = X.foo` for module-load side effects must be rewritten to call an explicit registration function. There is no implicit module-load-time side effect to hook in wasm."))
    (coprocessor-port-discipline
      (rule "Before any .res -> .affine kernel translation aimed at wasm, load the (coprocessor-integerization) stanza above. Apply the integerization patterns: encode host values to integers and keep strings/state/dispatch host-side; re-decompose (do not transliterate); gate on differential parity against an INDEPENDENT oracle compiled HOST-NATIVE (not the retired podman container); thread state/time/log explicitly because modules have no load-time side effect; honour the String ABI only when a string must genuinely cross.")
      (anti-pattern "1:1 porting a ReScript kernel; copying vm/tests/*_diff.ts container invocations; calling Int.toString or relying on module-load mutation / Date.now / Console.log inside a wasm-targeted kernel; treating build-green as acceptance.")))
  )

  (relationship-with-typed-wasm-project
    (role "AffineScript compiles to Typed WASM as its primary target.")
    (shared-infrastructure "phantom types — the bridging abstraction. The same phantom mechanism that tags UserId vs ProductId in AffineScript will tag Meter<Int> vs Second<Int> and Tropical<Int> vs Standard<Int> in Typed WASM.")
    (independence "Typed WASM is designed as a legitimate compile target for languages beyond AffineScript. It is not an AffineScript subsystem. Other languages can and should target it.")
    (deferred-features "dependent types, tropical types, units of measure, shape-indexed vectors, and any advanced type-level computation live in Typed WASM, not in AffineScript.")
    (secondary-backend "Julia — secondary compile target for scientific computing use cases where AffineScript's type safety complements Julia's numeric ecosystem.")
  )

  (faces
    (note "AffineScript is one core, many faces. A face is a sugared surface syntax that lowers to the canonical AST via a single OCaml module in lib/<name>_face.ml. Established faces: canonical, RattleScript (Python-like), JaffaScript (JS/TS-like), PseudoScript (pedagogy), LucidScript (PureScript/Haskell-like), CafeScripto (CoffeeScript-like). The rules below were earned during the May-2026 LucidScript+CafeScripto buildout and the eject of the five non-canonical faces into top-level brand-surface repos at github.com/hyperpolymath/{rattlescript,jaffascript,pseudoscript,lucidscript,cafescripto}.")

    (rules
      (brand-surface-not-fork
        (rule "A face is a transformer module in this repo PLUS a brand repo carrying README, examples, justfile, and a thin shim that defaults --face. The brand repo MUST NOT carry a parser, type checker, borrow checker, codegen, or vendored copy of the canonical compiler. There is exactly one compiler.")
        (shim-pattern "#!/usr/bin/env bash\nif [ $# -eq 0 ]; then\n  exec affinescript\nfi\nsubcmd=\"$1\"\nshift\nexec affinescript \"$subcmd\" --face <face_name> \"$@\"")
        (anti-pattern "Cargo.toml, vendored affinescript/ submodule, separate <face>script binary, per-face parser. Each is a divergence vector that forces re-porting every canonical change to every face."))

      (identity-in-content
        (rule "Every face uses the canonical .affine extension. Face is selected by a face: pragma on the first comment line (# face:, // face:, -- face:, (* face: *)) or by --face on the CLI. Per-face extensions (.rattle, .pyaff, .jsaff) were tried and abandoned.")
        (why "Single extension means build tools, editors, and LSP configure once. File renames don't change semantics. Polyglot directories don't need per-face globs.")
        (deprecation-path "Extension dispatch is preserved in resolve_face for migration only; pragma takes precedence and emits a deprecation warning when extension dispatch is used."))

      (snapshot-plus-roundtrip
        (rule "Every face transformer ships with a snapshot test PLUS a round-trip parse. The snapshot diffs preview-<face> output against tests/faces/<base>.expected.txt; the round-trip parses that output as canonical to confirm grammar legality. Either alone is insufficient; visual review without round-trip parse missed five distinct transformer bugs in the May-2026 buildout.")
        (harness "tools/run_face_transformer_tests.sh — modes: check (default), --update (rewrite snapshots), --record-missing (bootstrap)")
        (just-recipes "test-faces, test-faces-update, test-faces-record")
        (generalization "Any pure text-to-text transformer in the codebase (formatter, codegen text emitters, source rewriters) deserves the same harness. The pattern is not face-specific."))

      (examples-are-tests
        (rule "Every file under examples/faces/ MUST round-trip clean through the snapshot test. If an example exercises a known transformer gap and won't round-trip, simplify the example to use the documented workaround — do not ship a broken example with a hand-wave.")
        (gap-disclosure "Known transformer gaps go in examples/faces/README.adoc under 'Caveats' → 'Known transformer gaps' with one-line workaround. The example file uses the workaround. Users find gaps via docs, not via confused parse errors on their own code.")
        (consequence-of-violation "An example that breaks round-trip but ships in the README is a documented falsehood. README and test suite must agree, or one is lying."))

      (error-vocabulary-cost
        (rule "Adding a face means adding one arm per user-facing error kind to lib/face.ml's format_*_for_face dispatch tables. There is no shortcut. Face-shaped vocabulary is part of the face contract: a Haskell-shaped face must not say 'borrow checker rejected this binding'; a CoffeeScript-shaped face must not reference 'do notation' that the surface lacks.")
        (cost-shape "O(faces × error_kinds). Currently 6 faces × ~12 error formatters; growth in either dimension multiplies.")
        (drift-symptom "If the dispatch table lags the parser/checker, the face leaks canonical-flavoured errors and the brand promise erodes. Fix is mechanical, not creative — keep the table in sync."))

      (span-fidelity-honesty
        (rule "A face transformer rewrites user source; subsequent error line numbers refer to the transformed canonical text, not the user's original face source. This is a genuine UX regression. Document it in the face's brand-repo README under 'Caveats'. Do NOT paper over it with phantom span improvements that don't actually translate locations.")
        (honest-options
          "1. document the regression in the brand-repo README (chosen for all 5 non-canonical faces, May-2026, alpha status)"
          "2. have the transformer record a canonical→source span map and have the diagnostic layer translate"
          "3. build the face into the parser proper (cmdliner already supports --face), trading parser complexity for span fidelity")
        (escalation "When promoting a face from alpha to beta, revisit the choice. The current option-1 stance is honest for alpha; for beta, option-2 is the minimum honest improvement.")))

    (when-adding-face-N+1
      (step (n 1) (do "create lib/<name>_face.ml; clone the closest existing face module (python_face.ml for indent-based; js_face.ml for brace-based; pseudocode_face.ml for keyword-heavy) and substitute the lowering rules"))
      (step (n 2) (do "register the face in lib/face_pragma.ml's alias table and lib/face.ml's Face variant"))
      (step (n 3) (do "extend bin/main.ml: parse_with_face arm, fmt_file arm, face_arg cmdliner enum, preview-<name> subcommand"))
      (step (n 4) (do "add the face name to lib/dune modules list"))
      (step (n 5) (do "build: opam exec -- dune build lib/ bin/"))
      (step (n 6) (do "create examples/faces/hello-<name>.affine; iterate until snapshot+roundtrip pass via just test-faces-record then just test-faces"))
      (step (n 7) (do "extend lib/face.ml format_*_for_face dispatch tables — one arm per error kind, in face-appropriate vocabulary"))
      (step (n 8) (do "ship the brand repo: README.adoc + LICENSE + CONTRIBUTING.md + bin/<face> shim + justfile + examples/hello.affine + .gitignore. NO Cargo.toml, NO vendored compiler, NO separate binary"))
      (step (n 9) (do "add link to the brand repo in this repo's README.adoc faces list"))
      (step (n 10) (do "document any transformer gap in examples/faces/README.adoc 'Known transformer gaps'; if the gap is severe enough, demote the example to a workaround variant rather than ship a broken round-trip")))

    (known-limitations
      (limitation
        (name "transformer-span-loss")
        (since "0.1.0")
        (impact "Diagnostic line numbers refer to transformed canonical, not original face source. Acceptable for alpha; must be addressed before beta. See span-fidelity-honesty rule for the three escalation options.")
        (recommendation "Document in each brand-repo README until span-map translation lands."))

      (limitation
        (name "deferred-face-transformations")
        (since "0.1.0")
        (impact "Several Python/Haskell/Coffee idioms are not yet lowered: multi-clause def-bodies, do-notation, where-blocks, list comprehensions, splats, no-paren calls. These need AST-level rewrites rather than text-level substitution and are deferred to a face-AST-rewriter milestone.")
        (recommendation "When sweeping the gap list, skip these categories — they require an AST-rewriter pass that doesn't exist yet, not just sugar additions to the existing text transformer.")
        (tracked-in "examples/faces/README.adoc → Known transformer gaps"))

      (limitation
        (name "no-face-aware-formatter")
        (since "0.1.0")
        (impact "affinescript fmt produces canonical output regardless of input face. Round-tripping face → canonical → face is not supported; once you canonicalize, you lose the face presentation.")
        (recommendation "Treat face source as the human-edited form and canonical as the build-time artifact. Do not run fmt on face files until per-face formatters exist.")))
  )

  (backends
    (note "AffineScript is one frontend, many backends. There is no middle-end IR — each backend lowers AST→target directly. Adding a new backend is templated work; the lessons below were earned during the May-2026 backend buildout (JS/C/WGSL/Faust/ONNX/Rust/LLVM/OCaml/Lua/Bash/Nickel/ReScript/Verilog/Gleam landed alongside the pre-existing WASM/WASM-GC/Julia).")

    (tiers
      (tier
        (name "A — host-language emitters")
        (members "WASM 1.0, WASM-GC, Julia, JS, C, Rust, OCaml, ReScript, Lua, Bash, Gleam, Nickel")
        (acceptance "full AffineScript surface; missing features error loudly at codegen time")
        (file-shape "lib/<name>_codegen.ml, ~200-400 lines, single-file output with a tiny inline runtime prelude"))
      (tier
        (name "B — IR / native")
        (members "LLVM IR (Cranelift candidate)")
        (acceptance "full surface, lower level of abstraction; some passes (closures, traits) need lowering that may not exist yet")
        (file-shape "SSA-shaped emitter; needs current-block tracking for phi predecessors"))
      (tier
        (name "C — kernel sublanguages")
        (members "WGSL, SPIR-V (planned), CUDA C++ (planned), Metal (planned), OpenCL (planned), Faust, ONNX, MLIR/StableHLO (planned)")
        (acceptance "DELIBERATELY restricted subset; everything outside that subset must reject explicitly via `unsupported`")
        (file-shape "validation pre-pass on the AST shape (e.g., 'first param is Int', 'all types are Float'), then targeted lowering")))

    (rules
      (clone-don't-redesign
        (rule "When adding a new backend, clone the closest existing emitter and substitute lowering rules. Do NOT redesign the codegen module shape from scratch — every backend after the first cost ~200 lines because the modules are isomorphic.")
        (template-pairs
          "JS/C/Lua/Rust/OCaml/ReScript/Gleam/Nickel/Bash → julia_codegen.ml"
          "WGSL/SPIR-V/CUDA/Metal/OpenCL → wgsl_codegen.ml"
          "Faust/Csound → faust_codegen.ml"
          "ONNX/MLIR/TFLite → onnx_codegen.ml"
          "Verilog/VHDL → verilog_codegen.ml"))

      (use-shared-kernel-sublang-module
        (rule "Every Tier-C kernel sublanguage backend opens [Kernel_sublang] and reuses its primitives instead of inlining its own. The module provides: the [Unsupported] exception (one shared type, not per-backend), [pick_entry ?names], [strip_ownership], [is_unit_ty], [array_element], [require_array_element], [math_builtins], [is_math_builtin], [validate_return], [validate_params], and [validate_compute_kernel_shape] (the canonical 'first param Int, rest Array buffers, returns Unit' predicate used by WGSL/SPIR-V/CUDA/Metal/OpenCL).")
        (location "lib/kernel_sublang.ml")
        (extend-not-fork "If a new Tier-C backend needs an intrinsic the shared math_builtins list lacks, add it to that list rather than maintaining a parallel allowlist. If a new validation rule emerges that two or more backends need, factor it into Kernel_sublang rather than copying."))

      (validate-end-to-end
        (rule "Every shipped backend must round-trip through the target's reference toolchain. Bytes a human read are not bytes that work. The honest MVP claim is 'the bytes round-trip,' not 'the bytes look right.'")
        (validators "naga (WGSL), faust (Faust), oxionnx-proto (ONNX), cc (C), rustc (Rust), ocaml (OCaml), lua5.4 (Lua), bash -n (Bash), nickel typecheck (Nickel), llc (LLVM), node/deno (JS)")
        (when-no-validator "If the target has no installable validator in the dev environment, document that explicitly in the recap. Do not claim end-to-end validation by inspection alone."))

      (single-file-no-runtime
        (rule "Generated output is one file. Tiny runtime preludes (Some/None constructors, println shims, a few factories) go INLINE at the top of the emitted file, not as a separate runtime.h / runtime.lua / runtime.rs. This dodges version-skew bugs that would otherwise haunt every compile."))

      (loud-failures
        (rule "Surface unsupported source shapes with `raise (X_unsupported \"...\")` so the codegen errors at compile time. Never emit silently miscompilable output. A loud `unsupported` is the load-bearing API contract of every backend."))

      (frontend-gaps-don't-paper-at-backend
        (rule "If a backend can't lower something the surface syntax accepts, fix the frontend or document the restriction. Do NOT compensate at the backend layer — that produces silent miscompilation later. Examples from the May-2026 buildout: the typechecker's monomorphic-Int restriction (fixed in typecheck.ml) and the borrow checker's missing mut-param seeding (fixed in borrow.ml) both first surfaced as 'WGSL won't accept Float kernels' / 'Faust SAXPY won't borrow-check'; both were frontend bugs masquerading as backend limitations."))

      (stubs-as-intrinsics
        (rule "When a target has runtime ops AffineScript's resolver doesn't know about (CUDA's __shfl_xor, ONNX's Relu, Faust's tanh, Verilog's $display), declare them in user code as stub functions and recognise them by name in the codegen. The stub body is a placeholder; the resolver passes; the backend pattern-matches. This is the canonical way to smuggle target intrinsics through a universal frontend.")
        (example "fn relu(x: Array[Float]) -> Array[Float] { x }  // stub for ONNX Relu")
        (rationale "Avoids extending the language for every target's vocabulary; keeps each backend self-contained.")))

    (when-adding-backend-N+1
      (step (n 1) (do "identify the tier (A/B/C above); pick the template emitter from the tier's template-pairs list"))
      (step (n 2) (do "create lib/<name>_codegen.ml as a clone with target-specific lowering rules; reuse mangle / type-mapping / gen_lit shape"))
      (step (n 3) (do "for Tier-C backends: open Kernel_sublang and use its shared primitives (pick_entry / strip_ownership / array_element / math_builtins / Unsupported) instead of inlining your own"))
      (step (n 4) (do "register the module in lib/dune (alphabetical) and add `.<ext>` dispatch in bin/main.ml (both --json and human branches)"))
      (step (n 5) (do "build: `eval $(opam env --switch=default) && dune build lib bin/main.exe`"))
      (step (n 6) (do "validate end-to-end with the target's reference toolchain on /tmp/cdemo.affine (the canonical Int/branch/let test); record the result in the recap"))
      (step (n 7) (do "for tier-C kernel backends: also test Float arithmetic, since the typechecker patch landed 2026-05-02"))
      (step (n 8) (do "for Tier-A backends: also test the Phase-4 trio (tuple p4a, record p4b, variant+match p4c) end-to-end; emit `Unsupported` loudly for any shape outside scope"))
      (step (n 9) (do "document any frontend gap surfaced by the work in this AI.a2ml file under (backends → known-limitations); fix it BEFORE the next adaptive task per the corrective-before-adaptive ordering rule")))

    (known-limitations
      (limitation
        (name "wasm-stdin-and-exit-propagation")
        (since "0.1.0")
        (resolved "partial — 2026-05-03")
        (impact
          "WASM Phase 6 is now end-to-end on wasmtime — println of string literals works for any number of calls; the heap-alignment trap that surfaced after the first println was an iovec-layout bug in [wasi_runtime.gen_println] (1-byte newline placed before a 4-byte iovec, causing an unaligned i32.store at temp+1). Fixed by reordering the layout so the iovec sits at temp+0 (aligned) and the newline byte moves to temp+12. Total allocation rounded from 13 to 16 bytes so subsequent allocs stay aligned.")
        (deferred
          "Three pieces remain for a fully Phase-6-and-7 WASM backend, all in [lib/wasi_runtime.ml] / [lib/codegen.ml]:
           (1) [fd_read] import + [gen_read_line] helper — mirror [create_fd_write_import] / [gen_print_str]; the line-reader needs a byte-loop because [fd_read] returns N bytes, not lines (~80 lines of wasm IR);
           (2) Exit-code propagation — replace the [(start $main)] directive with an exported [_start] that calls [main] then [proc_exit(retval)] via a [wasi_snapshot_preview1.proc_exit] import. Side effect: wasmtime runs without [--invoke main] and exit codes flow naturally;
           (3) Real [OpConcat] (currently a placeholder [I32Add]) — needs allocation + byte-copy. Without [I32Load8U]/[I32Store8] in [lib/wasm.ml]'s instruction set the byte loop is awkward; consider adding bulk-memory ([memory.copy], opcode 0xfc 0x0a) to the AST first.
           Estimated scope: 2-3 hours of focused WASM work in the 1900-line backend, additive to the existing helper structure.")
        (recommendation
          "Tackle exit-code propagation first — it's the smallest of the three (one new import, swap [(start)] for [(_start)], propagate a single i32 to proc_exit) and unblocks running WASM programs without the [--invoke main] flag. Then add [fd_read]/[gen_read_line]. [OpConcat] last; consider extending [wasm.ml] with [MemoryCopy] before writing the helper."))

      (limitation
        (name "no-target-intrinsic-protocol")
        (since "0.1.0")
        (impact "Each backend keeps its own ad-hoc builtin allowlist (sin/cos/relu/etc). User stubs work as a workaround per the stubs-as-intrinsics rule, but unifying behind @target(\"foo\") fn declarations would let one stdlib serve all backends. Perfective task; not blocking.")
        (recommendation "Don't extend per-backend builtin lists ad-hoc; if a target needs a new op, prefer the stub-recognise-by-name pattern."))

      (limitation
        (name "no-tensor-type")
        (since "0.1.0")
        (impact "The frontend has no Tensor[T, S]; ONNX is currently a 'proof of wire format' rather than a real ML target. Adding tensor types is multiple weeks of frontend work and belongs in (out-of-scope deferred-to-sibling-project) until/unless a user demands it.")
        (recommendation "When ONNX scope expands, file the request and check (out-of-scope) before extending."))

      (limitation
        (name "kernel-sublangs-share-no-validator")
        (since "0.1.0")
        (resolved "0.1.1 — 2026-05-03")
        (impact "Originally each Tier-C backend kept its own restriction predicate; resolved by factoring the shared primitives into [lib/kernel_sublang.ml]. ~140 lines of duplicated boilerplate removed across 7 backends; the module exposes Unsupported, pick_entry, strip_ownership, is_unit_ty, array_element, require_array_element, math_builtins, validate_compute_kernel_shape, etc.")
        (recommendation "Use Kernel_sublang for any new Tier-C backend; extend the module rather than fork its rules.")))
  )
)
