Skip to content

cloudflare-export: num() reinterprets empty, hex and scientific strings as real measurements — a drifted '' request count exports as a measured 0 #2791

Closed

Description

@dezren39

cloudflare-export: num() reinterprets empty, hex and scientific strings as real measurements — a drifted '' request count exports as a measured 0

Evidence (verified this run, main da42bfe7)

pm-stack/cloudflare-export/cloudflare-quota-export.mjs, num() (lines 85–92):

const num = (v, field) => {
  const n = typeof v === "string" || typeof v === "number" ? Number(v) : NaN;
  if (!Number.isFinite(n) || n < 0) {
    throw new ExportError("unparseable", 6, `graphql: field ${field} not a finite non-negative number`);
  }
  return n;
};

The helper is the exporter's fail-closed gate for every GraphQL measurement it publishes — its error text promises that anything not "a finite non-negative number" aborts the tick as unparseable (exit 6) instead of reaching a status: "ok" document. But bare Number() accepts far more than the API's decimal spellings, so the gate passes values it exists to reject. Every measurement flows through it: R2 request counts (classifyR2, line 136), KV request counts (foldKv, line 161), the generic firstSum totals (line 173), and R2 storage payloadSize (line 216).

Probe (laptop, importing the real module — file md5 0fbd8dae4f5eba83adb88ec6092d1039 identical on both clones; probe ~/git/fleet/probe-2026-10-30-laneB-cfnum/; one sum.requests value mutated per case through classifyR2 with actionType PutObject):

input result
"" OK — classA = 0 (a drifted empty value exports as a measured zero)
" " (whitespace) OK — classA = 0
"0x10" OK — classA = 16 (hex reinterpreted)
"1e2" OK — classA = 100 (scientific reinterpreted)
"1.5" OK — classA = 1.5 (fractional request count)
" 12 " OK — classA = 12 (whitespace-trimmed by coercion)
controls: "12abc" / "-3" / "Infinity" correctly throw unparseable (exit 6)

foldKv shows the same hole (requests: "" → reads = 0, no throw).

Why it matters

This exporter follows the PM stack's fail-closed design: a drifted upstream shape must produce a failed tick, never a plausible reading (compare classifyR2/foldKv's own missing-groups guards in the same file, which throw precisely so a missing field is not published as zero). An empty-string count — the classic shape-drift / missing-value spelling from an upstream API — currently sails through num() because Number("") === 0, and is published in a status: "ok" document as a measured 0 requests / 0 payload bytes. The aggregator ingests that as fact, so real Cloudflare usage silently reads as zero exactly when the GraphQL API starts drifting. Hex and scientific spellings are silently reinterpreted into different numbers instead of failing closed, and a fractional "count" is published as-is.

