---
phase: 20.1.1-change-the-i-button-on-cards-to-show-a-scrollable-history-of
plan: 02
subsystem: api
tags: [rust, github, gh-cli, serde_json, ww-note, issues-client]

# Dependency graph
requires:
  - phase: 20.1.1-01
    provides: upsert_notes_for_card and NoteEntry with author field in SQLite
provides:
  - GhNoteComment struct with author/created_at/content fields
  - list_issue_comments(issue_number) method on GhIssuesClient
  - parse_note_comment(body) free function (inverse of format_note_comment)
  - parse_comments_json(stdout) extracted for unit testability
affects:
  - 20.1.1-04 (fetch_notes_for_card uses list_issue_comments + parse_comments_json)

# Tech tracking
tech-stack:
  added: []
  patterns:
    - parse_comments_json extracted as pub(crate) associated fn for subprocess-free unit testing
    - TDD RED/GREEN on pure parser functions before subprocess-dependent method

key-files:
  created: []
  modified:
    - crates/integrations/src/github/issues_client.rs

key-decisions:
  - "parse_comments_json extracted as pub(crate) on impl GhIssuesClient for unit testability without spawning gh"
  - "issue_number is i64, passed via args[] (not string interpolation) — no shell injection surface (T-2012-01 mitigated)"
  - "parse_note_comment uses skip(1) + line-by-line strip rather than trim_start_matches to avoid repeated-prefix pitfall"

patterns-established:
  - "Subprocess JSON parsers extracted as pub(crate) associated fns for testability without mock infrastructure"

requirements-completed: []

# Metrics
duration: 3min
completed: 2026-04-15
---

# Phase 20.1.1 Plan 02: GH Comment Read-Back (list_issue_comments + parse_note_comment) Summary

**GhIssuesClient gains list_issue_comments via gh CLI subprocess and parse_note_comment as the exact inverse of format_note_comment, with 10 new unit tests covering round-trip, filtering, and error paths**

## Performance

- **Duration:** ~3 min
- **Started:** 2026-04-15T09:34:13Z
- **Completed:** 2026-04-15T09:36:51Z
- **Tasks:** 2
- **Files modified:** 1

## Accomplishments

- `parse_note_comment(body) -> Option<String>` pure free function with 6 passing tests (round-trip single/multi-line, non-ww-note rejection, empty input, trailing whitespace, tag-not-at-start)
- `GhNoteComment { author, created_at, content }` public struct (Debug, Clone, PartialEq, Eq)
- `list_issue_comments(issue_number: i64) -> Result<Vec<GhNoteComment>, GhIssuesError>` on GhIssuesClient using structured `args[]` (shell-injection safe per T-2012-01)
- `parse_comments_json(stdout: &[u8])` extracted as `pub(crate)` for unit testability — 4 tests covering mixed filtering, empty array, malformed JSON error, and missing comments key

## Task Commits

Each task was committed atomically:

1. **Task 1: parse_note_comment inverse parser** - `f96fa9d` (feat)
2. **Task 2: GhNoteComment struct + list_issue_comments** - `32f93fb` (feat)

**Plan metadata:** (docs commit follows)

_Note: Both tasks used TDD — RED (failing tests) committed first, then GREEN (implementation)._

## Files Created/Modified

- `crates/integrations/src/github/issues_client.rs` — Added GhNoteComment struct, parse_note_comment free function, list_issue_comments method, parse_comments_json associated fn, and 10 unit tests

## Decisions Made

- `parse_comments_json` is a `pub(crate)` associated function (not a free function) so tests can call `GhIssuesClient::parse_comments_json(bytes)` without constructing a client or spawning `gh`
- `issue_number` parameter is `i64`, never a user-controlled string — passed directly into `.args([...])` slice with no interpolation into shell command strings (mitigates T-2012-01)
- `parse_note_comment` uses `lines().skip(1)` to skip the `` `ww-note` `` header line, then strips `> _` prefix and `_` suffix per line — more robust than `trim_start_matches` which could strip repeated prefixes

## Deviations from Plan

None - plan executed exactly as written.

## Issues Encountered

None.

## User Setup Required

None - no external service configuration required.

## Next Phase Readiness

- Plan 04 (`fetch_notes_for_card`) can now call `list_issue_comments(number)` then map results to `NoteEntry` rows for `upsert_notes_for_card` (Plan 01)
- All public API shapes match the interfaces specified in RESEARCH.md Area 3
- `cargo check --workspace` exits 0; all 23 `github::issues_client` tests pass

---
*Phase: 20.1.1-change-the-i-button-on-cards-to-show-a-scrollable-history-of*
*Completed: 2026-04-15*
