Skip to content

ISACloseGate passes unitIsClaimable to .filter(), so its opts parameter receives the array index #1766

Description

@xmasyx

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions