Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 7 additions & 22 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
.PHONY: test vet lint race tidy fmt build cert audit-render audit-geometry \
audit-duplicates audit-todo audit-deadcode audit-docs arch-cert
.PHONY: test vet lint race tidy fmt build cert audit-render \
audit-todo audit-deadcode doc-freshness arch-cert

# ============================================================================
# Standard Go gates
Expand Down Expand Up @@ -53,23 +53,6 @@ audit-render:
echo " PASS: no render mutations"; \
fi

audit-geometry:
@echo "[arch] Geometry ownership audit..."
@if grep -qn '\.SetWidth\|\.SetHeight\|\.Width =\|\.Height =' internal/tui/render_*.go; then \
echo " FAIL: geometry mutation in render"; \
exit 1; \
else \
echo " PASS: no geometry mutations in render"; \
fi

audit-duplicates:
@echo "[arch] Duplicate helper audit..."
@# Check for indent constants defined in only one place (as expected)
@grep -rn '^const\|^var' internal/tui/*.go | grep -v '_test.go' | grep -i 'indent\|gap\|pad' \
| awk '{count[$$0]++} END {for (k in count) if (count[k] > 1) print " DUPLICATE: " k}'
@# Check for identical function implementations across files
@echo " PASS"

audit-todo:
@echo "[arch] TODO audit..."
@if grep -rn 'TODO\|FIXME\|HACK\|XXX' internal/tui/*.go | grep -v '_test.go' | grep . > /dev/null 2>&1; then \
Expand All @@ -91,7 +74,9 @@ audit-deadcode:

doc-freshness:
@echo "[arch] Documentation freshness audit..."
@for doc in RENDERING.md STATE_OWNERSHIP.md; do \
@for doc in README.md ARCHITECTURE.md RENDERING.md \
internal/tui/README.md internal/tui/STATE_OWNERSHIP.md \
internal/tui/COMPARE_CONSTITUTION.md internal/tui/COMPARE_WORKFLOW.md; do \
if [ -f "$$doc" ]; then \
echo " OK: $$doc"; \
else \
Expand All @@ -107,10 +92,10 @@ doc-freshness:
compilation: fmt vet build tidy
@echo "[compilation] All compilation gates PASS"

quick: test vet lint
quick: test lint
@echo "[quick] All quick gates PASS"

arch-cert: audit-render audit-geometry audit-duplicates audit-todo audit-deadcode doc-freshness
arch-cert: audit-render audit-todo audit-deadcode doc-freshness
@echo "[arch] All architectural gates PASS"

cert: compilation quick race arch-cert
Expand Down
1 change: 1 addition & 0 deletions internal/model/result.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package model
import "time"

type Result struct {
Sequence int `json:"sequence,omitempty"`
Status int `json:"status"`
Latency time.Duration `json:"latencyNs"`
Timestamp time.Time `json:"timestamp"`
Expand Down
38 changes: 21 additions & 17 deletions internal/tui/COMPARE_CONSTITUTION.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Comparison Constitution

↩ [internal/tui/README.md](README.md) · [COMPARE_WORKFLOW.md](COMPARE_WORKFLOW.md)

## Purpose

This document defines what Compare is, what it is not, what operator questions it answers, and the architectural boundaries that govern its implementation. It serves as the specification for the v0.10.x "Explain Comparisons" release.
Expand Down Expand Up @@ -159,7 +161,7 @@ current `m.results` slice to know what it is comparing.
type CompareWorkspace struct {
Baseline *model.Result // current baseline (resolved value)
Candidate *model.Result // current candidate (resolved value)
PinnedBaseline *model.Result // persists across runs
Reference *model.Result // persists across runs
State CompareState
View CompareView
Analysis *ComparisonAnalysis
Expand All @@ -181,7 +183,7 @@ type CompareContext struct {

`CompareWorkspace.Context()` resolves the workspace into a renderer-facing
projection. The renderer never knows whether `Baseline` originated from the
current session or a pinned baseline — it simply renders what it is given.
current session or a reference — it simply renders what it is given.

### Workflow operations

Expand All @@ -190,17 +192,16 @@ Every transition is expressed through exactly one method on `CompareWorkspace`:
| Operation | Effect |
|-----------|--------|
| `MarkBaseline(r)` | Establish r as baseline; enter `BaselineMarked`; clear candidate + analysis. |
| `Unmark()` | Clear baseline + candidate; return to `Idle`. Pinned baseline preserved. |
| `Unmark()` | Clear baseline + candidate; return to `Idle`. Reference preserved. |
| `SelectCandidate(r)` | Establish r as candidate; enter `Comparing`; recompute analysis once. |
| `ReplaceCandidate(r)` | Replace candidate; recompute analysis once. |
| `ReplaceBaseline(r)` | Replace baseline; recompute analysis once. |
| `Swap()` | Exchange baseline and candidate; recompute analysis (perspective changes, content deltas do not). |
| `Clear()` | Reset baseline, candidate, state, analysis to zero; `Idle`. Pinned baseline preserved. |
| `Exit()` | No mutation — preserve the entire comparison for resume. |
| `Clear()` | Reset baseline, candidate, state, analysis to zero; `Idle`. Reference preserved. |
| `NextView()` / `PrevView()` | Advance / retreat the active view, wrapping around. |

Predicates replace ad-hoc enum checks: `IsIdle`, `HasBaseline`, `HasCandidate`,
`IsComparing`, `HasPinnedBaseline`, `IsBaselineResult`, `IsCandidateResult`.
Predicates replace ad-hoc enum checks: `HasBaseline`, `IsComparing`,
`HasReference`, `IsBaselineResult`, `IsCandidateResult`.

### Analysis is computed exactly once per transition

Expand All @@ -213,7 +214,7 @@ renderers consume the immutable `*ComparisonAnalysis` and never recompute.
```
Idle
│
│ c on result (no PinnedBaseline)
│ c on result (no Reference)
▼
BaselineMarked
│
Expand All @@ -236,10 +237,10 @@ BaselineMarked
```

Notes:
- `x` (Clear) resets the workspace (Baseline, Candidate, State, Analysis). PinnedBaseline is preserved.
- `x` (Clear) resets the workspace (Baseline, Candidate, State, Analysis). Reference is preserved.
- `c` on the baseline result (in any state) clears the workspace — the same way `x` does.
- `c` on a different result (in Comparing) replaces the candidate.
- `c` with `PinnedBaseline` set (in Idle) immediately enters Compare, using the pinned result as the implicit baseline.
- `c` with `Reference` set (in Idle) immediately enters Compare, using reference result as the implicit baseline.
- `Esc` (Exit) performs no mutation; the operator resumes exactly where they left off.

---
Expand Down Expand Up @@ -316,7 +317,7 @@ This structure is the single source of truth for the renderer. The renderer rece
| Regression coloring | Red for regressions, green for improvements, yellow for anomalies |
| Workflow | Mark from Observe, swap, replace candidate, separate clear from exit, cross-run pinning |
| Discoverability | Ribbon actions, active/marked/pinned markers in Observe list |
| State model | ComparisonSession with lifecycle, Pinned flag for cross-run survival |
| State model | CompareWorkspace with lifecycle, Reference for cross-run survival |

### Explicitly deferred (v1.0 or later)

Expand Down Expand Up @@ -396,7 +397,7 @@ maintainable. Nothing outside the Compare subsystem changes.
Compare is no longer a modal or a temporary screen. It is a persistent
workspace that evolves as the operator works:

* **Idle** — no active comparison. A pinned baseline may still be displayed.
* **Idle** — no active comparison. A reference may still be displayed.
* **BaselineMarked** — a baseline is chosen; Observe shows a collapsed preview.
* **Comparing** — both baseline and candidate are set; the workspace is live.

Expand Down Expand Up @@ -425,7 +426,7 @@ Every view renderer consumes the identical immutable `CompareContext`.

### State is always visible

The operator never mentally tracks what is marked, pinned, baseline, candidate,
The operator never mentally tracks what is marked, referenced, baseline, candidate,
or resumable. The interface communicates it continuously via timeline markers
(▶ Candidate, ◆ Baseline, ● Pinned) and the Compare identity header
(`renderComparisonIdentityBlock`).
Expand All @@ -444,11 +445,14 @@ a `Model` value — a pointer receiver would cause an interface conversion panic
| CompareWorkspace | Single source of truth for all comparison state; resolved `*model.Result` pointers |
| CompareContext | Renderer-facing projection; immutable during a frame |
| View-oriented rendering | Overview / Evidence / Diff / Headers / Body / Raw, bracket-navigable |
| Collapsed preview | The preview **is** the Compare workspace with partial context — same renderer and identity block, plus verdict and an orientation/resume hint |
| Collapsed preview | The preview **is** the Compare workspace with partial context — same renderer and identity block, plus verdict and a context-specific keybinding line (open / clear / swap / renounce) |
| Resumable workspace | `Esc` preserves the workspace; `c` on the candidate (▶) re-enters Compare without disturbing the comparison |
| Orientation at every width | Below 140 columns the status line reports the active comparison, so the operator never loses context |
| Request identity | Every participant's identity header carries its request number (`#N`) and timestamp; the Raw view prints a `Request:` line with the same |
| Persistence renounce | `x` outside Compare clears the active comparison (reference preserved); with only a reference it renounces the reference via `RenounceReference()` |
| Scrollable bodies | Body and Raw views scroll through their full content; scroll position persists across view switches |
| Orientation at every width | Below 140 columns the status line reports the active comparison (incl. `Reference · x renounces`), so the operator never loses context |
| Workflow convergence | `c`, `s`, `x`, `Esc` each own exactly one meaning |
| Terminology convergence | Mark / Pinned Baseline / Candidate / Compare / Swap / Clear / Exit / Resume |
| Terminology convergence | Mark / Reference / Candidate / Compare / Swap / Clear / Exit / Resume |

---

Expand All @@ -460,7 +464,7 @@ a `Model` value — a pointer receiver would cause an interface conversion panic

3. **This release does not add statistical or multi-sample analysis.** Every Comparison compares exactly two single-shot results.

4. **This release does not implement a comparison history.** Sessions are in-memory only. Pinned baselines survive one startRun cycle but are not persisted to disk.
4. **This release does not implement a comparison history.** Sessions are in-memory only. References survive one startRun cycle but are not persisted to disk.

5. **This release does not change the Observe result list beyond adding comparison markers** (▶, ◆, ●).

Expand Down
63 changes: 42 additions & 21 deletions internal/tui/COMPARE_WORKFLOW.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Compare Workflow

↩ [internal/tui/README.md](README.md) · [COMPARE_CONSTITUTION.md](COMPARE_CONSTITUTION.md)

## Purpose

This document defines the Compare workflow lifecycle, behavioural invariants, and canonical vocabulary. It is the operator-facing companion to `COMPARE_CONSTITUTION.md` (which covers architecture and rendering).
Expand Down Expand Up @@ -39,7 +41,7 @@ BaselineMarked

| State | Meaning |
|-------|---------|
| Idle | No comparison workflow active. PinnedBaseline may still exist. |
| Idle | No comparison workflow active. Reference may still exist. |
| BaselineMarked | A baseline result is selected, but no candidate has been chosen. |
| Comparing | Both baseline and candidate are set. Comparison analysis is computed and displayed. |

Expand All @@ -50,18 +52,23 @@ candidate are resolved `*model.Result` pointers — there are no indices.

| From | Trigger | To | Effect |
|------|---------|----|--------|
| Idle | `c` on result (no PinnedBaseline) | BaselineMarked | `MarkBaseline(r)`; resolved baseline pointer set. |
| Idle | `c` on result (PinnedBaseline set) | Comparing | Baseline = PinnedBaseline, then `SelectCandidate(r)`. |
| BaselineMarked | `c` on same result | Idle | `Unmark()`; PinnedBaseline preserved. |
| Idle | `c` on result (no Reference) | BaselineMarked | `MarkBaseline(r)`; resolved baseline pointer set. |
| Idle | `c` on result (Reference set) | Comparing | Baseline = Reference, then `SelectCandidate(r)`. |
| BaselineMarked | `c` on same result | Idle | `Unmark()`; Reference preserved. |
| BaselineMarked | `c` on different result | Comparing | `SelectCandidate(r)`; analysis recomputed once. |
| Comparing | `c` on baseline result | Idle | `Clear()`; PinnedBaseline preserved. |
| Comparing | `c` on baseline result | Idle | `Clear()`; Reference preserved. |
| Comparing | `c` on candidate result | Comparing | Resume workspace (`Enter Compare`); comparison unchanged. |
| Comparing | `c` on different result | Comparing | `ReplaceCandidate(r)`; analysis recomputed once. |
| Comparing | `s` | Comparing | `Swap()`; baseline/candidate exchanged, analysis recomputed. |
| Comparing | `x` | Idle | `Clear()`; PinnedBaseline preserved, returns to Observe. |
| Any | `Esc` | Observe | `Exit()` — no mutation; session preserved for resume. |
| Comparing | `x` | Idle | `Clear()`; Reference preserved, returns to Observe. |
| Any | `Esc` | Observe | Esc handled inline (no workspace mutation); session preserved for resume. |
| Any | `q` | Quit dialog | Application quit on confirm. |

Outside Compare mode (Observe / Inspect), `x` is context-sensitive: it clears
the active comparison (Baseline + Candidate + State + Analysis) while preserving
the reference; when **only** a reference remains, `x` renounces the
reference entirely (`RenounceReference()`), ending its persistence across runs.

---

## Views
Expand All @@ -70,20 +77,26 @@ Once Comparing, the workspace is presented through discrete views. Every view
consumes the identical immutable `CompareContext`; switching views only changes
presentation, never the data.

Each participant's identity header carries its **request number** (`#001`,
1-based arrival order) and **timestamp**, so the operator always knows *what*
and *when* without leaving the workspace. The Raw view additionally prints a
`Request:` line with the same number and time for both baseline and candidate.

| View | Content |
|------|---------|
| Overview | Verdict + Why (direction-only sentences). Default. |
| Evidence | Categorical deltas: Status, Latency, Headers, Body, Errors. |
| Diff | URL change and full body diff. |
| Headers | Added / removed / changed response headers. |
| Body | Full baseline and candidate response bodies. |
| Raw | Full baseline and candidate result dumps. |
| Body | Full baseline and candidate response bodies — scrollable. |
| Raw | Full baseline and candidate result dumps — scrollable. |

Navigation:

* `[` / `]` — previous / next view (wraps around).
* `Tab` / `Shift+Tab` — same as bracket navigation.
* `up` / `k` / `down` / `j` — scroll the active view.
* `up` / `k` / `down` / `j` — scroll the active view (Body and Raw scroll through
their full content; scroll position persists across view switches).
* `Esc` — Exit (preserves the view for resume).

The current view name is shown in the status line (`Comparing · <View>`).
Expand All @@ -98,14 +111,22 @@ context panel the moment a baseline is marked, and persists until cleared.

It always shows:

* baseline identity (◆) and, when present, candidate identity (▶),
* baseline identity (◆) and, when present, candidate identity (▶), each with its
request number and timestamp; a pinned-only state shows `● Pinned Baseline`,
* the verdict when a comparison is active,
* an orientation hint: `c on ▶ to open · x clears` (comparing) or
`c on a result to compare` (baseline only).
* a context-specific keybinding line so the next action is obvious without
leaving the drawer: `c on ▶ open · x clear · s swap · [ ] view` (comparing),
`c compare · x clear` (baseline marked), or `c compare · x renounce`
(reference only).

The preview is the right-side drawer: it is deliberately richer than a preview
so the operator can act on the comparison (open, clear, swap, renounce) directly
from Observe/Inspect without entering the full workspace.

Even when the context panel is hidden (terminal narrower than 140 columns), the
status line reports the active comparison (`Comparing · c on ▶ to open` /
`Baseline marked · c to compare`), so the operator never loses orientation.
`Baseline marked · c to compare` / `Reference · x renounces`), so the
operator never loses orientation.

Because the preview shares `renderComparisonIdentityBlock` with the full
workspace, the two never drift.
Expand All @@ -114,13 +135,13 @@ workspace, the two never drift.

## Behavioural Invariants

1. **Compare never destroys a baseline implicitly.** Clear (`x`) resets the session but leaves PinnedBaseline intact. Only a new `startRun` may update PinnedBaseline.
1. **Compare never destroys a baseline implicitly.** Clear (`x`) resets the session but leaves Reference intact. Only a new `startRun` may update Reference.

2. **Swap never changes comparison content.** Swap exchanges the resolved baseline and candidate pointers. The same two results are compared — only the perspective changes, so the directional verdict reflects the new orientation.

3. **Exit never clears comparison.** `Esc` from Compare mode returns to Observe while preserving Session state. The operator can resume viewing the comparison at any time.

4. **Clear only clears the comparison session.** `x` resets Baseline, Candidate, State, and Analysis to their zero values via `Clear()`. PinnedBaseline is never modified by Clear.
4. **Clear only clears the comparison session.** `x` resets Baseline, Candidate, State, and Analysis to their zero values via `Clear()`. Reference is never modified by Clear.

5. **Rendering never mutates workflow state.** All render functions accept the model as a value receiver and produce output without side effects. Workflow state changes occur only in `handle*Key` methods.

Expand All @@ -141,13 +162,13 @@ workspace, the two never drift.
| Exchange baseline and candidate | Swap | Ribbon, action |
| End the active comparison | Clear Comparison | Ribbon, status |
| Leave Compare without clearing | Exit Compare | Status, docs |
| Start comparison with PinnedBaseline | Compare (auto) | Status, implicit |
| Start comparison with Reference | Compare (auto) | Status, implicit |

### Prohibited synonyms

| Avoid | Use Instead |
|-------|-------------|
| Mark / Pin / Reference interchangeably | Mark Baseline / Pinned Baseline |
| Mark / Reference interchangeably | Mark Baseline / Reference |
| Active result | Candidate |
| Clear All / Reset | Clear Comparison |
| Flip / Reverse | Swap |
Expand All @@ -162,13 +183,13 @@ Workflow tests in `architecture_test.go` verify operator-facing behaviour:
| Test | Story |
|------|-------|
| `TestV0102Workflow_MarkReplace` | Mark A, c on B → B is candidate |
| `TestV0102Workflow_ClearPreservesPin` | Compare, x → session cleared, pin intact |
| `TestV0102Workflow_ClearPreservesReference` | Compare, x → session cleared, reference intact |
| `TestV0102Workflow_SwapTwiceReturns` | s then s → original orientation restored |
| `TestV0102Workflow_ExitPreservesSession` | Esc → Observe → session preserved |
| `TestV0102Workflow_ClearComparison` | Compare, x → Idle, Observe |
| `TestV0102Workflow_CrossRunPin` | Mark → startRun → new results → compare with pin |
| `TestV0102Workflow_CrossRunReference` | Mark → startRun → new results → compare with reference |
| `TestV0102Workflow_MarkSameUnmarks` | c on already-marked → clears |
| `TestV0102Workflow_NoBaselineCleared` | PinnedBaseline only → UI shows ● Pinned Baseline |
| `TestV0102Workflow_NoBaselineCleared` | Reference only → UI shows ● Pinned Baseline |

---

Expand Down
2 changes: 2 additions & 0 deletions internal/tui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,8 @@ New to the TUI codebase? Read in this order:
| Document | What it answers |
|---|---|
| `STATE_OWNERSHIP.md` | Model field ownership, lifetime, mutation rules |
| `COMPARE_CONSTITUTION.md` | Comparison engine contract: state model, analysis, render projections |
| `COMPARE_WORKFLOW.md` | Comparison UX: keybindings, lifecycle, persistence, preview behaviour |
| [RENDERING.md](../../RENDERING.md) | TUI rendering architecture, layout, render lifecycle, constitution |
| [ARCHITECTURE.md](../../ARCHITECTURE.md) | System architecture, components, APIs, engine, concurrency |
| [README.md](../../README.md) | Product overview, installation, quick start |
Expand Down
Loading
Loading