diff --git a/changelog/unreleased/1589.md b/changelog/unreleased/1589.md new file mode 100644 index 0000000000..1c4264b9a0 --- /dev/null +++ b/changelog/unreleased/1589.md @@ -0,0 +1,13 @@ +- removes 273 of the 357 inline `/** @type {T} */ (v)` casts under `fjs/`, + which AGENTS.md asks to avoid: 182 were redundant outright, 23 became + `@satisfies` or an annotated declaration, and 68 became the runtime check + they stood in for — `assertNotNullish`, a discriminant `assert`, or a + checked accessor in the MCP and CAS proofs, where a response is `unknown` + and its shape is the very thing the proof exists to establish +- `effects/node/virtual`, `effects/node` and `ci` proofs narrow `_Entity` to + `Dir` with `instanceof Array` rather than a cast; `Array.isArray` narrows to + `any[]`, which `readonly Vec[]` is not assignable to, so its negative branch + never removed a `readonly` array from the union +- `fjs run` asserts a module's `main` is callable before invoking it, failing + with the file name instead of `main is not a function` from inside the + effect runner diff --git a/fjs/asn.1/module.f.mjs b/fjs/asn.1/module.f.mjs index 9da03a9ade..35b997354c 100644 --- a/fjs/asn.1/module.f.mjs +++ b/fjs/asn.1/module.f.mjs @@ -19,6 +19,7 @@ import { vec, vec8, } from '../types/bit_vec/module.f.mjs' +import { assert } from '../asserts/module.f.mjs' import { identity } from '../types/function/module.f.mjs' import { max } from '../types/function/compare/module.f.mjs' import { encode as b128encode, decode as b128decode } from '../basen/base128/module.f.mjs' @@ -65,7 +66,13 @@ const parsedTagEncode = ([classPc, number]) => { /** @type {(v: Vec) => readonly[_ParsedTag, Vec]} */ const parsedTagDecode = v => { const [firstByte, rest] = pop8(v) - const classPc = /** @type {_ClassPc} */(firstByte & classPcMask) + const classPc = firstByte & classPcMask + // `classPcMask` is the top three bits, so the result is one of eight values; + // the assert is what narrows `bigint` to `_ClassPc`. + assert(classPc === 0b000_00000n || classPc === 0b001_00000n + || classPc === 0b010_00000n || classPc === 0b011_00000n + || classPc === 0b100_00000n || classPc === 0b101_00000n + || classPc === 0b110_00000n || classPc === 0b111_00000n, classPc) const firstByteNumber = firstByte & tagNumberMask const [number, rest1] = firstByteNumber < tagNumberMask ? [firstByteNumber, rest] diff --git a/fjs/bnf/ll1/module.f.mjs b/fjs/bnf/ll1/module.f.mjs index 9c2fc4aa0e..d53d37c660 100644 --- a/fjs/bnf/ll1/module.f.mjs +++ b/fjs/bnf/ll1/module.f.mjs @@ -25,6 +25,7 @@ */ import { strictEqual } from '../../types/function/operator/module.f.mjs' +import { assertNotNullish } from '../../asserts/module.f.mjs' import { toArray } from '../../types/list/module.f.mjs' import { rangeMap } from '../../types/range_map/module.f.mjs' import { contains, set } from '../../types/string_set/module.f.mjs' @@ -95,7 +96,7 @@ export const dispatchMap = ruleSet => { result = result.map(x => [addRuleToDispatch(x[0], item), x[1]]) } else { dm = dispatchRule(dm, item, newCurrent) - const dr = /** @type {_DispatchRule} */ (dm[item]) + const dr = assertNotNullish(dm[item]) if (emptyTag === true) { result = result.map(x => [addRuleToDispatch(x[0], item), x[1]]) result = toArray(dispatchOp.merge(result)(dr.rangeMap)) @@ -116,7 +117,7 @@ export const dispatchMap = ruleSet => { let emptyTag = undefined for (const [tag, item] of entries) { dm = dispatchRule(dm, item, newCurrent) - const dr = /** @type {_DispatchRule} */ (dm[item]) + const dr = assertNotNullish(dm[item]) if (nullMap[item] !== undefined) { emptyTag = tag } else { @@ -255,7 +256,7 @@ export const parserRuleSet = ruleSet => { const map = dispatchMap(ruleSet) /** @type {(name: string) => _DispatchRule} */ - const dispatched = name => /** @type {_DispatchRule} */ (map[name]) + const dispatched = name => assertNotNullish(map[name]) // The matcher as an explicit-stack machine: each iteration either starts the // current task (dispatching one rule and pushing a frame for the rules chain diff --git a/fjs/cas/evo/module.f.mjs b/fjs/cas/evo/module.f.mjs index 55f7f812e6..0a3a662c8b 100644 --- a/fjs/cas/evo/module.f.mjs +++ b/fjs/cas/evo/module.f.mjs @@ -45,10 +45,9 @@ * @import { Effect, Operation } from '../../effects/types.ts' * @import { Key, MemOp } from '../../effects/memory/types.ts' * @import { Cas } from '../types.ts' - * @import { Ok, Result } from '../../types/result/types.ts' + * @import { Result } from '../../types/result/types.ts' * @import { Vec } from '../../types/bit_vec/types.ts' * @import { IoResult } from '../../effects/node/types.ts' - * @import { List } from '../../effects/list/types.ts' * @import { LockField, LockMap, Revision } from '../../media/revision/types.ts' * @import { Hash, Subject, RevisionData, SubjectState, Cache, Evo } from './types.ts' */ @@ -67,7 +66,8 @@ import { isNotFound } from '../../effects/node/module.f.mjs' import { decodeText, encodeText, dialect, checkReferences, isHash } from '../../media/revision/module.f.mjs' /** A cache with no known subjects yet — the starting point for {@link buildCache}. */ -export const emptyCache = /** @type {Cache} */ ({ bySubject: {} }) +/** @type {Cache} */ +export const emptyCache = { bySubject: {} } /** @type {SubjectState} */ const emptySubjectState = { hashes: [], parents: [], archived: [] } @@ -291,8 +291,7 @@ const resolveParents = cas => parents => { return mapStep( resolveParent(cas)(parentRef), (/** @type {Result} */ parentResult) => - /** @type {Result} */ - (parentResult[0] === 'error' ? parentResult : ok([...acc[1], parentResult[1]]))) + parentResult[0] === 'error' ? parentResult : ok([...acc[1], parentResult[1]])) }) } @@ -461,7 +460,7 @@ export const addRevision = cas => cacheKey => input => return pure(error('revision too large to encode')) } return step( - cas.write(nonEmpty(ok(bytes), /** @type {List>} */ (elEmpty()))), + cas.write(nonEmpty(ok(bytes), elEmpty())), (/** @type {IoResult} */ writeResult) => { if (writeResult[0] === 'error') { return /** @type {Effect>} */ (pure(error('failed to write revision to CAS'))) diff --git a/fjs/cas/evo/proof.f.mjs b/fjs/cas/evo/proof.f.mjs index b61a8164bd..4125565b01 100644 --- a/fjs/cas/evo/proof.f.mjs +++ b/fjs/cas/evo/proof.f.mjs @@ -2,7 +2,6 @@ * @import { Cas } from '../types.ts' * @import { Vec } from '../../types/bit_vec/types.ts' * @import { Ok } from '../../types/result/types.ts' - * @import { IoResult } from '../../effects/node/types.ts' * @import { List } from '../../effects/list/types.ts' * @import { RevisionData } from './types.ts' */ @@ -43,7 +42,7 @@ const writeFailingCas = { // large for `collectRead` to buffer looks like to a caller. /** @type {Cas} */ const readFailingCas = { - read: () => nonEmpty(/** @type {IoResult} */ (error('boom')), elEmpty()), + read: () => nonEmpty(error('boom'), elEmpty()), write: () => pure(error('write not supported')), list: () => pure([]), } @@ -57,8 +56,8 @@ const fixedCas = entries => ({ read: hash => { const found = entries.find(([h]) => vecToCBase32(h) === vecToCBase32(hash)) return found === undefined - ? nonEmpty(/** @type {IoResult} */ (error('not found')), elEmpty()) - : nonEmpty(/** @type {IoResult} */ (ok(found[1])), elEmpty()) + ? nonEmpty(error('not found'), elEmpty()) + : nonEmpty(ok(found[1]), elEmpty()) }, write: () => pure(error('write not supported')), list: () => pure(entries.map(([h]) => h)), @@ -73,7 +72,7 @@ export const proof = { buildCacheSkipsNonRevisionBlob: () => { const c = fileCas(sha256)(home) const content = vec8(0x41n) // 'A' — valid UTF-8, not revision JSON - const [state1] = virtual(emptyState)(c.write(nonEmpty(ok(content), /** @type {List>} */ (elEmpty())))) + const [state1] = virtual(emptyState)(c.write(nonEmpty(ok(content), /** @satisfies {List>} */ (elEmpty())))) const [, cache] = virtual(state1)(buildCache(c)) assertEq(Object.keys(cache.bySubject).length, 0) }, @@ -85,7 +84,7 @@ export const proof = { decodeRevisionBlobNonUtf8IsNull: () => { const c = fileCas(sha256)(home) const oddVec = vec(5n)(0b10101n) // not a whole number of bytes - const [state1, w] = virtual(emptyState)(c.write(nonEmpty(ok(oddVec), /** @type {List>} */ (elEmpty())))) + const [state1, w] = virtual(emptyState)(c.write(nonEmpty(ok(oddVec), /** @satisfies {List>} */ (elEmpty())))) assert(w[0] === 'ok', ['expected write ok', w]) const [, revision] = virtual(state1)(decodeRevisionBlob(c)(w[1])) assertEq(revision, null) @@ -93,7 +92,7 @@ export const proof = { decodeRevisionBlobInvalidJsonIsNull: () => { const c = fileCas(sha256)(home) const content = vec8(0x7bn) // '{' alone: valid UTF-8, not parseable JSON - const [state1, w] = virtual(emptyState)(c.write(nonEmpty(ok(content), /** @type {List>} */ (elEmpty())))) + const [state1, w] = virtual(emptyState)(c.write(nonEmpty(ok(content), /** @satisfies {List>} */ (elEmpty())))) assert(w[0] === 'ok', ['expected write ok', w]) const [, revision] = virtual(state1)(decodeRevisionBlob(c)(w[1])) assertEq(revision, null) @@ -104,7 +103,7 @@ export const proof = { const text = `{"dialect":"${revisionDialect}","subject":"${subjectHash}","parents":[],"snapshot":"${subjectHash}","generation":0}` const bytes = tryUtf8(text) assert(bytes !== null, 'expected the sample revision text to encode as UTF-8') - const [state1, w] = virtual(emptyState)(c.write(nonEmpty(ok(bytes), /** @type {List>} */ (elEmpty())))) + const [state1, w] = virtual(emptyState)(c.write(nonEmpty(ok(bytes), /** @satisfies {List>} */ (elEmpty())))) assert(w[0] === 'ok', ['expected write ok', w]) const [, revision] = virtual(state1)(decodeRevisionBlob(c)(w[1])) assert(revision !== null, 'expected a decoded revision') @@ -120,7 +119,7 @@ export const proof = { const text = `{"dialect":"${revisionDialect}","subject":"${subjectHash}","parents":[],"snapshot":"${subjectHash}","generation":0}` const bytes = tryUtf8(text) assert(bytes !== null, 'expected the sample revision text to encode as UTF-8') - const [state1, w] = virtual(emptyState)(fileCas(sha256)(home).write(nonEmpty(ok(bytes), /** @type {List>} */ (elEmpty())))) + const [state1, w] = virtual(emptyState)(fileCas(sha256)(home).write(nonEmpty(ok(bytes), /** @satisfies {List>} */ (elEmpty())))) assert(w[0] === 'ok', ['expected write ok', w]) const [, cache] = virtual(state1)(buildCache(c)) assertEq(cache.bySubject[subjectHash]?.hashes.length, 1) @@ -582,7 +581,7 @@ export const proof = { const [state0, cacheKey] = virtual(emptyState)(initEvo(c)) const e = evo(c)(cacheKey) const content = vec8(0x41n) // 'A' — valid UTF-8, not revision JSON - const [state1, w] = virtual(state0)(c.write(nonEmpty(ok(content), /** @type {List>} */ (elEmpty())))) + const [state1, w] = virtual(state0)(c.write(nonEmpty(ok(content), /** @satisfies {List>} */ (elEmpty())))) assert(w[0] === 'ok', ['expected write ok', w]) const [, result] = virtual(state1)(e.revision(vecToCBase32(w[1]))) assertEq(result[0], 'error') @@ -606,7 +605,7 @@ export const proof = { const text = `{"dialect":"${revisionDialect}","subject":"doc","parents":["${parentAlias}"],"snapshot":"${snapshotAlias}","generation":1}` const bytes = tryUtf8(text) assert(bytes !== null, 'expected the sample revision text to encode as UTF-8') - const [state1, w] = virtual(state0)(c.write(nonEmpty(ok(bytes), /** @type {List>} */ (elEmpty())))) + const [state1, w] = virtual(state0)(c.write(nonEmpty(ok(bytes), /** @satisfies {List>} */ (elEmpty())))) assert(w[0] === 'ok', ['expected write ok', w]) const [, result] = virtual(state1)(e.revision(vecToCBase32(w[1]))) assert(result[0] === 'ok', ['expected revision ok', result]) diff --git a/fjs/cas/module.f.mjs b/fjs/cas/module.f.mjs index 90fc8d87d4..9a0ddfda26 100644 --- a/fjs/cas/module.f.mjs +++ b/fjs/cas/module.f.mjs @@ -249,7 +249,7 @@ export const fileCas = sha2 => path => { const loop = offset => step( readBytes(p, offset, chunkBytes), - /** @type {(result: IoResult) => List>} */ (result) => { + (result) => { const [t, v] = result // A missing shard or read error is an explicit error item, never EOF. if (t === 'error') { @@ -271,7 +271,7 @@ export const fileCas = sha2 => path => { // genuine storage error and is surfaced, not masked as "no hashes". step(access(storePrefix), a => { if (a[0] === 'error') { - if (isNotFound(a[1])) { return pure(/** @type {readonly Vec[]} */ ([])) } + if (isNotFound(a[1])) { return pure([]) } throw a[1] } return mapStep( @@ -306,7 +306,7 @@ const streamFile = filePath => { const loop = offset => step( readBytes(filePath, offset, chunkBytes), - /** @type {(result: IoResult) => List>} */ (result) => { + (result) => { if (result[0] === 'error') { return nonEmpty(result, elEmpty()) } diff --git a/fjs/cas/proof.f.mjs b/fjs/cas/proof.f.mjs index bc9cae8672..b221757938 100644 --- a/fjs/cas/proof.f.mjs +++ b/fjs/cas/proof.f.mjs @@ -53,7 +53,7 @@ const casDefaultResponse = cmd => { case 'mkdir': case 'createExclusive': case 'rename': case 'rm': case 'writeBytes': case 'access': return ok(undefined) - case 'readdir': return ok(/** @type {readonly unknown[]} */ ([])) + case 'readdir': return ok([]) case 'stat': return ok({ size: 0 }) default: return ok(undefined) } @@ -88,7 +88,7 @@ const drive = overrides => { const queue = overrides[cmd] return queue !== undefined && queue.length > 0 ? queue.shift() : casDefaultResponse(cmd) } - const handlers = /** @type {Parameters[0]} */ ({ + const handlers = { access: () => next('access'), createExclusive: () => next('createExclusive'), mkdir: () => next('mkdir'), @@ -100,18 +100,30 @@ const drive = overrides => { rm: () => next('rm'), stat: () => next('stat'), writeBytes: () => next('writeBytes'), - }) + } const matcher = match(handlers) /** @type {(e: Effect) => unknown} */ const run_ = e => { const m = matcher(e) - return m[0] === 'done' ? m[1] : run_(m[2](/** @type {any} */ (m[1]))) + return m[0] === 'done' ? m[1] : run_(m[2](m[1])) } return e => [run_(e), log] } // Create a 128 KiB big file content (at the max Vec size limit) // This tests the boundary where files are at the chunk size limit +/** + * The message of an `IoResult` the driver returned as `unknown`. Checks the + * result really is an `error` pair rather than assuming it: these proofs exist + * to establish that a write fails closed, so the shape is the claim. + */ +/** @type {(result: unknown) => unknown} */ +const errorMessage = result => { + assert(result instanceof Array && result.length === 2 && result[0] === 'error', + ['expected an error result', result]) + return result[1] +} + /** @type {() => Vec} */ const createBigFileContent = () => { const byteCount = 128n * 1024n // 128 KiB @@ -243,7 +255,7 @@ export const proof = { /** @type {List>} */ const payload = chunks.reduceRight( (tail, chunk) => nonEmpty(ok(chunk), tail), - /** @type {List>} */ (empty())) + /** @satisfies {List>} */ (empty())) const [state1, writeResult] = virtual(emptyState)(c.write(payload)) assert(writeResult[0] === 'ok', ['expected write ok', writeResult]) const hash = writeResult[1] @@ -288,7 +300,7 @@ export const proof = { /** @type {IoResult} */ const errItem = error({ code: 'BOOM' }) /** @type {List>} */ - const payload = nonEmpty(okItem, nonEmpty(errItem, /** @type {List>} */ (empty()))) + const payload = nonEmpty(okItem, nonEmpty(errItem, /** @satisfies {List>} */ (empty()))) const [state1, result] = virtual(emptyState)(c.write(payload)) assert(result[0] === 'error', ['expected write error', result]) const [, hashes] = virtual(state1)(c.list()) @@ -307,7 +319,7 @@ export const proof = { /** @type {List>} */ const payload = chunks.reduceRight( (tl, chunk) => nonEmpty(ok(chunk), tl), - /** @type {List>} */ (empty())) + /** @satisfies {List>} */ (empty())) const [state1, w] = virtual(emptyState)(c.write(payload)) assert(w[0] === 'ok', ['expected write ok', w]) const hash = w[1] @@ -340,7 +352,7 @@ export const proof = { } const content = vec8(0x2An) const c = fileCas(sha256)('.') - const x = c.write(nonEmpty(ok(content), /** @type {List>} */ (empty()))) + const x = c.write(nonEmpty(ok(content), /** @satisfies {List>} */ (empty()))) const [state1, w] = virtual(state0)(x) assert(w[0] === 'ok', ['expected write ok', w]) const [, present] = virtual(state1)(access(stalePath)) @@ -357,7 +369,7 @@ export const proof = { } const content = vec8(0x2An) const c = fileCas(sha256)('.') - const x = c.write(nonEmpty(ok(content), /** @type {List>} */ (empty()))) + const x = c.write(nonEmpty(ok(content), /** @satisfies {List>} */ (empty()))) const [state1, w] = virtual(state0)(x) assert(w[0] === 'ok', ['expected write ok', w]) const [, present] = virtual(state1)(access(livePath)) @@ -371,8 +383,7 @@ export const proof = { /** @type {List>} */ const payload = nonEmpty(ok(vec8(0x11n)), empty()) const [result, log] = drive({ writeBytes: [error('disk full')] })(c.write(payload)) - assert(/** @type {IoResult} */ (result)[0] === 'error', ['expected write error', result]) - assertEq(/** @type {IoResult} */ (result)[1], 'disk full') + assertEq(errorMessage(result), 'disk full') // The cleanup `rm` of the partial staging file must actually run, not just be // implied by the returned error tag. assertEq(log[log.length - 1], 'rm', ['expected cleanup rm to run', log]) @@ -384,8 +395,7 @@ export const proof = { /** @type {List>} */ const payload = nonEmpty(ok(vec8(0x11n)), empty()) const [result, log] = drive({ rename: [error('rename failed')] })(c.write(payload)) - assert(/** @type {IoResult} */ (result)[0] === 'error', ['expected write error', result]) - assertEq(/** @type {IoResult} */ (result)[1], 'rename failed') + assertEq(errorMessage(result), 'rename failed') assertEq(log[log.length - 1], 'rm', ['expected cleanup rm to run', log]) }, casWritePublishSizeMismatchErrors: () => { @@ -400,8 +410,7 @@ export const proof = { /** @type {List>} */ const payload = nonEmpty(ok(vec8(0x11n)), empty()) const [result] = drive({ stat: [ok({ size: 999 })] })(c.write(payload)) - assert(/** @type {IoResult} */ (result)[0] === 'error', ['expected write error', result]) - assertEq(/** @type {IoResult} */ (result)[1], 'publish size mismatch') + assertEq(errorMessage(result), 'publish size mismatch') }, casWritePublishStatErrorErrorsEvenWithMatchingSize: () => { // Pins the tag half of the same check: a `stat` that fails outright must still @@ -414,8 +423,7 @@ export const proof = { /** @type {List>} */ const payload = nonEmpty(ok(vec8(0x11n)), empty()) const [result] = drive({ stat: [error({ size: 1 })] })(c.write(payload)) - assert(/** @type {IoResult} */ (result)[0] === 'error', ['expected write error', result]) - assertEq(/** @type {IoResult} */ (result)[1], 'publish size mismatch') + assertEq(errorMessage(result), 'publish size mismatch') }, casUploadSuccess: () => { // A successful upload returns the hash and deletes the source file from cas_upload/. @@ -454,7 +462,7 @@ export const proof = { /** @type {IoResult} */ const boom = error('boom') /** @type {List>} */ - const stream = nonEmpty(ok(vec8(0x11n)), nonEmpty(boom, /** @type {List>} */ (empty()))) + const stream = nonEmpty(ok(vec8(0x11n)), nonEmpty(boom, /** @satisfies {List>} */ (empty()))) const o = runPure(collectRead(stream)) assert(o.length === 1, 'expected collectRead to finish without issuing a command') const [r] = o diff --git a/fjs/ci/nix/module.f.mjs b/fjs/ci/nix/module.f.mjs index bc8d88b652..366780b0da 100644 --- a/fjs/ci/nix/module.f.mjs +++ b/fjs/ci/nix/module.f.mjs @@ -91,14 +91,15 @@ export const nixFlakes = jobs => forEachStep(pure(jobs), writeFlake) /** Path a workflow passes to `nix develop`, for the job of the given id. */ -export const flakePath = /** @type {(id: string) => string} */ (id => `./${generatedDirectory}/${id}`) +/** @type {(id: string) => string} */ +export const flakePath = id => `./${generatedDirectory}/${id}` /** Installs Nix, with `nix-command` and `flakes` enabled by the action's defaults. */ export const nixInstall = install(uses('cachix/install-nix-action')) /** Runs one command inside a job's generated development shell. */ -export const nixDevelop = /** @type {(id: string, command: string) => string} */ - ((id, command) => `nix develop ${flakePath(id)} --command ${command}`) +/** @type {(id: string, command: string) => string} */ +export const nixDevelop = (id, command) => `nix develop ${flakePath(id)} --command ${command}` /** * Wraps a string so a POSIX shell reproduces it exactly. Single quotes protect diff --git a/fjs/ci/proof.f.mjs b/fjs/ci/proof.f.mjs index d291efee16..3557a99091 100644 --- a/fjs/ci/proof.f.mjs +++ b/fjs/ci/proof.f.mjs @@ -39,8 +39,11 @@ const makeState = (/** @type {boolean} */ rust, /** @type {string | undefined} * /** @type {(dir: Dir, name: string) => Dir} */ const subDir = (dir, name) => { const entity = dir[name] - assert(typeof entity === 'object' && !Array.isArray(entity), entity) - return /** @type {Dir} */ (entity) + // `Array.isArray` narrows to `any[]`, which `readonly Vec[]` is not assignable + // to, so its negative branch never removes a `readonly` array from a union. + // `instanceof Array` does, so this `assert` narrows `entity` to `Dir`. + assert(typeof entity === 'object' && !(entity instanceof Array), entity) + return entity } /** @type {(dir: Dir, name: string) => string} */ diff --git a/fjs/djs/parser/module.f.mjs b/fjs/djs/parser/module.f.mjs index fb30a10801..88a16c01c2 100644 --- a/fjs/djs/parser/module.f.mjs +++ b/fjs/djs/parser/module.f.mjs @@ -508,7 +508,7 @@ const foldOp = token => state => { export const parseFromTokens = tokenList => { const state = fold(foldOp)({ state: '', module: { refs: null, modules: null, consts: null } })(tokenList) switch (state.state) { - case 'result': return ok(/** @type {AstModule} */ ([toArray(state.module.modules), toArray(state.module.consts)])) + case 'result': return ok(/** @satisfies {AstModule} */ ([toArray(state.module.modules), toArray(state.module.consts)])) case 'error': return error(state.error) default: return error({ message: 'unexpected end', metadata: null }) } diff --git a/fjs/djs/proof.f.mjs b/fjs/djs/proof.f.mjs index 89a2ad8191..64dfd0e6cf 100644 --- a/fjs/djs/proof.f.mjs +++ b/fjs/djs/proof.f.mjs @@ -1,5 +1,4 @@ /** - * @import { Vec } from '../types/bit_vec/types.ts' */ import { compile } from './module.f.mjs' @@ -11,7 +10,7 @@ import { assert, assertEq } from '../asserts/module.f.mjs' const readOutput = (root, path) => { const file = root[path] if (!Array.isArray(file) || file.length === 0) { throw `${path} is not a file` } - return utf8ToString(/** @type {readonly Vec[]} */ (file)[0]) + return utf8ToString(file[0]) } export const proof = { diff --git a/fjs/djs/tokenizer/module.f.mjs b/fjs/djs/tokenizer/module.f.mjs index 96a139554f..26d2146a16 100644 --- a/fjs/djs/tokenizer/module.f.mjs +++ b/fjs/djs/tokenizer/module.f.mjs @@ -9,7 +9,7 @@ * AstTag, * CodePointMeta, * DescentMatch, - * DescentMatchResult + * DescentMatchResult, * } from '../../bnf/descent/types.ts' * @import { DataRule, Rule } from '../../bnf/types.ts' * @import { @@ -29,7 +29,6 @@ * @import { CodePoint } from '../../text/utf16/types.ts' * @import { StateScan } from '../../types/function/operator/types.ts' * @import { List } from '../../types/list/types.ts' - * @import { Unknown } from '../types.ts' * @import { DjsToken, DjsTokenWithMetadata } from './types.ts' */ @@ -302,7 +301,7 @@ const scanFunc = (input, state) => { } // All operator tag strings produced by the grammar's operator rule -const operatorTags = /** @type {ReadonlySet} */ (new Set([ +const operatorTags = new Set([ '.', '=>', '===', '==', '=', '!==', '!=', '!', '>>>=', '>>>', '>>=', '>>', '>=', '>', '<<=', '<<', '<=', '<', @@ -311,7 +310,7 @@ const operatorTags = /** @type {ReadonlySet} */ (new Set([ '&&=', '&&', '&=', '&', '||=', '||', '|=', '|', '^=', '^', '~', '??=', '??', '?.', '?', '[', ']', '{', '}', '(', ')', ',', ':' -])) +]) /** @type {(tk: _FlatToken) => boolean} */ const filterFunc = tk => { @@ -466,9 +465,9 @@ const getTokensFromAstRule = ast => { export const tokenizeString = s => { const cp = toArray(stringToCodePointList(s)) if (cp.length === 0) { - return stringify(/** @type {Unknown} */ ([{ kind: 'eof' }])) + return stringify([{ kind: 'eof' }]) } - const m = /** @type {DescentMatch} */ (descentParser(jsGrammar())) + const m = descentParser(jsGrammar()) const cpm = codePointsWithMetadata('')(cp) const { ast, success: ok, idx: len } = m('', cpm) if (!ok || len !== cp.length) @@ -483,7 +482,7 @@ export const tokenizeString = s => { const tokens = flat(stateScan(scanFunc)(['', null, []])(filterTokens)) const jsTokens = concat(flatMap(toJsTokens)(tokens))([{ kind: 'eof' }]) const result = toArray(jsTokens) - return stringify(/** @type {Unknown} */ (result)) + return stringify(result) } // Finds `tag` in flatTokens and returns the metadata of the next code point after it. @@ -506,7 +505,7 @@ export const tokenizeJs = input => path => { const initial = { path, line: 1, column: 1 } if (cp.length === 0) return [{ token: { kind: 'eof' }, metadata: initial }] - const m = /** @type {DescentMatch} */ (descentParser(jsGrammar())) + const m = descentParser(jsGrammar()) const cpm = codePointsWithMetadata(path)(cp) const { ast, success: ok, idx: len } = m('', cpm) const finalMetadata = fold(advanceMetadata)(initial)(cp) diff --git a/fjs/djs/tokenizer/proof.f.mjs b/fjs/djs/tokenizer/proof.f.mjs index 5ecf273271..348cc36293 100644 --- a/fjs/djs/tokenizer/proof.f.mjs +++ b/fjs/djs/tokenizer/proof.f.mjs @@ -1,5 +1,4 @@ /** - * @import { Unknown } from '../types.ts' */ import { descentParser } from '../../bnf/descent/module.f.mjs' @@ -886,19 +885,19 @@ export const proof = { () => { // keywords other than true/false/null/undefined become plain ids const result = toArray(tokenize(stringToList('break'))('')) - assertEq(stringify(/** @type {Unknown} */ (result)), '[{"metadata":{"column":1,"line":1,"path":""},"token":{"kind":"id","value":"break"}},{"metadata":{"column":6,"line":1,"path":""},"token":{"kind":"eof"}}]') + assertEq(stringify(result), '[{"metadata":{"column":1,"line":1,"path":""},"token":{"kind":"id","value":"break"}},{"metadata":{"column":6,"line":1,"path":""},"token":{"kind":"eof"}}]') }, () => { const result = toArray(tokenize(stringToList('-10'))('')) - assertEq(stringify(/** @type {Unknown} */ (result)), '[{"metadata":{"column":2,"line":1,"path":""},"token":{"kind":"number","value":"-10"}},{"metadata":{"column":4,"line":1,"path":""},"token":{"kind":"eof"}}]') + assertEq(stringify(result), '[{"metadata":{"column":2,"line":1,"path":""},"token":{"kind":"number","value":"-10"}},{"metadata":{"column":4,"line":1,"path":""},"token":{"kind":"eof"}}]') }, () => { const result = toArray(tokenize(stringToList('-0'))('')) - assertEq(stringify(/** @type {Unknown} */ (result)), '[{"metadata":{"column":2,"line":1,"path":""},"token":{"kind":"number","value":"-0"}},{"metadata":{"column":3,"line":1,"path":""},"token":{"kind":"eof"}}]') + assertEq(stringify(result), '[{"metadata":{"column":2,"line":1,"path":""},"token":{"kind":"number","value":"-0"}},{"metadata":{"column":3,"line":1,"path":""},"token":{"kind":"eof"}}]') }, () => { const result = toArray(tokenize(stringToList('-1234567890n'))('')) - assertEq(stringify(/** @type {Unknown} */ (result)), '[{"metadata":{"column":2,"line":1,"path":""},"token":{"kind":"bigint","value":-1234567890n}},{"metadata":{"column":13,"line":1,"path":""},"token":{"kind":"eof"}}]') + assertEq(stringify(result), '[{"metadata":{"column":2,"line":1,"path":""},"token":{"kind":"bigint","value":-1234567890n}},{"metadata":{"column":13,"line":1,"path":""},"token":{"kind":"eof"}}]') }, () => { // `js/tokenizer` merges '--' into one decrement-operator token, so @@ -906,31 +905,31 @@ export const proof = { // it's one error straight from the default state's unknown-token // fallback, mapping the whole '--' token to a single error. const result = toArray(tokenize(stringToList('--'))('')) - assertEq(stringify(/** @type {Unknown} */ (result)), '[{"metadata":{"column":1,"line":1,"path":""},"token":{"kind":"error","message":"invalid token"}},{"metadata":{"column":3,"line":1,"path":""},"token":{"kind":"eof"}}]') + assertEq(stringify(result), '[{"metadata":{"column":1,"line":1,"path":""},"token":{"kind":"error","message":"invalid token"}},{"metadata":{"column":3,"line":1,"path":""},"token":{"kind":"eof"}}]') }, () => { const result = toArray(tokenize(stringToList('---'))('')) - assertEq(stringify(/** @type {Unknown} */ (result)), '[{"metadata":{"column":1,"line":1,"path":""},"token":{"kind":"error","message":"invalid token"}},{"metadata":{"column":4,"line":1,"path":""},"token":{"kind":"error","message":"invalid token"}},{"metadata":{"column":4,"line":1,"path":""},"token":{"kind":"eof"}}]') + assertEq(stringify(result), '[{"metadata":{"column":1,"line":1,"path":""},"token":{"kind":"error","message":"invalid token"}},{"metadata":{"column":4,"line":1,"path":""},"token":{"kind":"error","message":"invalid token"}},{"metadata":{"column":4,"line":1,"path":""},"token":{"kind":"eof"}}]') }, () => { // dangling '-' at eof const result = toArray(tokenize(stringToList('-'))('')) - assertEq(stringify(/** @type {Unknown} */ (result)), '[{"metadata":{"column":2,"line":1,"path":""},"token":{"kind":"error","message":"invalid token"}},{"metadata":{"column":2,"line":1,"path":""},"token":{"kind":"eof"}}]') + assertEq(stringify(result), '[{"metadata":{"column":2,"line":1,"path":""},"token":{"kind":"error","message":"invalid token"}},{"metadata":{"column":2,"line":1,"path":""},"token":{"kind":"eof"}}]') }, () => { // '-' followed by neither a number/bigint nor eof: one error for // the dangling '-', then the following token maps through normally. const result = toArray(tokenize(stringToList('-{'))('')) - assertEq(stringify(/** @type {Unknown} */ (result)), '[{"metadata":{"column":2,"line":1,"path":""},"token":{"kind":"error","message":"invalid token"}},{"metadata":{"column":2,"line":1,"path":""},"token":{"kind":"{"}},{"metadata":{"column":3,"line":1,"path":""},"token":{"kind":"eof"}}]') + assertEq(stringify(result), '[{"metadata":{"column":2,"line":1,"path":""},"token":{"kind":"error","message":"invalid token"}},{"metadata":{"column":2,"line":1,"path":""},"token":{"kind":"{"}},{"metadata":{"column":3,"line":1,"path":""},"token":{"kind":"eof"}}]') }, () => { const result = toArray(tokenize(stringToList('[-1234567890n]'))('')) - assertEq(stringify(/** @type {Unknown} */ (result)), '[{"metadata":{"column":1,"line":1,"path":""},"token":{"kind":"["}},{"metadata":{"column":3,"line":1,"path":""},"token":{"kind":"bigint","value":-1234567890n}},{"metadata":{"column":14,"line":1,"path":""},"token":{"kind":"]"}},{"metadata":{"column":15,"line":1,"path":""},"token":{"kind":"eof"}}]') + assertEq(stringify(result), '[{"metadata":{"column":1,"line":1,"path":""},"token":{"kind":"["}},{"metadata":{"column":3,"line":1,"path":""},"token":{"kind":"bigint","value":-1234567890n}},{"metadata":{"column":14,"line":1,"path":""},"token":{"kind":"]"}},{"metadata":{"column":15,"line":1,"path":""},"token":{"kind":"eof"}}]') }, () => { // grammar-level tokenizer error position flows through the DJS wrapper unchanged const result = toArray(tokenize(stringToList('00'))('')) - assertEq(stringify(/** @type {Unknown} */ (result)), '[{"metadata":{"column":2,"line":1,"path":""},"token":{"kind":"error","message":"invalid token"}}]') + assertEq(stringify(result), '[{"metadata":{"column":2,"line":1,"path":""},"token":{"kind":"error","message":"invalid token"}}]') }, ], // Regression coverage for large inputs: both a file with many short tokens and a diff --git a/fjs/effects/eff/proof.f.mjs b/fjs/effects/eff/proof.f.mjs index 8f693280c1..b85b197298 100644 --- a/fjs/effects/eff/proof.f.mjs +++ b/fjs/effects/eff/proof.f.mjs @@ -1,5 +1,5 @@ /** - * @import { Effect, OperationMap } from '../types.ts' + * @import { Effect } from '../types.ts' */ import { do_, match, pure } from '../module.f.mjs' @@ -12,7 +12,7 @@ import { eff } from './module.f.mjs' /** @type {(command: 'add') => (a: number, b: number) => Effect<_AddOp, number>} */ const doAdd = do_ -const next = match(/** @type {OperationMap<_AddOp, number>} */ ({ add: (a, b) => a + b })) +const next = match({ add: (a, b) => a + b }) export const proof = { value: () => { diff --git a/fjs/effects/memory/module.f.mjs b/fjs/effects/memory/module.f.mjs index 5496cd4681..c857a07164 100644 --- a/fjs/effects/memory/module.f.mjs +++ b/fjs/effects/memory/module.f.mjs @@ -40,6 +40,5 @@ export const read = (do_('memRead')) /** Replaces the current contents of a typed memory slot. */ -export const write = - /** @type {(key: Key, value: T) => Effect} */ - (do_('memWrite')) +/** @type {(key: Key, value: T) => Effect} */ +export const write = do_('memWrite') diff --git a/fjs/effects/module.f.mjs b/fjs/effects/module.f.mjs index 33ab0fbb52..c48d2cfc27 100644 --- a/fjs/effects/module.f.mjs +++ b/fjs/effects/module.f.mjs @@ -376,8 +376,7 @@ export const match = map => e => { if (typeof e === 'function') { return ['done', e()] } const { command, payload, continuation } = e - const handler = /** @type {(...payload: readonly unknown[]) => R} */ - (at(command)(/** @type {any} */ (map))) + const handler = at(command)(map) assert(handler !== null, command) return ['cont', handler(...payload), continuation] } diff --git a/fjs/effects/node/module.f.mjs b/fjs/effects/node/module.f.mjs index 63322c79c2..a823575ac8 100644 --- a/fjs/effects/node/module.f.mjs +++ b/fjs/effects/node/module.f.mjs @@ -35,7 +35,7 @@ import { do_, mapStep, okStep, pure, step } from '../module.f.mjs' * @type {(e: unknown) => boolean} */ export const isNotFound = e => - typeof e === 'object' && e !== null && (/** @type {{ readonly code?: unknown }} */ (e)).code === 'ENOENT' + typeof e === 'object' && e !== null && 'code' in e && e.code === 'ENOENT' // all @@ -173,7 +173,7 @@ const writeLoop = path => { export const writeFromStream = (path, e) => step( createExclusive(path), - okStep(() => /** @type {Effect>} */ (writeLoop(path)(0, e)))) + okStep(() => writeLoop(path)(0, e))) // stat @@ -204,7 +204,8 @@ export const import_ = do_('import') // write /** Emits a `Write` effect to the given named stream. */ -export const write = /** @type {Func} */ (do_('write')) +/** @type {Func} */ +export const write = do_('write') /** * Encodes `s + '\n'` as UTF-8 and emits a `Write` effect to `stream`. @@ -216,15 +217,18 @@ const writeString = stream => s => write(stream, utf8(s + '\n')) /** Writes a line to `stdout`. Replaces the retired `Log` effect. */ -export const log = /** @type {Console} */ (writeString('stdout')) +/** @type {Console} */ +export const log = writeString('stdout') /** Writes a line to `stderr`. Replaces the retired `Error` effect. */ -export const error = /** @type {Console} */ (writeString('stderr')) +/** @type {Console} */ +export const error = writeString('stderr') // read /** Emits a `Read` effect, yielding the next input byte or `null` at EOF. */ -export const read = /** @type {Func} */ (do_('read')) +/** @type {Func} */ +export const read = do_('read') /** @type {(bytes: _UtfList) => string} */ const utf8ListToString = bytes => diff --git a/fjs/effects/node/module.mjs b/fjs/effects/node/module.mjs index 7d9dd350f7..7ee0dca869 100644 --- a/fjs/effects/node/module.mjs +++ b/fjs/effects/node/module.mjs @@ -115,7 +115,7 @@ const sandbox = async f => { p = await p after = performance.now() } - result = ok(/** @type {T} */ (p)) + result = ok(p) } catch (e) { after = performance.now() result = error(e) @@ -190,7 +190,7 @@ const waitReadableOrEnd = stdin => const readStdinByte = async () => { const stdin = process.stdin while (true) { - const chunk = /** @type {Uint8Array | null} */ (stdin.read(1)) + const chunk = stdin.read(1) if (chunk !== null) { return chunk[0] } diff --git a/fjs/effects/node/proof.f.mjs b/fjs/effects/node/proof.f.mjs index e73c687dd2..684582470a 100644 --- a/fjs/effects/node/proof.f.mjs +++ b/fjs/effects/node/proof.f.mjs @@ -1,7 +1,6 @@ /** * @import { Vec } from "../../types/bit_vec/types.ts" * @import { IoResult, ReadFile } from "./types.ts" - * @import { Dir } from "./virtual/types.ts" * @import { List } from "../list/types.ts" * @import { OperationMap } from "../types.ts" */ @@ -81,8 +80,11 @@ export const proof = { ) assert(t === 'ok', result) const tmp = state.root.tmp - assert(!(typeof tmp !== 'object' || Array.isArray(tmp)), state.root) - const cache = (/** @type {Dir} */ (tmp)).cache + // `instanceof Array`, not `Array.isArray`: only the former's negative + // branch removes a `readonly` array from a union, so only it narrows + // `_Entity` to `Dir`. + assert(!(typeof tmp !== 'object' || tmp instanceof Array), state.root) + const cache = tmp.cache assert(!(typeof cache !== 'object' || Array.isArray(cache)), tmp) }, nonRec: () => { @@ -122,7 +124,8 @@ export const proof = { nestedPath: () => { const [_, [t, result]] = virtual(emptyState)(readFile('tmp/cache')) assert(t === 'error', result) - if ((/** @type {{ code?: unknown }} */ (result)).code !== 'ENOENT') { throw result } + assert(typeof result === 'object' && result !== null && 'code' in result, result) + if (result.code !== 'ENOENT') { throw result } }, withinLimit: () => { // Test with a small file well within the 131,072 byte limit @@ -272,8 +275,8 @@ export const proof = { })(rm('tmp/cache')) assert(t === 'ok', result) const tmp = state.root.tmp - assert(!(typeof tmp !== 'object' || Array.isArray(tmp)), state.root) - assertEq((/** @type {Dir} */ (tmp)).cache, undefined, tmp) + assert(!(typeof tmp !== 'object' || tmp instanceof Array), state.root) + assertEq(tmp.cache, undefined, tmp) }, noSuchFile: () => { const [_, [t, result]] = virtual(emptyState)(rm('hello')) @@ -313,7 +316,7 @@ export const proof = { // (and `duration`) instead of the runner measuring. ok: () => { const [_, { result, duration }] = virtual(emptyState)( - sandbox(() => /** @type {never} */ ({ result: ['ok', 42], duration: 0 }))) + sandbox(() => ({ result: ['ok', 42], duration: 0 }))) assert(result[0] === 'ok', result) assertEq(result[1], 42) assertEq(duration, 0) @@ -321,7 +324,7 @@ export const proof = { error: () => { const err = new Error('fail') const [_, { result }] = virtual(emptyState)( - sandbox(() => /** @type {never} */ ({ result: ['error', err], duration: 0 }))) + sandbox(() => ({ result: ['error', err], duration: 0 }))) assert(result[0] === 'error', result) assertEq(result[1], err) }, @@ -351,7 +354,7 @@ export const proof = { assert(t === 'ok', result) assertEq(state.root.src, undefined, state.root) assert(Array.isArray(state.root.dst), state.root) - assertEq(uint((/** @type {readonly Vec[]} */ (state.root.dst))[0]), 0x2An, state.root) + assertEq(uint(state.root.dst[0]), 0x2An, state.root) }, nestedRename: () => { const [state, [t, result]] = virtual({ @@ -360,8 +363,8 @@ export const proof = { })(rename('tmp/src', 'tmp/dst')) assert(t === 'ok', result) const tmp = state.root.tmp - assert(!(typeof tmp !== 'object' || Array.isArray(tmp)), state.root) - assertEq((/** @type {Dir} */ (tmp)).src, undefined, tmp) + assert(!(typeof tmp !== 'object' || tmp instanceof Array), state.root) + assertEq(tmp.src, undefined, tmp) }, dirOverFile: () => { const [state, [t, result]] = virtual({ diff --git a/fjs/effects/node/todo/async-operation-map-assignability.md b/fjs/effects/node/todo/async-operation-map-assignability.md new file mode 100644 index 0000000000..a7aebf2415 --- /dev/null +++ b/fjs/effects/node/todo/async-operation-map-assignability.md @@ -0,0 +1,38 @@ +# `ToAsyncOperationMap` rejects the operation maps built for it + +**Priority:** P3 +**Status:** open + +### Problem + +`asyncRun` takes a `ToAsyncOperationMap`, and `memoryOperationMap()` exists to +supply one — but the result is not assignable, so both call sites cast: + +- `fjs/effects/node/memory/module.mjs:60` — `asyncRun(/** @type {ToAsyncOperationMap} */ (memoryOperationMap()))` +- `fjs/effects/node/memory/proof.mjs:28` — the same cast, spelled with an inline `import(…)` + +`npx tsc` reports `MemoryOperationMap` is not assignable to +`ToAsyncOperationMap`, so the mismatch is between the map type the +factory returns and the shape the runner asks for — not between `MemOp` and some +other operation set. + +This matters beyond tidiness: [`fjs/AGENTS.md`](../../../AGENTS.md) notes that a +cast around a value handed to a `ToAsyncOperationMap`-shaped parameter blocks +TypeScript from checking each operation's implementation against `O`, so a +drifted handler shape is absorbed rather than reported. These two casts are that +exact hazard. + +Two nearby casts in the same area may or may not share a cause, and are worth +checking at the same time: + +- `fjs/effects/node/module.mjs:287` — `Erl` on a request listener +- `fjs/effects/node/virtual/module.f.mjs:410` — `SandboxResult` on `f()` + +### Proposal + +Make `memoryOperationMap()` return something `asyncRun` accepts, so the object +literal is checked structurally against `O` at the call site and both casts go. + +### Related + +- [`todo/inline-type-casts.md`](../../../../todo/inline-type-casts.md) diff --git a/fjs/effects/node/virtual/module.f.mjs b/fjs/effects/node/virtual/module.f.mjs index 8df056b4a6..0685aac54a 100644 --- a/fjs/effects/node/virtual/module.f.mjs +++ b/fjs/effects/node/virtual/module.f.mjs @@ -5,7 +5,6 @@ * * @import { Vec } from '../../../types/bit_vec/types.ts' * @import { MemOperationMap, RunInstance } from '../../mock/types.ts' - * @import { Key } from '../../memory/types.ts' * @import { Dirent, FileStat, IoResult, Module, NodeOp, NodeProgramOptions, SandboxResult } from '../types.ts' * @import { Dir, State, _Entity } from './types.ts' */ @@ -44,10 +43,13 @@ const operation = op => { } const [first, ...rest] = path const subDir = dir[first] - if (typeof subDir !== 'object' || Array.isArray(subDir)) { + // `instanceof Array`, not `Array.isArray`: the latter narrows to `any[]`, + // which `readonly Vec[]` is not assignable to, so its negative branch never + // removes a `readonly` array from `_Entity`. Only `instanceof` narrows here. + if (typeof subDir !== 'object' || subDir instanceof Array) { return op(dir, path) } - const [newSubDir, r] = f(/** @type {Dir} */ (subDir), rest) + const [newSubDir, r] = f(subDir, rest) return [{ ...dir, [first]: newSubDir }, r] } return path => state => { @@ -146,10 +148,10 @@ const readdir = (base, recursive) => readOperation((dir, path) => { let result = [] for (const [name, content] of entries(d)) { if (content === undefined) { continue } - const isFile = Array.isArray(content) || typeof content !== 'object' + const isFile = content instanceof Array || typeof content !== 'object' result = [...result, { name, parentPath, isFile }] if (!isFile && recursive) { - result = [...result, ...f(join(parentPath, name), /** @type {Dir} */ (content))] + result = [...result, ...f(join(parentPath, name), content)] } } return result @@ -194,8 +196,8 @@ const extractEntity = (dir, path) => { } const [first, ...rest] = path const sub = dir[first] - if (sub === undefined || Array.isArray(sub) || typeof sub === 'function') { return [dir, enoent] } - const [newSub, result] = extractEntity(/** @type {Dir} */ (sub), rest) + if (sub === undefined || sub instanceof Array || typeof sub === 'function') { return [dir, enoent] } + const [newSub, result] = extractEntity(sub, rest) if (result[0] === 'error') { return [dir, result] } return [{ ...dir, [first]: newSub }, result] } @@ -222,7 +224,7 @@ const insertEntityAt = (dir, path, entity) => { return [dir, error(`'${name}' is a directory`)] } if (entityIsDir && existingIsDir) { - const existingDir = /** @type {Dir} */ (existing) + const existingDir = existing const hasContent = Object.values(existingDir).some(v => v !== undefined) if (hasContent) { return [dir, error(`cannot overwrite non-empty directory '${name}'`)] @@ -234,8 +236,8 @@ const insertEntityAt = (dir, path, entity) => { const [first, ...rest] = path const sub = dir[first] if (sub === undefined) { return [dir, enoent] } - if (Array.isArray(sub) || typeof sub === 'function') { return [dir, error('not a directory')] } - const [newSub, result] = insertEntityAt(/** @type {Dir} */ (sub), rest, entity) + if (sub instanceof Array || typeof sub === 'function') { return [dir, error('not a directory')] } + const [newSub, result] = insertEntityAt(sub, rest, entity) if (result[0] === 'error') { return [dir, result] } return [{ ...dir, [first]: newSub }, result] } @@ -326,7 +328,7 @@ const writeBytesRawOp = (offset, data) => (dir, p) => { if (file === undefined) { return [dir, enoent] } // writeBytes never creates if (!Array.isArray(file)) { return [dir, error(`'${name}' is not a file`)] } if (!Number.isInteger(offset) || offset < 0) { return [dir, error(`Offset ${offset} is invalid`)] } - const chunks = /** @type {readonly Vec[]} */ (file) + const chunks = file if (offset !== fileSizeBytes(chunks)) { return [dir, error(`writeBytes offset ${offset} must equal the file size (append-only)`)] } @@ -342,7 +344,7 @@ const statOp = readOperation((dir, path) => { const file = dir[path[0]] if (file === undefined) { return enoent } if (!Array.isArray(file)) { return error(`'${path[0]}' is not a file`) } - return ok({ size: fileSizeBytes(/** @type {readonly Vec[]} */ (file)) }) + return ok({ size: fileSizeBytes(file) }) }) /** @type {MemOperationMap} */ @@ -359,7 +361,7 @@ const map = { }, memCreate: value => state => { const id = `mem${state.memoryNext}` - const key = /** @type {Key} */ (asNominal(id)) + const key = asNominal(id) return [{ ...state, memoryNext: state.memoryNext + 1, diff --git a/fjs/effects/node/virtual/proof.f.mjs b/fjs/effects/node/virtual/proof.f.mjs index 3c261e733d..5078e62868 100644 --- a/fjs/effects/node/virtual/proof.f.mjs +++ b/fjs/effects/node/virtual/proof.f.mjs @@ -1,5 +1,5 @@ /** - * @import { Dir, JsModule } from './types.ts' + * @import { Dir } from './types.ts' */ import { assert, assertEq } from '../../../asserts/module.f.mjs' @@ -44,7 +44,7 @@ export const proof = { // `!Array.isArray(file)` branch of writeFileOp: the entry exists but is // neither undefined nor an array. /** @type {Dir} */ - const root = { 'a.f.ts': /** @type {JsModule} */ (() => ({})) } + const root = { 'a.f.ts': () => ({}) } const [, result] = virtual({ ...emptyState, root })(writeFile('a.f.ts', vec8(0x42n))) assert(result[0] === 'error') }, @@ -110,13 +110,13 @@ export const proof = { readFileOnJsModule: () => { // readFile on a JsModule path covers typeof file === 'function' branch /** @type {Dir} */ - const root = { 'a.f.ts': /** @type {JsModule} */ (() => ({})) } + const root = { 'a.f.ts': () => ({}) } virtual({ ...emptyState, root })(readFile('a.f.ts')) }, readBytesOnJsModule: () => { // readBytes on a JsModule path covers typeof file === 'function' branch /** @type {Dir} */ - const root = { 'a.f.ts': /** @type {JsModule} */ (() => ({})) } + const root = { 'a.f.ts': () => ({}) } virtual({ ...emptyState, root })(readBytes('a.f.ts', 0, 1)) }, }, @@ -220,7 +220,7 @@ export const proof = { // branch (unlike readFile/readBytes, writeBytes has no separate throw // for JsModule, so this is the reachable way to hit "not a file"). /** @type {Dir} */ - const root = { 'a.f.ts': /** @type {JsModule} */ (() => ({})) } + const root = { 'a.f.ts': () => ({}) } const [, result] = virtual({ ...emptyState, root })(writeBytes('a.f.ts', 0, vec8(0x1n))) assert(result[0] === 'error') }, @@ -348,7 +348,7 @@ export const proof = { // stat on a JsModule entry (neither an array nor a descendable // directory) covers the !Array.isArray(file) branch of statOp. /** @type {Dir} */ - const root = { 'a.f.ts': /** @type {JsModule} */ (() => ({})) } + const root = { 'a.f.ts': () => ({}) } const [, result] = virtual({ ...emptyState, root })(stat('a.f.ts')) assert(result[0] === 'error') assertEq(result[1], `'a.f.ts' is not a file`) diff --git a/fjs/effects/proof.f.mjs b/fjs/effects/proof.f.mjs index 95e351015d..1f129f4b18 100644 --- a/fjs/effects/proof.f.mjs +++ b/fjs/effects/proof.f.mjs @@ -1,5 +1,5 @@ /** - * @import { Effect, Operation, OperationMap } from './types.ts' + * @import { Effect, Operation } from './types.ts' */ import { step, do_, foldStep, forEachStep, mapStep, match, okStep, history, pure, runPure, historyStep } from './module.f.mjs' @@ -29,7 +29,7 @@ export const assertPure = (e, expected) => { /** @type {(command: 'add') => (a: number, b: number) => Effect<_AddOp, number>} */ const doAdd = do_ -const next = match(/** @type {OperationMap<_AddOp, number>} */ ({ add: (a, b) => a + b })) +const next = match({ add: (a, b) => a + b }) /** * An operation set whose command is any `string`, which is what a `Do` node @@ -43,12 +43,12 @@ const next = match(/** @type {OperationMap<_AddOp, number>} */ ({ add: (a, b) => /** @type {(command: string) => (a: number) => Effect<_AnyOp, number>} */ const doAny = do_ -const anyNext = match(/** @type {OperationMap<_AnyOp, number>} */ ({ add: a => a + 1 })) +const anyNext = match({ add: a => a + 1 }) export const proof = { foldStep: { empty: () => { - const e = foldStep(pure(/** @type {readonly number[]} */ ([])), 10, x => s => pure(s + x)) + const e = foldStep(pure([]), 10, x => s => pure(s + x)) assertPure(e, 10) }, threadsState: () => { @@ -62,7 +62,7 @@ export const proof = { }, forEachStep: { empty: () => { - const e = forEachStep(pure(/** @type {readonly number[]} */ ([])), () => pure(undefined)) + const e = forEachStep(pure([]), () => pure(undefined)) assertPure(e, undefined) }, runs: () => { @@ -81,7 +81,7 @@ export const proof = { }, error: () => { const e = step( - pure(error(/** @type {string} */ ('oops'))), + pure(error('oops')), okStep(/** @type {(value: number) => Effect>} */ (v => pure(ok(v * 2))))) const o = runPure(e) diff --git a/fjs/effects/todo/do-generic-operation-signatures.md b/fjs/effects/todo/do-generic-operation-signatures.md new file mode 100644 index 0000000000..0e74261024 --- /dev/null +++ b/fjs/effects/todo/do-generic-operation-signatures.md @@ -0,0 +1,44 @@ +# `do_` cannot express a generic operation signature + +**Priority:** P3 +**Status:** open + +### Problem + +`do_` builds an effect constructor from a command name: + +```js +export const do_ = command => (...payload) => ({ command, payload, continuation: pure }) +``` + +Its result is typed by `Func = (..._: Param) => Effect>`, which +works whenever the operation's parameter and return types are fixed. Three +operations are generic in a type parameter `Func` has nowhere to put, so each +is cast instead: + +| Site | Cast | +| --- | --- | +| `fjs/effects/memory/module.f.mjs:34` | `(value: T) => Effect>` | +| `fjs/effects/memory/module.f.mjs:39` | `(key: Key) => Effect` | +| `fjs/effects/node/module.f.mjs:47` | `(...a: readonly Effect[]) => Effect` | + +The contrast is visible in one file: `fjs/effects/node/module.f.mjs` writes the +non-generic cases as `/** @type {Func} */ const stat = do_('stat')` — an +annotated declaration, checked — and the generic one as an inline cast, which +is not. `memWrite` moved to the declaration form during the cast cleanup for +exactly this reason; `memCreate` and `memRead` could not. + +These are the last casts in the effects API that are load-bearing rather than +noise. See [`todo/inline-type-casts.md`](../../../todo/inline-type-casts.md). + +### Proposal + +Give `Operation` a way to declare type parameters, so `Func` can produce a +generic signature and the three sites become annotated declarations like their +non-generic neighbours. Failing that, record here why it cannot be done, so the +casts stop reading as an oversight. + +### Related + +- [`todo/inline-type-casts.md`](../../../todo/inline-type-casts.md) — the audit + these three came out of. diff --git a/fjs/effects/todo/step-continuation-operation-union.md b/fjs/effects/todo/step-continuation-operation-union.md new file mode 100644 index 0000000000..87dc7b4923 --- /dev/null +++ b/fjs/effects/todo/step-continuation-operation-union.md @@ -0,0 +1,46 @@ +# `step` continuations widen their operation union with a cast + +**Priority:** P3 +**Status:** open + +### Problem + +`step` is typed + +``` +(e: Effect, f: (t: T) => Effect) => Effect +``` + +so a continuation may introduce operations of its own and the result carries the +union. In practice six call sites cast the continuation — or its result — to the +union the caller wants, because inference lands on a narrower or differently +shaped `Q`: + +| Site | Cast | +| --- | --- | +| `fjs/cas/evo/module.f.mjs:456` | `Effect>` around `pure(error(…))` | +| `fjs/cas/module.f.mjs:348` | `(v: Vec) => Effect>` | +| `fjs/djs/module.f.mjs:41` | `(result: Result) => Effect<_CompileOp, number>` | +| `fjs/djs/transpiler/module.f.mjs:103` | `(context: ParseContext) => Effect>` | +| `fjs/effects/proof.f.mjs:85` | `(value: number) => Effect>` | +| `fjs/emergent_testing/proof.f.mjs:448` | `Effect<_RegisterMockOps \| Readdir \| Import, number>` | + +A branch returning `pure(error(…))` infers `Effect>`, and a +branch returning an effect infers its own operation set; the two do not join to +the declared union without help. The `okStep` wrapper shows the same shape. + +Each cast is an override, not a check: if a continuation ever gains an operation +the runner cannot interpret, the cast hides it and the failure surfaces as a +missing handler at run time. + +### Proposal + +Work out whether this is inference losing the contextual type, or `step`'s +signature being unable to express "at least these operations". If the former, a +contextual annotation on the continuation parameter may be enough; if the +latter, `step` needs a way to name the target union. Either way the six sites +should end up checked rather than cast. + +### Related + +- [`todo/inline-type-casts.md`](../../../todo/inline-type-casts.md) diff --git a/fjs/emergent_testing/proof.f.mjs b/fjs/emergent_testing/proof.f.mjs index 489d0df71d..e15959a534 100644 --- a/fjs/emergent_testing/proof.f.mjs +++ b/fjs/emergent_testing/proof.f.mjs @@ -166,10 +166,10 @@ export const returnValueSubTree = () => { const [events, exit] = run({ 'r.proof.f.ts': () => ({ proof: { - outer: /** @type {() => unknown} */ (() => ({ + outer: () => ({ result: /** @type {const} */ (['ok', { inner }]), duration: 0, - })), + }), } }), }) @@ -336,7 +336,7 @@ const makeRegisterRunner = testOp => { effects.reduce( ([st, rs], e) => { const [ns, r] = runner(st)(e) - return /** @type {readonly [_RegisterMockState, readonly unknown[]]} */ ([ns, [...rs, r]]) + return [ns, [...rs, r]] }, /** @type {readonly [_RegisterMockState, readonly unknown[]]} */ ([s, []]), ), @@ -438,7 +438,7 @@ export const registerSelectsContextAndStar = () => { effects.reduce( ([st, rs], e) => { const [ns, r] = runner(st)(e) - return /** @type {readonly [undefined, readonly unknown[]]} */ ([ns, [...rs, r]]) + return [ns, [...rs, r]] }, /** @type {readonly [undefined, readonly unknown[]]} */ ([s, []]), ), @@ -535,13 +535,13 @@ export const helpers = { parseTestSet: { nullReturnsEmpty: () => { const result = parseTestSet(false, null) - assertEq(Array.isArray(result), true) - assertEq((/** @type {unknown[]} */ (result)).length, 0) + assert(result instanceof Array, result) + assertEq(result.length, 0) }, functionWithParamsReturnsEmpty: () => { const result = parseTestSet(false, (/** @type {number} */ _x) => _x) - assertEq(Array.isArray(result), true) - assertEq((/** @type {unknown[]} */ (result)).length, 0) + assert(result instanceof Array, result) + assertEq(result.length, 0) }, }, } diff --git a/fjs/emergent_testing/todo/mockrun-parameters-inference.md b/fjs/emergent_testing/todo/mockrun-parameters-inference.md new file mode 100644 index 0000000000..bfe447498b --- /dev/null +++ b/fjs/emergent_testing/todo/mockrun-parameters-inference.md @@ -0,0 +1,34 @@ +# `mockRun`'s operation map needs `Parameters>` to type-check + +**Priority:** P4 +**Status:** open + +### Problem + +Two proofs in `fjs/emergent_testing/proof.f.mjs` build an operation map for +`mockRun` and cast it to the parameter type, spelled through the function's own +type: + +- line 333 — `/** @type {Parameters>[0]} */` +- line 434 — `/** @type {Parameters>[0]} */` + +Reaching for `Parameters>[0]` to name an argument type is a sign +the call cannot infer it: the map is an object literal whose handlers should be +contextually typed by `mockRun`'s parameter, and instead the literal is typed +first and then forced. + +As with any cast around a value handed to a generic parameter, the enclosing +cast strips the context the callee relies on, so a drifted handler shape is +absorbed rather than reported — the failure mode +[`fjs/AGENTS.md`](../../AGENTS.md) describes for `ToAsyncOperationMap`. + +### Proposal + +Find why `mockRun`'s type arguments are not inferred from the map, and let the +literal be checked at the call site. If explicit type arguments are genuinely +needed, a named helper with the two parameters bound would express that better +than `Parameters` at each site. + +### Related + +- [`todo/inline-type-casts.md`](../../../todo/inline-type-casts.md) diff --git a/fjs/js/todo/token-kind-narrowing.md b/fjs/js/todo/token-kind-narrowing.md new file mode 100644 index 0000000000..589dcae0fa --- /dev/null +++ b/fjs/js/todo/token-kind-narrowing.md @@ -0,0 +1,45 @@ +# A token built from a `string` kind needs a cast to become a `JsToken` + +**Priority:** P3 +**Status:** open + +### Problem + +`JsToken` is a union discriminated by `kind`, and the tokenizers build tokens +from strings that are *known* to be valid kinds — but the collections holding +those strings are typed over `string`, so `has`/lookup does not narrow and each +construction is cast: + +| Site | Cast | +| --- | --- | +| `fjs/js/tokenizer/module.f.mjs:262` | `[kind, /** @type {JsToken} */ ({ kind })]` building `keywordEntries` from `keywords` | +| `fjs/djs/tokenizer/module.f.mjs:393` | `if (keywordSet.has(value)) return /** @type {JsToken} */ ({ kind: value })` | +| `fjs/djs/tokenizer/module.f.mjs:406` | `return /** @type {JsToken} */ ({ kind: tag })` | +| `fjs/djs/tokenizer/module.f.mjs:295` | `/** @type {TokenMetadata} */ (stateMetadata)` — same family, on the metadata rather than the token | + +The `js/tokenizer` one carries a comment saying the claim holds "by +construction": `_KeywordToken` derives its kinds from the same `keywords` list +the entries are built from. That is true, and it is exactly the kind of +invariant that should be expressible rather than asserted in prose — the list is +the source of both. + +`Set.prototype.has` is the obstacle in the `djs` cases: it takes the set's +element type and returns `boolean`, so a `ReadonlySet` cannot narrow +`value` to a keyword union no matter how it was built. + +### Proposal + +Type the keyword and operator collections over the kind union rather than +`string` — `keywords` is already the single source of truth, so `ReadonlySet` +should be derivable from it. Then `has` narrows where the repo's own +`at(op)(operatorMap)` pattern already does, and the four casts go. + +Note that a type predicate would also remove them, and +[`fjs/AGENTS.md`](../../AGENTS.md) allows one where the alternative is a cast — +but only where the predicate body *is* the structural check that defines +membership. Here it would be a `Set` lookup asserting a union, which is the +error-prone shape that section warns about. Prefer typing the collection. + +### Related + +- [`todo/inline-type-casts.md`](../../../todo/inline-type-casts.md) diff --git a/fjs/js/tokenizer/module.f.mjs b/fjs/js/tokenizer/module.f.mjs index 9a500901aa..01d053310b 100644 --- a/fjs/js/tokenizer/module.f.mjs +++ b/fjs/js/tokenizer/module.f.mjs @@ -338,15 +338,15 @@ const hasOperatorToken = op => at(op)(operatorMap) !== null /** @type {(state: _InitialState) => (input: number) => readonly [List, _TokenizerState]} */ const initialStateOp = create( - /** @type {_CreateToToken<_TokenizerState>} */ (state => () => [[{ kind: 'error', message: 'unexpected character' }], state]) + state => () => [[{ kind: 'error', message: 'unexpected character' }], state] )([ - rangeFunc(rangeOneNine)(/** @type {_CreateToToken<_TokenizerState>} */ (() => input => [empty, { kind: 'number', value: fromCharCode(input), numberKind: 'int' }])), - rangeSetFunc(rangeIdStart)(/** @type {_CreateToToken<_TokenizerState>} */ (() => input => [empty, { kind: 'id', value: fromCharCode(input) }])), - rangeSetFunc(rangeSetWhiteSpace)(/** @type {_CreateToToken<_TokenizerState>} */ (() => () => [empty, { kind: 'ws' }])), - rangeSetFunc(rangeSetNewLine)(/** @type {_CreateToToken<_TokenizerState>} */ (() => () => [empty, { kind: 'nl' }])), - rangeFunc(one(quotationMark))(/** @type {_CreateToToken<_TokenizerState>} */ (() => () => [empty, { kind: 'string', value: '' }])), - rangeFunc(one(digit0))(/** @type {_CreateToToken<_TokenizerState>} */ (() => input => [empty, { kind: 'number', value: fromCharCode(input), numberKind: '0' }])), - rangeSetFunc(rangeOpStart)(/** @type {_CreateToToken<_TokenizerState>} */ (() => input => [empty, { kind: 'op', value: fromCharCode(input) }])) + rangeFunc(rangeOneNine)(() => input => [empty, { kind: 'number', value: fromCharCode(input), numberKind: 'int' }]), + rangeSetFunc(rangeIdStart)(() => input => [empty, { kind: 'id', value: fromCharCode(input) }]), + rangeSetFunc(rangeSetWhiteSpace)(() => () => [empty, { kind: 'ws' }]), + rangeSetFunc(rangeSetNewLine)(() => () => [empty, { kind: 'nl' }]), + rangeFunc(one(quotationMark))(() => () => [empty, { kind: 'string', value: '' }]), + rangeFunc(one(digit0))(() => input => [empty, { kind: 'number', value: fromCharCode(input), numberKind: '0' }]), + rangeSetFunc(rangeOpStart)(() => input => [empty, { kind: 'op', value: fromCharCode(input) }]) ]) /** @type {_CreateToToken<_ParseNumberState>} */ @@ -467,12 +467,12 @@ const parseNumberStateOp = create(invalidNumberToToken)([ /** @type {(state: _InvalidNumberState) => (input: number) => readonly [List, _TokenizerState]} */ const invalidNumberStateOp = create( - /** @type {_CreateToToken<_InvalidNumberState>} */ (() => () => [empty, { kind: 'invalidNumber' }]) + () => () => [empty, { kind: 'invalidNumber' }] )([ - rangeSetFunc(rangeSetTerminalForNumber)(/** @type {_CreateToToken<_InvalidNumberState>} */ (() => input => { + rangeSetFunc(rangeSetTerminalForNumber)(() => input => { const next = tokenizeCharCodeOp(input, { kind: 'initial' }) return [{ first: { kind: 'error', message: 'invalid number' }, tail: next[0] }, next[1]] - })) + }) ]) /** @type {readonly NumberRange[]} */ @@ -484,12 +484,12 @@ const rangeSetStringControl = [ /** @type {(state: _ParseStringState) => (input: number) => readonly [List, _TokenizerState]} */ const parseStringStateOp = create( - /** @type {_CreateToToken<_ParseStringState>} */ (state => input => [empty, { kind: 'string', value: appendChar(state.value)(input) }]) + state => input => [empty, { kind: 'string', value: appendChar(state.value)(input) }] )([ - rangeFunc(one(quotationMark))(/** @type {_CreateToToken<_ParseStringState>} */ (state => () => [[{ kind: 'string', value: state.value }], { kind: 'initial' }])), - rangeFunc(one(reverseSolidus))(/** @type {_CreateToToken<_ParseStringState>} */ (state => () => [empty, { kind: 'escapeChar', value: state.value }])), - rangeSetFunc(rangeSetNewLine)(/** @type {_CreateToToken<_ParseStringState>} */ (() => () => [[{ kind: 'error', message: 'unterminated string literal' }], { kind: 'nl' }])), - rangeSetFunc(rangeSetStringControl)(/** @type {_CreateToToken<_ParseStringState>} */ (state => () => [[{ kind: 'error', message: 'unescaped control character in string' }], { kind: 'string', value: state.value }])) + rangeFunc(one(quotationMark))(state => () => [[{ kind: 'string', value: state.value }], { kind: 'initial' }]), + rangeFunc(one(reverseSolidus))(state => () => [empty, { kind: 'escapeChar', value: state.value }]), + rangeSetFunc(rangeSetNewLine)(() => () => [[{ kind: 'error', message: 'unterminated string literal' }], { kind: 'nl' }]), + rangeSetFunc(rangeSetStringControl)(state => () => [[{ kind: 'error', message: 'unescaped control character in string' }], { kind: 'string', value: state.value }]) ]) /** @type {_CreateToToken<_ParseEscapeCharState>} */ @@ -500,13 +500,13 @@ const parseEscapeDefault = state => input => { /** @type {(state: _ParseEscapeCharState) => (input: number) => readonly [List, _TokenizerState]} */ const parseEscapeCharStateOp = create(parseEscapeDefault)([ - rangeSetFunc([one(quotationMark), one(reverseSolidus), one(solidus)])(/** @type {_CreateToToken<_ParseEscapeCharState>} */ (state => input => [empty, { kind: 'string', value: appendChar(state.value)(input) }])), - rangeFunc(one(latinSmallLetterB))(/** @type {_CreateToToken<_ParseEscapeCharState>} */ (state => () => [empty, { kind: 'string', value: appendChar(state.value)(backspace) }])), - rangeFunc(one(latinSmallLetterF))(/** @type {_CreateToToken<_ParseEscapeCharState>} */ (state => () => [empty, { kind: 'string', value: appendChar(state.value)(ff) }])), - rangeFunc(one(latinSmallLetterN))(/** @type {_CreateToToken<_ParseEscapeCharState>} */ (state => () => [empty, { kind: 'string', value: appendChar(state.value)(lf) }])), - rangeFunc(one(latinSmallLetterR))(/** @type {_CreateToToken<_ParseEscapeCharState>} */ (state => () => [empty, { kind: 'string', value: appendChar(state.value)(cr) }])), - rangeFunc(one(latinSmallLetterT))(/** @type {_CreateToToken<_ParseEscapeCharState>} */ (state => () => [empty, { kind: 'string', value: appendChar(state.value)(ht) }])), - rangeFunc(one(latinSmallLetterU))(/** @type {_CreateToToken<_ParseEscapeCharState>} */ (state => () => [empty, { kind: 'unicodeChar', value: state.value, unicode: 0, hexIndex: 0 }])), + rangeSetFunc([one(quotationMark), one(reverseSolidus), one(solidus)])(state => input => [empty, { kind: 'string', value: appendChar(state.value)(input) }]), + rangeFunc(one(latinSmallLetterB))(state => () => [empty, { kind: 'string', value: appendChar(state.value)(backspace) }]), + rangeFunc(one(latinSmallLetterF))(state => () => [empty, { kind: 'string', value: appendChar(state.value)(ff) }]), + rangeFunc(one(latinSmallLetterN))(state => () => [empty, { kind: 'string', value: appendChar(state.value)(lf) }]), + rangeFunc(one(latinSmallLetterR))(state => () => [empty, { kind: 'string', value: appendChar(state.value)(cr) }]), + rangeFunc(one(latinSmallLetterT))(state => () => [empty, { kind: 'string', value: appendChar(state.value)(ht) }]), + rangeFunc(one(latinSmallLetterU))(state => () => [empty, { kind: 'unicodeChar', value: state.value, unicode: 0, hexIndex: 0 }]), ]) /** @type {_CreateToToken<_ParseUnicodeCharState>} */ @@ -543,7 +543,7 @@ const parseIdDefault = state => input => { /** @type {(state: _ParseIdState) => (input: number) => readonly [List, _TokenizerState]} */ const parseIdStateOp = create(parseIdDefault)([ - rangeSetFunc(rangeId)(/** @type {_CreateToToken<_ParseIdState>} */ (state => input => [empty, { kind: 'id', value: appendChar(state.value)(input) }])) + rangeSetFunc(rangeId)(state => input => [empty, { kind: 'id', value: appendChar(state.value)(input) }]) ]) /** @type {(state: _ParseOperatorState) => (input: number) => readonly [List, _TokenizerState]} */ @@ -563,30 +563,30 @@ const parseOperatorStateOp = state => input => { /** @type {(state: _ParseCommentState) => (input: number) => readonly [List, _TokenizerState]} */ const parseSinglelineCommentStateOp = create( - /** @type {_CreateToToken<_ParseCommentState>} */ (state => input => [empty, { ...state, value: appendChar(state.value)(input) }]) + state => input => [empty, { ...state, value: appendChar(state.value)(input) }] )([ - rangeSetFunc(rangeSetNewLine)(/** @type {_CreateToToken<_ParseCommentState>} */ (state => () => [[{ kind: '//', value: state.value }], { kind: 'nl' }])) + rangeSetFunc(rangeSetNewLine)(state => () => [[{ kind: '//', value: state.value }], { kind: 'nl' }]) ]) /** @type {(state: _ParseCommentState) => (input: number) => readonly [List, _TokenizerState]} */ const parseMultilineCommentStateOp = create( - /** @type {_CreateToToken<_ParseCommentState>} */ (state => input => [empty, { ...state, value: appendChar(state.value)(input) }]) + state => input => [empty, { ...state, value: appendChar(state.value)(input) }] )([ - rangeFunc(one(asterisk))(/** @type {_CreateToToken<_ParseCommentState>} */ (state => () => [empty, { ...state, kind: '/**' }])), - rangeSetFunc(rangeSetNewLine)(/** @type {_CreateToToken<_ParseCommentState>} */ (state => input => [empty, { ...state, value: appendChar(state.value)(input), newLine: true }])), + rangeFunc(one(asterisk))(state => () => [empty, { ...state, kind: '/**' }]), + rangeSetFunc(rangeSetNewLine)(state => input => [empty, { ...state, value: appendChar(state.value)(input), newLine: true }]), ]) /** @type {(state: _ParseCommentState) => (input: number) => readonly [List, _TokenizerState]} */ const parseMultilineCommentAsteriskStateOp = create( - /** @type {_CreateToToken<_ParseCommentState>} */ (state => input => [empty, { ...state, kind: '/*', value: appendChar(appendChar(state.value)(asterisk))(input) }]) + state => input => [empty, { ...state, kind: '/*', value: appendChar(appendChar(state.value)(asterisk))(input) }] )([ - rangeFunc(one(asterisk))(/** @type {_CreateToToken<_ParseCommentState>} */ (state => () => [empty, { ...state, value: appendChar(state.value)(asterisk) }])), - rangeSetFunc(rangeSetNewLine)(/** @type {_CreateToToken<_ParseCommentState>} */ (state => input => [empty, { kind: '/*', value: appendChar(appendChar(state.value)(asterisk))(input), newLine: true }])), - rangeFunc(one(solidus))(/** @type {_CreateToToken<_ParseCommentState>} */ (state => () => { + rangeFunc(one(asterisk))(state => () => [empty, { ...state, value: appendChar(state.value)(asterisk) }]), + rangeSetFunc(rangeSetNewLine)(state => input => [empty, { kind: '/*', value: appendChar(appendChar(state.value)(asterisk))(input), newLine: true }]), + rangeFunc(one(solidus))(state => () => { /** @type {List} */ const tokens = state.newLine ? [{ kind: '/*', value: state.value }, { kind: 'nl' }] : [{ kind: '/*', value: state.value }] return [tokens, { kind: 'initial' }] - })) + }) ]) /** @type {_CreateToToken<_ParseWhitespaceState>} */ @@ -597,8 +597,8 @@ const parseWhitespaceDefault = () => input => { /** @type {(state: _ParseWhitespaceState) => (input: number) => readonly [List, _TokenizerState]} */ const parseWhitespaceStateOp = create(parseWhitespaceDefault)([ - rangeSetFunc(rangeSetWhiteSpace)(/** @type {_CreateToToken<_ParseWhitespaceState>} */ (state => () => [empty, state])), - rangeSetFunc(rangeSetNewLine)(/** @type {_CreateToToken<_ParseWhitespaceState>} */ (() => () => [empty, { kind: 'nl' }])) + rangeSetFunc(rangeSetWhiteSpace)(state => () => [empty, state]), + rangeSetFunc(rangeSetNewLine)(() => () => [empty, { kind: 'nl' }]) ]) /** @type {_CreateToToken<_ParseNewLineState>} */ @@ -609,13 +609,13 @@ const parseNewLineDefault = () => input => { /** @type {(state: _ParseNewLineState) => (input: number) => readonly [List, _TokenizerState]} */ const parseNewLineStateOp = create(parseNewLineDefault)([ - rangeSetFunc(rangeSetWhiteSpace)(/** @type {_CreateToToken<_ParseNewLineState>} */ (state => () => [empty, state])), - rangeSetFunc(rangeSetNewLine)(/** @type {_CreateToToken<_ParseNewLineState>} */ (state => () => [empty, state])) + rangeSetFunc(rangeSetWhiteSpace)(state => () => [empty, state]), + rangeSetFunc(rangeSetNewLine)(state => () => [empty, state]) ]) /** @type {(state: _EofState) => (input: number) => readonly [List, _TokenizerState]} */ const eofStateOp = create( - /** @type {_CreateToToken<_EofState>} */ (state => () => [[{ kind: 'error', message: 'eof' }], state]) + state => () => [[{ kind: 'error', message: 'eof' }], state] )([]) /** @type {StateScan>} */ diff --git a/fjs/mcp/cas/module.f.mjs b/fjs/mcp/cas/module.f.mjs index a10bc882c6..b960a98651 100644 --- a/fjs/mcp/cas/module.f.mjs +++ b/fjs/mcp/cas/module.f.mjs @@ -101,17 +101,12 @@ * * @module * - * @import { Effect } from '../../effects/types.ts' * @import { MemOp } from '../../effects/memory/types.ts' * @import { Vec } from '../../types/bit_vec/types.ts' - * @import { Ok } from '../../types/result/types.ts' - * @import { ToolEntry, ToolsCallResult } from '../../protocol/mcp/types.ts' + * @import { ToolEntry } from '../../protocol/mcp/types.ts' * @import { FileCasOperation } from '../../cas/types.ts' * @import { Cache } from '../../cas/evo/types.ts' * @import { Key } from '../../effects/memory/types.ts' - * @import { Ts } from '../../types/rtti/ts/types.ts' - * @import { List } from '../../effects/list/types.ts' - * @import { IoResult } from '../../effects/node/types.ts' */ import { string, option, or, boolean } from '../../types/rtti/module.f.mjs' @@ -191,8 +186,7 @@ export const casToolRegistry = home => cacheKey => { 'cas_add', 'Store content and return its hash (cBase32). Pass type:"base64" for binary; omit or pass type:"text" for UTF-8 text (default). Inline content is capped at 128 KiB (131072 bytes) — larger content is rejected. For larger content, store the file with the `cas` CLI instead: run `npx functionalscript cas add ` yourself if you have shell access, or give the user that exact command to run — it prints the resulting hash on stdout.', casAddArgs, - /** @type {(args: Ts) => Effect} */ - (({ type, content }) => { + ({ type, content }) => { // type:'text' or 'base64' — resolve content to Vec, store via c.write() /** @type {Vec | null} */ let x = type === 'base64' @@ -202,25 +196,23 @@ export const casToolRegistry = home => cacheKey => { ? pure(errorResult('too large or malformed — for large content, run `npx functionalscript cas add ` (or have the user run it) instead')) // The resolved content fits in one chunk; feed it as a single-item stream. : step( - c.write(nonEmpty(ok(x), /** @type {List>} */ (elEmpty()))), - /** @type {(writeResult: IoResult) => Effect} */ - (writeResult => { + c.write(nonEmpty(ok(x), elEmpty())), + writeResult => { if (writeResult[0] === 'error') { return pure(errorResult('write')) } const hash = writeResult[1] return step( - syncRevision(cacheKey)(hash)(/** @type {Vec} */ (x)), + syncRevision(cacheKey)(hash)(x), () => pure(okResult(vecToCBase32(hash))) ) - }), + }, ) - }), + }, ), toolEntry( 'cas_get', 'Inspect a blob by hash. Always returns JSON {length,mimeType,type[,uri]} where type is "text" or "base64". Pass content:true to also include the inline payload as text (type:"text") or blob (type:"base64"), but content is capped at 128 KiB (131072 bytes) — a larger blob is rejected with an error. To download a blob, prefer the uri field returned in the result instead of requesting inline content.', casGetArgs, - /** @type {(args: Ts) => Effect} */ - (r => { + r => { const key = cBase32ToVec(r.hash) if (key === null) { return pure(errorResult(`invalid cBase32 hash: ${r.hash}`)) @@ -295,17 +287,16 @@ export const casToolRegistry = home => cacheKey => { ) }, ) - }), + }, ), toolEntry( 'cas_list', 'List all stored content hashes (cBase32), one per line.', casListArgs, - /** @type {() => Effect} */ - (() => step( + () => step( c.list(), hashes => pure(okResult(hashes.map(vecToCBase32).join('\n'))) - )), + ), ), ] } diff --git a/fjs/mcp/evo/module.f.mjs b/fjs/mcp/evo/module.f.mjs index 011341b20b..df959e7f5b 100644 --- a/fjs/mcp/evo/module.f.mjs +++ b/fjs/mcp/evo/module.f.mjs @@ -43,11 +43,10 @@ * * @module * - * @import { Effect, Operation } from '../../effects/types.ts' + * @import { Operation } from '../../effects/types.ts' * @import { MemOp } from '../../effects/memory/types.ts' - * @import { ToolEntry, ToolsCallResult } from '../../protocol/mcp/types.ts' + * @import { ToolEntry } from '../../protocol/mcp/types.ts' * @import { Evo } from '../../cas/evo/types.ts' - * @import { Ts } from '../../types/rtti/ts/types.ts' */ import { string, option, array } from '../../types/rtti/module.f.mjs' @@ -121,7 +120,7 @@ export const evoToolRegistry = e => [ // constrained to a newline-free alphabet), so a `join('\n')` line // format could not represent an empty subject or one containing a // newline without ambiguity — JSON encoding can. - /** @type {(args: Ts) => Effect} */ + (({ archived }) => step( e.list(archived), subjects => pure(okResult(toJson(subjects))) @@ -131,11 +130,10 @@ export const evoToolRegistry = e => [ 'evo_head', 'List the current head hashes (cBase32) of a subject, one per line. Empty when the subject is unknown.', evoHeadArgs, - /** @type {(args: Ts) => Effect} */ - (({ subject }) => step( + ({ subject }) => step( e.head(subject), heads => pure(okResult(heads.join('\n'))), - )), + ), ), toolEntry( 'evo_revision', @@ -145,7 +143,7 @@ export const evoToolRegistry = e => [ // `evo_list`'s. An encoded response that outgrows the transport cap is // the transport's `-32603`, not a tool-level error — see "Result size" // in the module doc. - /** @type {(args: Ts) => Effect} */ + (({ hash }) => step( e.revision(hash), result => pure(result[0] === 'error' ? errorResult(result[1]) : okResult(toJson(result[1]))) @@ -155,10 +153,9 @@ export const evoToolRegistry = e => [ 'evo_add', 'Add a new revision (a `vnd.fjs.revision` blob) and return its hash (cBase32). `subject` is required unless there is exactly one parent, from which it is inherited. `snapshot`, when omitted, is resolved from the parents (zero parents → `subject`, one parent → the parent\'s snapshot; a merge requires an explicit `snapshot`) and written explicitly. `generation` is computed by the server. `lock` is optional resolver input: a map from dependency subject to the cBase32 hash of the content it resolves to, or to a nested map scoping further bindings under that subject (use nesting only for conflicting choices a flat map cannot express, e.g. two dependencies needing different versions of a third). Pass a cBase32 hash instead of a map to share one already stored as a `vnd.fjs.lock` blob; the server records the reference and does not follow it.', evoAddArgs, - /** @type {(input: Ts) => Effect} */ - (input => step( + input => step( e.add(input), result => pure(result[0] === 'error' ? errorResult(result[1]) : okResult(result[1])) - )), + ), ), ] diff --git a/fjs/mcp/evo/proof.f.mjs b/fjs/mcp/evo/proof.f.mjs index 8276faaddb..7c6ea50aa4 100644 --- a/fjs/mcp/evo/proof.f.mjs +++ b/fjs/mcp/evo/proof.f.mjs @@ -32,14 +32,14 @@ const parseSubjects = rttiParse(array(rttiString)) const findEntry = (registry, name) => { const entry = registry.find(e => e.name === name) assert(entry !== undefined, ['missing tool entry', name]) - return /** @type {ToolEntry} */ (entry) + return entry } /** @type {(result: ToolsCallResult) => string} */ const textOf = result => { const [item] = result.content assert(item.type === 'text', ['expected a text content item', item]) - return /** @type {{ text: string }} */ (item).text + return item.text } export const proof = { diff --git a/fjs/mcp/proof.f.mjs b/fjs/mcp/proof.f.mjs index 842f0f25f6..7298aaf391 100644 --- a/fjs/mcp/proof.f.mjs +++ b/fjs/mcp/proof.f.mjs @@ -5,8 +5,8 @@ * @import { Vec } from '../types/bit_vec/types.ts' * @import { FileCasOperation } from '../cas/types.ts' * @import { List } from '../effects/list/types.ts' - * @import { McpSessionState, ToolsCallResult } from '../protocol/mcp/types.ts' - * @import { IoResult, Mkdir, Now, RandomInt, ReadBytes, Rename, } from '../effects/node/types.ts' + * @import { ContentItem, ToolsCallResult } from '../protocol/mcp/types.ts' + * @import { IoResult, Mkdir, Now, RandomInt, ReadBytes, Rename } from '../effects/node/types.ts' * @import { Dir } from '../effects/node/virtual/types.ts' */ @@ -99,7 +99,7 @@ const seedBlob = (root, home = '/home/user') => chunks => { const c = fileCas(sha256)(home) const stream = chunks.reduceRight( (/** @type {List>} */ tail, chunk) => nonEmpty(resultOk(chunk), tail), - /** @type {List>} */ (elEmpty())) + /** @satisfies {List>} */ (elEmpty())) const [state, result] = virtual({ ...emptyState, root })(c.write(stream)) assert(result[0] === 'ok', result) return [state.root, vecToCBase32(result[1])] @@ -143,7 +143,7 @@ const runStdio = const effect = step( initEvo(fileCas(sha256)(home)), cacheKey => step( - create(/** @type {McpSessionState} */ (uninitializedState)), + create(uninitializedState), sessionKey => stdioTransport(mcpStep(casConfig)(casMcpHandlers(home)(cacheKey))(sessionKey)) ) @@ -154,15 +154,80 @@ const runStdio = return stdout.split('\n').filter(line => line.length > 0).slice(1).map(line => unwrap(parseJson(line))) } +/** + * The `result` of a `tools/call` response. + * + * This is the one place a response crosses from `unknown` to a typed + * {@link ToolsCallResult}. The structural essentials are checked here so the + * accessors below — and every call site — do not have to assume them; only the + * final step from a checked `content` array to `ToolsCallResult` is taken on + * trust, because rtti's `parse` reads `Unknown`, and getting there from + * `unknown` is the same problem one level down. + */ /** @type {(resp: unknown) => ToolsCallResult} */ -const resultOf = resp => - /** @type {{ readonly result: ToolsCallResult }} */ (resp).result +const resultOf = resp => { + assert(typeof resp === 'object' && resp !== null && 'result' in resp, resp) + const { result } = resp + assert(typeof result === 'object' && result !== null && 'content' in result, result) + const { content } = result + assert(content instanceof Array, content) + return /** @type {ToolsCallResult} */ (result) +} -/** @type {(resp: unknown) => unknown} */ -const item0 = resp => resultOf(resp).content[0] +/** @type {(resp: unknown) => ContentItem} */ +const item0 = resp => { + const [item] = resultOf(resp).content + assert(item !== undefined, resp) + return item +} /** @type {(resp: unknown) => string} */ -const textOf = resp => (/** @type {{ readonly text: string }} */ (item0(resp))).text +const textOf = resp => { + const item = item0(resp) + assert(item.type === 'text', item) + return item.text +} + +// The accessors below read a response that is `unknown` at this boundary. They +// check their way in rather than casting, so a malformed response fails at the +// read with the offending value attached. + +/** Whether `resp` is an object carrying `key`. */ +/** @type {(resp: unknown, key: 'error' | 'result') => boolean} */ +const has = (resp, key) => typeof resp === 'object' && resp !== null && key in resp + +/** @type {(resp: unknown) => number} */ +const errorCode = resp => { + assert(typeof resp === 'object' && resp !== null && 'error' in resp, resp) + const { error } = resp + assert(typeof error === 'object' && error !== null && 'code' in error, error) + const { code } = error + assert(typeof code === 'number', code) + return code +} + +/** @type {(resp: unknown) => unknown} */ +const idOf = resp => { + assert(typeof resp === 'object' && resp !== null && 'id' in resp, resp) + return resp.id +} + +/** The `name` and `inputSchema.type` of each tool in a `tools/list` result. */ +/** @type {(resp: unknown) => readonly { readonly name: string, readonly schemaType: unknown }[]} */ +const toolsOf = resp => { + assert(typeof resp === 'object' && resp !== null && 'result' in resp, resp) + const { result } = resp + assert(typeof result === 'object' && result !== null && 'tools' in result, result) + const { tools } = result + assert(tools instanceof Array, tools) + return tools.map(t => { + assert(typeof t === 'object' && t !== null && 'name' in t && 'inputSchema' in t, t) + const { name, inputSchema } = t + assert(typeof name === 'string', name) + assert(typeof inputSchema === 'object' && inputSchema !== null && 'type' in inputSchema, inputSchema) + return { name, schemaType: inputSchema.type } + }) +} /** The `text` payload of a `cas_get` response, parsed and checked against {@link casGetResult}. */ const casGetResultOf = (/** @type {unknown} */ resp) => @@ -176,12 +241,12 @@ const textSample = 'hello, world!' const revisionSample = `{"dialect":"${revisionDialect}","subject":"8","parents":[],"snapshot":"8","generation":0}` // A base64-encoded binary payload for binary add→get round-trips. -const binarySample = /** @type {string} */ (base64Encode(vec8(0x2An))) +const binarySample = base64Encode(vec8(0x2An)) // A base64 blob whose leading bytes are the PNG magic-byte signature, so // `cas_get` detects its type and returns base64 with mimeType image/png. -const pngSample = /** @type {string} */ (base64Encode( - u8ListToVec(msb)([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x01]))) +const pngSample = base64Encode( + u8ListToVec(msb)([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x01])) // Returns the RFC 4648 base64 encoding of `n` zero bytes, computed directly // without bigint arithmetic — independent of `base64.encode` so these tests @@ -209,10 +274,10 @@ const largeMultiChunkBlobMeta = (/** @type {Vec} */ chunk0, /** @type {Vec} */ chunk1, /** @type {string} */ expectedType, /** @type {string} */ expectedMime) => () => { const [root, hash] = seedBlob({})([chunk0, chunk1]) - const [metaResp] = /** @type {readonly unknown[]} */ (runSessionVirtual(root)([ + const [metaResp] = runSessionVirtual(root)([ init, initialized, call(2, 'cas_get', { hash }), - ]).slice(2)) + ]).slice(2) assert(!resultOf(metaResp).isError) const meta = casGetResultOf(metaResp) assertEq(meta.type, expectedType) @@ -265,10 +330,10 @@ export const proof = { // buffered inline. The metadata-only path (above) still returns its size/type. getContentLargeBlobTooLargeError: () => { const [root, hash] = seedBlob({})([asciiChunk, asciiChunk]) - const [getResp] = /** @type {readonly unknown[]} */ (runSessionVirtual(root)([ + const [getResp] = runSessionVirtual(root)([ init, initialized, call(2, 'cas_get', { hash, content: true }), - ]).slice(2)) + ]).slice(2) assertEq(resultOf(getResp).isError, true) const text = textOf(getResp) assert(text.includes('too large')) @@ -302,9 +367,8 @@ export const proof = { const [getResp] = runStdio(root)([ call(2, 'cas_get', { hash, content: true }), ]) - const err = /** @type {{ readonly error?: { readonly code: number }, readonly id: unknown }} */ (getResp) - assertEq(err.error?.code, -32603) - assertEq(err.id, 2) + assertEq(errorCode(getResp), -32603) + assertEq(idOf(getResp), 2) }, // The paired boundary case: a blob whose base64 inflation leaves enough @@ -337,19 +401,18 @@ export const proof = { const [getResp] = runStdio(root)([ call(2, 'cas_get', { hash, content: true }), ]) - const err = /** @type {{ readonly error?: { readonly code: number }, readonly id: unknown }} */ (getResp) - assertEq(err.error?.code, -32603) - assertEq(err.id, 2) + assertEq(errorCode(getResp), -32603) + assertEq(idOf(getResp), 2) }, toolsListAdvertisesSevenTools: () => { const [resp] = runSessionVirtual({})([init, initialized, list(2)]).slice(2) - const tools = (/** @type {{ result: { tools: readonly { name: string }[] } }} */ (resp)).result.tools + const tools = toolsOf(resp) assertEq(tools.length, 7) assertEq(tools.map(t => t.name).join(','), 'cas_add,cas_get,cas_list,evo_list,evo_head,evo_revision,evo_add') - const add = (/** @type {{ result: { tools: readonly { inputSchema: { type?: string } }[] } }} */ (resp)) - .result.tools[0] - assertEq(add.inputSchema.type, 'object') + const [add] = tools + assert(add !== undefined, tools) + assertEq(add.schemaType, 'object') }, // The same server also exposes Evo (fjs/cas/evo): add a revision, then @@ -476,7 +539,7 @@ export const proof = { call(2, 'cas_add', { content: textSample }), call(3, 'cas_get', { hash, content: true }), ) - assertEq(item0(getResp) === null ? null : (/** @type {{ type: string }} */ (item0(getResp))).type, 'text') + assertEq(item0(getResp).type, 'text') const result = casGetResultOf(getResp) assertEq(result.type, 'text') assertEq(result.mimeType, 'text/plain') @@ -491,7 +554,7 @@ export const proof = { call(3, 'cas_get', { hash, content: true }), ) assert(!resultOf(getResp).isError) - assertEq((/** @type {{ type: string }} */ (item0(getResp))).type, 'text') + assertEq(item0(getResp).type, 'text') const result = casGetResultOf(getResp) assertEq(result.type, 'base64') assertEq(result.mimeType, 'image/png') @@ -620,8 +683,8 @@ export const proof = { toolErrorIsNotJsonRpcError: () => { const [resp] = session(call(2, 'cas_add', { content: 'not valid!', type: 'base64' })) - assert(!('error' in /** @type {object} */ (resp))) - assert('result' in /** @type {object} */ (resp)) + assert(!has(resp, 'error'), resp) + assert(has(resp, 'result'), resp) }, // cas_get without content:true returns only metadata. @@ -657,7 +720,7 @@ export const proof = { getMetaOctetStreamForUnknownBinary: () => { const binaryContent = u8ListToVec(msb)([0xFF, 0xFE, 0x00, 0x01]) - const binaryB64 = /** @type {string} */ (base64Encode(binaryContent)) + const binaryB64 = base64Encode(binaryContent) const [addResp] = session(call(2, 'cas_add', { content: binaryB64, type: 'base64' })) const hash = textOf(addResp) const [, metaResp] = session( @@ -675,7 +738,7 @@ export const proof = { // base64/octet-stream, not text/plain. getMetaOctetStreamForNulBlob: () => { const nulContent = u8ListToVec(msb)([0x00, 0x00, 0x00]) - const nulB64 = /** @type {string} */ (base64Encode(nulContent)) + const nulB64 = base64Encode(nulContent) const [addResp] = session(call(2, 'cas_add', { content: nulB64, type: 'base64' })) const hash = textOf(addResp) const [, metaResp] = session( @@ -702,7 +765,7 @@ export const proof = { // cas_get with content:true on octet-stream (no magic bytes, not UTF-8) returns inline base64. getOctetStreamWithContentIncludesBase64: () => { const binaryContent = u8ListToVec(msb)([0xFF, 0xFE, 0x00, 0x01]) - const binaryB64 = /** @type {string} */ (base64Encode(binaryContent)) + const binaryB64 = base64Encode(binaryContent) const [addResp] = session(call(2, 'cas_add', { content: binaryB64, type: 'base64' })) const hash = textOf(addResp) const [, getResp] = session( diff --git a/fjs/media/html/module.f.mjs b/fjs/media/html/module.f.mjs index 5a2bc00058..ffc2b360c5 100644 --- a/fjs/media/html/module.f.mjs +++ b/fjs/media/html/module.f.mjs @@ -67,7 +67,7 @@ const escapeTable = /** @type {const} */ ({ /** @type {(code: number) => string} */ const escapeCharCode = code => - escapeTable[/** @type {keyof typeof escapeTable} */ (code)] ?? fromCharCode(code) + escapeTable[code] ?? fromCharCode(code) const escape = compose(stringToList)(map(escapeCharCode)) diff --git a/fjs/media/json/parser/module.f.mjs b/fjs/media/json/parser/module.f.mjs index f33c9c859d..b55a195c84 100644 --- a/fjs/media/json/parser/module.f.mjs +++ b/fjs/media/json/parser/module.f.mjs @@ -26,6 +26,7 @@ * @import { NumberPolicy, ParseUnknown, _JsonObject, _JsonArray, _StateParse, _JsonState, _JsonStack, _ValueToken } from './types.ts' */ +import { assert } from '../../../asserts/module.f.mjs' import { error, ok } from '../../../types/result/module.f.mjs' import { fold, next, toArray, concat } from '../../../types/list/module.f.mjs' import { setReplace } from '../../../types/ordered_map/module.f.mjs' @@ -62,11 +63,15 @@ const addToArray = * @param {_StateParse

} state * @returns {(key: string) => _JsonState

} */ -const pushKey = state => value => ({ - status: '{k', - top: addKeyToObject(/** @type {_JsonObject

} */ (state.top))(value), - stack: state.stack, -}) +const pushKey = state => value => { + const { top } = state + assert(top !== null && top.kind === 'object', top) + return { + status: '{k', + top: addKeyToObject(top)(value), + stack: state.stack, + } +} /** @type {

(state: _StateParse

) => (value: ParseUnknown

) => _JsonState

} */ const pushValue = state => value => { @@ -105,7 +110,9 @@ const popStack = stack => { * @returns {_JsonState

} */ const endArray = state => { - const array = toArray(/** @type {_JsonArray

} */ (state.top).values) + const { top } = state + assert(top !== null && top.kind === 'array', top) + const array = toArray(top.values) const newState = popStack(state.stack) return pushValue(newState)(array) } @@ -125,7 +132,9 @@ const startObject = state => { * @returns {_JsonState

} */ const endObject = state => { - const obj = fromMap(/** @type {_JsonObject

} */ (state.top).values) + const { top } = state + assert(top !== null && top.kind === 'object', top) + const obj = fromMap(top.values) const newState = popStack(state.stack) return pushValue(newState)(obj) } diff --git a/fjs/media/json/schema/proof.f.mjs b/fjs/media/json/schema/proof.f.mjs index 6e37311e4a..0a9c4a7395 100644 --- a/fjs/media/json/schema/proof.f.mjs +++ b/fjs/media/json/schema/proof.f.mjs @@ -1,5 +1,4 @@ /** - * @import { Unknown as JsonValue } from '../types.ts' * @import { Unknown } from './module.f.mjs' * @import { Data } from '../../../types/rtti/data/types.ts' */ @@ -11,7 +10,7 @@ import { unitBit } from '../../../types/rtti/data/module.f.mjs' import { assert, assertEq } from '../../../asserts/module.f.mjs' /** @type {(v: Unknown) => string} */ -const serialize = v => stringify(e => e)(/** @type {JsonValue} */ (/** @type {unknown} */ (v))) +const serialize = v => stringify(e => e)(v) /** @type {(rtti: Parameters[0], expected: Unknown) => () => void} */ const eq = (rtti, expected) => () => { diff --git a/fjs/media/json/serializer/module.f.mjs b/fjs/media/json/serializer/module.f.mjs index 6945ae5523..c988c7f806 100644 --- a/fjs/media/json/serializer/module.f.mjs +++ b/fjs/media/json/serializer/module.f.mjs @@ -76,7 +76,7 @@ const unicodeEscape = unit => const escapeCodePoint = codePoint => (codePoint & errorMask) !== 0 ? unicodeEscape(codePoint & 0xffff) - : escapeTable[/** @type {keyof typeof escapeTable} */ (codePoint)] + : escapeTable[codePoint] ?? (codePoint < space ? unicodeEscape(codePoint) : codePointToString(codePoint)) /** diff --git a/fjs/media/type/proof.f.mjs b/fjs/media/type/proof.f.mjs index 7ab111df88..df4f7e2a91 100644 --- a/fjs/media/type/proof.f.mjs +++ b/fjs/media/type/proof.f.mjs @@ -24,7 +24,7 @@ const bytes = (...b) => u8ListToVec(msb)(b) const stream = (...chunks) => chunks.reduceRight( (tail, c) => nonEmpty(ok(c), tail), - /** @type {List>} */ (emptyList())) + /** @satisfies {List>} */ (emptyList())) // Runs the streaming detector over the given chunks and unwraps the metadata. /** @type {(...chunks: readonly Vec[]) => DetectMeta} */ diff --git a/fjs/module.f.mjs b/fjs/module.f.mjs index 2c8f211ac0..3c2b53a3fc 100644 --- a/fjs/module.f.mjs +++ b/fjs/module.f.mjs @@ -7,6 +7,7 @@ * @import { Commands } from './cli/types.ts' */ +import { assert } from './asserts/module.f.mjs' import { compile } from './djs/module.f.mjs' import { main as testMain } from './emergent_testing/module.f.mjs' import { commands as casCommands } from './cas/cli/module.f.mjs' @@ -53,7 +54,14 @@ const commands = [ const [file, ...args] = options.args return step( import_(file), - x => (/** @type {NodeProgram} */ (unwrap(x).main))({ ...options, args })) + x => { + const { main } = unwrap(x) + // A module named on the command line may export anything; + // fail here with the value rather than as `main is not a + // function` from inside the effect runner. + assert(typeof main === 'function', ['not a NodeProgram', file]) + return /** @type {NodeProgram} */ (main)({ ...options, args }) + }) }, }, ] diff --git a/fjs/nanvm/proof.f.mjs b/fjs/nanvm/proof.f.mjs index df50f3be07..9c794ea5c7 100644 --- a/fjs/nanvm/proof.f.mjs +++ b/fjs/nanvm/proof.f.mjs @@ -121,8 +121,8 @@ const eqProof = (() => { }) /** @type {(c: EqCase) => readonly[string, () => void]} */ const leaf = c => [c.name, () => { - const a = /** @type {any} */(operand(c.a)) - const b = /** @type {any} */(operand(c.b)) + const a = operand(c.a) + const b = operand(c.b) assertEq(a === b, c.eq, [a, c.eq ? '===' : '!==', b]) assertEq(b === a, c.eq) }] @@ -157,7 +157,7 @@ const jsOnly = { }, throw: { toStringThrows: () => String({ toString: () => { throw 'Custom error' } }), - toStringNotAFunction: () => String(/** @type {any} */({ toString: 'hello' })), + toStringNotAFunction: () => String({ toString: 'hello' }), toStringNotPrimitive: () => String({ toString: () => [] }), /** `throws` describes a case's outcome; it is not a value to build. */ throwsIsNotAValue: () => value(() => ['throw']), diff --git a/fjs/nanvm/rust/module.f.mjs b/fjs/nanvm/rust/module.f.mjs index 29a5f15568..ab1e8bb9de 100644 --- a/fjs/nanvm/rust/module.f.mjs +++ b/fjs/nanvm/rust/module.f.mjs @@ -78,7 +78,7 @@ export const valueExpr = v => { case 'bigint': { return `bigint_any(${i64Literal(v)})` } } if (Array.isArray(v)) { - const items = /** @type {readonly Value[]} */(v) + const items = v return items.length === 0 ? 'Array::default().to_any()' : `[${items.map(valueExpr).join(', ')}].to_array().to_any()` diff --git a/fjs/protocol/mcp/module.f.mjs b/fjs/protocol/mcp/module.f.mjs index dc112600fd..846cb6da8c 100644 --- a/fjs/protocol/mcp/module.f.mjs +++ b/fjs/protocol/mcp/module.f.mjs @@ -20,9 +20,16 @@ * @import { Response, Id, RpcError } from '../json_rpc/types.ts' * @import { Type } from '../../types/rtti/types.ts' * @import { - * Implementation, ServerCapabilities, InitializeResult, Tool, - * ToolsListParams, ToolsCallResult, McpHandlers, ToolEntry, - * InitializedState, McpSessionState, McpConfig, + * Implementation, + * ServerCapabilities, + * InitializeResult, + * Tool, + * ToolsListParams, + * ToolsCallResult, + * McpHandlers, + * ToolEntry, + * McpSessionState, + * McpConfig, * } from './types.ts' */ @@ -232,7 +239,8 @@ export const notInitialized = rpcError(-32002)('Server not initialized') const _noParams = option(record(unknown)) /** Initial session state — always start here. */ -export const uninitializedState = /** @type {McpSessionState} */ (['uninitialized']) +/** @type {McpSessionState} */ +export const uninitializedState = ['uninitialized'] /** * State-machine step for an MCP session using memory effects. @@ -284,7 +292,7 @@ export const mcpStep = ({ read(stateKey), ([t]) => t === 'initializing' ? step( - write(stateKey, ['initialized', /** @type {InitializedState} */ (true)]), + write(stateKey, ['initialized', true]), () => pure(null), ) : pure(null), diff --git a/fjs/protocol/mcp/proof.f.mjs b/fjs/protocol/mcp/proof.f.mjs index 157f4258d2..344ed62b00 100644 --- a/fjs/protocol/mcp/proof.f.mjs +++ b/fjs/protocol/mcp/proof.f.mjs @@ -3,10 +3,12 @@ * @import { Effect, Operation } from '../../effects/types.ts' * @import { MemOperationMap } from '../../effects/mock/types.ts' * @import { Key, MemOp } from '../../effects/memory/types.ts' - * @import { Ts } from '../../types/rtti/ts/types.ts' * @import { - * ToolsListParams, ToolsListResult, ToolsCallParams, ToolsCallResult, - * McpHandlers, McpConfig, McpSessionState, + * ToolsListParams, + * ToolsCallParams, + * McpHandlers, + * McpConfig, + * McpSessionState, * } from './types.ts' */ @@ -59,13 +61,11 @@ const configNoTools = { ...config, capabilities: {} } const handlers = { // Echoes a received cursor as `nextCursor` so tests can observe pagination params. toolsList: (/** @type {ToolsListParams} */ p) => - /** @type {Effect<_Op, ToolsListResult>} */ - (pure(p.cursor === undefined + pure(p.cursor === undefined ? { tools: [{ name: 'greet', inputSchema: {} }] } - : { tools: [], nextCursor: p.cursor })), + : { tools: [], nextCursor: p.cursor }), toolsCall: (/** @type {ToolsCallParams} */ _p) => - /** @type {Effect<_Op, ToolsCallResult>} */ - (pure({ content: [{ type: 'text', text: 'hello' }] })), + pure({ content: [{ type: 'text', text: 'hello' }] }), } /** @typedef {readonly [unknown, McpSessionState]} _StepResult */ @@ -78,7 +78,7 @@ const runMem = effect => // TypeScript infers O = Operation (the upper bound) rather than O = never when // O flows through McpHandlers, so we cast the widened type down to MemOp. /** @type {(e: Effect) => Effect} */ -const asMemEffect = e => /** @type {Effect} */ (/** @type {unknown} */ (e)) +const asMemEffect = e => /** @type {Effect} */ (e) // Pairs the last step's response with the session state read back afterwards. // The response is still needed after the read, so it is carried forward in a @@ -90,35 +90,115 @@ const withState = key => e => { } // Run one step from uninitializedState, return [response, newState]. -/** @type {(cfg: McpConfig) => (msg: unknown) => _StepResult} */ +/** @type {(cfg: McpConfig) => (msg: Unknown) => _StepResult} */ const step1 = cfg => msg => runMem(asMemEffect(step( - create(/** @type {McpSessionState} */ (uninitializedState)), - key => withState(key)(mcpStep(cfg)(handlers)(key)(/** @type {Unknown} */ (msg)))))) + create(uninitializedState), + key => withState(key)(mcpStep(cfg)(handlers)(key)(msg))))) // Run initialize then a second step, return [response, newState] of the second. -/** @type {(cfg: McpConfig) => (msg1: unknown) => (msg2: unknown) => _StepResult} */ +/** @type {(cfg: McpConfig) => (msg1: Unknown) => (msg2: Unknown) => _StepResult} */ const step2 = cfg => msg1 => msg2 => runMem(asMemEffect(step( - create(/** @type {McpSessionState} */ (uninitializedState)), + create(uninitializedState), key => { - const r1 = mcpStep(cfg)(handlers)(key)(/** @type {Unknown} */ (msg1)) - const r2 = step(r1, () => mcpStep(cfg)(handlers)(key)(/** @type {Unknown} */ (msg2))) + const r1 = mcpStep(cfg)(handlers)(key)(msg1) + const r2 = step(r1, () => mcpStep(cfg)(handlers)(key)(msg2)) return withState(key)(r2) }))) // Run initialize, notifications/initialized, then a third step; return [response, newState] of the third. -/** @type {(cfg: McpConfig) => (msg1: unknown) => (msg2: unknown) => (msg3: unknown) => _StepResult} */ +/** @type {(cfg: McpConfig) => (msg1: Unknown) => (msg2: Unknown) => (msg3: Unknown) => _StepResult} */ const step3 = cfg => msg1 => msg2 => msg3 => runMem(asMemEffect(step( - create(/** @type {McpSessionState} */ (uninitializedState)), + create(uninitializedState), key => { - const r1 = mcpStep(cfg)(handlers)(key)(/** @type {Unknown} */ (msg1)) - const r2 = step(r1, () => mcpStep(cfg)(handlers)(key)(/** @type {Unknown} */ (msg2))) - const r3 = step(r2, () => mcpStep(cfg)(handlers)(key)(/** @type {Unknown} */ (msg3))) + const r1 = mcpStep(cfg)(handlers)(key)(msg1) + const r2 = step(r1, () => mcpStep(cfg)(handlers)(key)(msg2)) + const r3 = step(r2, () => mcpStep(cfg)(handlers)(key)(msg3)) return withState(key)(r3) }))) +// ── Response accessors ──────────────────────────────────────────────────────── +// +// A step's response is `unknown`, and its shape is exactly what these proofs +// exist to check — so reading it through a cast would assume the thing being +// proved. These accessors check their way in instead, and fail at the read +// rather than somewhere downstream. + +/** Whether `resp` is an object carrying an `error` member. */ +/** @type {(resp: unknown) => boolean} */ +const hasError = resp => + typeof resp === 'object' && resp !== null && 'error' in resp + +/** @type {(resp: unknown) => number} */ +const errorCode = resp => { + assert(hasError(resp) && typeof resp === 'object' && resp !== null && 'error' in resp, resp) + const { error } = resp + assert(typeof error === 'object' && error !== null && 'code' in error, error) + const { code } = error + assert(typeof code === 'number', code) + return code +} + +/** @type {(resp: unknown) => unknown} */ +const errorId = resp => { + assert(typeof resp === 'object' && resp !== null && 'id' in resp, resp) + return resp.id +} + +/** @type {(resp: unknown) => object} */ +const resultOf = resp => { + assert(typeof resp === 'object' && resp !== null && 'result' in resp, resp) + const { result } = resp + assert(typeof result === 'object' && result !== null, result) + return result +} + +/** @type {(resp: unknown) => string} */ +const protocolVersion = resp => { + const result = resultOf(resp) + assert('protocolVersion' in result, result) + const { protocolVersion: v } = result + assert(typeof v === 'string', v) + return v +} + +/** @type {(resp: unknown) => readonly string[]} */ +const toolNames = resp => { + const result = resultOf(resp) + assert('tools' in result, result) + const { tools } = result + assert(tools instanceof Array, tools) + return tools.map(t => { + assert(typeof t === 'object' && t !== null && 'name' in t, t) + const { name } = t + assert(typeof name === 'string', name) + return name + }) +} + +/** @type {(resp: unknown) => unknown} */ +const nextCursor = resp => { + const result = resultOf(resp) + assert('nextCursor' in result, result) + return result.nextCursor +} + +/** The `text` of the first content item of a `tools/call` result. */ +/** @type {(resp: unknown) => string} */ +const firstText = resp => { + const result = resultOf(resp) + assert('content' in result, result) + const { content } = result + assert(content instanceof Array && content.length !== 0, content) + const [item] = content + assert(typeof item === 'object' && item !== null && 'text' in item, item) + const { text } = item + assert(typeof text === 'string', text) + return text +} + // ── Test messages ───────────────────────────────────────────────────────────── const initMsg = { jsonrpc: '2.0', method: 'initialize', id: 1, @@ -147,16 +227,14 @@ export const proof = { initializeReturnsResult: () => { const [resp] = step1(config)(initMsg) - assert(resp !== null && typeof resp === 'object' && 'result' in /** @type {object} */ (resp)) - const r = /** @type {{ result: { protocolVersion: string } }} */ (resp).result - assertEq(r.protocolVersion, '2024-11-05') + assertEq(protocolVersion(resp), '2024-11-05') }, initializeWithBadParamsReturnsInvalidParams: () => { const bad = { jsonrpc: '2.0', method: 'initialize', id: 2, params: { wrong: true } } const [resp, newState] = step1(config)(bad) assert(newState[0] === 'uninitialized') - assertEq(/** @type {{ error: { code: number } }} */ (resp).error.code, -32602) + assertEq(errorCode(resp), -32602) }, notificationBeforeInitReturnNull: () => { @@ -174,39 +252,39 @@ export const proof = { doubleInitializeReturnsInvalidRequest: () => { const [resp, newState] = step2(config)(initMsg)(initMsg) assert(newState[0] === 'initializing') - assertEq(/** @type {{ error: { code: number } }} */ (resp).error.code, -32600) + assertEq(errorCode(resp), -32600) }, pingBeforeInitSucceeds: () => { const msg = { jsonrpc: '2.0', method: 'ping', id: 11 } const [resp, newState] = step1(config)(msg) assert(newState[0] === 'uninitialized') - assert(!('error' in /** @type {object} */ (resp))) + assert(!hasError(resp), resp) }, pingDuringInitializingSucceeds: () => { const msg = { jsonrpc: '2.0', method: 'ping', id: 12 } const [resp, newState] = step2(config)(initMsg)(msg) assert(newState[0] === 'initializing') - assert(!('error' in /** @type {object} */ (resp))) + assert(!hasError(resp), resp) }, pingAfterInitSucceeds: () => { const msg = { jsonrpc: '2.0', method: 'ping', id: 15 } const [resp] = step3(config)(initMsg)(initNotif)(msg) - assert(!('error' in /** @type {object} */ (resp))) + assert(!hasError(resp), resp) }, pingWithObjectParamsSucceeds: () => { const msg = { jsonrpc: '2.0', method: 'ping', id: 19, params: {} } const [resp] = step1(config)(msg) - assert(!('error' in /** @type {object} */ (resp))) + assert(!hasError(resp), resp) }, pingInvalidParamsReturnsInvalidParams: () => { const msg = { jsonrpc: '2.0', method: 'ping', id: 20, params: 1 } const [resp] = step1(config)(msg) - assertEq(/** @type {{ error: { code: number } }} */ (resp).error.code, -32602) + assertEq(errorCode(resp), -32602) }, initializedNotificationObjectParamsTransitions: () => { @@ -227,21 +305,21 @@ export const proof = { const msg = { jsonrpc: '2.0', method: 'tools/list', id: 3 } const [resp, newState] = step1(config)(msg) assert(newState[0] === 'uninitialized') - assertEq(/** @type {{ error: { code: number } }} */ (resp).error.code, notInitialized.code) + assertEq(errorCode(resp), notInitialized.code) }, methodDuringInitializingReturnsNotInitialized: () => { const msg = { jsonrpc: '2.0', method: 'tools/list', id: 16 } const [resp, newState] = step2(config)(initMsg)(msg) assert(newState[0] === 'initializing') - assertEq(/** @type {{ error: { code: number } }} */ (resp).error.code, notInitialized.code) + assertEq(errorCode(resp), notInitialized.code) }, invalidEnvelopeReturnsInvalidRequest: () => { const bad = { jsonrpc: '1.0', method: 'ping', id: 4 } const [resp] = step1(config)(bad) - assertEq(/** @type {{ error: { code: number }; id: unknown }} */ (resp).error.code, -32600) - assertEq(/** @type {{ error: { code: number }; id: unknown }} */ (resp).id, null) + assertEq(errorCode(resp), -32600) + assertEq(errorId(resp), null) }, }, @@ -249,68 +327,68 @@ export const proof = { toolsListSucceeds: () => { const msg = { jsonrpc: '2.0', method: 'tools/list', id: 5 } const [resp] = step3(config)(initMsg)(initNotif)(msg) - assertEq(/** @type {{ result: ToolsListResult }} */ (resp).result.tools.length, 1) - assertEq(/** @type {{ result: ToolsListResult }} */ (resp).result.tools[0].name, 'greet') + assertEq(toolNames(resp).length, 1) + assertEq(toolNames(resp)[0], 'greet') }, toolsListPassesCursorToHandler: () => { const msg = { jsonrpc: '2.0', method: 'tools/list', id: 17, params: { cursor: 'page-2' } } const [resp] = step3(config)(initMsg)(initNotif)(msg) - assertEq(/** @type {{ result: ToolsListResult }} */ (resp).result.nextCursor, 'page-2') + assertEq(nextCursor(resp), 'page-2') }, toolsListInvalidCursorReturnsInvalidParams: () => { const msg = { jsonrpc: '2.0', method: 'tools/list', id: 18, params: { cursor: 42 } } const [resp] = step3(config)(initMsg)(initNotif)(msg) - assertEq(/** @type {{ error: { code: number } }} */ (resp).error.code, -32602) + assertEq(errorCode(resp), -32602) }, toolsCallSucceeds: () => { const msg = { jsonrpc: '2.0', method: 'tools/call', id: 6, params: { name: 'greet', arguments: {} } } const [resp] = step3(config)(initMsg)(initNotif)(msg) - assertEq(/** @type {{ text: string }} */ (/** @type {{ result: ToolsCallResult }} */ (resp).result.content[0]).text, 'hello') + assertEq(firstText(resp), 'hello') }, toolsCallBadParamsReturnsInvalidParams: () => { const msg = { jsonrpc: '2.0', method: 'tools/call', id: 7, params: { missing: true } } const [resp] = step3(config)(initMsg)(initNotif)(msg) - assertEq(/** @type {{ error: { code: number } }} */ (resp).error.code, -32602) + assertEq(errorCode(resp), -32602) }, toolsCallAbsentArgumentsSucceeds: () => { const msg = { jsonrpc: '2.0', method: 'tools/call', id: 13, params: { name: 'greet' } } const [resp] = step3(config)(initMsg)(initNotif)(msg) - assertEq(/** @type {{ text: string }} */ (/** @type {{ result: ToolsCallResult }} */ (resp).result.content[0]).text, 'hello') + assertEq(firstText(resp), 'hello') }, toolsCallNullArgumentsReturnsInvalidParams: () => { const msg = { jsonrpc: '2.0', method: 'tools/call', id: 14, params: { name: 'greet', arguments: null } } const [resp] = step3(config)(initMsg)(initNotif)(msg) - assertEq(/** @type {{ error: { code: number } }} */ (resp).error.code, -32602) + assertEq(errorCode(resp), -32602) }, toolsListWithoutCapabilityReturnsMethodNotFound: () => { const msg = { jsonrpc: '2.0', method: 'tools/list', id: 8 } const [resp] = step3(configNoTools)(initMsg)(initNotif)(msg) - assertEq(/** @type {{ error: { code: number } }} */ (resp).error.code, -32601) + assertEq(errorCode(resp), -32601) }, toolsCallWithoutCapabilityReturnsMethodNotFound: () => { const msg = { jsonrpc: '2.0', method: 'tools/call', id: 9, params: { name: 'greet', arguments: {} } } const [resp] = step3(configNoTools)(initMsg)(initNotif)(msg) - assertEq(/** @type {{ error: { code: number } }} */ (resp).error.code, -32601) + assertEq(errorCode(resp), -32601) }, unknownMethodReturnsMethodNotFound: () => { const msg = { jsonrpc: '2.0', method: 'resources/list', id: 10 } const [resp] = step3(config)(initMsg)(initNotif)(msg) - assertEq(/** @type {{ error: { code: number } }} */ (resp).error.code, -32601) + assertEq(errorCode(resp), -32601) }, }, @@ -324,12 +402,13 @@ export const proof = { toolsCallAbsentArgumentsDefaultsToEmptyObject: () => { const echoArgs = /** @type {const} */ ({}) const entry = toolEntry('echo', 'echoes', echoArgs, - /** @type {(a: Ts) => Effect} */ - (() => pure(okResult('ok')))) + () => pure(okResult('ok'))) const handlers = fromRegistry([entry]) const [result] = runPure(handlers.toolsCall({ name: 'echo' })) assert(result !== undefined) - assertEq(/** @type {{ readonly text: string }} */ (result.content[0]).text, 'ok') + const [item] = result.content + assert(item.type === 'text', item) + assertEq(item.text, 'ok') }, }, } diff --git a/fjs/protocol/mcp/stdio/proof.f.mjs b/fjs/protocol/mcp/stdio/proof.f.mjs index 1a5cceaa96..8570e74baf 100644 --- a/fjs/protocol/mcp/stdio/proof.f.mjs +++ b/fjs/protocol/mcp/stdio/proof.f.mjs @@ -2,7 +2,7 @@ * @import { Unknown } from '../../../media/json/types.ts' * @import { Effect } from '../../../effects/types.ts' * @import { State } from '../../../effects/node/virtual/types.ts' - * @import { Id, Response } from '../../json_rpc/types.ts' + * @import { Id } from '../../json_rpc/types.ts' * @import { Step } from './types.ts' */ @@ -51,15 +51,15 @@ const run = runStep(echoStep) /** @type {(id: Id) => string} */ const okResponse = id => - stringifyJson(/** @type {Unknown} */ ({ jsonrpc, result: { ok: true }, id })) + '\n' + stringifyJson({ jsonrpc, result: { ok: true }, id }) + '\n' /** @type {string} */ const parseErrorLine = - stringifyJson(/** @type {Unknown} */ ({ jsonrpc, error: parseError, id: null })) + '\n' + stringifyJson({ jsonrpc, error: parseError, id: null }) + '\n' /** @type {(id: Id) => string} */ const internalErrorLine = id => - stringifyJson(/** @type {Unknown} */ ({ jsonrpc, error: internalError, id })) + '\n' + stringifyJson({ jsonrpc, error: internalError, id }) + '\n' /** @type {(id: number) => string} */ const ping = id => `{"jsonrpc":"2.0","method":"ping","id":${id}}` @@ -122,7 +122,7 @@ export const proof = { const id = idOf(value) return pure(id === undefined ? null - : /** @type {Response} */ (/** @type {unknown} */ ({ jsonrpc, result: { ok: true, nextCursor: undefined }, id }))) + : { jsonrpc, result: { ok: true, nextCursor: undefined }, id }) } const state = runStep(step)(ping(1) + '\n') assertEq(state.stdout, okResponse(1)) diff --git a/fjs/sul/id/module.f.mjs b/fjs/sul/id/module.f.mjs index e101517e88..d0877bad44 100644 --- a/fjs/sul/id/module.f.mjs +++ b/fjs/sul/id/module.f.mjs @@ -7,14 +7,13 @@ * @module * * @import { Vec } from '../../types/bit_vec/types.ts' - * @import { Point2D } from '../../crypto/secp/types.ts' * @import { V8 } from '../../crypto/sha2/types.ts' * @import { Id } from './types.ts' */ import { toArray } from '../../types/list/module.f.mjs' import { length, msb, uint, uintChunkList, unpack, vec } from '../../types/bit_vec/module.f.mjs' -import { assertEq } from '../../asserts/module.f.mjs' +import { assertEq, assertNotNullish } from '../../asserts/module.f.mjs' import { utf8 } from '../../text/module.f.mjs' import { secp256r1 } from '../../crypto/secp/module.f.mjs' import { base32 } from '../../crypto/sha2/module.f.mjs' @@ -33,7 +32,7 @@ const utf8IvSeed = utf8(ivSeed) const c = secp256r1 /** @type {bigint} */ -const ivUint = /** @type {Point2D} */ (c.mul(uint(utf8IvSeed))(c.g))[0] +const ivUint = assertNotNullish(c.mul(uint(utf8IvSeed))(c.g))[0] // 64 hex = 256 bits = 32 bytes: assertEq( diff --git a/fjs/sul/level/hash/module.f.mjs b/fjs/sul/level/hash/module.f.mjs index 5cde93a196..4bab962b0e 100644 --- a/fjs/sul/level/hash/module.f.mjs +++ b/fjs/sul/level/hash/module.f.mjs @@ -9,6 +9,7 @@ * @import { Add, EncodeState } from './types.ts' */ +import { assertNotNullish } from '../../../asserts/module.f.mjs' import { emptyState, patriciaTrie } from '../../../types/patricia_trie/module.f.mjs' import { compress } from '../../id/module.f.mjs' import { asBase } from '../../../types/nominal/module.f.mjs' @@ -49,7 +50,7 @@ export const encode = return [undefined, push([asBase(symbol), symbol], state)] } const [root1, storage1] = end(state) - const [root2, storage2] = rootCreate(/** @type {Id} */ (root1), symbol, storage1) + const [root2, storage2] = rootCreate(assertNotNullish(root1), symbol, storage1) return [root2, [storage2, []]] } } diff --git a/fjs/sul/level/hash/proof.f.mjs b/fjs/sul/level/hash/proof.f.mjs index 3fa6964ce2..793260fce6 100644 --- a/fjs/sul/level/hash/proof.f.mjs +++ b/fjs/sul/level/hash/proof.f.mjs @@ -3,7 +3,7 @@ * @import { EncodeState } from './types.ts' */ -import { assert, assertEq } from '../../../asserts/module.f.mjs' +import { assert, assertEq, assertNotNullish } from '../../../asserts/module.f.mjs' import { compress, level3Id } from '../../id/module.f.mjs' import { emptyEncodeState, encode } from './module.f.mjs' @@ -80,7 +80,7 @@ export const proof = { // Output equals the merged value in the last add call output_is_last_add: () => { const [out, storage] = runWord([s1, s0, s1]) - assertEq(out, /** @type {_NodeList[number]} */ (storage.at(-1))[2]) + assertEq(out, assertNotNullish(storage.at(-1))[2]) }, // Stack is empty after flush; storage is preserved diff --git a/fjs/text/sgr/module.f.mjs b/fjs/text/sgr/module.f.mjs index 23c3f1b5cb..b3bed8c2c2 100644 --- a/fjs/text/sgr/module.f.mjs +++ b/fjs/text/sgr/module.f.mjs @@ -49,13 +49,13 @@ export const csi = end => code => export const sgr = csi('m') /** Resets all SGR styles to terminal defaults. */ -export const reset = /** @type {string} */ (sgr(0)) +export const reset = sgr(0) /** Enables bold/intense text rendering when supported by the terminal. */ -export const bold = /** @type {string} */ (sgr(1)) +export const bold = sgr(1) /** Applies red foreground color to subsequent text. */ -export const fgRed = /** @type {string} */ (sgr(31)) +export const fgRed = sgr(31) /** Applies green foreground color to subsequent text. */ -export const fgGreen = /** @type {string} */ (sgr(32)) +export const fgGreen = sgr(32) const { max } = Math diff --git a/fjs/types/bit_vec/proof.f.mjs b/fjs/types/bit_vec/proof.f.mjs index 3fe10ae381..408f4e4a5b 100644 --- a/fjs/types/bit_vec/proof.f.mjs +++ b/fjs/types/bit_vec/proof.f.mjs @@ -193,9 +193,9 @@ export const proof = { }, removeBack: () => { const v = vec(17n)(0x12345n) - assertEq(v, unsafeVec(0x12345n), (/** @type {bigint} */ (asBase(v))).toString(16)) + assertEq(v, unsafeVec(0x12345n), (asBase(v)).toString(16)) const r = lsb.removeFront(9n)(v) - assertEq(r, unsafeVec(0x91n), (/** @type {bigint} */ (asBase(r))).toString(16)) + assertEq(r, unsafeVec(0x91n), (asBase(r)).toString(16)) }, uint: [ // 0 diff --git a/fjs/types/btree/remove/module.f.mjs b/fjs/types/btree/remove/module.f.mjs index 206bd16e7f..c93b27602b 100644 --- a/fjs/types/btree/remove/module.f.mjs +++ b/fjs/types/btree/remove/module.f.mjs @@ -130,7 +130,7 @@ export const nodeRemove = c => node => { const { first, tail } = find(c)(node) /** @type {(n: TNode) => (f: (v: T) => PathItem) => _RemovePath} */ const branch = n => f => { - const [v, p] = path(/** @type {Path} */(null))(n) + const [v, p] = path(null)(n) return { first: p.first, tail: concat(p.tail)({ first: f(v), tail }) } } const [i, n] = first diff --git a/fjs/types/btree/todo/find-path-item-typing.md b/fjs/types/btree/todo/find-path-item-typing.md new file mode 100644 index 0000000000..27d923a1c9 --- /dev/null +++ b/fjs/types/btree/todo/find-path-item-typing.md @@ -0,0 +1,36 @@ +# `btree/find` casts every tuple it builds or indexes + +**Priority:** P3 +**Status:** open + +### Problem + +Three casts in `fjs/types/btree/find/module.f.mjs`, all the same shape — a +tuple the compiler will not build or read at the width the code knows it has: + +| Line | Cast | | +| --- | --- | --- | +| 17 | `TNode` | `item[1][item[0]]` — indexing a node by a variable index yields the union of its element types, not the element at that index | +| 29 | `PathItem` | `[index, node]` — the pair widens instead of staying a tuple | +| 33 | `First` | `[index, node]` — likewise | + +The two constructions are the more tractable half: a tuple literal in an +argument or property position widens unless something pins it, and the repo +already has that problem solved elsewhere with an annotated `const` or +`@type {const}`. The indexed read is harder, and is the same limitation +`Index<3>`/`Index<5>` ran into in `types/function/compare` — TypeScript cannot +correlate a variable index with the element it selects. + +### Proposal + +Try the annotated-declaration form for lines 29 and 33 first; if the tuple stays +pinned, those two are cast-free with no type changes. For line 17, work out +whether `PathItem` can carry the index as a literal type so `item[1][item[0]]` +resolves, or record that it cannot. + +### Related + +- [`todo/inline-type-casts.md`](../../../../todo/inline-type-casts.md) — where + these three were measured; the `compare` sites in the same audit became + `assert`s over the literal range, which is the fallback if the types cannot + express it. diff --git a/fjs/types/function/compare/module.f.mjs b/fjs/types/function/compare/module.f.mjs index 8e8fa310e5..3cf0409608 100644 --- a/fjs/types/function/compare/module.f.mjs +++ b/fjs/types/function/compare/module.f.mjs @@ -7,14 +7,21 @@ * @import { Cmp1, Cmp2, Compare, Sign } from './types.ts' */ +import { assert } from '../../../asserts/module.f.mjs' + /** @type {(cmp: Compare) => (value: T) => Index<3>} */ -export const index3 - = cmp => value => /** @type {Index<3>} */(cmp(value) + 1) +export const index3 = cmp => value => { + const i = cmp(value) + 1 + assert(i === 0 || i === 1 || i === 2, i) + return i +} /** @type {(cmp: Compare) => (v2: Tuple<2, T>) => Index<5>} */ export const index5 = cmp => ([v0, v1]) => { const _0 = cmp(v0) - return /** @type {Index<5>} */(_0 <= 0 ? _0 + 1 : cmp(v1) + 3) + const i = _0 <= 0 ? _0 + 1 : cmp(v1) + 3 + assert(i === 0 || i === 1 || i === 2 || i === 3 || i === 4, i) + return i } /** @type {(a: A) => >(b: B) => Sign} */ diff --git a/fjs/types/rtti/common/module.f.mjs b/fjs/types/rtti/common/module.f.mjs index f1301130a8..513a1e0cc5 100644 --- a/fjs/types/rtti/common/module.f.mjs +++ b/fjs/types/rtti/common/module.f.mjs @@ -28,12 +28,13 @@ * @module * * @import { Primitive, Unknown } from '../ts/types.ts' - * @import { Const, Info0, Primitive0, Struct, Tag1, Tuple, Type } from '../types.ts' + * @import { Const, Info0, Primitive0, Tag1, Tuple, Type } from '../types.ts' * @import { Error, Result as CommonResult } from '../../result/types.ts' * @import { StringMap } from '../../object/types.ts' * @import { Validate, Visitor, IsContainer, Container, ResultE, ValidateE, ValidationError } from './types.ts' */ +import { assert } from '../../../asserts/module.f.mjs' import { error, ok } from '../../result/module.f.mjs' import { isArray as commonIsArray } from '../../array/module.f.mjs' import { isObject as commonIsObject } from '../../object/module.f.mjs' @@ -80,8 +81,8 @@ export const constPrimitiveValidate = /** @type {(v: Visitor) => (c: Const) => R} */ const visitConst = v => c => typeof c === 'object' && c !== null - ? (commonIsArray(c) ? v.tuple(c) : v.struct(/** @type {Struct} */ (c))) - : v.constPrimitive(/** @type {Primitive} */ (c)) + ? (commonIsArray(c) ? v.tuple(c) : v.struct(c)) + : v.constPrimitive(c) /** `IsContainer` guard for arrays, shared by `validate` and `parse`. */ /** @type {IsContainer>} */ @@ -173,13 +174,19 @@ export const visit = if (typeof rtti === 'function') { const [tag, ...value] = rtti() switch (tag) { - case 'const': return visitConst(v)(/** @type {Const} */ (value[0])) + case 'const': { + const [c] = value + // `Type` is `Const | Thunk`, and a `Thunk` is a function, so + // this is exactly the check that defines `Const`. + assert(typeof c !== 'function', c) + return visitConst(v)(c) + } case 'array': return v.array(value[0]) case 'record': return v.record(value[0]) case 'unknown': return v.unknown() case 'or': return v.or(value) } - return v.primitive0(/** @type {Primitive0} */ (tag)) + return v.primitive0(tag) } return visitConst(v)(rtti) } diff --git a/fjs/types/rtti/common/proof.f.mjs b/fjs/types/rtti/common/proof.f.mjs index b7859f14ea..1b40353114 100644 --- a/fjs/types/rtti/common/proof.f.mjs +++ b/fjs/types/rtti/common/proof.f.mjs @@ -19,12 +19,12 @@ const collect = (acc, k, v) => [...acc, [k, v]] export const proof = { empty: () => { - const r = eachEntry(/** @type {_Entries} */ ([]), item, /** @type {_Entries} */ ([]), collect) + const r = eachEntry([], item, [], collect) assert(r[0] === 'ok') assertEq(r[1].length, 0) }, allOk: () => { - const r = eachEntry([['a', 1], ['b', 2]], item, /** @type {_Entries} */ ([]), collect) + const r = eachEntry([['a', 1], ['b', 2]], item, [], collect) assert(r[0] === 'ok') assertEq(r[1].length, 2) assertEq(r[1][0][0], 'a') @@ -39,7 +39,7 @@ export const proof = { assertEq(r[1], undefined) }, firstErrorWins: () => { - const r = eachEntry([['a', -1], ['b', -2]], item, /** @type {_Entries} */ ([]), collect) + const r = eachEntry([['a', -1], ['b', -2]], item, [], collect) assert(r[0] === 'error') assertEq(r[1].message, 'negative at a') }, @@ -50,7 +50,7 @@ export const proof = { calls++ return item(k, v) } - const r = eachEntry([['a', -1], ['b', -2], ['c', -3]], counting, /** @type {_Entries} */ ([]), collect) + const r = eachEntry([['a', -1], ['b', -2], ['c', -3]], counting, [], collect) assert(r[0] === 'error') assertEq(calls, 1) }, @@ -58,7 +58,7 @@ export const proof = { /** @type {(k: string, v: number) => Result} */ const nested = (k, v) => v < 0 ? error({ path: ['inner'], message: 'bad' }) : ok(v) - const r = eachEntry([['outer', -1]], nested, /** @type {_Entries} */ ([]), collect) + const r = eachEntry([['outer', -1]], nested, [], collect) assert(r[0] === 'error') assertEq(r[1].path.length, 2) assertEq(r[1].path[0], 'outer') diff --git a/fjs/types/rtti/data/module.f.mjs b/fjs/types/rtti/data/module.f.mjs index 36186ab2d5..5f78d4c79a 100644 --- a/fjs/types/rtti/data/module.f.mjs +++ b/fjs/types/rtti/data/module.f.mjs @@ -22,7 +22,7 @@ * @import { ArraySet, Data, KindSet, Node, ObjectSet, RuleSet, UnionSet } from './types.ts' */ -import { assertNotNullish } from '../../../asserts/module.f.mjs' +import { assert, assertNotNullish } from '../../../asserts/module.f.mjs' import { at, definedEntries, definedValues } from '../../object/module.f.mjs' import { ok } from '../../result/module.f.mjs' import { eachEntry, isArray, verror } from '../common/module.f.mjs' @@ -766,7 +766,11 @@ const orUnion = (state, t, operands) => { const thunkUnion = (state, t) => { const [tag, ...rest] = t() switch (tag) { - case 'const': { return constUnion(state, /** @type {Const} */ (rest[0])) } + case 'const': { + const [c] = rest + assert(typeof c !== 'function', c) + return constUnion(state, c) + } case 'boolean': { return [state, { unit: booleanUnits }] } case 'number': { return [state, { number: true }] } case 'string': { return [state, { string: true }] } diff --git a/fjs/types/rtti/parse/module.f.mjs b/fjs/types/rtti/parse/module.f.mjs index 01958f22d7..2491b3a90f 100644 --- a/fjs/types/rtti/parse/module.f.mjs +++ b/fjs/types/rtti/parse/module.f.mjs @@ -32,7 +32,7 @@ * @import { Result as CommonResult } from '../../result/types.ts' * @import { StringMap } from '../../object/types.ts' * @import { List } from '../../list/types.ts' - * @import { Container, IsContainer, ValidateE, ValidationError, Visitor } from '../common/types.ts' + * @import { Container, IsContainer, ValidationError, Visitor } from '../common/types.ts' * @import { Unknown } from '../ts/types.ts' * @import { Parse } from './types.ts' */ @@ -100,7 +100,7 @@ const containerParse = if (e.length === 0) { return /** @type {any} */ (ok(rebuild([]))) } - const itemParse = /** @type {(v: Unknown) => _ItemResult} */ (/** @type {any} */ (parse(item))) + const itemParse = /** @type {any} */ (parse(item)) const r = eachEntry(e, (_k, v) => itemParse(v), emptyEntries, consEntry) return r[0] === 'error' ? r : /** @type {any} */ (ok(rebuild(orderedEntries(r[1])))) } @@ -130,7 +130,7 @@ const constContainerParse = } const r = eachEntry( entries(rtti), - (k, t) => /** @type {_ItemResult} */ (/** @type {any} */ (parse(t))(getItem(value, k))), + (k, t) => (/** @type {any} */ (parse(t))(getItem(value, k))), emptyEntries, consEntry, ) @@ -156,7 +156,7 @@ const orParse = * @returns {Parse<() => readonly ['or', ...T]>} */ rtti => - /** @type {any} */ (orVisit(/** @type {(t: Type) => ValidateE} */ (/** @type {any} */ (parse)))(rtti)) + /** @type {any} */ (orVisit(/** @type {any} */ (parse))(rtti)) /** * Creates a parser function for the given RTTI schema. @@ -195,4 +195,4 @@ const parseVisitor = /** @type {any} */ ({ /** @type {(rtti: T) => Parse} */ export const parse = rtti => - /** @type {any} */ (visit(parseVisitor)(rtti)) + (visit(parseVisitor)(rtti)) diff --git a/fjs/types/rtti/parse/proof.f.mjs b/fjs/types/rtti/parse/proof.f.mjs index e856ec3df2..5fa0c4aab7 100644 --- a/fjs/types/rtti/parse/proof.f.mjs +++ b/fjs/types/rtti/parse/proof.f.mjs @@ -109,32 +109,32 @@ export const proof = { error: () => assertError(parse(/** @type {const} */ (42))(43)), }, nan: { - ok: () => assertOk(parse(/** @type {number} */ (NaN))(NaN)), + ok: () => assertOk(parse(NaN)(NaN)), error: () => { - assertError(parse(/** @type {number} */ (NaN))(0)) + assertError(parse(NaN)(0)) assertError(parse(/** @type {const} */ (0))(NaN)) assertError(parse(/** @type {const} */ (42))(NaN)) }, }, infinity: { ok: () => { - assertOk(parse(/** @type {number} */ (Infinity))(Infinity)) - assertOk(parse(/** @type {number} */ (-Infinity))(-Infinity)) + assertOk(parse(Infinity)(Infinity)) + assertOk(parse(-Infinity)(-Infinity)) }, error: () => { - assertError(parse(/** @type {number} */ (Infinity))(-Infinity)) - assertError(parse(/** @type {number} */ (Infinity))(0)) + assertError(parse(Infinity)(-Infinity)) + assertError(parse(Infinity)(0)) }, }, signedZero: { // `Object.is` distinguishes +0 and -0; `===` treats them equal. distinct: () => { assertError(parse(/** @type {const} */ (0))(-0)) - assertError(parse(/** @type {number} */ (-0))(0)) + assertError(parse(-0)(0)) }, self: () => { assertOk(parse(/** @type {const} */ (0))(0)) - assertOk(parse(/** @type {number} */ (-0))(-0)) + assertOk(parse(-0)(-0)) }, }, string: { @@ -196,7 +196,7 @@ export const proof = { const input = [1, 2, 3] /** @type {readonly number[]} */ const out = unwrap(parse(array(number))(input)) - assert(out !== /** @type {unknown} */ (input), 'expected a fresh array') + assert(out !== input, 'expected a fresh array') assertStructurallySame(out, [1, 2, 3]) }, error: () => { @@ -224,7 +224,7 @@ export const proof = { const input = { a: 1, b: 2 } /** @type {Record} */ const out = unwrap(parse(record(number))(input)) - assert(out !== /** @type {unknown} */ (input), 'expected a fresh record') + assert(out !== input, 'expected a fresh record') assertStructurallySame(out, { a: 1, b: 2 }) }, error: () => { diff --git a/fjs/types/rtti/proof.f.mjs b/fjs/types/rtti/proof.f.mjs index 47109f61ae..10836fed79 100644 --- a/fjs/types/rtti/proof.f.mjs +++ b/fjs/types/rtti/proof.f.mjs @@ -2,6 +2,8 @@ * @import { StringMap } from '../object/types.ts' */ +import { assertNotNullish } from '../../asserts/module.f.mjs' + /** @typedef {StringMap} _Tests */ /** @type {_Tests} */ @@ -16,7 +18,7 @@ const tests = { } export const proof = { - typeof: Object.fromEntries(Object.entries(tests).map(([k, a]) => [k, /** @type {readonly unknown[]} */ (a).map(v => () => { + typeof: Object.fromEntries(Object.entries(tests).map(([k, a]) => [k, assertNotNullish(a).map(v => () => { if (typeof v !== k) { throw `typeof ${v} !== ${k}` } })])), } diff --git a/fjs/types/rtti/validate/module.f.mjs b/fjs/types/rtti/validate/module.f.mjs index 4919658114..d7e63f613e 100644 --- a/fjs/types/rtti/validate/module.f.mjs +++ b/fjs/types/rtti/validate/module.f.mjs @@ -32,7 +32,7 @@ * @import { Unknown } from '../ts/types.ts' * @import { Info1, Struct, Tag1, Tuple, Type } from '../types.ts' * @import { StringMap } from '../../object/types.ts' - * @import { Container, IsContainer, Validate, ValidateE, Visitor } from '../common/types.ts' + * @import { Container, IsContainer, Validate, Visitor } from '../common/types.ts' */ import { ok } from '../../result/module.f.mjs' @@ -137,7 +137,7 @@ const orValidate = * @returns {Validate<() => readonly ['or', ...T]>} */ rtti => - /** @type {any} */ (orVisit(/** @type {(t: Type) => ValidateE} */ (/** @type {any} */ (validate)))(rtti)) + /** @type {any} */ (orVisit(/** @type {any} */ (validate))(rtti)) /** * Creates a validator function for the given RTTI schema. @@ -168,4 +168,4 @@ const validateVisitor = /** @type {any} */ ({ /** @type {(rtti: T) => Validate} */ export const validate = rtti => - /** @type {any} */ (visit(validateVisitor)(rtti)) + (visit(validateVisitor)(rtti)) diff --git a/fjs/types/rtti/validate/proof.f.mjs b/fjs/types/rtti/validate/proof.f.mjs index bd189d7fa7..456e2b214e 100644 --- a/fjs/types/rtti/validate/proof.f.mjs +++ b/fjs/types/rtti/validate/proof.f.mjs @@ -107,32 +107,32 @@ export const proof = { error: () => assertError(validate(/** @type {const} */ (42))(43)), }, nan: { - ok: () => assertOk(validate(/** @type {number} */ (NaN))(NaN)), + ok: () => assertOk(validate(NaN)(NaN)), error: () => { - assertError(validate(/** @type {number} */ (NaN))(0)) + assertError(validate(NaN)(0)) assertError(validate(/** @type {const} */ (0))(NaN)) assertError(validate(/** @type {const} */ (42))(NaN)) }, }, infinity: { ok: () => { - assertOk(validate(/** @type {number} */ (Infinity))(Infinity)) - assertOk(validate(/** @type {number} */ (-Infinity))(-Infinity)) + assertOk(validate(Infinity)(Infinity)) + assertOk(validate(-Infinity)(-Infinity)) }, error: () => { - assertError(validate(/** @type {number} */ (Infinity))(-Infinity)) - assertError(validate(/** @type {number} */ (Infinity))(0)) + assertError(validate(Infinity)(-Infinity)) + assertError(validate(Infinity)(0)) }, }, signedZero: { // `Object.is` distinguishes +0 and -0; `===` treats them equal. distinct: () => { assertError(validate(/** @type {const} */ (0))(-0)) - assertError(validate(/** @type {number} */ (-0))(0)) + assertError(validate(-0)(0)) }, self: () => { assertOk(validate(/** @type {const} */ (0))(0)) - assertOk(validate(/** @type {number} */ (-0))(-0)) + assertOk(validate(-0)(-0)) }, }, string: { diff --git a/todo/inline-type-casts.md b/todo/inline-type-casts.md index 5925e9a287..ec04c0cd78 100644 --- a/todo/inline-type-casts.md +++ b/todo/inline-type-casts.md @@ -1,7 +1,7 @@ # Audit: inline `/** @type {T} */ (v)` casts **Priority:** P2 -**Status:** open +**Status:** implemented — 273 of 357 removed or converted; 84 remain, each with a reason below ### Problem @@ -16,472 +16,225 @@ 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. +This issue was the audit of every site, and then the cleanup. ### 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 | +repository `tsconfig.json`) in two variants: with the cast deleted, and with +`@type` rewritten to `@satisfies`. Sites failing both were inspected by hand — +the compiler error says what the cast is hiding, which decides between `assert*` +and "no replacement available". + +The tree was re-measured after each landed step, because removing one cast can +make another redundant, and because casts in the rtti visitors compete for the +same instantiation-depth budget. + +**`tsc --noEmit` passing is not sufficient**, and review caught it doing the +wrong thing twice (below). Two further checks are required before a removal is +safe: + +1. **Compare the emitted declarations.** `tsc --noEmit false + --emitDeclarationOnly --declarationDir

` before and after; a removal + that changes an exported declaration's *type* is a published-API change no + amount of `--noEmit` will surface. +2. **Watch `--noUnusedLocals`.** A removal that orphans a `@typedef` or an + `@import` entry has taken the last reference to a type — which is a signal + worth reading, not just tidying. + +### What landed + +357 casts at the start, excluding 221 `/** @type {const} */` (out of scope) and +one existing `@satisfies`. **84 of them remain.** Every step kept `npx tsc` and +`fjs t` green. + +| Step | Casts | What changed | +| --- | --: | --- | +| Delete the redundant ones | 182 | `tsc` passes with them gone. 172 in one sweep; the rtti visitors one at a time. `fjs/js/tokenizer` alone lost 37 — the `_CreateToToken` lambdas were already contextually typed through `create(def)(…)`. | +| `Array.isArray` → `instanceof Array` | 8 | `Array.isArray` narrows to `any[]`, which `readonly Vec[]` is not assignable to, so its negative branch never removes a `readonly` array from a union. Swapping the eight guards the `Dir` casts depended on made the surrounding `assert`/`if` narrow on their own. | +| `@satisfies` and annotated declarations | 23 | 6 at a `const` initializer became the declaration form; 17 became `@satisfies`. | +| Cast → runtime check | 17 | `assertNotNullish` for nullish lookups, `assert(top.kind === 'object')` for discriminated unions, literal-range asserts for `_ClassPc`/`Index<3>`/`Index<5>`, `typeof c !== 'function'` for the rtti `'const'` tag. | +| Checked accessors in proofs | 53 | `protocol/mcp/proof` 33 → 0, `mcp/proof` 11 → 1, `cas/proof` 8 → 0, plus five singles. A response is `unknown` and its shape is what the proof exists to check, so every one of those casts assumed the thing being proved. | +| Reverted in review | −10 | see below | +| **Total** | **273** | | + +Two changes were not cast removals but are what made them possible: the local +`step1`/`step2`/`step3` helpers in `protocol/mcp/proof` declared `msg: unknown` +when every caller passes a JSON-RPC object literal, and `fjs run` now asserts a +module's `main` is callable before invoking it. + +### Corrections found in review + +Ten removals were wrong, and `npx tsc` was green for every one of them. + +**Four changed the published API.** Diffing the emitted `.d.mts` against `main` +found: + +| Export | Was | Became | | --- | --- | --- | -| **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 +| `effects/node` `createServer` | `(listener: RequestListener) => Effect` | `(...payload: never) => Effect` | +| `effects/node` `log`, `error` | `Console` | `(s: string) => Effect` | +| `cas/evo` `emptyCache` | `Cache` | `{ bySubject: {} }` | +| `types/range_map` (merge result) | `RangeMapArray` | `[T, number][]` — **`readonly` lost** | + +The `range_map` one is the sharpest: a purely functional library published a +mutable array type. `createServer`'s replacement is barely callable. Three are +now annotated declarations; `createServer` must stay an inline cast, because +`Func` has nowhere to put a type parameter. + +**Six deleted the assertion itself.** `types/nominal/proof.f.mjs` demonstrates, +per branding strategy, whether `<` compiles between two branded values. The +casts *are* the demonstration — they give the values the brand — and a brand is +unconstructible by design, so the declaration form rejects what the inline cast +accepted. Removing them left `const a = {}` comparing against `const b = {}`, +which proves nothing about the brand. `noUnusedLocals` reported the orphaned +`_IntersectionSafeId` typedef, which is how it was caught. + +The lesson generalises: **in a proof about types, the annotation is the test**, +and a checker that only asks "does this still compile" cannot see it being +deleted. + +### What remains, and why + +84 of the 95 sites in the table below are from the audited set; the other 11 +arrived on `main` after the audit (`mcp/cas/proof`, `types/patricia_trie`) and +are untouched here — new code bringing new casts is the argument for +[eslint.md](./eslint.md), not for widening this PR. + +Of the 84, none can be deleted or turned into a meaningful `@satisfies` without +one of the two failures above. `@satisfies {any}` is excluded on +principle: it checks nothing, so an `any` cast is either load-bearing or it +should go. + +| Reason | Count | +| --- | --: | +| Cast overrides a genuine type mismatch (TS2322 / TS2345) | 37 | +| `any` bridge — generic erasure, mostly the rtti visitors and `effects/module` | 24 | +| Unconstructible brand — the cast is the demonstration (6 restored, 2 never removed) | 8 | +| No overlap without `unknown` — a deliberately wrong value (TS2352) | 6 | +| Reads an `unknown` the surrounding code has established (TS18046) | 4 | +| Compiles without it, but the emitted `.d.mts` changes | 2 | +| One each: TS7022 cycle cut, TS2589 depth, nominal `identity` | 3 | +| Arrived on `main` after the audit | 11 | +| **Total** | **95** | + +The 37 TS2322/TS2345 cases are the ones AGENTS.md means by "it usually means the +types or the code structure should be improved instead". Each needs its own +issue against the API it is papering over — `do_('memRead')` typed as +`(key: Key) => Effect`, `ToAsyncOperationMap` at the +node runners, `Index`/tuple arity in `types/btree` — not a different cast +syntax. They are deliberately left rather than rewritten. + +### Follow-ups + +- [strict-static-analysis.md](./strict-static-analysis.md) — its proposed + declaration-emit check is no longer hypothetical: it is what caught the four + API regressions above, and it belongs in CI. +- [eslint.md](./eslint.md) — nothing stops these from becoming 357 again, and + 11 new ones already arrived from `main` mid-cleanup. + `no-unnecessary-type-assertion` would have found most of the first 182 + automatically — though, on the evidence above, it would also have proposed the + ten that were wrong, so the allowlist matters as much as the rule. +- [`spec/todo/3360-type-annotations.md`](../spec/todo/3360-type-annotations.md) — + where the type layer is eventually going. + +### Remaining sites + +| File | Line | `@type {T}` | Why it stays | +| --- | --- | --- | --- | +| `fjs/bnf/descent/module.f.mjs` | 199 | `_Task` | breaks a control-flow inference cycle | +| `fjs/cas/evo/module.f.mjs` | 466 | `Effect>` | cast overrides the inferred type — needs a type/API change, not a different cast | +| `fjs/cas/module.f.mjs` | 348 | `(v: Vec) => Effect>` | cast overrides the inferred type — needs a type/API change, not a different cast | +| `fjs/crypto/sign/proof.f.mjs` | 65 | `any` | `any` bridge — generic erasure with no runtime counterpart; nothing for a check to check | +| `fjs/djs/module.f.mjs` | 41 | `(result: Result) => Effect<_CompileOp…` | cast overrides the inferred type — needs a type/API change, not a different cast | +| `fjs/djs/tokenizer/module.f.mjs` | 295 | `TokenMetadata` | cast overrides the inferred type — needs a type/API change, not a different cast | +| `fjs/djs/tokenizer/module.f.mjs` | 393 | `JsToken` | cast overrides the inferred type — needs a type/API change, not a different cast | +| `fjs/djs/tokenizer/module.f.mjs` | 406 | `JsToken` | cast overrides the inferred type — needs a type/API change, not a different cast | +| `fjs/djs/transpiler/module.f.mjs` | 103 | `(context: ParseContext) => Effect(value: T) => Effect>` | cast overrides the inferred type — needs a type/API change, not a different cast | +| `fjs/effects/memory/module.f.mjs` | 39 | `(key: Key) => Effect` | cast overrides the inferred type — needs a type/API change, not a different cast | +| `fjs/effects/node/memory/module.mjs` | 60 | `ToAsyncOperationMap` | cast overrides the inferred type — needs a type/API change, not a different cast | +| `fjs/effects/node/memory/proof.mjs` | 28 | `import('../../types.ts').ToAsyncOperationMap` | cast overrides the inferred type — needs a type/API change, not a different cast | +| `fjs/effects/node/module.f.mjs` | 47 | `(...a: readonly Effect[]) =>…` | cast overrides the inferred type — needs a type/API change, not a different cast | +| `fjs/effects/node/module.f.mjs` | 57 | `any` | `any` bridge — generic erasure with no runtime counterpart; nothing for a check to check | +| `fjs/effects/node/module.f.mjs` | 186 | `(listener: RequestListener) => Eff…` | `tsc --noEmit` passes without it, but the emitted `.d.mts` changes — see "Corrections found in review" | +| `fjs/effects/node/module.mjs` | 287 | `Erl` | cast overrides the inferred type — needs a type/API change, not a different cast | +| `fjs/effects/node/module.mjs` | 305 | `_Server` | reads an `unknown` the surrounding code has already established | +| `fjs/effects/node/virtual/module.f.mjs` | 409 | `SandboxResult` | cast overrides the inferred type — needs a type/API change, not a different cast | +| `fjs/effects/proof.f.mjs` | 85 | `(value: number) => Effect` | cast overrides the inferred type — needs a type/API change, not a different cast | +| `fjs/fsc/proof.f.mjs` | 21 | `any` | `any` bridge — generic erasure with no runtime counterpart; nothing for a check to check | +| `fjs/js/tokenizer/module.f.mjs` | 262 | `JsToken` | cast overrides the inferred type — needs a type/API change, not a different cast | +| `fjs/js/tokenizer/module.f.mjs` | 689 | `List>` | cast overrides the inferred type — needs a type/API change, not a different cast | +| `fjs/mcp/cas/proof.f.mjs` | 33 | `readonly unknown[]` | arrived on `main` after the audit — not measured here | +| `fjs/mcp/cas/proof.f.mjs` | 38 | `unknown` | arrived on `main` after the audit — not measured here | +| `fjs/mcp/cas/proof.f.mjs` | 65 | `Parameters[0]` | arrived on `main` after the audit — not measured here | +| `fjs/mcp/cas/proof.f.mjs` | 85 | `any` | arrived on `main` after the audit — not measured here | +| `fjs/mcp/cas/proof.f.mjs` | 93 | `Key` | arrived on `main` after the audit — not measured here | +| `fjs/mcp/cas/proof.f.mjs` | 93 | `any` | arrived on `main` after the audit — not measured here | +| `fjs/mcp/cas/proof.f.mjs` | 101 | `NonNullable` | arrived on `main` after the audit — not measured here | +| `fjs/mcp/cas/proof.f.mjs` | 121 | `ToolsCallResult` | arrived on `main` after the audit — not measured here | +| `fjs/mcp/cas/proof.f.mjs` | 132 | `ToolsCallResult` | arrived on `main` after the audit — not measured here | +| `fjs/mcp/cas/proof.f.mjs` | 149 | `ToolsCallResult` | arrived on `main` after the audit — not measured here | +| `fjs/mcp/proof.f.mjs` | 67 | `Unknown` | cast overrides the inferred type — needs a type/API change, not a different cast | +| `fjs/mcp/proof.f.mjs` | 174 | `ToolsCallResult` | cast overrides the inferred type — needs a type/API change, not a different cast | +| `fjs/media/json/proof.f.mjs` | 18 | `null` | cast overrides the inferred type — needs a type/API change, not a different cast | +| `fjs/media/json/proof.f.mjs` | 18 | `unknown` | no overlap without going through `unknown` — a deliberately wrong value, or a nominal brand | +| `fjs/media/json/proof.f.mjs` | 23 | `null` | cast overrides the inferred type — needs a type/API change, not a different cast | +| `fjs/media/json/proof.f.mjs` | 23 | `unknown` | no overlap without going through `unknown` — a deliberately wrong value, or a nominal brand | +| `fjs/module.f.mjs` | 63 | `NodeProgram` | `tsc --noEmit` passes without it, but the emitted `.d.mts` changes — see "Corrections found in review" | +| `fjs/nanvm/proof.f.mjs` | 59 | `readonly any[]` | reads an `unknown` the surrounding code has already established | +| `fjs/protocol/mcp/module.f.mjs` | 171 | `any` | `any` bridge — generic erasure with no runtime counterpart; nothing for a check to check | +| `fjs/protocol/mcp/module.f.mjs` | 174 | `Ts` | "instantiation excessively deep" | +| `fjs/protocol/mcp/proof.f.mjs` | 81 | `Effect` | cast overrides the inferred type — needs a type/API change, not a different cast | +| `fjs/protocol/mcp/stdio/proof.f.mjs` | 26 | `{ readonly id?: Id }` | cast overrides the inferred type — needs a type/API change, not a different cast | +| `fjs/sul/id/module.f.mjs` | 45 | `V8` | cast overrides the inferred type — needs a type/API change, not a different cast | +| `fjs/text/code_point/module.f.mjs` | 49 | `List>` | cast overrides the inferred type — needs a type/API change, not a different cast | +| `fjs/types/btree/find/module.f.mjs` | 17 | `TNode` | cast overrides the inferred type — needs a type/API change, not a different cast | +| `fjs/types/btree/find/module.f.mjs` | 29 | `PathItem` | cast overrides the inferred type — needs a type/API change, not a different cast | +| `fjs/types/btree/find/module.f.mjs` | 33 | `First` | cast overrides the inferred type — needs a type/API change, not a different cast | +| `fjs/types/function/compare/module.f.mjs` | 29 | `any` | `any` bridge — generic erasure with no runtime counterpart; nothing for a check to check | +| `fjs/types/function/compare/module.f.mjs` | 29 | `any` | `any` bridge — generic erasure with no runtime counterpart; nothing for a check to check | +| `fjs/types/nominal/module.f.mjs` | 12 | `(b: B) => Nominal(n: Nominal` | cast overrides the inferred type — needs a type/API change, not a different cast | +| `fjs/types/rtti/common/module.f.mjs` | 61 | `any` | `any` bridge — generic erasure with no runtime counterpart; nothing for a check to check | +| `fjs/types/rtti/common/module.f.mjs` | 78 | `any` | `any` bridge — generic erasure with no runtime counterpart; nothing for a check to check | +| `fjs/types/rtti/module.f.mjs` | 27 | `any` | `any` bridge — generic erasure with no runtime counterpart; nothing for a check to check | +| `fjs/types/rtti/module.f.mjs` | 69 | `any` | `any` bridge — generic erasure with no runtime counterpart; nothing for a check to check | +| `fjs/types/rtti/parse/module.f.mjs` | 101 | `any` | `any` bridge — generic erasure with no runtime counterpart; nothing for a check to check | +| `fjs/types/rtti/parse/module.f.mjs` | 103 | `any` | `any` bridge — generic erasure with no runtime counterpart; nothing for a check to check | +| `fjs/types/rtti/parse/module.f.mjs` | 105 | `any` | `any` bridge — generic erasure with no runtime counterpart; nothing for a check to check | +| `fjs/types/rtti/parse/module.f.mjs` | 133 | `any` | `any` bridge — generic erasure with no runtime counterpart; nothing for a check to check | +| `fjs/types/rtti/parse/module.f.mjs` | 137 | `any` | `any` bridge — generic erasure with no runtime counterpart; nothing for a check to check | +| `fjs/types/rtti/parse/module.f.mjs` | 159 | `any` | `any` bridge — generic erasure with no runtime counterpart; nothing for a check to check | +| `fjs/types/rtti/parse/module.f.mjs` | 159 | `any` | `any` bridge — generic erasure with no runtime counterpart; nothing for a check to check | +| `fjs/types/rtti/parse/module.f.mjs` | 185 | `any` | `any` bridge — generic erasure with no runtime counterpart; nothing for a check to check | +| `fjs/types/rtti/parse/proof.f.mjs` | 30 | `T` | cast overrides the inferred type — needs a type/API change, not a different cast | +| `fjs/types/rtti/parse/proof.f.mjs` | 37 | `ValidationError` | reads an `unknown` the surrounding code has already established | +| `fjs/types/rtti/parse/proof.f.mjs` | 316 | `_A` | cast overrides the inferred type — needs a type/API change, not a different cast | +| `fjs/types/rtti/parse/proof.f.mjs` | 316 | `unknown` | no overlap without going through `unknown` — a deliberately wrong value, or a nominal brand | +| `fjs/types/rtti/validate/module.f.mjs` | 88 | `any` | `any` bridge — generic erasure with no runtime counterpart; nothing for a check to check | +| `fjs/types/rtti/validate/module.f.mjs` | 114 | `any` | `any` bridge — generic erasure with no runtime counterpart; nothing for a check to check | +| `fjs/types/rtti/validate/module.f.mjs` | 120 | `any` | `any` bridge — generic erasure with no runtime counterpart; nothing for a check to check | +| `fjs/types/rtti/validate/module.f.mjs` | 140 | `any` | `any` bridge — generic erasure with no runtime counterpart; nothing for a check to check | +| `fjs/types/rtti/validate/module.f.mjs` | 140 | `any` | `any` bridge — generic erasure with no runtime counterpart; nothing for a check to check | +| `fjs/types/rtti/validate/module.f.mjs` | 158 | `any` | `any` bridge — generic erasure with no runtime counterpart; nothing for a check to check | +| `fjs/types/rtti/validate/proof.f.mjs` | 23 | `ValidationError` | reads an `unknown` the surrounding code has already established | +| `fjs/types/rtti/validate/proof.f.mjs` | 307 | `_A` | cast overrides the inferred type — needs a type/API change, not a different cast | +| `fjs/types/rtti/validate/proof.f.mjs` | 307 | `unknown` | no overlap without going through `unknown` — a deliberately wrong value, or a nominal brand | \ No newline at end of file