Compare Workflow¶
Example entry & triage (Phase 107)¶
- Try a sample pair:
TryCompareExampleChipsin capture (until a comparison exists) and Compare guide loads curated fixtures fromsrc/examples/plans/;onCompare(overrideA?, overrideB?)passes fresh text intocompareWithPlanTexts. - Summary: Start here band surfaces
comparisonStory.overviewonce; the long wall-clock vs structure paragraph is under details; Next steps pinning explanation is shortened with optional details; metric meta line uses saved in this app’s database instead of “server SQLite” on the user-facing string. - Empty state:
compareEmptyStateCopypoints at samples + ? tour.
Phase 108 — scan-first Compare, second sample, diff strip¶
- Samples: Two pairs — seq scan → index scan and bitmap heap → index scan (see
src/examples/README.md). - Lead:
compareLeadTakeawayheadline is Change at a glance;compareFollowUpDiffSignalsrenderscompare-scan-signals(Also scan) with Highlight in diffs buttons (pinsfd_*via existing highlight state). - Disclosure: Walkthrough + structural readout paragraphs are behind Walkthrough and structural readout
<details>by default.
Phase 110 — selected-pair bridge, Show row proof, calmer meta copy¶
- Selected pair:
compareTriagePairBridgeLinerenders a one-line bridge when the pair matches pins or top worsened / top improved (Phase 111 renames the label to Context and extends the bridge—see below). - Scan strip: Show in list (was Show row) is covered by Playwright (active finding-diff outline in viewport).
- Copy: Summary meta and lanes hint use clearer “saved snapshot / who can open” wording.
Phase 111 — decision rhythm, briefing → row → pair¶
- Bridge:
compareTriagePairBridgeLinealso matchescomparisonStory.changeBeatspair focus (Open pair from the story) before falling back to top worsened/improved. - Navigator:
briefingHighlightPairadds Same row as Change briefing on worsened/improved rows when a finding diff is highlighted from the scan strip; finding-diff rows show Highlighted from Change briefing when active. - Selected pair: Bridge label Context; follow-up block Suggested follow-up with
pqat-comparePairNextStepvisual chain from bridge to next step. - Navigator filter: Open top regression (was jump to hottest).
Phase 131 — pair thread hint (Analyze parity)¶
- Handoff:
ComparePagederivescomparePairHandoffKind(summary|briefing|pinned|navigator) from triage bridge, continuity fallback, URL pins, or default list selection. - UI:
CompareSelectedPairPanelshows Pair eyebrow + compact hint;data-testid="compare-visual-pair-continuation-contract"fore2e-visual. (Phase 133 refines visible copy for saved-link reopen and regionaria-describedby— see below.) - Analyze: Ranked header uses Continues from plan (no repeated operator mono); row Opened from detail cue removed when the header band is present; visual contract
analyze-visual-ranked-continuation-contract.
Phase 132 — responsive continuity, saved-link handoff copy, structural contracts¶
- Compare handoff origin:
compareHandoffOrigin—linkwhen the comparison is loaded viagetComparison(/compare?comparison=…),sessionafter Compare runs from plan text in this tab (onCompare). DrivescomparePairHandoffDisplayText(session strings: From the summary, From the briefing, Pinned focus, From the lists — Phase 134 refineslink+ pinned/navigator; see Phase 133–134). - Narrow layout:
layoutTier === 'narrow'always stacks navigator above pair in the DOM (reading + tab order), independent ofmainColumnOrder(medium/wide still respect column order). - UI: Continuation band
role="region"+aria-labelledbyoncompare-selected-pair-heading;data-testid="compare-pair-handoff-hint"+data-pqat-handoff-origin(link|session). Phase 133 addsaria-describedbyon the region (hint id) and visible — reopened wording for summary/briefing whenlink. - Analyze narrow: Lower band maps in findings → suggestions → selected node order when stacked; ranked continuation contract
role="region"+aria-labelledby,analyze-ranked-handoff-hinttest id.
Phase 133 — legible reopen provenance, narrow skip-to-pair, calmer region announcements¶
- Visible handoff (saved link):
comparePairHandoffDisplayTextuses Summary — reopened / Briefing — reopened whencompareHandoffOrigin === 'link'and the handoff kind issummary/briefing; session paths stay From the summary / From the briefing. Pinned / navigator +linkwere later aligned to Pinned — reopened / From the lists — reopened (Phase 134). - Narrow keyboard path:
SkipToPairInspectorLink(Skip to pair inspector) between navigator and pair whenlayoutTier === 'narrow'; target#compare-pair-inspector-region(tabIndex={-1}onComparePairColumnroot) — same pattern as Analyze Skip to ranked findings. - Continuation a11y: Pair continuation
role="region"usesaria-labelledby="compare-selected-pair-heading"+aria-describedby→ thread hint id only (hint not duplicated onh2). Analyze pivot band: inner contractaria-describedby→analyze-ranked-handoff-hintid;h2does not also describe the thread hint when arrival cue is off. - Tests: Playwright reopen asserts
toContainText(/Summary — reopened/)oncompare-pair-handoff-hintfor restored?comparison=/ deep link; corrupt-analysis visual seed uses retry onPOST /api/e2e/seed/corrupt-analysis.
Phase 134 — Analyze reopened ranked cues, narrow Compare keyboard proof, handoff matrix¶
- Analyze:
analyzeRankedHandoffOriginonAnalyzePage(linkaftergetAnalysis,sessionononAnalyze/ Clear).analyzeRankedContinuityCopy: non-pivot bandRanked — reopened(analyze-ranked-restored-hint); graph pivot thread Continues from plan — reopened whenlink. Ranked rootaria-describedby→ restored hint id when the band hint is shown. - Compare copy:
link+ pinned / navigator → Pinned — reopened / From the lists — reopened (same — reopened family as Analyze + summary/briefing). - E2E:
viewport800px — Tab pathfocus()+Enteron Skip to pair inspector →#compare-pair-inspector-regionfocused; Analyze reopen + pivot proves Ranked — reopened then Continues from plan — reopened; reopen→export asserts Ranked — reopened. - Vitest:
analyzeRankedContinuityCopy.test.ts;CompareSelectedPairPanelmatrix (briefing / pinned / navigator +link);AnalyzeFindingsPanel.virtualrestored + pivotlink.
Phase 135 — Broader handoff proof, Tab-chain skip, shared reopened copy, matrix doc¶
- Shared suffix:
withReopenedSuffixinreopenedContinuityCopy.ts— single place for… — reopenedused bycomparePairHandoffDisplayTextandanalyzeRankedContinuityCopy. - Export:
exportDownloadSuccessHint(..., { restoredFromLink })when snapshot export follows a saved?analysis=/?comparison=load (aligned with reopened cues). - Pair panel cue gate:
compareContinuityCueIsSpecifictreats join strategy shift / join strategy phrases as specific so short engine cues like Same region · join strategy shift can drive the soft Reading thread panel (compare-selected-pair-continuity-fallback) and thebriefinghandoff thread (multi-pair compares only). - E2E: Briefing — reopened — real compare on
rewrite_nl_orders_lineitems+rewrite_hash_orders_lineitems(synced intoe2e/fixtures), scan Worsened / Improved rows until the soft continuity panel is visible without the Context summary bridge, assert From the briefing, persist URL, reopen and assert Briefing — reopened + continuity fallback visible; narrow Compare Tab from first Worsened pair row untilcompare-skip-to-pair-inspectorfocused, thenEnter→#compare-pair-inspector-regionfocused.
Phase 136 — Analyze URL stability, lists handoff proof, cue rules table¶
- Analyze: deep-link query sync (
?analysis=/node=) runs inuseLayoutEffectso the address bar matches the committed analysis before paint; Playwright waits ontoHaveURL(/analysis=/)before readingpage.url()on persist/reopen flows. - Compare E2E: From the lists — reopened on nested loop amplification ↔ misestimation fixtures (no per-pair continuity cues; navigator-only row, then
?comparison=reopen). - Cue gating:
compareContinuityCueSpecificity.ts+COMPARE_CONTINUITY_CUE_CLASSIFICATION_FIXTURESgolden table (compareContinuityCueSpecificity.test.ts);compareOutputGuidancere-exportscompareContinuityCueIsSpecific. - Docs: handoff-kind vs Context / Reading thread footnote under the pair-thread matrix (this section).
Compare pair thread (user-visible, compare-pair-handoff-hint)
| Kind | Session | Saved link (compareHandoffOrigin === 'link') |
|---|---|---|
| summary | From the summary | Summary — reopened |
| briefing | From the briefing | Briefing — reopened |
| pinned | Pinned focus | Pinned — reopened |
| navigator | From the lists | From the lists — reopened |
Handoff kind vs pair context (Phase 136, contributor QA):
summary:compareTriagePairBridgeLinereturned a Context line (top worsened/improved, story beat, or URL pin that maps to this pair) — not the soft Reading thread panel.briefing: no Context bridge, butregionContinuitySummaryCuepassedcompareContinuityCueIsSpecific→ softcompare-selected-pair-continuity-fallback(Reading thread body). Needs a pair with a cue; on single-pair compares the only row is often “top worsened” →summaryinstead.navigator: no Context bridge, no qualifying cue (or cue filtered as non-specific), no URL pins → From the lists.pinned: afinding=/suggestion=/indexDiff=pin is active and the Context bridge from pins does not override (seecomparePairHandoffKindinComparePage).
Analyze Ranked (analyze-ranked-handoff-hint / analyze-ranked-restored-hint)
| Surface | Session | Saved link |
|---|---|---|
| Graph pivot thread | Continues from plan | Continues from plan — reopened |
| Ranked band (no pivot) | (no reopened band) | Ranked — reopened |
Phase 112 — URL/summary bridge, reading-thread language, export parity¶
- Summary cue: Change briefing meta uses Reading thread · (replaces Continuity ·) so tone matches Context on the pair panel.
- Selected pair: When
compareTriagePairBridgeLineis empty but the summary still has acontinuitySummaryCue,continuityPairFallbackshows a soft Reading thread note (compare-selected-pair-continuity-fallback) so deep-link / URL-only emphasis still reads as one thread. - Bridge copy:
compareOutputGuidanceuses highlighted phrasing for finding / index / suggestion URL-driven lines. - Clipboard: Pinned-summary line label is Link includes: (
formatComparePinnedSummaryLineinartifactLinks.ts).
Phase 113 — Compare export triage, guided cohesion, continuity proof¶
- Download: With both plan texts in Plan inputs, Export Markdown / HTML / JSON calls
/api/compare/report/*and prepends/injectscompareExportTriage.ts(Reading thread block: change lead + optional pair id).
Phase 115 — Reopen→export confidence, server reading thread, calmer trust states¶
- Exports: Server-rendered Markdown/HTML include
## Reading thread/ Change at a glance (compact lead from the change story or top finding diff). The client adds Selection context only when the UI has a pair bridge or primary pair id—no duplicate “full preamble” vs server. - Reopen→export: Playwright proves
?comparison=reopen with empty plan boxes → Markdown download → request body containscomparisonsnapshot; file contains server Reading thread markers. - Trust copy: Compare/Analyze loading banners, artifact error titles, export success hint, Compare Running comparison banner.
- Export UX:
compare-export-snapshot-cuewhen exports use loaded snapshot; post-export status line.
Phase 116 — Export trust, Analyze reopen parity, clearer failures¶
- Serializer alignment:
ArtifactPersistenceJson.ApplyToHttpSerializerOptionsapplies persistenceJsonSerializerOptionstoConfigureHttpJsonOptionsso report POST bodies cannot silently drift from SQLite JSON (duplicate custom converters are skipped if the helper runs twice). - Compare continuity:
compareContinuityCueIsSpecifictreats very short vague tokens (mixed, unclear, varies, …) as non-specific so weak Reading thread fallback panels stay hidden. - Analyze parity: Reopen→export browser proof and capture Download UX live in Analyze workflow (
analyze-export-snapshot-cue,formatApiErrorResponsefor export failures).
Phase 117 — Clearer export failures, Compare parity, quieter guidance¶
- Report API:
/api/compare/report/*shares the same 400 JSON contract as Analyze report routes (request_body_invalid/export_request_incomplete); see API & Reports. - Export UX: Compare Download success copy matches Analyze (Saved result exported… when both plan boxes are empty and a snapshot drives the export; otherwise Download started…).
compare-export-statususesaria-live="polite"+aria-atomic="true"; capture hint text is slightly tighter. - Trust:
formatApiErrorResponsemaps the new codes to a single productmessageline (noerror:prefix for those codes).
Phase 118 — Stable export contracts, browser-proven trust, calmer sharing copy¶
- Backend: Analyze report semantic validation is
ReportExportValidation+ReportExportBadRequestExceptionHandler(ProblemDetails for other failures). See API & Reports. - Playwright:
Compare: reopen with empty plan inputs exports HTML using snapshot payloadproves snapshotPOST /api/compare/report/html(not only markdown). - Sharing / auth help:
ArtifactSharingPaneltitles and fine print are more product-facing.
Phase 119 — Take-with-you export, JSON parity, calmer success¶
- Capture: Eyebrow Take with you; shorter snapshot vs paste hints; format button
titles. - Parity: Playwright
Compare: reopen with empty plan inputs exports JSON using snapshot payload(POST /api/compare/report/json). Analyze gains reopen→HTML proof in Analyze workflow. - Sharing: Save confirmation Sharing updated.
Phase 120 — Analyze JSON parity, Take-with-you legend, sharing scope note¶
- Analyze parity: Playwright
Analyze: reopen with empty plan input exports JSON using snapshot payload(POST /api/report/json) — closes the reopen→export matrix alongside markdown/HTML (Phase 119) and Compare JSON. - Take with you:
compare-export-format-legendone-liner (Markdown / HTML / JSON roles);compare-export-snapshot-cuetightened; Ready / Started export status lines. - Sharing:
artifact-sharing-effect-note+ Sharing saved… (scope applies on next open) — aligned with Analyze workflow.
Phase 121 — Handoff surface, export voice, continuity cues¶
- Take with you: Same handoff pattern as Analyze workflow —
pqat-handoffBand,compare-export-handoff-kicker,pqat-formatLegend, sharedexportDownloadSuccessHintfor export status. - Continuity:
compareContinuityCueIsSpecific/resolveComparePairFallbackDisplay— seecompareOutputGuidance.test.ts(Phase 121 cases).
Phase 114 — Reopen/export cohesion, landing proof, lower-noise guidance¶
- Download: If both plan fields are filled, the server rebuilds the report from that text; if not (typical reopen from link), the client sends
{ comparison }to/api/compare/report/*so exports stay coherent without re-pasting plans. Per-format buttons show Preparing… only on the active download. - Pair fallback: Short non-specific continuity cues no longer show a filler Briefing link panel—Reading thread still appears when the cue is structurally specific (
compareContinuityCueIsSpecific). - Analyze guide echo: Overview + Also scan path uses copy that does not pretend the rail is always the same Start here finding/step thread.
- Playwright: Long virtualized suggestions assert viewport + scroller intersection for the triage-aligned card (
analyze-suggestion-card-triage-aligned), not only DOM presence. - Sharing / auth help: Calmer titles (Who can load this, link checkbox wording).
Phase 109 — compare triage deck, reliable scan actions, narrow stickiness¶
compare-triage-deck: bounded Change at a glance + Also scan with Show row actions that scroll and highlight the target artifact (scrollArtifactIntoViewcenters the row).- Narrow:
summaryStickyNarrowkeeps the summary triage block visible early when columns stack. - Density: extra operator/lanes/index keyboard nuance remains behind
<details>so the default path stays scannable.
Input (Phase 36)¶
- Paste Plan A and Plan B as raw text (plain JSON or
psqlQUERY PLANoutput—samePlanInputNormalizerpath as Analyze). - Optionally expand Optional: source SQL + EXPLAIN metadata to attach
queryTextA/queryTextB, shared EXPLAIN toggles, per-side recorded EXPLAIN commands, and suggested EXPLAIN snippets (mirrors Analyze ergonomics). Metadata is optional and client-declared. - Click Compare — the API runs analyze twice (with per-side context), then the comparison engine. The full
PlanComparisonResultV2is stored in SQLite withartifactSchemaVersion(Phase 49); the UI syncs?comparison=<comparisonId>with existingpair=,finding=,indexDiff=,suggestion=params. - Reopen:
GET /api/comparisons/{id}powers?comparison=loads (same durability rules as analyses). Legacysuggestion=values may still match viaalsoKnownAson compare suggestion rows after server normalization—canonicalsuggestionIdis written back into the URL when the UI syncs. - Copy share link / Copy artifact link in the summary header copies the current URL (includes
comparison=when synced), labeled from/api/configin auth deployments. Copy link on the selected pair builds a multi-line clipboard block (URL +PQAT compare:+ optionalPair ref:+ optionalHighlighted finding:/Highlighted index change:/Highlighted next step:when those URL highlights are active — Phase 112 renames from older Pinned … wording) so tickets get stable ids without hunting the query string (Phase 86). When a pin is active, Copy pin context (Phase 96) copies the same ids without the URL line for short chat/ticket pastes (Phase 112: compact line uses Link includes:) — use Copy link when the recipient needs the reopenable URL; use Copy pin context when you only need PQAT ids + pinned readout (Phase 97 clarifies the split). Optional Sharing panel when auth is enabled (owner can change scope / groups / link access). Phase 38: same identity modes as Analyze (JWT / API key / legacy bearer / proxy);authHelpfrom/api/configsummarizes the active mode.
Compare URL parameters (quick reference)¶
| Param | Role | When active (what to expect) |
|---|---|---|
comparison |
Persisted snapshot id (required for reopen). | Workspace loads that comparison; other params apply on top. |
pair |
Selected pair artifact id (pair_*). |
Selected node pair matches that artifact; Copy link includes Pair ref:. Removing pair= from the URL drops that explicit selection; the UI falls back to the current navigator choice or a default pair, and the sync effect rewrites the query string without pair= until you pin a pair again from the URL or UI. |
finding |
Highlights a findings diff row (fd_*). |
Navigator finding row shows active outline; Copy link adds Highlighted finding:; scroll targets the row. |
indexDiff |
Highlights an index insight diff (ii_*). |
Index changes row is active; click row in UI also pins (Phase 95); Copy link adds Highlighted index change:. |
suggestion |
Highlights a compare next step (sg_*). Values matching suggestionId or alsoKnownAs resolve to the same row; after hydrate the UI typically syncs the query string to the canonical suggestionId. The settled sg_* in the URL is the durable handle for reopen tests (Phase 93–94). |
Next-step row highlighted; Pin or Focus plan B sets pins; Copy link adds Highlighted next step:. |
URL params × first load vs in-place edit (Phase 101)¶
| Situation | comparison |
pair / pins |
|---|---|---|
| First open / paste URL | Hydrates snapshot from server; guide may stay collapsed if comparison= present. |
Parsed into selection + highlights; hydrate line in compare-pin-live when pins present. |
| In-place URL edit (same snapshot) | Unchanged id keeps data; useLayoutEffect reapplies parseCompareUrlPinAndPairState. |
pair= removed → no valid URL pair → falls back to selectedPair or effective default; sync writes canonical params back. |
| Clear / new Compare | Param stripped; empty workspace. | All pair/pin params cleared. |
In-product workflow guide (Phase 101)¶
- Replaces the old
CompareIntroPanel(pqat-panel--capture) withCompareWorkflowGuideinside the same guide visual language as Analyze (teal rail, dotted border, instructional tone). - Toggle:
How to use Compare/Hide guide(always available). Default open when the page is empty depends on workspaceintrovisibility (“Empty page: open workflow guide by default” in the customizer). - Context hints:
pqat-help-inlinenear Plan inputs and Copy reference / Copy link / Copy pin context explains capture vs diff output vs clipboard shapes. - Pin live region:
compare-pin-liveusesaria-relevant="additions text"so screen readers treat hydrate/transition lines as supplemental (Phase 101 a11y pass).
Workflow guide persistence & re-entry (Phase 102)¶
- Dismissal:
compareDismissedin the samepqat_workflow_guide_v1object. Empty-page default islayout.visibility.intro && !compareDismissed; hiding the guide persists dismissal. Clear reopens the guide (same skip-sync ref pattern as Analyze so workspace intro off does not immediately collapse it).?guide=is applied in auseEffect(aligned with Analyze) so open-from-link transitions and announcers stay consistent—not read in the initialuseState. - Keyboard / URL:
?/ Shift+/ and/compare?guide=1match Analyze semantics;?guide=strips on close after an explicit open→close transition (not on first paint). - Summary column: One
pqat-help-inlineat the top of the summary shell distinguishes Change briefing vs Index changes vs Next steps relative to Copy link. - Empty state: Compare empty hint text was shortened so the workflow guide carries primary onboarding copy.
Help lifecycle, focus, and support entry (Phase 103)¶
- Focus / Esc / guided links: Same model as Analyze workflow: explicit open targets the guide title; Esc closes when not typing in a field; footer Copy merged guided link / Copy entry guided link are onboarding-only (
?guide=1), not persisted comparison URLs. - Copy trio clarity: Selected node pair
pqat-help-inlinedistinguishes Copy link (shareable snapshot + pins), Copy pin context (no URL), and the guide’s merged / entry guided links. - E2E: Dismissal persistence across reload is asserted against
pqat_workflow_guide_v1. If persistedpqat.compareWorkspaceLayout.v1exists, the test setsvisibility.introto true instead of deleting the whole layout—closer to real “re-enable intro” usage.
Help accessibility and guided-link merge (Phase 104)¶
- Announcer:
compare-workflow-guide-announcermirrors Analyze (open / close / from link). - Tab loop & merged guided URL: Same as Analyze workflow.
- Copy trio: Selected node pair buttons use
aria-label+titleto separate Copy link (shareable snapshot) vs Copy pin context (no URL) vs the guide’s merged / entry guided links.
Help landmarks and announcer E2E (Phase 105)¶
- Landmarks & dual links: Same shell
role="region", footerrole="group", and two guided copy actions as documented in Analyze workflow (Phase 105). - Playwright: Compare announcer lifecycle + Copy entry guided link behavior are asserted alongside Analyze in
persisted-flows.spec.ts.
Guided-link parity and first-frame polish (Phase 106)¶
?guide=open uses the sameuseLayoutEffect+openWorkflowGuideWhenUrlRequestspath as Analyze (see Analyze workflow, Phase 106) — no one-frame closed flash; announcer unchanged.- Merged guided link:
persisted-flowsasserts Compare merged clipboard retains decoy params and thatgotoreopens the guide.
After a run, open Plan capture / EXPLAIN context (A vs B) for a compact two-column view: source query present/absent, planner costs (from JSON), input normalization line, declared options, and recorded command per side.
Phase 58: each side’s analyze pass builds PlanSummary.bottlenecks and the expanded narrative/suggestion cohesion described in Analyze workflow. Phase 59: the compare API adds bottleneckBrief — a short list of lines describing bottleneck posture (A vs B). Phase 60: comparisonStory adds a compact change narrative (runtime/structure/findings beats, investigation walkthrough, structural vs superficial heuristic) rendered in the summary meta column before bottleneck posture; persisted comparisons missing the field get it backfilled on read. Phase 61: change beats are structured ComparisonStoryBeat rows (text, optional focusNodeIdA/focusNodeIdB, pairAnchorLabel) so the summary can offer Open pair for the primary regression without naming internal ids; compare narrative lines for findings use the same human-reference builder as Analyze. Phase 62: UI labels this block Change briefing with section lanes (runtime posture, structural beats, walkthrough, engineering read); Focus plan B on compare suggestions uses humanNodeAnchorFromPlan when targetDisplayLabel is absent; selected pair readout adds collapsed Technical pair ids. Phase 63: change story consumes BottleneckComparisonBrief for posture-aware beats (e.g. runtime regression vs unchanged bottleneck class), adds severe-finding-count deltas, and rewrites pair-evidence / linked-narrative / findings lines to read more like an engineer walkthrough (fewer rule-id-first fragments). Selected pair shows per-side operatorBriefingLine when the analyze payload included it. Markdown/HTML reports use Change briefing / Plan briefing headings with pair labels when present.
Phase 64: primary regression ComparisonStoryBeat rows may include beatBriefing (plan B operatorBriefingLine) rendered as a compact strip under the beat text. FindingIndexDiffLinker and CompareOptimizationSuggestionEngine tie-ins lean on finding titles and human anchors instead of raw ruleId tokens in primary copy. Hash probe/build references improve when Hash sits under Materialize or when both join children are Hash (row-magnitude tie-break).
Phase 67: NodePairDetail.regionContinuityHint (JSON regionContinuityHint) carries a medium+-confidence sentence when the mapper ties the same relation (seq ↔ index scan rewrites) or the same join tables with a join-strategy change (e.g. nested loop ↔ hash). ComparisonStoryBuilder and PairHumanLabel use continuity for improved/regression beats; CompareOptimizationSuggestionEngine may add “Rewrite changed operator shape — track where the work moved” when such a hint exists. The selected pair panel and What changed most rows show continuity + the hint so users can tell problem region continuity from operator rename alone. Fixtures nl_inner_seq_heavy, rewrite_nl_orders_lineitems / rewrite_hash_orders_lineitems back tests.
Phase 68: PairRegionContinuityHint distinguishes broad seq → narrower index (same relation) from sort-over-seq → order-supported index when the mapped scan pair sits under a Sort on A and sort key tokens overlap index cond / presorted key on B (conservative token match). A Sort → index scan direct mapping (same relation + ordering evidence) gets its own continuity line when the mapper pairs those nodes. NodeMappingEngine adds a small access-rewrite bonus for same-relation Seq Scan ↔ index-backed scan so those pairs more often reach Medium+ confidence (enabling hints). ComparisonStoryBuilder appends continuity to the largest regression beat when present; CompareOptimizationSuggestionEngine tail sentences vary for access vs ordering vs join continuity. UI pairContinuitySectionTitle (compareContinuityPresentation) picks Access path · same relation, Ordering · same region, or Join · same tables for kickers on the selected pair and What changed most. Fixtures rewrite_access_seq_shipments / rewrite_access_idx_shipments, rewrite_sort_seq_shipments / rewrite_index_ordered_shipments (+ .sql) back tests.
Phase 69: Ordering continuity prefers structured sort-column ↔ index cond / presorted key alignment when the JSON exposes parseable column names; token overlap remains a weak, explicitly hedged fallback. Same-relation hints cover seq ↔ bitmap heap, bitmap ↔ index / index-only, and index ↔ index-only, with copy that stresses residual cost (recheck, heap volume, parents). NodePairDetail.regionContinuitySummaryCue (JSON regionContinuitySummaryCue) is a short chip string from CompareContinuitySummaryCue.FromHint; the Change briefing summary lane shows it when the selected pair (or first anchored story beat with a mapped pair) has a cue. Fixtures rewrite_access_bitmap_shipments, rewrite_access_idxonly_shipments (+ .sql) extend the rewrite set.
Phase 70: Continuity is TryPairRegionContinuity → RegionContinuityData (KindKey + ContinuityOutcome) with human Hint; the summary chip uses CompareContinuitySummaryCue.FromContinuity (substring FromHint only as fallback). Regression-style access paths (e.g. index → bitmap heap, index → seq, index-only → heap-backed index) get non-optimistic chips. Query text supplies a bounded ORDER BY tie-breaker when JSON ordering evidence is thin. Partial vs finalize aggregates can emit output-shaping continuity when GROUP BY keys match and Partial Mode differs. continuityKindKey is exposed for debugging/telemetry; the UI prefixes the chip with Continuity · for context. Playwright persisted-flows covers a real Compare run asserting the cue.
Phase 71: Fixture-backed query-assisted ordering when sort keys do not appear in index cond but captured ORDER BY aligns (rewrite_queryassist_sort_priority_shipments vs rewrite_access_idx_shipments). Gather Merge ↔ single-node aggregate continuity (aggregate.gatherVsSingle / aggregate.singleVsGather) with a NodeMappingEngine gather↔aggregate score bonus. Partial/final aggregate hints gain GROUP BY text bridging when planner Group Key strings differ but a GROUP BY clause references overlapping column identifiers; optional time_bucket wording when SQL and group-key labels suggest bucketing (bounded, hedged). Compare story and compare suggestion follow-ups add grouped-output / residual feed framing; chips distinguish ordering region, grouped output, gather vs single, and SQL bridge kinds. Playwright adds index → bitmap heap regression continuity. New fixtures: rewrite_aggregate_*_shipments, rewrite_aggregate_*_bucket_shipments.
Phase 72: Compare Copy reference / Copy link / navigator copy use the same copyToClipboard + useCopyFeedback stack as Analyze; pair copy text is human-first, then Pair artifact (when present) and Plan A node / Plan B node lines (Phase 86 replaces the terse A:/B:-only tail).
Phase 86: pairReferenceText / navigator / top-changes / selected-pair Copy reference prepend PQAT compare: <comparisonId> when known. Copy link on the selected pair copies compareDeepLinkClipboardPayload (URL + PQAT compare: + optional Pair ref:). Next steps after this change in the summary column includes Copy for ticket on each compare suggestion (suggestionReferenceText with comparisonId and optional pinned pair ref when the highlighted suggestion matches the selected pair). HTML compare export gains the same Next steps section as markdown when suggestions exist.
Phase 87: Compare markdown uses the same Next steps after this change heading as HTML (no “(compare)” suffix). POST /api/compare/report/json parity is covered by unit tests (compareOptimizationSuggestions, rewriteVerdictOneLiner on pairs). Playwright persisted-flows asserts Copy for ticket on a real compare suggestion (PQAT compare: + structured suggestion lines). Analyze Copy share link E2E asserts URL + PQAT analysis:.
Phase 88: Compare is tuned as a rewrite decision surface: continuity precedes Rewrite outcome in the selected pair; region continuity uses a labeled role="region"; Copy reference appends Rewrite outcome: when a verdict exists. Summary Next steps uses an h2 and chips (Same pair as sidebar / Other region) when a pair is selected; Copy for ticket may include Pair scope: aligns with selected pair (Plan B node …) when the suggestion’s focus matches Plan B. HTML compare export gains plan capture (per side), change briefing with Story beats when present, bottleneck posture, narrative, then top pairs — closer to markdown flow; top pair lines use Rewrite outcome (not “readout”). Auth API key Playwright asserts Copy artifact link captures URL + PQAT analysis: (clipboard hook). useCopyFeedback clears timeouts and skips setState after unmount so heavy graph tests do not trip Vitest unhandled errors.
Phase 89: Graph layout: buildAnalyzeGraph / layoutTopDown coerce dagre coordinates to finite x/y; AnalyzePlanGraphCore clamps node positions before ReactFlow; fitView runs after requestAnimationFrame when the viewport is finite. Vitest: AnalyzePage.interaction.test.tsx alone filters the known transient React NaN-attribute console.error pair (other suites stay noisy-clean). Playwright: Compare: deep link from Copy link restores pair param and rewrite outcome (clipboard URL + Pair ref: → reopen → pair= + Rewrite outcome); Compare: Copy for ticket on same-pair suggestion includes Pair scope line. useCopyFeedback Vitest regression: unmount before success timer / late copyToClipboard resolve. Compare a11y: Next steps block is a section role="region" with aria-labelledby the h2; Selected node pair heading has id="compare-selected-pair-heading". Copy: change-briefing subtitle avoids “verdict” next to Rewrite outcome (headline judgment wording).
Phase 90: Compare collaboration parity: Phase 101 moved first-run orientation into CompareWorkflowGuide (h2 “Compare plans” inside the guide shell); Summary, Navigator / Findings diff, What changed most, Selected node pair use a coherent heading ladder (summary shell aria-labelledby="compare-summary-heading"). Next steps rows: each suggestion title is an h3; Focus plan B / Copy for ticket precede Pin in Tab order (DOM order), with Pin as a compact top-right control. Playwright: Compare: suggestion= deep link highlight survives reopen in fresh tab ( suggestion= highlight beyond pair= round-trip); jwt-auth-smoke.spec.ts — Compare Copy link clipboard (URL + PQAT compare: + Pair ref:) in JWT mode. AnalyzePlanGraphCore sets defaultViewport to reduce transient viewport edge cases; NaN console noise remains file-scoped in AnalyzePage.interaction.test.tsx if jsdom still logs one frame. Vitest: next-step button order regression.
Phase 91: Frontend CI: ci.yml frontend job runs ./scripts/verify-frontend-docker.sh (digest-pinned Node container) so Vitest / Rolldown no longer depends on GitHub-hosted npm ci optional native bindings. Contributing documents the Docker path as canonical for CI parity. Compare: proxy Playwright (proxy-auth-smoke.spec.ts) Compare Copy link clipboard parity with JWT; Next steps Pin uses aria-describedby the suggestion h3; Summary “share” vs selected-pair Copy link title tooltips clarify URL-only vs ticket-sized payload. Visual baselines: still a follow-up when refreshing e2e-visual PNGs.
Phase 92: e2e-visual Analyze first PNG targets data-testid="analyze-visual-summary-contract" (metric deck + Plan briefing only), not the full summary card (share/metadata/footer). Playwright persisted-flows: Compare: finding= deep link highlight survives reopen in fresh tab (findings diff row pqat-artifactOutline--active after goto in a second tab).
Phase 93: persisted-flows: Compare: indexDiff= deep link highlight survives reopen in fresh tab (seq↔index fixtures — Index changes callout data-testid="compare-index-changes-callout", pqat-indexInsightItem--active) + URL bar asserts comparison= / finding= / indexDiff= / settled suggestion= after reload. suggestion= reopen: URL may normalize to a different stable id than the seed’s canonicalSuggestionId; tests assert reopen preserves the settled suggestion= value. Vitest: getBoundingClientRect stub for .react-flow zero-size rects (with existing offset stubs) reduces transient React NaN-attribute noise; AnalyzePage.interaction.test.tsx no longer file-scopes console.error filtering. compareDeepLinkClipboardPayload ( shareAppUrl.ts ) adds optional pinned lines.
Phase 94: persisted-flows: Compare: Copy link with pinned index insight includes ticket lines and round-trips URL — clipboard Pinned index insight: + PQAT compare: + Pair ref:; first URL line goto in a fresh tab restores highlight. indexDiff= test adds toBeInViewport on compare-index-changes-callout. jwt-auth-smoke.spec.ts: pinned index insight copy in JWT mode. Compare HTML export gains Index changes; markdown ## Index changes (replaces “Index comparison” heading). resolveCompareSuggestionParamToCanonicalId JSDoc + E2E seed comment clarify suggestion= / alsoKnownAs. Docs: URL parameter table (above); api-and-reports.md.
Phase 95: Index changes insight rows are keyboard-focusable and click-to-pin (clears other pin types); Highlight finding uses stopPropagation. Selected pair shows compare-pinned-summary when any pin is active. aria-current on pinned finding / index / suggestion rows. persisted-flows: click index row → URL + summary; Copy link with pinned finding / suggestion; finding= deep link asserts row toBeInViewport. jwt-auth-smoke: pinned finding + suggestion Copy link lines in JWT mode. URL table adds When active column.
Phase 96: Index changes list (CompareIndexInsightRows) uses roving tabIndex — only one row is tab-stopping at a time; Arrow Up/Down moves between rows, Enter/Space pins. Next steps (CompareNextStepsList) Pin controls chain with Arrow Up/Down for faster keyboard traversal. Hint copy states one primary pin at a time for Copy link (pinning replaces the previous). Selected pair adds Copy pin context (compare-copy-pin-context) — short clipboard via compareCompactPinContextPayload (no URL line; PQAT compare:, Pair ref:, Link includes: readout, optional Rewrite outcome). suggestion= Playwright flows assert toBeInViewport on the highlighted next-step row. Vitest: comparePinning, CompareIndexInsightRows, shareAppUrl compact payload. CSS focus-within on active finding / suggestion outlines.
Phase 97: indexDiff= (URL) updates move roving focus to the matching Index changes row with focus({ preventScroll: true }) so the viewport does not jump when the highlight syncs. Next steps Pin row supports Home / End (Vitest CompareNextStepsList). Analyze Plan workspace + Plan guide paired layout: companionRailSurfaceStyle, pqat-analyzeWorkspaceRow--paired, graphFillColumn — see Analyze workflow (Plan workspace section).
Phase 98 — Keyboard, pins, and assistive feedback
- One primary pin for Copy link / deep-link params: at most one of
finding=,indexDiff=,suggestion=is active as the pinned highlight; changing pins replaces the previous. aria-live="polite"(data-testid="compare-pin-live", visually hidden): transition copy when the user changes the pin after load (comparePinAnnouncementForFingerprint). Phase 99 adds a distinct hydrate line when a comparison first opens with a valid deep-link pin (comparePinHydrateAnnouncementForFingerprint). Phase 100 auto-clears hydrate copy afterCOMPARE_PIN_HYDRATE_CLEAR_MS. A short defer before transition lines (PIN_LIVE_ANNOUNCE_DEFER_MS) reduces overlap with copy success feedback (COPY_FEEDBACK_SUCCESS_CLEAR_MS).- Keyboard (summary column): Index changes — roving
listitem, Arrow Up/Down, Enter/Space to pin (Phase 95–96). Next steps — Pin buttons: Arrow Up/Down, Home, End (Phase 97). Phase 99: compactaria-describedbytarget (COMPARE_WORKSPACE_KEYBOARD_HINTS_ID,COMPARE_WORKSPACE_KEYBOARD_HINTS_TEXT) shared by Index changes and Next steps lists; Arrow roving usespreventScroll: trueon Index rows and Next steps Pin focus moves. - Focus affordance: shared
focus-within/ outline treatment on findings diff shells (pqat-artifactOutline), index insight rows, and next-step rows so keyboard focus matches the workstation chrome. Phase 99:focus-withinis suppressed when the same surface is already active / highlight so pinned + focused states do not double-outline.
Phase 99 — Pinning completeness + paired text polish
- Playwright
persisted-flows: Index changes keyboard test usesrewrite_sort_seq_shipmentsvsrewrite_index_ordered_shipmentsso two+ insight rows always exist; asserts ArrowDown roving then Enter pin + live region. E2E fixtures sync includes those JSON files (scripts/sync-e2e-fixtures.sh/check-e2e-fixtures.mjs). BackendIndexComparisonAnalyzerTestsalready guards ≥2 non-unchanged insight diffs on that pair.
Phase 100 — Deep-link resync + pin lifecycle
- URL → state on every
location.searchchange (samecomparisonId):parseCompareUrlPinAndPairState(compareDeepLinkSync.ts) reappliesfinding=/indexDiff=/suggestion=with one-primary-pin precedence (finding > index > suggestion); validpair=updatesselectedPairso bookmark edits and shared links stay aligned with an already-open comparison. - Hydrate vs transition: Hydrate “Opened with …” runs only when
comparisonIdfirst loads in layout; auseEffectoncomparePinLiveMessageauto-clears lines matching/^opened with /iafterCOMPARE_PIN_HYDRATE_CLEAR_MS(or sooner if the message changes).suppressNextPinTransitionForFpRefdrops one redundant transition right after hydrate when React would otherwise echo the same fingerprint. User-driven pin changes still usecomparePinAnnouncementForFingerprint+PIN_LIVE_ANNOUNCE_DEFER_MS. - Playwright: hydrate announcement + empty live region after clear; same snapshot navigates
indexDiff=from row A to row B; Space pins an index row (roving + Space). Fixtures:rewrite_access_*JSON included in check/sync (continuity regression path).
Phase 82: Change briefing lead (wall-clock & rewrite result) can append a second sentence when net faster root coexists with ranked worsened pairs, or net slower with improved pairs—surfacing “pain moved” vs “uniform win/loss.” Grouped-output continuity tails stress scans/joins feeding buckets or partial aggregates. UI adds a short subtitle under Change briefing explaining how to read lead line vs beats; section eyebrows are plainer (what moved, how to read this comparison). Continuity section title Grouped / bucket output · same region clarifies analytical workloads.
Phase 83: Selected pair shows a Rewrite outcome one-liner (rewriteVerdictOneLiner) from inclusive-time / shared-read deltas plus continuity hints; low mapping confidence prefixes Weak mapping— when metrics are still quoted so users do not over-trust the line. Related compare next step for the pair remains the compact compare-scoped suggestion block when present.
Phase 84: Copy actions use the same copyToClipboard stack as Analyze (sync execCommand first). POST /api/compare/report/html returns a compact HTML report whose Top worsened/improved lines include Rewrite outcome when present (parity with markdown). Playwright can assert Rewrite outcome for this pair on the seq↔index fixture path.
Visual hierarchy (Phase 43) + Phase 55 polish¶
Compare uses the same pqat-* styling as Analyze: capture card, summary metric tiles, navigator and pair workspace panels, and a dashed customizer well. Phase 55 adds the same state banners for reopen/load/errors, a comparing info banner during POST, and summary shell glow/border tuning. Phase 101 replaces the old intro metric-tile card with the shared guide shell at the top of the page (see In-product workflow guide above). Behavior, URL params, and layout persistence are unchanged aside from intro controlling default guide visibility when empty. Phase 65–66: the shared Theme appearance control and data-theme / data-effective-theme tokens apply here too (Change briefing, selected-pair panel, What changed most row tints, meta/customizer panels). Optional server appearance_theme_v1 sync matches Analyze when auth + credentials are configured.
Patterns & delivery (Phase 44)¶
Optional: source SQL + EXPLAIN metadata and Advanced (matcher diagnostics) use the same pqat-textarea, pqat-fieldLabel, and pqat-details patterns as Analyze. Navigator pair rows, findings-diff cards, and summary index / suggestions callouts use shared workstation-patterns.css classes (instead of ad hoc inline layout). The Compare route is lazy-loaded with a Loading Compare… shell; Customize workspace loads DnD reorder UI on first open, matching Analyze.
List performance (Phase 45)¶
When findings diff has many rows, CompareNavigatorPanel wraps the list in VirtualizedListColumn (same helper as Analyze) so scrolling stays light. Small diffs keep a non-virtual list; ClickableRow selection, copy, and index-cross-link buttons behave the same.
Selected-pair progressive detail (Phase 46)¶
See Selected pair panel below: CompareSelectedPairHeavySections is code-split and loaded after the eager header/actions so the pair column stays responsive on large comparisons.
Pair heavy prefetch (Phase 48)¶
prefetchCompareSelectedPairHeavySections() warms the same lazy chunk as CompareSelectedPairPanel’s Suspense boundary. It is coalesced (one in-flight dynamic import) and triggered when:
- the user hovers or focuses primary pair-selection
ClickableRowtargets (navigator worsened/improved, findings diff rows, What changed most, Branch context mapped rows), - the user hovers/focuses Focus plan B on a compare suggestion in the summary column,
- the browser is idle shortly after a comparison result is shown (
requestIdleCallback,setTimeoutfallback)—same pattern as Analyze graph prefetch.
This reduces perceived delay when moving quickly between pairs without blocking the first paint of the pair column header.
Compare workspace layout (Phase 41 + Phase 42)¶
The Compare page mirrors Analyze’s workstation model:
- Customize workspace (inside the capture card): presets (Balanced, Wide pair / wide-graph emphasis, Review, Diff-heavy, Compact), per-panel visibility toggles, drag handle + Up/Down reorder for summary-column sections and navigator blocks (worsened/improved, findings diff, unmatched), and Swap main columns (navigator vs pair detail). Reset to defaults restores the balanced preset.
- Responsive layout: same three tiers as Analyze (narrow <900px, medium 900–1319px, wide ≥1320px). Medium and wide keep the summary + “what changed most” row and navigator | pair main grid side-by-side with tuned
minmax/frratios; only narrow stacks to a single column. Summary metric cards use anauto-fitgrid so five cards wrap on mid-width screens instead of forcing one cramped row. - Persistence: layout is stored under
pqat.compareWorkspaceLayout.v1in localStorage. When auth is enabled and the SPA has credentials (VITE_AUTH_*), the same JSON syncs toPUT /api/me/preferences/compare_workspace_v1(debounced), matching the Analyze preference pattern. - Hidden inputs: if Plan inputs is toggled off, a dashed recovery strip offers Show plan inputs so Compare remains usable.
- Semantics: hiding a panel does not clear selection or URL params—restore panels from Customize to see branch strip, findings diff, or pair detail again. Reordering uses stable region ids;
coerceCompare*merge helpers reject corrupted orders.
What compare does (and does not)¶
- Does: heuristically maps nodes between Plan A and Plan B, computes deltas, summarizes context diffs, and highlights findings changes.
- Does not: prove mathematical identity of nodes. Low-confidence matches are leads, not guarantees.
Confidence¶
Mapping confidence is emitted per pair. Treat low-confidence pairs as “suspects” until validated by nearby structure and evidence.
Reading the compare UI¶
At the top: summary + “what changed most”¶
After a compare run, start at the top:
- Summary cards: total runtime, shared reads, severe findings, node count, max depth (Plan B value + delta vs Plan A).
- Index changes (Phase 30+31, Phase 33 ids): plan-level scan-mix deltas, chunked-bitmap posture lines when applicable, and a short list of bounded index insight diffs. Each insight row can show Supported by N finding change(s) with rule tails and a Highlight finding control. Cross-links use stable ids (
fd_*/ii_*) inrelatedFindingDiffIds/relatedIndexDiffIds; legacyrelatedFindingDiffIndexes/relatedIndexDiffIndexesremain for backward compatibility but are not the primary reference. - Next steps after this change (Phase 32 + Phase 47): compact
compareOptimizationSuggestionslist (not a verbatim copy of plan B’s analyze suggestions). Suggestions use the same human-readable fields as analyze (suggestionFamily,recommendedNextAction,whyItMatters,targetDisplayLabel) and compare-specific wording (“after this change”, plan B + diff context). Suggestion ids use thesg_prefix (content-hash style). Phase 48: suggestions carried from plan B’s high-priority analyze list use ids derived from structured fields + sourcesg_*id (not the prefixed display title). Phase 49: persisted rows may includealsoKnownAs(e.g. legacy carried-id forms) sosuggestion=deep links keep working; the SPA resolves aliases to the canonical id for highlight + URL sync. Rows may includerelatedFindingDiffIds/relatedIndexInsightDiffIdswhen the engine ties a suggestion to specific diff rows. Rows may offer Focus plan B · … when a mapped match exists (label preferstargetDisplayLabel). Server read path normalizes older suggestion JSON; the client keeps a thin display fallback. - Findings ↔ index deltas (Phase 31+33): finding diff rows can show Related index change with stable id buttons (and legacy Index Δ #k when needed). Outlining uses
ii_*targets so highlights survive reordering. The selected pair panel adds Finding ↔ index corroboration when structured links exist on that mapped pair. Links are heuristic (node ids, relation evidence, rule id ↔signalKindsalignment)—not proof of causality. - What changed most: quick-jump to the top worsened and top improved mapped pairs; when
regionContinuityHintis present on that pair, a short continuity line appears under the title (same as selected-pair treatment). Phase 69:regionContinuitySummaryCuealso drives a compact continuity chip in the Change briefing block (selected pair first, else story-beat focus).
Navigator: improved / worsened lists + “what changed most”¶
These surfaces share the same interaction model as Analyze hotspots/findings and the findings diff:
- Each row is a
ClickableRow(role=button,Enter/Spaceactivates selection). Inner Copy is a real<button>withstopPropagation, so copy never toggles selection. aria-pressedreflects whether the row’s mapped pair is the currently selected pair. Selected rows get a restrained accent treatment (tint in the navigator; a left accent bar on tinted “what changed most” callouts so severity coloring stays readable).- Copy pair reference on navigator and top-change rows copies the same style of human-readable pair reference as the selected-pair panel (via
pairReferenceText), with local “Copied …” feedback in the navigator column.
Each row uses human-readable pair labels and may include badges:
- generic context badges (hash pressure, scan waste, sort spill, memoize, nested loop)
- side-aware join badges when the evidence is explicitly side-scoped (build side / inner side)
- optional
index Δchip when the mapped pair has non-empty index delta cues (access-path family change and/or pair-scoped index insight diff summaries)
Branch context (twin path strip)¶
Above Selected node pair, the Branch context section is the visual counterpart to the navigator:
- Two columns (Plan A / Plan B) show the path from root to the selected node on each side, using the same
nodeShortLabelsystem as the rest of the app (no raw internal ids in primary labels). - The focal row (current selection) uses
aria-pressedand the same selected styling as otherClickableRowtargets. - Mapped ancestors and children are clickable: choosing a row selects the mapped pair
(nodeIdA, nodeIdB)from the comparematchestable, so the navigator, findings diff, detail panel, and branch strip stay aligned. - Rows without a mapping partner render as static rows tagged unmapped; nodes that appear in the unmatched id lists show a small A-only / B-only chip.
- Compact cues under the heading summarize the focal pair (e.g. confidence, time/read deltas from
nodeDeltas, operator-family shift, first context-diff highlight, severe finding hits on that pair, join-side hints when present). - Downstream: immediate children of the focal node on each side (capped for density), with the same mapping / unmatched semantics.
Selected pair panel¶
Phase 46 — staged hydration: the panel paints immediately with the pair heading, Copy reference / Copy link, optional subtitle, Related compare next step (when present), and the confidence · score · depth line. Denser blocks load in a second lazy chunk behind a calm skeleton (pqat-pairHeavySkeleton) so selection still feels instant while metrics and evidence hydrate.
The selected pair shows (after load):
- readable pair heading + join branch subtitle (when applicable) — eager
- Related compare next step (Phase 32 + Phase 47) — eager when a
compareOptimizationSuggestiontargets this pair’s plan BnodeIdB(title, summary, Next ·, Why · when present) - Copy reference and Copy link (full URL with
comparison=,?pair=, optionalfinding=,indexDiff=,suggestion=) — eager - Access path / index delta (Phase 30): bullets from
pairDetails[].indexDeltaCueswhen present; fallback line when families differ — deferred - Finding ↔ index corroboration cues — deferred
- Join side change summary when supported (hash build / inner waste) — deferred
- context change summary highlights — deferred
- raw operator fields and evidence side-by-side, Key metric deltas, Findings for this pair, optional matcher diagnostics — deferred
Findings diff¶
Diff finding rows include a subtle Copy action that copies a concise human-readable reference for the anchored node (optionally annotated with the change type / rule id).
Interaction model: the row is a single keyboard-accessible target to select a pair. When both nodeIdA and nodeIdB are present, that pair is used; when only one side is anchored, the UI resolves the partner from the compare matches list (resolveFindingDiffPair) so diff rows still drive selection and branch context when a mapping exists. Copy is a separate button (aligned with Analyze hotspots/findings). When the resolved or explicit pair matches the selected pair, aria-pressed is true so the navigator, branch strip, and diff stay visually in sync.
Notes: - Compare is heuristic: mapping confidence is shown because some rewrites change structure and labels. Treat low-confidence pairs as leads to validate, not guarantees.
Stable artifact ids & deep links (Phase 33 + 36)¶
- Comparison:
comparisonIdidentifies the persisted snapshot; URL keycomparison(withpair,finding,indexDiff,suggestion). - Pair: each
pairDetails[]row haspairArtifactId(pair_+ short hash) scoped tocomparisonIdand the mapped node ids. - Finding diff:
findingsDiff.items[].diffId(fd_*). - Index insight diff:
indexComparison.insightDiffs[].insightDiffId(ii_*).
Limits: artifact ids are deterministic from structured fields for a given comparison payload; they are not stable across different plan JSON or a different comparisonId. If query params reference ids that are not in the current result, the UI ignores them for selection. comparison= must match a row still present in SQLite (TTL / pruning / DB deletion ⇒ 404-style error in the UI).
Guardrails¶
Side attribution is intentionally conservative. If evidence is ambiguous, the UI falls back to non-side-specific badges.