Ship declarations only: drop the JS emit pass from prepack - #1520
Conversation
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
Deploying with
|
| 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
left a comment
There was a problem hiding this comment.
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 prepackfrom agit cleaned tree → exit 0.npm test→ 2522/2522. That equals the merge-base (7ae03f04) count, so the PR changes nothing. (mainis 2524 because this branch predates #1519okThen(+1) and #1518 (+1).)- Packed contents, before/after.
npm pack --dry-runonmain(695 files) vs this branch (599). The set difference is exactly 96 files, all.js, and nothing is added. The 85types.jsreally are inert: stripping comments andexport {}from each leaves an empty string in all 85. The other 11 are theemergent_testingcompiled fixtures. - Nothing resolves them. Across the 346 emitted
.d.mts/.d.ts: 801…/types.tsspecifiers (matching the count in the TODO), 0.jsspecifiers. 0 runtime.jsimports in any shipped.mjs.scenarios/run.shlinks the authored.tsfixtures directly.bin.fjs→fjs/module.mjs, still shipped. The only CI steps that could care arenpx tscandnpm pack. - Packed tarball actually works. Packed this branch, installed the tarball into an empty directory, and:
parse/stringifyround-trip runs on Node;npx fjsruns from the installed package;tsc 5.9.3nodenext+stricttype-checks a consumer importing…/types.js-specifier types → exit 0. Negative control:const bad: List<number> = 'not a list'and a function assigned toUnknownare both rejected with TS2322, so the.d.mts→./types.ts→ shippedtypes.d.tssubstitution is real and not ananyfallback. 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 onmainand 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 2 — fjs/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: true — tsconfig.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.mjsprintsok { 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 generatedtypes.jsremains necessary."fjs/fsc/README.md:192-193— "do not assume the TypeScript runtime-emission pass can disappear while package resolution may still require a generatedtypes.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.
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
left a comment
There was a problem hiding this comment.
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→ 0npm test→ 2524 pass / 0 fail, exactly matchingorigin/main(2524)npm run prepackfrom a clean tree → 0npm pack --dry-run→ 599 files vs 695 on main; the file-set diff is exactly the 96types.jsstubs 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 | 2 — fjs/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:
tscexits 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 generatedtypes.jsremains 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 generatedtypes.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
|
All three review points are addressed in 1. Round-trip check restored, your one-liner adopted verbatim. 2. Documented as a rule rather than marked breaking. The decision: the fully erased 3. Both On the part you could not verify: the Deno/Bun measurements are recorded with exact versions and commands in Generated by Claude Code |
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
left a comment
There was a problem hiding this comment.
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 --noEmitexit 0;npm run prepackfrom 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.jsstubs
and 11fjs/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.
Onlypackage.jsondiffers. - Broken relative links: identical sets, 140 on both trees; 0 extension mismatches. No source
files are touched by this PR, so there is no newexport typeto prefix-check. - Spot-checked two rows of the new matrix in
packed-consumer-validation.mdagainst the
packed tarball: Node--experimental-strip-typesdoes giveERR_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
prepackwhile any.ts/.f.tssource
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.
| "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", |
There was a problem hiding this comment.
| "prepack": "tsc --noEmit false --emitDeclarationOnly && tsc --noEmit", | |
| "prepack": "tsc --noEmit false --emitDeclarationOnly && tsc", |
|
|
||
| ## Unreleased | ||
|
|
||
| - The npm package no longer ships `.js` files compiled from the type-only |
There was a problem hiding this comment.
I would consider it a BREAKING CHANGE.
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
|
Round 3 addressed in
Generated by Claude Code |
|
@codex review |
There was a problem hiding this comment.
💡 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".
| 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: |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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
|
@codex review |
There was a problem hiding this comment.
💡 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".
| "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", |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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 1 — fjs/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 --noEmit0;npm run prepackfrom clean 0;npm test2531/0,
identical toorigin/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 -rqover
every emitted.d.ts/.d.mtsin both trees: no differences. The published
type surface is unchanged; only the.jsfiles leave. - Broken relative markdown links: the set is identical to
main's (140),
despite the newfjs/ci/packed-consumer-validation.mdand the new
cross-links into it. - CI's
npm packstep (ci.yml:508) is covered by the clean-tree prepack run. - Spot-checked one recorded measurement: the second pass emitted exactly 96
files, 85types.js+ 11 scenario/all.test, as
migrate-typescript-to-mjs.mdstates. Matches.
One correction, non-blocking
fjs/ci/packed-consumer-validation.md, end of the types.js section:
import * asand bare side-effect imports of a…/types.jspath 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.
o2alexanderfedin
left a comment
There was a problem hiding this comment.
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 areexport {};stubs (84 bare, plusfjs/types/ts/types.js, whose only non-comment code isexport {};) and 11 arefjs/emergent_testing/*.js— the 10 scenario fixtures andall.test.js. - added:
fjs/emergent_testing/register.mjsand itsregister.d.mts. - of the 599 common files, exactly two differ in bytes:
package.json(theprepackscript) andall.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.mtswith 2/*elided*/and 7any, - 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 test— 2532 / 0 / 2532, identical tofs-mainatfa812785.node --test fjs/emergent_testing/all.test.tsandnode --test fjs/emergent_testing/register.mjseach give 2468 pass / 0 fail, so the delegation costs nothing in-repo.- Dual-axis public surface diff (
extract.mjs46,202 bytes,consts.mjs65,107 bytes — non-empty) — byte-identical between main and this head. No new unprefixed publicexport type, no signature widened toany. - Link check — 140 broken relative links, and the set is identical to main's. Nothing stranded.
- AGENTS.md conventions:
register.mjsis a non-module.*file, so §4 correctly gives it a leading doc block with no@moduletag, followed by the load-bearing blank line — and the block does survive intoregister.d.mts. An arbitrarily-named non-module.mjshas precedent (fjs/types/bigint/benchmark.mjs). §8.3: the changelog edit extends the existing**BREAKING CHANGES:**entry inUnreleasedand links only/pull/1520. - No lingering prescriptive
all.test.jsreferences: the only two left in the tree are the changelog line describing the removal and the historical note intodo/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
|
Superseding the
Verified at this head: One pre-existing observation, not from this PR: Generated by Claude Code |
|
@codex review |
There was a problem hiding this comment.
💡 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".
| - **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 |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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.tsplus the 10 deleted scenario fixtures' declarations, which shipped on main while their.tssources never did), +2 (all.test.mjs,all.test.d.mts). - Of the 588 files common to both tarballs, only
package.jsondiffers in bytes. Every shipped.mjsand.d.mtsis byte-identical to main. - Exported type surface: 526 vs 526, zero added, zero removed, zero changed. No unprefixed public
export typeadditions; no signature widened toany. - Exported const surface: 898 → 889. The nine removals are exactly the deleted scenario fixtures'
proofconsts. 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.tsfiles, all of themtypes.ts, matching the 85types.d.tsin the tarball. - "
scenarios.mdrecords their sources verbatim" — holds: all 11 deleted files, includingrun.sh, appear verbatim infjs/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.mdis right — it was entirely about naming the scenarios'all.ts. - CHANGELOG:
## Unreleased, PR-only links,**BREAKING CHANGES:**on the packaging entry. Correct. - The
.f.tssweep rulers intodo/migrate-typescript-to-mjs.mdare 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
left a comment
There was a problem hiding this comment.
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 packfrom a freshly cleaned tree — exit 0, 590 files, matching the
settled baseline. The prepack gate (--emitDeclarationOnlythen plaintsc
withnoEmitfrom 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 testfrom a cleaned tree — pass 2532, fail 0, matchingmain.- Broken-link sets are now identical to
main: 140 vs 140,diffempty.
The previously reported extra entry —todo/migrate-typescript-to-mjs.md
linking thefjs/emergent_testing/todo/205.mdthat 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
.jsfiles" — the packed tarball has zero.js
entries. Extension histogram of the 590 files: 251.mts, 251.mjs, 85
.d.ts(no plain.ts), pluspackage.json,README.md,LICENSE. - "
types.tsis now the only authored TypeScript" —git ls-files '*.ts' '*.tsx' '*.mts' '*.cts'returns 85 paths, all namedtypes.ts. - The
**BREAKING CHANGES:**prefix is retained, correctly: the
verbatimModuleSyntaxconsumer break is real. Dropping the enumeration of the
non-erasing import forms (import { type X },import * as, bare
side-effect) in favour of "fully erasedimport 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
deleted205.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.
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
What
prepacknow runs only the declaration emit:The second pass (
tsc --noEmit false --declaration false) is removed. Its entire output was 96 dead files: 85 emptyexport {}stubs compiled from the type-onlytypes.tsmodules, 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.jsfiles;filesinpackage.jsonis 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:
.mjshas a runtime import of any.jspath; alltypes.tsreferences areimport type(erased underverbatimModuleSyntax) or JSDoc@importcomments..d.ts/.d.mtsfiles keep their…/types.tsspecifiers, and consumers resolve those to the shippedtypes.d.ts— the published package has never contained.tsfiles, so this substitution path was already load-bearing.Verification
All checks ran against the exact tarball produced by the new
prepack(0.js; 85types.d.ts, 250.d.mts, 250.mjs), installed in a clean consumer importingmodule.f.mjsat runtime andtypes.js-specifier types:nodenext+strict, default settings--experimental-strip-types)fjsCLI bin from the installed packagebun runandbun build(Node target)deno runanddeno check(node_modules resolution)Negative control: a deliberate
List<number> = 'not a list'misuse is rejected (TS2322) by bothdeno checkand tsc, proving the types genuinely resolve through.d.mts→types.tsspecifier →types.d.tsrather than falling back toany. 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:
**/*.jsstays infiles, 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