Skip to content

feat(ui): make the failure pages easier to read and act on - #788

Merged
PhenX merged 96 commits into
mainfrom
claude/modest-newton-zbvctt
Oct 9, 2026
Merged

PhenX merged 96 commits into
mainfrom
claude/modest-newton-zbvctt

Conversation

@PhenX-Pro

@PhenX-Pro PhenX-Pro commented Oct 9, 2026 •

Copy link
Copy Markdown
Collaborator

What & why

The execution page and the failure cluster page are where a failure gets understood and fixed. A UI/UX review found them complete but hard to land on: lines that contradict each other, a main button that copies a change the reader never sees, no answer to "is it still failing?", everything at the same weight, a timeline that colors almost every step, and tracker controls with no fixed place. This branch implements the whole review (priorities 1 to 10).

Situation block, both pages

  • One Most likely rule on both pages; the Next row names its source and shows the change it copies (up to six lines of the patch, or the failing and the recommended locator), with Copy apply command, Full patch and Download .patch.
  • The execution page says under its headline whether the failure still happens ("Not the latest Chromium execution: failed again in run Implement Playwright test results dashboard with SQLite storage, REST API, file upload support, custom reporter, comprehensive functional tests, and interactive charts #2…"); rows read explanation, action, context; the cluster facts and the ticket share one Cluster row.
  • The cluster page leads with its latest headline, then Most likely, State, Next, Issue, Occurrences, What changed; the section Next points at follows as its own card; one affected test is named in Occurrences; Activity and the occurrences chart fold at the foot.
  • A verified fix whose patch still applies at the fix's commit says so; no "unassigned"; the CODEOWNERS owner is named; a Done ticket reads the same on both pages.
  • Other states: a test that passed on retry opens on its failed attempt (headline, "failed on attempt 1, passed on attempt 2", Next compares the attempts, Evidence opens on Attempts); a test that did not run gives its reason as Most likely and its Next opens the run or the failure that stopped it. Status chips are in sentence case ("Passed on retry", "Didn't run").

Issue tracker

  • One filing per cluster (cluster page and executions share it), a removed or Done issue files anew, a tracker timeout is answered as queued, the regression check comes before "ticket Done", and the resolve/reopen policies act on a move of the ticket, for filed and linked issues alike.
  • One Issue line in the same place on both pages, one known-issue rule, the key as a link, a refused filing named, the execution's links grouped in Details, search by issue key, Piwi's writes in the cluster Activity.

Evidence and timeline

  • A duration is colored only when it stands out: at least 1 s, and a third of the test, or twice its usual time (median of the last passing runs on the same browser) and 1 s more. The Timeline, the Network tab, the trace's request list and the Performance panel use that one rule and one warning tone.
  • No Category column or "slowest" badge; parameters open from the step; cut bars are marked; bars placed from the test start.
  • The rows and tabs Most likely cites are marked ("Cited by Most likely", a dot on the tab); a citation opens its tab with the tab strip in view and rings the request or console entry for a moment.
  • Green belongs to the page's one primary action: selections, current-item markers, tabs and switches are neutral (SELECTED_ROW_CLASS, CURRENT_ITEM_RING_CLASS, …), held by tests/unit/evidence-accent.test.ts and tests/failure-page-accent.spec.ts.

Demo data and tooling

  • Seeded failures follow their stories (steps, durations, 30 s timeouts), cluster Implement Nuxt-based Playwright Dashboard with REST API and Copilot guidance #1's fix waits for the quote instead of networkidle, retry passes store their failed attempt, did-not-run rows store no steps; tests/unit/failure-lines-coherence.test.ts checks every seeded failure's lines agree.
  • app:measure --check holds the failure pages to budgets on a freshly seeded database.
  • Docs, inline help and MCP descriptions follow the pages; the docs illustrations whose subject changed are recaptured on a fresh seed.

Merges main (0.50.0, rule-based filing from #787): automatic filing goes through the per-cluster dedupe and Done rule, and its writes show in the Issue line and Activity.

Budgets (app:measure --check, 1280×800)

Route Start of branch Now Next row shows its change
/test-run-cases/37 38 controls above the fold 33 (navbar 8 · block 15 · below 10) not on the page → in the block
/test-run-cases/13 38 25 ✓ not on the page → in the block
/test-run-cases/87 38 35 (8 · 17 · 10) 1,782 px below → in the block
/test-run-cases/768 28 (no next step) 32 (8 · 11 · 13) n/a (compare attempts)
/test-run-cases/748 21 30 (8 · 7 · 15) n/a (open the run)
/failure-clusters/1 30 21 ✓ 2,447 px below → in the block
/failure-clusters/2 29 23 ✓ 2,265 px below → in the block
/failure-clusters/5 30 23 ✓ not on the page → in the block
/failure-clusters/10 33 33 (7 · 13 · 13) n/a (mark resolved)

14 budget breaches at the start, 5 now. The five left are all over the 25-control budget, and in each one the situation block itself stays at 7 to 17 controls: the excess is what sits below the block and now enters the first screen because the block got shorter, the evidence card's tab strip (8 to 10 tabs) on #37, #87, #768 and cluster #10, and the history strip's 15 links on #748. Text styles, Next content distance and the one solid primary button pass everywhere.

How was it tested?

  • Full unit suite on the final tree (6,960 passed), app:typecheck, app:lint, app:format, commitlint, app:screens:check, app:generate:demo + app:check:demo:runtime.
  • E2E: the specs for the touched pages ran locally against a seeded dev server for each milestone (test-run-case-page, timeline-durations, execution-states, cluster-page-layout, integrations-*, fix-plan, fix-verification, failure-clusters, mobile-responsiveness, mcp); this PR's CI runs the whole suite on a built server.
  • Before/after screenshots and app:measure tables for each milestone; an independent review of the riskier lots, with its findings fixed.

Checklist

  • PR title follows Conventional Commits (type(scope): subject)
  • Tests added/updated for behavior changes
  • Docs updated if user-facing (apps/docs/)

🤖 Generated with Claude Code

https://claude.ai/code/session_01DN1TP3gsavgzTbFgfPDUkb

claude added 30 commits October 8, 2026 18:16
…fails

Filing and the tracker sync could silently do the wrong thing:

- A create from the cluster page and from one of its executions used two
  dedupe keys, so the same failure could be filed twice. Filings are now keyed
  on the cluster (a filing recorded against an execution still answers), and
  recorded against it.
- After the link to a Piwi-filed issue was removed, filing again answered with
  the old key, said "created" and linked nothing. A done filing now answers
  only while its link is on the cluster; otherwise it is retired and a new
  issue is filed. A skipped filing no longer blocks the cluster.
- A Jira timeout or 5xx was reported as a failure although the outbox retries
  it. It now comes back as queued, and the cluster and execution pages show
  "Filing queued" with a retry instead of a fresh Create.
- The "ticket is Done, mark resolved?" state hid a regression and showed while
  the cluster still failed. The regression now comes first, and the reconcile
  is offered only once the newest finished run no longer fails; otherwise the
  state notes the Done ticket.
- resolve-on-close and reopen-on-ticket-reopen acted on the ticket's state at
  every sweep, undoing a reopen or a manual resolve. They now act on a move,
  never resolve a regressed cluster, and a created link records its status
  category so the first sync can see a move.
- The create modal's error rendered below the preview, out of view; it now
  shows at the top, and an already-filed issue is named as such. Link errors
  were shown as a green "Copied to clipboard" toast and overwrote the
  clipboard; they are error toasts now, a bare key gets a hint, and adding a
  link confirms it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN1TP3gsavgzTbFgfPDUkb
…ages

The tracker's actions and links were spread over the state line, a chip,
the Details popover and the menus, and differed between the cluster page and
the execution page. They now sit in the same places on both:

- The situation block has an Issue line: "Tracked in PROJ-123, To Do in
  Jira." with the key as a link, "No issue yet." with Create issue and Link an
  issue, or the queued filing. A Done issue on a failure that goes on (it
  failed in the latest finished run) says so and offers a new issue.
- One rule names the cluster's known issue on every surface: its most
  recently linked tracker issue, sent by the server with the cluster and
  with the execution.
- The page's More menu offers Open PROJ-123, Create issue or Filing queued ·
  Retry, and Link an issue, which links an issue to the cluster.
- A link chip's key and title are the link; its remove button is labelled
  and shows on focus and on touch screens, and removing an issue Piwi filed
  asks first.
- The execution's Details lists its links by what they belong to (this
  execution, this test, the run) and edits them there.
- The Situation leaves the ticket to the Issue line. A Done ticket on a
  failure that stopped leads the Next line to "Mark the cluster resolved —
  PROJ-123 is Done", and the State line leaves out a reconcile button the
  Next line already offers.
- The create modal names the cluster, says when the project has no default
  Jira project (with a link to set one), and explains a disabled share link.
  The issue title is the cluster's name from either page.
- Search (Ctrl K and the search MCP tool) finds a cluster by its name,
  signature, #id or the key of a linked issue.
- The cluster's Activity lists what Piwi wrote to its issue: the filing, its
  comments and moves, queued or failed.
- Copy summary and Copy failure name the ticket.
- The Details popover scrolls within the viewport.

Linking an issue by hand now records its status category, as filing one
does, so resolve-on-close and reopen-on-ticket-reopen act on its first move.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN1TP3gsavgzTbFgfPDUkb
The timeline colored every step over 500 ms orange and every one over 2 s
red, so a test of quick steps showed a column of orange, while the 28.4 s
request the Most likely line names stayed grey and read "28400 ms". A green
check on every passed step, a "slowest" badge, category badges and a green
dot on almost every tab added more color that said nothing.

- One rule says when a duration stands out: at least 1 s, and a third of the
  test or more (shared/duration-standout.ts, unit-tested). It takes an input
  object and returns the reason, so a usual time from past runs can join it.
  Steps and requests both follow it.
- A duration is one TimelineDuration: the number through DurationValue
  (28.4s, never 28400 ms), its share of the test and a bar on the test's
  clock, requests included. Only a duration that stands out takes the one
  warning tone of app/utils/duration-tone.ts, says why on hover and carries
  data-standout. The failure keeps the status palette: the failing row's
  tint, title and bar, the failed marks and the failure line on the axis
  come from STATUS_PALETTE, which gains a row tint per outcome.
- The Category column, the per-row category badges and the "slowest" badge
  are gone. The per-category rollup stays as one meta text line, and an
  event row names its kind (console warning, backend error) before its text.
- A passed step's mark is a muted check; the failure's cross is the only
  colored mark in the column.
- A step's parameters are closed by default, the failing step's too, and
  open from the step's title (aria-expanded); StepParamsDisclosure becomes
  StepParams, the list alone.
- A bar the "Around the failure" window cuts ends in an arrow at the cut
  edge, and its tooltip says it started before or runs on past the window.
- Evidence tabs drop the green "has data" dot and show their count as meta
  text. The open tab (SELECTED_TAB_CLASS) and the window switch, now one
  segmented control with aria-pressed, are neutral, so green stays the
  primary action's. The switch shares the type chips' row with the mark key,
  which leaves out the Calls band the axis already labels: all five lanes of
  execution 37 fit on the first screen at 1280x800.

Docs (features/evidence.md, within its word budget), the Evidence help, the
AGENTS rules for durations and selected states, the step-params scene and new
timeline-durations scenes follow. E2E: test-run-case-page and performance-ui
check the standout rule, the switch and the parameters instead of the badge;
timeline-durations covers a request longer than the test and the cut arrow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN1TP3gsavgzTbFgfPDUkb
The duration bars in the timeline table were placed with `item.at / testMs`,
but `at` counts from the timeline's origin, the earliest timestamp anywhere in
the execution. One console entry or backend log stamped before startedAt (a
server whose clock runs behind) pushed every bar right, and with 10 s of skew
every step of a 4.4 s test became a sliver at the end of its track.

- buildFailureTimeline returns `testStart`, the test's own start on the
  timeline's clock (0 when nothing predates startedAt). The REST endpoint and
  the demo share it through getFailureTimeline; the endpoint's OpenAPI
  description names it.
- The bar geometry moves to pure helpers in app/utils/timeline-rows.ts:
  testTrack (from the test start, as long as the test) and barPlace (clamped,
  never narrower than 1.5%), unit-tested with evidence 3 s and 10 s before the
  start and a request that outlasts the test. The no-axis waterfall uses
  barPlace too.
- windowCuts and windowCutText replace the card's inline check: a bar the
  window cuts on both sides says "Started before and runs on past this window".
- A step title's aria-controls is set only while its parameter list is open,
  so it never names an id missing from the DOM. The title carries
  data-testid="step-params-toggle", which the step-params scene clicks, and
  test-run-case-page checks aria-controls closed and open.
- SEGMENTED_SELECTED_CLASS joins SELECTED_TAB_CLASS in app/utils/index.ts; the
  window switch, the failing step's screen view, the locator drawer's reading
  switch and the project's failure segments use it, and the AGENTS rule names
  both constants (a tab strip, a segmented control or view switch).
- The AGENTS duration rule covers the execution timeline, and says the Network
  tab and the trace's request list still color by fixed thresholds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN1TP3gsavgzTbFgfPDUkb
A findability test of the Issue line left four gaps, closed here on the
cluster page and on the execution page alike:

- A filing Jira refused for good (its create-issue action failed or was
  skipped, and nothing newer replaced it) is named on the Issue line in one
  meta sentence quoting Jira's reason, wherever filing is offered ("The last
  filing failed: Jira answered ..."). So File a new issue and Create issue
  are no longer a blind retry. The server sends it with the cluster and with
  the execution's failureCluster (issueFilingFailure), read by
  clusterIssueFilings, which also gives the queued filings in the same two
  queries; the demo reads it through the same shared handlers.
- The issue key gets a tooltip with the issue's summary and says it opens in
  Jira; the known issue the server sends now carries that summary (title).
- "The issue is Done, but the failure goes on." is body text on the sentence
  that names the issue, not a muted line under it.
- The dialog opened from File a new issue is titled so and says a new issue
  is filed next to the Done one, which stays linked. Link an issue on a
  tracked cluster says the current issue stays linked, and that a Jira issue
  (or issue on a connected tracker) linked now takes its place, since the
  cluster's issue is its newest such link, as clusterKnownIssues decides.

The issue-tracking page and the issue help topics say the same; the page
stays within its word budget.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN1TP3gsavgzTbFgfPDUkb
…he cluster's links

The Details popover opened upward once the Issue line pushed the facts line
down. At 1280x800 on a failing execution it reached the top of the screen,
covered the navbar's Share and Export, and cut its last links group with no
sign that more was below. Its Links section also left out the cluster's
links, so the ticket the Issue line names was not there.

- The popover opens below the Details button and scrolls inside when it is
  taller than the room there, with a "More below" fade while content is
  hidden; it opens above only when the button sits less than 200 px from the
  bottom of the screen (a phone), and never over the navbar.
- From md up its groups sit in two columns, so it is about half as tall.
- Links ends with a read-only "Cluster #N" group listing the cluster's links,
  its issue among them, with a link to the cluster page that edits them. The
  server sends those links with the execution's failureCluster through the
  shared handler, so the demo shows them too.

The navbar's More actions menu opened on every click made once the page had
hydrated (/failure-clusters/10 and /test-run-cases/5, dev server); a click
before hydration ends does nothing, as for any server-rendered control, so
the menu is left as it is.

The execution-details-links scene captures the popover open at 1280x800,
scrolled to its links.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN1TP3gsavgzTbFgfPDUkb
…ed filing at once

Review follow-ups on the Details popover and the Issue line.

- The execution's Details open below the button when the content fits
  there, or when there is at least as much room below as above, and above
  otherwise. The 200 px rule kept it below on a failing execution at
  1280x800 and hid the Cluster #2 group under "More below"; it now opens
  above there and shows every group.
- The popover's top limit is the top of the panel body, read when it
  opens, so it stays under the navbar when the public demo's banner pushes
  the navbar down. On a phone it stays within the screen width.
- Links spans both columns from md up, so link chips are as wide as
  elsewhere.
- "Edit on the cluster page" opens /failure-clusters/N#links, where the
  cluster's Details open on load, aligned to their button.
- When Jira refuses a filing, the Create issue dialog stays open with the
  reason and the page behind it refreshes (a new `failed` event), so the
  Issue line names the refusal without a reload.
