Skip to content
Merged
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
dc1b5f3
todo: separate private types into private.ts
sergey-shandar Aug 27, 2026
202f749
todo: prohibit typedefs in implementation files
sergey-shandar Aug 27, 2026
fbe4f48
todo: define private declaration cleanup
sergey-shandar Aug 27, 2026
f1154ec
todo: validate private types in packed artifact
sergey-shandar Aug 27, 2026
beef545
todo: add rtti.f.mjs type definition convention
sergey-shandar Aug 27, 2026
9bddb33
todo: rename RTTI companion to meta
sergey-shandar Aug 27, 2026
a14d7b8
todo: allow function-local typedefs
sergey-shandar Aug 27, 2026
18ab3d9
todo: put private declaration cleanup in prepack
sergey-shandar Aug 27, 2026
0b1e6f9
todo: cover meta FunctionalScript files
sergey-shandar Aug 27, 2026
3ddac53
todo: require import type in TypeScript type files
sergey-shandar Aug 27, 2026
c64c52e
todo: allow private helpers in public type graph
sergey-shandar Aug 27, 2026
de51397
todo: make public type relocation breaking
sergey-shandar Aug 27, 2026
32eec93
todo: broaden meta constants convention
sergey-shandar Aug 27, 2026
556e535
todo: make meta moves breaking API changes
sergey-shandar Aug 27, 2026
7368e06
todo: reconcile private type policy
sergey-shandar Aug 27, 2026
89e33c1
Merge branch 'main' into todo/private-types-ts
sergey-shandar Aug 27, 2026
18c604e
todo: reconcile authored TypeScript policy
sergey-shandar Aug 27, 2026
304e544
todo: define public declaration closure
sergey-shandar Aug 27, 2026
5ffffd7
todo: apply typedef rule to all f.mjs files
sergey-shandar Aug 27, 2026
0401908
todo: apply typedef rule to all mjs
sergey-shandar Aug 27, 2026
8622f5a
todo: clarify optional meta proof imports
sergey-shandar Aug 27, 2026
59c1468
todo: treat retained JSDoc imports as comments
sergey-shandar Aug 27, 2026
0a948de
todo: keep meta coverage policy minimal
sergey-shandar Aug 27, 2026
01e4309
todo: mark private metadata constants with underscore
sergey-shandar Aug 27, 2026
0d839d9
todo: preserve type module dependency order
sergey-shandar Aug 27, 2026
114fd58
todo: make mjs typedef rule repository-wide
sergey-shandar Aug 27, 2026
be38c09
todo: clarify recursive metadata placement
sergey-shandar Aug 27, 2026
1e69c5a
todo: make private/meta files optional tools
sergey-shandar Aug 27, 2026
e785b3b
todo: make metadata an optional submodule
sergey-shandar Aug 27, 2026
addb6d9
Merge branch 'main' into todo/private-types-ts
sergey-shandar Aug 27, 2026
21c444e
todo: keep dependency diagram intra-module
sergey-shandar Aug 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
286 changes: 286 additions & 0 deletions fjs/todo/separate-private-types.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,286 @@
## Keep private types out of public declarations

**Priority:** P2
**Status:** open

### Problem

TypeScript declaration emit turns file-scope JSDoc `@typedef`s in authored
`.mjs` files into declaration aliases. Implementation-private `_` types therefore
leak into generated `.d.ts` / `.d.mts` files and add noise to the public surface.

The requirement is a clean, self-contained public declaration/API boundary.
`private.ts` and subordinate modules such as `meta/module.f.mjs` are **tools** for
reaching that result, not required companion files.

### Rules

#### No file-scope typedefs in authored `.mjs`

No authored `.mjs` anywhere in the repository may contain a **file-scope** JSDoc
`@typedef`, regardless of directory, basename, or whether the file is
FunctionalScript. This includes `module.f.mjs`, `proof.f.mjs`, host `.mjs` files,
descriptive companions such as `testlib.f.mjs`, and root/`todo/` files such as
`todo/proof.f.mjs`.

Function-local typedefs remain allowed. This is especially useful for compile-time
proofs that need lexical or downstream runtime values:

```js
const signatures = () => {
/** @typedef {Assert<Equal<ReturnType<typeof step<...>>, Effect<...>>>} _Step */
/** @typedef {Assert<Equal<ReturnType<typeof catchStep<...>>, Effect<...>>>} _CatchStep */
}
```

Private type and runtime constant names continue to use a leading `_`.

#### Public declaration closure

`types.ts` describes the public declaration closure:

- public types;
- private `_` helpers required transitively by shipped public declarations,
including declarations of exported runtime functions/values.

For example, if an exported `find` declaration contains `_SortedArray<T>`, then
`_SortedArray` is part of the public declaration closure and stays in `types.ts`
(or is inlined). Moving it to an unshipped private module would make the public
declaration incomplete.

`types.ts` must not depend on `private.ts`.

