|
| 1 | +// SPDX-License-Identifier: PMPL-1.0-or-later |
| 2 | +// SPDX-FileCopyrightText: 2024-2026 hyperpolymath |
| 3 | += Estate ReScript-surface elimination — authoritative ledger (issue #229) |
| 4 | +:toc: macro |
| 5 | +:toclevels: 3 |
| 6 | +:icons: font |
| 7 | + |
| 8 | +[IMPORTANT] |
| 9 | +==== |
| 10 | +This document is authoritative for issue #229: the post-#228 re-audit numbers, |
| 11 | +the *true* RS-surface scope, the language-grounded canonical RS→AffineScript |
| 12 | +map, the escalation set (constructs with no clean target), and the per-repo |
| 13 | +port plan. It is the #229 analogue of the front-loaded ledger established by |
| 14 | +#239 for the spine. Per-feature readiness remains |
| 15 | +link:CAPABILITY-MATRIX.adoc[CAPABILITY-MATRIX.adoc]; the coordination ledger |
| 16 | +remains link:TECH-DEBT.adoc[TECH-DEBT.adoc]; the spine remains |
| 17 | +link:ECOSYSTEM.adoc[ECOSYSTEM.adoc]. Reproducer: |
| 18 | +`tools/estate-rs-audit/`. |
| 19 | +==== |
| 20 | + |
| 21 | +toc::[] |
| 22 | + |
| 23 | +== What #229 is |
| 24 | + |
| 25 | +A subset of estate `.affine` files are unfinished ReScript→AffineScript |
| 26 | +hand-ports: the file declares itself AffineScript (`module …;`) but the body |
| 27 | +still carries ReScript surface syntax. Such a file is neither valid ReScript |
| 28 | +nor valid AffineScript — debt in limbo that cannot be oracle-certified. The |
| 29 | +committed goal is *zero ReScript surface in any estate `.affine`*. #229 was |
| 30 | +blocked on #228 (ADR-014, module-qualified type/effect paths) because the |
| 31 | +oracle stops at the *first* parse error: qualified-path faults masked each |
| 32 | +file's full RS inventory. #228 landed on `main` via #241 — this re-audit is |
| 33 | +the now-unblocked step 2. |
| 34 | + |
| 35 | +== Post-#228 re-audit (authoritative) |
| 36 | + |
| 37 | +Oracle = affinescript `main` with #241/ADR-014 (parse-equivalent to the |
| 38 | +`int01-178-xmod` build: zero `lib/`+`bin/` delta vs `origin/main`). Same |
| 39 | +cached estate corpus as the pre-#228 baseline (controlled: only the oracle |
| 40 | +changed). Harness + raw data: `tools/estate-rs-audit/`. |
| 41 | + |
| 42 | +[cols="2,1,4"] |
| 43 | +|=== |
| 44 | +|Class |n |Meaning |
| 45 | + |
| 46 | +|`PASS` |552 |parses + typechecks on the oracle |
| 47 | +|`DRIFT-SYNTAX` |491 |first line is a parse/syntax error |
| 48 | +|`TYPE-ONLY` |133 |parses; fails later (resolution/type) — not a syntax port |
| 49 | +|*total* |*1176* |`.affine` across 28 estate repos |
| 50 | +|=== |
| 51 | + |
| 52 | +Zero class-delta vs the superseded #231 postfix — #241 reproduces ADR-014 |
| 53 | +parse behaviour exactly; #231 was superseded for *implementation* reasons, not |
| 54 | +behaviour. This table is now the post-#228 baseline of record. |
| 55 | + |
| 56 | +== The honest scope correction |
| 57 | + |
| 58 | +[WARNING] |
| 59 | +==== |
| 60 | +`DRIFT-SYNTAX` (491) is *not* the #229 workload. Most estate DRIFT is *non-RS* |
| 61 | +syntax drift — out of #229's contract by design. The true #229 scope is the |
| 62 | +files that carry a detected ReScript construct in |
| 63 | +`tools/estate-rs-audit/data/rs-inventory.tsv`: *~84 files across 12 repos*, |
| 64 | +two of which hold 71%. |
| 65 | +==== |
| 66 | + |
| 67 | +=== True RS-surface scope (excludes the affinescript repo's own intentional negative fixtures) |
| 68 | + |
| 69 | +[cols="2,1,4"] |
| 70 | +|=== |
| 71 | +|Repo |RS files |Shape |
| 72 | + |
| 73 | +|`burble` |32 |Whole-repo RS port; richest construct mix (e.g. |
| 74 | +`client/lib/src/BurbleClient.affine` carries 7 RS construct families). Not |
| 75 | +module-import-coupled — proceeds first. |
| 76 | +|`idaptik-dlc-vm` |28 |Uniform `import … as` VM dispatch. *Gated on the |
| 77 | +INT-01 #178 qualified-value resolver* (see §"Cross-unit gating"). |
| 78 | +|`standards` |4 | |
| 79 | +|`developer-ecosystem` |4 | |
| 80 | +|`stapeln` |3 | |
| 81 | +|`proof-burrower` |3 | |
| 82 | +|`idaptik` |3 | |
| 83 | +|`bofj-kitt` |3 | |
| 84 | +|`panll` |1 |`src/ui/tea/wizard.affine` — `mutable-field` |
| 85 | +|`invariant-path` |1 |`src/ui/tea/invariant_path_gui.affine` — `List(X)` only |
| 86 | +|`git-scripts` |1 |`src/ui/tea/git_scripts_gui.affine` — `List(X)` only |
| 87 | +|`game-server-admin` |1 |`src/ui/tea/gsa_gui.affine` — `List(X)` only |
| 88 | +|*total* |*~84* |over 12 repos |
| 89 | +|=== |
| 90 | + |
| 91 | +The four `*_gui.affine` single-file repos are pure `List(X)` — Tier-1 |
| 92 | +mechanical, no module coupling: the genuine quick wins. |
| 93 | + |
| 94 | +=== Non-RS DRIFT (NOT #229 scope — recorded so it is not mistaken for it) |
| 95 | + |
| 96 | +`standards` 112, `proof-burrower` 98, `developer-ecosystem` 97, `bofj-kitt` |
| 97 | +96, `affinescript` 84 (its own negative fixtures), `airborne-submarine-squadron` |
| 98 | +13, `accessibility-everywhere` 13, `idaptik` 9, `stapeln` 6, `burble` 3, |
| 99 | +singletons elsewhere. These DRIFT for non-ReScript reasons; they belong to |
| 100 | +separate workstreams, not #229. |
| 101 | + |
| 102 | +== Construct frequency (estate, excl. affinescript) |
| 103 | + |
| 104 | +`import-as` 31 · `mutable-field` 29 · `rs-generic<>` 26 · `array<T>` 17 · |
| 105 | +`%%raw` 14 · `labelled-(~x)` 12 · `List(X)` 7 · `JSON.t` 7 · `Dict.t` 6 · |
| 106 | +`rs-stdlib` 4 · `open-Mod` 4 · `type-rec` 3. |
| 107 | + |
| 108 | +== The canonical RS→AffineScript map (language-grounded) |
| 109 | + |
| 110 | +Derived from the language side — grammar / spec v2.0 / stdlib — *not guessed*. |
| 111 | +Every target form carries its grounding citation. Four tiers. |
| 112 | + |
| 113 | +=== Tier 1 — Mechanical (clean grammar target; scriptable, oracle-revalidated) |
| 114 | + |
| 115 | +[cols="2,2,3"] |
| 116 | +|=== |
| 117 | +|ReScript |AffineScript |Grounding |
| 118 | + |
| 119 | +|`array<T>` |`[T]` |list/array type is `[T]` — `stdlib/collections.affine`, |
| 120 | +`stdlib/prelude.affine`; `lib/parser.mly:486,394` (type-app is `[ ]`) |
| 121 | +|`option<T>` / `result<T,E>` |`Option[T]` / `Result[T, E]` | |
| 122 | +`stdlib/prelude.affine:19-21` `pub type Option<T> = Some(T) \| None` / |
| 123 | +`Result<T,E> = Ok(T) \| Err(E)`; type-app `[ ]` per `lib/parser.mly:486`, |
| 124 | +`stdlib/traits.affine:89` `Into[T]` |
| 125 | +|generic `name<T>` |`Name[T]` |capitalise + `[ ]`; `lib/parser.mly:486` |
| 126 | +|`List(X)` |`[X]` |list type is `[T]` (the four `*_gui.affine` quick wins) |
| 127 | +|`open Mod` |`use Mod::*;` |`lib/parser.mly:172` `ImportGlob` |
| 128 | +|`type rec t = …` |`type t = …` |AS `type`/`enum` decls are self-referential |
| 129 | +by default (`stdlib/prelude.affine:19-21`); drop `rec` |
| 130 | +|=== |
| 131 | + |
| 132 | +=== Tier 2 — Semantic redesign (maps onto the affine model; per-case, NO blind codemod) |
| 133 | + |
| 134 | +[cols="2,3"] |
| 135 | +|=== |
| 136 | +|ReScript |Why it is not mechanical |
| 137 | + |
| 138 | +|`mutable` record field |AffineScript is immutable-by-default with *explicit |
| 139 | +mutation via ownership* (`docs/spec.md:38`; `mut τ` `docs/spec.md:563`). |
| 140 | +There is no `mutable` field keyword; each record is re-expressed against the |
| 141 | +ownership model — a redesign, reviewed per record. |
| 142 | +|`(~x) =>` labelled args |No labelled/named-argument syntax in the grammar or |
| 143 | +stdlib (stdlib is uniformly positional, e.g. `stdlib/option.affine`). |
| 144 | +Rewrite to positional — a call-convention change, reviewed per call site. |
| 145 | +|`Belt.*` / `Js.*` / `Rescript.*` |ReScript stdlib; no 1:1 AffineScript |
| 146 | +target. Per-symbol remap to the estate stdlib, semantic. |
| 147 | +|=== |
| 148 | + |
| 149 | +=== Tier 3 — Escalate as a language-side issue/ADR (bidirectional evidence — the #228 discipline) |
| 150 | + |
| 151 | +These have *no clean AffineScript target today*. Per #229 step 3 they are |
| 152 | +escalated language-side rather than patched divergently across N repos — the |
| 153 | +same discipline that produced #228. |
| 154 | + |
| 155 | +[cols="1,2,4"] |
| 156 | +|=== |
| 157 | +|Esc |Construct |Finding |
| 158 | + |
| 159 | +|*ESC-01* (#245) |`%%raw("…")` (14) |AffineScript has *no raw-host-expression |
| 160 | +/ FFI escape*. The only host bridge is typed `extern fn` / `extern type` |
| 161 | +(`lib/parser.mly:185+`) — host-supplied, typed, no arbitrary-source escape. |
| 162 | +Needs a language decision on a raw/FFI form (or an explicit "port every |
| 163 | +`%%raw` to typed `extern`" doctrine). |
| 164 | +|*ESC-02* (#246) |`JSON.t` (7) |No stdlib JSON type (`stdlib/` has `Ajv` but |
| 165 | +no `Json`). Needs a stdlib JSON type. |
| 166 | +|*ESC-03* (#247) |`Dict.t` (6) |No stdlib `Map`/`Dict` type |
| 167 | +(`stdlib/collections.affine` is list ops only; `stdlib/Http.affine:16` |
| 168 | +already flags the `Dict` gap, tied to #160/#162). Needs a stdlib `Map` type — |
| 169 | +coordinate with #160/#162. |
| 170 | +|=== |
| 171 | + |
| 172 | +=== Tier 4 — Cross-unit gating (a sequencing finding, language-grounded) |
| 173 | + |
| 174 | +`import X as Y` ports to `use X as Y;`. That *parses* and the alias *registers* |
| 175 | +(`lib/parser.mly:168` `ImportSimple(path, Some alias)`; |
| 176 | +`lib/resolve.ml:787-797` registers the alias on `lookup_qualified` success). |
| 177 | +*But* qualified-value call sites `Y.fn(x)` hit the post-#228 INT-01 #178 |
| 178 | +qualified-*value* resolution gap (`lib/resolve.ml:719,797` |
| 179 | +`UndefinedModule`) — the exact gap recorded as the next spine unit. |
| 180 | + |
| 181 | +⇒ *`idaptik-dlc-vm` (28 files, 33% of #229 scope) is gated on the INT-01 #178 |
| 182 | +qualified-value resolver.* `burble` (rs-generic/array/mutable/%%raw-dominated, |
| 183 | +not module-coupled) and the four `List(X)` quick-win repos are not gated and |
| 184 | +proceed first. This does not reorder the mandate (#229 foundation first, as |
| 185 | +instructed); it is the foundation's own finding that the `idaptik-dlc-vm` |
| 186 | +*slice* of the per-repo work naturally sequences after INT-01 #178. |
| 187 | + |
| 188 | +== Per-repo port plan |
| 189 | + |
| 190 | +Each repo: oracle-validate locally first (estate CI does not compile |
| 191 | +`.affine`), one branch + squash-merge PR per repo, noreply author, |
| 192 | +`Refs #229` (multi-repo, sequenced, human-gated — never `Closes`). *Per-repo |
| 193 | +hands-off confirm before touching*: policy hands-off is the ReScript |
| 194 | +*ecosystem* (`.res`); `.affine`-with-RS is an in-scope unfinished port — but |
| 195 | +confirm per repo it is an intended AffineScript target, not a deliberate |
| 196 | +interop artefact (burble's standing caveat), before removal. |
| 197 | + |
| 198 | +. *Quick wins (Tier-1 only, no coupling):* `git-scripts`, `game-server-admin`, |
| 199 | + `invariant-path` (one `List(X)` file each), then `panll` (`mutable-field`, |
| 200 | + Tier-2 single file). |
| 201 | +. *`burble`* (32): Tier-1 mechanical pass first (re-validated), Tier-2 |
| 202 | + `mutable`/labelled per-case, Tier-3 constructs blocked on ESC-01..03. |
| 203 | +. *Small tail:* `standards`, `developer-ecosystem`, `stapeln`, |
| 204 | + `proof-burrower`, `idaptik`, `bofj-kitt` (≤4 RS files each). |
| 205 | +. *`idaptik-dlc-vm`* (28): after INT-01 #178 resolver lands (Tier-4 gate). |
| 206 | + |
| 207 | +== Reproduce / see also |
| 208 | + |
| 209 | +* `tools/estate-rs-audit/` — harness + captured data + how-to. |
| 210 | +* link:ECOSYSTEM.adoc[ECOSYSTEM.adoc] — the spine; #229 is its estate-port arm. |
| 211 | +* link:TECH-DEBT.adoc[TECH-DEBT.adoc] — coordination ledger. |
| 212 | +* #228 / link:specs/SETTLED-DECISIONS.adoc[ADR-014] — the unblocker. |
0 commit comments