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
- Sanitize numbers before building the payload: coerce NaN/Infinity to null or 0, and normalize -0 to 0.
- Compute metrics defensively: guard divide-by-zero and empty-averages.
- Validate the payload with assertJsonValue yourself before signing so the error surfaces at the source.
- 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
- Guard divide-by-zero and empty-set averages that yield NaN/Infinity.
- Normalize -0 to 0 (Object.is(x, -0)) before building payloads.
- Run hasNonCanonicalNumbers over the payload before signing to fail fast at the source.
- Brand metric values as finite-numbers via a type to prevent undefined leaking into arithmetic.
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
- Import file must contain a JSON object
- Invalid completion type
- browser/eval: script must not be empty
- Invalid target format: ${target}. Use agent:<id> or human:<i
- ${label} requires a JSON argument
AI-assisted analysis of ruvnet/ruflo@6b01dc5a68 (2026-08-12).
Data as JSON: /api/errors/e20dd931f804a74d.
Report an issue: GitHub.