Skip to content

[DRAFT] LNT v5 rewrite#206

Draft
ldionne wants to merge 143 commits into
mainfrom
v5
Draft

[DRAFT] LNT v5 rewrite#206
ldionne wants to merge 143 commits into
mainfrom
v5

Conversation

@ldionne
Copy link
Copy Markdown
Member

@ldionne ldionne commented Mar 31, 2026

THIS IS A DRAFT. IT WILL BE THE TARGET OF A RFC BEFORE ANYTHING HAPPENS.

@ldionne ldionne force-pushed the v5 branch 6 times, most recently from dcece23 to 36d36eb Compare April 4, 2026 18:50
ldionne added 2 commits April 6, 2026 12:28
Assisted-by: Claude Code
Assisted-by: Claude Code
ldionne and others added 18 commits April 6, 2026 17:53
Add mount/unmount tests for all v5 UI pages that were missing coverage:
dashboard, machine-list, machine-detail, run-detail, order-detail, and
graph (mount tests added alongside existing pure function tests).

Tests follow the established pattern: mock API functions, call
page.mount(container, params), assert on rendered DOM and API call
arguments. Each test file includes afterEach cleanup to prevent
module-level controller state leakage between tests.

Coverage: 93 new page-level tests across 6 files (511 total frontend
tests, all passing).

Assisted-by: Claude Code
Switch the Compare page bar chart from linear percentage to log₂(ratio)
y-axis. This makes equal multiplicative changes visually symmetric (e.g.
2× faster and 2× slower produce equal-height bars).

Tick labels show percentage change at "nice" values (±1%, ±5%, ±50%,
etc.), auto-adapting to the visible range. On zoom, ticks recompute
dynamically via Plotly.relayout() with a guard flag to prevent infinite
loops. Noise bands are converted to log₂ space.

Assisted-by: Claude Code
The link was buried inside the test suite dropdown menu. Move it to a
standalone top-level link in the nav bar's right section for visibility.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SPA navigation links unconditionally called e.preventDefault(), blocking
Cmd+Click / Ctrl+Click from opening pages in new tabs. Add an
isModifiedClick() helper that lets modified clicks fall through to the
browser. Set real href values on all nav links (not href="#") so the
browser knows where to navigate.

Also fix the LNT brand link on the admin page: it called navigate('/')
but only the /admin route was registered, causing a 404. In admin
context, the brand now uses full-page navigation to the selected suite's
dashboard.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Graph and Compare are now "analysis tools" at /v5/graph and /v5/compare
(suite-agnostic), while browsing pages stay suite-scoped at /v5/{ts}/...
This enables cross-suite comparisons (e.g. libc++ vs libstdc++) where
each has its own test suite with incompatible order spaces.

Key changes:
- Flask: consolidated v5_global() serves /v5/admin, /v5/graph, /v5/compare
- Router: initRouter() takes context {testsuite, testsuites}, exports
  getTestsuites(); fixes testsuite derivation for agnostic context
- Nav bar: three link categories (suite-scoped SPA, analysis full-page
  with ?suite= prefill, admin)
- Graph page: suite <select> dropdown, currentSuite replaces closure ts,
  suiteGeneration counter guards async callbacks, full state reset on
  suite change
