Implement private type module separation and prepack validation - #1743
Implement private type module separation and prepack validation#1743sergey-shandar wants to merge 1 commit into
Conversation
Establishes the rule, the documents, and the packaging step that fjs/todo/separate-private-types.md needs, then migrates one module to prove the shape end to end. Rule: no authored .mjs anywhere in the repository carries a file-scope JSDoc @typedef, since declaration emit turns one into an exported type alias. A typedef inside a function is unaffected. Root AGENTS.md carries the repository-wide rule; fjs/AGENTS.md gains a "Private types" section with the public declaration closure, the optional private.ts, the optional meta/module.f.mjs, and the intra-directory dependency order; fjs/fsc/README.md replaces its `_`-leak-tolerance policy with the private-type contract. todo/blocked/jsdoc-typedef-strip-internal.md is deleted rather than narrowed - splitting private types out closes the leak, so the repository no longer waits on @internal/stripInternal or keeps two conflicting strategies - and its referrers now point at the migration. Packaging: prepack ends with node ./fjs/ci/prepack.mjs, which deletes every private.d.ts generated from an authored private.ts and then fails packaging if a remaining declaration still imports a private module. The check is semantic, not textual: it reads static module specifiers as tokens with `specifiers` from fjs/website/browser-source.mjs, so a JSDoc @import comment TypeScript kept in a declaration is read as a comment and no emitted text is rewritten. First private.ts: fjs/djs/tokenizer. _Token, _FlatToken, _TokenScanState, _StringDecodeState and _DjsScanState are reached only by module-private constants, so they move out of the public surface entirely - the emitted module.f.d.mts names none of them, and _StringDecodeState loses the spurious `any |` arm the multi-line @typedef form used to emit. Verified: npx tsc, the full proof suite (3477 pass), npm pack (no private artifact in the tarball), and a clean TypeScript consumer of fjs/djs/tokenizer installed from that tarball, with its negative control still failing TS2322. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BHDDUXrAnbCeGBuGgbuPuw
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
functionalscript | 0dc4169 | Commit Preview URL Branch Preview URL |
Aug 27 2026, 06:35 PM |
o2alexanderfedin
left a comment
There was a problem hiding this comment.
Measured at 0dc41692f. Gates match main: npm test 3477/3477 exit 0, tsc --noEmit exit 0, npm run prepack exit 0 ("private type modules removed: 1; declarations checked: 375").
The tokenizer conversion is real, not nominal: main's fjs/djs/tokenizer/module.f.d.mts:54-70 emits _Token, _FlatToken, _TokenScanState, _StringDecodeState, _DjsScanState; at this head none of those _ exports remain in the emitted declaration. The reconciliation is honest too — fsc/README.md's section is rewritten rather than patched, todo/blocked/jsdoc-typedef-strip-internal.md is deleted outright, and separate-private-types.md checks off exactly what shipped and no more.
Two things before it leaves draft:
-
The validator does not enforce the rule it is presented alongside.
fjs/ci/prepack.mjschecks only that no remaining declaration statically imports aprivate.ts— it never looks for file-scope@typedef. I tested all three ways: adding/** @typedef {number} _LeakedTypedef */at file scope in untouchedfjs/edag/module.f.mjsgivesnpm run prepackexit 0 whilefjs/edag/module.f.d.mts:35literally emitsexport type _LeakedTypedef = number; leaking aprivate.tstype into a public signature gives exit 1 with the right message; unmodified code passes. So it is not a check that cannot fail — but "no file-scope@typedefin authored.mjs", which this PR adds to bothAGENTS.mdandfjs/AGENTS.mdas the headline rule, has zero enforcement. Either widen the check or say plainly infjs/ci/README.mdthat it guards only theprivate.tspath, so a reviewer does not over-trust "prepack validation". -
Changelog is owed and missing. This changes shipped
.d.mtsoutput (five exported_types leave the packed tokenizer declaration) and givesprepacka new failure mode onnpm pack/publish — both are the "affects behavior or the public API" case. There is nochangelog/unreleased/1743.mdand noChangelog:section in the body.
Worth recording since it is easy to miss: grep .github/workflows/ for prepack finds nothing, but ci.yml's node26 job runs npm pack, which fires the prepack lifecycle hook — so the check does run in CI, just not under that name.
Summary
This change implements the private type module separation strategy to keep implementation-private types out of public declarations. Rather than waiting for TypeScript's
@internal/stripInternalsupport for JSDoc typedefs, the repository now enforces that authored.mjsfiles contain no file-scope@typedefdeclarations, moving private types to optionalprivate.tsmodules that are deleted during packaging.Key Changes
Repository-wide rule: No authored
.mjsfile may contain a file-scope JSDoc@typedef. Typedefs inside functions (for compile-time proofs) remain allowed. This prevents declaration emit from leaking implementation details as exported type aliases.Private type placement strategy:
types.ts(the public declaration closure)private.tssiblingsmeta/module.f.mjsfor metaprogramming constantsPackaging validation (
fjs/ci/prepack.mjs):prepackstep after declaration emitprivate.d.tsfiles (never shipped)First migration:
fjs/djs/tokenizernow usesprivate.tsfor_Token,_FlatToken,_TokenScanState,_StringDecodeState, and_DjsScanState, with the implementation updated to import these types via JSDoc@import.Documentation updates:
AGENTS.md: Repository-wide prohibition on file-scope@typedeffjs/AGENTS.md: Public declaration closure, optionalprivate.ts, dependency ordering, and optionalmeta/modulesfjs/fsc/README.md: Replaces leak-tolerance policy with the new private-type contractfjs/todo/separate-private-types.md: Marked as WIP with progress trackingtodo/blocked/jsdoc-typedef-strip-internal.md: The@internalwait is supersededPackage.json: Updated
prepackscript to runnode ./fjs/ci/prepack.mjsafter declaration emitImplementation Details
The prepack validation is semantic rather than textual: it reads static module specifiers as tokens using the existing
specifiersutility, so retained JSDoc@importcomments in declarations are correctly identified as comments and not mistaken for actual dependencies. This allows TypeScript to preserve source documentation while the validation ensures no shipped declaration actually depends on a private module.The dependency direction within a module directory is:
types.ts←private.ts←module.f.mjs←proof.f.mjs←module.mjs←proof.mjs, guiding where types and verification belong without requiring every file to exist.https://claude.ai/code/session_01BHDDUXrAnbCeGBuGgbuPuw