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
- Convert the fractional value at the reported path to a scale-12 decimal string
- Run the receipt through encodePolicyFractions before signing or verifying
- 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
- Always run encodePolicyFractions before signing or verifying receipts
- Never hand-write fractional JSON numbers in receipt fixtures
- Persist decimals as strings end-to-end across producers and consumers
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
- policy value must be finite
- actualUsd must be a non-negative finite number
- Agent config must include id, name, and type
- agent must be an object
- anchor task has no query
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 theView on GitHub (pinned to 2602b642d9)