Skip to content

feat: aggregates carry their own provenance (UX Rule 4, step 1) - #346

Merged
ntatschner merged 1 commit into
nextfrom
feat/aggregate-provenance
Jul 31, 2026
Merged

feat: aggregates carry their own provenance (UX Rule 4, step 1)#346
ntatschner merged 1 commit into
nextfrom
feat/aggregate-provenance

Conversation

@ntatschner

Copy link
Copy Markdown
Collaborator

First implementation step of docs/superpowers/specs/2026-07-31-ux-rules-2-and-4-design.md.

The problem

An aggregate hides its uncertainty precisely by aggregating.

LifeRow already carries death_inferred, and the session/event detail views already render an InferredBadge. But LivesResponse.deaths was a bare count — so a widget showing "12 deaths" gave a user no way to know some were reconstructed from Corpse lines after CIG removed the Actor Death log entries.

Measured: InferredBadge appears on 2 surfaces (both raw event views) and 0 of 25 widgets. Exactly inverted from where it matters.

The change

  • LifeSummary.deaths_inferred, computed in the same pass as deaths over the same list, so the split and the total cannot drift. A separately-derived split eventually disagrees with the total it describes.
  • LivesResponse.deaths_inferred on the wire.
  • <Provenance> — a sibling of InferredBadge, not a change to it. That component answers "is this row inferred"; aggregates need "how much of this number is inferred", and its two existing call sites are correct.
  • Wired to the lives widget.

It renders nothing when inferred === 0. Badging every number teaches people to ignore the badge; the signal is only worth having while it's rare.

Two corrections made during implementation

I corrected the spec. An earlier draft claimed this step needed no backend work. Checking found per-row provenance exists but per-aggregate does not — so wiring the component would have produced a mechanism with nothing to display, which is the exact failure this codebase keeps producing. The spec now records that, rather than quietly fixing it.

A test caught a real bug in my own component. My clamp was Math.min(inferred, Math.max(total, inferred)), which defeats the clamp entirely — it would have rendered "15 of 12", broadcasting an upstream bug to the user as a finding. Now Math.min(inferred, total).

Test plan

387 + 2 core, 1015 + 987 server, 670 web, clippy clean, lint clean, no codegen drift.

Mutation-verified both halves:

  • Removing the inferred <= 0 short-circuit → 5 tests become 2 failed, restored to 5.
  • Making the core split count all deaths rather than only inferred ones → both core tests fail.

The core tests drive the real derive_lives function rather than re-implementing its filter — the first draft did the latter, which asserted nothing about the code.

Not in this PR

Rule 2 (context) and the remaining aggregates. deaths is the proving case because death_inferred already existed per-life, so the split needed no new inference logic.

An aggregate hides its uncertainty precisely BY aggregating. LifeRow
already carries death_inferred, but LivesResponse.deaths was a bare
count - so a widget showing '12 deaths' gave a user no way to know some
were reconstructed from Corpse lines after CIG removed the Actor Death
log entries.

deaths_inferred is computed in the SAME pass as deaths, over the same
list, so the split and the total cannot drift. A separately-derived
split would eventually disagree with the total it describes.

<Provenance> is a sibling of InferredBadge, not a change to it - that
component answers 'is this row inferred', which is a different question
from 'how much of this number is inferred', and its two call sites are
correct. It renders NOTHING when inferred is 0: badging every number
teaches people to ignore the badge, and the signal is only worth having
while it is rare.

Corrected the spec mid-implementation: an earlier draft claimed this
step needed no backend work. Checking found per-row provenance exists
but per-aggregate does not, so wiring the component would have built a
mechanism with nothing to display - the exact failure this codebase
keeps producing.

A test caught a real bug in my own clamp: Math.max(total, inferred)
defeated the clamp entirely and would have rendered '15 of 12',
broadcasting an upstream bug to the user as a finding.

Mutation-verified both halves.
@ntatschner
ntatschner merged commit e577372 into next Jul 31, 2026
13 checks passed
@ntatschner
ntatschner deleted the feat/aggregate-provenance branch July 31, 2026 13:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant