Skip to content

aggregator: a future-dated read is 'today' forever until its date passes — derive freshness() has no upper bound, so one skewed measuredAt pins a hub card fresh for weeks #1568

Closed

Description

@dezren39

Summary

freshness() in the aggregator's derive module has no upper bound: a future-dated measuredAt is reported as age 0 — "today", not stale — and stays that way until cadenceDays after its future date arrives. A read stamped +30 days in the future with a 7-day cadence is presented as fresh for 37 days; a read stamped +365 days is fresh for a year. Both branches (date-only and datetime) clamp the negative age to 0, so the stale check ageDays > cadenceDays can never fire for a future stamp.

This is the freshness stamp on the hub's SaaS-service and PAYG cards — exactly the surface whose job is to say a dated read is old (§1.1). Garbage and missing stamps are handled correctly (stale: true, ageDays: null); only the future direction is missing.

Evidence (verified this run on main db6f7d00)

  • pm-stack/aggregator/src/derive.ts:855-868 — freshness(measuredAt, cadenceDays, now):
    • date-only branch (:862): ageDays = Math.max(0, Math.round((startOfToday - t) / 86_400_000))
    • datetime branch (:867): ageDays = Math.max(0, Math.floor((now.getTime() - t) / 86_400_000))
    • both then return stale: ageDays > cadenceDays — for any future t, age is clamped to 0 and stale is false.
  • Laptop probe (Bun 1.4.2, real derive.ts, sha256 312ac0fb identical on sandbox and laptop; probe + log in the fleet's probe records for this run), fixed now = 2026-10-27T12:00:00Z, cadence 7:
    • controls correct: 2026-10-26 → {stale:false, ageDays:1}; 2026-10-19 → {stale:true, ageDays:8}; garbage → {stale:true, ageDays:null}; null → {stale:true, ageDays:null}
    • 2026-10-28 (+1d), 2026-11-26 (+30d, both date-only and datetime forms), and 2027-10-27T12:00:00Z (+365d) all → {stale:false, ageDays:0}
    • the +30d stamp evaluated forward: still {stale:false, ageDays:0} at +29d and +30d, ageDays:7 (still not stale) at +37d, and only stale at +38d.
  • Render surface: freshnessStamp (pm-stack/aggregator/src/render.ts:1651-1659) renders ageDays === 0 as "· today" and adds the stale badge only when f.stale; callers are the PAYG card (render.ts:1704 via serviceCardHtml's freshness, and the PAYG line stamps), serviceCardHtml (:1704), and the service detail page (render.ts:1889).
  • A future stamp is reachable, not hypothetical: the SaaS extractors set measuredAt from the freshest date in a section when no **Last checked:** bullet is present (pm-stack/aggregator/src/saas-extract.ts:628, :651-652 — freshestDate(section)), and sections legitimately carry future operational dates (renewals, cycle ends — the exact hazard snapshot-records.ts's lastCheckedDate comment warns about). One future renewal date in a section without a Last-checked bullet becomes that service's measuredAt, and this bug then certifies it "today" for weeks. A typo'd year in the human-maintained quota view does the same.
  • Existing tests pin only the past/null directions: pm-stack/aggregator/test/usage.test.ts:266-270 (1-day-old not stale, 25-day-old stale, null stale). No future-dated case exists.

Why it matters

The freshness stamp is the dashboard's honesty mechanism for dated reads: it is what stops an old console read from being presented as current. A future stamp defeats it in the worst direction — the card claims the read happened today, carries no stale badge, and keeps claiming that until well after the stamped date — so a mis-dated or renewal-contaminated read silently pins a service's card as current while the real data ages underneath.

Acceptance criteria

  • A measuredAt in the future (beyond a small, named clock-skew tolerance that triage pins — e.g. 1 day for date-only stamps) is never reported as ageDays: 0 / "today" with stale: false: it is stale, or carries an explicit future/invalid age state the render surfaces honestly.
  • The rule applies to both branches (date-only and datetime) in freshness().
  • Controls unchanged: yesterday is 1 day old and not stale at cadence 7, a read older than its cadence is stale, garbage/null stay {stale: true, ageDays: null}.
  • Tests in usage.test.ts (or the render freshness tests) pin: +1d, +30d, and +365d future stamps in both stamp forms; the boundary at the chosen skew tolerance.
  • freshnessStamp does not render a future stamp as "· today" without a stale/future indication.

Dedup / distinctness

Searched open + closed issues for freshness, future-dated, and ageDays: no filing covers derive.ts freshness(). Adjacent but different modules, gates, and fix surfaces:

The fix for either of those does not touch derive.ts freshness() or its render callers.

Activity

  1. developing-today commented on Oct 28, 2026

    @developing-today
    Contributor
  2. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    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
    priority/normalOrdinary priority (the default)
    app/aggregatorPackage/app: aggregator (pm-stack in this repo)
    effort/sSmall: expected under 2 hours of work
    on Oct 28, 2026
  3. dezren39 commented on Oct 28, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

    • Agent: new-issue-creator (lane A, new-issue-creator-code-a)
    • Session / ID: cron new-issue-creator-code-a, run 2026-10-27 21:35 CDT
    • Found via: filed this run
    • Task: Triage the future-dated measuredAt freshness defect: derive.ts freshness() clamps negative ages to 0 in both branches, so a future stamp renders "today"/not-stale until cadenceDays after its future date (verified +30d fresh for 37 days, +365d fresh for a year, laptop Bun probe on the real derive.ts).
    • Expectation: effort/s — pin a small clock-skew tolerance, make future stamps stale/honest in both branches, pin with tests; controls (past/garbage/null) unchanged.
    • Links: probe hidden_files/probe-2026-10-27-laneA-freshness/ (fleet goal workspace)
  4. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed on Oct 28, 2026
  5. 4 remaining items

  6. added
    queue/queuedHas a queued task in the Supabase agent queue
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/claimedQueue worker lifecycle state
    work/assignedQueue worker lifecycle state
    queue/queuedHas a queued task in the Supabase agent queue
    on Oct 28, 2026
  7. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  8. added 2 commits that reference this issue on Oct 31, 2026
  9. dezren39 commented on Oct 31, 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)effort/sSmall: expected under 2 hours 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