diff --git a/spec/README.md b/spec/README.md index ce59f6190c..f7e1bf5314 100644 --- a/spec/README.md +++ b/spec/README.md @@ -130,18 +130,20 @@ The FJS can have functions. The format requires additional run-time information ### 3.3. Priority 3 1. [ ] Regular Expressions. -2. [ ] [type inference](./todo/3370-type-inference.md) -3. [ ] [promise](./todo/3380-promise.md). Needed for JavaScript interop only — +2. [ ] [type-annotations](./todo/3360-type-annotations.md) +3. [ ] [type inference](./todo/3370-type-inference.md) +4. [ ] [promise](./todo/3380-promise.md). Needed for JavaScript interop only — I/O is done with effects and requires no promises (§5). -4. [ ] [class](./todo/3390-class.md) -5. [ ] Temporal classes. See https://github.com/functionalscript/functionalscript/pull/801 +5. [ ] [class](./todo/3390-class.md) +6. [ ] Temporal classes. See https://github.com/functionalscript/functionalscript/pull/801 ### 3.4. Syntactic Sugar 1. [ ] [expression](./todo/3410-expression.md) 2. [ ] [one-parameter](./todo/3420-one-parameter.md) 3. [ ] [assignments](./todo/3430-assignments.md) -4. [ ] `async`/`await`. Depends on the implementation of promises. +4. [ ] [template-literals](./todo/3440-template-literals.md) +5. [ ] `async`/`await`. Depends on the implementation of promises. ## 4. ECMAScript Proposals diff --git a/spec/todo/2460-js-string-literals.md b/spec/todo/2460-js-string-literals.md index 7ce87e28d4..a66305d1a7 100644 --- a/spec/todo/2460-js-string-literals.md +++ b/spec/todo/2460-js-string-literals.md @@ -37,7 +37,8 @@ mechanically: `'x'` → `"x"`, literal TAB → `\t`, `\v` → `\u000b`, `\x41` → `A`. **Note**: template literals are not part of this feature — they involve -expression interpolation, not just lexical syntax. +expression interpolation, not just lexical syntax. They are tracked separately +as [template-literals](./3440-template-literals.md). **Note**: if a universal parser — one that recognizes JSON, DJS, and FS in a single pass — implements this feature, it must still distinguish JS strings diff --git a/spec/todo/3360-type-annotations.md b/spec/todo/3360-type-annotations.md new file mode 100644 index 0000000000..6c534149ef --- /dev/null +++ b/spec/todo/3360-type-annotations.md @@ -0,0 +1,179 @@ +# Type Annotations + +```js +import { number, or, string } from 'functionalscript/fjs/types/rtti/module.f.mjs' + +export const myType = or(number, string) + +export const a /*: myType */ = 'hello' +``` + +An annotation is a comment holding an ordinary expression that evaluates to an +RTTI schema. The compiler loads that expression at compile time and checks the +annotated value against it. + +Depends on [expression](./3410-expression.md) and on the compiler being able to +load and run a module as meta-programming +([`fjs/fsc/todo/47.md`](../../fjs/fsc/todo/47.md)) — nothing here can start +before both. This is a working draft of a direction, not a plan: TypeScript +remains the type checker meanwhile, and the near-term work is to turn the +standard toolchain up as far as it goes +([`todo/strict-static-analysis.md`](../../todo/strict-static-analysis.md)). + +Not to be confused with the TC39 +[Type Annotations](https://github.com/tc39/proposal-type-annotations) proposal +(§4.1), which is erasable syntax with no checker attached. This feature is the +opposite: no new syntax beyond a comment, and a checker that is an ordinary +library. + +## Why not a type language + +TypeScript's answer to typing is a superset of JavaScript with its own type +grammar. JSDoc's answer is the same grammar again, only noisier. Neither is +wanted here: a type should be an ordinary **value**, built from +[`fjs/types/rtti`](../../fjs/types/rtti/README.md), and an annotation should be +an ordinary **expression** naming one. + +`.d.ts` can be generated from the same schemas, and inference should carry as +much of the burden as possible so annotations stay rare. `/*: … */` and JSDoc's +`/** … */` coexist while the tree migrates. + +## What this settles about the parser + +There is no type grammar to write. A JSDoc-shaped design would need one — a +block grammar, and underneath it a grammar for a subset of TypeScript's type +expressions — and that second layer is the superset this project exists to +avoid, re-implemented in the repository's own BNF. The annotation body is an +expression in the module's own scope, which the FunctionalScript parser already +handles. What is needed is only a way to recognize the annotation and hand its +body to the existing expression parser. + +That recognition is nearly free today. The tokenizer keeps a block comment's +body verbatim, so the three forms differ in their first character: + +```js +// export const a /*: myType */ = "hello" +{ kind: '/*', value: ': myType ' } // annotation +{ kind: '/*', value: '* @type {X} ' } // JSDoc +{ kind: '/*', value: ' plain ' } // comment +``` + +which is exactly why the two annotation forms can coexist during the +transition. A distinct token kind would be cleaner than inspecting the first +character, but no new grammar is involved either way. + +## What already exists + +More than half of this is built: + +| Piece | Where | State | +| --- | --- | --- | +| Schema constructors | `fjs/types/rtti/module.f.mjs` | `boolean`, `number`, `string`, `bigint`, `unknown`, `array`, `record`, `or`, `option`, `never`, plus `Const` (primitive / tuple / struct used directly as its own schema) | +| Value checking | `fjs/types/rtti/validate/`, `parse/` | `validate(schema)(value)`, `parse(schema)(value)` | +| Canonical data form | `fjs/types/rtti/data/` | `toData`, `cmp`, `equal`, **`subset`**, data-driven `validate` | +| TypeScript emission | `fjs/types/rtti/ts/module.f.mjs` | runtime printer: `thunk RTTI → toData → dataToTs`, emitting canonical type aliases, recursion included | +| Compile-time bridge | `Ts` in `fjs/types/rtti/ts/types.ts` | maps a schema to its TypeScript type, so `npx tsc` keeps working through the transition | + +Two of these matter more than they look. `data`'s **`subset`** is assignability +as a decidable operation on the canonical form — the primitive a checker needs. +And `ts/module.f.mjs` is already the `.d.ts` generator: schemas in, canonical +TypeScript aliases out. + +## Open questions + +1. **Compile-time evaluation and staging.** Checking `a /*: myType */` requires + evaluating `myType`, which requires evaluating its imports. Which expressions + may an annotation reference — module-level constants only, or anything the + compiler can reduce? What happens when an annotation depends on a value that + is not compile-time known? + +2. **Non-literal right-hand sides.** `validate(myType)('hello')` settles the + literal case. For `const a /*: t */ = f(x)` the checker must infer an RTTI + for `f(x)` and ask `subset(inferred, declared)`. `subset` exists; the + inference does not. This is where "more type inference" has to land, and it + is most of the work. + +3. **Function types.** `Type` has no function case, and FunctionalScript modules + are almost entirely functions — 1318 of the 3772 JSDoc type bodies in the + tree are function types. The schema side is already tracked as + [`fjs/types/rtti/todo/668-rtti-function-types.md`](../../fjs/types/rtti/todo/668-rtti-function-types.md), + which reaches the same conclusion the annotation side needs: a function can + be checked as callable, but its contract is only observable when it is + called. What remains open here is what an annotation on a function should + therefore *mean* — a compile-time check that cannot be completed, or a + wrapper that validates each call. Until that is settled, `/*: */` can join + `@type` but not replace it. + +4. **Generic schemas.** 169 `@template` uses today. A generic type is naturally a + *function from schemas to schemas* — `array` and `record` already are — so + the value layer needs nothing new. What needs design is `Ts<>` and `.d.ts` + emission for a parameterised alias. + +5. **Nominal types.** [`fjs/types/nominal`](../../fjs/types/nominal/module.f.mjs) + has no RTTI representation and no issue of its own. Branding is a + compile-time-only fiction — `asNominal` is `identity` — so either RTTI gains + a nominal wrapper carrying a brand, or nominal types stay a TypeScript-era + construct. + +## Sketch of an order, when the time comes + +1. Recognize `/*: … */` in the compiler's parser and hand its body to the + existing expression parser. +2. Evaluate the annotation expression at compile time + ([`fjs/fsc/todo/47.md`](../../fjs/fsc/todo/47.md)). +3. Generate `.d.ts` from the schemas — `fjs/types/rtti/ts` is already the + printer, so this is plumbing plus a `fjs` command, and it is the step that + could land earliest and independently. +4. Check literal right-hand sides with `validate`. +5. Design inference, then check general right-hand sides with `subset`. +6. Resolve the function-schema question + ([668-rtti-function-types](../../fjs/types/rtti/todo/668-rtti-function-types.md)) + before `/*: */` goes beyond constants. + +## Depends on + +- [`fjs/fsc/todo/47.md`](../../fjs/fsc/todo/47.md) — the compiler loading and + running modules as meta-programming, which is what compile-time evaluation of + an annotation expression means. +- [fjs-nanvm-integration.md](../../todo/fjs-nanvm-integration.md) and + [migrate-typescript-to-mjs.md](../../todo/migrate-typescript-to-mjs.md) — the path to a + compiler that parses authored FunctionalScript. +- [js-string-literals](./2460-js-string-literals.md) — FunctionalScript's string + grammar is JSON's, so the repository's own single-quoted `.mjs` sources are + not yet input the parser accepts. Normalizing them is part of + [migrate-typescript-to-mjs.md](../../todo/migrate-typescript-to-mjs.md) stage 3, not a + tokenizer defect. + +## Consequences for the TypeScript-era work + +- [inline-type-casts.md](../../todo/inline-type-casts.md) stands unchanged. It describes + the code as it is today, and 208 of its 357 sites are noise under any type + system. +- [eslint.md](../../todo/eslint.md)'s `no-inline-type-cast` and `no-unknown-jsdoc-tag` + are **transitional**: worth having while JSDoc is the annotation form, but + they must not be used to justify building a TypeScript-type grammar. Both are + satisfiable by matching on the comment's first character plus the JS token + stream, with no type parsing. +- [tsconfig-strict-flags.md](../../todo/tsconfig-strict-flags.md) and + [strict-static-analysis.md](../../todo/strict-static-analysis.md) are unaffected, and + are the near-term work. `npx tsc` and the standard toolchain remain the + checker until all of the above exists. + +## Related + +- [`fjs/types/rtti/README.md`](../../fjs/types/rtti/README.md) — the schema system + this builds on. +- [`fjs/types/rtti/todo/668-rtti-function-types.md`](../../fjs/types/rtti/todo/668-rtti-function-types.md) — + the schema-side half of open question 3. +- [type inference](./3370-type-inference.md) — the other half: annotations are + only as useful as what can be inferred without them, and open question 2 below + is where the two meet. +- [new-pl.md § Type System](../../todo/new-pl.md#type-system) — the same idea one + level further out: type checking as an opt-in library rather than a language + feature. This document is the FunctionalScript-scoped version. +- [ast-spec.md](../../todo/ast-spec.md) — already specifies the AST with RTTI and + generates Rust from it; the same schemas would feed both. +- [types-for-fs.md](../../todo/types-for-fs.md) — why TypeScript's own type system is not + the target. +- [`fjs/bnf/todo/layered-parser.md`](../../fjs/bnf/todo/layered-parser.md) — the + transducer stack the tokenizer work belongs to. diff --git a/spec/todo/3370-type-inference.md b/spec/todo/3370-type-inference.md index 4fd0e476e2..a46fdc300b 100644 --- a/spec/todo/3370-type-inference.md +++ b/spec/todo/3370-type-inference.md @@ -2,6 +2,10 @@ We need type inference to prove that specific values have specific types. Type annotations can help, but we can't trust them. +See [type-annotations](./3360-type-annotations.md) for the annotation form those +would take: an RTTI schema named by an ordinary expression, checked against the +annotated value at compile time. + ## Level 1 ```rust diff --git a/spec/todo/3440-template-literals.md b/spec/todo/3440-template-literals.md new file mode 100644 index 0000000000..2b23f56f3c --- /dev/null +++ b/spec/todo/3440-template-literals.md @@ -0,0 +1,64 @@ +# Template Literals + +```js +const name = "world" +export default `Hello, ${name}!` +``` + +Untagged template literals: backtick-delimited strings with `${expression}` +substitutions, the `` \` `` and `\${` escapes, and literal line terminators +normalized to `\n`. + +Depends on [expression](./3410-expression.md): a substitution embeds a full +expression, so this is an FJS-level feature, not the lexical sugar that +[js-string-literals](./2460-js-string-literals.md) describes — which is why +that document excludes it. + +## Semantics + +A template literal is sugar for concatenating its cooked string parts with its +substitutions. `` `a${x}b` `` denotes the same value as `"a" + x + "b"` once +`x` is a string. + +## Open questions + +1. **Substitution type.** ECMAScript applies `ToString` to every substitution, + so `` `${1n}` `` is `"1"`, `` `${undefined}` `` is `"undefined"`, and + `` `${obj}` `` calls `obj.toString()`. Implicit coercion of this kind is + what FunctionalScript avoids elsewhere (see the `bigint`/`number` mixing + rule in [new-pl.md](../../todo/new-pl.md)). The alternatives are to require + substitutions to be `string` already, to permit the primitives with an + unambiguous spelling, or to follow ECMAScript exactly for interop. This + should be settled before the feature is implemented, since each choice + makes a different set of programs valid. + +2. **Tagged templates.** `` tag`a${b}c` `` is a function call receiving the + cooked strings, a `raw` property, and the substitutions. It is a separate + feature with its own grammar and a mutable-array-shaped argument; it is not + assumed to be in scope here. + +3. **Canonical form.** Content addressing wants one spelling per value. A + template literal with no substitutions denotes exactly what a JSON string + denotes, so — as with single-quoted strings — the parser has to record which + sub-language a literal stayed within, or normalize on the way in. + +## Rationale for deferring + +Template literals add no values that JSON string syntax cannot already express; +they add a spelling, and — unlike the rest of the JS string forms — an +expression form as well. By the design rule in +[js-string-literals](./2460-js-string-literals.md), alternative spellings of +expressible values are syntactic sugar and get the lowest priority. + +Until implemented, a template literal is mechanically rewritable as +concatenation: `` `a${x}b` `` → `"a" + x + "b"`, and a substitution-free +`` `abc` `` → `"abc"`. + +That rewrite is not free in this repository: 202 of the 260 `.mjs` files use +template literals, with 494 substitutions between them. Like single-quoted +strings, they are part of what +[migrate-typescript-to-mjs.md](../../todo/migrate-typescript-to-mjs.md) stage 3 +has to normalize before the parser accepts the repository's own sources. + +See + diff --git a/todo/eslint.md b/todo/eslint.md new file mode 100644 index 0000000000..1b02c35135 --- /dev/null +++ b/todo/eslint.md @@ -0,0 +1,120 @@ +# ESLint for rules `tsc` cannot express + +**Priority:** P2 +**Status:** open + +### Problem + +The repository has no linter. Everything is enforced either by `npx tsc` or by +review against [AGENTS.md](../fjs/AGENTS.md). That leaves a class of rules with +no mechanical enforcement at all, and the cast audit made the cost visible. + +Three concrete gaps: + +1. **Inline `/** @type {T} */ (expr)` casts.** AGENTS.md asks for an annotated + declaration, `@satisfies`, or `assert*` instead. TypeScript has no option to + ban `as` or its JSDoc equivalent, so the count only moves when a human + notices. [inline-type-casts.md](./inline-type-casts.md) found 357 of them, + 208 of which need no cast at all. Without a check they creep back after the + cleanup. + +2. **Misspelled JSDoc tags are silently ignored.** This compiles clean: + + ```js + /** @tpye {string} */ + export const h = 1 + + export const i = /** @tpye {string} */ (1) + ``` + + Neither annotation exists as far as the compiler is concerned, and nothing + reports it. This is the one place where JSDoc-on-`.mjs` is genuinely weaker + than authored TypeScript — in a `.ts` file the annotation is syntax, so a + typo is a parse error. A rule that rejects unknown tags in `/** … */` blocks + would close it. + +3. **Type predicates and other AGENTS.md prohibitions.** "Avoid type + predicates", "avoid `as`", the `@type {const}` placement rule — all currently + review-only. + +### ESLint is the near-term answer + +`eslint` + `typescript-eslint` gives `no-unnecessary-type-assertion` — which +would have found most of the audit's 181-cast "remove" bucket on its own — +plus `no-unnecessary-condition`, `no-explicit-any`, +`consistent-type-assertions`, and a plugin surface for the three rules above. + +The cost is real and should be stated plainly: the repository has exactly two +devDependencies (`typescript`, `@types/node`) and no other JavaScript tooling. +ESLint with a TypeScript parser is a large dependency tree, needs its own +config and CI step, and its typed rules re-run the type checker. + +It is still worth it, because it is the only entry in +[strict-static-analysis.md](./strict-static-analysis.md) that reaches +**type-aware** rules. Everything else there — `tsc` flags, `deno lint`, +`deno fmt`, `knip`, `publint` — is syntactic or structural. The single most +valuable rule found by the cast audit needs the checker, and nothing but +`typescript-eslint` provides it. + +### The `fjs lint` alternative, and why it waits + +FunctionalScript has its own tokenizer, parser and CLI, so a `fjs lint` +subcommand could carry the three syntactic rules with no dependency at all, and +would exercise the compiler on the repository itself. That is the right +long-term home for them. + +It is not the near-term answer, for two reasons. FunctionalScript's string +grammar is JSON's — double quotes only, by design +([`spec/todo/2460-js-string-literals.md`](../spec/todo/2460-js-string-literals.md)) — so the +tokenizer does not accept the single-quoted, template-literal `.mjs` sources +this repository is currently written in, and a linter built on it could not +read the code it is meant to check until those sources are normalized. And the +rules it would carry are the ones ESLint would carry anyway. Deferring it costs +nothing; deferring ESLint leaves the type-aware rules unrun. + +### Whichever route: do not build a JSDoc type grammar + +An earlier revision of this issue proposed parsing JSDoc properly: a block +grammar plus a grammar for the subset of TypeScript's type expressions the tree +uses. The second half is a mistake. FunctionalScript's direction is +[`/*: type */` annotations checked by RTTI](../spec/todo/3360-type-annotations.md), where a +type is an ordinary value and an annotation is an ordinary expression — so a +TypeScript-type grammar would be re-implementing, in the repository's own BNF, +exactly the superset the project exists to avoid. + +All three rules are satisfiable without it. The tokenizer keeps a block +comment's body verbatim, so `'* @type {X} '` (JSDoc), `': myType '` (annotation) +and `' plain '` (comment) are told apart by the first character; the rules then +need only the JS token stream around them. That is a recognizer, not a type +parser, and it stays correct when JSDoc goes away. + +These rules are **transitional** in the sense that they police the JSDoc era — +but that era is the whole of the foreseeable future, since `/*: type */` waits +on the compiler. They are worth writing now. + +### Proposal + +1. Decide whether the dependency is acceptable. This is a policy call, not a + technical blocker, and it is the only thing standing in the way. +2. Land `eslint` + `typescript-eslint` with the recommended type-aware set, + starting from `no-unnecessary-type-assertion`. +3. Add the three custom rules — inline `@type` cast, unknown JSDoc tag, type + predicate — since they are what AGENTS.md already forbids and nothing checks. +4. Add it to the generated workflow via `fjs/ci/` (not to `ci.yml` directly), + next to `npx tsc` and `fjs test`. +5. Gate the cast rule behind an allowlist or a warning level until + [inline-type-casts.md](./inline-type-casts.md) is worked through, so the + cleanup and the enforcement can land independently. + +### Related + +- [inline-type-casts.md](./inline-type-casts.md) — the 357 sites this would keep + from regressing. +- [tsconfig-strict-flags.md](./tsconfig-strict-flags.md) — what `tsc` *can* + enforce, and at what cost. +- [strict-static-analysis.md](./strict-static-analysis.md) — the umbrella: every + standard tool that could check this code base, and where ESLint sits among + them. +- [`spec/todo/3360-type-annotations.md`](../spec/todo/3360-type-annotations.md) — + the eventual type layer, gated on the compiler; it does not change what to do + now. diff --git a/todo/inline-type-casts.md b/todo/inline-type-casts.md new file mode 100644 index 0000000000..5925e9a287 --- /dev/null +++ b/todo/inline-type-casts.md @@ -0,0 +1,487 @@ +# Audit: inline `/** @type {T} */ (v)` casts + +**Priority:** P2 +**Status:** open + +### Problem + +[AGENTS.md](../fjs/AGENTS.md) ("Avoid `as` type assertions") says an inline +`/** @type {T} */ (expr)` cast is the JSDoc equivalent of `as` and carries the +same hazard: it overrides whatever the compiler inferred and is erased at +runtime. It asks for one of three things instead — an annotated declaration +(`/** @type {T} */ const o = v`), `/** @satisfies {T} */ (v)` when the goal is +to *check* rather than *override*, or `assert` / `assertNotNullish` from +[`fjs/asserts/module.f.mjs`](../fjs/asserts/module.f.mjs) when the claim is an +invariant a runtime check can verify. + +`@type {const}` is explicitly excluded: it must stay an inline cast. + +This issue is the audit of every remaining site. + +### Method + +Every `/** @type {T} */ (…)` in `fjs/` was enumerated mechanically, then each +site was probed against a clean `npx tsc` baseline (TypeScript 7.0.2, the +repository `tsconfig.json`) in two variants: + +1. **delete the cast** — if `tsc` still passes, the cast is redundant; +2. **`@type` → `@satisfies`** — if that passes, the expression really is + assignable to `T`, so the cast only *pins* a type and never overrides one. + The same check licenses the annotated-declaration form, which checks the + initializer against `T` the same way. + +Sites failing both are inspected by hand: the compiler error says what the cast +is actually hiding, which decides between `assert*` and "no replacement +available". + +### Findings + +Counts exclude 221 `/** @type {const} */` casts (out of scope) and the one +existing `@satisfies`. + +| Verdict | Count | Meaning | +| --- | --- | --- | +| **remove** | 181 | The cast is redundant: `npx tsc` passes with it deleted. No replacement needed at all. | +| **`@satisfies`** | 21 | Expression is assignable to `T`; the cast pins a type rather than overriding one. Use `@satisfies`, or hoist to an annotated `const`. | +| **declare** | 6 | Same, and the cast is already the whole initializer of a `const`, so the annotated-declaration form is a direct rewrite. | +| **assert** | 72 | A runtime check can establish the claim: `assertNotNullish`, `assert(typeof …)`, `assert(x instanceof Array)`, or a literal-range `assert`. | +| **keep** | 77 | None of the three applies: `any` bridges, generic erasure, nominal branding, TS2589 depth limits, or a genuine type/API mismatch the cast is papering over. | +| **total** | **357** | | + +So **208 of 357 (58%) need no cast and no replacement machinery at all** — 181 +simply delete, 27 become a check instead of an override. Another 72 have a real +runtime check available. Only 77 are load-bearing. + +Deleting the 181 is not quite one sweep: 172 of them come out together with +`npx tsc` still clean, but the 9 in `fjs/types/rtti/parse/module.f.mjs` and +`fjs/types/rtti/validate/module.f.mjs` are each redundant *individually* and +trip TS2589 ("instantiation excessively deep") when removed as a group. Those +two files need one cast at a time, keeping whichever removal the depth limit +still tolerates. + +#### Sub-findings worth acting on first + +- **`fjs/js/tokenizer/module.f.mjs`** — 37 of its 39 casts are redundant. The + `_CreateToToken<…>` casts on the arrow literals passed to + `rangeFunc(…)`/`rangeSetFunc(…)` are already contextually typed through + `create(def)(…)`; deleting all of them keeps `npx tsc` green. +- **`Array.isArray` never narrows `readonly T[]` out of a union.** Eight `Dir` + casts (`fjs/ci/proof.f.mjs`, `fjs/effects/node/proof.f.mjs`, + `fjs/effects/node/virtual/module.f.mjs`) sit right after a guard that *looks* + like it should have narrowed. Replacing `Array.isArray(x)` with + `x instanceof Array` makes the existing `assert` / `if` narrow, and all eight + casts delete — as does the ninth `Dir` cast in the same family, which is + already redundant. Verified: `npx tsc` clean. +- **`unknown` in MCP proofs** — 40 of the 72 `assert` candidates are + `fjs/protocol/mcp/proof.f.mjs`, `fjs/mcp/proof.f.mjs` and `fjs/cas/proof.f.mjs` + reaching into a JSON-RPC response typed `unknown`. Each cast is an unchecked + claim about a response shape the proof is supposed to be testing. A small set + of `assert`-based accessors (`errorCode(resp)`, `resultOf(resp)`, + `textOf(resp)`) — or rtti `validate` — would replace all of them and make the + proofs actually assert what they claim. +- **Literal-range casts are assertable.** `fjs/asn.1/module.f.mjs:68` + (`_ClassPc`) and `fjs/types/function/compare/module.f.mjs:12,17` + (`Index<3>`, `Index<5>`) narrow an arithmetic result to a literal union; + `assert(i === 0 || i === 1 || i === 2)` narrows identically and checks. + Verified: `npx tsc` clean. + +#### What genuinely has to stay + +- `/** @type {any} */` bridges inside the rtti visitors + (`fjs/types/rtti/{parse,validate,common,data}`) and `fjs/effects/module.f.mjs` + — generic erasure with no runtime counterpart, several hitting TS2589. +- `asNominal` / `asBase` in `fjs/types/nominal/module.f.mjs` — branding + `identity`, by construction unrepresentable. +- `fjs/bnf/descent/module.f.mjs:199` — documented TS7022 cycle cut. +- The `TS2322`/`TS2345`/`TS2352`/`TS2339` group: the cast is overriding a real + mismatch (e.g. `do_('memRead')` typed as `(key: Key) => Effect`, + `Unknown` vs `unknown` parameters in the MCP proofs, `Index`/tuple arity). + These are the ones AGENTS.md means by "it usually means the types or the code + structure should be improved instead" — each needs an API change, not a + different cast syntax. + +### Proposal + +Land in this order, each step independently verifiable with `npx tsc` and `fjs t`: + +1. Delete the 172 redundant casts outside the two rtti visitors; then take the + remaining 9 one at a time. +2. Swap `Array.isArray` → `instanceof Array` at the eight `Dir` sites and delete + those casts. +3. Convert the 21 + 6 checking casts to `@satisfies` / annotated declarations. +4. Introduce the `assert`-based accessors for the MCP/CAS proofs and convert the + 72 `assert` candidates. +5. For the remaining 77, open follow-up issues per type/API problem rather than + rewriting the cast. + +### Related + +- [tsconfig-strict-flags.md](./tsconfig-strict-flags.md) — + `noUncheckedIndexedAccess` overlaps with this issue's `assert` bucket and + should land after it. +- [eslint.md](./eslint.md) — no `tsc` flag can ban an inline cast, so without a + linter this count creeps back after the cleanup. + +### Full table + +Verdict per site. `Line` is the line of the opening `/**` in the current tree. + +| File | Line | `@type {T}` | Verdict | Replacement / why not | +| --- | --- | --- | --- | --- | +| `fjs/asn.1/module.f.mjs` | 68 | `_ClassPc` | **assert** | `assert(v === … \|\| …)` narrows the literal range | +| `fjs/bnf/descent/module.f.mjs` | 199 | `_Task` | keep | breaks a control-flow inference cycle (TS7022) | +| `fjs/bnf/ll1/module.f.mjs` | 98 | `_DispatchRule` | **assert** | `assertNotNullish` / `assert(v !== undefined)` | +| `fjs/bnf/ll1/module.f.mjs` | 119 | `_DispatchRule` | **assert** | `assertNotNullish` / `assert(v !== undefined)` | +| `fjs/bnf/ll1/module.f.mjs` | 258 | `_DispatchRule` | **assert** | `assertNotNullish` / `assert(v !== undefined)` | +| `fjs/cas/evo/module.f.mjs` | 70 | `Cache` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/cas/evo/module.f.mjs` | 284 | `Result` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/cas/evo/module.f.mjs` | 454 | `List>` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/cas/evo/module.f.mjs` | 457 | `Effect>` | keep | cast overrides the inferred type (TS2345) — needs a type/API change, not a check | +| `fjs/cas/evo/proof.f.mjs` | 46 | `IoResult` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/cas/evo/proof.f.mjs` | 60 | `IoResult` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/cas/evo/proof.f.mjs` | 61 | `IoResult` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/cas/evo/proof.f.mjs` | 76 | `List>` | **`@satisfies`** | `@satisfies` (checks instead of overrides); or hoist to an annotated `const` | +| `fjs/cas/evo/proof.f.mjs` | 88 | `List>` | **`@satisfies`** | `@satisfies` (checks instead of overrides); or hoist to an annotated `const` | +| `fjs/cas/evo/proof.f.mjs` | 96 | `List>` | **`@satisfies`** | `@satisfies` (checks instead of overrides); or hoist to an annotated `const` | +| `fjs/cas/evo/proof.f.mjs` | 107 | `List>` | **`@satisfies`** | `@satisfies` (checks instead of overrides); or hoist to an annotated `const` | +| `fjs/cas/evo/proof.f.mjs` | 123 | `List>` | **`@satisfies`** | `@satisfies` (checks instead of overrides); or hoist to an annotated `const` | +| `fjs/cas/evo/proof.f.mjs` | 585 | `List>` | **`@satisfies`** | `@satisfies` (checks instead of overrides); or hoist to an annotated `const` | +| `fjs/cas/evo/proof.f.mjs` | 609 | `List>` | **`@satisfies`** | `@satisfies` (checks instead of overrides); or hoist to an annotated `const` | +| `fjs/cas/module.f.mjs` | 252 | `(result: IoResult) => List) => List>` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/cas/module.f.mjs` | 348 | `(v: Vec) => Effect>` | keep | cast overrides the inferred type (TS2345) — needs a type/API change, not a check | +| `fjs/cas/proof.f.mjs` | 56 | `readonly unknown[]` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/cas/proof.f.mjs` | 91 | `Parameters[0]` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/cas/proof.f.mjs` | 108 | `any` | keep | `any` / `unknown` escape hatch — nothing to check at runtime | +| `fjs/cas/proof.f.mjs` | 246 | `List>` | **`@satisfies`** | `@satisfies` (checks instead of overrides); or hoist to an annotated `const` | +| `fjs/cas/proof.f.mjs` | 291 | `List>` | **`@satisfies`** | `@satisfies` (checks instead of overrides); or hoist to an annotated `const` | +| `fjs/cas/proof.f.mjs` | 310 | `List>` | **`@satisfies`** | `@satisfies` (checks instead of overrides); or hoist to an annotated `const` | +| `fjs/cas/proof.f.mjs` | 343 | `List>` | **`@satisfies`** | `@satisfies` (checks instead of overrides); or hoist to an annotated `const` | +| `fjs/cas/proof.f.mjs` | 360 | `List>` | **`@satisfies`** | `@satisfies` (checks instead of overrides); or hoist to an annotated `const` | +| `fjs/cas/proof.f.mjs` | 374 | `IoResult` | **assert** | value is `unknown` (JSON / IO): needs a real check — `assert(typeof …)`, `in`, or rtti `validate`/`parse` | +| `fjs/cas/proof.f.mjs` | 375 | `IoResult` | **assert** | value is `unknown` (JSON / IO): needs a real check — `assert(typeof …)`, `in`, or rtti `validate`/`parse` | +| `fjs/cas/proof.f.mjs` | 387 | `IoResult` | **assert** | value is `unknown` (JSON / IO): needs a real check — `assert(typeof …)`, `in`, or rtti `validate`/`parse` | +| `fjs/cas/proof.f.mjs` | 388 | `IoResult` | **assert** | value is `unknown` (JSON / IO): needs a real check — `assert(typeof …)`, `in`, or rtti `validate`/`parse` | +| `fjs/cas/proof.f.mjs` | 403 | `IoResult` | **assert** | value is `unknown` (JSON / IO): needs a real check — `assert(typeof …)`, `in`, or rtti `validate`/`parse` | +| `fjs/cas/proof.f.mjs` | 404 | `IoResult` | **assert** | value is `unknown` (JSON / IO): needs a real check — `assert(typeof …)`, `in`, or rtti `validate`/`parse` | +| `fjs/cas/proof.f.mjs` | 417 | `IoResult` | **assert** | value is `unknown` (JSON / IO): needs a real check — `assert(typeof …)`, `in`, or rtti `validate`/`parse` | +| `fjs/cas/proof.f.mjs` | 418 | `IoResult` | **assert** | value is `unknown` (JSON / IO): needs a real check — `assert(typeof …)`, `in`, or rtti `validate`/`parse` | +| `fjs/cas/proof.f.mjs` | 457 | `List>` | **`@satisfies`** | `@satisfies` (checks instead of overrides); or hoist to an annotated `const` | +| `fjs/ci/nix/module.f.mjs` | 94 | `(id: string) => string` | **declare** | hoist to `/** @type {(id: string) => string} */ const flakePath = …` | +| `fjs/ci/nix/module.f.mjs` | 100 | `(id: string, command: string) => string` | **declare** | hoist to `/** @type {(id: string, command: string) => string} */ const nixDevelop = …` | +| `fjs/ci/proof.f.mjs` | 43 | `Dir` | **assert** | guard uses `Array.isArray`, which never removes `readonly T[]` from a union — swap for `instanceof Array` and the existing check narrows | +| `fjs/crypto/sign/proof.f.mjs` | 65 | `any` | keep | `any` / `unknown` escape hatch — nothing to check at runtime | +| `fjs/djs/module.f.mjs` | 41 | `(result: Result) => Effect<_CompileOp, …` | keep | cast overrides the inferred type (TS2345) — needs a type/API change, not a check | +| `fjs/djs/parser/module.f.mjs` | 511 | `AstModule` | **`@satisfies`** | `@satisfies` (checks instead of overrides); or hoist to an annotated `const` | +| `fjs/djs/proof.f.mjs` | 14 | `readonly Vec[]` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/djs/tokenizer/module.f.mjs` | 295 | `TokenMetadata` | keep | cast overrides the inferred type (TS2322) — needs a type/API change, not a check | +| `fjs/djs/tokenizer/module.f.mjs` | 304 | `ReadonlySet` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/djs/tokenizer/module.f.mjs` | 393 | `JsToken` | keep | cast overrides the inferred type (TS2322) — needs a type/API change, not a check | +| `fjs/djs/tokenizer/module.f.mjs` | 406 | `JsToken` | keep | cast overrides the inferred type (TS2322) — needs a type/API change, not a check | +| `fjs/djs/tokenizer/module.f.mjs` | 468 | `Unknown` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/djs/tokenizer/module.f.mjs` | 470 | `DescentMatch` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/djs/tokenizer/module.f.mjs` | 485 | `Unknown` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/djs/tokenizer/module.f.mjs` | 508 | `DescentMatch` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/djs/tokenizer/proof.f.mjs` | 889 | `Unknown` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/djs/tokenizer/proof.f.mjs` | 893 | `Unknown` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/djs/tokenizer/proof.f.mjs` | 897 | `Unknown` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/djs/tokenizer/proof.f.mjs` | 901 | `Unknown` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/djs/tokenizer/proof.f.mjs` | 909 | `Unknown` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/djs/tokenizer/proof.f.mjs` | 913 | `Unknown` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/djs/tokenizer/proof.f.mjs` | 918 | `Unknown` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/djs/tokenizer/proof.f.mjs` | 924 | `Unknown` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/djs/tokenizer/proof.f.mjs` | 928 | `Unknown` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/djs/tokenizer/proof.f.mjs` | 933 | `Unknown` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/djs/transpiler/module.f.mjs` | 103 | `(context: ParseContext) => Effect` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/effects/memory/module.f.mjs` | 34 | `(value: T) => Effect>` | keep | cast overrides the inferred type (TS2322) — needs a type/API change, not a check | +| `fjs/effects/memory/module.f.mjs` | 39 | `(key: Key) => Effect` | keep | cast overrides the inferred type (TS2322) — needs a type/API change, not a check | +| `fjs/effects/memory/module.f.mjs` | 44 | `(key: Key, value: T) => Effect` | **declare** | hoist to `/** @type {(key: Key, value: T) => Effect} */ const write = …` | +| `fjs/effects/module.f.mjs` | 379 | `(...payload: readonly unknown[]) => R` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/effects/module.f.mjs` | 380 | `any` | keep | `any` / `unknown` escape hatch — nothing to check at runtime | +| `fjs/effects/node/memory/module.mjs` | 60 | `ToAsyncOperationMap` | keep | cast overrides the inferred type (TS2345) — needs a type/API change, not a check | +| `fjs/effects/node/memory/proof.mjs` | 28 | `import('../../types.ts').ToAsyncOperationMap` | keep | cast overrides the inferred type (TS2345) — needs a type/API change, not a check | +| `fjs/effects/node/module.f.mjs` | 38 | `{ readonly code?: unknown }` | **assert** | value is `unknown` (JSON / IO): needs a real check — `assert(typeof …)`, `in`, or rtti `validate`/`parse` | +| `fjs/effects/node/module.f.mjs` | 47 | `(...a: readonly Effect[]) => E…` | keep | cast overrides the inferred type (TS2322) — needs a type/API change, not a check | +| `fjs/effects/node/module.f.mjs` | 57 | `any` | keep | `any` / `unknown` escape hatch — nothing to check at runtime | +| `fjs/effects/node/module.f.mjs` | 176 | `Effect>` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/effects/node/module.f.mjs` | 186 | `(listener: RequestListener) => Effec…` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/effects/node/module.f.mjs` | 207 | `Func` | **declare** | hoist to `/** @type {Func} */ const write = …` | +| `fjs/effects/node/module.f.mjs` | 219 | `Console` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/effects/node/module.f.mjs` | 222 | `Console` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/effects/node/module.f.mjs` | 227 | `Func` | **declare** | hoist to `/** @type {Func} */ const read = …` | +| `fjs/effects/node/module.mjs` | 118 | `T` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/effects/node/module.mjs` | 193 | `Uint8Array \| null` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/effects/node/module.mjs` | 287 | `Erl` | keep | cast overrides the inferred type (TS2345) — needs a type/API change, not a check | +| `fjs/effects/node/module.mjs` | 305 | `_Server` | **assert** | value is `unknown` (JSON / IO): needs a real check — `assert(typeof …)`, `in`, or rtti `validate`/`parse` | +| `fjs/effects/node/proof.f.mjs` | 85 | `Dir` | **assert** | guard uses `Array.isArray`, which never removes `readonly T[]` from a union — swap for `instanceof Array` and the existing check narrows | +| `fjs/effects/node/proof.f.mjs` | 125 | `{ code?: unknown }` | **assert** | value is `unknown` (JSON / IO): needs a real check — `assert(typeof …)`, `in`, or rtti `validate`/`parse` | +| `fjs/effects/node/proof.f.mjs` | 276 | `Dir` | **assert** | guard uses `Array.isArray`, which never removes `readonly T[]` from a union — swap for `instanceof Array` and the existing check narrows | +| `fjs/effects/node/proof.f.mjs` | 316 | `never` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/effects/node/proof.f.mjs` | 324 | `never` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/effects/node/proof.f.mjs` | 354 | `readonly Vec[]` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/effects/node/proof.f.mjs` | 364 | `Dir` | **assert** | guard uses `Array.isArray`, which never removes `readonly T[]` from a union — swap for `instanceof Array` and the existing check narrows | +| `fjs/effects/node/virtual/module.f.mjs` | 50 | `Dir` | **assert** | guard uses `Array.isArray`, which never removes `readonly T[]` from a union — swap for `instanceof Array` and the existing check narrows | +| `fjs/effects/node/virtual/module.f.mjs` | 152 | `Dir` | **assert** | guard uses `Array.isArray`, which never removes `readonly T[]` from a union — swap for `instanceof Array` and the existing check narrows | +| `fjs/effects/node/virtual/module.f.mjs` | 198 | `Dir` | **assert** | guard uses `Array.isArray`, which never removes `readonly T[]` from a union — swap for `instanceof Array` and the existing check narrows | +| `fjs/effects/node/virtual/module.f.mjs` | 225 | `Dir` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/effects/node/virtual/module.f.mjs` | 238 | `Dir` | **assert** | guard uses `Array.isArray`, which never removes `readonly T[]` from a union — swap for `instanceof Array` and the existing check narrows | +| `fjs/effects/node/virtual/module.f.mjs` | 329 | `readonly Vec[]` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/effects/node/virtual/module.f.mjs` | 345 | `readonly Vec[]` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/effects/node/virtual/module.f.mjs` | 362 | `Key` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/effects/node/virtual/module.f.mjs` | 407 | `SandboxResult` | keep | cast overrides the inferred type (TS2322) — needs a type/API change, not a check | +| `fjs/effects/node/virtual/proof.f.mjs` | 47 | `JsModule` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/effects/node/virtual/proof.f.mjs` | 113 | `JsModule` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/effects/node/virtual/proof.f.mjs` | 119 | `JsModule` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/effects/node/virtual/proof.f.mjs` | 223 | `JsModule` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/effects/node/virtual/proof.f.mjs` | 351 | `JsModule` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/effects/proof.f.mjs` | 32 | `OperationMap<_AddOp, number>` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/effects/proof.f.mjs` | 46 | `OperationMap<_AnyOp, number>` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/effects/proof.f.mjs` | 51 | `readonly number[]` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/effects/proof.f.mjs` | 65 | `readonly number[]` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/effects/proof.f.mjs` | 84 | `string` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/effects/proof.f.mjs` | 85 | `(value: number) => Effect unknown` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/emergent_testing/proof.f.mjs` | 333 | `Parameters` | keep | cast overrides the inferred type (TS2345) — needs a type/API change, not a check | +| `fjs/emergent_testing/proof.f.mjs` | 539 | `unknown[]` | **assert** | guard uses `Array.isArray`, which never removes `readonly T[]` from a union — swap for `instanceof Array` and the existing check narrows | +| `fjs/emergent_testing/proof.f.mjs` | 544 | `unknown[]` | **assert** | guard uses `Array.isArray`, which never removes `readonly T[]` from a union — swap for `instanceof Array` and the existing check narrows | +| `fjs/fsc/proof.f.mjs` | 21 | `any` | keep | `any` / `unknown` escape hatch — nothing to check at runtime | +| `fjs/js/tokenizer/module.f.mjs` | 262 | `JsToken` | keep | cast overrides the inferred type (TS2322) — needs a type/API change, not a check | +| `fjs/js/tokenizer/module.f.mjs` | 341 | `_CreateToToken<_TokenizerState>` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/js/tokenizer/module.f.mjs` | 343 | `_CreateToToken<_TokenizerState>` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/js/tokenizer/module.f.mjs` | 344 | `_CreateToToken<_TokenizerState>` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/js/tokenizer/module.f.mjs` | 345 | `_CreateToToken<_TokenizerState>` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/js/tokenizer/module.f.mjs` | 346 | `_CreateToToken<_TokenizerState>` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/js/tokenizer/module.f.mjs` | 347 | `_CreateToToken<_TokenizerState>` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/js/tokenizer/module.f.mjs` | 348 | `_CreateToToken<_TokenizerState>` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/js/tokenizer/module.f.mjs` | 349 | `_CreateToToken<_TokenizerState>` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/js/tokenizer/module.f.mjs` | 470 | `_CreateToToken<_InvalidNumberState>` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/js/tokenizer/module.f.mjs` | 472 | `_CreateToToken<_InvalidNumberState>` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/js/tokenizer/module.f.mjs` | 487 | `_CreateToToken<_ParseStringState>` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/js/tokenizer/module.f.mjs` | 489 | `_CreateToToken<_ParseStringState>` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/js/tokenizer/module.f.mjs` | 490 | `_CreateToToken<_ParseStringState>` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/js/tokenizer/module.f.mjs` | 491 | `_CreateToToken<_ParseStringState>` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/js/tokenizer/module.f.mjs` | 492 | `_CreateToToken<_ParseStringState>` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/js/tokenizer/module.f.mjs` | 503 | `_CreateToToken<_ParseEscapeCharState>` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/js/tokenizer/module.f.mjs` | 504 | `_CreateToToken<_ParseEscapeCharState>` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/js/tokenizer/module.f.mjs` | 505 | `_CreateToToken<_ParseEscapeCharState>` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/js/tokenizer/module.f.mjs` | 506 | `_CreateToToken<_ParseEscapeCharState>` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/js/tokenizer/module.f.mjs` | 507 | `_CreateToToken<_ParseEscapeCharState>` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/js/tokenizer/module.f.mjs` | 508 | `_CreateToToken<_ParseEscapeCharState>` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/js/tokenizer/module.f.mjs` | 509 | `_CreateToToken<_ParseEscapeCharState>` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/js/tokenizer/module.f.mjs` | 546 | `_CreateToToken<_ParseIdState>` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/js/tokenizer/module.f.mjs` | 566 | `_CreateToToken<_ParseCommentState>` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/js/tokenizer/module.f.mjs` | 568 | `_CreateToToken<_ParseCommentState>` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/js/tokenizer/module.f.mjs` | 573 | `_CreateToToken<_ParseCommentState>` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/js/tokenizer/module.f.mjs` | 575 | `_CreateToToken<_ParseCommentState>` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/js/tokenizer/module.f.mjs` | 576 | `_CreateToToken<_ParseCommentState>` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/js/tokenizer/module.f.mjs` | 581 | `_CreateToToken<_ParseCommentState>` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/js/tokenizer/module.f.mjs` | 583 | `_CreateToToken<_ParseCommentState>` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/js/tokenizer/module.f.mjs` | 584 | `_CreateToToken<_ParseCommentState>` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/js/tokenizer/module.f.mjs` | 585 | `_CreateToToken<_ParseCommentState>` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/js/tokenizer/module.f.mjs` | 600 | `_CreateToToken<_ParseWhitespaceState>` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/js/tokenizer/module.f.mjs` | 601 | `_CreateToToken<_ParseWhitespaceState>` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/js/tokenizer/module.f.mjs` | 612 | `_CreateToToken<_ParseNewLineState>` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/js/tokenizer/module.f.mjs` | 613 | `_CreateToToken<_ParseNewLineState>` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/js/tokenizer/module.f.mjs` | 618 | `_CreateToToken<_EofState>` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/js/tokenizer/module.f.mjs` | 689 | `List>` | keep | cast overrides the inferred type (TS2322) — needs a type/API change, not a check | +| `fjs/mcp/cas/module.f.mjs` | 187 | `(args: Ts) => Effect>` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/mcp/cas/module.f.mjs` | 199 | `(writeResult: IoResult) => Effect` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/mcp/cas/module.f.mjs` | 204 | `Vec` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/mcp/cas/module.f.mjs` | 215 | `(args: Ts) => Effect Effect` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/mcp/evo/module.f.mjs` | 122 | `(args: Ts) => Effect) => Effect) => Effect) => Effect` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/mcp/evo/proof.f.mjs` | 42 | `{ text: string }` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/mcp/proof.f.mjs` | 67 | `Unknown` | keep | cast overrides the inferred type (TS2345) — needs a type/API change, not a check | +| `fjs/mcp/proof.f.mjs` | 102 | `List>` | **`@satisfies`** | `@satisfies` (checks instead of overrides); or hoist to an annotated `const` | +| `fjs/mcp/proof.f.mjs` | 146 | `McpSessionState` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/mcp/proof.f.mjs` | 159 | `{ readonly result: ToolsCallResult }` | **assert** | value is `unknown` (JSON / IO): needs a real check — `assert(typeof …)`, `in`, or rtti `validate`/`parse` | +| `fjs/mcp/proof.f.mjs` | 165 | `{ readonly text: string }` | **assert** | value is `unknown` (JSON / IO): needs a real check — `assert(typeof …)`, `in`, or rtti `validate`/`parse` | +| `fjs/mcp/proof.f.mjs` | 179 | `string` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/mcp/proof.f.mjs` | 183 | `string` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/mcp/proof.f.mjs` | 212 | `readonly unknown[]` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/mcp/proof.f.mjs` | 268 | `readonly unknown[]` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/mcp/proof.f.mjs` | 305 | `{ readonly error?: { readonly code: number }, readonly id: u…` | **assert** | value is `unknown` (JSON / IO): needs a real check — `assert(typeof …)`, `in`, or rtti `validate`/`parse` | +| `fjs/mcp/proof.f.mjs` | 340 | `{ readonly error?: { readonly code: number }, readonly id: u…` | **assert** | value is `unknown` (JSON / IO): needs a real check — `assert(typeof …)`, `in`, or rtti `validate`/`parse` | +| `fjs/mcp/proof.f.mjs` | 347 | `{ result: { tools: readonly { name: string }[] } }` | **assert** | value is `unknown` (JSON / IO): needs a real check — `assert(typeof …)`, `in`, or rtti `validate`/`parse` | +| `fjs/mcp/proof.f.mjs` | 350 | `{ result: { tools: readonly { inputSchema: { type?: string }…` | **assert** | value is `unknown` (JSON / IO): needs a real check — `assert(typeof …)`, `in`, or rtti `validate`/`parse` | +| `fjs/mcp/proof.f.mjs` | 479 | `{ type: string }` | **assert** | value is `unknown` (JSON / IO): needs a real check — `assert(typeof …)`, `in`, or rtti `validate`/`parse` | +| `fjs/mcp/proof.f.mjs` | 494 | `{ type: string }` | **assert** | value is `unknown` (JSON / IO): needs a real check — `assert(typeof …)`, `in`, or rtti `validate`/`parse` | +| `fjs/mcp/proof.f.mjs` | 623 | `object` | **assert** | value is `unknown` (JSON / IO): needs a real check — `assert(typeof …)`, `in`, or rtti `validate`/`parse` | +| `fjs/mcp/proof.f.mjs` | 624 | `object` | **assert** | value is `unknown` (JSON / IO): needs a real check — `assert(typeof …)`, `in`, or rtti `validate`/`parse` | +| `fjs/mcp/proof.f.mjs` | 660 | `string` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/mcp/proof.f.mjs` | 678 | `string` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/mcp/proof.f.mjs` | 705 | `string` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/media/html/module.f.mjs` | 70 | `keyof typeof escapeTable` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/media/json/parser/module.f.mjs` | 67 | `_JsonObject

