# Test Matrix

Current proof status is stored in the Harness durable layer and queried with:

```bash
scripts/bin/harness-cli query matrix
```

This markdown file is a human-readable fallback for the initial brownfield baseline.

## Status Values

| Status | Meaning |
| --- | --- |
| planned | Accepted as intended behavior, not implemented |
| in_progress | Actively being built |
| implemented | Implemented and proof exists |
| changed | Contract changed after earlier implementation |
| retired | No longer part of the product contract |

## Matrix

| Story | Contract | Unit | Integration | E2E | Platform | Status | Evidence |
| --- | --- | --- | --- | --- | --- | --- | --- |
| H-001 | Harness docs describe the existing TypeScript Pi UI styling extension accurately | no | no | no | yes | implemented | `srcwalk review --limit 10`, `srcwalk overview --scope . --depth 2`, `scripts/bin/harness-cli query matrix`, docs placeholder/upstream grep checks |
| US-008 | Fixed-zone selection supports drag range, double-click word, triple-click line, release-to-copy feedback, fixed cluster selection, OSC 52 clipboard propagation for terminal proxies, and auto-scroll on drag past viewport edges | no | yes | no | pending | implemented | `npm run test:user-zone-style` covers fixed cluster drag highlight/release-copy, terminal-scoped OSC 52 emission, long root selection release inside the fixed cluster, and auto-scroll content after drag past viewport edge with throttled/no-op scroll renders; repeated-copy transport uses immediate OSC 52 plus serialized host clipboard writes; `srcwalk review --scope .`; scoped TypeScript no-emit for `index.ts`, fixed-zone install/split/selection; `git diff --check`; `PI_DROID_PROFILE_BENCH_ROOT_LINES=6000 PI_DROID_PROFILE_BENCH_ITERATIONS=30 npm run profile:render`; manual jump/browser OSC 52 smoke pending |
| US-009 | Fixed-zone notices render in a reserved themed bottom footer row and copy feedback uses that local surface | yes | yes | no | pending | implemented | Pending validation for notice render smoke, scoped TypeScript no-emit, render profile, and srcwalk review; manual Pi themed pill smoke pending |
| US-014 | `customWorkingMessage` defaults to an object of loader labels, migrates legacy booleans, backfills partial objects, and renders configured loader labels | yes | yes | no | pending | implemented | `npm run test:working-message`; `git diff --check`; `semantic_review` |
| US-016 | Theme extras ending in `Color` resolve semantic theme tokens/aliases while non-color extras remain literal | yes | yes | no | no | implemented | `npm run test:theme-extras`; `npm run test:startup-resources`; `npm run test:working-message`; `git diff --check`; `semantic_review` |
| US-017 | `userZoneStyle` defaults to `gemini` and can select `droid`, `gemini`, or `cli-dock` presets without changing theme format, with Gemini status/input/footer, `❯` user-prefix accent isolation from `bashPromptColor`, droid legacy layout, and cli-dock outlined prompt/prompt/status-line layout | yes | yes | no | pending | implemented | `npm run test:user-zone-style` with cli-dock render and inputBox.style override guard smoke; `npm run test:working-message`; `npm run test:theme-extras`; `npm run test:startup-resources`; `git diff --check`; `srcwalk review --limit 10` |
| US-018 | Pi core special message blocks (Compaction, Skill, Branch, and Custom) render as boxed blocks with page-surface background, reload-safe patches, custom fallback deduplication, no-box fallback background, custom renderer wrapping, and distinct special titles | yes | yes | no | no | implemented | `npm run test:core-message-blocks`; `npm run test:user-zone-style`; `semantic_review working-tree` |
| US-019 | `tasksWidgetStyle` defaults to `compact`, can opt back to `default` with no `pi-tasks` patching, renders compact as a one-line summary with `[id]`, dim elapsed time, token counters stripped, done/running counts, blocked/idle/done states, real-width truncation, and 3s rotation through in-progress tasks | yes | yes | no | no | implemented | `node scripts/tasks-widget-compact-smoke.mjs`; `semantic_review working-tree`; reviewer agent |
| US-020 | `visibleChatTail` defaults to 30, scaffolds/backfills config, accepts 0 as render-all, renders custom tails with an omitted-history indicator, live-reconfigures patched chat containers (including nonzero growth restore + reload-safe InteractiveMode getter), keeps fixed-zone tail=0 windowing bounded, and enforces tail=N under fixed-zone windowed root (leaf + config fallback slice) | yes | yes | no | no | implemented | `npm run test:tasks-widget-compact` covers config normalization/backfill, tail=N, tail=0, reconfigure 2→0 / 2→3 / 3→10, interactive hook reinstall with new getter; `npm run test:user-zone-style` covers fixed-zone + tail=0 windowed root, fixed-zone + tail=N leaf (no oldest history), and fixed-zone + tail=N config fallback without virtualize patch; `git diff --check`; `semantic_review working-tree` |
| US-021 | `presentationStyle` defaults to `droid` and can select `reasonix` for compact turns, a shared column-3 baseline for user/assistant/tool content, colored tool statuses, single-row Droid-compact tools plus non-compact header + `└─` metrics rows with one shared responsive 80% collapsed cap, width-safe corner-connected `Ctrl+O` bodies aligned in column 6, and one spacer row between blocks; droid remains unchanged | yes | yes | no | no | implemented | `npm run test:reasonix-conversation` covers config, semantic statuses, collapsed Bash preview removal, single-row command/error normalization, first-line/continuation/tool alignment plus inline italic theme-native `thinkingText` thinking and thinking-only→answer streaming, 24/80/160-column responsive caps, compact/non-compact metrics placement and right-side metric preservation, 24/80/160 expanded body budgets, semantic-dim custom/generic collapsed/expanded connectors matching status/footer color, connector/continuation/footer alignment, ANSI-padding removal, generic overflow clamps, legacy-divider removal, user/assistant/tool spacing, quick-edit states, and droid fallback; `npm run test:session-resume-styling` covers Jiti runtime A→B ownership plus next-tick definition/display refresh for visible and virtualized-hidden restored tool rows; PTY resume smoke with fixed-zone enabled; full focused suite; `git diff --check`; `srcwalk review`. |
| US-022 | `userZoneStyle` accepts `nvim` as a fourth preset (data-only addition, config validation/fallback/backfill unchanged) rendering a `line`-framed input plus one full-width statusline bar: a leading solid-block badge (reverse video) shows the thinking level uppercased verbatim from Pi's own six levels or `BASH` while the input starts with `!`, coloured by mode not by level (`accent` for normal input — the theme's own general highlight token, so retuning `accent` deliberately carries the badge along with every other accented element; measured alternatives (`syntaxNumber`, `syntaxType`) scored higher on raw contrast/distinctness but borrow meaning from syntax highlighting, an unrelated part of the theme; known and accepted that `accent === bashMode` in 5-of-26 themes, where bash mode recolours only the label; `theme.getBashModeBorderColor()` for bash — never `getThinkingBorderColor`, whose tokens are tuned for a thin border line and turn into a muddy slab as a filled block), and no badge at all for a non-reasoning model (including `reasoning: undefined`); `provider · model` on the left, `branch · tokens ctx% · CH%` right-aligned with a four-rung width degradation ladder that keeps the row at exactly the terminal width, and any other extension's status appended at the far right of that same row, truncated with `…` if it overflows and never wrapped onto a second row, and never fully dropped once the bar has room for it (at very narrow widths the status can still be truncated away entirely — the accepted limitation recorded below); `droid`/`gemini`/`cli-dock` stay byte-identical, and presentation rendering (prose, tool calls, tool bodies) is untouched under every `presentationStyle` | yes | yes | no | no | implemented | `npm run test:nvim-user-zone` covers config validation/fallback/reload, style resolver data, badge label verbatim for all six thinking levels plus `BASH` and no invented `NORMAL` label, no badge at all for non-reasoning/undefined-reasoning models, badge coloured by mode not level (all six thinking levels share the exact `accent` colour, bash uses `theme.getBashModeBorderColor()`, and a trap asserts `getThinkingBorderColor` is never called); row-shape/placeholder/autocomplete assertions locate the statusline bar by its `selectedBg` background wrapper, which is unique only for the `line`/`outline` input frames this suite renders (`solid`/`halfblock` also wrap their padding in the same background, so this marker is NOT globally unique) and is never the badge's reverse-video escape, which is unsafe even for `line`/`outline` because the input box's own text cursor renders with the same escapes — verified with a real slash-autocomplete session (`editor.autocompleteState`/`autocompleteList` populated in the exact shape the base editor and this preset's `renderSlashAutocomplete` override both read) proving the bar stays uniquely located with extra rows appended after it; every other content assertion calls the statusline renderer directly instead, sidestepping row-location entirely. Left-cluster room allocation: the model id is hard-capped at source — `NVIM_MODEL_ID_MAX=40` visible columns (ellipsis included), applied by `truncatePlain` before any rung or the recompose path sees the id — because real-world ids top out at ~36 columns (`anthropic/claude-3.5-sonnet-20241022` = 35), so 40 always shows a real id in full while a pathological 100+-column fixture id can no longer budget the status or chrome off its own untruncated width; every rung renders the same capped id, so no rung switch can shrink the model; `avail` is `width - actualLeftW - 2` (the scoring-side `NVIM_LEFT_AVAIL_CAP` clamp is deleted — with the id capped at source the left cluster's actual width is bounded by construction, making a second, scoring-side cap redundant; not a runtime switch between two formulas either — that was tried, fixed the status side, but broke chrome monotonicity at the switch boundary, reproduced against a rebuilt pre-fix baseline and reverted); within `recomposeNvimLeft`, the provider is sacrificed before the model id, which is protected down to `NVIM_MIN_MODEL_WIDTH=8` visible columns; badge breath gap: whenever the badge block and the model id are both rendered, exactly one space sits OUTSIDE the reverse-video block between its edge and the next character (the badge's own trailing space lives INSIDE the block), a real column of the left cluster on every rung and through the recompose branch — a deliberate, user-requested baseline change on the two narrow ladder rungs, which now read `" HIGH  claude"` instead of the old glued `" HIGH claude"` (the three wide rungs are unchanged); known, accepted limitation: at very narrow widths (~30 columns) a long model id still shows no status, because the left cluster itself needs the room — the same width where even the default short model is already visibly cramped; five independent properties scanned across width 1..200 (the chrome one across 20..140): line integrity (no full `\x1b[0m` reset — the bar truncates on plain text only via a `truncatePlain` helper that strips the reset pi-tui's own truncator injects on plain Unicode/CJK/emoji input, its ASCII fast path is clean — and exact row `visibleWidth`, not `.length`, so a CJK/emoji fixture can't silently under/over-count) over ASCII/150-char-ASCII/CJK/emoji model ids each crossed with no-status/ASCII-status/long-ASCII-status fixtures (CJK/emoji-status fixtures are run separately against the default model to keep the scan proportionate), plus a dedicated missing-`model.id` fixture (the field is optional on the model-info type) and a bash-mode recompose scan; status retention (monotonic — never shrinking as the bar widens, reaching its full length by width 200, never showing a bare `…` once the status is fully dropped) measured via three repeated-sentinel fixtures (ASCII/CJK/emoji) run as a TRUE cross product against all three model fixtures (short/150-char-ASCII/CJK), so retention is now verified for the same over-long models Property 1 only checked for integrity, where any contiguous run of the identical sentinel character surviving at the tail is, by construction, a correct prefix of the full status; and chrome (branch, token/context usage, context%, cache-hit%, and the provider name) never disappearing once shown as the bar widens, guarding the exact regression the reverted two-formula attempt introduced; model-id retention — the id's visible sentinel run never shrinking as the bar widens (150-char ASCII and 100-char CJK ids, each crossed with no-status/ASCII/CJK/emoji statuses; written RED-FIRST against the pre-fix allocator, where the with-provider rung switched on at w45/w54-class boundaries and silently stole columns from the model, and made green by the source cap; by width 200 the capped id shows its full cap — 39 `A` + `…` = 40 columns, while the CJK fixture shows 19 double-width `模` + `…` = 39 columns, one short of the cap because a double-width grapheme cannot be split to fill the last column); and the badge breath gap asserted on the RAW row at escape level — after the badge's `\x1b[27m` reverse-off, the run of literal spaces before the next SGR code must be exactly 1 column whenever that code is not the row-end background close `\x1b[49m` (a `\x1b[49m` there means a badge-only row, where the run is row padding and the property does not apply), with the match itself asserted so a non-match is a failure, never a pass — across the recompose and non-recompose branches and both normal and bash badge colourings; the oracle is mutation-proven: Biscuit's `badgeGap="  "` mutation, which the first version of this property survived green (a vacuous `\x1b[27m\x1b[49m`-adjacency match that never fired on real output), now turns it red; one chrome bar with zero duplicated values, extension status appended to the same row (constant row count) with truncation instead of wrapping, bash-mode boundaries (a leading newline or leading whitespace before the bang counts via trimStart; only-whitespace input or a bang embedded mid-line does not), a pinned `CH%` extraction contract including `CH100.0%`/`CH0.0%` against `getFooterTokenUsage`'s existing bracket string, `inputBoxStyle` override still winning over the `nvim` default, placeholder show/hide, and all 26 bundled companion themes resolving the eight tokens the chrome uses; `npm run test:user-zone-style` unchanged for `droid`/`gemini`/`cli-dock` including the cli-dock placeholder after it moved into preset data; `npm run test:reasonix-conversation` untouched; `git -c core.whitespace=-blank-at-eol diff --check`; `npm run check` |
| US-023 | The nvim preset embeds the git branch label into the TOP rule of its `line` input frame — `rule xN + ' ⎇ name +N -M ' + one trailing dash`, exactly the terminal width — using the SHARED branch-badge FORMATTER (`buildBranchBadge`) that all existing non-nvim branch-badge call sites use (droid's `renderTopRow` and gemini's status row — cli-dock renders no branch at all) for token order/spaces/brackets, with COLOUR caller-decided (a `tones` map: tone NAME, custom colorizer FUNCTION, or `null` for terminal-default fg) — the nvim rule colours `⎇` and the branch name `muted` (FINAL user approval: the same tier the model id uses on the bar, because the branch is identity like the model id; earlier rounds tried unstyled and rule-tone and were rejected by the user as too bright/too faint), the LOC rendered BARE (`+2 -1`, no brackets, per the gitsigns/lualine convention) keeping the semantic `success`/`error` tones, and the rule's dashes keep `inputRuleColorizer()` as their single source; the legacy call sites keep their historic `bashMode`/`mdLinkUrl` styling byte-identical via the formatter's default tones; the name is normalized like status text and capped at source (`NVIM_BRANCH_MAX=24`, `…`), the LOC tail is never truncated, and the label degrades in two monotonic rungs — full, then name-only (churn yields to identity), then a plain rule (needs 2 leading dashes + block + trailing dash, below that no label and no dangling ellipsis); no branch or a non-`line` frame override renders no label and no error; `⎇ branch` is REMOVED from the statusline right cluster (a value must never appear twice), its columns flowing to the extension status through the unchanged candidate scoring; the degradation ladder changes DELIBERATELY, twice: first when the branch left the bar, then by post-2.12.0 tuning — the context metric OUTRANKS the provider (decoration, sacrificed first), so the rung order is P+full > M+full > M+tokensCtx > M+ctx% > none, the full metric (incl. CH%) fits from w45 on the model-only left, the provider re-joins only from w57, and the metric cluster is rendered `muted` (the tier the model id uses — a value the user reads constantly) while the extension status stays `dim` (both pinned on RAW via the suite's tone source, and the muted-→dim chrome mutation is proven red); the other three presets and the bottom rule stay byte-identical | yes | yes | no | no | implemented | `npm run test:nvim-user-zone` — Property 7 (red-first greenfield: written before implementation, red because the label never appeared) scans 8 branch fixtures (main, CJK `feature/rename-用戶-flow`, 40-char, CJK 60-col, git-legal `fix+2-retry`, ins-only, del-only, no-LOC) x width 1..200: label at most once, two-rung degrade monotonic AND the middle rung proven REACHABLE for LOC-carrying fixtures (reachability is what mutation M3 exposed: monotonicity alone survives deleting the LOC-degrade branch), exact `rule + ' label ' + one dash` shape, LOC success/error tones iff stage 2 on RAW, no `\x1b[0m` leak from the label sitting between the rule's border colour, exact `visibleWidth`; plus no-git plain-rule scan and a P3 negative assert (no `⎇` in the statusline at any width/fixture); mutations proven red: glued label (M1), drop-law removed (M2), LOC-rung removed (M3), degrade order inverted (M4), `⎇` re-added to the statusline (P3), name switched to ruleFg or unstyled (M6-final, both variants), source cap removed (M7), and bare→brackets on the nvim path (M8, locking the FORMAT); the label's `⎇ <name>` glyphs are asserted to carry the MUTED SGR (via the suite's tone source, each segment closed with `\x1b[39m`), the displayed name is pinned BOTH ways against `truncatePlain` at the contract cap (40-char ASCII exact cut `x`×23+`…` plus no 24+ `x` run; CJK fixtures by the same helper, no hardcoded strings), and stage detection is FORMATTER-OWNED — the row is classified against `buildBranchBadge`'s own plain output per rung (anchored by the rule's spaces and trailing dash), so a branch named `fix+2-retry` (git-legal `+`/`-` in names, pinned fixture) can never be mistaken for LOC, each LOC sign+number must be ONE coloured segment (`\x1b[92m+2\x1b[39m`, not a bare sign beside a coloured digit), zeros self-hide, and an unknown label fails outright; P1/P2/P4/P6 stay green; top-border shape gate updated to the new label-bearing rule |

## Evidence Rules

- Unit proof covers pure helpers such as formatting, parsing, and status calculations when tests exist.
- Integration proof covers behavior that depends on Pi component APIs or Node process interactions.
- E2E proof covers user-visible Pi flows when an automated Pi runner exists.
- Platform proof covers terminal rendering, theme sync, shell/process behavior, and manual Pi smoke checks.
- Do not mark automated proof columns as passing until corresponding commands exist and have been run.
