Migrate fjs/effects/node/virtual to .f.mjs - #1494
Conversation
Splits Dir, State, JsModule, and the internal _Entity type into a sibling types.ts. proof.f.ts migrates alongside it since its only runtime dependency is the module itself. Generic helper functions (operation, readOperation) needed the curried @template/@param/@returns JSDoc form rather than a single @type annotation, since a bare @type on a generic curried function doesn't expose the type parameter inside the function body. The five call sites that pass an operation callback inline (mkdir, writeFile, rm, createExclusive, writeBytes) are pulled out into named, explicitly typed helpers, since TypeScript's contextual typing did not flow the expected return type through the generic operation(...) call when the callback was anonymous. Updates all 18 dependents (proof files and fjs/dev/module.f.ts) to import runtime values from module.f.mjs and types from types.ts, plus stale doc references and line-number citations in fjs/effects/node/virtual/README.md and fjs/effects/node/virtual/todo/{dir-spine-descend,resolve-file-helper}.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y9bV7Civ4LaC6GPsjcgnJY
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
functionalscript | cb4de3f | Commit Preview URL Branch Preview URL |
Aug 12 2026, 03:23 AM |
|
@codex review |
o2alexanderfedin
left a comment
There was a problem hiding this comment.
Reviewed at 2d37a40. tsc clean, 2357 pass / 0 fail, no any in any emitted declaration, and the mkdir restructure is the familiar annotate-via-named-const idiom (mkdirOp extracted so @type can name it) — operation receives the same function, so it is inert.
Nice to see this one land: effects/node/virtual is what has been blocking dev/update, website, ci, and ci/nix proofs from moving, and its own proof.f.ts migrates alongside here.
Entity was public on main, and the CHANGELOG says it wasn't
This is the first genuinely removed public type I have seen in this migration series. On main, fjs/effects/node/virtual/module.f.ts exports four types:
export type JsModule = () => Module
export type Entity = readonly Vec[] | Dir | JsModule
export type Dir = { … }
export type State = { … }JsModule, Dir, and State stay public in types.ts. Entity becomes _Entity, so it leaves the published surface entirely.
The CHANGELOG entry describes it as "the internal _Entity type". That is accurate about the destination but not about the origin — it was exported, not internal, and a reader of the changelog would conclude nothing was narrowed.
To be clear about the size of this: no in-repo code imports Entity, I checked, so nothing breaks here, and making it private is defensible on the merits — it is the union extractEntity/insertEntityAt pass around, which does read as an implementation detail rather than API. My concern is only that a deliberate narrowing of the public surface is going out described as a no-op. An external consumer with import type { Entity } gets no signal from that line.
Either resolution works:
- keep it exported as
Entityintypes.tsif the intent was purely mechanical, or - keep
_Entityand say so — the entry is already marked**BREAKING CHANGES:**, so it only needs the wording to stop calling it internal, e.g. "…and narrowing the previously-exportedEntityto the internal_Entity".
I would not hold the PR for the code either way; it is the changelog line that should not claim a type was always internal when it was not.
Everything else checks out: Dir changes only because its member references _Entity, JsModule and State are untouched, and no exported const signature moved.
|
Codex Review: Didn't find any major issues. Keep them coming! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
Splits Module, ModuleMap, and LoadModuleOperations into a sibling types.ts. proof.f.ts migrates alongside it since its only runtime dependency is the module itself; the embedded proof export inside module.f.ts (allFiles/loadModuleMap coverage) moves over unchanged. Updates the one dependent, fjs/emergent_testing, and stale doc references and line-number citations across fjs/dev/todo, fjs/effects/todo, fjs/effects/node/todo, and fjs/emergent_testing/todo. Also fixes the previous commit's CHANGELOG entry for fjs/effects/node/virtual: Entity was a publicly exported type on main, not internal, so the entry undersold the change by calling it "the internal _Entity type". Reworded to "narrowing the previously-exported Entity to the internal _Entity", per review feedback from o2alexanderfedin. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y9bV7Civ4LaC6GPsjcgnJY
Pre-existing dead import (unrelated to this PR's migration, just touched by it) flagged by github-code-quality's review bot. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y9bV7Civ4LaC6GPsjcgnJY
Splits Cas, FileCas, and FileCasOperation into a sibling types.ts. proof.f.ts migrates alongside it since its only runtime dependency is the module itself. The generic write() facade needed extracting into a standalone named function (writeImpl) with a curried @template/@param/@returns JSDoc declaration, since a bare @type on write as an object-literal property does not introduce O1 as a body-visible type parameter for the nested loop/publish/fail helpers — the same generic-scoping gotcha as previous migrations, applied to a method rather than a top-level const. A few .step() callbacks also needed explicit parameter/return JSDoc annotations where their generic operation-set inference widened past the declared FileCasOperation/ReadBytes. Updates all dependents (fjs/cas/evo, fjs/cas/cli, fjs/mcp, fjs/mcp/cas, fjs/mcp/proof, fjs/mcp/evo/proof) and stale doc references and line-number citations across fjs/cas/README.md, fjs/cas/todo, fjs/cas/plan, fjs/effects/todo, fjs/effects/node/todo, fjs/mcp/README.md, fjs/todo, and todo/plan/roadmap.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y9bV7Civ4LaC6GPsjcgnJY
No local types to split. proof.f.ts migrates alongside it since its only runtime dependencies (module.f.mjs, cli/module.f.mjs, effects/node/virtual/module.f.mjs) are already clean. Updates the one dependent, fjs/module.f.ts, and stale doc references and line-number citations across fjs/cas/README.md, fjs/cas/todo, fjs/cli/todo, fjs/effects/todo, and fjs/emergent_testing/todo. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y9bV7Civ4LaC6GPsjcgnJY
Splits TestFn, TestEntry, TestSet, Path, Reporter, and the internal _TestState/_TestAndPath types into a sibling types.ts. proof.f.ts stays TypeScript for now — it imports media/json/module.f.ts, still unmigrated — and switches to importing runtime values from module.f.mjs and the Reporter type from types.ts directly. The doubly-curried generic functions (runModule, runModuleMap, testAll, all parameterized by <O extends Operation>) use the curried @template/@param/@returns JSDoc form rather than a single @type annotation, per the established generic-scoping gotcha: a bare @type on a curried arrow function doesn't expose the type parameter to nested helper closures in its body. Updates the two dependents (fjs/module.f.ts, fjs/emergent_testing/all.test.ts) and stale doc references and line-number citations across fjs/effects/todo, fjs/emergent_testing/todo, fjs/fsc/todo, fjs/js/identifier/todo, and fjs/media/json/todo. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y9bV7Civ4LaC6GPsjcgnJY
o2alexanderfedin
left a comment
There was a problem hiding this comment.
Reviewed at 7c30b78. The Entity wording is fixed exactly right — "narrowing the previously-exported Entity to the internal _Entity" says what actually happens. Four more modules join: fjs/dev, fjs/cas, fjs/cas/cli, fjs/emergent_testing.
npx tsc clean, 2357 pass / 0 fail matching main, no any in any emitted declaration.
One thing: TestOp needs the _ prefix
fjs/cas/proof.f.mjs declares a proof-local TestOp, and because JSDoc @typedef has no non-exported form it lands on the published surface:
// fjs/cas/proof.f.d.mts
export type TestOp = FileCasOperation | WriteFile | ReadFile | Rm | Mkdir;Nothing like it exists on main. It is the same case as _NodeList on #1485 and _Tests on #1488 — a type that is proof scaffolding rather than API, made public by the format rather than by intent. The convention answer is the prefix, and this PR already applies it correctly everywhere else: _TestAndPath (private on main, prefixed here) and _Entity both landed right.
It is the only unprefixed addition in the diff.
What I verified on the other four
fjs/devandfjs/cas/cliare token-identical tomain's compiled output.fjs/casdiffers by one extraction — thewritebranch offileCasbecomes a namedwriteImpl(sha2, path, stageDir, payload)so JSDoc can annotate it, with the previously-closed-over values passed explicitly. Same idiom asdispatchProps/foldStep/mkdirOp, semantically inert.fjs/emergent_testingdiffers only by droppingshouldLoadfrom an import. I checked it was genuinely unused —mainreferences it once (the import itself) and zero times in the body — and the test count is unchanged at 2357, so proof discovery is unaffected. TheshouldLoadthat governs discovery lives infjs/dev/module.f.tsand is untouched.- The two changed signatures are inert:
collectTestspicks up the_TestAndPathrename, anddefaultTest's{ fn, throws }: TestEntrybecomes a namedentryparameter. effects/node/virtualstill differs only by themkdirOpextraction from the previous head.
Happy to approve once TestOp is _TestOp.
JSDoc @typedef has no non-exported form, so the proof-local TestOp type was landing on the published .d.mts surface. Per the established underscore convention for proof-only scaffolding types (same case as _NodeList and _Tests in earlier PRs). Flagged by o2alexanderfedin's review. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y9bV7Civ4LaC6GPsjcgnJY
Splits the public token/metadata types (StringToken, NumberToken, BigIntToken, ErrorToken, WhitespaceToken, NewLineToken, IdToken, CommentToken, EofToken, JsToken, TokenMetadata, JsTokenWithMetadata) and the internal state-machine types (tokenizer states, error messages, the range-map generic helpers) into a sibling types.ts. proof.f.ts stays TypeScript for now — it depends on djs/serializer/module.f.ts, still unmigrated — and switches to importing runtime values from module.f.mjs and types from types.ts. The range-map combinators (union, rangeMapMerge, rangeFunc, scanRangeOp, reduceRangeMap, scanRangeSetOp, rangeSetFunc, create) all use the curried @template/@param/@returns JSDoc form. The many `rangeFunc<TokenizerState>(...)`/`rangeSetFunc<ParseNumberState>(...)` explicit-generic-instantiation call sites (JS has no syntax for explicit type arguments at a call) are replaced with an inline `/** @type {_CreateToToken<X>} */` cast on the passed-in handler function, which lets each generic combinator infer its type parameter from the argument's declared type instead. Updates all four dependents (fjs/djs/parser, fjs/djs/tokenizer, fjs/media/json/tokenizer, fjs/js/tokenizer/proof.f.ts) and stale doc references and line-number citations across fjs/bnf/todo, fjs/djs/todo, fjs/djs/tokenizer/todo, fjs/js/todo, fjs/media/json/todo, fjs/media/type/todo, and fjs/text/todo. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y9bV7Civ4LaC6GPsjcgnJY
Splits JsonToken and the internal _ScanState/_ScanInput types into a sibling types.ts. proof.f.ts stays TypeScript for now — it depends on djs/serializer/module.f.ts, still unmigrated — and switches to importing runtime values from module.f.mjs and types from types.ts. Updates the three dependents (fjs/media/json/module.f.ts, fjs/media/json/parser/module.f.ts, fjs/media/json/parser/proof.f.ts) and stale doc references across fjs/djs/todo, fjs/js/todo, and fjs/media/json/todo. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y9bV7Civ4LaC6GPsjcgnJY
|
@codex review |
|
Codex Review: Didn't find any major issues. Swish! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
These two modules had a type-only import cycle: parser/module.f.ts
imported type Unknown from ../module.f.ts, while module.f.ts imports
parser's runtime parse. Splitting Primitive, Unknown, Object, Array,
and Entry into a shared fjs/media/json/types.ts breaks the cycle —
parser now imports the type from types.ts instead of module.f.ts —
which unblocks migrating both modules together, along with their
proof.f.ts files (parser/proof.f.ts needed json/module.f.ts's runtime
stringify, so it could only migrate once that module did too).
Primitive is Ts<typeof primitive> derived from a runtime rtti schema
value defined in module.f.mjs; types.ts references it via an inline
`typeof import('./module.f.mjs').primitive` type query rather than a
value import, keeping types.ts free of runtime bindings.
Updates every consumer of fjs/media/json/module.f.ts across the tree:
fjs/djs, fjs/protocol/mcp, fjs/protocol/json_rpc, fjs/mcp, fjs/mcp/cas,
fjs/mcp/evo, fjs/cas (via fjs/ci), fjs/fsm, fjs/fsc, fjs/bnf/data,
fjs/dev/package_json, fjs/media/revision, fjs/media (root),
fjs/media/json/schema, fjs/emergent_testing, and about a dozen
fjs/types/* proof files that stringify to JSON for assertions. Also
fixes stale doc references and line-number citations across
fjs/djs/todo, fjs/effects/node/todo, fjs/media/json/todo,
fjs/media/html/todo, fjs/media/type/todo, and fjs/media/todo.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y9bV7Civ4LaC6GPsjcgnJY
o2alexanderfedin
left a comment
There was a problem hiding this comment.
Reviewed at f4b0f8d. _TestOp is fixed. Four more modules join: fjs/js/tokenizer, fjs/media/json/tokenizer, fjs/media/json, fjs/media/json/parser.
npx tsc clean, 2357 pass / 0 fail matching main, no any in any emitted declaration, and three of the four new modules are token-identical to main's compiled output. fjs/js/tokenizer differs by an extraction of the same shape as the others (parseSinglelineCommentStateOp named so JSDoc can annotate it).
Nine types that were private on main are now public
The same case as _TestOp, but at scale. Each of these was a non-exported type in its module.f.ts on main, and each is now a JSDoc @typedef in module.f.mjs, so each lands on the published surface unprefixed:
| Type | Now emitted from |
|---|---|
MapEntries |
fjs/media/json/module.f.d.mts |
JsonArray, JsonObject, JsonStack, JsonStackElement, JsonState, StateError, StateParse, StateResult |
fjs/media/json/parser/module.f.d.mts |
I confirmed all nine were type X rather than export type X on main, so this is a widening rather than a preserved contract.
They are JSDoc typedefs rather than types.ts entries, so the types.ts option that worked for Handler on #1491 — a plain non-exported type — is not available here. The prefix is the applicable convention, same as _Byte, _NodeList, _Tests, _Signature, and _TestOp.
Nothing breaks either way: no in-repo consumer imports them, and tsc is clean. It is the published type surface picking up nine implementation details, which is the thing the prefix exists to prevent.
Also still open from earlier heads
Entity remains the one genuinely removed public type in this PR. That is deliberate and the CHANGELOG now describes it accurately, so no action — just noting it is still the only removal, and the nine above are the only unprefixed additions.
Happy to approve once those nine carry the prefix.
This reverts commit f4b0f8d.
|
The nine unprefixed types flagged in the last review ( Reason: that migration passed The other three migrations from this PR ( Generated by Claude Code |
o2alexanderfedin
left a comment
There was a problem hiding this comment.
Reviewed at 0a9f1f050beebd2e7a784c08476205ae12627f8b. The nine unprefixed types are gone,
and the way they went is the cleanest possible: 0a9f1f05 reverts f4b0f8df exactly.
I checked that by tree hash rather than by reading the diff —
tree(0a9f1f05) == tree(fdc4af4a) == e2dfc76d, so the head is byte-identical to the
state before the migration commit I flagged. Nothing else was touched on the way out,
and the CHANGELOG entry for the reverted migration is gone with it (7 entries now, one
per migration commit). Approving.
The rest of the battery, re-run against this head:
Build and tests. tsc --noEmit exits 0. npm test gives 2357 pass / 0 fail, against
main's 2357 pass / 0 fail — no proof lost or gained, as a behaviour-preserving migration
should be. Branch is 10 commits ahead of 2b058dc with nothing on main it doesn't have.
Public surface, both axes. 36 type-alias differences against main, and every one is
either a _ privatization or a mechanical reference to one — Entity → _Entity,
JsToken's union members, and the ~30 tokenizer state types that had no exported form on
main because they were plain non-exported types in .f.ts. Zero unprefixed
additions, which is the thing I was asking for. Const signatures show 2 differences:
collectTests returning readonly _TestAndPath[], which is the same rename, and
defaultTest taking entry: TestEntry where main destructured { fn, throws } —
parameter names in a .d.ts are documentation, so that one is cosmetic.
Token equivalence, all 11 migrated files. fjs/cas/cli/module, fjs/dev/module,
fjs/dev/proof and fjs/media/json/tokenizer/module normalize identically to main's
compiled output. Three differ only in hex-literal letter case — 0x2an→0x2An,
0xaan→0xAAn, 0xbbn→0xBBn — same values. fjs/emergent_testing/module differs by
the dropped shouldLoad import from 66fea797; that one is safe because shouldLoad is
still exported from fjs/dev/module.f.mjs and still proved in
emergent_testing/proof.f.ts — the module genuinely wasn't using it, and tsc would
have failed if it were. The remaining three are the extraction idiom JSDoc forces —
writeImpl in cas, mkdirOp in virtual, parseSinglelineCommentStateOp in
js/tokenizer.
One thing worth recording since it looked alarming for a minute: a naive scan says
js/tokenizer loses a top-level startNegativeNumber. It doesn't — that binding is
inside a /* … */ block on main, and the grep was matching comment text. The migration
drops the commented-out block, which is the right call.
Docs. The AGENTS.md and todo/plan/roadmap.md edits are the two path references
the migrations invalidated, and both new targets exist. Resolving every relative markdown
link against the filesystem gives byte-identical broken-link sets on both sides — 144
pre-existing on main, none added, none fixed.
CHANGELOG. Seven entries, one per migration commit, correctly marked breaking. They
run ~420–560 characters, which is longer than §8.3's "about three wrapped lines" read
literally, but main's own merged migration entries sit at 283–476, so this is the
established shape for this class of entry rather than a drift I should be flagging on
this PR.
Two standalone BNF grammar files (JSON grammar, and an FJS-source grammar built on it) with no local types to split — both use only Rule/TerminalRange from fjs/bnf/types.ts, already TypeScript. Neither has an existing dependent or proof; verified via a resolved-path scan across all .md/.ts/.mjs files. Verified against the actual declaration-emit build (npm run prepack), not just tsc --noEmit, per the lesson from the reverted media/json attempt earlier in this PR. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y9bV7Civ4LaC6GPsjcgnJY
|
@codex review |
|
Codex Review: Didn't find any major issues. Keep it up! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
#1494 migrated fjs/effects/node/virtual, so the writer's proof could no longer resolve its `virtual` import and every job failed on tsc. The proof's whole runtime closure is now .f.mjs, so it moves rather than just repointing — fjs/nanvm holds no authored TypeScript beyond types.ts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FyMg2KPgfJeQ5FXGXjyFmd
Summary
fjs/effects/node/virtual/module.f.tsto authored JSDoc-typed.f.mjs, splittingDir,State,JsModule, and the internal_Entitytype into a siblingtypes.ts.proof.f.tsmigrates alongside it since its only runtime dependency is the module itself.fjs/dev/module.f.ts) to import runtime values frommodule.f.mjsand types fromtypes.ts.fjs/effects/node/virtual/README.mdandfjs/effects/node/virtual/todo/{dir-spine-descend,resolve-file-helper}.md.Notes
The module's two generic helpers (
operation,readOperation) needed the curried@template/@param/@returnsJSDoc form rather than a single@typeannotation on a curried arrow function, since a bare@typedoesn't expose the generic parameter inside the function body. The five call sites that previously passed anoperation(...)callback inline (mkdir,writeFile,rm,createExclusive,writeBytes) are pulled into named, explicitly-typed helpers, since TypeScript's contextual typing did not reliably flow the expected return type through the generic call when the callback was anonymous.Test plan
npx tsc --noEmitcleannode ./fjs/module.ts t— 2357/2357 passCo-Authored-By: Claude Sonnet 5 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01Y9bV7Civ4LaC6GPsjcgnJY
Generated by Claude Code