- Compare page: per-side suite selector, initSelection() replaces
  setCachedData(), per-side order/field fetching via fetchSideData(),
  per-side sample fetching (side A runs use side A's suite)
- Combobox: ComboboxContext uses getSuiteName(side)/getOrderData(side)
- Types: SideSelection.suite field, state encodes suite_a/suite_b
- Design and implementation plan docs updated

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Pinned orders were same-suite reference lines. Baselines are cross-suite
(suite, machine, order) tuples that allow comparing against any test suite.

Key changes:
- PinnedOrder → PinnedBaseline in time-series-chart.ts (label instead of
  orderValue, updated hover template)
- Graph page: baseline selector panel with cascading Suite → Machine →
  Order dropdowns, "+ Add baseline" expand/collapse UX
- Baseline data fetched independently from each baseline's suite via
  queryDataPoints(), cached per suite::machine::order::metric
- buildRefsFromCache → buildBaselinesFromData (reads from baseline cache
  instead of main trace data)
- URL encoding: baseline={suite}::{machine}::{order} (repeated param)
- Removed scaffold-based pinned order suggestions (rebuildSuggestions,
  cachedOrders, cachedSuggestions) — baseline order search uses per-suite
  API-based search

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… comments

- Promote blMachineCleanup/blOrderCleanup to module scope and clean up
  in unmount() to prevent listener leaks when navigating away with the
  baseline form open
- Call fetchAllBaselineData() in doPlot() so baselines are re-fetched
  when the metric changes (previously silently disappeared)
- Add comment documenting :: separator limitation in baseline URL encoding
- Update stale "pinned order" references in comments, test descriptions,
  and CSS
- Remove dead getOrders mock from graph tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Place the suite selector in the same flex row as the metric, filter, and
aggregation controls instead of its own full-width block.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Baseline form fields (Suite, Machine, Order) now display horizontally
  instead of stacking vertically, preventing misalignment with Machines
- Auto-add baseline when order is selected (no separate Add button needed)
- Removed blSelectedTag (unused) and blAddBtn (no longer needed)
- Normalize agg-select padding/font-size to match other controls, fixing
  vertical label alignment in the controls row
- Add CSS for baseline form horizontal layout

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use flex-start alignment on the second controls row so Machines and
Baselines labels stay top-aligned regardless of input height differences.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…age regressions

Extract createOrderPicker from createOrderCombobox so Graph baseline and
Compare page share the same order combobox UX with machine-order filtering
and tag display. Use lazy getOrderData getter so async-fetched order data
is available when the dropdown opens (fixes empty suggestions regression).

Also fix Compare page "Select a test suite first" hint: render Machine,
Order, Runs, and Run aggregation sections unconditionally and move the
hint into the Runs panel where it belongs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The query endpoint only supported after_order/before_order range filters
with strict inequality (> and <), making it impossible to query data at
a single order. Add an order parameter for exact matching (=), mutually
exclusive with the range filters (returns 400 if combined).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Baselines were never rendering because fetchAllBaselineData used
afterOrder=X & beforeOrder=X (strict exclusive range = empty result).
Switch to the new order param for exact-match queries.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add red halo (.combobox-invalid) to machine and order comboboxes when
  no suggestions match; block acceptance via Enter/blur while invalid.
- Enforce suite → machine → order dependency chain on Compare page by
  disabling each input until its predecessor is selected.
- Require exact-match for order picker Enter/blur acceptance — partial
  substring matches are rejected.
- Machine comboboxes fetch the full machine list once on creation and
  filter locally by case-insensitive substring (no per-keystroke API
  calls). Input is disabled with "Select a suite first" when no suite
  is selected.
- Fix arrow-key navigation: blur handlers check FocusEvent.relatedTarget
  to keep dropdown open during keyboard navigation. Add :focus style to
  dropdown items.
- Remove 20-item cap on machine suggestions (machines are always < 100).
- On Graph baseline form, onClear callback destroys the order picker
  when the machine is cleared. Clearing an order clears downstream runs.
- Run UUIDs in Compare runs panel are now links to Run Detail page.
- Metric area shows "Select a suite to load metrics..." before any suite
  is loaded.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…to /tests

Change the /query endpoint from GET to POST with a JSON body to
eliminate URL length limits when querying many tests with long names.
The test field accepts a list for disjunction queries. Unknown test
names are silently skipped. The schema uses marshmallow's unknown=RAISE
to reject unknown fields (returning 422 instead of the previous 400).

Add optional machine= and metric= query parameters to GET /tests so
clients can discover which tests have actual data for a given machine
and/or metric combination, joining through Sample -> Run with DISTINCT.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…uery

Restructure the Graph page data loading to fetch only the tests being
displayed instead of all data for a machine+metric:

- Discover matching test names server-side via GET /tests with machine,
  metric, and name_contains filters
- Fetch data only for discovered tests via POST /query with multi-value
  test in the JSON body (eliminates URL length limits)
- Cache data per (machine, metric, test) with LRU eviction at 500
  entries -- filter changes only fetch uncached tests (the delta)
- Enforce a hard cap of 50 displayed tests (replaces soft cap of 20)
- Case-sensitive test filtering (matches server-side SQL LIKE behavior)
- Baseline data fetching scoped to discovered tests only

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… Suites

Replace the three-category navbar (suite-scoped, analysis, admin) and suite
selector dropdown with a simpler flat layout where all links are suite-agnostic:

  [LNT] [Test Suites] [Graph] [Compare] [API]  <-->  [v4 UI] [Admin] [Settings]

- Remove suite selector dropdown from navbar entirely
- Remove suite-scoped links (Dashboard, Regressions, Machines) from navbar
- Add "Test Suites" link (suite-agnostic placeholder page at /v5/test-suites)
- Add "API" link (opens Swagger UI in new tab)
- Add suite-agnostic Dashboard placeholder at /v5/
- Move Admin and v4 UI to right side
- v4 UI link now points to v4 root page instead of suite-specific URL
- Extract buildNavLink() helper to unify link construction
- Pass abort signal to getApiKeys() refresh calls in admin page

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Serve a plain-text orientation document at GET /llms.txt following the
llms.txt convention (analogous to robots.txt). Helps AI agents quickly
understand LNT's domain concepts, API structure, and common workflows.

Content includes: what LNT is, key concepts (test suite, machine, order,
run, test, sample, regression, field change), endpoint listing, pagination
format, links to Swagger UI and OpenAPI spec, and common workflows.

- Registered as a plain Flask blueprint (not flask-smorest) to stay out
  of the OpenAPI spec
- Static content with Cache-Control (24h) and ETag headers
- Points to OpenAPI spec for full write endpoint documentation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extract all graph page caching into a GraphDataCache class that manages
test data (LRU), baseline data, test names, and scaffolds behind a clean
async/sync API. This fixes three bugs:

- Baselines now appear immediately when added (not only after filter change)
- Changing aggregation now re-plots traces immediately
- Text filter changes use cached test names instead of re-querying the API

Also: remove batched chart rendering (caused flicker on legend toggle),
remove dead setsEqual, deduplicate test-name discovery logic, parallelize
baseline fetches, fix doPlot/filter race condition.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ldionne and others added 2 commits April 22, 2026 16:46
…y and filter

- Make getFields() delegate to getTestSuiteInfoCached(), eliminating
  duplicate HTTP requests to /api/v5/test-suites/{ts} across 5 pages.
- Show "X tests across Y machines across Z metrics" in the indicators
  heading, with proper singular/plural and null-entity handling.
- Add a debounced text filter above the indicators table matching on
  machine, test, or metric (OR logic, case-insensitive). Heading shows
  filtered-vs-total context. Batch remove operates on visible rows only.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@ldionne ldionne changed the title [DRAFT] Agent-based rewrite of a new REST API and UI [DRAFT] LNT v5 rewrite Apr 23, 2026
ldionne and others added 27 commits April 23, 2026 07:41
- Tests endpoint: replace open question with concrete TODO to switch
  from ?name_contains/?name_prefix to unified ?search= with prefix
  matching, reusing the DB layer's list_tests().
- Tests detail: replace question with TODO to remove GET /tests/{name}
  (unused by frontend, response contains only the name).
- Commit deletion: remove TODO — current behavior is correct and
  well-tested (cascades to runs/samples, blocked by regressions,
  11 tests across 3 layers).
- Fix manage scope description to include commit deletion.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The detail endpoint returned only {"name": "..."} — information the caller
already knows. It was never used by the frontend, and the catch-all path
converter for slash-containing test names added routing complexity with no
benefit. Remove the endpoint, its tests, its llms.txt entry, and the design
doc routing caveat.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace ?name_contains (substring) and ?name_prefix with a single ?search=
parameter using prefix matching, consistent with how /machines and /commits
handle search (design doc D9). Remove the unused list_tests() DB method.

The old parameters now return 400, verified by dedicated tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Split the monolithic graph.ts (~1100 lines) and graph-data-cache.ts (~270
lines) into 8 focused modules under pages/graph/:

- state.ts: URL state encode/decode (GraphState, BaselineRef types)
- data-cache.ts: data cache + fetch with delta-fetch for baselines,
  regression caching, and clearSuite() that preserves cross-suite data
- traces.ts: pure trace-building functions (buildTraces, buildChartData,
  buildBaselinesFromData, buildRegressionOverlays, buildColorMap)
- controls.ts: control panel UI (suite, machines, metric, filter, agg)
- baselines.ts: baseline panel with cascading suite/machine/commit
- time-series-chart.ts: chart with hover sync and double-click-to-isolate
- test-selection-table.ts: moved from components/ (graph-only consumer)
- index.ts: page orchestrator wiring all modules together

Key improvements over the old implementation:
- Clear module boundaries with sub-modules that don't import from each other
- Per-machine abort controllers (removing one machine doesn't abort others)
- plotGeneration counter + selectionAbort for proper cancellation on
  metric change, machine add/remove, and rapid selection changes
- Suite change resets regression mode dropdown
- Delta-fetch for baselines (only fetches un-cached tests)
- Parallel baseline fetches (Promise.all instead of sequential loop)
- isComplete check across all machines (bug fix: was only checking first)
- regressionMode persisted in URL for shareability
- Cached colorMap and scaffoldUnion (computed once, not per-render)
- O(1) Map-based raw values lookup for hover scatter (was O(n) scan)
- Data building moved inside rAF (coalesced during progressive loading)
- Double-click chart trace to isolate test in selection table
- Always rebuild table in handleSelectionChange (fixes deselection bug)
- Baseline chips resolve display values via POST /commits/resolve so they
  survive page reload, with cross-suite support
- Baseline "+" button uses align-self: flex-start (no width stretching)
- Machine combobox always rendered (disabled placeholder when no suite)
- "Discovering tests..." progress indicator during test discovery phase

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…orderless commits

The query endpoint's cursor pagination unconditionally appended (commit, test)
as tiebreakers, which mapped to (Commit.ordinal, Test.name). This caused two
problems:

1. Every query filtered out commits without ordinals (via the ordinal IS NOT
   NULL guard), even when the caller didn't request ordinal-based sorting.
   This violated D10 which states: "When not sorting by ordinal, all runs are
   included regardless of their commit's ordinal."

2. The (ordinal, test_name) tuple is not unique when multiple runs exist for
   the same commit, so cursor pagination could lose or duplicate rows at page
   boundaries.

Fix: Replace the (commit, test) tiebreaker with Sample.id (the primary key),
which is guaranteed unique and non-null. Change the default sort (when no sort
param is provided) from (commit, test) to just (id), providing an arbitrary but
deterministic order that excludes no data. The ordinal IS NOT NULL filter now
only fires when 'commit' appears in the caller's explicit sort specification.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove RESOLVED_STATES from regression-utils.ts (never imported)
- Remove readCachedTests() from data-cache.ts (never called from
  production code; discoverTests() returns the list directly)
- Make PLOTLY_COLORS module-private in utils.ts (only used internally
  by machineColor())

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The frontend getMachines function was sending name_prefix and name_contains
query parameters, but the backend machines endpoint only accepts ?search=.
This caused a 400 error when using the Machines tab search on the Test Suites
page. Update the function, its call site, and its tests to use the correct
parameter name.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Removed (already implemented):
- Machine field search: ?search= already searches name + searchable fields
- Commit filtering by machine: ?machine= on /commits already in use

Added from frontend API call audit:
- Multi-value machine= on /tests (regression detail N+1)
- Multi-value machine= on /commits (graph scaffold)
- EXISTS instead of JOIN+DISTINCT on /tests
- Broaden page size TODO to cover all fetchAllCursorPages callers
- Expand profiles N+1 TODO to include missing ?machine= usage
- Expand regression search TODO to cover both list and compare pages,
  use ?search= for consistency

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Align cursor-paginated endpoints with the query endpoint's existing
10,000 max. This reduces round-trips for bulk consumers: a typical NTS
run with ~7,500 samples now needs 1 request instead of 15.

Changes across all layers:
- Backend: cursor_paginate() and machines offset pagination caps
- Frontend: fetchAllCursorPages and data-cache regression loop
- Schema docs, design docs (R4), llms.txt
- New test proving limits above the old 500 cap work

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…orchestrator

The Graph page rewrite left three modules with insufficient test coverage:
controls.ts (zero tests), baselines.ts (zero tests), and index.ts (13 tests
covering only structural concerns). This adds 55 new tests across those
three modules, bringing total Graph page test coverage from 144 to 199 tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…hold bug

Add a Computation Reference subsection to the Compare page design doc
with precise mathematical definitions for all derived quantities (Delta,
Delta %, Ratio, Status, Geomean, chart Y-axis, noise band). This makes
the formulas unambiguous for anyone relying on comparison results.

Fix a bug in the Delta % noise knob: change from <= to strict < so that
setting the threshold to 0 no longer misclassifies identical values as
"noise" instead of "unchanged".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…al badge

Client-side:
- Add shared matchesFilter()/isFilterValid()/updateFilterValidation()
  utilities in utils.ts with re: regex support and compiled regex caching
- Replace all 18 inline .includes()/.startsWith() filter sites with
  matchesFilter() calls
- Add .filter-invalid CSS class for invalid regex red halo
- Add .filter-regex-badge for visual "regex" badge at input right edge
- Add .filter-input-wrapper for lazy positioning context on plain inputs
- updateFilterValidation() called at all 13 filter input sites

Server-side:
- Switch all ?search= endpoints (tests, machines, commits) from
  prefix match (.like('term%')) to case-insensitive substring match
  (.ilike('%term%'))
- Fix case-sensitivity bug: API endpoints used .like() (case-sensitive)
  while DB layer used .ilike() (case-insensitive). Now both use .ilike().
- Update DB layer list_commits/list_machines to substring matching
- Update marshmallow schema metadata descriptions

Design docs updated: D9, D4, R5, endpoints.md, browsing.md,
architecture.md (text filtering convention), implementation guide.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…percentages, tooltips

The summary bar percentages now use only comparable tests (improved,
regressed, noise, unchanged) as the denominator instead of all tests.
Non-comparable categories (Only in A, Only in B, N/A) show bare counts
with no percentage. Percentages display one decimal place with trailing
.0 stripped for whole numbers. A tooltip on each percentage explains the
denominator.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Filter typing with 8k tests caused ~300-400ms freezes. Three fixes:

1. Filter-pass deduplication: pre-compute the filtered test set once
   and share it across the table, chart, summary bar, and regression
   panel (syncChartAndSummary helper). Previously matchesFilter ran 5
   independent passes over all rows per debounce tick.

2. RAF-batch chart: defer Plotly.react to requestAnimationFrame so the
   table updates instantly without waiting for the chart render.

3. Display:none fast path: on filter-only changes, toggle visibility
   on existing rows instead of destroying and recreating all DOM nodes.
   Full rebuilds only happen on data or sort changes. The table module
   builds all rows regardless of the current filter so the fast path
   can widen as well as narrow.

   Compare: new applyTableFilters() export returns the matching set.
   Graph: new setFilter() handle method on the test-selection table.

Design docs updated with behavioral requirements; implementation guide
updated with the display:none and RAF-batch patterns.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Pin a comparison as a shadow, then change side B to overlay both
on the chart. The shadow renders as a thin blue line independently
sorted by its own ratio, enabling visual comparison of how a metric
profile evolved across two versions of side B against a shared
baseline.

- Pin button in Side B panel header, shadow chip above chart
- State persisted to URL via encodeSide/decodeSide('shadow_b')
- Auto-unpin on side A change or swap
- Shadow recompute reuses cached side A aggregation
- Shadow fetch errors isolated from main comparison
- Hide-noise, text filter, and manual toggles apply to shadow
- 24 new tests (state URL round-trip, auto-unpin, chart data prep)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
After creating a regression or adding indicators to an existing one from the
Compare page, feedback now shows a clickable link to the regression detail
page (using the title if available, otherwise the short UUID). Also clears
the title input after successful creation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extract the custom one-off regression search widget in the Compare page's
"Add to Existing" tab into a standalone regression-combobox component that
follows the established machine-combobox pattern: fetch-once with local
filtering, ARIA roles, keyboard navigation, collapse on select, blur and
outside-click dismiss, and proper lifecycle cleanup.

Also adds TODOs for pre-existing issues found during review: shadow toolbar
broken for unauthenticated users, machine-combobox swallowing fetch errors,
and setupComboboxKeyboard not being shared across combobox implementations.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Profiles page commit picker had three performance issues: an N+1
pattern in loadMachineCommits (1 + N requests to check which commits
have profiles), an N+1 in loadRuns (checking each run for profiles),
and an unnecessary bulk fetch of all suite commits. Replace all three
with server-side has_profiles= boolean filters on GET /commits and
GET /runs, using EXISTS subqueries against the Profile table. The
frontend now makes one API call per cascade step instead of 1+N.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Allow clients to include a `uuid` field in POST /runs. When provided,
the server uses it (normalized to lowercase) instead of generating one.
Duplicate UUIDs are rejected with 409 Conflict via a savepoint-based
IntegrityError handler. When omitted, behavior is unchanged (server
generates UUID v4).

This enables stable UUIDs across re-submissions, mirroring between LNT
instances, and pre-craftable URLs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Small clipboard icon next to the summary bar copies the visible
comparison table data as CSV to the clipboard. Respects all active
filters (noise hiding, manual click-hiding, text filter, chart zoom)
and current sort order. Includes geomean summary row.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extract shared combobox boilerplate (ARIA, keyboard nav, blur/outside-click
dismiss, validation halo) into a generic base component. Machine, commit,
and regression comboboxes become thin wrappers that provide data fetching
and selection semantics.

- New: components/combobox.ts (generic base)
- New: components/commit-combobox.ts (extracted from old combobox.ts)
- Refactored: machine-combobox.ts (214 -> 95 lines)
- Refactored: regression-combobox.ts (226 -> 100 lines)
- Deleted: src/combobox.ts (464 lines, fully replaced)
- Moved Compare-page wiring into selection.ts, eliminated ComboboxContext
- Fixed ARIA inconsistency (role=option now on all dropdown items)
- Fixed listener leak on re-render (combobox handles now destroyed)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Profiles page commit picker was showing raw commit strings while the
Compare page showed display values with tags. Both use createCommitPicker
which supports displayMap, but the Profiles page never built one.

Store full CommitSummary[] instead of Set<string> from getCommits, fetch
commit_fields via getTestSuiteInfoCached on suite change, and build a
displayMap in the getCommitData callback using commitDisplayValue.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The "Missing tests (N)" section header in the Compare page table did
not update when a text filter or chart zoom was active. Individual
missing rows were hidden via display:none, but the <h4> header always
showed the total count. Now the header updates to "Missing tests (M
of N matching)" when filtering is active, and missing rows also
respect chart zoom (previously only text filter applied to them).

Also adds a TODO for a GET /runs/{uuid}?samples=true endpoint to
enable round-tripping run data.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Copy as CSV button on the Compare page would vanish when the user
zoomed into the chart. Two issues:

1. applyTableFilters() in table.ts set summaryMessageEl.textContent,
   which destroys all child nodes -- including the copyBtn that
   compare.ts appends to the .table-message div. Fix: wrap the summary
   text in a dedicated <span> so textContent updates only touch the
   span, leaving siblings intact.

2. The CHART_ZOOM handler in compare.ts didn't update copyBtn
   visibility, so even if the button survived, it wouldn't hide when
   zooming filtered to zero visible rows. Fix: add the visibility
   update after renderSummaryBar().

Also removed the now-dead summaryMessageEl module variable from
table.ts (only used locally in redraw()).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Value A and Value B cells now show a native browser tooltip indicating
how many raw samples and contributing runs produced the aggregated
value (e.g. "6 samples across 2 runs"). Delta, Delta %, and Ratio
cells show both sides (e.g. "A: 6 samples across 2 runs, B: 4
samples across 1 run"). Counts reflect only runs that have data for
the specific test, with singular/plural applied. Geomean and missing-
test rows are excluded.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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