Skip to content

feat(OMN-20009): Overview shows the served run-locally share and saving per call - #359

Merged
jake-b-omni merged 14 commits into
devfrom
lakshman/OMN-20009-overview-served-exposures
Oct 6, 2026
Merged

jake-b-omni merged 14 commits into
devfrom
lakshman/OMN-20009-overview-served-exposures

Conversation

@Patel230

@Patel230 Patel230 commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Summary

For OMN-20009 AC1, AC2 and AC4: two Overview cards, Run locally and Avg saving / call, each bound to one served field, with no arithmetic in the browser and a typed reason, never 0, 0% or $0, when there is no measurement.

Stacked on omnidash#355 (OMN-20225) at 850f939. #355 removes the reader's hand-written model-routing fold, which this card must not read. Until #355 merges, this PR's diff carries its commits too. Merge order: after omnidash#354 (OMN-20008) and #355, which edit the same Overview files. The served fields come from the omnimarket PR for OMN-20009, which is stacked on omnimarket#3368.

What changed

Two commits on #355's head:

  • eacb277 the cards.
    • Run locally (overview-run-locally) binds onex.snapshot.projection.delegation.model-routing.v1 and shows by_tier.local_call_share (local runs over all runs) as a percent. Under it are the served local_call_count, total_call_count and not_tier_routed_count, so the gap to the tier-routed share stays visible. On the lab catalogue fixture that is 83.9%, "104 of 124 runs local · 20 not tier-routed".
    • Avg saving / call (overview-avg-saving-per-call) is declared with no binding and upstream-blocked, and renders "Not served yet: waits on metering-summary.v1". The captured catalogue does not serve that exposure yet, and the page contract test refuses a binding to an exposure the catalogue does not mark ok. Once it is served, SavingsPerRunCard reads savings_per_measured_run_usd from the all-time row as served decimal text.
    • No row is the no-runs state; a missing field says what it waits on; a null is "Not measured", or "Baseline unresolved" / "Not measured: no measured runs" for the saving.
    • In src/layout/local-page-loader.ts, a metering-summary row with no saving no longer switches the whole page to Baseline unresolved: that card states its own reason.
    • overview.page.yaml places both cards in row 8 and moves feat(OMN-20225): run rows show the served verdict, backend, host and tier; Overview gains quality and tier-mix panels #355's Quality and Tier mix panels to row 10.
    • Tests: src/pages/local/omn20009.contracts.test.ts, src/pages/LocalDashboardPage.omn20009.test.tsx (21 tests), the Overview metric-key assertion in pages.test.ts, and tests/e2e/local-dashboard-omn20009.spec.ts with two 1440x900 screenshots.
  • be39357 drops an unused binding from the E2E spec. The repo's pre-commit ESLint covers tests/, which npm run lint does not.

How it was verified

Update 2026-10-06 08:18Z: review finding fixed, dev brought in (head d33d431)

Supersedes the counts in the 03:43Z update for the current head.

  • Review finding: SavingsPerRunCard refused whenever metering-summary.v1 had more than one all row. metering_summary is keyed by tenant, window and baseline model, so a baseline change can leave two all rows, and the card would show "Not measured: 2 baseline models" for good.
  • Fix (c1ff9cf, signed): the card takes the newest as_of among window_kind all rows and refuses only a tie on it, the same rule as omnidash#364. The caption shows the shown row's own baseline.
  • Tests, written first:
    • S4: two baselines tied on as_of are refused.
    • S4b (two cases): different as_of shows the newest row's value and baseline, in either order.
    • S4c: a day row newer than the all row is ignored.
    • RED on 4517cf3: 2 failed, 19 passed (the two S4b cases). GREEN: 21/21.
  • Mutation: going back to "refuse when more than one all row" fails the same 2 tests.
  • Dev: signed merge of dev 61a76f4 as d33d431, no conflicts.
  • Suites at d33d431: full vitest 2025 passed, 15 skipped, 0 failed. npm run check, npx tsc -p tsconfig.node.json --noEmit, lint and build all exit 0. Playwright omn20009 and contracts specs pass 20/20. pre-commit run --files passes.

Update 2026-10-06 03:43Z: dev brought in again (head 4517cf3)

