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
82 changes: 82 additions & 0 deletions .agent/context/20260913T085602Z-recovery-proof-palette.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Recovery proof palette and Graph backfill — active context

## Goal

Make Graph candidate discovery appear for historical committed settlements and
bring Payment proof / Recovery control into the main OneShot palette without
removing evidence or weakening settlement safety.

## Acceptance criteria

- Historical `COMMITTED` settlements missing Graph evidence receive one durable,
idempotent `capture_graph_evidence` outbox job.
- New settlement behavior and Graph authority remain unchanged.
- Missing Graph copy distinguishes an observation that is still pending from
proof that no payment happened.
- Payment proof and recovery use the shared surface, ink, line, radius, and font
tokens used by the authenticated workspace.
- Existing responsive and accessibility checks remain green.

## Constraints

- Graph remains non-authoritative candidate discovery; Arc and OneShot remain
authoritative.
- No live payment, deployment, or external configuration change.
- The dirty `fix/x402-wallet-proof-recovery` worktree is unrelated user work and
must remain untouched.

## Branch state

- Branch: `fix/recovery-proof-palette`
- Base: `origin/develop` at `72c4bd7` (rebased by fast-forward from `964d475`
after PR #130 landed; no file overlap with this change)
- Worktree: `recovery-proof-palette`

## Plan

1. Add the smallest append-only migration that backfills missing Graph capture
jobs from durable settlements.
2. Reuse the shared brand tokens in both read-only UI slices and clarify missing
Graph evidence copy.
3. Run focused tests, visual browser checks, the implementation loop, and review
gates required for the final staged tree.

## Review gates

- Gate A: pending
- Gate B: pending

## Base update

- While the work was in progress, `origin/develop` advanced from `964d475` to
`72c4bd7` (merge of PR #130, MCP generated request keys). The branch had no
commits yet, so it fast-forwarded; the diff has zero file overlap with this
change and local checks were re-run on the updated candidate tree.

## Implementation

- Added migration `013_backfill_graph_evidence.sql`; it enqueues one Graph
capture job only when a durable settlement has neither Graph evidence nor an
existing capture job.
- Missing Graph evidence now reads `CAPTURE PENDING` for committed settlements
and `NOT YET REPORTED` for non-terminal recovery states.
- Recovery uses the shared Rubik/mono typography, brand radii, pill actions,
lime authoritative-state summary, and a light/dark-aware Graph evidence card.
- Payment proof uses the same lime request summary and shared typography/radii;
long policy values wrap instead of colliding with adjacent facts.

## Validation

- `pnpm test` — PASS, 80 files / 1,056 tests.
- `pnpm lint` — PASS.
- `pnpm typecheck` — PASS.
- `pnpm format:check` — PASS.
- `pnpm check:generated` — PASS.
- `git diff --check` — PASS.
- Recovery and settlement standalone pages rendered and inspected at desktop
width, including the missing-Graph state.
- PostgreSQL integration test added for the backfill and migration idempotency,
but not executed locally because Docker/PostgreSQL is unavailable.
- Existing composed-browser test could not unlock the latest Privy-first app
because its `Machine token (advanced)` locator is stale; this reproduces on
the unmodified `origin/develop` UI and is not caused by this change.
21 changes: 16 additions & 5 deletions packages/recovery-ui/src/RecoveryTimeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,20 +109,31 @@ function graphSourceLabel(graph: GraphObservationSummary): string {
: 'The Graph provider';
}

function GraphPanel({ graph }: { readonly graph: GraphObservationSummary | null }) {
function GraphPanel({
graph,
state,
}: {
readonly graph: GraphObservationSummary | null;
readonly state: RecoveryTimelinePage['authoritativeState'];
}) {
if (graph === null) {
const committed = state === 'COMMITTED';
return (
<section className="panel graph-panel graph-unavailable" aria-labelledby="graph-heading">
<div className="section-heading">
<div>
<p className="eyebrow">Candidate discovery</p>
<h2 id="graph-heading">The Graph</h2>
</div>
<span className="health health-unavailable">NOT REPORTED</span>
<span className="health health-unavailable">
{committed ? 'CAPTURE PENDING' : 'NOT YET REPORTED'}
</span>
</div>
<p className="observation-copy">
No Graph observation was returned for this request. This is not proof that no payment
happened; Arc and OneShot evidence remain the authority.
{committed
? 'No Graph observation is recorded for this confirmed settlement yet. Historical settlements are queued for capture; refresh after the evidence worker completes.'
: 'No Graph observation is recorded for this request yet. This is not proof that no payment happened.'}{' '}
Arc and OneShot evidence remain authoritative.
</p>
</section>
);
Expand Down Expand Up @@ -288,7 +299,7 @@ export function RecoveryTimeline({
</div>
)}

<GraphPanel graph={current.graph} />
<GraphPanel graph={current.graph} state={current.authoritativeState} />

<div className="action-row" aria-label="Safe recovery actions">
<button
Expand Down
59 changes: 41 additions & 18 deletions packages/recovery-ui/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@
:scope {
color: var(--ink);
background: var(--os-panel);
font-family: var(--os-font-secondary);
font-family: var(--os-font-primary);
font-weight: 300;
font-synthesis: none;
color-scheme: dark;
--ink: var(--os-panel-ink);
--muted: var(--os-panel-ink-muted);
--line: var(--os-panel-line);
Expand Down Expand Up @@ -122,7 +121,7 @@
.binding-warning,
.duplicate-note,
.order-note {
font-family: 'Cascadia Mono', 'SFMono-Regular', Consolas, monospace;
font-family: var(--os-font-mono);
text-transform: uppercase;
letter-spacing: 0.08em;
}
Expand Down Expand Up @@ -191,7 +190,7 @@

dd {
margin: 4px 0 0;
font-family: 'Cascadia Mono', 'SFMono-Regular', Consolas, monospace;
font-family: var(--os-font-mono);
font-size: 0.79rem;
}

Expand All @@ -202,6 +201,9 @@
gap: 28px;
padding: clamp(24px, 5vw, 42px);
border: 1px solid var(--line);
border-radius: var(--os-radius-lg);
background: var(--os-field);
color: var(--os-on-field);
overflow: hidden;
}

Expand All @@ -222,7 +224,7 @@
.state-banner p {
max-width: 680px;
margin-bottom: 0;
color: var(--muted);
color: var(--os-on-field-muted);
line-height: 1.6;
}

Expand All @@ -239,14 +241,16 @@
align-content: center;
min-width: 230px;
padding: 20px;
border: 1px solid var(--line);
background: var(--panel);
border: 1px solid var(--os-field-line);
border-radius: var(--os-radius);
background: transparent;
color: var(--os-on-field);
}

.lock-status span,
.lock-status small {
color: var(--amber);
font-family: 'Cascadia Mono', 'SFMono-Regular', Consolas, monospace;
color: var(--os-on-field-muted);
font-family: var(--os-font-mono);
font-size: 0.68rem;
letter-spacing: 0.08em;
}
Expand Down Expand Up @@ -277,7 +281,7 @@
min-height: 44px;
padding: 11px 18px;
border: 1px solid var(--cyan);
border-radius: 2px;
border-radius: 999px;
cursor: pointer;
font-weight: 700;
}
Expand Down Expand Up @@ -328,6 +332,7 @@
.diagnostics {
padding: clamp(22px, 4vw, 32px);
border: 1px solid var(--line);
border-radius: var(--os-radius);
background: var(--panel);
}

Expand Down Expand Up @@ -486,7 +491,7 @@
.decision-grid small {
display: block;
color: var(--muted);
font-family: 'Cascadia Mono', 'SFMono-Regular', Consolas, monospace;
font-family: var(--os-font-mono);
font-size: 0.66rem;
text-transform: uppercase;
}
Expand Down Expand Up @@ -518,6 +523,24 @@
margin-top: 20px;
}

.graph-panel {
--ink: var(--os-ink);
--muted: var(--os-ink-muted);
--line: var(--os-line);
--panel: var(--os-surface);
--panel-strong: var(--os-ground);
--cyan: var(--os-accent-ink);
--green: var(--os-accent-ink);
color: var(--ink);
background: var(--os-surface);
}

.graph-panel .health,
.graph-panel .binding-ok,
.graph-panel .binding-warning {
background: var(--os-panel);
}

.health-fresh,
.binding-ok {
border-color: var(--line);
Expand Down Expand Up @@ -570,7 +593,7 @@
padding: 7px 10px;
border: 1px solid var(--line);
color: var(--amber);
font-family: 'Cascadia Mono', 'SFMono-Regular', Consolas, monospace;
font-family: var(--os-font-mono);
font-size: 0.7rem;
}

Expand Down Expand Up @@ -608,7 +631,7 @@
place-items: center;
border: 1px solid var(--cyan);
color: var(--cyan);
font-family: 'Cascadia Mono', 'SFMono-Regular', Consolas, monospace;
font-family: var(--os-font-mono);
}

.card-heading > div {
Expand Down Expand Up @@ -740,13 +763,13 @@
padding: 14px 16px;
border: 1px solid var(--line);
border-left: 2px solid var(--cyan);
border-radius: 12px;
border-radius: var(--os-radius);
}

.state-banner {
gap: 18px;
padding: 20px 24px;
border-radius: 12px;
border-radius: var(--os-radius-lg);
}

.state-banner h2 {
Expand All @@ -757,13 +780,13 @@
.lock-status {
min-width: 210px;
padding: 14px 16px;
border-radius: 10px;
border-radius: var(--os-radius);
}

.graph-panel {
margin-top: 16px;
padding: 18px 22px;
border-radius: 12px;
border-radius: var(--os-radius);
}

.graph-panel .identity-grid {
Expand All @@ -777,7 +800,7 @@
.panel,
.diagnostics {
padding: 20px 22px;
border-radius: 12px;
border-radius: var(--os-radius);
}

.dashboard-grid {
Expand Down
16 changes: 15 additions & 1 deletion packages/recovery-ui/test/component.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,21 @@ describe('RecoveryTimeline', () => {
renderScenario('fallback-disabled');
expect(screen.queryByRole('heading', { name: 'Subgraph MCP' })).toBeNull();
expect(screen.getByRole('heading', { name: 'The Graph' })).toBeTruthy();
expect(screen.getByText('NOT REPORTED')).toBeTruthy();
expect(screen.getByText('NOT YET REPORTED')).toBeTruthy();
});

it('explains when Graph capture is pending for a historical settlement', () => {
const page = recoveryScenarioPages['fallback-disabled'][0]!;
render(
createElement(RecoveryTimeline, {
pages: [{ ...page, authoritativeState: 'COMMITTED' }],
onRefresh: vi.fn(async () => receipt),
onEscalate: null,
onLoadMore: null,
}),
);
expect(screen.getByText('CAPTURE PENDING')).toBeTruthy();
expect(screen.getByText(/Historical settlements are queued for capture/u)).toBeTruthy();
});

it('offers no retry, force-pay, or settlement action', () => {
Expand Down
Loading
Loading