---
phase: 18-gh-issues-write-back-notes-and-card-cloud-storage
verified: 2026-03-27T00:00:00Z
status: passed
score: 10/10 must-haves verified
re_verification: false
---

# Phase 18: GH Issues Write-Back — Notes and Card Cloud Storage Verification Report

**Phase Goal:** Wire ww-card GH Issue creation/update for every recipient card, push notes as GH Issue comments, migrate PendingEditQueue from JSON file to SQLite, and deliver a hybrid PendingEditFlusher (immediate attempt + timer retry).
**Verified:** 2026-03-27
**Status:** passed
**Re-verification:** No — initial verification

---

## Goal Achievement

### Observable Truths

| # | Truth | Status | Evidence |
|---|-------|--------|----------|
| 1 | V006 migration adds github_issue_number to cards, synced_at to notes, retry_count and last_attempted_at to pending_edits | VERIFIED | `V006__card_issue_and_note_sync.sql` has all 4 ALTER TABLE statements |
| 2 | CardIssueBody struct serializes/deserializes the ww-card JSON body schema | VERIFIED | `issues_client.rs:100` `pub struct CardIssueBody`; `parse_card_body` / `format_card_body` exist at lines 505, 511 |
| 3 | GhIssuesClient can create issue comments and edit issue titles | VERIFIED | `create_issue_comment` at line 383, `edit_issue_title` at line 407, `edit_issue_body` at line 353 |
| 4 | SqliteStore has methods for card issue numbers, unsynced notes, note sync marking, and pending edit CRUD | VERIFIED | All 7 methods found: `set_card_issue_number` (562), `read_unsynced_notes` (576), `mark_note_synced` (596), `insert_pending_edit` (609), `read_pending_edits` (635), `delete_pending_edit` (659), `increment_pending_edit_retry` (669) |
| 5 | Cards without a github_issue_number get a ww-card GH Issue created during sync | VERIFIED | `sync_card_issues` (live_client.rs:1092) skips cards with `github_issue_number.is_some()`, calls `create_issue(&title, &body, "ww-card")` |
| 6 | Unassigned cards are skipped during issue creation | VERIFIED | `if card.is_unassigned { continue; }` at live_client.rs:1109 |
| 7 | Existing notes are backfilled as GH Issue comments when the card issue is created | VERIFIED | `backfill_card_notes` called after `set_card_issue_number` succeeds (live_client.rs:1145); uses `read_unsynced_notes` + `create_issue_comment` + `mark_note_synced` |
| 8 | Saving a note immediately attempts to post a GH Issue comment, falling back to pending_edits on failure | VERIFIED | `save_note` (live_client.rs:342) spawns background thread, calls `create_issue_comment`; on failure calls `insert_pending_edit(..., "SaveNote", ...)` at lines 392 and 401 |
| 9 | PendingEditFlusher reads pending_edits from SQLite and flushes them to GH Issues with exponential backoff | VERIFIED | `pending_edit_flusher.rs` has `run_flush_pass` (reads `read_pending_edits`), `flush_one` (handles SaveNote/UpdateCardBody/UpdateCardTitle), `backoff_secs` (60/120/240/480/900 sequence) |
| 10 | Old JSON-file PendingEditQueue is removed; flusher runs on 60s background timer | VERIFIED | `edit_queue.rs` is hollowed out (deprecation comment only); no `PendingEditQueue` references in workspace; `start_flusher` spawns named thread with 60s sleep interval; legacy `pending_edits.json` deleted on startup |

**Score:** 10/10 truths verified

---

## Required Artifacts

| Artifact | Expected | Status | Details |
|----------|----------|--------|---------|
| `crates/service/src/db/migrations/V006__card_issue_and_note_sync.sql` | Schema migration for card cloud storage | VERIFIED | 4 ALTER TABLE statements; adds `github_issue_number INTEGER`, `synced_at TEXT`, `retry_count INTEGER NOT NULL DEFAULT 0`, `last_attempted_at TEXT` |
| `crates/integrations/src/github/issues_client.rs` | CardIssueBody, CardProductRef, format helpers, create_issue_comment, edit_issue_title | VERIFIED | All 8 required exports found at expected line numbers |
| `crates/service/src/db/sqlite.rs` | SqliteStore extensions + PendingEditRow | VERIFIED | `PendingEditRow` struct at line 101; `github_issue_number: Option<i64>` on CardRow (lines 72, 83, 95); all 7 new methods present |
| `crates/app/src/live_client.rs` | sync_card_issues(), backfill_card_notes(), detect_card_changes(), Arc<GhIssuesClient>, save_note extension | VERIFIED | All 4 functions present; field is `Option<Arc<integrations::github::issues_client::GhIssuesClient>>` at line 37 |
| `crates/app/src/dashboard/pending_edit_flusher.rs` | PendingEditFlusher with backoff, flush_one, run_flush_pass, start_flusher | VERIFIED | All 4 functions present; handles all 3 edit types; 4 unit tests covering backoff math |
| `crates/app/src/dashboard/mod.rs` | Module declaration for pending_edit_flusher | VERIFIED | `pub mod pending_edit_flusher;` at line 6 |

---

## Key Link Verification

