Skip to content

feat(OMN-19980): Overview Spend, Measured runs, Tokens and Savings read one metering-summary all row - #364

Closed
Patel230 wants to merge 3 commits into
lakshman/OMN-19980-savings-source-guardfrom
lakshman/OMN-19980-step-b-overview-savings
Closed

Patel230 wants to merge 3 commits into
lakshman/OMN-19980-savings-source-guardfrom
lakshman/OMN-19980-step-b-overview-savings

Conversation

@Patel230

@Patel230 Patel230 commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Summary

OMN-19980 AC2, Step B: the Overview Spend, Measured runs, Tokens and Savings cards read one metering-summary.v1 row, the all row with the newest as_of, so the four figures cannot disagree with each other or with onex metering. It is stacked on #361 (Step A, the guard) and carries #359's stack beneath it; retarget it to dev when #361 merges.

What changed

  • Bindings, before to after (src/pages/local/overview.contracts.yaml, overview.page.yaml):

    Overview card before after
    Spend cost.savings-overview.v1 total_cost_usd metering-summary.v1 spend_usd
    Savings total cost.savings-overview.v1 total_savings_usd, total_baseline_cost_usd, plus delegation.savings.v1 baseline labels metering-summary.v1 savings_usd, baseline_model, pricing_manifest_version, baseline_state
    Measured runs cost.savings-overview.v1 measured_run_count and the zero-token, estimated and unknown counts metering-summary.v1 runs_measured, runs_total, runs_unknown_tokens, runs_unknown_spend
    Tokens no binding (upstream-blocked) metering-summary.v1 tokens_in, tokens_out, runs_total, runs_unknown_tokens

    All four select window_kind = all and order by as_of descending. The Overview no longer binds cost.savings-overview.v1.

  • src/pages/LocalDashboardPage.tsx: the Overview cards render those fields; a card whose field is not served shows the typed waiting state, never a number or $0.

  • The guard src/pages/local/omn19980.savings-source.test.ts drops the Step A allowlist entry (overview-savings-snapshot), so any Overview binding to another savings exposure now fails it.

  • delegation.savings.v1 stays only for per-run cost lookups (local_cost_usd), as ruled in the OMN-19980 spec's Amendment 2.

  • Avg saving / call (OMN-20009's card) binds only once the field savings_per_measured_run_usd is served, not merely the exposure. The lakshman-lane GET /projections read at 2026-10-06T07:34:32Z (omnimarket 0.4.301) serves metering-summary.v1 without that column.

  • src/pages/local/served-catalogue.lab.json re-captured with per-exposure columns (sha256 ba54e427...; the old transform's control output is byte-equal, sha256 19c35077...). Both digests are from the implementing helper's measurement.

  • Tests: guard 30, Overview page tests 22 (LocalDashboardPage.omn19980.test.tsx), OMN-20009 contract tests 4; existing tests updated to the new shape; 8 screenshots regenerated.

Size: 21 files, 1812 insertions and 217 deletions against #361's head (a996599); 8 of the files are regenerated screenshots, 1204 of the changed lines are the re-captured catalogue fixture, and 162 changed lines are production code and contracts (LocalDashboardPage.tsx and the two Overview yaml files).

How it was verified

Commits 10c3450, 1a6159b and f9d10bf are each signed (git log --format=%G? prints G for all three).

  • Re-run at head f9d10bf at 2026-10-06T07:57Z (measured by me): npx vitest run src/pages/local/omn19980.savings-source.test.ts src/pages/LocalDashboardPage.omn19980.test.tsx src/pages/local/omn20009.contracts.test.ts exits 0, 3 files and 56 tests passed, 1.91 s.
  • Red then green (implementing helper's measurement): the tests were written first; against 10c3450's code 14 of 52 failed, and at 1a6159b 52 of 52 passed.
  • Mutations (helper's measurement), each caught: reverting Spend failed 15; rebinding Measured runs failed 5; unbinding Tokens failed 12; putting the baseline everywhere failed 3; removing the no-token check failed 1.
  • f9d10bf, the field-served tripwire (helper's measurement): OMN-20009's C2 failed 1 of 3 before the change and passed 4 of 4 after. A mutation that adds savings_per_measured_run_usd to the catalogue makes C2 fail, so the tripwire turns red the moment the field is served.
  • Whole suite (helper's measurement): full vitest 2039 passed, 15 skipped, 0 failed; npm run check, tsc -p tsconfig.node.json --noEmit, lint and build exit 0; Playwright contracts plus omn20009 20 passed; pre-commit passed.
  • Not run by me: the full suite, Playwright and the build; the numbers in the four bullets above are the helper's.

Failure paths

n/a: this change renders served fields and adds tests; it writes no receipt, verdict or report.

Open defects

  • The OMN-19977 AC3 lab readback of the Overview against a served metering-summary.v1 is not done. metering_summary has 0 rows on the lakshman lane, and the cause is under investigation; until rows exist, the Overview shows the waiting state, and no figure on this page has been compared with onex metering on live data. No fixing commit yet.
  • R4 of the guard scans lower-snake-case data field names only. A savings figure computed from cost fields under a camelCase name would not be caught (carried from feat(OMN-19980): guard savings to metering-summary.v1 and drop per-run savings #361).

Local gates

  • No gate was bypassed — no --no-verify, no SKIP=, no --no-gpg-sign, no core.hooksPath override. Every pre-commit and pre-push hook ran.
  • Any hook that failed was fixed at the input, not worked around.

Not in this change

  • The omnimarket half of OMN-19980 (AC1, omnimarket#3373, merged).
  • The Avg saving / call card's served field savings_per_measured_run_usd: it stays waiting until the exposure serves it (OMN-20009).
  • The legacy delegation pages, which are outside the six.
  • The live readback of the four figures against onex metering (see Open defects).

Overlapping open PRs

Read live at 2026-10-06T07:57Z: all seven open omnidash PRs were listed and each one's changed-file list compared with this branch's.

Overlap-Reviewed: #361 — OMN-19980 Step A, the base of this PR; an ancestor of this head (git merge-base --is-ancestor true), so no conflict. Step B removes the allowlist entry that Step A adds.
Overlap-Reviewed: #359 — OMN-20009, beneath #361 in the stack; its head 4517cf3 is in this head's history. It owns the Avg saving / call card; this PR changes only how that card is gated (the field, not the exposure).
Overlap-Reviewed: #358 — OMN-20006 adds the Usage page's Savings per day card waiting on metering-summary.v1. It shares LocalDashboardPage.tsx, LocalDashboardPage.live.test.tsx, pages.test.ts and local-dashboard-contracts.spec.ts with this branch; this PR does not change the Usage page's cards, and both read metering-summary.v1. Both are needed; expect a textual conflict in those four files on whichever merges second.
No file overlap with #356, #352, #350 or #337 (bot pin bumps, a receipt-gate pin, and a parked graph spike).

Evidence companion: onex_change_control#13027 (item dod-omn19980-pr364-overview-metering-vitest, AC2, receipt captured at f9d10bf: 56 passed).

Evidence-Source: OCC#13027
Evidence-Ticket: OMN-19980

…ll-time row

Step B of AC2. The Overview Savings total was the last savings figure on the
six local pages still read from another exposure (cost.savings-overview.v1,
let through by the guard's Step A allowlist), and its baseline caption came
from delegation.savings.v1. Both now come from one metering-summary.v1 row,
the all-time row `onex metering` renders, so the figure and the baseline
under it cannot disagree. The allowlist is empty and asserted empty, so the
guard now holds AC2 as written.

The fold keys rows by baseline model and keeps an older baseline's rows, so
a baseline change leaves two all rows. The card shows the one refreshed last
(every refresh runs under the resolved baseline) and refuses, by name, two
refreshed at the same moment rather than picking one. An unresolved baseline
reads "Baseline unresolved", a null saving "Not measured", no all row "Not
measured: no all-time row", and an unserved exposure names itself; never $0.

Spend and Measured runs stay on cost.savings-overview.v1 (ruling 2: AC2
covers savings only).

Known red until the lab serves the exposure: four pages.test.ts cases that
compare Overview's bindings with served-catalogue.lab.json. The capture
(2026-10-02) has no metering-summary.v1 and the catalogue is not edited by
hand; it turns green when a release carrying omnimarket#3368 is deployed and
the catalogue is re-captured.
…me metering all row

Amendment 2 (Lakshman, 2026-10-06 ~07:20Z: "Move all to metering"). After the
Savings rebind, Overview still took Spend and Measured runs from
cost.savings-overview.v1 and left Tokens unbound, so the four headline figures
came from different rows and could disagree with `onex metering` and with each
other. All four now bind metering-summary.v1 and pick one row the same way: the
all-time row refreshed last, refusing by name two refreshed at the same moment.
Spend is spend_usd, Measured runs is runs_measured with the same row's run
classes (runs_total, unknown-token and unknown-spend runs) as its caption, and
Tokens shows tokens_in and tokens_out side by side, never summed.

Only Savings is priced at the baseline, so an unresolved baseline is that
card's state alone; MetricCard applies it only to a binding that asks for
baseline_state. A null spend or measured count reads Not measured, and token
totals with no run that recorded tokens read Not measured rather than 0.

Overview no longer binds cost.savings-overview.v1 at all. The old Measured runs
caption (estimated and unknown run counts) had no metering equivalent for
"estimated"; it is replaced by metering's own run classes, every run in exactly
one. delegation.savings.v1 stays only for the per-run cost lookups (ruling 2).

The guard gains a headline rule with planted controls (another exposure, a
second caption exposure, an unbound card, no all-row fields, the wrong column),
and the page tests cover served, unserved, unresolved baseline, null figures,
a baseline change, a same-moment tie and day rows only, for every card.
…d, not the exposure

The lakshman lane (omnimarket 0.4.301, read 2026-10-06T07:34:32Z) now serves
onex.snapshot.projection.metering-summary.v1 ok, but its columns are tenant_id,
window_kind, window_start, window_end, as_of, baseline_model,
pricing_manifest_version, baseline_state, runs_total, runs_measured,
runs_unknown_tokens, runs_unknown_spend, tokens_in, tokens_out, spend_usd,
counterfactual_usd, savings_usd, summary_json. savings_per_measured_run_usd is
not among them (git grep over omnimarket origin/dev finds nothing), and the
dashboard must never divide savings by runs in the browser. C2 assumed
"exposure served" meant "field served" and went red on the new catalogue.

The catalogue entries now carry the served columns, taken from the saved GET
(no new remote read), and C2 binds the card only when the exposure is ok AND
lists savings_per_measured_run_usd. Until then the card stays unbound with
upstream-blocked. Planted controls (in-test catalogue copies) cover served
without the field, served with the field, and degraded with the field.
OMN-20009 owns the card and binds it when the field is served.
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