Skip to content

aggregator(dns): isDnsSection never checks totalAnnualListUsd or entry priceUsd — a feed section that passes validation crashes /dns and the usage view on .toFixed #2412

Closed

Description

@dezren39

aggregator(dns): isDnsSection never checks totalAnnualListUsd or entry priceUsd — a feed section that passes validation crashes /dns and the usage view on .toFixed

Summary

isDnsSection (pm-stack/aggregator/src/dns.ts:73-88) is the structural gate for the DNS register section in the fleet feed: sources/live.ts only serves feed.dns (live.ts:946-950, dns()) and only builds the usage view's domains block from it (live.ts:800-807) when this check passes. The check validates only generatedAt (string), a non-empty entries array, and each entry's domain/renewal strings. It never checks:

  • totalAnnualListUsd — declared number in the model (model.ts:650), and
  • each entry's priceUsd — used as a number everywhere it renders.

Both unchecked values are dereferenced with .toFixed(2) on live render paths, so a feed section that passes validation crashes rendering instead of being rejected as invalid:

  • renderDns: section.totalAnnualListUsd.toFixed(2) (render.ts:2537), next.priceUsd.toFixed(2) (render.ts:2541), and dnsTable's e.priceUsd.toFixed(2) (render.ts:2515).
  • Usage view: live.ts:803 copies feed.dns.totalAnnualListUsd verbatim into domains.totalAnnualUsd, which render.ts:1813 formats with .toFixed(2).

The /dns route (server.ts:526-528) awaits source.dns() and calls renderDns with no try/catch, and Bun.serve is configured with no error handler (server.ts:809-815), so the throw surfaces as a 500 for the whole page — permanently, on every request, for as long as the poisoned section stays in the feed. The same poisoned section also breaks the usage page that embeds the domains card.

Evidence (verified this run, main 5a34fa7)

Probe: laptop ~/git/fleet/probe-2026-10-29-laneA-dnsvalidator/probe.ts (Bun 1.4.2), importing the real dns.ts and render.ts from the laptop clone — both files md5-identical to sandbox main 5a34fa7 (dns.ts b5829b32b50a5a0b7484a05123c94790, render.ts ab6a7524f90bf11d80b71fd0c6a0b756). A minimal provenance stub is the only stand-in.

  • Control: a well-formed section (totalAnnualListUsd: 11.08, entry priceUsd: 11.08) — isDnsSection true, renderDns OK (19,979 chars).
  • totalAnnualListUsd missing — isDnsSection true; renderDns throws TypeError: undefined is not an object (evaluating 'section.totalAnnualListUsd.toFixed').
  • totalAnnualListUsd: "11.08" (string) — isDnsSection true; renderDns throws TypeError: section.totalAnnualListUsd.toFixed is not a function.
  • totalAnnualListUsd: null — isDnsSection true; renderDns throws the same TypeError on null.
  • Entry priceUsd: "11.08" (string, valid total) — isDnsSection true; renderDns throws TypeError: next.priceUsd.toFixed is not a function.
  • Usage path with the missing-total section: domains.totalAnnualUsd.toFixed(2) throws the identical TypeError.