| From | To | Via | Status | Details |
|------|----|----|--------|---------|
| `V006__card_issue_and_note_sync.sql` | `sqlite.rs` | refinery migration runner | VERIFIED | `github_issue_number.*INTEGER` pattern matches line 5 of migration |
| `live_client.rs` | `issues_client.rs` | `GhIssuesClient.create_issue()`, `create_issue_comment()`, `format_card_body()`, `format_card_title()` | VERIFIED | `create_issue(&title, &body, "ww-card")` at line 1138; all format helpers imported and used |
| `live_client.rs` | `sqlite.rs` | `set_card_issue_number()`, `read_unsynced_notes()`, `mark_note_synced()`, `insert_pending_edit()` | VERIFIED | All 4 called in sync_card_issues / backfill_card_notes / save_note |
| `pending_edit_flusher.rs` | `sqlite.rs` | `read_pending_edits`, `delete_pending_edit`, `increment_pending_edit_retry` | VERIFIED | `read_pending_edits` at flusher line 125; `delete_pending_edit` at line 140; `increment_pending_edit_retry` at line 158 |
| `pending_edit_flusher.rs` | `issues_client.rs` | `create_issue_comment`, `edit_issue_body`, `edit_issue_title` | VERIFIED | All 3 called in `flush_one` at lines 90, 103, 116 |
| `live_client.rs` | `pending_edit_flusher.rs` | `start_flusher` called from `LiveClient::new` | VERIFIED | `crate::dashboard::pending_edit_flusher::start_flusher(flusher_store, flusher_gh)` at live_client.rs:212 |

---

## Data-Flow Trace (Level 4)

Not applicable. Phase 18 delivers write-back infrastructure (outbound GH API calls), not data-rendering components. The artifacts are background services and sync functions, not UI components that render dynamic data from a state source. No Level 4 data-flow trace required.

---

## Behavioral Spot-Checks

Step 7b: SKIPPED. The deliverables are background service code (GH API calls, SQLite mutations) that require a running app with configured GH credentials to execute. No entry points testable via static invocation in under 10 seconds.

---

## Requirements Coverage

| Requirement | Source Plan(s) | Description | Status | Evidence |
|-------------|---------------|-------------|--------|----------|
| CLOUD-01 | 18-01, 18-02 | System can create GH Issues tagged `ww-card` for recipient card records | SATISFIED | `sync_card_issues` creates issues with `"ww-card"` label; `set_card_issue_number` persists the returned number |
| CLOUD-02 | 18-01, 18-02, 18-03 | System can update `ww-card` GH Issues when card data changes | SATISFIED | `detect_card_changes` queues `UpdateCardBody`/`UpdateCardTitle` pending edits; `PendingEditFlusher` flushes them via `edit_issue_body`/`edit_issue_title` |
| CLOUD-05 | 18-01, 18-02 | Notes are stored as GH Issue comments on `ww-card` issues and synced as `Vec<NoteEntry>` | SATISFIED | `save_note` posts immediately via `create_issue_comment`; `backfill_card_notes` handles pre-existing notes; `synced_at` marks posted notes |

**Orphaned requirements check:** CLOUD-03 and CLOUD-04 appear in REQUIREMENTS.md and are assigned to Phase 17 (not Phase 18). No orphaned requirements for this phase.

---

## Anti-Patterns Found

| File | Pattern | Severity | Assessment |
|------|---------|----------|------------|
| `live_client.rs` lines 25, 411, 431, 439, 447, 462 | `TODO Phase 17:` comments | Info | Pre-existing from Phase 17 work; not introduced by Phase 18; not blocking Phase 18 goal |

No Phase 18 anti-patterns found. `edit_queue.rs` is correctly hollowed out with a deprecation comment (not an empty impl — it is a module-level note). No stub implementations, no hardcoded empty returns in any Phase 18 deliverable.

---

## Human Verification Required

### 1. End-to-End ww-card Issue Creation

**Test:** With GH credentials configured, trigger a full sync cycle on a fresh database (no existing cards). Observe `[sync] Created ww-card issue #N for card-id` in stderr output.
**Expected:** A new GH Issue appears in `BigscreenVR/beyond-outgoing` tagged `ww-card`, with JSON body matching `CardIssueBody` schema and title matching `format_card_title` output.
**Why human:** Requires running app with real GH token; can't test `gh` CLI subprocess calls statically.

### 2. Note GH Comment Posting (Immediate Path)

**Test:** With a card that has a `github_issue_number`, save a note via the UI. Wait 1-2 seconds.
**Expected:** A new comment appears on the linked GH Issue formatted as `` `ww-note` `` + italic-quoted content. The note's `synced_at` is set in SQLite.
**Why human:** Requires live UI + GH API + background thread; fire-and-forget thread not inspectable statically.

### 3. PendingEditFlusher Retry Behavior

**Test:** With GH credentials temporarily invalid, save a note. Verify a `SaveNote` row appears in the `pending_edits` SQLite table. Restore credentials and wait 60+ seconds.
**Expected:** The pending edit is flushed (row deleted), the note appears as a GH Issue comment, and `[flusher] Flushed SaveNote` appears in stderr.
**Why human:** Requires timing, real GH API, and SQLite inspection across a 60-second window.

---

## Gaps Summary

No gaps. All must-haves are verified at all levels (exists, substantive, wired). All three requirements (CLOUD-01, CLOUD-02, CLOUD-05) have implementation evidence. The phase goal is fully achieved.

---

_Verified: 2026-03-27_
_Verifier: Claude (gsd-verifier)_
