types/uint8array: derive toVec from listToVec instead of a size bound - #1543
Conversation
toVec precomputed a byte-count bound to predict whether the input would fit, which AGENTS.md 5.6 rules out: attempt the real conversion and branch on its result. The guard was also redundant, since u8ListToVec is the unwrapping form of tryU8ListToVec and the real check already ran inside it. toVec is now listToVec([input]) -- the same byte list, the same "the array is too big" message on overflow -- and assert, maxLengthBytes and u8ListToVec drop out of the imports. listToVec moves above it as the primitive. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016HvbYkBMYWwQECL7myLhqs
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016HvbYkBMYWwQECL7myLhqs
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
functionalscript | 3758356 | Commit Preview URL Branch Preview URL |
Aug 14 2026, 07:28 AM |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6bbb8647bb
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| * | ||
| * @type {(input: Uint8Array) => Vec} | ||
| */ | ||
| export const toVec = input => listToVec([input]) |
There was a problem hiding this comment.
Add the required changelog entry
When this exported-code change is released, the Unreleased section will contain no record of it because the diff changes module.f.mjs but never updates CHANGELOG.md. Repository workflow requires every code-changing PR, unlike documentation-only work, to add a changelog entry using the real PR number, so add that entry before merging.
AGENTS.md reference: AGENTS.md:L132-L135
Useful? React with 👍 / 👎.
o2alexanderfedin
left a comment
There was a problem hiding this comment.
Approving. The risk in a DRY derivation like this is that the two forms are not actually equivalent, so I swept them against each other rather than reasoning about it.
Baseline: origin/main = 699c960e ("types/object: add structurallySame ... (#1538)").
Equivalence sweep. Imported toVec from an origin/main tree and from this head, and compared outcomes — not just return values, but (kind, typeof thrown, String(thrown)) so a differing throw would show up:
- empty array, and every single-byte value 0..255;
- every 2-byte pair over a 32-value subset and every 3-byte triple over a 16-value subset;
- every length 0..3000 x three fill patterns, including all-zero (which is the pattern most likely to expose a
Vecnormalization difference) and all-0xFF; - a dense band straight across the bound the old code enforced — every length in [131000..131200] x 2 fills — plus 131071 / 131072 / 131073 / 131074 / 140000 / 200000 / 300000;
- a stride sample across the middle of the range.
14,928 probes, 0 divergences. 14,662 of them returned a Vec and 266 threw, so the throwing path is genuinely exercised and the sweep is not vacuously one-sided. Negative control: injecting a one-element truncation into the new side makes the same comparison report 4,099 divergences, so the harness can see a divergence when there is one.
The boundary specifically. The removed bound was input.length <= maxLengthBytes, and maxLengthBytes = maxLength >> 3n = 131072. Both old and new accept exactly 131072 bytes and both throw from 131073 up — the 266 throws are precisely the lengths above 131072 in the swept set, in both implementations. So tryU8ListToVec's real check lands on the same cut point the precomputed bound did, which is the substance of the §5.6 claim.
Worth noting since it is the kind of thing that hides a difference: the old guard compared a number (input.length) against a bigint (maxLengthBytes). That is legal in JS and, per the band above, agrees with the new path at the exact boundary.
Error identity. assertNotNullish delegates to assert, and assert throws the raw msg rather than an Error. Both sides therefore throw the string "the array is too big" — same type, same value, which is what the CHANGELOG's "error message unchanged" needs to mean here. My comparison checked typeof as well as the string, so a swap from string to Error would have been caught.
No divergence appeared, so there is nothing to classify as regression-vs-latent-bug (unlike #1533, where the single boundary divergence turned out to be a latent bug being fixed).
Battery
npx tsc --noEmit— exit 0.npm run prepackfrom a freshly cleaned tree — exit 0.npm test— 2617 pass / 0 fail, identical toorigin/main.- Dual-axis public surface diff (
prepackin both trees): exported type aliases identical; exported const signatures identical apart fromtoVecmoving afterlistToVecin declaration order, which is the source reordering and not a signature change.toVecstill emits(input: Uint8Array) => Vec. - Emitted
fjs/types/uint8array/module.f.d.mts:@moduleheader survives (the blank line after it is intact), and noelidedor type-levelany— the newlistToVecJSDoc emits with its fullList<Uint8Array>signature. node bin/linkcheck.mjs— broken-link sets identical to main. The deletedtodo/tovec-precomputed-bound.mdhas no remaining references anywhere in the tree (grep across the repo, not just the link checker).
Conventions. §8.3: code change, entry present, links only /pull/1543 — correct. §8.4: no **BREAKING CHANGES:** prefix, right given the sweep shows identical outcomes including the throw. §6.2: no new exported types, so the _-prefix rule is not engaged. The toVec/listToVec JSDoc reshuffle keeps both public and documented.
o2alexanderfedin
left a comment
There was a problem hiding this comment.
Re-reviewed at 151b3a47 after the previous approval at df48c0b5. The delta is merge-only.
What moved, precisely:
git diff df48c0b5 151b3a47 -- fjs/types/uint8array/is empty — the implementation and its proof are byte-identical to what was reviewed.- The only CHANGELOG delta is the arrival of #1541's
basen/base64entry frommain; this PR's owntypes/uint8arrayentry is unchanged. git merge-base origin/main 151b3a47ise692e595, i.e. the branch is exactly currentmainplus this change, with no conflict resolution touching the PR's own files.
Since neither the implementation nor the proof moved, I did not re-run the 14,928-probe old-vs-new equivalence sweep or the 131,072/131,073 cut-point reproduction from the previous round; those results still describe this code verbatim. I did re-run the gates that the merge could plausibly disturb, at 151b3a47 from a freshly cleaned tree:
npx tsc --noEmit— exit 0.npm test— pass: 2617, fail: 0. Baselinefs-mainate692e595(currentorigin/main): pass: 2617, fail: 0. Exact match.npm run prepack— exit 0 from a clean tree (both passes).
No new findings. Approving.
Implements
fjs/types/uint8array/todo/tovec-precomputed-bound.md(deleted here).Why
Two adjacent functions did the same job with opposite discipline:
AGENTS.md §5.6 — "never precompute a size to predict whether something fits" — names
tryU8ListToVecas thetry*variant to use instead.listToVecobeyed it;toVecre-derived a byte-count bound. The guard was also redundant:u8ListToVecismapUnwrap(tryU8ListToVec(bo)), so the real check already ran inside it.What
export const toVec = input => listToVec([input]).flat(map(fromArrayLike)([input]))is the same byte listfromArrayLike(input)produced, andassertNotNullishcarries the same"the array is too big"message, so behavior is unchanged.assert,maxLengthBytesandu8ListToVecdrop out of the imports.listToVecmoves abovetoVecso the primitive precedes what derives from it, and picks up a JSDoc note on why the bound isn't precomputed.Verification
npx tscclean.fjs test: 2617 pass, 0 fail. The proof already pinned both boundaries —maxLength: () => toVec(new Uint8Array(Number(maxLengthBytes)))andthrow.toVecone byte over — and both still pass unchanged.maxLengthBytesthe result is 1048576 bits, and one byte over throws the string"the array is too big"as before.npm run cov:fjs/types/uint8array/module.f.mjsat 100% lines/branches/functions.🤖 Generated with Claude Code
https://claude.ai/code/session_016HvbYkBMYWwQECL7myLhqs
Generated by Claude Code