ruvnet/ruflo · error · Error

non-canonical number at ${path}

Error message

non-canonical number at ${path}

What it means

assertJsonValue enforces canonical JSON (RFC 8785) for receipt payloads: numbers must be finite and must not be negative zero (-0). NaN and Infinity are not representable in JSON, and -0 serializes distinctly from +0 across implementations, so both are rejected to keep signed receipts byte-deterministic.

Source

Thrown at v3/@claude-flow/cli/src/services/flywheel-receipt.ts:151

  /** Task-level paired outcomes behind heldOutDeltas (same order). */
  pairedOutcomes?: PairedTaskOutcome[];
  frozenAnchorRegression: number;
  gates: Record<string, boolean>;
  resourceEvidence?: Partial<ResourceEvidence>;
  evidence?: EvaluationEvidence;
  termVerification?: TermVerification[];
  now?: number;
  ttlMs?: number;
  privateKeyPem?: string;
  publicKeyPem?: string;
  bootstrapIterations?: number;
}

function assertJsonValue(value: unknown, path = '$'): void {
  if (value === null || typeof value === 'string' || typeof value === 'boolean') return;
  if (typeof value === 'number') {
    if (!Number.isFinite(value) || Object.is(value, -0)) {
      throw new Error(`non-canonical number at ${path}`);
    }
    return;
  }
  if (Array.isArray(value)) {
    value.forEach((v, i) => {
      if (v === undefined) throw new Error(`undefined array member at ${path}[${i}]`);
      assertJsonValue(v, `${path}[${i}]`);
    });
    return;
  }
  if (typeof value === 'object') {
    for (const [key, child] of Object.entries(value as Record<string, unknown>)) {
      if (child === undefined) throw new Error(`undefined property at ${path}.${key}`);
      assertJsonValue(child, `${path}.${key}`);
    }
    return;
  }
  throw new Error(`unsupported JSON value at ${path}`);

View on GitHub (pinned to 6b01dc5a68)

Solutions

  1. Sanitize numbers before building the payload: coerce NaN/Infinity to null or 0, and normalize -0 to 0.
  2. Compute metrics defensively: guard divide-by-zero and empty-averages.
  3. Validate the payload with assertJsonValue yourself before signing so the error surfaces at the source.
  4. Type the payload builder inputs as numbers-known-finite via a branded type.

Example fix

// before
const payload = { mrr: ratio, cost: maybeInfinity };
signReceipt(payload); // throws 'non-canonical number' on NaN/Infinity/-0

// after — canonicalize numbers first
function canon(n: number): number | null {
  if (!Number.isFinite(n)) return null;
  return Object.is(n, -0) ? 0 : n;
}
const payload = { mrr: canon(ratio), cost: canon(maybeInfinity) };
signReceipt(payload);
Defensive patterns

Strategy: validation

Validate before calling

function canonNumber(n: number): number | null {
  if (!Number.isFinite(n)) return null;
  return Object.is(n, -0) ? 0 : n;
}
const payload = { mrr: canonNumber(ratio), cost: canonNumber(maybeInfinity) };
signReceipt(payload);

Type guard

function isCanonicalNumber(n: unknown): n is number {
  return typeof n === 'number' && Number.isFinite(n) && !Object.is(n, -0);
}
function hasNonCanonicalNumbers(v: unknown, path = '$'): boolean {
  if (typeof v === 'number') return !isCanonicalNumber(v);
  if (Array.isArray(v)) return v.some((x, i) => hasNonCanonicalNumbers(x, `${path}[${i}]`));
  if (v && typeof v === 'object') return Object.values(v).some((x) => hasNonCanonicalNumbers(x));
  return false;
}

Try / catch

try {
  signReceipt(payload);
} catch (e) {
  if ((e as Error).message.startsWith('non-canonical number')) {
    // sanitize all numbers and retry
    payload = sanitizeNumbers(payload);
    signReceipt(payload);
  } else throw e;
}

Prevention

When it happens

Trigger: Calling the receipt builder/signer with a payload (or nested field) that contains a non-finite number (NaN, Infinity, -Infinity) or a negative-zero value (Object.is(x, -0)), typically from a metric computation that divided by zero, returned NaN, or was produced via multiplication by -1 of zero.

Common situations: Latency/cost metrics computed as x/0 yielding Infinity; an undefined propagated into arithmetic producing NaN; normalization code that does value * -1 on a zero producing -0; stats aggregated over an empty set returning NaN.

Related errors


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