The producer in this repo always builds a numeric total (dnsSectionFromRegister, dns.ts:64-69 sums priceUsd ?? 0), so this is a validator/render contract gap, not a claim that the current collector emits bad values: the feed is a file boundary shared with the collector, older feeds, and hand/backfill tooling, and isDnsSection is documented (dns.ts:72) as "the structural check shared by the collector and the live source" — the one place that is supposed to keep a malformed section out of the renderers. Sibling validators in this codebase were hardened for exactly this shape (e.g. #2376/#2408 registry, #2347 comments/actions); the DNS gate was not.

Secondary, same gate: entry status is also unchecked — an entry with a missing/unknown status is counted in the register total and the full table but silently appears in none of summarizeDns's active/earmarked/unused buckets (dns.ts:99-110), so the "Portfolio shape" counts on /dns under-count without any crash. Triage may fold this into the same fix or split it; the crash is the primary defect.

Why it matters

/dns is the fleet's domain-spend register page and the usage hub embeds its total. One malformed feed section — a renamed/retyped field from a collector change, a hand-edited feed, a partial backfill — takes both pages down with a 500 on every hit instead of the section being treated as absent/invalid (the honest states the rest of the aggregator already implements for other sections). The validator exists precisely to draw that line; today it certifies records the renderers cannot render.

Acceptance criteria

  • isDnsSection rejects a section whose totalAnnualListUsd is not a finite number, and rejects an entry whose priceUsd is present but not a finite number (or, equivalently, the renderers never call .toFixed on a value the validator has not guaranteed numeric — the owning layer is triage's pin, applied consistently to dns(), the usage domains block, and /api/dns.json consumers).
  • A poisoned section in the feed no longer 500s /dns or the usage page: it is treated as absent/invalid with the existing provenance wording, while /api/dns.json keeps serving the absent form.
  • Regression tests: sections with missing, string, and null totalAnnualListUsd, and an entry with a string priceUsd, each fail the gate (or render safely, per the pinned rule); the well-formed fixture section (slice3.test.ts) still renders unchanged, including its totals.
  • Triage rules on the secondary status gap (validate it in the same gate, coerce unknown to unused, or explicitly leave it) and records the ruling on this issue.
  • No change to the on-disk feed format or to dnsSectionFromRegister output for valid registers.

Notes

Activity

  1. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    task/triageQueue packet kind: verify, reproduce, and route this item
    on Oct 29, 2026
  2. developing-today commented on Oct 29, 2026

    @developing-today
    Contributor
  3. added
    source/creatorFiled by the new-issue-creator job
    priority/normalOrdinary priority (the default)
    queue/queuedHas a queued task in the Supabase agent queue
    effort/xsTrivial: expected under 30 minutes of work
    app/aggregatorPackage/app: aggregator (pm-stack in this repo)
    on Oct 29, 2026
  4. dezren39 commented on Oct 29, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

    • Agent: new-issue-creator job (new-issue-creator-code-a)
    • Session / ID: new-issue-creator-code-a cron run 2026-10-29 14:35 CDT
    • Found via: filed this run
    • Task: Triage the isDnsSection numeric-field gap: pin whether the finite-number rule for totalAnnualListUsd / entry priceUsd lives in the validator or the renderers, and route a small_fix packet.
    • Expectation: effort xs — done when a poisoned feed section can no longer 500 /dns or the usage page, with regression tests for missing/string/null total and string priceUsd.
    • Links: evidence in the issue body; probe on the laptop at ~/git/fleet/probe-2026-10-29-laneA-dnsvalidator/ (real dns.ts/render.ts, md5-identical to main 5a34fa7)
  5. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed on Nov 1, 2026
  6. 26 remaining items

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

    @dezren39
    MemberAuthor

    agent-note:v1
    transition: parked
    worker: repo-worker-code
    queue: repo-health-code-pilot
    task: 5307
    at: 2026-11-02T05:45:35Z
    finding: CI test check is QUEUED on canonical PR #4174; PR remains OPEN against main and Drew-authored. This lane made no PR or branch changes.
    disposition: Parked 20 minutes under the CI-pending rule.
    resume: Revalidate #2412/#4174 and current check state after the park interval. Do not touch the Drew-authored PR; proceed only if ownership restrictions are resolved and checks are green.
    links: #4174

  9. 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 2, 2026
  10. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor
  11. added a commit that references this issue on Nov 2, 2026
  12. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app/aggregatorPackage/app: aggregator (pm-stack in this repo)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/normalOrdinary priority (the default)source/creatorFiled by the new-issue-creator jobtask/triageQueue packet kind: verify, reproduce, and route this item

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions