Skip to content

Commit 507602d

Browse files
docs(migrations): canonical JS→AffineScript campaign documentation (Refs #254 #263) (#314)
## Summary Lands canonical documentation for the **Unnecessarily-JavaScript → AffineScript** estate migration (umbrella #254 + step issues #263 #266 #271 #274 #277, all labelled `js-to-affinescript`). Consolidates what was scattered across issue bodies + memory notes into one navigable doc at `docs/migrations/js-to-affinescript/README.adoc` (378 lines): - Campaign overview + inventory snapshot (1,609 → 1,724 fresh count) - 4-layer architecture (POLICY / TRIAGE / PORTS / BINDING-GAP) - Per-step acceptance criteria + cross-references - Per-repo ownership gate - Memory cross-references ## Resolves - **#263 open SHIP-MODE design question** → Mode A (WARNING first) with rationale and reversibility note. The Hypatia rule implementation + tests remain owed work tracked on #263. ## Does not resolve (implementation still owed) - #266 (LOAD-BEARING triage tool — spec landed, code TBD) - #271 (physical ports) - #274 (binding-gap discharge) - #277 (re-triage cycles) - #254 (umbrella stays open until campaign converges per #277's criterion) ## Test plan - [ ] docs render cleanly in asciidoctor - [ ] no broken cross-references --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 5f83ea9 commit 507602d

4 files changed

Lines changed: 655 additions & 0 deletions

File tree

Lines changed: 378 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,378 @@
1+
// SPDX-License-Identifier: MPL-2.0
2+
= Unnecessarily-JavaScript → AffineScript estate migration
3+
:revdate: 2026-05-30
4+
:status: ACTIVE
5+
:campaign: hyperpolymath/standards#254
6+
:label: js-to-affinescript
7+
8+
This is the canonical campaign documentation for the
9+
**Unnecessarily-JavaScript → AffineScript** estate migration.
10+
11+
Tracked under https://github.com/hyperpolymath/standards/issues/254[UMBRELLA #254].
12+
Step issues #263, #266, #271, #274, #277 are the canonical
13+
implementation plan and are labelled `js-to-affinescript`.
14+
15+
Sibling migration campaigns (not in scope here):
16+
17+
- https://github.com/hyperpolymath/standards/issues/239[TS → AffineScript] (#239)
18+
- https://github.com/hyperpolymath/standards/issues/252[ReScript → AffineScript] (#252)
19+
- https://github.com/hyperpolymath/standards/issues/253[npm → Deno] (#253)
20+
21+
== Campaign overview
22+
23+
Estate-wide migration of "unnecessarily-JavaScript" `.js` / `.jsx` files
24+
to AffineScript. Distinct from TS (#239) and RS (#252): **JavaScript is
25+
*allowed* in policy "only where AffineScript cannot reach"** — so this
26+
campaign targets the gap between current AffineScript bindings and
27+
current JavaScript usage.
28+
29+
A `.js` / `.jsx` file qualifies as "unnecessarily JS" if every API
30+
surface it uses is already bound in AffineScript stdlib (Deno, json,
31+
Vscode, collections, etc.). Files using surface NOT yet bound stay as
32+
JavaScript until bindings ship.
33+
34+
*Scope safety*: only `hyperpolymath/` repos that are non-fork. Per-PR
35+
ownership gate mandatory on every PR landed under this campaign.
36+
37+
== Inventory snapshot
38+
39+
[cols="1h,1,4", options="header"]
40+
|===
41+
| Source | Count | Notes
42+
43+
| Initial inventory (2026-05-28)
44+
| 1,609
45+
| Post-excludes: `node_modules`, `dist`, `build`, `target`, `_build`,
46+
`_opam`, `.deno`, `hyperpolymath-archive`, `rescript`, `servers`,
47+
`repos-monorepo`, `linguist`, `deps`, `out`, `lib/js`, `.d.ts`,
48+
`*/bindings/{deno,typescript,ts}/`, `*.config.{js,cjs,mjs}`
49+
50+
| Fresh `find` (2026-05-30)
51+
| 1,724
52+
| Same excludes; the +115 delta is informational only. STEP 2's
53+
triage tool MUST re-enumerate with documented excludes.
54+
|===
55+
56+
=== Top backlog (initial inventory, 2026-05-28)
57+
58+
[cols="1,3", options="header"]
59+
|===
60+
| Files | Repo
61+
62+
| 144 | boj-server
63+
| 142 | panll
64+
| 105 | stapeln
65+
| 103 | proven-servers, idaptik
66+
| 78 | polyglot-i18n
67+
| 70 | rrecord-verity
68+
| 64 | ssg-collection
69+
| 62 | proven
70+
| 49 | reposystem
71+
| 48 | developer-ecosystem
72+
| 38 | ubicity
73+
| 34 | zotero-tools, burble
74+
| 33 | airborne-submarine-squadron, affinescript
75+
| 32 | isers
76+
| 25 | affinescript-stdlib-pr
77+
| 22 | standards, kaldor-iiot
78+
| <20 | tail
79+
|===
80+
81+
== 4-layer architecture
82+
83+
The campaign separates responsibility across four layers; each layer's
84+
work surfaces as one or more of the five step issues below.
85+
86+
[cols="1h,2,4", options="header"]
87+
|===
88+
| Layer | Concern | Step issues
89+
90+
| L1
91+
| Policy & enforcement — Hypatia rule + standards-docs carve-outs
92+
| #263
93+
94+
| L2
95+
| Triage tooling — classifier that maps every `.js` file to one of three
96+
buckets (`portable now`, `blocked on binding X`, `keep as JS`)
97+
| #266
98+
99+
| L3
100+
| Physical ports — `.js` → `.affine` for everything in the `portable
101+
now` bucket
102+
| #271
103+
104+
| L4
105+
| Binding-gap discharge — ship AffineScript bindings that move files
106+
from `blocked on binding X` to `portable now`, then re-triage
107+
| #274, #277
108+
|===
109+
110+
== STEP 1 — POLICY (issue #263)
111+
112+
Ship the Hypatia `:javascript_detected` rule (parallel to
113+
`:typescript_detected` and `:rescript_detected`) plus matching
114+
standards-docs language so that new `.js` / `.jsx` outside documented
115+
carve-outs is automatically flagged estate-wide.
116+
117+
=== Ship-mode decision
118+
119+
Issue #263 left this as an open design question between two ship modes.
120+
The campaign documentation records the decision here so the rule
121+
implementation has a clear target.
122+
123+
**DECISION: Mode A (WARNING first).**
124+
125+
The rule emits an informational finding on every new `.js` / `.jsx`
126+
outside carve-outs. It flips to HARD-BLOCK only after STEP 2 triage has
127+
classified the bulk of the backlog and the `keep as JS` carve-outs are
128+
empirically validated.
129+
130+
*Rationale.* JavaScript is *permanently* legitimate in this estate — it
131+
is the escape hatch for surfaces AffineScript cannot reach. Until STEP
132+
2's classifier has run estate-wide and STEP 5 has converged on the
133+
permanent-residue set, a HARD-BLOCK rule has high false-positive blast
134+
radius. WARNING-first preserves merge throughput while the long-tail
135+
classification settles.
136+
137+
*Reversibility.* The mode flip is a one-line change in the Hypatia rule
138+
config (severity field). The flip itself is mechanical; the decision to
139+
flip is operator-driven once STEP 5 produces convergence evidence.
140+
141+
=== Carve-out classes (Hypatia `path_allow_prefixes`)
142+
143+
The rule allows JavaScript in these eight classes:
144+
145+
[cols="1,2,3", options="header"]
146+
|===
147+
| # | Class | Pattern
148+
149+
| 1 | host-required by ecosystem
150+
| MCP servers; plugin entry points that the upstream ecosystem
151+
demands be JS
152+
153+
| 2 | tooling configs
154+
| `*.config.{js,cjs,mjs}` — bundler / lint / framework configs
155+
156+
| 3 | bootstrap shims
157+
| `affinescript-{deno-test,cli}/` — surfaces that compile AffineScript
158+
to JS and therefore cannot themselves be AffineScript
159+
160+
| 4 | upstream forks
161+
| `.fork == true` repos — out-of-scope per estate convention
162+
163+
| 5 | archived
164+
| `hyperpolymath-archive/**` — frozen history
165+
166+
| 6 | vendored deps
167+
| `**/deps/**`, `**/node_modules/**` — third-party
168+
169+
| 7 | compiled output
170+
| `**/out/**`, `**/lib/js/**`, `**/.deno/**` — derived artefacts
171+
172+
| 8 | host extension entry
173+
| `**/vscode/**`, `**/extensions/vscode/**` — VSCode requires JS at the
174+
extension activation boundary
175+
|===
176+
177+
=== Acceptance (#263)
178+
179+
- New Hypatia rule `:javascript_detected` in `hypatia` repo with the
180+
eight carve-out classes encoded as `path_allow_prefixes` (or
181+
equivalent).
182+
- Ship mode set to **WARNING** (Mode A) per the decision above.
183+
- Standards docs language updated to reference the rule + this
184+
campaign doc.
185+
- Hypatia tests cover at least one positive case per carve-out class
186+
and one negative case (a `.js` file outside all carve-outs that the
187+
rule flags).
188+
189+
This documentation pass satisfies the *design + ship-mode-decision*
190+
portion of #263's acceptance. The Hypatia rule implementation + tests
191+
remain owed work tracked on the same issue.
192+
193+
== STEP 2 — TRIAGE TOOLING (issue #266) — LOAD-BEARING
194+
195+
Build the classifier that makes the rest of this campaign tractable.
196+
Without it, the migration is open-ended: 1,609+ `.js` / `.jsx` files
197+
cannot be hand-classified at scale.
198+
199+
=== Spec
200+
201+
The tool enumerates JavaScript API surface per file, cross-checks it
202+
against the current AffineScript stdlib catalogue, and outputs three
203+
buckets:
204+
205+
[cols="1,3", options="header"]
206+
|===
207+
| Bucket | Meaning
208+
209+
| `portable now`
210+
| Every API surface this file uses is already bound in AffineScript
211+
stdlib (Deno, json, collections, string, Vscode, etc.). Port directly.
212+
213+
| `blocked on binding X`
214+
| Most surfaces are bound but at least one missing binding gates the
215+
port. Tool emits the specific missing binding name so STEP 4 can group
216+
work by binding.
217+
218+
| `keep as JS`
219+
| File falls in one of the eight carve-out classes from STEP 1. Tool
220+
emits the carve-out class number.
221+
|===
222+
223+
=== Implementation choices
224+
225+
- *AST walk*: acorn-style preferred for fidelity. Regex sufficient for
226+
v0 — record the choice in tool docstring.
227+
- *Exclude list*: reproducible — record in a sibling config file
228+
adjacent to the tool. Re-running the enumeration must produce the same
229+
file set.
230+
- *Tool location*: `standards/scripts/js-triage/` is the suggested
231+
seam. If hypatia is the better home (because the rule from STEP 1
232+
lives there), record that decision in the commit message.
233+
- *Output format*: bucket counts as a comment on #254; per-file
234+
classifications as TSV / JSONL for STEP 3's port-order picker.
235+
236+
This documentation gives STEP 2 a concrete spec to land against. The
237+
tool implementation itself remains owed.
238+
239+
== STEP 3 — PORTS (issue #271)
240+
241+
Execute the physical `.js` / `.jsx` → `.affine` ports for every file
242+
STEP 2 placed in the `portable now` bucket. Smallest-first within each
243+
repo to bank throughput and validate per-idiom-cluster patterns before
244+
tackling the long tail.
245+
246+
=== Expected idiom clusters
247+
248+
Many JavaScript files likely fall in 1–2 idiom clusters. STEP 2's
249+
bucket comment confirms:
250+
251+
- Deno CLI scripts — small, `.ts`-equivalent surface, fully bindable.
252+
- VSCode extension JavaScript — covered by `Vscode.affine` 55 fns.
253+
- MCP glue — async stdio loop, protocol. **Should NOT appear in
254+
`portable now`**; if it does, treat as a triage-tool bug and fix STEP 2.
255+
- WebExtension scripts — blocked on host-API bindings, same as TS/RS.
256+
**Should NOT appear in `portable now`**; same diagnostic.
257+
258+
=== Per-port checklist
259+
260+
- `.affine` builds (`just check` green for the host repo).
261+
- Original `.js` / `.jsx` removed in the same PR.
262+
- Downstream build wiring updated (e.g., `package.json` `main` /
263+
`exports`, `deno.json` task entries).
264+
- Per-PR ownership gate (`gh repo view ... --json owner,isFork`).
265+
- PRs grouped by repo, smallest-files-first within each repo.
266+
- Carve-out classes from STEP 1 respected — no port attempts on
267+
`vscode/**`, `**/deps/**`, etc.; those belong to STEPs 4–5 or are
268+
permanent residue.
269+
270+
== STEP 4 — BINDING-GAP DISCHARGE (issue #274)
271+
272+
Discharge the AffineScript binding gaps that block ports in STEP 2's
273+
`blocked on binding X` bucket. For each missing binding the triage tool
274+
emits, ship the AffineScript binding (or escalate the binding as a
275+
permanent JS-residue if the API surface is fundamentally non-bindable).
276+
277+
=== Cross-references
278+
279+
- AffineScript bindings top-50 roadmap: `affinescript#446`. T1 covers
280+
idaptik blockers; T2–T5 cover estate near-term + web universals +
281+
backend + tooling.
282+
- ReScript umbrella `#252` STEP 4 has a parallel "stdlib fill" step
283+
that depends on the same top-50 roadmap. Cross-coordinate: when a
284+
binding closes a gap that unblocks both `.res` and `.js`, count both
285+
campaigns toward the binding's discharge priority. Avoid shipping the
286+
same binding twice or in inconsistent shapes.
287+
288+
=== Disposition per binding
289+
290+
For every binding name STEP 2 emits as blocking ports:
291+
292+
- **Bind it.** PR merged in `affinescript` or `affinescript-stdlib*`.
293+
STEP 2's tool re-runs and reclassifies the affected files into
294+
`portable now`.
295+
- **Carve it out.** The binding is documented as permanent JS-residue,
296+
added to STEP 1's carve-out class 1 with rationale, and STEP 2's tool
297+
reclassifies the affected files into `keep as JS`.
298+
299+
Each binding PR cross-references both #254 (this step) and the AS
300+
top-50 roadmap issue it discharges. Each binding PR cross-references
301+
#252 if the same gap also blocks RS ports.
302+
303+
== STEP 5 — RE-TRIAGE (issue #277)
304+
305+
Re-run STEP 2's triage tool after each significant batch of bindings
306+
lands in STEP 4. Move newly-portable files into the active port queue
307+
(feeding STEP 3 in a second / third / Nth wave), and reclassify files
308+
that turned out to be permanent JS-residue.
309+
310+
=== Iteration pattern
311+
312+
. STEP 4 lands a batch of N bindings.
313+
. Re-run STEP 2's tool with the same exclude-list as the initial
314+
enumeration.
315+
. Diff the bucket counts vs the prior comment on #254 — publish the
316+
delta as a fresh #254 comment.
317+
. Files that moved `blocked on binding X` → `portable now` feed a new
318+
wave of STEP 3 ports.
319+
. Files that triage now classifies as `keep as JS` (because the
320+
binding-gap analysis confirmed a permanent-residue surface) get
321+
codified into STEP 1's carve-out classes via a hypatia-rule update.
322+
. Repeat until the `blocked on binding X` bucket converges to
323+
"permanent residue or out-of-scope".
324+
325+
=== Convergence criterion
326+
327+
The campaign is "complete" when:
328+
329+
- `portable now` bucket is empty (all ported via STEP 3 waves).
330+
- `blocked on binding X` bucket is empty OR every remaining entry has
331+
an open AS top-50 roadmap issue tracking the missing binding.
332+
- `keep as JS` bucket is fully covered by STEP 1's carve-out rules (no
333+
unexplained residue).
334+
335+
The final closeout comment on #254 records the realistic PR count
336+
(umbrella estimates 50–150 — refine with actuals).
337+
338+
== Sequencing
339+
340+
[source]
341+
----
342+
STEP 1 (POLICY) ──┬─→ STEP 2 (TRIAGE TOOLING) ─→ STEP 3 (PORTS, wave 1)
343+
│ │
344+
│ ↓
345+
└─────────────────────────→ STEP 4 (BINDING-GAP) ─→ STEP 5 (RE-TRIAGE)
346+
347+
348+
STEP 3 (wave N) → STEP 4 (wave N) → STEP 5 (wave N) → ...
349+
----
350+
351+
STEP 1 unblocks STEP 2 (the rule needs to exist before the classifier
352+
matches against it). STEP 2 unblocks STEP 3 and STEP 4. STEP 5 cycles
353+
back into STEP 3 / STEP 4 until convergence.
354+
355+
== Per-repo ownership gate
356+
357+
Every PR landed under this campaign verifies the target repo is owned
358+
by `hyperpolymath` and is not a fork:
359+
360+
[source,bash]
361+
----
362+
gh repo view "$REPO" --json owner,isFork \
363+
--jq 'select(.owner.login == "hyperpolymath" and .isFork == false)'
364+
----
365+
366+
If the gate is silent (no JSON output), the PR is rejected — the repo
367+
is either upstream-owned or a fork, and this campaign does not touch
368+
either.
369+
370+
== Cross-reference to memory
371+
372+
Working memory entries:
373+
374+
- `project_estate_unnecessary_js_2026_05_28.md` — campaign trajectory
375+
- `feedback_estate_lang_policy_2026_05_25.md` — banned-language policy
376+
- `project_affinescript_bindings_top50_roadmap.md` — STEP 4 cross-link
377+
- `session_2026_05_30_issue_audit_branch_cleanup.md` — most recent
378+
housekeeping pass that surfaced this documentation gap

0 commit comments

Comments
 (0)