Supersedes the counts in the 22:07Z update for the current head.

  • Merge: signed merge of dev bad7af9, which adds omnidash#353 (typed tenant-not-configured state). It is a fast-forward over b1064cb.
  • Conflicts: none. fix(OMN-19994): render typed tenant state across widgets #353 edits local-page-loader.ts and LocalDashboardPage.tsx in other hunks. With no tenant configured, the Run locally and Avg saving / call cards stay empty, and the page shows TenantNotConfiguredState once.
  • Screenshots: regenerated from the merged tree by the contracts and omn20009 specs.
  • Verification at 4517cf3:
    • Full vitest: 209 files passed, 2 skipped; 1986 tests passed, 15 skipped, 0 failed.
    • npm run check, npm run lint and npm run build each exit 0.
    • Playwright (contracts + omn20009): 20/20 passed.
    • pre-commit run --files exits 0.

Update 2026-10-05 22:07Z: dev brought in (head b1064cb)

The verification above is from the earlier head be39357; the counts below supersede it for the current head.

Branch head be39357, stack base 850f939 (#355's live head), dev 58b2f89. #355's head contains dev.

  1. New tests, red then green (vitest; the 21 new tests plus pages.test.ts, 89 in all): branch 89 passed. At 850f939 with only the test files laid over it: 22 failed, the 21 new tests and the updated metric-key case. At dev: 29 failed, the extra 7 being feat(OMN-20225): run rows show the served verdict, backend, host and tier; Overview gains quality and tier-mix panels #355's own tests in pages.test.ts.
  2. Planted negatives, each restored byte-identical (sha256), then 89 passed again:
    • share computed from the served counts in the browser: 2 failed;
    • saving computed as savings_usd / runs_measured in the browser: 4 failed;
    • a null share rendered as 0.0%: 1 failed;
    • an unmeasured saving rendered as $0: 2 failed;
    • Run locally bound to cost.savings-overview.v1: 3 failed.
  3. Full vitest suite: branch 1943 passed, 15 skipped, 0 failed. 850f939: 1921 passed, 1 failed, 15 skipped. Dev: 1898 passed, 1 failed, 15 skipped. The one base failure is synthetic-fixtures-hygiene.test.ts, which runs git ls-files and failed only because the base was an export with no .git; it passes on the branch. Branch minus 850f939 is +21, the new tests.
  4. Playwright, Chromium at 1440x900 (local-dashboard-contracts.spec.ts and the new spec): 19 of 19 passed on the branch; the 2 new scenarios fail at 850f939. The render shows Run locally 83.9% with its counts, and Avg saving / call "Not served yet: waits on metering-summary.v1". This run re-rendered 13 committed screenshots a few KB differently: Playwright's browsers had to be reinstalled (chromium-headless-shell build 1217). The committed bytes were kept, because 11 of the 13 belong to feat(OMN-20225): run rows show the served verdict, backend, host and tier; Overview gains quality and tier-mix panels #355's scenarios.
  5. npm run check (tsc), npm run lint and ESLint on the three new test files: all exit 0.
  6. Producer side, lakshman lane at 2026-10-05T03:00:57Z: model-routing.v1 served local_call_share 0.84 = 105/125, and metering-summary.v1 answered 404 unknown_topic, which is why the saving card waits.

Failure paths

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

Open defects

  • Avg saving / call is not bound yet. It needs metering-summary.v1 served on the lab (omnimarket#3368, then the OMN-20009 omnimarket PR deployed) and the captured catalogue recaptured, then a one-line binding in overview.contracts.yaml. Until then the card says what it waits on. The spec's matching-value criterion for this card is not done.

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 served fields themselves (omnimarket), the vendor copy (omnibase_infra) and the evidence contract (onex_change_control), all under OMN-20009. Compression and Cache hit rate belong to OMN-20226; the billing figures are out of scope.

Overlapping open PRs

Read live at 2026-10-05T03:47Z. Each was trial-merged with this head (git merge-tree), and again with #355's head to see which conflicts this PR adds.

Overlap-Reviewed: #355 — the parent of this stack (OMN-20225): it removes the hand-written model-routing fold and adds the Quality and Tier mix panels, which this PR moves down a row; this PR carries its commits until it merges, and merges clean onto it.
Overlap-Reviewed: #354 — OMN-20008 edits LocalDashboardPage.tsx, overview.contracts.yaml and the Overview screenshots for savings provenance. It conflicts with #355's head already, in the same files (LocalDashboardPage.tsx, LocalDashboardPage.runs.test.tsx, 9 screenshots); this PR adds no new conflicting file. Both are needed, and the spec orders this PR after it.
Overlap-Reviewed: #358 — omnidash#358 (OMN-20006) rewrote the OMN-19980 guard src/pages/local/omn19980.savings-source.test.ts (2026-10-06, head 2246648). Its R4 now follows every in-repo import from the page and allows a savings read only as a named entry (file, owner, fields, component). Against this PR's head f65ea8b that guard fails 4 tests: this PR's SavingsPerRunCard read of savings_per_measured_run_usd and the <SavingsPerRunCard> tag. A trial merge (git merge-tree of f65ea8b and 2246648) conflicts only in that file; LocalDashboardPage.tsx and pages.test.ts merge cleanly, and #358 no longer adds a PENDING_SOURCES entry. A reconciled guard is drafted and run on the merged tree: 38 of 38. It models this PR's read as a pending entry while overview-avg-saving-per-call binds nothing, and becomes a bound entry once omnimarket#3406 serves the field. It keeps this PR's JSX-tag exemption. Full vitest on the merged tree: 2107 passed, plus 1 failure from a git-tracked-files test that cannot pass in an export. Whichever PR merges second carries it.
Overlap-Reviewed: #353 — OMN-19994 shares only .github/workflows/call-receipt-gate.yml, which comes from #355's commits, not this PR's. Merges clean.
Overlap-Reviewed: #350 — OMN-20375 repins the receipt gate in .github/workflows/call-receipt-gate.yml; the conflict is with #355's change to that file, present at #355's head; this PR does not touch it.

Evidence-Ticket: OMN-20009
Evidence-Source: OCC#12863

…tier; Overview gains quality and tier-mix panels

Last run, Recent runs, Runs and the Workflow routing step read backend_id and
host from delegation.decisions.v1 (OMN-20162, omnimarket#3343, migration 0054)
instead of the hard-coded "Not served (OMN-20162)"; a missing backend or host
is the typed "Not recorded", never a blank. The tier shows its served cost
regime beside it, and Last run gains its quality score.

Overview adds a quality panel bound to delegation.quality-gate.v1 and a tier-mix
panel bound to delegation.model-routing.v1 by_tier. Both render served fields
only; nothing is summed, divided or rounded in the browser.

The direct-Postgres reader's own model-routing and quality-gate folds are
removed: a second, drifted copy of omnimarket's views (no by_tier, escalations
hard-coded to 0, a 0 pass rate with no runs). The projection API serves both
topics; direct-Postgres mode now answers them with no rows. This keeps the
OMN-19981 AC2 page test green with the panels bound.

Onex-Lane: omn20225-run-backend
Onex-Session: 7366ac205743487999083c1c8fcb35cb
…ier and the two Overview panels

Fixtures serve OMN-20162's backend_id and host on one run and leave the other
without them, plus the quality-gate and model-routing rows. Overview and Runs show
the served values and a typed Not recorded; the quality and tier-mix panels show
served fields only and sit below Recent runs, which still starts above the fold
(OMN-19981 AC4). The scroll test now counts Recent runs' own rows, since the
tier-mix panel is a second table on Overview. Screenshots regenerated.

Onex-Lane: omn20225-run-backend
Onex-Session: 7366ac205743487999083c1c8fcb35cb
…ert its tier and score

From the independent review: the Workflow run-path binding now requests the
fields WorkflowPath reads, its routing and quality-gate steps are asserted, and
the e2e Not recorded check reads the unmeasured run's Backend and Host cells.

Onex-Lane: omn20225-run-backend
Onex-Session: 7366ac205743487999083c1c8fcb35cb
scripts/check-hardcoded-delegation-refs.sh refuses private LAN addresses in src/,
and the new view tests used the lab hosts as fixture values. They now use
gpu-host-a and gpu-host-b; what the tests prove is unchanged.

Onex-Lane: omn20225-run-backend
Onex-Session: 7366ac205743487999083c1c8fcb35cb
check-no-env-contamination.sh and hardcoded-model-config (E-LAN in TEST) refuse
the lab subnet in tests/; the e2e fixture now uses gpu-host-b. Screenshots
regenerated; 17 of 17 Chromium scenarios pass.

Onex-Lane: omn20225-run-backend
Onex-Session: 7366ac205743487999083c1c8fcb35cb
…lidator accepts artifact_sha256

verify / verify on this PR fails with extra_forbidden on artifact_sha256: the
caller's workflow (1e69bec909f2) hard-pins its nested validator to d35ae63d3687,
whose ModelDodReceipt forbids that field, and the merged receipts carry it. The
caller moves to fad21c40911b (omnibase_core#1853), the same one-line move made on
omnidash#353 (OMN-19994, with its regression test) and proposed in omnidash#350.
Only this caller line changes.

Onex-Lane: receipt-gate-pin-OMN-20225
Onex-Session: d99c149d-35af-4f1e-b791-28a28187bfc4
…ng per call

Two Overview cards, each from one served field, with no arithmetic in the
browser. Run locally shows delegation.model-routing.v1's local_call_share
(local runs over all runs) as a percent, with the served local, total and
not-tier-routed counts under it, so the gap to the tier-routed share stays
visible. Avg saving / call shows metering-summary.v1's
savings_per_measured_run_usd as served decimal text from the all-time row.

The lab catalogue does not serve metering-summary.v1 yet, and the page
contract test refuses a binding to an exposure it does not mark ok, so the
card stays unbound and says "Not served yet: waits on metering-summary.v1".
Its contract test binds it to that exposure, and nothing else, as soon as
the captured catalogue serves it. A row with no saving on that exposure
also no longer switches the whole page to Baseline unresolved.

Absent rows, absent fields and null values each render a typed reason,
never 0, 0% or $0. Stacked on omnidash#355 (OMN-20225), which removes the
reader's hand-written model-routing fold this card must not read.

Vitest: 21 new tests RED 21/21 before the change, GREEN after; six
mutations (share from counts, null share as 0, saving from savings over
runs, null saving as 0, Run locally bound to savings-overview, metering
rows switching the page state) each turn them red. Full suite 1943 passed
on the branch against 1922 at 850f939 and 1899 at dev 58b2f89. Chromium
1440x900: 19 of 19 local-dashboard scenarios pass, and the 2 new ones fail
at 850f939.

Onex-Lane: omn20009-overview
Onex-Session: d99c149d-35af-4f1e-b791-28a28187bfc4
The repo pre-commit ESLint hook covers tests/, which npm run lint does not,
and it refused the destructured-but-unused routing fixture. The spec now
filters the served map instead; both scenarios still pass at 1440x900.

Onex-Lane: omn20009-overview
Onex-Session: d99c149d-35af-4f1e-b791-28a28187bfc4
@onexbot-occ-writer

Copy link
Copy Markdown
Contributor

OCC autobind did not mint a companion for this PR: no changed-file candidate could be proven RED against the merge base, and emitting a PR-existence probe instead would be non-falsifiable evidence (OMN-15247). Hand-authored evidence is required.

Considered 30 changed file(s):

  • .github/workflows/call-receipt-gate.yml: no candidate grammar reads this file type (only Python declarations, uv.lock lines, release-artefact lines and contract-pin lines are proposed)
  • server/__tests__/postgres-projection-reader.test.ts: no candidate grammar reads this file type (only Python declarations, uv.lock lines, release-artefact lines and contract-pin lines are proposed)
  • server/postgres-projection-reader.ts: no candidate grammar reads this file type (only Python declarations, uv.lock lines, release-artefact lines and contract-pin lines are proposed)
  • src/layout/local-page-loader.ts: no candidate grammar reads this file type (only Python declarations, uv.lock lines, release-artefact lines and contract-pin lines are proposed)
  • src/pages/LocalDashboardPage.backend.test.tsx: no candidate grammar reads this file type (only Python declarations, uv.lock lines, release-artefact lines and contract-pin lines are proposed)
  • src/pages/LocalDashboardPage.omn20009.test.tsx: no candidate grammar reads this file type (only Python declarations, uv.lock lines, release-artefact lines and contract-pin lines are proposed)
  • src/pages/LocalDashboardPage.runs.test.tsx: no candidate grammar reads this file type (only Python declarations, uv.lock lines, release-artefact lines and contract-pin lines are proposed)
  • src/pages/LocalDashboardPage.tsx: no candidate grammar reads this file type (only Python declarations, uv.lock lines, release-artefact lines and contract-pin lines are proposed)
  • src/pages/local/omn20009.contracts.test.ts: no candidate grammar reads this file type (only Python declarations, uv.lock lines, release-artefact lines and contract-pin lines are proposed)
  • src/pages/local/overview.contracts.yaml: no candidate grammar reads this file type (only Python declarations, uv.lock lines, release-artefact lines and contract-pin lines are proposed)
  • src/pages/local/overview.page.yaml: no candidate grammar reads this file type (only Python declarations, uv.lock lines, release-artefact lines and contract-pin lines are proposed)
  • src/pages/local/pages.test.ts: no candidate grammar reads this file type (only Python declarations, uv.lock lines, release-artefact lines and contract-pin lines are proposed)
  • src/pages/local/runs.contracts.yaml: no candidate grammar reads this file type (only Python declarations, uv.lock lines, release-artefact lines and contract-pin lines are proposed)
  • src/pages/local/runs.page.yaml: no candidate grammar reads this file type (only Python declarations, uv.lock lines, release-artefact lines and contract-pin lines are proposed)
  • src/pages/local/workflow.contracts.yaml: no candidate grammar reads this file type (only Python declarations, uv.lock lines, release-artefact lines and contract-pin lines are proposed)
  • tests/e2e/local-dashboard-contracts.spec.ts: no candidate grammar reads this file type (only Python declarations, uv.lock lines, release-artefact lines and contract-pin lines are proposed)
  • tests/e2e/local-dashboard-omn20009.spec.ts: no candidate grammar reads this file type (only Python declarations, uv.lock lines, release-artefact lines and contract-pin lines are proposed)
  • tests/e2e/screenshots/local-dashboard-controls-dark-1440x900.png: no candidate grammar reads this file type (only Python declarations, uv.lock lines, release-artefact lines and contract-pin lines are proposed)
  • tests/e2e/screenshots/local-dashboard-controls-light-1440x900.png: no candidate grammar reads this file type (only Python declarations, uv.lock lines, release-artefact lines and contract-pin lines are proposed)
  • tests/e2e/screenshots/local-dashboard-navigation-dark-1440x900.png: no candidate grammar reads this file type (only Python declarations, uv.lock lines, release-artefact lines and contract-pin lines are proposed)
  • tests/e2e/screenshots/local-dashboard-navigation-light-1440x900.png: no candidate grammar reads this file type (only Python declarations, uv.lock lines, release-artefact lines and contract-pin lines are proposed)
  • tests/e2e/screenshots/local-dashboard-omn20009-overview-1440x900.png: no candidate grammar reads this file type (only Python declarations, uv.lock lines, release-artefact lines and contract-pin lines are proposed)
  • tests/e2e/screenshots/local-dashboard-omn20009-overview-unserved-1440x900.png: no candidate grammar reads this file type (only Python declarations, uv.lock lines, release-artefact lines and contract-pin lines are proposed)
  • tests/e2e/screenshots/local-dashboard-overview-1440x900.png: no candidate grammar reads this file type (only Python declarations, uv.lock lines, release-artefact lines and contract-pin lines are proposed)
  • tests/e2e/screenshots/local-dashboard-overview-dark-1440x900.png: no candidate grammar reads this file type (only Python declarations, uv.lock lines, release-artefact lines and contract-pin lines are proposed)
  • tests/e2e/screenshots/local-dashboard-overview-light-1440x900.png: no candidate grammar reads this file type (only Python declarations, uv.lock lines, release-artefact lines and contract-pin lines are proposed)
  • tests/e2e/screenshots/local-dashboard-overview-panels-1440x900.png: no candidate grammar reads this file type (only Python declarations, uv.lock lines, release-artefact lines and contract-pin lines are proposed)
  • tests/e2e/screenshots/local-dashboard-runs-1440x900.png: no candidate grammar reads this file type (only Python declarations, uv.lock lines, release-artefact lines and contract-pin lines are proposed)
  • tests/e2e/screenshots/local-dashboard-runs-backend-1440x900.png: no candidate grammar reads this file type (only Python declarations, uv.lock lines, release-artefact lines and contract-pin lines are proposed)
  • tests/e2e/screenshots/local-dashboard-runs-scroll-1440x900.png: no candidate grammar reads this file type (only Python declarations, uv.lock lines, release-artefact lines and contract-pin lines are proposed)

This verdict is occ-autobind's, derived from this head and its merge base alone. A second producer, the occ-companion-effect backstop, consumes its own command on the lab bus and may still mint a companion under the generic binding. Whether it does depends on that consumer running, not on this diff (OMN-18876).

Onex-Lane: omn20009-served-exposures
Onex-Session: e72e9157-677b-4b13-bd77-daa2be198c16
#355 landed on dev as a squash while this branch still carried its
unsquashed commits, so the two Overview files and two of #355's
screenshots conflicted.

overview.contracts.yaml and overview.page.yaml: kept this branch's
Run locally and Avg saving / call cards in row 8 with Quality and Tier
mix moved to row 10. Outside those hunks dev and the branch agree, so
the Basis column from #354 and #355's backend, host and tier columns
and panels stay as dev has them.

Screenshots: every 1440x900 screenshot written by the contracts and
OMN-20009 E2E specs was re-rendered from the merged tree (Chromium
headless shell 1217), not picked from either side, because the Overview
layout differs from both parents.

Onex-Lane: omn20009-served-exposures
Onex-Session: e72e9157-677b-4b13-bd77-daa2be198c16
Patel230 added a commit that referenced this pull request Oct 5, 2026
…9980

This branch was cut from #359 before #354's per-run Basis landed, so the
run tables and the Overview run bindings conflicted with this PR's
removal of per-run savings.

Resolution: per-run savings stay removed (no Savings, Baseline cost or
savings_usd binding), and #354's Basis column stays. On Runs, the
Usage source column becomes #354's Basis and reads usage_source only,
without falling back to savings_method, which #354's provenance spec
forbids. B2 in LocalDashboardPage.omn19980.test.tsx now expects the
Basis header for that reason; it was red on the merge until then.

Screenshots written by the contracts and OMN-20009 E2E specs were
re-rendered from the merged tree rather than picked from either side.
#353 (OMN-19994) adds a tenant-not-configured failure kind to the local
page loader and a typed TenantNotConfiguredState in WidgetReadState.
It touched local-page-loader.ts and LocalDashboardPage.tsx in hunks
apart from this branch's, so git merged them without conflicts.

The two compose: a tenant-scoped binding with no tenant fails with the
new kind, bindingFailedEmpty() then keeps the Run locally and Avg
saving / call cards empty, and WidgetReadState shows the typed state
once. The metering-summary filter in resolveLocalPageEmptyState and
the Basis, Host, Backend and tier columns from #354 and #355 stay as
they were.

Screenshots: every 1440x900 screenshot written by the contracts and
OMN-20009 E2E specs was re-rendered from the merged tree, not picked
from either side.

Onex-Lane: omn20009-served-exposures
Onex-Session: 369fcbc6-265e-4c01-a677-b457ad3d09b2
Patel230 added a commit that referenced this pull request Oct 6, 2026
…MN-19980

#359's branch took dev's #353 (OMN-19994): a tenant-not-configured
failure kind in the local page loader and a typed
TenantNotConfiguredState in WidgetReadState. Those hunks sit apart from
this branch's, so local-page-loader.ts and LocalDashboardPage.tsx
merged without conflicts. The per-run savings stay removed from Runs
and Overview, and #354's Basis column still reads usage_source only.

Only the 1440x900 screenshots conflicted (binary). Every one written by
the contracts and OMN-20009 E2E specs was re-rendered from the merged
tree, not picked from either side.
…ses only a tie

metering_summary is keyed by tenant/window/baseline_model, so a baseline change
leaves the previous and current all rows together. Select the newest as_of and
refuse only rows tied on it, the rule omnidash#364's metering cards use.
S4 now ties on as_of; S4b (different as_of) and S4c (newer day row) added.

Onex-Lane: omn20009-served-exposures
Onex-Session: e72e9157-677b-4b13-bd77-daa2be198c16
Patel230 added a commit that referenced this pull request Oct 6, 2026
…tate

Step B binds Overview's four headline cards to metering-summary.v1, so
its rows reached resolveLocalPageEmptyState. A store with a metering row
but no decisions then lost its NO_RUNS_YET page, and a metering row
carrying baseline_state BASELINE_UNRESOLVED switched the whole Overview
to Baseline unresolved, when each card already says why its own figure
is missing. Components whose rows come from metering-summary.v1 are now
left out of that decision.

This is the same filter OMN-20009 (omnidash#359) adds as its C3, line for
line, so when #359 is brought onto dev the conflict is trivial.
Patel230 added a commit that referenced this pull request Oct 6, 2026
…ings stays on the one definition (#365)

* feat(OMN-19980): guard savings to metering-summary.v1 and drop per-run savings

The ruling for AC2 is "window totals only": every savings figure on the six
local pages comes from metering-summary.v1. Merged dev broke that in four
bindings, and no test could catch it. That is why the closer rejected AC2.

Step A of the approved Amendment 1:

- omn19980.savings-source.test.ts is the T2.4 page test that AC2's
  falsifier names. It has four rules, each with planted controls: R1 checks
  bindings, R2 widgets, R3 per-run figures, and R4 scans the page and loader
  source. It also has a field classifier, instrument checks, and cost
  false-positive controls. The one total still on cost.savings-overview.v1
  (overview-savings-snapshot) has a time-boxed allowlist entry, asserted by
  name. Step B empties it, so AC2 stays open until then.
- Overview last run, Overview recent runs and the Runs lookup no longer ask
  delegation.savings.v1 for savings_usd, counterfactual_baseline_usd or
  cloud_cost_usd. The Savings and Baseline cost columns and the savingsOf
  helper are gone. Each run keeps its local_cost_usd. Runs keeps the
  baseline_model, usage_source and savings_method labels, as ruled.
- The loader no longer reads savings_usd to decide the page state. Only the
  served baseline_state does.
- A metric card with no served row reads Not measured instead of Baseline
  unresolved, because nothing was priced. A savings card with no served
  source still names metering-summary.v1.

The old-shape assertions in pages.test.ts, the LocalDashboardPage
overview/runs/RunsTable tests and the local-dashboard e2e spec are updated.
The Overview and Runs screenshots are regenerated, since their columns
changed.

(cherry picked from commit a163a49)

* feat(OMN-19980): Overview Savings total reads metering-summary.v1's all-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.

(cherry picked from commit 10c3450)

* feat(OMN-19980): Overview Spend, Measured runs and Tokens read the same 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.

(cherry picked from commit 1a6159b)

* test(OMN-19980): Avg saving / call binds only once the field is served, 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.

On dev only the catalogue half is carried: omn20009.contracts.test.ts and
the Avg saving / call card belong to OMN-20009 (omnidash#359), which is not
on dev, so its C2 test change has nothing to apply to.

(cherry picked from commit f9d10bf)

* test(OMN-19980): point the savings guards at dev's Overview, without OMN-20009's cards

The OMN-19980 commits were written on top of OMN-20009 (omnidash#359), which
is still waiting on the omnibase_infra release, and they leaned on three of
its pieces that dev does not have. Carrying them onto dev on their own needs
these adjustments, and nothing else changes in behaviour:

- local-page-loader exports METERING_SUMMARY_TOPIC. Step B's renderer
  dispatches the four headline cards on it; on the stack the constant came
  from #359. The C3 empty-state filter #359 added next to it stays with
  OMN-20009.
- C2 to C5 exercised #359's Avg saving / call card through a mocked
  binding. On dev the Savings card is the one bound to metering-summary.v1,
  so they now exercise it, and the loader mock is gone.
- R4 allowed SavingsPerRunCard, which dev does not have; on dev it allows
  no owner, its positive control plants a read in each real source file,
  and the planted case now expects SavingsPerRunCard to be caught too.
- With Tokens bound, no Overview card is left unbound on dev, so the
  unbound-savings population is asserted empty (its planted case remains
  the control) and the SV-3 pending-state test uses an unbound card that
  falls back to "its exposure".

* test(OMN-19980): re-render the Overview and Runs screenshots on dev

The cherry-picked commits kept dev's screenshots, because theirs were
rendered on the OMN-20009 stack (Avg saving / call and Run locally cards).
These come from local-dashboard-contracts on this branch: Overview's
headline cards now read metering-summary.v1, and Runs drops the per-run
savings columns and says "with its cost". Only Overview and Runs pages
are here; the other screenshot is unchanged.

* test(OMN-19980): R4 catches a camelCase or PascalCase savings read

Codex's review of omnidash#361 found that R4 only matched lower_snake
tokens, so row.savingsUsd, row['SavingsUsd'] or a made-up savingsPerRunUsd
in the page or loader code walked past the guard while the suite read
clean. R4 now folds camelCase and PascalCase identifiers and string keys
to lower snake before classing them, so every alias of every savings
field is caught, as a property or as a bracket key.

What it still leaves out is on purpose and tested: cost names in any case,
SCREAMING_SNAKE constants (DELEGATION_SAVINGS_TOPIC), single words and
prose, and the name a function, class, interface or type declares, which
names code and reads nothing. The two real source files give no finding.

* fix(OMN-19980): metering-summary rows never decide the page's empty state

Step B binds Overview's four headline cards to metering-summary.v1, so
its rows reached resolveLocalPageEmptyState. A store with a metering row
but no decisions then lost its NO_RUNS_YET page, and a metering row
carrying baseline_state BASELINE_UNRESOLVED switched the whole Overview
to Baseline unresolved, when each card already says why its own figure
is missing. Components whose rows come from metering-summary.v1 are now
left out of that decision.

This is the same filter OMN-20009 (omnidash#359) adds as its C3, line for
line, so when #359 is brought onto dev the conflict is trivial.
Keeps both behaviours: the Run locally card and the Avg saving / call card
(SavingsPerRunCard, newest as_of rule) beside the four metering headline cards.

- local-page-loader.ts: one METERING_SUMMARY_TOPIC export, one metering
  empty-state filter (the C3 copy and #365's are the same line).
- LocalDashboardPage.tsx: one pickAllTimeRow helper shared by MeteringTotalCard and
  SavingsPerRunCard; PENDING_SOURCES keeps only the Avg saving entry.
- savings-source guard R4: SavingsPerRunCard may read savings_per_measured_run_usd and
  nothing else; planted controls prove another component, a look-alike name and a
  second field in the same card still fail; JSX tag names are not reads.
- C2 binds the card only once the catalogue lists the column, not the topic.
- Screenshots regenerated from the merged page.

Onex-Lane: omn20009-served-exposures
Onex-Session: e72e9157-677b-4b13-bd77-daa2be198c16
@jake-b-omni
jake-b-omni merged commit e4f5ebb into dev Oct 6, 2026
43 checks passed
@jake-b-omni
jake-b-omni deleted the lakshman/OMN-20009-overview-served-exposures branch October 6, 2026 23:43
Patel230 added a commit that referenced this pull request Oct 7, 2026
omnidash#359 landed first and added one R4 savings-read exception for the
Avg saving / call card, as an owner-name map. This branch had replaced the
R4 allowlist with typed allowances bound to a file, owner, field set and
component, each checked against that component's metering-summary.v1
binding. The two collided in omn19980.savings-source.test.ts.

The merge keeps the typed allowances and gives each a state. #359's card
reads savings_per_measured_run_usd before any exposure serves it, so its
component binds nothing; it enters as 'pending', which is valid only while
that component has no binding and fails, naming the fix, the moment one
appears. Both name exemptions are kept: topic-key properties from this
branch and JSX tag names from #359. #359's own planted test now passes its
allowance explicitly, since allowances are scoped to a file.

Mutation: dropping the pending check turns the new planted test red.
Onex-Lane: omn20006-usage-page
Onex-Session: d99c149d-35af-4f1e-b791-28a28187bfc4
@Patel230

Patel230 commented Oct 7, 2026

Copy link
Copy Markdown
Contributor Author

Close-out for OMN-20009: merged 2026-10-06 as e4f5ebbcd5.

  • Landed: the Overview shows the run-locally share and the saving per call from served exposures, and a typed reason instead of 0 when unmeasured.
  • Left: the two exposures it reads are served by omnimarket#3406, which is green and in review.

Thank you!

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.

2 participants