summaryrefslogtreecommitdiff
path: root/CLAUDE.md
blob: 50e1fcb798547300204fcf827ed67f7f1cf5d08b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
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.