diff --git a/CHANGELOG.md b/CHANGELOG.md index bc7f0152b..243516324 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,10 @@ history. ## Unreleased +- **BREAKING CHANGES:** `fjs/crypto/sha2` migrates from authored TypeScript + (`.f.ts`) to JSDoc-typed JavaScript (`.f.mjs`) under the stage-1 + TypeScript-to-mjs migration — importers must use the `.f.mjs` specifier + [#1472](https://github.com/functionalscript/functionalscript/pull/1472) - **BREAKING CHANGES:** `fjs/types/btree/set` and `fjs/types/btree/remove` migrate from authored TypeScript (`.f.ts`) to JSDoc-typed JavaScript (`.f.mjs`) under the stage-1 TypeScript-to-mjs migration — importers must use diff --git a/fjs/basen/cbase32/todo/178.md b/fjs/basen/cbase32/todo/178.md index 2295a263e..783a7ad36 100644 --- a/fjs/basen/cbase32/todo/178.md +++ b/fjs/basen/cbase32/todo/178.md @@ -75,7 +75,7 @@ and `cBase32ToVec = s => { const v = cBase32ToVec5x(s); return v === null ? null - **Single primary consumer.** Today only `cbase32` round-trips this padding, so this is the "single-consumer abstraction justified by clarity/separation" case, not a 2-consumer DRY extraction. SHA-2's padding - (`crypto/sha2/module.f.ts:234`) looks similar but is *not* the same operation: + (`crypto/sha2/module.f.mjs:252`) looks similar but is *not* the same operation: it appends a length field and relies on the fixed-width compress word's implicit zero-extension rather than an explicit `padToMultiple`. Do **not** try to force SHA-2 onto this helper. diff --git a/fjs/cas/cli/module.f.ts b/fjs/cas/cli/module.f.ts index 3cfa43ce8..fde829ab7 100644 --- a/fjs/cas/cli/module.f.ts +++ b/fjs/cas/cli/module.f.ts @@ -3,7 +3,7 @@ * * @module */ -import { sha256 } from '../../crypto/sha2/module.f.ts' +import { sha256 } from '../../crypto/sha2/module.f.mjs' import { cBase32ToVec, vecToCBase32 } from '../../basen/cbase32/module.f.mjs' import { forEachStep, pure, step } from '../../effects/module.f.ts' import { diff --git a/fjs/cas/cli/proof.f.ts b/fjs/cas/cli/proof.f.ts index b8f6def26..fb19ed284 100644 --- a/fjs/cas/cli/proof.f.ts +++ b/fjs/cas/cli/proof.f.ts @@ -1,5 +1,5 @@ import { commands } from './module.f.ts' -import { computeSync, sha256 } from '../../crypto/sha2/module.f.ts' +import { computeSync, sha256 } from '../../crypto/sha2/module.f.mjs' import { maxLength, vec, vec8 } from '../../types/bit_vec/module.f.mjs' import { defaultNodeProgramOptions, emptyState, virtual } from '../../effects/node/virtual/module.f.ts' import { type NodeProgramOptions } from '../../effects/node/module.f.ts' diff --git a/fjs/cas/evo/proof.f.ts b/fjs/cas/evo/proof.f.ts index a695c5794..104679c3d 100644 --- a/fjs/cas/evo/proof.f.ts +++ b/fjs/cas/evo/proof.f.ts @@ -1,7 +1,7 @@ import { assert, assertEq } from '../../asserts/module.f.mjs' import { pure } from '../../effects/module.f.ts' import { fileCas, type Cas } from '../module.f.ts' -import { sha256 } from '../../crypto/sha2/module.f.ts' +import { sha256 } from '../../crypto/sha2/module.f.mjs' import { emptyState, virtual } from '../../effects/node/virtual/module.f.ts' import { vec, vec8, type Vec } from '../../types/bit_vec/module.f.mjs' import { cBase32ToVec, vecToCBase32 } from '../../basen/cbase32/module.f.mjs' diff --git a/fjs/cas/module.f.ts b/fjs/cas/module.f.ts index 2c5b0590e..72ba62ecb 100644 --- a/fjs/cas/module.f.ts +++ b/fjs/cas/module.f.ts @@ -3,7 +3,7 @@ * * @module */ -import { sha256, type Sha2, type State as Sha2State } from '../crypto/sha2/module.f.ts' +import { sha256, type Sha2, type State as Sha2State } from '../crypto/sha2/module.f.mjs' import { join, normalize, parse } from '../path/module.f.ts' import { empty, length, maxLength, maxLengthBytes, msb, vec, type Vec } from '../types/bit_vec/module.f.mjs' import { cBase32ToVec, vecToCBase32 } from '../basen/cbase32/module.f.mjs' diff --git a/fjs/cas/proof.f.ts b/fjs/cas/proof.f.ts index fc944f0e6..3945149e6 100644 --- a/fjs/cas/proof.f.ts +++ b/fjs/cas/proof.f.ts @@ -1,6 +1,6 @@ import { length, maxLength, msb, vec, vec8, type Vec } from '../types/bit_vec/module.f.mjs' import { cBase32ToVec, vecToCBase32 } from '../basen/cbase32/module.f.mjs' -import { computeSync, sha256 } from '../crypto/sha2/module.f.ts' +import { computeSync, sha256 } from '../crypto/sha2/module.f.mjs' import { fileCas, casAddFile, collectRead, type FileCasOperation, casUpload } from './module.f.ts' import { match, pure, runPure, step, type Effect } from '../effects/module.f.ts' import { mkdir, writeFile, rm, readFile, type ReadFile, type WriteFile, type Rm, type Mkdir, type IoResult, access } from '../effects/node/module.f.ts' diff --git a/fjs/crypto/hmac/module.f.ts b/fjs/crypto/hmac/module.f.ts index ed69c6315..b13554870 100644 --- a/fjs/crypto/hmac/module.f.ts +++ b/fjs/crypto/hmac/module.f.ts @@ -11,7 +11,7 @@ * ```ts * import { vec } from '../../types/bit_vec/module.f.ts' * import { msbUtf8 } from '../../text/module.f.ts' - * import { sha256 } from '../sha2/module.f.ts' + * import { sha256 } from '../sha2/module.f.mjs' * * const r = hmac(sha256)(msbUtf8('key'))(msbUtf8('The quick brown fox jumps over the lazy dog')) * if (r !== vec(256n)(0xf7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8n)) { throw r } @@ -19,7 +19,7 @@ */ import { length, type Vec, msb, vec, vec8, type Reduce, repeat } from '../../types/bit_vec/module.f.mjs' -import { computeSync, type Sha2 } from '../sha2/module.f.ts' +import { computeSync, type Sha2 } from '../sha2/module.f.mjs' const { concat, xor } = msb diff --git a/fjs/crypto/hmac/proof.f.ts b/fjs/crypto/hmac/proof.f.ts index fe053d237..df06d0add 100644 --- a/fjs/crypto/hmac/proof.f.ts +++ b/fjs/crypto/hmac/proof.f.ts @@ -1,7 +1,7 @@ import { assertEq } from '../../asserts/module.f.mjs' import { utf8 } from '../../text/module.f.ts' import { uint, vec } from '../../types/bit_vec/module.f.mjs' -import { sha256, sha384, sha512 } from '../sha2/module.f.ts' +import { sha256, sha384, sha512 } from '../sha2/module.f.mjs' import { hmac } from './module.f.ts' export const proof = { diff --git a/fjs/crypto/pow/module.f.ts b/fjs/crypto/pow/module.f.ts index 5eb48d9ad..e52a05a30 100644 --- a/fjs/crypto/pow/module.f.ts +++ b/fjs/crypto/pow/module.f.ts @@ -7,7 +7,7 @@ import { mask } from '../../types/bigint/module.f.mjs' import { type Vec, uint } from '../../types/bit_vec/module.f.mjs' import type { Nullable } from '../../types/nullable/module.f.mjs' -import { computeSync, sha256, type Sha2 } from '../sha2/module.f.ts' +import { computeSync, sha256, type Sha2 } from '../sha2/module.f.mjs' const nBitsMantissa = mask(24n) const mantissaSign = 0x00800000n diff --git a/fjs/crypto/pow/proof.f.ts b/fjs/crypto/pow/proof.f.ts index ac0b99b62..bcda36409 100644 --- a/fjs/crypto/pow/proof.f.ts +++ b/fjs/crypto/pow/proof.f.ts @@ -1,6 +1,6 @@ import { utf8 } from '../../text/module.f.ts' import { empty, uint } from '../../types/bit_vec/module.f.mjs' -import { computeSync, sha224, sha256 } from '../sha2/module.f.ts' +import { computeSync, sha224, sha256 } from '../sha2/module.f.mjs' import { bitcoinPow, genesisNBits, genesisTarget, pow, sha256Pow, targetFromNBits } from './module.f.ts' import { assert, assertEq, assertNotNullish } from '../../asserts/module.f.mjs' diff --git a/fjs/crypto/sha2/module.f.ts b/fjs/crypto/sha2/module.f.mjs similarity index 70% rename from fjs/crypto/sha2/module.f.ts rename to fjs/crypto/sha2/module.f.mjs index d8c0862a8..9736a5e2c 100644 --- a/fjs/crypto/sha2/module.f.ts +++ b/fjs/crypto/sha2/module.f.mjs @@ -3,8 +3,10 @@ * * @module */ -import type { Tuple } from '../../types/array/module.f.mjs' -import { mask, type Reduce } from "../../types/bigint/module.f.mjs" + +/** @import { Tuple } from '../../types/array/module.f.mjs' */ +import { mask } from '../../types/bigint/module.f.mjs' +/** @import { Reduce } from '../../types/bigint/module.f.mjs' */ import { vec, length, @@ -12,10 +14,11 @@ import { msb, chunkList, uint, - type Vec } from '../../types/bit_vec/module.f.mjs' -import type { Fold } from '../../types/function/operator/module.f.mjs' -import { fold, type List } from '../../types/list/module.f.mjs' +/** @import { Vec } from '../../types/bit_vec/module.f.mjs' */ +/** @import { Fold } from '../../types/function/operator/module.f.mjs' */ +import { fold } from '../../types/list/module.f.mjs' +/** @import { List } from '../../types/list/module.f.mjs' */ const { concat, front } = msb @@ -23,70 +26,77 @@ const { concat, front } = msb // across every `base(...)` config (32-bit and 64-bit SHA-2 variants). const chunkListMsb = chunkList(msb) -type V3 = Tuple<3, bigint> +/** @typedef {Tuple<3, bigint>} _V3 */ -type V4 = Tuple<4, bigint> +/** @typedef {Tuple<4, bigint>} _V4 */ -/** 8-word SHA-2 state vector. */ -export type V8 = Tuple<8, bigint> +/** + * 8-word SHA-2 state vector. + * + * @typedef {Tuple<8, bigint>} V8 + */ -/** 16-word SHA-2 message schedule chunk. */ -export type V16 = Tuple<16, bigint> +/** + * 16-word SHA-2 message schedule chunk. + * + * @typedef {Tuple<16, bigint>} V16 + */ /** - * Type definition for the state of the SHA-2 algorithm. + * State of the SHA-2 algorithm: `hash` is the current hash value, `len` the + * length of the data processed so far, and `remainder` the data that has not + * yet been processed. + * + * @typedef {{ + * readonly hash: V8, + * readonly len: bigint, + * readonly remainder: Vec, + * }} State */ -export type State = { - /** - * The current hash value. - */ - readonly hash: V8 - /** - * The length of the data processed so far. - */ - readonly len: bigint - /** - * The remaining data that has not yet been processed. - */ - readonly remainder: Vec -} -export type Base = { - readonly bitLength: bigint - readonly chunkLength: bigint - readonly compress: (i: V8) => (u: bigint) => V8 - readonly fromV8: (a: V8) => bigint - readonly append: Fold - readonly end: (hashLength: bigint) => (state: State) => Vec -} +/** + * @typedef {{ + * readonly bitLength: bigint, + * readonly chunkLength: bigint, + * readonly compress: (i: V8) => (u: bigint) => V8, + * readonly fromV8: (a: V8) => bigint, + * readonly append: Fold, + * readonly end: (hashLength: bigint) => (state: State) => Vec, + * }} Base + */ -type BaseInit = { - readonly logBitLen: bigint - readonly k: readonly V16[] - readonly bs0: V3 - readonly bs1: V3 - readonly ss0: V3 - readonly ss1: V3 -} +/** + * @typedef {{ + * readonly logBitLen: bigint, + * readonly k: readonly V16[], + * readonly bs0: _V3, + * readonly bs1: _V3, + * readonly ss0: _V3, + * readonly ss1: _V3, + * }} _BaseInit + */ -const lastOne: Vec = vec(1n)(1n) +/** @type {Vec} */ +const lastOne = vec(1n)(1n) -const base = ({ logBitLen, k, bs0, bs1, ss0, ss1 }: BaseInit): Base => { +/** @type {(init: _BaseInit) => Base} */ +const base = ({ logBitLen, k, bs0, bs1, ss0, ss1 }) => { const bitLength = 1n << logBitLen - const rotr = (d: bigint) => { + /** @type {Reduce} */ + const rotr = d => { const r = bitLength - d - return (n: bigint) => n >> d | n << r + return n => n >> d | n << r } - const sigma: (third: Reduce) => (..._: V3) => (x: bigint) => bigint = - third => (a, b, c) => { - const ra = rotr(a) - const rb = rotr(b) - const rc = third(c) - return x => ra(x) ^ rb(x) ^ rc(x) - } + /** @type {(third: Reduce) => (..._: _V3) => (x: bigint) => bigint} */ + const sigma = third => (a, b, c) => { + const ra = rotr(a) + const rb = rotr(b) + const rc = third(c) + return x => ra(x) ^ rb(x) ^ rc(x) + } const bigSigma = sigma(rotr) @@ -100,16 +110,20 @@ const base = ({ logBitLen, k, bs0, bs1, ss0, ss1 }: BaseInit): Base => { const smallSigma1 = smallSigma(...ss1) - const ch = (x: bigint, y: bigint, z: bigint) => x & y ^ ~x & z + /** @type {(x: bigint, y: bigint, z: bigint) => bigint} */ + const ch = (x, y, z) => x & y ^ ~x & z - const maj = (x: bigint, y: bigint, z: bigint) => x & y ^ x & z ^ y & z + /** @type {(x: bigint, y: bigint, z: bigint) => bigint} */ + const maj = (x, y, z) => x & y ^ x & z ^ y & z const m = mask(bitLength) - const wi: (..._: V4) => bigint = (a0, a1, a2, a3) => + /** @type {(..._: _V4) => bigint} */ + const wi = (a0, a1, a2, a3) => (smallSigma1(a0) + a1 + smallSigma0(a2) + a3) & m - const nextW: (...w: V16) => V16 + /** @type {(...w: V16) => V16} */ + const nextW = (w0, w1, w2, w3, w4, w5, w6, w7, w8, w9, wA, wB, wC, wD, wE, wF) => { w0 = wi(wE, w9, w1, w0) w1 = wi(wF, wA, w2, w1) @@ -132,8 +146,8 @@ const base = ({ logBitLen, k, bs0, bs1, ss0, ss1 }: BaseInit): Base => { const kLength = k.length - const compressV16: (..._: V8) => (_: V16) => V8 = - (a0, b0, c0, d0, e0, f0, g0, h0) => w => { + /** @type {(..._: V8) => (_: V16) => V8} */ + const compressV16 = (a0, b0, c0, d0, e0, f0, g0, h0) => w => { let a = a0 let b = b0 let c = c0 @@ -175,10 +189,12 @@ const base = ({ logBitLen, k, bs0, bs1, ss0, ss1 }: BaseInit): Base => { ] } - const at: Reduce = u => i => + /** @type {Reduce} */ + const at = u => i => (u >> (i << logBitLen)) & m - const compress: (i: V8) => (u: bigint) => V8 = i => u => { + /** @type {(i: V8) => (u: bigint) => V8} */ + const compress = i => u => { const a = at(u) return compressV16(...i)([ a(15n), @@ -211,7 +227,8 @@ const base = ({ logBitLen, k, bs0, bs1, ss0, ss1 }: BaseInit): Base => { // possibly the last one, which is why `remainder` only ever holds that // last chunk (`empty` otherwise) — same shape as `State` itself, so no // separate accumulator type is needed. - const appendChunk = (chunk: Vec) => (state: State): State => + /** @type {Fold} */ + const appendChunk = chunk => state => length(chunk) === chunkLength ? { hash: compress(state.hash)(uint(chunk)), len: state.len + chunkLength, remainder: empty } : { ...state, remainder: chunk } @@ -220,7 +237,8 @@ const base = ({ logBitLen, k, bs0, bs1, ss0, ss1 }: BaseInit): Base => { // starting accumulator passed to it per `append` call does. const foldChunks = fold(appendChunk) - const fromV8 = (a: V8) => a.reduce((p, v) => (p << bitLength) | v) + /** @type {(a: V8) => bigint} */ + const fromV8 = a => a.reduce((p, v) => (p << bitLength) | v) // See https://www.rfc-editor.org/rfc/rfc6234#section-4 const lastChunkLength = chunkLength - 1n - (bitLength << 1n) @@ -230,12 +248,12 @@ const base = ({ logBitLen, k, bs0, bs1, ss0, ss1 }: BaseInit): Base => { chunkLength, compress, fromV8, - append: (v: Vec) => (state: State): State => + append: v => state => foldChunks({ ...state, remainder: empty })(chunkListChunkLength(concat(state.remainder)(v))), - end: (hashLength: bigint) => { + end: hashLength => { const offset = (bitLength << 3n) - hashLength const result = vec(hashLength) - return (state: State): Vec => { + return state => { const { len, remainder } = state let { hash } = state const rLen = length(remainder) @@ -254,6 +272,10 @@ const base = ({ logBitLen, k, bs0, bs1, ss0, ss1 }: BaseInit): Base => { /** * SHA2. See https://en.wikipedia.org/wiki/SHA-2 * + * `hashLength` is a hash length, `blockLength` an internal block length, + * `init` the initial state of the SHA-2 algorithm, `append` adds data to a + * state and returns the new state, and `end` finalizes the hash of a state. + * * @example * * ```js @@ -262,38 +284,18 @@ const base = ({ logBitLen, k, bs0, bs1, ss0, ss1 }: BaseInit): Base => { * state = sha224.append(state)(s) * const h = sha224.end(state) // 0x1_619cba8e8e05826e9b8c519c0a5c68f4fb653e8a3d8aa04bb2c8cd4cn * ``` + * + * @typedef {{ + * readonly hashLength: bigint, + * readonly blockLength: bigint, + * readonly init: State, + * readonly append: Fold, + * readonly end: (state: State) => Vec, + * }} Sha2 */ -export type Sha2 = { - /** - * A hash length. - */ - readonly hashLength: bigint - /** - * An internal block length. - */ - readonly blockLength: bigint - /** - * Initial state of the SHA-2 algorithm. - */ - readonly init: State - /** - * Appends data to the state and returns the new state. - * - * @param v The data to append. - * @param state The current state. - * @returns The new state after appending data. - */ - readonly append: Fold - /** - * Finalizes the hash and returns the result as a bigint. - * - * @param state The final state. - * @returns The resulting hash. - */ - readonly end: (state: State) => Vec -} -const sha2 = ({ append, end, chunkLength }: Base, hash: V8, hashLength: bigint): Sha2 => ({ +/** @type {(base: Base, hash: V8, hashLength: bigint) => Sha2} */ +const sha2 = ({ append, end, chunkLength }, hash, hashLength) => ({ hashLength, blockLength: chunkLength, init: { @@ -308,16 +310,19 @@ const sha2 = ({ append, end, chunkLength }: Base, hash: V8, hashLength: bigint): /** * Computes a SHA-2 hash from a list of message chunks. * - * @param sha2 A SHA-2 algorithm configuration. - * @returns A function that hashes the full list of chunks. + * @type {(sha2: Sha2) => (list: List) => Vec} */ -export const computeSync = ({ append, init, end }: Sha2): (list: List) => Vec => { +export const computeSync = ({ append, init, end }) => { const f = fold(append)(init) - return (list: List): Vec => end(f(list)) + return list => end(f(list)) } -/** 32-bit SHA-2 base configuration shared by SHA-224 and SHA-256. */ -export const base32: Base = base({ +/** + * 32-bit SHA-2 base configuration shared by SHA-224 and SHA-256. + * + * @type {Base} + */ +export const base32 = base({ logBitLen: 5n, k: [ [ @@ -343,8 +348,12 @@ export const base32: Base = base({ ss1: [17n, 19n, 10n], }) -/** 64-bit SHA-2 base configuration shared by SHA-384, SHA-512, SHA-512/224 and SHA-512/256. */ -export const base64: Base = base({ +/** + * 64-bit SHA-2 base configuration shared by SHA-384, SHA-512, SHA-512/224 and SHA-512/256. + * + * @type {Base} + */ +export const base64 = base({ logBitLen: 6n, k: [ [ @@ -384,22 +393,34 @@ export const base64: Base = base({ ss1: [19n, 61n, 6n], }) -/** SHA-256 */ -export const sha256: Sha2 = sha2( +/** + * SHA-256 + * + * @type {Sha2} + */ +export const sha256 = sha2( base32, [0x6a09e667n, 0xbb67ae85n, 0x3c6ef372n, 0xa54ff53an, 0x510e527fn, 0x9b05688cn, 0x1f83d9abn, 0x5be0cd19n], 256n, ) -/** SHA-224 */ -export const sha224: Sha2 = sha2( +/** + * SHA-224 + * + * @type {Sha2} + */ +export const sha224 = sha2( base32, [0xc1059ed8n, 0x367cd507n, 0x3070dd17n, 0xf70e5939n, 0xffc00b31n, 0x68581511n, 0x64f98fa7n, 0xbefa4fa4n], 224n, ) -/** SHA-512 */ -export const sha512: Sha2 = sha2( +/** + * SHA-512 + * + * @type {Sha2} + */ +export const sha512 = sha2( base64, [ 0x6a09e667f3bcc908n, 0xbb67ae8584caa73bn, 0x3c6ef372fe94f82bn, 0xa54ff53a5f1d36f1n, @@ -408,8 +429,12 @@ export const sha512: Sha2 = sha2( 512n, ) -/** SHA-384 */ -export const sha384: Sha2 = sha2( +/** + * SHA-384 + * + * @type {Sha2} + */ +export const sha384 = sha2( base64, [ 0xcbbb9d5dc1059ed8n, 0x629a292a367cd507n, 0x9159015a3070dd17n, 0x152fecd8f70e5939n, @@ -418,8 +443,12 @@ export const sha384: Sha2 = sha2( 384n, ) -/** SHA-512/256 */ -export const sha512x256: Sha2 = sha2( +/** + * SHA-512/256 + * + * @type {Sha2} + */ +export const sha512x256 = sha2( base64, [ 0x22312194fc2bf72cn, 0x9f555fa3c84c64c2n, 0x2393b86b6f53b151n, 0x963877195940eabdn, @@ -428,8 +457,12 @@ export const sha512x256: Sha2 = sha2( 256n, ) -/** SHA-512/224 */ -export const sha512x224: Sha2 = sha2( +/** + * SHA-512/224 + * + * @type {Sha2} + */ +export const sha512x224 = sha2( base64, [ 0x8c3d37c819544da2n, 0x73e1996689dcd4d6n, 0x1dfab7ae32ff9c82n, 0x679dd514582f9fcfn, diff --git a/fjs/crypto/sha2/proof.f.ts b/fjs/crypto/sha2/proof.f.ts index 1acf942cb..a976192d3 100644 --- a/fjs/crypto/sha2/proof.f.ts +++ b/fjs/crypto/sha2/proof.f.ts @@ -14,7 +14,7 @@ import { sha512, sha512x224, sha512x256, -} from './module.f.ts' +} from './module.f.mjs' const checkEmpty = ({ init, end, hashLength }: Sha2) => (x: bigint) => { const result = end(init) diff --git a/fjs/crypto/sign/module.f.ts b/fjs/crypto/sign/module.f.ts index 78855d790..79c4e3ad6 100644 --- a/fjs/crypto/sign/module.f.ts +++ b/fjs/crypto/sign/module.f.ts @@ -9,7 +9,7 @@ import { bitLength, divUp8, roundUp8 } from '../../types/bigint/module.f.mjs' import { empty, length, msb, repeat, unpack, vec, vec8, type Vec } from '../../types/bit_vec/module.f.mjs' import { hmac } from '../hmac/module.f.ts' import type { Curve } from '../secp/module.f.ts' -import { computeSync, type Sha2 } from '../sha2/module.f.ts' +import { computeSync, type Sha2 } from '../sha2/module.f.mjs' export type All = { readonly q: bigint diff --git a/fjs/crypto/sign/proof.f.ts b/fjs/crypto/sign/proof.f.ts index 95ec7254c..4ee1f4c14 100644 --- a/fjs/crypto/sign/proof.f.ts +++ b/fjs/crypto/sign/proof.f.ts @@ -3,7 +3,7 @@ import type { Tuple } from "../../types/array/module.f.mjs" import { empty, msb, repeat, vec, vec8, type Vec } from "../../types/bit_vec/module.f.mjs" import { hmac } from "../hmac/module.f.ts" import { secp192r1, secp256r1, secp384r1, secp521r1, type Curve } from "../secp/module.f.ts" -import { computeSync, sha224, sha256, sha384, sha512, type Sha2 } from "../sha2/module.f.ts" +import { computeSync, sha224, sha256, sha384, sha512, type Sha2 } from "../sha2/module.f.mjs" import { all, concat, computeK, fromCurve, sign } from "./module.f.ts" import { assertEq } from '../../asserts/module.f.mjs' diff --git a/fjs/mcp/cas/module.f.ts b/fjs/mcp/cas/module.f.ts index cfa0d66aa..c0bf5df89 100644 --- a/fjs/mcp/cas/module.f.ts +++ b/fjs/mcp/cas/module.f.ts @@ -120,7 +120,7 @@ import { import { collectRead, fileCas, type FileCasOperation } from '../../cas/module.f.ts' import { fromVec } from '../../text/utf8/module.f.ts' import { identity } from '../../types/function/module.f.mjs' -import { sha256 } from '../../crypto/sha2/module.f.ts' +import { sha256 } from '../../crypto/sha2/module.f.mjs' import { nonEmpty, empty as elEmpty } from '../../effects/list/module.f.ts' import { syncRevision, type Cache } from '../../cas/evo/module.f.ts' import type { Key } from '../../effects/memory/module.f.ts' diff --git a/fjs/mcp/evo/proof.f.ts b/fjs/mcp/evo/proof.f.ts index f65ccaae8..114d43fdf 100644 --- a/fjs/mcp/evo/proof.f.ts +++ b/fjs/mcp/evo/proof.f.ts @@ -1,6 +1,6 @@ import { assert, assertEq } from '../../asserts/module.f.mjs' import { fileCas } from '../../cas/module.f.ts' -import { sha256 } from '../../crypto/sha2/module.f.ts' +import { sha256 } from '../../crypto/sha2/module.f.mjs' import { emptyState, virtual } from '../../effects/node/virtual/module.f.ts' import { vec8 } from '../../types/bit_vec/module.f.mjs' import { vecToCBase32 } from '../../basen/cbase32/module.f.mjs' diff --git a/fjs/mcp/module.f.ts b/fjs/mcp/module.f.ts index a8df36045..7da97c002 100644 --- a/fjs/mcp/module.f.ts +++ b/fjs/mcp/module.f.ts @@ -36,7 +36,7 @@ import { } from '../protocol/mcp/module.f.ts' import { fileCas, type FileCasOperation } from '../cas/module.f.ts' import { initEvo, evo, type Cache } from '../cas/evo/module.f.ts' -import { sha256 } from '../crypto/sha2/module.f.ts' +import { sha256 } from '../crypto/sha2/module.f.mjs' import { casToolRegistry } from './cas/module.f.ts' import { evoToolRegistry } from './evo/module.f.ts' import type { Key } from '../effects/memory/module.f.ts' diff --git a/fjs/mcp/proof.f.ts b/fjs/mcp/proof.f.ts index 7085e8fc3..e1dced66d 100644 --- a/fjs/mcp/proof.f.ts +++ b/fjs/mcp/proof.f.ts @@ -11,7 +11,7 @@ import { encode as base64Encode } from '../basen/base64/module.f.mjs' import { utf8 } from '../text/module.f.ts' import { fileCas, type FileCasOperation } from '../cas/module.f.ts' import { dialect as revisionDialect, mediaType as revisionMediaType } from '../media/revision/module.f.ts' -import { sha256 } from '../crypto/sha2/module.f.ts' +import { sha256 } from '../crypto/sha2/module.f.mjs' import { nonEmpty, empty as elEmpty, type List } from '../effects/list/module.f.ts' import { mcpStep, uninitializedState, type McpSessionState, type ToolsCallResult, diff --git a/fjs/sul/id/module.f.ts b/fjs/sul/id/module.f.ts index b6060cee1..6a99e3531 100644 --- a/fjs/sul/id/module.f.ts +++ b/fjs/sul/id/module.f.ts @@ -19,7 +19,7 @@ import { import { assertEq } from '../../asserts/module.f.mjs' import { utf8 } from '../../text/module.f.ts' import { secp256r1, type Point2D } from '../../crypto/secp/module.f.ts' -import { base32, type V8 } from '../../crypto/sha2/module.f.ts' +import { base32, type V8 } from '../../crypto/sha2/module.f.mjs' import { literal3ToVec } from '../level/literal/module.f.ts' import { log2 } from '../../types/bigint/module.f.mjs' import { asBase, asNominal, type Nominal } from '../../types/nominal/module.f.mjs' diff --git a/fjs/sul/todo/186.md b/fjs/sul/todo/186.md index 15de00beb..3bbea1ddc 100644 --- a/fjs/sul/todo/186.md +++ b/fjs/sul/todo/186.md @@ -23,9 +23,10 @@ const hashMerge = (a: Id, b: Id): Id => MSB-concatenates them, and reads the result as a `bigint`. But `sha2` already exports that exact packing on the `Base` it returns: -```ts -// fjs/crypto/sha2/module.f.ts:200 -const fromV8 = (a: V8) => a.reduce((p, v) => (p << bitLength) | v) +```js +// fjs/crypto/sha2/module.f.mjs:239 +/** @type {(a: V8) => bigint} */ +const fromV8 = a => a.reduce((p, v) => (p << bitLength) | v) ``` For `base32`, `bitLength === 32n`, so `fromV8` computes diff --git a/todo/migrate-typescript-to-mjs.md b/todo/migrate-typescript-to-mjs.md index e958a6954..3dee226b8 100644 --- a/todo/migrate-typescript-to-mjs.md +++ b/todo/migrate-typescript-to-mjs.md @@ -190,6 +190,79 @@ This convention is temporary. Once TypeScript can strip `@internal` JSDoc typedefs correctly, replace the underscore workaround as tracked by [`blocked/jsdoc-typedef-strip-internal.md`](./blocked/jsdoc-typedef-strip-internal.md). +#### Typedef documentation does not survive declaration emit + +The same upstream gap has a second, opposite-facing symptom: declaration emit +drops the documentation written on a JSDoc `@typedef`. A TypeScript +`/** 8-word SHA-2 state vector. */ export type V8 = …` keeps its comment in the +emitted `.d.ts`; the equivalent `@typedef` in a `.mjs` emits as a bare +`export type V8 = …`, and the prose — including any `@example` — is gone from the +published declaration. Documentation on `export const` declarations is +unaffected, so a migrated module loses exactly its *type* documentation. + +`fjs/crypto/sha2` is the clearest case so far: `V8`, `V16`, `State` and `Sha2` +were documented types, and `Sha2` carried the module's `@example` walkthrough. +All of it survives in the source and none of it reaches `module.f.d.mts`. The +loss is therefore invisible to anyone reading the repository and visible only to +a consumer of the published package. + +Related upstream behavior: TypeScript sometimes re-emits a bare `@typedef` +comment attached to the *following* declaration instead +([microsoft/TypeScript#43534](https://github.com/microsoft/TypeScript/issues/43534), +fixed for the services layer), and +[microsoft/TypeScript#61664](https://github.com/microsoft/TypeScript/issues/61664) +proposes stripping redundant JSDoc type directives from declaration emit while +keeping documentation. Neither tracks this loss directly; no upstream issue for +it has been identified yet. + +This does not block any migration group — it is a documentation-fidelity +regression, not a type-contract one. Record it, keep writing the documentation in +the source, and file an upstream issue so the gap is tracked rather than +rediscovered by each migration. + +#### Separate the `@module` header from the first import with a blank line + +A module's `@module` header can disappear from the emitted declaration too, but +that one is **not** an upstream gap — it is a source-formatting requirement, and +a blank line fixes it: + +```js +/** + * ... + * @module + */ + // <- this blank line is load-bearing +/** @import { Tuple } from '...' */ +import { mask } from '...' +``` + +Without the blank line, the header is the leading comment of the first `import` +*statement* (an `@import` tag is a comment, not a statement, so it does not +separate them). Declaration emit rewrites the import list — dropping +runtime-only imports and synthesizing `import type` for what the declarations +actually reference — and when the statement carrying the header is not among the +survivors, the header goes with it. With the blank line the header detaches from +that statement and is emitted as the file's own leading comment. + +Checked against every `.mjs` in the repository carrying an `@module` header — 26 +modules, no exceptions: + +| header separated from first `import` statement | header kept | count | +| ---------------------------------------------- | ----------- | ----- | +| yes | yes | 13 | +| no `import` statement at all | yes | 8 | +| no | **no** | 5 | + +A module with no `import` statement keeps its header unconditionally: there is no +statement for the comment to attach to, so it is already the file's own leading +comment. That is why the loss looks intermittent rather than systematic — most +migrated modules are in one of the two safe categories by accident, not by +intent. + +`fjs/common/monoid`, `fjs/types/btree/remove`, `fjs/types/btree/set`, +`fjs/types/list` and `fjs/types/nullable` are the five that currently lose their +header and want the same one-line fix. + #### Known TypeScript-to-JSDoc hard cases Do not require the migration plan to pre-design every TypeScript-only type @@ -280,6 +353,15 @@ compiler-compatibility rename. unprefixed, judged by what the module should offer its consumers rather than by what the `.f.ts` happened to export or by what a pending refactor plans to delete. +- [ ] Keep a blank line between a module's `@module` header and its first + `import` statement so the header survives declaration emit; fix the + modules that already lost theirs (`fjs/common/monoid`, + `fjs/types/btree/remove`, `fjs/types/btree/set`, `fjs/types/list`, + `fjs/types/nullable`). +- [ ] File an upstream issue for typedef documentation being dropped from + declaration emit, and keep writing type documentation in the source + meanwhile — the loss is a published-package regression only, and blocks no + migration group. - [ ] Treat `_`-prefixed typedef names as private even when declarations emit them as exports, but still require `**BREAKING CHANGES:**` whenever a change to one alters the assignability of a public declaration. @@ -334,6 +416,12 @@ compiler-compatibility rename. their JSDoc `@template` equivalents; public assignability is not weakened. - Implementation-only JSDoc typedefs use `_`-prefixed names and are treated as private API even when TypeScript emits them as exported declaration aliases. +- Documentation lost from emitted declarations because it was attached to a + JSDoc `@typedef` is recorded as a known upstream gap, not treated as a reason + to keep a module in TypeScript or to stop documenting its types. +- Every migrated module's `@module` header survives into its emitted + declaration, which requires a blank line between that header and the first + `import` statement. - Renaming or removing an emitted `_`-prefixed alias is not breaking solely due to that alias being emitted; any resulting change to a public declaration's assignability is still a breaking change.