Summary
hooks/ISACloseGate.hook.ts:59 passes unitIsClaimable directly to Array.prototype.filter. filter invokes its callback with (value, index, array), so the function's second parameter — declared as an options object — receives the array index at runtime.
Verified in v7.28.3:
// hooks/ISACloseGate.hook.ts:59
for (const u of splitIntoUnits(stripNoise(message)).filter(unitIsClaimable)) {
// hooks/VerificationGate.hook.ts:110
export function unitIsClaimable(u: string, opts?: { allowNarration?: boolean }): boolean {
Impact
It does not currently throw: opts is a number, and opts?.allowNarration on a number is undefined, which happens to coincide with the intended default. So the behaviour is accidentally correct today — but the signature is being violated on every call, and the moment unitIsClaimable starts reading opts in any way that a number can satisfy (typeof opts === "object", Object.keys(opts), a truthiness check on opts itself), the gate changes behaviour silently.
It is also a hard type error, so the file cannot pass a strict typecheck:
error TS2769: No overload matches this call.
Argument of type '(u: string, opts?: { allowNarration?: boolean | undefined; } | undefined) => boolean'
is not assignable to parameter of type '(value: string, index: number, array: string[]) => unknown'.
Types of parameters 'opts' and 'index' are incompatible.
Type 'number' has no properties in common with type '{ allowNarration?: boolean | undefined; }'.
Reproduction
bunx tsc --noEmit --strict # over hooks/ISACloseGate.hook.ts
Proposed fix
Wrap the callback so only the value is forwarded, which also makes the intended opts default explicit at the call site:
- for (const u of splitIntoUnits(stripNoise(message)).filter(unitIsClaimable)) {
+ for (const u of splitIntoUnits(stripNoise(message)).filter((u) => unitIsClaimable(u))) {
Worth a grep for other bare .filter(fn) / .map(fn) call sites where fn takes an optional second parameter — this failure mode is invisible until the second parameter is actually read.
Summary
hooks/ISACloseGate.hook.ts:59passesunitIsClaimabledirectly toArray.prototype.filter.filterinvokes its callback with(value, index, array), so the function's second parameter — declared as an options object — receives the array index at runtime.Verified in
v7.28.3:Impact
It does not currently throw:
optsis a number, andopts?.allowNarrationon a number isundefined, which happens to coincide with the intended default. So the behaviour is accidentally correct today — but the signature is being violated on every call, and the momentunitIsClaimablestarts readingoptsin any way that a number can satisfy (typeof opts === "object",Object.keys(opts), a truthiness check onoptsitself), the gate changes behaviour silently.It is also a hard type error, so the file cannot pass a strict typecheck:
Reproduction
bunx tsc --noEmit --strict # over hooks/ISACloseGate.hook.tsProposed fix
Wrap the callback so only the value is forwarded, which also makes the intended
optsdefault explicit at the call site:Worth a grep for other bare
.filter(fn)/.map(fn)call sites wherefntakes an optional second parameter — this failure mode is invisible until the second parameter is actually read.