ruvnet/ruflo · error

fractional number at

Error message

fractional number at ${path} must be a scale-12 decimal string (ADR-322C rule 2)

What it means

assertReceiptNumberDomain validates that receipt numbers conform to ADR-322C rule 2: every fractional value must be represented as a scale-12 decimal string, and plain JSON numbers must be integers. It throws when it encounters a fractional (non-integer) number because numbers like 0.1 are not exact in binary floating point and would make signed receipts non-deterministic.

Solutions

  1. Convert the fractional value at the reported path to a scale-12 decimal string
  2. Run the receipt through encodePolicyFractions before signing or verifying
  3. Fix the producer so fractional values are emitted as strings (see the ruvnet/ruflo#3229 fixture lesson)

Example fix

// before
{ "hybridWeight": 0.65 }
// after
{ "hybridWeight": "0.650000000000" }
Defensive patterns

Strategy: validation

Validate before calling

function assertScale12Strings(obj, path='$') { for (const [k,v] of Object.entries(obj)) { if (typeof v === 'number' && !Number.isInteger(v)) throw new Error(`fractional at ${path}.${k}: use scale-12 string`); if (v && typeof v === 'object') assertScale12Strings(v, `${path}.${k}`); } }

Type guard

const isReceiptNumberOk = (v: unknown): boolean => typeof v !== 'number' || Number.isInteger(v);

Try / catch

try { verifyFlywheelReceipt(receipt); } catch (e) { if (String(e.message).match(/fractional number at (.+) must be a scale-12/)) { console.error(`Fix field ${e.message.match(/at (\S+) /)?.[1]} to a decimal string`); } throw e; }

Prevention

When it happens

Trigger: Calling assertReceiptNumberDomain (directly or via signedBytes/verifyFlywheelReceipt) on a receipt object that contains a non-integer JS number at the given path, e.g. { amount: 0.65 } instead of { amount: "0.650000000000" }.

Common situations: Deserializing a receipt that was produced by a client encoding decimals as JSON numbers; hand-writing a fixture with fractional literals; bypassing encodePolicyFractions and feeding raw policy objects to the verifier.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


AI-assisted analysis of ruvnet/ruflo@2602b642d9 (2026-09-15). Data as JSON: /api/errors/7cbf0d8200fd6027. Report an issue: GitHub.

Appendix: source

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

 * Enforce ADR-322C rule 2 / conformance-checklist A3 over a receipt payload:
 * "Every fractional value is a canonical decimal string, not a binary float."
 *
 * This lives at the RECEIPT boundary rather than inside `canonicalizeJcs`,
 * which is shared with the proposer envelope (`flywheel-proposer.ts`) and the
 * promotion ledger (`flywheel-transaction.ts`) — structures the contract does
 * not govern. A first attempt put the check in the canonicalizer and broke
 * those callers, which is the reason the scope is spelled out here.
 *
 * Integers and scaled integers stay JSON numbers (currency micros, durations,
 * iteration counts). A fractional JSON number anywhere in the payload is a
 * contract violation, and the error names the path — the original bug
 * (ruvnet/ruflo#3229) needed a live fixture to find precisely because neither
 * verifier said which field was wrong.
 */
export function assertReceiptNumberDomain(value: unknown, path = '$'): void {
  if (typeof value === 'number') {
    if (!Number.isInteger(value)) {
      throw new Error(
        `fractional number at ${path} must be a scale-12 decimal string (ADR-322C rule 2)`,
      );
    }
    return;
  }
  if (Array.isArray(value)) {
    value.forEach((v, i) => assertReceiptNumberDomain(v, `${path}[${i}]`));
    return;
  }
  if (value && typeof value === 'object') {
    for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
      assertReceiptNumberDomain(v, `${path}.${k}`);
    }
  }
}

export function policyCandidateId(policy: Record<string, unknown>): string {
  // Encode at the hashing boundary so the content ID is always over the

View on GitHub (pinned to 2602b642d9)