`private.ts` is optional. Use it only when separating implementation-private
file-scope types outside the public declaration closure makes the design cleaner.
Do not create it mechanically for every `_` name.

#### Dependency order

Within one module directory, preserve the dependency direction for the roles that
exist:

```text
types.ts <- private.ts <- module.f.mjs <- proof.f.mjs <- module.mjs <- proof.mjs
```
Comment thread
sergey-shandar marked this conversation as resolved.

The arrow points from dependency to dependent. This is a layering guide, not a
requirement that every file or edge exists. A subordinate module such as
`meta/module.f.mjs` is a separate module and is therefore described separately
below rather than appearing in this intra-directory diagram.

Move verification downstream before moving implementation upstream. For example,
`fjs/effects/types.ts` currently imports implementation functions only to assert
`ReturnType<typeof ...>` signatures. Those assertions verify `module.f.mjs`, so
move them into one or more proof functions in `proof.f.mjs`; keep the functions
in `module.f.mjs`.

Analyze constrained and recursive cases individually rather than inventing broad
exceptions:

- `fjs/media/revision`: `LockMap` / `LockSchema` can remain in `types.ts`; recursive
`lock` can remain in `module.f.mjs` when it requires the named `LockSchema`
annotation; move `Assert<Check<...>>` consistency checks into a proof function.
- `fjs/edag`: recursive RTTI such as `exp` can remain in `module.f.mjs` when its
annotation depends on public EDAG types; move file-scope consistency asserts
into proof functions.

The goal is to preserve the dependency direction and simplify the public surface,
not to satisfy a mechanical file-placement rule.

### Optional metaprogramming submodule

When declarative runtime constants are shared between TypeScript and runtime code,
it can be useful to split them into a normal subordinate module, for example:

```text
meta/
module.f.mjs
```

`meta` here means **metaprogramming**: declarative definitions of types/schema-like
information that are useful at both compile time (TypeScript through `typeof`,
RTTI conversion, indexed access, etc.) and runtime.

Typical examples are:

- RTTI/schema constants;
- `as const`-style literal data;
- declarative lookup tables whose literal shape defines or constrains types.

This is only a suggestion. Do not create `meta/` merely because a runtime value
appears in a type proof. Ordinary implementation functions stay in
`module.f.mjs`; recursively annotated metadata may also stay there when moving it
would reverse the dependency direction.

The parent module may depend on `meta/module.f.mjs` like any other lower-level
module. The `meta/` module itself follows the same normal module conventions and,
if it grows additional files, its own intra-directory dependency order.

A private constant exported from `meta/module.f.mjs` for sibling-module linkage
uses a leading `_`:

```js
// meta/module.f.mjs
export const _framingKeywords =
/** @type {const} */ (['import', 'const', 'export', 'default', 'from'])
```

```ts
// private.ts or types.ts
import type { _framingKeywords } from './meta/module.f.mjs'
```

```js
// module.f.mjs
import { _framingKeywords } from './meta/module.f.mjs'
```

Exportability is linkage, not API status: `_` means consumers must not depend on
the name. Renaming/removing it is not breaking solely because it is exported.

Because `meta/module.f.mjs` is just another `module.f.mjs`, existing tooling
already handles it:

- emergent testing loads it as `*.f.mjs`;
- the existing Node `**/module.f.mjs` coverage filter includes it;
- the existing Deno `.*module\\.f\\.mjs` filter includes it.

No special metadata filename or coverage rule is needed.

### Breaking migrations

Moving an existing public type from an authored `.mjs` declaration surface to
`types.ts` changes its public type import path. Moving an existing public runtime
constant into a subordinate module such as `meta/module.f.mjs` changes its runtime
import path.

When such moves are chosen, treat them as intentional breaking changes:

- update every repository importer;
- update the changelog;
- do **not** add compatibility typedefs, exports, or re-exports to preserve the
old entry point.

Private `_` names are not public API merely because declaration emit or module
linkage exposes them.

### Declaration emission and packaging

If `private.ts` is used, keep it in the normal TypeScript program so source users
are checked. Declaration emit may therefore create an intermediate
`private.d.ts`.

Do not try to exclude `private.ts` from checking. Instead delete generated
`private.d.ts` files as the final `prepack` step, after declaration emit and the
existing declaration round-trip check, before package contents are selected.

Do **not** rewrite/post-process emitted declaration text. TypeScript may retain a
source comment such as:

```js
/** @import { _Private } from './private.ts' */
```

inside an emitted declaration. In `.d.ts` / `.d.mts` this is only a comment, not
a TypeScript module dependency, so it may remain after the private declaration is
removed.

Package validation must check semantic dependencies, not raw text:

- no authored/generated private type artifact that is intended to be unshipped is
present in the tarball;
- no packed declaration semantically depends on an unshipped private type module;
- a clean TypeScript consumer installed from the tarball type-checks successfully.

### Repository policy

When this TODO is implemented:

