|
| 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 | +|`invariant-path` |1 |`src/ui/tea/invariant_path_gui.affine` — `List(X)` + |
| 85 | +record sigil (layered) |
| 86 | +|`git-scripts` |1 |`src/ui/tea/git_scripts_gui.affine` — `List(X)` + |
| 87 | +record sigil (layered) |
| 88 | +|`game-server-admin` |1 |`src/ui/tea/gsa_gui.affine` — `List(X)` + record |
| 89 | +sigil (layered) |
| 90 | +|*total* |*~83* |over 11 repos (panll removed — see note) |
| 91 | +|=== |
| 92 | + |
| 93 | +[WARNING] |
| 94 | +==== |
| 95 | +*The text-scan inventory is a lower-bound triage signal, not the true |
| 96 | +per-file inventory.* The oracle stops at the first parse error, so a file's |
| 97 | +deeper RS layers are invisible until earlier ones are removed (e.g. the |
| 98 | +`*_gui.affine` files scan as "`List(X)` only" but in fact also need the |
| 99 | +`#{` record sigil beneath it). And the scanner has false positives: |
| 100 | +`mutable-field` matched a *comment* in `panll/src/ui/tea/wizard.affine` |
| 101 | +("no shared mutable state") — panll carries *no* listed RS construct; its |
| 102 | +real first fault is a trailing comma in an `enum` variant list, which is |
| 103 | +not a #229 named construct. *`panll` is reclassified out of #229 scope.* |
| 104 | +The true per-file inventory only emerges by iterative oracle-peeling |
| 105 | +*during* each port — which is exactly why #229 mandates per-repo, |
| 106 | +oracle-validated, human-gated ports rather than one blind sweep. |
| 107 | +==== |
| 108 | + |
| 109 | +=== Non-RS DRIFT (NOT #229 scope — recorded so it is not mistaken for it) |
| 110 | + |
| 111 | +`standards` 112, `proof-burrower` 98, `developer-ecosystem` 97, `bofj-kitt` |
| 112 | +96, `affinescript` 84 (its own negative fixtures), `airborne-submarine-squadron` |
| 113 | +13, `accessibility-everywhere` 13, `idaptik` 9, `stapeln` 6, `burble` 3, |
| 114 | +singletons elsewhere. These DRIFT for non-ReScript reasons; they belong to |
| 115 | +separate workstreams, not #229. |
| 116 | + |
| 117 | +== Construct frequency (estate, excl. affinescript) |
| 118 | + |
| 119 | +`import-as` 31 · `mutable-field` 29 · `rs-generic<>` 26 · `array<T>` 17 · |
| 120 | +`%%raw` 14 · `labelled-(~x)` 12 · `List(X)` 7 · `JSON.t` 7 · `Dict.t` 6 · |
| 121 | +`rs-stdlib` 4 · `open-Mod` 4 · `type-rec` 3. |
| 122 | + |
| 123 | +== The canonical RS→AffineScript map (language-grounded) |
| 124 | + |
| 125 | +Derived from the language side — grammar / spec v2.0 / stdlib — *not guessed*. |
| 126 | +Every target form carries its grounding citation. Four tiers. |
| 127 | + |
| 128 | +=== Tier 1 — Mechanical (clean grammar target; scriptable, oracle-revalidated) |
| 129 | + |
| 130 | +[cols="2,2,3"] |
| 131 | +|=== |
| 132 | +|ReScript |AffineScript |Grounding |
| 133 | + |
| 134 | +|*expression-/pattern-position record literal* `{ f: v }` → `#{ f: v }`; |
| 135 | +typed `T { f: v }` → `T #{ f: v }` |the *record sigil* `#{…}` |*The |
| 136 | +dominant estate blocker — and NOT in #229's named construct set.* |
| 137 | +`docs/spec.md:414-421` prescribes exactly this rewrite ("rewrite each |
| 138 | +expression-position record literal `{`→`#{`; leave struct/type declaration |
| 139 | +bodies"); examples `spec.md:901,1320`. Oracle-verified: bare `{x:1}` / |
| 140 | +`M{x:1}` / `M(x:1)` all parse-error; `Type #{…}` is the form. Applies to |
| 141 | +record *patterns* in `match` too. Position-aware codemod (expr/pattern vs |
| 142 | +decl), *not* naive regex. |
| 143 | +|`List(X)` |`[X]` |list type is `[T]`; oracle-verified `List(Int)` |
| 144 | +parse-errors, `[Int]` passes. `stdlib/collections.affine`, |
| 145 | +`stdlib/prelude.affine` |
| 146 | +|lowercase `array<…>` / `option<…>` / `result<…>` |`[…]` / `Option[…]` / |
| 147 | +`Result[…]` (capitalise the type name) |The RS tell is the *lowercase type |
| 148 | +name*, **not** the angle brackets. Oracle-verified: `Option<Int>` *passes* |
| 149 | +— both `<>` and `[ ]` type-application parse (`lib/parser.mly:486`). |
| 150 | +Capitalising the name is the fix; bracket style is free choice (`[ ]` |
| 151 | +canonical per spec v2.0 / `stdlib/traits.affine:89`) |
| 152 | +|`open Mod` |`use Mod::*;` |`lib/parser.mly:172` `ImportGlob` |
| 153 | +|`type rec t = …` |`type t = …` |AS `type`/`enum` decls are self-referential |
| 154 | +by default (`stdlib/prelude.affine:19-21`); drop `rec` |
| 155 | +|=== |
| 156 | + |
| 157 | +[NOTE] |
| 158 | +==== |
| 159 | +The record-sigil row was discovered by oracle-peeling the four allegedly |
| 160 | +"quick-win" `*_gui.affine` files: each is a *layered* port (`List(X)` was |
| 161 | +only the first wall; `#{` record literals + record patterns lie beneath). |
| 162 | +This corrects two earlier overclaims: (a) angle-brackets are *not* RS |
| 163 | +surface; (b) there are *no* trivial single-construct quick wins — even |
| 164 | +one-RS-flagged files are multi-layer. |
| 165 | +==== |
| 166 | + |
| 167 | +=== Tier 2 — Semantic redesign (maps onto the affine model; per-case, NO blind codemod) |
| 168 | + |
| 169 | +[cols="2,3"] |
| 170 | +|=== |
| 171 | +|ReScript |Why it is not mechanical |
| 172 | + |
| 173 | +|`mutable` record field |AffineScript is immutable-by-default with *explicit |
| 174 | +mutation via ownership* (`docs/spec.md:38`; `mut τ` `docs/spec.md:563`). |
| 175 | +There is no `mutable` field keyword; each record is re-expressed against the |
| 176 | +ownership model — a redesign, reviewed per record. |
| 177 | +|`(~x) =>` labelled args |No labelled/named-argument syntax in the grammar or |
| 178 | +stdlib (stdlib is uniformly positional, e.g. `stdlib/option.affine`). |
| 179 | +Rewrite to positional — a call-convention change, reviewed per call site. |
| 180 | +|`Belt.*` / `Js.*` / `Rescript.*` |ReScript stdlib; no 1:1 AffineScript |
| 181 | +target. Per-symbol remap to the estate stdlib, semantic. |
| 182 | +|=== |
| 183 | + |
| 184 | +=== Tier 3 — Escalate as a language-side issue/ADR (bidirectional evidence — the #228 discipline) |
| 185 | + |
| 186 | +These have *no clean AffineScript target today*. Per #229 step 3 they are |
| 187 | +escalated language-side rather than patched divergently across N repos — the |
| 188 | +same discipline that produced #228. |
| 189 | + |
| 190 | +[cols="1,2,4"] |
| 191 | +|=== |
| 192 | +|Esc |Construct |Finding |
| 193 | + |
| 194 | +|*ESC-01* (#245) |`%%raw("…")` (14) |AffineScript has *no raw-host-expression |
| 195 | +/ FFI escape*. The only host bridge is typed `extern fn` / `extern type` |
| 196 | +(`lib/parser.mly:185+`) — host-supplied, typed, no arbitrary-source escape. |
| 197 | +Needs a language decision on a raw/FFI form (or an explicit "port every |
| 198 | +`%%raw` to typed `extern`" doctrine). |
| 199 | +|*ESC-02* (#246) |`JSON.t` (7) |No stdlib JSON type (`stdlib/` has `Ajv` but |
| 200 | +no `Json`). Needs a stdlib JSON type. |
| 201 | +|*ESC-03* (#247) |`Dict.t` (6) |No stdlib `Map`/`Dict` type |
| 202 | +(`stdlib/collections.affine` is list ops only; `stdlib/Http.affine:16` |
| 203 | +already flags the `Dict` gap, tied to #160/#162). Needs a stdlib `Map` type — |
| 204 | +coordinate with #160/#162. |
| 205 | +|=== |
| 206 | + |
| 207 | +=== Tier 4 — Cross-unit gating (a sequencing finding, language-grounded) |
| 208 | + |
| 209 | +`import X as Y` ports to `use X as Y;`. That *parses* and the alias *registers* |
| 210 | +(`lib/parser.mly:168` `ImportSimple(path, Some alias)`; |
| 211 | +`lib/resolve.ml:787-797` registers the alias on `lookup_qualified` success). |
| 212 | +*But* qualified-value call sites `Y.fn(x)` hit the post-#228 INT-01 #178 |
| 213 | +qualified-*value* resolution gap (`lib/resolve.ml:719,797` |
| 214 | +`UndefinedModule`) — the exact gap recorded as the next spine unit. |
| 215 | + |
| 216 | +⇒ *`idaptik-dlc-vm` (28 files, 33% of #229 scope) is gated on the INT-01 #178 |
| 217 | +qualified-value resolver.* `burble` (rs-generic/array/mutable/%%raw-dominated, |
| 218 | +not module-coupled) and the three smallest non-coupled repos |
| 219 | +(`git-scripts`, `game-server-admin`, `invariant-path`) are not gated and |
| 220 | +proceed first. This does not reorder the mandate (#229 foundation first, as |
| 221 | +instructed); it is the foundation's own finding that the `idaptik-dlc-vm` |
| 222 | +*slice* of the per-repo work naturally sequences after INT-01 #178. |
| 223 | + |
| 224 | +== Per-repo port plan |
| 225 | + |
| 226 | +Each repo: oracle-validate locally first (estate CI does not compile |
| 227 | +`.affine`), one branch + squash-merge PR per repo, noreply author, |
| 228 | +`Refs #229` (multi-repo, sequenced, human-gated — never `Closes`). *Per-repo |
| 229 | +hands-off confirm before touching*: policy hands-off is the ReScript |
| 230 | +*ecosystem* (`.res`); `.affine`-with-RS is an in-scope unfinished port — but |
| 231 | +confirm per repo it is an intended AffineScript target, not a deliberate |
| 232 | +interop artefact (burble's standing caveat), before removal. |
| 233 | + |
| 234 | +. *Smallest non-coupled (Tier-1, layered — not trivial):* `git-scripts`, |
| 235 | + `game-server-admin`, `invariant-path` (one file each: `List(X)` *and* the |
| 236 | + `#{` record sigil in expr + `match` pattern position; iterative |
| 237 | + oracle-peel). `panll` is *excluded* (no listed RS construct — see the |
| 238 | + lower-bound-triage warning above). |
| 239 | +. *`burble`* (32): Tier-1 mechanical pass first (re-validated), Tier-2 |
| 240 | + `mutable`/labelled per-case, Tier-3 constructs blocked on ESC-01..03. |
| 241 | +. *Small tail:* `standards`, `developer-ecosystem`, `stapeln`, |
| 242 | + `proof-burrower`, `idaptik`, `bofj-kitt` (≤4 RS files each). |
| 243 | +. *`idaptik-dlc-vm`* (28): after INT-01 #178 resolver lands (Tier-4 gate). |
| 244 | + |
| 245 | +== Reproduce / see also |
| 246 | + |
| 247 | +* `tools/estate-rs-audit/` — harness + captured data + how-to. |
| 248 | +* link:ECOSYSTEM.adoc[ECOSYSTEM.adoc] — the spine; #229 is its estate-port arm. |
| 249 | +* link:TECH-DEBT.adoc[TECH-DEBT.adoc] — coordination ledger. |
| 250 | +* #228 / link:specs/SETTLED-DECISIONS.adoc[ADR-014] — the unblocker. |
0 commit comments