Acceptance criteria

  • num() accepts only plain decimal spellings for string inputs (the GraphQL API's actual format): empty, whitespace-only or whitespace-padded, hex (0x…), scientific (1e…), and fractional strings throw ExportError("unparseable", 6, …) naming the field, at every call site (R2, KV, firstSum, R2 storage payload).
  • Numeric (non-string) inputs keep working exactly as today, including the existing finite / non-negative checks; legitimate decimal-string counts (e.g. "1200") still parse.
  • New tests in cloudflare-quota-export.test.mjs cover: "" and " " request counts rejected; "0x10" / "1e2" / "1.5" rejected; a decimal-string control still accepted; all existing tests still pass (node --test pm-stack/cloudflare-export/).

Dedup

Activity

  1. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    priority/lowNice to have; do when convenient
    queue/queuedHas a queued task in the Supabase agent queue
    source/creatorFiled by the new-issue-creator job
    task/triageQueue packet kind: verify, reproduce, and route this item
    app/pm-stackpm-stack (Pad/Grist) in developing-today/code
    effort/xsTrivial: expected under 30 minutes of work
    on Oct 30, 2026
  2. dezren39 commented on Oct 30, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

    • Agent: new-issue-creator (lane B, new-issue-creator-code-b)
    • Session / ID: cron new-issue-creator-code-b run 2026-10-30 12:20 CDT
    • Found via: filed this run
    • Task: Triage the num() string-coercion hole in pm-stack/cloudflare-export (cloudflare-quota-export.mjs:85-92): pin the strict decimal-only spelling contract for all call sites (R2 :136, KV :161, firstSum :173, R2 storage :216), then route a small-fix packet.
    • Expectation: effort/xs — done when triage pins the accepted-spelling shape and the follow-up fix is queued.
    • Links: evidence + probe in the issue body; probe laptop ~/git/fleet/probe-2026-10-30-laneB-cfnum/
  3. dezren39 commented on Oct 30, 2026

    @dezren39
    MemberAuthor

    Agent note — triage verdict: CONFIRMED (queue task 3961)

    Re-verified independently this shift, not taken from the filing probe:

    • Code, current main b706a837: num() at pm-stack/cloudflare-export/cloudflare-quota-export.mjs:85–92 is byte-identical to the issue's quote (bare Number() + finite/non-negative gate). Call sites confirmed at lines 136 (classifyR2), 161 (foldKv), 173 (firstSum), 216 (R2 storage payloadSize).
    • Behavior, laptop probe against the real module (classifyR2, one mutated sum.requests per case): "" → classA 0, " " → 0, "0x10" → 16, "1e2" → 100, "1.5" → 1.5, " 12 " → 12 — all no-throw; controls "12abc" / "-3" / "Infinity" correctly throw unparseable (exit 6). foldKv with requests: "" → reads 0, no throw. Every row of the issue's probe table reproduced exactly.

    Follow-up: fix enqueued as queue task 4021 (small_fix, dedupe repo-health:v1:fix:developing-today/code:issue:2791) carrying the issue's acceptance criteria verbatim. This issue stays open until that fix lands.

  4. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    🤖 Design-architect pass 25 (2026-10-31) — agent note: this issue is ADOPTED as Phase 3 (with #2743) of design #3005 (a measurement is a plain decimal), linked as its sub-issue and labelled source/design-architect. Triage: do NOT re-file, re-scope, or split it; record the phase structure instead. The design generalises the landed sibling gate (meta-exporter via #2789, CLOSED) to this exporter: plain-decimal spellings only, with this exporter's existing fail-closed outcome on refusal. The closing inventory audit for the whole family is #3006, gated on this phase landing. Flag genuine blockers on #3005.

  5. 10 remaining items

  6. added
    task/small-fixQueue packet kind: bounded fix with reproduction and targeted tests
    on Nov 1, 2026
  7. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · parked

  8. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  9. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · parked

  10. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · parked

  11. added
    queue/claimedQueue worker lifecycle state
    and removed
    status/parkedDeliberately paused with a recorded resume condition
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    on Nov 1, 2026
  12. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  13. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done

    • Agent: repo-worker-code, lane E
    • Session / ID: queue task 4021, lane E shift 2026-11-01
    • Outcome: PR fix(cloudflare-export): reject non-decimal measurement strings #3488 merged (merge commit c31fc8f); this issue auto-closed by the merge. Codex P2 dispositioned on the PR (fixed at 239ed1e, thread resolved, re-review clean). Tests: cloudflare-export suite 19/19 (laptop, prior lane). Zero-job Jules workflow failures proven pre-existing on clean base.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app/pm-stackpm-stack (Pad/Grist) in developing-today/codeeffort/xsTrivial: expected under 30 minutes of workkind/bugSomething isn't working (prefixed form of the classic bug label)priority/lowNice to have; do when convenientsource/creatorFiled by the new-issue-creator jobsource/design-architectFiled by the repo design architect jobtask/small-fixQueue packet kind: bounded fix with reproduction and targeted tests

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions