Skip to content

Ship declarations only: drop the JS emit pass from prepack - #1520

Merged
sergey-shandar merged 18 commits into
mainfrom
claude/ts-js-migration-strategy-jpz3bm
Aug 13, 2026
Merged

Ship declarations only: drop the JS emit pass from prepack#1520
sergey-shandar merged 18 commits into
mainfrom
claude/ts-js-migration-strategy-jpz3bm

Conversation

@sergey-shandar

Copy link
Copy Markdown
Contributor

What

prepack now runs only the declaration emit:

"prepack": "tsc --noEmit false --emitDeclarationOnly"

The second pass (tsc --noEmit false --declaration false) is removed. Its entire output was 96 dead files: 85 empty export {} stubs compiled from the type-only types.ts modules, plus 11 compiled test files (all.test.js, scenarios/*.js) that nothing imports — the test fixtures run natively from their authored .ts. The packed tarball now contains zero .js files; files in package.json is intentionally unchanged.

Related TODO files (todo/migrate-typescript-to-mjs.md, fjs/ci/todo/f-mjs-package-support.md) are updated to record the measurements below and check off the items this settles.

Why it's safe

Nothing resolves the removed files, at type level or at runtime:

  • No shipped .mjs has a runtime import of any .js path; all types.ts references are import type (erased under verbatimModuleSyntax) or JSDoc @import comments.
  • The shipped .d.ts/.d.mts files keep their …/types.ts specifiers, and consumers resolve those to the shipped types.d.ts — the published package has never contained .ts files, so this substitution path was already load-bearing.

Verification

All checks ran against the exact tarball produced by the new prepack (0 .js; 85 types.d.ts, 250 .d.mts, 250 .mjs), installed in a clean consumer importing module.f.mjs at runtime and types.js-specifier types:

Consumer Result
tsc 5.9.3, nodenext + strict, default settings type-checks
tsc 7.0.2, same config type-checks
Node v22 (--experimental-strip-types) runs
fjs CLI bin from the installed package runs
Bun 1.3.11 bun run and bun build (Node target) runs; type-only imports erased, not resolved
Deno 2.9.5 deno run and deno check (node_modules resolution) runs and type-checks

Negative control: a deliberate List<number> = 'not a list' misuse is rejected (TS2322) by both deno check and tsc, proving the types genuinely resolve through .d.mtstypes.ts specifier → types.d.ts rather than falling back to any. This also corrects the assumption recorded in the TODO files that Deno does not perform this substitution — Deno 2.9.5 (TypeScript 6.0.3) does.

Repo test suite: 2522/2522 pass.

Note for publishing: **/*.js stays in files, so publishes must come from a clean checkout (as CI does) — a working tree with stale output from the old prepack would ship it.


Generated by Claude Code

The second tsc pass only produced empty `export {}` stubs from the
type-only `types.ts` modules plus compiled test files, none of which
anything imports at runtime. Consumers resolve the `types.ts` specifiers
in shipped `.d.ts`/`.d.mts` files to `types.d.ts`, so the package works
identically without any emitted `.js` (verified against TypeScript 5.9
and 7.0 under nodenext, and at runtime under Node).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GkqgqTaffDpYFQEJydpqHF
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 13, 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 b5017f0 Commit Preview URL

Branch Preview URL
Aug 13 2026, 04:13 PM

Check off the settled items: generated types.js is not required for
portable resolution, the JavaScript-emitting tsc pass is removed, and
the package emit path is simplified to declaration-only. Correct the
recorded assumption that Deno does not substitute .ts specifiers with
.d.ts files — Deno 2.9.5 does, measured against the packed tarball with
a negative control. Link the changelog entry to the pull request.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GkqgqTaffDpYFQEJydpqHF
Record the consumer sources, the per-runtime install-from-tarball
commands, and the negative control used by #1520, all re-verified with
real installers: npm install <tgz> for Node, bun add <tgz> for Bun, and
Deno running on top of the installed node_modules. Also record the newly
measured boundary of Deno's .ts -> .d.ts substitution: it applies to
packages resolved as npm packages through node_modules, but not to
file:-linked directories, which Deno treats as first-party source — and
a file: tarball dependency does not install at all. Refine the earlier
Deno notes in the migration TODO files accordingly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GkqgqTaffDpYFQEJydpqHF

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

Verified at head 36b5f2f4 (the head moved from dfd97055 to 36b5f2f4 mid-review; everything below was re-run at 36b5f2f4, and the added todo/1520-packed-consumer-validation.md changes none of the results).

The measurement work here is genuinely good, and the central claim holds: I reproduced it independently rather than taking it on trust. Two things I think need attention before merge, one of them substantive.

What I verified (all clean)

  • npx tsc --noEmit → exit 0. npm run prepack from a git cleaned tree → exit 0.
  • npm test → 2522/2522. That equals the merge-base (7ae03f04) count, so the PR changes nothing. (main is 2524 because this branch predates #1519 okThen (+1) and #1518 (+1).)
  • Packed contents, before/after. npm pack --dry-run on main (695 files) vs this branch (599). The set difference is exactly 96 files, all .js, and nothing is added. The 85 types.js really are inert: stripping comments and export {} from each leaves an empty string in all 85. The other 11 are the emergent_testing compiled fixtures.
  • Nothing resolves them. Across the 346 emitted .d.mts/.d.ts: 801 …/types.ts specifiers (matching the count in the TODO), 0 .js specifiers. 0 runtime .js imports in any shipped .mjs. scenarios/run.sh links the authored .ts fixtures directly. bin.fjsfjs/module.mjs, still shipped. The only CI steps that could care are npx tsc and npm pack.
  • Packed tarball actually works. Packed this branch, installed the tarball into an empty directory, and: parse/stringify round-trip runs on Node; npx fjs runs from the installed package; tsc 5.9.3 nodenext+strict type-checks a consumer importing …/types.js-specifier types → exit 0. Negative control: const bad: List<number> = 'not a list' and a function assigned to Unknown are both rejected with TS2322, so the .d.mts./types.ts → shipped types.d.ts substitution is real and not an any fallback. That is the PR's core claim, confirmed.
  • Public type surface: byte-identical to main. Const surface: the only difference is /fjs/types/result::okThen, present on main and absent here — that is #1519, which this branch predates, not a regression.
  • Broken-link sets: identical to main (162 lines, same entries), despite the many links added to the TODO files.
  • CHANGELOG: entry sits in ## Unreleased, links only /pull/1520, no released section touched. npm run ci-update + git add -A && git diff --cached --exit-code → clean.

1. Dropping pass 2 removes the repo's only declaration-emit round-trip check

This is the part I would not merge without a decision on. Pass 2 emitted 96 dead files, yes — but that was not all it did. Because pass 2 runs after pass 1 has written .d.mts/.d.ts into the tree, TypeScript resolves every cross-module import through the emitted declarations rather than through the authored source. That made prepack the one place in the repo where a type that survives source checking but degrades in declaration emit gets caught.

AGENTS.md §6.2 documents exactly this failure mode and says of it: "Only a consumer type-checking against the published .d.mts sees the difference, which is why this needs to be a rule rather than something review catches." Pass 2 was the mechanism that made it more than a rule.

Reproduced, using §6.2's own counter-example — replacing the typeof-cross-referencing @type on fjs/media/json/rtti/module.f.mjs's unknown with @type {const}, one edit, nothing else:

tree command result
main npx tsc --noEmit exit 0
main npm run prepack (two passes) exit 2fjs/media/json/types.ts(29,24): error TS2344: Type 'false' does not satisfy the constraint 'true'
this branch npx tsc --noEmit exit 0
this branch npm run prepack (one pass) exit 0, and the emitted module.f.d.mts contains /*elided*/ any

The error on main is the Assert<Equal<Unknown, Ts<typeof unknown>>> pin in fjs/media/json/types.ts — the very "round-trip assert" §6.2 tells you to pair the annotation with — evaluated against the emitted declaration. Negative control both ways: with the tree unmutated, both prepack variants exit 0.

So after this PR, that defect class reaches a release silently: npm test, npx tsc, and CI all pass. It is the class that closed #1497 twice.

The good news is the fix is one line and costs nothing the old pass 2 did not already cost. The property comes from re-checking with declarations present, not from emitting JavaScript, so:

"prepack": "tsc --noEmit false --emitDeclarationOnly && tsc --noEmit"

Verified on this branch: with the same mutation, that second invocation reports the identical TS2344 at fjs/media/json/types.ts(29,24) and exits non-zero — while still emitting zero .js, so the tarball stays exactly as this PR makes it (I re-confirmed 599 files / 96 .js removed). That keeps everything the PR is actually after and gives up nothing.

If you would rather not pay a second tsc in prepack, that is a legitimate call — but then it is worth saying so out loud in todo/migrate-typescript-to-mjs.md, because the TODO currently records the pass as removed on the grounds that its output was dead, and its output was never the whole story.

2. …/types.js runtime specifiers now fail — the "nothing imports those modules at runtime" claim is too strong

The PR says "No shipped .mjs has a runtime import of any .js path; all types.ts references are import type (erased under verbatimModuleSyntax)". That is true of this repo's sources. It is not true of every consumer, and the exception is the repo's own tsconfig setting:

Under verbatimModuleSyntax: truetsconfig.json:6 here — an inline type modifier is not erased. import { type Unknown } from 'functionalscript/fjs/media/json/types.js' compiles to a retained import {} from 'functionalscript/fjs/media/json/types.js'. Only import type { … } is elided entirely.

Measured with one compiled consumer file, swapping only the installed tarball:

  • against main's tarball → node out/app.mjs prints ok { a: 1 }, exit 0
  • against this branch's tarball → ERR_MODULE_NOT_FOUND … /fjs/media/json/types.js, exit 1

import * as t from '…/types.js' behaves the same way: resolves to a 0-export module on main, throws here.

Nothing of value was in those modules — they export nothing — so this is a break in shape only, and I do not think it is worth reverting for. But it is an observable runtime behaviour change in the published package, which is what §8.4 is about. Either the **BREAKING CHANGES:** prefix, or a clause in the CHANGELOG entry saying that …/types.js specifiers must be written import type (fully erased) and not import { type X }, would make it honest. The new todo/1520-packed-consumer-validation.md already uses import type throughout — worth stating there that this is load-bearing, not stylistic.

3. fjs/fsc/README.md still forbids what this PR does

The PR carefully retires the "do not simplify the emit pipeline yet" instruction in todo/migrate-typescript-to-mjs.md, fjs/ci/todo/f-mjs-package-support.md and todo/fjs-nanvm-integration.md, but the same instruction is still live in two places it did not touch:

  • fjs/fsc/README.md:118-119 — "Do not simplify the TypeScript runtime-emission pass until that experiment establishes whether generated types.js remains necessary."
  • fjs/fsc/README.md:192-193 — "do not assume the TypeScript runtime-emission pass can disappear while package resolution may still require a generated types.js."

Both now describe a question this PR answered. Docs-only, but fjs/fsc/README.md is the file a future reader consults first, and leaving it contradicting the other three is how the next change gets talked out of something that is already settled.

What I could not verify

Deno and Bun: not installed here, so the Deno 2.9.5 / Bun 1.3.11 rows of your table are unreproduced on my side. TypeScript 7.0.2 likewise — I used 5.9.3 for the clean-consumer check. The TypeScript-side conclusion is confirmed independently; the Deno/Bun conclusions rest on your measurement alone.

claude and others added 4 commits August 13, 2026 10:40
todo/ tracks issues; this is durable reference documentation, so it
lives beside the CI sources and the package-support issue that will
eventually turn it into a fixture. Links updated, and the issue-style
status header replaced with plain prose.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GkqgqTaffDpYFQEJydpqHF
Root-level developer documentation beside README.md and CONTRIBUTING.md,
named PACKAGE_VALIDATION.md to match the root doc style. Linked from
CONTRIBUTING.md's testing section; all references updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GkqgqTaffDpYFQEJydpqHF
Developer docs live near their corresponding README.md; ci is the scope
that owns packaging. Keep the CONTRIBUTING.md pointer, now into fjs/ci/.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GkqgqTaffDpYFQEJydpqHF

@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 8a6960e3 (the head moved twice while I was working — 3041f573, then 45a1c40a, then 8a6960e3; everything below was re-run against 8a6960e3 from clean trees).

The new fjs/ci/packed-consumer-validation.md is a genuinely good addition — the four-file consumer, the bad.ts negative control, and the Deno file: caveats are all things I could not have derived from the diff. Thank you for writing the measurement down.

The baseline gates are green:

  • npx tsc --noEmit → 0
  • npm test → 2524 pass / 0 fail, exactly matching origin/main (2524)
  • npm run prepack from a clean tree → 0
  • npm pack --dry-run → 599 files vs 695 on main; the file-set diff is exactly the 96 types.js stubs and nothing else
  • broken-link sets identical to main (140 both, empty set diff)

The three findings from 36b5f2f4 are unfortunately all still open. I re-ran each rather than assuming.

1. The declaration round-trip check is still gone (substantive)

prepack is now a single --emitDeclarationOnly pass, so nothing in the repo compiles against the declarations it just emitted. I re-ran the AGENTS.md §6.2 counter-example against this head — swapping fjs/media/json/rtti/module.f.mjs's unknown from the typeof cross-reference to /** @type {const} */, which §6.2 documents as the exact defect that produces /*elided*/ any:

this branch origin/main
npx tsc --noEmit 0 0
npm run prepack (clean tree) 0 2fjs/media/json/types.ts(29,24): error TS2344: Type 'false' does not satisfy the constraint 'true'
emitted module.f.d.mts contains /*elided*/ any n/a, build failed

So the branch packs and publishes a degraded declaration silently. This is the same class of defect as #1497, and prepack was the only gate that caught it.

The one-line fix I suggested last time still works and I re-verified it on this head — replace the dropped second pass with a check-only one:

"prepack": "tsc --noEmit false --emitDeclarationOnly && tsc --noEmit"

With the mutation in place that exits 1 with the same TS2344; without it, prepack exits 0 and npm pack produces the identical 599-file tarball. It costs one type-check and gives back the round-trip. (The --noEmit second pass resolves the .d.mts files the first pass just wrote, which is where the check lives — it does not re-emit any .js, so it does not undo what this PR is for.)

I'd be glad to be talked out of this if there's a reason the second pass has to emit, but as it stands the check is not replaced by anything — packed-consumer-validation.md is a manual procedure, and its own bad.ts control tests type resolution, not declaration fidelity, so it would not have caught the mutation above either.

2. Consumers using an inline type import break at runtime, and the CHANGELOG says the opposite

The entry states "nothing imports those modules at runtime". That holds for the package's own internals, but not for consumers. Under verbatimModuleSyntax (which this repo itself sets, so it is a natural thing for a consumer to copy), the inline form keeps the specifier:

import { type List } from 'functionalscript/fjs/types/list/types.js'
import { length } from 'functionalscript/fjs/types/list/module.f.mjs'
const l: List<number> = [1, 2, 3]
console.log('ran', length(l))

emits import {} from 'functionalscript/fjs/types/list/types.js'. I installed both tarballs into identical consumer directories:

  • tsc exits 0 against both packages
  • against main's tarball: prints ran 3
  • against this branch's tarball: Error [ERR_MODULE_NOT_FOUND]: Cannot find module '…/functionalscript/fjs/types/list/types.js'

Worth noting that packed-consumer-validation.md's test.ts uses import type { List } from …, the top-level form, which erases the specifier entirely — so the documented procedure passes on this branch precisely because it doesn't exercise the case that breaks. Adding the inline-type line to that fixture would make the boundary visible.

Per §8.4 this wants the **BREAKING CHANGES:** prefix, or at minimum an explicit note in the entry that consumers must use top-level import type for types.js specifiers. The removal itself seems right to me — it's the silence about it that's the problem.

3. fjs/fsc/README.md still forbids exactly this change

Three TODO files were updated with the measurements, but fjs/fsc/README.md was not:

  • :118 — "Do not simplify the TypeScript runtime-emission pass until that experiment establishes whether generated types.js remains necessary."
  • :191-193 — "Remove obsolete generated runtime outputs only as allowed by the package-support fixture; do not assume the TypeScript runtime-emission pass can disappear while package resolution may still require a generated types.js."

The first is arguably now satisfied — the experiment was run and written up, which is the precondition it names — but it reads as a live prohibition and should record the outcome. The second is not satisfied: it gates the removal on the package-support fixture, and packed-consumer-validation.md opens by saying it is a stand-in "until a committed CI fixture … replaces it". This is the lowest-severity of the three, purely a docs-consistency matter, but it leaves the repo's own guidance contradicting its state.

What I did not verify

Rust gates (nanvm-lib/ untouched) and the generator round-trip (fjs/nanvm/ untouched). I did not re-run the Deno and Bun arms of packed-consumer-validation.md; the Node arm and both tarball comparisons above are all first-hand.

…s of types modules

Review on #1520 caught that the removed second tsc pass was not only dead
output: running tsc with declarations present re-checks every .mjs import
through the emitted .d.mts, which is what made the Assert<Equal<...>>
round-trip pins evaluate against what consumers see. Restore that
property without the output: prepack now emits declarations, then runs
tsc --noEmit against them. Reproduced both ways with the AGENTS.md 6.2
counter-example: the seeded defect passes one-pass prepack silently and
fails the re-check with TS2344.

Review also measured that under verbatimModuleSyntax the inline form
`import { type X } from '.../types.js'` retains a runtime import and now
fails with ERR_MODULE_NOT_FOUND. Make the fully erased `import type` the
rule: stated in AGENTS.md 6.2, the changelog entry, and the packed
consumer validation doc.

Also retire the two passages in fjs/fsc/README.md that still forbade
simplifying the emission pass, and amend the TODO records that described
the pass as dead output only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GkqgqTaffDpYFQEJydpqHF

Copy link
Copy Markdown
Contributor Author

All three review points are addressed in 2bc0b0a. Thank you for the independent reproduction — point 1 in particular caught something the PR's own analysis missed: I had evaluated pass 2 by its output alone, and the output was never the whole story.

1. Round-trip check restored, your one-liner adopted verbatim. prepack is now tsc --noEmit false --emitDeclarationOnly && tsc --noEmit. I reproduced your experiment before adopting it: the seeded §6.2 counter-example (@type {const} on rtti unknown) passes the one-pass prepack silently and fails the re-check with the identical TS2344 at fjs/media/json/types.ts(29,24); the clean tree passes both invocations, and the tarball still contains zero .js (re-verified via npm pack after the change). The TODO records that described the pass as "dead output, removed" are amended to record the re-check property and why it is kept.

2. Documented as a rule rather than marked breaking. The decision: the fully erased import type { X } from '…/types.js' (and JSDoc @import) are the only permitted forms — the inline import { type X }, import * as, and bare side-effect imports all retain a runtime import of a module that no longer exists. Stated in three places: AGENTS.md §6.2 (the consumer rule, with the verbatimModuleSyntax mechanism), the CHANGELOG entry (so a consumer hitting ERR_MODULE_NOT_FOUND finds the explanation where they'll look first), and fjs/ci/packed-consumer-validation.md (that import type in the consumer sources is load-bearing, not stylistic — your exact suggestion).

3. Both fjs/fsc/README.md passages updated (former lines 118–119 and 192–193): each now records that the experiment ran, what it concluded, and that prepack keeps the no-emit re-check.

On the part you could not verify: the Deno/Bun measurements are recorded with exact versions and commands in fjs/ci/packed-consumer-validation.md, including the negative control and two Deno caveats found along the way (a file: tarball dependency fails to install, and a file:-linked directory resolves at runtime but fails deno check — substitution applies only to packages resolved as npm packages through node_modules).


Generated by Claude Code

claude added 2 commits August 13, 2026 11:22
Measured against the packed tarball: every type checker accepts the
inline form, and the runtime outcome depends on whether the toolchain
elides the emptied import statement (tsc default, Bun) or keeps it
(verbatimModuleSyntax, Node type stripping, Deno). Document the matrix
and the rule that types.js is not a real module and must only be
imported with the fully erased `import type`.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GkqgqTaffDpYFQEJydpqHF
The forbidden inline import forms are a one-time documented measurement
in packed-consumer-validation.md: their behavior belongs to consumer
toolchains, not to this package, so the CI fixture tracked in
f-mjs-package-support.md need not re-test them every run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GkqgqTaffDpYFQEJydpqHF

@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 65930588 (head moved from 2bc0b0a4 mid-review; package.json is
byte-identical between the two and the delta is docs-only, but the counter-example below
was re-run at 65930588 itself rather than carried over).

The substantive finding is resolved

prepack is now tsc --noEmit false --emitDeclarationOnly && tsc --noEmit, and the second
invocation genuinely restores the round-trip property. Re-ran AGENTS.md §6.2's own
counter-example at this head — replacing unknown's explicit @type with
() => /** @type {const} */([...]) in fjs/media/json/rtti/module.f.mjs:

step (clean tree, seeded defect) result
npx tsc --noEmit exit 0 — nothing else catches it
pass 1 alone (--emitDeclarationOnly) exit 0, emits 1 /*elided*/ and 6 any
npm run prepack (both passes) exit 1, fjs/media/json/types.ts(29,24): error TS2344

Negative control: unmutated tree, same commands, prepack exits 0. So the check fails on
the defect and passes on clean source — the property that closed #1497 twice is back, without
the JS output. This is the right shape of fix.

Rest of the battery at 65930588, all clean:

  • npx tsc --noEmit exit 0; npm run prepack from a freshly cleaned tree exit 0.
  • npm test: 2524 pass / 0 fail, exactly matching the merge-base (e51530dd) 2524/0.
  • Packed tarball vs merge-base: 695 → 599 files. The 96 removals are 85 types.js stubs
    and 11 fjs/emergent_testing/** scenario .js; nothing shipped imports the latter. Every
    one of the 599 remaining files is byte-identical to main, declarations included — so the
    public type/const surface is provably unchanged, which is stronger than a surface diff.
    Only package.json differs.
  • Broken relative links: identical sets, 140 on both trees; 0 extension mismatches. No source
    files are touched by this PR, so there is no new export type to prefix-check.
  • Spot-checked two rows of the new matrix in packed-consumer-validation.md against the
    packed tarball: Node --experimental-strip-types does give ERR_MODULE_NOT_FOUND, and tsc
    with default elision does drop the import and run. The matrix is accurate, and the doc now
    covers the case its fixture cannot observe — that half of the earlier finding is settled.

Still open: §8.4 prefix on the CHANGELOG entry

Reproduced again at this head. Same consumer source, verbatimModuleSyntax: true,
type-checks clean against both packages:

import { length } from 'functionalscript/fjs/types/list/module.f.mjs'
import { type List } from 'functionalscript/fjs/types/list/types.js'

Compiled output is identical on both (import {} from '…/types.js' retained). Against the
main tarball it prints ok 3; against this branch's tarball it throws ERR_MODULE_NOT_FOUND.
The entry has no **BREAKING CHANGES:** prefix.

The entry itself spends four lines telling consumers which import forms they must stop
writing, which is the definition of a breaking change, and the repo already treats
specifier-level changes this way — 0.44.0 carries
"BREAKING CHANGES: fjs/djs/examples/input.f.ts … migrate to .f.mjs. Being .mjs they
now ship in the tarball, which .f.ts did not". This PR removes 96 specifiers from the
tarball; that is the same class, one direction over.

To be fair about the size of it: ## Unreleased already contains the #1516 breaking entry,
so the next release is 0.45.0 either way and no consumer on ^0.44.0 is exposed. The
practical cost is only that a reader scanning for the marker to find what breaks them misses
the one entry that would break their build at runtime. One-word fix.

Minor: one TODO still prescribes the removed pass

fjs/ci/todo/publishing-packages.md (status open), under "Stage-1 emission", still gives
the two-pass command as current instruction:

Use the two ordered TypeScript passes directly in prepack while any .ts / .f.ts source
remains

followed by the literal
"prepack": "tsc --noEmit false --emitDeclarationOnly && tsc --noEmit false --declaration false"
and "A separate runtime-emission configuration is therefore unnecessary…". fjs/fsc/README.md
:118 and :191-193 were correctly retired, and f-mjs-package-support.md,
migrate-typescript-to-mjs.md and fjs-nanvm-integration.md were all amended — this one
looks like it was simply missed, and it is the only remaining place a reader is told to write
a prepack that no longer matches package.json.

Nit, take it or leave it: the paragraph added at AGENTS.md:614-621 has no blank line after
it, so the pre-existing sentence "A declaration-only module belongs in types.ts …" — which
used to close the "Both forms are type-only" paragraph — now renders as part of the new
ERR_MODULE_NOT_FOUND paragraph.

Comment thread package.json Outdated
"description": "FunctionalScript is a purely functional subset of JavaScript",
"scripts": {
"prepack": "tsc --noEmit false --emitDeclarationOnly && tsc --noEmit false --declaration false",
"prepack": "tsc --noEmit false --emitDeclarationOnly && tsc --noEmit",

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Suggested change
"prepack": "tsc --noEmit false --emitDeclarationOnly && tsc --noEmit",
"prepack": "tsc --noEmit false --emitDeclarationOnly && tsc",

Comment thread CHANGELOG.md Outdated

## Unreleased

- The npm package no longer ships `.js` files compiled from the type-only

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I would consider it a BREAKING CHANGE.

claude added 2 commits August 13, 2026 11:35
The one-time measured validation in #1520 is sufficient for the
migration: mark the two prerequisite completions and the fixture
validation done in todo/migrate-typescript-to-mjs.md, and note in both
companion files that the committed CI fixtures remain future regression
infrastructure on their own schedule, no longer gating anything. The
test-and-coverage fixture drops from P1 to P2 accordingly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GkqgqTaffDpYFQEJydpqHF
The prepack re-check drops the redundant --noEmit flag per review
suggestion (tsconfig.json already sets noEmit); verified the seeded
declaration defect still fails with the same TS2344 and a clean tree
still passes. The changelog entry gets the BREAKING CHANGES prefix the
convention requires for an observable runtime change in the published
package. publishing-packages.md, the last file still prescribing the
removed two-pass prepack, now records the outcome, and the AGENTS.md
paragraph break lost in the previous edit is restored.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GkqgqTaffDpYFQEJydpqHF

Copy link
Copy Markdown
Contributor Author

Round 3 addressed in 3b1db81:

  • prepack re-check simplified per @sergey-shandar's suggestion to tsc --noEmit false --emitDeclarationOnly && tsctsconfig.json already sets noEmit, so the flag was redundant. Re-verified at this head: the seeded §6.2 defect fails the plain-tsc second invocation with the same TS2344 at fjs/media/json/types.ts(29,24), a clean tree passes both invocations, and the tarball still contains zero .js. The TODO records quoting the command are synced.
  • **BREAKING CHANGES:** prefix added to the CHANGELOG entry per @sergey-shandar's review comment — which also settles @o2alexanderfedin's §8.4 point. The entry keeps the import-form guidance so a reader scanning for the marker finds both what breaks and what to write instead.
  • fjs/ci/todo/publishing-packages.md updated — it was indeed simply missed; its "Stage-1 emission" section now records the two-pass command as history and shows the current prepack with the reasoning.
  • AGENTS.md paragraph break restored — the "A declaration-only module belongs in types.ts…" sentence is its own paragraph again instead of merging into the ERR_MODULE_NOT_FOUND paragraph.

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: 3b1db81f0a

ℹ️ 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 on lines +98 to +100
that no generated `.js` is required and replaced the second pass with a plain
check, which re-resolves the tree through the just-emitted declarations and so
keeps the declaration round-trip property:

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 Synchronize the rest of the packaging design

Update the remainder of this design to match the newly documented no-emit second pass. The same section still says that the second invocation emits source.js (lines 121–128), then instructs maintainers to replace the two-pass command with a declaration-only invocation (lines 148–157 and 179–180). That is now incorrect: tsconfig.json sets noEmit: true, and tsc --help --all defines --noEmit as “Disable emitting files from a compilation,” so following the stale instructions would remove the declaration round-trip check that these added lines explicitly preserve.

AGENTS.md reference: AGENTS.md:L389-L396

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 287648f — the section's prose now describes the second invocation as the no-emit declaration round-trip check (with the old emitting form recorded as history), the "reduce prepack to bare --emitDeclarationOnly" instruction is replaced with an explicit warning that doing so would drop the only gate for declaration-emit degradation, and the two completed tasks record their outcomes.


Generated by Claude Code

…it check

The previous commit updated the prepack snippet but left the section's
prose describing the second invocation as emitting source.js, and left
later instructions telling maintainers to reduce prepack to the bare
declaration-only command — which would silently remove the declaration
round-trip check. The section now describes the check, warns against
dropping it, and the two completed tasks record their outcomes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GkqgqTaffDpYFQEJydpqHF
@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: 287648f7d2

ℹ️ 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 package.json
"description": "FunctionalScript is a purely functional subset of JavaScript",
"scripts": {
"prepack": "tsc --noEmit false --emitDeclarationOnly && tsc --noEmit false --declaration false",
"prepack": "tsc --noEmit false --emitDeclarationOnly && tsc",

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 Keep the external-runner entry point in the package

For consumers using the documented external-runner flow, this removes the only step that creates fjs/emergent_testing/all.test.js: its source is all.test.ts, the package excludes .ts, and the declaration pass produces only all.test.d.ts. Both fjs/emergent_testing/README.md and fjs/ci/README.md still instruct Node, Bun, and Deno projects to import that .js entry, so those runners now fail with module-not-found before registering any proofs. Retain a packaged runtime entry or cut every documented consumer over to a replacement in this change.

AGENTS.md reference: AGENTS.md:L365-L368

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Real catch — one of the 11 "dead" compiled test files was not dead: nothing inside the tarball imports all.test.js, but the documented external-runner flow has consumers side-effect-import it from their own entry file, which is exactly the blind spot of the earlier "nothing resolves them" analysis.

Fixed in 2b7e138 by shipping a runtime entry instead of resurrecting the compiled one: authored fjs/emergent_testing/register.mjs carries the same two .mjs imports and top-level await; the repository's all.test.ts now delegates to it (single registration source), both READMEs and the **BREAKING CHANGES:** entry point consumers at the new specifier. Verified end-to-end against the packed tarball: a consumer all.test.ts containing import 'functionalscript/fjs/emergent_testing/register.mjs' loads under node --test, bun test, and deno test with no module-not-found; in-repo node --test fjs/emergent_testing/all.test.ts still passes 2467/0 through the delegation.


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.

Approving. All three findings from the 65930588 round are resolved, and I
re-derived each one rather than reading the diff for reassurance.

Head note: I ran the battery at 3b1db81f, and the head moved to
287648f7 while I was writing this up (fifth round running). The delta is
docs-only — fjs/ci/todo/publishing-packages.md alone — so I re-ran the parts
that could be affected at 287648f7: npx tsc --noEmit 0, npm run prepack
from clean 0, npm test 2531/0, the §6.2 counter-example mutation, the stale
prescription grep, and the link-check. Everything below holds at 287648f7.

1. prepack — resolved, and the round-trip gate re-confirmed

"prepack": "tsc --noEmit false --emitDeclarationOnly && tsc". The second
invocation inherits noEmit: true from tsconfig.json, so it is a pure check.

Re-ran the AGENTS.md §6.2 counter-example at this head — pinning the recursive
rtti tuple with a const cast:

export const unknown = () => /** @type {const} */ (['or', primitive, object, array])
step result
npx tsc --noEmit on clean sources exit 0 — the degradation is invisible here
pass 1 alone (--emitDeclarationOnly) exit 0, emits /*elided*/ ×2 and any ×7 into fjs/media/json/rtti/module.f.d.mts
pass 2 (plain tsc, declarations present) exit 1fjs/media/json/types.ts(29,24): error TS2344: Type 'false' does not satisfy the constraint 'true'
full npm run prepack from clean+mutated exit 1, same error

Unmutated, npm run prepack from a clean tree exits 0. So the pass still earns
its place: the Assert<Equal<Unknown, Ts<typeof unknown>>> pin only bites once
the .d.mts outranks the .mjs in resolution, and nothing else in the repo
catches it. Good to see that reasoning now written into
publishing-packages.md as an explicit "do not reduce prepack to the bare
--emitDeclarationOnly invocation" — the task list previously prescribed
exactly the reduction that would have removed the gate.

2. **BREAKING CHANGES:** prefix — resolved

The prefix is there, and §8.3 is otherwise clean: Unreleased, one /pull/1520
link, no issue or todo/ link, no released section touched. 659 chars, within
the house shape for this series.

I re-verified the break itself end-to-end rather than taking the previous
round's word for it. Packed both trees with npm pack from clean checkouts and
diffed the tarballs:

main:    695 entries, 96 .js, 96 .d.ts, 250 .mjs, 250 .d.mts
this PR: 599 entries,  0 .js, 96 .d.ts, 250 .mjs, 250 .d.mts

diff -rq over the two unpacked packages reports the 96 .js files and
package.json — nothing else. Of the 96: 85 are types.js reducing to
export {}; after comments, and 11 are fjs/emergent_testing scenario
fixtures. That is exactly what the CHANGELOG says, and nothing shipped imports
the fixtures (run.sh consumes the authored .ts).

Installed each tarball into an identical clean consumer (nodenext, strict,
no allowImportingTsExtensions) and ran the matrix:

consumer main tarball this PR
import type { List } from '…/types.js' — tsc passes passes
same, plus verbatimModuleSyntax passes passes
same, node --experimental-strip-types runs runs
bad.ts negative control TS2322 TS2322
import { type List } — tsc / tsc+vms passes passes
↳ compiled under verbatimModuleSyntax, then node runs ERR_MODULE_NOT_FOUND
↳ compiled under default elision, then node runs runs
node --experimental-strip-types directly runs ERR_MODULE_NOT_FOUND
import * as T under vms, then node runs ERR_MODULE_NOT_FOUND
bare side-effect import, node runs ERR_MODULE_NOT_FOUND

The verbatimModuleSyntax row is the specifier-level break, and it is as clean
a demonstration as one gets: the compiled JS is byte-identical
(import {} from 'functionalscript/fjs/types/list/types.js';) and only the
tarball under it differs. **BREAKING CHANGES:** is right.

The bad.ts row matters too — List<number> = 'not a list' is rejected with
TS2322 against the PR tarball, so …/types.js genuinely resolves to the shipped
types.d.ts and is not silently falling back to any. That is the claim the
whole change rests on.

I could not re-check the Deno 2.9.5 and Bun 1.3.11 rows of
packed-consumer-validation.md: neither runtime is installed here. Those stay
on your measurement.

3. fjs/ci/todo/publishing-packages.md — resolved

The Stage-1 code block is now "tsc --noEmit false --emitDeclarationOnly && tsc",
matching package.json, and 287648f7 finished the job on the rest of the file
(the source.ts -> source.js emission block and the two open tasks).

Repo-wide, --declaration false survives in exactly two places, both explicitly
past-tense records amended with a #1520 link rather than prescriptions —
publishing-packages.md:129 ("this second step was … an output retired by
#1520") and f-mjs-package-support.md:90, whose code block is immediately
followed by the correction paragraph. That reads as deliberate history-keeping,
not a miss.

Rest of the battery

  • npx tsc --noEmit 0; npm run prepack from clean 0; npm test 2531/0,
    identical to origin/main (no source change, as expected).
  • Public surface: type-alias extract byte-identical to main (526 entries);
    normalized const-signature diff 0 deltas over 898 entries. diff -rq over
    every emitted .d.ts / .d.mts in both trees: no differences. The published
    type surface is unchanged; only the .js files leave.
  • Broken relative markdown links: the set is identical to main's (140),
    despite the new fjs/ci/packed-consumer-validation.md and the new
    cross-links into it.
  • CI's npm pack step (ci.yml:508) is covered by the clean-tree prepack run.
  • Spot-checked one recorded measurement: the second pass emitted exactly 96
    files, 85 types.js + 11 scenario/all.test, as
    migrate-typescript-to-mjs.md states. Matches.

One correction, non-blocking

fjs/ci/packed-consumer-validation.md, end of the types.js section:

import * as and bare side-effect imports of a …/types.js path fail the
same way under every toolchain, since no elision applies to them.

The stated reason does not hold for import * as. TypeScript's default elision
does drop a namespace import used only in type positions — my star.ts
compiles to console.log('star ok'); export {}; under plain tsc and runs fine
against the PR tarball. It only fails under verbatimModuleSyntax (and Node
strip-types), which makes it behave exactly like the inline form the table above
it already covers, not worse. Bare side-effect imports are never elided, so that
half is right.

The error is in the safe direction — the doc over-warns — but this file is
explicitly a record for someone to re-check against, so it is worth the one-line
fix. AGENTS.md's own wording is already correct, since it scopes the same claim
to verbatimModuleSyntax.

Everything else checks out.

@sergey-shandar
sergey-shandar added this pull request to the merge queue Aug 13, 2026

@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-review of the delta since 287648f7 (my last approval). New on the branch: one commit, 2b7e1381 "Ship an authored external-runner entry: register.mjs", plus a merge of main at fa812785. Everything below is re-derived at 3be05f63, not carried over.

The finding this commit answers was mine to have missed

At 287648f7 I characterised the tarball delta as "96 removed .js = 85 export {} stubs + 11 unreferenced test fixtures". The second half of that was wrong: fjs/emergent_testing/all.test.js was not a fixture, it was the entry fjs/ci/README.md and fjs/emergent_testing/README.md both told external test runners to import. I called it dead; it was load-bearing. The fix here is right, and I verified it end to end rather than taking the commit message for it.

register.mjs ships and works from the packed tarball. Installed this PR's tarball into a scratch consumer with all.test.ts containing only import 'functionalscript/fjs/emergent_testing/register.mjs', dropped a mymod/proof.f.mjs beside it, and ran node --test:

✔ import("./mymod/proof.f.mjs").proof.passes() ... (0.499667ms)
ℹ tests 1  pass 1  fail 0

Negative-controlled: changing that proof to throw 'deliberate' gives pass 0 / fail 1, exit 1 — so discovery and registration are real, not a no-op import that happens to exit 0. The same consumer against main's tarball via the old all.test.js specifier behaves identically, and the old specifier against this tarball exits 1 with ERR_MODULE_NOT_FOUND. So the entry moved, the replacement is equivalent, and the break is exactly the one the changelog now names.

Packaging delta re-derived (npm pack both trees, 695 → 601 files):

  • removed: 96 .js, of which 85 are export {}; stubs (84 bare, plus fjs/types/ts/types.js, whose only non-comment code is export {};) and 11 are fjs/emergent_testing/*.js — the 10 scenario fixtures and all.test.js.
  • added: fjs/emergent_testing/register.mjs and its register.d.mts.
  • of the 599 common files, exactly two differ in bytes: package.json (the prepack script) and all.test.d.ts (export {};import './register.mjs';). Everything else is byte-identical to main.

The prepack gate still holds. package.json and tsconfig.json are untouched in this delta, but this is the property the PR exists to preserve, so I re-ran AGENTS.md §6.2's own counter-example at this head rather than assuming: mutating fjs/media/json/rtti/module.f.mjs's recursive @type into an inline @type {const} gives

  • npx tsc --noEmit → exit 0 (source-only check does not see it),
  • pass 1 alone → module.f.d.mts with 2 /*elided*/ and 7 any,
  • full npm run prepack → exit 1, fjs/media/json/types.ts(29,24): error TS2344: Type 'false' does not satisfy the constraint 'true'.

Unmutated tree: npm run prepack exit 0. Same numbers as last round.

The import * as nit is fixed, and the correction is accurate

I measured the new wording against the packed tarball rather than reading it. import * as T from 'functionalscript/fjs/types/list/types.js' with a T.List<number> annotation:

toolchain emitted runtime
tsc, default elision no import statement runs, exit 0
tsc, verbatimModuleSyntax: true import * as T from '…/types.js'; retained ERR_MODULE_NOT_FOUND
Node type stripping (node ns.ts) ERR_MODULE_NOT_FOUND

and a bare import '…/types.js' is retained even under default elision → ERR_MODULE_NOT_FOUND. That is exactly what the revised paragraph now says. I cannot verify the Bun 1.3.11 and Deno 2.9.5 rows of that matrix — neither runtime is installed here — so those remain the author's measurement, unchecked by me, as in previous rounds.

Rest of the battery at this head

  • npx tsc --noEmit — exit 0.
  • npm test2532 / 0 / 2532, identical to fs-main at fa812785. node --test fjs/emergent_testing/all.test.ts and node --test fjs/emergent_testing/register.mjs each give 2468 pass / 0 fail, so the delegation costs nothing in-repo.
  • Dual-axis public surface diff (extract.mjs 46,202 bytes, consts.mjs 65,107 bytes — non-empty) — byte-identical between main and this head. No new unprefixed public export type, no signature widened to any.
  • Link check — 140 broken relative links, and the set is identical to main's. Nothing stranded.
  • AGENTS.md conventions: register.mjs is a non-module.* file, so §4 correctly gives it a leading doc block with no @module tag, followed by the load-bearing blank line — and the block does survive into register.d.mts. An arbitrarily-named non-module .mjs has precedent (fjs/types/bigint/benchmark.mjs). §8.3: the changelog edit extends the existing **BREAKING CHANGES:** entry in Unreleased and links only /pull/1520.
  • No lingering prescriptive all.test.js references: the only two left in the tree are the changelog line describing the removal and the historical note in todo/migrate-typescript-to-mjs.md.

No findings. The one open nit from 287648f7 is closed, the miss it exposed in my own earlier packaging claim is fixed properly, and the prepack round-trip gate — the reason this PR matters — is intact at this head.

The native-TypeScript-execution scenario suite never ran in CI, so keep
the record instead of the code: fjs/emergent_testing/scenarios.md
preserves run.sh and all nine scenario sources verbatim with the
recreation caveats. all.test.ts and the interim register.mjs collapse
into a single authored all.test.mjs — consumers migrate from the removed
all.test.js by one letter — and types.ts is now the only authored
TypeScript in the repository. The obsolete entry-rename issue
todo/205.md is deleted with the suite; AGENTS.md, both READMEs, the
changelog, and the migration TODO record the decision.

Verified: zero non-types.ts .ts files tracked; npx tsc clean;
npm test 2531/0; node --test 2467/0 and deno test 2467/0 discover
all.test.mjs; bun test 2465/2 with both failures reproduced identically
at the parent commit (pre-existing fjs/fsc proofs, unrelated); packed
tarball ships all.test.mjs + all.test.d.mts, zero .js; the documented
consumer entry loads under node --test, bun test, and deno test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GkqgqTaffDpYFQEJydpqHF

Copy link
Copy Markdown
Contributor Author

Superseding the register.mjs fix per @sergey-shandar's direction (637d263): the scenario suite never ran in CI, so instead of keeping a TypeScript entry around for its sake —

  • fjs/emergent_testing/scenarios/ is deleted; fjs/emergent_testing/scenarios.md preserves run.sh and all nine scenario sources verbatim, plus the two recreation caveats (the stale npm run fst runner arm, and the all.ts/_all.test.ts double-discovery noted in todo/65z-singleton-effect.md). The obsolete entry-rename issue todo/205.md goes with it.
  • all.test.ts and the interim register.mjs collapse into one authored all.test.mjs — consumers migrate from the removed all.test.js by one letter. types.ts is now the only authored TypeScript in the repository (git ls-files '*.ts' | grep -v 'types\.ts$' returns 0).

Verified at this head: npx tsc clean; npm test 2531/0; node --test and deno test both discover all.test.mjs and pass 2467/0; the packed tarball ships all.test.mjs + all.test.d.mts with zero .js, and the documented consumer entry (import 'functionalscript/fjs/emergent_testing/all.test.mjs') loads under node --test, bun test, and deno test.

One pre-existing observation, not from this PR: bun test fails 2 of 2467 (fjs/fsc/proof.f.mjs f1/f2) — reproduced identically at the parent commit with the old all.test.ts, so it's on main too and unrelated to the rename. May deserve its own issue.


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: 637d263315

ℹ️ 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 +25
- **BREAKING CHANGES:** the npm package no longer ships `.js` files compiled
from the type-only `types.ts` modules (empty `export {}` stubs) or from test
files: `prepack` emits declarations only, then re-checks the tree with those

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 Condense both #1520 changelog entries

The two new #1520 entries span 17 wrapped lines and include migration instructions, implementation details, and packaging rationale. New entries are limited to roughly three wrapped lines or 250 characters, with walkthroughs and rationale moved to the PR description or relevant documentation; reduce these entries to concise user-visible release notes and their PR link.

AGENTS.md reference: AGENTS.md:L1203-L1208

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Condensed in b5017f0: both entries are now short release notes with their PR link — the breaking one names what's gone and the two things a consumer must do (import type, the all.test.mjs entry); the fixtures one names the deletion and the recreation doc. The import-form mechanics and rationale stay in AGENTS.md §6.2, fjs/ci/packed-consumer-validation.md, and this PR.


Generated by Claude Code

renamed to authored `all.test.mjs` (which the package also ships as the
external-runner entry), and the repository's only authored TypeScript is
now `types.ts`. The obsolete rename issue
`fjs/emergent_testing/todo/205.md` is deleted with the suite.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3 Badge Remove issue 205 from the survivor inventory

When this deletion lands, the same document's survivor inventory still lists and links ../fjs/emergent_testing/todo/205.md at line 1029, so the audit count is stale and the link targets a missing file. A repo-wide rg finds no replacement for the deleted issue; remove it from that inventory and recompute the stated survivor count.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in b5017f0: 205.md is out of the survivor inventory, the count reads 21 (with the 22 kept as history and the deletion attributed), and no link targets the removed file.


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 637d2633 (previously approved at 3be05f63). One new commit — "Delete the scenario fixtures; the test entry is authored all.test.mjs" — which moves both of the things the earlier approval was resting on, so I re-ran them from scratch rather than carrying the old verdict forward. Everything holds. Approving; one stranded markdown link noted below as a nit.

The prepack gate still bites

prepack is unchanged (tsc --noEmit false --emitDeclarationOnly && tsc) and tsconfig.json is untouched, but the emit pipeline's inputs changed (11 .ts files deleted, one renamed), so I re-ran AGENTS.md §6.2's own counter-example from a clean tree rather than assuming.

Mutating fjs/media/json/rtti/module.f.mjs to the forbidden @type {const} form:

step result
npx tsc --noEmit on the mutated source exit 0 — defect invisible
pass 1 alone (--emitDeclarationOnly) exit 0; emits 2 /*elided*/ and 7 any into module.f.d.mts
full npm run prepack exit 1, fjs/media/json/types.ts(29,24): error TS2344: Type 'false' does not satisfy the constraint 'true'
full npm run prepack, unmutated exit 0

Identical to the numbers from the previous round. The declaration-emit round-trip check that closed #1497 is intact at this head.

External-runner entry, re-verified end to end from the tarball

The entry was register.mjs + a delegating all.test.ts last round; it is now a single authored all.test.mjs. Re-verified against the packed tarball rather than the diff.

files is unchanged, and all.test.mjs matches **/*.mjs, so it ships — confirmed by unpacking: fjs/emergent_testing/all.test.mjs and all.test.d.mts are both present, and the .d.mts retains the leading doc block (§4 blank line is doing its job; no @module is required here since this is not a module.* file).

Installed that tarball into a scratch consumer with its own lib/proof.f.mjs and an entry doing import 'functionalscript/fjs/emergent_testing/all.test.mjs':

  • proofs discovered and registered through the installed package — tests 2 / pass 2 / fail 0
  • negative control: replacing one proof with a thrower gives pass 1 / fail 1, exit 1

In-repo, the new header's claim that the runner discovers it by its .test. name holds for Node: default node --test at the repo root finds it and reports 2468 pass / 0 fail, exactly the same count as naming the file explicitly — so no double registration. npm run cov, retargeted in this commit, runs and reports real coverage (99.94 / 98.20 / 99.73 all-files), not the vacuous 100-over-0 the old default-discovery invocation used to give.

I could not verify the bun test and deno test halves of that claim — neither runtime is installed here. Same caveat as previous rounds.

Packaging and public surface

Tarball vs origin/main (fa812785), both built by npm pack from clean trees:

  • 695 → 590 files. Fully accounted for: −96 .js, −11 .d.ts (all.test.d.ts plus the 10 deleted scenario fixtures' declarations, which shipped on main while their .ts sources never did), +2 (all.test.mjs, all.test.d.mts).
  • Of the 588 files common to both tarballs, only package.json differs in bytes. Every shipped .mjs and .d.mts is byte-identical to main.
  • Exported type surface: 526 vs 526, zero added, zero removed, zero changed. No unprefixed public export type additions; no signature widened to any.
  • Exported const surface: 898 → 889. The nine removals are exactly the deleted scenario fixtures' proof consts. Nothing added, nothing changed.

npm test (built-in runner): 2532 / 2532 pass, matching origin/main exactly. npx tsc --noEmit exit 0.

Claims in the docs, re-derived

  • "the repository's only authored TypeScript is types.ts" — holds: 85 tracked .ts files, all of them types.ts, matching the 85 types.d.ts in the tarball.
  • "scenarios.md records their sources verbatim" — holds: all 11 deleted files, including run.sh, appear verbatim in fjs/emergent_testing/scenarios.md.
  • "the suite never ran in CI" — holds: no reference to scenarios, run.sh, or any test entry in .github/workflows/.
  • Deleting fjs/emergent_testing/todo/205.md is right — it was entirely about naming the scenarios' all.ts.
  • CHANGELOG: ## Unreleased, PR-only links, **BREAKING CHANGES:** on the packaging entry. Correct.
  • The .f.ts sweep rulers in todo/migrate-typescript-to-mjs.md are unaffected: the resolve-against-the-tree measurement still returns exactly 9, in exactly the two files named (fjs/fsc/README.md, this file), and the file count is still 22.

Nit: one stranded link

todo/migrate-typescript-to-mjs.md still enumerates the deleted file:

[`205.md`](../fjs/emergent_testing/todo/205.md) and

Broken-link sets against main differ by exactly this one entry (141 vs 140). The same list is introduced as "All 22 files that still contain the old extension anywhere are listed below" — the count survives by coincidence, since scenarios.md (added by this commit, and it does contain .f.ts) replaces 205.md in the set, but it is not in the list. Two lines up, the same file already records that 205.md "is deleted with the suite", so this is just the enumeration not being re-swept — which is the thing that paragraph itself warns about ("Re-measure with the same resolve-against-the-tree method, at the final commit"). Drop the 205.md link and add scenarios.md. Docs-only, in a todo/ file, not worth holding the PR for.

The two entries carried migration walkthroughs and rationale that the
convention sends to the PR description and docs; they are now short
release notes with their PR link. The prose-sweep survivor inventory no
longer counts or links the deleted 205.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GkqgqTaffDpYFQEJydpqHF

@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 b5017f0e (previously approved at 637d2633). The delta is one
commit touching two files — CHANGELOG.md and todo/migrate-typescript-to-mjs.md
— and nothing else: no source, no package.json, no tsconfig.json, no
fjs/emergent_testing/, no files / packaging change. origin/main has not
moved either (merge base fa81278 == origin/main), so the load-bearing results
from the previous round stand on an unchanged tree, and the public surface is
unchanged by construction. Still approving.

Re-verified at this head

  • npx tsc --noEmit — exit 0.
  • npm pack from a freshly cleaned tree — exit 0, 590 files, matching the
    settled baseline. The prepack gate (--emitDeclarationOnly then plain tsc
    with noEmit from tsconfig) is untouched by this commit, so I did not re-run
    the AGENTS.md §6.2 counter-example mutation; nothing it depends on moved.
  • npm test from a cleaned tree — pass 2532, fail 0, matching main.
  • Broken-link sets are now identical to main: 140 vs 140, diff empty.
    The previously reported extra entry — todo/migrate-typescript-to-mjs.md
    linking the fjs/emergent_testing/todo/205.md that this PR deletes — is
    fixed.

CHANGELOG rewrite checks out

Both entries are additive to Unreleased, no released section is rewritten, and
each links only /pull/1520. The condensed claims are true as measured:

  • "the npm package ships no .js files" — the packed tarball has zero .js
    entries. Extension histogram of the 590 files: 251 .mts, 251 .mjs, 85
    .d.ts (no plain .ts), plus package.json, README.md, LICENSE.
  • "types.ts is now the only authored TypeScript" — git ls-files '*.ts' '*.tsx' '*.mts' '*.cts' returns 85 paths, all named types.ts.
  • The **BREAKING CHANGES:** prefix is retained, correctly: the
    verbatimModuleSyntax consumer break is real. Dropping the enumeration of the
    non-erasing import forms (import { type X }, import * as, bare
    side-effect) in favour of "fully erased import type" loses detail but not
    correctness.

Non-blocking nit: the survivor inventory fix overshot

The other half of that commit changed the inventory in
todo/migrate-typescript-to-mjs.md from "All 22 files" to

All 21 files that still contain the old extension anywhere (22 before #1520
deleted 205.md) are listed below

Measured against the tree with the same ruler (markdown files containing .f.ts
anywhere, CHANGELOG.md excluded), both trees hold 22, not 22 → 21:

main: 22    PR: 22
diff:  > fjs/emergent_testing/scenarios.md      (added by this PR)
       < fjs/emergent_testing/todo/205.md       (deleted by this PR)

One file left the set and one joined it. scenarios.md line 41 records
# scenario: path to a *.pass.f.ts, *.fail.f.ts, *.pass.ts or *.fail.ts file
verbatim, which is deliberate and belongs in the "recording a superseded
convention" group — but it is still not enumerated, so the prose now lists 21 of
22 and states a total that was correct on main and is wrong here. This is the
failure mode the paragraph itself warns about two sentences later ("prose that
enumerates survivors can itself add mentions"). Docs-only, in a todo/ file, so
not a blocker — but if you touch it again, 22 and one more list entry is the
whole fix.

Not verifiable here

The Bun and Deno rows of the consumer matrix remain unverified in this
environment — neither runtime is installed. I am not asserting or disputing them.

@sergey-shandar
sergey-shandar added this pull request to the merge queue Aug 13, 2026
Merged via the queue into main with commit 283db73 Aug 13, 2026
19 checks passed
@sergey-shandar
sergey-shandar deleted the claude/ts-js-migration-strategy-jpz3bm branch August 13, 2026 17:19
@sergey-shandar sergey-shandar mentioned this pull request Aug 14, 2026
sergey-shandar pushed a commit that referenced this pull request Aug 14, 2026
Release under the new directory-per-version changelog workflow:
changelog/unreleased/ is renamed to changelog/0.45.0/ with its entry
files kept as they are (.gitkeep dropped — the next entry PR recreates
unreleased/). Minor bump: the release contains BREAKING CHANGES entries
(#1516, #1520, #1530, #1531, #1547).

Update AGENTS.md §8.3–8.4 and changelog/README.md for the new workflow:
releasing renames the directory instead of concatenating entries, and
future entries carry no PR number or link inside the file — the file
name already has it. Released entries are kept as-is. Extend
todo/changelog-website.md so the future generator reads both release
forms: <version>.md files (through 0.44.0) and <version>/ directories.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M296KXwQHHuUGhryRReKpJ
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