diff options
Diffstat (limited to 'CLAUDE.md')
| -rw-r--r-- | CLAUDE.md | 91 |
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. |
