{"record":{"id":"c476aebc6d2426b7","repo":"ruvnet/ruflo","slug":"canonical-json-requires-finite-safe-non-negative","errorCode":null,"errorMessage":"canonical JSON requires finite, safe, non-negative-zero numbers","messagePattern":"canonical JSON requires finite, safe, non-negative-zero numbers","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"v3/@claude-flow/codex/src/harness/repository-state.ts","lineNumber":123,"sourceCode":"  return left < right ? -1 : left > right ? 1 : 0;\n}\n\n/** Recursive, locale-independent canonical JSON for JSON-safe contract values. */\nexport function canonicalJson(value: unknown): string {\n  const ancestors = new Set<object>();\n  const encode = (item: unknown): string => {\n    if (item === null || typeof item === 'boolean') return JSON.stringify(item);\n    if (typeof item === 'string') {\n      assertUnicodeScalarString(item);\n      return JSON.stringify(item);\n    }\n    if (typeof item === 'number') {\n      if (\n        !Number.isFinite(item)\n        || Object.is(item, -0)\n        || (Number.isInteger(item) && !Number.isSafeInteger(item))\n      ) {\n        throw new Error('canonical JSON requires finite, safe, non-negative-zero numbers');\n      }\n      return JSON.stringify(item);\n    }\n    if (item === undefined) throw new Error('canonical JSON does not support undefined');\n    if (typeof item !== 'object') {\n      throw new Error(`canonical JSON does not support ${typeof item}`);\n    }\n    if (ancestors.has(item)) throw new Error('canonical JSON does not support cycles');\n    ancestors.add(item);\n    try {\n      if (Array.isArray(item)) return `[${item.map(encode).join(',')}]`;\n      const prototype = Object.getPrototypeOf(item);\n      if (prototype !== Object.prototype && prototype !== null) {\n        throw new Error('canonical JSON supports only arrays and plain objects');\n      }\n      const entries = Object.entries(item as Record<string, unknown>)\n        .sort(([left], [right]) => codeUnitCompare(left, right));\n      return `{${entries.map(([key, child]) => {","sourceCodeStart":105,"sourceCodeEnd":141,"githubUrl":"https://github.com/ruvnet/ruflo/blob/fa13ee4ad60ac2090b1480656eb233521790d640/v3/@claude-flow/codex/src/harness/repository-state.ts#L105-L141","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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"],"exampleFix":"// before\n{ sequence: 9007199254740993n-sourced number, offset: Math.round(-0.4) }\n\n// after\n{ sequence: '9007199254740993', offset: Math.round(-0.4) + 0 }","handlingStrategy":"validation","validationCode":"function assertCanonicalNumbers(value: unknown): void {\n  if (typeof value === 'number') {\n    if (!Number.isFinite(value) || Object.is(value, -0) || (Number.isInteger(value) && !Number.isSafeInteger(value))) {\n      throw new TypeError(`non-canonical number: ${value}`);\n    }\n    return;\n  }\n  if (Array.isArray(value)) { value.forEach(assertCanonicalNumbers); return; }\n  if (typeof value === 'object' && value !== null) Object.values(value).forEach(assertCanonicalNumbers);\n}","typeGuard":"function isCanonicalNumber(value: unknown): value is number {\n  return typeof value === 'number'\n    && Number.isFinite(value)\n    && !Object.is(value, -0)\n    && (!Number.isInteger(value) || Number.isSafeInteger(value));\n}","tryCatchPattern":"try {\n  return canonicalJson(payload);\n} catch (error) {\n  if (error instanceof Error && error.message === 'canonical JSON requires finite, safe, non-negative-zero numbers') {\n    return canonicalJson(normalizeNumbers(payload)); // stringify big ints/numbers, map -0 to 0, NaN to null\n  }\n  throw error;\n}","preventionTips":["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"],"tags":["canonical-json","number","safe-integer","serialization"],"backgroundTag":"unsafe-number-serialization","analyzedSha":"fa13ee4ad60ac2090b1480656eb233521790d640","analyzedAt":"2026-08-18T21:34:22.708Z","schemaVersion":2},"datasetVersion":"2026-08-22T09:17:25.309Z"}