Repository navigation
feat(OMN-21015): the Runs list shows each run's modelled saving against its named baseline - #411
Merged
Merged
Conversation
Requirement RU-1 lists backend among the Runs filters, and the page had status, cause, model and window only. The backend is the backend_id that delegation.decisions.v1 already serves and the table already shows, so this is a filter over a served field and nothing is derived. A run served without a backend is filtered as Not recorded, the same word its cell shows, so it is never matched under another backend and never unreachable. The filter is carried in the address beside the other four, and an address naming a backend the data no longer has says so in the select instead of showing an empty choice.
…UTC) and all time Requirement OV-3 asks for the headline figures in a selectable window, and the page read the all-time row only. The fold already serves one row per UTC day beside the all-time row, so "today" is the day row whose window_start is today's UTC date: a row the runtime served, never a sum made in the browser. The page keeps the chosen window and hands it to every card that reads metering-summary.v1, so the cards cannot show different windows side by side. A UTC day with no runs has no row; the cards say so instead of showing 0 or another day's figures. The baseline rule the all-time row already followed (the row refreshed last wins, a tie is refused by name) applies to a day's rows in the same way.
… UTC day The demo shows spend over time against its baseline; the local Overview had headline totals only. metering-summary.v1 already serves one row per UTC day with the actual and the baseline spend, so the panel is a new declared table over rows that are already there. The panel renders the columns the page declares and nothing else: the page code names no savings field, the money columns are written as served, and the last line of each table is the served all-time row, the same row the headline cards read. It adds nothing up. A figure that was not measured says so, a day under an unresolved baseline shows no baseline figure, and after a baseline change each baseline keeps its own table. The test's two-day fixture is a real record: one install's three stored metering rows, read on 2026-10-13, without its tenant id.
…acked by model Requirement US-1 asks for tokens per day as a stacked series split by model; the page had the per-model-per-day table only. The rows are the ones usage-by-model-day.v1 already serves. Each segment is one served row's count and grows by that count, so the stacking is done by the layout: nothing is added up in the browser and no total is written. Tokens in and tokens out are separate bars and never one figure. A model whose count was not recorded is named under its bar rather than drawn as zero, and a served zero is drawn as a zero. The series has its own empty-state sentence. With the table's sentence it would have said the same thing twice on the Usage page, which an existing test of that page rightly reads as one statement.
…d order usage-by-model-day.v1 serves the newest day first, then models ascending. The fixture had the oldest day first while its comment called that the served order, and the one order test would have passed a component that sorted days newest first. The fixture now matches the exposure, and the order test is two: rows in the served order keep it, and rows served oldest first are rendered oldest first. A re-sort in either direction now fails exactly one of them.
On the Overview's Spend per day table a missing cell read "Baseline unresolved" whenever the row's baseline was unresolved, including actual spend and the run count, which are measured from the runs and do not depend on a baseline. The metering fold serves exactly that row when no run was measured and no baseline resolves. The cell now says "Not measured" for the run-measured figures and keeps "Baseline unresolved" for the rest on an unresolved row. The page names the run-measured fields only, so the savings-source guard's rule R4 still holds. A test pins the grouping to the captured lab catalogue's column list, so a column served later has to be placed before the suite passes again. Found in review of the previous head.
… count Each segment of the Tokens per day bars carried its label, padding and a border and grew from its content width, so the drawn widths did not follow the counts: in the review's Chromium fixture 1 token beside 1,000 drew at a ratio of 0.129 instead of 0.001. The drawn segments are now empty and grow from a zero basis, so a segment's width is its count's share of the bar and nothing else. Every figure is written in a list beside the bar, where a very small count still reads; the bar itself is hidden from assistive technology. The unit tests assert the zero basis, the empty segment and a stylesheet rule with no padding, border, margin or width. jsdom lays nothing out, so a Playwright spec measures the widths in Chromium on the Usage page: 1 against 1,000 and 300 against 1,200, each within 0.0005 of the counts' ratio. With padding and a border put back on the segment both tests fail (the spec reads 0.0153 for 1 against 1,000). Found in review of the previous head.
…ackend filter The existing browser run fails because the all-time Savings figure is now shown twice and Backend adds a fifth select. Scope the Refresh assertion to its headline card and retain the themed-control checks for every select. Lab browser baseline: 18 pass, 2 fail; corrected: 20 pass with no skips or flaky results.
After dev's OMN-20008 percentage line merged into this branch, the Savings tile reads its figure and its percentage line from the row picked for the chosen window. Nothing pinned that: the OMN-20008 tests run on the default window only. Five tests, on the card and on the Overview page: today shows today's served ratio beside today's saving; all time shows the all-time ratio; a today row with no ratio shows no line and never the all-time one; a day with no row says not measured and shows no line; on the page the switch moves the figure and its line together and all time brings both back. Each planted fault is caught and the file restored after it: the line always read from the all-time row (3 fail); the line read from a day row (3 fail); a null ratio falling back to all time (1 fails); a missing day shown as all time (1 fails). The comment on that line in MeteringTotalCard said the same all-time row; it now says the same row as the figure. No behaviour change.
…today is chosen Seen on a lab store of two real days (2026-10-14, omnimarket dev 07fec7b8): with Figures for set to Today (UTC) the metering tiles showed today's 4 runs while Run locally read 6 of 6 runs local and Tier mix read 6, with nothing saying those two were all time. Only the panels bound to metering-summary.v1 have a row per window; the decisions, model-routing and quality-gate exposures serve no day rows. With today chosen, each Overview panel not bound to metering-summary.v1 now carries one line: All time: this panel does not follow the window above. Nothing is shown when all time is chosen, so the all-time page is unchanged, and no figure moves. Two tests, written first. Q6 failed before the change (no note on Run locally) and passes after it. Q7 holds the other side: no note on a panel that follows the window, and none after all time is chosen again. Planted faults, each caught and the file restored: the note on every panel (Q7 fails), the note whatever the window (Q7 fails), the note never shown (Q6 fails). My first version of Q6 listed Compression and Cache hit rate as outside the window. They are bound to metering-summary.v1 and follow it; the lists in the test now follow overview.contracts.yaml.
…the served modelled saving Jonah's rulings on OMN-21015: 2026-10-13T06:59Z "serve one per-run savings figure from the metering compute on delegation.decisions.v1 ... and amend R3 to allow exactly that one served field", and 2026-10-14T04:50Z "amending R3 does mean the same one-field exception in R1, R3 and R4 with its labels classed as labels". - PER_RUN_SAVING names the exception: modelled_savings_usd, from delegation.decisions.v1, in the runs-decisions binding of recent-runs. R1, R2 and R3 skip exactly that; R4's allowance check accepts an allowance whose field that binding requires. The R4 allowance itself lands with the page code that reads the field. - modelled_savings_usd is classed as a savings figure; its labels savings_baseline_model, savings_pricing_manifest_version and savings_measurement are classed as labels. - Planted controls: the exact exception passes; another savings field on the same binding, the same field in another component, the same field from another exposure, and an R4 allowance for another field are each still refused. Guard tests 46 passed. Planted faults: the exception switched off fails the exact-exception test; the exception widened to any field fails the same-binding control.
…st its named baseline AC3. A new "Modelled saving" column beside Local cost reads the run's own served figure on delegation.decisions.v1 (modelled_savings_usd, with its labels savings_baseline_model and savings_measurement) and computes nothing: - a measured run: the figure, exact digits, "vs <baseline model>"; - tokens not recorded: "Not measured: tokens not recorded"; - cost not recorded: "Not measured: cost not recorded"; - no figure and no state: "Not measured", never 0; a measured zero is $0.00. The runs-decisions binding requires the three fields, the Runs page declares the column, and the guard's R4 allowance names the one reader, runModelledCell (the PER_RUN_SAVING exception of the previous commit). Tests first: 7 new page tests failed before the column existed and pass now. Planted faults, each caught: an unstamped run shown as $0.00 (1 fails), the baseline left unnamed (2 fail). Three existing tests changed, each for a reason this change makes true: - the savings guard's "every bound savings figure reads metering-summary.v1" now sets aside exactly the PER_RUN_SAVING field and asserts it appears in that one binding and nowhere else; - the Runs "Baseline model label" test asserted no "Not measured" in the row, which encoded the old no-per-run-saving rule; it now asserts the new cell's typed state and still that the session's savings_usd is never shown; - the fixture-badge test's row selector /measured/ now also matched the new "Not measured" text; it is anchored to the run id. Whole suite: 233 files passed, 2 skipped; 2,275 tests passed, 15 skipped. tsc --noEmit 0; eslint --max-warnings=0 on the changed files 0. Not shown here: the served columns. They come from the omnimarket half of AC3, not yet released, so this lands after it.
…or digit Found by independent review of 1e4e947: runModelledCell passed the served decimal through Number and back, so 123456789.123456789 rendered as 123456789.12345679. It now matches the served text as a plain decimal (optional minus, digits, optional fraction), pads the fraction to two places and never converts to a binary float. A negative saving keeps its sign before the dollar; a value that is not a plain decimal (1e3) is not shown as a figure. Tests first: three new cases (precision, negative, not a decimal) failed before the fix and pass after it. Runs and local page tests 382 passed; whole suite 2,278 passed, 15 skipped; tsc 0; eslint 0.
…ced figure omnidash#397 re-captured the lab catalogue (captured_at 2026-10-14T04:30:11Z), and metering-summary.v1 now lists savings_pct_of_counterfactual. S9 failed on the merge with dev, as designed, until the new column is placed in a group. It is the ratio of the modelled saving to the baseline's spend, so it is baseline-priced: on an unresolved-baseline day it reads Baseline unresolved, which is what declaredCell already gives a column outside RUN_MEASURED_FIELDS.
…/OMN-21015-runs-savings
…/OMN-21015-runs-savings
…AC7 and AC8 omnidash#394 made repo evidence required: dod-verify on #396 refused with "pull request cites OMN-21015 but carries no contracts/OMN-21015.yaml". contracts/OMN-21015.yaml copies the four criteria from the ticket body and binds each to the page test that proves it (test_passes, run with npx vitest from the omnidash tree). It validates against omnibase_core's ModelTicketContract, and omnimarket 0.4.310's node_dod_verify (the pinned verifier), hosted audience, on a clean copy with no node_modules, returns verified: 4 verified, 0 failed, 0 skipped, no unbound criterion.
…/OMN-21015-runs-savings
Extends contracts/OMN-21015.yaml (from #396) with the criteria this change proves, each bound to its page test with test_passes. Validated against omnibase_core's ModelTicketContract; omnimarket 0.4.310's node_dod_verify, hosted audience, on a clean copy returns verified with every check verified, none failed or skipped, and no unbound criterion.
The backend serves savings_measurement baseline_unresolved (the run has tokens and cost but no baseline to price against) and unknown_timestamp (no time to pick a pricing manifest), each with no figure. The cell said a bare Not measured; it now says why, as it already did for unknown_tokens and unknown_spend, and still shows no number. R11 and R12 failed first and pass on the lab; tsc 0; the full suite passes but for the synthetic-fixtures check that needs git metadata.
…s-savings # Conflicts: # contracts/OMN-21015.yaml
Merged
1 task done
…ard under tests/ The hosted evidence check refused both AC3 and AC5 on this PR with "no test-side change at the merge base": a test colocated under src/ is not read as a test-side change, so the base control ran dev's code and dev's copy of the guard. Both bound files move to tests/ unchanged except their imports, which now use the @ alias; the contract's two commands follow.
Their relative imports pointed into src/ from the old location; from tests/ they resolve through the @ alias instead. No assertion changes.
Contributor
Author
|
Close-out for OMN-21015 AC3, the omnidash page half: merged at 2026-10-16 20:22:01Z as What landed
How it was verified
1 sources read: this PR's body, checks and merge record, the OMN-21015 thread · 2 live at 20:23Z: MERGED, omnidash dev identical to Thank you! |
1 task done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
OMN-21015 AC3: "The Runs list shows savings per run against the named baseline (requirement RU-1), and a typed not-measured state for an unmeasured run". This is the page half. The served half is omnimarket#4001, which serves
modelled_savings_usdwith its baseline and measurement state on each row ofdelegation.decisions.v1, written in one transaction by omnibase_infra#5016 (with migration 0059).Merge order: after #5016 (with 0059 applied) and #4001. Until they merge, the served rows carry no figure, and every run reads "Not measured".
What changed
src/pages/LocalDashboardPage.tsx): a "Modelled saving" column beside "Local cost". It shows each run's servedmodelled_savings_usdagainst the baseline it was priced against, for example "$0.010 vs fixture-baseline". The page formats the served figure and computes nothing.savings_measurement: tokens not recorded, cost not recorded, baseline not resolved, or time not recorded. A run served with no state reads "Not measured". A measured zero is shown as zero.src/pages/local/omn19980.savings-source.test.ts: one exception, as Jonah ruled on the ticket (2026-10-13 06:59Z, and 2026-10-14 04:50Z: "the same one-field exception in R1, R3 and R4"). It allows exactlymodelled_savings_usd, from exactly the Runs decisions binding, formatted only byrunModelledCell. Planted controls show any other savings field on that binding, or the same field anywhere else, is still refused.runs.contracts.yamlrequires the figure and its two labels on the decisions binding;runs.page.yamllays the column out.contracts/OMN-21015.yaml: binds AC3 to its page test and AC5 to the guard, beside feat(OMN-21015): one look for the six local pages, with charts and run links #399's items.How it was verified
On the lab host, at the head
2c48ef15(a signed merge of deva2fb9b6b):src/pages/LocalDashboardPage.omn21015.run-saving.test.tsx, R1 to R12: the served saving with its baseline; each of the four not-measured states; no state reads Not measured; a measured zero; exact digits; a negative sign; a non-decimal; the column's place; the page declarations.a2fb9b6b's code with this change's three test files laid over it fails all three files: 15 failed, 63 passed.tsc --noEmit: 0.server/__tests__/synthetic-fixtures-hygiene.test.ts, which runsgit ls-filesand the lab's archive copy is not a git repository. In a git copy of the same tree it passes (1 passed).Not in this change
The served figure is omnimarket#4001's and omnibase_infra#5016's. AC2 (Tier mix, omnidash#409) and AC4's by-model table land in their own PRs.
Failure paths
Open defects
None.
Local gates
--no-verify, noSKIP=, no--no-gpg-sign, nocore.hooksPathoverride. Every pre-commit and pre-push hook ran.Overlap-Reviewed: #409 — both are OMN-21015: #409 is AC2 (Overview's Tier mix,
TierMixPanel), this PR is AC3 (the Runs list's saving column,RunsTableandrunModelledCell). Different components ofLocalDashboardPage.tsx; a trial merge of the two heads auto-merges it and conflicts only incontracts/OMN-21015.yaml, where each adds its own items. Both are needed; whichever lands second merges dev and keeps both sets of items.