BREAKING CHANGES:effects: RawEffect is deleted, and Effect<O, T, E> is the representation — a Pure thunk yielding Result<T, E>, or a Do node. Pure, Cont and Do carry the channel. The two-name split was defended as composition against representation, on the ground that the representation is generic over its payload by definition. It is — but Operation requires a Result return, so that payload is always a Result, and the second name described no reachable case. (#1643)
BREAKING CHANGES:effects: fjs/effects/io/ is gone, merged into fjs/effects/, and there is one set of combinators instead of two. step, catchStep, resultStep, mapStep, resultMapStep, unwrapStep, history, historyStep, foldStep, forEachStep, pureOk, pureError and notImplemented are exported from fjs/effects/module.f.mjs beside pure, do_, match, partialMatch and runPure. The subdirectory was never an IO layer — the name came from its being the fallible half — and it existed because its step collided by name with a Result-blind one. Those twins were corrections of each other rather than alternatives: the raw step ran its continuation whether or not the previous effect failed, the raw historyStep carried each link's Result into the history rather than its value, and the raw forEachStep ran every item whatever each one answered. resultStepis the former raw step, now the primitive that step and catchStep are written in terms of. (#1643)
BREAKING CHANGES:protocol/mcp, cas/evo, effects/node: the last library types that answered a raw effect now answer an Effect with a never channel — Handle, Step, toolsList, toolsCall, ToolEntry.handle, decodeRevisionBlob, and the Test operation's callback. None of them is infallible: each performs effects that can fail and absorbs the failure into its own vocabulary — a JSON-RPC error response, an isError tool result, a null, a panic. never states that decision where a reader can disagree with it, which an opaque payload could not. (#1643)
BREAKING CHANGES:effects/node: All answers each effect's whole Result (OpResult<readonly Result<T, E>[]>), the nesting allOk already collapsed; RequestListener answers Effect<O, ServerResponse, never>, the response frame being where a listener puts its failures; NodeEffect<T> takes a channel, defaulting to IoChannel. The async runners (asyncRun, runNodeEffect, run) and RunInstance answer Result<T, E> — an interpreter never separates the channels, since a runner hands a failed command back through the ordinary continuation. (#1643)
effects: resultMapStep is new — the trailing projection over the whole Result, as mapStep is over the value. It is what the call sites that used the Result-blind mapStep meant, and it says so: mapStep cannot change the error channel, resultMapStep replaces it, so a site that discards a failure is written as one that took both branches. unwrapStep answers Effect<O, T, never> rather than a bare-payload effect — the panic empties the channel rather than leaving the layer. (#1643)
effects: pure takes the Result a Pure holds, and reads both channels off it with the new OkOf / ErrOf. Matching Result<T, E> directly cannot place a one-sided argument — pure(ok(v)) would infer the error channel as T as readily as never — which is also why resultMapStep projects its output channels the same way. pureOk / pureError are unchanged. (#1643)
cas, mcp, protocol/mcp, emergent_testing: the proof mock runners answer an Effect too. Every one of them was generic in its payload (<T>(e: RawEffect<O, T>) => …), which is a type parameter rather than a payload shape, so it becomes Effect<O, T, E> and wraps nothing new. Two of them stop hand-rolling what a type already states: fjs/mcp/cas's driver unwraps its never channel with unwrap instead of an indexed cast, and fjs/protocol/mcp's withState reads a history, whose links hold ok values rather than results. The emergent_testing runners name RunInstance — the type mockRun already returns — instead of respelling it. (#1643)
cas: converting addBigFile/addAndGetBigFile to the error-aware step surfaced a cleanup that had never worked. Both chains ended in rm(testDir) whose result the Result-blind step discarded, and the virtual filesystem cannot remove a non-empty directory, so the rm failed on every run unobserved. The cleanup is gone — each run interprets against a fresh emptyState — and both proofs now assert their chain's final result, which an error would otherwise short-circuit past silently. (#1643)
effects, docs: the rationale for two effect types is retired everywhere it was recorded — fjs/effects/types.ts, fjs/AGENTS.md §3.4, fjs/effects/README.md and fjs/effects/todo/io-effect-migration.md. It rested on "absorb points" that should stay raw behind their conversion, with the MCP handler as the clearest case; that handler says never now, because never is a claim a reader can disagree with where an opaque payload left the question unasked. The arithmetic offered alongside it is corrected in the same places: most of the payloads counted as needing an ok wrapper were type parameters, not payload shapes. Two smaller ones travel with them: spec/todo/io-effects.md no longer points at the deleted directory, and fjs/cas/evo/todo/proof-fixture.md no longer calls assertPure exported. (#1643)
effects, docs: the entries below no longer announce RawEffect as a public type that stays public — it was created and deleted inside one unreleased window, so no published API is called that. The claim that every operation already returned OpResult or IoResult when the Result constraint landed is corrected too: the host operations did, four of the six declared inside proofs returned a bare Result instead, and the two that returned a bare number were rewritten by that same commit. (#1643)
BREAKING CHANGES:djs/transpiler: ParseContext no longer carries an error field. The first parse error is the effect's channel (Effect<ReadFile, ParseContext, ParseError>), so step short-circuits instead of three separate places testing context.error before doing any work, and a context that exists is a context that is still good. (#1642)
djs, ci, fjs: three entry points that observe both branches of a Result say resultStep instead of the raw step. Same function, but the name states the intent and the narrower type checks it. (#1642)
BREAKING CHANGES:effects: foldStep and forEachStep take an Effect for items rather than a raw effect, so a producer that can fail feeds the fold directly. Lifting a held list is pureOk instead of pure — the same one call — where the raw version made the fallible producer wrap the whole fold in a step whose only job was to unwrap and re-wrap the list. (#1642)
BREAKING CHANGES:effects: an Operation's return type must be a Result. A runner may decline any command — partialMatch answers error(notImplemented(command)) through the command's own output — so an operation returning something with no error case was a hole in that mechanism. Every host operation already complied, as did four of the six declared inside proofs; the two in fjs/effects/proof.f.mjs returned a bare number and this change rewrites them. It makes the return a rule and a latch, so a new operation cannot be declared infallible. (#1642)
docs: the design docs no longer cite okStep, an export removed when it was inlined into the Io step, nor spell types the migration replaced — List<O, IoResult<Vec>> and the two-parameter Effect<O, IoResult<T>>, both of which would now mean a doubly-wrapped Result. This closes the last open item of io-effect-migration.md. (#1642)
effects/node: IoChannel names the standard IO error channel (NotImplemented | IoError), which was spelled out 51 times across 50 lines. IoResult<T> is now Result<T, IoChannel>. Same type throughout — an IO-touching effect says it fails the way node IO fails, so gaining a new way to do so no longer walks up every enclosing signature. (#1641)
BREAKING CHANGES:cas/evo: Evo.add and Evo.revision no longer nest a domain Result inside the effect's value. Both answer Effect<…, T, EvoChannel>, where EvoChannel is the new tagged EvoError or NotImplemented. A rejected revision now short-circuits step like any other failure, and evoSummary renders either case. (#1641)
BREAKING CHANGES:effects: unwrapStep takes a second argument, a summary for the channel it panics on. It was generic in the error type, so it compiled however far a channel widened and quietly enlarged what it crashed on; a renderer written for one channel does not accept a wider one, so that is now a compile error at the site that chose to panic. errorSummary is the renderer for the node channel. (#1641)
BREAKING CHANGES:effects/node: a Program answers Effect<O, 0, number> rather than RawEffect<O, number> — ok(0) for success, error(n) for failure. A bare number could not say which codes meant failure, so nothing short-circuited on one and a continuation that dropped a failure and answered 0 type-checked. errorExit is Effect<Write, never, number>; the new exitCode reads the code from either branch. (#1641)
BREAKING CHANGES:effects/list: a List cell can fail. List<O, T, E> = Effect<O, Next<O, T, E>, E>, so a producer that cannot deliver fails the stream instead of yielding an error item followed by a tail nobody pulls. Cas.read is (hash) => List<O, Vec, IoChannel>. Six consumers hand-rolled the same short-circuit because the failure sat in T, where a generic combinator could not see it; step does it now. (#1641)
djs/transpiler: the five ParseError-valued signatures say Effect<ReadFile, T, ParseError> rather than the equivalent RawEffect<ReadFile, Result<T, ParseError>>. The same type, spelled as what it means: a parse failure is a failure, so it belongs in the channel. (#1641)
effects: Effect<O, T, E> replaces RawEffect<O, Result<T, E>> wherever the Result is the effect channel — cas, cas/evo's runner unwrap, effects/memory, effects/node's all/both/writeFromStream/ createServer/Console, media/type's detectStream, and MCP stdio's response writer. The two spellings are the same type, so nothing changes at runtime; Effect's doc now states the rule that decides between them. (#1640)
effects: a runner may implement part of an operation set. partialMatch and the mock partialRun answer a declared-but-unimplemented command with error(notImplemented(command)) through the ordinary continuation, so a program can recover; a command outside the operation set still panics. (#1639)
BREAKING CHANGES:effects/node/virtual: exec, createServer, listen, forever and test are no longer implemented. They used to throw when reached and now answer NotImplemented. (#1639)
BREAKING CHANGES:effects: IoEffect is renamed Effect, with E defaulting to NotImplemented. The Pure | Do representation it was built on took the interim name RawEffect, which the entries above refer to; that name is deleted again before this release ships, and Effect is the representation now, so nothing in the published API is called RawEffect. (#1636)
BREAKING CHANGES:effects: okStep is removed. It was a step adapter whose only caller was the Io step, which now writes that branch itself. (#1636)
BREAKING CHANGES:fjs: run reports a target it cannot use instead of panicking. A file that will not import, or one that imports without exporting a main function, is now a message on stderr and exit 1. (#1635)
BREAKING CHANGES:protocol/mcp: a session slot the runner cannot reach answers JSON-RPC -32603 instead of panicking, and a failed initialize transition answers it too rather than a successful handshake it did not record. (#1633)
BREAKING CHANGES:protocol/mcp/stdio: stdioTransport answers an IoEffect. A stdin that cannot be read ends the loop and hands the failure back, where it used to panic; EOF is still a clean shutdown. (#1633)
BREAKING CHANGES:cas: Cas.list answers IoResult. A store that exists and cannot be walked is now an error the caller handles, where it used to throw; an absent store still answers ok([]). (#1632)
BREAKING CHANGES:cas/evo: Evo.list and Evo.head are fallible, and Evo.add nests its domain Result inside the effect channel. A cache slot the runner cannot reach becomes an MCP isError result instead of a panic. (#1632)
effects/node: new errorSummary — a channel error rendered for a remote caller, carrying the OS error code rather than the host message the path lives in. The MCP tools report through it. (#1632)
BREAKING CHANGES:djs: the parser reads FunctionalScript modules only. A statement begins with import, const, or export and never with a value, so a JSON document no longer parses as a module; imports come before constants, and export default is required and last (#1631)
BREAKING CHANGES:djs: fjs compile reads a .json input with fjs/media/json instead of the module parser, so a .json file is JSON and nothing more — no bigint, undefined, comments, or identifier keys — and it cannot be imported until the language has with { type: "json" }. The parser export parseJsonFromTokens is gone with it (#1631)
djs: an error with no token to point at names the file being compiled instead of printing undefined:undefined:undefined (#1631)
BREAKING CHANGES:dev: loadModuleMap is a fallible IoEffect. A directory it cannot list or a module that will not import is now an error the caller handles, where discovery used to panic. (#1630)
BREAKING CHANGES:emergent_testing: Reporter's members and the registerModule / runModuleMap / testAll chain are fallible IoEffects. A failed reporter write now ends the run with exit 1, as does a failed register, which used to answer 0 either way. (#1629)
effects/node: new allOk — all in the ok channel, answering with the first error instead of a list of results for the caller to collapse. (#1629)
BREAKING CHANGES:djs: { __proto__: v } and { "__proto__": v } are compilation errors in a FunctionalScript module; the key is written { ["__proto__"]: v }, which is also what the module emitter now produces (#1628)
djs: computed property keys { ["a"]: 1 } with a constant string key (#1628)
djs: fjs compile picks its input's language by extension the way it already picks its output's — a .json input is a JSON document, where "__proto__" is an ordinary data key, so it compiles to {["__proto__"]:…} and back. Only the file named on the command line: an imported file is read as a FunctionalScript module until the language has with { type: "json" }. New parser export parseJsonFromTokens (#1628)
BREAKING CHANGES:dev/package_json is removed. It had no consumers, and its schema is three optional fields any caller can declare and parse. (#1625)
BREAKING CHANGES:types/rtti: validate is deleted — parse is the way to read a value against a schema. Structs and tuples are open: extras are accepted and absent from the value parse constructs. (#1624)
BREAKING CHANGES:types/rtti: validate requires an array to match its tuple schema's length exactly, matching Ts<T> and the data form. An extra element is rejected, and so is a missing trailing option/unknown element, which used to pass. Structs are unchanged: extra keys are still accepted. (#1622)
BREAKING CHANGES:effects/eff: the experimental Eff fluent wrapper is removed. It had no consumers left; compose with step / mapStep / historyStep instead. (#1619)
media/json: the serializer exports colon, the key : value separator, which the DJS serializer now shares instead of declaring its own (#1618)
effects: new history, historyStep, foldStep and forEachStep — the fallible chain keeps its earlier values, and a fold stops at the first error instead of running every item (#1615)
ci, dev/update, nanvm/update, cas list: a failed write is reported on stderr with exit code 1 rather than thrown (#1615)
cas: the staging upload composes through fjs/effects. A failed now or randomInt propagates instead of panicking, and a failed sweep can no longer fail the upload it runs before (#1615)
BREAKING CHANGES:text/utf16: the decoder's out-of-range guard emits the shared errorMask instead of a magic 0xffffffff, matching the UTF-8 decoder. A code unit outside [0x0000, 0xFFFF] or non-integer now decodes to 2147483648, not 4294967295. (#1614)
basen/base128: the varint continuation-bit layout is named once and shared by encode and decode instead of being spelled with literals in each (#1612)
BREAKING CHANGES:bnf/ll1: the AST now matches bnf/descent's — a node per rule invocation, entered before its first symbol is consumed, and a flat node per Repeat — so the AST is one contract across backends. dispatchMap stores per-rule first sets instead of consumed-symbol rule chains and throws left recursion … at build time for a left-recursive grammar (#1611)
media/note: new module — the vnd.fjs.note dialect, served as application/vnd.fjs.note+json: a human-authored text item (a note, todo, issue, or calendar event) as a history-free blob; edits, merges, and archiving come from vnd.fjs.revision. The shape is deliberately minimal — dialect, text, an optional dependencies list naming the subjects an item depends on, referenceable from the text by index as [0], and an optional priority on the P1–P5 scale of todo/README.md — so every future capability lands as an additive optional field under the same tag (#1610)
mcp: cas_get detects vnd.fjs.note blobs and reports them under their own media type (#1610)
js/tokenizer: new mergeTrivia states the whitespace/newline coalescing rule once; js and djs tokenizers both read it instead of each encoding the four cases (#1609)
BREAKING CHANGES: new bnf/matcher, one owner for what every BNF matcher backend shares: the Cursor a match runs on, the Ast<L> / AstSequence<L> / AstTag family it builds, the AstResult<L, P> it returns, and the leafAt / symbolAt / physicalIdx / mrSuccess / mrFail functions over them. bnf/ll1 and bnf/descent derived all of it separately and identically (#1608)
BREAKING CHANGES:bnf/descent: AstRuleMeta<T> and AstSequenceMeta<T> are now Ast<CodePointMeta<T>> and AstSequence<CodePointMeta<T>> from bnf/matcher, and AstTag moves there too. CodePointMeta<T> stays — a leaf that carries metadata is this backend's own concept (#1608)
BREAKING CHANGES:bnf/ll1: _AstRule, AstSequence and AstTag are gone; MatchResult's first element is Ast<CodePoint> (#1608)
djs/tokenizer takes the AST types from bnf/matcher rather than bnf/descent (#1608)
BREAKING CHANGES:effects: every operation's return type carries a Result. Infallible operations answer OpResult<T> (Result<T, NotImplemented>); host IO answers IoResult<T>, whose error is now the structured NotImplemented | IoError rather than unknown. Runner handlers wrap their output in ok(...); isNotFound takes a channel error (#1607)
effects/node: new IoError / IoErrorInfo / OpResult types with ioError, toIoError, errorMessage and exitStep, the NodeProgram exit-code policy (#1607)
effects: new unwrapStep, which leaves the layer by panicking on the error branch (#1607)
crypto/sha2: Sha2 publishes hashBytes and blockBytes, so consumers sizing byte buffers read them instead of converting bit lengths themselves (#1606)
ci: npm run cov now fails the build if aggregate line, branch, or function coverage falls under 100%, instead of only reporting the numbers (#1605)
effects: the IoEffect composition API — step propagates an error, catchStep recovers, resultStep observes both — with the pureOk / pureError lifts and mapStep. No operation or runner produces an IoEffect yet (#1604)
effects: okStep unions its two error types instead of unifying them, matching okThen. A strict generalization — existing call sites are the F = E instantiation (#1604)
media/type: detect reads the magic state off the streaming detector's fold instead of driving a second copy of it; same results, but it now reads a whole text Vec rather than stopping at the first non-signature byte (#1602)
BREAKING CHANGES:cas: casUpload is removed. It had no caller and was the remnant of an upload flow deleted in 0.32.2; content is added through casAddFile (CLI) or the MCP cas_add tool (#1601)
djs/tokenizer: restores 100% branch coverage — narrows stringDecodeScan's escape-character switch via assert instead of a dead default arm, and removes an unreachable fallback in tokenizeJs's error-position lookup (#1600)
effects: new IoEffect<O, T, E> (Effect<O, Result<T, E>>) and NotImplemented types — the fallible effect abstraction with an explicit error channel. Types only; no operation or runner changed yet (#1599)
types/bit_vec: the Vec and U8 list concatenations share one combinator, mirroring mappedChunkList in the chunking direction; behavior unchanged (#1598)
dev: adds proof coverage for loadModuleMap's INIT_CWD-prefix stripping, bringing the module to 100% line/branch/function coverage (#1597)
asn.1: adds proof coverage for parsedTagDecode's tag class × primitive/constructed combinations, bringing the module to 100% line/branch/function coverage (#1596)
BREAKING CHANGES:bnf/data: Rule gains a Repeat kind — the bare name of the rule to repeat, which keeps the four kinds disjoint by JavaScript type alone — and toData folds the unambiguous 0-or-more right-recursive shape into it, dropping the rules that fold orphans. Serialized rule sets change, and a repetition's intermediate rule names no longer exist, so match by the entry name toData returns. New exports isRepeat and detectRepeat (#1595)
BREAKING CHANGES:bnf/descent: a repetition now matches iteratively and produces one AST node holding a flat sequence of its items instead of a right-recursive chain of some/none nodes; per-item tags are unchanged. New export descentParserRuleSet, the matcher over an already materialized RuleSet (#1595)
bnf/ll1: a Repeat compiled back to right-recursion here — the dispatch model inlined a nullable item's first set and had nowhere to put a looping frame — leaving seven ways this backend's AST differed from bnf/descent's, one of which lost grouping outright. All seven are gone again before this release ships: the entry above makes the AST one contract across backends, and the descentEquivalence proof group pins all eight cases side by side (#1595)
djs/tokenizer: new export jsMatcher, the whole-file matcher together with its entry rule name, replacing descentParser(jsGrammar()) plus a hard-coded rule name at each call site (#1595)
text/code_point: bmpMax is exported, and text/utf8's encoder takes its BMP and supplementary-plane guards from code_point instead of re-spelling the boundaries (#1593)
types/rtti/data: new withoutUnits removes unit bits from a union set, dropping the unit key when it empties. media/json/schema uses it instead of rebuilding the union field by field (#1591)
removes 273 of the 357 inline /** @type {T} */ (v) casts under fjs/, which AGENTS.md asks to avoid: 182 were redundant outright, 23 became @satisfies or an annotated declaration, and 68 became the runtime check they stood in for — assertNotNullish, a discriminant assert, or a checked accessor in the MCP and CAS proofs, where a response is unknown and its shape is the very thing the proof exists to establish (#1589)
effects/node/virtual, effects/node and ci proofs narrow _Entity to Dir with instanceof Array rather than a cast; Array.isArray narrows to any[], which readonly Vec[] is not assignable to, so its negative branch never removed a readonly array from the union (#1589)
fjs run asserts a module's main is callable before invoking it, failing with the file name instead of main is not a function from inside the effect runner (#1589)
BREAKING CHANGES:fsm: DFA state keys are sorted_set's canonical toKey rather than JSON text, so run returns different strings for the same automaton. fjs/fsm no longer depends on fjs/media/json (#1588)
types/sorted_set: new toKey builds a canonical, collision-free key for a set of strings (#1588)
sul: rewrites cascade's for(;;) loop as a recursive cascadeFrom, removing a phantom loop-exit branch V8's coverage instrumentation could never mark taken, bringing the module to 100% line/branch/function coverage (#1587)
mcp/cas: cas_get's unreachable fromVec/base64Encode null-checks become assertNotNullish; adds proof coverage for cas_add's writeBytes-failure race and cas_get's hash-vanished-between-reads races, bringing the module to 100% line/branch/function coverage (#1583)
types/patricia_trie: end is a right fold over the candidate stack and push splits instead of indexing; identical output (#1582)
media/lock: new module — the vnd.fjs.lock dialect, served as application/vnd.fjs.lock+json: a revision's lock map as a history-free blob several revisions can share. It reuses media/revision's lock schema and hash check, so a map serializes identically inline and shared (#1581)
BREAKING CHANGES:media/revision: lock is now an inline map or a cbase32 hash naming a vnd.fjs.lock blob, so the field reads as string | LockMap (new LockField) and needs narrowing. Widening the field instead of adding a lockRef sibling keeps the vnd.fjs.revision dialect: an old reader rejects a reference rather than misreading it as "no bindings" (#1581)
cas/evo: RevisionData.lock carries either form; add and revision validate and canonicalize a reference like snapshot, and never follow it (#1581)
mcp: cas_get detects vnd.fjs.lock blobs, and evo_add accepts and advertises a shared-lock reference alongside the inline map (#1581)
media/json/tokenizer: removes parseMinusState's unreachable case '-' arm — the JS tokenizer always merges adjacent - into a single '--' token, bringing the module to 100% line/branch/function coverage (#1580)
nanvm-lib: remove the Serializable trait, its per-type impls and tag constants, the little-endian helpers, and the IContainer serialization defaults; the tagged binary format is superseded by CBOR of Any (#1579)
the language spec drops the Tag columns from its JSON, DJS, and FJS tables (#1579)
BREAKING CHANGES:djs: fjs compile exits with code 1 instead of 0 when the input cannot be read or fails to parse, so a failed compile is detectable from the exit status (#1577)
cas: adds proof coverage for write's op-failure branches (a writeBytes/lease-renewal-rename call failing, and a mismatched final stat), bringing the module to 100% line/branch/function coverage (#1576)
BREAKING CHANGES:fsm: toRange no longer throws on a one-character argument — it is the singleton range, via text/ascii's range. toUnion is no longer exported; it was used only by the module's own proof (#1575)
types/array: head and tail share one emptiness guard; behavior unchanged (#1574)
BREAKING CHANGES:media/json: new extended codec — bare integer syntax parses to bigint and serializes back as ordinary JSON, -0 stays a number, and out-of-range tokens are parse errors. parser.parse now takes a numeric policy, so standard and extended parsing share one state machine (#1573)
BREAKING CHANGES:js/tokenizer: a number token carries only its exact lexeme; the derived bf field is gone. It accumulated its exponent as a number, so a long one silently lost digits, and its coefficient had to be built as a bigint before the token existed (#1573)
media/json: new number module with bounded lexical helpers (numberLexeme, isBareInteger, isIntegral) that classify a token in its own length, without narrowing it or evaluating 10 ** exponent (#1573)
types/object: definedEntries infers its value type from the record it is given, so a record whose value type is generic can be passed (#1573)
emergent_testing: adds proof coverage for register's empty-module-map path and its inlineTestContext/engine branches, bringing the module to 100% line/branch/function coverage (#1571)
media/json/parser: the nine inline unexpected token error values are one shared constant, and pushKey drops a guard unreachable through parse (#1569)
BREAKING CHANGES:types/byte_set: toRangeMap takes only the set and returns RangeMap<boolean> instead of taking a state name and returning RangeMap<SortedSet<string>>. Callers label the ranges themselves (#1566)
BREAKING CHANGES:media/revision: a lock value is now a hash or a nested lock map, to any depth, so LockMap values read as string | LockMap and need narrowing. Every flat map stays valid, and the vnd.fjs.revision dialect is unchanged. The schema export _lock is now lock (#1565)
cas/evo: add and revision validate and canonicalize direct lock hashes at every depth, preserving nested scopes (#1565)
mcp/evo: evo_add accepts and advertises lock, reusing the media format's recursive schema (#1565)
js/tokenizer: removes tokenizeOp, a wrapper whose input === null branch was unreachable — every caller's own type already guarantees a non-null number input; call sites now use tokenizeCharCodeOp directly (#1564)
types/bit_vec: front and removeFront are derived once from each bit order's unpackSplit instead of being hand-written per order. Behavior and public types are unchanged (#1563)
js/keywords: new module — the one source of truth for JavaScript keywords (reservedWords, strictModeReservedWords, restrictedNames, and the aggregate keywords). The JavaScript and DJS tokenizers and the rtti TypeScript printer derive their sets from it instead of keeping copies: FunctionalScript is a strict subset of JavaScript, so every consumer must agree on what a keyword is #1562.
effects/node/virtual: readFile/readBytesOp narrow via assert(Array.isArray(file), …) instead of an unreachable defensive return; rmOp drops its unreachable "is a directory" guard entirely #1559
types/nullable: match accepts independent result types for its two branches and no longer widens the result to Nullable when they agree. map is now derived from it; its own signature is unchanged (#1558)
djs/parser: tokenToValue drops its defensive default arm — narrowed to a new _ValueToken type derived from isValueToken's type guard, so the switch is exhaustive #1556
fsm: the two state-free stream transforms are map projections instead of self-referential scan operators; internal only #1554