summaryrefslogtreecommitdiff
path: root/CLAUDE.md
diff options
context:
space:
mode:
authorClaude <noreply@anthropic.com>2026-09-21 08:13:49 +0000
committerClaude <noreply@anthropic.com>2026-09-21 08:13:49 +0000
commit5876025559ba0ad287d3a7a4d9fa0246509e668e (patch)
treec918d01e2b74d205ee00fe96926232475840ff97 /CLAUDE.md
Scaffold Listen, with a podcast-native feed parser
Sibling to FeeDreader, not a shared module. That was a considered call: its FeedParser only inspects <enclosure> while hunting for a thumbnail, and its FilterRuleEngine is 46 lines of title substring matching that doesn't generalise to queue rules, so only Opml.kt would genuinely have transferred. The parser is the piece everything else sits on, so it goes first, test-first: 18 tests covering audio/video enclosure selection, iTunes durations in both second and clock form, season/episode numbers, podcast:chapters for the skip feature, RFC 5005 archive paging for drip subscriptions, and episode identity falling back to the enclosure URL when a guid is missing. Two things worth recording: - Built on XmlPullParserFactory rather than android.util.Xml. The latter returns null off-device, which is why FeeDreader's 297-line parser has no tests at all. Both resolve to the same KXmlParser on Android. - SimpleDateFormat with isLenient=false rejects "Tue, 02 Sep 2026 06:00:00 GMT" -- an entirely ordinary RFC 822 date. Left strict, every pubDate in the app would have been null and episode ordering would have been silently wrong everywhere. minSdk is 29, deliberately not FeeDreader's 36, which would restrict the app to Android 16 and newer. Permissions: INTERNET and ACCESS_NETWORK_STATE for feeds; FOREGROUND_SERVICE, FOREGROUND_SERVICE_MEDIA_PLAYBACK, POST_NOTIFICATIONS and WAKE_LOCK because Android requires a foreground service with a notification to play audio in the background. That notification is the transport control, not an alert -- Listen posts no new-episode notifications by design. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018AwnALZL6bD4VBtATDVv3B
Diffstat (limited to 'CLAUDE.md')
-rw-r--r--CLAUDE.md91
1 files changed, 91 insertions, 0 deletions
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..50e1fcb
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,91 @@
+# Listen — Agent Instructions
+
+Personal Android podcast app, `org.terst.listen`. Sibling to FeeDreader (`/workspace/feedreader`),
+built for Peter's own use. Greenfield, hands-on-testing-rounds workflow: sideload a debug build,
+use it, batch feedback.
+
+Design plan: https://claude.ai/code/artifact/85137e78-3718-4d84-8ab8-f72885170653
+
+## What this app is, and isn't
+
+Replacing Pocket Casts completely. The distinguishing features are audio-based intro/outro
+skipping, slow-drip archive subscriptions, and rule-driven auto-queueing.
+
+**Non-goals, deliberately:** anything social, new-episode notifications (or any alert — the media
+notification is a transport control and is mandatory for background audio, which is a different
+thing), favourites, bookmarks. Discovery and video were both cut from scope.
+
+## Relationship to FeeDreader
+
+Separate repos, no shared module. That was a considered call, not an accident: FeeDreader's
+`FeedParser` treats `<enclosure>` as an image source and its `FilterRuleEngine` is 46 lines of
+substring matching that doesn't generalise to queue rules, so only `Opml.kt` (~96 lines) genuinely
+transferred.
+
+**Standing instruction:** watch for things that do generalise, and avoid pointless drift between
+the two apps. If a change here would obviously improve FeeDreader too, say so. Don't force sharing
+where the problems are actually different — but don't let two solutions to the same problem quietly
+diverge either.
+
+## Test-driven development is mandatory
+
+Same rule as FeeDreader, same reasons. Every change to `app/src/main/` follows red-green-refactor,
+in that literal order:
+
+1. **Write the test first**, for the specific behaviour you're about to add or change.
+2. **Run it. Confirm it fails, and fails for the right reason** — an assertion mismatch showing the
+ missing behaviour, not a compile error.
+3. Write the minimal production code to pass.
+4. Run again. Confirm green.
+5. Refactor if needed, keeping it green.
+6. Run the full quality gate before committing.
+
+**If production code was written before or without a preceding failing test**, discard the diff and
+redo it test-first. Do not backfill tests onto existing code — a test written against working code
+confirms what the code does, not what it should do.
+
+**Scoping exception:** pure resource/config/manifest changes with no unit-testable behaviour don't
+get a red step; the static-analysis gate covers those.
+
+## Verification — mandatory
+
+**Never conclude a build or test result from grepped console output**, and never verify a launch
+with `adb shell monkey`. Use the shared wrappers, which report from exit codes, parsed JUnit XML,
+and a logcat capture started before launch:
+
+```
+/workspace/devtools/bin/gradle-test /workspace/listen -- --rerun-tasks
+/workspace/devtools/bin/android-launch-check org.terst.listen \
+ --apk app/build/outputs/apk/debug/app-debug.apk --settle 15
+```
+
+See `/workspace/devtools/README.md`. These exist because grepped console text has produced wrong
+claims three separate times across these repos.
+
+## Quality gate — every commit
+
+1. **Build** — `./gradlew compileDebugKotlin` succeeds.
+2. **Tests** — `./gradlew testDebugUnitTest` passes in full.
+3. **Static analysis** — `./gradlew detekt lintDebug` clean: zero lint errors, zero new detekt
+ findings beyond the baseline.
+4. **Coverage must not decrease** against `.coverage-baseline` (line coverage over
+ `app/src/main/java` excluding `ui/**`). Set `COVERAGE_EXCEPTION="reason"` for justified drops,
+ e.g. deleting covered dead code.
+5. **Manifest permission diff** — if `<uses-permission>` changed, the commit message says why.
+6. **Schema migrations** — if `AppDatabase.kt`'s `version` changed, a migration test must be
+ touched in the same diff. Room tracks by version; a wrong migration corrupts a real,
+ non-reproducible library.
+
+## Domain notes worth keeping
+
+- **Playback position is the crown jewel of the data model.** It must survive process death. Save
+ on a timer, on pause, and on lifecycle transitions — not just on stop.
+- **Never assume audio.** An enclosure carries a MIME type. Video was cut from scope, but the model
+ stays media-agnostic so it isn't a retrofit.
+- **GUIDs move.** Publishers rewrite feeds; match episodes on GUID first, then enclosure URL, then
+ title+date. An episode that loses its identity loses its playback position.
+- **Retention is auto-delete-once-played** with a grace period, so a mis-tap doesn't destroy an
+ episode you were still listening to.
+- **YouTube never touches this app.** Audio arrives via podkit running in Termux on the phone,
+ writing to a local folder Listen imports. No extractor code here, nothing to break when Google
+ ships a change.