From 44da38a7146a726da7395c0f2a3368da79c60da3 Mon Sep 17 00:00:00 2001 From: hyperpolymath <6759885+hyperpolymath@users.noreply.github.com> Date: Tue, 7 Jul 2026 00:46:23 +0100 Subject: [PATCH 1/2] refactor(descriptiles): migrate .machine_readable/6a2 -> descriptiles (estate naming canon) Estate naming mandate (2026-06-30): the machine-readable metadata family is `descriptiles`; `6a2` is fully deprecated and must never be reintroduced. `rsr-template-repo` already led this rename (PR #128). This brings AffineScript into line. Changes (mechanical, word-boundaried `\b6a2\b` -> `descriptiles`, so checksum substrings are untouched; Cargo.lock hash coincidences excluded): - `git mv .machine_readable/6a2 .machine_readable/descriptiles` (6 .a2ml files) - updated the one code dependency: tools/check-doc-truthing.sh STATE_FILE path - updated .machine_readable/TRUST.contractile deny-rule path - updated ~30 doc/metadata references (paths + "6a2 metadata" family-name prose) Verified (dune 3.24.0, system OCaml 4.14.1 + opam switch as-verify): - `dune build` = exit 0 (no build impact) - zero `\b6a2\b` references remain (excl. Cargo.lock checksums) - check-doc-truthing.sh reads the renamed STATE path correctly Two doc gates (README->SOUNDNESS.adoc, README->CAPABILITY-MATRIX.adoc) are RED on main independently of this change (README untouched here); tracked separately. Co-Authored-By: Claude Opus 4.8 (1M context) --- .machine_readable/descriptiles/AGENTIC.a2ml | 53 + .machine_readable/descriptiles/ECOSYSTEM.a2ml | 27 + .machine_readable/descriptiles/META.a2ml | 1616 +++++++++++++++++ .machine_readable/descriptiles/NEUROSYM.a2ml | 21 + .machine_readable/descriptiles/PLAYBOOK.a2ml | 74 + .machine_readable/descriptiles/STATE.a2ml | 258 +++ 6 files changed, 2049 insertions(+) create mode 100644 .machine_readable/descriptiles/AGENTIC.a2ml create mode 100644 .machine_readable/descriptiles/ECOSYSTEM.a2ml create mode 100644 .machine_readable/descriptiles/META.a2ml create mode 100644 .machine_readable/descriptiles/NEUROSYM.a2ml create mode 100644 .machine_readable/descriptiles/PLAYBOOK.a2ml create mode 100644 .machine_readable/descriptiles/STATE.a2ml diff --git a/.machine_readable/descriptiles/AGENTIC.a2ml b/.machine_readable/descriptiles/AGENTIC.a2ml new file mode 100644 index 00000000..890ad441 --- /dev/null +++ b/.machine_readable/descriptiles/AGENTIC.a2ml @@ -0,0 +1,53 @@ +# SPDX-License-Identifier: MPL-2.0 +# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) +# +# AGENTIC.a2ml — AI agent constraints and capabilities +[metadata] +version = "0.1.0" +last-updated = "2026-04-11" + +[agent-permissions] +can-edit-source = true +can-edit-tests = true +can-edit-docs = true +can-edit-config = true +can-create-files = true + +[agent-constraints] +# What AI agents must NOT do: +# - Never use banned language patterns (believe_me, unsafeCoerce, etc.) +# - Never commit secrets or credentials +# - Never use banned languages (TypeScript, Python, Go, etc.) +# - Never place state files in repository root (must be in .machine_readable/) +# - Never use AGPL license (use MPL-2.0) + +[maintenance-integrity] +fail-closed = true +require-evidence-per-step = true +allow-silent-skip = false +require-rerun-after-fix = true +release-claim-requires-hard-pass = true + +[automation-hooks] +# on-enter: Read 0-AI-MANIFEST.a2ml, then STATE.a2ml +# on-exit: Update STATE.a2ml with session outcomes +# on-commit: Run just validate-rsr + +# ── Disambiguation ──────────────────────────────────────────────────────── +# AI agents have repeatedly conflated this repo (AffineScript) with the +# unrelated language Ephapax (hyperpolymath/ephapax). The two share exactly +# one thing — both target hyperpolymath/typed-wasm — and otherwise have +# different ASTs, type checkers, compilers, and proof stories. The shared +# `affine` syllable names a substructural-logic property in BOTH cases +# (it's a logic-family name), not a project relationship. Ephapax is itself +# dyadic and contains an `ephapax-affine` *sublanguage* — that sublanguage +# is NOT AffineScript. +[disambiguation] +this = "hyperpolymath/affinescript" +canonical-name = "AffineScript" +distinct-from = ["hyperpolymath/ephapax"] +shared-with-distinct = ["hyperpolymath/typed-wasm"] +ephapax-affine-sublanguage-is-not-affinescript = true +canonical-doc = "https://github.com/hyperpolymath/nextgen-languages/blob/main/docs/disambiguation/ephapax-vs-affinescript.md" +canonical-memory-entry = "feedback_affinescript_ephapax_siblings_not_impl_proof.md" +do-not-apply-cross-repo-lessons-without-explicit-check = true diff --git a/.machine_readable/descriptiles/ECOSYSTEM.a2ml b/.machine_readable/descriptiles/ECOSYSTEM.a2ml new file mode 100644 index 00000000..4a29adf0 --- /dev/null +++ b/.machine_readable/descriptiles/ECOSYSTEM.a2ml @@ -0,0 +1,27 @@ +# SPDX-License-Identifier: MPL-2.0 +# ECOSYSTEM.a2ml — Ecosystem position +# Converted from ECOSYSTEM.scm on 2026-03-21 + +[metadata] +project = "affinescript" +ecosystem = "hyperpolymath" + +[position] +type = "programming-language" +purpose = "A systems programming language combining affine types, dependent types, row polymorphism, and extensible effects, compiling to WebAssembly" + +[pipeline] +position = "co-product" +chain = "katagoria → typell → typed-wasm → PanLL" +notes = "AffineScript compiles to typed WasmGC. TypeLL grounds its type system. typed-wasm provides the shared binary layout and ABI conventions." +coordination = "nextgen-typing" + +[related-projects] +projects = [ + { name = "typell", relationship = "type-theory-foundation", notes = "TypeLL's open-ended progressive framework grounds AffineScript's type system. typell-affinescript bridge crate." }, + { name = "typed-wasm", relationship = "aggregate-library", notes = "Shared binary layout conventions for WasmGC cross-language calls. AffineScript Option[T]/Result[T,E]/String have agreed layouts here." }, + { name = "nextgen-typing", relationship = "coordination-parent", notes = "Coordination monorepo for the type theory pipeline. Canonical architecture doc." }, + { name = "ephapax", relationship = "sibling-wasm-target", notes = "Both compile to typed WasmGC. Independent designs; converge at binary level via typed-wasm." }, + { name = "katagoria", relationship = "research-upstream", notes = "Type theory research origination. Future AffineScript type system extensions may originate there." }, + { name = "panll", relationship = "consumer", notes = "AffineScript is one of PanLL's development language targets." }, +] diff --git a/.machine_readable/descriptiles/META.a2ml b/.machine_readable/descriptiles/META.a2ml new file mode 100644 index 00000000..81a323b5 --- /dev/null +++ b/.machine_readable/descriptiles/META.a2ml @@ -0,0 +1,1616 @@ +# SPDX-License-Identifier: MPL-2.0 +# META.a2ml — Project meta-information +# Converted from META.scm on 2026-03-21 + +[metadata] +project = "affinescript" +author = "Jonathan D.A. Jewell " +license = "MPL-2.0" +standard = "RSR 2026" + +# ────────────────────────────────────────────────────────────────────────────── +# Architecture Decision Records +# ────────────────────────────────────────────────────────────────────────────── +# ADRs are numbered sequentially and never renumbered. Superseded ADRs remain +# in place with status = "superseded" and a pointer to the replacement. +# Added 2026-04-10 during Track F1 (Idris2 Solo-core formalisation) — the +# formal-verification track surfaced three decisions about the QTT type system +# that were previously implicit. + +[[adr]] +id = "ADR-001" +status = "accepted" +date = "2026-04-10" +title = "Split-Γ is the canonical presentation of the QTT type system" +context = """ +The Idris2 Solo-core formalisation (docs/academic/formal-verification/solo-core/) +revealed that docs/spec.md §3.6 presents typing rules against a shared Γ +(writing e.g. T-App with both premises typed under the same Γ), while +lib/quantity.ml runs a post-hoc usage walk that accumulates per-variable +usage summed across subterms. The literature-standard QTT presentation +(Atkey 2018, Granule, Idris2 core) uses explicit context splitting + Γ = Γ₁ + q·Γ₂ +at every eliminator position. + +Both forms are extensionally equivalent for the Solo core (same programs +accepted/rejected). The difference is purely presentational. +""" +decision = """ +Split-Γ is canonical. The spec, the formalisation, and all future +documentation present typing rules in split-Γ form. The post-hoc usage walk +in lib/quantity.ml is an equivalent implementation strategy, documented as +such in a comment, not the definition of the type system. +""" +consequences = """ +- docs/spec.md §3.6 is rewritten to show explicit context splitting (~30-60 LOC). +- lib/quantity.ml gets a header comment explaining that its flat walk is an + equivalent implementation of the split-Γ rules, with a pointer to this ADR. +- The Idris2 formalisation's Typing.idr (which uses split-Γ) becomes the + mechanised witness of the spec — no translation layer required. +- Progress + preservation proofs can proceed in textbook form. +- Dependability gain: one source of truth. The spec-vs-impl gap (the failure + mode flagged in Track B's audit of TYPECHECKER-COMPLETION.md) cannot + structurally hide in this part of the system. +- Interop gain: the spec becomes citable and legible to anyone with QTT + background (Atkey, Idris2, Granule, Quill readers). +- Reader-tax cost: split-Γ notation is heavier than shared-Γ for newcomers. + Mitigated by surface-language tutorials keeping informal shared-Γ intuition + while the spec remains the formal reference. +""" + +[[adr]] +id = "ADR-002" +status = "accepted" +date = "2026-04-10" +title = "Let rule scales value context by binder quantity (QTT-orthodox)" +context = """ +During the Decision-2 audit of docs/spec.md T-Let against lib/typecheck.ml +and lib/quantity.ml, the current implementation was found to handle Let +unsoundly: + +- lib/typecheck.ml:631-665 (ExprLet) does zero quantity handling — pure + HM-style inference with let-generalisation, quantities entirely absent. +- lib/quantity.ml:251-253 (ExprLet in infer_usage_expr) does a flat walk + that sums e1's and e2's usages without scaling e1 by the binder's quantity. + +This is equivalent to the rule + Γ₁ ⊢ e1 : A (Γ₂, x:A) ⊢ e2 : B + ───────────────────────────────── + Γ₁ + Γ₂ ⊢ let x = e1 in e2 : B +rather than the QTT-orthodox + Γ₁ ⊢ e1 : A (Γ₂, x:^q A) ⊢ e2 : B + ───────────────────────────────────── + q·Γ₁ + Γ₂ ⊢ let x :^q = e1 in e2 : B + +Two concrete soundness bugs follow: + +BUG-001 (high severity — linear-value smuggling): + let x :ω = linear_resource() in use_x_once(x) + Current impl accepts; QTT-correct form rejects (e1's Γ must be scaled by + ω, so any Γ₁ containing a linear variable becomes ill-formed). + +BUG-002 (medium severity — erasure failure): + let x :0 = expensive_proof_term() in body_not_using_x + Current impl evaluates e1 and consumes its resources; QTT-correct form + scales e1's Γ by 0, producing the zero context, which means e1 can be + erased at runtime. + +BUG-001 is a soundness hole that defeats the point of affine types in any +program where ω-binders and linear values coexist. +""" +decision = """ +The Let rule in both the spec and the implementation scales the value +context by the binder's quantity. T-Let becomes: + + Γ₁ ⊢ e1 : A (Γ₂, x:^q A) ⊢ e2 : B + ───────────────────────────────────── + q·Γ₁ + Γ₂ ⊢ let x :^q = e1 in e2 : B + +Unannotated Let defaults to q = ω (matching current behaviour for all +existing unannotated code — so the fix is non-breaking for the common case). +""" +consequences = """ +- lib/quantity.ml ExprLet case grows a scale-by-q step before summing + into the outer context (~10 LOC). +- lib/typecheck.ml ExprLet case threads quantities (folded into Track A3 + which is already rewriting quantity flow through typecheck.ml). +- docs/spec.md T-Let rule is rewritten to show the scaling explicitly. +- Two regression tests added as Track A2 fixtures covering BUG-001 and + BUG-002. +- The Idris2 formalisation (Typing.idr THLet) already assumes the scaled + form — no rework there. +- Users who had previously (accidentally) relied on the unsound behaviour + to duplicate linear values through ω-lets will now get a compile error. + Expected blast radius: near-zero because affine enforcement was never + wired in the first place (see ADR-001 rationale). +""" + +[[adr]] +id = "ADR-003" +status = "accepted" +date = "2026-04-10" +title = "Evaluation order is strict CBV, left-to-right, for all n-ary forms" +context = """ +During the Decision-3 audit of lib/interp.ml for the Track F1 Step relation, +the evaluator was found to be internally inconsistent about evaluation order +of n-ary expression forms. + +- ExprBinary (lib/interp.ml:135-138) is explicitly left-to-right: + let* left_val = eval env left in + let* right_val = eval env right in + eval_binop op left_val right_val +- ExprApp args (:130), ExprTuple (:144), ExprArray (:148) all delegate to + eval_list, which is implemented with List.fold_right: + and eval_list env exprs = + List.fold_right (fun expr acc -> + let* vals = acc in + let* v = eval env expr in + Ok (v :: vals) + ) exprs (Ok []) + OCaml is strict, so in f (fold_right f [a;b;c] init) the innermost + application runs first — eval env c is forced before eval env b before + eval env a. Net result: right-to-left. + +This was almost certainly unintentional; nothing currently depends on the +order because affine enforcement was never wired. However: + +- The WASM and Julia backends will naturally generate left-to-right code, + producing cross-backend semantic divergence for any program with effectful + or resource-consuming arguments. +- The effect system (70% complete per STATE.a2ml) would observe handler + ordering tied to evaluation order, making this a latent soundness hazard. +- A Track F1 Step relation defined in textbook CBV left-to-right form would + disagree with the interpreter, requiring a non-standard right-to-left + presentation or a proof of order-independence (which is false in general + for effectful languages). +""" +decision = """ +All n-ary expression forms evaluate their subterms strict left-to-right. +This applies to: application arguments, tuples, arrays, record fields, +function calls in all positions. + +lib/interp.ml eval_list is fixed to left-to-right: + and eval_list env exprs = + let rec loop acc = function + | [] -> Ok (List.rev acc) + | e :: rest -> + let* v = eval env e in + loop (v :: acc) rest + in + loop [] exprs + +Track F1's Step relation uses the standard CBV left-to-right presentation + e1 → e1' ⟹ e1 e2 → e1' e2 (reduce function first) + e2 → e2' ⟹ v1 e2 → v1 e2' (then argument, only once function is a value) +with the symmetric congruences for pairs, tuples, etc. +""" +consequences = """ +- lib/interp.ml eval_list is replaced (~6 LOC diff). +- docs/spec.md gains (or receives an edit to) an explicit operational + semantics section stating strict CBV, left-to-right. +- A regression test lands in Track A2 exercising a program whose output + depends on left-to-right evaluation (e.g. a tuple of two side-effecting + calls that mutate a shared counter). +- WASM, Julia, and the effect system all become consistent with the + interpreter by default — no backend fix required. +- Track F1 can proceed on its current footing to mechanise Step and the + progress/preservation proofs without author input. +- Existing programs are unaffected unless they rely on observable right- + to-left evaluation of tuple/arg components. The interpreter is the only + place that did this, and nobody has filed a bug, so blast radius is + expected to be zero. +""" + +[[adr]] +id = "ADR-007" +status = "accepted" +date = "2026-04-10" +accepted = "2026-04-10" +title = "Surface syntax for quantity annotations: @linear/@erased/@unrestricted (primary) + :1/:0/:ω (sugar)" +context = """ +The Track A audit on 2026-04-10 found that affine enforcement is wired +through the CLI (Quantity.check_program_quantities runs from +lib/typecheck.ml:1206 on every check/compile/eval invocation) but +**unreachable** from user programs because the surface syntax for declaring +quantity annotations on let-binders does not exist. Specifically: + +- lib/ast.ml ExprLet and StmtLet have no quantity field. They carry + el_mut/sl_mut, el_pat/sl_pat, el_ty/sl_ty, el_value/sl_value, el_body + — no el_quantity. Only function `param` and `type_param` carry + `p_quantity`/`tp_quantity`. +- lib/lexer.ml never emits ZERO/ONE tokens. Source `0` and `1` are lexed + as `INT 0`/`INT 1`. The only quantity literal that round-trips through + source today is `omega`/`ω` (lexer.ml:53 and lexer.ml:153 respectively). +- lib/parser.mly's `quantity` rule (lines 180-183) takes ZERO|ONE|OMEGA + but only OMEGA is reachable from source. Even on function parameters, + `:1` and `:0` annotations cannot be written. +- ADR-002 specifies the QTT-orthodox scaled Let rule (q·Γ₁ + Γ₂), which + requires the binder to carry a quantity. Without surface syntax to + attach one, the rule has no input to scale by, BUG-001 cannot be + closed, and the headline affine-types feature remains theatre. + +Surface syntax is a language-design decision, not an implementation +detail. The decision needs author input before any of items 1-6 in +[track-a-manhattan] proceed. +""" +options = """ +Three candidate surface syntaxes are listed. None has been chosen. + +# Option A — caret-quantity suffix on `let` keyword + + let^1 x = linear_resource() in use_x_once(x) + let^0 _proof = expensive_term() in body + let^ω y = pure_int() in use_y_many(y) + + Pros: + - Visually parallel to T-Let's `let x :^q A` notation in QTT papers. + - The `^` is currently unused in the lexer, so no disambiguation cost. + - The annotation lives on the keyword, not the binder, which mirrors + the type-theory framing where `q` parameterises the rule, not the + variable. + - Easy parser rule: LET (CARET quantity)? pat (COLON ty)? EQ value. + + Cons: + - `^` is unfamiliar surface syntax for users coming from Rust/Idris2. + - The `^` may collide with future bitwise-XOR or pow operators + (currently OpBitXor exists in ast.ml but the lexer surface form is + unclear). + - Requires users to type a non-letter character on every annotation. + +# Option B — colon-quantity prefix on the type ascription + + let x :1 :Int = linear_resource() + let x :0 :Int = expensive_term() + let x :ω :Int = pure_int() + let x :1 = linear_resource() # type inferred + + Pros: + - Reuses the existing COLON token; no new operators needed. + - Mirrors the function parameter syntax, which already accepts + `qty? own? name COLON ty` (parser.mly:186). + - Familiar to readers of Idris2 (`x : 1 A`) and Quantitative Type + Theory papers. + + Cons: + - Two consecutive colons looks awkward and may confuse readers + expecting `::` (the path separator COLONCOLON token). + - When the type is inferred (`let x :1 = e`), the parser needs + one-token lookahead to distinguish `:1` (quantity) from `:Int` + (type). This is a minor parser challenge but non-zero. + - The lexer must additionally route `INT 0`/`INT 1` to ZERO/ONE in + quantity position, which is context-sensitive lexing — best handled + by accepting `INT 0|INT 1` directly in the parser's quantity rule + instead, but that introduces a small grammar wart. + +# Option C — annotation-style attribute before `let` + + @linear let x = linear_resource() in use_x_once(x) + @erased let _proof = expensive_term() in body + @unrestricted let y = pure_int() in use_y_many(y) + let z = pure_int() in use_z(z) # default = unrestricted + + Pros: + - Reads naturally; `@linear` and `@erased` are self-documenting and + require no quantity-theory background. + - Trivially extensible to future modal annotations (e.g. `@borrowed`, + `@owned`) without grammar changes. + - Parser change is local: AT (ident) LET pat (COLON ty)? EQ value. + No COLON disambiguation, no new operator tokens. + - Aligns with the language-policy preference for descriptive names + over symbolic ones (CLAUDE.md: "Always use descriptive variable + names"; the same instinct applied to annotations). + + Cons: + - Verbose. `@linear let x = ...` is six tokens vs `let^1 x = ...`'s + four. + - Diverges from the QTT mathematical notation, making it harder for + paper-trained readers to map source to spec. Mitigation: spec.md + can show both forms in parallel. + - The `@` token is currently unused, so it's free, but introducing + it for one feature creates pressure to use it for others (which + may or may not be desirable depending on language direction). + - Requires a small mapping table from attribute name to quantity + value (`linear → QOne`, `erased → QZero`, `unrestricted → QOmega`). +""" +decision = """ +Hybrid: Option C accepted as primary, Option B accepted as sugar. + +Both surface syntaxes parse to the same internal representation. +Tutorials, error messages, IDE tooling, and the spec's prose all use +Option C. Option B remains legal for users porting from QTT papers or +who prefer the compact form. Option A is rejected outright. + +# Primary surface form (Option C — what tutorials and error messages use) + + @linear — used exactly once (QOne) + @erased — must not be used at runtime (QZero) + @unrestricted — any number of uses (QOmega), the default + +Examples: + @linear let x = linear_resource() in use_x_once(x) + @erased let _proof = expensive_term() in body_not_using_proof + @unrestricted let y = pure_int() in use_y_many(y) + let z = pure_int() in use_z(z) # default: @unrestricted + + fn consume(@linear x: Resource) -> Unit = release(x) + fn weigh(@erased _proof: Even(n), n: Nat) -> Nat = n / 2 + +# Sugar surface form (Option B — accepted but not promoted) + + let x :1 :Resource = linear_resource() in use_x_once(x) + let _proof :0 :Even(n) = expensive_term() in body + let y :ω :Int = pure_int() in use_y_many(y) + let z :Int = pure_int() in use_z(z) # no quantity = @unrestricted + + fn consume(x :1: Resource) -> Unit = release(x) + fn weigh(_proof :0: Even(n), n: Nat) -> Nat = n / 2 + +# Equivalence + + @linear ≡ :1 + @erased ≡ :0 + @unrestricted ≡ :ω (also: omitted entirely) + +Both forms parse to the same `el_quantity = Some QOne | QZero | QOmega` +in lib/ast.ml. The compiler chooses the canonical form (Option C) when +emitting diagnostics, formatter output, and pretty-printed source. The +formatter rewrites Option B to Option C on `affinescript fmt`, with an +opt-out flag `--keep-quantity-sugar` for users who deliberately prefer +the compact form. + +Rationale (decided per the standing priority order +dependability > security > interop > usability/accessibility/marketability +> performance > versatility > functional extension): + +- Usability/accessibility/marketability beats consistency-with-papers + in the priority order, so Option C must be the primary form a new + user encounters in tutorials and error messages. A new user reading + `@linear let x = e` can form a working hypothesis without consulting + the spec. The same user looking at `let x :1 = e` has nothing to + Google. +- However, the priority order does not require *exclusivity*. Accepting + Option B as sugar costs ~15 LOC of parser surface area and one + paragraph in the spec, and in return it: + * Preserves consistency with the existing function-parameter syntax + (which uses `qty? own? name COLON ty`), so the param-quantity gap + closes with the *same* grammar production rather than a parallel + one. Users learning the param syntax already see `:1` in the + grammar; the sugar makes that knowledge transfer to let-binders. + * Preserves citation isomorphism with QTT papers — anyone porting an + Idris2 example or a Granule example can paste the numeric form + directly without rewriting it. + * Gives advanced users a denser form for code where annotations are + frequent and English keywords would create visual noise. +- Compiler errors and source code share the C vocabulary regardless of + which form the user wrote: "`@linear` binding 'x' used 2 times" reads + cleanly without mental translation, even if the source was written + as `:1`. The diagnostic always speaks the canonical form. +- Tutorials can be example-driven from the first runnable program with + Option C; the QTT semiring and Option B sugar become optional + advanced reading rather than a prerequisite for "hello world with + affine types." +- The `@` namespace is now committed to AffineScript and may grow to + cover additional modal annotations (e.g. `@total`, `@pure`, + `@borrowed`, `@owned`) in future ADRs. This is an intentional + reservation, not a side effect. +- The Rust/Linear-Haskell precedent is real: Rust ships affine + semantics under approachable keywords and reaches a vastly larger + audience than Linear Haskell, which exposes the type-theory + machinery directly. AffineScript chooses Rust's marketing instinct + for the primary form, while keeping Linear Haskell's notation + available for users who already think in it. + +# Style guide commitment + +The spec commits explicitly to keeping both forms supported. Future +contributors proposing to drop Option B sugar must amend this ADR +rather than removing it silently. The recommended convention in +published AffineScript code is: + +- New code: Option C (`@linear let x = e`). +- Code accompanying a paper: either form, author's choice. +- Auto-formatted code: Option C (formatter rewrites unless + `--keep-quantity-sugar` is set). +- Generated code (compiler diagnostics, IDE quick-fixes, refactoring + tools): Option C exclusively. + +Rejected alternatives: + +- Option A (`let^q x = e`): rejected. Lowest implementation cost but + worst on the priority order — symbolic, opaque to non-TT readers, + forecloses future use of `^` for bitwise XOR or exponentiation. +- Option B exclusively (without C): rejected for the same + accessibility/adoption reasons that make C the primary form. +- Option C exclusively (without B sugar): considered and rejected. The + marginal cost of accepting B is low (~15 LOC parser, one paragraph + spec), and the benefit — closing the param-quantity gap with one + grammar production and preserving paper-citation isomorphism — is + worth the cost given the standing priority order's high weighting + of interop alongside accessibility. +""" +consequences = """ +- lib/ast.ml gains el_quantity / sl_quantity fields on ExprLet and + StmtLet. ~10 modules pattern-matching on Let must be updated + mechanically (typecheck, interp, codegen, codegen_gc, julia_codegen, + formatter, sexpr_dump, json_output, linter, opt, desugar_traits). +- lib/lexer.ml gains an AT token for `@`. ~2 LOC. +- lib/token.ml gains the AT token kind. ~2 LOC. +- lib/parser.mly gains: + * AT ident — a quantity_attr rule that maps `@linear`/`@erased`/ + `@unrestricted` to QOne/QZero/QOmega and rejects unknown attribute + names with a parse error pointing at the documented set. + * Optional quantity_attr prefix on let_decl, stmt_let, param, and + lambda_param productions. ~15 LOC. + * The existing numeric quantity rule (ZERO|ONE|OMEGA) gains INT 0 + and INT 1 as accepted lexemes (since the lexer emits INT not + ZERO/ONE for `0` and `1`). This makes Option B sugar reachable + from source on let-binders and params, in addition to type_params + where it already worked. ~5 LOC. + * Both productions resolve to the same el_quantity / sl_quantity / + p_quantity slot — they are alternative concrete syntaxes for the + identical abstract syntax. +- lib/quantity.ml ExprLet/StmtLet cases gain context scaling per + ADR-002. ~25 LOC including a `scale_usage_by` helper. +- Error messages in lib/quantity.ml format_quantity_error are updated + to use `@linear` / `@erased` / `@unrestricted` vocabulary instead of + the show_quantity numeric forms. +- Regression fixtures land covering BOTH surface forms: + * test/e2e/fixtures/bug_001_omega_let_smuggles_linear.affine — must + be rejected (Option C form: `@unrestricted let x = ...`). + * test/e2e/fixtures/bug_001_sugar_form.affine — must be rejected + (Option B form: `let x :ω = ...`). Proves both surface forms are + enforced through the same code path. + * test/e2e/fixtures/affine_let_valid.affine — must pass (Option C). + * test/e2e/fixtures/affine_let_valid_sugar.affine — must pass + (Option B). +- docs/spec.md T-Let rule is rewritten to show the formal QTT notation + alongside both surface forms (Option C primary, Option B sugar) so + paper-trained readers and pedagogy-first readers both see their + preferred presentation. +- docs/spec.md gains a "Surface syntax for quantity annotations" + section documenting the equivalence table and the style-guide + commitment from this ADR. +- BUG-002 (`:0` lets do not erase their RHS) closes in the same change, + per ADR-002's consequence list. +- The affine-types feature flag in STATE.a2ml [features] flips from + `wired-but-unreachable` to `wired-and-reachable` once the change + lands. +- Future modal annotations (`@total`, `@pure`, `@borrowed`, `@owned`, + etc.) inherit the same `@`-attribute parsing infrastructure for free. +- The formatter (lib/formatter.ml) gains a `--keep-quantity-sugar` flag + that defaults to OFF (so default `affinescript fmt` rewrites Option + B sugar to Option C primary). Users opting in retain their chosen + form across format passes. +""" +references = [ + ".machine_readable/descriptiles/STATE.a2ml [track-a-manhattan]", + ".machine_readable/descriptiles/STATE.a2ml [[open-bug]] BUG-001", + ".machine_readable/descriptiles/STATE.a2ml [[open-bug]] BUG-002", + ".machine_readable/descriptiles/META.a2ml [[adr]] ADR-002 (scaled Let rule)", + "lib/ast.ml ExprLet (lines 105-111), StmtLet (lines 170-176)", + "lib/lexer.ml (ZERO/ONE never emitted)", + "lib/parser.mly quantity rule (lines 180-183)", +] + +[[adr]] +id = "ADR-008" +status = "accepted" +date = "2026-04-11" +title = "Effect invocation uses direct call syntax — no 'perform' keyword" +context = """ +Algebraic effects require a syntax for invoking an effect operation at a +call site. Two candidates exist in the literature: + +1. Direct call: `Http.get(url)` — the effect operation looks like a + namespaced function call. The effect is declared in the return type + annotation (`-> T / Http, Async`) not at each call site. + +2. Explicit perform: `perform Http.get(url)` — the keyword makes the + effectful nature of the call visible at every use site. Used by Koka + (earlier versions) and some effect-system research languages. + +The AffineScript DESIGN-VISION.adoc already shows direct call style in its +effect examples. The decision needed to be made explicit so the conformance +suite, parser, and documentation are unambiguous. +""" +decision = """ +Effect operations are invoked with direct call syntax. No `perform` keyword. + + fn fetch_user(id: Int) -> User / Http, Async { + let resp = Http.get("/users/" ++ show(id)) + Async.await(resp.json()) + } + +The effect is declared once — in the return type annotation (`/ Http, Async`). +The call sites are plain calls. The type signature is the contract; the call +site is just a call. +""" +consequences = """ +- The conformance suite, parser, and spec must not admit or require `perform`. +- Effect operation calls are syntactically indistinguishable from regular + function calls; the distinction is entirely in the type system. +- Error messages for unhandled effects reference the return type annotation, + not a `perform` site. +- This is consistent with the ergonomics goal: effect-heavy code does not + accumulate keyword noise at every call site. +- Face parsers (Python-face, JS-face) can map `async/await` to the Async + effect without introducing a `perform` concept — `await` desugars to + `Async.await(...)`, which is a direct call. +""" +references = [ + "docs/DESIGN-VISION.adoc (effect examples)", + "docs/specs/effects.md", +] + +[[adr]] +id = "ADR-009" +status = "accepted" +date = "2026-04-11" +title = "The conformance suite is authoritative — parser must conform to spec" +context = """ +The conformance suite (test/conformance/) contains valid AffineScript programs +drawn from the spec. As of 2026-04-11, 8 of 12 valid conformance tests fail +to parse. The three categories of failure are: + +1. Uppercase type names — `Int`, `String`, `Bool`, `Option` are written in + the spec with PascalCase. The parser's `ident` rule accepts only lowercase, + so `Int` fails to parse as a type name. + +2. ML-style enum syntax — the spec's enum declaration syntax does not match + what the parser accepts. + +3. Effect op type parameters — the spec includes type parameters on effect + operations; the parser does not support them. + +Two possible authorities: the spec (conformance suite) or the parser +(current implementation). +""" +decision = """ +The spec is authoritative. The parser must conform to the spec. This is +consistent with the standing estate-wide rule: 'Language scope lives in a +written thesis. Thesis authoritative, code must conform. If disagreement, +code is wrong.' + +Specific required changes: +1. Parser must accept PascalCase type names (`Int`, `String`, `Bool`, user- + defined types). The type name namespace is PascalCase; value names remain + camelCase/snake_case. This also fixes the conformance suite failure where + effect and enum type names could not be written. +2. Parser must accept the spec's enum declaration syntax (to be confirmed + against spec and fixed accordingly). +3. Parser must support type parameters on effect operations. + +The target is 12/12 conformance suite passing. +""" +consequences = """ +- lib/lexer.ml and lib/parser.mly are updated to accept PascalCase type names. +- The `ident` vs `type_ident` distinction is formalised in the grammar: + `ident` = lowercase-leading (values, variables, functions) + `type_ident` = uppercase-leading (types, effects, enums, type aliases) +- lib/parser.mly enum and effect rules are audited against spec and fixed. +- 12/12 conformance tests pass. The conformance suite becomes a live + regression suite — any future parser change that breaks a conformance test + is a bug, not a spec disagreement. +- All fixture files under test/e2e/fixtures/ that use lowercase type names + (e.g. `type point = ...`) are reviewed; lowercase type aliases remain valid + (they are value-level names) but builtin types (`int` written lowercase) + are normalised to PascalCase. +- Error messages and diagnostics use PascalCase type names consistently. +""" +references = [ + "test/conformance/", + "lib/lexer.ml", + "lib/parser.mly", + "docs/spec.md", +] + +[[adr]] +id = "ADR-010" +status = "accepted" +date = "2026-04-11" +title = "Face-aware error formatting is a first-class toolchain concern" +context = """ +AffineScript supports syntactic face layers (Python-face, JS-face, +pseudocode-face, canonical AffineScript) that allow developers to write +AffineScript using familiar surface syntax from other languages. The compiler +pipeline forks only at the parser; everything downstream (type checker, +codegen) is shared and operates on the canonical AST. + +Without face-aware error formatting, a developer writing Python-face +AffineScript receives type errors expressed in canonical AffineScript +syntax — terms and constructs they have deliberately not learned yet. This +shatters the face illusion at the worst possible moment (when the developer +is stuck and needs help) and is a direct contradiction of the adoption goal. +""" +decision = """ +Face-aware error formatting is a first-class toolchain concern, not an +afterthought. It is implemented as a formatting layer between the compiler +and the terminal, not inside the compiler itself. + +Architecture: + compiler (emits errors in canonical AST terms) + ↓ + face-aware error formatter ← separate concern, swappable + ↓ + terminal / IDE / LSP + +The compiler's error representation is canonical and face-agnostic. The +formatter receives: (error, active-face) → formatted error string. + +Each face provides an error vocabulary mapping: + canonical term → face term + e.g. (python-face) "affine binding" → "single-use variable" + e.g. (js-face) "Option[T]" → "T | null" [with note: 'use .unwrap_or()'] + +Error source spans are always reported in the face's syntax, not canonical +AffineScript syntax, since the user's file is written in the face. +""" +consequences = """ +- The compiler's error types carry enough information for the formatter to + reconstruct face-appropriate messages. No compiler internals leak face + knowledge. +- Each face ships with an error vocabulary file (part of the face definition). +- The toolchain (CLI, LSP server, playground) passes the active face to the + formatter. The formatter is the single point of face-awareness for errors. +- A developer on Python-face who hits a linearity violation sees: + Error: single-use variable 'x' used twice + → line 7, in greet + not: + Error: affine binding 'x' used 2 times (quantity violation: QOne, found 2) +- The face formatter is versioned alongside the face. When a face is + deprecated, its error formatter is deprecated with it. +- The IDE/LSP integration carries face information in the project config so + hover types, completions, and inline errors all speak the face's vocabulary. +- This is a design commitment, not an immediate implementation task. The + canonical compiler error representation must be designed with this + formatability requirement in mind from the start. +""" +references = [ + "docs/DESIGN-VISION.adoc (faces section)", + "docs/specs/faces.adoc", +] + +[[adr]] +id = "ADR-011" +status = "accepted" +date = "2026-05-17" +title = "Stdlib namespace model: real modules with qualified paths" +context = """ +The AffineScript stdlib was never compiled through the static +resolve → typecheck → codegen pipeline as a coherent unit (issue #128). +Its core files (prelude, option, result, collections, string, io, +testing, effects, math, traits) are interpreter-era code: a flat global +namespace with no `module`/`use`/`open`, and conflicting duplicate +definitions across files — `prelude.affine` and `option.affine` both +define `is_some`/`unwrap`/`map`/… with *incompatible* signatures +(`prelude.map(arr, f)` vs `option.map(f, opt)`). Commit b895374 seeded +`Some/None/Ok/Err` as builtins to make `string.affine` resolve +standalone, a band-aid that entrenches the flat namespace. + +Issue #132 required a single ruling — real modules vs intentionally +flat-and-deduplicated — because #133 (dedup), #135 (whole-stdlib +compile), #137 (multi-module integration test) and #138 (band-aid +removal) all branch on it. + +The compiler already has the machinery: the grammar accepts +`module X;`, `use path;` and `::`-qualified paths; `module_loader.ml` +resolves module paths to files with search paths, nested modules and +caching; and the *newer* stdlib files (Core, Crypto, Ajv, Sqlite, +Grammy, Deno, Network, Vscode, VscodeLanguageClient) already declare +`module X;` and use qualified constructors (`Ordering::Less` in +traits.affine). Only the legacy core files are flat. +""" +decision = """ +The stdlib uses **real modules with qualified paths**, not a flat +de-duplicated prelude. + +- Every `stdlib/*.affine` declares `module ;` and is addressed by + its module path. +- Cross-file use is explicit: `use option::{Option, Some, None};` / + qualified references `Result::unwrap`. +- There is exactly one canonical definition per name, owned by its + module. The `prelude`/`option`/`result` overlaps are resolved by + giving each name a single owning module and having the others `use` + it (no duplicate, signature-divergent copies). +- A minimal, explicit prelude module may re-export the few universally + needed names (`Option`, `Result`, `Some`, `None`, `Ok`, `Err`); it + contains *re-exports*, not independent redefinitions. +- The b895374 seeded builtins are removed once resolution flows through + the module path (tracked by #138); they are not load-bearing. + +This is the path consistent with the machinery the compiler and the +newer stdlib already use; it makes the AOT pipeline exercise real +cross-module resolution, which is the actual #128 objective. +""" +consequences = """ +- #133 removes the duplicate/conflicting prelude/option/result defs by + assigning each name one owning module; non-owners `use` it. +- #135 brings the legacy core files under `module`/`use` as it makes + each compile through resolve → typecheck → codegen. +- #137 (multi-module integration test) is meaningful: it exercises + several modules `use`d together, which the flat model could not test. +- #138 can delete the seeded-builtins band-aid once the prelude + re-export module exists and resolution uses it. +- Slightly more up-front import wiring than a flat prelude, but it is + the model the language already commits to elsewhere (ADR pointer: + newer stdlib + `module_loader.ml`), so no new compiler design debt. +- This decision is settled; do not reopen without amending this ADR. +""" +references = [ + "https://github.com/hyperpolymath/affinescript/issues/128", + "https://github.com/hyperpolymath/affinescript/issues/132", + "docs/history/MODULE-SYSTEM-PROGRESS.md (2026-05-17 decision section)", + "lib/module_loader.ml", + "lib/parser.mly (module_decl / import_decl / COLONCOLON)", +] + +[[adr]] +id = "ADR-012" +status = "accepted" +date = "2026-05-18" +title = "Grammar changes are correctness assertions; parser-conflict disclosure" +context = """ +The Menhir parser emitted a wall of "N shift/reduce conflicts were +arbitrarily resolved" notices. Issues #215/#218 set out to address the +real concern: a correct toolchain looking broken — acutely damaging when +correctness is the product. Two grammar changes were made along the way +(#222 record `#{ }`; #215 family B `return`/`resume` hoist). It became +necessary to state, permanently, *why* the grammar may change and how the +residual benign notices are handled — so that in the long term no one +mistakes either the changes or the remaining notices for sloppiness. +""" +decision = """ +The grammar is changed only to make it assert something TRUE about the +language's semantics; it is never contorted to lower a cosmetic metric. + +- `#{ }` records (#218): semantic — kills block-vs-record ambiguity and + the struct-literal-in-scrutinee hazard by construction. Conflict-count + fall was a consequence, not the reason. +- `return`/`resume` as diverging prefix expressions (#215 family B): + semantic — `return e : Never`, never an operator operand; + `(return a) + b` now needs explicit parens (a feature: post-divergence + dead code made visible). NOT motivated by, and does NOT reduce, the + conflict count (proven: 21->21 S/R states, net-neutral). +- Residual ~68 S/R + small R/R (incl. state 401, the block + trailing-expr-vs-statement boundary): inherent LALR(1) artefacts that + Menhir resolves CORRECTLY (257-case gate green). Eliminating them = + systemic precedence/left-factoring surgery, estate-wide parse blast + radius, cosmetic-only payoff = exactly the contortion this ADR + forbids. Intentionally LEFT IN PLACE; documented won't-fix on #215. +- Disclosure: default `just build` MASKS these specific benign notices + but prints the masked count + correctness proof + this ADR pointer + + the exact reveal command (`just build-loud` / + AFFINESCRIPT_SHOW_MENHIR_NOISE=1 / plain `dune build`). The + parser-generator output is not suppressed at source (that would be + risky change for a cosmetic end); the build is unchanged and fully + transparent on demand. Masking is disclosure, not concealment. +""" +consequences = """ +- Every grammar change in the history has a recorded semantic + justification; none was made to chase a number. +- The residual notices are settled won't-fix — do not reopen as "bugs" + without amending this ADR. +- Contributors see a calm, honest build; auditors see everything via + one documented command. +- This decision is settled; do not reopen without amending this ADR. +""" +references = [ + "https://github.com/hyperpolymath/affinescript/issues/215", + "https://github.com/hyperpolymath/affinescript/issues/218", + "https://github.com/hyperpolymath/affinescript/pull/222", + "docs/specs/SETTLED-DECISIONS.adoc (ADR-012 section)", + "justfile (build / build-loud recipes)", + "lib/parser.mly (expr_assign return/resume; record #{ )", +] + +[[adr]] +id = "ADR-013" +status = "accepted" +date = "2026-05-18" +title = "Async on the WasmGC backend: transparent CPS continuation transform" +context = """ +stdlib/Http.affine exposes one portable surface +`fetch(req) -> Response / { Net, Async }`. The Deno-ESM backend lowers +it to a direct `await` (#226, shipped). The WasmGC backend cannot: +extern calls are synchronous, the boundary is i32-only, and the +estate's async-over-wasm mechanism (#205) is callback-shaped +(`-> Thenable` + thenableThen/thenableResultJson). Issue #225 Option 2 +(owner-chosen) requires one BYTE-IDENTICAL source surface on both +targets, so the wasm backend must hide the continuation plumbing. +typed-wasm is the shared convergence ABI (ADR-004); Ephapax is a +co-stakeholder for the async ABI. +""" +decision = """ +On the WasmGC backend ONLY, functions whose effect row includes `Async` +are compiled via a selective continuation-passing (CPS) transform. +Pure / non-Async functions are untouched (no codegen or perf change). + +- Each async boundary (call to an extern returning under /Async, or to + another Async function) splits the body; code after it becomes a + generated continuation function whose env is the live-local capture + set, marshalled via the EXISTING #199 closure ABI + ([fnId@0,envPtr@4] through __indirect_function_table). +- The split lowers to thenableThen(handle, ) (the + EXISTING #205 host->guest re-entry); the enclosing Async function + itself returns a Thenable handle, so Thenable composes transparently + up the call chain. At the host boundary the outermost Thenable is + awaited. The programmer never sees Thenable — effect row is the + abstraction, backend chooses the mechanism (as on the Deno side). +- New orchestration over three proven primitives (find_free_vars, #199 + closure ABI, #205 re-entry); NOT a new runtime, NOT JSPI (rejected), + NOT a general delimited-continuation feature. + +Correctness obligations (binding, pre-merge): +1. A linear/own local captured into a continuation is the borrow + checker's single use; double-resumption impossible (Thenable settles + once; thenableThen fires once — asserted). +2. Transform triggers iff Async in fd_eff; Net/other effects ride + along; fd_is_async reused as predicate. +3. Response reconstruction uses a minimal typed reader (jsonField is + insufficient for headers: [(String,String)]); general decode + deferred to #161. No silent lossiness. +4. Full `dune test --force` (258) green at every commit; wasm e2e + parity test mirrors tests/codegen-deno/http_fetch.*. +""" +consequences = """ +- Single portable Http surface across Deno-ESM and WasmGC; #160 closes + only when both targets are green (joint-close with #225). +- The Thenable-handle + thenableThen continuation protocol is the + agreed async ABI for the typed-wasm convergence layer; Ephapax + co-stakeholder review is required for the convergence-spec section. +- Delivered incrementally (4 PRs, each behind the 258 gate); scope is + WasmGC Async functions only. +- This decision is settled; do not reopen without amending this ADR. +""" +references = [ + "https://github.com/hyperpolymath/affinescript/issues/225", + "https://github.com/hyperpolymath/affinescript/issues/160", + "https://github.com/hyperpolymath/affinescript/pull/226", + "docs/specs/async-on-wasm-cps.adoc (full design)", + "docs/specs/SETTLED-DECISIONS.adoc (ADR-013 section)", + "lib/codegen.ml (find_free_vars; #199 closure ABI)", + "stdlib/Vscode.affine + packages/affine-vscode/mod.js (#205 thenableThen)", + "typed-wasm ADR-004 (convergence / aggregate library; Ephapax)", +] + +[[adr]] +id = "ADR-014" +status = "accepted" +date = "2026-05-19" +title = "Module-qualified type/effect path separator: both `.` and `::`, `::` canonical" +context = """ +The type/effect grammar had NO module-qualified path production: a +qualified reference such as `Externs.Res` or `Externs.Net` was +unrepresentable in any type or effect position and failed with +`parse error` at the `.` (issue #228). An audit of the estate `.affine` +corpus (compiler-as-oracle, origin/main) found this the SINGLE dominant +fault estate-wide: 525 of ~1177 files fail to parse, qualified-path the +leading cause in every hand-written Frontier-playbook TS-port. ADR-011 +already settled "real modules with qualified paths"; the consequence was +unspeakable because the grammar could not name a module-qualified +entity. Per ADR-012 (grammar changes are correctness assertions), +closing this is asserting a settled truth, not adding sugar. The +separator was the open, escalated language-design call: `module_path` +(parser.mly) uses DOT; `use`/value paths use COLONCOLON (ADR-011 +`Result::unwrap`, `use option::{…}`); the estate corpus uses `.` for +qualified types. +""" +decision = """ +Accept BOTH `.` and `::` (mixed permitted) as the module-qualified +type/effect path separator. `Pkg::Type` is the CANONICAL form +(consistent with ADR-011 value paths — one canonical separator +estate-wide long-term). `.` is tolerated and normalised to `::`. + +Implementation (parser.mly, conflict-neutral): +- New `qualified_type_name` (>=2 `upper_ident` segments, right-recursive, + `qsep = DOT | COLONCOLON`), folded into a single canonical `::`-joined + ident string. Added to `type_expr_primary` (TyCon / TyApp via `[ ]` + and `< >`) and `effect_term` (EffVar / EffCon). +- Because the qualified name is folded to one ident, downstream + (resolve/typecheck/all codegens) sees a single ident and needs no + change; the formatter prints the stored `::` form, so `.`→`::` + normalisation is automatic with NO formatter change. +- Conflict-neutral by construction: it only adds DOT/COLONCOLON as + lookahead after a type/effect-position `upper_ident`, where no reduce + action previously existed (that void is precisely the + `parse error at .` #228 reports). Measured: Menhir conflict states + unchanged at 21 S/R + 1 R/R; item counts unchanged at 68 S/R / 7 R/R. +- Resolution of the qualified name against a real module is a separate + concern (the parse gap is what #228 names); the grammar PR unblocks + parsing — most of the 525 estate parse failures clear with zero + consumer churn. Genuine ReScript-surface residue (#229) is tracked + separately. +""" +consequences = """ +- The estate `.affine` corpus parses without rewriting `.`→`::`; the + formatter migrates to the `::` canonical form opportunistically. +- No estate consumer porting was needed for the grammar gap (rejected + alternative: rewriting hundreds of files entrenches a workaround + against the Frontier playbook). +- This decision is settled; do not reopen without amending this ADR. + CORE-03 in docs/TECH-DEBT.adoc. +""" +references = [ + "https://github.com/hyperpolymath/affinescript/issues/228", + "https://github.com/hyperpolymath/affinescript/issues/229", + "lib/parser.mly (qualified_type_name; type_expr_primary; effect_term)", + "docs/specs/SETTLED-DECISIONS.adoc (ADR-014 section)", + "META.a2ml [[adr]] ADR-011 (real modules / qualified paths)", + "META.a2ml [[adr]] ADR-012 (grammar changes are correctness assertions)", +] + +[[adr]] +id = "ADR-015" +status = "accepted" +date = "2026-05-19" +title = "WASI Preview2 / WASM Component-Model migration (INT-03)" +context = """ +lib/wasi_runtime.ml emits only `wasi_snapshot_preview1.fd_write` to +stdout (fd 1). There is no file, socket, environment, clock, or argv +access, so there is no server-side runtime profile (INT-06 is blocked) +and no real host I/O. INT-03 (#180) is the substrate fix. The owner was +asked (2026-05-19, AskUserQuestion one-way-door fork) to choose between +(a) expanding the preview1 import surface, (b) preview1 + an external +preview1->preview2 adapter as the run path, or (c) a full re-target to +the WebAssembly Component Model with WASI 0.2 (preview2). The owner +chose (c) — explicitly the largest, one-way, highest-blast-radius path. +""" +decision = """ +Re-target the WASM output to the WebAssembly Component Model with WASI +0.2 (preview2) WIT worlds. Binding constraints: + +- STAGED, non-breaking per slice; each slice is its own PR behind the + full gate. The legacy core-wasm + preview1 stdout path stays the + DEFAULT until the final slice flips it, so the migration is + reversible while in progress even though the end-state is one-way. +- The `affinescript.ownership` custom section is a MULTI-PRODUCER ABI + (shared with hyperpolymath/ephapax; the typed-wasm contract carrier, + see docs/ECOSYSTEM.adoc). It MUST survive verbatim onto the + component's embedded core module. Its format MUST NOT change here; + any change is coordinated in hyperpolymath/typed-wasm, never made + unilaterally for this migration. +- Only the `wasm`/`wasm-gc` target re-points. The 22+ source-to-source + targets (Deno-ESM, Node-CJS, Julia, …) are unaffected. +- WIT world of record: `wit/affinescript.wit` — world + `affinescript:cli/run` importing the `wasi:cli`, `wasi:clocks`, + `wasi:filesystem`, and `wasi:sockets` 0.2 interfaces. + +Staged plan (ledger INT-03; each row = one gated PR): +- S1 (this PR): ADR-015 + WIT world + staged plan + tooling-prerequisite + + roadmap truthing. No codegen change. +- S2: toolchain provisioning — `wasm-tools` + `wasm-component-ld` (and + `wac`) into guix.scm/flake.nix. ABSENT in the current toolchain ⇒ a + HARD GATE on S3+: a component cannot be built or tested without them. +- S3: componentize on-ramp — codegen still emits core wasm; a + post-codegen step wraps it with the standard preview1->preview2 + adapter into a component; ownership section survival asserted; + wasmtime component-run smoke. Codegen unchanged ⇒ reversible. +- S4 (DONE, slices S4a clock + S4b env_count/arg_count): on-demand + preview1 imports (`wasi_snapshot_preview1.clock_time_get`, + `environ_sizes_get`, `args_sizes_get`) via the Effect_sites + pre-scan, canonical-order indexing through + `ctx.wasi_func_indices`. NOTE divergence from this ADR's original + S4 wording ("native `wasi:clocks` + environment + argv via + preview2"): the slice shipped as preview1-shim, NOT native + preview2 calls. This is sufficient under the component path + because both adapters bridge the preview1 imports to + `wasi:clocks` / `wasi:cli` internally; switching the codegen + to emit `wasi:clocks/monotonic-clock@0.2.x.now()` directly only + pays off once preview2 is the default (deferred into S6c). The + ADR is amended here to record what actually shipped rather than + pretending S4 fulfilled the original wording. String accessors + (env_at/arg_at) need byte-level wasm IR (I32Load8U/I32Store8) for + buffer marshalling and remain a tracked follow-up. +- S5: `wasi:filesystem` (open/read/write/close) — unblocks INT-06. +- S6a (WIT export lifting, DONE): codegen emits a `_start : () -> ()` + shim that calls `main` and drops its i32 result whenever a + parameter-less `fn main()` is present (`lib/codegen.ml`); + `tools/componentize.sh --command` wraps with the fetch-pinned + preview1->preview2 *command* adapter (wasmtime v44.0.1, sha256 + 8ff2ea78...) producing a component that exports `wasi:cli/run@0.2.x` + per `wit/affinescript.wit`. End-to-end gate: + `tests/componentize/command_smoke.sh` proves `wasmtime run` invokes + the component (exit 0), the ownership section survives, and the + lift is asserted. Purely additive: reactor consumers + game-loop + hooks are byte-unchanged. +- S6b (sockets on-ramp, DONE): new `net_shutdown(fd, how) -> Int` + builtin (Effect `Net`, reserved) lowers to + `wasi_snapshot_preview1.sock_shutdown` via the same on-demand + canonical-order pattern as S4a/S4b (`optional_wasi` entry 4 in + `lib/codegen.ml`). The command adapter bridges to + `wasi:sockets/tcp` internally without surfacing a host-side + `wasi:sockets/*` requirement; `tests/componentize/sockets_smoke.sh` + gates the lift + a clock+env+sock combo for canonical-order + regression. Real-host invoke under wasmtime exits 0 on + `net_shutdown(stdin, RDWR)` (ENOTSOCK dropped). Larger socket + primitives (recv/send/accept) need byte-level wasm IR for buffer + / network-address marshalling and remain a tracked follow-up + alongside env_at/arg_at. +- S6c: flip the default wasm target to component and demote the + preview1 stdout path to a named legacy target. Bundled cleanup: + replace the on-demand preview1 shims emitted by S4a/S4b/S6b + (clock_time_get, environ_sizes_get, args_sizes_get, + sock_shutdown) with native preview2 calls + (`wasi:clocks/monotonic-clock`, `wasi:cli/environment`, + `wasi:sockets/tcp`) — this only pays off once preview2 is the + default, hence its placement here rather than as a standalone + pre-flip slice. +""" +consequences = """ +- End-state is one-way (the component model becomes the canonical wasm + target) but reversible-in-progress: preview1 retained as the default + through S6b; the flip happens at S6c. +- S3..S6 are HARD-GATED on S2 (toolchain). Tracked as a follow-up + issue; this is disclosed, not hidden. +- typed-wasm ownership-section ABI is unchanged; the coordination + obligation with hyperpolymath/ephapax + hyperpolymath/typed-wasm is + recorded and binding. +- Source-to-source targets are untouched; no estate consumer churn from + this migration until S6, and even then only wasm-target consumers. +- This decision is settled; do not reopen without amending this ADR. + Ledger INT-03 in docs/TECH-DEBT.adoc; roadmap in docs/ECOSYSTEM.adoc. +""" +references = [ + "https://github.com/hyperpolymath/affinescript/issues/180", + "https://github.com/hyperpolymath/affinescript/issues/160", + "https://github.com/hyperpolymath/affinescript/issues/225", + "wit/affinescript.wit", + "lib/wasi_runtime.ml", + "lib/codegen.ml / lib/codegen_node.ml (wasm target)", + "docs/specs/SETTLED-DECISIONS.adoc (ADR-015 section)", + "docs/ECOSYSTEM.adoc (AffineScript <-> typed-wasm contract; INT-03)", + "hyperpolymath/typed-wasm (multi-producer ownership-section ABI)", + "WASI 0.2 / WebAssembly Component Model specification", +] + +[[adr]] +id = "ADR-016" +status = "accepted" +date = "2026-05-19" +title = "Effect-threaded async-boundary detection (side-table; #234)" +context = """ +The WasmGC CPS transform (ADR-013) detects the async boundary +*structurally*: a `let` whose RHS calls a primitive in a hardcoded set +`async_primitives = ["http_request_thenable"]` (lib/codegen.ml). This +was the deliberately-contained #225 PR3a scope. It is brittle: every +new async stdlib primitive must be added to the set, and a +user-defined `Async`-effecting function is invisible to it — blocking +the fully transparent ADR-013 surface for user async code. #234 asks +for the principled generalisation: the boundary is any call whose +effect row ⊇ `Async`. This requires per-call effect information at +codegen time. The AST (`lib/ast.ml`) carries no location or node id on +`ExprApp`, and the owner-chosen mechanism (AskUserQuestion +2026-05-19) is a typecheck→codegen side-table, NOT AST annotation. +""" +decision = """ +Thread per-call-site effect rows from typecheck to codegen via a +*side-table*, keyed by a deterministic shared call-site numbering. + +- *Keying.* A single shared traversal (new `lib/effect_sites.ml`) + assigns every `ExprApp` an ordinal in a fixed pre-order walk of the + program. Both typecheck and codegen obtain ordinals by calling the + *same* function, so keys cannot drift. No AST shape change (the + pipeline already threads one `prog` value through resolve → + typecheck → borrow → quantity → codegen unmodified, but ordinals — + not physical identity — are the contract, which is robust to any + future node rebuild). +- *Producer.* `Typecheck.check_program` populates + `(int, effect_row) Hashtbl.t` (call-site ordinal → resolved effect + row, declared *and* inferred) and returns it in its result context. +- *Pipeline.* `bin/main.ml` threads the table into the codegen entry + alongside `prog` (one extra argument; the existing source-to-source + backends ignore it). +- *Consumer.* The CPS detector's boundary predicate becomes “the + `let`-RHS call's effect row ⊇ `Async`” via a table lookup, replacing + `is_async_prim_call`/`async_primitives`. +- *Fallback / safety.* If the table has no entry for a site (e.g. a + pre-typecheck embedder path, or a synthesised node), codegen falls + back to the structural recogniser. The hardcoded set is retired only + once the table path is proven (final slice); over-conservative + fallback is always sound (= today's behaviour). + +Staged (ledger #234; each a gated PR, full `dune test --force` + +wasm e2e): +- S1 (this): ADR-016 + plan. No code change. +- S2: `lib/effect_sites.ml` shared numbering + typecheck builds & + returns the side-table. NO codegen behaviour change (table built, + unused) — pure, gate-neutral. +- S3: pipeline threads the table; codegen boundary predicate switches + to the table with structural fallback; new e2e proving a + *user-defined* `Async` fn triggers the transform (the payoff). All + existing http_cps_* / http_response_reader stay green. +- S4: retire the hardcoded `async_primitives` set (fallback remains + for table-miss only); doc truthing. +""" +consequences = """ +- Generalises to user-defined `Async` functions; new async primitives + need no codegen edit (closes the brittleness #234 names). +- One-way-ish: introduces a typecheck→codegen data channel (shared + infra). Reversible per-slice (S2/S3 are additive; the structural + recogniser stays as the sound fallback throughout). +- Source-to-source backends unaffected (they ignore the table arg). +- This decision is settled; do not reopen without amending this ADR. + Ledger #234 / CORE-02 in docs/TECH-DEBT.adoc. +""" +references = [ + "https://github.com/hyperpolymath/affinescript/issues/234", + "https://github.com/hyperpolymath/affinescript/issues/225", + "docs/specs/async-on-wasm-cps.adoc (ADR-013)", + "lib/codegen.ml (async_primitives / detect_async_base_case)", + "lib/typecheck.ml (check_program)", + "lib/effect_sites.ml (new; shared call-site numbering)", + "docs/specs/SETTLED-DECISIONS.adoc (ADR-016 section)", +] + +[[adr]] +id = "ADR-017" +status = "accepted" +date = "2026-05-19" +title = "ReScript block-module disposition: one module per file (split, do not nest)" +context = """ +ReScript `module Name { … }` block modules have no AffineScript block +form: the grammar (parser.mly:130-134) is a single optional `module +Path;` header, before imports — `module A { }` parse-errors. ADR-011 +settled "real modules": the file IS the module. The estate #229 ports +carry block modules; a SINGLE one per file is mechanical (hoist header, +drop braces, dedent — in the #229 canonical map, verified to parse), but +MULTIPLE block-modules in one file (e.g. standards/lol/.../OpenCyc.affine: +module Config{} module Concepts{} module Types{}…; 14 estate +occurrences) had no clean target. Escalated language-side as ESC-04 +(#262) — the bidirectional-evidence discipline of ADR-014 / #228. +(ADR-016 is effect-threaded async-boundary, #234/#270; ADR-018 is the +no-raw-escape doctrine, #245 — this is ADR-017, sequential.) +""" +decision = """ +One module per file — split, do not nest. Each ReScript `module X { +body }` becomes its own `X.affine` whose first declaration is `module +X;`, body dedented, `use` after the header. A file with N block-modules +is split into N files. The grammar is NOT extended with a block/nested +module form: that contradicts ADR-011 (file = module) and is a major +conflict-risky grammar change for cosmetic gain — the contortion ADR-012 +forbids. No compiler change (the file-header form already parses); this +ADR settles the porting doctrine + the #229 canonical-map structural +rule. Adjacent and explicitly OUT of scope: a split file still hits +Resolve.UndefinedModule until the repo module-path<->file-layout matches +the loader — cross-module graph coherence (INT-02 loader-bridge), +tracked in RESCRIPT-ELIMINATION.adoc Tier-4, never conflated here. +""" +consequences = """ +- The #229 block-module residue (idaptik-dlc-vm, standards/lol, parts of + burble) has a defined target: split-per-file. Full validation still + needs the per-repo module graph (INT-02), tracked separately. +- No language/compiler change; no estate consumer churn from this ADR. +- This decision is settled; do not reopen without amending this ADR. + ESC-04 #262; #229 canonical map in docs/RESCRIPT-ELIMINATION.adoc. +""" +references = [ + "https://github.com/hyperpolymath/affinescript/issues/262", + "https://github.com/hyperpolymath/affinescript/issues/229", + "lib/parser.mly (module_decl; one `module Path;` header per file)", + "docs/specs/SETTLED-DECISIONS.adoc (ADR-017 section)", + "docs/RESCRIPT-ELIMINATION.adoc (#229 canonical map; Tier-4 INT-02)", + "META.a2ml [[adr]] ADR-011 (real modules: file = module)", + "META.a2ml [[adr]] ADR-012 (grammar changes are correctness assertions)", +] + +[[adr]] +id = "ADR-018" +status = "accepted" +date = "2026-05-19" +title = "No raw/FFI escape: typed `extern` is the only host bridge" +context = """ +ReScript `%%raw("")` / `%raw` injects arbitrary untyped +host source. AffineScript's only host bridge is `extern fn` / `extern +type` (parser.mly extern_fn_decl/extern_type_decl, FnExtern body) — +typed, host-supplied, no body, no arbitrary-source escape. 14 estate +`%%raw` occurrences (#229 Tier-3, ESC-01 #245) had no clean target. +Escalated language-side — the bidirectional-evidence discipline of +ADR-014 / #228 and the affine spirit of explicit, typed boundaries. +(ADR-017 is the block-module disposition #262; this is ADR-018 because +main merged ADR-016 = effect-threaded async-boundary, #234.) +""" +decision = """ +No raw escape — there is no `%%raw` analogue, by design. Typed `extern +fn` / `extern type` is the SOLE FFI surface. Every estate `%%raw` ports +to a typed `extern` whose signature states the host contract the raw +blob assumed implicitly; the host implementation moves to the +embedder/runtime shim. AffineScript will NOT gain an untyped +intrinsic/`extern raw` block: an arbitrary-source hole defeats +affine/effect tracking at the boundary where the guarantees matter most +— precisely what the type-and-effect discipline (and ADR-012) exists to +prevent. Doctrine decision: `extern` already exists, no compiler change; +it settles the #229 canonical-map target and the FFI stance. +""" +consequences = """ +- A `%%raw` encoding genuine logic (not just a host call) is a design + smell the port surfaces: re-express as real AffineScript + a typed + `extern` for any true host primitive, never smuggled through. +- `%%raw`-bearing #229 files (idaptik Main/StartupError, parts of + burble) port under this doctrine; per-file execution is #229 per-repo. +- No language/compiler change; no estate consumer churn from this ADR. +- Settled; do not reopen without amending. ESC-01 #245. +""" +references = [ + "https://github.com/hyperpolymath/affinescript/issues/245", + "https://github.com/hyperpolymath/affinescript/issues/229", + "lib/parser.mly (extern_fn_decl; extern_type_decl; FnExtern)", + "docs/specs/SETTLED-DECISIONS.adoc (ADR-018 section)", + "docs/RESCRIPT-ELIMINATION.adoc (#229 canonical map; Tier-3 ESC-01)", + "META.a2ml [[adr]] ADR-012 (grammar changes are correctness assertions)", +] + +[[adr]] +id = "ADR-019" +status = "accepted" +date = "2026-05-19" +title = "Compiler distribution: GitHub Releases binaries + thin Deno/JSR shim (#260)" +context = """ +INT-04 (#181) is "publish compiler + runtime". The runtime JS packages +are JSR-publishable and that path is done (affinescript#261). The +*compiler* is a native OCaml binary — it is NOT a JSR or npm package, +so "publish the compiler" needed a distribution strategy. This gates +INT-10 (`affinescript-lsp` distribution, which "waits on a published +compiler"). The fork was escalated (issue #260; AskUserQuestion +2026-05-19) over four options: (1) GitHub Releases binaries, (2) +Guix/Nix channel, (3) thin JSR/npm shim, (4) Releases + shim. The +owner chose (4). +""" +decision = """ +Dual-channel, Releases-canonical: + +- *Canonical artifact.* Extend the existing `release.yml` (`v*` tags) + to build per-platform compiler binaries (Linux x86_64 first; + macOS/Windows as the build matrix allows) and attach them to the + GitHub Release together with a `SHA256SUMS` manifest. Releases are + the single source of truth; Guix/Nix and any npm tail become + *additive fetch-derivations over the same artifact* later (NOT in + this ADR's slices) — no second producer of the binary. +- *Ergonomic front door.* A thin Deno/JSR package + `@hyperpolymath/affinescript` (Deno-first per CLAUDE.md; no OCaml + source shipped) whose setup step downloads the host-platform binary + from the pinned Release, verifies it against the `SHA256SUMS` + checksum *embedded in that shim version*, caches it, and execs it. + HTTPS-only; no secrets in the shim; provenance = this repo's own + release workflow. Each shim version pins exactly one compiler + version+checksum (supply-chain: no floating fetch). +- *Consumer.* INT-10 `affinescript-lsp` resolves the compiler via the + shim (`deno add` ergonomics) — this is what unblocks it. +- npm tail: only if/when an npm-native consumer needs it, mirroring + the owner-sanctioned affine-vscode npm exception; deferred, not in + scope here. + +Staged (ledger #260 / INT-10; each a gated PR): +- S1 (this): ADR-019 + plan; file INT-10. No code. +- S2: `release.yml` per-platform binary + `SHA256SUMS` matrix. +- S3: the `@hyperpolymath/affinescript` shim package (download + + checksum-verify + cache + exec) + its tests; publish is owner-gated + via the existing manual JSR workflow (like INT-04). +- S4: wire INT-10 `affinescript-lsp` onto the shim. +""" +consequences = """ +- One-way: fixes the install contract (Releases artifact name/layout + + shim package name). Reversible per-slice; Releases-canonical keeps + Guix/Nix/npm strictly additive. +- INT-10 (`affinescript-lsp` distribution) is unblocked once S2+S3 + land; filed now. +- Supply chain: every shim release pins one checksummed binary; no + floating download. +- This decision is settled; do not reopen without amending this ADR. + Ledger #260 / INT-10 in docs/TECH-DEBT.adoc. +""" +references = [ + "https://github.com/hyperpolymath/affinescript/issues/260", + "https://github.com/hyperpolymath/affinescript/issues/181", + ".github/workflows/release.yml", + ".github/workflows/publish-jsr.yml (INT-04; owner-gated publish)", + "docs/PACKAGING.adoc (INT-04; the JS-package half)", + "docs/specs/SETTLED-DECISIONS.adoc (ADR-019 section)", +] + +[[adr]] +id = "ADR-020" +status = "accepted" +date = "2026-05-23" +ratified = "2026-05-24" +title = "Ownership-section schema versioning (0xAF sentinel + u8 version)" +context = """ +The `affinescript.ownership` Wasm custom section has been frozen at +v1 since typed-wasm Level 10 closure (2026-04-19) and is emitted in +identical form by AffineScript (`lib/codegen.ml`) and by ephapax +(`hyperpolymath/ephapax/src/ephapax-wasm`). The Rust verifier in +`hyperpolymath/typed-wasm` consumes it as the cross-compat target. + +V1 has *no version field*. Today this is fine because both +producers are at v1 and the verifier expects v1 unconditionally. +The first multi-producer ABI change — when L1–6 / L14–16 emission +lands — will need version discrimination. Adding the version field +*now*, while we are still at v1, is much cheaper than retrofitting +it under load. + +This ADR is filed alongside the broader typed-wasm widening roadmap +in `docs/specs/TYPED-WASM-ROADMAP.adoc` §"Tranche B". +""" +decision = """ +Move from v1 (unversioned) to v2 (versioned, with sentinel): + + v1 payload: + u32 entry_count + entry* + + v2 payload: + u8 version_tag ; 0xAF — "AffineScript Format" + u8 version ; 0x02 for v2.0 + u32 entry_count + entry* ; same entry shape as v1; no new entry fields in v2.0 + +The 0xAF sentinel is byte-distinct from any plausible v1 entry-count +low byte for a module with ≤ 0xAE entries — i.e. practically every +module in existence. v1 readers fail cleanly on a v2 section (bad +entry-count); v2 readers see the sentinel and dispatch. + +Coordinated landing protocol: + 1. Land ADR-020 in `hyperpolymath/affinescript` (this repo). + 2. Mirror ADR in `hyperpolymath/typed-wasm` (Rust verifier + dispatches on sentinel; accepts both v1 and v2 during + migration). + 3. Mirror ADR in `hyperpolymath/ephapax` (second producer emits + v2). + 4. Verifier ships v2-parse FIRST. Producers stay on v1 emit. + 5. Once verifier is deployed (in both OCaml + Rust paths), + producers flip to v2 emit together. + 6. After a deprecation window, v1-parse path is removed from + verifier. + +The v2.0 entry shape is identical to v1 — this ADR introduces no +new ownership kinds, no new per-entry fields. Future widening +(e.g. v2.1 adding a region-id field per entry) is a separate ADR. +""" +consequences = """ +- Forward-compatible: any future widening (L1–6 / L14–16 carriers, + per-entry annotations) can negotiate via the version byte without + another sentinel-or-no-sentinel war. +- v1 callers fail cleanly on v2 sections (bad entry-count parse) — + no silent corruption. +- Single coordinated landing across three repos; complexity is in + the coordination, not the bytes. +- This decision is PROPOSED. Land or supersede explicitly; do not + silently drift. +- Cross-references the typed-wasm roadmap in + `docs/specs/TYPED-WASM-ROADMAP.adoc` §"B1". +""" +references = [ + "docs/specs/TYPED-WASM-INTERFACE.adoc", + "docs/specs/TYPED-WASM-INTERFACE.a2ml", + "docs/specs/TYPED-WASM-ROADMAP.adoc", + "lib/codegen.ml (build_ownership_section, ~line 159)", + "lib/tw_verify.ml (parse_ownership_section)", + "hyperpolymath/typed-wasm (Rust verifier crate — coordination target)", + "hyperpolymath/ephapax (second producer — coordination target)", +] + +[[adr]] +id = "ADR-021" +status = "accepted" +date = "2026-05-23" +ratified = "2026-05-24" +title = "Multi-producer ABI coordination model for typed-wasm carriers" +context = """ +typed-wasm is a separate, language-agnostic compilation target with +its own repository (`hyperpolymath/typed-wasm`) and its own +producers (today: AffineScript + ephapax; potentially more later). +The `affinescript.ownership` custom section is the first +multi-producer ABI we own. + +Today coordination happens by `Refs #N` across repositories and +good intent. As the typed-wasm widening proposals land (ADR-020 +schema versioning, then carriers for L1–6 and L14–16), this +informal model will break. The session-note record makes clear that +each ABI conversation has been one-off and ad-hoc; the failure mode +is two producers landing incompatible changes in parallel because +nobody owns "the protocol". + +This ADR is filed alongside the typed-wasm widening roadmap in +`docs/specs/TYPED-WASM-ROADMAP.adoc` §"Tranche D" — D1 explicitly. +The reason to land *this* ADR before any concrete widening proposal +is that the coordination model is cheap now (no code), expensive +later (under conflicting pressure from multiple in-flight proposals). +""" +decision = """ +Adopt a formal four-axis multi-producer coordination model: + +(1) Spec authority — which artefact is the source of record: + - Today: `lib/tw_verify.ml` (OCaml) is the spec of record. + - On C5.1 closure (INT-12 / CONV-05): authority flips to + `hyperpolymath/typed-wasm` Rust verifier crate. + - The flip is itself an ADR — does not happen silently. + - Until the flip, OCaml binds; on conflict, OCaml wins. + +(2) Coordinated landing protocol: + - All ABI-touching ADRs live in all three repos (originated in + one, mirrored in the other two with `Mirrors hyperpolymath/X#N` + cross-references). + - Verifier always ships parse-support FIRST. + - Producers flip to emit support together, in a coordinated + window. + - Deprecation: a removed format MUST have a deprecation window + of at least the cycle between two coordinated landings. + +(3) Test parity protocol: + - Every producer maintains a fixture corpus matching the + typed-wasm cross-compat suite (INT-12 / C5.1). + - Each ABI change requires a new fixture in the cross-compat + suite from each producer. + - Verifier accepts a producer's emit only if the cross-compat + corpus is up-to-date for that producer. + +(4) Conflict resolution: + - In-flight ABI proposals from different producers MUST be + serialised — no parallel ABI ADRs. + - The serialisation queue is owned by `hyperpolymath/typed-wasm` + (today via issues; future a dedicated coordination ledger). + - A proposal blocked behind another in the queue is paused, not + refused. +""" +consequences = """ +- Single coordination ledger replaces ad-hoc Refs trails. +- ABI change cost goes up (more paperwork) but predictability goes + way up. +- Compatible with the existing Hypatia / gitbot rules (Hypatia + DOC-FORMAT / STACK-SIGNAL apply; ISSUE-CLOSURE applies to the + shared ADR mirrors). +- This decision is PROPOSED. Owner ratification required before any + ABI ADR (ADR-020 included) lands; ADR-020 is a "first test" of + this model and its own landing exercises the protocol. +- Cross-references the typed-wasm roadmap in + `docs/specs/TYPED-WASM-ROADMAP.adoc` §"D1". +""" +references = [ + "docs/specs/TYPED-WASM-INTERFACE.adoc", + "docs/specs/TYPED-WASM-ROADMAP.adoc", + ".claude/CLAUDE.md §Hypatia and gitbot-fleet standing rules", + "hyperpolymath/typed-wasm (coordination target)", + "hyperpolymath/ephapax (second producer — coordination target)", +] + +[[adr]] +id = "ADR-022" +status = "proposed" +date = "2026-05-26" +title = "Origin (region) variables and a Polonius-style loan solver for the borrow checker" +context = """ +lib/borrow.ml (~1585 LOC, OCaml) is the operational borrow checker. Slices +A, B, C-light landed plus the in-flight ref-to-ref / Slice C' / Slice D +PRs (#395, #396, #397) close out the lexical / operational fragment of +CORE-01 (#177). The remaining residual, called out in lib/borrow.ml's +closing TODO comment (lines 1575-1578), is the architectural shift to +*origin (region) variables*: a region var on each TyRef / TyMut with +subset constraints and a datalog-style loan-live-at-point solver. + +The operational checker is fundamentally lexical: each borrow has a span +where it was created and lives until its containing block exits, with NLL +last-use shortening via merge_arm_results, the ref_bindings graph, and +block-local sym tracking. Lifetime variables do not appear in types — +`&Int` is `TyRef TyInt`, no `'a` parameter. The checker cannot reason +about cross-function borrows that outlive a returned struct field +(callee_owned_params is a name-set heuristic, not a constraint +discharge), true reborrows through indirection (the matching residual +TODO at lib/borrow.ml:1570-1574), or CFG-join soundness beyond what +merge_arm_results handles inline — each new join point (ExprHandle, +ExprTry, ExprMatch) is retro-fitted with snapshot-restore-merge. + +STATE 2026-05-26 names this the dominant single item on the path to 1.0, +~6-12 weeks single-author. +""" +decision = """ +Adopt a Polonius-style origin/region constraint solver written in OCaml +in lib/borrow_polonius/, staged M1-M4 with the lexical checker as the +merge oracle through M3. + +Option survey: +- (a) Keep lexical + patch gaps — rejected; the next two soundness gains + (cross-fn reborrow soundness; multi-iter loop soundness without + spurious re-assignment rejection) are not expressible in the current + shape. +- (b) Custom origin-based solver in OCaml under lib/borrow_polonius/ + using Polonius's published `dlv_naive` shape (subset/3, + loan_live_at/2, loan_invalidated_at/2, error/1). CHOSEN. +- (c) Embed polonius-engine via Rust FFI — rejected; new toolchain dep, + cross-language error reporting, and the estate language policy + reserves Rust for performance-critical systems code with no native + alternative. There is one here (OCaml). + +Surface syntax: FULL ELISION for v1. `&T` / `&mut T` retain their spelling; +origins are introduced implicitly by the elaborator. Function signatures +the user writes today remain accepted; inference attaches and constrains +the origins. A future surface escape hatch (`&'a T`, `where 'a: 'b`) is +NOT introduced here — that is ADR-Future-A, contingent on grammar work +under ADR-012's Menhir-conflict-disclosure rule. + +AST changes (lib/ast.ml): +- `type origin_var = int`, fresh-int identity. +- `TyRef of origin_var option * type_expr` (was `TyRef of type_expr`). +- `TyMut of origin_var option * type_expr` (was `TyMut of type_expr`). +- TyOwn unchanged. + +borrow.ml changes: +- `type borrow` gains `b_origin : origin_var`. +- M1 defaults all sites to a single shared `default_origin = 0` ⇒ + semantically identical to today's lexical checker. + +Datalog shape: +- borrow_at(L, P) input +- loan_origin(L, O) input +- subset(O1, O2, P) input +- killed(L, P) input +- cfg_edge(P1, P2) input +- loan_live_at(L, P) derived (LFP over cfg_edge + killed) +- loan_invalidated_at(L, P) derived (loan_live_at ∩ access-conflict) +- error(P) derived (any invalidated) + +Subset constraints emitted at: let-binding of a ref RHS, assignment to +a ref binder, function call sites (formal-actual origin flow). Solver = +naive bottom-up worklist; complexity O((P × L) + (E × L)) per iteration; +adequate for the AffineScript corpus. Migrate to Polonius opt_naive only +if a corpus turns pathological. + +Migration plan (each milestone = own PR, all behind full dune runtest gate): +- M1: AST adds `origin_var option` field defaulted to None; b_origin + on borrow defaults to shared id 0. lib/borrow_polonius/types.ml stub + created, NOT wired into bin/main.ml. No behaviour change. +- M2: Elaborator emits fresh origins at each borrow site as + loan_origin facts; no subset constraints yet. No behaviour change. +- M3: Subset constraints from let/assign/call; solve.ml implements the + fixpoint; bin/main.ml runs BOTH checkers and diffs; new soundness-gain + fixtures land. CI gate: zero divergence on existing corpus. +- M4: Solver-only; lib/borrow.ml shrinks ~30-40% as ref_bindings / + block_local_syms / callee_owned_params / merge_arm_results retire. + CAPABILITY-MATRIX flips CORE-01 to fully closed; the residual TODO + comment is deleted. + +Test surface: each milestone keeps `dune runtest --force` green. M3 lands +≥5 new negative fixtures (programs Polonius rejects that lexical accepts — +the soundness gain). M4 lands ≥10 fixtures across both axes (Polonius +accepts what lexical spuriously rejects; Polonius catches what lexical +missed). + +This ADR records the SHAPE. It is opened as Draft pending owner +ratification of the shape before any code-changing PR follows. On +ratification, M1-M4 land as separate PRs. +""" +consequences = """ +- Closes the last named base-language soundness gap (CORE-01 residual + Slice C-full) → ROADMAP Phase 2 unblocked, Phase 3 (1.0) unblocked. +- lib/borrow.ml shrinks; each new join construct stops requiring a + bespoke retrofit. +- The reborrow-through-indirection residual TODO is discharged by M3 as + a side-effect — subset constraints chain reborrows naturally. +- Future surface origins (Rust-style `&'a T`) become additive — the type + system already carries the variable, the syntax PR exposes it. +- Multi-week effort (~6-12 weeks single-author per STATE estimate). + Smaller CORE-01 slices, INT-03, and CONV-04 can proceed in parallel. +- The OCaml solver carries proof-debt of its own (fixpoint must be + sound). M3's parallel-run diffing is the empirical correctness gate; + no mechanised solver proof is attempted here. +- lib/ast.ml shape change ripples through every pass that destructures + TyRef / TyMut. Mitigation: M1's `option` keeps case-pattern surface + compatible with one-liner `_ ->` wildcards in passes that don't yet + care. +- No new build-time dependency. No new surface syntax. The + affinescript.ownership typed-wasm carrier section is unaffected — + origins are internal to the borrow checker, never serialised. +- This decision is PROPOSED. Owner ratification required before M1 + lands. Once ratified, this ADR is settled; do not reopen without + amending it. +""" +references = [ + "https://github.com/hyperpolymath/affinescript/issues/177", + "lib/borrow.ml:1569-1584 (residual TODO comment)", + "lib/borrow.ml:1570-1574 (matching reborrow-through-indirection TODO)", + "lib/ast.ml (TyRef/TyMut/TyOwn definitions, lines 60-62)", + "docs/decisions/0022-polonius-origin-variables.adoc (long-form ADR)", + "docs/STATE-2026-05-26.adoc §Dominant cost item / §Critical path", + "docs/CAPABILITY-MATRIX.adoc (CORE-01 row)", + "docs/specs/SETTLED-DECISIONS.adoc (section pending on ratification)", + "ADR-012 (grammar changes are correctness assertions — gates future surface-origin ADR)", + "Niko Matsakis, \"An alias-based formulation of the borrow checker\", 2018 (subset/3 + loan_live_at/2 shape)", +] diff --git a/.machine_readable/descriptiles/NEUROSYM.a2ml b/.machine_readable/descriptiles/NEUROSYM.a2ml new file mode 100644 index 00000000..e1d34c09 --- /dev/null +++ b/.machine_readable/descriptiles/NEUROSYM.a2ml @@ -0,0 +1,21 @@ +# SPDX-License-Identifier: MPL-2.0 +# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) +# +# NEUROSYM.a2ml — Neurosymbolic integration metadata +[metadata] +version = "0.1.0" +last-updated = "2026-04-11" + +[hypatia-config] +scan-enabled = true +scan-depth = "standard" # quick | standard | deep +report-format = "logtalk" + +[symbolic-rules] +# Custom symbolic rules for this project +# - { name = "no-unsafe-ffi", pattern = "believe_me|unsafeCoerce", severity = "critical" } + +[neural-config] +# Neural pattern detection settings +# confidence-threshold = 0.85 +# model = "hypatia-v2" diff --git a/.machine_readable/descriptiles/PLAYBOOK.a2ml b/.machine_readable/descriptiles/PLAYBOOK.a2ml new file mode 100644 index 00000000..06ede86c --- /dev/null +++ b/.machine_readable/descriptiles/PLAYBOOK.a2ml @@ -0,0 +1,74 @@ +# SPDX-License-Identifier: MPL-2.0 +# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) +# +# PLAYBOOK.a2ml — Operational playbook +[metadata] +version = "0.1.0" +last-updated = "2026-05-26" + +[deployment] +# method = "gitops" # gitops | manual | ci-triggered +# target = "container" # container | binary | library | wasm + +[incident-response] +# 1. Check .machine_readable/STATE.a2ml for current status +# 2. Review recent commits and CI results +# 3. Run `just validate` to check compliance +# 4. Run `just security` to audit for vulnerabilities + +[release-process] +# 1. Update version in STATE.a2ml, META.a2ml +# 2. Run `just release-preflight` (validate + quality + security + maint-hard-pass) +# 3. Tag and push + +[maintenance-operations] +# Baseline audit: just maint-audit +# Hard release gate: just maint-hard-pass + +[npm-publish] +# Runbook for publishing a @hyperpolymath/* package to npm (lineage from +# issue #104, first applied to @hyperpolymath/affine-vscode@0.1.0 +# on 2026-05-26). +# +# Prerequisites (one-time per estate, completed 2026-05-26): +# * npm org `hyperpolymath` exists on registry.npmjs.org (free public tier) +# * Repo secret NPM_TOKEN holds an automation token with read+write +# access to the @hyperpolymath/* scope +# +# Per-package workflow: +# 1. packages//package.json +# "name": "@hyperpolymath/" +# "version": "X.Y.Z" +# "license": "MPL-2.0" +# "publishConfig": { "access": "public" } +# "repository": { "type": "git", "url": "...", "directory": "packages/" } +# "files": [...] # explicit allowlist; never publish the whole tree +# 2. .github/workflows/-publish.yml +# on push tag '-v*' +# - verify GITHUB_REF_NAME (minus '-v') == package.json version +# - write ${HOME}/.npmrc from secrets.NPM_TOKEN, then npm publish --access public +# - rm -f ${HOME}/.npmrc (always) +# Mirror affine-vscode-publish.yml's shape. +# 3. Tag push (signed, at origin/main): +# git tag -s -vX.Y.Z origin/main -m " X.Y.Z — npm publish (Refs #N)" +# git push origin -vX.Y.Z +# 4. Verify: npm view @hyperpolymath/@X.Y.Z +# +# Tag naming: distinct prefix per package so they never collide with the +# OCaml compiler's `v*` Release-workflow tags. e.g. affine-vscode-v*. +# +# Token rotation: rotate NPM_TOKEN after any wire-up where the token +# value transited a session transcript. + +[ci-required-checks] +# A required status check must be emitted UNCONDITIONALLY on every PR that can +# target the protected branch. Otherwise the unproduced context sits forever at +# "Expected — Waiting for status to be reported" — a silent merge block. +# Causes: (1) a producing workflow with an on.pull_request.branches filter; +# (2) a pin naming a renamed/migrated job (e.g. a standards reusable job name); +# (3) an external GitHub App check that doesn't post on every PR. +# Diagnosed + fixed 2026-06-21 (#645). Human doc: docs/ci/required-checks.adoc. +rule = "required-check implies emitted on every PR base (no pull_request.branches filter)" +diagnosed = "2026-06-21" +reference = "docs/ci/required-checks.adoc" +tracking-issue = "650" diff --git a/.machine_readable/descriptiles/STATE.a2ml b/.machine_readable/descriptiles/STATE.a2ml new file mode 100644 index 00000000..cf2d45e2 --- /dev/null +++ b/.machine_readable/descriptiles/STATE.a2ml @@ -0,0 +1,258 @@ +# SPDX-License-Identifier: MPL-2.0 +# STATE.a2ml — Project state checkpoint + +[metadata] +project = "affinescript" +version = "0.1.1" +last-updated = "2026-06-21" +status = "active" +authoritative-status-doc = "docs/CAPABILITY-MATRIX.adoc" +drift-flag = "STALE as of 2026-05-23 PM: this file's [components]/[features]/[project-context] still predate landed PRs since 2026-05-19. It MIRRORS, it does not LEAD. Authoritative sources by topic — readiness: docs/CAPABILITY-MATRIX.adoc; spine + AS↔typed-wasm contract: docs/ECOSYSTEM.adoc; coordination ledger / critical path: docs/TECH-DEBT.adoc; test taxonomy + PR-level gates: docs/standards/TESTING.adoc (added 2026-05-23); panic-attack SOP: docs/standards/PANIC-ATTACK.adoc (added 2026-05-23). Gate baseline: CAPABILITY-MATRIX records 260/260 at 2026-05-19 reconstruction; subsequent borrow-checker work has lifted it (#240 → 263, return-escape → 271/274, &mut surface → 278/281). The exact live number for any given commit comes from `dune runtest --force` — do not hard-code it here. (DOC-05, issue #176.)" +session-note-2026-06-21 = "SESSION: release-binaries pipeline repair + downstream gap triage + onboarding polish. (1) RELEASE — PR #641 (merged b8ba479) fixed .github/workflows/release.yml, which had been shipping v0.2.0 SOURCE-ONLY (regression from v0.1.1). Two bugs, both confirmed from the failed v0.2.0 run 26694097435: (a) immutable releases now forbid adding assets to a PUBLISHED release (linux leg died HTTP 422), and (b) the v0.2.0-added version-bake step used GNU `sed -i` which BSD/macOS sed rejects (extra characters at the end of l command), so both mac legs failed before building. Fix: create the release as a DRAFT, upload all binaries + SHA256SUMS into it, then publish (--draft=false --latest) last so it seals atomically; portable `sed -i.bak`. Compiler verified to build in release mode locally; --version/check/eval/compile work. v0.2.0 is immutable+assetless and cannot be amended, so binaries ship via a fresh v0.2.1 tag — OWNER action: a Claude-Code-web session cannot push v* tags (HTTP 403). Tracked in #646 (incl. downstream quandledb re-pin). (2) WASM cross-module constructor-link task was ALREADY LANDED before this session: #602 / ac98c81 (Closes #138) — register_imported_types in Codegen.gen_imports registers imported public enum constructors into variant_tags via the local gen_decl path; core-Wasm regression test in test_stdlib_aot.ml; verified locally 534/534. Downstream stapeln re-pins to ac98c81 and drops its carried git apply. (3) svalinn-gateway compiler gaps confirmed + filed: #642 (compile has no module search-path / project mode for nested src/**), #643 (float_to_string raises UnboundVariable at core-Wasm compile; wired only in resolve/typecheck/interp/Deno), #644 (empty match-arm body Pat => {} is a parse error). `pub extern fn` was checked and is SUPPORTED — not a gap. (4) ONBOARDING POLISH (this change): added tutorial lesson-01 (the docs/tutorial track previously started at lesson-02), an audience router (developers / maintainers / end-users) in README + NAVIGATION, and this note. Docs consolidation (two overlapping lesson tracks, wiki sync) tracked in #647. Note: guides/lessons/01-hello bare-println example is stale (parse error on current compiler) — folded into #647. This file MIRRORS; authoritative sources unchanged (see drift-flag)." +session-note-2026-05-26-publish-104 = "ISSUE #104 CLOSED — FIRST NPM PUBLISH LANDED. @hyperpolymath/affine-vscode@0.1.0 is now on registry.npmjs.org. Owner-action sequence completed today: (1) npm org `hyperpolymath` created on free public-package tier; (2) Granular Access Token generated for scope @hyperpolymath/* with Read+Write, uploaded to repo secret NPM_TOKEN; (3) signed annotated tag affine-vscode-v0.1.0 pushed at origin/main (RSA key 9639451754496E51D6B537CAD119017EBF695AB1); (4) .github/workflows/affine-vscode-publish.yml ran green — `npm publish --access public` succeeded; (5) `npm view @hyperpolymath/affine-vscode` resolves. Downstream consumers (my-lang#66, standards#160) which un-vendored their adapters on 2026-05-21 now have a working `npm install` path; vscode-smoke workflow (skipped per #381 while package was unpublished) will start exercising the live package on its next PR run. Lineage for future @hyperpolymath/* publishes: org+token are reusable; mirror `affine-vscode-publish.yml`'s shape (tag trigger, version-match guard, .npmrc write from secret, npm publish --access public). NPM_TOKEN rotation advised post-publish (token value transited a session transcript during wire-up); see .machine_readable/descriptiles/PLAYBOOK.a2ml [npm-publish] for the runbook." +session-note-2026-05-30 = "DOC-16 + DOC-17 — DOC-TRUTHING MONITOR FULLY MECHANICAL (issue #176). Two complementary in-repo guards now enforce the doc-truthing rules that were previously external-bot-only. DOC-16 (PR #476, tools/check-doc-truthing.sh): banner-PRESENCE invariant — fails if any over-claiming doc loses its CAPABILITY-MATRIX banner, if the matrix stops self-declaring primacy / loses its 'What AffineScript is NOT' section, or if STATE.a2ml drops its mirror keys; deliberately does NOT phrase-scan (a naive grep false-positives on the negating banners + future-roadmap text). DOC-17 (PR #475, tools/check-doc-overclaims.sh + tools/doc-overclaims.allow): the phrase-detection complement — a frozen-baseline RATCHET over README + docs/** (excluding CAPABILITY-MATRIX + TECH-DEBT, which quote the rule) that fails any NEW backend-breadth / 'production-ready' / stdlib-% occurrence beyond the 13-line baseline (all current entries legit: future-tense roadmap milestones + dated history snapshots + corrective banners); re-baseline via `--update` / `just doc-overclaims-bless`, diff = audit trail. Both wired into `just check` (guard recipe) + ci.yml build job, both toolchain-free bash (run without OCaml). #475 was originally a superset of #476 (built in parallel, same session); on discovering #476 had merged first, #475 was reworked to drop the duplicated banner check and keep only the unique ratchet, renamed check-doc-truth.sh→check-doc-overclaims.sh to avoid the name clash (DOC-DEDUP-clean). FOLLOW-UP (same day, post-#475-merge): the two near-identically-named guards were consolidated so neither lingers — the ratchet (incl. `--update` mode + tools/doc-overclaims.allow baseline) was folded INTO tools/check-doc-truthing.sh using #476's fail-accumulator/note() structure, and tools/check-doc-overclaims.sh was deleted. A SINGLE toolchain-free guard now enforces both presence invariants (DOC-04/05) and the over-claim ratchet (DOC-08/09) in one run; one CI step; the `guard` recipe runs it; re-baseline via `just doc-truth-bless`. DOC-17 folded into DOC-16 in the ledger. Gate-number policy unchanged (DOC-05): live `dune runtest --force` count never hard-coded. No compiler code touched. Refs #176 / #175." +session-note-2026-05-31-partial-port-488 = "RES-TO-AFFINE PARTIAL-PORT MODE #488 SLICES 1-3 (successor to closed #57). New --partial flag, a DISTINCT model from --translate: renders module-top-level functions (let f = (params) => body) into AffineScript fn skeletons whose output DELIBERATELY does not type-check (un-annotated ReScript fns can't yield a compilable fn) but DOES parse — un-inferable types are `_` holes, un-translatable expr/pattern are () /* TODO */ / _ /* TODO */ islands. (1) Slice 1 (#494 / c157a0f): fn skeletons + switch->match (variant/tuple/literal patterns) + expression translation (literals / idents / calls / binary-ops with float-op +.->+ and identity-equality ===->== normalisation / ++ concat / member + module-qualified access / ternary). The binary operator is an anonymous tree-sitter token, sliced from source between the operands. (2) Slice 2 (#495 / 78906f9): pipe-first -> desugaring (a->f(b) -> f(a, b); chains are left-nested so x->f->g(2) -> g(f(x), 2) falls out of the recursion); if/else; blocks with let statements. (3) Slice 3 (#496 / 4d4d1d4): array literals ([a, b] -> [a, b]) + record literals ({x, y} -> Rec #{ x: x, y: y }; AffineScript records are NOMINAL so an anonymous record gets the placeholder type Rec for the human to rename; field punning {x} -> x: x). Walker-internal: translate_expr / translate_switch / translate_pipe / translate_if / translate_as_block / translate_block(_inner) / translate_block_let (mutually recursive) + translate_pattern + partial_function + collect_partial + Walker.translate_partial; emitter emit_partial; main --partial (precedence over --translate, walker-only). 32 res-to-affine walker tests. VERIFIED LOCALLY each slice (apt-bootstrapped toolchain): full dune build exit 0; dune runtest green; main.exe check on the generated skeletons reaches resolution/type-checking WITHOUT a parse error (the partial-port bar — type/resolution errors are expected). REMAINING #488: JS objects / interpolated template strings, labelled-arg refinement, combining --partial with --translate (declarations + functions in one pass), and module-qualified-reference RESOLUTION (a ReScript->AffineScript module-mapping POLICY decision, deliberately not built without owner scoping). #488 stays OPEN. Refs #488 / #57 (closed)." +session-note-2026-05-31-migration-phase3-slice3-and-closure = "MIGRATION-ASSISTANT PHASE 3 SLICE 3 + #57 CLOSURE + SUCCESSOR #488. (1) PR #484 (squash ea8bef5): res-to-affine --translate extended to module-level literal value bindings — let = lowered to a typed AffineScript const name: T = value; for int/float/string/bool (AffineScript has no module-level let; a top-level value binding is const, annotation + semicolon required). New walker.ml helpers classify_number / translate_literal / translate_let_const + a let_declaration branch in collect_translations. Conservative: call bodies, ref(...) mutable-globals, destructuring patterns, and exotic numbers (hex/octal/binary/signed/scientific/underscored) are skipped — number forms limited to plain decimal int and D+.D+ float. New fixture test/fixtures/phase3c.res + 4 gated walker tests (walker-phase3c-let-const). (2) VERIFIED LOCALLY (apt-bootstrapped toolchain): full dune build exit 0; 22 res-to-affine walker tests green against pinned grammar 990214a; compiler oracle main.exe check on the generated consts returns Type checking passed. (3) #57 CLOSED as completed (owner-directed) — the declaration-translation goal is delivered across slices 1 (#477 / 2763909) / 2 (#481 / 3484010) / 3 (#484 / ea8bef5) plus the session-start toolchain hook (#482 / 138baac); --translate now renders every self-contained top-level declaration (type aliases, sums, structs, generics, literal consts) to compiler-verified AffineScript. (4) SUCCESSOR #488 filed: res-to-affine partial-port mode (switch->match / function translation) + module-qualified-reference resolution — both OUT of the every-emitted-form-type-checks model by construction (switch->match is an expression needing whole-function translation of usually-un-annotated ReScript fns, which AffineScript fn signatures can't type-check; qualified refs parse post-#228 but won't resolve without a module-mapping story). #488 is a child of estate migration epic #406; remaining migration work continues there. Refs #57 (closed) / #488 / #406." +session-note-2026-05-30-migration-phase3 = "MIGRATION-ASSISTANT PHASE 3 SLICES 1+2 (issue #57) + SESSION-START HOOK. (1) res-to-affine --translate now turns fully-structural ReScript type declarations into compiler-verified AffineScript; two slices landed off main. Slice 1 (PR #477, squash 2763909): primitive type aliases (type userId = int -> type UserId = Int) and simple sum types (type color = Red | Green -> type Color = | Red | Green; payloads mapped, Circle(float) -> Circle(Float)). Slice 2 (PR #481, squash 3484010): record types -> AffineScript struct (type point = {x: int} -> struct Point { x: Int }) and GENERICS — ReScript type params 'a mapped to AffineScript [A], threaded through aliases, sums, and records (param refs in bodies map 'a -> A). (2) New code is walker-internal (tools/res-to-affine/walker.ml): translate_type_ref / translate_variant / translate_record_fields / affine_type_param / extract_type_params / translate_type_binding / collect_translations, plus Walker.translate and Emitter.emit_translation; CLI gains --translate (walker-only; no-op under --engine=scanner). Detection keys on tree-sitter node TYPE not field labels, robust to the grammar's optional alias/body-field ambiguity. (3) CONSERVATIVE BY CONSTRUCTION — a decl translates only when every part is representable; qualified-path RHS, non-primitive/opaque refs, nested generics (array), object types, GADT returns, variant spreads, and records with mutable or optional-? fields are SKIPPED (left in the marker block + quoted original), never mis-translated. Two normalisations keep output referenceable: lower-case type names capitalised (color->Color) and type vars mapped ('a->A), because lib/parser.mly reads a lower-case name in type position as a TyVar not a TyCon. (4) QUALIFIED-PATH RHS DEFERRED: Belt.Map.t now PARSES (the #228 grammar gap closed — parser.mly:515 wires qualified_type_name into type_expr_primary) but Belt::Map::T would not RESOLVE against a non-existent target module, which would break the every-translated-form-type-checks guarantee; it waits for a module-mapping story. Remaining Phase-3 slices: let->const for literal bindings, switch->match (needs body translation), qualified-path resolution. #57 stays OPEN. (5) VERIFICATION: unlike earlier res-to-affine work this was built+tested locally — the OCaml toolchain was apt-bootstrapped (opam.ocaml.org is network-blocked here, but Ubuntu packages match the project pins: dune 3.14 / cmdliner 1.2 / alcotest 1.7 / menhir 20231231 / sedlex 3.2 / ppx_deriving 5.2 / ppxlib 0.32 / yojson 2.1 / js_of_ocaml 5.6). Full dune build exit 0; dune runtest green (21 res-to-affine cases incl. gated walker tests against pinned grammar 990214a); and the COMPILER ORACLE (main.exe check, the #228 method) returns Type checking passed on every generated form. (6) CONFIG: added .claude/hooks/session-start.sh (SessionStart hook) registered in .claude/settings.json so future web sessions auto-provision this exact apt toolchain + tree-sitter CLI + pinned grammar — synchronous, idempotent, best-effort, web-only (CLAUDE_CODE_REMOTE guard); settings.json also gains dune/tree-sitter build+test permissions. (7) Both #477 and #481 were admin-merged while CI runners were backlogged; #481 was fully locally verified before merge. Refs #57 / #228." +session-note-2026-05-26 = "MIGRATION-ASSISTANT PHASE 2C + REPO-TIDY STACK (T-1..T-7) + STDLIB BLOCKER CLOSURES. (1) PR #357 — feat(res-to-affine) Phase 2c on branch claude/epic-gauss-Mbi0E: tree-sitter walker extended from #322's single Side_effect_import detector to all six anti-patterns. New detectors in tools/res-to-affine/walker.ml — detect_raw_js (any extension_expression node), detect_untyped_exception (try_expression / call to value_identifier \"raise\" / member_expression starting with Js.Exn or ending with Promise.catch), detect_mutable_global (top-level let_declaration whose body is call to value_identifier \"ref\", OR top-level mutation_expression), detect_inline_callback_record (>=3 inline function values in a record literal or a call_expression's arguments list — handles direct function children + labeled_argument + record_field wrappers), detect_oversized_function (function node whose stop.row - start.row + 1 > 50). Module-toplevel predicate refactored to a single at_module_toplevel helper that walks the ancestor chain refusing on `function` or `let_binding` body. Findings deduped by (kind, line) so the AST walker doesn't emit more bullets than the line-based scanner. CLI flipped --engine=walker to the default in tools/res-to-affine/main.ml (scanner remains as fallback when grammar / tree-sitter CLI missing — pre-existing graceful-fallback path from #322 unchanged). Scanner.kind extended with Inline_callback_record + Oversized_function variants; scanner.ml gives them labels + guidance. New fixture test/fixtures/phase2c.res exercises the two walker-only kinds. test_walker.ml grew per-kind tests under three new suites (walker-side-effect-import / walker-phase2c-parity / walker-phase2c-new-kinds). README + walker.mli updated. (2) Phase 2c CI fix push (e7a3a44): initial Phase 2c commit had build+lint failing; defensive rewrite replaced the labeled-only `mk_finding ~kind:K ~line:L ~excerpt:E :: acc` emit pattern with explicit `let finding : Scanner.finding = { ... } in finding :: acc` matching Phase 2b's working style, plus replaced non-ASCII glyphs in comments (≥ → >=, … → ...) defensively. (3) DIAGNOSED: `main` itself is red. PR #359 (T-1, pure file-rename + delete + one-line CONTRIBUTING.md edit, zero OCaml touched) also fails on build+lint. That conclusively shows the build failure is inherited from main, not introduced by Phase 2c or any tidy work. Matches CLAUDE.md §\"CI signal reliability\" — auto-merge fires even when build is red; the historical pattern of #334/#335/#336/#344 landing red applies. Root cause on main is NOT diagnosed in this session (needs the actual `dune build` log; WebFetch on the actions UI returns React skeletons, not log content; container has no OCaml toolchain to repro locally). Filed implicit follow-up: someone with shell access should run `gh run view --log-failed ` against any of the recent failing runs to identify the underlying lib/ compile error. (4) REPO-TIDY STACK (T-1..T-7) — six small PRs landed off origin/main, each reviewable in isolation, none touching .ml/.mli, all inheriting the same baseline build/lint failure as PR #359 (proof above). T-1 PR #359 *MERGED* — AI.a2ml → 0-AI-MANIFEST.a2ml (Hypatia root_hygiene rule + sibling-repo convention), delete AI.djot (superseded), docs/TECH-DEBT-alt.adoc → docs/TECH-DEBT.adoc (restoring the canonical name every cross-link in the repo already points at; PR #356's -alt suffix during the #351 split is dead weight post-#355). T-2 PR #360 — delete 2,182 lines of submarine-game docs (DAMAGE-SYSTEM.md, CONTROLS-REFERENCE.md, GAME-BUNDLING-STRATEGY.md), zero cross-references. T-3 PR #365 — 13 loose root .md/.adoc moved into docs/ subtree (ABI-FFI-README → docs/reference/ABI-FFI.md, ALPHA-1-RELEASE-NOTES → docs/history/, BACKEND-{ANALYSIS,IMPLEMENTATION} → docs/architecture/, COMPILER-CAPABILITIES → docs/reference/, EXPLAINME/KNOWN-ISSUES/NAVIGATION/PROOF-NEEDS/ROADMAP → docs/, LICENSING-GUIDE/SECURITY-SETUP → docs/governance/, RSR_OUTLINE → docs/standards/RSR-OUTLINE.adoc), two new subdirs docs/architecture/ + docs/reference/, cross-refs fixed in CAPABILITY-MATRIX / BACKEND-IMPLEMENTATION / CONTRIBUTING / NAVIGATION / res-to-affine; root drops 17→5 community-health files. T-4 PR #366 — RSR_COMPLIANCE.adoc at root with four documented deviations (no guix.scm/flake.nix; STATE.scm substituted by .machine_readable/descriptiles/STATE.a2ml because .scm is reserved for Guix per language policy; 7 TS exemptions; 2 runtime exemptions — all cite CLAUDE.md as authoritative). T-5 PR #367 — wiki/README.md rewrite (drops ~20 dead links to non-existent .md files, updates stale ../ROADMAP.md → ../docs/ROADMAP.adoc path, annotates Features-at-a-Glance examples with current maturity, switches Ownership example from &File/&mut File sigil syntax to canonical ref File/mut File keyword types) + parse-only honesty banner on wiki/language-reference/dependent-types.md + partial honesty banner on wiki/language-reference/traits.md. T-6 — direct issue triage, no PR: closed #246 (ESC-02 JSON.t — stdlib/json.affine delivers Json ADT + encoders/decoders/get_field/stringify) and #247 (ESC-03 Dict.t — stdlib/dict.affine delivers empty/from_pairs/get/contains/size/insert/set/remove/keys/values), both with confirmation comments referencing the LANDED status in docs/TECH-DEBT.adoc STDLIB-02/03 rows. T-7 this PR — adds this session-note + the T-1..T-7 ledger entry below in docs/TECH-DEBT.adoc. (5) ALSO RESOLVED EARLIER IN SESSION (pre-T-6, as part of addressing the sustainabot tracker blockers the user asked about): #161 (Json) and #162 (Dict) closed with explicit owner-author-acknowledged closure comments dated 2026-05-24; this session was the explicit owner request to act on those. Migration-assistant tracker #57 stays open — Phase 2c lands #322's Phase 2b → 2c walker work but Phase 3 (partial translation of pure-structural forms — the phase the sustainabot tracker's exit criterion 3 specifically gates on) remains unstarted. (6) OPEN PRs AT SESSION END: #357 (Phase 2c, ready to merge — author/owner discretion on the baseline-red CI), #360/#365/#366/#367 (T-2/T-3/T-4/T-5, same baseline-red situation; pure-rename / pure-delete / pure-doc-add changes, no behavioural risk). (7) NOT DONE IN THIS SESSION: actual root-cause diagnosis of main's red build/lint; ~20 missing wiki pages (installation, hello-world, expressions, errors, package-manager, lsp, formatter, linter, stdlib subpages, design/*); guix.scm or flake.nix addition; full wiki content refresh beyond README + two banners; sync of wiki/ to the GitHub-wiki upstream repo (separate git repo, owner pushes manually). (8) CROSS-REPO: this session is AffineScript-only — the gitbot-fleet sustainabot tracker that prompted #161/#162/#57 lives in a different repo and was not touched here." +typed-wasm-formalisation-2026-05-23 = "AS↔TYPED-WASM INTERFACE FORMALISED + WIDENING ROADMAP + TWO PROPOSED ADRs. (1) docs/specs/TYPED-WASM-INTERFACE.adoc + .a2ml — formal spec of the v1 interface (was previously scattered across ECOSYSTEM.adoc + code comments in lib/codegen.ml, tw_interface.ml, tw_verify.ml). Captures: enforced surface (L7 aliasing + L10 linearity + L13 module isolation negative-form); carrier binary layout (u32 entry_count + per-entry [u32 func_index | u8 param_count | u8[n] param_kind | u8 ret_kind], kind ∈ {0 Unrestricted, 1 Linear, 2 SharedBorrow, 3 ExclBorrow}, all LE, omit-when-empty, no version field in v1); producer machinery file-line pointers (lib/codegen.ml ~110-177 + ctx.ownership_annots ~2312/~2629); consumer machinery (tw_verify.ml 332 LOC, tw_interface.ml 276 LOC, primary entry verify_from_module); CLI surface (verify / verify-boundary / verify-bridge / interface — verify-boundary's pre-2026-04-19 silent-success bug fixed by f6089a2); test coverage (test_tw_isolation.ml 5 cases + E2E Boundary Verify 3 cases + ownership-roundtrip 3 cases; honest gap: no C5.1 cross-compat fixtures shipped); multi-producer responsibilities (AS = ref producer + spec of record until C5.1; ephapax = second producer same format; typed-wasm = spec + Rust verifier + cross-compat target); deliberate non-features NF-001..NF-005 with roadmap anchors; stability promise (v1 frozen; unilateral changes forbidden). (2) docs/specs/TYPED-WASM-ROADMAP.adoc + .a2ml — making typed-wasm a natural + optimal target. Five tranches with unblock graph: A (ergonomics, no ABI change: A1 --typed-wasm gating flag, A2 report-typed-wasm JSON, A3 extract ownership-section to dedicated module, A4 diagnostic span parity) → B (schema hygiene, additive single-byte ABI: B1 = ADR-020) → C (compiler-side foundations for L1–6/L14–16: C1 region inference / CORE-01 Phase 3, C2 session-type+capability tracking, C3 CONV-02/ADR-016 S2..S4 async recogniser, C4 capability-typed externs) → D (multi-producer ABI widening: D1 = ADR-021, D2 affinescript.regions, D3 affinescript.{capabilities,session,choreography}) → E (cross-compat closure: E1 INT-12/CONV-05 fixtures into typed-wasm C5.1, E2 Rust-verifier parity out-of-scope-here). Recommended sequencing: A1→A3→A2→E1→B1→A4→D1→C1..C4→D2/D3→E2. (3) ADR-020 (PROPOSED, META.a2ml): ownership-section schema versioning — v1 unversioned → v2 with 0xAF sentinel + u8 version + entry_count. v1 readers fail cleanly on v2 (bad entry-count parse); v2 readers dispatch on sentinel. Coordinated landing: verifier ships parse first, producers flip emit together. (4) ADR-021 (PROPOSED, META.a2ml): multi-producer ABI coordination model — four axes (spec authority OCaml→Rust on C5.1 closure; coordinated landing protocol; test parity protocol; conflict resolution via serialised queue owned by typed-wasm). ADR-020 is itself the first test of ADR-021's protocol. (5) Cross-links: ECOSYSTEM.adoc §contract paragraph now points at TYPED-WASM-INTERFACE.adoc as authoritative; CAPABILITY-MATRIX.adoc typed-wasm row links to INTERFACE + ROADMAP and notes L13 alongside L7/L10; TECH-DEBT.adoc Stage E section gets header pointer + DOC-13/14/15 ledger entries (interface, roadmap, ADRs); docs/README.adoc + NAVIGATION.adoc list both new specs. (6) Pure documentation work — no compiler code changed, no behaviour change. Both ADRs require owner ratification before they bind." +stage-d-batch-2026-05-23 = "INT-01 ::-in-value-expr + BUG-005 deferred fixture + STDLIB-04 audit. (1) INT-01 follow-up: lib/parser.mly line ~835 — added `upper_ident COLONCOLON lower_ident` production emitting `ExprField(ExprVar Mod, lower_ident)`, the same AST shape `Mod.fn` already produces. Disambiguated from the line-above `Type::Variant` rule by lower_ident vs upper_ident. No resolver change required: Resolve.lower_qualified_value_paths handles both `.` and `::` syntaxes identically because the lowering pattern-matches on the ExprField shape, not the source separator. test/e2e/fixtures/cross_caller_qualified_colon{,_alias}.affine + 2 alcotest cases in qualified_value_tests pin both `use Mod; Mod::fn(x)` and `use Mod as M; M::fn(x)`. The remaining INT-01 'parser gap, not resolver' note in the ledger is now closed. (2) BUG-005 deferred regression test landed (test/test_e2e.ml, wasm_gc_loud_fail_tests): `fn main() -> Int { return totally_undefined_callee(42); }` is fed directly to Codegen_gc.generate_gc_module (no resolve step) and the emitted error must contain the unknown name — which UnboundFunction's format string includes verbatim. Closes the deferred-status entry on the [[closed-bug]] BUG-005 record. (3) docs/STDLIB-EXTERN-AUDIT.adoc — full triage of 135 extern fn + 24 extern type across 11 stdlib files into 4 classes (built-in / typed-boundary-bridge / adapter-Deno / adapter-Node); per-row status (done/partial/stub-only) and unblock condition for each module. STDLIB-04's ledger entry now points here. Implementation PRs close one row at a time; the audit doc is the canonical roadmap, not STATE.a2ml. (4) Not yet verified locally — no OCaml toolchain in this remote execution environment. CI is the verification surface; parser change is low-risk (single production, no Menhir conflict — `upper_ident COLONCOLON lower_ident` is unambiguous against the prior `upper_ident COLONCOLON upper_ident` rule by token class)." +hygiene-uplift-2026-05-23 = "TEST + BENCH + COVERAGE + PANIC-ATTACK SOPs LANDED. (1) docs/standards/TESTING.adoc — authoritative test taxonomy (alcotest unit / alcotest E2E / fixture / golden / smoke / microbench), PR-level expectations, deferred-regression-test protocol. Replaces the stale docs/guides/TESTING-REPORT.adoc (2025-12-29 snapshot, 47/27 pass rate, contradicted by live gate since early 2026); retired in-place with redirect to CAPABILITY-MATRIX + standards/TESTING. (2) bench/ scaffold — dune stanza under @bench alias, 4 phase microbenches (lex/parse/typecheck+quantity/codegen-wasm) over 3 shared fixtures (tiny_arith, medium_struct_match, larger_enum). just bench / just bench-record recipes. CI job `bench-visibility` uploads bench-output.log as artifact. Visibility-only — no merge-blocking threshold. (3) bisect_ppx coverage wiring — added as with-test depend in dune-project + affinescript.opam. just coverage recipe (HTML to _coverage/, gitignored). CI job `coverage-visibility` uploads _coverage as artifact + summary to step output. Visibility-only — no enforced floor. (4) docs/standards/PANIC-ATTACK.adoc — SOP for the estate compliance scanner. Disposition vocabulary (fix/accept/suppress/defer); standing-suppression record format; cross-refs to MAINTENANCE-CHECKLIST. .github/workflows/panic-attack.yml: weekly Sunday 03:00 UTC cron + workflow_dispatch, installs panic-attacker via cargo from road-skate/features/panic-attacker, uploads log as artifact. (5) docs/README.adoc + NAVIGATION.adoc updated — standards/ now lists TESTING + PANIC-ATTACK; guides/TESTING-REPORT marked RETIRED. (6) .gitignore — added /_coverage/, bisect*.coverage, /bench-runs/." +session-note-2026-05-20-pm = "POST-#303 CATCH-UP: #297/#300/#301/#302/#304 + repos-monorepo retirement decision. (1) PR #300 MERGED (sha 8110548) — closed #297. lib/version.ml single source of truth + release.yml sed-bake step. Binary --version self-reports 0.1.1 now. (2) PR #304 OPENED for #301 (repo-wide PMPL→MPL-2.0 SPDX surface sweep, Option 3 of the three in the issue): 673 files / +1127 −711 / 1116 SPDX headers flipped / code generators emit MPL-2.0 / .machine_readable license fields reconciled / dune-project (license …) reconciled from stale MIT-OR-AGPL → MPL-2.0 / affinescript.opam regenerated → license: \"MPL-2.0\" / LICENSES/LICENSE-MPL-2.0 added (canonical Mozilla 373-line text) / root LICENSE narrative preserved unchanged (PMPL still narrated as preferred + MPL-2.0 fallback explanation intact) / vendored sister-repo subtrees (road-skate/ + affinescript-vite/) explicitly excluded. Verification: 295/295 dune tests + 6/6 shim tests + binary 0.1.1 + grep for residual PMPL SPDX → empty (excl. vendored). Side-effect noted in PR body: pre-existing dune-project symlink-to-.build/dune-project got broken by sed atomic-rename; both files now real-file content-identical. (3) Issue #302 CLOSED — owner confirmed GitLab+Bitbucket mirroring is intentionally off; Codeberg + Radicle are the active forges. (4) repos-monorepo RETIREMENT decided: snapshot tarred (23MB gzip / 144MB extracted / 23,970 entries / canonical origin/main shallow-clone / .git excluded) to /mnt/c/Users/USER/Downloads/repos-monorepo-snapshot-2026-05-20.tar.gz; owner-upload-to-Google-Drive then `gh repo delete hyperpolymath/repos-monorepo --yes`. repos-monorepo#9 (nested casket-ssg coherence) becomes moot post-delete since the aggregator that would have carried it is gone — standalone hyperpolymath/casket-ssg already merged casket-ssg#8 + has its own instant-sync.yml for forge propagation. (5) DOC/MEMORY: this session-note + the TECH-DEBT.adoc INT-04 update (mentioned #297/#300 + #301/#304) are part of follow-up PR to #303. The 4 earlier-saved reference memories (macos-13 retired, gitbot Refs-auto-close, estate mirror state, JSR publish recipe) all still apply unchanged." +session-note-2026-05-20 = "INT-10 / #282 CLOSURE + JSR PUBLISH + ESTATE macos-13 SWEEP + DOC/MEMORY HARDENING. (1) ISSUE #282 ACTUALLY-DELIVERED end-to-end. PRs that landed today: #291 (partial pins.js fill for linux-x64 + macos-arm64, shim 0.1.1 — bridging before macos-x64 leg landed); #292 (release.yml: retired macos-13 GH-hosted runner → macos-15-intel — root cause of v0.1.0 macos-x64 leg sitting queued ~10h, traced to actions/runner-images#13046/#13402/#13634 'macos-13 fully unsupported since 2025-12-04'); v0.1.1 tag cut, full 3-platform release (linux-x64/macos-arm64/macos-x64) + SHA256SUMS published; #293 (delete 4 orphan .res files + the affine-res ReScript-bindings package — same precedent as packages/affine-ts/ removal 2026-05-11, fixed the recurring governance Language/anti-pattern check failure on every PR); #294 (release.yml checksums job: pass --repo \"$GITHUB_REPOSITORY\" so `gh release download` doesn't probe a missing .git — bug hidden on v0.1.0 by the macos-13 stall that pre-empted the checksums leg); #295 (final shim closure: pins.js VERSION→v0.1.1, all 3 sha256 fields filled, shim @hyperpolymath/affinescript 0.1.1→0.1.2, SHIM_SPEC bumped lock-step in tools/affinescript-lsp/src/compiler.rs); #298 (cross-runtime refactor: Deno/Bun/Node detection at module load + helper layer for hostOs/hostArch/envGet/readBytes/writeBytes/mkdirRecursive/chmodExec/spawnInherit/thisIsMain — was Deno-only — plus mod.d.ts + triple-slash reference so JSR fast-check finds the types and emits no warning; .claude/CLAUDE.md gained Runtime Exemptions section as the carve-out from the estate-wide Bun/Node ban); #299 (relicense the JSR shim package only — packages/affinescript-cli/{deno.json + 4 SPDX headers} — from MPL-2.0 to MPL-2.0 because JSR validates against the SPDX list and `MPL-2.0` isn't on it; wider repo PMPL→MPL-2.0 deliberately deferred — #301). (2) JSR FIRST-TIME PUBLISH RECIPE walked: claim scope @hyperpolymath → create package record (jsr.io/new — dry-run won't catch this) → link trusted GitHub repo at package or scope settings (OIDC actorNotAuthorized otherwise — dry-run won't catch this either) → SPDX licence → mod.d.ts + triple-slash → cross-runtime tickbox. publish-jsr.yml workflow (manual workflow_dispatch); 6 dispatched runs today before all gates aligned (~7:33 UTC). Verified live: https://jsr.io/@hyperpolymath/affinescript 200; meta.json `latest: 0.1.2`. Real fetch+verify+exec smoke against the v0.1.1 release ran green on linux-x64 (resolveCompiler → SHA-verify → cache → exec `--version`). (3) ESTATE macos-13 SWEEP — sole non-affinescript first-party hits: `casket-ssg#8` (release.yml: `runner: macos-13` → `runner: macos-15-intel`, merged) + `proven#29` (zig-ffi.yml: `os: macos-13` → `os: macos-15-intel`, merged) + `repos-monorepo#9` (nested casket-ssg copy in boj-cartridges/polystack/poly-ssg/casket-ssg/.github/workflows/release.yml — still open as of writing, coherence-only since instant-sync.yml in each repo independently propagates). gh search code confirmed only 3 distinct first-party repo hits across the 360-repo active estate. (4) DISCOVERED — GitLab + Bitbucket mirrors NOT operating: empirical inspection of mirror.yml runs on affinescript / casket-ssg / repos-monorepo all `completed/skipped`; `GITLAB_MIRROR_ENABLED` / `BITBUCKET_MIRROR_ENABLED` repo vars unset; `GITLAB_TOKEN` / `BITBUCKET_TOKEN` secrets absent (only FARM_DISPATCH_TOKEN present); gitlab.com probes 302, bitbucket.org probes 404. repos-monorepo DOES have CODEBERG_MIRROR_ENABLED + RADICLE_MIRROR_ENABLED set → active forges are Codeberg + Radicle + whatever `.git-private-farm` fans out to. Filed as open question: #302. (5) FOLLOW-UPS: #297 (version-string drift — bin/main.ml/lib/repl.ml/lib/lsp_server.ml/lib/onnx_codegen.ml/dune-project all hardcoded `0.1.0` even on the v0.1.1 binary; fix PR #300 introduces lib/version.ml single-source-of-truth + release.yml sed-bake step on every tag); #301 (wider PMPL→MPL-2.0 estate-relicense decision needed); #302 (mirror status — intentional-off or unfinished-setup). (6) DOCUMENTATION: this file (a session-note); docs/PACKAGING.adoc (status table for all 3 JSR-publishable packages + first-publish-gotchas section); docs/TECH-DEBT.adoc (INT-04 / INT-10 lines updated to reflect actual publish). Memory: reference_macos_13_runner_retired.md, reference_estate_gitbot_auto_closes_on_refs.md, reference_estate_mirror_state_2026_05_20.md, reference_jsr_publish_recipe.md all saved + indexed in MEMORY_STANDING.md." +session-note-2026-05-03-c = "EXTERN/VSCODE/ARRAY/PATCON BATCH. (1) `extern fn name(...) -> Ret;` and `extern type Name;` now parse — added EXTERN keyword to lexer/token/parse_driver, FnExtern to fn_body and TyExtern to type_body in AST, extern_fn_decl + extern_type_decl rules in parser.mly. Resolve registers the symbol; Typecheck.check_fn_decl special-cases FnExtern to register the polymorphic scheme without body checking; Codegen.gen_decl emits a real `(import \"env\" \"\" (func ...))` for each extern fn, mirroring gen_imports's cross-module shape. Borrow + Quantity skip extern fns. lib/dune now demotes warning 8/9 from error to warning so the new variants don't require lock-step updates across all 27 codegens (any non-Wasm codegen that doesn't handle FnExtern raises Match_failure with file:line at runtime — correct signal for 'this target has no story for host-supplied implementations'). 192 tests; 0 regressions. (2) Issue #35 Phase 2: stdlib/Vscode.affine and stdlib/VscodeLanguageClient.affine ship the ~12 + 3 binding declarations from the issue's API inventory (registerCommand, getConfiguration, showInformationMessage, createTerminal, ...). packages/affine-vscode/mod.js is the JS-side adapter that translates each `extern fn` invocation into the corresponding vscode/lc API call, with a JS-side handle table for opaque host objects and a string-marshal helper that reads `[u32 length][utf-8 bytes]` out of the wasm memory. Adapter returns a namespaced object `{ Vscode: {...}, VscodeLanguageClient: {...} }` matching the WASM cross-module imports' module names. examples/vscode_extension_minimal.affine demonstrates an end-to-end VS Code extension authored in AffineScript using `use Vscode::{registerCommand, showInformationMessage, pushSubscription}` — compiles to .cjs via the Node-CJS path. (3) issues-drafts/02 closed: `[T]` array type now parses via `LBRACKET type_expr RBRACKET → TyApp (Array, [TyArg elem])` in lib/parser.mly's type_expr_primary rule. Verified for fn params, return types, struct fields, and nested `[[T]]`. Other stdlib files (Option/math/io/string) still fail with distinct issues (`fn() -> T` type syntax, `Option` angle brackets) — the array fix advanced math.affine's failure point from line 349 to 354 etc. (4) PatCon sub-pattern destructuring under WasmGC: `match Mk(7, 99) { Mk(a, b) => a }` now lowers to RefCast + StructGet for the tag check + per-field RefCast HtI31 + I31GetS unboxing for each PatVar sub-pattern. Validated end-to-end: emitted .wasm instantiates in Deno and `main()` returns 7. The mixed-arity case (zero-arg + with-args in same enum, e.g. Option) errors loudly with the workaround documented (split into two matches, or use Wasm 1.0). Unifying variant rep — uniform `struct {tag, payload}` so Some+None can share — is the next destructuring milestone. 195 → 198 → 200 tests; 0 regressions throughout." +session-note-2026-05-03-b = "FOLLOW-ON BATCH AFTER TYPED-WASM CLOSURE: variant-with-args under WasmGC, Node-target codegen Phase 1 (issue #35), stdlib Core.affine fix, cross-module for other codegens. (1) lib/codegen_gc.ml — added gen_variant_with_args helper that lowers ExprApp(ExprVariant(_), args) and bare-name `Some(42)` (via variant_tags lookup in ExprApp ExprVar) to a tagged anon-struct allocation [tag: i32, payload: anyref, ...] with i32 args boxed via ref.i31. ExprVar gained variant_tags fallback so `return Happy` works. gen_gc_function now post-processes body_code: when result_vt ≠ I32 the trailing `push_i32 0` fallback (emitted by gen_gc_block when blk_expr=None) is swapped for RefNull HtAny, which fixes 'end[0] expected type anyref, found i32' validator errors on functions that explicitly return a struct. Validated: emitted .wasm instantiates in Node 18 / V8 14, main() returns the GC struct. f64 args remain UnsupportedFeature (no i31 boxing path). (2) lib/codegen_node.ml — new module implementing issue #35 Phase 1 (Node-CJS emit). Wraps Codegen.generate_module output in a CJS shim with: inline base64-encoded wasm constant, lazy WebAssembly.instantiate on first activate/deactivate call, JS-side opaque-handle table (_registerHandle / _getHandle / _freeHandle exported for Phase 2 vscode bindings), minimal WASI fd_write so println-style codegen works, exports.activate/exports.deactivate re-exports of the wasm module's same-named exports. .cjs output extension dispatches in bin/main.ml (both JSON and non-JSON paths). Smoke-tested: real Node 18 require()s a generated .cjs and successfully calls activate(fakeContext) → 0, deactivate() → 0. Issue #35 Phases 2-4 (stdlib/Vscode.affine bindings, extension.ts → extension.affine migration, rattlescript-face sweep) remain as separate work. (3) stdlib/Core.affine — three parser-collision fixes: `pub fn const[A,B]` → `always` (const is a reserved keyword for compile-time bindings); `fn(x: A) -> C { ... }` lambdas → `|x: A|` form (the parser's actual lambda syntax); `flip` rewritten from `(A, B) -> C` to curried `A -> B -> C` to dodge tuple-arrow ambiguity. The originally-blocked tests/modules/test_simple_import.affine (use Core::{min}; min(10,20)) now compiles end-to-end. Other stdlib files (Option/math/io/string/...) still don't parse — each has distinct issues (fn() type syntax, `[T]` array type per issues-drafts/02, `Option` angle brackets) that need their own passes or compiler-level fixes. (4) lib/module_loader.ml — new flatten_imports : t -> program -> program that prepends imported public TopFns into the importer's prog_decls (deduplicating against local fn names). bin/main.ml now binds [let flat_prog = Module_loader.flatten_imports loader prog in] in both compile_file paths and threads flat_prog through all 22 non-Wasm codegens (Julia, JS, C, WGSL, Faust, ONNX, OCaml, Lua, Bash, Nickel, ReScript, Rust, LLVM, Verilog, Gleam, CUDA, Metal, OpenCL, MLIR, Why3, Lean, SPIR-V) — Wasm and Wasm-GC keep the original prog because Codegen.gen_imports handles their cross-module needs natively. Smoke-tested: caller_ok.affine (use CrossCallee::{consume}) now compiles to JS / Julia / Rust / Lua with consume's body inlined. The non-JSON compile_file path also gained ~loader on its previously-loaderless Codegen.generate_module call (latent bug for cross-module wasm via this path). 188 → 190 tests; 0 regressions." +session-note-2026-05-03 = "TYPED-WASM CROSS-MODULE CLOSURE + MCP CARTRIDGE REWIRE + WASMGC LOUD-FAIL HARDENING. (1) lib/codegen.ml — generate_module gained ?loader and a new gen_imports pass that walks prog.prog_imports, loads each referenced module via Module_loader, and emits one (import \"\" \"\" (func ...)) entry per imported function plus a (local_alias_name → import_func_idx) entry in func_indices. ImportSimple is namespace-only (no emit), ImportList emits per item, ImportGlob enumerates public TopFns. Closes the cross-module WASM import emission gap called out in session-note-2026-04-19-a — `verify-boundary CALLEE.affine CALLER.affine` now works on user-authored AffineScript pairs, not just hand-assembled bridges. (2) lib/resolve.ml — import_resolved_symbols / import_specific_items / ImportGlob inline path now also write to dest type_ctx.name_types (not just var_types), with a new lookup_source_scheme helper that falls back from sym_id-keyed source_types to name-keyed source_name_types because resolve_and_typecheck_module's per-decl Typecheck.check_decl populates name_types but never var_types. lib/typecheck.ml — Typecheck.check_program gained ?import_types : (string, scheme) Hashtbl.t that seeds name_types after register_builtins, supplied by the resolver. bin/main.ml compile_file (JSON + non-JSON paths), compile_to_wasm_module, verify_file all updated to thread import_type_ctx.name_types through and pass ~loader to Codegen.generate_module. (3) test/e2e/fixtures/ — CrossCallee.affine + cross_caller_{ok,dup,drop}.affine, plus 3 new alcotest cases under E2E Boundary Verify exercising the full pipeline (parse → resolve_with_loader → typecheck-with-import-types → codegen-with-loader → Tw_interface.verify_cross_module). All three boundary outcomes (clean / LinearImportCalledMultiple / LinearImportDroppedOnSomePath) confirmed end-to-end. (4) boj-server/cartridges/typed-wasm-mcp — mod.js rewritten to call `affinescript` (was: nonexistent `typed-wasm` binary), with cwd set to the source's directory so Module_loader resolves relative imports correctly. cartridge.json bumped to v0.2.0 with corrected input descriptions (.affine source paths) and a new typed_wasm_verify_boundary tool exposing the cross-module verifier. README.adoc updated to match. (5) lib/codegen_gc.ml — eliminated three silent-bad-codegen fallbacks (same class as BUG-005): wildcard ExprLambda/ExprUnsafe → RefNull replaced with explicit UnsupportedFeature errors; match-arm wildcard PatTuple/PatRecord → fall-to-default replaced with UnsupportedFeature; PatLit fallback for LitFloat/LitString replaced with explicit errors. test/test_e2e.ml gained E2E WasmGC Loud-Fail suite with 2 regression markers. Bumps wasm-gc-codegen from 70% to 85% (silent-fallback gap is gone; effects/try-catch/lambda/call_ref remain genuinely deferred to upstream EH proposal or whole-program CPS). 180 → 182 tests; 0 regressions." +session-note-2026-05-02 = "FACE BUILDOUT + BRAND-SURFACE EJECT + TS-EXEMPTION DOCS + FRONTIER-PRACTICES FORMALIZATION. (1) lib/lucid_face.ml + lib/cafe_face.ml — full transformer parity with python_face/js_face/pseudocode_face. lib/face_pragma.ml — pragma detection (`# face: ` / `// face: ` / `-- face: ` / `(* face: *)`) with alias table covering canonical/python/py/rattle/rattlescript/js/javascript/jaffa/jaffascript/pseudocode/pseudo/pseudoscript/lucid/lucidscript/purescript/ps/cafe/cafescripto/coffee/coffeescript. (2) Single .affine extension across all faces; per-face extensions (.rattle/.pyaff/.jsaff) deprecated to migration path with warning. (3) lib/face.ml extended with format_*_for_face dispatch tables for Lucid (Haskell-flavoured: 'Linearity error', 'Variable not in scope', 'Could not match type') and Cafe (concise JS-flavoured). (4) tools/run_face_transformer_tests.sh + just test-faces / test-faces-record / test-faces-update — snapshot + round-trip parse harness. Caught 5 transformer bugs on first run that visual review had missed. (5) examples/faces/ + README.adoc with per-face hello programs and 'Known transformer gaps' list for the categories deferred to a future AST-rewriter milestone (multi-clause defs, do-notation, where-blocks, list comprehensions, splats, no-paren calls). (6) Five non-canonical faces ejected to top-level brand-surface repos: github.com/hyperpolymath/{rattlescript,jaffascript,pseudoscript,lucidscript,cafescripto}. Each has README, LICENSE, CONTRIBUTING, bin/ shim that exec's affinescript with `--face ` injected after the subcommand, justfile, examples/. NO compiler forks — all live and evolving compiler logic stays here in affinescript. Eject pattern: 'adapt-then-commit' for the pre-existing rattlescript repo (preserved 7 commits + v0.1.0-alpha tag while replacing the wrong-shape Cargo+vendored architecture). (7) docs/guides/frontier-programming-practices/Human_Programming_Guide.adoc + AI.a2ml — new 'Faces: Frontend Surfaces' section formalizing 6 lessons earned during the buildout: faces-as-brand-surfaces (not compiler forks), identity-in-content-not-filesystem, snapshot-plus-roundtrip for any pure text-to-text transformer, examples-are-tests-not-demonstrations, error-vocabulary-cost-O(faces × error_kinds), span-fidelity-honesty (acknowledge UX regression rather than paper over). AI.a2ml gained parallel (faces) section mirroring the existing (backends) shape with rules, when-adding-face-N+1 recipe, known-limitations. (8) .claude/CLAUDE.md TypeScript Exemptions table — 9 approved files with rationale and unblock condition. Mirror tables landed in standards / my-lang / boj-server. Audit follow-up issues #63-66 track port work blocked on #35 (Node target), #42 (extern types), and stdlib network/crypto. Commits: 74024c6 (Lucid+Cafe + unification), 82eec92 (rattlescript subtree eject), 133bb53 (frontier guide section), 116ea5d (CLAUDE.md exemptions). Side effect across the wider hyperpolymath estate: TS file count 94 → 27 after vendored-snapshot deletions (rsr-template-repo, idaptik dlc subtrees migrated to dedicated repos via PRs #1 against idaptik-dlc-iky and idaptik-dlc-reversibley + cleanup PR #70 against idaptik). Burble canary CI gate (.github/workflows/affinescript-canary.yml in PR #21) lands an advisory gate compiling every .affine file on PR — promote to required-for-merge as Burble grade B target." +session-note-2026-04-19-a = "CODEGEN BUG FIX + TYPED-WASM LEVEL 10 CLI SURFACE CLOSED. (1) lib/codegen.ml commit 35c476d — three fixes: (a) PatCon-with-args stack imbalance in gen_pattern: was LocalTee match_result + LocalGet match_result around stack-neutral bind_fields; removed save/restore, I32Eq sits on stack directly. Fixes 'expected 1 elements on the stack for fallthru, found 2' on match-in-enum returning distinct zero-arity or arg constructors across arms. (b) ExprVar falls back to ctx.variant_tags when lookup_local misses, so bare `Initialised` (parens omitted) resolves as the variant tag — parser accepts the form, codegen previously failed with UnboundVariable. (c) ctx.struct_layouts + ctx.fn_ret_structs: struct layouts registered globally from TopType(TyStruct), propagated to function parameters via p_ty, call-result lets via fn_ret_structs, let-bindings with sl_ty annotation, let-from-let passthroughs. Fixes the .field_1_or_later=0 bug on struct function parameters. struct_name_of_ty handles TyCon / TyApp / TyOwn / TyRef / TyMut wrappers. (2) bin/main.ml commit f6089a2 — new verify-boundary CALLEE CALLER subcommand: compiles two .affine source files, extracts callee's ownership-annotated export interface, runs Tw_interface.verify_cross_module on the caller, exits 0/1 correctly. Shared compile_to_wasm_module helper factored out of verify_file. Fixed verify_boundary_fn exit-code bug (verify-bridge handler): was always returning Ok () even on violations; now tracks violation count and maps to Error so CI catches boundary drift. Level 10 cross-module boundary verifier now has public CLI surface. Evidence: 177/177 tests green unchanged; affinescript-deno-test smoke suite 7/7 (up from 4/4) with new codegen_regression_test.affine; double-track-browser extension_lifecycle_test.affine pilot 10/10 without tagged-struct workaround. Scope note: AffineScript use A.B imports inline at AST layer, so caller from AffineScript source today produces no cross-module WASM imports — new CLI immediately useful for hand-assembled / bridge-pattern callers (IDApTIK bridges, composition tools). Broader applicability gated on cross-module WASM import emission, a separate compiler feature." +session-note-2026-04-12-c = "STAGE 12 (SECOND IDAPTIK SCREEN) IN PROGRESS — CharacterSelectScreen chosen as dogfood target (6 class cards: Assault/Recon/Engineer/Signals/Medic/Logistics, 1 confirm → JessicaCustomise). Plan: (1) lib/tea_cs_bridge.ml — new Wasm module, same API surface as tea_bridge.ml, 7 msgs (0-5=select class, 6=confirm), update: selected_tag=msg+1, selected_tag 1-6=class highlighted, 7=navigate. (2) lib/dune + bin/main.ml — add cs-bridge subcommand and update which_arg/interface/verify-bridge. (3) IDApTIK: AffineTEARouter.js+.res add screenJessicaCustomise=6. (4) CharacterSelectScreen.res: csTeaBridge module-level ref, card click handlers → teaDrive, applyView syncs Wasm→ReScript selectedClass, confirm → AffineTEA.update+navigate via showScreenWithTag. Seam check: push TitleScreen→CharacterSelect, select class, confirm→JessicaCustomise, back-stack correct. 173/173 tests currently passing." +session-note-2026-04-12-b = "STAGES 7-11 COMPLETE (173/173 tests). Stage 9: per-path min/max linearity (count_uses_range, LinearDroppedOnSomePath, fn_push else-branch fix). Stage 10: tw_interface.ml multi-module boundary verifier (LinearImportCalledMultiple, LinearImportDroppedOnSomePath, CLI interface+verify-bridge cmds). Stage 11: source-level Cmd linearity — Cmd[ClickMsg] bracket syntax, quantity_of_ty_annotation returns QOne for Cmd[_], infer_usage_block declares QOne locals before subsequent stmts so uses tracked, fires LinearVariableUnused when Cmd dropped; builtins cmd_none+cmd_perform in typecheck/resolve/interp. Commit b8a0f9c." +session-note-2026-04-12-a = "STAGE 6 (CADRE ROUTER) COMPLETE: lib/tea_router.ml generates Wasm 1.0 router module (915 bytes, WebAssembly.validate=true). RouterModel at offset 64: screen_w(+0), screen_h(+4), stack_len(+8), popup_tag(+12), stack_data[8](+16). 11 functions: init, push(Linear), pop, present_popup(Linear), dismiss_popup, resize(Linear×2), get_screen_w/h/stack_len/stack_top/popup_tag. affinescript.ownership: 3 functions with Linear params (push, present_popup, resize). affinescript.tea_layout: version=1, 5 fields. router-bridge subcommand added. 8 new E2E tests; 113 total, 0 regressions. IDApTIK: AffineTEARouter.js + AffineTEARouter.res; Navigation.res: routerBridge field + setRouterBridge + showScreenWithTag + presentPopupWithTag + dismissPopup router sync + resize router sync; TitleScreen.res: teaDrive uses showScreenWithTag/presentPopupWithTag, router.wasm loaded on first show alongside titlescreen.wasm, graceful fallback. router.wasm committed to idaptik/public/assets/wasm/. Both repos pushed. commit: affinescript 5969b7b, idaptik e3eafd1." +session-note-2026-04-11-h = "STAGE 5 (AFFINATEA DRIVES SCENE) COMPLETE: teaDrive(engine, msgTag) is the single entry point from all 4 button handlers — update → applyView → navigate. applyView reads selected_tag via getSelected() and alpha-pulses the chosen button (0.65 → animate 1.0 over 0.22s). teaBridge loaded asynchronously on first show; graceful fallback to direct ReScript navigation when bridge is None. resize handler calls AffineTEA.setScreen(tea, width, height) to keep screen_w/h in sync with canvas. Button handlers reduced to single teaDrive call each. Navigation decision comes FROM Wasm state (selected_tag), not from button identity. Credits button wired (msgCredits) with TODO screen. IDApTIK TitleScreen is fully AffineTEA-driven for Stages 4+5." +session-note-2026-04-11-g = "STAGE 4 (WASM/JS BRIDGE FOR IDAPTIK) COMPLETE: lib/tea_bridge.ml generates complete TitleScreen TEA bridge Wasm 1.0 module (512 bytes). TitleModel at offset 64: screen_w(+0), screen_h(+4), bgm_playing(+8), selected_tag(+12). 7 functions: init, update (branchless selected_tag=msg+1), get_screen_w/h/bgm_playing/selected, set_screen. affinescript.ownership custom section: update param kind=1 (Linear). affinescript.tea_layout custom section: version=1, 4 fields. affinescript tea-bridge subcommand added to bin/main.ml. 6 bridge E2E tests added; 101 total tests passing. AffineTEA.js + AffineTEA.res bindings in idaptik/src/app/tea/. titlescreen.wasm (512 bytes, WebAssembly.validate=true, end-to-end Node.js verified) in idaptik/public/assets/wasm/." +session-note-2026-04-11-f = "LSP PHASE C COMPLETE: completion candidates subcommand shipped. extract_prefix_at scans backward from cursor col to extract ident prefix + dot-context flag. collect_completions filters symbol table by prefix and appends affine_keywords (suppressed in dot context). emit_completions emits JSON array {name,kind,type,detail}. complete_file + complete_cmd added to bin/main.ml (FILE LINE COL args). 6 E2E tests. 101 total, 0 regressions. NEXT: LSP Phase D." +session-note-2026-04-11-e = "LSP PHASE B COMPLETE (commit 79c0829): hover/goto-def subcommands shipped. span_contains, find_symbol_at (refs-first then def-spans), hover_to_json/goto_def_to_json, emit_hover/emit_goto_def, run_pipeline_for_query. 89 tests. NEXT: LSP Phase C (completion candidates from symbol table)." +session-note-2026-04-11-d = "TRAITS WIRED (commit 1ca143e): trait_registry in context; TopTrait/TopImpl in forward pass; ExprField trait fallback; find_impl unification-based; TopImpl bodies typechecked; 80 tests. NEXT: LSP Phase B." +session-note-2026-04-11-c = "LINEAR ARROWS ENFORCED (commit d2f9b7b): (1) lambda synth — |@linear x: T| e now synthesises T -[1]-> U; (2) lambda check mode — explicit quantity annotation validated against expected TArrow; (3) quantity.ml ExprLambda — annotated params declared in env, usage verified, violations accumulated in env.errors and drained after check_function_quantities step 3; scope leakage prevented via quantities save/restore. Also fixed codegen.ml missing custom_sections field (pre-existing wasm.ml change). 75 tests, 0 regressions. NEXT: traits generic resolution → unification integration; then LSP Phase B." +session-note-2026-04-11-b = "BUG-005 CLOSED: WasmGC silent drop+null fallbacks replaced with explicit CodegenError (UnboundFunction for missing func_indices entries, UnsupportedFeature for indirect/higher-order calls). Effects runtime SHIPPED: TopEffect now registers each op as a PerformEffect-raising VBuiltin in interp.ml (was no-op). Multi-arg handler arg_vals bug fixed. ExprResume wired to __resume__ continuation in handler env. WasmGC path: TopEffect ops registered as unreachable stubs with correct func_indices offsets; ExprHandle and ExprResume reject with UnsupportedFeature (EH proposal or CPS transform required for full dispatch)." + +[project-context] +name = "affinescript" +completion-percentage = 94 +phase = "affinatea-stage6-complete" +tagline = "Rust-inspired language with affine types, dependent types, row polymorphism, and extensible effects" +note = "Session 2026-04-11 final checkpoint. DONE THIS SESSION: (1) JS-face (lib/js_face.ml) + Pseudocode-face (lib/pseudocode_face.ml) — both wired into face.ml and bin/main.ml. resolve_face() wired into ALL six file handlers in bin/main.ml (parse/check/eval/compile/fmt/lint) — .rattle/.pyaff/.jsaff/.pseudoaff auto-detected without --face flag. (2) P3 packages: packages/affine-js/ (Deno ESM WASM loader, marshal/unmarshal, types.d.ts), packages/affine-ts/ (typed call helpers), packages/affine-res/ (ReScript bindings). (3) P4 educational: docs/guides/frontier-guide.adoc (6-chapter tutorial), docs/guides/warmup/ (4 warmup scripts). (4) CoffeeScript + ActionScript roadmap faces added to docs/specs/faces.md. (5) RattleScript distribution: distributions/rattlescript/ in-tree (Rust wrapper, build.rs, examples/), standalone repo hyperpolymath/rattlescript (GitHub, starred, v0.1.0-alpha released, 8 topics set, affinescript submodule, just bootstrap workflow). (6) dune build + dune runtest: clean, 0 regressions throughout. NEXT SESSION (clean thread): BUG-005 first (WasmGC silent bad codegen — replace fallback arms with CodegenError, ~30min), then effects runtime (interpreter handler dispatch + WASM codegen — the headline missing feature). Previous session note: P3 (aggregate library ecosystem) and P4 (educational materials) complete. P2 faces extended. P2 faces extended: JS-face (lib/js_face.ml) and Pseudocode-face (lib/pseudocode_face.ml) added, face.ml and bin/main.ml updated with all four active faces (canonical/python/js/pseudocode). aggregate packages: packages/affine-js/ (Deno ESM, WASM loader, marshal/unmarshal, types.d.ts), packages/affine-ts/ (typed call helpers, narrowing predicates), packages/affine-res/ (ReScript bindings with typed shortcuts and exhaustive matching). educational materials: docs/guides/frontier-guide.adoc (6-chapter unveiling tutorial), docs/guides/warmup/ (4 warmup scripts: basics/ownership/effects/rows). faces.md updated with JS-face and Pseudocode-face entries. Previously: Faces architecture (ADR-010, Priority 2) complete 2026-04-11. (1) lib/python_face.ml: source-level text preprocessor mapping Python surface syntax to canonical AffineScript — indentation→braces, def/True/False/None/and/or/not/class/pass keyword subs, elif/else chains, tail-position semicolon suppression. (2) lib/face.ml: face-aware error formatter mapping canonical error terms to face-specific vocabulary — Python face uses 'Ownership error / single-use variable' for linear violations, 'Name not found' for unbound vars, 'if/else type mismatch' for branch mismatches, renders Unit as None and Bool as bool. (3) bin/main.ml: --face [canonical|python] flag on all subcommands; parse_with_face selects preprocessor path; all error sites route through Face.format_*_error. (4) Span fidelity deferred: error spans currently refer to transformed canonical text positions (planned follow-up). (5) docs/specs/faces.md: face architecture reference doc written. (6) panic-attack audit: 1 High (search.js innerHTML — false positive but fixed to char-replacement), 2 Medium (LSP unwraps/shell heredoc — both false positives), 2 Low (TODO markers — informational). 73/73 E2E tests: 0 regressions. Seam check (items 1+2) verified: --face python on @linear double-use produces correct Python-face ownership error end-to-end." + +[components] +lexer = "complete-minus-affine-tokens (ZERO/ONE tokens for quantity literals NOT emitted; blocks QTT surface syntax). float_exponent now correctly named sub-regexp — scientific notation crash fixed 2026-04-11." +parser = "conformance-complete (ADR-009 achieved 2026-04-11). 12/12 valid tests parse. Added: optional SEMICOLON on type aliases/enums, enum leading-pipe syntax, effect op type params, ROW_VAR type params, ty_record_body/expr_record_body recursive rules for row-polymorphic records, field_name contextual keyword rule, struct literal expressions. CORE-03 / ADR-014 (PR #241, Refs #228, 2026-05-19, gate 267/267, ZERO Menhir conflict delta — 21 S/R + 1 R/R unchanged): module-qualified type/effect paths Pkg.Type / Pkg::Type (mixed, deep) parse — folded to one canonical ::-joined ident so resolve/typecheck/codegen/fmt unchanged. Cleared the estate's dominant parse blocker (was 525/1177). Authoritative: docs/CAPABILITY-MATRIX.adoc + docs/TECH-DEBT.adoc CORE-03." +type-checker-rules = "99% (bidirectional inference, Never/bottom, block divergence, variant/constructor/record-spread/mutable bindings all implemented in lib/typecheck.ml)" +type-checker-enforcement-wiring = "wired (Typecheck.check_program is invoked from bin/main.ml on every check/compile/eval path; Quantity.check_program_quantities runs after typecheck at lib/typecheck.ml:1206; the gating IS live for the rules and quantity annotations that exist today)" +borrow-checker = "live-gate (2026-04-11): Borrow.check_program wired into check/compile pipeline at all 4 Typecheck success sites. Emits E0501-E0506 diagnostics. PlaceVar carries variable name for readable errors (file:line:col at both use and move sites). ExprMatch arm state merging fixed. Lexical borrow lifetime clearing in check_block. BUG-004 (lambda capture tracking) still deferred — requires type info propagation. lib/borrow.ml 669 LOC." +interpreter = "95%" +wasm-codegen = "95% (cross-module imports + extern fn → WASM import emission for both `use Foo::{f}` and `extern fn f` user-source forms; landed 2026-05-03c)" +wasm-gc-codegen = "91% (variant-with-args + PatCon same-arity destructuring 2026-05-03c. Silent-bad-codegen fallbacks eliminated 2026-05-03a. Mixed-arity matches still need uniform variant rep; effects/try-catch/call_ref still deferred to upstream EH or CPS.)" +node-cjs-codegen = "Phase 1+2+3 complete (2026-05-03d): codegen_node.ml emits CJS shim; stdlib/Vscode.affine + stdlib/VscodeLanguageClient.affine ship 19 binding declarations; packages/affine-vscode/mod.js is the JS-side adapter (now using shared handle table via exposed exports._instance / exports._registerHandle); editors/vscode/src/extension.ts deleted, replaced by extension.affine compiled to out/extension.cjs (verified end-to-end: real Node 18 dispatches activate(ctx), 5 commands register, lsp.enabled probed). package.json's main field now ./out/extension.cjs; compile script invokes `affinescript compile`. tools/check-no-extension-ts.sh + .github/workflows/ci.yml step + just check guard ensure the .ts cannot drift back. CLAUDE.md exemptions table reduced from 9 → 8 with the closed-exemption note. Phase 4 (rattlescript-face sweep) is the only remaining task on this issue." +session-note-2026-05-03-e = "STDLIB-PARSER UNBLOCKERS: 3 type-syntax sugars added to lib/parser.mly. (1) `fn(A, B, ...) -> T` lowers to the curried arrow chain `A -> B -> ... -> T`. Zero-arg `fn() -> T` lowers to `Unit -> T` (TyTuple [] -> T). Required by stdlib/Option.affine's `unwrap_or_else[T](opt: Option[T], f: fn() -> T) -> T` and similar higher-order signatures. (2) Angle-bracket aliases for type application AND type parameters: `Option` ≡ `Option[T]`, `fn f(x: T)` ≡ `fn f[T](x: T)`, `type Option = ...` ≡ `type Option[T] = ...`. The `<` / `>` are unambiguous in type position because comparison operators don't appear there. (3) `(A, B) -> C` now lowers to the curried arrow `A -> B -> C` instead of being parsed as a single tuple-arg arrow. Disambiguated from plain tuple types by the trailing ARROW; bare `(A, B)` (no arrow) still parses as a TyTuple. Stdlib/Core.affine's `flip` reverted from the earlier curried workaround to its natural `(A, B) -> C` form. Stdlib audit: 4 files now check (Core, Math, Vscode, VscodeLanguageClient), 5 parse but fail check (Option/Result/io/prelude/string — actual code-level errors), 7 still fail to parse (collections/effects/option/result/math/testing/traits) with DISTINCT remaining issues — slice syntax `list[1:]`, `effect io;` decl form, `=>` lambda spelling, `Self` in trait method sigs. Each is its own task. 7 new regression tests under E2E Type Syntax Sugar. 200 → 207 tests; 0 regressions." +session-note-2026-05-03-d = "ISSUE #35 PHASE 3 CLOSED + REGRESSION GUARD WIRED. (1) editors/vscode/src/extension.affine (160 lines) replaces extension.ts, using the Phase 2 Vscode + VscodeLanguageClient bindings from stdlib/. Adds 9 new extern fns to Vscode.affine for the API surface the migration actually needed: editorActiveFilePath / editorActiveLanguageId (collapse vscode.window.activeTextEditor.document.{uri.fsPath,languageId}), workspaceConfigGetBool, consoleLog, execSync, stringConcat / stringEndsWith / stringReplaceSuffix. (2) packages/affine-vscode/mod.js refactored to take the host shim as 3rd arg and share its handle table — fixes a bug where the wasm-side ExtensionContext handle wasn't visible to adapter functions. lib/codegen_node.ml's shim now exposes exports._instance + exports._memory after init so adapters can read string args out of wasm linear memory; replaces the constant `_extraImports()` with a mutable exports.extraImports hook. (3) editors/vscode/package.json `main` field flipped from ./out/extension.js to ./out/extension.cjs; scripts.compile is now `affinescript compile src/extension.affine -o out/extension.cjs`; obsolete tsconfig.json deleted; @types/node + typescript devDependencies removed. (4) tools/check-no-extension-ts.sh fails CI / `just check` if any extension.ts reappears under editors/vscode/src or faces/*/affinescript/editors/vscode/src — wired into both .github/workflows/ci.yml and the just check recipe. (5) .claude/CLAUDE.md TypeScript Exemptions table: editors/vscode/src/extension.ts row removed (count 9 → 8), moved to a 'Closed exemptions' note with the 2026-05-03 closure date and pointer to the regression guard. End-to-end smoke-tested: real Node 18 require()s the .cjs, dispatches activate(fakeContext), all 5 commands register with their full names, both lsp.enabled and lsp.serverPath are probed via getConfiguration. 200 tests still green; 0 regressions." +extern-decls = "complete (2026-05-03c): `extern fn` and `extern type` both parse, resolve, typecheck, and emit (import \"env\" \"\" ...) in the WASM target." +array-type-sugar = "complete (2026-05-03c, closes issues-drafts/02): `[T]` desugars to Array[T] in any type-expr position." +type-syntax-sugars = "complete (2026-05-03e): three forms accepted — `fn(A,B) -> T` (curried lowering), `Option` / `Result` / `fn f` (angle-bracket aliases for both type-app and type-param), and `(A,B) -> C` (true 2-arg arrow distinct from tuple-arg). Plain tuple types still parse when no ARROW follows." +xmod-other-codegens = "complete (2026-05-03b): Module_loader.flatten_imports inlines public TopFns from imported modules; threaded through all 22 non-Wasm backends in bin/main.ml. Smoke-tested with JS/Julia/Rust/Lua emission of caller-of-Callee." +julia-codegen = "exists" +lsp-phase-a = "complete" +lsp-phase-b = "complete (2026-04-11, commit 79c0829): hover/goto-def subcommands shipped. json_output.ml: span_contains (1-based, single+multi-line), find_symbol_at (references-first then def-spans), hover_to_json/goto_def_to_json/not_found_json, emit_hover/emit_goto_def. bin/main.ml: hover + goto-def subcommands, run_pipeline_for_query tolerates typecheck errors. 4 E2E tests. 89 total." +lsp-phase-c = "complete (2026-04-11): completion candidates subcommand shipped. json_output.ml: extract_prefix_at (scans backward from cursor col to find ident prefix + dot-context flag), collect_completions (symbol table prefix filter + affine_keywords list suppressed in dot context), emit_completions (JSON array). bin/main.ml: complete_file handler + complete_cmd (FILE LINE COL args). 6 E2E tests added (prefix extraction, prefix match, empty prefix, no-match, keyword included, dot-ctx suppresses keywords). 101 total tests, 0 regressions." +lsp-phase-d = "complete (2026-04-11): JSON-RPC LSP server shipped as `affinescript server --stdio`. lib/lsp_server.ml: Content-Length framing, document store (URI→source), pipeline cache (URI→symbols+refs+diags), run_pipeline (temp-file + span path fixup), lsp_range (1→0-based conversion), diag_to_lsp, publish_diagnostics. Handlers: initialize (hover+definition+completion caps), didOpen/didChange (pipeline+cache+push), didClose (clear), hover (markdown+type+quantity), definition (URI+range), completion (LSP kind mapping). bin/main.ml: server_cmd in default group. 4 E2E tests. 105 total, 0 regressions." +stdlib = "95% (5 stubs remain as extern builtins)" + +[stats] +compiler-loc = 12750 +compiler-modules = 39 +lsp-files = 5 +test-files = 54 + +[features] +# Honest status of the headline language features advertised in README.adoc. +# "declared-but-unwired" = surface syntax and/or internal module exists, but +# the feature is not enforced on user programs through the CLI pipeline. +affine-types = "wired-and-reachable (Track A Manhattan plan complete 2026-04-10. Quantity semiring in lib/quantity.ml; invoked from typecheck.ml:1206 inside the standard CLI pipeline. Surface syntax per ADR-007 hybrid: @linear/@erased/@unrestricted attributes (Option C primary) on let/stmt-let/param/lambda-param, AND :1/:0/:ω numeric sugar (Option B) on let/stmt-let. Scaled Let rule per ADR-002 implemented in lib/quantity.ml ExprLet/StmtLet — closes BUG-001 (ω-let smuggles linear values) and BUG-002 (zero-let erasure). Four regression fixtures in test/e2e/fixtures/ exercise both surface forms. Behavioural enforcement verified via E2E Quantity test suite — 4 new passing tests, 0 regressions.)" +linear-arrows = "enforced (2026-04-11): Three-part fix landed. (1) typecheck.ml lambda synth: |@linear x: T| e now synthesises T -[1]-> U (was always QOmega). (2) typecheck.ml lambda check mode: explicit param quantity annotation validated against expected TArrow quantity; unannotated params inherit context quantity. (3) quantity.ml ExprLambda: added env.errors accumulator; annotated lambda params declared via env_declare so env_use tracks them; usage verified with check_quantity after body walk; violations pushed to env.errors and drained at end of check_function_quantities (step 4). Saved/restored env.quantities entries to prevent scope leakage. Two E2E fixtures + 2 passing tests. 75 tests total, 0 regressions. Commit d2f9b7b pushed." +borrow-checker = "phase-3-parts-1-3-Slices-A-B-C-light-landed (CORE-01, Refs #177, 2026-05-24): pt1 (#240, gate 263/263) borrow-graph validation wired — BorrowOutlivesOwner emitted, shared-XOR-exclusive enforced at use sites, ownership from param type TyOwn/TyRef/TyMut, call-arg borrows temporary, ref-binding graph tracked. pt2 (gate 271→274 and 278→281) return-escape + &mut e parser surface. pt3 Slice A (PR #335) NLL last-use expiry: compute_last_use_index pre-pass; check_block expires in-block ref-bindings once their binder is dead. pt3 Slice B (effectively #354/#355/#356 after #351 procedural close) flow-sensitive escape via re-assignment: `outer = &y` in StmtAssign pre-releases the held borrow and re-binds the ref-graph entry to the freshly-created borrow. pt3 Slice C-light (this PR) CFG-join semantics for non-match join constructs: ExprHandle handler arms and ExprTry catch arms are now isolated via snapshot-restore-merge (factored merge_arm_results helper, mirroring ExprMatch's inlined logic) — moves/borrows from arm i no longer pollute arm i+1. 2 hermetic tests (positive: independent catch-arm moves OK; anti-regression: body-side move persists past the try). Residual (Slices C-full, C', D): true Polonius origin/region variables on TyRef/TyMut with subset constraints + datalog-style solver (architectural; ADR-gated); loop soundness via 2-iter check (coupled to StmtAssign clear-on-rewrite fix); reborrow through indirection (ref-to-ref binding); quantity-checker tightening for captured linears. Authoritative: docs/CAPABILITY-MATRIX.adoc + docs/TECH-DEBT-alt.adoc CORE-01." +row-polymorphism = "60% (records + effects rows implemented in typecheck/unify; not fully exercised end-to-end)" +effects = "interpreter-complete (handler dispatch, PerformEffect propagation, ExprResume, multi-arg ops all wired in interp.ml 2026-04-11). WasmGC: ops registered as unreachable stubs; ExprHandle/ExprResume reject with UnsupportedFeature. Every compiled backend (core-WASM, WasmGC, Deno-ESM, JS-text, C) now fails loud on handle/resume (#555 fenced); interp multi-shot fails loud; one non-tail single-shot residual is pinned by a test. Authoritative: docs/SOUNDNESS.adoc." +dependent-types = "removed-v1 (refinement/dependent surface withdrawn 2026-04-10 per #558; no TRefined node remains; assume(...) rejected at parse; T where (P) is a parse error — no unenforced predicate is expressible). Authoritative: docs/SOUNDNESS.adoc + docs/CAPABILITY-MATRIX.adoc" +traits = "90% (2026-04-11, commit 1ca143e): trait_registry added to typecheck context; TopTrait/TopImpl processed in forward pass; ExprField falls back to Trait.find_method_for_type on record-field failure; find_impl/find_impls_for_type use unification-based matching via fresh_impl_self_ty + Unify.unify instead of name string comparison; TopImpl bodies type-checked via check_fn_decl with Self bound; check_impl_satisfies_trait verifies required methods present. Two E2E fixtures + 2 tests. 80 total, 0 regressions. Coherence checking now wired (#559, concrete overlaps whose self types unify are rejected; typecheck.ml check_all_coherence). Remaining: associated type substitution in method bodies, where-clause supertrait enforcement, generic-subsumption overlap (#559 follow-up). Authoritative: docs/SOUNDNESS.adoc.)" + +[track-a-manhattan] +owner = "primary" +scope = """ +Close the surface-syntax gap that makes affine enforcement unreachable from +user programs. Revised 2026-04-10 after the original ETA was set against the +incorrect premise that `Quantity.check_program_quantities` was unwired from +the CLI — it is wired (typecheck.ml:1206) and has been since at least +the Manhattan-recovery refactor (#19). The actual remaining work: + +1. AST: add `el_quantity : quantity option` to ExprLet, `sl_quantity` to + StmtLet. (~4 LOC + cascading pattern updates across ~10 modules that + match on Let.) +2. Lexer/parser: make ZERO/ONE tokens reachable in quantity position + (cheapest path: accept INT 0/INT 1 in the parser's `quantity` rule, + leaving the lexer alone). ~3 LOC. +3. Parser: extend `let_decl` and `stmt_let` rules with an optional + binder-quantity annotation. Surface-syntax choice is a language-design + call — see META.a2ml ADR-007 candidate. +4. quantity.ml ExprLet/StmtLet: implement context scaling per ADR-002. + Snapshot before walking el_value, walk it, scale all newly-recorded + usages by `q`, then merge back. ~25 LOC including a `scale_usage_by` + helper. +5. Two regression fixtures covering BUG-001 and the dual valid case. +6. spec.md T-Let rewrite to show the scaling explicitly. + +Out of scope for Track A: +- BUG-003 (eval_list L-to-R): SHIPPED 2026-04-10. interp.ml:347 now uses + fold_left + List.rev. Behavioural regression test deferred — depends on + the broken `let mut` interpreter path (separately tracked in the failing + baseline interp tests). +- BUG-004 (lambda capture tracking): blocked on borrow checker Phase 3. +- BUG-002 (zero-let erasure semantics): folds in with BUG-001 once + surface syntax exists. +""" +spine-files = ["lib/ast.ml", "lib/parser.mly", "lib/quantity.ml", "lib/typecheck.ml", "lib/interp.ml", "lib/codegen.ml", "lib/codegen_gc.ml", "lib/julia_codegen.ml", "lib/formatter.ml", "lib/sexpr_dump.ml", "lib/json_output.ml", "lib/linter.ml", "lib/opt.ml", "lib/desugar_traits.ml", "test/e2e/fixtures/", "docs/spec.md"] +eta = "T+7d from 2026-04-10 (ADR-007 accepted, implementation chain unblocked)" +blocking-decision = "RESOLVED 2026-04-10: ADR-007 accepted as hybrid C-primary + B-sugar. @linear/@erased/@unrestricted are the canonical forms emitted by tutorials, error messages, IDE tooling, and the formatter. The :1/:0/:ω numeric forms are accepted as sugar (legal in source, rewritten to attribute form by `affinescript fmt` unless --keep-quantity-sugar is set). See META.a2ml ADR-007 for full decision text." + +[[closed-bug]] +id = "BUG-001" +severity = "high" +category = "soundness" +title = "ω-let smuggles linear values" +discovered = "2026-04-10" +fixed = "2026-04-10" +description = """ +let x :ω = linear_resource() in use_x_once(x) was previously accepted. +The QTT-orthodox scaled Let rule (ADR-002) rejects this because scaling +the value context by ω promotes the linear usage of `linear_resource()` +to UMany, which the quantity checker reports as a linear-binding violation. +""" +fix = "lib/quantity.ml ExprLet/StmtLet cases now snapshot the env, walk the value, scale the per-variable delta by the binder quantity (via the new scale_usage helper), and merge the scaled deltas back. ExprLet was extended with el_quantity, StmtLet with sl_quantity (lib/ast.ml). Surface syntax landed per ADR-007 hybrid: @linear/@erased/@unrestricted primary + :1/:0/:ω sugar." +verification = "test/e2e/fixtures/bug_001_omega_let_smuggles_linear.affine (Option C form) and test/e2e/fixtures/bug_001_sugar_form.affine (Option B form) — both produce the @linear-vocabulary error message and are pinned in test/test_e2e.ml as `BUG-001 attr/sugar form rejects ω-let smuggling @linear`. Behaviourally verified end-to-end through `dune runtest`. The dual valid cases (affine_let_valid.affine and affine_let_valid_sugar.affine) confirm the rule does not over-reject." + +[[closed-bug]] +id = "BUG-002" +severity = "medium" +category = "semantics" +title = ":0 lets do not erase their RHS" +discovered = "2026-04-10" +fixed = "2026-04-10" +description = """ +let x :0 = expensive_proof_term() in body_not_using_x previously evaluated +expensive_proof_term() and consumed its resources. The QTT-orthodox scaled +Let rule scales the value context by 0, producing the zero context, which +means the RHS carries no runtime obligations and may be erased. +""" +fix = "Closed via the same scaled-Let infrastructure as BUG-001. The quantity-checking pass now correctly drops usage contributions from the RHS of an @erased-bound let (scale_usage QZero _ = UZero), so any linear variables consumed in the RHS no longer count against their owner. The optional interpreter-side erasure (skipping eval of @erased-bound RHS) is deferred — it is an optimisation, not a correctness requirement." +verification = "Covered transitively by the BUG-001 fixtures: the same scaling logic that closes BUG-001 closes BUG-002. A focused @erased fixture is on the follow-up backlog but not required to declare the bug fixed at the rule level." + +[[closed-bug]] +id = "BUG-003" +severity = "medium" +category = "semantics" +title = "eval_list evaluates right-to-left via List.fold_right" +discovered = "2026-04-10" +fixed = "2026-04-10" +description = """ +lib/interp.ml:347 eval_list previously used List.fold_right with monadic +bind, which in strict OCaml evaluates arguments right-to-left. ExprBinary +was already left-to-right, creating internal evaluator inconsistency and a +latent divergence point for future effects/affine enforcement. See ADR-003. +""" +fix = "Replaced fold_right with an explicit L-to-R recursive loop that accumulates results in reverse and reverses at the end. See lib/interp.ml `eval_list` (~12 LOC including the doc comment referencing ADR-003)." +regression-test-status = "deferred — observable order verification requires either the mutable-binding interpreter path (currently broken in baseline; the failing 'lambda'/'simple eval' interp tests share the same root cause) or a side-effecting builtin reachable from a fixture. Both routes are out of scope for the BUG-003 fix and should be revisited once the interpreter mutation path is repaired." +verification = "Code review against ADR-003 + dune build clean + dune runtest baseline-equivalent (22 pre-existing failures, none new, none resolved)." + +[[closed-bug]] +id = "BUG-004" +severity = "medium" +category = "soundness" +title = "Lambda-body usage not tracked against outer captures" +discovered = "2026-04-10" +fixed = "2026-04-11" +description = """ +lib/quantity.ml ExprLambda was a no-op for outer variable usage: captures +were not scaled by QOmega, so a lambda capturing a linear variable did not +raise LinearVariableUsedMultiple. Borrow checker also ignored captures, +allowing move of a captured variable after lambda creation. +""" +fix = """ +Two coordinated fixes (commit 48422d1): +1. quantity.ml ExprLambda: snapshot env, walk body (shadowing lambda params), + compute per-variable delta, restore env, re-apply delta scaled by QOmega. + scale_usage QOmega UOne = UMany → LinearVariableUsedMultiple for linear captures. +2. borrow.ml ExprLambda: collect_free walker finds all free variables in body, + creates Shared borrows for each captured place. Move of captured variable + → MoveWhileBorrowed. Borrows expire at block exit via lexical-lifetime clearing. + borrow_kind_name replaces show_borrow_kind for readable error messages. +""" +verification = "Manual smoke tests: lambda capturing moved value → 'cannot move `v` while shared-borrowed'; valid lambda (no owned captures) → passes. 73/73 tests pass." + +[[closed-bug]] +id = "BUG-005" +severity = "medium" +category = "codegen" +title = "WasmGC backend silently drops unknown function calls" +discovered = "2026-04-10" +fixed = "2026-04-11" +description = """ +lib/codegen_gc.ml ExprApp handling silently emitted 'drop all args + push null' +when the callee was not in the func_indices table, and did the same for +indirect calls. This was placeholder behaviour that produced wrong code rather +than failing loudly. +""" +fix = "Both fallback arms replaced with explicit CodegenError. Direct-call miss → Error (UnboundFunction id.name). Indirect/higher-order callee → Error (UnsupportedFeature \"indirect / higher-order call in WasmGC backend (call_ref not yet implemented)\"). New UnboundFunction variant added to codegen_error and format_codegen_error. commit b58178a." +regression-test-status = "landed 2026-05-23 — test/test_e2e.ml `test_gc_unbound_function_loud_fail` in `wasm_gc_loud_fail_tests`. Parses `fn main() -> Int { return totally_undefined_callee(42); }`, feeds it directly to Codegen_gc.generate_gc_module (no resolve step), asserts the emitted error message contains 'totally_undefined_callee' — which UnboundFunction's format string includes verbatim. Pins the explicit-error path so a regression to silent drop+null would fail the gate." + +# ─── Deferred upgrade: bring .machine_readable/ to current standards ────────── +# AffineScript predates most of the current hyperpolymath .machine_readable/ +# suite. The descriptiles core is present; the surrounding machinery is not. This is +# logged as deferred work, separate from the Manhattan recovery, to avoid +# scope creep into that plan. + +[deferred-upgrade] +# DONE 2026-04-12 — suite upgrade completed in dedicated session (no .ml changes) +status = "done" +completed = "2026-04-12" + +added = [ + ".machine_readable/CLADE.a2ml", + ".machine_readable/contractiles/ (migrated from root via git mv — history preserved)", + ".machine_readable/contractiles/adjust/Adjustfile.a2ml", + ".machine_readable/contractiles/intend/Intentfile.a2ml", + ".machine_readable/agent_instructions/README.adoc", + ".machine_readable/agent_instructions/coverage.a2ml", + ".machine_readable/agent_instructions/debt.a2ml", + ".machine_readable/agent_instructions/methodology.a2ml", + ".machine_readable/integrations/verisimdb.a2ml", + ".machine_readable/integrations/vexometer.a2ml", + ".machine_readable/ADJUST.contractile", + ".machine_readable/INTENT.contractile", + ".machine_readable/MUST.contractile", + ".machine_readable/TRUST.contractile" +] + +[[session-delta]] +# Appended 2026-05-31 — bindings/stdlib roadmap closeout session (PR #474). +# Mirror entry only; authoritative status remains docs/CAPABILITY-MATRIX.adoc +# + the per-row status in docs/{bindings,stdlib}-roadmap.adoc. Live test +# counts come from `dune runtest --force`, never hard-coded here (DOC-05, #176). +date = "2026-05-31" +pr = "#474" +narrative = "docs/history/BINDINGS-STDLIB-CODEGEN-2026-05-31.{adoc,a2ml}" +landed = [ + "bindings #19 (Zig C-ABI FFI patterns) — docs/specs/zig-ffi-patterns.adoc; roadmap row ◐->●", + "stdlib #25 (hex + RFC4648 base64) — stdlib/encoding.affine; roadmap row ○->◑ (Bytes overloads deferred to #30)", + "compiler: string_char_code_at / string_from_char_code byte builtins (resolve/typecheck/interp/codegen_deno)", + "codegen fix #478 — Int/Int truncates via Math.trunc on Deno-ESM (Float/Float untouched); js/node/wasm follow-ups tracked in #478", + "docs: bindings/stdlib/alib roadmap Tracking sections de-staled to live umbrellas #446/#412/#413" +] +umbrella-446 = "stays open — STEP 1-3 done, STEP 4/5 long-running" From 0146174989d3d1319569a9dd5cda062661cfe2fe Mon Sep 17 00:00:00 2001 From: hyperpolymath <6759885+hyperpolymath@users.noreply.github.com> Date: Tue, 7 Jul 2026 00:47:20 +0100 Subject: [PATCH 2/2] =?UTF-8?q?refactor(descriptiles):=20complete=20migrat?= =?UTF-8?q?ion=20=E2=80=94=20deletions=20+=20doc/content=20edits?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The prior commit captured only the renamed descriptiles/ files (index desync from a stash round-trip during verification). This adds the 6a2/ deletions and the ~30 doc/metadata + TRUST.contractile + check-doc-truthing.sh edits so the migration is whole. Co-Authored-By: Claude Opus 4.8 (1M context) --- .claude/CLAUDE.md | 14 +- .machine_readable/6a2/AGENTIC.a2ml | 53 - .machine_readable/6a2/ECOSYSTEM.a2ml | 27 - .machine_readable/6a2/META.a2ml | 1616 ----------------- .machine_readable/6a2/NEUROSYM.a2ml | 21 - .machine_readable/6a2/PLAYBOOK.a2ml | 74 - .machine_readable/6a2/STATE.a2ml | 258 --- .machine_readable/TRUST.contractile | 2 +- .machine_readable/anchors/ANCHOR.a2ml | 2 +- 0-AI-MANIFEST.a2ml | 4 +- CONTRIBUTING.md | 4 +- RSR_COMPLIANCE.adoc | 2 +- docs/CAPABILITY-MATRIX.adoc | 2 +- docs/EFFECTS-IMPLEMENTATION.adoc | 2 +- docs/NAVIGATION.adoc | 2 +- docs/ROADMAP.adoc | 2 +- docs/STATE-2026-05-26.adoc | 2 +- docs/TECH-DEBT.adoc | 4 +- docs/ci/required-checks.adoc | 2 +- .../decisions/0019-compiler-distribution.adoc | 4 +- .../0022-polonius-origin-variables.adoc | 4 +- docs/decisions/0023-finality-types.adoc | 2 +- docs/history/MODULE-SYSTEM-PROGRESS.md | 2 +- docs/history/SESSION-HANDOFF-2026-05-27.adoc | 2 +- .../TYPECHECKER-COMPLETION-2026-01-23.adoc | 2 +- ...YPED-WASM-LEVEL-10-CLOSURE-2026-04-19.adoc | 2 +- ...FFINESCRIPT-ROADMAP-STATUS-2026-04-13.adoc | 2 +- docs/spec.adoc | 2 +- docs/specs/SETTLED-DECISIONS.adoc | 20 +- .../specs/TYPED-WASM-COORDINATION-LEDGER.a2ml | 6 +- .../specs/TYPED-WASM-COORDINATION-LEDGER.adoc | 2 +- docs/specs/TYPED-WASM-ROADMAP.a2ml | 2 +- docs/specs/TYPED-WASM-ROADMAP.adoc | 2 +- docs/specs/async-on-wasm-cps.adoc | 2 +- docs/standards/PANIC-ATTACK.adoc | 2 +- docs/standards/TESTING.a2ml | 2 +- docs/standards/TESTING.adoc | 2 +- tools/check-doc-truthing.sh | 2 +- 38 files changed, 54 insertions(+), 2103 deletions(-) delete mode 100644 .machine_readable/6a2/AGENTIC.a2ml delete mode 100644 .machine_readable/6a2/ECOSYSTEM.a2ml delete mode 100644 .machine_readable/6a2/META.a2ml delete mode 100644 .machine_readable/6a2/NEUROSYM.a2ml delete mode 100644 .machine_readable/6a2/PLAYBOOK.a2ml delete mode 100644 .machine_readable/6a2/STATE.a2ml diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index fe9902fc..bee5f6fd 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -29,12 +29,12 @@ https://github.com/hyperpolymath/nextgen-languages/blob/main/docs/disambiguation The following files in `.machine_readable/` contain structured project metadata: -- `.machine_readable/6a2/STATE.a2ml` - Current project state and progress -- `.machine_readable/6a2/META.a2ml` - Architecture decisions and development practices -- `.machine_readable/6a2/ECOSYSTEM.a2ml` - Position in the ecosystem and related projects -- `.machine_readable/6a2/AGENTIC.a2ml` - AI agent interaction patterns -- `.machine_readable/6a2/NEUROSYM.a2ml` - Neurosymbolic integration config -- `.machine_readable/6a2/PLAYBOOK.a2ml` - Operational runbook +- `.machine_readable/descriptiles/STATE.a2ml` - Current project state and progress +- `.machine_readable/descriptiles/META.a2ml` - Architecture decisions and development practices +- `.machine_readable/descriptiles/ECOSYSTEM.a2ml` - Position in the ecosystem and related projects +- `.machine_readable/descriptiles/AGENTIC.a2ml` - AI agent interaction patterns +- `.machine_readable/descriptiles/NEUROSYM.a2ml` - Neurosymbolic integration config +- `.machine_readable/descriptiles/PLAYBOOK.a2ml` - Operational runbook --- @@ -57,7 +57,7 @@ The following files in `.machine_readable/` contain structured project metadata: | **Bash/POSIX Shell** | Scripts, automation | Keep minimal | | **Python** | SaltStack only | No other Python permitted | | **Nickel** | Configuration language | For complex configs | -| **A2ML** | State/meta files | .machine_readable/6a2/STATE.a2ml, .machine_readable/6a2/META.a2ml, .machine_readable/6a2/ECOSYSTEM.a2ml | +| **A2ML** | State/meta files | .machine_readable/descriptiles/STATE.a2ml, .machine_readable/descriptiles/META.a2ml, .machine_readable/descriptiles/ECOSYSTEM.a2ml | | **Julia** | Batch scripts, data processing | Per RSR | | **OCaml** | AffineScript compiler | Language-specific (compiler-host tooling lives here) | | **Ada** | Safety-critical systems | Where required | diff --git a/.machine_readable/6a2/AGENTIC.a2ml b/.machine_readable/6a2/AGENTIC.a2ml deleted file mode 100644 index 890ad441..00000000 --- a/.machine_readable/6a2/AGENTIC.a2ml +++ /dev/null @@ -1,53 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) -# -# AGENTIC.a2ml — AI agent constraints and capabilities -[metadata] -version = "0.1.0" -last-updated = "2026-04-11" - -[agent-permissions] -can-edit-source = true -can-edit-tests = true -can-edit-docs = true -can-edit-config = true -can-create-files = true - -[agent-constraints] -# What AI agents must NOT do: -# - Never use banned language patterns (believe_me, unsafeCoerce, etc.) -# - Never commit secrets or credentials -# - Never use banned languages (TypeScript, Python, Go, etc.) -# - Never place state files in repository root (must be in .machine_readable/) -# - Never use AGPL license (use MPL-2.0) - -[maintenance-integrity] -fail-closed = true -require-evidence-per-step = true -allow-silent-skip = false -require-rerun-after-fix = true -release-claim-requires-hard-pass = true - -[automation-hooks] -# on-enter: Read 0-AI-MANIFEST.a2ml, then STATE.a2ml -# on-exit: Update STATE.a2ml with session outcomes -# on-commit: Run just validate-rsr - -# ── Disambiguation ──────────────────────────────────────────────────────── -# AI agents have repeatedly conflated this repo (AffineScript) with the -# unrelated language Ephapax (hyperpolymath/ephapax). The two share exactly -# one thing — both target hyperpolymath/typed-wasm — and otherwise have -# different ASTs, type checkers, compilers, and proof stories. The shared -# `affine` syllable names a substructural-logic property in BOTH cases -# (it's a logic-family name), not a project relationship. Ephapax is itself -# dyadic and contains an `ephapax-affine` *sublanguage* — that sublanguage -# is NOT AffineScript. -[disambiguation] -this = "hyperpolymath/affinescript" -canonical-name = "AffineScript" -distinct-from = ["hyperpolymath/ephapax"] -shared-with-distinct = ["hyperpolymath/typed-wasm"] -ephapax-affine-sublanguage-is-not-affinescript = true -canonical-doc = "https://github.com/hyperpolymath/nextgen-languages/blob/main/docs/disambiguation/ephapax-vs-affinescript.md" -canonical-memory-entry = "feedback_affinescript_ephapax_siblings_not_impl_proof.md" -do-not-apply-cross-repo-lessons-without-explicit-check = true diff --git a/.machine_readable/6a2/ECOSYSTEM.a2ml b/.machine_readable/6a2/ECOSYSTEM.a2ml deleted file mode 100644 index 4a29adf0..00000000 --- a/.machine_readable/6a2/ECOSYSTEM.a2ml +++ /dev/null @@ -1,27 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# ECOSYSTEM.a2ml — Ecosystem position -# Converted from ECOSYSTEM.scm on 2026-03-21 - -[metadata] -project = "affinescript" -ecosystem = "hyperpolymath" - -[position] -type = "programming-language" -purpose = "A systems programming language combining affine types, dependent types, row polymorphism, and extensible effects, compiling to WebAssembly" - -[pipeline] -position = "co-product" -chain = "katagoria → typell → typed-wasm → PanLL" -notes = "AffineScript compiles to typed WasmGC. TypeLL grounds its type system. typed-wasm provides the shared binary layout and ABI conventions." -coordination = "nextgen-typing" - -[related-projects] -projects = [ - { name = "typell", relationship = "type-theory-foundation", notes = "TypeLL's open-ended progressive framework grounds AffineScript's type system. typell-affinescript bridge crate." }, - { name = "typed-wasm", relationship = "aggregate-library", notes = "Shared binary layout conventions for WasmGC cross-language calls. AffineScript Option[T]/Result[T,E]/String have agreed layouts here." }, - { name = "nextgen-typing", relationship = "coordination-parent", notes = "Coordination monorepo for the type theory pipeline. Canonical architecture doc." }, - { name = "ephapax", relationship = "sibling-wasm-target", notes = "Both compile to typed WasmGC. Independent designs; converge at binary level via typed-wasm." }, - { name = "katagoria", relationship = "research-upstream", notes = "Type theory research origination. Future AffineScript type system extensions may originate there." }, - { name = "panll", relationship = "consumer", notes = "AffineScript is one of PanLL's development language targets." }, -] diff --git a/.machine_readable/6a2/META.a2ml b/.machine_readable/6a2/META.a2ml deleted file mode 100644 index 13efb13c..00000000 --- a/.machine_readable/6a2/META.a2ml +++ /dev/null @@ -1,1616 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# META.a2ml — Project meta-information -# Converted from META.scm on 2026-03-21 - -[metadata] -project = "affinescript" -author = "Jonathan D.A. Jewell " -license = "MPL-2.0" -standard = "RSR 2026" - -# ────────────────────────────────────────────────────────────────────────────── -# Architecture Decision Records -# ────────────────────────────────────────────────────────────────────────────── -# ADRs are numbered sequentially and never renumbered. Superseded ADRs remain -# in place with status = "superseded" and a pointer to the replacement. -# Added 2026-04-10 during Track F1 (Idris2 Solo-core formalisation) — the -# formal-verification track surfaced three decisions about the QTT type system -# that were previously implicit. - -[[adr]] -id = "ADR-001" -status = "accepted" -date = "2026-04-10" -title = "Split-Γ is the canonical presentation of the QTT type system" -context = """ -The Idris2 Solo-core formalisation (docs/academic/formal-verification/solo-core/) -revealed that docs/spec.md §3.6 presents typing rules against a shared Γ -(writing e.g. T-App with both premises typed under the same Γ), while -lib/quantity.ml runs a post-hoc usage walk that accumulates per-variable -usage summed across subterms. The literature-standard QTT presentation -(Atkey 2018, Granule, Idris2 core) uses explicit context splitting - Γ = Γ₁ + q·Γ₂ -at every eliminator position. - -Both forms are extensionally equivalent for the Solo core (same programs -accepted/rejected). The difference is purely presentational. -""" -decision = """ -Split-Γ is canonical. The spec, the formalisation, and all future -documentation present typing rules in split-Γ form. The post-hoc usage walk -in lib/quantity.ml is an equivalent implementation strategy, documented as -such in a comment, not the definition of the type system. -""" -consequences = """ -- docs/spec.md §3.6 is rewritten to show explicit context splitting (~30-60 LOC). -- lib/quantity.ml gets a header comment explaining that its flat walk is an - equivalent implementation of the split-Γ rules, with a pointer to this ADR. -- The Idris2 formalisation's Typing.idr (which uses split-Γ) becomes the - mechanised witness of the spec — no translation layer required. -- Progress + preservation proofs can proceed in textbook form. -- Dependability gain: one source of truth. The spec-vs-impl gap (the failure - mode flagged in Track B's audit of TYPECHECKER-COMPLETION.md) cannot - structurally hide in this part of the system. -- Interop gain: the spec becomes citable and legible to anyone with QTT - background (Atkey, Idris2, Granule, Quill readers). -- Reader-tax cost: split-Γ notation is heavier than shared-Γ for newcomers. - Mitigated by surface-language tutorials keeping informal shared-Γ intuition - while the spec remains the formal reference. -""" - -[[adr]] -id = "ADR-002" -status = "accepted" -date = "2026-04-10" -title = "Let rule scales value context by binder quantity (QTT-orthodox)" -context = """ -During the Decision-2 audit of docs/spec.md T-Let against lib/typecheck.ml -and lib/quantity.ml, the current implementation was found to handle Let -unsoundly: - -- lib/typecheck.ml:631-665 (ExprLet) does zero quantity handling — pure - HM-style inference with let-generalisation, quantities entirely absent. -- lib/quantity.ml:251-253 (ExprLet in infer_usage_expr) does a flat walk - that sums e1's and e2's usages without scaling e1 by the binder's quantity. - -This is equivalent to the rule - Γ₁ ⊢ e1 : A (Γ₂, x:A) ⊢ e2 : B - ───────────────────────────────── - Γ₁ + Γ₂ ⊢ let x = e1 in e2 : B -rather than the QTT-orthodox - Γ₁ ⊢ e1 : A (Γ₂, x:^q A) ⊢ e2 : B - ───────────────────────────────────── - q·Γ₁ + Γ₂ ⊢ let x :^q = e1 in e2 : B - -Two concrete soundness bugs follow: - -BUG-001 (high severity — linear-value smuggling): - let x :ω = linear_resource() in use_x_once(x) - Current impl accepts; QTT-correct form rejects (e1's Γ must be scaled by - ω, so any Γ₁ containing a linear variable becomes ill-formed). - -BUG-002 (medium severity — erasure failure): - let x :0 = expensive_proof_term() in body_not_using_x - Current impl evaluates e1 and consumes its resources; QTT-correct form - scales e1's Γ by 0, producing the zero context, which means e1 can be - erased at runtime. - -BUG-001 is a soundness hole that defeats the point of affine types in any -program where ω-binders and linear values coexist. -""" -decision = """ -The Let rule in both the spec and the implementation scales the value -context by the binder's quantity. T-Let becomes: - - Γ₁ ⊢ e1 : A (Γ₂, x:^q A) ⊢ e2 : B - ───────────────────────────────────── - q·Γ₁ + Γ₂ ⊢ let x :^q = e1 in e2 : B - -Unannotated Let defaults to q = ω (matching current behaviour for all -existing unannotated code — so the fix is non-breaking for the common case). -""" -consequences = """ -- lib/quantity.ml ExprLet case grows a scale-by-q step before summing - into the outer context (~10 LOC). -- lib/typecheck.ml ExprLet case threads quantities (folded into Track A3 - which is already rewriting quantity flow through typecheck.ml). -- docs/spec.md T-Let rule is rewritten to show the scaling explicitly. -- Two regression tests added as Track A2 fixtures covering BUG-001 and - BUG-002. -- The Idris2 formalisation (Typing.idr THLet) already assumes the scaled - form — no rework there. -- Users who had previously (accidentally) relied on the unsound behaviour - to duplicate linear values through ω-lets will now get a compile error. - Expected blast radius: near-zero because affine enforcement was never - wired in the first place (see ADR-001 rationale). -""" - -[[adr]] -id = "ADR-003" -status = "accepted" -date = "2026-04-10" -title = "Evaluation order is strict CBV, left-to-right, for all n-ary forms" -context = """ -During the Decision-3 audit of lib/interp.ml for the Track F1 Step relation, -the evaluator was found to be internally inconsistent about evaluation order -of n-ary expression forms. - -- ExprBinary (lib/interp.ml:135-138) is explicitly left-to-right: - let* left_val = eval env left in - let* right_val = eval env right in - eval_binop op left_val right_val -- ExprApp args (:130), ExprTuple (:144), ExprArray (:148) all delegate to - eval_list, which is implemented with List.fold_right: - and eval_list env exprs = - List.fold_right (fun expr acc -> - let* vals = acc in - let* v = eval env expr in - Ok (v :: vals) - ) exprs (Ok []) - OCaml is strict, so in f (fold_right f [a;b;c] init) the innermost - application runs first — eval env c is forced before eval env b before - eval env a. Net result: right-to-left. - -This was almost certainly unintentional; nothing currently depends on the -order because affine enforcement was never wired. However: - -- The WASM and Julia backends will naturally generate left-to-right code, - producing cross-backend semantic divergence for any program with effectful - or resource-consuming arguments. -- The effect system (70% complete per STATE.a2ml) would observe handler - ordering tied to evaluation order, making this a latent soundness hazard. -- A Track F1 Step relation defined in textbook CBV left-to-right form would - disagree with the interpreter, requiring a non-standard right-to-left - presentation or a proof of order-independence (which is false in general - for effectful languages). -""" -decision = """ -All n-ary expression forms evaluate their subterms strict left-to-right. -This applies to: application arguments, tuples, arrays, record fields, -function calls in all positions. - -lib/interp.ml eval_list is fixed to left-to-right: - and eval_list env exprs = - let rec loop acc = function - | [] -> Ok (List.rev acc) - | e :: rest -> - let* v = eval env e in - loop (v :: acc) rest - in - loop [] exprs - -Track F1's Step relation uses the standard CBV left-to-right presentation - e1 → e1' ⟹ e1 e2 → e1' e2 (reduce function first) - e2 → e2' ⟹ v1 e2 → v1 e2' (then argument, only once function is a value) -with the symmetric congruences for pairs, tuples, etc. -""" -consequences = """ -- lib/interp.ml eval_list is replaced (~6 LOC diff). -- docs/spec.md gains (or receives an edit to) an explicit operational - semantics section stating strict CBV, left-to-right. -- A regression test lands in Track A2 exercising a program whose output - depends on left-to-right evaluation (e.g. a tuple of two side-effecting - calls that mutate a shared counter). -- WASM, Julia, and the effect system all become consistent with the - interpreter by default — no backend fix required. -- Track F1 can proceed on its current footing to mechanise Step and the - progress/preservation proofs without author input. -- Existing programs are unaffected unless they rely on observable right- - to-left evaluation of tuple/arg components. The interpreter is the only - place that did this, and nobody has filed a bug, so blast radius is - expected to be zero. -""" - -[[adr]] -id = "ADR-007" -status = "accepted" -date = "2026-04-10" -accepted = "2026-04-10" -title = "Surface syntax for quantity annotations: @linear/@erased/@unrestricted (primary) + :1/:0/:ω (sugar)" -context = """ -The Track A audit on 2026-04-10 found that affine enforcement is wired -through the CLI (Quantity.check_program_quantities runs from -lib/typecheck.ml:1206 on every check/compile/eval invocation) but -**unreachable** from user programs because the surface syntax for declaring -quantity annotations on let-binders does not exist. Specifically: - -- lib/ast.ml ExprLet and StmtLet have no quantity field. They carry - el_mut/sl_mut, el_pat/sl_pat, el_ty/sl_ty, el_value/sl_value, el_body - — no el_quantity. Only function `param` and `type_param` carry - `p_quantity`/`tp_quantity`. -- lib/lexer.ml never emits ZERO/ONE tokens. Source `0` and `1` are lexed - as `INT 0`/`INT 1`. The only quantity literal that round-trips through - source today is `omega`/`ω` (lexer.ml:53 and lexer.ml:153 respectively). -- lib/parser.mly's `quantity` rule (lines 180-183) takes ZERO|ONE|OMEGA - but only OMEGA is reachable from source. Even on function parameters, - `:1` and `:0` annotations cannot be written. -- ADR-002 specifies the QTT-orthodox scaled Let rule (q·Γ₁ + Γ₂), which - requires the binder to carry a quantity. Without surface syntax to - attach one, the rule has no input to scale by, BUG-001 cannot be - closed, and the headline affine-types feature remains theatre. - -Surface syntax is a language-design decision, not an implementation -detail. The decision needs author input before any of items 1-6 in -[track-a-manhattan] proceed. -""" -options = """ -Three candidate surface syntaxes are listed. None has been chosen. - -# Option A — caret-quantity suffix on `let` keyword - - let^1 x = linear_resource() in use_x_once(x) - let^0 _proof = expensive_term() in body - let^ω y = pure_int() in use_y_many(y) - - Pros: - - Visually parallel to T-Let's `let x :^q A` notation in QTT papers. - - The `^` is currently unused in the lexer, so no disambiguation cost. - - The annotation lives on the keyword, not the binder, which mirrors - the type-theory framing where `q` parameterises the rule, not the - variable. - - Easy parser rule: LET (CARET quantity)? pat (COLON ty)? EQ value. - - Cons: - - `^` is unfamiliar surface syntax for users coming from Rust/Idris2. - - The `^` may collide with future bitwise-XOR or pow operators - (currently OpBitXor exists in ast.ml but the lexer surface form is - unclear). - - Requires users to type a non-letter character on every annotation. - -# Option B — colon-quantity prefix on the type ascription - - let x :1 :Int = linear_resource() - let x :0 :Int = expensive_term() - let x :ω :Int = pure_int() - let x :1 = linear_resource() # type inferred - - Pros: - - Reuses the existing COLON token; no new operators needed. - - Mirrors the function parameter syntax, which already accepts - `qty? own? name COLON ty` (parser.mly:186). - - Familiar to readers of Idris2 (`x : 1 A`) and Quantitative Type - Theory papers. - - Cons: - - Two consecutive colons looks awkward and may confuse readers - expecting `::` (the path separator COLONCOLON token). - - When the type is inferred (`let x :1 = e`), the parser needs - one-token lookahead to distinguish `:1` (quantity) from `:Int` - (type). This is a minor parser challenge but non-zero. - - The lexer must additionally route `INT 0`/`INT 1` to ZERO/ONE in - quantity position, which is context-sensitive lexing — best handled - by accepting `INT 0|INT 1` directly in the parser's quantity rule - instead, but that introduces a small grammar wart. - -# Option C — annotation-style attribute before `let` - - @linear let x = linear_resource() in use_x_once(x) - @erased let _proof = expensive_term() in body - @unrestricted let y = pure_int() in use_y_many(y) - let z = pure_int() in use_z(z) # default = unrestricted - - Pros: - - Reads naturally; `@linear` and `@erased` are self-documenting and - require no quantity-theory background. - - Trivially extensible to future modal annotations (e.g. `@borrowed`, - `@owned`) without grammar changes. - - Parser change is local: AT (ident) LET pat (COLON ty)? EQ value. - No COLON disambiguation, no new operator tokens. - - Aligns with the language-policy preference for descriptive names - over symbolic ones (CLAUDE.md: "Always use descriptive variable - names"; the same instinct applied to annotations). - - Cons: - - Verbose. `@linear let x = ...` is six tokens vs `let^1 x = ...`'s - four. - - Diverges from the QTT mathematical notation, making it harder for - paper-trained readers to map source to spec. Mitigation: spec.md - can show both forms in parallel. - - The `@` token is currently unused, so it's free, but introducing - it for one feature creates pressure to use it for others (which - may or may not be desirable depending on language direction). - - Requires a small mapping table from attribute name to quantity - value (`linear → QOne`, `erased → QZero`, `unrestricted → QOmega`). -""" -decision = """ -Hybrid: Option C accepted as primary, Option B accepted as sugar. - -Both surface syntaxes parse to the same internal representation. -Tutorials, error messages, IDE tooling, and the spec's prose all use -Option C. Option B remains legal for users porting from QTT papers or -who prefer the compact form. Option A is rejected outright. - -# Primary surface form (Option C — what tutorials and error messages use) - - @linear — used exactly once (QOne) - @erased — must not be used at runtime (QZero) - @unrestricted — any number of uses (QOmega), the default - -Examples: - @linear let x = linear_resource() in use_x_once(x) - @erased let _proof = expensive_term() in body_not_using_proof - @unrestricted let y = pure_int() in use_y_many(y) - let z = pure_int() in use_z(z) # default: @unrestricted - - fn consume(@linear x: Resource) -> Unit = release(x) - fn weigh(@erased _proof: Even(n), n: Nat) -> Nat = n / 2 - -# Sugar surface form (Option B — accepted but not promoted) - - let x :1 :Resource = linear_resource() in use_x_once(x) - let _proof :0 :Even(n) = expensive_term() in body - let y :ω :Int = pure_int() in use_y_many(y) - let z :Int = pure_int() in use_z(z) # no quantity = @unrestricted - - fn consume(x :1: Resource) -> Unit = release(x) - fn weigh(_proof :0: Even(n), n: Nat) -> Nat = n / 2 - -# Equivalence - - @linear ≡ :1 - @erased ≡ :0 - @unrestricted ≡ :ω (also: omitted entirely) - -Both forms parse to the same `el_quantity = Some QOne | QZero | QOmega` -in lib/ast.ml. The compiler chooses the canonical form (Option C) when -emitting diagnostics, formatter output, and pretty-printed source. The -formatter rewrites Option B to Option C on `affinescript fmt`, with an -opt-out flag `--keep-quantity-sugar` for users who deliberately prefer -the compact form. - -Rationale (decided per the standing priority order -dependability > security > interop > usability/accessibility/marketability -> performance > versatility > functional extension): - -- Usability/accessibility/marketability beats consistency-with-papers - in the priority order, so Option C must be the primary form a new - user encounters in tutorials and error messages. A new user reading - `@linear let x = e` can form a working hypothesis without consulting - the spec. The same user looking at `let x :1 = e` has nothing to - Google. -- However, the priority order does not require *exclusivity*. Accepting - Option B as sugar costs ~15 LOC of parser surface area and one - paragraph in the spec, and in return it: - * Preserves consistency with the existing function-parameter syntax - (which uses `qty? own? name COLON ty`), so the param-quantity gap - closes with the *same* grammar production rather than a parallel - one. Users learning the param syntax already see `:1` in the - grammar; the sugar makes that knowledge transfer to let-binders. - * Preserves citation isomorphism with QTT papers — anyone porting an - Idris2 example or a Granule example can paste the numeric form - directly without rewriting it. - * Gives advanced users a denser form for code where annotations are - frequent and English keywords would create visual noise. -- Compiler errors and source code share the C vocabulary regardless of - which form the user wrote: "`@linear` binding 'x' used 2 times" reads - cleanly without mental translation, even if the source was written - as `:1`. The diagnostic always speaks the canonical form. -- Tutorials can be example-driven from the first runnable program with - Option C; the QTT semiring and Option B sugar become optional - advanced reading rather than a prerequisite for "hello world with - affine types." -- The `@` namespace is now committed to AffineScript and may grow to - cover additional modal annotations (e.g. `@total`, `@pure`, - `@borrowed`, `@owned`) in future ADRs. This is an intentional - reservation, not a side effect. -- The Rust/Linear-Haskell precedent is real: Rust ships affine - semantics under approachable keywords and reaches a vastly larger - audience than Linear Haskell, which exposes the type-theory - machinery directly. AffineScript chooses Rust's marketing instinct - for the primary form, while keeping Linear Haskell's notation - available for users who already think in it. - -# Style guide commitment - -The spec commits explicitly to keeping both forms supported. Future -contributors proposing to drop Option B sugar must amend this ADR -rather than removing it silently. The recommended convention in -published AffineScript code is: - -- New code: Option C (`@linear let x = e`). -- Code accompanying a paper: either form, author's choice. -- Auto-formatted code: Option C (formatter rewrites unless - `--keep-quantity-sugar` is set). -- Generated code (compiler diagnostics, IDE quick-fixes, refactoring - tools): Option C exclusively. - -Rejected alternatives: - -- Option A (`let^q x = e`): rejected. Lowest implementation cost but - worst on the priority order — symbolic, opaque to non-TT readers, - forecloses future use of `^` for bitwise XOR or exponentiation. -- Option B exclusively (without C): rejected for the same - accessibility/adoption reasons that make C the primary form. -- Option C exclusively (without B sugar): considered and rejected. The - marginal cost of accepting B is low (~15 LOC parser, one paragraph - spec), and the benefit — closing the param-quantity gap with one - grammar production and preserving paper-citation isomorphism — is - worth the cost given the standing priority order's high weighting - of interop alongside accessibility. -""" -consequences = """ -- lib/ast.ml gains el_quantity / sl_quantity fields on ExprLet and - StmtLet. ~10 modules pattern-matching on Let must be updated - mechanically (typecheck, interp, codegen, codegen_gc, julia_codegen, - formatter, sexpr_dump, json_output, linter, opt, desugar_traits). -- lib/lexer.ml gains an AT token for `@`. ~2 LOC. -- lib/token.ml gains the AT token kind. ~2 LOC. -- lib/parser.mly gains: - * AT ident — a quantity_attr rule that maps `@linear`/`@erased`/ - `@unrestricted` to QOne/QZero/QOmega and rejects unknown attribute - names with a parse error pointing at the documented set. - * Optional quantity_attr prefix on let_decl, stmt_let, param, and - lambda_param productions. ~15 LOC. - * The existing numeric quantity rule (ZERO|ONE|OMEGA) gains INT 0 - and INT 1 as accepted lexemes (since the lexer emits INT not - ZERO/ONE for `0` and `1`). This makes Option B sugar reachable - from source on let-binders and params, in addition to type_params - where it already worked. ~5 LOC. - * Both productions resolve to the same el_quantity / sl_quantity / - p_quantity slot — they are alternative concrete syntaxes for the - identical abstract syntax. -- lib/quantity.ml ExprLet/StmtLet cases gain context scaling per - ADR-002. ~25 LOC including a `scale_usage_by` helper. -- Error messages in lib/quantity.ml format_quantity_error are updated - to use `@linear` / `@erased` / `@unrestricted` vocabulary instead of - the show_quantity numeric forms. -- Regression fixtures land covering BOTH surface forms: - * test/e2e/fixtures/bug_001_omega_let_smuggles_linear.affine — must - be rejected (Option C form: `@unrestricted let x = ...`). - * test/e2e/fixtures/bug_001_sugar_form.affine — must be rejected - (Option B form: `let x :ω = ...`). Proves both surface forms are - enforced through the same code path. - * test/e2e/fixtures/affine_let_valid.affine — must pass (Option C). - * test/e2e/fixtures/affine_let_valid_sugar.affine — must pass - (Option B). -- docs/spec.md T-Let rule is rewritten to show the formal QTT notation - alongside both surface forms (Option C primary, Option B sugar) so - paper-trained readers and pedagogy-first readers both see their - preferred presentation. -- docs/spec.md gains a "Surface syntax for quantity annotations" - section documenting the equivalence table and the style-guide - commitment from this ADR. -- BUG-002 (`:0` lets do not erase their RHS) closes in the same change, - per ADR-002's consequence list. -- The affine-types feature flag in STATE.a2ml [features] flips from - `wired-but-unreachable` to `wired-and-reachable` once the change - lands. -- Future modal annotations (`@total`, `@pure`, `@borrowed`, `@owned`, - etc.) inherit the same `@`-attribute parsing infrastructure for free. -- The formatter (lib/formatter.ml) gains a `--keep-quantity-sugar` flag - that defaults to OFF (so default `affinescript fmt` rewrites Option - B sugar to Option C primary). Users opting in retain their chosen - form across format passes. -""" -references = [ - ".machine_readable/6a2/STATE.a2ml [track-a-manhattan]", - ".machine_readable/6a2/STATE.a2ml [[open-bug]] BUG-001", - ".machine_readable/6a2/STATE.a2ml [[open-bug]] BUG-002", - ".machine_readable/6a2/META.a2ml [[adr]] ADR-002 (scaled Let rule)", - "lib/ast.ml ExprLet (lines 105-111), StmtLet (lines 170-176)", - "lib/lexer.ml (ZERO/ONE never emitted)", - "lib/parser.mly quantity rule (lines 180-183)", -] - -[[adr]] -id = "ADR-008" -status = "accepted" -date = "2026-04-11" -title = "Effect invocation uses direct call syntax — no 'perform' keyword" -context = """ -Algebraic effects require a syntax for invoking an effect operation at a -call site. Two candidates exist in the literature: - -1. Direct call: `Http.get(url)` — the effect operation looks like a - namespaced function call. The effect is declared in the return type - annotation (`-> T / Http, Async`) not at each call site. - -2. Explicit perform: `perform Http.get(url)` — the keyword makes the - effectful nature of the call visible at every use site. Used by Koka - (earlier versions) and some effect-system research languages. - -The AffineScript DESIGN-VISION.adoc already shows direct call style in its -effect examples. The decision needed to be made explicit so the conformance -suite, parser, and documentation are unambiguous. -""" -decision = """ -Effect operations are invoked with direct call syntax. No `perform` keyword. - - fn fetch_user(id: Int) -> User / Http, Async { - let resp = Http.get("/users/" ++ show(id)) - Async.await(resp.json()) - } - -The effect is declared once — in the return type annotation (`/ Http, Async`). -The call sites are plain calls. The type signature is the contract; the call -site is just a call. -""" -consequences = """ -- The conformance suite, parser, and spec must not admit or require `perform`. -- Effect operation calls are syntactically indistinguishable from regular - function calls; the distinction is entirely in the type system. -- Error messages for unhandled effects reference the return type annotation, - not a `perform` site. -- This is consistent with the ergonomics goal: effect-heavy code does not - accumulate keyword noise at every call site. -- Face parsers (Python-face, JS-face) can map `async/await` to the Async - effect without introducing a `perform` concept — `await` desugars to - `Async.await(...)`, which is a direct call. -""" -references = [ - "docs/DESIGN-VISION.adoc (effect examples)", - "docs/specs/effects.md", -] - -[[adr]] -id = "ADR-009" -status = "accepted" -date = "2026-04-11" -title = "The conformance suite is authoritative — parser must conform to spec" -context = """ -The conformance suite (test/conformance/) contains valid AffineScript programs -drawn from the spec. As of 2026-04-11, 8 of 12 valid conformance tests fail -to parse. The three categories of failure are: - -1. Uppercase type names — `Int`, `String`, `Bool`, `Option` are written in - the spec with PascalCase. The parser's `ident` rule accepts only lowercase, - so `Int` fails to parse as a type name. - -2. ML-style enum syntax — the spec's enum declaration syntax does not match - what the parser accepts. - -3. Effect op type parameters — the spec includes type parameters on effect - operations; the parser does not support them. - -Two possible authorities: the spec (conformance suite) or the parser -(current implementation). -""" -decision = """ -The spec is authoritative. The parser must conform to the spec. This is -consistent with the standing estate-wide rule: 'Language scope lives in a -written thesis. Thesis authoritative, code must conform. If disagreement, -code is wrong.' - -Specific required changes: -1. Parser must accept PascalCase type names (`Int`, `String`, `Bool`, user- - defined types). The type name namespace is PascalCase; value names remain - camelCase/snake_case. This also fixes the conformance suite failure where - effect and enum type names could not be written. -2. Parser must accept the spec's enum declaration syntax (to be confirmed - against spec and fixed accordingly). -3. Parser must support type parameters on effect operations. - -The target is 12/12 conformance suite passing. -""" -consequences = """ -- lib/lexer.ml and lib/parser.mly are updated to accept PascalCase type names. -- The `ident` vs `type_ident` distinction is formalised in the grammar: - `ident` = lowercase-leading (values, variables, functions) - `type_ident` = uppercase-leading (types, effects, enums, type aliases) -- lib/parser.mly enum and effect rules are audited against spec and fixed. -- 12/12 conformance tests pass. The conformance suite becomes a live - regression suite — any future parser change that breaks a conformance test - is a bug, not a spec disagreement. -- All fixture files under test/e2e/fixtures/ that use lowercase type names - (e.g. `type point = ...`) are reviewed; lowercase type aliases remain valid - (they are value-level names) but builtin types (`int` written lowercase) - are normalised to PascalCase. -- Error messages and diagnostics use PascalCase type names consistently. -""" -references = [ - "test/conformance/", - "lib/lexer.ml", - "lib/parser.mly", - "docs/spec.md", -] - -[[adr]] -id = "ADR-010" -status = "accepted" -date = "2026-04-11" -title = "Face-aware error formatting is a first-class toolchain concern" -context = """ -AffineScript supports syntactic face layers (Python-face, JS-face, -pseudocode-face, canonical AffineScript) that allow developers to write -AffineScript using familiar surface syntax from other languages. The compiler -pipeline forks only at the parser; everything downstream (type checker, -codegen) is shared and operates on the canonical AST. - -Without face-aware error formatting, a developer writing Python-face -AffineScript receives type errors expressed in canonical AffineScript -syntax — terms and constructs they have deliberately not learned yet. This -shatters the face illusion at the worst possible moment (when the developer -is stuck and needs help) and is a direct contradiction of the adoption goal. -""" -decision = """ -Face-aware error formatting is a first-class toolchain concern, not an -afterthought. It is implemented as a formatting layer between the compiler -and the terminal, not inside the compiler itself. - -Architecture: - compiler (emits errors in canonical AST terms) - ↓ - face-aware error formatter ← separate concern, swappable - ↓ - terminal / IDE / LSP - -The compiler's error representation is canonical and face-agnostic. The -formatter receives: (error, active-face) → formatted error string. - -Each face provides an error vocabulary mapping: - canonical term → face term - e.g. (python-face) "affine binding" → "single-use variable" - e.g. (js-face) "Option[T]" → "T | null" [with note: 'use .unwrap_or()'] - -Error source spans are always reported in the face's syntax, not canonical -AffineScript syntax, since the user's file is written in the face. -""" -consequences = """ -- The compiler's error types carry enough information for the formatter to - reconstruct face-appropriate messages. No compiler internals leak face - knowledge. -- Each face ships with an error vocabulary file (part of the face definition). -- The toolchain (CLI, LSP server, playground) passes the active face to the - formatter. The formatter is the single point of face-awareness for errors. -- A developer on Python-face who hits a linearity violation sees: - Error: single-use variable 'x' used twice - → line 7, in greet - not: - Error: affine binding 'x' used 2 times (quantity violation: QOne, found 2) -- The face formatter is versioned alongside the face. When a face is - deprecated, its error formatter is deprecated with it. -- The IDE/LSP integration carries face information in the project config so - hover types, completions, and inline errors all speak the face's vocabulary. -- This is a design commitment, not an immediate implementation task. The - canonical compiler error representation must be designed with this - formatability requirement in mind from the start. -""" -references = [ - "docs/DESIGN-VISION.adoc (faces section)", - "docs/specs/faces.adoc", -] - -[[adr]] -id = "ADR-011" -status = "accepted" -date = "2026-05-17" -title = "Stdlib namespace model: real modules with qualified paths" -context = """ -The AffineScript stdlib was never compiled through the static -resolve → typecheck → codegen pipeline as a coherent unit (issue #128). -Its core files (prelude, option, result, collections, string, io, -testing, effects, math, traits) are interpreter-era code: a flat global -namespace with no `module`/`use`/`open`, and conflicting duplicate -definitions across files — `prelude.affine` and `option.affine` both -define `is_some`/`unwrap`/`map`/… with *incompatible* signatures -(`prelude.map(arr, f)` vs `option.map(f, opt)`). Commit b895374 seeded -`Some/None/Ok/Err` as builtins to make `string.affine` resolve -standalone, a band-aid that entrenches the flat namespace. - -Issue #132 required a single ruling — real modules vs intentionally -flat-and-deduplicated — because #133 (dedup), #135 (whole-stdlib -compile), #137 (multi-module integration test) and #138 (band-aid -removal) all branch on it. - -The compiler already has the machinery: the grammar accepts -`module X;`, `use path;` and `::`-qualified paths; `module_loader.ml` -resolves module paths to files with search paths, nested modules and -caching; and the *newer* stdlib files (Core, Crypto, Ajv, Sqlite, -Grammy, Deno, Network, Vscode, VscodeLanguageClient) already declare -`module X;` and use qualified constructors (`Ordering::Less` in -traits.affine). Only the legacy core files are flat. -""" -decision = """ -The stdlib uses **real modules with qualified paths**, not a flat -de-duplicated prelude. - -- Every `stdlib/*.affine` declares `module ;` and is addressed by - its module path. -- Cross-file use is explicit: `use option::{Option, Some, None};` / - qualified references `Result::unwrap`. -- There is exactly one canonical definition per name, owned by its - module. The `prelude`/`option`/`result` overlaps are resolved by - giving each name a single owning module and having the others `use` - it (no duplicate, signature-divergent copies). -- A minimal, explicit prelude module may re-export the few universally - needed names (`Option`, `Result`, `Some`, `None`, `Ok`, `Err`); it - contains *re-exports*, not independent redefinitions. -- The b895374 seeded builtins are removed once resolution flows through - the module path (tracked by #138); they are not load-bearing. - -This is the path consistent with the machinery the compiler and the -newer stdlib already use; it makes the AOT pipeline exercise real -cross-module resolution, which is the actual #128 objective. -""" -consequences = """ -- #133 removes the duplicate/conflicting prelude/option/result defs by - assigning each name one owning module; non-owners `use` it. -- #135 brings the legacy core files under `module`/`use` as it makes - each compile through resolve → typecheck → codegen. -- #137 (multi-module integration test) is meaningful: it exercises - several modules `use`d together, which the flat model could not test. -- #138 can delete the seeded-builtins band-aid once the prelude - re-export module exists and resolution uses it. -- Slightly more up-front import wiring than a flat prelude, but it is - the model the language already commits to elsewhere (ADR pointer: - newer stdlib + `module_loader.ml`), so no new compiler design debt. -- This decision is settled; do not reopen without amending this ADR. -""" -references = [ - "https://github.com/hyperpolymath/affinescript/issues/128", - "https://github.com/hyperpolymath/affinescript/issues/132", - "docs/history/MODULE-SYSTEM-PROGRESS.md (2026-05-17 decision section)", - "lib/module_loader.ml", - "lib/parser.mly (module_decl / import_decl / COLONCOLON)", -] - -[[adr]] -id = "ADR-012" -status = "accepted" -date = "2026-05-18" -title = "Grammar changes are correctness assertions; parser-conflict disclosure" -context = """ -The Menhir parser emitted a wall of "N shift/reduce conflicts were -arbitrarily resolved" notices. Issues #215/#218 set out to address the -real concern: a correct toolchain looking broken — acutely damaging when -correctness is the product. Two grammar changes were made along the way -(#222 record `#{ }`; #215 family B `return`/`resume` hoist). It became -necessary to state, permanently, *why* the grammar may change and how the -residual benign notices are handled — so that in the long term no one -mistakes either the changes or the remaining notices for sloppiness. -""" -decision = """ -The grammar is changed only to make it assert something TRUE about the -language's semantics; it is never contorted to lower a cosmetic metric. - -- `#{ }` records (#218): semantic — kills block-vs-record ambiguity and - the struct-literal-in-scrutinee hazard by construction. Conflict-count - fall was a consequence, not the reason. -- `return`/`resume` as diverging prefix expressions (#215 family B): - semantic — `return e : Never`, never an operator operand; - `(return a) + b` now needs explicit parens (a feature: post-divergence - dead code made visible). NOT motivated by, and does NOT reduce, the - conflict count (proven: 21->21 S/R states, net-neutral). -- Residual ~68 S/R + small R/R (incl. state 401, the block - trailing-expr-vs-statement boundary): inherent LALR(1) artefacts that - Menhir resolves CORRECTLY (257-case gate green). Eliminating them = - systemic precedence/left-factoring surgery, estate-wide parse blast - radius, cosmetic-only payoff = exactly the contortion this ADR - forbids. Intentionally LEFT IN PLACE; documented won't-fix on #215. -- Disclosure: default `just build` MASKS these specific benign notices - but prints the masked count + correctness proof + this ADR pointer + - the exact reveal command (`just build-loud` / - AFFINESCRIPT_SHOW_MENHIR_NOISE=1 / plain `dune build`). The - parser-generator output is not suppressed at source (that would be - risky change for a cosmetic end); the build is unchanged and fully - transparent on demand. Masking is disclosure, not concealment. -""" -consequences = """ -- Every grammar change in the history has a recorded semantic - justification; none was made to chase a number. -- The residual notices are settled won't-fix — do not reopen as "bugs" - without amending this ADR. -- Contributors see a calm, honest build; auditors see everything via - one documented command. -- This decision is settled; do not reopen without amending this ADR. -""" -references = [ - "https://github.com/hyperpolymath/affinescript/issues/215", - "https://github.com/hyperpolymath/affinescript/issues/218", - "https://github.com/hyperpolymath/affinescript/pull/222", - "docs/specs/SETTLED-DECISIONS.adoc (ADR-012 section)", - "justfile (build / build-loud recipes)", - "lib/parser.mly (expr_assign return/resume; record #{ )", -] - -[[adr]] -id = "ADR-013" -status = "accepted" -date = "2026-05-18" -title = "Async on the WasmGC backend: transparent CPS continuation transform" -context = """ -stdlib/Http.affine exposes one portable surface -`fetch(req) -> Response / { Net, Async }`. The Deno-ESM backend lowers -it to a direct `await` (#226, shipped). The WasmGC backend cannot: -extern calls are synchronous, the boundary is i32-only, and the -estate's async-over-wasm mechanism (#205) is callback-shaped -(`-> Thenable` + thenableThen/thenableResultJson). Issue #225 Option 2 -(owner-chosen) requires one BYTE-IDENTICAL source surface on both -targets, so the wasm backend must hide the continuation plumbing. -typed-wasm is the shared convergence ABI (ADR-004); Ephapax is a -co-stakeholder for the async ABI. -""" -decision = """ -On the WasmGC backend ONLY, functions whose effect row includes `Async` -are compiled via a selective continuation-passing (CPS) transform. -Pure / non-Async functions are untouched (no codegen or perf change). - -- Each async boundary (call to an extern returning under /Async, or to - another Async function) splits the body; code after it becomes a - generated continuation function whose env is the live-local capture - set, marshalled via the EXISTING #199 closure ABI - ([fnId@0,envPtr@4] through __indirect_function_table). -- The split lowers to thenableThen(handle, ) (the - EXISTING #205 host->guest re-entry); the enclosing Async function - itself returns a Thenable handle, so Thenable composes transparently - up the call chain. At the host boundary the outermost Thenable is - awaited. The programmer never sees Thenable — effect row is the - abstraction, backend chooses the mechanism (as on the Deno side). -- New orchestration over three proven primitives (find_free_vars, #199 - closure ABI, #205 re-entry); NOT a new runtime, NOT JSPI (rejected), - NOT a general delimited-continuation feature. - -Correctness obligations (binding, pre-merge): -1. A linear/own local captured into a continuation is the borrow - checker's single use; double-resumption impossible (Thenable settles - once; thenableThen fires once — asserted). -2. Transform triggers iff Async in fd_eff; Net/other effects ride - along; fd_is_async reused as predicate. -3. Response reconstruction uses a minimal typed reader (jsonField is - insufficient for headers: [(String,String)]); general decode - deferred to #161. No silent lossiness. -4. Full `dune test --force` (258) green at every commit; wasm e2e - parity test mirrors tests/codegen-deno/http_fetch.*. -""" -consequences = """ -- Single portable Http surface across Deno-ESM and WasmGC; #160 closes - only when both targets are green (joint-close with #225). -- The Thenable-handle + thenableThen continuation protocol is the - agreed async ABI for the typed-wasm convergence layer; Ephapax - co-stakeholder review is required for the convergence-spec section. -- Delivered incrementally (4 PRs, each behind the 258 gate); scope is - WasmGC Async functions only. -- This decision is settled; do not reopen without amending this ADR. -""" -references = [ - "https://github.com/hyperpolymath/affinescript/issues/225", - "https://github.com/hyperpolymath/affinescript/issues/160", - "https://github.com/hyperpolymath/affinescript/pull/226", - "docs/specs/async-on-wasm-cps.adoc (full design)", - "docs/specs/SETTLED-DECISIONS.adoc (ADR-013 section)", - "lib/codegen.ml (find_free_vars; #199 closure ABI)", - "stdlib/Vscode.affine + packages/affine-vscode/mod.js (#205 thenableThen)", - "typed-wasm ADR-004 (convergence / aggregate library; Ephapax)", -] - -[[adr]] -id = "ADR-014" -status = "accepted" -date = "2026-05-19" -title = "Module-qualified type/effect path separator: both `.` and `::`, `::` canonical" -context = """ -The type/effect grammar had NO module-qualified path production: a -qualified reference such as `Externs.Res` or `Externs.Net` was -unrepresentable in any type or effect position and failed with -`parse error` at the `.` (issue #228). An audit of the estate `.affine` -corpus (compiler-as-oracle, origin/main) found this the SINGLE dominant -fault estate-wide: 525 of ~1177 files fail to parse, qualified-path the -leading cause in every hand-written Frontier-playbook TS-port. ADR-011 -already settled "real modules with qualified paths"; the consequence was -unspeakable because the grammar could not name a module-qualified -entity. Per ADR-012 (grammar changes are correctness assertions), -closing this is asserting a settled truth, not adding sugar. The -separator was the open, escalated language-design call: `module_path` -(parser.mly) uses DOT; `use`/value paths use COLONCOLON (ADR-011 -`Result::unwrap`, `use option::{…}`); the estate corpus uses `.` for -qualified types. -""" -decision = """ -Accept BOTH `.` and `::` (mixed permitted) as the module-qualified -type/effect path separator. `Pkg::Type` is the CANONICAL form -(consistent with ADR-011 value paths — one canonical separator -estate-wide long-term). `.` is tolerated and normalised to `::`. - -Implementation (parser.mly, conflict-neutral): -- New `qualified_type_name` (>=2 `upper_ident` segments, right-recursive, - `qsep = DOT | COLONCOLON`), folded into a single canonical `::`-joined - ident string. Added to `type_expr_primary` (TyCon / TyApp via `[ ]` - and `< >`) and `effect_term` (EffVar / EffCon). -- Because the qualified name is folded to one ident, downstream - (resolve/typecheck/all codegens) sees a single ident and needs no - change; the formatter prints the stored `::` form, so `.`→`::` - normalisation is automatic with NO formatter change. -- Conflict-neutral by construction: it only adds DOT/COLONCOLON as - lookahead after a type/effect-position `upper_ident`, where no reduce - action previously existed (that void is precisely the - `parse error at .` #228 reports). Measured: Menhir conflict states - unchanged at 21 S/R + 1 R/R; item counts unchanged at 68 S/R / 7 R/R. -- Resolution of the qualified name against a real module is a separate - concern (the parse gap is what #228 names); the grammar PR unblocks - parsing — most of the 525 estate parse failures clear with zero - consumer churn. Genuine ReScript-surface residue (#229) is tracked - separately. -""" -consequences = """ -- The estate `.affine` corpus parses without rewriting `.`→`::`; the - formatter migrates to the `::` canonical form opportunistically. -- No estate consumer porting was needed for the grammar gap (rejected - alternative: rewriting hundreds of files entrenches a workaround - against the Frontier playbook). -- This decision is settled; do not reopen without amending this ADR. - CORE-03 in docs/TECH-DEBT.adoc. -""" -references = [ - "https://github.com/hyperpolymath/affinescript/issues/228", - "https://github.com/hyperpolymath/affinescript/issues/229", - "lib/parser.mly (qualified_type_name; type_expr_primary; effect_term)", - "docs/specs/SETTLED-DECISIONS.adoc (ADR-014 section)", - "META.a2ml [[adr]] ADR-011 (real modules / qualified paths)", - "META.a2ml [[adr]] ADR-012 (grammar changes are correctness assertions)", -] - -[[adr]] -id = "ADR-015" -status = "accepted" -date = "2026-05-19" -title = "WASI Preview2 / WASM Component-Model migration (INT-03)" -context = """ -lib/wasi_runtime.ml emits only `wasi_snapshot_preview1.fd_write` to -stdout (fd 1). There is no file, socket, environment, clock, or argv -access, so there is no server-side runtime profile (INT-06 is blocked) -and no real host I/O. INT-03 (#180) is the substrate fix. The owner was -asked (2026-05-19, AskUserQuestion one-way-door fork) to choose between -(a) expanding the preview1 import surface, (b) preview1 + an external -preview1->preview2 adapter as the run path, or (c) a full re-target to -the WebAssembly Component Model with WASI 0.2 (preview2). The owner -chose (c) — explicitly the largest, one-way, highest-blast-radius path. -""" -decision = """ -Re-target the WASM output to the WebAssembly Component Model with WASI -0.2 (preview2) WIT worlds. Binding constraints: - -- STAGED, non-breaking per slice; each slice is its own PR behind the - full gate. The legacy core-wasm + preview1 stdout path stays the - DEFAULT until the final slice flips it, so the migration is - reversible while in progress even though the end-state is one-way. -- The `affinescript.ownership` custom section is a MULTI-PRODUCER ABI - (shared with hyperpolymath/ephapax; the typed-wasm contract carrier, - see docs/ECOSYSTEM.adoc). It MUST survive verbatim onto the - component's embedded core module. Its format MUST NOT change here; - any change is coordinated in hyperpolymath/typed-wasm, never made - unilaterally for this migration. -- Only the `wasm`/`wasm-gc` target re-points. The 22+ source-to-source - targets (Deno-ESM, Node-CJS, Julia, …) are unaffected. -- WIT world of record: `wit/affinescript.wit` — world - `affinescript:cli/run` importing the `wasi:cli`, `wasi:clocks`, - `wasi:filesystem`, and `wasi:sockets` 0.2 interfaces. - -Staged plan (ledger INT-03; each row = one gated PR): -- S1 (this PR): ADR-015 + WIT world + staged plan + tooling-prerequisite - + roadmap truthing. No codegen change. -- S2: toolchain provisioning — `wasm-tools` + `wasm-component-ld` (and - `wac`) into guix.scm/flake.nix. ABSENT in the current toolchain ⇒ a - HARD GATE on S3+: a component cannot be built or tested without them. -- S3: componentize on-ramp — codegen still emits core wasm; a - post-codegen step wraps it with the standard preview1->preview2 - adapter into a component; ownership section survival asserted; - wasmtime component-run smoke. Codegen unchanged ⇒ reversible. -- S4 (DONE, slices S4a clock + S4b env_count/arg_count): on-demand - preview1 imports (`wasi_snapshot_preview1.clock_time_get`, - `environ_sizes_get`, `args_sizes_get`) via the Effect_sites - pre-scan, canonical-order indexing through - `ctx.wasi_func_indices`. NOTE divergence from this ADR's original - S4 wording ("native `wasi:clocks` + environment + argv via - preview2"): the slice shipped as preview1-shim, NOT native - preview2 calls. This is sufficient under the component path - because both adapters bridge the preview1 imports to - `wasi:clocks` / `wasi:cli` internally; switching the codegen - to emit `wasi:clocks/monotonic-clock@0.2.x.now()` directly only - pays off once preview2 is the default (deferred into S6c). The - ADR is amended here to record what actually shipped rather than - pretending S4 fulfilled the original wording. String accessors - (env_at/arg_at) need byte-level wasm IR (I32Load8U/I32Store8) for - buffer marshalling and remain a tracked follow-up. -- S5: `wasi:filesystem` (open/read/write/close) — unblocks INT-06. -- S6a (WIT export lifting, DONE): codegen emits a `_start : () -> ()` - shim that calls `main` and drops its i32 result whenever a - parameter-less `fn main()` is present (`lib/codegen.ml`); - `tools/componentize.sh --command` wraps with the fetch-pinned - preview1->preview2 *command* adapter (wasmtime v44.0.1, sha256 - 8ff2ea78...) producing a component that exports `wasi:cli/run@0.2.x` - per `wit/affinescript.wit`. End-to-end gate: - `tests/componentize/command_smoke.sh` proves `wasmtime run` invokes - the component (exit 0), the ownership section survives, and the - lift is asserted. Purely additive: reactor consumers + game-loop - hooks are byte-unchanged. -- S6b (sockets on-ramp, DONE): new `net_shutdown(fd, how) -> Int` - builtin (Effect `Net`, reserved) lowers to - `wasi_snapshot_preview1.sock_shutdown` via the same on-demand - canonical-order pattern as S4a/S4b (`optional_wasi` entry 4 in - `lib/codegen.ml`). The command adapter bridges to - `wasi:sockets/tcp` internally without surfacing a host-side - `wasi:sockets/*` requirement; `tests/componentize/sockets_smoke.sh` - gates the lift + a clock+env+sock combo for canonical-order - regression. Real-host invoke under wasmtime exits 0 on - `net_shutdown(stdin, RDWR)` (ENOTSOCK dropped). Larger socket - primitives (recv/send/accept) need byte-level wasm IR for buffer - / network-address marshalling and remain a tracked follow-up - alongside env_at/arg_at. -- S6c: flip the default wasm target to component and demote the - preview1 stdout path to a named legacy target. Bundled cleanup: - replace the on-demand preview1 shims emitted by S4a/S4b/S6b - (clock_time_get, environ_sizes_get, args_sizes_get, - sock_shutdown) with native preview2 calls - (`wasi:clocks/monotonic-clock`, `wasi:cli/environment`, - `wasi:sockets/tcp`) — this only pays off once preview2 is the - default, hence its placement here rather than as a standalone - pre-flip slice. -""" -consequences = """ -- End-state is one-way (the component model becomes the canonical wasm - target) but reversible-in-progress: preview1 retained as the default - through S6b; the flip happens at S6c. -- S3..S6 are HARD-GATED on S2 (toolchain). Tracked as a follow-up - issue; this is disclosed, not hidden. -- typed-wasm ownership-section ABI is unchanged; the coordination - obligation with hyperpolymath/ephapax + hyperpolymath/typed-wasm is - recorded and binding. -- Source-to-source targets are untouched; no estate consumer churn from - this migration until S6, and even then only wasm-target consumers. -- This decision is settled; do not reopen without amending this ADR. - Ledger INT-03 in docs/TECH-DEBT.adoc; roadmap in docs/ECOSYSTEM.adoc. -""" -references = [ - "https://github.com/hyperpolymath/affinescript/issues/180", - "https://github.com/hyperpolymath/affinescript/issues/160", - "https://github.com/hyperpolymath/affinescript/issues/225", - "wit/affinescript.wit", - "lib/wasi_runtime.ml", - "lib/codegen.ml / lib/codegen_node.ml (wasm target)", - "docs/specs/SETTLED-DECISIONS.adoc (ADR-015 section)", - "docs/ECOSYSTEM.adoc (AffineScript <-> typed-wasm contract; INT-03)", - "hyperpolymath/typed-wasm (multi-producer ownership-section ABI)", - "WASI 0.2 / WebAssembly Component Model specification", -] - -[[adr]] -id = "ADR-016" -status = "accepted" -date = "2026-05-19" -title = "Effect-threaded async-boundary detection (side-table; #234)" -context = """ -The WasmGC CPS transform (ADR-013) detects the async boundary -*structurally*: a `let` whose RHS calls a primitive in a hardcoded set -`async_primitives = ["http_request_thenable"]` (lib/codegen.ml). This -was the deliberately-contained #225 PR3a scope. It is brittle: every -new async stdlib primitive must be added to the set, and a -user-defined `Async`-effecting function is invisible to it — blocking -the fully transparent ADR-013 surface for user async code. #234 asks -for the principled generalisation: the boundary is any call whose -effect row ⊇ `Async`. This requires per-call effect information at -codegen time. The AST (`lib/ast.ml`) carries no location or node id on -`ExprApp`, and the owner-chosen mechanism (AskUserQuestion -2026-05-19) is a typecheck→codegen side-table, NOT AST annotation. -""" -decision = """ -Thread per-call-site effect rows from typecheck to codegen via a -*side-table*, keyed by a deterministic shared call-site numbering. - -- *Keying.* A single shared traversal (new `lib/effect_sites.ml`) - assigns every `ExprApp` an ordinal in a fixed pre-order walk of the - program. Both typecheck and codegen obtain ordinals by calling the - *same* function, so keys cannot drift. No AST shape change (the - pipeline already threads one `prog` value through resolve → - typecheck → borrow → quantity → codegen unmodified, but ordinals — - not physical identity — are the contract, which is robust to any - future node rebuild). -- *Producer.* `Typecheck.check_program` populates - `(int, effect_row) Hashtbl.t` (call-site ordinal → resolved effect - row, declared *and* inferred) and returns it in its result context. -- *Pipeline.* `bin/main.ml` threads the table into the codegen entry - alongside `prog` (one extra argument; the existing source-to-source - backends ignore it). -- *Consumer.* The CPS detector's boundary predicate becomes “the - `let`-RHS call's effect row ⊇ `Async`” via a table lookup, replacing - `is_async_prim_call`/`async_primitives`. -- *Fallback / safety.* If the table has no entry for a site (e.g. a - pre-typecheck embedder path, or a synthesised node), codegen falls - back to the structural recogniser. The hardcoded set is retired only - once the table path is proven (final slice); over-conservative - fallback is always sound (= today's behaviour). - -Staged (ledger #234; each a gated PR, full `dune test --force` + -wasm e2e): -- S1 (this): ADR-016 + plan. No code change. -- S2: `lib/effect_sites.ml` shared numbering + typecheck builds & - returns the side-table. NO codegen behaviour change (table built, - unused) — pure, gate-neutral. -- S3: pipeline threads the table; codegen boundary predicate switches - to the table with structural fallback; new e2e proving a - *user-defined* `Async` fn triggers the transform (the payoff). All - existing http_cps_* / http_response_reader stay green. -- S4: retire the hardcoded `async_primitives` set (fallback remains - for table-miss only); doc truthing. -""" -consequences = """ -- Generalises to user-defined `Async` functions; new async primitives - need no codegen edit (closes the brittleness #234 names). -- One-way-ish: introduces a typecheck→codegen data channel (shared - infra). Reversible per-slice (S2/S3 are additive; the structural - recogniser stays as the sound fallback throughout). -- Source-to-source backends unaffected (they ignore the table arg). -- This decision is settled; do not reopen without amending this ADR. - Ledger #234 / CORE-02 in docs/TECH-DEBT.adoc. -""" -references = [ - "https://github.com/hyperpolymath/affinescript/issues/234", - "https://github.com/hyperpolymath/affinescript/issues/225", - "docs/specs/async-on-wasm-cps.adoc (ADR-013)", - "lib/codegen.ml (async_primitives / detect_async_base_case)", - "lib/typecheck.ml (check_program)", - "lib/effect_sites.ml (new; shared call-site numbering)", - "docs/specs/SETTLED-DECISIONS.adoc (ADR-016 section)", -] - -[[adr]] -id = "ADR-017" -status = "accepted" -date = "2026-05-19" -title = "ReScript block-module disposition: one module per file (split, do not nest)" -context = """ -ReScript `module Name { … }` block modules have no AffineScript block -form: the grammar (parser.mly:130-134) is a single optional `module -Path;` header, before imports — `module A { }` parse-errors. ADR-011 -settled "real modules": the file IS the module. The estate #229 ports -carry block modules; a SINGLE one per file is mechanical (hoist header, -drop braces, dedent — in the #229 canonical map, verified to parse), but -MULTIPLE block-modules in one file (e.g. standards/lol/.../OpenCyc.affine: -module Config{} module Concepts{} module Types{}…; 14 estate -occurrences) had no clean target. Escalated language-side as ESC-04 -(#262) — the bidirectional-evidence discipline of ADR-014 / #228. -(ADR-016 is effect-threaded async-boundary, #234/#270; ADR-018 is the -no-raw-escape doctrine, #245 — this is ADR-017, sequential.) -""" -decision = """ -One module per file — split, do not nest. Each ReScript `module X { -body }` becomes its own `X.affine` whose first declaration is `module -X;`, body dedented, `use` after the header. A file with N block-modules -is split into N files. The grammar is NOT extended with a block/nested -module form: that contradicts ADR-011 (file = module) and is a major -conflict-risky grammar change for cosmetic gain — the contortion ADR-012 -forbids. No compiler change (the file-header form already parses); this -ADR settles the porting doctrine + the #229 canonical-map structural -rule. Adjacent and explicitly OUT of scope: a split file still hits -Resolve.UndefinedModule until the repo module-path<->file-layout matches -the loader — cross-module graph coherence (INT-02 loader-bridge), -tracked in RESCRIPT-ELIMINATION.adoc Tier-4, never conflated here. -""" -consequences = """ -- The #229 block-module residue (idaptik-dlc-vm, standards/lol, parts of - burble) has a defined target: split-per-file. Full validation still - needs the per-repo module graph (INT-02), tracked separately. -- No language/compiler change; no estate consumer churn from this ADR. -- This decision is settled; do not reopen without amending this ADR. - ESC-04 #262; #229 canonical map in docs/RESCRIPT-ELIMINATION.adoc. -""" -references = [ - "https://github.com/hyperpolymath/affinescript/issues/262", - "https://github.com/hyperpolymath/affinescript/issues/229", - "lib/parser.mly (module_decl; one `module Path;` header per file)", - "docs/specs/SETTLED-DECISIONS.adoc (ADR-017 section)", - "docs/RESCRIPT-ELIMINATION.adoc (#229 canonical map; Tier-4 INT-02)", - "META.a2ml [[adr]] ADR-011 (real modules: file = module)", - "META.a2ml [[adr]] ADR-012 (grammar changes are correctness assertions)", -] - -[[adr]] -id = "ADR-018" -status = "accepted" -date = "2026-05-19" -title = "No raw/FFI escape: typed `extern` is the only host bridge" -context = """ -ReScript `%%raw("")` / `%raw` injects arbitrary untyped -host source. AffineScript's only host bridge is `extern fn` / `extern -type` (parser.mly extern_fn_decl/extern_type_decl, FnExtern body) — -typed, host-supplied, no body, no arbitrary-source escape. 14 estate -`%%raw` occurrences (#229 Tier-3, ESC-01 #245) had no clean target. -Escalated language-side — the bidirectional-evidence discipline of -ADR-014 / #228 and the affine spirit of explicit, typed boundaries. -(ADR-017 is the block-module disposition #262; this is ADR-018 because -main merged ADR-016 = effect-threaded async-boundary, #234.) -""" -decision = """ -No raw escape — there is no `%%raw` analogue, by design. Typed `extern -fn` / `extern type` is the SOLE FFI surface. Every estate `%%raw` ports -to a typed `extern` whose signature states the host contract the raw -blob assumed implicitly; the host implementation moves to the -embedder/runtime shim. AffineScript will NOT gain an untyped -intrinsic/`extern raw` block: an arbitrary-source hole defeats -affine/effect tracking at the boundary where the guarantees matter most -— precisely what the type-and-effect discipline (and ADR-012) exists to -prevent. Doctrine decision: `extern` already exists, no compiler change; -it settles the #229 canonical-map target and the FFI stance. -""" -consequences = """ -- A `%%raw` encoding genuine logic (not just a host call) is a design - smell the port surfaces: re-express as real AffineScript + a typed - `extern` for any true host primitive, never smuggled through. -- `%%raw`-bearing #229 files (idaptik Main/StartupError, parts of - burble) port under this doctrine; per-file execution is #229 per-repo. -- No language/compiler change; no estate consumer churn from this ADR. -- Settled; do not reopen without amending. ESC-01 #245. -""" -references = [ - "https://github.com/hyperpolymath/affinescript/issues/245", - "https://github.com/hyperpolymath/affinescript/issues/229", - "lib/parser.mly (extern_fn_decl; extern_type_decl; FnExtern)", - "docs/specs/SETTLED-DECISIONS.adoc (ADR-018 section)", - "docs/RESCRIPT-ELIMINATION.adoc (#229 canonical map; Tier-3 ESC-01)", - "META.a2ml [[adr]] ADR-012 (grammar changes are correctness assertions)", -] - -[[adr]] -id = "ADR-019" -status = "accepted" -date = "2026-05-19" -title = "Compiler distribution: GitHub Releases binaries + thin Deno/JSR shim (#260)" -context = """ -INT-04 (#181) is "publish compiler + runtime". The runtime JS packages -are JSR-publishable and that path is done (affinescript#261). The -*compiler* is a native OCaml binary — it is NOT a JSR or npm package, -so "publish the compiler" needed a distribution strategy. This gates -INT-10 (`affinescript-lsp` distribution, which "waits on a published -compiler"). The fork was escalated (issue #260; AskUserQuestion -2026-05-19) over four options: (1) GitHub Releases binaries, (2) -Guix/Nix channel, (3) thin JSR/npm shim, (4) Releases + shim. The -owner chose (4). -""" -decision = """ -Dual-channel, Releases-canonical: - -- *Canonical artifact.* Extend the existing `release.yml` (`v*` tags) - to build per-platform compiler binaries (Linux x86_64 first; - macOS/Windows as the build matrix allows) and attach them to the - GitHub Release together with a `SHA256SUMS` manifest. Releases are - the single source of truth; Guix/Nix and any npm tail become - *additive fetch-derivations over the same artifact* later (NOT in - this ADR's slices) — no second producer of the binary. -- *Ergonomic front door.* A thin Deno/JSR package - `@hyperpolymath/affinescript` (Deno-first per CLAUDE.md; no OCaml - source shipped) whose setup step downloads the host-platform binary - from the pinned Release, verifies it against the `SHA256SUMS` - checksum *embedded in that shim version*, caches it, and execs it. - HTTPS-only; no secrets in the shim; provenance = this repo's own - release workflow. Each shim version pins exactly one compiler - version+checksum (supply-chain: no floating fetch). -- *Consumer.* INT-10 `affinescript-lsp` resolves the compiler via the - shim (`deno add` ergonomics) — this is what unblocks it. -- npm tail: only if/when an npm-native consumer needs it, mirroring - the owner-sanctioned affine-vscode npm exception; deferred, not in - scope here. - -Staged (ledger #260 / INT-10; each a gated PR): -- S1 (this): ADR-019 + plan; file INT-10. No code. -- S2: `release.yml` per-platform binary + `SHA256SUMS` matrix. -- S3: the `@hyperpolymath/affinescript` shim package (download + - checksum-verify + cache + exec) + its tests; publish is owner-gated - via the existing manual JSR workflow (like INT-04). -- S4: wire INT-10 `affinescript-lsp` onto the shim. -""" -consequences = """ -- One-way: fixes the install contract (Releases artifact name/layout + - shim package name). Reversible per-slice; Releases-canonical keeps - Guix/Nix/npm strictly additive. -- INT-10 (`affinescript-lsp` distribution) is unblocked once S2+S3 - land; filed now. -- Supply chain: every shim release pins one checksummed binary; no - floating download. -- This decision is settled; do not reopen without amending this ADR. - Ledger #260 / INT-10 in docs/TECH-DEBT.adoc. -""" -references = [ - "https://github.com/hyperpolymath/affinescript/issues/260", - "https://github.com/hyperpolymath/affinescript/issues/181", - ".github/workflows/release.yml", - ".github/workflows/publish-jsr.yml (INT-04; owner-gated publish)", - "docs/PACKAGING.adoc (INT-04; the JS-package half)", - "docs/specs/SETTLED-DECISIONS.adoc (ADR-019 section)", -] - -[[adr]] -id = "ADR-020" -status = "accepted" -date = "2026-05-23" -ratified = "2026-05-24" -title = "Ownership-section schema versioning (0xAF sentinel + u8 version)" -context = """ -The `affinescript.ownership` Wasm custom section has been frozen at -v1 since typed-wasm Level 10 closure (2026-04-19) and is emitted in -identical form by AffineScript (`lib/codegen.ml`) and by ephapax -(`hyperpolymath/ephapax/src/ephapax-wasm`). The Rust verifier in -`hyperpolymath/typed-wasm` consumes it as the cross-compat target. - -V1 has *no version field*. Today this is fine because both -producers are at v1 and the verifier expects v1 unconditionally. -The first multi-producer ABI change — when L1–6 / L14–16 emission -lands — will need version discrimination. Adding the version field -*now*, while we are still at v1, is much cheaper than retrofitting -it under load. - -This ADR is filed alongside the broader typed-wasm widening roadmap -in `docs/specs/TYPED-WASM-ROADMAP.adoc` §"Tranche B". -""" -decision = """ -Move from v1 (unversioned) to v2 (versioned, with sentinel): - - v1 payload: - u32 entry_count - entry* - - v2 payload: - u8 version_tag ; 0xAF — "AffineScript Format" - u8 version ; 0x02 for v2.0 - u32 entry_count - entry* ; same entry shape as v1; no new entry fields in v2.0 - -The 0xAF sentinel is byte-distinct from any plausible v1 entry-count -low byte for a module with ≤ 0xAE entries — i.e. practically every -module in existence. v1 readers fail cleanly on a v2 section (bad -entry-count); v2 readers see the sentinel and dispatch. - -Coordinated landing protocol: - 1. Land ADR-020 in `hyperpolymath/affinescript` (this repo). - 2. Mirror ADR in `hyperpolymath/typed-wasm` (Rust verifier - dispatches on sentinel; accepts both v1 and v2 during - migration). - 3. Mirror ADR in `hyperpolymath/ephapax` (second producer emits - v2). - 4. Verifier ships v2-parse FIRST. Producers stay on v1 emit. - 5. Once verifier is deployed (in both OCaml + Rust paths), - producers flip to v2 emit together. - 6. After a deprecation window, v1-parse path is removed from - verifier. - -The v2.0 entry shape is identical to v1 — this ADR introduces no -new ownership kinds, no new per-entry fields. Future widening -(e.g. v2.1 adding a region-id field per entry) is a separate ADR. -""" -consequences = """ -- Forward-compatible: any future widening (L1–6 / L14–16 carriers, - per-entry annotations) can negotiate via the version byte without - another sentinel-or-no-sentinel war. -- v1 callers fail cleanly on v2 sections (bad entry-count parse) — - no silent corruption. -- Single coordinated landing across three repos; complexity is in - the coordination, not the bytes. -- This decision is PROPOSED. Land or supersede explicitly; do not - silently drift. -- Cross-references the typed-wasm roadmap in - `docs/specs/TYPED-WASM-ROADMAP.adoc` §"B1". -""" -references = [ - "docs/specs/TYPED-WASM-INTERFACE.adoc", - "docs/specs/TYPED-WASM-INTERFACE.a2ml", - "docs/specs/TYPED-WASM-ROADMAP.adoc", - "lib/codegen.ml (build_ownership_section, ~line 159)", - "lib/tw_verify.ml (parse_ownership_section)", - "hyperpolymath/typed-wasm (Rust verifier crate — coordination target)", - "hyperpolymath/ephapax (second producer — coordination target)", -] - -[[adr]] -id = "ADR-021" -status = "accepted" -date = "2026-05-23" -ratified = "2026-05-24" -title = "Multi-producer ABI coordination model for typed-wasm carriers" -context = """ -typed-wasm is a separate, language-agnostic compilation target with -its own repository (`hyperpolymath/typed-wasm`) and its own -producers (today: AffineScript + ephapax; potentially more later). -The `affinescript.ownership` custom section is the first -multi-producer ABI we own. - -Today coordination happens by `Refs #N` across repositories and -good intent. As the typed-wasm widening proposals land (ADR-020 -schema versioning, then carriers for L1–6 and L14–16), this -informal model will break. The session-note record makes clear that -each ABI conversation has been one-off and ad-hoc; the failure mode -is two producers landing incompatible changes in parallel because -nobody owns "the protocol". - -This ADR is filed alongside the typed-wasm widening roadmap in -`docs/specs/TYPED-WASM-ROADMAP.adoc` §"Tranche D" — D1 explicitly. -The reason to land *this* ADR before any concrete widening proposal -is that the coordination model is cheap now (no code), expensive -later (under conflicting pressure from multiple in-flight proposals). -""" -decision = """ -Adopt a formal four-axis multi-producer coordination model: - -(1) Spec authority — which artefact is the source of record: - - Today: `lib/tw_verify.ml` (OCaml) is the spec of record. - - On C5.1 closure (INT-12 / CONV-05): authority flips to - `hyperpolymath/typed-wasm` Rust verifier crate. - - The flip is itself an ADR — does not happen silently. - - Until the flip, OCaml binds; on conflict, OCaml wins. - -(2) Coordinated landing protocol: - - All ABI-touching ADRs live in all three repos (originated in - one, mirrored in the other two with `Mirrors hyperpolymath/X#N` - cross-references). - - Verifier always ships parse-support FIRST. - - Producers flip to emit support together, in a coordinated - window. - - Deprecation: a removed format MUST have a deprecation window - of at least the cycle between two coordinated landings. - -(3) Test parity protocol: - - Every producer maintains a fixture corpus matching the - typed-wasm cross-compat suite (INT-12 / C5.1). - - Each ABI change requires a new fixture in the cross-compat - suite from each producer. - - Verifier accepts a producer's emit only if the cross-compat - corpus is up-to-date for that producer. - -(4) Conflict resolution: - - In-flight ABI proposals from different producers MUST be - serialised — no parallel ABI ADRs. - - The serialisation queue is owned by `hyperpolymath/typed-wasm` - (today via issues; future a dedicated coordination ledger). - - A proposal blocked behind another in the queue is paused, not - refused. -""" -consequences = """ -- Single coordination ledger replaces ad-hoc Refs trails. -- ABI change cost goes up (more paperwork) but predictability goes - way up. -- Compatible with the existing Hypatia / gitbot rules (Hypatia - DOC-FORMAT / STACK-SIGNAL apply; ISSUE-CLOSURE applies to the - shared ADR mirrors). -- This decision is PROPOSED. Owner ratification required before any - ABI ADR (ADR-020 included) lands; ADR-020 is a "first test" of - this model and its own landing exercises the protocol. -- Cross-references the typed-wasm roadmap in - `docs/specs/TYPED-WASM-ROADMAP.adoc` §"D1". -""" -references = [ - "docs/specs/TYPED-WASM-INTERFACE.adoc", - "docs/specs/TYPED-WASM-ROADMAP.adoc", - ".claude/CLAUDE.md §Hypatia and gitbot-fleet standing rules", - "hyperpolymath/typed-wasm (coordination target)", - "hyperpolymath/ephapax (second producer — coordination target)", -] - -[[adr]] -id = "ADR-022" -status = "proposed" -date = "2026-05-26" -title = "Origin (region) variables and a Polonius-style loan solver for the borrow checker" -context = """ -lib/borrow.ml (~1585 LOC, OCaml) is the operational borrow checker. Slices -A, B, C-light landed plus the in-flight ref-to-ref / Slice C' / Slice D -PRs (#395, #396, #397) close out the lexical / operational fragment of -CORE-01 (#177). The remaining residual, called out in lib/borrow.ml's -closing TODO comment (lines 1575-1578), is the architectural shift to -*origin (region) variables*: a region var on each TyRef / TyMut with -subset constraints and a datalog-style loan-live-at-point solver. - -The operational checker is fundamentally lexical: each borrow has a span -where it was created and lives until its containing block exits, with NLL -last-use shortening via merge_arm_results, the ref_bindings graph, and -block-local sym tracking. Lifetime variables do not appear in types — -`&Int` is `TyRef TyInt`, no `'a` parameter. The checker cannot reason -about cross-function borrows that outlive a returned struct field -(callee_owned_params is a name-set heuristic, not a constraint -discharge), true reborrows through indirection (the matching residual -TODO at lib/borrow.ml:1570-1574), or CFG-join soundness beyond what -merge_arm_results handles inline — each new join point (ExprHandle, -ExprTry, ExprMatch) is retro-fitted with snapshot-restore-merge. - -STATE 2026-05-26 names this the dominant single item on the path to 1.0, -~6-12 weeks single-author. -""" -decision = """ -Adopt a Polonius-style origin/region constraint solver written in OCaml -in lib/borrow_polonius/, staged M1-M4 with the lexical checker as the -merge oracle through M3. - -Option survey: -- (a) Keep lexical + patch gaps — rejected; the next two soundness gains - (cross-fn reborrow soundness; multi-iter loop soundness without - spurious re-assignment rejection) are not expressible in the current - shape. -- (b) Custom origin-based solver in OCaml under lib/borrow_polonius/ - using Polonius's published `dlv_naive` shape (subset/3, - loan_live_at/2, loan_invalidated_at/2, error/1). CHOSEN. -- (c) Embed polonius-engine via Rust FFI — rejected; new toolchain dep, - cross-language error reporting, and the estate language policy - reserves Rust for performance-critical systems code with no native - alternative. There is one here (OCaml). - -Surface syntax: FULL ELISION for v1. `&T` / `&mut T` retain their spelling; -origins are introduced implicitly by the elaborator. Function signatures -the user writes today remain accepted; inference attaches and constrains -the origins. A future surface escape hatch (`&'a T`, `where 'a: 'b`) is -NOT introduced here — that is ADR-Future-A, contingent on grammar work -under ADR-012's Menhir-conflict-disclosure rule. - -AST changes (lib/ast.ml): -- `type origin_var = int`, fresh-int identity. -- `TyRef of origin_var option * type_expr` (was `TyRef of type_expr`). -- `TyMut of origin_var option * type_expr` (was `TyMut of type_expr`). -- TyOwn unchanged. - -borrow.ml changes: -- `type borrow` gains `b_origin : origin_var`. -- M1 defaults all sites to a single shared `default_origin = 0` ⇒ - semantically identical to today's lexical checker. - -Datalog shape: -- borrow_at(L, P) input -- loan_origin(L, O) input -- subset(O1, O2, P) input -- killed(L, P) input -- cfg_edge(P1, P2) input -- loan_live_at(L, P) derived (LFP over cfg_edge + killed) -- loan_invalidated_at(L, P) derived (loan_live_at ∩ access-conflict) -- error(P) derived (any invalidated) - -Subset constraints emitted at: let-binding of a ref RHS, assignment to -a ref binder, function call sites (formal-actual origin flow). Solver = -naive bottom-up worklist; complexity O((P × L) + (E × L)) per iteration; -adequate for the AffineScript corpus. Migrate to Polonius opt_naive only -if a corpus turns pathological. - -Migration plan (each milestone = own PR, all behind full dune runtest gate): -- M1: AST adds `origin_var option` field defaulted to None; b_origin - on borrow defaults to shared id 0. lib/borrow_polonius/types.ml stub - created, NOT wired into bin/main.ml. No behaviour change. -- M2: Elaborator emits fresh origins at each borrow site as - loan_origin facts; no subset constraints yet. No behaviour change. -- M3: Subset constraints from let/assign/call; solve.ml implements the - fixpoint; bin/main.ml runs BOTH checkers and diffs; new soundness-gain - fixtures land. CI gate: zero divergence on existing corpus. -- M4: Solver-only; lib/borrow.ml shrinks ~30-40% as ref_bindings / - block_local_syms / callee_owned_params / merge_arm_results retire. - CAPABILITY-MATRIX flips CORE-01 to fully closed; the residual TODO - comment is deleted. - -Test surface: each milestone keeps `dune runtest --force` green. M3 lands -≥5 new negative fixtures (programs Polonius rejects that lexical accepts — -the soundness gain). M4 lands ≥10 fixtures across both axes (Polonius -accepts what lexical spuriously rejects; Polonius catches what lexical -missed). - -This ADR records the SHAPE. It is opened as Draft pending owner -ratification of the shape before any code-changing PR follows. On -ratification, M1-M4 land as separate PRs. -""" -consequences = """ -- Closes the last named base-language soundness gap (CORE-01 residual - Slice C-full) → ROADMAP Phase 2 unblocked, Phase 3 (1.0) unblocked. -- lib/borrow.ml shrinks; each new join construct stops requiring a - bespoke retrofit. -- The reborrow-through-indirection residual TODO is discharged by M3 as - a side-effect — subset constraints chain reborrows naturally. -- Future surface origins (Rust-style `&'a T`) become additive — the type - system already carries the variable, the syntax PR exposes it. -- Multi-week effort (~6-12 weeks single-author per STATE estimate). - Smaller CORE-01 slices, INT-03, and CONV-04 can proceed in parallel. -- The OCaml solver carries proof-debt of its own (fixpoint must be - sound). M3's parallel-run diffing is the empirical correctness gate; - no mechanised solver proof is attempted here. -- lib/ast.ml shape change ripples through every pass that destructures - TyRef / TyMut. Mitigation: M1's `option` keeps case-pattern surface - compatible with one-liner `_ ->` wildcards in passes that don't yet - care. -- No new build-time dependency. No new surface syntax. The - affinescript.ownership typed-wasm carrier section is unaffected — - origins are internal to the borrow checker, never serialised. -- This decision is PROPOSED. Owner ratification required before M1 - lands. Once ratified, this ADR is settled; do not reopen without - amending it. -""" -references = [ - "https://github.com/hyperpolymath/affinescript/issues/177", - "lib/borrow.ml:1569-1584 (residual TODO comment)", - "lib/borrow.ml:1570-1574 (matching reborrow-through-indirection TODO)", - "lib/ast.ml (TyRef/TyMut/TyOwn definitions, lines 60-62)", - "docs/decisions/0022-polonius-origin-variables.adoc (long-form ADR)", - "docs/STATE-2026-05-26.adoc §Dominant cost item / §Critical path", - "docs/CAPABILITY-MATRIX.adoc (CORE-01 row)", - "docs/specs/SETTLED-DECISIONS.adoc (section pending on ratification)", - "ADR-012 (grammar changes are correctness assertions — gates future surface-origin ADR)", - "Niko Matsakis, \"An alias-based formulation of the borrow checker\", 2018 (subset/3 + loan_live_at/2 shape)", -] diff --git a/.machine_readable/6a2/NEUROSYM.a2ml b/.machine_readable/6a2/NEUROSYM.a2ml deleted file mode 100644 index e1d34c09..00000000 --- a/.machine_readable/6a2/NEUROSYM.a2ml +++ /dev/null @@ -1,21 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) -# -# NEUROSYM.a2ml — Neurosymbolic integration metadata -[metadata] -version = "0.1.0" -last-updated = "2026-04-11" - -[hypatia-config] -scan-enabled = true -scan-depth = "standard" # quick | standard | deep -report-format = "logtalk" - -[symbolic-rules] -# Custom symbolic rules for this project -# - { name = "no-unsafe-ffi", pattern = "believe_me|unsafeCoerce", severity = "critical" } - -[neural-config] -# Neural pattern detection settings -# confidence-threshold = 0.85 -# model = "hypatia-v2" diff --git a/.machine_readable/6a2/PLAYBOOK.a2ml b/.machine_readable/6a2/PLAYBOOK.a2ml deleted file mode 100644 index 06ede86c..00000000 --- a/.machine_readable/6a2/PLAYBOOK.a2ml +++ /dev/null @@ -1,74 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) -# -# PLAYBOOK.a2ml — Operational playbook -[metadata] -version = "0.1.0" -last-updated = "2026-05-26" - -[deployment] -# method = "gitops" # gitops | manual | ci-triggered -# target = "container" # container | binary | library | wasm - -[incident-response] -# 1. Check .machine_readable/STATE.a2ml for current status -# 2. Review recent commits and CI results -# 3. Run `just validate` to check compliance -# 4. Run `just security` to audit for vulnerabilities - -[release-process] -# 1. Update version in STATE.a2ml, META.a2ml -# 2. Run `just release-preflight` (validate + quality + security + maint-hard-pass) -# 3. Tag and push - -[maintenance-operations] -# Baseline audit: just maint-audit -# Hard release gate: just maint-hard-pass - -[npm-publish] -# Runbook for publishing a @hyperpolymath/* package to npm (lineage from -# issue #104, first applied to @hyperpolymath/affine-vscode@0.1.0 -# on 2026-05-26). -# -# Prerequisites (one-time per estate, completed 2026-05-26): -# * npm org `hyperpolymath` exists on registry.npmjs.org (free public tier) -# * Repo secret NPM_TOKEN holds an automation token with read+write -# access to the @hyperpolymath/* scope -# -# Per-package workflow: -# 1. packages//package.json -# "name": "@hyperpolymath/" -# "version": "X.Y.Z" -# "license": "MPL-2.0" -# "publishConfig": { "access": "public" } -# "repository": { "type": "git", "url": "...", "directory": "packages/" } -# "files": [...] # explicit allowlist; never publish the whole tree -# 2. .github/workflows/-publish.yml -# on push tag '-v*' -# - verify GITHUB_REF_NAME (minus '-v') == package.json version -# - write ${HOME}/.npmrc from secrets.NPM_TOKEN, then npm publish --access public -# - rm -f ${HOME}/.npmrc (always) -# Mirror affine-vscode-publish.yml's shape. -# 3. Tag push (signed, at origin/main): -# git tag -s -vX.Y.Z origin/main -m " X.Y.Z — npm publish (Refs #N)" -# git push origin -vX.Y.Z -# 4. Verify: npm view @hyperpolymath/@X.Y.Z -# -# Tag naming: distinct prefix per package so they never collide with the -# OCaml compiler's `v*` Release-workflow tags. e.g. affine-vscode-v*. -# -# Token rotation: rotate NPM_TOKEN after any wire-up where the token -# value transited a session transcript. - -[ci-required-checks] -# A required status check must be emitted UNCONDITIONALLY on every PR that can -# target the protected branch. Otherwise the unproduced context sits forever at -# "Expected — Waiting for status to be reported" — a silent merge block. -# Causes: (1) a producing workflow with an on.pull_request.branches filter; -# (2) a pin naming a renamed/migrated job (e.g. a standards reusable job name); -# (3) an external GitHub App check that doesn't post on every PR. -# Diagnosed + fixed 2026-06-21 (#645). Human doc: docs/ci/required-checks.adoc. -rule = "required-check implies emitted on every PR base (no pull_request.branches filter)" -diagnosed = "2026-06-21" -reference = "docs/ci/required-checks.adoc" -tracking-issue = "650" diff --git a/.machine_readable/6a2/STATE.a2ml b/.machine_readable/6a2/STATE.a2ml deleted file mode 100644 index f52ebf21..00000000 --- a/.machine_readable/6a2/STATE.a2ml +++ /dev/null @@ -1,258 +0,0 @@ -# SPDX-License-Identifier: MPL-2.0 -# STATE.a2ml — Project state checkpoint - -[metadata] -project = "affinescript" -version = "0.1.1" -last-updated = "2026-06-21" -status = "active" -authoritative-status-doc = "docs/CAPABILITY-MATRIX.adoc" -drift-flag = "STALE as of 2026-05-23 PM: this file's [components]/[features]/[project-context] still predate landed PRs since 2026-05-19. It MIRRORS, it does not LEAD. Authoritative sources by topic — readiness: docs/CAPABILITY-MATRIX.adoc; spine + AS↔typed-wasm contract: docs/ECOSYSTEM.adoc; coordination ledger / critical path: docs/TECH-DEBT.adoc; test taxonomy + PR-level gates: docs/standards/TESTING.adoc (added 2026-05-23); panic-attack SOP: docs/standards/PANIC-ATTACK.adoc (added 2026-05-23). Gate baseline: CAPABILITY-MATRIX records 260/260 at 2026-05-19 reconstruction; subsequent borrow-checker work has lifted it (#240 → 263, return-escape → 271/274, &mut surface → 278/281). The exact live number for any given commit comes from `dune runtest --force` — do not hard-code it here. (DOC-05, issue #176.)" -session-note-2026-06-21 = "SESSION: release-binaries pipeline repair + downstream gap triage + onboarding polish. (1) RELEASE — PR #641 (merged b8ba479) fixed .github/workflows/release.yml, which had been shipping v0.2.0 SOURCE-ONLY (regression from v0.1.1). Two bugs, both confirmed from the failed v0.2.0 run 26694097435: (a) immutable releases now forbid adding assets to a PUBLISHED release (linux leg died HTTP 422), and (b) the v0.2.0-added version-bake step used GNU `sed -i` which BSD/macOS sed rejects (extra characters at the end of l command), so both mac legs failed before building. Fix: create the release as a DRAFT, upload all binaries + SHA256SUMS into it, then publish (--draft=false --latest) last so it seals atomically; portable `sed -i.bak`. Compiler verified to build in release mode locally; --version/check/eval/compile work. v0.2.0 is immutable+assetless and cannot be amended, so binaries ship via a fresh v0.2.1 tag — OWNER action: a Claude-Code-web session cannot push v* tags (HTTP 403). Tracked in #646 (incl. downstream quandledb re-pin). (2) WASM cross-module constructor-link task was ALREADY LANDED before this session: #602 / ac98c81 (Closes #138) — register_imported_types in Codegen.gen_imports registers imported public enum constructors into variant_tags via the local gen_decl path; core-Wasm regression test in test_stdlib_aot.ml; verified locally 534/534. Downstream stapeln re-pins to ac98c81 and drops its carried git apply. (3) svalinn-gateway compiler gaps confirmed + filed: #642 (compile has no module search-path / project mode for nested src/**), #643 (float_to_string raises UnboundVariable at core-Wasm compile; wired only in resolve/typecheck/interp/Deno), #644 (empty match-arm body Pat => {} is a parse error). `pub extern fn` was checked and is SUPPORTED — not a gap. (4) ONBOARDING POLISH (this change): added tutorial lesson-01 (the docs/tutorial track previously started at lesson-02), an audience router (developers / maintainers / end-users) in README + NAVIGATION, and this note. Docs consolidation (two overlapping lesson tracks, wiki sync) tracked in #647. Note: guides/lessons/01-hello bare-println example is stale (parse error on current compiler) — folded into #647. This file MIRRORS; authoritative sources unchanged (see drift-flag)." -session-note-2026-05-26-publish-104 = "ISSUE #104 CLOSED — FIRST NPM PUBLISH LANDED. @hyperpolymath/affine-vscode@0.1.0 is now on registry.npmjs.org. Owner-action sequence completed today: (1) npm org `hyperpolymath` created on free public-package tier; (2) Granular Access Token generated for scope @hyperpolymath/* with Read+Write, uploaded to repo secret NPM_TOKEN; (3) signed annotated tag affine-vscode-v0.1.0 pushed at origin/main (RSA key 9639451754496E51D6B537CAD119017EBF695AB1); (4) .github/workflows/affine-vscode-publish.yml ran green — `npm publish --access public` succeeded; (5) `npm view @hyperpolymath/affine-vscode` resolves. Downstream consumers (my-lang#66, standards#160) which un-vendored their adapters on 2026-05-21 now have a working `npm install` path; vscode-smoke workflow (skipped per #381 while package was unpublished) will start exercising the live package on its next PR run. Lineage for future @hyperpolymath/* publishes: org+token are reusable; mirror `affine-vscode-publish.yml`'s shape (tag trigger, version-match guard, .npmrc write from secret, npm publish --access public). NPM_TOKEN rotation advised post-publish (token value transited a session transcript during wire-up); see .machine_readable/6a2/PLAYBOOK.a2ml [npm-publish] for the runbook." -session-note-2026-05-30 = "DOC-16 + DOC-17 — DOC-TRUTHING MONITOR FULLY MECHANICAL (issue #176). Two complementary in-repo guards now enforce the doc-truthing rules that were previously external-bot-only. DOC-16 (PR #476, tools/check-doc-truthing.sh): banner-PRESENCE invariant — fails if any over-claiming doc loses its CAPABILITY-MATRIX banner, if the matrix stops self-declaring primacy / loses its 'What AffineScript is NOT' section, or if STATE.a2ml drops its mirror keys; deliberately does NOT phrase-scan (a naive grep false-positives on the negating banners + future-roadmap text). DOC-17 (PR #475, tools/check-doc-overclaims.sh + tools/doc-overclaims.allow): the phrase-detection complement — a frozen-baseline RATCHET over README + docs/** (excluding CAPABILITY-MATRIX + TECH-DEBT, which quote the rule) that fails any NEW backend-breadth / 'production-ready' / stdlib-% occurrence beyond the 13-line baseline (all current entries legit: future-tense roadmap milestones + dated history snapshots + corrective banners); re-baseline via `--update` / `just doc-overclaims-bless`, diff = audit trail. Both wired into `just check` (guard recipe) + ci.yml build job, both toolchain-free bash (run without OCaml). #475 was originally a superset of #476 (built in parallel, same session); on discovering #476 had merged first, #475 was reworked to drop the duplicated banner check and keep only the unique ratchet, renamed check-doc-truth.sh→check-doc-overclaims.sh to avoid the name clash (DOC-DEDUP-clean). FOLLOW-UP (same day, post-#475-merge): the two near-identically-named guards were consolidated so neither lingers — the ratchet (incl. `--update` mode + tools/doc-overclaims.allow baseline) was folded INTO tools/check-doc-truthing.sh using #476's fail-accumulator/note() structure, and tools/check-doc-overclaims.sh was deleted. A SINGLE toolchain-free guard now enforces both presence invariants (DOC-04/05) and the over-claim ratchet (DOC-08/09) in one run; one CI step; the `guard` recipe runs it; re-baseline via `just doc-truth-bless`. DOC-17 folded into DOC-16 in the ledger. Gate-number policy unchanged (DOC-05): live `dune runtest --force` count never hard-coded. No compiler code touched. Refs #176 / #175." -session-note-2026-05-31-partial-port-488 = "RES-TO-AFFINE PARTIAL-PORT MODE #488 SLICES 1-3 (successor to closed #57). New --partial flag, a DISTINCT model from --translate: renders module-top-level functions (let f = (params) => body) into AffineScript fn skeletons whose output DELIBERATELY does not type-check (un-annotated ReScript fns can't yield a compilable fn) but DOES parse — un-inferable types are `_` holes, un-translatable expr/pattern are () /* TODO */ / _ /* TODO */ islands. (1) Slice 1 (#494 / c157a0f): fn skeletons + switch->match (variant/tuple/literal patterns) + expression translation (literals / idents / calls / binary-ops with float-op +.->+ and identity-equality ===->== normalisation / ++ concat / member + module-qualified access / ternary). The binary operator is an anonymous tree-sitter token, sliced from source between the operands. (2) Slice 2 (#495 / 78906f9): pipe-first -> desugaring (a->f(b) -> f(a, b); chains are left-nested so x->f->g(2) -> g(f(x), 2) falls out of the recursion); if/else; blocks with let statements. (3) Slice 3 (#496 / 4d4d1d4): array literals ([a, b] -> [a, b]) + record literals ({x, y} -> Rec #{ x: x, y: y }; AffineScript records are NOMINAL so an anonymous record gets the placeholder type Rec for the human to rename; field punning {x} -> x: x). Walker-internal: translate_expr / translate_switch / translate_pipe / translate_if / translate_as_block / translate_block(_inner) / translate_block_let (mutually recursive) + translate_pattern + partial_function + collect_partial + Walker.translate_partial; emitter emit_partial; main --partial (precedence over --translate, walker-only). 32 res-to-affine walker tests. VERIFIED LOCALLY each slice (apt-bootstrapped toolchain): full dune build exit 0; dune runtest green; main.exe check on the generated skeletons reaches resolution/type-checking WITHOUT a parse error (the partial-port bar — type/resolution errors are expected). REMAINING #488: JS objects / interpolated template strings, labelled-arg refinement, combining --partial with --translate (declarations + functions in one pass), and module-qualified-reference RESOLUTION (a ReScript->AffineScript module-mapping POLICY decision, deliberately not built without owner scoping). #488 stays OPEN. Refs #488 / #57 (closed)." -session-note-2026-05-31-migration-phase3-slice3-and-closure = "MIGRATION-ASSISTANT PHASE 3 SLICE 3 + #57 CLOSURE + SUCCESSOR #488. (1) PR #484 (squash ea8bef5): res-to-affine --translate extended to module-level literal value bindings — let = lowered to a typed AffineScript const name: T = value; for int/float/string/bool (AffineScript has no module-level let; a top-level value binding is const, annotation + semicolon required). New walker.ml helpers classify_number / translate_literal / translate_let_const + a let_declaration branch in collect_translations. Conservative: call bodies, ref(...) mutable-globals, destructuring patterns, and exotic numbers (hex/octal/binary/signed/scientific/underscored) are skipped — number forms limited to plain decimal int and D+.D+ float. New fixture test/fixtures/phase3c.res + 4 gated walker tests (walker-phase3c-let-const). (2) VERIFIED LOCALLY (apt-bootstrapped toolchain): full dune build exit 0; 22 res-to-affine walker tests green against pinned grammar 990214a; compiler oracle main.exe check on the generated consts returns Type checking passed. (3) #57 CLOSED as completed (owner-directed) — the declaration-translation goal is delivered across slices 1 (#477 / 2763909) / 2 (#481 / 3484010) / 3 (#484 / ea8bef5) plus the session-start toolchain hook (#482 / 138baac); --translate now renders every self-contained top-level declaration (type aliases, sums, structs, generics, literal consts) to compiler-verified AffineScript. (4) SUCCESSOR #488 filed: res-to-affine partial-port mode (switch->match / function translation) + module-qualified-reference resolution — both OUT of the every-emitted-form-type-checks model by construction (switch->match is an expression needing whole-function translation of usually-un-annotated ReScript fns, which AffineScript fn signatures can't type-check; qualified refs parse post-#228 but won't resolve without a module-mapping story). #488 is a child of estate migration epic #406; remaining migration work continues there. Refs #57 (closed) / #488 / #406." -session-note-2026-05-30-migration-phase3 = "MIGRATION-ASSISTANT PHASE 3 SLICES 1+2 (issue #57) + SESSION-START HOOK. (1) res-to-affine --translate now turns fully-structural ReScript type declarations into compiler-verified AffineScript; two slices landed off main. Slice 1 (PR #477, squash 2763909): primitive type aliases (type userId = int -> type UserId = Int) and simple sum types (type color = Red | Green -> type Color = | Red | Green; payloads mapped, Circle(float) -> Circle(Float)). Slice 2 (PR #481, squash 3484010): record types -> AffineScript struct (type point = {x: int} -> struct Point { x: Int }) and GENERICS — ReScript type params 'a mapped to AffineScript [A], threaded through aliases, sums, and records (param refs in bodies map 'a -> A). (2) New code is walker-internal (tools/res-to-affine/walker.ml): translate_type_ref / translate_variant / translate_record_fields / affine_type_param / extract_type_params / translate_type_binding / collect_translations, plus Walker.translate and Emitter.emit_translation; CLI gains --translate (walker-only; no-op under --engine=scanner). Detection keys on tree-sitter node TYPE not field labels, robust to the grammar's optional alias/body-field ambiguity. (3) CONSERVATIVE BY CONSTRUCTION — a decl translates only when every part is representable; qualified-path RHS, non-primitive/opaque refs, nested generics (array), object types, GADT returns, variant spreads, and records with mutable or optional-? fields are SKIPPED (left in the marker block + quoted original), never mis-translated. Two normalisations keep output referenceable: lower-case type names capitalised (color->Color) and type vars mapped ('a->A), because lib/parser.mly reads a lower-case name in type position as a TyVar not a TyCon. (4) QUALIFIED-PATH RHS DEFERRED: Belt.Map.t now PARSES (the #228 grammar gap closed — parser.mly:515 wires qualified_type_name into type_expr_primary) but Belt::Map::T would not RESOLVE against a non-existent target module, which would break the every-translated-form-type-checks guarantee; it waits for a module-mapping story. Remaining Phase-3 slices: let->const for literal bindings, switch->match (needs body translation), qualified-path resolution. #57 stays OPEN. (5) VERIFICATION: unlike earlier res-to-affine work this was built+tested locally — the OCaml toolchain was apt-bootstrapped (opam.ocaml.org is network-blocked here, but Ubuntu packages match the project pins: dune 3.14 / cmdliner 1.2 / alcotest 1.7 / menhir 20231231 / sedlex 3.2 / ppx_deriving 5.2 / ppxlib 0.32 / yojson 2.1 / js_of_ocaml 5.6). Full dune build exit 0; dune runtest green (21 res-to-affine cases incl. gated walker tests against pinned grammar 990214a); and the COMPILER ORACLE (main.exe check, the #228 method) returns Type checking passed on every generated form. (6) CONFIG: added .claude/hooks/session-start.sh (SessionStart hook) registered in .claude/settings.json so future web sessions auto-provision this exact apt toolchain + tree-sitter CLI + pinned grammar — synchronous, idempotent, best-effort, web-only (CLAUDE_CODE_REMOTE guard); settings.json also gains dune/tree-sitter build+test permissions. (7) Both #477 and #481 were admin-merged while CI runners were backlogged; #481 was fully locally verified before merge. Refs #57 / #228." -session-note-2026-05-26 = "MIGRATION-ASSISTANT PHASE 2C + REPO-TIDY STACK (T-1..T-7) + STDLIB BLOCKER CLOSURES. (1) PR #357 — feat(res-to-affine) Phase 2c on branch claude/epic-gauss-Mbi0E: tree-sitter walker extended from #322's single Side_effect_import detector to all six anti-patterns. New detectors in tools/res-to-affine/walker.ml — detect_raw_js (any extension_expression node), detect_untyped_exception (try_expression / call to value_identifier \"raise\" / member_expression starting with Js.Exn or ending with Promise.catch), detect_mutable_global (top-level let_declaration whose body is call to value_identifier \"ref\", OR top-level mutation_expression), detect_inline_callback_record (>=3 inline function values in a record literal or a call_expression's arguments list — handles direct function children + labeled_argument + record_field wrappers), detect_oversized_function (function node whose stop.row - start.row + 1 > 50). Module-toplevel predicate refactored to a single at_module_toplevel helper that walks the ancestor chain refusing on `function` or `let_binding` body. Findings deduped by (kind, line) so the AST walker doesn't emit more bullets than the line-based scanner. CLI flipped --engine=walker to the default in tools/res-to-affine/main.ml (scanner remains as fallback when grammar / tree-sitter CLI missing — pre-existing graceful-fallback path from #322 unchanged). Scanner.kind extended with Inline_callback_record + Oversized_function variants; scanner.ml gives them labels + guidance. New fixture test/fixtures/phase2c.res exercises the two walker-only kinds. test_walker.ml grew per-kind tests under three new suites (walker-side-effect-import / walker-phase2c-parity / walker-phase2c-new-kinds). README + walker.mli updated. (2) Phase 2c CI fix push (e7a3a44): initial Phase 2c commit had build+lint failing; defensive rewrite replaced the labeled-only `mk_finding ~kind:K ~line:L ~excerpt:E :: acc` emit pattern with explicit `let finding : Scanner.finding = { ... } in finding :: acc` matching Phase 2b's working style, plus replaced non-ASCII glyphs in comments (≥ → >=, … → ...) defensively. (3) DIAGNOSED: `main` itself is red. PR #359 (T-1, pure file-rename + delete + one-line CONTRIBUTING.md edit, zero OCaml touched) also fails on build+lint. That conclusively shows the build failure is inherited from main, not introduced by Phase 2c or any tidy work. Matches CLAUDE.md §\"CI signal reliability\" — auto-merge fires even when build is red; the historical pattern of #334/#335/#336/#344 landing red applies. Root cause on main is NOT diagnosed in this session (needs the actual `dune build` log; WebFetch on the actions UI returns React skeletons, not log content; container has no OCaml toolchain to repro locally). Filed implicit follow-up: someone with shell access should run `gh run view --log-failed ` against any of the recent failing runs to identify the underlying lib/ compile error. (4) REPO-TIDY STACK (T-1..T-7) — six small PRs landed off origin/main, each reviewable in isolation, none touching .ml/.mli, all inheriting the same baseline build/lint failure as PR #359 (proof above). T-1 PR #359 *MERGED* — AI.a2ml → 0-AI-MANIFEST.a2ml (Hypatia root_hygiene rule + sibling-repo convention), delete AI.djot (superseded), docs/TECH-DEBT-alt.adoc → docs/TECH-DEBT.adoc (restoring the canonical name every cross-link in the repo already points at; PR #356's -alt suffix during the #351 split is dead weight post-#355). T-2 PR #360 — delete 2,182 lines of submarine-game docs (DAMAGE-SYSTEM.md, CONTROLS-REFERENCE.md, GAME-BUNDLING-STRATEGY.md), zero cross-references. T-3 PR #365 — 13 loose root .md/.adoc moved into docs/ subtree (ABI-FFI-README → docs/reference/ABI-FFI.md, ALPHA-1-RELEASE-NOTES → docs/history/, BACKEND-{ANALYSIS,IMPLEMENTATION} → docs/architecture/, COMPILER-CAPABILITIES → docs/reference/, EXPLAINME/KNOWN-ISSUES/NAVIGATION/PROOF-NEEDS/ROADMAP → docs/, LICENSING-GUIDE/SECURITY-SETUP → docs/governance/, RSR_OUTLINE → docs/standards/RSR-OUTLINE.adoc), two new subdirs docs/architecture/ + docs/reference/, cross-refs fixed in CAPABILITY-MATRIX / BACKEND-IMPLEMENTATION / CONTRIBUTING / NAVIGATION / res-to-affine; root drops 17→5 community-health files. T-4 PR #366 — RSR_COMPLIANCE.adoc at root with four documented deviations (no guix.scm/flake.nix; STATE.scm substituted by .machine_readable/6a2/STATE.a2ml because .scm is reserved for Guix per language policy; 7 TS exemptions; 2 runtime exemptions — all cite CLAUDE.md as authoritative). T-5 PR #367 — wiki/README.md rewrite (drops ~20 dead links to non-existent .md files, updates stale ../ROADMAP.md → ../docs/ROADMAP.adoc path, annotates Features-at-a-Glance examples with current maturity, switches Ownership example from &File/&mut File sigil syntax to canonical ref File/mut File keyword types) + parse-only honesty banner on wiki/language-reference/dependent-types.md + partial honesty banner on wiki/language-reference/traits.md. T-6 — direct issue triage, no PR: closed #246 (ESC-02 JSON.t — stdlib/json.affine delivers Json ADT + encoders/decoders/get_field/stringify) and #247 (ESC-03 Dict.t — stdlib/dict.affine delivers empty/from_pairs/get/contains/size/insert/set/remove/keys/values), both with confirmation comments referencing the LANDED status in docs/TECH-DEBT.adoc STDLIB-02/03 rows. T-7 this PR — adds this session-note + the T-1..T-7 ledger entry below in docs/TECH-DEBT.adoc. (5) ALSO RESOLVED EARLIER IN SESSION (pre-T-6, as part of addressing the sustainabot tracker blockers the user asked about): #161 (Json) and #162 (Dict) closed with explicit owner-author-acknowledged closure comments dated 2026-05-24; this session was the explicit owner request to act on those. Migration-assistant tracker #57 stays open — Phase 2c lands #322's Phase 2b → 2c walker work but Phase 3 (partial translation of pure-structural forms — the phase the sustainabot tracker's exit criterion 3 specifically gates on) remains unstarted. (6) OPEN PRs AT SESSION END: #357 (Phase 2c, ready to merge — author/owner discretion on the baseline-red CI), #360/#365/#366/#367 (T-2/T-3/T-4/T-5, same baseline-red situation; pure-rename / pure-delete / pure-doc-add changes, no behavioural risk). (7) NOT DONE IN THIS SESSION: actual root-cause diagnosis of main's red build/lint; ~20 missing wiki pages (installation, hello-world, expressions, errors, package-manager, lsp, formatter, linter, stdlib subpages, design/*); guix.scm or flake.nix addition; full wiki content refresh beyond README + two banners; sync of wiki/ to the GitHub-wiki upstream repo (separate git repo, owner pushes manually). (8) CROSS-REPO: this session is AffineScript-only — the gitbot-fleet sustainabot tracker that prompted #161/#162/#57 lives in a different repo and was not touched here." -typed-wasm-formalisation-2026-05-23 = "AS↔TYPED-WASM INTERFACE FORMALISED + WIDENING ROADMAP + TWO PROPOSED ADRs. (1) docs/specs/TYPED-WASM-INTERFACE.adoc + .a2ml — formal spec of the v1 interface (was previously scattered across ECOSYSTEM.adoc + code comments in lib/codegen.ml, tw_interface.ml, tw_verify.ml). Captures: enforced surface (L7 aliasing + L10 linearity + L13 module isolation negative-form); carrier binary layout (u32 entry_count + per-entry [u32 func_index | u8 param_count | u8[n] param_kind | u8 ret_kind], kind ∈ {0 Unrestricted, 1 Linear, 2 SharedBorrow, 3 ExclBorrow}, all LE, omit-when-empty, no version field in v1); producer machinery file-line pointers (lib/codegen.ml ~110-177 + ctx.ownership_annots ~2312/~2629); consumer machinery (tw_verify.ml 332 LOC, tw_interface.ml 276 LOC, primary entry verify_from_module); CLI surface (verify / verify-boundary / verify-bridge / interface — verify-boundary's pre-2026-04-19 silent-success bug fixed by f6089a2); test coverage (test_tw_isolation.ml 5 cases + E2E Boundary Verify 3 cases + ownership-roundtrip 3 cases; honest gap: no C5.1 cross-compat fixtures shipped); multi-producer responsibilities (AS = ref producer + spec of record until C5.1; ephapax = second producer same format; typed-wasm = spec + Rust verifier + cross-compat target); deliberate non-features NF-001..NF-005 with roadmap anchors; stability promise (v1 frozen; unilateral changes forbidden). (2) docs/specs/TYPED-WASM-ROADMAP.adoc + .a2ml — making typed-wasm a natural + optimal target. Five tranches with unblock graph: A (ergonomics, no ABI change: A1 --typed-wasm gating flag, A2 report-typed-wasm JSON, A3 extract ownership-section to dedicated module, A4 diagnostic span parity) → B (schema hygiene, additive single-byte ABI: B1 = ADR-020) → C (compiler-side foundations for L1–6/L14–16: C1 region inference / CORE-01 Phase 3, C2 session-type+capability tracking, C3 CONV-02/ADR-016 S2..S4 async recogniser, C4 capability-typed externs) → D (multi-producer ABI widening: D1 = ADR-021, D2 affinescript.regions, D3 affinescript.{capabilities,session,choreography}) → E (cross-compat closure: E1 INT-12/CONV-05 fixtures into typed-wasm C5.1, E2 Rust-verifier parity out-of-scope-here). Recommended sequencing: A1→A3→A2→E1→B1→A4→D1→C1..C4→D2/D3→E2. (3) ADR-020 (PROPOSED, META.a2ml): ownership-section schema versioning — v1 unversioned → v2 with 0xAF sentinel + u8 version + entry_count. v1 readers fail cleanly on v2 (bad entry-count parse); v2 readers dispatch on sentinel. Coordinated landing: verifier ships parse first, producers flip emit together. (4) ADR-021 (PROPOSED, META.a2ml): multi-producer ABI coordination model — four axes (spec authority OCaml→Rust on C5.1 closure; coordinated landing protocol; test parity protocol; conflict resolution via serialised queue owned by typed-wasm). ADR-020 is itself the first test of ADR-021's protocol. (5) Cross-links: ECOSYSTEM.adoc §contract paragraph now points at TYPED-WASM-INTERFACE.adoc as authoritative; CAPABILITY-MATRIX.adoc typed-wasm row links to INTERFACE + ROADMAP and notes L13 alongside L7/L10; TECH-DEBT.adoc Stage E section gets header pointer + DOC-13/14/15 ledger entries (interface, roadmap, ADRs); docs/README.adoc + NAVIGATION.adoc list both new specs. (6) Pure documentation work — no compiler code changed, no behaviour change. Both ADRs require owner ratification before they bind." -stage-d-batch-2026-05-23 = "INT-01 ::-in-value-expr + BUG-005 deferred fixture + STDLIB-04 audit. (1) INT-01 follow-up: lib/parser.mly line ~835 — added `upper_ident COLONCOLON lower_ident` production emitting `ExprField(ExprVar Mod, lower_ident)`, the same AST shape `Mod.fn` already produces. Disambiguated from the line-above `Type::Variant` rule by lower_ident vs upper_ident. No resolver change required: Resolve.lower_qualified_value_paths handles both `.` and `::` syntaxes identically because the lowering pattern-matches on the ExprField shape, not the source separator. test/e2e/fixtures/cross_caller_qualified_colon{,_alias}.affine + 2 alcotest cases in qualified_value_tests pin both `use Mod; Mod::fn(x)` and `use Mod as M; M::fn(x)`. The remaining INT-01 'parser gap, not resolver' note in the ledger is now closed. (2) BUG-005 deferred regression test landed (test/test_e2e.ml, wasm_gc_loud_fail_tests): `fn main() -> Int { return totally_undefined_callee(42); }` is fed directly to Codegen_gc.generate_gc_module (no resolve step) and the emitted error must contain the unknown name — which UnboundFunction's format string includes verbatim. Closes the deferred-status entry on the [[closed-bug]] BUG-005 record. (3) docs/STDLIB-EXTERN-AUDIT.adoc — full triage of 135 extern fn + 24 extern type across 11 stdlib files into 4 classes (built-in / typed-boundary-bridge / adapter-Deno / adapter-Node); per-row status (done/partial/stub-only) and unblock condition for each module. STDLIB-04's ledger entry now points here. Implementation PRs close one row at a time; the audit doc is the canonical roadmap, not STATE.a2ml. (4) Not yet verified locally — no OCaml toolchain in this remote execution environment. CI is the verification surface; parser change is low-risk (single production, no Menhir conflict — `upper_ident COLONCOLON lower_ident` is unambiguous against the prior `upper_ident COLONCOLON upper_ident` rule by token class)." -hygiene-uplift-2026-05-23 = "TEST + BENCH + COVERAGE + PANIC-ATTACK SOPs LANDED. (1) docs/standards/TESTING.adoc — authoritative test taxonomy (alcotest unit / alcotest E2E / fixture / golden / smoke / microbench), PR-level expectations, deferred-regression-test protocol. Replaces the stale docs/guides/TESTING-REPORT.adoc (2025-12-29 snapshot, 47/27 pass rate, contradicted by live gate since early 2026); retired in-place with redirect to CAPABILITY-MATRIX + standards/TESTING. (2) bench/ scaffold — dune stanza under @bench alias, 4 phase microbenches (lex/parse/typecheck+quantity/codegen-wasm) over 3 shared fixtures (tiny_arith, medium_struct_match, larger_enum). just bench / just bench-record recipes. CI job `bench-visibility` uploads bench-output.log as artifact. Visibility-only — no merge-blocking threshold. (3) bisect_ppx coverage wiring — added as with-test depend in dune-project + affinescript.opam. just coverage recipe (HTML to _coverage/, gitignored). CI job `coverage-visibility` uploads _coverage as artifact + summary to step output. Visibility-only — no enforced floor. (4) docs/standards/PANIC-ATTACK.adoc — SOP for the estate compliance scanner. Disposition vocabulary (fix/accept/suppress/defer); standing-suppression record format; cross-refs to MAINTENANCE-CHECKLIST. .github/workflows/panic-attack.yml: weekly Sunday 03:00 UTC cron + workflow_dispatch, installs panic-attacker via cargo from road-skate/features/panic-attacker, uploads log as artifact. (5) docs/README.adoc + NAVIGATION.adoc updated — standards/ now lists TESTING + PANIC-ATTACK; guides/TESTING-REPORT marked RETIRED. (6) .gitignore — added /_coverage/, bisect*.coverage, /bench-runs/." -session-note-2026-05-20-pm = "POST-#303 CATCH-UP: #297/#300/#301/#302/#304 + repos-monorepo retirement decision. (1) PR #300 MERGED (sha 8110548) — closed #297. lib/version.ml single source of truth + release.yml sed-bake step. Binary --version self-reports 0.1.1 now. (2) PR #304 OPENED for #301 (repo-wide PMPL→MPL-2.0 SPDX surface sweep, Option 3 of the three in the issue): 673 files / +1127 −711 / 1116 SPDX headers flipped / code generators emit MPL-2.0 / .machine_readable license fields reconciled / dune-project (license …) reconciled from stale MIT-OR-AGPL → MPL-2.0 / affinescript.opam regenerated → license: \"MPL-2.0\" / LICENSES/LICENSE-MPL-2.0 added (canonical Mozilla 373-line text) / root LICENSE narrative preserved unchanged (PMPL still narrated as preferred + MPL-2.0 fallback explanation intact) / vendored sister-repo subtrees (road-skate/ + affinescript-vite/) explicitly excluded. Verification: 295/295 dune tests + 6/6 shim tests + binary 0.1.1 + grep for residual PMPL SPDX → empty (excl. vendored). Side-effect noted in PR body: pre-existing dune-project symlink-to-.build/dune-project got broken by sed atomic-rename; both files now real-file content-identical. (3) Issue #302 CLOSED — owner confirmed GitLab+Bitbucket mirroring is intentionally off; Codeberg + Radicle are the active forges. (4) repos-monorepo RETIREMENT decided: snapshot tarred (23MB gzip / 144MB extracted / 23,970 entries / canonical origin/main shallow-clone / .git excluded) to /mnt/c/Users/USER/Downloads/repos-monorepo-snapshot-2026-05-20.tar.gz; owner-upload-to-Google-Drive then `gh repo delete hyperpolymath/repos-monorepo --yes`. repos-monorepo#9 (nested casket-ssg coherence) becomes moot post-delete since the aggregator that would have carried it is gone — standalone hyperpolymath/casket-ssg already merged casket-ssg#8 + has its own instant-sync.yml for forge propagation. (5) DOC/MEMORY: this session-note + the TECH-DEBT.adoc INT-04 update (mentioned #297/#300 + #301/#304) are part of follow-up PR to #303. The 4 earlier-saved reference memories (macos-13 retired, gitbot Refs-auto-close, estate mirror state, JSR publish recipe) all still apply unchanged." -session-note-2026-05-20 = "INT-10 / #282 CLOSURE + JSR PUBLISH + ESTATE macos-13 SWEEP + DOC/MEMORY HARDENING. (1) ISSUE #282 ACTUALLY-DELIVERED end-to-end. PRs that landed today: #291 (partial pins.js fill for linux-x64 + macos-arm64, shim 0.1.1 — bridging before macos-x64 leg landed); #292 (release.yml: retired macos-13 GH-hosted runner → macos-15-intel — root cause of v0.1.0 macos-x64 leg sitting queued ~10h, traced to actions/runner-images#13046/#13402/#13634 'macos-13 fully unsupported since 2025-12-04'); v0.1.1 tag cut, full 3-platform release (linux-x64/macos-arm64/macos-x64) + SHA256SUMS published; #293 (delete 4 orphan .res files + the affine-res ReScript-bindings package — same precedent as packages/affine-ts/ removal 2026-05-11, fixed the recurring governance Language/anti-pattern check failure on every PR); #294 (release.yml checksums job: pass --repo \"$GITHUB_REPOSITORY\" so `gh release download` doesn't probe a missing .git — bug hidden on v0.1.0 by the macos-13 stall that pre-empted the checksums leg); #295 (final shim closure: pins.js VERSION→v0.1.1, all 3 sha256 fields filled, shim @hyperpolymath/affinescript 0.1.1→0.1.2, SHIM_SPEC bumped lock-step in tools/affinescript-lsp/src/compiler.rs); #298 (cross-runtime refactor: Deno/Bun/Node detection at module load + helper layer for hostOs/hostArch/envGet/readBytes/writeBytes/mkdirRecursive/chmodExec/spawnInherit/thisIsMain — was Deno-only — plus mod.d.ts + triple-slash reference so JSR fast-check finds the types and emits no warning; .claude/CLAUDE.md gained Runtime Exemptions section as the carve-out from the estate-wide Bun/Node ban); #299 (relicense the JSR shim package only — packages/affinescript-cli/{deno.json + 4 SPDX headers} — from MPL-2.0 to MPL-2.0 because JSR validates against the SPDX list and `MPL-2.0` isn't on it; wider repo PMPL→MPL-2.0 deliberately deferred — #301). (2) JSR FIRST-TIME PUBLISH RECIPE walked: claim scope @hyperpolymath → create package record (jsr.io/new — dry-run won't catch this) → link trusted GitHub repo at package or scope settings (OIDC actorNotAuthorized otherwise — dry-run won't catch this either) → SPDX licence → mod.d.ts + triple-slash → cross-runtime tickbox. publish-jsr.yml workflow (manual workflow_dispatch); 6 dispatched runs today before all gates aligned (~7:33 UTC). Verified live: https://jsr.io/@hyperpolymath/affinescript 200; meta.json `latest: 0.1.2`. Real fetch+verify+exec smoke against the v0.1.1 release ran green on linux-x64 (resolveCompiler → SHA-verify → cache → exec `--version`). (3) ESTATE macos-13 SWEEP — sole non-affinescript first-party hits: `casket-ssg#8` (release.yml: `runner: macos-13` → `runner: macos-15-intel`, merged) + `proven#29` (zig-ffi.yml: `os: macos-13` → `os: macos-15-intel`, merged) + `repos-monorepo#9` (nested casket-ssg copy in boj-cartridges/polystack/poly-ssg/casket-ssg/.github/workflows/release.yml — still open as of writing, coherence-only since instant-sync.yml in each repo independently propagates). gh search code confirmed only 3 distinct first-party repo hits across the 360-repo active estate. (4) DISCOVERED — GitLab + Bitbucket mirrors NOT operating: empirical inspection of mirror.yml runs on affinescript / casket-ssg / repos-monorepo all `completed/skipped`; `GITLAB_MIRROR_ENABLED` / `BITBUCKET_MIRROR_ENABLED` repo vars unset; `GITLAB_TOKEN` / `BITBUCKET_TOKEN` secrets absent (only FARM_DISPATCH_TOKEN present); gitlab.com probes 302, bitbucket.org probes 404. repos-monorepo DOES have CODEBERG_MIRROR_ENABLED + RADICLE_MIRROR_ENABLED set → active forges are Codeberg + Radicle + whatever `.git-private-farm` fans out to. Filed as open question: #302. (5) FOLLOW-UPS: #297 (version-string drift — bin/main.ml/lib/repl.ml/lib/lsp_server.ml/lib/onnx_codegen.ml/dune-project all hardcoded `0.1.0` even on the v0.1.1 binary; fix PR #300 introduces lib/version.ml single-source-of-truth + release.yml sed-bake step on every tag); #301 (wider PMPL→MPL-2.0 estate-relicense decision needed); #302 (mirror status — intentional-off or unfinished-setup). (6) DOCUMENTATION: this file (a session-note); docs/PACKAGING.adoc (status table for all 3 JSR-publishable packages + first-publish-gotchas section); docs/TECH-DEBT.adoc (INT-04 / INT-10 lines updated to reflect actual publish). Memory: reference_macos_13_runner_retired.md, reference_estate_gitbot_auto_closes_on_refs.md, reference_estate_mirror_state_2026_05_20.md, reference_jsr_publish_recipe.md all saved + indexed in MEMORY_STANDING.md." -session-note-2026-05-03-c = "EXTERN/VSCODE/ARRAY/PATCON BATCH. (1) `extern fn name(...) -> Ret;` and `extern type Name;` now parse — added EXTERN keyword to lexer/token/parse_driver, FnExtern to fn_body and TyExtern to type_body in AST, extern_fn_decl + extern_type_decl rules in parser.mly. Resolve registers the symbol; Typecheck.check_fn_decl special-cases FnExtern to register the polymorphic scheme without body checking; Codegen.gen_decl emits a real `(import \"env\" \"\" (func ...))` for each extern fn, mirroring gen_imports's cross-module shape. Borrow + Quantity skip extern fns. lib/dune now demotes warning 8/9 from error to warning so the new variants don't require lock-step updates across all 27 codegens (any non-Wasm codegen that doesn't handle FnExtern raises Match_failure with file:line at runtime — correct signal for 'this target has no story for host-supplied implementations'). 192 tests; 0 regressions. (2) Issue #35 Phase 2: stdlib/Vscode.affine and stdlib/VscodeLanguageClient.affine ship the ~12 + 3 binding declarations from the issue's API inventory (registerCommand, getConfiguration, showInformationMessage, createTerminal, ...). packages/affine-vscode/mod.js is the JS-side adapter that translates each `extern fn` invocation into the corresponding vscode/lc API call, with a JS-side handle table for opaque host objects and a string-marshal helper that reads `[u32 length][utf-8 bytes]` out of the wasm memory. Adapter returns a namespaced object `{ Vscode: {...}, VscodeLanguageClient: {...} }` matching the WASM cross-module imports' module names. examples/vscode_extension_minimal.affine demonstrates an end-to-end VS Code extension authored in AffineScript using `use Vscode::{registerCommand, showInformationMessage, pushSubscription}` — compiles to .cjs via the Node-CJS path. (3) issues-drafts/02 closed: `[T]` array type now parses via `LBRACKET type_expr RBRACKET → TyApp (Array, [TyArg elem])` in lib/parser.mly's type_expr_primary rule. Verified for fn params, return types, struct fields, and nested `[[T]]`. Other stdlib files (Option/math/io/string) still fail with distinct issues (`fn() -> T` type syntax, `Option` angle brackets) — the array fix advanced math.affine's failure point from line 349 to 354 etc. (4) PatCon sub-pattern destructuring under WasmGC: `match Mk(7, 99) { Mk(a, b) => a }` now lowers to RefCast + StructGet for the tag check + per-field RefCast HtI31 + I31GetS unboxing for each PatVar sub-pattern. Validated end-to-end: emitted .wasm instantiates in Deno and `main()` returns 7. The mixed-arity case (zero-arg + with-args in same enum, e.g. Option) errors loudly with the workaround documented (split into two matches, or use Wasm 1.0). Unifying variant rep — uniform `struct {tag, payload}` so Some+None can share — is the next destructuring milestone. 195 → 198 → 200 tests; 0 regressions throughout." -session-note-2026-05-03-b = "FOLLOW-ON BATCH AFTER TYPED-WASM CLOSURE: variant-with-args under WasmGC, Node-target codegen Phase 1 (issue #35), stdlib Core.affine fix, cross-module for other codegens. (1) lib/codegen_gc.ml — added gen_variant_with_args helper that lowers ExprApp(ExprVariant(_), args) and bare-name `Some(42)` (via variant_tags lookup in ExprApp ExprVar) to a tagged anon-struct allocation [tag: i32, payload: anyref, ...] with i32 args boxed via ref.i31. ExprVar gained variant_tags fallback so `return Happy` works. gen_gc_function now post-processes body_code: when result_vt ≠ I32 the trailing `push_i32 0` fallback (emitted by gen_gc_block when blk_expr=None) is swapped for RefNull HtAny, which fixes 'end[0] expected type anyref, found i32' validator errors on functions that explicitly return a struct. Validated: emitted .wasm instantiates in Node 18 / V8 14, main() returns the GC struct. f64 args remain UnsupportedFeature (no i31 boxing path). (2) lib/codegen_node.ml — new module implementing issue #35 Phase 1 (Node-CJS emit). Wraps Codegen.generate_module output in a CJS shim with: inline base64-encoded wasm constant, lazy WebAssembly.instantiate on first activate/deactivate call, JS-side opaque-handle table (_registerHandle / _getHandle / _freeHandle exported for Phase 2 vscode bindings), minimal WASI fd_write so println-style codegen works, exports.activate/exports.deactivate re-exports of the wasm module's same-named exports. .cjs output extension dispatches in bin/main.ml (both JSON and non-JSON paths). Smoke-tested: real Node 18 require()s a generated .cjs and successfully calls activate(fakeContext) → 0, deactivate() → 0. Issue #35 Phases 2-4 (stdlib/Vscode.affine bindings, extension.ts → extension.affine migration, rattlescript-face sweep) remain as separate work. (3) stdlib/Core.affine — three parser-collision fixes: `pub fn const[A,B]` → `always` (const is a reserved keyword for compile-time bindings); `fn(x: A) -> C { ... }` lambdas → `|x: A|` form (the parser's actual lambda syntax); `flip` rewritten from `(A, B) -> C` to curried `A -> B -> C` to dodge tuple-arrow ambiguity. The originally-blocked tests/modules/test_simple_import.affine (use Core::{min}; min(10,20)) now compiles end-to-end. Other stdlib files (Option/math/io/string/...) still don't parse — each has distinct issues (fn() type syntax, `[T]` array type per issues-drafts/02, `Option` angle brackets) that need their own passes or compiler-level fixes. (4) lib/module_loader.ml — new flatten_imports : t -> program -> program that prepends imported public TopFns into the importer's prog_decls (deduplicating against local fn names). bin/main.ml now binds [let flat_prog = Module_loader.flatten_imports loader prog in] in both compile_file paths and threads flat_prog through all 22 non-Wasm codegens (Julia, JS, C, WGSL, Faust, ONNX, OCaml, Lua, Bash, Nickel, ReScript, Rust, LLVM, Verilog, Gleam, CUDA, Metal, OpenCL, MLIR, Why3, Lean, SPIR-V) — Wasm and Wasm-GC keep the original prog because Codegen.gen_imports handles their cross-module needs natively. Smoke-tested: caller_ok.affine (use CrossCallee::{consume}) now compiles to JS / Julia / Rust / Lua with consume's body inlined. The non-JSON compile_file path also gained ~loader on its previously-loaderless Codegen.generate_module call (latent bug for cross-module wasm via this path). 188 → 190 tests; 0 regressions." -session-note-2026-05-03 = "TYPED-WASM CROSS-MODULE CLOSURE + MCP CARTRIDGE REWIRE + WASMGC LOUD-FAIL HARDENING. (1) lib/codegen.ml — generate_module gained ?loader and a new gen_imports pass that walks prog.prog_imports, loads each referenced module via Module_loader, and emits one (import \"\" \"\" (func ...)) entry per imported function plus a (local_alias_name → import_func_idx) entry in func_indices. ImportSimple is namespace-only (no emit), ImportList emits per item, ImportGlob enumerates public TopFns. Closes the cross-module WASM import emission gap called out in session-note-2026-04-19-a — `verify-boundary CALLEE.affine CALLER.affine` now works on user-authored AffineScript pairs, not just hand-assembled bridges. (2) lib/resolve.ml — import_resolved_symbols / import_specific_items / ImportGlob inline path now also write to dest type_ctx.name_types (not just var_types), with a new lookup_source_scheme helper that falls back from sym_id-keyed source_types to name-keyed source_name_types because resolve_and_typecheck_module's per-decl Typecheck.check_decl populates name_types but never var_types. lib/typecheck.ml — Typecheck.check_program gained ?import_types : (string, scheme) Hashtbl.t that seeds name_types after register_builtins, supplied by the resolver. bin/main.ml compile_file (JSON + non-JSON paths), compile_to_wasm_module, verify_file all updated to thread import_type_ctx.name_types through and pass ~loader to Codegen.generate_module. (3) test/e2e/fixtures/ — CrossCallee.affine + cross_caller_{ok,dup,drop}.affine, plus 3 new alcotest cases under E2E Boundary Verify exercising the full pipeline (parse → resolve_with_loader → typecheck-with-import-types → codegen-with-loader → Tw_interface.verify_cross_module). All three boundary outcomes (clean / LinearImportCalledMultiple / LinearImportDroppedOnSomePath) confirmed end-to-end. (4) boj-server/cartridges/typed-wasm-mcp — mod.js rewritten to call `affinescript` (was: nonexistent `typed-wasm` binary), with cwd set to the source's directory so Module_loader resolves relative imports correctly. cartridge.json bumped to v0.2.0 with corrected input descriptions (.affine source paths) and a new typed_wasm_verify_boundary tool exposing the cross-module verifier. README.adoc updated to match. (5) lib/codegen_gc.ml — eliminated three silent-bad-codegen fallbacks (same class as BUG-005): wildcard ExprLambda/ExprUnsafe → RefNull replaced with explicit UnsupportedFeature errors; match-arm wildcard PatTuple/PatRecord → fall-to-default replaced with UnsupportedFeature; PatLit fallback for LitFloat/LitString replaced with explicit errors. test/test_e2e.ml gained E2E WasmGC Loud-Fail suite with 2 regression markers. Bumps wasm-gc-codegen from 70% to 85% (silent-fallback gap is gone; effects/try-catch/lambda/call_ref remain genuinely deferred to upstream EH proposal or whole-program CPS). 180 → 182 tests; 0 regressions." -session-note-2026-05-02 = "FACE BUILDOUT + BRAND-SURFACE EJECT + TS-EXEMPTION DOCS + FRONTIER-PRACTICES FORMALIZATION. (1) lib/lucid_face.ml + lib/cafe_face.ml — full transformer parity with python_face/js_face/pseudocode_face. lib/face_pragma.ml — pragma detection (`# face: ` / `// face: ` / `-- face: ` / `(* face: *)`) with alias table covering canonical/python/py/rattle/rattlescript/js/javascript/jaffa/jaffascript/pseudocode/pseudo/pseudoscript/lucid/lucidscript/purescript/ps/cafe/cafescripto/coffee/coffeescript. (2) Single .affine extension across all faces; per-face extensions (.rattle/.pyaff/.jsaff) deprecated to migration path with warning. (3) lib/face.ml extended with format_*_for_face dispatch tables for Lucid (Haskell-flavoured: 'Linearity error', 'Variable not in scope', 'Could not match type') and Cafe (concise JS-flavoured). (4) tools/run_face_transformer_tests.sh + just test-faces / test-faces-record / test-faces-update — snapshot + round-trip parse harness. Caught 5 transformer bugs on first run that visual review had missed. (5) examples/faces/ + README.adoc with per-face hello programs and 'Known transformer gaps' list for the categories deferred to a future AST-rewriter milestone (multi-clause defs, do-notation, where-blocks, list comprehensions, splats, no-paren calls). (6) Five non-canonical faces ejected to top-level brand-surface repos: github.com/hyperpolymath/{rattlescript,jaffascript,pseudoscript,lucidscript,cafescripto}. Each has README, LICENSE, CONTRIBUTING, bin/ shim that exec's affinescript with `--face ` injected after the subcommand, justfile, examples/. NO compiler forks — all live and evolving compiler logic stays here in affinescript. Eject pattern: 'adapt-then-commit' for the pre-existing rattlescript repo (preserved 7 commits + v0.1.0-alpha tag while replacing the wrong-shape Cargo+vendored architecture). (7) docs/guides/frontier-programming-practices/Human_Programming_Guide.adoc + AI.a2ml — new 'Faces: Frontend Surfaces' section formalizing 6 lessons earned during the buildout: faces-as-brand-surfaces (not compiler forks), identity-in-content-not-filesystem, snapshot-plus-roundtrip for any pure text-to-text transformer, examples-are-tests-not-demonstrations, error-vocabulary-cost-O(faces × error_kinds), span-fidelity-honesty (acknowledge UX regression rather than paper over). AI.a2ml gained parallel (faces) section mirroring the existing (backends) shape with rules, when-adding-face-N+1 recipe, known-limitations. (8) .claude/CLAUDE.md TypeScript Exemptions table — 9 approved files with rationale and unblock condition. Mirror tables landed in standards / my-lang / boj-server. Audit follow-up issues #63-66 track port work blocked on #35 (Node target), #42 (extern types), and stdlib network/crypto. Commits: 74024c6 (Lucid+Cafe + unification), 82eec92 (rattlescript subtree eject), 133bb53 (frontier guide section), 116ea5d (CLAUDE.md exemptions). Side effect across the wider hyperpolymath estate: TS file count 94 → 27 after vendored-snapshot deletions (rsr-template-repo, idaptik dlc subtrees migrated to dedicated repos via PRs #1 against idaptik-dlc-iky and idaptik-dlc-reversibley + cleanup PR #70 against idaptik). Burble canary CI gate (.github/workflows/affinescript-canary.yml in PR #21) lands an advisory gate compiling every .affine file on PR — promote to required-for-merge as Burble grade B target." -session-note-2026-04-19-a = "CODEGEN BUG FIX + TYPED-WASM LEVEL 10 CLI SURFACE CLOSED. (1) lib/codegen.ml commit 35c476d — three fixes: (a) PatCon-with-args stack imbalance in gen_pattern: was LocalTee match_result + LocalGet match_result around stack-neutral bind_fields; removed save/restore, I32Eq sits on stack directly. Fixes 'expected 1 elements on the stack for fallthru, found 2' on match-in-enum returning distinct zero-arity or arg constructors across arms. (b) ExprVar falls back to ctx.variant_tags when lookup_local misses, so bare `Initialised` (parens omitted) resolves as the variant tag — parser accepts the form, codegen previously failed with UnboundVariable. (c) ctx.struct_layouts + ctx.fn_ret_structs: struct layouts registered globally from TopType(TyStruct), propagated to function parameters via p_ty, call-result lets via fn_ret_structs, let-bindings with sl_ty annotation, let-from-let passthroughs. Fixes the .field_1_or_later=0 bug on struct function parameters. struct_name_of_ty handles TyCon / TyApp / TyOwn / TyRef / TyMut wrappers. (2) bin/main.ml commit f6089a2 — new verify-boundary CALLEE CALLER subcommand: compiles two .affine source files, extracts callee's ownership-annotated export interface, runs Tw_interface.verify_cross_module on the caller, exits 0/1 correctly. Shared compile_to_wasm_module helper factored out of verify_file. Fixed verify_boundary_fn exit-code bug (verify-bridge handler): was always returning Ok () even on violations; now tracks violation count and maps to Error so CI catches boundary drift. Level 10 cross-module boundary verifier now has public CLI surface. Evidence: 177/177 tests green unchanged; affinescript-deno-test smoke suite 7/7 (up from 4/4) with new codegen_regression_test.affine; double-track-browser extension_lifecycle_test.affine pilot 10/10 without tagged-struct workaround. Scope note: AffineScript use A.B imports inline at AST layer, so caller from AffineScript source today produces no cross-module WASM imports — new CLI immediately useful for hand-assembled / bridge-pattern callers (IDApTIK bridges, composition tools). Broader applicability gated on cross-module WASM import emission, a separate compiler feature." -session-note-2026-04-12-c = "STAGE 12 (SECOND IDAPTIK SCREEN) IN PROGRESS — CharacterSelectScreen chosen as dogfood target (6 class cards: Assault/Recon/Engineer/Signals/Medic/Logistics, 1 confirm → JessicaCustomise). Plan: (1) lib/tea_cs_bridge.ml — new Wasm module, same API surface as tea_bridge.ml, 7 msgs (0-5=select class, 6=confirm), update: selected_tag=msg+1, selected_tag 1-6=class highlighted, 7=navigate. (2) lib/dune + bin/main.ml — add cs-bridge subcommand and update which_arg/interface/verify-bridge. (3) IDApTIK: AffineTEARouter.js+.res add screenJessicaCustomise=6. (4) CharacterSelectScreen.res: csTeaBridge module-level ref, card click handlers → teaDrive, applyView syncs Wasm→ReScript selectedClass, confirm → AffineTEA.update+navigate via showScreenWithTag. Seam check: push TitleScreen→CharacterSelect, select class, confirm→JessicaCustomise, back-stack correct. 173/173 tests currently passing." -session-note-2026-04-12-b = "STAGES 7-11 COMPLETE (173/173 tests). Stage 9: per-path min/max linearity (count_uses_range, LinearDroppedOnSomePath, fn_push else-branch fix). Stage 10: tw_interface.ml multi-module boundary verifier (LinearImportCalledMultiple, LinearImportDroppedOnSomePath, CLI interface+verify-bridge cmds). Stage 11: source-level Cmd linearity — Cmd[ClickMsg] bracket syntax, quantity_of_ty_annotation returns QOne for Cmd[_], infer_usage_block declares QOne locals before subsequent stmts so uses tracked, fires LinearVariableUnused when Cmd dropped; builtins cmd_none+cmd_perform in typecheck/resolve/interp. Commit b8a0f9c." -session-note-2026-04-12-a = "STAGE 6 (CADRE ROUTER) COMPLETE: lib/tea_router.ml generates Wasm 1.0 router module (915 bytes, WebAssembly.validate=true). RouterModel at offset 64: screen_w(+0), screen_h(+4), stack_len(+8), popup_tag(+12), stack_data[8](+16). 11 functions: init, push(Linear), pop, present_popup(Linear), dismiss_popup, resize(Linear×2), get_screen_w/h/stack_len/stack_top/popup_tag. affinescript.ownership: 3 functions with Linear params (push, present_popup, resize). affinescript.tea_layout: version=1, 5 fields. router-bridge subcommand added. 8 new E2E tests; 113 total, 0 regressions. IDApTIK: AffineTEARouter.js + AffineTEARouter.res; Navigation.res: routerBridge field + setRouterBridge + showScreenWithTag + presentPopupWithTag + dismissPopup router sync + resize router sync; TitleScreen.res: teaDrive uses showScreenWithTag/presentPopupWithTag, router.wasm loaded on first show alongside titlescreen.wasm, graceful fallback. router.wasm committed to idaptik/public/assets/wasm/. Both repos pushed. commit: affinescript 5969b7b, idaptik e3eafd1." -session-note-2026-04-11-h = "STAGE 5 (AFFINATEA DRIVES SCENE) COMPLETE: teaDrive(engine, msgTag) is the single entry point from all 4 button handlers — update → applyView → navigate. applyView reads selected_tag via getSelected() and alpha-pulses the chosen button (0.65 → animate 1.0 over 0.22s). teaBridge loaded asynchronously on first show; graceful fallback to direct ReScript navigation when bridge is None. resize handler calls AffineTEA.setScreen(tea, width, height) to keep screen_w/h in sync with canvas. Button handlers reduced to single teaDrive call each. Navigation decision comes FROM Wasm state (selected_tag), not from button identity. Credits button wired (msgCredits) with TODO screen. IDApTIK TitleScreen is fully AffineTEA-driven for Stages 4+5." -session-note-2026-04-11-g = "STAGE 4 (WASM/JS BRIDGE FOR IDAPTIK) COMPLETE: lib/tea_bridge.ml generates complete TitleScreen TEA bridge Wasm 1.0 module (512 bytes). TitleModel at offset 64: screen_w(+0), screen_h(+4), bgm_playing(+8), selected_tag(+12). 7 functions: init, update (branchless selected_tag=msg+1), get_screen_w/h/bgm_playing/selected, set_screen. affinescript.ownership custom section: update param kind=1 (Linear). affinescript.tea_layout custom section: version=1, 4 fields. affinescript tea-bridge subcommand added to bin/main.ml. 6 bridge E2E tests added; 101 total tests passing. AffineTEA.js + AffineTEA.res bindings in idaptik/src/app/tea/. titlescreen.wasm (512 bytes, WebAssembly.validate=true, end-to-end Node.js verified) in idaptik/public/assets/wasm/." -session-note-2026-04-11-f = "LSP PHASE C COMPLETE: completion candidates subcommand shipped. extract_prefix_at scans backward from cursor col to extract ident prefix + dot-context flag. collect_completions filters symbol table by prefix and appends affine_keywords (suppressed in dot context). emit_completions emits JSON array {name,kind,type,detail}. complete_file + complete_cmd added to bin/main.ml (FILE LINE COL args). 6 E2E tests. 101 total, 0 regressions. NEXT: LSP Phase D." -session-note-2026-04-11-e = "LSP PHASE B COMPLETE (commit 79c0829): hover/goto-def subcommands shipped. span_contains, find_symbol_at (refs-first then def-spans), hover_to_json/goto_def_to_json, emit_hover/emit_goto_def, run_pipeline_for_query. 89 tests. NEXT: LSP Phase C (completion candidates from symbol table)." -session-note-2026-04-11-d = "TRAITS WIRED (commit 1ca143e): trait_registry in context; TopTrait/TopImpl in forward pass; ExprField trait fallback; find_impl unification-based; TopImpl bodies typechecked; 80 tests. NEXT: LSP Phase B." -session-note-2026-04-11-c = "LINEAR ARROWS ENFORCED (commit d2f9b7b): (1) lambda synth — |@linear x: T| e now synthesises T -[1]-> U; (2) lambda check mode — explicit quantity annotation validated against expected TArrow; (3) quantity.ml ExprLambda — annotated params declared in env, usage verified, violations accumulated in env.errors and drained after check_function_quantities step 3; scope leakage prevented via quantities save/restore. Also fixed codegen.ml missing custom_sections field (pre-existing wasm.ml change). 75 tests, 0 regressions. NEXT: traits generic resolution → unification integration; then LSP Phase B." -session-note-2026-04-11-b = "BUG-005 CLOSED: WasmGC silent drop+null fallbacks replaced with explicit CodegenError (UnboundFunction for missing func_indices entries, UnsupportedFeature for indirect/higher-order calls). Effects runtime SHIPPED: TopEffect now registers each op as a PerformEffect-raising VBuiltin in interp.ml (was no-op). Multi-arg handler arg_vals bug fixed. ExprResume wired to __resume__ continuation in handler env. WasmGC path: TopEffect ops registered as unreachable stubs with correct func_indices offsets; ExprHandle and ExprResume reject with UnsupportedFeature (EH proposal or CPS transform required for full dispatch)." - -[project-context] -name = "affinescript" -completion-percentage = 94 -phase = "affinatea-stage6-complete" -tagline = "Rust-inspired language with affine types, dependent types, row polymorphism, and extensible effects" -note = "Session 2026-04-11 final checkpoint. DONE THIS SESSION: (1) JS-face (lib/js_face.ml) + Pseudocode-face (lib/pseudocode_face.ml) — both wired into face.ml and bin/main.ml. resolve_face() wired into ALL six file handlers in bin/main.ml (parse/check/eval/compile/fmt/lint) — .rattle/.pyaff/.jsaff/.pseudoaff auto-detected without --face flag. (2) P3 packages: packages/affine-js/ (Deno ESM WASM loader, marshal/unmarshal, types.d.ts), packages/affine-ts/ (typed call helpers), packages/affine-res/ (ReScript bindings). (3) P4 educational: docs/guides/frontier-guide.adoc (6-chapter tutorial), docs/guides/warmup/ (4 warmup scripts). (4) CoffeeScript + ActionScript roadmap faces added to docs/specs/faces.md. (5) RattleScript distribution: distributions/rattlescript/ in-tree (Rust wrapper, build.rs, examples/), standalone repo hyperpolymath/rattlescript (GitHub, starred, v0.1.0-alpha released, 8 topics set, affinescript submodule, just bootstrap workflow). (6) dune build + dune runtest: clean, 0 regressions throughout. NEXT SESSION (clean thread): BUG-005 first (WasmGC silent bad codegen — replace fallback arms with CodegenError, ~30min), then effects runtime (interpreter handler dispatch + WASM codegen — the headline missing feature). Previous session note: P3 (aggregate library ecosystem) and P4 (educational materials) complete. P2 faces extended. P2 faces extended: JS-face (lib/js_face.ml) and Pseudocode-face (lib/pseudocode_face.ml) added, face.ml and bin/main.ml updated with all four active faces (canonical/python/js/pseudocode). aggregate packages: packages/affine-js/ (Deno ESM, WASM loader, marshal/unmarshal, types.d.ts), packages/affine-ts/ (typed call helpers, narrowing predicates), packages/affine-res/ (ReScript bindings with typed shortcuts and exhaustive matching). educational materials: docs/guides/frontier-guide.adoc (6-chapter unveiling tutorial), docs/guides/warmup/ (4 warmup scripts: basics/ownership/effects/rows). faces.md updated with JS-face and Pseudocode-face entries. Previously: Faces architecture (ADR-010, Priority 2) complete 2026-04-11. (1) lib/python_face.ml: source-level text preprocessor mapping Python surface syntax to canonical AffineScript — indentation→braces, def/True/False/None/and/or/not/class/pass keyword subs, elif/else chains, tail-position semicolon suppression. (2) lib/face.ml: face-aware error formatter mapping canonical error terms to face-specific vocabulary — Python face uses 'Ownership error / single-use variable' for linear violations, 'Name not found' for unbound vars, 'if/else type mismatch' for branch mismatches, renders Unit as None and Bool as bool. (3) bin/main.ml: --face [canonical|python] flag on all subcommands; parse_with_face selects preprocessor path; all error sites route through Face.format_*_error. (4) Span fidelity deferred: error spans currently refer to transformed canonical text positions (planned follow-up). (5) docs/specs/faces.md: face architecture reference doc written. (6) panic-attack audit: 1 High (search.js innerHTML — false positive but fixed to char-replacement), 2 Medium (LSP unwraps/shell heredoc — both false positives), 2 Low (TODO markers — informational). 73/73 E2E tests: 0 regressions. Seam check (items 1+2) verified: --face python on @linear double-use produces correct Python-face ownership error end-to-end." - -[components] -lexer = "complete-minus-affine-tokens (ZERO/ONE tokens for quantity literals NOT emitted; blocks QTT surface syntax). float_exponent now correctly named sub-regexp — scientific notation crash fixed 2026-04-11." -parser = "conformance-complete (ADR-009 achieved 2026-04-11). 12/12 valid tests parse. Added: optional SEMICOLON on type aliases/enums, enum leading-pipe syntax, effect op type params, ROW_VAR type params, ty_record_body/expr_record_body recursive rules for row-polymorphic records, field_name contextual keyword rule, struct literal expressions. CORE-03 / ADR-014 (PR #241, Refs #228, 2026-05-19, gate 267/267, ZERO Menhir conflict delta — 21 S/R + 1 R/R unchanged): module-qualified type/effect paths Pkg.Type / Pkg::Type (mixed, deep) parse — folded to one canonical ::-joined ident so resolve/typecheck/codegen/fmt unchanged. Cleared the estate's dominant parse blocker (was 525/1177). Authoritative: docs/CAPABILITY-MATRIX.adoc + docs/TECH-DEBT.adoc CORE-03." -type-checker-rules = "99% (bidirectional inference, Never/bottom, block divergence, variant/constructor/record-spread/mutable bindings all implemented in lib/typecheck.ml)" -type-checker-enforcement-wiring = "wired (Typecheck.check_program is invoked from bin/main.ml on every check/compile/eval path; Quantity.check_program_quantities runs after typecheck at lib/typecheck.ml:1206; the gating IS live for the rules and quantity annotations that exist today)" -borrow-checker = "live-gate (2026-04-11): Borrow.check_program wired into check/compile pipeline at all 4 Typecheck success sites. Emits E0501-E0506 diagnostics. PlaceVar carries variable name for readable errors (file:line:col at both use and move sites). ExprMatch arm state merging fixed. Lexical borrow lifetime clearing in check_block. BUG-004 (lambda capture tracking) still deferred — requires type info propagation. lib/borrow.ml 669 LOC." -interpreter = "95%" -wasm-codegen = "95% (cross-module imports + extern fn → WASM import emission for both `use Foo::{f}` and `extern fn f` user-source forms; landed 2026-05-03c)" -wasm-gc-codegen = "91% (variant-with-args + PatCon same-arity destructuring 2026-05-03c. Silent-bad-codegen fallbacks eliminated 2026-05-03a. Mixed-arity matches still need uniform variant rep; effects/try-catch/call_ref still deferred to upstream EH or CPS.)" -node-cjs-codegen = "Phase 1+2+3 complete (2026-05-03d): codegen_node.ml emits CJS shim; stdlib/Vscode.affine + stdlib/VscodeLanguageClient.affine ship 19 binding declarations; packages/affine-vscode/mod.js is the JS-side adapter (now using shared handle table via exposed exports._instance / exports._registerHandle); editors/vscode/src/extension.ts deleted, replaced by extension.affine compiled to out/extension.cjs (verified end-to-end: real Node 18 dispatches activate(ctx), 5 commands register, lsp.enabled probed). package.json's main field now ./out/extension.cjs; compile script invokes `affinescript compile`. tools/check-no-extension-ts.sh + .github/workflows/ci.yml step + just check guard ensure the .ts cannot drift back. CLAUDE.md exemptions table reduced from 9 → 8 with the closed-exemption note. Phase 4 (rattlescript-face sweep) is the only remaining task on this issue." -session-note-2026-05-03-e = "STDLIB-PARSER UNBLOCKERS: 3 type-syntax sugars added to lib/parser.mly. (1) `fn(A, B, ...) -> T` lowers to the curried arrow chain `A -> B -> ... -> T`. Zero-arg `fn() -> T` lowers to `Unit -> T` (TyTuple [] -> T). Required by stdlib/Option.affine's `unwrap_or_else[T](opt: Option[T], f: fn() -> T) -> T` and similar higher-order signatures. (2) Angle-bracket aliases for type application AND type parameters: `Option` ≡ `Option[T]`, `fn f(x: T)` ≡ `fn f[T](x: T)`, `type Option = ...` ≡ `type Option[T] = ...`. The `<` / `>` are unambiguous in type position because comparison operators don't appear there. (3) `(A, B) -> C` now lowers to the curried arrow `A -> B -> C` instead of being parsed as a single tuple-arg arrow. Disambiguated from plain tuple types by the trailing ARROW; bare `(A, B)` (no arrow) still parses as a TyTuple. Stdlib/Core.affine's `flip` reverted from the earlier curried workaround to its natural `(A, B) -> C` form. Stdlib audit: 4 files now check (Core, Math, Vscode, VscodeLanguageClient), 5 parse but fail check (Option/Result/io/prelude/string — actual code-level errors), 7 still fail to parse (collections/effects/option/result/math/testing/traits) with DISTINCT remaining issues — slice syntax `list[1:]`, `effect io;` decl form, `=>` lambda spelling, `Self` in trait method sigs. Each is its own task. 7 new regression tests under E2E Type Syntax Sugar. 200 → 207 tests; 0 regressions." -session-note-2026-05-03-d = "ISSUE #35 PHASE 3 CLOSED + REGRESSION GUARD WIRED. (1) editors/vscode/src/extension.affine (160 lines) replaces extension.ts, using the Phase 2 Vscode + VscodeLanguageClient bindings from stdlib/. Adds 9 new extern fns to Vscode.affine for the API surface the migration actually needed: editorActiveFilePath / editorActiveLanguageId (collapse vscode.window.activeTextEditor.document.{uri.fsPath,languageId}), workspaceConfigGetBool, consoleLog, execSync, stringConcat / stringEndsWith / stringReplaceSuffix. (2) packages/affine-vscode/mod.js refactored to take the host shim as 3rd arg and share its handle table — fixes a bug where the wasm-side ExtensionContext handle wasn't visible to adapter functions. lib/codegen_node.ml's shim now exposes exports._instance + exports._memory after init so adapters can read string args out of wasm linear memory; replaces the constant `_extraImports()` with a mutable exports.extraImports hook. (3) editors/vscode/package.json `main` field flipped from ./out/extension.js to ./out/extension.cjs; scripts.compile is now `affinescript compile src/extension.affine -o out/extension.cjs`; obsolete tsconfig.json deleted; @types/node + typescript devDependencies removed. (4) tools/check-no-extension-ts.sh fails CI / `just check` if any extension.ts reappears under editors/vscode/src or faces/*/affinescript/editors/vscode/src — wired into both .github/workflows/ci.yml and the just check recipe. (5) .claude/CLAUDE.md TypeScript Exemptions table: editors/vscode/src/extension.ts row removed (count 9 → 8), moved to a 'Closed exemptions' note with the 2026-05-03 closure date and pointer to the regression guard. End-to-end smoke-tested: real Node 18 require()s the .cjs, dispatches activate(fakeContext), all 5 commands register with their full names, both lsp.enabled and lsp.serverPath are probed via getConfiguration. 200 tests still green; 0 regressions." -extern-decls = "complete (2026-05-03c): `extern fn` and `extern type` both parse, resolve, typecheck, and emit (import \"env\" \"\" ...) in the WASM target." -array-type-sugar = "complete (2026-05-03c, closes issues-drafts/02): `[T]` desugars to Array[T] in any type-expr position." -type-syntax-sugars = "complete (2026-05-03e): three forms accepted — `fn(A,B) -> T` (curried lowering), `Option` / `Result` / `fn f` (angle-bracket aliases for both type-app and type-param), and `(A,B) -> C` (true 2-arg arrow distinct from tuple-arg). Plain tuple types still parse when no ARROW follows." -xmod-other-codegens = "complete (2026-05-03b): Module_loader.flatten_imports inlines public TopFns from imported modules; threaded through all 22 non-Wasm backends in bin/main.ml. Smoke-tested with JS/Julia/Rust/Lua emission of caller-of-Callee." -julia-codegen = "exists" -lsp-phase-a = "complete" -lsp-phase-b = "complete (2026-04-11, commit 79c0829): hover/goto-def subcommands shipped. json_output.ml: span_contains (1-based, single+multi-line), find_symbol_at (references-first then def-spans), hover_to_json/goto_def_to_json/not_found_json, emit_hover/emit_goto_def. bin/main.ml: hover + goto-def subcommands, run_pipeline_for_query tolerates typecheck errors. 4 E2E tests. 89 total." -lsp-phase-c = "complete (2026-04-11): completion candidates subcommand shipped. json_output.ml: extract_prefix_at (scans backward from cursor col to find ident prefix + dot-context flag), collect_completions (symbol table prefix filter + affine_keywords list suppressed in dot context), emit_completions (JSON array). bin/main.ml: complete_file handler + complete_cmd (FILE LINE COL args). 6 E2E tests added (prefix extraction, prefix match, empty prefix, no-match, keyword included, dot-ctx suppresses keywords). 101 total tests, 0 regressions." -lsp-phase-d = "complete (2026-04-11): JSON-RPC LSP server shipped as `affinescript server --stdio`. lib/lsp_server.ml: Content-Length framing, document store (URI→source), pipeline cache (URI→symbols+refs+diags), run_pipeline (temp-file + span path fixup), lsp_range (1→0-based conversion), diag_to_lsp, publish_diagnostics. Handlers: initialize (hover+definition+completion caps), didOpen/didChange (pipeline+cache+push), didClose (clear), hover (markdown+type+quantity), definition (URI+range), completion (LSP kind mapping). bin/main.ml: server_cmd in default group. 4 E2E tests. 105 total, 0 regressions." -stdlib = "95% (5 stubs remain as extern builtins)" - -[stats] -compiler-loc = 12750 -compiler-modules = 39 -lsp-files = 5 -test-files = 54 - -[features] -# Honest status of the headline language features advertised in README.adoc. -# "declared-but-unwired" = surface syntax and/or internal module exists, but -# the feature is not enforced on user programs through the CLI pipeline. -affine-types = "wired-and-reachable (Track A Manhattan plan complete 2026-04-10. Quantity semiring in lib/quantity.ml; invoked from typecheck.ml:1206 inside the standard CLI pipeline. Surface syntax per ADR-007 hybrid: @linear/@erased/@unrestricted attributes (Option C primary) on let/stmt-let/param/lambda-param, AND :1/:0/:ω numeric sugar (Option B) on let/stmt-let. Scaled Let rule per ADR-002 implemented in lib/quantity.ml ExprLet/StmtLet — closes BUG-001 (ω-let smuggles linear values) and BUG-002 (zero-let erasure). Four regression fixtures in test/e2e/fixtures/ exercise both surface forms. Behavioural enforcement verified via E2E Quantity test suite — 4 new passing tests, 0 regressions.)" -linear-arrows = "enforced (2026-04-11): Three-part fix landed. (1) typecheck.ml lambda synth: |@linear x: T| e now synthesises T -[1]-> U (was always QOmega). (2) typecheck.ml lambda check mode: explicit param quantity annotation validated against expected TArrow quantity; unannotated params inherit context quantity. (3) quantity.ml ExprLambda: added env.errors accumulator; annotated lambda params declared via env_declare so env_use tracks them; usage verified with check_quantity after body walk; violations pushed to env.errors and drained at end of check_function_quantities (step 4). Saved/restored env.quantities entries to prevent scope leakage. Two E2E fixtures + 2 passing tests. 75 tests total, 0 regressions. Commit d2f9b7b pushed." -borrow-checker = "phase-3-parts-1-3-Slices-A-B-C-light-landed (CORE-01, Refs #177, 2026-05-24): pt1 (#240, gate 263/263) borrow-graph validation wired — BorrowOutlivesOwner emitted, shared-XOR-exclusive enforced at use sites, ownership from param type TyOwn/TyRef/TyMut, call-arg borrows temporary, ref-binding graph tracked. pt2 (gate 271→274 and 278→281) return-escape + &mut e parser surface. pt3 Slice A (PR #335) NLL last-use expiry: compute_last_use_index pre-pass; check_block expires in-block ref-bindings once their binder is dead. pt3 Slice B (effectively #354/#355/#356 after #351 procedural close) flow-sensitive escape via re-assignment: `outer = &y` in StmtAssign pre-releases the held borrow and re-binds the ref-graph entry to the freshly-created borrow. pt3 Slice C-light (this PR) CFG-join semantics for non-match join constructs: ExprHandle handler arms and ExprTry catch arms are now isolated via snapshot-restore-merge (factored merge_arm_results helper, mirroring ExprMatch's inlined logic) — moves/borrows from arm i no longer pollute arm i+1. 2 hermetic tests (positive: independent catch-arm moves OK; anti-regression: body-side move persists past the try). Residual (Slices C-full, C', D): true Polonius origin/region variables on TyRef/TyMut with subset constraints + datalog-style solver (architectural; ADR-gated); loop soundness via 2-iter check (coupled to StmtAssign clear-on-rewrite fix); reborrow through indirection (ref-to-ref binding); quantity-checker tightening for captured linears. Authoritative: docs/CAPABILITY-MATRIX.adoc + docs/TECH-DEBT-alt.adoc CORE-01." -row-polymorphism = "60% (records + effects rows implemented in typecheck/unify; not fully exercised end-to-end)" -effects = "interpreter-complete (handler dispatch, PerformEffect propagation, ExprResume, multi-arg ops all wired in interp.ml 2026-04-11). WasmGC: ops registered as unreachable stubs; ExprHandle/ExprResume reject with UnsupportedFeature. Every compiled backend (core-WASM, WasmGC, Deno-ESM, JS-text, C) now fails loud on handle/resume (#555 fenced); interp multi-shot fails loud; one non-tail single-shot residual is pinned by a test. Authoritative: docs/SOUNDNESS.adoc." -dependent-types = "removed-v1 (refinement/dependent surface withdrawn 2026-04-10 per #558; no TRefined node remains; assume(...) rejected at parse; T where (P) is a parse error — no unenforced predicate is expressible). Authoritative: docs/SOUNDNESS.adoc + docs/CAPABILITY-MATRIX.adoc" -traits = "90% (2026-04-11, commit 1ca143e): trait_registry added to typecheck context; TopTrait/TopImpl processed in forward pass; ExprField falls back to Trait.find_method_for_type on record-field failure; find_impl/find_impls_for_type use unification-based matching via fresh_impl_self_ty + Unify.unify instead of name string comparison; TopImpl bodies type-checked via check_fn_decl with Self bound; check_impl_satisfies_trait verifies required methods present. Two E2E fixtures + 2 tests. 80 total, 0 regressions. Coherence checking now wired (#559, concrete overlaps whose self types unify are rejected; typecheck.ml check_all_coherence). Remaining: associated type substitution in method bodies, where-clause supertrait enforcement, generic-subsumption overlap (#559 follow-up). Authoritative: docs/SOUNDNESS.adoc.)" - -[track-a-manhattan] -owner = "primary" -scope = """ -Close the surface-syntax gap that makes affine enforcement unreachable from -user programs. Revised 2026-04-10 after the original ETA was set against the -incorrect premise that `Quantity.check_program_quantities` was unwired from -the CLI — it is wired (typecheck.ml:1206) and has been since at least -the Manhattan-recovery refactor (#19). The actual remaining work: - -1. AST: add `el_quantity : quantity option` to ExprLet, `sl_quantity` to - StmtLet. (~4 LOC + cascading pattern updates across ~10 modules that - match on Let.) -2. Lexer/parser: make ZERO/ONE tokens reachable in quantity position - (cheapest path: accept INT 0/INT 1 in the parser's `quantity` rule, - leaving the lexer alone). ~3 LOC. -3. Parser: extend `let_decl` and `stmt_let` rules with an optional - binder-quantity annotation. Surface-syntax choice is a language-design - call — see META.a2ml ADR-007 candidate. -4. quantity.ml ExprLet/StmtLet: implement context scaling per ADR-002. - Snapshot before walking el_value, walk it, scale all newly-recorded - usages by `q`, then merge back. ~25 LOC including a `scale_usage_by` - helper. -5. Two regression fixtures covering BUG-001 and the dual valid case. -6. spec.md T-Let rewrite to show the scaling explicitly. - -Out of scope for Track A: -- BUG-003 (eval_list L-to-R): SHIPPED 2026-04-10. interp.ml:347 now uses - fold_left + List.rev. Behavioural regression test deferred — depends on - the broken `let mut` interpreter path (separately tracked in the failing - baseline interp tests). -- BUG-004 (lambda capture tracking): blocked on borrow checker Phase 3. -- BUG-002 (zero-let erasure semantics): folds in with BUG-001 once - surface syntax exists. -""" -spine-files = ["lib/ast.ml", "lib/parser.mly", "lib/quantity.ml", "lib/typecheck.ml", "lib/interp.ml", "lib/codegen.ml", "lib/codegen_gc.ml", "lib/julia_codegen.ml", "lib/formatter.ml", "lib/sexpr_dump.ml", "lib/json_output.ml", "lib/linter.ml", "lib/opt.ml", "lib/desugar_traits.ml", "test/e2e/fixtures/", "docs/spec.md"] -eta = "T+7d from 2026-04-10 (ADR-007 accepted, implementation chain unblocked)" -blocking-decision = "RESOLVED 2026-04-10: ADR-007 accepted as hybrid C-primary + B-sugar. @linear/@erased/@unrestricted are the canonical forms emitted by tutorials, error messages, IDE tooling, and the formatter. The :1/:0/:ω numeric forms are accepted as sugar (legal in source, rewritten to attribute form by `affinescript fmt` unless --keep-quantity-sugar is set). See META.a2ml ADR-007 for full decision text." - -[[closed-bug]] -id = "BUG-001" -severity = "high" -category = "soundness" -title = "ω-let smuggles linear values" -discovered = "2026-04-10" -fixed = "2026-04-10" -description = """ -let x :ω = linear_resource() in use_x_once(x) was previously accepted. -The QTT-orthodox scaled Let rule (ADR-002) rejects this because scaling -the value context by ω promotes the linear usage of `linear_resource()` -to UMany, which the quantity checker reports as a linear-binding violation. -""" -fix = "lib/quantity.ml ExprLet/StmtLet cases now snapshot the env, walk the value, scale the per-variable delta by the binder quantity (via the new scale_usage helper), and merge the scaled deltas back. ExprLet was extended with el_quantity, StmtLet with sl_quantity (lib/ast.ml). Surface syntax landed per ADR-007 hybrid: @linear/@erased/@unrestricted primary + :1/:0/:ω sugar." -verification = "test/e2e/fixtures/bug_001_omega_let_smuggles_linear.affine (Option C form) and test/e2e/fixtures/bug_001_sugar_form.affine (Option B form) — both produce the @linear-vocabulary error message and are pinned in test/test_e2e.ml as `BUG-001 attr/sugar form rejects ω-let smuggling @linear`. Behaviourally verified end-to-end through `dune runtest`. The dual valid cases (affine_let_valid.affine and affine_let_valid_sugar.affine) confirm the rule does not over-reject." - -[[closed-bug]] -id = "BUG-002" -severity = "medium" -category = "semantics" -title = ":0 lets do not erase their RHS" -discovered = "2026-04-10" -fixed = "2026-04-10" -description = """ -let x :0 = expensive_proof_term() in body_not_using_x previously evaluated -expensive_proof_term() and consumed its resources. The QTT-orthodox scaled -Let rule scales the value context by 0, producing the zero context, which -means the RHS carries no runtime obligations and may be erased. -""" -fix = "Closed via the same scaled-Let infrastructure as BUG-001. The quantity-checking pass now correctly drops usage contributions from the RHS of an @erased-bound let (scale_usage QZero _ = UZero), so any linear variables consumed in the RHS no longer count against their owner. The optional interpreter-side erasure (skipping eval of @erased-bound RHS) is deferred — it is an optimisation, not a correctness requirement." -verification = "Covered transitively by the BUG-001 fixtures: the same scaling logic that closes BUG-001 closes BUG-002. A focused @erased fixture is on the follow-up backlog but not required to declare the bug fixed at the rule level." - -[[closed-bug]] -id = "BUG-003" -severity = "medium" -category = "semantics" -title = "eval_list evaluates right-to-left via List.fold_right" -discovered = "2026-04-10" -fixed = "2026-04-10" -description = """ -lib/interp.ml:347 eval_list previously used List.fold_right with monadic -bind, which in strict OCaml evaluates arguments right-to-left. ExprBinary -was already left-to-right, creating internal evaluator inconsistency and a -latent divergence point for future effects/affine enforcement. See ADR-003. -""" -fix = "Replaced fold_right with an explicit L-to-R recursive loop that accumulates results in reverse and reverses at the end. See lib/interp.ml `eval_list` (~12 LOC including the doc comment referencing ADR-003)." -regression-test-status = "deferred — observable order verification requires either the mutable-binding interpreter path (currently broken in baseline; the failing 'lambda'/'simple eval' interp tests share the same root cause) or a side-effecting builtin reachable from a fixture. Both routes are out of scope for the BUG-003 fix and should be revisited once the interpreter mutation path is repaired." -verification = "Code review against ADR-003 + dune build clean + dune runtest baseline-equivalent (22 pre-existing failures, none new, none resolved)." - -[[closed-bug]] -id = "BUG-004" -severity = "medium" -category = "soundness" -title = "Lambda-body usage not tracked against outer captures" -discovered = "2026-04-10" -fixed = "2026-04-11" -description = """ -lib/quantity.ml ExprLambda was a no-op for outer variable usage: captures -were not scaled by QOmega, so a lambda capturing a linear variable did not -raise LinearVariableUsedMultiple. Borrow checker also ignored captures, -allowing move of a captured variable after lambda creation. -""" -fix = """ -Two coordinated fixes (commit 48422d1): -1. quantity.ml ExprLambda: snapshot env, walk body (shadowing lambda params), - compute per-variable delta, restore env, re-apply delta scaled by QOmega. - scale_usage QOmega UOne = UMany → LinearVariableUsedMultiple for linear captures. -2. borrow.ml ExprLambda: collect_free walker finds all free variables in body, - creates Shared borrows for each captured place. Move of captured variable - → MoveWhileBorrowed. Borrows expire at block exit via lexical-lifetime clearing. - borrow_kind_name replaces show_borrow_kind for readable error messages. -""" -verification = "Manual smoke tests: lambda capturing moved value → 'cannot move `v` while shared-borrowed'; valid lambda (no owned captures) → passes. 73/73 tests pass." - -[[closed-bug]] -id = "BUG-005" -severity = "medium" -category = "codegen" -title = "WasmGC backend silently drops unknown function calls" -discovered = "2026-04-10" -fixed = "2026-04-11" -description = """ -lib/codegen_gc.ml ExprApp handling silently emitted 'drop all args + push null' -when the callee was not in the func_indices table, and did the same for -indirect calls. This was placeholder behaviour that produced wrong code rather -than failing loudly. -""" -fix = "Both fallback arms replaced with explicit CodegenError. Direct-call miss → Error (UnboundFunction id.name). Indirect/higher-order callee → Error (UnsupportedFeature \"indirect / higher-order call in WasmGC backend (call_ref not yet implemented)\"). New UnboundFunction variant added to codegen_error and format_codegen_error. commit b58178a." -regression-test-status = "landed 2026-05-23 — test/test_e2e.ml `test_gc_unbound_function_loud_fail` in `wasm_gc_loud_fail_tests`. Parses `fn main() -> Int { return totally_undefined_callee(42); }`, feeds it directly to Codegen_gc.generate_gc_module (no resolve step), asserts the emitted error message contains 'totally_undefined_callee' — which UnboundFunction's format string includes verbatim. Pins the explicit-error path so a regression to silent drop+null would fail the gate." - -# ─── Deferred upgrade: bring .machine_readable/ to current standards ────────── -# AffineScript predates most of the current hyperpolymath .machine_readable/ -# suite. The 6a2 core is present; the surrounding machinery is not. This is -# logged as deferred work, separate from the Manhattan recovery, to avoid -# scope creep into that plan. - -[deferred-upgrade] -# DONE 2026-04-12 — suite upgrade completed in dedicated session (no .ml changes) -status = "done" -completed = "2026-04-12" - -added = [ - ".machine_readable/CLADE.a2ml", - ".machine_readable/contractiles/ (migrated from root via git mv — history preserved)", - ".machine_readable/contractiles/adjust/Adjustfile.a2ml", - ".machine_readable/contractiles/intend/Intentfile.a2ml", - ".machine_readable/agent_instructions/README.adoc", - ".machine_readable/agent_instructions/coverage.a2ml", - ".machine_readable/agent_instructions/debt.a2ml", - ".machine_readable/agent_instructions/methodology.a2ml", - ".machine_readable/integrations/verisimdb.a2ml", - ".machine_readable/integrations/vexometer.a2ml", - ".machine_readable/ADJUST.contractile", - ".machine_readable/INTENT.contractile", - ".machine_readable/MUST.contractile", - ".machine_readable/TRUST.contractile" -] - -[[session-delta]] -# Appended 2026-05-31 — bindings/stdlib roadmap closeout session (PR #474). -# Mirror entry only; authoritative status remains docs/CAPABILITY-MATRIX.adoc -# + the per-row status in docs/{bindings,stdlib}-roadmap.adoc. Live test -# counts come from `dune runtest --force`, never hard-coded here (DOC-05, #176). -date = "2026-05-31" -pr = "#474" -narrative = "docs/history/BINDINGS-STDLIB-CODEGEN-2026-05-31.{adoc,a2ml}" -landed = [ - "bindings #19 (Zig C-ABI FFI patterns) — docs/specs/zig-ffi-patterns.adoc; roadmap row ◐->●", - "stdlib #25 (hex + RFC4648 base64) — stdlib/encoding.affine; roadmap row ○->◑ (Bytes overloads deferred to #30)", - "compiler: string_char_code_at / string_from_char_code byte builtins (resolve/typecheck/interp/codegen_deno)", - "codegen fix #478 — Int/Int truncates via Math.trunc on Deno-ESM (Float/Float untouched); js/node/wasm follow-ups tracked in #478", - "docs: bindings/stdlib/alib roadmap Tracking sections de-staled to live umbrellas #446/#412/#413" -] -umbrella-446 = "stays open — STEP 1-3 done, STEP 4/5 long-running" diff --git a/.machine_readable/TRUST.contractile b/.machine_readable/TRUST.contractile index a1f855ab..f1ee48c3 100644 --- a/.machine_readable/TRUST.contractile +++ b/.machine_readable/TRUST.contractile @@ -26,7 +26,7 @@ (deny "push without confirmation in the same session") (deny "add a language feature not in ANCHOR.a2ml thesis") (deny "reverse any ADR (ADR-001 through ADR-010) without a new ADR") - (deny "delete or overwrite the 6a2/ core files (STATE/META/ECOSYSTEM/AGENTIC/NEUROSYM/PLAYBOOK)") + (deny "delete or overwrite the descriptiles/ core files (STATE/META/ECOSYSTEM/AGENTIC/NEUROSYM/PLAYBOOK)") (deny "modify IDApTIK without explicit instruction (AGPL — separate repo, separate session)") ) diff --git a/.machine_readable/anchors/ANCHOR.a2ml b/.machine_readable/anchors/ANCHOR.a2ml index 96427e39..663b0d4c 100644 --- a/.machine_readable/anchors/ANCHOR.a2ml +++ b/.machine_readable/anchors/ANCHOR.a2ml @@ -61,7 +61,7 @@ thesis: authorities: scope-statement: - - ".machine_readable/6a2/META.a2ml (ADRs)" + - ".machine_readable/descriptiles/META.a2ml (ADRs)" - "~/Desktop/Frontier_Programming_Practices_AffineScript/AI.a2ml (v2.0)" human-guide: diff --git a/0-AI-MANIFEST.a2ml b/0-AI-MANIFEST.a2ml index e8ab0b19..c361cad3 100644 --- a/0-AI-MANIFEST.a2ml +++ b/0-AI-MANIFEST.a2ml @@ -4,7 +4,7 @@ Project metadata and AI agent configuration files live under `.machine_readable/`. All metadata files use the `.a2ml` extension; the `.scm` extension is reserved for Guix (`guix.scm`, `.guix-channel`). -## Core 6a2 metadata (`.machine_readable/6a2/`) +## Core descriptiles metadata (`.machine_readable/descriptiles/`) - **STATE.a2ml** - Current project state, progress, blockers, next actions, session notes - **ECOSYSTEM.a2ml** - Position in ecosystem, relationships to sibling projects @@ -29,7 +29,7 @@ Project metadata and AI agent configuration files live under `.machine_readable/ ## Workflow -1. Read `.machine_readable/6a2/*.a2ml` files at session start +1. Read `.machine_readable/descriptiles/*.a2ml` files at session start 2. Check `STATE.a2ml` for current phase, blockers, next actions 3. Follow patterns and constraints from `AGENTIC.a2ml` 4. Follow methodology gates from `agent_instructions/methodology.a2ml` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 41d3f311..16cd75a6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,7 @@ Thank you for your interest in AffineScript — a practical language for resource-safe systems, compiling to typed WebAssembly. This guide covers how to set up a working tree, file useful bugs, and submit changes. -For the language itself, start from [`README.adoc`](README.adoc). For project state, blockers, and next-actions, see [`.machine_readable/6a2/STATE.a2ml`](.machine_readable/6a2/STATE.a2ml). +For the language itself, start from [`README.adoc`](README.adoc). For project state, blockers, and next-actions, see [`.machine_readable/descriptiles/STATE.a2ml`](.machine_readable/descriptiles/STATE.a2ml). --- @@ -75,7 +75,7 @@ affinescript/ ### Suggesting Features **Before suggesting:** -1. Skim [`docs/ROADMAP.adoc`](docs/ROADMAP.adoc) and `.machine_readable/6a2/STATE.a2ml`. +1. Skim [`docs/ROADMAP.adoc`](docs/ROADMAP.adoc) and `.machine_readable/descriptiles/STATE.a2ml`. 2. Search existing issues and discussions. **When suggesting:** use the [feature request template](.github/ISSUE_TEMPLATE/feature_request.md) and include: diff --git a/RSR_COMPLIANCE.adoc b/RSR_COMPLIANCE.adoc index 102a6bb0..6256dec6 100644 --- a/RSR_COMPLIANCE.adoc +++ b/RSR_COMPLIANCE.adoc @@ -34,7 +34,7 @@ toc::[] |`.well-known/humans.txt` |✅ |Human-readable maintainer / contributor surface. |`guix.scm` or `flake.nix` |⚠️ |Neither file currently committed; the `affinescript.opam` package descriptor + the global `justfile` recipes carry the build contract today. Adding a Guix channel definition is a planned follow-up (estate primary is Guix per RSR-OUTLINE §"Language Tiers"); a Nix fallback would be acceptable for early CI. |`0-AI-MANIFEST.a2ml` |✅ |Adopted 2026-05-25 (PR #359) replacing the older `AI.a2ml` per Hypatia `root_hygiene` rule. Matches sibling-repo convention (`hyperpolymath/road-skate`, `affinescript-vite/`, `affinescriptiser/` all carry the same). -|`STATE.scm` |❌ |*Documented exemption.* This repo's `.scm` extension is reserved for Guix (`guix.scm`, `.guix-channel`) per the language policy in `0-AI-MANIFEST.a2ml`. Project state lives at `.machine_readable/6a2/STATE.a2ml` in the canonical 6a2 schema (matches sibling repos in the same estate). Substituting `.a2ml` for `.scm` is the cross-repo standard, not an AffineScript outlier — see also `hyperpolymath/road-skate`'s `.machine_readable/STATE.a2ml`. +|`STATE.scm` |❌ |*Documented exemption.* This repo's `.scm` extension is reserved for Guix (`guix.scm`, `.guix-channel`) per the language policy in `0-AI-MANIFEST.a2ml`. Project state lives at `.machine_readable/descriptiles/STATE.a2ml` in the canonical descriptiles schema (matches sibling repos in the same estate). Substituting `.a2ml` for `.scm` is the cross-repo standard, not an AffineScript outlier — see also `hyperpolymath/road-skate`'s `.machine_readable/STATE.a2ml`. |`RSR_COMPLIANCE.adoc` |✅ |This file. Added 2026-05-25. |=== diff --git a/docs/CAPABILITY-MATRIX.adoc b/docs/CAPABILITY-MATRIX.adoc index d31612f0..2c51efa2 100644 --- a/docs/CAPABILITY-MATRIX.adoc +++ b/docs/CAPABILITY-MATRIX.adoc @@ -13,7 +13,7 @@ readiness.* Where any other document (`README.adoc`, `docs/architecture/BACKEND-IMPLEMENTATION.md`, `docs/reference/COMPILER-CAPABILITIES.md`, `docs/history/ALPHA-1-RELEASE-NOTES.md`, `docs/ROADMAP.adoc`, -`.machine_readable/6a2/STATE.a2ml`) +`.machine_readable/descriptiles/STATE.a2ml`) disagrees with this matrix, *this matrix wins* and the other document is drift to be corrected. Reconstructed and verified 2026-05-19 against `origin/main`@dc5b817 with the compiler as oracle. Tracks issue diff --git a/docs/EFFECTS-IMPLEMENTATION.adoc b/docs/EFFECTS-IMPLEMENTATION.adoc index 7fefd294..3590872c 100644 --- a/docs/EFFECTS-IMPLEMENTATION.adoc +++ b/docs/EFFECTS-IMPLEMENTATION.adoc @@ -5,7 +5,7 @@ == Status Sync (2026-04-12) This document tracks the implementation status of algebraic effects in -AffineScript and is aligned with `.machine++_++readable/6a2/STATE.a2ml`. +AffineScript and is aligned with `.machine++_++readable/descriptiles/STATE.a2ml`. == Syntax (Current Parser Form) diff --git a/docs/NAVIGATION.adoc b/docs/NAVIGATION.adoc index 9d02adeb..ad9f85c2 100644 --- a/docs/NAVIGATION.adoc +++ b/docs/NAVIGATION.adoc @@ -9,7 +9,7 @@ This guide helps you navigate the AffineScript repository structure. * *New users* — link:tutorial/lesson-01-hello.adoc[tutorial (lessons 1–10)], link:guides/warmup/[runnable warm-ups], link:../wiki/language-reference/[language reference]. * *Developers / contributors* — this guide, link:../wiki/compiler/architecture.md[compiler architecture], link:decisions/[ADRs], link:governance/CONTRIBUTING.adoc[contributing]. -* *Maintainers* — link:governance/MAINTAINERS.adoc[maintainers], link:standards/RELEASE.adoc[release process], link:../.machine_readable/6a2/PLAYBOOK.a2ml[ops playbook], link:SOUNDNESS.adoc[soundness ledger]. +* *Maintainers* — link:governance/MAINTAINERS.adoc[maintainers], link:standards/RELEASE.adoc[release process], link:../.machine_readable/descriptiles/PLAYBOOK.a2ml[ops playbook], link:SOUNDNESS.adoc[soundness ledger]. == Quick Start Files (Root Level) diff --git a/docs/ROADMAP.adoc b/docs/ROADMAP.adoc index ffffdd62..fefa6bd8 100644 --- a/docs/ROADMAP.adoc +++ b/docs/ROADMAP.adoc @@ -18,7 +18,7 @@ order: *Ecosystem Integration Track* INT-01..12. * `docs/TECH-DEBT.adoc` — the coordination ledger (DOC/CORE/STDLIB/INT/SAT). -`.machine_readable/6a2/STATE.a2ml` mirrors these; it does not lead, and is +`.machine_readable/descriptiles/STATE.a2ml` mirrors these; it does not lead, and is currently flagged stale (DOC-05, issue #176). ==== diff --git a/docs/STATE-2026-05-26.adoc b/docs/STATE-2026-05-26.adoc index 31521bc9..2ae03e7f 100644 --- a/docs/STATE-2026-05-26.adoc +++ b/docs/STATE-2026-05-26.adoc @@ -157,7 +157,7 @@ This is important and routinely misunderstood: Canonical disambiguation: https://github.com/hyperpolymath/nextgen-languages/blob/main/docs/disambiguation/ephapax-vs-affinescript.md -Repo-level hooks landed 2026-05-26: README admonitions in affinescript (PR #393), ephapax (ephapax#152), typed-wasm (typed-wasm#73); CLAUDE.md disambiguation sections; `.machine_readable/6a2/AGENTIC.a2ml` `[disambiguation]` blocks; hypatia DR001 advisory rule (hypatia#347). +Repo-level hooks landed 2026-05-26: README admonitions in affinescript (PR #393), ephapax (ephapax#152), typed-wasm (typed-wasm#73); CLAUDE.md disambiguation sections; `.machine_readable/descriptiles/AGENTIC.a2ml` `[disambiguation]` blocks; hypatia DR001 advisory rule (hypatia#347). == Distance to 1.0 (per ROADMAP timeline) diff --git a/docs/TECH-DEBT.adoc b/docs/TECH-DEBT.adoc index 09d327e7..3e2db99a 100644 --- a/docs/TECH-DEBT.adoc +++ b/docs/TECH-DEBT.adoc @@ -66,7 +66,7 @@ lines 25/614 also fixed — pointed both at CAPABILITY-MATRIX). MONITOR posture stays live via DOC-08/09 |DOC-05 |`STATE.a2ml` drift flagged (it mirrors, does not lead) |*DONE in tree* 2026-05-21 (`authoritative-status-doc` + `drift-flag` keys -present in `.machine_readable/6a2/STATE.a2ml`; README "Documentation" +present in `.machine_readable/descriptiles/STATE.a2ml`; README "Documentation" section now also labels STATE.a2ml as the mirror) |DOC-06 |`ROADMAP.adoc` Ecosystem Integration Track (INT-01..12) cross-links this ledger |*DONE in tree* 2026-05-21 (ROADMAP.adoc §"Status & @@ -460,7 +460,7 @@ Widening roadmap (tranches A..E, recommended sequencing): link:specs/TYPED-WASM-ROADMAP.adoc[specs/TYPED-WASM-ROADMAP.adoc]. Proposed ADRs: ADR-020 (schema versioning), ADR-021 (multi-producer coordination model) — both PROPOSED in -`.machine_readable/6a2/META.a2ml`. +`.machine_readable/descriptiles/META.a2ml`. [cols="1,3,1,2"] |=== diff --git a/docs/ci/required-checks.adoc b/docs/ci/required-checks.adoc index 52309aff..aebf271f 100644 --- a/docs/ci/required-checks.adoc +++ b/docs/ci/required-checks.adoc @@ -7,7 +7,7 @@ ==== *Sketch / operational note* (seeded 2026-06-21). Captures a CI failure class that blocked merges across the estate and the rule that prevents it. Pairs with -the machine entry in `.machine_readable/6a2/PLAYBOOK.a2ml` (`[ci-required-checks]`). +the machine entry in `.machine_readable/descriptiles/PLAYBOOK.a2ml` (`[ci-required-checks]`). ==== toc::[] diff --git a/docs/decisions/0019-compiler-distribution.adoc b/docs/decisions/0019-compiler-distribution.adoc index 6548d00c..5a5da428 100644 --- a/docs/decisions/0019-compiler-distribution.adoc +++ b/docs/decisions/0019-compiler-distribution.adoc @@ -9,7 +9,7 @@ Jonathan D.A. Jewell Status:: Accepted Date:: 2026-05-19 (accepted); implementation S1–S4 landed 2026-05-20 Issue:: https://github.com/hyperpolymath/affinescript/issues/260[INT-04 #260] (split from https://github.com/hyperpolymath/affinescript/issues/181[#181]) -Series:: Settled-decisions ledger; companion entry in `.machine_readable/6a2/META.a2ml [[adr]] id="ADR-019"`; narrative in `docs/specs/SETTLED-DECISIONS.adoc` §"Compiler Distribution". +Series:: Settled-decisions ledger; companion entry in `.machine_readable/descriptiles/META.a2ml [[adr]] id="ADR-019"`; narrative in `docs/specs/SETTLED-DECISIONS.adoc` §"Compiler Distribution". == Context @@ -125,7 +125,7 @@ tag-time bake step (#297/#300). this record. * `docs/PACKAGING.adoc` §"The compiler itself — decided (ADR-019 / #260)". * `docs/TECH-DEBT.adoc` INT-04 / INT-10 rows. -* `.machine_readable/6a2/META.a2ml [[adr]] id="ADR-019"` — the machine record. +* `.machine_readable/descriptiles/META.a2ml [[adr]] id="ADR-019"` — the machine record. * `.github/workflows/release.yml` (S2 producer), `.github/workflows/publish-jsr.yml` (owner-gated shim publish), `packages/affinescript-cli/` (the shim). diff --git a/docs/decisions/0022-polonius-origin-variables.adoc b/docs/decisions/0022-polonius-origin-variables.adoc index 47060039..7dfb423f 100644 --- a/docs/decisions/0022-polonius-origin-variables.adoc +++ b/docs/decisions/0022-polonius-origin-variables.adoc @@ -9,7 +9,7 @@ Jonathan D.A. Jewell Status:: Accepted (architecture ratified in PR #407 thread, 2026-05-27; staged implementation tracked in #553) Date:: 2026-05-26 Issue:: https://github.com/hyperpolymath/affinescript/issues/177[CORE-01 #177] -Series:: Settled-decisions ledger; companion entry in `.machine_readable/6a2/META.a2ml`. +Series:: Settled-decisions ledger; companion entry in `.machine_readable/descriptiles/META.a2ml`. == Context @@ -339,7 +339,7 @@ At each milestone: binding), #396 (Slice C′ loop soundness), #397 (Slice D captured-linears). * `docs/STATE-2026-05-26.adoc` §"Dominant cost item" / §"Critical path". * `docs/CAPABILITY-MATRIX.adoc` CORE-01 row. -* `.machine_readable/6a2/META.a2ml [[adr]] id="ADR-022"`. +* `.machine_readable/descriptiles/META.a2ml [[adr]] id="ADR-022"`. * `docs/specs/SETTLED-DECISIONS.adoc` (section to be added on ratification). * Polonius reference: Niko Matsakis, "An alias-based formulation of the borrow checker", 2018 (the `subset/3` + `loan_live_at/2` shape, naive diff --git a/docs/decisions/0023-finality-types.adoc b/docs/decisions/0023-finality-types.adoc index 7136cc86..856dac5c 100644 --- a/docs/decisions/0023-finality-types.adoc +++ b/docs/decisions/0023-finality-types.adoc @@ -9,7 +9,7 @@ Jonathan D.A. Jewell Status:: Draft (stub — design-only; no implementation authorised by this document) Date:: 2026-06-11 Issue:: https://github.com/hyperpolymath/affinescript/issues/554[#554 (soundness gate)], https://github.com/hyperpolymath/affinescript/issues/553[#553 (ADR-022 sequencing)] -Series:: Settled-decisions ledger; companion entry to be added to `.machine_readable/6a2/META.a2ml` at ratification. +Series:: Settled-decisions ledger; companion entry to be added to `.machine_readable/descriptiles/META.a2ml` at ratification. == The hard question, on page one diff --git a/docs/history/MODULE-SYSTEM-PROGRESS.md b/docs/history/MODULE-SYSTEM-PROGRESS.md index 65435366..425907af 100644 --- a/docs/history/MODULE-SYSTEM-PROGRESS.md +++ b/docs/history/MODULE-SYSTEM-PROGRESS.md @@ -360,7 +360,7 @@ have real modules, or stay flat-and-deduplicated? **Decision: real modules with qualified paths.** Not a flat de-duplicated prelude. Status: **accepted, settled** (ADR-011 in -`.machine_readable/6a2/META.a2ml`; ledger entry in +`.machine_readable/descriptiles/META.a2ml`; ledger entry in `docs/specs/SETTLED-DECISIONS.adoc`). ### Rationale diff --git a/docs/history/SESSION-HANDOFF-2026-05-27.adoc b/docs/history/SESSION-HANDOFF-2026-05-27.adoc index 5acb2957..c4121f02 100644 --- a/docs/history/SESSION-HANDOFF-2026-05-27.adoc +++ b/docs/history/SESSION-HANDOFF-2026-05-27.adoc @@ -18,7 +18,7 @@ Actions queue drain. == What this session actually did 1. Drafted **ADR-022** at `docs/decisions/0022-polonius-origin-variables.adoc` - + a structured `[[adr]]` block in `.machine_readable/6a2/META.a2ml`, + + a structured `[[adr]]` block in `.machine_readable/descriptiles/META.a2ml`, shipped as draft PR **#398**. 2. Cut a minimal **M1 sketch** for ADR-022 (`origin_var option` on `TyRef`/`TyMut` + a `lib/borrow_polonius/` sublibrary skeleton) — kept diff --git a/docs/history/TYPECHECKER-COMPLETION-2026-01-23.adoc b/docs/history/TYPECHECKER-COMPLETION-2026-01-23.adoc index ac03b7cb..a8c2fbdb 100644 --- a/docs/history/TYPECHECKER-COMPLETION-2026-01-23.adoc +++ b/docs/history/TYPECHECKER-COMPLETION-2026-01-23.adoc @@ -2,7 +2,7 @@ // SPDX-FileCopyrightText: 2024-2026 hyperpolymath (Jonathan D.A. Jewell ) ____ Historical snapshot from 2026-01-23. Superseded by -.machine++_++readable/6a2/STATE.a2ml. Retained for audit trail only. +.machine++_++readable/descriptiles/STATE.a2ml. Retained for audit trail only. ____ = Type Checker Completion Report diff --git a/docs/history/TYPED-WASM-LEVEL-10-CLOSURE-2026-04-19.adoc b/docs/history/TYPED-WASM-LEVEL-10-CLOSURE-2026-04-19.adoc index fc767972..f5dd0997 100644 --- a/docs/history/TYPED-WASM-LEVEL-10-CLOSURE-2026-04-19.adoc +++ b/docs/history/TYPED-WASM-LEVEL-10-CLOSURE-2026-04-19.adoc @@ -149,4 +149,4 @@ not a typed-wasm gap. * Submodule bumps: `nextgen-languages@61f69e5`, `nextgen-languages@24a3205` * Sibling doc commit: `developer-ecosystem@b7fbe5f` (deno-test README + regression) * `KNOWN-ISSUES.md` — flipped to RESOLVED with root-cause summary retained for history -* `.machine_readable/6a2/STATE.a2ml` — session-note-2026-04-19-a +* `.machine_readable/descriptiles/STATE.a2ml` — session-note-2026-04-19-a diff --git a/docs/reports/AFFINESCRIPT-ROADMAP-STATUS-2026-04-13.adoc b/docs/reports/AFFINESCRIPT-ROADMAP-STATUS-2026-04-13.adoc index f81eead2..c6238a3a 100644 --- a/docs/reports/AFFINESCRIPT-ROADMAP-STATUS-2026-04-13.adoc +++ b/docs/reports/AFFINESCRIPT-ROADMAP-STATUS-2026-04-13.adoc @@ -243,7 +243,7 @@ implementation, not source analysis tools. === ⏳ .machine++_++readable Suite Upgrade * Status: Completed 2026-04-12 -* All 6a2 core present +* All descriptiles core present * Surrounding machinery added === ⏳ Performance Benchmarks diff --git a/docs/spec.adoc b/docs/spec.adoc index dbc13426..d4e55493 100644 --- a/docs/spec.adoc +++ b/docs/spec.adoc @@ -14,7 +14,7 @@ project. *The authoritative current scope lives in:* - `.machine++_++readable/anchors/ANCHOR.a2ml` (canonical identity {plus} -thesis) - `.machine++_++readable/6a2/META.a2ml` (architecture decision +thesis) - `.machine++_++readable/descriptiles/META.a2ml` (architecture decision records) - `~/Desktop/Frontier++_++Programming++_++Practices++_++AffineScript/AI.a2ml` (v2.0) - diff --git a/docs/specs/SETTLED-DECISIONS.adoc b/docs/specs/SETTLED-DECISIONS.adoc index 69da63ae..5239dac4 100644 --- a/docs/specs/SETTLED-DECISIONS.adoc +++ b/docs/specs/SETTLED-DECISIONS.adoc @@ -7,7 +7,7 @@ Jonathan D.A. Jewell This document records design decisions that are fully settled. Each entry gives the decision, the reasoning, and a pointer to the full ADR in -`.machine_readable/6a2/META.a2ml`. +`.machine_readable/descriptiles/META.a2ml`. These are not open questions. Do not reopen them without amending the ADR. @@ -153,7 +153,7 @@ the machinery the compiler (`module_loader.ml`, `module`/`use`/`::` grammar) and the newer stdlib files (Core, Deno, Vscode, …) already use. Settles issue #132; gates #133/#135/#137/#138. Full ADR in -`.machine_readable/6a2/META.a2ml` (ADR-011). +`.machine_readable/descriptiles/META.a2ml` (ADR-011). == Grammar Changes Are Correctness Assertions; Parser-Conflict Disclosure (ADR-012) @@ -210,7 +210,7 @@ full raw output. Nothing is suppressed at the parser-generator level build is byte-for-byte unchanged and fully transparent on demand. Settles the disposition of issue #215 residual families. Full ADR in -`.machine_readable/6a2/META.a2ml` (ADR-012). +`.machine_readable/descriptiles/META.a2ml` (ADR-012). == Async on WasmGC: Transparent CPS Continuation Transform (ADR-013) @@ -242,7 +242,7 @@ The `Thenable`-handle + `thenableThen` continuation protocol is the agreed async ABI for the typed-wasm convergence layer; Ephapax is a co-stakeholder (typed-wasm ADR-004). Delivered as 4 incremental, gated PRs. Full design in `docs/specs/async-on-wasm-cps.adoc`; full ADR -in `.machine_readable/6a2/META.a2ml` (ADR-013). +in `.machine_readable/descriptiles/META.a2ml` (ADR-013). == Module-Qualified Type/Effect Path Separator (ADR-014) @@ -268,7 +268,7 @@ The grammar PR unblocks *parsing*; most estate parse failures clear with zero consumer churn (genuine ReScript-surface residue is #229). This decision is settled; do not reopen without amending the ADR. Full -ADR in `.machine_readable/6a2/META.a2ml` (ADR-014); ledger CORE-03 in +ADR in `.machine_readable/descriptiles/META.a2ml` (ADR-014); ledger CORE-03 in `docs/TECH-DEBT.adoc`. == WASI Preview2 / WASM Component-Model Migration (ADR-015) @@ -322,7 +322,7 @@ native-preview2 cleanup that replaces the on-demand preview1 shims emitted by S4a/S4b/S6b with direct `wasi:*` calls. This decision is settled; do not reopen without amending the ADR. Full -ADR in `.machine_readable/6a2/META.a2ml` (ADR-015); ledger INT-03 in +ADR in `.machine_readable/descriptiles/META.a2ml` (ADR-015); ledger INT-03 in `docs/TECH-DEBT.adoc`; roadmap in `docs/ECOSYSTEM.adoc`. == Effect-Threaded Async-Boundary Detection (ADR-016) @@ -356,7 +356,7 @@ triggers the transform; *S4* retire the hardcoded set (fallback kept for table-miss only). This decision is settled; do not reopen without amending the ADR. Full -ADR in `.machine_readable/6a2/META.a2ml` (ADR-016); ledger #234 / +ADR in `.machine_readable/descriptiles/META.a2ml` (ADR-016); ledger #234 / CORE-02 in `docs/TECH-DEBT.adoc`. == ReScript Block-Module Disposition: One Module Per File (ADR-017) @@ -390,7 +390,7 @@ loader-bridge territory), tracked in `RESCRIPT-ELIMINATION.adoc` Tier-4 and the INT-02 ledger, never conflated with this disposition. This decision is settled; do not reopen without amending the ADR. Full -ADR in `.machine_readable/6a2/META.a2ml` (ADR-017); #229 canonical map in +ADR in `.machine_readable/descriptiles/META.a2ml` (ADR-017); #229 canonical map in `docs/RESCRIPT-ELIMINATION.adoc`; escalation issue #262. == No Raw/FFI Escape: Typed `extern` Is the Only Host Bridge (ADR-018) @@ -423,7 +423,7 @@ parts of `burble`) port under this doctrine; per-file execution is the #229 per-repo work. This decision is settled; do not reopen without amending the ADR. Full -ADR in `.machine_readable/6a2/META.a2ml` (ADR-018); #229 canonical map in +ADR in `.machine_readable/descriptiles/META.a2ml` (ADR-018); #229 canonical map in `docs/RESCRIPT-ELIMINATION.adoc`; escalation issue #245. == Compiler Distribution: GitHub Releases Binaries + Thin Deno/JSR Shim (ADR-019) @@ -456,6 +456,6 @@ tests, publish owner-gated via the existing manual JSR workflow; *S4* wire INT-10 `affinescript-lsp` onto the shim. This decision is settled; do not reopen without amending the ADR. Full -ADR in `.machine_readable/6a2/META.a2ml` (ADR-019) and the per-file +ADR in `.machine_readable/descriptiles/META.a2ml` (ADR-019) and the per-file decision record link:../decisions/0019-compiler-distribution.adoc[docs/decisions/0019-compiler-distribution.adoc]; ledger #260 / INT-10 in `docs/TECH-DEBT.adoc`. diff --git a/docs/specs/TYPED-WASM-COORDINATION-LEDGER.a2ml b/docs/specs/TYPED-WASM-COORDINATION-LEDGER.a2ml index 4c991642..a8e2ba0a 100644 --- a/docs/specs/TYPED-WASM-COORDINATION-LEDGER.a2ml +++ b/docs/specs/TYPED-WASM-COORDINATION-LEDGER.a2ml @@ -67,7 +67,7 @@ state-since = "2026-05-24" owner = "@hyperpolymath" [queue-entry.adr] -local = { repo = "hyperpolymath/affinescript", id = "ADR-020", status = "accepted", file = ".machine_readable/6a2/META.a2ml" } +local = { repo = "hyperpolymath/affinescript", id = "ADR-020", status = "accepted", file = ".machine_readable/descriptiles/META.a2ml" } mirror-typed-wasm = { repo = "hyperpolymath/typed-wasm", status = "to-be-filed" } mirror-ephapax = { repo = "hyperpolymath/ephapax", status = "to-be-filed" } @@ -115,8 +115,8 @@ items = ["sister-repo ADR mirrors", "verifier-parse-support PRs in typed-wasm + # ────────────────────────────────────────────────────────────────────────────── [cross-references] -adr-020 = ".machine_readable/6a2/META.a2ml [[adr]] id=ADR-020" -adr-021 = ".machine_readable/6a2/META.a2ml [[adr]] id=ADR-021" +adr-020 = ".machine_readable/descriptiles/META.a2ml [[adr]] id=ADR-020" +adr-021 = ".machine_readable/descriptiles/META.a2ml [[adr]] id=ADR-021" interface-spec = "docs/specs/TYPED-WASM-INTERFACE.adoc" roadmap = "docs/specs/TYPED-WASM-ROADMAP.adoc" human-companion = "docs/specs/TYPED-WASM-COORDINATION-LEDGER.adoc" diff --git a/docs/specs/TYPED-WASM-COORDINATION-LEDGER.adoc b/docs/specs/TYPED-WASM-COORDINATION-LEDGER.adoc index 2be21d90..ba9c10e5 100644 --- a/docs/specs/TYPED-WASM-COORDINATION-LEDGER.adoc +++ b/docs/specs/TYPED-WASM-COORDINATION-LEDGER.adoc @@ -159,7 +159,7 @@ ledger entry*, never by silent drift. * link:TYPED-WASM-ROADMAP.adoc[TYPED-WASM-ROADMAP.adoc] — widening tranches; this ledger is the apparatus that makes tranche D land safely. -* `.machine_readable/6a2/META.a2ml` — ADR-020 (accepted), ADR-021 +* `.machine_readable/descriptiles/META.a2ml` — ADR-020 (accepted), ADR-021 (accepted). * link:TYPED-WASM-COORDINATION-LEDGER.a2ml[TYPED-WASM-COORDINATION-LEDGER.a2ml] — machine-readable companion. diff --git a/docs/specs/TYPED-WASM-ROADMAP.a2ml b/docs/specs/TYPED-WASM-ROADMAP.a2ml index ee98047c..378882b3 100644 --- a/docs/specs/TYPED-WASM-ROADMAP.a2ml +++ b/docs/specs/TYPED-WASM-ROADMAP.a2ml @@ -298,5 +298,5 @@ current-spec = "docs/specs/TYPED-WASM-INTERFACE.adoc" ledger = "docs/TECH-DEBT.adoc" ecosystem = "docs/ECOSYSTEM.adoc" settled-decisions = "docs/specs/SETTLED-DECISIONS.adoc" -adr-store = ".machine_readable/6a2/META.a2ml" +adr-store = ".machine_readable/descriptiles/META.a2ml" proposed-adrs = ["ADR-020 (schema versioning)", "ADR-021 (multi-producer coordination model)"] diff --git a/docs/specs/TYPED-WASM-ROADMAP.adoc b/docs/specs/TYPED-WASM-ROADMAP.adoc index 39bce663..443a3ab0 100644 --- a/docs/specs/TYPED-WASM-ROADMAP.adoc +++ b/docs/specs/TYPED-WASM-ROADMAP.adoc @@ -394,7 +394,7 @@ Pragmatic order, smallest-blast-radius first: * link:../TECH-DEBT.adoc[../TECH-DEBT.adoc] — CONV-01..05 ledger units; INT-12 entry. * link:../ECOSYSTEM.adoc[../ECOSYSTEM.adoc] — A–E spine. -* `.machine_readable/6a2/META.a2ml` — ADR-020 / ADR-021 (proposed, +* `.machine_readable/descriptiles/META.a2ml` — ADR-020 / ADR-021 (proposed, pending owner ratification). * link:TYPED-WASM-ROADMAP.a2ml[TYPED-WASM-ROADMAP.a2ml] — machine-readable companion. diff --git a/docs/specs/async-on-wasm-cps.adoc b/docs/specs/async-on-wasm-cps.adoc index f0c9969d..fe3e0bad 100644 --- a/docs/specs/async-on-wasm-cps.adoc +++ b/docs/specs/async-on-wasm-cps.adoc @@ -8,7 +8,7 @@ Jonathan D.A. Jewell == Status ACCEPTED (owner-approved 2026-05-18). Recorded as ADR-013 -(`SETTLED-DECISIONS.adoc` + `.machine_readable/6a2/META.a2ml`). The +(`SETTLED-DECISIONS.adoc` + `.machine_readable/descriptiles/META.a2ml`). The async ABI section is mirrored into the typed-wasm convergence layer with an Ephapax co-stakeholder review flag (typed-wasm ADR-004 — tracked separately). Implementation proceeds as the 4-PR plan below, diff --git a/docs/standards/PANIC-ATTACK.adoc b/docs/standards/PANIC-ATTACK.adoc index 4653272d..caa1c2f6 100644 --- a/docs/standards/PANIC-ATTACK.adoc +++ b/docs/standards/PANIC-ATTACK.adoc @@ -130,7 +130,7 @@ opened : YYYY-MM-DD == Session-note history The most recent ad-hoc panic-attack audit was logged in -`.machine_readable/6a2/STATE.a2ml`, session-note 2026-04-11 +`.machine_readable/descriptiles/STATE.a2ml`, session-note 2026-04-11 (end-of-Affinatea-stage-6 batch): * 1 High — `search.js` `innerHTML` → false positive but diff --git a/docs/standards/TESTING.a2ml b/docs/standards/TESTING.a2ml index 000973d6..ce6511e7 100644 --- a/docs/standards/TESTING.a2ml +++ b/docs/standards/TESTING.a2ml @@ -133,7 +133,7 @@ target-file = "docs/TECH-DEBT.adoc" # one of the recognised reasons. [deferred-regression-test] -recorded-in = ".machine_readable/6a2/STATE.a2ml" +recorded-in = ".machine_readable/descriptiles/STATE.a2ml" recorded-field = "[[closed-bug]].regression-test-status" unblock-condition-required = true diff --git a/docs/standards/TESTING.adoc b/docs/standards/TESTING.adoc index 4e748e02..e54aeee4 100644 --- a/docs/standards/TESTING.adoc +++ b/docs/standards/TESTING.adoc @@ -145,7 +145,7 @@ test is not. To declare a deferred regression test, add a `regression-test-status = "deferred — "` field to the bug's `[[closed-bug]]` record -in `.machine_readable/6a2/STATE.a2ml`. Use one of these reasons: +in `.machine_readable/descriptiles/STATE.a2ml`. Use one of these reasons: * `observability` — no side-effecting builtin or harness reaches the bug's failure mode (e.g. BUG-003's eval order requires diff --git a/tools/check-doc-truthing.sh b/tools/check-doc-truthing.sh index c8328c4a..705235a5 100755 --- a/tools/check-doc-truthing.sh +++ b/tools/check-doc-truthing.sh @@ -67,7 +67,7 @@ BANNERED_DOCS=( # The machine-readable mirror (DOC-05): it follows the matrix, it does not # lead. These keys assert that contract in-band. -STATE_FILE=".machine_readable/6a2/STATE.a2ml" +STATE_FILE=".machine_readable/descriptiles/STATE.a2ml" STATE_KEYS=( "authoritative-status-doc" "drift-flag"