` | **assert** | `assertNotNullish` / `assert(v !== undefined)` | +| `fjs/media/json/parser/module.f.mjs` | 108 | `_JsonArray

` | **assert** | `assertNotNullish` / `assert(v !== undefined)` | +| `fjs/media/json/parser/module.f.mjs` | 128 | `_JsonObject

` | **assert** | `assertNotNullish` / `assert(v !== undefined)` | +| `fjs/media/json/proof.f.mjs` | 18 | `null` | keep | cast overrides the inferred type (TS2345) — needs a type/API change, not a check | +| `fjs/media/json/proof.f.mjs` | 18 | `unknown` | keep | `any` / `unknown` escape hatch — nothing to check at runtime | +| `fjs/media/json/proof.f.mjs` | 23 | `null` | keep | cast overrides the inferred type (TS2345) — needs a type/API change, not a check | +| `fjs/media/json/proof.f.mjs` | 23 | `unknown` | keep | `any` / `unknown` escape hatch — nothing to check at runtime | +| `fjs/media/json/schema/proof.f.mjs` | 14 | `JsonValue` | keep | cast overrides the inferred type (TS2345) — needs a type/API change, not a check | +| `fjs/media/json/schema/proof.f.mjs` | 14 | `unknown` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/media/json/serializer/module.f.mjs` | 79 | `keyof typeof escapeTable` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/media/type/proof.f.mjs` | 27 | `List>` | **`@satisfies`** | `@satisfies` (checks instead of overrides); or hoist to an annotated `const` | +| `fjs/module.f.mjs` | 56 | `NodeProgram` | **assert** | value is `unknown` (JSON / IO): needs a real check — `assert(typeof …)`, `in`, or rtti `validate`/`parse` | +| `fjs/nanvm/proof.f.mjs` | 59 | `readonly any[]` | **assert** | value is `unknown` (JSON / IO): needs a real check — `assert(typeof …)`, `in`, or rtti `validate`/`parse` | +| `fjs/nanvm/proof.f.mjs` | 124 | `any` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/nanvm/proof.f.mjs` | 125 | `any` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/nanvm/proof.f.mjs` | 160 | `any` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/nanvm/rust/module.f.mjs` | 81 | `readonly Value[]` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/protocol/mcp/module.f.mjs` | 164 | `any` | keep | `any` / `unknown` escape hatch — nothing to check at runtime | +| `fjs/protocol/mcp/module.f.mjs` | 167 | `Ts` | keep | "instantiation excessively deep" (TS2589) | +| `fjs/protocol/mcp/module.f.mjs` | 235 | `McpSessionState` | **declare** | hoist to `/** @type {McpSessionState} */ const uninitializedState = …` | +| `fjs/protocol/mcp/module.f.mjs` | 287 | `InitializedState` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/protocol/mcp/proof.f.mjs` | 62 | `Effect<_Op, ToolsListResult>` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/protocol/mcp/proof.f.mjs` | 67 | `Effect<_Op, ToolsCallResult>` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/protocol/mcp/proof.f.mjs` | 81 | `Effect` | keep | cast overrides the inferred type (TS2322) — needs a type/API change, not a check | +| `fjs/protocol/mcp/proof.f.mjs` | 81 | `unknown` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/protocol/mcp/proof.f.mjs` | 96 | `McpSessionState` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/protocol/mcp/proof.f.mjs` | 97 | `Unknown` | keep | cast overrides the inferred type (TS2345) — needs a type/API change, not a check | +| `fjs/protocol/mcp/proof.f.mjs` | 103 | `McpSessionState` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/protocol/mcp/proof.f.mjs` | 105 | `Unknown` | keep | cast overrides the inferred type (TS2345) — needs a type/API change, not a check | +| `fjs/protocol/mcp/proof.f.mjs` | 106 | `Unknown` | keep | cast overrides the inferred type (TS2345) — needs a type/API change, not a check | +| `fjs/protocol/mcp/proof.f.mjs` | 114 | `McpSessionState` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/protocol/mcp/proof.f.mjs` | 116 | `Unknown` | keep | cast overrides the inferred type (TS2345) — needs a type/API change, not a check | +| `fjs/protocol/mcp/proof.f.mjs` | 117 | `Unknown` | keep | cast overrides the inferred type (TS2345) — needs a type/API change, not a check | +| `fjs/protocol/mcp/proof.f.mjs` | 118 | `Unknown` | keep | cast overrides the inferred type (TS2345) — needs a type/API change, not a check | +| `fjs/protocol/mcp/proof.f.mjs` | 150 | `object` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/protocol/mcp/proof.f.mjs` | 151 | `{ result: { protocolVersion: string } }` | keep | cast overrides the inferred type (TS2339) — needs a type/API change, not a check | +| `fjs/protocol/mcp/proof.f.mjs` | 159 | `{ error: { code: number } }` | **assert** | value is `unknown` (JSON / IO): needs a real check — `assert(typeof …)`, `in`, or rtti `validate`/`parse` | +| `fjs/protocol/mcp/proof.f.mjs` | 177 | `{ error: { code: number } }` | **assert** | value is `unknown` (JSON / IO): needs a real check — `assert(typeof …)`, `in`, or rtti `validate`/`parse` | +| `fjs/protocol/mcp/proof.f.mjs` | 184 | `object` | **assert** | value is `unknown` (JSON / IO): needs a real check — `assert(typeof …)`, `in`, or rtti `validate`/`parse` | +| `fjs/protocol/mcp/proof.f.mjs` | 191 | `object` | **assert** | value is `unknown` (JSON / IO): needs a real check — `assert(typeof …)`, `in`, or rtti `validate`/`parse` | +| `fjs/protocol/mcp/proof.f.mjs` | 197 | `object` | **assert** | value is `unknown` (JSON / IO): needs a real check — `assert(typeof …)`, `in`, or rtti `validate`/`parse` | +| `fjs/protocol/mcp/proof.f.mjs` | 203 | `object` | **assert** | value is `unknown` (JSON / IO): needs a real check — `assert(typeof …)`, `in`, or rtti `validate`/`parse` | +| `fjs/protocol/mcp/proof.f.mjs` | 209 | `{ error: { code: number } }` | **assert** | value is `unknown` (JSON / IO): needs a real check — `assert(typeof …)`, `in`, or rtti `validate`/`parse` | +| `fjs/protocol/mcp/proof.f.mjs` | 230 | `{ error: { code: number } }` | **assert** | value is `unknown` (JSON / IO): needs a real check — `assert(typeof …)`, `in`, or rtti `validate`/`parse` | +| `fjs/protocol/mcp/proof.f.mjs` | 237 | `{ error: { code: number } }` | **assert** | value is `unknown` (JSON / IO): needs a real check — `assert(typeof …)`, `in`, or rtti `validate`/`parse` | +| `fjs/protocol/mcp/proof.f.mjs` | 243 | `{ error: { code: number }; id: unknown }` | **assert** | value is `unknown` (JSON / IO): needs a real check — `assert(typeof …)`, `in`, or rtti `validate`/`parse` | +| `fjs/protocol/mcp/proof.f.mjs` | 244 | `{ error: { code: number }; id: unknown }` | **assert** | value is `unknown` (JSON / IO): needs a real check — `assert(typeof …)`, `in`, or rtti `validate`/`parse` | +| `fjs/protocol/mcp/proof.f.mjs` | 252 | `{ result: ToolsListResult }` | **assert** | value is `unknown` (JSON / IO): needs a real check — `assert(typeof …)`, `in`, or rtti `validate`/`parse` | +| `fjs/protocol/mcp/proof.f.mjs` | 253 | `{ result: ToolsListResult }` | **assert** | value is `unknown` (JSON / IO): needs a real check — `assert(typeof …)`, `in`, or rtti `validate`/`parse` | +| `fjs/protocol/mcp/proof.f.mjs` | 260 | `{ result: ToolsListResult }` | **assert** | value is `unknown` (JSON / IO): needs a real check — `assert(typeof …)`, `in`, or rtti `validate`/`parse` | +| `fjs/protocol/mcp/proof.f.mjs` | 267 | `{ error: { code: number } }` | **assert** | value is `unknown` (JSON / IO): needs a real check — `assert(typeof …)`, `in`, or rtti `validate`/`parse` | +| `fjs/protocol/mcp/proof.f.mjs` | 274 | `{ text: string }` | keep | cast overrides the inferred type (TS2339) — needs a type/API change, not a check | +| `fjs/protocol/mcp/proof.f.mjs` | 274 | `{ result: ToolsCallResult }` | **assert** | value is `unknown` (JSON / IO): needs a real check — `assert(typeof …)`, `in`, or rtti `validate`/`parse` | +| `fjs/protocol/mcp/proof.f.mjs` | 280 | `{ error: { code: number } }` | **assert** | value is `unknown` (JSON / IO): needs a real check — `assert(typeof …)`, `in`, or rtti `validate`/`parse` | +| `fjs/protocol/mcp/proof.f.mjs` | 287 | `{ text: string }` | keep | cast overrides the inferred type (TS2339) — needs a type/API change, not a check | +| `fjs/protocol/mcp/proof.f.mjs` | 287 | `{ result: ToolsCallResult }` | **assert** | value is `unknown` (JSON / IO): needs a real check — `assert(typeof …)`, `in`, or rtti `validate`/`parse` | +| `fjs/protocol/mcp/proof.f.mjs` | 294 | `{ error: { code: number } }` | **assert** | value is `unknown` (JSON / IO): needs a real check — `assert(typeof …)`, `in`, or rtti `validate`/`parse` | +| `fjs/protocol/mcp/proof.f.mjs` | 300 | `{ error: { code: number } }` | **assert** | value is `unknown` (JSON / IO): needs a real check — `assert(typeof …)`, `in`, or rtti `validate`/`parse` | +| `fjs/protocol/mcp/proof.f.mjs` | 307 | `{ error: { code: number } }` | **assert** | value is `unknown` (JSON / IO): needs a real check — `assert(typeof …)`, `in`, or rtti `validate`/`parse` | +| `fjs/protocol/mcp/proof.f.mjs` | 313 | `{ error: { code: number } }` | **assert** | value is `unknown` (JSON / IO): needs a real check — `assert(typeof …)`, `in`, or rtti `validate`/`parse` | +| `fjs/protocol/mcp/proof.f.mjs` | 327 | `(a: Ts) => Effect` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/protocol/mcp/proof.f.mjs` | 332 | `{ readonly text: string }` | keep | cast overrides the inferred type (TS2339) — needs a type/API change, not a check | +| `fjs/protocol/mcp/stdio/proof.f.mjs` | 26 | `{ readonly id?: Id }` | keep | cast overrides the inferred type (TS2322) — needs a type/API change, not a check | +| `fjs/protocol/mcp/stdio/proof.f.mjs` | 54 | `Unknown` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/protocol/mcp/stdio/proof.f.mjs` | 58 | `Unknown` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/protocol/mcp/stdio/proof.f.mjs` | 62 | `Unknown` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/protocol/mcp/stdio/proof.f.mjs` | 125 | `Response` | keep | cast overrides the inferred type (TS2322) — needs a type/API change, not a check | +| `fjs/protocol/mcp/stdio/proof.f.mjs` | 125 | `unknown` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/sul/id/module.f.mjs` | 36 | `Point2D` | **assert** | `assertNotNullish` / `assert(v !== undefined)` | +| `fjs/sul/id/module.f.mjs` | 46 | `V8` | keep | cast overrides the inferred type (TS2345) — needs a type/API change, not a check | +| `fjs/sul/level/hash/module.f.mjs` | 52 | `Id` | **assert** | `assertNotNullish` / `assert(v !== undefined)` | +| `fjs/sul/level/hash/proof.f.mjs` | 83 | `_NodeList[number]` | **assert** | `assertNotNullish` / `assert(v !== undefined)` | +| `fjs/text/code_point/module.f.mjs` | 49 | `List>` | keep | cast overrides the inferred type (TS2322) — needs a type/API change, not a check | +| `fjs/text/sgr/module.f.mjs` | 52 | `string` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/text/sgr/module.f.mjs` | 54 | `string` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/text/sgr/module.f.mjs` | 56 | `string` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/text/sgr/module.f.mjs` | 58 | `string` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/types/bit_vec/proof.f.mjs` | 196 | `bigint` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/types/bit_vec/proof.f.mjs` | 198 | `bigint` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/types/btree/find/module.f.mjs` | 17 | `TNode` | keep | cast overrides the inferred type (TS2322) — needs a type/API change, not a check | +| `fjs/types/btree/find/module.f.mjs` | 29 | `PathItem` | keep | cast overrides the inferred type (TS2345) — needs a type/API change, not a check | +| `fjs/types/btree/find/module.f.mjs` | 33 | `First` | keep | cast overrides the inferred type (TS2322) — needs a type/API change, not a check | +| `fjs/types/btree/remove/module.f.mjs` | 133 | `Path` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/types/function/compare/module.f.mjs` | 12 | `Index<3>` | **assert** | `assert(v === … \|\| …)` narrows the literal range | +| `fjs/types/function/compare/module.f.mjs` | 17 | `Index<5>` | **assert** | `assert(v === … \|\| …)` narrows the literal range | +| `fjs/types/function/compare/module.f.mjs` | 22 | `any` | **`@satisfies`** | `@satisfies` (checks instead of overrides); or hoist to an annotated `const` | +| `fjs/types/function/compare/module.f.mjs` | 22 | `any` | **`@satisfies`** | `@satisfies` (checks instead of overrides); or hoist to an annotated `const` | +| `fjs/types/nominal/module.f.mjs` | 12 | `(b: B) => Nominal(n: Nominal)…` | keep | nominal branding of `identity` — no runtime representation | +| `fjs/types/nominal/proof.f.mjs` | 26 | `_IntersectionSafeId` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/types/nominal/proof.f.mjs` | 27 | `_IntersectionSafeId` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/types/nominal/proof.f.mjs` | 41 | `_SymbolKeyBranded` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/types/nominal/proof.f.mjs` | 42 | `_SymbolKeyBranded` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/types/nominal/proof.f.mjs` | 49 | `_SymbolIntersectionBranded` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/types/nominal/proof.f.mjs` | 49 | `any` | keep | `any` / `unknown` escape hatch — nothing to check at runtime | +| `fjs/types/nominal/proof.f.mjs` | 50 | `_SymbolIntersectionBranded` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/types/nominal/proof.f.mjs` | 50 | `any` | keep | `any` / `unknown` escape hatch — nothing to check at runtime | +| `fjs/types/range_map/module.f.mjs` | 114 | `RangeMapArray` | **`@satisfies`** | `@satisfies` (checks instead of overrides); or hoist to an annotated `const` | +| `fjs/types/rtti/common/module.f.mjs` | 60 | `any` | keep | `any` / `unknown` escape hatch — nothing to check at runtime | +| `fjs/types/rtti/common/module.f.mjs` | 77 | `any` | keep | `any` / `unknown` escape hatch — nothing to check at runtime | +| `fjs/types/rtti/common/module.f.mjs` | 83 | `Struct` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/types/rtti/common/module.f.mjs` | 84 | `Primitive` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/types/rtti/common/module.f.mjs` | 176 | `Const` | **assert** | assert on the visitor tag before the branch | +| `fjs/types/rtti/common/module.f.mjs` | 182 | `Primitive0` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/types/rtti/common/proof.f.mjs` | 22 | `_Entries` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/types/rtti/common/proof.f.mjs` | 22 | `_Entries` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/types/rtti/common/proof.f.mjs` | 27 | `_Entries` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/types/rtti/common/proof.f.mjs` | 42 | `_Entries` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/types/rtti/common/proof.f.mjs` | 53 | `_Entries` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/types/rtti/common/proof.f.mjs` | 61 | `_Entries` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/types/rtti/data/module.f.mjs` | 750 | `Const` | **assert** | assert on the visitor tag before the branch | +| `fjs/types/rtti/module.f.mjs` | 27 | `any` | keep | `any` / `unknown` escape hatch — nothing to check at runtime | +| `fjs/types/rtti/module.f.mjs` | 69 | `any` | keep | `any` / `unknown` escape hatch — nothing to check at runtime | +| `fjs/types/rtti/parse/module.f.mjs` | 101 | `any` | **`@satisfies`** | `@satisfies` (checks instead of overrides); or hoist to an annotated `const` | +| `fjs/types/rtti/parse/module.f.mjs` | 103 | `(v: Unknown) => _ItemResult` | **remove** | redundant on its own, but removing it together with the other `remove` casts in this file trips TS2589 — remove one at a time | +| `fjs/types/rtti/parse/module.f.mjs` | 103 | `any` | **remove** | redundant on its own, but removing it together with the other `remove` casts in this file trips TS2589 — remove one at a time | +| `fjs/types/rtti/parse/module.f.mjs` | 105 | `any` | **`@satisfies`** | `@satisfies` (checks instead of overrides); or hoist to an annotated `const` | +| `fjs/types/rtti/parse/module.f.mjs` | 133 | `_ItemResult` | **remove** | redundant on its own, but removing it together with the other `remove` casts in this file trips TS2589 — remove one at a time | +| `fjs/types/rtti/parse/module.f.mjs` | 133 | `any` | keep | `any` / `unknown` escape hatch — nothing to check at runtime | +| `fjs/types/rtti/parse/module.f.mjs` | 137 | `any` | keep | `any` / `unknown` escape hatch — nothing to check at runtime | +| `fjs/types/rtti/parse/module.f.mjs` | 159 | `any` | keep | `any` / `unknown` escape hatch — nothing to check at runtime | +| `fjs/types/rtti/parse/module.f.mjs` | 159 | `(t: Type) => ValidateE` | **remove** | redundant on its own, but removing it together with the other `remove` casts in this file trips TS2589 — remove one at a time | +| `fjs/types/rtti/parse/module.f.mjs` | 159 | `any` | **remove** | redundant on its own, but removing it together with the other `remove` casts in this file trips TS2589 — remove one at a time | +| `fjs/types/rtti/parse/module.f.mjs` | 185 | `any` | keep | `any` / `unknown` escape hatch — nothing to check at runtime | +| `fjs/types/rtti/parse/module.f.mjs` | 198 | `any` | **remove** | redundant on its own, but removing it together with the other `remove` casts in this file trips TS2589 — remove one at a time | +| `fjs/types/rtti/parse/proof.f.mjs` | 30 | `T` | keep | cast overrides the inferred type (TS2322) — needs a type/API change, not a check | +| `fjs/types/rtti/parse/proof.f.mjs` | 37 | `ValidationError` | **assert** | value is `unknown` (JSON / IO): needs a real check — `assert(typeof …)`, `in`, or rtti `validate`/`parse` | +| `fjs/types/rtti/parse/proof.f.mjs` | 112 | `number` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/types/rtti/parse/proof.f.mjs` | 114 | `number` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/types/rtti/parse/proof.f.mjs` | 121 | `number` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/types/rtti/parse/proof.f.mjs` | 122 | `number` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/types/rtti/parse/proof.f.mjs` | 125 | `number` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/types/rtti/parse/proof.f.mjs` | 126 | `number` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/types/rtti/parse/proof.f.mjs` | 133 | `number` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/types/rtti/parse/proof.f.mjs` | 137 | `number` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/types/rtti/parse/proof.f.mjs` | 199 | `unknown` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/types/rtti/parse/proof.f.mjs` | 227 | `unknown` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/types/rtti/parse/proof.f.mjs` | 316 | `_A` | keep | cast overrides the inferred type (TS2322) — needs a type/API change, not a check | +| `fjs/types/rtti/parse/proof.f.mjs` | 316 | `unknown` | keep | `any` / `unknown` escape hatch — nothing to check at runtime | +| `fjs/types/rtti/proof.f.mjs` | 19 | `readonly unknown[]` | **assert** | `assertNotNullish` / `assert(v !== undefined)` | +| `fjs/types/rtti/validate/module.f.mjs` | 88 | `any` | keep | `any` / `unknown` escape hatch — nothing to check at runtime | +| `fjs/types/rtti/validate/module.f.mjs` | 114 | `any` | keep | `any` / `unknown` escape hatch — nothing to check at runtime | +| `fjs/types/rtti/validate/module.f.mjs` | 120 | `any` | keep | `any` / `unknown` escape hatch — nothing to check at runtime | +| `fjs/types/rtti/validate/module.f.mjs` | 140 | `any` | keep | `any` / `unknown` escape hatch — nothing to check at runtime | +| `fjs/types/rtti/validate/module.f.mjs` | 140 | `(t: Type) => ValidateE` | **remove** | redundant on its own, but removing it together with the other `remove` casts in this file trips TS2589 — remove one at a time | +| `fjs/types/rtti/validate/module.f.mjs` | 140 | `any` | **remove** | redundant on its own, but removing it together with the other `remove` casts in this file trips TS2589 — remove one at a time | +| `fjs/types/rtti/validate/module.f.mjs` | 158 | `any` | keep | `any` / `unknown` escape hatch — nothing to check at runtime | +| `fjs/types/rtti/validate/module.f.mjs` | 171 | `any` | **remove** | redundant on its own, but removing it together with the other `remove` casts in this file trips TS2589 — remove one at a time | +| `fjs/types/rtti/validate/proof.f.mjs` | 23 | `ValidationError` | **assert** | value is `unknown` (JSON / IO): needs a real check — `assert(typeof …)`, `in`, or rtti `validate`/`parse` | +| `fjs/types/rtti/validate/proof.f.mjs` | 110 | `number` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/types/rtti/validate/proof.f.mjs` | 112 | `number` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/types/rtti/validate/proof.f.mjs` | 119 | `number` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/types/rtti/validate/proof.f.mjs` | 120 | `number` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/types/rtti/validate/proof.f.mjs` | 123 | `number` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/types/rtti/validate/proof.f.mjs` | 124 | `number` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/types/rtti/validate/proof.f.mjs` | 131 | `number` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/types/rtti/validate/proof.f.mjs` | 135 | `number` | **remove** | the cast is redundant — `npx tsc` passes with it deleted | +| `fjs/types/rtti/validate/proof.f.mjs` | 307 | `_A` | keep | cast overrides the inferred type (TS2322) — needs a type/API change, not a check | +| `fjs/types/rtti/validate/proof.f.mjs` | 307 | `unknown` | keep | `any` / `unknown` escape hatch — nothing to check at runtime | \ No newline at end of file diff --git a/todo/strict-static-analysis.md b/todo/strict-static-analysis.md new file mode 100644 index 0000000000..a1df3e576a --- /dev/null +++ b/todo/strict-static-analysis.md @@ -0,0 +1,85 @@ +# Check the JavaScript side as strictly as the Rust side + +**Priority:** P2 +**Status:** open + +### Problem + +CI checks the two code bases to very different standards. + +The Rust half is held hard: `cargo clippy -- -D warnings` and +`cargo clippy --release -- -D warnings` across nine targets, `cargo fmt --check`, +and `cargo test` in both profiles on every one of them. + +The JavaScript half runs `npx tsc` and the test suites (`fjs test`, +`node --test`, `deno task cov`, `bun test --coverage`) — and nothing else. No +lint, no formatting check, no unused-code check, no package-correctness check. +`tsconfig.json` leaves eight checking flags off. There is no equivalent of +`-D warnings` anywhere on this side. + +That asymmetry is the gap to close, using well-known tools rather than +home-grown ones. FunctionalScript's own [`/*: type */` + +RTTI direction](../spec/todo/3360-type-annotations.md) is a long way out and +gated on the compiler; until it arrives, `tsc` and the standard JavaScript toolchain are the +checker, and they should be turned up as far as they go. + +### How to add a check + +The workflow is **generated**: `.github/workflows/ci.yml` is written by +`fjs/ci/` via `npm run ci-update`, and CI verifies the committed file matches +(`git add -A && git diff --cached --exit-code`). A new check is a change to +`fjs/ci/`, not to the YAML. Deno and Bun are already installed on every runner, +so tools shipping with them cost no extra setup step. + +### Candidates + +Ordered by value per unit of cost. Each entry says what it catches that the +others do not. + +| Tool | Catches | Cost | Issue | +| --- | --- | --- | --- | +| `tsc` strictness flags | index access without a check, unused locals, property access from index signatures | none — already installed | [tsconfig-strict-flags.md](./tsconfig-strict-flags.md) | +| `deno fmt --check` | formatting drift | none — Deno is on the runners and `deno.json` **already configures** `fmt` (4-space, no semicolons, single quotes, width 80); it simply is not run | this issue | +| `deno lint` | a rule set disjoint from `tsc`'s, no new dependency | none — Deno is on the runners | this issue | +| ESLint + `typescript-eslint` | type-aware rules — `no-unnecessary-type-assertion`, `no-unnecessary-condition`, `no-floating-promises` | a large dependency tree, config, and a CI step | [eslint.md](./eslint.md) | +| `knip` | unused **exports** and dependencies across modules — `noUnusedLocals` is file-local and cannot see these | one devDependency | this issue | +| `publint`, `@arethetypeswrong/cli` | broken `exports`/`types` resolution in the published package — the repo publishes `.d.mts` from `prepack`, which is exactly where these break | two devDependencies, run at pack time | this issue | +| `madge` / `dpdm` | import cycles | one devDependency | this issue | + +One repo-specific check is worth listing beside them: **grep the emitted +`.d.mts` for `any` and `/*elided*/`**. `fjs/AGENTS.md` records a real case where +a `@type {const}` cast made declaration emit give up and write `/*elided*/ any` +— 4 `any` and 2 `/*elided*/` in one module — visible only to a consumer type-checking +against the published declarations. `npm run prepack` already emits them; nothing +inspects the output. + +### Proposal + +1. Enable the four zero-cost `tsc` flags + ([tsconfig-strict-flags.md](./tsconfig-strict-flags.md) step 1). +2. Add `deno fmt --check` and `deno lint` to the generated workflow. Both are + free in setup terms; expect one cleanup commit each. +3. Add the declaration-emit check for `any` / `/*elided*/`. +4. Add `knip`, then `publint` + `attw`. +5. Decide ESLint ([eslint.md](./eslint.md)) — the only entry that costs a real + dependency tree, and the only one that reaches type-aware rules. +6. Work through the remaining `tsc` flags, `noUncheckedIndexedAccess` last since + [inline-type-casts.md](./inline-type-casts.md) shrinks it first. + +Each step lands as its own commit, verifiable with `npx tsc` and `fjs t`. + +### Open question + +Formatting is the one entry likely to be contentious: `deno fmt` would rewrite +the whole tree to its own idea of the configured style, and the repository has a +deliberate hand-maintained layout. It may be worth running it once to see the +size of the diff before committing to it — or scoping it to new files only. + +### Related + +- [tsconfig-strict-flags.md](./tsconfig-strict-flags.md) — the `tsc` half, measured. +- [eslint.md](./eslint.md) — the linter decision. +- [inline-type-casts.md](./inline-type-casts.md) — 357 sites no current check sees. +- [`spec/todo/3360-type-annotations.md`](../spec/todo/3360-type-annotations.md) — + where the type layer is eventually going, and why it does not change what to + do now. diff --git a/todo/tsconfig-strict-flags.md b/todo/tsconfig-strict-flags.md new file mode 100644 index 0000000000..600bb47574 --- /dev/null +++ b/todo/tsconfig-strict-flags.md @@ -0,0 +1,75 @@ +# Additional strictness flags for `tsconfig.json` + +**Priority:** P2 +**Status:** open + +### Problem + +[`tsconfig.json`](../tsconfig.json) enables `strict`, `exactOptionalPropertyTypes`, +`erasableSyntaxOnly` and `verbatimModuleSyntax`, but leaves eight further +checking flags commented out. Nothing records whether they were rejected or +merely never tried, so each new contributor re-asks the question. + +Four of them cost nothing today: the tree is already clean under them. Leaving +them off means the property is unenforced and can silently regress. + +One of them — `noUncheckedIndexedAccess` — matters beyond hygiene. It makes +every index access yield `T | undefined`, which is exactly the obligation +[AGENTS.md](../fjs/AGENTS.md) wants discharged by `assertNotNullish` rather than +by an unchecked cast. Its error sites cluster in the same modules where +[inline-type-casts.md](./inline-type-casts.md) already found `assert` +candidates: `fjs/effects/node/virtual/`, `fjs/bnf/descent/`, +`fjs/types/rtti/data/`, `fjs/sul/level/hash/`. + +### Measurements + +Error counts from `npx tsc --noEmit --` on a clean tree (TypeScript +7.0.2), one flag at a time: + +| Flag | New errors | Notes | +| --- | --: | --- | +| `noImplicitReturns` | 0 | free | +| `noFallthroughCasesInSwitch` | 0 | free | +| `noImplicitOverride` | 0 | free | +| `isolatedModules` | 0 | free | +| `noUnusedParameters` | 8 | | +| `noPropertyAccessFromIndexSignature` | 31 | | +| `noUncheckedIndexedAccess` | 202 | the one with design value, see above | +| `noUnusedLocals` | 209 | 130 `TS6196` + 79 `TS6133`, see below | + +`noUnusedLocals` splits into two unrelated populations: + +- **130 `TS6196`** — type names pulled in by a JSDoc + `@import { … } from './types.ts'` list and never referenced. `@import` lists + drift as a module changes and nothing catches it today; this is real dead + weight and the only JSDoc-specific hygiene gap the audit found. +- **79 `TS6133`** — unused values, e.g. the ASN.1 universal tag constants + (`eoc`, `bitString`, `null_`, `external`, …) kept as documentation of the tag + space. Those are deliberate; enabling the flag forces a decision about them + (export, drop, or annotate). + +### Proposal + +1. Enable the four zero-cost flags now, in one commit, to lock in properties the + tree already has: `noImplicitReturns`, `noFallthroughCasesInSwitch`, + `noImplicitOverride`, `isolatedModules`. +2. Enable `noUnusedParameters` (8 sites) and `noPropertyAccessFromIndexSignature` + (31 sites) as small follow-ups. +3. Take `noUncheckedIndexedAccess` as its own task, sequenced **after** the + `assert` conversions in [inline-type-casts.md](./inline-type-casts.md) — the + two overlap, and doing the casts first shrinks the 202. +4. For `noUnusedLocals`, fix the 130 stale `@import` entries first; that is + worth doing on its own even if the flag stays off. Decide the 79 unused + values separately. + +Each step is independently verifiable with `npx tsc` and `fjs t`. + +### Related + +- [strict-static-analysis.md](./strict-static-analysis.md) — the umbrella this + is the first step of. +- [inline-type-casts.md](./inline-type-casts.md) — the cast audit; overlaps with + `noUncheckedIndexedAccess`. +- [eslint.md](./eslint.md) — the rules no `tsc` flag can express. +- [123-tsgo-types-node.md](./123-tsgo-types-node.md) — the other open + `tsconfig.json` question.