← Spec review

**Grade: C.** A first-rate runtime semantics (Part III) is bolted to a call-semantics design (Part II) that decides what `f(x)` means from the expected type of an overload candidate, the presence of a type annotation, assignability at a return boundary, and in five places the spelling of a name, with no hygiene rule, no lexical-context rule, and an evaluation-order section of four sentences; the compiler's "compiler-101" bugs are the predictable product of those rules, not deviations from them.

Reviewer: fable. Sources: `~/vibe/review-spec/fable/spec/design/src/` (33 chapters) and `DECISIONS.md` (D1-D313), read in full. Twelve probes under `~/vibe/review-spec/fable/probes/` were compiled and run with the audit build; each finding says which probe confirms it. The compiler passed six probes the brief's bug list predicted it would fail (evaluation order, hygiene, lexical context, live bindings, `??`/`||`, `finally` cause combination); the confirmed failures are all places where the compiler does exactly what the spec says.

---

## 1. Call semantics: inventory and the minimal design

The principle under review: *as few call semantics as possible, dispatched only on call-site syntax.* The deeper principle behind it, which the spec violates more often, is **context-freedom**: the meaning of an expression must not change when it is moved to another position, given a type annotation, or passed to a different overload. ECMAScript has this property for every expression; TypeScript preserves it by erasing types (TypeScript design non-goal 5: never "emit different code based on the results of the type system"). VibeLang breaks it in eleven of the seventeen rows below.

### 1.1 Inventory (as the spec stands)

