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
- Encode large numbers as strings (the harness convention for IDs and digests)
- Normalize -0 to 0 before recording (Object.is(x, -0) ? 0 : x, or x + 0)
- 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
- Represent 64-bit IDs, nanosecond timestamps, and large counters as strings
- Normalize -0 with (Object.is(x, -0) ? 0 : x) after rounding-heavy arithmetic
- Validate numeric leaves recursively before building digests
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
- canonical JSON does not support lone UTF-16 surrogates
- canonical JSON does not support undefined
- canonical JSON does not support ${typeof item}
- canonical JSON does not support cycles
- canonical JSON supports only arrays and plain objects
AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18).
Data as JSON: /api/errors/c476aebc6d2426b7.
Report an issue: GitHub.