Skip to content

Commit ed8b175

Browse files
Merge pull request #37 from hyperpolymath/docs/migration-playbook-v1.1
docs(playbook): v1.1 — service-locator row + coupled-writes subsection
2 parents 7644c85 + b11555e commit ed8b175

2 files changed

Lines changed: 34 additions & 5 deletions

File tree

docs/guides/lessons/migrations/idaptik-user-settings.adoc

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -287,9 +287,13 @@ when the stdlib catches up.
287287
the actual destination is `docs/guides/lessons/migrations/`.
288288
*Proposed fix: one-line correction, alongside this lesson's commit.*
289289

290-
Gap (3) is fixed in the same commit as this file. Gaps (1) and (2) are
291-
playbook-text changes deferred to a v1.1 follow-up so the policy PR (#36)
292-
can land focused.
290+
Gap (3) is fixed in the same commit as this file. Gaps (1) and (2) were
291+
deferred to a v1.1 follow-up so the policy PR (#36) could land focused;
292+
both are now addressed in playbook v1.1 — see the
293+
xref:../../migration-playbook.adoc#source-pattern-index[service-locator
294+
row in the ReScript table] and the
295+
xref:../../migration-playbook.adoc#decision-criteria[coupled-writes
296+
subsection in Decision Criteria].
293297

294298
== What this lesson covers, generalised
295299

docs/guides/migration-playbook.adoc

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22
// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
33
= AffineScript Migration Playbook: Re-decomposition Patterns
44
Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
5-
v1.0, 2026-05-02
5+
v1.1, 2026-05-02
66
:sectnums:
77
:toc: left
88
:icons: font
99
:source-highlighter: rouge
10-
:revnumber: 1.0
10+
:revnumber: 1.1
1111
:revdate: 2026-05-02
1212

1313
[NOTE]
@@ -90,6 +90,10 @@ The rest of this document is a catalogue of common source-language patterns and
9090
| `unit` returns from impure code
9191
| `() / IO` (or whichever effects apply)
9292
| In ReScript, `unit` says nothing about whether the function did IO. In AffineScript, the effect row makes it visible.
93+
94+
| Module-level getter for a singleton runtime — `Module.get(): Option<Thing>`
95+
| Lift to an effect (`effect Thing { fn current() -> Option[ref Thing]; }`); make the "is the runtime available?" branch a typed `Result`, not a silent no-op
96+
| A service-locator is mutable global state with a thin `Option` wrapper. Every consumer gains a hidden dependency on whether the global has been initialised, and the "not initialised" branch tends to become a silent no-op that masks real bugs. Lifting it to an effect makes the dependency visible at the call site and lets tests install a mock without monkey-patching a global.
9397
|===
9498

9599
=== TypeScript → AffineScript
@@ -157,6 +161,23 @@ The default for resources (files, sockets, tokens, allocations) is `own` — any
157161

158162
Prefer `own`. Reach for `@linear` only when forgetting to consume the value is a real bug — typically protocol handles, transactions, and capability tokens.
159163

164+
=== Coupled writes — multiple effects in one action
165+
166+
When a single user action must update both an in-memory subsystem AND persist to storage (volume that touches `Audio` and `Storage`, theme that touches `UI` and `Storage`, etc.), there are two ways to express it:
167+
168+
[cols="1,2"]
169+
|===
170+
| Shape | When to choose
171+
172+
| **One function calling two (or more) effects.** Default.
173+
| Each effect is independently meaningful and independently testable. The wrapper function *is* the coupling; it is visible at every call site through both effects appearing in the row.
174+
175+
| **A wrapper effect that internally sequences the two.** E.g. `effect Settings { fn set_master_volume(v: Float) -> Result[(), NoEngine]; }`.
176+
| The runtime invariant is "Audio and Storage must always agree" and partial success is a bug. The wrapper's handler enforces atomicity (both succeed or neither does), which the two-effect call site cannot.
177+
|===
178+
179+
The decision tracks the same logic as `mut` vs `State`: keep coupling local unless the invariant must be observable across callers. If two callers can validly compose `Audio.set_master_volume` and `Storage.set_number` differently — for example, "set the audio without persisting" for a transient ducking effect — the wrapper effect is wrong, because it forecloses that composition.
180+
160181
[#anti-patterns]
161182
== Anti-patterns: Faithful-but-monolithic Translation
162183

@@ -274,4 +295,8 @@ If you complete a non-trivial `.res → .affine` translation and the re-decompos
274295
| 1.0
275296
| 2026-05-02
276297
| Initial draft. ReScript and TypeScript pattern indices, decision criteria, file-buffer anti-pattern. Companion to `frontier-guide.adoc` v1.0.
298+
299+
| 1.1
300+
| 2026-05-02
301+
| Two additions surfaced by the link:lessons/migrations/idaptik-user-settings.adoc[idaptik UserSettings design walk-through]: (1) service-locator-globals row added to the ReScript pattern index; (2) coupled-writes subsection added to Decision Criteria, with the rule "keep coupling local unless the invariant must be observable across callers." No removals; existing v1.0 guidance unchanged.
277302
|===

0 commit comments

Comments
 (0)