- The refusal sentence shows only where filing is offered, as documented,
  and says how long ago the filing failed ("The last filing failed 4 hours
  ago: Jira answered ..."). IssueFilingFailure drops its unused status.
- The two screenshot scenes that track cluster 1 share one prepare helper.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN1TP3gsavgzTbFgfPDUkb
app:measure printed numbers but held the failure pages to nothing, and it
could not say whether the Next step's button sits near what it copies.

The budgets now live in a pure module, scripts/lib/detail-page-budgets.mjs,
shared by the script and a unit test. At 1280x800: at most 15 text styles in
the execution page's situation block and 12 in the cluster page's, at most 25
controls above the fold with the navbar included, at most one solid primary
button above the fold, and a Next step that copies a code change
(copy-git-apply, copy-patch) shows that change in the block. copy-recipe and
copy-flake-command are reported without a verdict. The report prints a verdict
per route and a summary of breaches; --check exits 1 on a breach, --port picks
the port of the server the script boots.

The page measure gains the Next step (kind, action, how far below its button
the visible [data-copies] content sits), the controls above the fold split into
navbar, block and below, the Most likely grade (replacing the clue strengths,
which read nothing since the grade moved to StoryLine), the solid primaries,
and the positions of Most likely, Issue, the occurrences trend and Activity
(the cluster-history hook no longer exists). distinctTextStyles skips SVG
<title>/<desc> text and visually hidden text: the empty sparkline bars' titles
counted as a 13th style on cluster #2. The script waits for the page's API
requests before reading, since the cluster's What changed line fills after
hydration and two runs read different pages otherwise, and it reports a route
whose record answers 404 instead of measuring the page's error state.

Hooks, attributes only: data-next-kind and data-next-action on NextStepLine,
data-shot="most-likely" on StoryLine, data-copies on DiagnosisResult's
PatchBlock (copy-git-apply), LocatorHealingPanel's suggested edit (copy-patch)
and ReproduceSection (copy-recipe). The seeded #37 E2E pins data-next-action.
A unit test reads shared/next-step.ts and fails when a copy-... primary is
missing from NEXT_STEP_COPIES.

Measured on a copy of the dev database, 1280x800, before -> after: controls
above the fold unchanged on every route (#37 38, #13 38, #87 38, #768 28,
#748 21, cluster #10 33, #2 29, #5 30, #1 30); text styles unchanged on the
executions, one fewer on each cluster (#2 13 -> 12, #10/#5/#1 12 -> 11);
Next content: #37 and #13 not on the page, #87 1 782 px below, cluster #2
2 265 px, #1 2 447 px, #5 not on the page, #10 mark-resolved (copies nothing).

The rule in apps/application/AGENTS.md, the root command table and the run-app
skill (default routes, budgets, the ?tab=diagnosis and ?tab=history routes
that no longer exist, #768 and #748) follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN1TP3gsavgzTbFgfPDUkb
…atabase

Without --url, app:measure booted a server on the local dev database. That
database carries whatever earlier work left in it (simulated Jira links such as
CHK-101 and CHK-7, triage changes), so the Issue line, the Next step and the
control counts depended on the machine and the verdicts were not reproducible.

dev-server.mjs gains seedThrowawayDb(dir): it empties the directory,
regenerates public/demo/seed.sql when it is missing or no longer hashes to
seed.version.json (written to the throwaway directory first and copied, so the
tracked version file is left alone), then runs app:seed:dev with
PIWI_DATABASE_PATH and PIWI_STORAGE_PATH pointing inside it. startServer takes
an optional env for the server, and skips seeding the dev database when that
env names its own; take-feature-screenshots is unchanged. Progress messages go
to stderr so a --json report stays parseable.

Without --url the measure seeds .data/measure/ and boots its server on it; the
report says so, and with --url it says it measured the server's own data and
that a connected tracker's state is not covered by the budgets.

Two runs without --url (1280x800, default routes) give identical objects for
every route. Controls above the fold: executions 37, 13, 87, 768 and 748 at
38, 38, 37, 28 and 21; clusters 10, 2, 5 and 1 at 32, 29, 29 and 26. The
content a Next step copies sits 2 453 px below its button on cluster 10 and
2 109 px on cluster 2, and is not on the page of executions 37 and 13. The same
script with --url on a copy of the dev database reads execution 87 at 38 and
clusters 10, 5 and 1 at 33, 30 and 30, with Mark resolved as cluster 10's Next.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN1TP3gsavgzTbFgfPDUkb
ARCHITECTURE.md still described the execution page as diagnosis-first, with a
Diagnosis tab, a right rail of cards and ?tab= aliases. The page reads top to
bottom in one column now, and its component table listed cards that are gone.

The paragraph now walks both failure pages: SituationBlock and the order of
its ROWS (with the component behind each row and the bug report's use of the
same block), the facts line, where the rows are built (the shared handlers,
shared/situation.ts, shared/cluster-state.ts, computeNextStep) and how the
Next action reaches the page (useNextStepActions), then what sits under the
block on each page and the section locator both provide. The component table
describes test-case/ and cluster/ as they are and gains integrations/ (the
Issue line and the tracker dialogs), bug-reports/, gaps/, notifications/ and
reports/. A "Failure pages" entry joins the shared building blocks.

The execution-history scene opens /test-run-cases/229 without ?tab=history,
describes the History block, and no longer waits for chart geometry: the block
is a strip of squares with no SVG, so the scene timed out on its chart wait.
Four report scenes capture the first screen of a pass on retry (execution 768)
and a test that did not run (execution 748) at 1280x800 and 390x800, so every
route app:measure reads by default has its picture.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN1TP3gsavgzTbFgfPDUkb
app:measure booted its server on 3050, the port app:screens uses, and never
checked it was free. nuxt dev then bound another port quietly, the health
check passed against the server already on 3050, the run measured that
server's code and data, and the shutdown wait failed after 30 s. The
throwaway server also inherited the shell and .env (nuxt dev fills only the
keys still undefined): a Postgres URL routed it away from the seeded SQLite
file, an AI provider turned execution #748's Next from copy-recipe into
diagnose, and Jira credentials connected a tracker, while the report still
said it measured a freshly seeded database with no tracker.

dev-server.mjs gains portInUse(port), a TCP probe of localhost. startServer
refuses a port something listens on, and waitForPortFree waits on the same
probe rather than on a failing health request. seedThrowawayDb returns the
settings the server must not take from the machine: no database URL, local
storage, authentication off, no AI provider or key, no Jira credentials, the
default locale and time zone. app:measure boots on 3060 by default, checks
the port before seeding, and the report names no AI provider alongside no
issue tracker. The run-app skill says so and gains the port pitfall.

Checked: with a listener on 3060 the measure stops at once ("port 3060 is
already in use; stop that server or pass --port"), and startServer refuses
the same way for app:screens. With a .env setting a Postgres URL, an AI
provider, Jira credentials and fr-FR, the measure reads the same numbers as
before on every default route (executions 37, 13, 87, 768 and 748 at 38, 38,
37, 28 and 21 controls; #748's Next copy-recipe); the previous script with
only the AI provider in .env read #748 at 20 controls with diagnose as Next.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN1TP3gsavgzTbFgfPDUkb
The skill said --expand unfolds every collapsed section, the More ways to fix
sections included. It clicks the collapsible cards' role="button" toggles
only: More ways to fix uses plain buttons and is an accordion, which opens the
section the Next step points at and holds one section open at a time. An
agent following the skill believed it had seen every section.

The line now names the evidence cards as what --expand unfolds, and says to
click another More ways to fix section in a scene's run to see it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN1TP3gsavgzTbFgfPDUkb
The drift guard checked that every copy-... primary of the next-step policy
has a class in NEXT_STEP_COPIES, but nothing checked that a component carries
each budgeted action in a data-copies attribute. A dropped or mistyped carrier
made every distance for that action read "not on the page", which looks like
a page regression rather than a missing hook.

The test reads app/components/**/*.vue and requires each budgeted action
(copy-git-apply, copy-patch) among the tokens of some data-copies value.
Renaming the DiagnosisResult carrier to copy-git-aply fails it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN1TP3gsavgzTbFgfPDUkb
…rywhere

Piwi's own diagnosis stores its patch validation at the top of the details,
an agent's diagnosis inside its suggested fix. Each reader picked its own
place. The fix plan read only the suggested fix and otherwise re-validated
the patch against an empty file map, so every Piwi-made fix plan said
"unchecked". The diagnosis card and the diagnosis history read only the top
level, so an agent's patch carried no badge. The AI usage tally and the
next-step policy read both, in opposite orders.

storedPatchValidation (shared/patch.ts) is now the one reader: the suggested
fix's validation, else the top-level one, when it is an object with a string
status. The order is the one the next-step policy already used, so no next
step changes. The fix plan, getClusterPatchFacts, the AI usage tally, the
diagnosis card and the diagnosis history all use it, and the fix plan
re-validates structurally only when nothing is stored.

Visible change: get_fix_plan, GET /api/failure-clusters/:id/fix-plan and the
fix plan's "Copy as Markdown" now report the stored verdict ("Applies
cleanly" on the seeded clusters) instead of "Unverified", agreeing with the
next step that proposes the patch. The issue body built from the fix plan does
not print the validation, so filed issues are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN1TP3gsavgzTbFgfPDUkb
…k steps denying the clues

The failure pages said "unassigned" next to the code owner ("cluster #1
(open, unassigned). Owner @Checkout-Team."), which reads as a contradiction:
an assignee and a code owner are two different things. The situation's
cluster clause now names an assignee only when one is set ("(open, assigned
to Avery)", "(open; fixed once before, the fix did not hold)"), and the
cluster state's failing sentence ends after the run ("Still failing — last
seen 8 hours ago in run #1.", where it ended ", open, unassigned."). The
reports and analytics keep their "Unassigned" category.

The two fallback next steps claimed that nothing explained the failure,
while the Most likely line above them often showed a strong clue: the policy
never sees the clues. They now say what the step does. "Diagnose with AI"
gives "A diagnosis reads the clues, the evidence and the recent commits
together and proposes a fix." and "Reproduce locally" gives "Run it on the
recorded commit and browser to watch it fail."

The execution route added a CODEOWNERS owner to the verdict after
getTestRunCase had built the situation sentence, so the sentence never
named that owner. getTestRunCase now takes an optional resolveOwner, called
for a failing test with no piwi:owner annotation before the verdict and the
situation are built; the verdict accepts an owner with its source. The route
passes a resolver around resolveOwners (still caught, so a CODEOWNERS read
never blocks the page). The demo, MCP and the export pass none in this
commit.

getTestRunCase also reads the cluster's diagnosis with scope 'cluster', as
the cluster page does, so an execution-scoped diagnosis on the same cluster
is never shown as the cluster's.

MCP output change: get_fix_plan and explain_failure return these two next
steps with the new title and why in nextStep, and their situation text no
longer says "unassigned" in the cluster clause.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN1TP3gsavgzTbFgfPDUkb
readLocatorTarget lower-cased the accessible name it read from the failing
locator so it could match the console and the ARIA snapshot without regard
to case, and the sentences printed that lower-cased value: the Most likely
line of the seeded checkout failure read 'The button "pay" stayed
disabled', under a headline that says getByRole('button', { name: 'Pay' }).

The target now keeps the name as the test wrote it beside a lower-cased
needle. The story, the blocked element's clue and the console clue print the
name; the ARIA match uses the needle and the console match lower-cases both
sides, so matching stays case-insensitive.

The docs already write "Pay"; the tour-execution and gather-evidence images
show the line and are recaptured with the other failure-page illustrations.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN1TP3gsavgzTbFgfPDUkb
… on both pages

Two rules decided what a Done ticket means, each in two forms. The cluster
page compared run positions to tell whether the failure went on and offered
"Mark the cluster resolved — KEY is Done" from its state; the execution page
compared run start times and never offered it, so on the dev database
(CHK-7 Done on cluster #10) the cluster proposed marking it resolved while
its latest execution proposed applying the patch. The cluster page's Issue
line also read "the failure goes on" from any state that was not
ticket-done, so a snoozed cluster whose failure had stopped said "The issue
is Done, but the failure goes on."

shared/cluster-state.ts now holds both rules:
- failureGoesOn(lastSeen, latestFinished): the cluster was last seen in the
  latest finished run or a later one, or no run has finished. Runs are
  compared by start time, then by id when two started in the same second
  (run start times are stored to the second, and a failing run followed at
  once by a passing one otherwise read as "goes on" on the execution page).
- ticketReconcileKey(cluster, { failureGoesOn, now }): the Done ticket's key
  when the cluster is open, not snoozed, its fix did not regress and the
  failure stopped.

computeClusterState takes failureGoesOn instead of the latest finished run
id and offers the reconcile through ticketReconcileKey. getFailureCluster
computes failureGoesOn from the runs it already loads (now ordered by start
time then id), returns it, and passes the helper's key to the next step.
getTestRunCase uses the same two helpers and passes the key to its next
step, so the execution page offers "Mark the cluster resolved — KEY is
Done" as the cluster page does. The cluster page hands
cluster.failureGoesOn to its Issue line.

MCP output change: explain_failure returns nextStep.kind 'mark-resolved'
for an execution whose cluster's Done ticket no longer fails, and
get_fix_plan's nextStep follows the same failureGoesOn rule.

features/issue-tracking.md already describes this behavior.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN1TP3gsavgzTbFgfPDUkb
…turn

The execution page's Situation sentence names the owner the repository's
CODEOWNERS gives a failing test with no piwi:owner annotation ("... Owner
@payments."), through getTestRunCase's resolveOwner. explain_failure and
get_fix_plan built their situation from getTestRunCase with no resolver, so
for such a test the agent read a sentence without the owner the page shows,
and get_fix_plan returned that sentence next to an ownership block that
names the CODEOWNERS owner.

codeownersOwnerResolver (server/utils/scm/ownership.ts) is now the one
resolver, returning the CODEOWNERS owner of one spec file, a failed read
resolving to no owner. The execution route, explain_failure and get_fix_plan pass it, so
the situation text both tools return names the same owner as the page.
explain_failure resolves the execution's project itself to build it.

MCP output change: explain_failure.situation and get_fix_plan.situation end
with "Owner @team." when only CODEOWNERS names one.

The docs say the cluster and execution pages (not the inbox) fall back to
CODEOWNERS; the comment on the cluster handler's owner says how each page
resolves it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN1TP3gsavgzTbFgfPDUkb
…e ticket stopped failing

The Issue line's header comment said only the cluster page offers to
resolve the cluster once its Done ticket's failure stopped. Both failure
pages do, through ticketReconcileKey: the execution page proposes "Mark the
cluster resolved — KEY is Done" as the cluster page does. Comment only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN1TP3gsavgzTbFgfPDUkb
The toolbox renders a section's body only while it is open, so an action
that drives a panel inside a folded section found no panel and did
nothing. On the execution page that was Copy patch, Copy locator, Pick
from snapshot and All alternatives once Locator fix was folded; on the
cluster page, More actions > Show context and Copy prompt and the next
step's Re-diagnose while Diagnosis was folded (cluster #2 opens on
Locator fix, so its Diagnosis is folded by default).

- Toolbox exposes openSection and scrollToSection as promises that
  resolve once the section's body is mounted.
- useNextStepActions waits for that before every locator panel call:
  the copies open the section without scrolling, Pick from snapshot and
  All alternatives scroll to it. With no panel on the page, an error
  toast says so instead of a dead button. Read the diagnosis, Diagnose
  and Reproduce await the same promise.
- The cluster page opens Diagnosis before Show context and Re-diagnose,
  with the same toast when the panel is missing. Copy prompt copies from
  the cluster's context endpoint through useCopyAiPrompt, the path the
  next step's Copy AI prompt already takes, so it needs no panel and
  leaves the sections as they are; DiagnosisPanel drops its duplicate copy.

In E2E, Copy prompt with Diagnosis folded shows the toast and fills the
clipboard; on a seeded replace-locator execution with a line edit
(#87 or #533, read from the API), Pick from snapshot opens the picker
and Copy patch copies the healing edit's diff with Locator fix folded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN1TP3gsavgzTbFgfPDUkb
… step checked

The cluster's next step decides on replace-locator from the healing of
its latest occurrence (latestTestRunsCaseId), but the page's Locator fix
section, its hoisted healing fetch and the LocatorHealingPanel read the
most-affected test's newest execution (affectedTestCases[0]). The two
differ on every seeded cluster (#1: 2 against 37, #2: 3 against 87,
#5: 497 against 586), and on #5 the cited source line differs (15
against 7), so the step and the section could reason on two failures.

- locatorCaseId, hasLocatorPanel, the hoisted fetch and the panel read
  latestExecId, the execution computeNextStep checks.
- On both failure pages the healing fetch is awaited (not lazy, and
  immediate) when the next step is replace-locator, so the Locator fix
  section is in the page's first render after a client-side navigation
  rather than added once the fetch lands. The execution page fetches it
  for that step even when the verdict alone would not. The server render
  already waited for an immediate fetch, lazy or not.

In E2E, on seeded clusters #2 and #5 (skipped elsewhere, and unless the
step is replace-locator), a client-side navigation from the project's
cluster list with a slowed healing answer renders the section at once,
fetches only the latest occurrence's healing and shows its recommended
locator.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN1TP3gsavgzTbFgfPDUkb
Both failure pages carry More actions in the navbar with the same issue
labels, but in two orders: the cluster page listed the ticket, then
Quarantine all affected tests, Show context, Copy prompt, Re-run in CI,
Copy retry command and Copy summary; the execution page listed Copy
retry command and Quarantine first, then the ticket and Copy failure.
Someone moving between the two pages had to read each menu again.

Both menus are now grouped arrays (the dropdown draws a separator
between groups), in one order:

- the ticket: Open KEY, Create issue or Filing queued · Retry, then
  Link an issue;
- the test actions: Quarantine this test or Release from quarantine,
  Quarantine all affected tests, Re-run in CI;
- the copies: Copy failure or Copy summary, Copy retry command, and on
  the cluster page Show context and Copy prompt;
- Refresh, alone.

Each page shows only the items it has; an empty group is dropped.
SituationBlock keeps its #actions slot, which the bug report page uses.

In E2E, the execution page's menu test also checks that order and that
Refresh closes the menu in its own group; a new cluster-page test does
the same on the seeded two-test cluster, with Copy summary present.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN1TP3gsavgzTbFgfPDUkb
Both failure pages fetch the execution's locator healing to decide whether
the Locator fix section has anything to show, and the panel inside that
section fetched it again under the same key. Outside hydration Nuxt's
default getCachedData returns nothing, so every mount of the panel sent a
second request and rendered nothing until it landed: after a client-side
navigation the section's row was in the first render but its body came
about 1.5 s later on a slow answer, and on the execution page each fold
and unfold of the section asked for the healing again.

- LocatorHealingPanel takes the healing as a prop and keeps no fetch of
  its own; both pages pass the answer they already hold. The panel
  renders it as soon as it is there, and keeps it on screen while a
  refresh runs.
- A confirmed snapshot pick refreshes the page's fetch by its key
  (locator-healing-<id>), the key the page uses; the panel refreshed a
  URL that matched no key, so a pick never updated the list.

In E2E, the cluster test counts the healing requests in an array and
expects exactly one, for the latest occurrence, with the panel's body
([data-shot=alternative-locators]) present at first render; the execution
test expects no healing request while the Locator fix section is folded
and opened again. Both fail on the previous panel (no body at first
render; four extra requests).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN1TP3gsavgzTbFgfPDUkb
… made

The server proposes replace-locator whenever the healing carries a
recommendation, and a rename found in the run's own diff returns its
locators in fromDiffRename alone. The panel counted that list, but the two
pages' gates for the Locator fix section did not, so such a failure had
no section. The next step's actions open that section first, so all four
of them (Copy patch, Copy locator, Pick from snapshot, All alternatives)
ended in the "Locator fix not available" toast.

hasHealingAlternatives(healing), in #shared/locator-healing, is the one
rule, used by the panel and by both pages' section gates. It asks for a
source other than none and at least one ranked list from any rung of the
lookup (the diff, the failing page, a prior pass, the ARIA snapshot).

Unit tests cover each list on its own, a diff rename alone, and the empty
cases (no answer, source none, empty lists).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN1TP3gsavgzTbFgfPDUkb
…g one

Toolbox.openSection set the open key to whatever it was given, even a
section the toolbox does not show. A next-step action whose section is
missing (Copy patch with no Locator fix section, as on a project that
declines fixtures) folded the section the reader had open, such as
Diagnosis, opened nothing, then showed its error toast.

- openSection returns at once for a key outside the sections it renders;
  scrollToSection then finds no section to scroll to, and the callers
  fall through to their panel-missing toast.
- useNextStepActions documents scrollToSection and openSection by the
  actions that use each.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN1TP3gsavgzTbFgfPDUkb
…on folded

On the cluster page, More actions › Show context and the next step's
Re-diagnose act on the Diagnosis panel, which the toolbox mounts only
while its section is open; both go through the page's onDiagnosisPanel,
which opens the section first. Only Copy prompt was covered, and it no
longer touches the panel.

The new test runs in the AI diagnosis block (mock provider configured,
the cluster diagnosed, so its next step is Follow the diagnosis with
Re-diagnose inline). With Diagnosis folded, Show context opens the
section and the "Context sent to AI" dialog; folded again, Re-diagnose
opens the section and asks for a new diagnosis, a request the test
aborts so the stored diagnosis stays for the tests that follow. It fails
when onDiagnosisPanel does not open the section.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN1TP3gsavgzTbFgfPDUkb
The execution timeline's table header carries a visually hidden "Kind"
label (sr-only, so absolutely positioned). Its header cell was not
positioned, so the label's containing block was the dashboard panel and
it sat about 890 px down the fixed dashboard group, outside the panel's
own scroller. At 1280x800 that gave the fixed group about 90 px of
overflow, and any scrollIntoView on the page (Pick from snapshot opening
the Locator fix section, a clue revealing its evidence) scrolled the
whole dashboard up: the navbar went off screen and a blank band stayed
at the bottom, even after the picker closed.

The header cell is relative, so the label stays inside the table and
its scroller clips it; the fixed group no longer overflows on the
execution or cluster pages.

In E2E, after Pick from snapshot and Cancel on the seeded replace-locator
execution, the navbar's More actions button is still at or below the top
of the viewport (it was at -46 px).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN1TP3gsavgzTbFgfPDUkb
The run page loads its rows after hydration, so the issue key chip on the
failing execution's row can appear a few seconds after navigation on a local
dev server, which compiles each page on its first visit. The check now waits
for hydration and gives the row the 15 s other run-page checks use.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN1TP3gsavgzTbFgfPDUkb
…s in cluster #10's diagnosis

Cluster #1's suggested fix added page.waitForLoadState('networkidle'),
a wait Playwright advises against, in a product that recommends better
waits. The story's own evidence says why the click times out: the Pay
button stays disabled until POST /api/checkout/quote answers, which takes
28.4 s on failing runs. The fix now registers a wait on that response
before the fills, awaits it before the click and marks the flow
test.slow(). derivePatch takes several edits and groups them into hunks
the way git diff does, so the patch is a two-hunk diff that
validatePatch reports as applying at its stated lines.

Diagnosis 1 (summary, root cause, hypotheses, evidence, investigation
steps, prevention tips) is written around the slow quote, and its
earlier version says the network capture superseded it. The demo AI's
timeout-interaction template recommends waiting on the request or the
web-first condition the element depends on instead of networkidle.

Cluster #10's error says "Expected 26 rows, found 51" while its
diagnosis said 50 instead of 25. The summary and the first sentence of
the root cause now quote 51 and 26 and say the header row accounts for
the difference. The spec and the error are unchanged, so the
fingerprint, the cluster title and the demo examples hold.

demo-seed-consistency checks that the checkout patch has several hunks
and applies at its stated lines, that no story patch, seeded diagnosis
or demo AI template recommends networkidle, and that a stored diagnosis
of a count assertion quotes both counts of its error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN1TP3gsavgzTbFgfPDUkb
…est that did not run

The reporter, the blob import and the run simulator store every attempt
as its own execution, but the seed collapsed the Web Dashboard's flaky
test into one passing row, so the red 1/2 attempt on #738 and #768 had
no execution to link to. FLAKY_CASES gives that test the error its
failed attempt reports (toHaveAttribute on locator('html'), ending in a
tests/admin/settings.spec.ts frame), and an rng-free block after the
dedicated flake blocks splits every remaining retry pass with
splitFlakyExecution. The new rows take the next ids: 829 is the failed
attempt of #738 and 830 the one of #768. Every existing id is unchanged,
and the stored flaky root cause of the test stays 'other'.

A test the run never reached carried four planned 0 ms steps, a start
time and a worker, which the reporter never sends: materializeUnrunTests
emits no steps, startedAt or workerIndex, and no attempts. The
max-failures rows and the serial cascade's blocked rows now store none
of these (the cascade keeps its one didnotrun attempt with no start, as
onTestEnd reports it), so the execution page shows no empty Evidence
card on #748. The slowest-step reduce and the worker-lane re-pack skip
rows without steps or a start time.

demo-seed-consistency checks that every retry pass has its failed
attempt stored as its own execution with an error, and that a test that
did not run carries no steps, start time, worker or step events.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN1TP3gsavgzTbFgfPDUkb
claude added 4 commits October 9, 2026 07:55
Copy apply command is a heredoc for bash, zsh or Git Bash, so on the locator
step with an edit a PowerShell user had no way to apply the edit from the Next
row, although the docs name Download .patch as the portable route.

The step's secondary actions are Copy locator, Download .patch, Pick from
snapshot and All alternatives: Copy locator stays inline and Download .patch
opens the overflow menu, so the row shows no new control. The download saves
the change the row shows (the edit's diff), named after the cluster
(piwi-fix-cluster-N, or piwi-fix without one). A locator step without an edit
offers no download.

Docs: fix-plans.md says Copy locator copies the recommended locator and is the
primary when there is no line to rewrite, since it is also offered next to
Copy apply command; the portable route holds for both code-change steps.

Tests: the policy's secondary actions with and without an edit; E2E, the
seeded locator step with an edit (#87) downloads the edit's diff from the
menu.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN1TP3gsavgzTbFgfPDUkb
… demo seed

The two failure pages build their lines from the same shared handlers, but
nothing checked that the lines agree once they meet real data. This test
regenerates the demo seed into its own temporary directory, loads it into a
temporary libSQL database and builds, for every failure cluster, every
problem execution (failed, timed out, did not run) and every cluster's latest
occurrence, the lines each page shows: Most likely through pickMostLikely
(gated on the verdict on the execution page), the Next step and its source
line through nextStepSourceLine, the cluster's State and the execution's
Situation. It does so with and without an AI provider.

Six rules, each on kinds and sources rather than sentences:
- a verified fix reads the same in State and Next: a State that offers Mark
  resolved sits beside a Next that marks it resolved, and a Next that applies
  the diagnosed patch to a verified fix sits beside an unconfirmed fix with
  no Mark resolved of its own;
- a Next from the diagnosis quotes its summary when Most likely shows
  something else;
- a cluster and its latest occurrence's execution lead with the same Most
  likely;
- no Next says nothing explains the failure beside a strong or medium story
  or clue;
- no Situation or State sentence says "unassigned";
- a cluster-level Next (mark resolved, replace the locator, apply the patch,
  follow the diagnosis, see what changed) is the same step on the latest
  occurrence's execution.

The seed's cluster #10 reads "verified" because the seed stores no at-fix
patch check, so the test also rebuilds every diagnosis-verified cluster with
its patch recorded as still applying at the fix's commit, which exercises the
unconfirmed-fix pairing through the real handlers. A last test names the
seeded pages each rule must reach (cluster #10, #37 and cluster #1, clusters
#1, #2 and #10), so the file cannot pass without checking anything. Every
rule was seen failing against a deliberately broken builder. The did-not-run
rule joins with the step that opens the run.

The current code and seed break none of the rules. The test runs in about
20 s under app:test:unit. apps/application/AGENTS.md names it under Demo data
requirements.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN1TP3gsavgzTbFgfPDUkb
…lines coherence test

The rule that no Next says nothing explains the failure beside a strong or
medium Most likely is about the fallback steps, Diagnose with AI and
Reproduce locally. Its coverage pin named execution #37, whose Next applies
the patch in both AI modes, so a seed with no fallback step beside a strong
or medium clue would still pass. The pin now names execution #682 (a medium
clue, Next Diagnose with AI, and Reproduce locally without a provider), and
the test also asserts that both fallback steps are among the pages the rule
checks.

The execution pages built for the cluster rules now come from the cluster
handler's own latest occurrence, with the page's fallback to the
most-affected test's latest execution, instead of a hand-copied SQL query.
A cluster whose latest occurrence has no page built is a violation of the
same-Most-likely and same-step rules, never a silent skip.

The source line is built with the facts the pages pass: the patch and its
validation from the cluster's fix plan (buildFixPlan) and, when the step
replaces a locator, the execution's locator healing (getLocatorHealing), so
a failure prints the line the page shows.

The State kinds that offer Mark resolved are a set typed against
ClusterState['kind'], so a renamed kind fails the typecheck. The test header
lists the six rules, and apps/application/AGENTS.md shortens to one sentence
that points to it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN1TP3gsavgzTbFgfPDUkb
The next-step paragraph grew with the change shown in the Next row and the
fix checked at its commit, which took the page to 1,506 words.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN1TP3gsavgzTbFgfPDUkb
@github-actions

github-actions Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Performance

main fbc9ddd → #788 5d2783b · PostgreSQL 16.15 · large dataset (2,160 runs, 474,922 executions) · authentication on

No regression · 0 improvements · 217 unchanged

Pages

Page Server render SQL / render HTML Largest paint Full load SQL / full load API calls Blocking
Projects list 110 ms → 106 ms 33 174 kB 274 ms → 276 ms 373 ms → 376 ms 36 1 253 ms → 254 ms
Project › Runs 230 ms → 229 ms 61 515 kB 490 ms → 492 ms 1.17 s 94 7 506 ms → 500 ms
Project › Tests 251 ms → 233 ms 66 422 kB 470 ms → 486 ms 1.06 s → 1.08 s 99 7 456 ms → 453 ms
Project › Failures › Clusters 151 ms → 138 ms 61 309 kB 940 ms → 942 ms 836 ms → 837 ms 103 9 360 ms
Project › Failures › Flaky 144 ms → 142 ms 61 309 kB 350 ms → 352 ms 1.07 s → 1.06 s 115 10 431 ms → 417 ms
Project › Failures › Quarantine 176 ms → 172 ms 66 316 kB 392 ms 889 ms → 887 ms 99 7 382 ms → 374 ms
Project › Flake Lab 140 ms → 131 ms 61 304 kB 340 ms → 348 ms 873 ms → 907 ms 103 8 350 ms → 354 ms
Project › Gaps 133 ms → 139 ms 61 306 kB 354 ms → 348 ms 906 ms → 889 ms 103 10 346 ms → 354 ms
Project › Performance 146 ms → 147 ms 61 313 kB 358 ms → 356 ms 1.15 s 113 11 534 ms → 525 ms
Project › Settings 154 ms → 145 ms 63 326 kB 378 ms → 364 ms 870 ms → 864 ms 96 7 374 ms → 377 ms
Test run › Tests 146 ms → 156 ms 53 348 kB 876 ms → 888 ms 863 ms → 871 ms 71 2 402 ms → 385 ms
Test run › Changes 145 ms → 144 ms 54 337 kB 890 ms → 838 ms 760 ms → 707 ms 82 3 234 ms → 254 ms
Test run › Timeline 163 ms → 158 ms 53 675 kB 508 ms → 486 ms 988 ms → 977 ms 71 2 408 ms → 346 ms
API calls (29)
Request Response time SQL Response size
/api/auth/me 3 ms 1 270 B
/api/capabilities 6 ms 19 1 kB
/api/channels 4 ms 3 35 B
/api/projects 12 ms 9 23 kB
/api/projects/1 32 ms 7 168 kB
/api/projects/1/ai-steps?days=90 44 ms → 40 ms 4 91 B
/api/projects/1/capabilities 8 ms 21 1 kB
/api/projects/1/cluster-merge-suggestions 4 ms 2 12 B
/api/projects/1/failure-clusters?allBranches=false&fullRunsOnly=true 18 ms 7 15 kB
/api/projects/1/feature-map 4 ms 3 68 B
/api/projects/1/flake-lab?allBranches=false&fullRunsOnly=true 79 ms → 78 ms 9 6 kB
/api/projects/1/flake-suspects?testCaseIds=213,282,167,6,29,52,75,144,98,121,190,236,259,240,229,201,262,202,297 5 ms 3 14 kB
/api/projects/1/flaky-tests?runs=50&allBranches=false&fullRunsOnly=true 78 ms → 71 ms 8 8 kB
/api/projects/1/flaky-tests?runs=50&enrich=false&allBranches=false&fullRunsOnly=true 72 ms → 74 ms 7 7 kB
/api/projects/1/gaps?status=open&limit=200 6 ms 4 12 B
/api/projects/1/gaps/precision 3 ms 2 12 B
/api/projects/1/kept-runs 7 ms 6 2 kB
/api/projects/1/markers 4 ms 2 2 kB
/api/projects/1/performance?runs=50&allBranches=false&fullRunsOnly=true 6 ms 4 12 kB
/api/projects/1/quarantine 93 ms → 96 ms 10 938 B
/api/projects/1/quarantine?candidates=false 28 ms 5 938 B
/api/projects/1/slow-tests?allBranches=false&fullRunsOnly=true 18 ms 5 4 kB
/api/projects/1/timeout-opportunities?allBranches=false&fullRunsOnly=true 29 ms 6 16 kB
/api/projects/menu 3 ms 2 553 B
/api/settings/locale 2 ms 2 136 B
/api/subscriptions?projectId=1 4 ms 2 12 B
/api/test-runs/1500 20 ms → 21 ms 15 232 kB
/api/test-runs/1500/failure-groups 17 ms 15 2 kB
/api/test-runs/1500/insights 14 ms → 15 ms 10 13 kB
SQL per request, where it changed (4)

Test run › Tests — full load in the browser — 70 → 70 statements

Statement base head
select "failure_cluster_id", "key", "url", "provider", "connection_id", "title", "status_text", "status_color", "metadata", "origin", "created_at" from "entity… 0 1
select "failure_cluster_id", "key", "url", "provider", "connection_id", "status_text", "status_color" from "entity_links" where ("entity_links"."failure_cluste… 1 0

Requests the server made to itself while rendering (head): GET (document) 53 · GET /api/test-runs/1500/failure-groups 15 · GET /api/settings/locale 2

Test run › Changes — full load in the browser — 81 → 81 statements

Statement base head
select "failure_cluster_id", "key", "url", "provider", "connection_id", "title", "status_text", "status_color", "metadata", "origin", "created_at" from "entity… 0 1
select "failure_cluster_id", "key", "url", "provider", "connection_id", "status_text", "status_color" from "entity_links" where ("entity_links"."failure_cluste… 1 0

Requests the server made to itself while rendering (head): GET (document) 54 · GET /api/test-runs/1500/failure-groups 15 · GET /api/test-runs/1500/insights 10 · GET /api/settings/locale 2

Test run › Timeline — full load in the browser — 70 → 70 statements

Statement base head
select "failure_cluster_id", "key", "url", "provider", "connection_id", "title", "status_text", "status_color", "metadata", "origin", "created_at" from "entity… 0 1
select "failure_cluster_id", "key", "url", "provider", "connection_id", "status_text", "status_color" from "entity_links" where ("entity_links"."failure_cluste… 1 0

Requests the server made to itself while rendering (head): GET (document) 53 · GET /api/test-runs/1500/failure-groups 15 · GET /api/settings/locale 2

GET /api/test-runs/1500/failure-groups — 15 → 15 statements

Statement base head
select "failure_cluster_id", "key", "url", "provider", "connection_id", "title", "status_text", "status_color", "metadata", "origin", "created_at" from "entity… 0 1
select "failure_cluster_id", "key", "url", "provider", "connection_id", "status_text", "status_color" from "entity_links" where ("entity_links"."failure_cluste… 1 0
How this is measured

Each build ran as its own production server (node .output/server/index.mjs) on its own copy of the same dataset, signed in as an administrator. Servers were measured in turn, alternating order, after 2 warm-up rounds: 12 server renders and 6 browser loads (Chromium, warm cache, until the page's own requests stop) per page, and 8 calls per API request. Times are medians. Largest paint is when the page's main content first shows (LCP); full load is when its last request ends; blocking is main-thread time past 50 ms per task.

A time is a regression when the median grows past both floors and the Mann–Whitney test gives p < 0.01 (which takes at least six samples a side); a count (SQL statements, bytes, requests) when it grows past both floors. Floors: server render 10 % and 15 ms, sql per render 10 % and 3, db pages read per render 25 % and 1000, html size 10 % and 20 kB, largest paint 10 % and 50 ms, full load 10 % and 75 ms, sql per full load 10 % and 5, api calls 25 % and 3, main-thread blocking 25 % and 50 ms, dom nodes 15 % and 1000, response time 15 % and 5 ms, sql 10 % and 2, response size 10 % and 20 kB.

SQL counts come from pg_stat_statements, which sees every build; statement lists are normalized by PostgreSQL.

Machine: 4 × AMD EPYC 7763 64-Core Processor, Node v24.21.0. Suite: apps/application/scripts/perf. Run and full results.

Copy link
Copy Markdown
Collaborator Author

Lint, typecheck and unit tests failed on 5a154b4 in packages/editor/tests/lsp.test.ts › reports its context (reachedFiles 0, expected 1). This PR does not touch packages/editor, and the same job passed on the previous head 50f1baa. The test is racy: beforeAll waits until piwi/status reports connected, but the code index (which sets reachedFiles) is fetched after the connection, asynchronously. Locally on this branch it fails 1 run in 5.

No fix exists yet. Proposed patch, outside this PR's scope:

// packages/editor/tests/lsp.test.ts, 'reports its context'
const status = await waitFor(async () => {
  const s = (await client.sendRequest('piwi/status')) as StatusResult;
  return s.contexts[0]?.reachedFiles ? s : undefined;
});

Re-running the failed job once.


Generated by Claude Code

claude added 21 commits October 9, 2026 08:20
The Attempts tab and the timeline each spelled out when one duration is much
slower than another. shared/duration-standout.ts now holds the rule once:
SLOWER_FACTOR (2) and SLOWER_MIN_DELTA_MS (1 s), with isMuchSlower(ms, thanMs),
and stepMatchKey(step), the label plus params key a step is matched on from one
execution to another (attempt-diff's private params signature).

attempt-diff imports them: a step is much slower by isMuchSlower, a request
keeps its absolute floor (REQUEST_SLOW_MS, 1 s in all) with the shared factor.
Its results do not change, attempt-diff.test.ts passes untouched. The usual
duration of a step, which reads the same rule, comes with the timeline next.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN1TP3gsavgzTbFgfPDUkb
…eline

A step or a request only stood out when it took a third of the test, so a
click that usually takes 600 ms and took 2.5 s in a long test stayed grey. The
timeline now carries each one's usual duration, and the slower-than rule of the
Attempts tab colors it.

getUsualDurations (shared/handlers/test-cases.ts) reads the test's last five
passing executions on the same browser, in runs eligible as a baseline, the
execution itself left out, then their requests: two bounded queries, called by
getFailureTimeline after the pure build, so the REST route and the demo share
it. buildUsualDurations and attachUsualDurations (shared/duration-standout.ts)
take the median per step (stepMatchKey, falling back on the label when the
params change from run to run) and per request route, kept when at least two
executions ran it, and set TimelineItem.usual.

durationStandout gains usualMs and the `usual` reason: at least 1 s, twice the
usual time and 1 s more; a third of the test still wins as `share`. A row that
stands out by its usual time says it under the duration ("usually 600ms"), the
hover names it too. On the seeded executions 37, 13 and the clusters' latest,
the rule colors no row beyond the share rule's (the 28 s Pay click and the
28.4 s request on #37); the usual durations cost about 1.5 ms per /timeline
call on the seeded SQLite database.

Docs: the OpenAPI description of /timeline, the case.evidence help (the exact
rule), the Timeline bullet of features/evidence.md (the page diff's `+3 -1 ~2`
summary cut to stay within the 1,500-word budget, now 1,498), the AGENTS.md
duration rule. New scene timeline-usual-duration; E2E in
timeline-durations.spec.ts (the API sends usual 600 and 300, the click and the
request stand out by `usual` and say their usual time).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN1TP3gsavgzTbFgfPDUkb
The Network tab and the trace's request list colored a request by fixed
thresholds (orange from 500 ms, red from 1 s), so the 28.4 s quote request of
execution 37 read in the warning tone on the Timeline and in failure red on the
Network tab, and every sub-second request turned orange.

useExecutionTimeline (app/composables) reads /timeline under one key per
execution (execution-timeline-<id>, dedupe 'defer', getCachedData
reuseWithinRender). FailureTimelineCard reads it there, and EvidenceTabs holds
it for its Timeline and Network tabs, so a tab switch never fetches it again; it
loads only for a failed execution, when one of those two tabs is open. The
EvidenceTabs change is kept to that call, the per-request usual map and two
props on TestCaseNetworkRequests.

TestCaseNetworkRequests applies durationStandout to each request (the test's
duration and the request's usual time from the timeline, by index) with the
duration-tone classes, data-standout and the reason on hover, and shows
"usually 300ms" beside a request that stands out against its usual time.
TraceNetworkList, which has no index in common with the timeline, applies the
share of the test; its bars take the warning tone when they stand out, the
failed palette when they failed, neutral otherwise. The hard-coded reds of both
views (failed span, failure text, error log counts, row accent, failing window)
come from STATUS_PALETTE and the status-failed token. TestCasePerformancePanel
keeps the standard Web Vitals bands through one ratingClass helper: poor in
text-error, needs improvement in the warning tone, good uncolored (no brand
green in the evidence card).

AGENTS.md's duration rule now covers the Network tab and the trace's list, and
the case.evidence help says Network follows it. New scenes
network-tab-durations(-mobile) on execution 37. E2E: the Network tab colors the
9 s quote and the usual-time report request, not the quick ones; on the
execution page fixture neither quick request is colored, no second /timeline
request on a tab switch, no hydration mismatch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN1TP3gsavgzTbFgfPDUkb
A test that failed and then passed on retry opened on a green "Passed"
chip on a purple edge, with no headline, a "Passed on retry" badge and no
next step; the server built no verdict for it because the passing row has
no error, so its `why` was null and its computed step was "Reproduce
locally" (or "Diagnose with AI").

Server: getTestRunCase loads the last failed attempt of the same run, test
and browser (one select, only for a retry pass) and builds the verdict from
its error and steps. FailureVerdict.attempt names that attempt and the
execution that holds it; the verdict carries no cluster, which stays on
the failed attempt's page. The policy reads `why` = passed-on-retry from
the status even when the failed attempt is not stored, so a retry pass
always reaches the Flake Lab rows or "Compare the failing attempt with the
passing one". The since line of a retry pass drops the "Passed on retry"
lead (the status chip says it) and "first failed in this run", leads with
"Newly flaky" only when the test newly passes on retry, and says "failed
on attempt 1, passed on attempt 2", with "attempt 1" a new `attempt` part
linked to the failed execution; commit, author and owner stay. MCP
explain_failure falls back to the verdict's headline, so an agent reads
the failed attempt's error too.

Page: StatusChip takes the execution's retries and reads "Passed on retry"
in the flaky color and icon, and every chip is in sentence case ("Didn't
run", "Timed out") rather than CSS capitalize. The header drops its
"Passed on retry" badge. A retry pass shows the failed attempt's headline
with the meta line under it (the attempt clause and the latest line), the
Next row, and its evidence on Attempts; it shows no Most likely, no
toolbox, and fetches neither /clues nor locator healing.

Coherence test: every retry pass of the seed enters the loop, and its
Next must be compare-attempts or a Flake Lab step, with no Most likely
(#768 and #21 are named as reached).

Docs: evidence.md says a retry pass leads with its failed attempt's error
and opens on Attempts, and cuts two sentences to stay inside its budget;
first-failure.md, concepts.md, flaky-tests.md (same word count) and the
regression-or-flaky recipe describe the chip and the attempt clause. The
help topic of the block names the attempt clause. Report scene
execution-retry-pass-flake-lab (#21); retry-pass-clarity(-mobile) are the
before and after of #768.

Tests: failure-verdict (attempt), situation (the retry-pass since line),
new execution-states (libsql: verdict, attempt clause, compare-attempts,
a retry pass without its failed attempt), status-palette
(statusChipLabel), mcp-situation-owner (explain_failure on a retry pass),
E2E execution-states.spec.ts and the seeded #768 and #21 in
test-run-case-page.spec.ts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN1TP3gsavgzTbFgfPDUkb
…a step that opens the run

A test that did not run opened with no Most likely, a "Did not run" card
under the block whose cause badge repeated its sentence (text-primary
links, grey prose), and a next step of "Reproduce locally" or "Diagnose
with AI" when no blocker was known: #748, cut by its run's failure
budget, offered to reproduce a test that never started.

Server: shared/did-not-run.ts builds one sentence per reason
(describeDidNotRun): the hook or the serial-group failure that blocked
it, with the blocking test as a part linked to its execution; the run's
failure budget with its failed count; the global timeout; an
interruption; no reason recorded. Run cutoffs carry the note "Reported by
Playwright", a failed beforeAll says what it does to its group.
getTestRunCase returns it as `didNotRun` (null for an execution that
ran), and MCP explain_failure adds its text.

Next: a new row 1b, kind `open-run` with no secondary actions, for a
test that did not run with no blocker. A max-failures cutoff with one
failed execution in the run opens it ("Open the failure that stopped the
run"; one select of at most two rows, only then); with several, or an
earlier failure of its group with no blocker found, "Open the run's
failures" (/test-runs/:id?tab=failure-groups); a global timeout "See what
made the run slow" (the run's Timeline tab); an interruption or no reason
"Open the run". Its why, and the open-blocker why, say what to do and
never restate the reason Most likely now gives. useNextStepActions
navigates `open-run` to the run and its tab.

Page: DidNotRunLine renders the sentence in the Most likely row (body
style, the blocker a sentence link, the note in the meta style, no badge
or icon). DidNotRunCard is deleted; its blocked-tests branch had no
caller, the toolbox's Blocked section lists them. The seed and the
reporter send no steps for an unrun test, so the evidence card already
stays away on #748; EvidenceTabs keeps its rule. adr/ui-simplification.md
is left as the record of its time.

Coherence test: its did-not-run rule is on: every did-not-run execution
gives its reason and its Next is open-blocker or open-run (#748 and #7 are
named as reached).

Docs: evidence.md (one sentence, and a shorter Attempts lead), the Most
likely and Next sections of first-failure.md, "open the run" in the
policy of fix-plans.md, which also drops a sentence of the plan's loop to
come back within its 1,500-word budget; the situation help names the
did-not-run reason. Report scene execution-did-not-run-blocked (#7);
did-not-run-clarity(-mobile) are the before and after of #748.

Tests: did-not-run (each reason, the hooks, the blocker part), next-step
(open-run by reason, one against several failures, the blocker row wins,
whys that do not restate the reason), execution-states (libsql: the
failed count, the blocker link, the run's one failure), fix-sections,
E2E execution-states.spec.ts (a run cut at its failure budget: Most
likely, no evidence card, Next opens the run; the blocked test links its
blocker; 375 px) and the seeded #748 in test-run-case-page.spec.ts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN1TP3gsavgzTbFgfPDUkb
…ce across its retries

A test that did not run because its run reached its maximum number of
failures opens the run's one failure when there is one. That case counted
the run's failed rows, and each attempt is its own row: a test that failed
on two attempts, or a test whose first attempt failed before it passed on
retry, gave two rows. With retries on, Most likely read "(1 failed)" from
the run's distinct-test counter while Next read "Open the run's failures".

getTestRunCase now reads the run's failed rows and its retry rows (the only
rows that can be or replace a final failed attempt), keeps one final
attempt per test and browser with finalAttempts, and opens the one left
when it is a failure. A test that failed on every retry opens its last
attempt, and a test that passed on retry is not counted. The query runs
only while the run's failed counter is at most one, so the sentence's count
and Next agree.

Tests: execution-states adds a run with one test failing on retries 0 and
1 and a flaky test whose first attempt failed: Next is open-execution on
the last attempt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN1TP3gsavgzTbFgfPDUkb
…explain_failure

explain_failure gives a test that passed on retry its failed attempt's
headline, while its error is empty and its steps are the passing
attempt's: an agent read a failure with nothing failing under it and no
way to open the attempt that failed without listing the run.

The tool adds `failedAttempt`, the executionId and retry index of the
attempt the headline comes from (the verdict's attempt), so an agent calls
explain_failure on it in turn. The field is additive and absent on any
other execution; the tool description in shared/mcp-tools.ts names it.

Tests: mcp-situation-owner checks failedAttempt on the retry pass and its
absence on the failed attempt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN1TP3gsavgzTbFgfPDUkb
… per row

The Situation help on an execution page was one 146-word paragraph whose
first sentence held four nested parentheticals. It is a lead sentence and
a list, as the inline-help rule asks of a hint longer than two sentences:
Headline (with the line under it, and the failed attempt of a test that
passed on retry), Most likely (the rule that picks it, or why a test did
not run), Next and Cluster.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN1TP3gsavgzTbFgfPDUkb
…Timeline

The evidence page said a passing execution other than a retry pass shows
"identity and facts only", which reads as if it had no evidence card; it
has one, open on Timeline. The sentence names it again, and two phrasings
elsewhere on the page are shorter so the page stays at 1,497 words.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN1TP3gsavgzTbFgfPDUkb
…ers neutral

The selected row of Affected tests, the opened attempt on the Attempts tab and
the current item in the history strip and the facts line's attempts were drawn
in the primary color, so green marked a selection as loudly as the page's one
primary action.

- TestRow marks the active row with SELECTED_ROW_CLASS (a raised surface in a
  neutral ring); the cluster's bulk bar sits on bg-elevated, and Quarantine is a
  neutral outline button.
- AttemptsCard: the opened attempt takes SELECTED_ROW_CLASS with aria-current,
  "this one" is meta text, "open", "View in" and the flake-suspect link are
  neutral links, and the only-on chips use the status palette.
- ExecutionFactsLine and HistoryStrip ring the current item with
  CURRENT_ITEM_RING_CLASS (ring-inverted, offset by the page background so it
  reads on any status color in both themes); the history's "Last passed in"
  link keeps its sentence's color.
- AGENTS.md names the two new constants in the one-accent rule.
- New report scenes cluster-affected-tests-selected(-dark); the cluster layout
  E2E checks the clicked row is the one pressed row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN1TP3gsavgzTbFgfPDUkb
The evidence card and the affected tests now draw failures in the status
palette, standing-out durations in the one warning tone and selections in
neutral classes. Two tests keep them that way.

- tests/unit/evidence-accent.test.ts scans the evidence card's and the affected
  tests' components (EvidenceTabs, FailureTimelineCard and its parts,
  FailingStepSnapshot, AttemptsCard, ClusterAffectedTests, TestRow) for a raw
  red, orange or green Tailwind shade and for a primary class outside hover: and
  focus-visible:. Run against the files before the timeline work and before the
  neutral selection commit, it fails on each of them.
- tests/failure-page-accent.spec.ts renders a failing execution (Timeline,
  Network and Console tabs) and a two-test cluster (a selected row and the bulk
  bar), swaps the primary color on the root and fails on any element whose
  color, background, border, ring, fill or stroke follows it; it also allows at
  most one solid primary button on each page.
- A card's header icon keeps SectionCard's color through one constant,
  CARD_ICON_CLASS, and carries data-card-icon so the E2E leaves it out.
- AGENTS.md cites both tests in the one-accent rule and the timeline duration
  rule.

The Network tab's TestCaseNetworkRequests and TraceNetworkList still color a
request by fixed thresholds, so the scan does not list them yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN1TP3gsavgzTbFgfPDUkb
The Most likely line names a request and a console entry, but nothing in the
evidence card pointed at them: the Timeline row of the 28.4 s request on
execution 37 read like any other, every tab looked alike, and a citation chip
opened its tab scrolled so the tab strip sat above the view, with no row
marked.

- pickMostLikely returns the line's citations: every citation of the story's
  clues, or the lone clue's; none for a diagnosis, whose citations name
  sections only. useEvidenceHint hands them to the evidence card.
- citedItemIds (app/utils/timeline-rows.ts) matches them to the timeline
  items, a request, a console entry, a dialog or a step by index, a request's
  backend logs to the request, the failing steps to the failed step. Those rows
  read "Most likely" in meta text and carry data-cited, in the table and the
  phone cards.
- The tabs holding a cited section carry a small neutral dot, titled "The Most
  likely line cites this tab" (Screen, Network and Console on execution 37).
- The section locator takes the citation's index: StoryLine, CluesCard and a
  click on a timeline request or console row pass it. EvidenceTabs keeps its
  tab strip at the top of the view and brings the cited row under it; a cited
  request or console entry is ringed for two seconds (CITED_ROW_CLASS, a
  neutral ring), and the Network tab shows it whatever filter was open.
- CluesCard's citation chips lose the down-arrow icon that read as Download.
- evidence.md says a citation opens and marks its evidence and that Most likely
  marks the rows and tabs it cites (two cuts keep the page at 1,497 words);
  the Evidence help and ARCHITECTURE.md say the same.
- Unit tests for citedItemIds and the line's citations; the timeline-durations
  E2E checks the cited request's row and tab, and that its Network citation
  opens the tab with the strip in view and the request ringed. New report scene
  timeline-cited-row (execution 37: the marks, then the Console and Network citations).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN1TP3gsavgzTbFgfPDUkb
…ary color

The Network tab drew its open filter (All, Failed, With logs) and its
Captured / Full trace switch in Nuxt UI's default primary color, and its
"Learn more" link sat in the primary green inside a sentence. Both are a
selection or a link in prose, never the page's primary action, so the failure
page accent E2E flags them on the evidence card.

- Both UTabs take color="neutral".
- The link keeps the sentence's color (text-inherit with SENTENCE_LINK_CLASS).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN1TP3gsavgzTbFgfPDUkb
The passed-on-retry, did-not-run and cited-row changes each kept
features/evidence.md under 1,500 words on their own; together they reached
1,506. The intro drops "where it came from" and the situation block's parts
are listed without articles.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN1TP3gsavgzTbFgfPDUkb
…y classes

The Network tab, the trace's request list and the Performance panel now color
a duration through durationStandout and the one warning tone, so the accent
scan covers TestCaseNetworkRequests, TraceNetworkList and
TestCasePerformancePanel too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN1TP3gsavgzTbFgfPDUkb
The server-side processing time was emerald, so a 28.4 s server time read as
good next to the same request's 28.4 s duration in the warning tone. It is a
duration like the others and takes no color of its own; the duration rule
colors the request's total when it stands out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN1TP3gsavgzTbFgfPDUkb
…he headline does

The Attempts tab listed "Attempt 1" then "Retry 1", while the line under the
headline and the facts line say "failed on attempt 1, passed on attempt 2"
for the same executions. Every row now reads "Attempt N", counted from 1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN1TP3gsavgzTbFgfPDUkb
…st likely"

A cited row read "Most likely" on its own, the same words as the situation
block's row label: a reader could take the request for the cause itself, and
a page query for the label matched the rows too. The rows now read "Cited by
Most likely", in the same meta text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN1TP3gsavgzTbFgfPDUkb
The passing-execution test looked for a "Fix" heading, which the toolbox no
longer carries under any condition ("More ways to fix"), so the check could not
fail. It now counts [data-shot="fix"]. The spec's comments name the card as the
page does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN1TP3gsavgzTbFgfPDUkb
…e pages

A pass over the docs-and-scenes checklist of the failure-pages plan:

- case.evidence help lists every tab (Attempts and Locators included), says
  which tab the card opens on (a retry pass on Attempts) and what the cluster's
  subject line names; case.timeline says a step opens its parameters from its
  title; fix.toolbox and first-failure say the execution links to the cluster's
  fix plan, which is Markdown on the cluster page only.
- evidence.md: the usual-time rule needs 1 s more as well as twice the time.
- ai-diagnosis.md: an execution shows the cluster diagnosis's summary and patch.
- mass-failure.md: the section the Next line points at follows the block as its
  own card.
- Comments name "More ways to fix" as the page does, the toolbox orders its
  sections, LocatorHealingPanel says when it folds, AttemptsCard numbers every
  attempt, and cluster-state.ts describes the line as it is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN1TP3gsavgzTbFgfPDUkb
Recaptured every docs scene on a freshly seeded database with the clock
frozen (app:screens:docs, the server started with an AI provider so the
diagnosis shows its configured state), and kept the images whose subject
changed on this branch:

- the execution page (gather-evidence, tour-execution) and its Locators tab;
- the cluster page's situation block (cluster-issue-line) and diagnosis
  (ai-diagnosis, tour-ai-diagnosis);
- locator healing's recommended fix, the Create issue dialog and its
  required fields;
- the run's Changes tab and the failure-clusters grouping, whose seeded
  timeouts now last their 30 s;
- the four other tour-*.png, recaptured with the two above so the README
  tour reads as one set.

The other docs images differed only by timestamps or by the server's
environment and are left as they were. The blog post's alt texts for
gather-evidence.png and ai-diagnosis.png describe what those images show.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DN1TP3gsavgzTbFgfPDUkb
@PhenX-Pro
PhenX-Pro marked this pull request as ready for review October 9, 2026 10:30
@PhenX
PhenX merged commit e8cbef2 into main Oct 9, 2026
60 of 62 checks passed
@PhenX
PhenX deleted the claude/modest-newton-zbvctt branch October 9, 2026 10:30
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.

3 participants