BREAKING CHANGES:types/rtti: a bare Struct or Tuple schema is closed — it admits the members it declares and no others, and a tuple bounds its length. close(c, rest) becomes rest(c, r), whose second argument is required, and open(c) is the old bare form. (#1732)
BREAKING CHANGES:types/rtti/common: the Visitor<R> handler close is renamed rest and its second parameter is a Type rather than Type | undefined; undeclaredEntries becomes undeclaredMembers. (#1732)
BREAKING CHANGES:types/rtti: an array whose element set admits nothing is the empty array, on every reader — array(or()) no longer accepts a hole-only array, which the data form already rejected. (#1732)
BREAKING CHANGES:media: dialectEntry takes a schema with a stated rest rather than a bare struct, so a registered dialect says open. (#1732)
types/rtti: an index a value's prototype supplies is a member like any other — held to a container's rest, and to array(t)'s element type — on all three readers; an own-entry walk skipped it. (#1732)
types/rtti: a canonical numeric key at or above 2 ** 32 - 1 is an ordinary property rather than an index, so a closed container rejects it; reading it as an index let it past both the length bound and the non-index check. (#1732)
types/rtti: reading an array's members is linear in its length again — the index walk added above deduplicated with a linear search per index, which cost every array(t) read 829 ms at 40 000 elements against 3 ms at 1 000. (#1732)
types/rtti/ts: Ts<> renders a stated rest's tuple tail as ...(Ts<R> | undefined)[], and the runtime printer prints the same; a bare tuple renders exactly, which makes validate's success cast sound. (#1732)
types/rtti/data: new equivalent (mutual subset) and emptyRest, the criterion deciding whether a stated rest makes any difference to the canonical form. (#1732)
website: generated page runs the FunctionalScript proof suite in the browser and renders a pass/fail report. (#1726)
emergent_testing: browser.mjs runs proof exports in a browser realm and publishes a serializable report. (#1726)
types/function: add generic sequential iterate for repeated function application. (#1721)
protocol/json_rpc: errorResponseOf is now exported and joined by successResponseOf, so protocols layered on this module build response envelopes with the owner's constructors instead of their own. (#1720)
protocol/json_rpc/types: new SuccessResponse and ErrorResponse — the two branches of the Response union, for a caller that holds one and wants .result or .error without an in check. (#1720)
protocol/mcp: new toolResultStep(e, text, errorText) — answers a tool call with the outcome of one fallible operation, rendering the value or the failure (as isError) into a ToolsCallResult. (#1718)
effects/node/memory: new memoryRun — an asyncRun runner owning a fresh memory store, so a caller can keep one across effects instead of building the operation map and the runner by hand (#1713)
BREAKING CHANGES:types/rtti: parse and validate read a tuple schema by length, so a hole in one is a declared position whose schema is undefined — validate([, number])([9, 5]) is now an error, agreeing with the data form. Dense schemas are unaffected (#1712)
BREAKING CHANGES:types/rtti: a tuple schema's non-index enumerable own property is no longer a declared member — a tuple is read by index, so such a key was matched against value[NaN]. The data form always ignored it (#1712)
BREAKING CHANGES:edag: chains are a grammar, not an array of steps. lambdas is gone; . and ?. carry an index plus a continuation, ?.() a continuation, and () is now ['()', exp, exp] with no chain at all — so a.b(...c) is ['.', a, 'b', ['|()', c, null]]. Three continuation types (propertyLambda, optionLambda, optionPropertyLambda) make the duplicate spellings unspellable rather than merely discouraged. (#1711)
BREAKING CHANGES:edag: new |!() step — the call a group puts outside an optional region, (a?.b)(...c) — replacing |?., which no longer exists: a guarded property access always starts a ?. node. (#1711)
BREAKING CHANGES:edag: every node tuple is closed, so a trailing element is now a validation error — ['args', 'extra'] no longer validates. (#1711)
basen/cbase32: decode vectors at the maxLength boundary without building an oversized sentinel-padded intermediate (#1710)
BREAKING CHANGES:types/rtti/ts: Ts renders the trailing positions of a tuple schema that admit undefined as optional, matching what parse and validate accept. A hand-written type pinned against one needs the ?. (#1708)
BREAKING CHANGES:path: .. can no longer escape an absolute path's root — parse('/..') is [] where it was ['..'] — and normalize and concat keep that root, so fjs compile accepts an absolute module path (#1702)
BREAKING CHANGES:path: a Windows C:/ is a root too, so .. cannot climb off the drive — normalize('C:/..') is 'C:/' where it was '' (#1702)
BREAKING CHANGES:path: concat no longer anchors a bare drive — concat('C:')('dir') is the drive-relative C:dir where it was C:/dir (#1702)
BREAKING CHANGES:path: an absolute right-hand side to concat replaces the left instead of being appended to it (#1702)
path: new root reports a path's root — '/', '//', 'C:/', or '' (#1702)
path: new escapes answers whether a .. climbs above a path's root, which parse cannot — it folds with the root in place, so the escaping .. is what it removes. fjs/web refuses a request path with it (#1702)
text/utf8: a non-integer byte is reported invalid instead of decoded. A fractional continuation byte used to decode to the same code point its integer part spells, silently, and a non-number in range passed through into the code-point list unchanged (#1698)
js/string_escape: new module — the simple string escapes as one table, with encode and decode views derived from it. The JSON serializer and both tokenizers now read it instead of each keeping a copy; output is unchanged and no longer disturbed by a numeric property on Object.prototype (#1696)
BREAKING CHANGES:effects/node: Listen takes the host to bind — listen(server, port, host) — and answers an IoResult, so a bind failure reaches the program instead of killing the process (#1693)
BREAKING CHANGES:effects/node: FileStat carries isFile beside size, so a caller can refuse to read a FIFO or a device — a read that reaches one never returns (#1693)
web: new fjs web [root] [port] command serving a directory over HTTP on 127.0.0.1, with index.html for a directory path and Content-Type by extension (#1693)
web: it refuses dotfiles, non-regular entries, a Host it does not answer for, userinfo in an authority or a port that is not a number below 65536, a scheme that is not http or https, and an absolute-form target whose host is empty (#1693)
web: percent escapes decode in linear time rather than quadratic — 5,000 in one target, about the most a request line can carry, took 101 ms of event loop and now take 43 (#1693)
effects/node: an over-cap request body is 413 and a listener that throws is 500, both closing the connection; either used to end the server process (#1693)
effects/node: a request body of many small chunks is read in linear time — 20,000 one-byte chunks took 2.8 seconds and now take 0.17 (#1693)
effects/node: a listen that fails removes its handlers, so a retry loop leaks none (#1693)
effects/node: a CONNECT is answered 501 instead of having its socket dropped without a response — it reaches Node's connect event, so no RequestListener can see it (#1693)
effects/node: listen refuses an empty host rather than forwarding it — Node binds every interface for '', which is the accident the required host argument exists to prevent (#1693)
effects/node/virtual: createServer and listen are implemented, reporting ERR_SOCKET_BAD_PORT, EADDRINUSE and ERR_SERVER_ALREADY_LISTEN as Node does, and in the order Node asks in; forever remains unimplemented (#1693)
effects/node/virtual: a bind address is compared with its host lower-cased, so LOCALHOST and localhost are one address as they are on a host (#1693)
effects/node/virtual: stat answers isFile: false for a directory and ENOENT for an empty path (#1693)
media/type: detectPath gives the Content-Type a file name implies, from its extension (#1693)
types/bigfloat: new tryDecToFormat and the binary64 format — a decimal rounds once onto the target's grid, so subnormals and overflow come out correctly-rounded. decToBin keeps its unbounded 53-bit contract unchanged (#1690)
types/array: splitFirst/splitLast no longer read a stored null or undefined element as an empty array — splitFirst([null]) is [null, []], not null (#1689)
BREAKING CHANGES:types/rtti/common's Visitor<R> gains a required close handler, so an external implementer has to add one. Both in-repo implementers, validate and parse, are updated. (#1687)
types/rtti: new close(c, rest?) schema — a closed container, admitting the members c declares and, with a rest, any number of members belonging to it. Structs and tuples on their own stay open. (#1687)
types/rtti/data: subset no longer conflates a key that is absent with one present holding undefined, which could answer true for a non-inclusion between an object pattern with declared keys and one with a rest. (#1687)
types/rtti/ts: printer and dataToTs render an object pattern with an empty rest as its fields alone — {} rather than {readonly[k in string]?:never}, which said the opposite of what an empty rest means. (#1687)
types/rtti/data: toData keeps a declared key whose set is the whole value domain when the pattern also has a rest; dropping it there widened the set. (#1687)
BREAKING CHANGES:djs/parser's parseFromTokens now requires exactly one final eof token, rejecting a stream missing one or holding two, which it used to accept. Only hand-built streams are affected — the tokenizer always emits exactly one. (#1684)
djs/parser: an unexpected import after a const now reports unexpected token, and an empty stream missing end-of-input token, at the same positions as before. (#1684)
djs/parser: when a module has both an unresolved name and a syntax error, the syntax error is now reported. A module with only an unresolved name still reports const not found as before. (#1684)
BREAKING CHANGES:types/rtti, types/rtti/validate, types/rtti/parse, types/result, protocol/mcp's toolEntry and bnf's option take const type parameters, so a literal argument keeps its literal type with no cast at the call site — and or infers a readonly tuple, so a schema built with it is Or<readonly [A, B]>. (#1683)
BREAKING CHANGES:types/rtti/data: toData maps a tuple schema to the open { prefix, rest: unknown }, matching parse/validate acceptance; { prefix } alone is now the exact-length set. Tuples render with a rest element and omittable positions optional (readonly[42,(undefined|string)?,...readonly(unknown)[]]), and as JSON Schema with items rather than items: false. (#1680)
edag/amnesia: new module — a tree-walking evaluator for exp. It memoizes nothing, so a shared node evaluates once per incoming edge and node identity is not preserved: it exists to prove node semantics, not to run FunctionalScript. (#1677)
BREAKING CHANGES:edag: a call now carries the chain it is made from. ['()', exp, lambdas, exp] is its own node, so () is no longer an op2 id and propertyCall (['.()', …]) is gone. (#1673)
edag: optional chaining — new ?. and ?.() nodes, plus the chain steps |., |(), |?., and |?.() that carry receiver and short-circuit control flow. (#1673)
edag: adds a spread node (['...', exp]), usable as an array element or object property via new items/properties unions that array/object entries now accept alongside plain expressions. (#1665)
BREAKING CHANGES:edag: the Phantom recursion break moves to exp alone. numberCast, propertyAccessor, propertyCall, comma, op0, op1, and op2 are no longer Phantom-wrapped — one wrapper on the union they all recurse through is enough to cut the cycle. Their runtime values and derived types are unchanged (each is still pinned with Check), so applying Ts to typeof op2 and friends resolves as before. What breaks is the documented phantomKey extraction: typeof op2 extends { [phantomKey]?: infer T } ? T : never yielded Op2 and now yields never, so an indexed access or conditional type built on it fails to compile. Read those types from fjs/edag/types.ts — Op2, Comma, … — or derive them by applying Ts to typeof op2; both are unaffected. (#1665)
BREAKING CHANGES:edag: UndefinedOp, Args, and Frame (plus their undefinedOp/args/frame runtime exports) are replaced by a third generic, id-parameterized node kind — Op0/op0Id/op0 (undefined/args/frame). UnaryOp/UnaryOpId/unaryOp/unaryOpId and BinaryOp/BinaryOpId/ binaryOp/binaryOpId (added in the previous PR) are renamed to Op1/Op1Id/op1/op1Id and Op2/Op2Id/op2/op2Id, joining Op0 under one naming scheme (#1662)
edag: Add, Sub, Neg, Eq, Neq, Gt, Lt, Ge, Own, Fn, Call, and StringCast are replaced by two generic, id-parameterized node kinds — UnaryOp (String/Number/neg/!/~) and BinaryOp (=>, own, (), ===, !==, >, >=, <, <=, +, -, *, /, %, **, &, |, ^, <<, >>, >>>, &&, ||, ??) — adding !, ~, <=, *, /, %, **, &, |, ^, <<, >>, >>>, &&, ||, and ?? to the exp union for the first time (#1660)
edag: add Eq (===), Neq (!==), Gt (>), Lt (<), and Ge (>=) comparison nodes, all now in the exp union (#1659)
BREAKING CHANGES:edag: Fn's first operand is Exp again (unrestricted) — the null-placeholder restriction from #1657 is reverted. Frame/frame is repurposed for a new, separate exp alternative: the ['frame'] read-form (the captured-consts frame array, the way ['args'] is for the arguments array) — unrelated to, and incompatible with, the previous null-only meaning of the same name (#1658)
edag: add Comma ([',', exps]) and Fn (['=>', frame, exp], non-capturing function) to the exp union, plus the Frame placeholder type (null for now, until frame/capture design lands). Retags Neg (shipped in #1656 as ['-', exp]) to ['neg', exp] — a word tag, not "-"'s unary arity (#1657)
types/rtti/ts: new Check3<T, R0, R1> type; types/ts: new And<A, B> boolean type-level helper (#1657)
edag: rename Plus to Add; add Sub (binary -), Own (['own', exp, exp]), and Neg (['-', exp], unary negation — arity, not tag, distinguishes it from Sub), all four now in the exp union (#1656)
edag: new Plus node for binary + (['+', exp, exp]), added to the exp union (#1655)
BREAKING CHANGES:protocol/mcp: McpConfig.protocolVersion (one string) is replaced by protocolVersions, a non-empty latest-first list. initialize now echoes the client's requested revision when the server supports it and counter-proposes the latest supported one otherwise. (#1654)
edag: new module — an rtti schema for expression-DAG nodes: primitives, arrays, objects, args, and the recursive propertyAccessor, call, and propertyCall forms. (#1653)
types/rtti/ts: new Check<A, B> type — Assert<Check<A, typeof schema>> pins a hand-written type against what an rtti schema actually derives to. (#1653)
BREAKING CHANGES:fjs/media/lock/types drops its LockMap re-export of fjs/media/revision's type — import it from fjs/media/revision/types.ts directly instead (#1647)