- update root `AGENTS.md` with the repository-wide rule that authored `.mjs` files
may not contain file-scope JSDoc `@typedef`;
- update `fjs/AGENTS.md` with the public-declaration-closure rule, optional
`private.ts`, optional subordinate metaprogramming modules such as
`meta/module.f.mjs`, and the dependency-order guidance;
- update `fjs/fsc/README.md` and delete or narrow
`todo/blocked/jsdoc-typedef-strip-internal.md` so the repository does not keep
two conflicting private-type strategies.

Authored TypeScript type modules (`types.ts`, and `private.ts` when present) remain
type-only and use named `import type { ... }` imports.

### Tasks

- [ ] Document the repository-wide prohibition on file-scope JSDoc `@typedef` in
authored `.mjs`; allow function-local typedefs.
- [ ] Migrate existing violations, including authored `.mjs` outside `fjs/` such
as `todo/proof.f.mjs`.
- [ ] Keep `types.ts` as the public declaration closure; retain/in-line private
helpers required by public declarations.
- [ ] Use `private.ts` only where separating implementation-private file-scope
types improves the design.
- [ ] Preserve the intra-directory dependency direction shown above; move
verification downstream when that is cleaner.
- [ ] Move the `fjs/effects/types.ts` implementation-signature asserts into proof
functions in `fjs/effects/proof.f.mjs`.
- [ ] Review recursive cases individually, including `fjs/media/revision` and
`fjs/edag`; keep recursive RTTI in `module.f.mjs` when required by layering
and move consistency asserts into proof functions.
- [ ] Where useful, split declarative compile-time/runtime constants into a normal
subordinate module such as `meta/module.f.mjs`; do not require it.
- [ ] Preserve leading `_` for private types and private runtime constants.
- [ ] Treat chosen public import-path moves as breaking changes with no
compatibility re-exports.
- [ ] If `private.ts` is used, delete generated `private.d.ts` as the final
`prepack` step.
- [ ] Do not text-postprocess emitted declarations; validate semantic private
dependencies and clean-consumer type checking instead.
- [ ] Add fixtures/examples covering: public-declaration helpers, optional
`private.ts`, function-local proof typedefs, recursive RTTI kept in
`module.f.mjs`, optional `meta/module.f.mjs`, retained non-semantic JSDoc
comments, and authored `.mjs` outside `fjs/`.
- [ ] Update root/fjs policy documentation and reconcile the old `_` leak policy.

### Acceptance criteria

- The public declaration/API surface is clean and self-contained.
- No authored `.mjs` anywhere in the repository contains a file-scope JSDoc
`@typedef`; function-local typedefs are allowed.
- `types.ts` contains the public declaration closure and does not depend on an
unshipped private type module.
- `private.ts`, when present, is an optional implementation tool rather than a
required companion.
- A subordinate module such as `meta/module.f.mjs`, when present, is an optional
metaprogramming/design tool rather than a special file role or requirement.
- The intra-directory dependency direction is preserved; assertions do not create
reverse edges merely for convenience.
- Private types/constants use leading `_`, even when linkage requires an export.
- Existing `module.f.mjs` discovery and coverage rules automatically include
`meta/module.f.mjs`; no metadata-specific coverage convention exists.
- Chosen public import-path moves are breaking migrations with importers/changelog
updated and no compatibility re-exports.
- If declaration emit creates `private.d.ts`, final-`prepack` cleanup removes it
before packaging.
- Emitted declarations are not text-postprocessed; retained JSDoc `@import`
comments are allowed when they are non-semantic.
- The packed artifact has no semantic dependency on an unshipped private type
module, and a clean TypeScript consumer type-checks successfully.
- Root `AGENTS.md`, `fjs/AGENTS.md`, `fjs/fsc/README.md`, and the blocked
`@internal` TODO no longer prescribe conflicting rules.

### Related

- [`../fsc/README.md`](../fsc/README.md) — current `_` leak-tolerance policy.
- [`../../AGENTS.md`](../../AGENTS.md) — root repository policy to update.
- [`../AGENTS.md`](../AGENTS.md) — `fjs/`-specific file/dependency policy.
- [`../../todo/blocked/jsdoc-typedef-strip-internal.md`](../../todo/blocked/jsdoc-typedef-strip-internal.md)
— current wait-for-`@internal`/`stripInternal` strategy.
- [microsoft/TypeScript#46407](https://github.com/microsoft/TypeScript/issues/46407)
— upstream JSDoc typedef stripping limitation.
- [`detect-unexported-types-referenced-by-exported-types.md`](./detect-unexported-types-referenced-by-exported-types.md)
— related declaration-leak detection.
- [`document-file-type-naming-conventions.md`](./document-file-type-naming-conventions.md)
— repository source-file roles.
- [`../../todo/migrate-typescript-to-mjs.md`](../../todo/migrate-typescript-to-mjs.md)
— current JavaScript/JSDoc migration and `_` convention.
- [`../ci/todo/f-mjs-package-support.md`](../ci/todo/f-mjs-package-support.md)
— declaration emission and clean package validation.
Loading