ruvnet/ruflo · error · RangeError

testIndex must be a positive integer

Error message

testIndex must be a positive integer

What it means

Thrown by alphaForTest() as a RangeError when testIndex is not a positive integer. testIndex is the 1-based position of this candidate in the lineage's sequential test stream; it determines the alpha share (alphaTotal * 6/(pi^2 * k^2)). Zero, negative, or fractional indices are meaningless for the family-wise error budget. Called transitively by sequentialEvidenceVerdict, minInformativePairsToClear, and remainingAlphaBudget.

Source

Thrown at v3/@claude-flow/cli/src/services/flywheel-sequential-evidence.ts:79

export interface SequentialEvidenceVerdict {
  significant: boolean;
  eValue: number;
  threshold: number;        // 1 / alphaAllocated
  alphaAllocated: number;   // this test's share of the family-wise budget
  testIndex: number;        // 1-based position in the lineage's test stream
  informativePairs: number; // discordant pairs — the only ones carrying signal
  totalPairs: number;
  version: typeof SEQUENTIAL_EVIDENCE_VERSION;
}

/**
 * Alpha share for the k-th test in the stream: alphaTotal * 6/(pi^2 k^2).
 * Chosen over 2^-k because it decays polynomially — test 10 still gets a
 * workable ~0.6% of a 5% budget instead of ~0.005%.
 */
export function alphaForTest(testIndex: number, alphaTotal = DEFAULT_ALPHA_TOTAL): number {
  if (!Number.isInteger(testIndex) || testIndex < 1) throw new RangeError('testIndex must be a positive integer');
  if (!(alphaTotal > 0 && alphaTotal < 1)) throw new RangeError('alphaTotal must be in (0, 1)');
  return (alphaTotal * 6) / (Math.PI * Math.PI * testIndex * testIndex);
}

/**
 * Fold paired outcomes into an anytime-valid e-value and judge it against the
 * k-th test's allocated alpha. Deterministic; order of outcomes does not
 * change the final e-value (the product commutes).
 */
export function sequentialEvidenceVerdict(
  outcomes: PairedTaskOutcome[],
  testIndex: number,
  config: SequentialEvidenceConfig = {},
): SequentialEvidenceVerdict {
  const alphaTotal = config.alphaTotal ?? DEFAULT_ALPHA_TOTAL;
  const lambda = config.lambda ?? DEFAULT_LAMBDA;
  const epsilon = config.epsilon ?? DEFAULT_SCORE_EPSILON;
  if (!(lambda > 0 && lambda < 1)) throw new RangeError('lambda must be in (0, 1)');

View on GitHub (pinned to 6b01dc5a68)

Solutions

  1. Use a 1-based index: testIndex = Object.keys(state.sequentialTests).length + 1 (the transaction layer allocates this).
  2. Validate before calling: if (!Number.isInteger(k) || k < 1) throw.
  3. Let the flywheel-transaction promotion path allocate the index rather than computing it manually.

Example fix

// before
const k = state.sequentialTests?.[receiptId] ?? 0; // 0-based, will throw
sequentialEvidenceVerdict(outcomes, k);
// after
const k = nextTestIndex(state); // returns 1-based positive integer
sequentialEvidenceVerdict(outcomes, k);
Defensive patterns

Strategy: validation

Validate before calling

function assertPositiveInt(x: number, name: string): void {
  if (!Number.isInteger(x) || x < 1) throw new RangeError(`${name} must be a positive integer, got ${x}`);
}
assertPositiveInt(testIndex, 'testIndex');
alphaForTest(testIndex);

Type guard

const isPositiveInt = (x: unknown): x is number => typeof x === 'number' && Number.isInteger(x) && x >= 1;

Prevention

When it happens

Trigger: Calling alphaForTest(0), alphaForTest(-1), alphaForTest(1.5), or sequentialEvidenceVerdict(outcomes, 0). Also if a caller computes testIndex from an empty/zero-based ledger without converting to 1-based.

Common situations: A transaction state with zero prior tests where the caller passes the raw length (0) instead of length+1; a 0-based array index used directly; an uninitialised sequentialTests ledger.

Related errors


AI-assisted analysis of ruvnet/ruflo@6b01dc5a68 (2026-08-12). Data as JSON: /api/errors/c11c8fa2a31580a3. Report an issue: GitHub.