summaryrefslogtreecommitdiff
path: root/CLAUDE.md
diff options
context:
space:
mode:
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.