ci: fjs ci also writes .github/workflows/npm-publish.yml, the npm publishing workflow that until now was hand-written and had drifted from every convention the generated one follows — it takes its runner image, Node version and pinned action refs from ci/config like any other generated job, and keeps the publish intent it had: a push to main, contents: read with id-token: write, and npm publish --provenance. A project regenerating after this release receives an active release workflow it did not have, and cannot yet decline it — deleting the file only means the next fjs ci writes it back; ci/todo/ci-generator-audience.md owns that (#1804)
ci: stepSchema admits continue-on-error as the literal true, and the workflow schema's on admits push with its branch list (#1804)
fjs t: a test's name and its result are one line, not two — the name is written before the test runs and the line is completed when it lands (#1799)
djs/spec: a statement may end with a ; as well as with the end of the line — the acceptance the DataJS inclusion requires, since DataJS demands a ; after every statement. A normalized DataJS document now parses verbatim, and whitespace (newlines included) may precede the ;, matching DataJS's whitespace-insignificance. One terminator per statement: ;; stays an error. spec/README.md's module-structure rule states the new terminator; the previous rule named export default 5; an error. (#1798)
djs: an unterminated string or comment, and unmatched input, report the span of the offending source as path:line:column-column rather than only its start. A malformed number still points at the character that spoiled it. (#1797)
BREAKING CHANGES:emergent_testing: the browser page runs the shared proof traversal. Leaves run sequentially where 25 ran at a time, and a returned tree's parent is now reported before its children rather than after (#1796)
emergent_testing: new runEntries runs one module's already-collected leaves, for a host that must enumerate the export itself (#1796)
emergent_testing: a browser run whose runner fails — an operation's error channel or an interpreter that cannot dispatch — reports infrastructure-error instead of a failed module (#1796)
BREAKING CHANGES:ci: a NixJob declares systems, a non-empty list, rather than a single system, and a NixPin declares an archive and hash per system — sources — rather than one url and hash. Both are exported and reachable through flakeText and nixFlakes, so an outside declaration has to move; every declaration in this repository is updated here. (#1795)
ci: fjs ci generates nix/dev, a development environment carrying every runtime the canonical jobs use — Node, Deno, Bun, a Rust toolchain with the WASM targets, Wasmtime, Wasmer and git — with a shell for each of aarch64-linux, x86_64-linux, aarch64-darwin and x86_64-darwin. (#1795)
BREAKING CHANGES:text/sgr: csiWrite takes an Std — the TTY flag per stream — where it took a whole NodeProgramOptions. A caller passing the options object now reads undefined.isTTY (#1794)
effects: the console family — Write, Read, log, error, errorExit, readLine — moves from effects/node to effects/common, which effects/node re-exports. The new Std type is declared there too (#1794)
ci: the bun job runs in a generated Nix flake instead of installing oven-sh/setup-bun. Nixpkgs ships a Bun two of this repository's proofs fail on, so the flake keeps that package's recipe and overrides the archive it unpacks with an exact upstream release, pinned by a hash in fjs/ci/config/module.f.mjs; the job asserts the resulting version from inside its shell. (#1793)
ci: the wasm job runs in a generated Nix flake instead of installing dtolnay/rust-toolchain, bytecodealliance/actions/wasmtime/setup and wasmerio/setup-wasmer. Its toolchain comes from a second flake input, oxalica/rust-overlay, because Nixpkgs builds no standard library for three of the job's four WASM targets; Wasmtime and Wasmer come from the pinned Nixpkgs snapshot, and the job asserts both versions from inside its shell. (#1792)
effects: new effects/common, for operations more than one host implements. Sandbox and Catch move there from effects/node, which re-exports both and spreads their handlers, so existing imports keep working (#1791)
BREAKING CHANGES:emergent_testing: Reporter gained start, called with a TestId before a leaf runs, and summary receives a RunState — the totals, the run's failures in order, and what abandoned it — where it took RunTotals (#1790)
fjs t: each test is named before it runs, so a slow test is visible while it runs and a run that dies names the test it died in (#1790)
fjs t: a failing test writes only its pass/fail line as it runs; the value it failed with is written with the other failures once the run has ended, and survives a run that ends early (#1790)
fjs t: every record now goes to stdout, failures and GitHub annotations included. stderr carries only a runner crash, so a log reads as one ordered sequence (#1790)
ci: each generated Nix job directory now holds a run script, and workflow steps invoke it — ./nix/node26/run npm run cov — instead of spelling out nix develop. The script passes --quiet, which drops Nix's substitution chatter from the logs without touching warnings, errors, or the command's own output. (#1787)
ci: the deno job runs through a generated Nix flake, on the Deno the pinned Nixpkgs snapshot provides (2.8.3) rather than the one setup-deno installed. (#1782)
ci: the deno and bun jobs no longer install or run a published functionalscript. They ran the CLI of a shipped release, not of the commit under review. (#1782)
djs/tokenizer: an unterminated block comment is reported at its /* rather than at the end of input, matching an unterminated string. Unterminated comments now say */ expected and malformed numbers invalid number, in place of a generic invalid token. (#1778)
ci: the Node 22 job runs npm ci and node --test through its generated Nix flake, so every canonical Node job now uses one; setup-node is left to the platform matrix and package-check. (#1777)
ci: every nix develop invocation passes --no-write-lock-file, so a CI run leaves no flake.lock behind. The pinned commit in flake.nix already determines every input. (#1777)
BREAKING CHANGES:nanvm: the shared operator corpus reuses the canonical EDAG operation ids and runs the expression each case denotes. Op is gone — unaryMinus is neg, mul is *, stringCoercion is String — and rust's call gives way to nodeExpr. (#1776)
BREAKING CHANGES:nanvm: Group is a union of Group1, Group2, and NonEdagGroup, which spells its operation nanvmOp, so op and commutative no longer resolve on a Group. Read them through opId and casesOf, or narrow the union first. (#1776)
BREAKING CHANGES:nanvm: Case takes the operand count its id fixes, so a bare Case — or Special, now generic — no longer resolves, and Value splits into Value, Operand, and Expectation, each thunk legal in one position only. (#1776)
BREAKING CHANGES:emergent_testing: the proof traversal is sequential — one leaf, its report included, finishes before the next starts. runModuleMap performs no all, so a proof relying on sibling concurrency now deadlocks, and fjs t times each leaf alone (#1774)
effects: foldStep and forEachStep no longer nest a continuation per item, which cost quadratic time and failed above ~10,000 items. Order, short-circuiting and signatures are unchanged (#1774)
effects: new walkStep, a foldStep whose body can add items, walked before the ones that remain — a depth-first walk that costs no continuation per level (#1774)
BREAKING CHANGES:fjs/ci/nix: generated flakes move one level up, so nix/generated/node24/flake.nix is now nix/node24/flake.nix and generatedDirectory is nix. (#1773)
BREAKING CHANGES:fjs/ci/nix: nixDevelopAll and nixVersionCheckStep are removed, with the nix-flakes CI job. A flake is checked by the job that uses it, and what it declares is asserted by proofs. (#1773)
BREAKING CHANGES:fjs/ci/node: nodeNixVersionSteps and nodeNixFlakeJob are removed with that job, and nodeVersionJobs takes no arguments — Node 22 was the only reader of the published version it was passed. (#1773)
ci: the Node 24 and Node 26 jobs run their commands through their generated Nix flakes, one step each, instead of installing Node with setup-node. (#1773)
ci: every Ubuntu Node job checks its runtime version before its first real command, and the Node 26 job runs npm run ci-update and its drift check last, after every other command. (#1773)
ci: the node26 job no longer greps .mjs files for a file-scope @typedef. The rule stands; a text pattern cannot answer a question about scope, so it approximated one as indentation. (#1773)
ci: the Node 22 job drops fjs test and the global install feeding it, running node --test like the other Node jobs. Its generated flake loses the shellHook that existed for that install. (#1773)
package: generated private.d.ts files are no longer published. package.json's files excludes them, dropping 16 files. They were implementation-private by contract, so nothing public depended on them. (#1771)
BREAKING CHANGES:fjs/fsc: the unused, unproven bnf.f.mjs and json.f.mjs BNF grammars are removed, with no drop-in replacement. deterministic in fjs/bnf/testlib.f.mjs is the surviving JSON grammar, but it wraps the value rule in whitespace and replaces none of the other exports. (#1768)
fjs ci now generates a package-check job for a project that pins TypeScript exactly: it downloads the packed tarball uploaded by the Node job, installs it as a dependency outside any checkout, and type-checks every declaration the package ships with that pinned compiler. (#1767)
rtti: validate and parse answer a closed tuple's or struct's shape before reading any member, so an or of two arities no longer walks shared operands once per arm. A shape error now wins over a member's. (#1766)
BREAKING CHANGES:edag: a chain ends by arity, not a null terminator — a plain read is ['.', a, 'b'] and a terminal call step ['|()', c], each one element shorter. Graphs and types written against the old spelling (['.', a, 'b', null]) no longer validate. (#1761)
BREAKING CHANGES:effects: the new catch operation joins NodeOp, so an exhaustive NodeOperationMap must supply a handler for it (#1758)
emergent_testing: a proof whose return value cannot be enumerated fails that proof instead of the whole run (#1758)
effects/node/virtual: a path naming an inherited property (toString, __proto__) is absent in every operation, rather than reading as a JsModule or a directory — readFile even threw outside the effect's channel (#1754)
effects: OpResult, IoChannel, IoError, IoErrorInfo, IoResult and the ioError / toIoError constructors are now importable from the core module, not only through effects/node, which re-exports them unchanged (#1753)
web: a request that descends through a regular file (GET /README.md/) answers 404 instead of 500 io error: ENOTDIR, so it no longer discloses whether a file is there; fjs web refuses a root that is not a directory before it binds, instead of failing per request (#1751)
BREAKING CHANGES:effects/node: FileStat gains isDirectory — isFile === false does not mean "directory" — so a runner implementing Stat must return it; the virtual file system now reports ENOTDIR for a path descending through a non-directory (#1751)
BREAKING CHANGES: type import paths changed: Grammar → fjs/fsm/types.ts; MemoryOperationMap, MemoryRun, Uuid → fjs/effects/node/memory/types.ts; BrowserTestReport → fjs/emergent_testing/types.ts; the JSON-Schema Unknown alias is gone — spell it Ts<typeof unknown>. (#1750)
BREAKING CHANGES:emergent_testing: Reporter.result now receives the normalized TestResult and Reporter.summary one RunTotals record; the new addResult and zeroTotals fold leaf results into every runner's totals. Printed output and the browser report are unchanged (#1749)
BREAKING CHANGES:rtti: option is a nullary schema denoting absence: an omittable member is or(option, t), which rejects a present undefined — the old option(t) set is or(option, t, undefined). (#1748)
rtti: parse omits an absent member — the struct kind drops the key, the array kind keeps holes and shortens a trailing absent run — so an optional member survives a JSON round-trip. (#1748)
rtti: unknown excludes absence, so the omittable top is or(option, unknown); Ts<>, the runtime printer and toJsonSchema derive optionality (?, required, minItems) from absence. (#1748)
rtti: a Phantom annotation on a schema whose root admits absence wraps its present part in the new AbsentOr, pinned with the new CheckRaw. (#1748)
rtti: parse builds its result without dispatching any overridable operation, and all three readers refuse a value whose accessors flip a decided member's presence mid-read, instead of answering wrongly. (#1748)
BREAKING CHANGES:emergent_testing: the browser runner awaits only instanceof Promise values, as fjs t does. A promise from another realm, or a value impersonating one, is walked as a proof tree rather than awaited — the Symbol.species machinery that handled those is gone (#1742)
emergent_testing: TestResult and testResult normalize one leaf's outcome — its identity, whether it passed once the throw expectation has been applied, and how long it took — so fjs t and the browser runner decide those the same way. The browser's result type is now TestResult plus its own message/stack; fjs t's output is unchanged (#1741)
BREAKING CHANGES:emergent_testing: a browser test result gains a required name — the test identity fjs t prints, built by the same fmtImport function — and the page renders it, so both runners spell a test identically. renderBrowserReport reads name, so a report built by hand against the previous { module, path, ... } shape renders undefined in place of every identity; reports produced by runBrowserProofs, startBrowserTests and startBrowserTestSources carry the field and are unaffected. module and path are unchanged (#1738)
BREAKING CHANGES:fjs/types/rtti/ moved to fjs/rtti/ — it is a peer of djs, not a member of types/. No code changed, only import paths; every importer inside and outside the repo needs the types/ segment dropped from functionalscript/fjs/types/rtti/…. (#1736)
BREAKING CHANGES:website/emergent_testing/browser: the generated browser test page starts idle instead of auto-running on load, and drops the run query parameter; its control is renamed Run and stays genuinely disabled while a suite is loading or running (#1734)