# BUILD_SPEC — Console v2 Audit Polish Pass 2

**Generated:** 2026-05-16
**Input:** `recoil/console-v2/audits/2026-05-14-walkthrough.md`
**Detail level:** high
**Visual design:** no (CSS micro-fix only)
**Phases:** 1
**Estimated build time:** 10-15 minutes

## Status of prior items

Phase 5 PASS (commit 7aa3199b) already addressed:
- M1 empty-state in TakesBrowser ✓
- M3 truncation tooltip on NavRow.label ✓
- H5 Events filter chip math ✓
- X3 timestamp format (EventTs component) ✓

H3 `/api/config` — already wired in `recoil/api/main.py` ✓

## Open items (this spec)

| ID | Description |
|----|-------------|
| M2 | TakeCard `.take-meta` row: EvalPill badge sits flush against model name string |
| M4 | FlatShotList: driver-beware shots show no take counter; all episode projects do |
| M5 | EngineMemoryInspector: LEARNING rows render blank NOTE cell when `m.text` is empty |
| M6 | EngineMemoryInspector: ANTI-PATTERN notes use `\|` as paragraph separator, renders as literal pipe |

## Validation command

```bash
pnpm --filter desktop test
```

Expected: all 124 tests pass (or more if new tests added). Zero TypeScript errors.

---

## Dependency Graph

Phase 1: none (standalone)

---

## Phase 1: Audit Polish — M2 + M4 + M5/M6

**Scope:** 3 files, all presentational, no logic or data changes.

### Files to modify

1. `recoil/console-v2/packages/desktop/src/stage/stage.css`
2. `recoil/console-v2/packages/desktop/src/nav/FlatShotList.tsx`
3. `recoil/console-v2/packages/desktop/src/stage/EngineMemoryInspector.tsx`

---

### M2 — TakeCard badge/model overlap (stage.css)

**Symptom:** In the expanded TakeCard `.take-meta` row, the EvalPill badge
(`• EVAL` / `× F`) sits flush against `gemini-3.1-flash-image-preview` —
letters visually collide at typical card widths (~280px).

**Root cause:** `.take-meta .left` has `flex-shrink: 0` which prevents the
badge area from shrinking. When the row is narrow, the badge takes its full
natural width and the model-name span in `.right` has no guaranteed left
clearance.

**Fix in `stage.css`:** In the `.take-meta .left` rule, remove `flex-shrink: 0`
and add `max-width: 56%`. This lets the badge area participate in flex
shrinking while capping it so the model name always gets ≥44% of the row.
Also add `overflow: hidden` to prevent visual bleed.

Current `.take-meta .left` rule (lines ~228–235 in stage.css):
```css
.take-meta .left {
  display: flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
  flex-wrap: wrap;
  flex-shrink: 0;          /* ← remove */
}
```

Replace with:
```css
.take-meta .left {
  display: flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
  max-width: 56%;
  overflow: hidden;
  flex-wrap: wrap;
}
```

**Scope boundary:** Do NOT touch `.take-meta .right`, `.take-meta .model`,
`.failure-pill`, or any other rule. One block only.

---

### M4 — Flat shot take counter (FlatShotList.tsx)

**Symptom:** In the driver-beware project (flat/client_video type), shot rows
in the hierarchy show no take counter. Episode-project shots show `21t`, `9t`
etc. via `sub={`${b.takes}t`}` in `BeatNode`.

**Fix in `FlatShotList.tsx`:** Add `sub={`${shot.takes}t`}` to the `NavRow`
call inside the shot map. The `Beat` type (`@recoil/contracts`) already has
`takes: number`.

Current NavRow call (inside `state.shots.map`):
```tsx
<NavRow
  key={id}
  depth={1}
  caret="·"
  label={shot.name ?? id}
  focused={isFocused}
  onClick={() => onSelect(id, projectId)}
  badges={...}
/>
```

Add `sub` prop:
```tsx
<NavRow
  key={id}
  depth={1}
  caret="·"
  label={shot.name ?? id}
  sub={`${shot.takes}t`}
  focused={isFocused}
  onClick={() => onSelect(id, projectId)}
  badges={...}
/>
```

**Scope boundary:** One prop addition to one JSX element. Do NOT change
NavRow, the Beat type, or any other component.

---

### M5/M6 — Memory view note rendering (EngineMemoryInspector.tsx)

**Symptoms:**
- M5: LEARNING rows with no `text` field render a blank NOTE cell — looks
  like a render bug. Should show a "—" placeholder.
- M6: ANTI-PATTERN notes with `|` as paragraph separator render as literal
  pipe characters. Should render as line breaks.

**Fix in `EngineMemoryInspector.tsx`:** Replace the `<span className="text">`
cell renderer to: (a) split on `|` and render each segment on its own line,
(b) fall back to `—` when text is empty/falsy.

Current render (inside the `.memory-row` map):
```tsx
<span className="text">{m.text}</span>
```

Replace with:
```tsx
<span className="text">
  {m.text
    ? m.text.split("|").map((seg, i) => (
        <React.Fragment key={i}>
          {i > 0 && <br />}
          {seg.trim()}
        </React.Fragment>
      ))
    : "—"}
</span>
```

`React` is already imported (`import * as React from "react"`). No new
imports needed.

**Scope boundary:** One expression change in the `.memory-row` map body.
Do NOT change the table header, the toggle handler, the confidence/hits
columns, or anything outside the single `<span className="text">` element.

---

### Validation

```bash
# TypeScript compile check
pnpm --filter desktop build --noEmit 2>&1 | grep -E "error|Error" | head -20

# Run full test suite
pnpm --filter desktop test
```

Expected output: `Tests 124 passed (124)` (or higher if tests were added).
Zero TypeScript errors.

### Structural checks

```bash
# M4: sub prop added
grep -n 'sub={`\${shot.takes}t`}' recoil/console-v2/packages/desktop/src/nav/FlatShotList.tsx

# M5/M6: pipe split renderer
grep -n 'split("|")' recoil/console-v2/packages/desktop/src/stage/EngineMemoryInspector.tsx

# M2: max-width on .take-meta .left
grep -n 'max-width.*56' recoil/console-v2/packages/desktop/src/stage/stage.css
```

All three greps must return at least one hit.

---

## What already exists

- `Beat` type in `@recoil/contracts` has `takes: number` (contracts/src/generated.ts:19)
- `NavRow` accepts `sub?: string | undefined` (nav/NavRow.tsx:15)
- `EngineMemoryInspector` already imports `React` (line 1)
- `stage.css` `.take-meta .left` rule at approximately lines 228–235
- `FlatShotList.tsx` NavRow map at approximately lines 80–110
