docs(runner): add Phase 7B scenario explorer UX spec and mockup

Interaction map, screenshot acceptance matrix, and annotated mockup for
the Phase 7 browser scenario explorer (PAP-16899). Binds picker facets,
transcript channels, inspector tabs, deterministic routes, and the
twelve-shot acceptance set to the 7A capability contract.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
DottaandClaude Fable 5 committed 2026-08-09 12:29:21 +00:00
1 parent 99a9fa30f7
commit 5e0014a239
5 files changed
+778

No files matched your search

@@ -0,0 +1,329 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Phase 7 scenario explorer — annotated mockup (ap-mcp-gate-01)</title>
<style>
/* Static mockup only. Values mirror the package --pcr-* token layer;
the implementation must consume the real tokens, never these copies. */
:root {
--bg: #f7f7f5; --fg: #191917; --card: #ffffff; --muted: #efefeb;
--muted-fg: #66665f; --border: #deded7; --primary: #242420; --primary-fg: #fff;
--success: #17633a; --success-surface: #e9f5ed;
--warning: #8a5a10; --warning-surface: #fff4dc;
--danger: #a02f2f; --danger-surface: #fceaea;
--accent: #2f5fa8; --accent-fg: #fff; --accent-surface: #e9f0fa;
--raised: #fcfcfa; --ring: #66665f;
--sans: Inter, ui-sans-serif, system-ui, sans-serif;
--mono: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
--r-sm: 0.375rem; --r-md: 0.625rem;
--shadow: 0 0.0625rem 0.125rem rgb(0 0 0 / 0.05);
}
* { box-sizing: border-box; min-width: 0; }
body { margin: 0; background: var(--bg); color: var(--fg); font: 400 0.875rem/1.5 var(--sans); }
.mono { font-family: var(--mono); }
.xs { font-size: 0.75rem; }
header.app {
display: flex; align-items: center; gap: 0.75rem; padding: 0.5rem 1rem;
background: var(--raised); border-bottom: 1px solid var(--border);
}
header.app h1 { font-size: 0.875rem; font-weight: 600; margin: 0; }
header.app .sub { color: var(--muted-fg); font-size: 0.75rem; }
.corpus { margin-left: auto; color: var(--muted-fg); font-size: 0.75rem; }
.shell { display: grid; grid-template-columns: 17rem minmax(0,1fr) 24rem; gap: 0; height: calc(100vh - 2.6rem); }
/* ---------- left rail ---------- */
nav.picker { border-right: 1px solid var(--border); background: var(--raised); overflow-y: auto; padding: 0.75rem; display: flex; flex-direction: column; gap: 0.75rem; }
.search { display: block; width: 100%; height: 2rem; border: 1px solid var(--border); border-radius: var(--r-sm); padding: 0 0.5rem; font: inherit; background: var(--card); color: var(--muted-fg); }
.facet h3 { margin: 0 0 0.25rem; font-size: 0.75rem; font-weight: 600; color: var(--muted-fg); text-transform: uppercase; letter-spacing: 0.04em; }
.chips { display: flex; flex-wrap: wrap; gap: 0.25rem; }
.chip { border: 1px solid var(--border); background: var(--card); border-radius: 999px; padding: 0.05rem 0.5rem; font-size: 0.75rem; color: var(--fg); }
.chip.on { background: var(--primary); color: var(--primary-fg); border-color: var(--primary); }
.chip.off { color: var(--muted-fg); opacity: 0.55; }
.active-filters { display: flex; flex-wrap: wrap; gap: 0.25rem; align-items: center; padding: 0.375rem; background: var(--accent-surface); border-radius: var(--r-sm); }
.active-filters .clear { margin-left: auto; font-size: 0.75rem; color: var(--accent); background: none; border: 0; cursor: pointer; }
.group-h { font-size: 0.75rem; font-weight: 600; color: var(--muted-fg); margin: 0.5rem 0 0.25rem; display: flex; justify-content: space-between; }
ul.cases { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
.case { display: grid; grid-template-columns: 1fr auto; gap: 0 0.5rem; padding: 0.375rem 0.5rem; border-radius: var(--r-sm); border-left: 3px solid transparent; }
.case .id { font-family: var(--mono); font-size: 0.72rem; color: var(--muted-fg); }
.case .t { font-size: 0.78rem; line-height: 1.25; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.case.sel { background: var(--accent-surface); border-left-color: var(--accent); }
.dot { width: 0.55rem; height: 0.55rem; border-radius: 999px; align-self: center; }
.dot.pass { background: var(--success); }
.dot.fail { background: var(--danger); }
.badge { display: inline-flex; align-items: center; gap: 0.25rem; border-radius: 999px; padding: 0.05rem 0.5rem; font-size: 0.7rem; font-weight: 500; border: 1px solid var(--border); background: var(--muted); color: var(--fg); white-space: nowrap; }
.badge.cp { background: var(--muted); color: var(--muted-fg); }
.badge.always { background: var(--accent-surface); color: var(--accent); border-color: transparent; }
.badge.optional { background: var(--warning-surface); color: var(--warning); border-color: transparent; }
.badge.grp { background: var(--card); }
.badge.deny { background: var(--danger-surface); color: var(--danger); border-color: transparent; }
.badge.allow { background: var(--success-surface); color: var(--success); border-color: transparent; }
/* ---------- center ---------- */
main.run { overflow-y: auto; padding: 1rem 1.25rem; display: flex; flex-direction: column; gap: 0.75rem; max-width: 48rem; width: 100%; margin: 0 auto; }
.runhead { display: flex; flex-direction: column; gap: 0.5rem; }
.runhead .row1 { display: flex; align-items: baseline; gap: 0.5rem; flex-wrap: wrap; }
.runhead h2 { margin: 0; font-size: 1rem; font-weight: 600; }
.banner { display: flex; gap: 0.5rem; align-items: center; border-radius: var(--r-md); padding: 0.5rem 0.75rem; font-weight: 500; }
.banner.pass { background: var(--success-surface); color: var(--success); }
.controls { display: flex; gap: 0.5rem; align-items: center; }
.seg { display: inline-flex; border: 1px solid var(--border); border-radius: var(--r-sm); overflow: hidden; }
.seg button { border: 0; background: var(--card); padding: 0.25rem 0.75rem; font: inherit; font-size: 0.78rem; color: var(--muted-fg); }
.seg button.on { background: var(--primary); color: var(--primary-fg); }
.btn { border: 1px solid var(--border); background: var(--card); border-radius: var(--r-sm); padding: 0.3rem 0.9rem; font: inherit; font-size: 0.8rem; font-weight: 500; }
.btn.primary { background: var(--primary); color: var(--primary-fg); border-color: var(--primary); }
.relay-note { color: var(--muted-fg); font-size: 0.72rem; }
.transcript { display: flex; flex-direction: column; gap: 0.5rem; }
.cpa { background: var(--muted); border-radius: var(--r-md); padding: 0.5rem 0.75rem; display: flex; gap: 0.5rem; align-items: flex-start; }
.cpa .glyph { color: var(--muted-fg); font-size: 0.8rem; line-height: 1.4; }
.cpa p { margin: 0; font-size: 0.8rem; }
.cpa .why { color: var(--muted-fg); font-size: 0.72rem; }
.agent-turn { background: var(--card); border: 1px solid var(--border); border-radius: var(--r-md); padding: 0.5rem 0.75rem; box-shadow: var(--shadow); }
.agent-turn p { margin: 0.15rem 0; font-size: 0.82rem; }
.toolcall { border: 1px solid var(--border); border-radius: var(--r-md); background: var(--raised); }
.toolcall summary, .toolcall .sumline { display: flex; gap: 0.5rem; align-items: center; padding: 0.45rem 0.75rem; font-size: 0.8rem; cursor: default; flex-wrap: wrap; }
.toolcall .op { font-family: var(--mono); font-weight: 600; font-size: 0.78rem; }
.toolcall .outcome { color: var(--muted-fg); font-size: 0.75rem; }
.toolcall .body { border-top: 1px solid var(--border); padding: 0.5rem 0.75rem; display: grid; gap: 0.4rem; }
.kv { display: grid; grid-template-columns: 7rem 1fr; gap: 0.15rem 0.5rem; font-size: 0.75rem; }
.kv dt { color: var(--muted-fg); }
.kv dd { margin: 0; font-family: var(--mono); font-size: 0.72rem; overflow-wrap: anywhere; }
pre.json { margin: 0; background: var(--muted); border-radius: var(--r-sm); padding: 0.4rem 0.5rem; font-family: var(--mono); font-size: 0.7rem; line-height: 1.45; overflow-x: auto; }
.toolcall.denied { border-color: var(--danger); }
.toolcall.denied .sumline { background: var(--danger-surface); border-radius: var(--r-md) var(--r-md) 0 0; }
.deny-detail { padding: 0.5rem 0.75rem; border-top: 1px solid var(--border); font-size: 0.78rem; }
.deny-detail strong { color: var(--danger); }
.redact { display: inline-flex; align-items: center; gap: 0.25rem; background: var(--muted); border: 1px dashed var(--muted-fg); border-radius: var(--r-sm); padding: 0 0.35rem; font-family: var(--mono); font-size: 0.7rem; color: var(--muted-fg); }
.sysnote { text-align: center; color: var(--muted-fg); font-size: 0.75rem; padding: 0.25rem; }
/* ---------- inspector ---------- */
aside.inspector { border-left: 1px solid var(--border); background: var(--raised); overflow-y: auto; display: flex; flex-direction: column; }
.tabs { display: flex; border-bottom: 1px solid var(--border); position: sticky; top: 0; background: var(--raised); }
.tabs button { flex: 1; border: 0; background: none; padding: 0.5rem 0.25rem; font: inherit; font-size: 0.72rem; color: var(--muted-fg); border-bottom: 2px solid transparent; display: inline-flex; justify-content: center; gap: 0.25rem; align-items: center; white-space: nowrap; }
.tabs button.on { color: var(--fg); font-weight: 600; border-bottom-color: var(--primary); }
.tab-chip { background: var(--danger-surface); color: var(--danger); border-radius: 999px; font-size: 0.65rem; padding: 0 0.35rem; font-weight: 600; }
.pane { padding: 0.75rem; display: flex; flex-direction: column; gap: 0.6rem; }
table.auth { width: 100%; border-collapse: collapse; font-size: 0.72rem; table-layout: fixed; }
table.auth td, table.auth th { overflow-wrap: anywhere; }
table.auth th:nth-child(1) { width: 1.2rem; }
table.auth th:nth-child(2) { width: 34%; }
table.auth th:nth-child(3) { width: 3.6rem; }
table.auth th { text-align: left; color: var(--muted-fg); font-weight: 500; padding: 0.25rem 0.35rem; border-bottom: 1px solid var(--border); }
table.auth td { padding: 0.3rem 0.35rem; border-bottom: 1px solid var(--border); vertical-align: top; }
table.auth tr.deny { background: var(--danger-surface); }
.authcard { border: 1px solid var(--border); border-radius: var(--r-sm); background: var(--card); padding: 0.5rem; font-size: 0.72rem; display: grid; gap: 0.3rem; }
/* annotations */
.ann { position: absolute; background: #1d4ed8; color: #fff; font: 600 0.68rem/1.2 var(--sans); border-radius: 0.25rem; padding: 0.15rem 0.4rem; z-index: 50; box-shadow: 0 1px 3px rgb(0 0 0 / 0.3); }
.wrap { position: relative; }
/* segmented mobile control hidden on desktop */
.mseg { display: none; }
@media (max-width: 63.99rem) {
.shell { display: block; height: auto; }
nav.picker, aside.inspector { border: 0; }
nav.picker { display: none; }
.mseg { display: flex; position: sticky; top: 0; z-index: 10; background: var(--raised); border-bottom: 1px solid var(--border); }
.mseg button { flex: 1; border: 0; background: none; padding: 0.6rem 0; font: inherit; font-size: 0.8rem; color: var(--muted-fg); min-height: 2.75rem; }
.mseg button.on { color: var(--fg); font-weight: 600; box-shadow: inset 0 -2px 0 var(--primary); }
main.run { padding: 0.75rem; }
aside.inspector { display: none; }
.corpus { display: none; }
.shell > .ann { display: none; }
.seg button { font-size: 0.72rem; padding: 0.25rem 0.5rem; white-space: nowrap; }
main.run { padding-top: 0.75rem !important; }
}
</style>
</head>
<body>
<header class="app">
<h1>Phase 7 scenario explorer</h1>
<span class="sub">mock control plane · no Paperclip services · no credentials</span>
<span class="corpus">106 cases · 16 groups · fake agent</span>
</header>
<div class="mseg" role="tablist" aria-label="Explorer sections">
<button>Scenarios</button><button class="on">Run</button><button>Inspect</button>
</div>
<div class="shell wrap">
<span class="ann" style="top:0.4rem; left:1rem;">1 · picker: search + 6 facets + grouped case list</span>
<span class="ann" style="top:0.4rem; left:19rem;">2 · run view: verdict banner + mode + transcript</span>
<span class="ann" style="top:0.4rem; right:1rem;">3 · inspector: Context · Authorization · Diff · Parity</span>
<nav class="picker" aria-label="Scenario picker" style="padding-top:2.2rem;">
<input class="search" value="" placeholder="Search 106 cases…" aria-label="Search cases" />
<div class="active-filters">
<span class="chip on">group: ap ×</span>
<button class="clear">Clear filters</button>
</div>
<div class="facet">
<h3>Group</h3>
<div class="chips">
<span class="chip">hb 5</span><span class="chip">co 6</span><span class="chip">st 8</span>
<span class="chip">cm 6</span><span class="chip">se 4</span><span class="chip">su 4</span>
<span class="chip">bl 5</span><span class="chip">dp 3</span><span class="chip">ix 9</span>
<span class="chip on">ap 6</span><span class="chip">ar 4</span><span class="chip">er 9</span>
<span class="chip">rf 22</span><span class="chip">mh 4</span><span class="chip">rs 3</span>
<span class="chip">wk 8</span>
</div>
</div>
<div class="facet">
<h3>Disposition</h3>
<div class="chips"><span class="chip">Control plane 1</span><span class="chip">Always tool 0</span><span class="chip">Optional tool 5</span></div>
</div>
<div class="facet">
<h3>Role</h3>
<div class="chips"><span class="chip">ic 5</span><span class="chip">reviewer 1</span><span class="chip off">manager 0</span></div>
</div>
<div class="facet">
<h3>Claim</h3>
<div class="chips"><span class="chip mono">approval:read 6</span><span class="chip mono off">routine:admin 0</span></div>
</div>
<div class="facet">
<h3>Parity</h3>
<div class="chips"><span class="chip">Pass 6</span><span class="chip off">Fail 0</span><span class="chip off">Gap 0</span></div>
</div>
<div class="group-h"><span>ap — approvals</span><span>6</span></div>
<ul class="cases" role="listbox" aria-label="Cases">
<li class="case"><span class="id">ap-approval-deny-01</span><span class="dot pass"></span><span class="t">A denied approval leaves the task open…</span></li>
<li class="case"><span class="id">ap-approval-wake-01</span><span class="dot pass"></span><span class="t">Approval wake reviews the approval…</span></li>
<li class="case"><span class="id">ap-board-approval-01</span><span class="dot pass"></span><span class="t">Spend needs a board approval, then waiting…</span></li>
<li class="case"><span class="id">ap-mcp-expiry-01</span><span class="dot pass"></span><span class="t">Expired MCP approval: one fresh re-call…</span></li>
<li class="case sel" aria-selected="true"><span class="id">ap-mcp-gate-01</span><span class="dot pass"></span><span class="t">Pending MCP tool approval means review posture…</span></li>
<li class="case"><span class="id">ap-mcp-pathmissing-01</span><span class="dot pass"></span><span class="t">approval_path_missing means stop and reroute…</span></li>
</ul>
</nav>
<main class="run" style="padding-top:2.4rem;">
<div class="runhead">
<div class="row1">
<h2><span class="mono">ap-mcp-gate-01</span></h2>
<span class="badge grp">ap</span>
<span class="badge optional">Optional tool</span>
<span class="badge">Authorization</span>
<span class="badge">Multi-hop</span>
</div>
<div class="row1" style="font-size:0.8rem; color:var(--muted-fg);">Pending MCP tool approval means review posture — no retry, no done.</div>
<div class="banner pass">✓ Parity: pass — 9 assertions · 9 pass · 0 gaps</div>
<div class="controls">
<div class="seg"><button class="on">Fake agent</button><button disabled>Codex (bounded)</button></div>
<button class="btn primary">Re-run</button>
<span class="relay-note">Codex disabled: provider relay not running — see tutorial §4</span>
</div>
</div>
<div class="transcript wrap" data-run-state="settled">
<span class="ann" style="top:-0.2rem; right:0;">4 · control-plane channel: muted, “no tool exists”</span>
<div class="cpa">
<span class="glyph">⛭</span>
<div>
<p>Control plane routed scoped wake and checked out task <span class="mono">MCK-102</span>.</p>
<p class="why">No agent tool exists for checkout — control-plane owned. <a href="#">Decision record</a></p>
</div>
</div>
<div class="agent-turn">
<p><strong>Agent</strong> — I need the workspace runtime status before restarting the flaky service.</p>
</div>
<details class="toolcall" open>
<summary class="sumline">
<span class="op">get_task_context</span>
<span class="badge always">Always tool</span>
<span class="badge allow">Allow</span>
<span class="outcome">committed · 41ms fixture-time</span>
</summary>
<div class="body">
<dl class="kv">
<dt>Operation</dt><dd>get_task_context · v1</dd>
<dt>Claims</dt><dd>(task-scoped, none required)</dd>
<dt>Idempotency</dt><dd>read-only</dd>
</dl>
<pre class="json">{ "task": "MCK-102", "wake": "scoped", "ancestors": 2, "documents": 1 }</pre>
</div>
</details>
<div class="toolcall denied wrap">
<span class="ann" style="top:-0.55rem; right:0;">5 · typed denial: missing claim, no protected state</span>
<div class="sumline">
<span class="op">control_workspace_service</span>
<span class="badge optional">Optional tool</span>
<span class="badge deny">Deny</span>
<span class="outcome">policy denial — approval gate pending</span>
</div>
<div class="deny-detail">
<strong>Denied:</strong> requires claim <span class="mono">workspace:control</span> — MCP approval
<span class="mono">apr-0117</span> is pending. Typed denial only; no workspace state returned.
</div>
</div>
<div class="agent-turn">
<p><strong>Agent</strong> — Approval is pending, so I will not retry. Parking the task for review with a durable explanation.</p>
</div>
<details class="toolcall" open>
<summary class="sumline">
<span class="op">request_review</span>
<span class="badge always">Always tool</span>
<span class="badge allow">Allow</span>
<span class="outcome">committed · task → in_review</span>
</summary>
<div class="body">
<dl class="kv">
<dt>Operation</dt><dd>request_review · v1</dd>
<dt>Redactions</dt><dd><span class="redact">••• api-key</span> in explanation draft, rule <span class="mono">secret-value</span></dd>
</dl>
<pre class="json">{ "disposition": "in_review", "reason": "mcp_approval_pending", "approval": "apr-0117" }</pre>
</div>
</details>
<div class="sysnote">No further operations — 3 forbidden operations, none invoked · <a href="#">Parity</a></div>
</div>
</main>
<aside class="inspector" aria-label="Inspector" style="padding-top:1.8rem;">
<div class="tabs" role="tablist">
<button>Context</button>
<button class="on">Authorization <span class="tab-chip">1 deny</span></button>
<button>State diff</button>
<button>Parity</button>
</div>
<div class="pane wrap">
<span class="ann" style="top:-0.1rem; right:0.5rem;">6 · authorization records, verbatim from mock core</span>
<table class="auth">
<thead><tr><th>#</th><th>Operation</th><th>Decision</th><th>Reason</th></tr></thead>
<tbody>
<tr><td>1</td><td class="mono">get_task_context</td><td><span class="badge allow">Allow</span></td><td>task-scoped read</td></tr>
<tr class="deny"><td>2</td><td class="mono">control_workspace_service</td><td><span class="badge deny">Deny</span></td><td>claim <span class="mono">workspace:control</span> gated by pending approval <span class="mono">apr-0117</span></td></tr>
<tr><td>3</td><td class="mono">request_review</td><td><span class="badge allow">Allow</span></td><td>always catalog · redaction <span class="mono">secret-value</span> applied</td></tr>
</tbody>
</table>
<div class="authcard">
<strong>Record 2 — full decision</strong>
<dl class="kv">
<dt>Claims considered</dt><dd>workspace:control (absent), task:active (present)</dd>
<dt>Redactions</dt><dd>none</dd>
<dt>State change</dt><dd>none — denial produced no mutation</dd>
<dt>Audit ref</dt><dd>authz-000214</dd>
</dl>
</div>
<div class="authcard">
<strong>Exposure summary</strong>
<dl class="kv">
<dt>Always tools</dt><dd>7 visible</dd>
<dt>Optional tools</dt><dd>2 visible · via approval:read, workspace:read</dd>
<dt>Control plane</dt><dd>9 owned actions — no tool</dd>
</dl>
</div>
</div>
</aside>
</div>
</body>
</html>
@@ -0,0 +1,433 @@
# Phase 7 Interaction Map — Scenario Explorer and Screenshot Acceptance Spec
Status: **approved UX contract for 7F implementation** (PAP-16899, 2026-08-09).
Owner: UXDesigner. Implementer: 7F (browser scenario explorer), integrating
7C mock adapter output and 7E parity output.
Companion records: [Phase 7A capability contract](../phase-07-capability-contract.md),
`spec/phase-07/eval-traceability.yaml`, [Phase 7 plan](/PAP/issues/PAP-16897#document-plan),
[Phase 4b interaction map](phase-4b-interaction-map.md),
[Phase 5 component decisions](phase-5-component-decisions.md).
Mockup: [`mockups/phase-7-scenario-explorer.html`](mockups/phase-7-scenario-explorer.html)
(annotated desktop + mobile renders in `knowledge/evidence/phase-07/`).
This map is the interaction contract for the Phase 7 browser scenario
explorer. The explorer is a **read surface over records the runtime already
produced**. It renders scenario metadata from the generated 7A traceability
manifest, run artifacts from the 7C mock control-plane adapter, and parity
results from the 7E conformance suite. The UI never owns state, policy,
credentials, or execution decisions: it does not compute tool exposure, does
not evaluate claims, does not apply redaction, and holds no Paperclip or
provider credential. Every allow/deny, redaction, and state transition shown
on screen must arrive as a record emitted by the mock core.
## 0. Naming, vocabulary, and canonical labels
- Product name in the UI: **Scenario Explorer** (header: “Phase 7 scenario
explorer — mock control plane”).
- UI copy uses the canonical term **task** (`DESIGN.md` principle 7). Eval
case IDs, fixture IDs, route paths, JSON payloads, and source anchors are
machine values and stay verbatim (`issues`, `i-0102`, `MCK-102`) in
monospace — never “translated.”
- The three dispositions render with exactly these labels, everywhere:
**Control plane** (`control_plane_owned`), **Always tool**
(`always_agent_tool`), **Optional tool** (`optional_agent_tool`). The
traceability panel and all badges must use the 7A contract values as their
source; no UI-local re-classification.
- The five assertion classes render as: **Invariant**
(`control_plane_invariant`), **Tool contract** (`agent_tool_contract`),
**Authorization** (`authorization_policy`), **Multi-hop**
(`combined_multi_hop`), **Restraint** (`restraint_no_call`).
- Parity statuses: **Pass**, **Fail**, **Intentional gap**, **Not run**.
Each has an icon + text label; color alone never encodes status (WCAG
color-independence).
## 1. Shell and information architecture
Package-local Vite entry `examples/phase7-explorer/` with script
`demo:phase7` (default port 4183; 4182 belongs to the Phase 6 demo). It runs
from static assets plus checked-in fixtures with **no Paperclip services and
no network dependency** in fake-agent mode. Reuse the frozen `0.1.2` SDK
surface (`@paperclipai/paperclip-runner/react` + `./styles.css`) through its
five approved extension points; do not fork the token layer.
Desktop layout (≥ 64rem), one React tree (Phase 5 finding — never render two
trees):
- **Left rail (`--pcr-rail-width`, 17rem):** scenario picker — search,
facet filters, case list grouped by eval group.
- **Center column (fluid, max `--pcr-center-width`, 48rem):** run view —
the chronological scenario transcript and the run header (verdict banner,
mode toggle, run/replay controls). Primary surface, strongest hierarchy.
- **Right rail (`--pcr-inspector-width`, 24rem):** inspector with four tabs
— **Context · Authorization · State diff · Parity**. Open by default on
desktop (unlike the 4b protocol inspector: here the inspector carries
first-class acceptance evidence, not diagnostics).
Mobile (< 64rem): single column behind a segmented control
(`Scenarios · Run · Inspect`), same pattern as the Phase 4b console. The
segmented control is sticky at the top; the Run segment is the default when
a case is selected via route. Touch targets ≥ `--pcr-touch-target` (2.75rem).
Density: diagnostic-console dense, like 4b. All spacing, color, type, radius,
motion from `--pcr-*` tokens only (`DESIGN.md` principles 2–4; `pnpm
check:token-gates` applies).
## 2. Scenario picker and filters (left rail)
**Source of truth:** the generated scenario index derived from
`spec/phase-07/eval-traceability.yaml` (all 106 cases, 16 groups) plus the
latest parity results artifact from 7E. The picker consumes these; it never
re-derives group membership or dispositions.
Contents, top to bottom:
1. **Search input** — substring match on case ID and title. Mono rendering
of matched IDs. Debounce ≤ 150ms (Doherty).
2. **Facet filters** (collapsible group, `--pcr-facet-height` baseline):
- **Group** — the 16 eval groups (`hb 5 · co 6 · st 8 · cm 6 · se 4 ·
su 4 · bl 5 · dp 3 · ix 9 · ap 6 · ar 4 · er 9 · rf 22 · mh 4 · rs 3 ·
wk 8`), each with its case count.
- **Case** — direct case-ID picker (combobox) for jump-to-case.
- **Disposition** — Control plane / Always tool / Optional tool, from
`primaryDisposition`.
- **Role** — actor role of the fixture (`ic`, `manager`, `reviewer`,
`board_user`, …) from the scenario index (§9 data contract).
- **Claim** — required grants from `requiredGrants` (e.g.
`approval:read`), rendered mono.
- **Parity status** — Pass / Fail / Intentional gap / Not run, from the
parity artifact.
Facets are AND-combined across facets, OR-combined within a facet. Every
facet value shows its live result count; zero-count values stay visible
but disabled (recognition over recall — the vocabulary stays learnable).
An active-filter chip row with per-chip remove and one **Clear filters**
affordance sits above the list (forgiveness).
3. **Case list** — grouped by eval group with sticky group headers. Each row:
case ID (mono, `--pcr-font-size-xs`), title (one line, truncated with
full text in a tooltip), disposition badge, parity status dot+icon.
Selected row uses `--pcr-accent-surface` with a `--pcr-accent` left rule.
Keyboard: the list is a single-select listbox (`role="listbox"`,
`aria-activedescendant`); Up/Down move, type-ahead jumps, Enter selects and
moves focus to the run header. Facets are disclosure buttons +
checkbox groups. Filter state mirrors into the route (§7) so any filtered
view is linkable; filter state is never stored as protocol state.
Empty results: “No scenarios match these filters.” + Clear filters button —
never a bare void.
## 3. Run view (center column)
### 3.1 Run header
- Case ID (mono) + title, group badge, disposition badge, assertion-class
badges.
- **Verdict banner** (SDK `Banner`): overall parity verdict for the last
completed run — Pass (success surface), Fail (danger surface), Not run
(muted). The banner names the assertion counts (“12 assertions · 12 pass”)
and deep-links the Parity tab.
- **Mode control:** `Fake agent` (default) / `Codex (bounded)` segmented
control. Fake mode is always available offline. Codex mode appears only
when the local relay is reachable; otherwise the option renders disabled
with the reason (“provider relay not running — see tutorial §4”). The
browser never holds a provider credential; Codex traffic goes through the
same server-side relay boundary proven in Phase 5.
- **Run / Re-run** button and replay controls (SDK `replay-controls`).
Deterministic fixtures mean re-run in fake mode reproduces the identical
timeline; state diffs and parity always describe the displayed run.
### 3.2 Scenario transcript
**Source of truth:** the ordered run artifact timeline emitted by the mock
core (§9). Items render in artifact order; the UI never reorders, merges, or
re-times entries.
The transcript interleaves two visually distinct channels (Similarity +
Common Region — a stranger must tell them apart in two seconds):
1. **Agent channel** — model turns and semantic tool calls/results. Aligned
with the standard conversation layout (SDK `conversation`/`message`/
`tool-item`). A semantic tool call renders collapsed by default with a
one-line summary: operation ID (mono) + disposition badge + one-phrase
outcome (“`finish_task` · Always tool · committed”). Expansion is the
item-body / request-detail extension point (§8) and shows: operation ID
and version, argument JSON, result JSON, idempotency behavior, claims the
call required, and redaction chips (§5) — exactly the descriptor fields
from the 7D catalog, never re-labeled.
2. **Control-plane channel** — actions the runtime performed with **no
tool** (checkout, wake routing, budget stops, reconciliation, blocker
wake scheduling…). These render full-width on a `--pcr-muted` surface
with a “Control plane” badge and a gutter glyph distinct from tool items,
and are indented from the agent lane. Copy states the actor plainly:
“Control plane checked out task MCK-102 — no agent tool exists for this.”
Each entry expands to the decision record (inputs considered, resulting
state change, audit reference).
Failed/denied entries render in place on `--pcr-danger-surface` with the
exact typed denial (§5) — failures are part of the record, never toast-only.
Restraint/no-call scenarios (`rs-*`, `er-*` restraint cases) must remain
legible when the correct behavior is *absence*: after the final agent turn
the transcript appends a system note “No further operations — N forbidden
operations, none invoked” which deep-links the Parity tab’s forbidden list.
Restraint evidence must never depend on an empty screen.
PRP events stay behind a per-item “Protocol events” disclosure (same nested
pattern as Phase 5 `debugEvents`) — the semantic view is primary; raw
protocol stays inspectable but secondary (progressive disclosure).
Auto-follow, jump-to-latest, streaming affordances, and reduced-motion rules
are inherited verbatim from the Phase 4b map §1 — do not redesign them.
## 4. Inspector tabs (right rail)
All four tabs are SDK `Tabs`; the active tab mirrors into the route (§7).
### 4.1 Context
Answers “who ran, under what authority, with which tools” before any
transcript reading (mental model first):
- **Actor block:** actor role, task mode, fixture ID (mono), wake payload
summary (reason + trigger), budget posture.
- **Capability grants:** the scenario’s `requiredGrants` plus every grant
the fixture actually issued, rendered as mono chips.
- **Tool exposure list** — the acceptance-critical view. Three labeled
sections, populated **only** from the mock core’s exposure record:
1. **Always tools (N)** — always-catalog operations visible this run;
2. **Optional tools (N)** — visible optional operations, each with the
grant that unlocked it (“`decide_approval` — via `approval:decide`”);
3. **Control plane — no tool (N)** — control-plane-owned capabilities
relevant to this scenario, explicitly listed so reviewers can verify
absence (negative evidence must be visible, not implied).
- **Traceability panel:** source anchor (file:line, mono), skill elements
(file · section · lines), evidence IDs, legacy MCP mapping row if any —
verbatim from the 7A contract.
### 4.2 Authorization
Chronological table of every authorization record the mock core emitted:
operation (mono), claims considered, decision (**Allow** / **Deny** with
icon + text), reason string, redactions applied, resulting state change
reference. Row expansion shows the full record JSON. Denied rows use
`--pcr-danger-surface`; a count chip on the tab surfaces denials without
opening it (“Authorization · 1 deny”). Selecting a row scrolls/flashes the
corresponding transcript entry (uniform connectedness across panels).
### 4.3 State diff
Before/after immutable snapshot diff for the displayed run, grouped by the
ten entity domains in the plan: **tasks, comments, documents, interactions,
approvals, artifacts, blockers, workspace, budget, run**. Each domain is a
collapsible section headed by a change summary (“tasks · 1 changed”,
“comments · 2 added”); unchanged domains collapse to a single muted row
(“unchanged”) so change carries the visual weight (Von Restorff). Rows show
entity ID (mono) + field-level changes as `field: before → after`, with
added/removed/changed markers as icon + label, not color alone. The diff is
computed by the runtime/test layer and delivered as data; the UI performs no
diffing of its own. A “final state” toggle shows the complete after-snapshot
JSON per domain behind a disclosure.
### 4.4 Parity
The eval acceptance surface:
- **Verdict header** repeating the banner verdict.
- **Assertion list:** one row per conformance assertion — assertion class
badge, human-readable expectation, Pass/Fail, and on fail an expected vs
observed block (mono, side-by-side at desktop, stacked at mobile).
- **Forbidden operations:** each `forbiddenSemantics` entry with “never
invoked ✓” or the violating transcript reference.
- **Intentional gaps:** entries marked as intentional exclusions render
under their own heading with the recorded rationale — visually distinct
from Fail (a gap is a decision, not a defect).
- **Legacy baseline note:** the case’s skill-present/skill-absent reference
note, verbatim, muted — labeled “reference baseline, not the pass
condition.”
## 5. Credentials, redaction, and denial rendering
- Fake mode performs zero network I/O; Codex mode talks only to the local
relay; no route, storage key, or bundle string may contain a Paperclip or
provider credential. `localStorage` may hold UI preferences only
(inspector tab, filter collapse state) — never run artifacts, grants, or
anything from a fixture.
- Redacted values arrive pre-redacted from the mock core. The UI renders a
redaction chip: `•••` + “redacted” label + the redaction-rule name in a
tooltip. The UI must not receive-and-hide secrets; if a raw secret ever
reaches the browser payload, that is a 7C/7D defect, and the explorer
additionally fails closed by rendering nothing for fields flagged
redacted.
- A **denied** tool call renders the typed policy denial: operation, the
missing claim, and the denial reason — and never any protected state. An
**absent** tool simply does not appear in exposure lists; the Context tab’s
control-plane section is the affordance proving absence is intentional.
- The generic escape hatch (`paperclipApiRequest` successor), when a test
scenario grants it, renders with a persistent `--pcr-warning-surface`
“test-only escape hatch” banner on every call card (plan §4: visually
flagged).
## 6. Accessibility, responsive, empty/error states
Accessibility (WCAG POUR; inherits all Phase 4b/5 baseline rules):
- Landmarks: `nav` (picker), `main` (run view), `complementary`
(inspector); one `h1`; group headers are `h2`/`h3` in order.
- Full keyboard operation: picker listbox (§2); `F6`/documented shortcut
cycles the three regions; tabs follow the WAI-ARIA tabs pattern
(arrow-key activation); every disclosure is a real `button`. Visible
focus ring via `--pcr-ring` everywhere.
- Live regions: run progress announces via `aria-live="polite"`
(“Scenario run settled — verdict pass”); denial entries announce
assertively once.
- Reduced motion: with `prefers-reduced-motion`, streaming reveal, banner
transitions, and scroll-flash connect cues render instantly/statically
(Phase 5 rule).
- Contrast from the existing token pairs only; parity/authorization
statuses always icon + text.
Responsive: content-driven single breakpoint at 64rem (§1). At mobile the
inspector tabs become full-width stacked sections inside the Inspect
segment; expected-vs-observed blocks stack; the transcript keeps every
descendant intrinsically shrinkable (`min-w-0` discipline) so no horizontal
scrollbar appears at 390px (Phase 5 finding).
States that must be designed, not defaulted:
| State | Surface | Rendering |
| --- | --- | --- |
| No scenario selected | Center + inspector | Explorer intro card: corpus stats (106 cases · 16 groups), “Pick a scenario” pointer at the rail, three example deep links. No dead panels. |
| Scenario selected, not run | Run view | Header + context render from the index; transcript area shows a primed empty state (“Run to produce the deterministic timeline”) with the Run button duplicated inline (Fitts). Diff/Parity tabs show “No run yet.” |
| Run in progress | All | Progress in the header (indeterminate, reduced-motion safe); transcript streams; tabs show live counts; controls disable with reasons. |
| Run failed (harness error) | Run view | Danger banner with the exact diagnostic + “Re-run”; partial timeline stays visible and labeled partial. Never silently reset. |
| Parity fail | Banner/Parity | Fail is a first-class rendered state (danger banner + failing assertion rows) — the explorer’s job is showing it, not avoiding it. |
| Denied/absent tool | Transcript/Context | Per §5. |
| Missing/invalid fixture or artifact | Shell | Error card naming the artifact path and the regenerate command (`pnpm generate:phase7-inventory` / 7E command). No blank screen. |
| Codex relay unavailable | Mode control | Disabled option + reason; fake mode remains fully usable. |
## 7. Deterministic screenshot routes
Hash routing (static hosting, no server rewrites):
```
#/ explorer home (picker + intro)
#/case/<case-id> scenario selected, not run
#/case/<case-id>?run=fake deterministic fake run, auto-executed
#/case/<case-id>?run=fake&view=<transcript|context|authorization|diff|parity>
#/?group=<g>&disposition=<d>&role=<r>&claim=<c>&parity=<p>&q=<text> filtered picker
```
Determinism contract for screenshot/CI use:
- `run=fake` auto-runs the scenario to completion on load; when the run and
all panels are settled the shell sets `data-run-state="settled"`
(Playwright wait target). Until then it is `pending`.
- Fake runs render **fixture time only** — no wall-clock timestamps,
durations, or locale-dependent formatting anywhere in fake mode; ordering
is by artifact sequence. Two loads of the same route are pixel-identical
at the same viewport (modulo font rasterization).
- `view=` selects the inspector tab (desktop) or segment+tab (mobile);
`view=transcript` closes nothing — it just guarantees focus/scroll starts
at the transcript top.
- Stable `data-testid`s (minimum set): `scenario-picker`, `facet-<name>`,
`case-row-<id>`, `run-header`, `verdict-banner`, `mode-toggle`,
`transcript`, `transcript-item-<seq>`, `cp-action-<seq>`,
`inspector-tab-<name>`, `exposure-always`, `exposure-optional`,
`exposure-control-plane`, `authorization-row-<seq>`, `diff-domain-<name>`,
`parity-assertion-<n>`, `redaction-chip`.
## 8. Package-local implementation guidance (7F)
- **Reuse first.** Shell, transcript, and inspector compose the frozen SDK:
`RunnerConsoleApp` is *not* the shell (this is not a chat console);
compose `conversation`, `message`, `tool-item`, `tabs`, `card`, `badge`,
`banner`, `menu`, `dialog`, `tooltip`, `replay-controls` from
`@paperclipai/paperclip-runner/react`. Semantic call detail uses the
item-body renderer (ext point 1); control-plane detail uses the
request-detail renderer contract (ext point 2); theming via scoped
`--pcr-*` overrides (ext point 4). Do not add SDK exports or break the
`0.1.2` freeze — explorer-only components live in the explorer entry.
- **New explorer-local components** (system proposals, one job each):
`ScenarioPicker` (search + facets + listbox), `ExposureList`,
`AuthorizationTable`, `StateDiff` (domain sections + field rows),
`ParityPanel`, `RedactionChip`, `ChannelEntry` (control-plane transcript
entry). Each uses tokens only; any genuinely missing token (e.g. a
`--pcr-diff-*` pair if muted/success/danger surfaces prove insufficient)
is proposed in the 7F PR as a token addition, never inlined.
- **Data contract consumed (owned by 7C/7E, not the UI):**
- *Scenario index* (generated from `eval-traceability.yaml`): `id`,
`title`, `group`, `fixture`, `actorRole`, `taskMode`,
`primaryDisposition`, `requiredGrants[]`, `assertionClasses[]`,
`sourceAnchor`, `skillElements[]`, `evidenceIds[]`,
`forbiddenSemantics[]`. If `actorRole`/`taskMode` are not yet in the
manifest, 7C adds them to the index generator — the UI must not infer
them from prose.
- *Run artifact* (per scenario × mode): ordered `timeline[]` entries
discriminated as `semantic_call` / `semantic_result` /
`control_plane_action` / `protocol_event_ref`; `exposure` (always /
optional-with-grant / control-plane lists); `authorizationRecords[]`
(operation, claims considered, decision, reason, redactions, state-change
ref); `snapshots.before` / `snapshots.after` per entity domain;
`parity` (assertions with class/expected/observed/status, intentional
gaps with rationale, legacy baseline note, verdict).
- **Authority rule (binding):** every rendered fact traces to one of those
records. Grep-able heuristics, UI-side claim math, or re-deriving
dispositions from case IDs are defects. If a view needs a fact the records
lack, extend the record upstream.
- **`DESIGN.md` alignment:** tokens-only values; one component per job;
hierarchy by structure not decoration (domain sections and channels use
surface + position, minimal borders); status vocabulary systematic (§0
labels everywhere); machine values (IDs, anchors, grants, JSON) in
`--pcr-font-mono`; canonical copy “task.”
- **Testing hooks 7F must ship:** component tests for the four inspector
tabs and both transcript channels; keyboard-path tests (listbox, tabs,
region cycle); reduced-motion test; the §7 route determinism test
(two loads → identical settled DOM); accessibility assertions (landmarks,
names, contrast tokens); screenshot capture script for §9.
## 9. Screenshot acceptance matrix
Evidence lands in `knowledge/evidence/phase-07/` as
`<slug>-<viewport>.png`, at both **1440×900** and **390×844**, captured from
the §7 routes after `data-run-state="settled"`. These twelve shots are the
UX acceptance set for 7F (they also cover the plan’s human-checkpoint
scenario families: heartbeat, planning, blockers, approvals, artifacts,
interactions, manager, multi-hop, restraint).
| # | Slug | Route | Expected visible evidence |
| --- | --- | --- | --- |
| 1 | `explorer-home` | `#/` | Picker with all 16 group facets and counts summing to 106; intro card with corpus stats; no dead panels. |
| 2 | `picker-filtered` | `#/?group=ix&disposition=always_agent_tool&parity=pass` | Active filter chips; filtered count visible; zero-count facet values disabled but legible; Clear filters affordance. |
| 3 | `hb-heartbeat` | `#/case/hb-scoped-wake-01?run=fake&view=transcript` | Control-plane channel entries for scoped-wake routing and checkout (“no agent tool exists for this”), then `get_task_context` agent call; pass banner. |
| 4 | `wk-planning` | `#/case/wk-plan-directive-01?run=fake&view=transcript` | `write_document` call with revision fields, revision-bound `request_human_input` (confirmation), review-parked disposition; no re-plan loop. |
| 5 | `bl-blockers-diff` | `#/case/bl-create-blocked-01?run=fake&view=diff` | State diff: tasks domain shows the new task created blocked; blockers domain shows first-class `blockedByIssueIds`; unchanged domains collapsed. |
| 6 | `ap-approval-denied` | `#/case/ap-mcp-gate-01?run=fake&view=authorization` | Authorization table with a Deny row (missing claim + reason, danger surface, no protected state); “Authorization · 1 deny” tab chip; transcript shows the typed denial card. |
| 7 | `ar-artifacts` | `#/case/ar-upload-before-done-01?run=fake&view=transcript` | `register_deliverable` completing **before** `finish_task` in artifact order; deliverable metadata in the expanded call card. |
| 8 | `ix-interaction` | `#/case/ix-checkbox-01?run=fake&view=transcript` | `request_human_input(kind=checkbox)` call card with options payload; control-plane wake/continuation entry; result inspection entry. |
| 9 | `rf-manager` | `#/case/rf-api-mgr-heartbeat-01?run=fake&view=context` | Context tab: manager role; optional discovery tools listed **with their unlocking grants**; control-plane “no tool” list populated; traceability panel showing source anchor + skill elements. |
| 10 | `mh-multihop` | `#/case/mh-subtask-tree-01?run=fake&view=parity` | Multi-hop assertion rows (child creation, chained blockers) all Pass; forbidden operations section showing “never invoked ✓”. |
| 11 | `rs-restraint` | `#/case/rs-question-only-01?run=fake&view=parity` | Restraint verdict: answer-only transcript with the “No further operations” note; parity forbidden list showing zero state writes; Pass banner. |
| 12 | `rs-secret-redaction` | `#/case/rs-secret-hygiene-01?run=fake&view=transcript` | Redaction chips (`•••` + rule name) in call/result cards; comment content free of the key; parity Pass. |
Mobile captures use the same routes; the segmented control must show the
correct active segment (`Run` for transcript shots, `Inspect` for tab shots)
and no horizontal scrollbar may be present.
Acceptance procedure: UXDesigner reviews all 24 images against this matrix
before 7F is accepted (plan §6: “UXDesigner supplies the interaction map and
reviews screenshots before implementation acceptance”). Any deviation is
either fixed or written into this document as a revision — silent divergence
fails the gate.
## 10. Out of scope / rejected
- No second app shell, no Tailwind/Radix, no dark mode, no markdown
renderer, no command palette, no transcript virtualization — Phase 5
rejections stand.
- No editing affordances of any kind: fixtures, grants, and policies are
changed in code/YAML, not in the browser. The mode toggle and run/replay
controls are the only mutating-looking controls, and they mutate only the
in-browser demo run.
- No parity “re-judging” in the UI: the Parity tab renders 7E output; it
never recomputes assertions.
- No live-Paperclip or ACPX affordances; the future binding boundary stays a
docs note, not a UI surface.
Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

@@ -0,0 +1,16 @@
import { chromium } from "@playwright/test";
const exe = "/srv/paperclip/home/paperclipai/paperclip/.paperclip/browser-runtime/chromium-arm64/bin/chromium-agent-browser";
const url = "file:///srv/paperclip/home/paperclipai/paperclip/.paperclip/worktrees/PAP-16679-paperclip-runner/packages/paperclip-runner/docs/design/mockups/phase-7-scenario-explorer.html";
const out = "knowledge/evidence/phase-07";
const browser = await chromium.launch({ executablePath: exe });
for (const [w, h, tag] of [[1440, 900, "1440x900"], [390, 844, "390x844"]]) {
const page = await browser.newPage({ viewport: { width: w, height: h } });
await page.goto(url);
await page.waitForTimeout(400);
await page.screenshot({ path: `${out}/mockup-explorer-ap-mcp-gate-${tag}.png` });
await page.close();
}
await browser.close();
console.log("done");