What
The JS frontend now desugars the spellings models write most often into the let/call steps the author could have written by hand, instead of rejecting them and costing a repair round-trip. Nothing about the plan format, validation, limits, or reconciliation changes: every commit is frontend-only, and the plan that comes out is one the author could already have written.
One commit per feature, reviewable independently:
- Destructuring –
const { items, total = 0 } = await repo.list({})is the call plus one derivation per name (items = s1.items);const [head, ...tail] = itemsreads by index andslice. Renames, defaults, nesting, string keys, object rest, and nested patterns in aPromise.alltuple are covered. Insidetry/catchthe fields carryif: !($errors.<id>). Computed keys, forbidden keys, and destructuring a detached call keep a pointed message.patternNamesrecurses so minted ids never collide with deep pattern names. inoperator –"labels" in issueevaluates asObject.hasOwn: plain data only, never a prototype chain, forbidden names still unreachable, non-object RHS is a type error as in JS.- Pointed rejections –
new DatenamesDate.parse/Date.now(it used to get thenew Setdedupe hint),new RegExpnamesincludes/startsWith/endsWith, a bareforEachnames thePromise.all(list.map(...))fan-out,console.logsays to return the value, and an unknown tool suggests the nearest mounted name (capped edit distance, or the same tool under another namespace). No guess when nothing is close;unknownToolHintstill rides along. - for..of bodies – a loop body may carry pure locals (inlined into the call as renames),
if (cond) continue, and a trailingif (cond) { ... }around the one awaited call. Guards become a.filteron the list, so theslice(0, N)bound still caps the fan-out. A second call, anelse, or areturnstays rejected with the fan-out spelling. - Nested awaits hoist –
return { issues: await repo.list({}) }andclose({ id: (await find({})).id })compile each nested awaited call into its own step in source order, and the expression reads the step id. Hoisted calls join the effect frontier like any awaited call. Not hoistable, with the message: an await inside an arrow, inside a fan-out's own arguments or a loop body, a nestedPromise.all, or an await on a non-call. let x = a; if (cond) x = b– the conditional-value idiom compiles to the single derivationx = cond ? b : a(else branch, block bodies, dead initializer covered; inherits an enclosing guard). Else-if chains, other work in a branch, or an await anywhere in the pair keep the single-assignment message.- Wider pure expression surface – arrays:
at,findLast,findLastIndex,lastIndexOf,toSorted,toReversed; strings:at,substring,trimStart,trimEnd,charCodeAt,concat; callable globalsparseInt,parseFloat,isNaN,isFinite,encodeURIComponent,decodeURIComponent,encodeURI,decodeURI, usable bare and as callbacks (xs.map(Number),xs.filter(Boolean)); spread in call arguments (Math.max(...xs)); non-mutatingObject.assign. Array mutators name the immutable spelling;Math.randomsays why it is out (a run must replay deterministically). - Wrappers unwrap –
(async () => { ... })()(awaited or not, arrow or function expression) andasync function main() { ... } main()compile as the program: the body'sreturnis the output, a leading comment the intent, inner names still reserve their ids. A.then/.catchchained on the wrapper is rejected with the reason. - Fan-out arrows with a block body –
list.map(async i => { ... })shares the for..of body grammar: locals,if (cond) return;as the skip guard, a trailingif (cond) { ... }, one awaited call (bound, bare, or returned). One shared walker (fanOutBody) and step builder (fanOutStep) serve both. - Docs (two commits) – README lists every new spelling and the pure natives expressions cover; the js.ts header points at the helper owning each desugar.
Why
On a probe of 35 scripts written the way models actually write them, 16 were rejected by the frontend, and most of the rejections were these shapes – destructuring alone hit on every retry. Each one had an exact spelling in the plan already; the frontend just didn't do the translation.
Tests
45 new cases across js.test.ts, expr.test.ts, and validate.test.ts: each form above, end-to-end runs through engine.run, and every rejection path. Suite: 318 passing, typecheck clean, biome clean. Probe of 35 model-written scripts: 28 accepted, up from 19 on main; the 7 left are deliberate and each names its fix.
@EzraBuild is attempting to deploy a commit to the Vercel Labs Team on Vercel.
A member of the Team first needs to authorize it.