ruvnet/ruflo · error · Error

canonical JSON requires finite, safe, non-negative-zero numb

Error message

canonical JSON requires finite, safe, non-negative-zero numbers

What it means

canonicalJson encodes numbers only when they are finite, not negative zero, and not unsafe integers (must satisfy |n| <= 2^53-1 when integral). Plain JSON.stringify renders these values ambiguously (NaN to null, 1e21 to '1e+21', -0 to '0'), which would break byte-stable digests, so they are refused. Finite non-integer decimals such as 0.1 are accepted.

Source

Thrown at v3/@claude-flow/codex/src/harness/repository-state.ts:123

  return left < right ? -1 : left > right ? 1 : 0;
}

/** Recursive, locale-independent canonical JSON for JSON-safe contract values. */
export function canonicalJson(value: unknown): string {
  const ancestors = new Set<object>();
  const encode = (item: unknown): string => {
    if (item === null || typeof item === 'boolean') return JSON.stringify(item);
    if (typeof item === 'string') {
      assertUnicodeScalarString(item);
      return JSON.stringify(item);
    }
    if (typeof item === 'number') {
      if (
        !Number.isFinite(item)
        || Object.is(item, -0)
        || (Number.isInteger(item) && !Number.isSafeInteger(item))
      ) {
        throw new Error('canonical JSON requires finite, safe, non-negative-zero numbers');
      }
      return JSON.stringify(item);
    }
    if (item === undefined) throw new Error('canonical JSON does not support undefined');
    if (typeof item !== 'object') {
      throw new Error(`canonical JSON does not support ${typeof item}`);
    }
    if (ancestors.has(item)) throw new Error('canonical JSON does not support cycles');
    ancestors.add(item);
    try {
      if (Array.isArray(item)) return `[${item.map(encode).join(',')}]`;
      const prototype = Object.getPrototypeOf(item);
      if (prototype !== Object.prototype && prototype !== null) {
        throw new Error('canonical JSON supports only arrays and plain objects');
      }
      const entries = Object.entries(item as Record<string, unknown>)
        .sort(([left], [right]) => codeUnitCompare(left, right));
      return `{${entries.map(([key, child]) => {

View on GitHub (pinned to fa13ee4ad6)

Solutions

  1. Encode large numbers as strings (the harness convention for IDs and digests)
  2. Normalize -0 to 0 before recording (Object.is(x, -0) ? 0 : x, or x + 0)
  3. Run a recursive safe-number scan over the payload before canonicalization and reject/convert offenders

Example fix

// before
{ sequence: 9007199254740993n-sourced number, offset: Math.round(-0.4) }

// after
{ sequence: '9007199254740993', offset: Math.round(-0.4) + 0 }
Defensive patterns

Strategy: validation

Validate before calling

function assertCanonicalNumbers(value: unknown): void {
  if (typeof value === 'number') {
    if (!Number.isFinite(value) || Object.is(value, -0) || (Number.isInteger(value) && !Number.isSafeInteger(value))) {
      throw new TypeError(`non-canonical number: ${value}`);
    }
    return;
  }
  if (Array.isArray(value)) { value.forEach(assertCanonicalNumbers); return; }
  if (typeof value === 'object' && value !== null) Object.values(value).forEach(assertCanonicalNumbers);
}

Type guard

function isCanonicalNumber(value: unknown): value is number {
  return typeof value === 'number'
    && Number.isFinite(value)
    && !Object.is(value, -0)
    && (!Number.isInteger(value) || Number.isSafeInteger(value));
}

Try / catch

try {
  return canonicalJson(payload);
} catch (error) {
  if (error instanceof Error && error.message === 'canonical JSON requires finite, safe, non-negative-zero numbers') {
    return canonicalJson(normalizeNumbers(payload)); // stringify big ints/numbers, map -0 to 0, NaN to null
  }
  throw error;
}

Prevention

When it happens

Trigger: Evidence payloads containing epoch-nanosecond timestamps (~1e18), snowflake or 64-bit IDs held as numbers, counters past Number.MAX_SAFE_INTEGER, Math.round(-0.4) producing -0, or arithmetic yielding NaN/Infinity (division by zero, failed parses).

Common situations: Nanosecond-resolution clocks; IDs received as JSON numbers from other services; float math leaking NaN into telemetry fields; sign-bit zero from bitwise or rounding operations.

Related errors


AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18). Data as JSON: /api/errors/c476aebc6d2426b7. Report an issue: GitHub.