Skip to content

Migrate the last 12 authored .f.ts files to .f.mjs — completes stage 1 - #1505

Merged
sergey-shandar merged 17 commits into
mainfrom
claude/convert-f-ts-to-f-mjs-3
Aug 12, 2026
Merged

Migrate the last 12 authored .f.ts files to .f.mjs — completes stage 1#1505
sergey-shandar merged 17 commits into
mainfrom
claude/convert-f-ts-to-f-mjs-3

Conversation

@sergey-shandar

@sergey-shandar sergey-shandar commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Summary

Continues the stage-1 TypeScript-to-.mjs migration (following #1503, #1504). Ten modules, one commit each. After this PR no authored TypeScript proof or implementation module remains — only fixtures whose .ts extension is load-bearing.

module notable conversions
todo/proof.f.ts 2 typed consts; TemplateType_TemplateType
fjs/media/json/parser/proof.f.ts 1 typed const; JsonToken@import
fjs/js/tokenizer/proof.f.ts 3 typed consts; 660-line body byte-identical
fjs/effects/node/proof.f.ts match<ReadFile, IoResult<Vec>>@type cast on the operation map; list.empty/nonEmpty → checked @type {List<never, IoResult<Vec>>} declarations; 7 as → inline casts
fjs/emergent_testing/proof.f.ts 6 module-local aliases → _-prefixed @typedefs; mockRun<...> → cast from the source's own Parameters<typeof mockRun<...>>[0]
fjs/types/rtti/parse/proof.f.ts 5 function-local round-trips; unwrap<T> → checked @type declarations; 30 as const/as number
fjs/types/rtti/validate/proof.f.ts 23 function-local round-trips; local generic aliases + generic func@template typedefs and a @template/@param/@returns block
fjs/emergent_testing/example.f.ts 4 typed consts
fjs/types/ts/proof.f.ts index-signature guidance block (declare const) → sibling types.ts, non-exported
fjs/types/nominal/proof.f.ts two unique symbol brands + their types → sibling types.ts as _-prefixed exports

Doc references updated in twelve files; CHANGELOG.md updated under ## Unreleased.

Conventions applied

  • Type annotations use the checked declaration form (/** @type {T} */ above the const), never an inline /** @type {T} */ (expr) cast — an inline cast is as and silently absorbs mismatches (AGENTS.md §6). Inline casts appear only where main already used as, or where JSDoc has no other way to instantiate a generic.
  • Function-local type aliases stay function-local as JSDoc @typedefs. These are function-scoped (same-named typedefs in sibling functions do not collide) and are not emitted into declarations, so the two rtti proofs needed no hoisting or renaming.
  • The last two modules are mixed runtime/type: declare const and unique symbol have no JavaScript form, so those declarations move to the sibling types.ts per the migration doc, rather than being dropped or given an invented runtime value. fjs/media/json/types.ts already uses this shape for its Assert<Equal<…>> pin.
  • @module is not added to proofs: it belongs to module.* entry points, so each proof's @import tags form a standalone leading block.

Test plan

  • npm run prepack — clean
  • npm test — 2495/2495 pass, matching origin/main
  • Every new cast and type-level assertion mutation-probed on a cleaned tree (stale emitted .d.mts files otherwise intercept resolution and make probes report false negatives). Includes the match map's key and return type; the rtti event schema, reporter summary and mockRun op key; all the validate/parse round-trips, the _F0 signature and funcObj's f1 body; the moved _X0/_X3 index-signature claims. All produce tsc errors; the unmutated tree is clean.
  • nominal still demonstrates what it claims. Uncommenting the two comparisons the file documents as compile errors — strA > strB on a Nominal, and a < b on the symbol-intersection brand — each still reports TS2469 after the split.
  • Public surface. Path-aware diff of exported types vs origin/main: only _-prefixed additions. fjs/types/ts/types.d.ts still exports exactly Equal and Printer; both migrated rtti proofs and example export no type, as on main.
  • js/tokenizer body verified byte-identical via diff

What remains — fixtures only

  • fjs/emergent_testing/scenarios/*.ts, all.ts, all.test.ts — cross-runner fixtures whose extension is load-bearing: run.sh dispatches on *.pass.ts / *.fail.ts and hard-links them to _scenario.proof.ts / _all.test.ts.
  • fjs/djs/examples/input.f.ts, m.f.ts — djs source-format examples. Nothing references them (the djs proofs use a virtual 'input.f.js'), so whether they should be migrated, kept as format examples, or deleted is a call for a maintainer, not a mechanical rename. todo/samples.md already tracks reorganising them.

The at/utf8 typed consts become JSDoc @type annotations, and the
TemplateType template-literal type becomes a JSDoc @typedef.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
functionalscript 09f54fd Commit Preview URL

Branch Preview URL
Aug 12 2026, 10:31 PM

Comment thread todo/proof.f.mjs Fixed
claude added 2 commits August 12, 2026 20:53
The rest parameter's type is the tuple itself, not its element type.
main's `const m: TemplateType = '<html>Hello</html>'` is a checked
annotation; migrating it to an inline `/** @type {T} */ (expr)` cast
made it an `as` assertion, which is what the proof exists to avoid.
Verified by negative control: with a non-matching string the cast form
compiles clean, while the declaration form reports TS2322 as main does.

Per AGENTS.md section 6 -- prefer annotating a separate const
declaration over casting an expression inline.
Comment thread todo/proof.f.mjs Fixed
The typed tokenizeString const becomes a JSDoc @type annotation, and
the JsonToken type import becomes an @import in a leading block.

Doc references updated in 66e-parser-container-stack-bookkeeping.md and
stringify-sorted-canonical.md; the latter's line citation was already
off by one before this migration (line 13 was blank, the binding was on
14), so it is corrected to the binding's actual new line rather than
merely shifted.
@sergey-shandar sergey-shandar changed the title Migrate todo/proof.f.ts to .f.mjs Migrate todo and fjs/media/json/parser proofs to .f.mjs Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

deno failed on c93eb34 for infrastructure reasons, not code: setup-deno could not download deno-aarch64-unknown-linux-gnu.zip from GitHub releases — socket hang up, twice, then the job errored. Nothing in the repository was checked out or built before it failed.

Every other completed check on this commit is green (wasm, node24, ubuntu-arm, nix-flakes, Workers Builds, all CodeQL analyses); the rest are still running. The same class of flake hit wasm on #1504 earlier today.

A re-run should clear it — I don't have permission to trigger one (rerun-failed-jobs returns 403 for this integration), so this needs a re-run from someone who does. No code change is warranted.


Generated by Claude Code

The three typed consts become JSDoc @type annotations and the JsToken /
JsTokenWithMetadata type imports become an @import in a leading block.
The 660-line proof body is carried over byte-identically.
@sergey-shandar sergey-shandar changed the title Migrate todo and fjs/media/json/parser proofs to .f.mjs Migrate todo, media/json/parser and js/tokenizer proofs to .f.mjs Aug 12, 2026
The explicit generic instantiation match<ReadFile, IoResult<Vec>>(...)
becomes an inline @type cast on the operation map, following the
precedent set for match/do_ in fjs/effects/proof.f.mjs. The
list.empty/list.nonEmpty instantiations cannot rely on the callee's
contextual type -- O widens to Operation rather than never -- so they
become checked @type {List<never, IoResult<Vec>>} declarations rather
than casts. The seven pre-existing `as` assertions carry over as inline
@type casts.

Verified the match cast is load-bearing, not inert: renaming the map's
command key and returning a wrong type from the handler each produce
tsc errors, while the unmutated file compiles clean.

@o2alexanderfedin o2alexanderfedin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed at 3ce4c78f (the head moved from f2657444 to 3ce4c78f mid-review — the
fjs/effects/node/proof.f.ts migration commit was added; everything below was re-run at
the new head).

One real problem, everything else verified clean.

todo/proof.f.mjs introduces an unprefixed public type TemplateType

todo/proof.f.mjs:1

/** @typedef {`<html>${string}</html>`} TemplateType */

On main, TemplateType was a non-exported TypeScript type alias, and
todo/proof.f.d.ts contained only export declare const proof. As a JSDoc @typedef
it is emitted as a public alias — the new todo/proof.f.d.mts begins:

export type TemplateType = `<html>${string}</html>`;

package.json files includes **/*.d.mts, so this ships in the tarball. It is an
implementation-only type used only inside this one file, so per AGENTS.md §6.2
("Name implementation-only JSDoc typedefs with a leading _") and
fjs/fsc/README.md#private-jsdoc-typedefs it should be _TemplateType.

Every other @typedef in a proof.f.mjs in the repo already follows this — 14 files,
all _-prefixed (_TestOp, _Benchmark, _NodeList, _Tests, _AddOp, …). This is
the only exception.

A whole-tree, file-aware diff of exported type names between emitted declarations on
origin/main and this head returns exactly one line:

> todo/proof.f.d.mts::TemplateType

Worth noting for future rounds: bin/extract.mjs-style scans that only walk fjs/
miss this, because the file is under todo/.

Verified clean

  • npx tsc --noEmit → exit 0.
  • npm run prepack from a freshly cleaned tree → exit 0 (both passes).
  • npm test → 2495 pass / 0 fail, exactly matching origin/main (2495) with a clean
    tree before each run.
  • §2 precondition. Walked the transitive runtime import graph of all four migrated
    proofs with an acorn parse of every reachable module: 47 files, zero relative .ts
    runtime edges, no missing targets. All four were eligible to become .f.mjs.
  • Token equivalence. Compared main's compiled .f.js against this PR's authored
    .f.mjs by acorn AST comparison (position/raw stripped), not a regex normalizer —
    these files contain '//singleline comment' and '/* multiline comment */' string
    literals that a comment-stripping regex would corrupt.
    • fjs/js/tokenizer/proof, fjs/media/json/parser/proof, todo/proof — AST-identical.
    • fjs/effects/node/proof — identical except the two deliberate hoists of
      listEmpty() / listNonEmpty(...) into const chunks (JSDoc cannot express
      explicit generic instantiation at a call site). Both hoisted expressions are pure,
      so the evaluation-order shift is unobservable; re-inlining them by hand makes the
      file AST-identical to main's compiled output.
    • Negative-controlled in both directions: one-token mutations on the PR side and on the
      main side (including a mutation inside a '/* … */' string literal) all report
      AST-DIFFERENT.
  • The match(...) cast keeps its checking. /** @type {OperationMap<ReadFile, IoResult<Vec>>} */ ({ readFile: … }) is a cast, so I tested rather than assumed:
    renaming the key to readFileX gives TS2352 (Property 'readFile' is missing) plus
    TS7006 on path, and changing the body to ok(42) gives TS2352
    (Type 'number' is not comparable to type 'Vec'). Key names, parameter contextual
    typing and return values are all still checked. The remaining new casts
    (@type {Dir}, @type {never}, @type {{ code?: unknown }}) replace as casts
    one-for-one.
  • The @type annotations are annotations, not casts. Mutation-probed all of them on
    a cleaned tree (no emitted .d.mts around to change resolution):
    '<htmlHello</html>' → TS2322 against the template-literal type;
    withoutMetada returning the whole token → TS2322 JsTokenWithMetadata not assignable
    to JsToken; tokenizeString returning number[] → TS2322; const chunks = 5
    TS2322 against List<never, IoResult<Vec>>.
  • Public surface. Exported const signatures: 0 added, 0 removed, 0 changed — nothing
    widened to any. Exported types: the single addition above.
  • Link check. 141 broken relative markdown links, an identical set to
    origin/main — the rename introduced none. The three proof.f.ts references in
    fjs/djs/todo/66e-…md, fjs/media/json/todo/stringify-sorted-canonical.md and
    fjs/effects/node/todo/ornotfound-combinator.md were all updated, and the only
    remaining proof.f.ts mentions in the tree are the four CHANGELOG entries, which
    correctly name the old path.
  • CHANGELOG. Four entries, all under ## Unreleased, all with the
    **BREAKING CHANGES:** prefix (a .f.ts.f.mjs specifier change is breaking), each
    linking only /pull/1505. No released section touched.
  • @import placement in a standalone leading JSDoc block matches the established shape
    for proof files (fjs/cas/proof.f.mjs, fjs/crypto/sha2/proof.f.mjs,
    fjs/types/sorted_set/proof.f.mjs, …), each followed by the load-bearing blank line.

Not verified: coverage — npm run cov reports a vacuous 100.00 over 0 tests in this
environment on main too, so it tells us nothing about this PR either way. Rust gates and
the generator round-trip were not run; nanvm-lib/ and fjs/nanvm/ are untouched.

claude added 2 commits August 12, 2026 21:28
The six module-local type aliases become _-prefixed JSDoc @typedefs,
since a JSDoc @typedef has no non-exported form. The
mockRun<RegisterMockOps, RegisterMockState>(...) instantiation becomes
an inline @type cast built from the same Parameters<typeof mockRun<...>>[0]
type the TypeScript source already spelled out. `as const` assertions
become inline @type {const} casts, per the rule that the const form
cannot be hoisted to a declaration annotation.

Verified the annotations are load-bearing rather than inert: breaking a
field type in the rtti event schema, passing a non-number to the
reporter's summary, and renaming an op key in the mockRun map each
produce tsc errors, while the unmutated file compiles clean.

Doc references updated in four files. Two carried line citations that
were already wrong before this migration -- remove-native-json.md cited
a blank line, and lift-js-lexical-predicates cited 313-328 (the
register-runner mock) for cases that actually lived at 387-402 -- so
both are corrected to the content's real new location.
TemplateType was a non-exported TypeScript type alias on main, but a
JSDoc @typedef has no non-exported form, so it emitted as a public
`export type TemplateType` in todo/proof.f.d.mts and shipped in the
tarball via package.json `files`. Renamed _TemplateType per the API
visibility convention.

Verified with a path-aware diff of every exported type name in every
emitted declaration against origin/main: the only remaining additions
are the seven _-prefixed typedefs this branch introduces, and no
unprefixed public type is added or removed.

Copy link
Copy Markdown
Contributor Author

Good catch — fixed in 6a6c48f. TemplateType_TemplateType, and the CHANGELOG entry now records the rename.

I confirmed the finding rather than taking it on faith: todo/proof.f.d.mts did begin export type TemplateType = ... while main's todo/proof.f.d.ts had only export declare const proof.

I also reran your whole-tree check with the scan rooted at the repository rather than fjs/, since that blind spot is exactly what let this through. Path-aware diff of every export type/interface in every emitted .d.ts/.d.mts against origin/main (3859e7d) now returns seven additions, all _-prefixed:

> ./fjs/emergent_testing/proof.f.d.mts::export type _Event
> ./fjs/emergent_testing/proof.f.d.mts::export type _RegisterMockOps
> ./fjs/emergent_testing/proof.f.d.mts::export type _RegisterMockState
> ./fjs/emergent_testing/proof.f.d.mts::export type _RegisterRunner
> ./fjs/emergent_testing/proof.f.d.mts::export type _RegisterTestOp
> ./fjs/emergent_testing/proof.f.d.mts::export type _TestReporter
> ./todo/proof.f.d.mts::export type _TemplateType

No unprefixed public type is added or removed. (First attempt at this comparison was against a stale baseline — cloning the local repo resolves origin/main to the local main ref, not the real one — so the numbers above come from a git worktree pinned to 3859e7d.)

Since your review, fjs/emergent_testing/proof.f.ts was also migrated (29a604e). Its six module-local aliases are _-prefixed for the same reason, and the mockRun<...> instantiation reuses the Parameters<typeof mockRun<...>>[0] type the TypeScript source already spelled out. I mutation-probed that one too: breaking a field type in the rtti event schema, passing a non-number to the reporter's summary, and renaming an op key in the map each produce tsc errors, with the unmutated file clean.

npm run prepack clean and npm test 2495/0 at 6a6c48f.


Generated by Claude Code

@sergey-shandar sergey-shandar changed the title Migrate todo, media/json/parser and js/tokenizer proofs to .f.mjs Migrate five proof modules to .f.mjs Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

Update on 6a6c48f: the download flake moved to wasm this time — setup@v1.1.3 couldn't fetch wasmtime-v47.0.3-aarch64-linux.tar.xz, socket hang up twice, job errored before the toolchain ran. deno is green-in-progress on this head, so the two are alternating rather than both being broken.

Same conclusion as before: no code change is warranted, and it needs a re-run from someone with permission (I get 403 on rerun-failed-jobs). Everything else completed on this commit is green — node24, ubuntu-arm, nix-flakes, Workers Builds, and all three CodeQL analyses.


Generated by Claude Code

The function-local `type _ = Assert<Equal<...>>` round-trips stay
function-local as JSDoc @typedefs. A JSDoc @typedef inside a function
body is function-scoped -- same-named typedefs in sibling functions do
not collide -- and is not emitted into the .d.mts, so this keeps the
module's public type surface identical to main and needs no hoisting or
renaming of the recursive schema thunks.

unwrap<T>(...) instantiations become checked @type declarations rather
than casts: T appears only in return position, so the contextual type
drives inference. The 30 `as const` / `as number` assertions carry over
as inline @type casts, which is the only form available for @type
{const}.

Verified the local typedefs are still load-bearing, probing on a
cleaned tree so stale emitted declarations could not intercept
resolution: breaking the boolean round-trip, the asserted recursive
_A shape, the asserted `list` schema tag, and the tree round-trip each
produce a tsc error, with the unmutated file clean. The two sites main
left unasserted remain unasserted, and the emitted declaration exports
no type, as on main.
@sergey-shandar
sergey-shandar force-pushed the claude/convert-f-ts-to-f-mjs-3 branch from b2c88e0 to e031c36 Compare August 12, 2026 21:42
claude added 2 commits August 12, 2026 21:46
The 23 function-local `type _ = Assert<Equal<...>>` round-trips stay
function-local as JSDoc @typedefs, so the ten function-local schema
consts they name via `Ts<typeof t>` need no hoisting and the emitted
declaration exports no type, as on main.

funcParam/funcObj additionally carry local generic aliases and a
generic arrow: `type F0<T>` becomes a `@template` @typedef and
`const func = <T>(f0, f1) => ...` becomes a
@template/@param/@returns block.

Verified load-bearing on a cleaned tree: mutating the boolean, tuple,
record, union and option round-trips, the _F0 signature, and funcObj's
f1 body each produce a tsc error, with the unmutated file clean.

proof-shared-asserts.md's six line citations are recomputed from the
suite boundaries they bracket rather than shifted by a guessed offset.
The four typed consts become JSDoc @type annotations. This is a live
proof module -- its 12 tests run in the suite -- not an inert fixture.
Doc reference updated in todo-property.md.
@sergey-shandar sergey-shandar changed the title Migrate five proof modules to .f.mjs Migrate eight proof modules to .f.mjs Aug 12, 2026
claude added 2 commits August 12, 2026 21:54
The runtime proofs convert directly -- no annotations to carry. The
trailing index-signature guidance block cannot: it uses `declare const`,
which has no JavaScript form. Per the migration doc's rule for mixed
runtime/type modules, those declarations move into the sibling
types.ts rather than being dropped or given an invented runtime value.

They are non-exported and _-prefixed, matching the existing pin in
fjs/media/json/types.ts. Verified they add nothing to the emitted
declaration -- fjs/types/ts/types.d.ts still exports exactly Equal and
Printer, and proof.f.d.mts exports no type, as on main.

Verified the moved asserts still assert on a cleaned tree: breaking the
_X0 and _X3 index-signature claims each produce a tsc error.
`declare const … : unique symbol` has no JavaScript form, so the two
brand carriers move to the sibling types.ts along with the two types
keyed on them, per the migration doc's rule for mixed runtime/type
modules. The proof keeps the expressions themselves -- they are
statement-level demonstrations of what TypeScript does and does not
reject, which cannot be expressed as declarations in types.ts.

The two moved types must be exported for the proof to @import them, so
they are _-prefixed and private by contract; nothing else in the
module's surface changes.

Verified the proof still demonstrates what it claims, by uncommenting
the two comparisons it documents as compile errors: `strA > strB` on a
Nominal and `a < b` on the symbol-intersection brand each still report
TS2469, and the file is otherwise clean.
@sergey-shandar sergey-shandar changed the title Migrate eight proof modules to .f.mjs Migrate the last ten proof modules to .f.mjs Aug 12, 2026
/** @type {_StringKeyBranded} */
const x = { _brand: 'NoCompare' }
// No Error
if (x < x) { }
const b = /** @type {_SymbolKeyBranded} */ ({})

// No Error
a < b
const b = /** @type {_IntersectionSafeId} */ ({ value: 2 })

// No Compile-time error
if (a < b) { }
const b = /** @type {_IntersectionSafeId} */ ({ value: 2 })

// No Compile-time error
if (a < b) { }
/** @type {_StringKeyBranded} */
const x = { _brand: 'NoCompare' }
// No Error
if (x < x) { }
/** @type {_StringKeyBranded} */
const x = { _brand: 'NoCompare' }
// No Error
if (x < x) { }
const b = /** @type {_SymbolKeyBranded} */ ({})

// No Error
a < b
const b = /** @type {_SymbolKeyBranded} */ ({})

// No Error
a < b
}
{

const a = /** @type {_SymbolIntersectionBranded} */ (/** @type {any} */ (undefined))
{

const a = /** @type {_SymbolIntersectionBranded} */ (/** @type {any} */ (undefined))
const b = /** @type {_SymbolIntersectionBranded} */ (/** @type {any} */ (undefined))

Copy link
Copy Markdown
Contributor Author

Answering the ten github-code-quality comments on fjs/types/nominal/proof.f.mjs together, since they all point at the same thing.

They are all correct as observations and all intentional: if (a < b) { }, if (x < x) { }, the bare a < b, and the unused const a / const b in the last block have no runtime effect. That is the proof. pre documents which brand encodings TypeScript refuses to order-compare — the value of each line is whether it compiles, not what it evaluates to. The // No Error / // No Compile-time error comments above them say so.

None of it is introduced by this PR. Every flagged construct is verbatim from main's proof.f.ts:

flagged in .f.mjs on main's .f.ts
if (a < b) { } line 28
if (x < x) { } line 36
a < b line 45
unused const a / const b lines 51–52

What this PR changed in that file is only how the types are spelled: the two declare const … : unique symbol brands moved to types.ts because they have no JavaScript form, and the local aliases became JSDoc.

I'd rather not "fix" these — deleting the comparisons or the two unused consts would delete what the proof asserts. I did verify the split preserved it, by uncommenting the two comparisons the file documents as compile errors: strA > strB on a Nominal still reports TS2469, and so does a < b on the symbol-intersection brand.

If the noise is unwanted, the options are a scoped suppression for pre or moving these demos into a types.ts — but the latter changes what they demonstrate, since a statement-level "this compiles" check can't be written as a declaration. Happy to do either if you'd prefer.


Generated by Claude Code

@o2alexanderfedin o2alexanderfedin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed at 57546e81. The head moved twice while I was working: I started at
f4d7e413 (eight modules), and by the time I went to post, 32b51288 and
57546e81 had added fjs/types/ts and fjs/types/nominal, taking the scope to
ten modules. Everything below was re-run from scratch at 57546e81, so the
numbers are for the current head, not for f4d7e413.

Previous finding: resolved

The one finding from my review at 3ce4c78ftodo/proof.f.ts's new
@typedef TemplateType being unprefixed, and therefore emitting as a public
export type TemplateType in the shipped todo/proof.f.d.mts — is fixed. It is
now _TemplateType, and the CHANGELOG entry explains why the rename was needed
("it was never exported, but a JSDoc @typedef has no non-exported form").

I re-ran the whole added-type surface at this head rather than just re-checking
that one file. Against origin/main (3859e7d4), with npm run prepack in both
trees, the emitted declarations gain exactly nine exported type aliases and lose
none:

+ _Event                       (emergent_testing/proof)
+ _TestReporter                (emergent_testing/proof)
+ _RegisterMockState           (emergent_testing/proof)
+ _RegisterMockOps             (emergent_testing/proof)
+ _RegisterRunner              (emergent_testing/proof)
+ _RegisterTestOp              (emergent_testing/proof)
+ _TemplateType                (todo/proof)
+ _SymbolKeyBranded            (types/nominal/types.ts)
+ _SymbolIntersectionBranded   (types/nominal/types.ts)

All nine carry the _ prefix required by AGENTS.md §6.2. No unprefixed public
type is added anywhere in the ten modules. On the const axis, 893 exported const
signatures on both sides, with the only difference being
emergent_testing/example.f.d.ts::*example.f.d.mts::* — same five names,
byte-identical signatures, no silent widening to any.

The 23 Assert<Equal<...>> round-trips in types/rtti/validate/proof.f.mjs and
their counterparts in parse are declared inside function bodies and, as the
CHANGELOG claims, do not reach the emitted declarations — none of them appear in
the extracted type surface.

One thing worth fixing

fjs/types/ts/types.ts:28 — the import type { Assert } sits in the middle of
the file, after two exported declarations.

AGENTS.md §4 lays out the required order for a TypeScript file: module JSDoc,
blank line, then type-only imports, then runtime import groups, then
declarations. Here the import lands at line 28, after export type Equal
(line 8) and export type Printer (line 16), because the moved index-signature
block was appended verbatim to the end of the file and its import came along with
it.

This is the only types.ts in the repo that does it — I checked all of them:

$ for f in $(git ls-files '*types.ts'); do <first-decl vs last-import check>; done
fjs/types/ts/types.ts firstDecl line 8 lastImport line 28

The precedent the CHANGELOG entry cites, fjs/media/json/types.ts, keeps all six
of its import type lines at the top, ahead of every declaration. Moving this one
line up next to the @module block would match it.

Nothing breaks as a result — tsc is clean, prepack is clean, and I confirmed
the @module header still survives declaration emit (grep -c '@module' returns
1 in each of fjs/types/ts/types.d.ts, fjs/types/nominal/types.d.ts, and
fjs/media/json/types.d.ts). It is a layout nit, not a defect.

What I verified

Gates. npx tsc --noEmit exit 0. npm run prepack exit 0 from a freshly
cleaned tree (both passes). npm test: pass: 2495, fail: 0, total: 2495,
exactly matching origin/main in a separate clean worktree — a behaviour-
preserving migration should match, and it does.

§2 precondition. I walked the transitive runtime import graph out of all ten
migrated files rather than assuming it. 59 runtime modules reachable from the
first eight, 6 from the two added later; zero relative .ts runtime edges in
either walk. Every .ts edge that remains is type-only (import type /
@import) into a types.ts, which §2 allows.

Runtime equivalence. For each of the ten, I compared main's compiled
.f.js against this PR's authored .f.mjs by acorn AST comparison (positions,
raw and comments stripped). Nine come back byte-identical at the AST level:

IDENTICAL  emergent_testing/proof   js/tokenizer/proof   media/json/parser/proof
IDENTICAL  types/rtti/parse/proof   types/rtti/validate/proof
IDENTICAL  emergent_testing/example todo/proof
IDENTICAL  types/nominal/proof      types/ts/proof

The tenth, fjs/effects/node/proof.f.mjs, differs in exactly the two places the
CHANGELOG describes: listEmpty<never, IoResult<Vec>>() and
listNonEmpty<never, IoResult<Vec>>(...) are hoisted into
/** @type {List<never, IoResult<Vec>>} */ const chunks because JSDoc has no
inline call-type-argument form. Both hoists move a pure call earlier in the same
statement sequence; nothing observable changes.

I negative-controlled the comparator both ways before trusting any IDENTICAL,
including a one-token mutation inside a '/* multiline comment */' string
literal in js/tokenizer/proof.f.mjs — the case that defeats regex-based
normalizers. All three controls reported DIFFERENT at the mutated node:

DIFFERENT  js/tokenizer/proof     "/* multiline comment */" -> "/* multilineX comment */"
DIFFERENT  media/json/parser/proof  sort -> sortX
DIFFERENT  todo/proof             '<html>Hello</html>' -> '<html>Hellox</html>'
DIFFERENT  types/nominal/proof    asNominal(123) -> asNominal(124)

Casts still check. The migration replaces a lot of as T with
/** @type {T} */ (...), and a cast on an object literal can silently drop
contextual checking. I mutation-probed the ones that matter rather than assuming;
every probe produced the error it should, and each was reverted afterwards:

  • match(/** @type {OperationMap<ReadFile, IoResult<Vec>>} */ ({...}))
    returning ok('not-a-vec') gives TS2352 (Ok<string> not comparable to
    IoResult<Vec>), and path.nonExistentProp gives TS2339 on string. The
    operation map is still fully checked, including its callback parameters.
  • mockRun(/** @type {Parameters<typeof mockRun<...>>[0]} */ ({...})) — the
    inner lambdas dropped their explicit parameter annotations and now rely on the
    cast for contextual typing. s.nonExistentProp in both the await and the
    all reducer gives TS2339 on _RegisterMockState, so they are contextually
    typed, not any.
  • writeEvent(['summary', pass, fail, 'x']) gives TS2322 — the _Event shape is
    enforced through the @typedef.
  • The function-scoped Assert<Equal<...>> typedefs are live, not decoration:
    flipping the first one in parse (booleannumber) gives
    proof.f.mjs(64,34): error TS2344: Type 'false' does not satisfy the constraint 'true', and flipping a late duplicate-named one in validate
    (number | undefinednumber | null) gives the same at (271,34). Neither
    shadowing nor position weakens them.
  • The block moved into fjs/types/ts/types.ts is still checked there:
    _X3's bigint | undefinedbigint gives TS2344 at types.ts(60,19).
  • In types/nominal/proof.f.mjs, uncommenting to(userId2A) gives TS2345
    (nominal brands still distinct), _ForbiddenCompare<number> gives TS2344
    against the @template {object} constraint, and uncommenting a < b on
    _SymbolIntersectionBranded gives exactly the TS2469 the surviving comment
    claims. The type-level demonstrations survived the move to types.ts intact.

Docs and packaging. linkcheck broken-link sets are identical to
origin/main (163 lines, diff empty), so no todo/*.md reference was
stranded. The eleven .md updates repoint .f.ts.f.mjs and re-anchor the
line references; I spot-checked several (emergent_testing/proof.f.mjs:47 is
indeed the JSON.stringify line, media/json/parser/proof.f.mjs:16 the
stringify alias, rtti/parse/proof.f.mjs:13-38 the shared-helper block) and
they land correctly. No stale .f.ts reference to any of the ten survives
outside the released CHANGELOG history at line ~292, which is correctly
untouched. package.json files already covers **/*.mjs and **/*.d.mts, and
there is no exports map to update.

CHANGELOG. Ten Unreleased entries, one per migrated module, each with the
**BREAKING CHANGES:** prefix (§8.4) — correct, since every one of these is a
specifier change — and each linking only /pull/1505, no issue or todo/ link.
Released sections untouched. I checked the counts the entries assert: the
"23 function-local Assert<Equal<...>> round-trips" in validate is exactly 23,
and the fjs/types/ts entry's claim that "no public type is added or removed"
matches the emitted types.d.ts, which contains only Equal and Printer.

Not verified

  • npm run cov reports a vacuous 100.00 over 0 tests on Node v23 in this
    environment, and does the same on origin/main. Coverage is unverifiable here;
    I am not reporting a number either way.
  • Rust gates and the nanvm generator round-trip were skipped — nanvm-lib/ and
    fjs/nanvm/ are untouched by this PR.

claude added 2 commits August 12, 2026 22:04
Two djs source-format examples with no type annotations; `input`'s
import specifier updates to ./m.f.mjs. Nothing references them -- the
djs proofs compile a virtual 'input.f.js' -- so this is a rename with
no consumer to update.

Note they now ship in the package: `files` covers `**/*.mjs` but no
`**/*.ts`, so these two were previously excluded from the tarball.
Appending the index-signature block left its `import type { Assert }` at
line 28, after two exported declarations. AGENTS.md section 4 puts
type-only imports directly after the module JSDoc block, ahead of every
declaration -- which is what the file this change cites as precedent,
fjs/media/json/types.ts, already does.

Layout only: tsc and prepack stay clean, the @module header still
survives declaration emit, and the moved _X3 assert still reports
TS2344 when broken.

Copy link
Copy Markdown
Contributor Author

Fixed in fd329dd. You're right that it was out of order, and the cause is exactly what you diagnosed — the block was appended verbatim and dragged its import along. import type { Assert } now sits directly after the @module block, ahead of both exported declarations, matching fjs/media/json/types.ts.

Checked rather than assumed: last import is line 8, first declaration line 10; tsc and prepack clean; @module still survives declaration emit in types.d.ts; and the moved _X3 assert still reports TS2344 when its bigint | undefined is broken to bigint, so the relocation didn't quietly deactivate it.

Two notes on scope, since the head has moved again:

fjs/djs/examples migrated (4867d6e), so no authored .f.ts remains anywhere. Nothing references those two — the djs proofs compile a virtual 'input.f.js' — so it was a rename with no consumer to update. One consequence worth flagging, recorded in the CHANGELOG entry: package.json's files covers **/*.mjs but no **/*.ts, so these two now ship in the tarball where they previously did not. Same mechanism applies to every proof migrated in this series; say the word if you'd rather they were deleted or excluded instead.

fjs/emergent_testing/scenarios/*.ts and all.test.ts are deliberately not migrated. run.sh hard-links a scenario to _scenario.proof.ts and all.ts to _all.test.ts, then runs node --test, bun test and deno test against them. What those fixtures exercise is each runner executing a TypeScript proof natively, so converting them to .mjs would delete the coverage rather than port it. They need a decision about whether that capability should still be tested, which is yours to make, not a mechanical rename.


Generated by Claude Code

@sergey-shandar

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fd329dd343

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

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".

Comment thread CHANGELOG.md Outdated
Comment on lines +23 to +27
- **BREAKING CHANGES:** `fjs/djs/examples/input.f.ts` and
`fjs/djs/examples/m.f.ts` migrate from authored TypeScript to
JavaScript (`.f.mjs`) — no type annotations to convert; `input`'s
import specifier updates to `./m.f.mjs`. Being `.mjs`, they are now
covered by `package.json`'s `files` and ship in the tarball, which

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Consolidate the changelog notes

This change adds ten separate, detailed entries for the same #1505 migration, totaling roughly 79 lines. Collapse these into one short, user-facing release note; the repository limits each change to a few wrapped lines and directs implementation walkthroughs to the PR description.

AGENTS.md reference: AGENTS.md:L1221-L1226

Useful? React with 👍 / 👎.

@@ -1,4 +1,4 @@
import c from "./m.f.ts"
import c from "./m.f.mjs"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Update the README link for the renamed example

Renaming this fixture to input.f.mjs leaves fjs/djs/README.md:22 linking to the deleted examples/input.f.ts, so readers following the documentation now receive a broken link. Update that link as part of the rename.

Useful? React with 👍 / 👎.

The examples rename left fjs/djs/README.md:22 pointing at the deleted
examples/input.f.ts. My earlier sweep for that rename searched .mjs/.ts/
.json/.sh but not .md, which is how it slipped through; a repo-wide
check of every relative markdown link to a source file now passes except
one pre-existing link to fjs/types/rtti/data/, a directory that has
never existed and is not a rename artifact.

Also repoints an already-stale link to text/utf16/module.f.ts, migrated
in an earlier PR, and its label.

The eleven #1505 changelog entries are cut from a median of 494
characters to 244, per AGENTS.md's "at most a few lines, ~250
characters" -- the detail they carried is already in the PR
description. They stay one-per-module: that is how all 130 existing
Unreleased entries are written, each an independent breaking specifier
change, many sharing a PR link.

Copy link
Copy Markdown
Contributor Author

Both addressed in 6e84d20, one fully and one partly.

README link (P2) — correct, and my miss. fjs/djs/README.md:22 did point at the deleted examples/input.f.ts. My doc sweep for that rename searched .mjs/.ts/.json/.sh but not .md, which is exactly how it slipped through. Fixed, and rather than just patching the one line I ran a repo-wide check of every relative markdown link to a source file. That turned up two more, both pre-existing on origin/main:

  • remove-native-json.md → text/utf16/module.f.ts — stale since that module migrated in an earlier PR. Fixed here, label and target.
  • serializable-data.md → ../data/module.f.ts — left alone. fjs/types/rtti/data/ has never existed, so it's a doc question about a planned module, not a rename artifact.

Changelog (P1) — half agreed. You're right on length: my entries ran a median of 494 characters against AGENTS.md's "~250". They're now cut to a median of 244, max 263, with the detail living in the PR description where §8.3 says it belongs.

I did not collapse them into one entry, because that would break the pattern rather than follow it. ## Unreleased currently holds 130 other entries, one per migrated module, many sharing a single PR link — that is how this whole migration has been recorded. Each is an independent breaking specifier change that a consumer may hit in isolation, so one line per module is what's useful to someone upgrading. For calibration, those 130 existing entries have a median of 351 characters and a maximum of 1127, and 96 of them exceed the ~250 guide — mine were long relative to the rule, but not anomalous, and they're now below the section median.

tsc, prepack and npm test (2495/0) all clean at this head.


Generated by Claude Code

@o2alexanderfedin o2alexanderfedin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed at 6e84d20b (head moved twice during this pass: fd329dd36e84d20b; everything below was re-run at 6e84d20b, and the diff fd329dd3..6e84d20b is docs-only — CHANGELOG.md, fjs/djs/README.md, fjs/media/json/todo/remove-native-json.md).

Approving. The one open item from the 57546e81 round is fixed, and the title's central claim checks out.

The prior open finding is closed

fjs/types/ts/types.ts — the import type { Assert } now sits at the top of the file, directly after the /** … @module */ block and its blank line, ahead of both exported declarations (commit fd329dd3). AGENTS.md §4 ordering restored, and the header still survives declaration emit:

fjs/types/ts/types.d.ts:    @module count = 1
fjs/types/nominal/types.d.ts: @module count = 1

"the last ten" — checked

Verified, and it is if anything an understatement. origin/main (3859e7d4) carries 12 authored .f.ts files; this PR migrates all 12, and

$ find . -name '*.f.ts' -not -path './node_modules/*' | wc -l
0

Nine of the twelve are literally proof.f.ts; the tenth "proof module" is fjs/emergent_testing/example.f.ts, and the last round added the two fjs/djs/examples fixtures, which are not proofs. So the count reads fine but the scope line no longer covers everything in the branch. Substantively: this closes AGENTS.md §2 Stage 1 — there is no authored implementation or proof .f.ts left anywhere in the tree.

Verification battery (all at 6e84d20b)

  • npx tsc --noEmit — exit 0.
  • npm run prepack from a freshly cleaned tree — exit 0 (both passes).
  • npm testpass: 2495, fail: 0, total: 2495, exactly matching fs-main on 3859e7d4 (pass: 2495, fail: 0, total: 2495). Cleaned before testing so the emitted .f.js did not inflate the count.
  • Public surface, dual axis. bin/extract.mjs / bin/consts.mjs over the whole tree (sanity-checked non-empty and non-blind: /todo::proof present in both snapshots).
    • Types: 514 → 523. All nine additions are _-prefixed — _Event, _RegisterMockOps, _RegisterMockState, _RegisterRunner, _RegisterTestOp, _SymbolIntersectionBranded, _SymbolKeyBranded, _TemplateType, _TestReporter. No unprefixed public export type added, so the defect that recurred through six rounds on #1503 and once here as TemplateType does not reappear. No existing type changed or was removed.
    • Consts: 893 → 893, zero changed signatures, nothing widened to any. The only key movement is fjs/emergent_testing/example.f.d.ts::*…f.d.mts::* with byte-identical signatures.
    • Emitted declarations for all 12 migrated modules: elided = 0, any = 0 in every one. (i: any) in todo/proof.f.mjs is verbatim from todo/proof.f.ts on main, not new.
  • §2 precondition, verified rather than assumed. Every relative runtime import in all 12 migrated modules targets .f.mjs. Repo-wide, no .mjs file imports a .ts at runtime, and since zero authored .f.ts remain the transitive runtime graph cannot reach one.
  • Token/AST equivalence, main's compiled .f.js vs the PR's authored .f.mjs, via acorn AST comparison (positions and raw stripped, module specifier extensions normalised). 11 of 12 IDENTICAL. Negative-controlled in both directions with a one-character mutation inside the '[{"kind":"/*","value":" multiline comment "},…]' string literal in fjs/js/tokenizer/proof.f.mjs — the exact literal that defeats regex normalisers — and both directions correctly reported DIFFERENT while the unmutated pair reported IDENTICAL.
    • The one difference is fjs/effects/node/proof.f.mjs, and it is the disclosed hoist: listEmpty() / listNonEmpty(…) moved out of the writeFromStream argument into preceding /** @type {List<never, IoResult<Vec>>} */ const chunks = … declarations. Both are pure constructors, so the evaluation-order shift is inert, and the CHANGELOG names it.
  • Casts, mutation-probed (clean tree, so nothing resolved through emitted .d.mts; baseline tsc confirmed clean first):
    • @type {List<never, IoResult<Vec>>} const chunks= 42 gives TS2322. Real annotation, still checked.
    • match(/** @type {OperationMap<ReadFile, IoResult<Vec>>} */ ({…})) with a wrong readFile return gives TS2352. Value types still checked.
    • mockRun(/** @type {Parameters<…>[0]} */ ({…})) with a wrong await return gives TS2352.
  • Link check. Broken relative link set went 141 → 140 versus main: nothing stranded, and fjs/media/json/todo/remove-native-json.md's fjs/text/utf16/module.f.ts reference was fixed along the way. At the earlier fd329dd3 head this was 141 → 142, because the fjs/djs/examples migration had stranded fjs/djs/README.md:22./examples/input.f.ts; 6e84d20b fixed it before I could report it.
  • CHANGELOG §8.3. Additions only, Unreleased only, no released section rewritten, **BREAKING CHANGES:** prefix on every migration entry, and the sole link in all added lines is /pull/1505 — no issue or todo/ links. The condensed entries are still accurate; I checked the tarball claim specifically, and npm pack --dry-run at this head does ship fjs/djs/examples/{input,m}.f.mjs plus their .d.mts, which .f.ts did not.
  • Rust and generator gates skipped: nanvm-lib/ and fjs/nanvm/ are untouched.
  • npm run cov is unverifiable here — it reports a vacuous 100.00 over 0 tests on main too (Node v23), so it is the environment, not this PR.

One non-blocking nit

fjs/effects/node/proof.f.mjs:22 — wrapping the operation map in a @type cast turns assignability into comparability, which keeps value-type checking (the TS2352 probe above) but drops excess-property checking. An unknown operation key now type-checks:

  • on origin/main, inserting bogusOp: 1 into match<ReadFile, IoResult<Vec>>({…})TS2353: Object literal may only specify known properties, and 'bogusOp' does not exist in type 'OperationMap<ReadFile, IoResult<Vec>>'
  • at this head, inserting it into the cast → tsc --noEmit clean

This is a narrow loss on a four-line, one-key map in a proof, and it matches existing house practice — mockRun in fjs/emergent_testing/proof.f.ts already used as Parameters<…>[0] on main and is equally lax there, so that half is pre-existing, not a regression. Not worth holding the PR for. If you want it back, the pattern you already used for chunks in this same file restores it; I confirmed both halves compile as expected:

/** @type {OperationMap<ReadFile, IoResult<Vec>>} */
const readHelloMap = {
    readFile: path => {
        assertEq(path, 'hello')
        return ok(vec8(0x15n))
    },
}
const readHello = match(readHelloMap)

That form is tsc-clean as written, and re-adding bogusOp: 1 to it brings TS2353 back.

Nothing else outstanding.

A `@type` cast checks comparability, not assignability, so it drops
excess-property checking: `bogusOp: 1` in the operation map compiled
clean, where main's match<ReadFile, IoResult<Vec>>({...}) rejected it
with TS2353.

Binding the literal to an annotated const restores that -- the same
declaration form already used for `chunks` in this file, and the form
AGENTS.md section 6 prefers over an inline cast. Verified: `bogusOp: 1`
is TS2353 again, and a wrong handler return is now TS2322
(assignability) rather than TS2352 (comparability), so value checking
is strictly stronger than before too.

Reported by o2alexanderfedin as a non-blocking nit; taking it because
it recovers a real check for four lines of change.
@sergey-shandar sergey-shandar changed the title Migrate the last ten proof modules to .f.mjs Migrate the last 12 authored .f.ts files to .f.mjs — completes stage 1 Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

Thanks — I took the nit rather than leaving it, in 09f54fd.

I reproduced the loss before changing anything: bogusOp: 1 in the cast form compiled clean at 6e84d20b, exactly as you found. Binding the literal to /** @type {OperationMap<ReadFile, IoResult<Vec>>} */ const readHelloMap restores TS2353, and it also strengthens the value check — a wrong handler return is now TS2322 (assignability) where the cast gave TS2352 (comparability). So this recovers a check and tightens another for four lines, which is worth it even as a non-blocker. It's also the form AGENTS.md §6 asks for over an inline cast, so the cast was the wrong reach on my part; the chunks binding two functions down was already the right pattern and I should have used it here.

I left the mockRun cast in emergent_testing alone, as you noted: main already used as Parameters<…>[0] there, so tightening it would be a change beyond the migration rather than a restoration. Happy to do it separately if you want the excess-property check there too.

Also updated the title — you're right that "the last ten" undercounts. It's 12 authored .f.ts files, and find . -name '*.f.ts' now returns 0, so the title now says so.

tsc, prepack and npm test (2495/0) clean at 09f54fd. The diff from 6e84d20b is that one file plus a one-word CHANGELOG correction ("@type casts or checked declarations" → "checked @type declarations"), since the cast it referred to is gone.


Generated by Claude Code

@sergey-shandar
sergey-shandar added this pull request to the merge queue Aug 12, 2026
Merged via the queue into main with commit 76dff87 Aug 12, 2026
19 checks passed
@sergey-shandar
sergey-shandar deleted the claude/convert-f-ts-to-f-mjs-3 branch August 12, 2026 22:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants