diff --git a/docs/guides/frontier-guide.adoc b/docs/guides/frontier-guide.adoc index 61cad74..da6790b 100644 --- a/docs/guides/frontier-guide.adoc +++ b/docs/guides/frontier-guide.adoc @@ -1,10 +1,19 @@ // SPDX-License-Identifier: PMPL-1.0-or-later = AffineScript Frontier Guide: The Unveiling Jonathan D.A. Jewell -2026-04-11 +v1.0, 2026-04-11 :toc: :icons: font :source-highlighter: rouge +:revnumber: 1.0 +:revdate: 2026-04-11 + +[NOTE] +==== +Status: **canonical, living document.** This guide is the design reference for AffineScript itself and for any migration into AffineScript. Edits are welcome; record them in the <> appendix so the timeline is visible to future readers and AI agents. + +For systematic translation patterns from ReScript, TypeScript, or another `-script` family language, read this guide first, then the link:migration-playbook.adoc[Migration Playbook]. +==== The Frontier Guide takes you from "I write JavaScript / Python" to "I understand why AffineScript's design choices produce better programs." @@ -359,6 +368,8 @@ fn fetch_with_retry( == What to Read Next - link:warmup/README.adoc[Warmup scripts] — hands-on exercises, 15 minutes each +- link:migration-playbook.adoc[Migration Playbook] — systematic re-decomposition rules for porting ReScript / TypeScript / other `-script` codebases into AffineScript +- link:frontier-programming-practices/Human_Programming_Guide.adoc[Frontier Programming Practices] — the wider design philosophy (v2.0, 2026-04-10) - link:../specs/SPEC.md[Language Specification] — the authoritative grammar and semantics - link:../specs/SETTLED-DECISIONS.adoc[Settled Decisions] — architectural choices and why they were made - link:../DESIGN-VISION.adoc[Design Vision] — the long view @@ -383,3 +394,18 @@ AffineScript. Error messages are translated back to the face you chose, so Additional faces (JS-face, Pseudocode-face, and others) are on the roadmap. See link:../specs/faces.md[faces.md] for the architecture. + +[appendix#revision-history] +== Revision History + +This document is intended to evolve. When you change it — adding a chapter, sharpening an example, retiring an obsolete claim — record the change here so that downstream readers (especially AI agents loading the guide at session start) can see what has moved. + +[cols="1,1,3"] +|=== +| Revision | Date | Notes + +| 1.0 +| 2026-04-11 +| Initial unveiling. The six X-Script problems and AffineScript's answers; chapters 1–6; complete-program example; faces. + +|=== diff --git a/docs/guides/lessons/migrations/idaptik-user-settings.adoc b/docs/guides/lessons/migrations/idaptik-user-settings.adoc new file mode 100644 index 0000000..c390c47 --- /dev/null +++ b/docs/guides/lessons/migrations/idaptik-user-settings.adoc @@ -0,0 +1,326 @@ +// SPDX-License-Identifier: PMPL-1.0-or-later +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) += Migration Lesson: idaptik `UserSettings.res` → `UserSettings.affine` (design-only) +:revdate: 2026-05-02 +:icons: font +:source-highlighter: rouge + +[NOTE] +==== +**Status: design walk-through, not yet compiled.** Unlike +xref:idaptik-hitbox.adoc[idaptik-hitbox], this lesson does not produce a +compilable `.affine` file against AffineScript v0.1.0. `UserSettings.res` +depends on five external modules (`Storage`, `Audio`, `DesktopIntegration`, +`GetEngine`, `Option.getOr`) and uses `Float` for volume values — neither the +dependency surface nor the Float-arithmetic typechecker gap is resolved yet. + +The lesson has value anyway: it stress-tests the +xref:../../migration-playbook.adoc[migration playbook] against a realistic, +dependency-heavy file rather than a pure leaf, and surfaces three small +playbook gaps that the hitbox lesson did not. +==== + +== Why this file + +`src/app/utils/UserSettings.res` (127 lines) is the persistence layer for +user-modifiable settings: character stance, particles, difficulty, three +volume levels, system tray. It is interesting because it does three jobs in +one place — read from Storage, push values into the runtime +(Audio/DesktopIntegration), supply defaults — with implicit IO and several +stringly-typed values. Every pattern in the +xref:../../migration-playbook.adoc#source-pattern-index[source-pattern index] +shows up at least once. + +Three of those patterns are walked through here. The rest are repetitions +of the same shape. + +== Pattern 1 — Sum-type round-trip through a string-keyed Storage + +=== Original (ReScript) + +[source,rescript] +---- +type stance = Jessica | Q +let keyStance = "character-stance" + +let getCharacterStance = (): stance => + switch Storage.getString(keyStance) { + | Some("Q") => Q + | _ => Jessica // accepts None, garbage, AND Some("Jessica") + } + +let setCharacterStance = (s: stance): unit => { + let value = switch s { | Jessica => "Jessica" | Q => "Q" } + Storage.setString(keyStance, value) +} +---- + +The asymmetry is the bug. `setCharacterStance(Jessica)` writes `"Jessica"`, +but `getCharacterStance` only matches `Some("Q") => Q` and falls through +everything else (including `Some("Jessica")` *and* arbitrary garbage in +storage) to `Jessica`. The default-on-fallthrough is doing two jobs: +"absent" and "malformed." + +=== Faithful-but-monolithic AffineScript + +[source,affinescript] +---- +// don't do this +type Stance = Jessica | Q + +fn get_character_stance() -> Stance { + match Storage.get_string("character-stance") { + Some("Q") => Q + _ => Jessica + } +} +---- + +Same bug, AffineScript syntax. Storage IO is invisible; "absent" and +"malformed" still collapse to the same default. + +=== Re-decomposed + +[source,affinescript] +---- +type Stance = Jessica | Q + +// Codecs are exhaustive. "absent" and "malformed" are different values. +fn parse_stance(s: ref String) -> Option[Stance] { + match s { + "Q" => Some(Q) + "Jessica" => Some(Jessica) + _ => None + } +} + +fn render_stance(s: Stance) -> String { + match s { Jessica => "Jessica", Q => "Q" } +} + +// IO is in the row. Defaults live in one place. +fn get_character_stance() -> Stance / Storage { + Storage.get_string("character-stance") + .and_then(parse_stance) + .unwrap_or(Jessica) +} +---- + +What the re-decomposition buys: + +- Reading garbage from Storage is now *separable from* reading nothing. A + malformed value can be logged, surfaced to the user, or — if the policy + is still "fall back to default" — handled at the same `unwrap_or` site + that handles absence. The choice becomes explicit. +- `parse_stance` is exhaustive and round-trips with `render_stance`. Adding + a third stance variant later is a single-file change with two compile + errors to fix; in the original it is a silent runtime bug. +- Every caller of `get_character_stance` declares `Storage` in its effect + row. No hidden persistence at the call site. + +== Pattern 2 — Stringly-typed config + +=== Original (ReScript) + +[source,rescript] +---- +let getDifficulty = (): string => + Storage.getString(keyDifficulty)->Option.getOr("normal") + +let setDifficulty = (d: string): unit => Storage.setString(keyDifficulty, d) +---- + +Difficulty is a `string` at the public boundary. Every call site has to +remember the magic values `"easy" | "normal" | "hard"`, and a typo +(`"hardd"`) becomes a silent default at the next read. + +=== Re-decomposed + +[source,affinescript] +---- +type Difficulty = Easy | Normal | Hard + +fn parse_difficulty(s: ref String) -> Option[Difficulty] { + match s { + "easy" => Some(Easy) + "normal" => Some(Normal) + "hard" => Some(Hard) + _ => None + } +} + +fn render_difficulty(d: Difficulty) -> String { + match d { Easy => "easy", Normal => "normal", Hard => "hard" } +} + +fn get_difficulty() -> Difficulty / Storage { + Storage.get_string("game-difficulty") + .and_then(parse_difficulty) + .unwrap_or(Normal) +} +---- + +This is the +xref:../../migration-playbook.adoc#source-pattern-index["any / unknown" rule] +applied to a ReScript file: a `string` at the public boundary is the same +abdication of a thesis as TypeScript's `any`. Pin it. + +== Pattern 3 — Coupled write through a service-locator global + +=== Original (ReScript) + +[source,rescript] +---- +let setMasterVolume = (value: float): unit => + switch GetEngine.get() { + | Some(_) => + Audio.setMasterVolume(value) + Storage.setNumber(keyVolumeMaster, value) + | None => () + } +---- + +This single function does three things: looks up a process-wide singleton +(`GetEngine.get()`), updates the audio runtime, persists to storage. If the +engine isn't available the entire operation silently no-ops — including the +storage write that *could* have succeeded without the engine. The UI thinks +the volume was set; it wasn't. + +=== Re-decomposed + +[source,affinescript] +---- +// 1. Engine is no longer a service-locator. It's an effect. +// Callers that need the engine declare it. Tests install a mock. +effect Engine { + fn current() -> Option[ref EngineHandle]; +} + +// 2. Audio and Storage are also effects. Their use shows up everywhere. +effect Audio { fn set_master_volume(v: Float) -> (); } +effect Storage { fn set_number(key: String, value: Float) -> (); } + +// 3. The "no engine" case is a typed Result, not a silent (). +// Callers must decide what to do with NoEngine. +type NoEngine = NoEngine + +fn set_master_volume(v: Float) + -> Result[(), NoEngine] / Storage + Audio + Engine +{ + match Engine.current() { + Some(_) => { + Audio.set_master_volume(v); + Storage.set_number("volume-master", v); + Ok(()) + } + None => Err(NoEngine) + } +} +---- + +The biggest win is the last one: the silent no-op becomes a `Result` the +caller must inspect. The UI can show a "settings unavailable" banner +instead of pretending the change took effect. This is the +xref:../../migration-playbook.adoc#anti-patterns[same failure-mode-made-visible +pattern as the file-buffer "use after close"] — the destination type system +is forced to do the work the source language was leaving to runtime +discipline. + +== Constraints from v0.1.0 — why this lesson does not compile + +The hitbox lesson translates to a working `Hitbox.affine` because hitbox is a +single-file, integer-arithmetic, dependency-free leaf. UserSettings is none +of those things. Three v0.1.0 gaps stand between this design and a real +compile: + +. **Float arithmetic typechecker gap.** Per the + xref:idaptik-hitbox.adoc[hitbox lesson], `+`/`-`/`*`/`/`/`<`/`>` default + to `Int` in v0.1.0. Volumes are `Float`. Even reading and writing them is + fine; any clamping or scaling is not. Workaround for now: avoid Float + *arithmetic* in the migrated code, only Float *plumbing*. +. **No `Storage` / `Audio` / `Engine` modules in the AffineScript stdlib.** + These would have to ship first (in idaptik or upstream) before any + `UserSettings.affine` could resolve its imports. Effects-as-modules is a + larger design discussion — see + xref:../../frontier-programming-practices/Human_Programming_Guide.adoc[Frontier + Programming Practices] §Effects. +. **Effect-row syntax (`/ Storage + Audio + Engine`) and `Result[T, E]` + propagation (`?`) are documented in the frontier-guide but not all yet + in the v0.1.0 typechecker.** Specific operator availability needs a + v0.1.x audit before this design can be committed as code. + +The honest reading: **UserSettings is the right second case study to +*design*, but the wrong second case study to *ship*.** A more dependency-light +file (e.g. `combat/PlayerHP.res` — six mutable Float fields, no IO, no +service-locators) is the natural next compilable target. UserSettings ships +when the stdlib catches up. + +== Playbook verdict + +=== Held up cleanly + +- *"Stringly-typed → sum type"* (TS table, Discriminated unions / `any`) — + covered Difficulty and Stance. +- *"`unit` returns from impure code → `() / IO`"* — covered Storage and + Audio effects. +- *"`option` → `Option[T]` direct mapping"* — covered the `Option.getOr` + pattern. +- *"`let _ = ` discarded `Result`"* — surfaced by the system-tray code + (`let _ = DesktopIntegration.toggleSystemTray(...)`); becomes explicit + `Result` handling in the re-decomposed form. + +=== Gaps the playbook should address + +. **Service-locator globals.** `GetEngine.get(): Option` is a + common ReScript pattern and its re-decomposition (lift to effect) is + not named in the source-pattern index. The playbook covers `ref` and + `let mutable` but not "module-level getter for a singleton runtime." + *Proposed addition: one row in the ReScript table.* +. **Coupled-write composition.** When one user action both updates an + in-memory subsystem AND persists to Storage (the volume case), the + playbook does not say whether to express this as one function calling + two effects or as a wrapper effect. *Proposed addition: a short + subsection in + xref:../../migration-playbook.adoc#decision-criteria[Decision Criteria].* +. **Lessons-folder pointer in the playbook is wrong.** It points at + `docs/guides/lessons/` but that folder is the 10-lesson tutorial track; + the actual destination is `docs/guides/lessons/migrations/`. + *Proposed fix: one-line correction, alongside this lesson's commit.* + +Gap (3) is fixed in the same commit as this file. Gaps (1) and (2) are +playbook-text changes deferred to a v1.1 follow-up so the policy PR (#36) +can land focused. + +== What this lesson covers, generalised + +When translating a multi-pattern, dependency-heavy `.res` file: + +. **Surface the asymmetry first.** Every "default on fallthrough" hides + two cases: absent and malformed. Split them in the codec. +. **A `string` at a module's public boundary is the same abdication as + `any`.** Pin every string that has a fixed set of legal values. +. **A service-locator (`Module.get()` returning `Option`) is mutable + global state.** It deserves the same treatment as `let mutable` — lift + to an effect. The "is the runtime available?" branch becomes a typed + Result, not a silent no-op. +. **If the destination toolchain cannot yet compile the file, write the + lesson anyway.** The design walk-through pressures the playbook the + same way a working compile does. Mark the status honestly so future + readers know what to verify. + +== Reproducibility + +This lesson is design-only against AffineScript v0.1.0 — it does not produce +a compilable `.affine` file. The original ReScript: + +[source,bash] +---- +$ wc -l idaptik/src/app/utils/UserSettings.res +127 idaptik/src/app/utils/UserSettings.res +---- + +A compilable translation will be added at `idaptik/src/app/utils/UserSettings.affine` +once the stdlib provides `Storage`, `Audio`, and `Engine` modules and the +effect-row + `Result[T, E]` operator surface is confirmed in the v0.1.x +typechecker. When that happens, this lesson will gain a Reproducibility +block matching xref:idaptik-hitbox.adoc[idaptik-hitbox]. diff --git a/docs/guides/migration-playbook.adoc b/docs/guides/migration-playbook.adoc new file mode 100644 index 0000000..68e2cbb --- /dev/null +++ b/docs/guides/migration-playbook.adoc @@ -0,0 +1,277 @@ +// SPDX-License-Identifier: PMPL-1.0-or-later +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) += AffineScript Migration Playbook: Re-decomposition Patterns +Jonathan D.A. Jewell +v1.0, 2026-05-02 +:sectnums: +:toc: left +:icons: font +:source-highlighter: rouge +:revnumber: 1.0 +:revdate: 2026-05-02 + +[NOTE] +==== +This guide is the **systematic companion** to link:frontier-guide.adoc[`frontier-guide.adoc`]. + +* The Frontier Guide unveils what AffineScript *is* — read it if you are starting a new program. +* This playbook describes how to **carry an existing codebase across the boundary** — read it if you are translating ReScript, TypeScript, or another `-script` family language into AffineScript. + +The machine-readable companion for AI agents is link:frontier-programming-practices/AI.a2ml[`AI.a2ml`]. +==== + +== The Cardinal Rule + +**Re-decompose. Do not transliterate.** + +A 1:1 syntactic port — `let mutable` becomes `mut`, `option` becomes `Option[T]`, `try/catch` becomes `try/catch`, classes become records-with-methods — produces code that _type-checks in AffineScript while still carrying the design problems AffineScript was built to solve_. + +The Frontier Guide's thesis: every X-Script's six pathologies share one root — *the runtime does not know the program's intent*. Translation must therefore re-express *intent*, not just syntax. If a faithful port leaves the original design untouched, the destination language is being used as a syntax veneer and none of its guarantees do any work. + +The rest of this document is a catalogue of common source-language patterns and the re-decompositions they invite. + +== How to Use This Playbook + +. Start from the source file. Identify the patterns it contains using <>. +. For each pattern, read the **Decomposition** column — that is the AffineScript *shape*, not just the AffineScript *syntax*. +. If the source mixes several patterns into one structure (a class with mutable fields, an exception path, and a callback), expect the AffineScript output to have **more, smaller** declarations — not the same number of larger ones. +. Where two decompositions are valid, the <> table tells you which one fits. +. The <> section shows what a faithful-but-monolithic translation looks like, paired with its re-decomposed form. + +[#source-pattern-index] +== Source-Pattern Index + +=== ReScript → AffineScript + +[cols="1,2,2"] +|=== +| Source | Decomposition | Why + +| `let mutable x = ...` +| `mut` field on an owned record, **or** lift to a `State` effect +| Local mutation in a single owner stays local; mutation visible across calls becomes an effect. + +| `ref<'a>` (mutable cell) +| Same choice as `let mutable`; default to lifting to an effect when the cell crosses a function boundary +| A bare `ref` is mutation that has already escaped its scope — typically the wrong default. + +| `option<'a>` (built-in) +| `Option[T]` +| Direct mapping; no decomposition needed. + +| `result<'a, 'e>` +| `Result[T, E]` +| Direct mapping; tighten `'e` from `string` or `exn` to a named error type if you can. + +| `exception Foo` + `try/catch` +| `Result[T, E]` where `E` is a named variant covering each failure +| Replaces an untyped throw path with an exhaustive return type. The compiler forces every caller to handle the `Err` shape. + +| `Js.Promise.t<'a>` / async ReScript +| `Async` effect on the function row +| Promises silently allow racing, ignoring errors, and detached lifetimes. The `Async` effect makes lifetime and error explicit. + +| ReScript object types `Js.t<{..}>` +| Records with row variables `{ field: T, ..r }` +| Row polymorphism replaces structural object types principle-for-principle. + +| Polymorphic variants `[#A \| #B]` +| Open variant rows / sum types +| Same idea, integrated with the rest of the type system rather than parallel to it. + +| Module functors +| Row-polymorphic functions parameterised over their effects +| Most functor uses are an indirect way to parameterise over a small set of operations — effects do this directly. + +| Class via PPX (`%@react.component`, etc.) +| Owned record + standalone functions; lift effectful methods into effect rows +| A class is a tangle of state, methods, and implicit lifetimes. Each strand becomes its own declaration. + +| `unit` returns from impure code +| `() / IO` (or whichever effects apply) +| In ReScript, `unit` says nothing about whether the function did IO. In AffineScript, the effect row makes it visible. +|=== + +=== TypeScript → AffineScript + +[cols="1,2,2"] +|=== +| Source | Decomposition | Why + +| `T \| null \| undefined` +| `Option[T]` +| Frontier Guide chapter 1: one nothing, not three states. + +| `throw new Error()` + `try/catch` +| `Result[T, E]` for recoverable errors; `Exn[E]` effect for cross-cutting failure +| A bare `throw` in TypeScript is invisible at the call site. Both replacements make it visible. + +| `Promise` / `async function` +| `Async` effect +| Same reasoning as the ReScript case. + +| `class Foo { private x; method() {} }` +| Owned record + standalone functions; lift mutation to `mut` or `State` +| Encapsulation comes from ownership and module scope, not from class privacy. + +| `any` / `unknown` +| Forbidden in AffineScript output. Pin the type, or describe the unknown shape with a row variable. +| `any` is the absence of a thesis. Every translation must replace it with one. + +| Discriminated unions (`{ kind: "a" } \| { kind: "b" }`) +| Sum types with named constructors; `match` on the constructor +| Pattern matching is exhaustive; the compiler enforces what TypeScript can only suggest. +|=== + +[#decision-criteria] +== Decision Criteria + +When a source pattern admits more than one AffineScript shape, choose with these tests. + +=== `Option[T]` vs `Result[T, E]` + +* **`Option[T]`** when *absence is normal* — a lookup that may not find anything, a config field that may be unset. +* **`Result[T, E]`** when *absence has a reason you can describe* — parse failure, validation error, IO error. + +If you find yourself reaching for `Result[T, ()]` or `Result[T, String]`, you probably wanted `Option[T]` (or a richer `E`). + +=== `mut` vs effect + +* **`mut`** for local mutation contained in a single owner — buffer construction, an accumulator inside a function. +* **`State[S]` effect** for mutation that must be observable across calls — game state, session state, a setting visible to many subsystems. + +If two callers need to see the same mutation, it is no longer *local* — lift it. + +=== `ref` vs `mut` vs `own` + +* **`ref Buffer`** — the caller keeps the value and the function only reads it. +* **`mut Buffer`** — the function modifies in place; the caller keeps using the value afterwards. +* **`own Buffer`** — the function consumes the value; the caller cannot use it after the call. + +The default for resources (files, sockets, tokens, allocations) is `own` — anything else hides the lifetime. + +=== Linear (`@linear`) vs affine (`own`) + +* **`own`** (affine) — *may* be used at most once. Dropping is allowed. +* **`@linear`** — *must* be used exactly once. Dropping is a compile error. + +Prefer `own`. Reach for `@linear` only when forgetting to consume the value is a real bug — typically protocol handles, transactions, and capability tokens. + +[#anti-patterns] +== Anti-patterns: Faithful-but-monolithic Translation + +The following ReScript is a small file buffer that conflates state, lifetime, and IO. + +[source,rescript] +---- +// ReScript — original +type fileBuffer = { + mutable data: array, + mutable open_: bool, +} + +let make = (): fileBuffer => { data: [], open_: true } + +let read = (b: fileBuffer): string => + if b.open_ { Array.joinWith(b.data, "") } else { raise(Failure("closed")) } + +let write = (b: fileBuffer, s: string): unit => { + if !b.open_ { raise(Failure("closed")) } + b.data = Array.concat(b.data, [s]) + Console.log("wrote " ++ s) +} + +let close = (b: fileBuffer): unit => { b.open_ = false } +---- + +=== Faithful-but-monolithic translation — don't do this + +[source,affinescript] +---- +// AffineScript — same shape, none of the guarantees doing any work +type FileBuffer = own { + data: mut Array[String], + open_: mut Bool, +} + +fn make() -> own FileBuffer { FileBuffer { data: [], open_: true } } + +fn read(b: ref FileBuffer) -> String { + if b.open_ { String.join(b.data, "") } else { panic("closed") } +} + +fn write(b: mut FileBuffer, s: String) -> () { + if !b.open_ { panic("closed") } + b.data = b.data ++ [s]; + // IO is silently invisible — same problem as the original +} + +fn close(b: mut FileBuffer) -> () { b.open_ = false } +---- + +This compiles. It is also no better than the ReScript original: the `closed` invariant is still a runtime check, the IO is still invisible, and `close` does not actually consume the buffer. + +=== Re-decomposed translation + +[source,affinescript] +---- +// Two types — a closed buffer is statically distinguishable from an open one +type OpenBuffer = own { data: Array[String] } +type ClosedBuffer = own { data: Array[String] } + +effect IO { fn log(s: String); } + +fn make() -> own OpenBuffer { OpenBuffer { data: [] } } + +// read takes a borrow — caller keeps the buffer: +fn read(b: ref OpenBuffer) -> String { String.join(b.data, "") } + +// write is in-place; IO is in the row: +fn write(b: mut OpenBuffer, s: String) -> () / IO { + b.data = b.data ++ [s]; + IO.log("wrote " ++ s) +} + +// close consumes Open and returns Closed — "use after close" becomes a type error: +fn close(b: own OpenBuffer) -> own ClosedBuffer { ClosedBuffer { data: b.data } } +---- + +What changed: + +. `closed` moved from a runtime field to a *type* (`OpenBuffer` vs `ClosedBuffer`) — "use after close" is now a compile error. +. `close` consumes its argument (`own`) — there is no `b` left to misuse afterwards. +. `IO` is in the effect row of `write` — every caller of `write` declares it transitively. +. The mutable boolean is gone; `data` is reached only through `mut OpenBuffer`, which makes ownership explicit. + +The two versions are roughly the same number of lines. They are different programs. + +== Lessons from Real Migrations + +Case studies from in-flight migrations land in link:lessons/migrations/[`docs/guides/lessons/migrations/`]. (The sibling `lessons/` folder is the 10-lesson tutorial track for new AffineScript users — different audience.) + +Existing entries: + +* link:lessons/migrations/idaptik-hitbox.adoc[idaptik `Hitbox.res` → `Hitbox.affine`] — the first compilable translation; pure-leaf collision primitives. Surfaces the v0.1.0 Float-arithmetic typechecker gap. +* link:lessons/migrations/idaptik-user-settings.adoc[idaptik `UserSettings.res` (design-only)] — multi-pattern, dependency-heavy persistence layer. Stress-tests the playbook against five external module dependencies; surfaces three small playbook gaps. + +If you complete a non-trivial `.res → .affine` translation and the re-decomposition reveals something future translators would benefit from, write it up there. The format is loose: original snippet, faithful port, re-decomposed port, and what the second one buys you. Mark the lesson "design-only" if the destination toolchain cannot yet compile it — the design walk-through still pressures the playbook. + +== See Also + +* link:frontier-guide.adoc[Frontier Guide: The Unveiling] — the *what* and *why* of AffineScript's design. +* link:frontier-programming-practices/Human_Programming_Guide.adoc[Frontier Programming Practices] — the wider design philosophy. +* link:frontier-programming-practices/AI.a2ml[`AI.a2ml`] — machine-readable companion, read by AI agents at session start. +* link:../specs/SETTLED-DECISIONS.adoc[Settled Decisions] — architectural choices and why they were made. +* link:../specs/SPEC.md[Language Specification] — authoritative grammar and semantics. + +[appendix] +== Revision History + +[cols="1,1,3"] +|=== +| Revision | Date | Notes + +| 1.0 +| 2026-05-02 +| Initial draft. ReScript and TypeScript pattern indices, decision criteria, file-buffer anti-pattern. Companion to `frontier-guide.adoc` v1.0. +|===