Repository navigation
Description
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— declarednumberin 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), anddnsTable'se.priceUsd.toFixed(2)(render.ts:2515).- Usage view: live.ts:803 copies
feed.dns.totalAnnualListUsdverbatim intodomains.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, entrypriceUsd: 11.08) —isDnsSectiontrue,renderDnsOK (19,979 chars). totalAnnualListUsdmissing —isDnsSectiontrue;renderDnsthrowsTypeError: undefined is not an object (evaluating 'section.totalAnnualListUsd.toFixed').totalAnnualListUsd: "11.08"(string) —isDnsSectiontrue;renderDnsthrowsTypeError: section.totalAnnualListUsd.toFixed is not a function.totalAnnualListUsd: null—isDnsSectiontrue;renderDnsthrows the same TypeError on null.- Entry
priceUsd: "11.08"(string, valid total) —isDnsSectiontrue;renderDnsthrowsTypeError: 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
isDnsSectionrejects a section whosetotalAnnualListUsdis not a finite number, and rejects an entry whosepriceUsdis present but not a finite number (or, equivalently, the renderers never call.toFixedon a value the validator has not guaranteed numeric — the owning layer is triage's pin, applied consistently todns(), the usagedomainsblock, and/api/dns.jsonconsumers).- 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.jsonkeeps serving the absent form. - Regression tests: sections with missing, string, and null
totalAnnualListUsd, and an entry with a stringpriceUsd, 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
statusgap (validate it in the same gate, coerce unknown tounused, or explicitly leave it) and records the ruling on this issue. - No change to the on-disk feed format or to
dnsSectionFromRegisteroutput for valid registers.
Notes
- Not self-solved by the creator lane: whether the numeric rule belongs in the validator or the renderers (and whether a non-finite total should reject the whole section or just the domains card) is a small contract decision for triage, and this gate is shared by the collector and the live source.
- Related, different defect: aggregator(health): dns/aa section stamps are never validated — a garbage dns generatedAt renders ageMs NaN/null, a future aa generatedAt renders age 0 (maximally fresh) for as long as the skew lasts #2242 (health.ts age computation never validates dns/aa
generatedAtstamps — NaN/future ages, no crash, different file and field). This issue is the dns.ts gate itself and the render-time.toFixedcrashes.
Activity
developing-today commentedon Oct 29, 2026
CODE-1231
dezren39 commentedon Oct 29, 2026
🤖 Agent note · filed
26 remaining items
dezren39 commentedon Nov 2, 2026
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
dezren39 commentedon Nov 2, 2026
🤖 Agent note · picked up
Merge pull request #4174 from developing-today/queue/5307-2412-dns-se…
dezren39 commentedon Nov 2, 2026
🤖 Agent note · done