| # | Semantics | Spec | Dispatched today by | Context-free? |
| --- | --- | --- | --- | --- |
| 1 | Run on this fiber (`yield* f(a)`) | ch 13 table rows 1, 3, 5 | Callee's instantiated return type is `Effect.Effect` itself (D24) | Yes (callee's declaration) |
| 2 | Return as data (`Config`, `Exit`, keys, error instances) | D24 | Return type is a *structured subtype* of Effect; plus S22-3: at a return boundary, "a returned value assignable to the contract's declared success type A returns as data; only a returned Effect subtype that is not assignable to A runs" | **No**: assignability to the contextual A |
| 3 | Union of Effects runs, union with anything else is data | D270 (not in spec text) | Union shape of the return type | Yes, but unspecified in the spec |
| 4 | Plain inline call `f$direct(a)` | ch 15, D10, D65, D268 | Callee's control class, a greatest fixpoint over its call-graph component | Yes, but two mechanisms (`f$direct` export vs `f[$direct]` property) |
| 5 | Plain-annotated call `f(a)` | D67 | Callee's return annotation is not an Effect type | Yes |
| 6 | Factory: body runs at call, result runs later | D269 | Declared Effect return type **and** "TypeScript would reject it as the success type A" | **No**: annotation and assignability |
| 7 | Passed by name (`Effect.suspend(() => f(a))`) | D147, ch 13 "How program values are spelled" | "The declared parameter type of the overload candidate TypeScript is trying, before instantiation" (D44), first candidate that accepts wins (D56), ranking hack for `never` (D257) | **No**: overload order of a third-party library |
| 8 | Held: `const p: X!! = f(a)`, object field, top-level const | D262, D265, D269 | A type annotation on the binding or the field's contextual type | **No**: annotations change evaluation |
| 9 | Pipe topic by name vs by value | D154, D155 | Whether the parameter `%` fills expects a program (row 7's rule) | **No** |
| 10 | Lambda form: plain / Effect function / program | D44, D56, D112, D113 | Per overload candidate's declared parameter; "its first classification against a candidate stands"; mixed `boolean \| Effect` prefers plain "when D10 allows" | **No**: overload order and body purity |
| 11 | Service member call: lookup, then rows 1/5 | D41, D90, D311 | Receiver resolves to a Context key; module-name-equals-export rule | Yes by symbol; D311 is name-coupled to Effect's export naming |
| 12 | Data-type method `x.m()` → `Module.m(x)` | D152, D153, D295 | Module that declares `x`'s type, a table of receiver parameter names | Yes by symbol; "a member that a later Effect release adds to a type takes the call" |
| 13 | Control operation (capture) | ch 9, D91 | Receiver extends `Control` | Yes |
| 14 | Trusted foreign call (listed exceptions → E) | D22, D23, D101, D249, D261 | Package origin, JSDoc tag, `trusted(impl, [E])` value, `trustedPackages` glob, **or** the enclosing function being plain-annotated (ch 15) | **No**: position (plain function) bypasses trust |
| 15 | Hatch (`sync`, `try`, `callback`), `module` block | D148, D229-D232, D283 | Call-site syntax: a prelude name resolved by binding (`hatchNames` + `shadowed()`), `module` keyword | Yes (the one row that follows the principle) |
| 16 | Keyword forms: `fork e`, `join e`, `await.x [..]`, block params, `do {}`, `x catch y`, `` tag`…` `` | D166, D196, D188, D187, D236, D267 | Syntax, then row 7 for the operand; block placement depends on whether the callee "is an Effect dual" (D188) | Mostly yes; D188 placement is type-dispatched |
| 17 | Runner / entry: starts a fiber, suppresses `$entry` | ch 13 ("resolved symbols"), D135 ("any call of a function named `runMain` … `runSync`") | **Spelling** in `entry.go:130 runnerNames`; symbol in EK1005 | **No**: contradiction inside the spec; probe `runnername` |

Seventeen rows, eleven context-dependent. The outside engineer's count is right.

### 1.2 Proposed minimal design: three semantics, dispatched by syntax, one nominal check

| Semantics | Syntax | Dispatch rule | What it replaces | Programs that change (LANGUAGE CHANGE) |
| --- | --- | --- | --- | --- |
| **Run** | `f(a)`, `x.m(a)`, `S.m(a)`, `` f`…` ``, `new C(a)` | The call is evaluated as ECMAScript evaluates it (13.3.6). If the callee's declared return type *symbol* is `Effect.Effect`, the result is run on this fiber. Every other return type, including structured subtypes (D24 kept, as a nominal symbol test, no assignability), is a value. `new` never runs. | Rows 1-6, 11-14, 17 | Row 6 factories: a function returning a program is an ordinary function returning a function. Row 14: EK4401 applies in plain functions too (probe `plaintrust`). Row 17: user functions named `runSync` stop hiding `main`. |
| **Quote** | `() => e`, `() => { … }`, `fork e`, `join e`, `await.x [e, …]`, `f(args) { … }`, `provide L { … }`, `uninterruptible { … }` | Only these forms build a program. A program is cold: `e` is evaluated once per run, nothing at construction. No expected type, annotation, or overload ever converts a running call into a program. | Rows 7, 8, 9, 10 (program form), D257 | `retry(fetchUser(id), p)` → `retry(() => fetchUser(id), p)` (their census: thunks were 2.6% of tokens); `fetchUser(id) \|> retry(%, p)` → `(() => fetchUser(id)) \|> retry(%, p)` or `retry(p) { return fetchUser(id) }`; `const p: X!! = make()` → `const p = () => make()`; `{ read: work() }` → `{ read: () => work() }`; `NodeRuntime.runMain(main())` → `runMain(main)`. EK1012/EK4802 and their whole class disappear. |
| **Escape** | `sync(…)`, `try(…)`, `callback(…)`, `module m { … }` | Call-site syntax; the first argument is TypeScript. Trust (`trusted(impl, [E])`, `trustedPackages`) classifies *failures* of a Run; it is not a fourth call semantics. | Row 15 | None. |

Lambdas (row 10): one calling convention. A lambda in a `.vibe` file compiles to its Effect form unless its *resolved* contextual type is a non-Effect function type, in which case it must be plain or EK4301. `$direct` stays an optimization that no library can observe: APIs that dispatch with `Effect.isEffect` at run time (`HttpRouter.add`, `Effect.withErrorReporting`, ch 13) see one form. Overload declaration order then selects a *type*, as in TypeScript, never an emitted form.

Falsifiable claim: at least half of the fifty audit bugs sit in rows 2, 6, 7, 8, 9, 10, 14, 17. If the audit reports show the majority in rows 1, 11, 13, 15, this section is wrong and the inventory should be re-cut by emitted shape rather than by dispatch.

---

## 2. Findings, ranked

### Blockers

**B1. The meaning of an argument depends on which overload candidate TypeScript tries, in which order.** Ch 13 "Function values" and "How program values are spelled"; D44, D56, D257.

> "The expected type" means exactly the declared parameter type of the overload candidate TypeScript is trying, before instantiation (D44). … With overloads, the first candidate that accepts the lambda in the form it expects wins (D56). … Swap the overloads and `a`'s lambda becomes an Effect function: declaration order decides, as in TypeScript. (ch 13)
> Each lambda is checked at most once per form, and its first classification against a candidate stands, so a deferred re-check never reclassifies it. (D56)
> A running call whose result type is `never` makes no parameter applicable during ranking. (D257)

First principles: overload resolution is a *typing* algorithm; TypeScript lets its order pick a signature because signatures are erased. VibeLang makes it pick an *evaluation strategy* (run now, suspend, plain body, generator body). A library that adds an overload, reorders two, or widens a parameter to `A | Effect<A>` changes when its callers' arguments evaluate, with no diagnostic. D257 is the tell: a ranking rule invented because `Effect.race(Effect.fail(a), Effect.fail(b))` picked the curried overload and ran the second argument as `options`. Prior art: Scala by-name parameters (SLS §4.6.1) are the closest model and are declared on the resolved signature by the *language's own* compiler; Algol 60 call-by-name showed the re-evaluation hazard (probe `byname`: `retry(fetchIt(nextId()), …)` evaluates `nextId()` once per attempt, an expression the author wrote once). Effect's signatures were never designed as by-name annotations.

Rewrite (ch 13, replacing "How program values are spelled" paragraphs 1-5 and the overload paragraph): "A call runs where it stands. The only expressions that build a program are a function with no parameters, the keyword forms `fork`, `join`, `await.all`/`allSettled`/`race`/`any`, a block param, and the block statements `provide`, `uninterruptible`, `restore`, `interruptible`. No parameter type, contextual type, annotation, or overload converts a call into a program. An argument whose parameter expects an `Effect` and whose expression is a running call is EK13xx: write `() =>` before it." LANGUAGE CHANGE (see §1.2).

**B2. A type annotation changes when a call runs.** Ch 5 "Storing, returning, and composing programs"; ch 13; D262, D265, D269.

> A `const` whose declared type is Effect holds its initializer in every scope (D269, extending D262 and D265). `const built: Made = make(config)` calls `make` here and runs none of the returned program … Mutable bindings and inferred or asserted types do not opt in. (ch 13)
> A return is a program when it is a parameterless function literal, converted by D7, or a non-call expression of Effect type, and TypeScript would reject it as the success type A. (ch 13)

Probe `heldconst`: `const a = make("a")` runs at the declaration; `const b: number!! = make("b")` does not run until `b()`. Same expression, one annotation, different trace. First principles: this is "emit depends on the type system" (TypeScript non-goal 5) in its purest form, and it compounds with B1: D269's own amendments (2026-10-23) had to decide separately whether holding reaches through parentheses, `as`, `satisfies`, `!`, a conditional's branches, and a pipe's body; each is a new depth of "initializer" the spec never defines. The factory rule's trigger, "TypeScript would reject it as the success type A", makes evaluation depend on an assignability failure. Rewrite: delete D262, D265, D269; add to ch 13: "An Effect-typed binding, field, or return whose expression is a running call is an error (EK13xx) with the fix `() =>`; a parameterless function literal is the program." LANGUAGE CHANGE.

**B3. An Effect value returned from a lambda becomes its success value; the spec downgrades this to a warning because fixing it "waits for The design".** D24 (S22-3 amendment), D241 (EK4802), ch 13.

> at an Effect return boundary, a returned value assignable to the contract's declared success type A returns as data; only a returned Effect subtype that is not assignable to A runs. A contextual A of `unknown` or `any` names no success value (D24)
> Running these instead would change programs that compile today, so it waits for The design. (D241, EK4802)

Probe `release`: `using h = Effect.acquireRelease(() => "h", () => cleanupProgram)` compiles with warning EK4802 and **never runs the release**; `() => { return cleanupProgram }` likewise. The resource leaks silently in the one construct whose whole purpose is not leaking. Probe `hygiene` hit the same warning on `() => Effect.void`. First principles: the typed error channel and the Scope guarantee are sound only if an `Effect`-typed expression in program position is the program. Effect's own `acquireRelease` typing (`release: (a, exit) => Effect<void, never, R>`) is satisfied by `Effect<Effect<void>>` only because `R` is an unconstrained type parameter, which S22-3 then reads as "A names no value". Rewrite (D24, replacing S22-3): "At a return boundary of an Effect-typed function, a returned expression whose type is `Effect` itself is the program the function returns, whatever A is; it is data only when the function's written success type is itself an Effect type. EK4802 is an error." LANGUAGE CHANGE: D241 counts four returns in 2 of 468 acceptance cases.

**B4. A `match` without `default` over an open error family compiles and dies with a defect.** Ch 3 "Matching errors and outcomes", ch 11, D209, D304.

> A match without `default` over a finite union must cover every case … Over any other type, an unmatched value throws the proposal's `TypeError` at runtime (`$noMatch`). (D209)

Probe `openfamily`: `error Base { _tag: string; code: string }`, `error Sub extends Base {}`, `catch (e) { return match (e) { when Sub: "sub"; } }` compiles clean and `h("base")` fails with `TypeError: No match arm matched Base`, a defect raised from a typed catch. D304 makes every family base's tag `string`, so every family is an open type, so every `match` on a caught family failure is unchecked. The typed error channel promises "the caught value's type is exactly the errors the try block can produce" (ch 3); here the handler turns an expected failure into a defect that no contract lists. Prior art: Rust `#[non_exhaustive]` forces a wildcard arm; Kotlin `when` used as an expression over a non-sealed type requires `else`. Rewrite (D209): "A `match` expression without `default` whose subject's declared type is not a finite union is EK47xx: add `default`." A `match` statement may keep the run-time check. LANGUAGE CHANGE (adds `default` arms).

**B5. Runners are recognized by spelling, contradicting ch 13.** Ch 1, ch 13 "Where programs start", D135; `entry.go:130`.

> The compiler recognizes runners by their resolved symbols (ch 13)
> A module that calls a runner at its top level, any call of a function named `runMain`, `runPromise`, `runFork`, `runSync`, or `runCallback`, starts its program itself and gets no entry statement (D135)

Probe `runnername`: a user function `function runSync(x: number): number` called at the top level removes the entry statement; `main` never runs; exit code 0; no diagnostic. The same family: EK2004's heuristic "a trusted export named `...Sync` or `...OrThrow...`" (D282 amendment; `failures.go:345`), and D208's matchers, "a local variable named `Option` that shadows the module still compiles to `Option.isSome`" (on this build the shadowed form is rejected with TS2339, so D208 is also stale). Hatches are done right (`hatchNames` plus `shadowed()`), which proves the compiler has the mechanism. Rewrite (ch 13 and D135): "A runner, a hatch, a matcher, a prelude member, and a data-type module are recognized by the symbol a reference resolves to. No rule in this specification may name a function by its spelling." Delete the `Sync`/`OrThrow` heuristic or make it a hover note. Not a language change: only wrong programs change.

### Major

**M1. No hygiene requirement exists.** Ch 26 (the chapter that owns generated code), ch 27, D62, D104, D123, D303 corrections.

The spec names generated identifiers everywhere (`scope_1`, `arg_1`, `super_1`, `arguments_1`, `Effect_1`, `runtime`, `$caught`, `$performed`, `$k`, `run$using`, `main.vibe.text.ts`) and never once says they must not capture or be captured. The decision log shows the cost: D303's 2026-10-24 note ("Both were `Effect_1` … the file declared the name twice and did not load"), D303's correction ("a body-local, imported, or file-level binding named `Object` cannot replace it"), D123 (free aliases in `.d.ts` only). Probe `hygiene` (locals named `runtime`, `scope`, `$caught`) passes on this build, so the fixes landed; the rule did not. Prior art: R7RS §4.3 hygiene for `syntax-rules`; Racket scope sets; TypeScript's own `factory.createUniqueName`/`GeneratedIdentifierFlags.Optimistic`, which the fork already has. Rewrite, new obligation beside the frame invariant in ch 26: "**Hygiene** [Rule]. Every binding the compiler introduces is a unique name the checker proves unbound in the enclosing scopes (`createUniqueName`); every runtime intrinsic is reached through one emitter-owned import binding that no source declaration can shadow; every reference the lowering moves resolves to the binding it resolved to in the source. A conformance case shadows every generated name and intrinsic."

**M2. Evaluation order is specified in four sentences and the by-name rule silently changes it.** Ch 13 "Evaluation order"; D147, D154, D269.

> Generated code keeps JavaScript's evaluation order. Arguments are evaluated left to right, each once. A receiver is evaluated once and keeps its `this`. `a() && b()` runs `b` only if `a()` is truthy. A getter runs once per access. (ch 13, the whole section)

Unstated: (a) a by-name argument's operands are evaluated once *per run*, not once (probe `byname`, `nextId` twice); (b) elements of `Effect.all([a(x()), b()])` start on separate fibers, so `x()` no longer precedes `b()`'s evaluation, breaking ECMAScript ArrayLiteral order; (c) D154's substitution rule, "a topic used once, before anything in the body that could have an effect, becomes the left side itself", with "could have an effect" undefined; (d) D269's three holding depths (initializer, conditional branches, pipe body) versus "calls in its operands keep their ordinary positions"; (e) in `S.m(a)` whether the service lookup (`yield* S`) precedes argument evaluation (the generated code says yes; the spec does not). Probe `order` shows the compiler keeps source order for everything that runs in place, so the gap is specification, not emit. Rewrite: a normative "Evaluation" section with numbered rules: E1 every operand of a running call is evaluated once, left to right, before the callee runs (ECMAScript 13.3.6.1); E2 a program literal evaluates its body once per run and nothing at construction; E3 a service member call looks the service up after its arguments are evaluated; E4 a pipe's left side is evaluated exactly once, before its body; E5 a receiver, a getter, and a tagged template's template object are evaluated once per access. Under §1.2, (a) and (b) disappear with by-name passing.

**M3. Whether a callback parameter is effect-polymorphic depends on whether its type is written in place or through an alias.** Ch 16; D12, D68.

> only a function type written in place gains a contract, so a named one keeps TypeScript's meaning and takes plain functions. (ch 16)

`f: (a: A) => B` and `f: Fn<A, B>` with `type Fn<A, B> = (a: A) => B` are the same type in TypeScript (handbook: an alias "is just a name"); here they have different contracts and different EK4301 behavior. Prior art: Koka and Effekt make effect polymorphism an explicit row variable in the type, never a property of spelling. Rewrite (ch 16): "A callback parameter whose *resolved* type is a function type returning a non-Effect type accepts any contract, through any alias. To take a plain function, write `(a: A) => B` with a `/** @vibelang plain */` tag, or `Predicate.Predicate<T>`'s declared form." LANGUAGE CHANGE (aliased callbacks gain channels).

**M4. A plain-annotated helper bypasses the trust boundary.** Ch 15 "The rule"; D282; probe `plaintrust`.

> Calls of TypeScript code, including callback parameters and trusted or untrusted imports, are ordinary calls needing no trust boundary; their exceptions propagate unchanged and become defects where they reach an effectful caller. (ch 15)

`function viaPlain(n: number): number { return risky(n) }` compiles; `function viaEffect(n) { sleep(…); return risky(n) }` is EK4401. The "vouch once" policy of ch 10 ("foreign code is untrusted until something vouches for it") has a two-line workaround. Rewrite: "EK4401 applies in every function of a `.vibe` file. A plain function calls untrusted TypeScript only through a hatch or a `module` block." LANGUAGE CHANGE.

**M5. The spec mandates double disposal and records the rule as open.** Ch 6; D25 (amended 2026-10-23: "Open."), D94 ("needs an owner decision"); ch 0: "Every rule in this specification is decided."

> A Disposable result also runs `[Symbol.dispose]()` at block exit. If its Effect release already disposes it, this disposes it twice (D94). (ch 6)

First principles: TC39 Explicit Resource Management calls `[Symbol.dispose]` exactly once per `using`; `Effect.acquireRelease` runs its release exactly once; composing the two must not double. Rewrite (ch 6): "`using x = e` where `e` runs an Effect registers nothing of its own: the acquisition's release is the only release. `using x = e` where `e` is a Disposable value registers `[Symbol.dispose]()` once." Delete the holder recipe. Mark ch 0's "every rule is decided" false until D25/D94 close. LANGUAGE CHANGE (a program depending on double disposal is broken already).

**M6. D10 is stated three incompatible ways.** DECISIONS.md D10 versus ch 15 "Recursion" and ch 29's D10 row.

> Pure recursion runs on the JavaScript stack, like any JavaScript function; past the host limit it is a `RangeError` defect (resolves O10). (DECISIONS.md D10)
> a function on a cycle of plain calls keeps its plain type, runs shallow recursion as fast JavaScript, then continues on heap frames without a recursion-depth limit (ch 29, D10)
> Private cross-module entries are cached against their live direct bindings, without changing function objects. Large native frames consume more of the budget. … This is a bounded eager optimization, not a guarantee (ch 15)

Ch 15's paragraph describes an implementation (an eager budget, a `$fast` protocol, cache keys) as a rule, and it contradicts the decision log it cites. Rewrite: one sentence in ch 15, "A function that compiles to plain JavaScript recurses on the host stack; a function that compiles to frames recurses on the heap," and move the budget text to `typescript/VIBELANG.md`. Choose, and amend D10 once.

**M7. Lexical context has no rule.** Ch 26; D92 (amended 2026-10-23), D274, "Known gaps" (F3, F6, F7: "Region closures capture their own `arguments`", "Producer bodies capture their own generator's `arguments`", "A hatch's function may still read an enclosing function's `this` or `arguments`").

Probe `selfref` passes on this build (named function expression self-reference, `arguments` inside a `try` region, `this` in class and object methods, `this` across a `finally`), so the compiler now does the right thing; the spec still says nothing, and the gaps list says the opposite. Rewrite, new obligation in ch 26: "**Lexical context** [Rule]. A lowering preserves the value of `this`, `arguments`, `new.target`, `super`, and the self-binding of a named function expression as ECMAScript 10.2 defines them for the source function. A body moved into a generator or a region closure binds them once at the source function's entry (`const this_1 = this`, …) and reads the bindings." Add the four cases to the acceptance suite.

**M8. Two mechanisms for one plain-call ABI, and ch 27 contradicts D268.** Ch 27 "Declarations and metadata"; D65, D268.

> Chapter 15's control-class and `$direct` JSDoc metadata is a design target; the compiler neither emits nor reads it. (ch 27)
> The checking projection merges a phantom namespace containing `$plain: $Plain<typeof f>` into the exported function; its plain call signature survives aliases and barrels. (D268)

D65 exports `f$direct` by name (with a CommonJS special case `(0, math_1.double$direct)(n)` and `default$direct`); D268 puts `f[$direct] = f$direct` on the function object and resolves through `$adapt`. Probe `livebind` shows both work through a re-export today. Zero-debt rule: one mechanism. Rewrite: keep D268's property (it survives re-exports, defaults, and CommonJS without special cases), delete D65's named variants, and correct ch 27.

**M9. "Interchangeable" spellings that are not.** Ch 14; D308, D309.

> The two spellings are interchangeable; this specification and the guides write the `!` form. They differ only where a channel is left out: `User!NotFound` on a function with a body infers R, while `Effect<User, NotFound>` checks it as `never`. (ch 14)

Inference by omission is the "silent confusion settled decision 2 forbids" (D57's own words). Rewrite: "`A!E` means `Effect<A, E, never>` everywhere. To infer a channel, write `infer`." LANGUAGE CHANGE: bodies that relied on `A!E` inferring R get EK2002 with a one-token fix.

**M10. The `handle` statement's `resume` type is unsound by decision, with a warning.** Ch 9; D97 (amended 2026-10-23).

> the narrower type compiled programs that failed when they ran, such as a clause that wrote `const n: number = resume()` while another clause returned `"early"` … A clause's return computed from resume's own result stays out of that type, since it would need the fixpoint; the warning EK2119 marks one that does not fit (D97)

A checker that knows a value may be outside its type and warns is not a checker. Rewrite: type a non-tail `resume()` as the handle statement's full completion type computed by the fixpoint D50 specifies, or make EK2119 an error. LANGUAGE CHANGE (warned programs become errors).

**M11. Lambda form follows body purity, which libraries can observe at run time.** Ch 13 "Function values" last row; D39, D112.

> Without [parameters]: the function if it compiles to plain JavaScript, a program if it can pause. … several Effect APIs choose at run time with `Effect.isEffect`. (ch 13)

Adding `log(…)` to a zero-parameter callback changes the value `HttpRouter.add` receives from a function to an Effect. Rewrite: see §1.2, one calling convention; D39 and D112 are deleted.

**M12. Name-coupled resolution across Effect upgrades.** D152, D311.

> a member that a later Effect release adds to a type takes the call from the module function of the same name (D152)
> The module's exports win: `Option.some`, `FileSystem.layerNoop` … keep their meaning (D311)

Both rules resolve by symbol at compile time, so they are not spelling bugs; they are *upgrade* bugs: an Effect release that adds `Stream.prototype.take` or exports `Path.join` as a module function silently retargets existing calls. Rewrite: pin both tables to the Effect version the runtime checks (it already pins 4.0.6 exactly), and make a resolution that changes across versions an error naming both targets, as `TestDataFirstParametersMatchEffect` already does for one table.

**M13. Decisions the spec text does not reflect.** D257 (overload ranking), D268 (purity marker), D270 (union calls), D271/D296 (JavaScript-iterator generators), D272 (curried topic stage), D273 (postfix after block params), D280 (`schema Name = type`), D283/D303 (`module` declarations: the spec defines them only in the EK4407/EK4408 rows and one aside in ch 11), D293 (catch-expression contextual typing), D294 (last arm untested), D297, D298. Each is a semantics-bearing rule a reader of Parts I-II cannot find. D303's snapshot export rule ("a later assignment to an exported `let` is not seen") is a documented departure from ECMAScript live bindings that belongs in ch 10 beside the live-binding promise the spec never makes (probe `livebind` shows ordinary `.vibe` modules keep live bindings; the spec should say so).

### Minor

- **m1.** Ch 29's decision table repeats the D13 row six times.
- **m2.** Ch 0 and Appendix A say `DECISIONS.md` "records … D1-D60"; it runs to D313.
- **m3.** DECISIONS.md D1 ("The `service` keyword is removed"), D2, and D5 ("`Effect.try` or `Effect.tryPromise`") are superseded by D128/D223 and D148 without an amendment note.
- **m4.** Ch 2: "There is one kind of function." Ch 15 and D269 define three (Effect, plain-annotated, factory).
- **m5.** Appendix B's TC39 table says pattern matching and throw expressions are "Not adopted", and ch 12 says the pattern-matching proposal "is not used", while ch 3, ch 11, D186, and D206 adopt both. Direct contradiction in the reader-facing part.
- **m6.** Ch 10 leaves `new URL("bad")`, `new Intl.…`, `structuredClone` unclassified: not platform, not in the trusted built-in list, so a TypeError from them has no stated classification.
- **m7.** Ch 7: "Returning a block-owned child currently returns a fiber already interrupted when that block exited; the compiler does not warn." An accident recorded as behavior; make it EK.
- **m8.** D91 ("pending F7") and D82 (`$trusted` by name or tag) describe a runtime the current one no longer has (`boundaries.ts:33` uses `instanceof`); retire the notes.
- **m9.** Ch 13 says `catch` binds "tighter than `? :`" and D154 says `|>` binds "looser than … the conditional operator"; consistent, but neither states the full precedence table in one place. Add one.
- **m10.** `infer Value` repeated in three channels "does not connect the channels", the opposite of TypeScript's `infer`. Rename the keyword or connect them.

---

## 3. What the spec gets right

- **The machine (ch 20-23).** A transition-rule semantics with atomicity stated, evidence tags per claim, two implementations (A and B) and a plan to run one law suite against both. Few language specs reach this standard.
- **Cause algebra (ch 17, D305, D306).** Aligned with Effect 4.0.6 and tested (F11, K14, K16). Probe `finally`: `try { throw A } finally { throw B }` yields `reasons=["Fail","Fail"]`; a finalizer failure is added, never substituted. The brief's "`finally` replaces the failure" bug is not what this build does for this shape.
- **Snapshot Context (ch 22)** with the DDB comparison and the security argument (CVE-2026-32887). Correct call, correctly argued.
- **One-shot ownership (ch 21)** and the prior-art table (Appendix B): every rule has a counterpart in OCaml 5, Kotlin, Swift, or WasmFX.
- **Frame invariant and `finally`-never-JavaScript (ch 26)**, EK2006, `onExit` over `ensuring`, nominal services with the optional brand (D45), self-contained hatches (D229): each closes a real hole.
- **Runners by symbol (ch 13)** and hatches by binding (`hatches.go:56`): the right rule exists; it needs to be the only rule.
- **The compiler on this build** keeps ECMAScript evaluation order (probe `order`: `a,…,m,get,n` in source order), short-circuits on the value not the Effect (probe `nullish`), preserves `this`/`arguments`/self-binding (probe `selfref`), keeps live bindings (probe `livebind`), and is hygienic for the names probed.

---

## 4. Reading list: the three changes that remove the most compiler bugs

1. **Make program construction syntactic** (B1, B2, B3, M2, M11): delete D147, D155, D257, D262, D265, D269, D24's S22-3, D39, D112; programs are `() =>` and the keyword forms only; EK4802 and EK1012 become errors or vanish. One rule replaces nine, and overload order stops choosing emit.
2. **Add three normative obligations to ch 26 with a conformance suite** (M1, M2, M7, B5): Hygiene, Lexical context, Evaluation order, each with numbered rules and cases, and the sentence "No rule may name a function by its spelling" (retire D135's runner list, the `Sync`/`OrThrow` heuristic, D208's shadow clause).
3. **One calling convention for lambdas and one ABI for plain calls** (M8, M11, D56): a lambda's form follows its resolved contextual type only; `$direct` is an unobservable optimization carried by D268's property alone.

Then close the open rules the spec claims are closed (M5, M6, M9, M10) and reconcile Parts I-II with D257-D303 (M13, m5).

---

## Appendix: probes

All under `~/vibe/review-spec/fable/probes/<name>/src/main.vibe`, run with `bash ~/vibe/fix/alpha/audit/probe.sh <dir>`.

| Probe | Checks | Result |
| --- | --- | --- |
| `nullish` | `??`, `\|\|`, `&&`, `?:`, `!` on an Effect call's value | Correct |
| `order` | 13 operands across `+`, array, object, call, getter, `timeout`, `fork`/`join` | Source order, each once |
| `selfref` | named function expression self-call, `arguments` in a `try` region, `this` in class/object methods and across `finally` | Correct |
| `hygiene` | locals named `runtime`, `scope`, `$caught` beside generated code | Correct; EK4802 warning on `() => Effect.void` (see B3) |
| `shadowopt` | D208's claim that a shadowing `Option` still lowers to `Option.isSome` | TS2339: rejected, not captured; D208 stale |
| `livebind` | `export let` read through a re-export after mutation; `$direct` through a barrel | Live; correct |
| `finally` | `try { throw A } finally { throw B }` cause | `["Fail","Fail"]`; correct |
| `openfamily` | `match` without `default` over an open error family | **Defect at run time** (B4) |
| `plaintrust` | untrusted TypeScript call inside a plain-annotated function | **Compiles** (M4); EK4401 only in the Effect function |
| `heldconst` | annotation-dependent holding (D269) | Holds as specified (B2) |
| `release` | Effect value returned from `acquireRelease`'s release lambda | **Release never runs**, warning only (B3) |
| `byname` | by-name re-evaluation and concurrent evaluation order | `nextId` evaluated per attempt (M2) |
| `runnername` | user function named `runSync` called at top level | **`main` never runs**, no diagnostic (B5) |