Repository navigation
feat(metrics): a bound widget names one series with dims: (abilityai/trinity-enterprise#730) - #3294
Conversation
…ontract (ent#730) Requirements §49.6/§49.7, the backend and frontend area files, the dashboard and custom-metrics feature flows, and the user doc (a copyable per-channel recipe, the selector rules and what each refusal means) for a dashboard.yaml widget naming one series of a dimensioned metric with dims:. Refs Abilityai/trinity-enterprise#730
…_dims The read path (a dashboard.yaml widget's dims: selector) reuses it, so a selector is valid exactly when a recorded point's dims would be. No alias, no behaviour change; its one caller is updated. Refs Abilityai/trinity-enterprise#730
A dashboard.yaml widget bound to a dimensioned metric could only show the
cross-series fold, so per-channel tiles all showed the same number with no
label. bind_dashboard_widgets now picks a source (the one series a dims:
selector names, or today's fold) and fills the widget once, so value,
freshness, colour and sparkline come from the same place on both paths.
- parse_dims_selector (public, pure, total) validates the selector with the
write path's validate_dims and maps its codes; {} and null are no selector.
- Matching is canonical_dims, exact match only.
- Three named refusals that never fall back to the fold:
metric_dimension_invalid, metric_dimension_undeclared,
metric_series_not_found (facts in binding_detail, never 'does not exist').
- Every bound widget carries bound_series facts saying what its number is.
- _latest_entry, fold, _chart and the route are untouched.
Refs Abilityai/trinity-enterprise#730
…declared direction
_threshold_verdict is now the one threshold rule; _threshold_color is a
two-line mapping of it, so a tile's colour and its verdict cannot disagree.
A successful bind writes threshold_verdict ({level: critical|warning|ok,
threshold}) whenever the metric is judgeable (not status, up_good/down_good,
at least one threshold) and the value is a number, so the field's presence
never changes between polls. Every successful bind writes the registry's
direction (neutral when none is declared). Refusals drop both.
Refs Abilityai/trinity-enterprise#730
… value on a bound tile The user doc recommends a value: 0 placeholder for agents on an older base image. The metric_store_unavailable arm left it in place, so an outage showed a believable 0 under the refusal. The arm now drops value; the cached dashboard path re-binds through the same function and gets it too. Refs Abilityai/trinity-enterprise#730
DashboardPanel prefers an author-typed trend/trend_value over the computed history.trend, so on an unselected bound tile an author arrow could contradict the sparkline beside it (with direction-aware colours, a green arrow over a red line). Every successful bind now drops them; the selected path already did. Refs Abilityai/trinity-enterprise#730
…g the dashboard read
A YAML metric: [ad_spend] or metric: {a: b} is truthy, so the widget is
bound, and by_name.get(<list>) raised TypeError out of the per-widget loop,
which runs outside the store try: one bad line took the whole dashboard read
down. That widget now gets a named refusal (metric_name_invalid, naming the
YAML kind, never the value) and the rest of the dashboard renders.
Refs Abilityai/trinity-enterprise#730
…ter and hints BoundMetricMark captions every bound tile whose number needs qualifying, from the backend's bound_series facts: channel=meta for a selected series, 'sum of 3 channel values' (with '· N stale') for a fold, 'newest of 3: channel=meta' for a last metric over several series. A metric_series_not_found refusal is a neutral footer row (chip, selector, 'no recent data') with a deterministic hint built from binding_detail; every dims: refusal links the docs. The copy lives in metricFormat.js (formatDims, boundSeriesNote, refusalHint), and chartBasisNote now shares formatDims (multi-key labels join with ', '). Refs Abilityai/trinity-enterprise#730
…ollow the declared direction A bound metric tile never rendered its threshold colour, so a breaching channel showed no red. BoundMetricMark now renders the backend's threshold_verdict as a BaseBadge (Critical / Warning, the threshold in its title) on bound metric tiles only; on ok the badge's footprint is reserved invisibly, so a poll crossing a threshold swaps it in place. DashboardPanel colours a bound widget's trend arrow and sparkline through metricFormat.trendClasses / sparklineColor with the widget's direction, so rising down_good spend is red and neutral is grey, matching the declared metric tile beside it. Gated on a truthy metric AND bound === true; unbound widgets keep the legacy colours. No class string added or removed in DashboardPanel.vue (raw-colour counts unchanged: 8 / 101 / 0). Refs Abilityai/trinity-enterprise#730
A dims: refusal renders only in the browser and the dashboard route is mcp: none, so an agent never saw its own selector mistakes. New SOFT static check X-009 reports them in the compatibility report, with the tile's own code and sentence: it validates each selector against the dimensions its metric declares in template.yaml through the binding's parse_dims_selector (which wraps validate_dims), and also reports a dims: with no metric: and a metric: that is not text (invalid_metric_name, now shared with the binding). It never consults points, skips undeclared/malformed metrics (D-009's) and partial selectors, fails closed with the type name only, and clips every echoed string. D-003 is untouched; a metric:+dims: widget still passes it. Catalog 91 -> 92. Refs Abilityai/trinity-enterprise#730
…c: (ent#730) The three refusal rows said only "a YAML list or mapping". The binding refuses any truthy non-text value: a number, boolean, list or mapping. Only a list or mapping used to fail the whole dashboard read; a scalar such as metric: 5 or metric: true read metric_undeclared and now reads metric_name_invalid. A falsy value stays unbound, as before. The backend.md catalog entry made the same overbroad "instead of raising" claim and is corrected the same way. Refs Abilityai/trinity-enterprise#730
…AML dates arrive as text)
… in the refusal hint
…ses and the catalog count match the code (ent#730) - metric_name_invalid drops the same full key set as the dims refusals; a resolved bind also pops binding_detail; the index row says only metric_series_not_found carries binding_detail facts. - metric_dimension_invalid also covers over-long and control-character values. - A bound neutral metric shows a grey arrow over a blue sparkline, not grey. - bound_series.dims on a non-numeric sum/avg is the newest series' dims. - backend.md states that a falsy metric: stays unbound (ROUND-3). - X-009 also skips an invalid dashboard.yaml and reports a truthy non-text metric: only; the compatibility flow states 92 checks. - The user-doc recipe shows a Critical badge on the Meta tile only.
…y into text (ent#730)
…t (ent#730) The reserved slot was an invisible "Critical" over a 76px min-width, on the claim that Critical is the widest label. In macOS system-ui "Warning" renders 77.34px, so crossing a threshold resized the badge by 1.34px; with the system font stack any fixed floor only moves that boundary. Both verdict words now share one grid cell inside the BaseBadge and only the current one is visible (the other is invisible + aria-hidden), so critical, warning and the reserved ok slot are as wide as the wider word in whatever font renders it. Measured in a live render: 77.34px in all three states in system-ui, 85.55 in Verdana, 86.14 in Courier New (where Critical is the wider word); an ok -> warning -> critical poll moves none of the tile's 24 elements. The words come from VERDICT_WORDS, derived from VERDICTS, so each is spelled once.
/review ReportBranch:
The adjacent route-level Execution coverage (Step 2.5)
Fix mutation (the two fixes bundled with the feature), reproduced locally:
Mutations were applied to the committed file and reverted with Local runs
Critical Findings (block merge)None. Informational Findings (review required)[I1] Scope: the verdict badge and direction-driven trend colours go beyond ent#730 (Confidence: 8/10) [I2] Behaviour: a successful bind now drops the author's [I3] Known limit: a selected series is looked up only inside the bounded latest-points read (Confidence: 9/10) [I4] Design system: the new not-found row reuses the hand-rolled gray pill (Confidence: 6/10) Clean Categories
Low confidence (appendix)
Summary
🤖 Generated with Claude Code |
dolho
left a comment
There was a problem hiding this comment.
Approved — /review found no blocking findings; see review comment above.
|
merge-train (2026-10-07): changed the body's |
…oth on append-only files
Four user-docs files conflicted with this train's siblings (#3294, #3297, #3299, #3303), which documented their own changes on the same lines. Each hunk keeps both sides' facts once: dev's new text, plus this sync's additions (wired-boundaries list, receipt contract, new FAQ entries, bound-widget field rules). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
merge-train (2026-10-07): merged as part of train #3329 (green). Before the squash, |
Description
A dashboard widget bound to a declared metric (
metric: ad_spend) could not name one series of a dimensioned metric. Every tile bound to the metric got the same cross-series fold, so three per-channel spend tiles all showed the same number and nothing said which number it was.A bound
metric/status/progresswidget may now carrydims: {channel: google}:bind_dashboard_widgetspicks a source (the series the selector names, or today's fold when there is no selector) and one fill block writesvalue,last_point_at,stale,freshness, colour andhistoryfrom it, so the number and the sparkline always describe the same series. Matching is bycanonical_dims(key order does not matter), exact match only.dims: {}/nullmean "no selector".metric_dimension_invalid(with a type hint, e.g. "quote it"),metric_dimension_undeclared(lists the declared keys),metric_series_not_found(a neutral grey footer with a deterministic hint built frombinding_detailfacts). The selector is validated by the write path's own leaf (metric_points_service.validate_dims, made public), throughmetric_read_service.parse_dims_selector._latest_entry's fold is untouched, so a tile withoutdims:and the objective join still show the same number (the ent#479 parity tests pass unchanged).bound_seriesfacts andBoundMetricMarkcaptions them:channel=meta,sum of 3 channel values,newest of 3: channel=meta.metrictiles. The backend sends a typedthreshold_verdict(critical/warning/ok) from the one threshold rule that also drivescolor; the tile renders it as aBaseBadgewhose footprint is reserved on every judgeable tile, so crossing a threshold on a background poll swaps the badge in place instead of shifting the layout.directionon bound tiles (risingdown_goodis red;neutralis a grey arrow over a blue sparkline). Unbound widgets are unchanged.dims:selector that can never match the dimensions its metric declares, adims:with nometric:, and ametric:that is not text, reported with the tile's own code and sentence, so an agent sees its own mistake throughget_agent_compatibility_report(the dashboard route is# mcp: none). It judges each widget as the tile receives it over the agent server's JSON response. Catalog 91 → 92.value: 0placeholder on screen (it read as a real 0), and a non-textmetric:(a YAML list or mapping) refuses that one widget (metric_name_invalid) instead of failing the whole dashboard read. A successful bind also drops an author-typedtrend/trend_valueso it cannot contradict the computed sparkline.Related Issue
Fixes abilityai/trinity-enterprise#730
dimsselector onGET /api/agents/{name}/metricsand MCPget_metricsis not in this PR (follow-up: abilityai/trinity-enterprise#828).binding_detail.window_points/series_cap). Follow-up: A bound dashboard widget's dims: selector can miss a series that reports rarely beside a busy one #3293 (a selector-specific latest-point lookup).Journey Impact
Journey Impact: extends: J12
Type of Change
Visible change for existing dashboards, with no author action: a bound tile over several series gains a caption, a breached bound
metrictile gains a verdict badge, bound trend arrows and sparklines follow the metric'sdirection(neutralis no longer green/red), and the declared-metric tiles'chart:label joins dimensions with,instead of,.Testing
I have tested this locally (targeted, see below; CI runs the full suites)
New tests added (if applicable)
All existing tests pass (targeted set)
Every new test executes the changed path
Backend: new
tests/unit/test_ent730_bound_widget_dims.py(incl. a Hypothesis property over arbitrarydimsvalues: binds or refuses by name, never raises) andtests/unit/test_ent730_dims_compat.py;test_compatibility_checks.pycatalog 91 → 92 plus ametric:+dims:widget with novaluestill passes D-003. The 22 test files that cover the touched modules (ent#477/478/479/666/729 metrics, compatibility, hardened YAML): 1099 passed on each of 3 random seeds (1, 12345, 99999);lint_sys_modules.pyandlint_root_test_placement.pyclean.Frontend:
metricFormat.spec.js(caption, hint, verdict helpers) and mounteddashboardPanelBoundWidget.spec.js(caption, not-found footer, verdict badge incl. in-place swap, direction colours);npm run test:unit280 files / 4905 tests passed;npm run buildgreen; raw-colour ratchet:DashboardPanel.vueexactly at its baseline,BoundMetricMark.vuezero non-gray.UI: rendered the real
DashboardPanelin a throwaway component harness (fixture API responses, no stack) in light and dark: caption variants, a longdimslabel wrapping at 240px, verdict badges, a live ok → warning → critical transition at 240px with nothing in the tile moving, direction colours, the not-found footer. The badge stacks both verdict words in one grid cell so Critical, Warning and the reserved ok slot are one width in any font (measured equal in the system font, Verdana and Courier New). Text contrast ≥ 4.5:1 in both themes.Mutation: the outage fix is pinned by
test_T19_an_outage_drops_the_authors_placeholder_value/test_T19_the_cached_dashboard_path_drops_it_too; the non-textmetric:fix bytest_a_non_text_metric_refuses_that_widget_only(ondeva list/mapping raisedTypeErrorout of the bind loop). Both go red with the fix reverted.Pre-existing, not fixed here
BoundMetricMarkare hand-rolled pills (ent#479); the new not-found row reuses the same chip so the two stay identical, and the new verdict is aBaseBadge.trendClassesusesstatus-*-600trend text in light mode and the sparkline colours are literal hexes (shared with the declared-metric tiles).metrictile shows-whilestatus/progressshow—; a refused or zero-pointprogresstile draws an empty bar.history/colorstill survives the outage refusal arm (only the placeholdervalueis dropped, as decided).AC5is "same series", not equal endpoints.Checklist
architecture/backend.md,architecture/frontend.md, both feature flows + index, the user doc,agent-validation-spec.md)