summaryrefslogtreecommitdiff
path: root/CLAUDE.md
diff options
context:
space:
mode:
Diffstat (limited to 'CLAUDE.md')
-rw-r--r--CLAUDE.md114
1 files changed, 114 insertions, 0 deletions
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..59b2350
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,114 @@
+# FeeDreader — Agent Instructions
+
+Personal Android RSS/Atom reader, `org.terst.feedreader`. Built for Peter's own use, greenfield,
+hands-on-testing-rounds workflow (sideload a debug build, use it, batch feedback). See `BACKLOG.md`
+for open work.
+
+## Test-driven development is mandatory
+
+Every change to production code (`app/src/main/`) follows red-green-refactor, in that literal order:
+
+1. **Write the test first**, for the specific behavior you're about to add or change. Not a test
+ for the whole feature at the end — the test that would fail without the change you're about to
+ make.
+2. **Run it. Confirm it fails, and fails for the right reason** — an assertion mismatch that
+ demonstrates the missing behavior, not a compile error, not a typo, not a test that was
+ accidentally already passing.
+3. Only now write the minimal production code to make it pass.
+4. Run again. Confirm green.
+5. Refactor if needed, keeping it green.
+6. Run the full quality gate (below) before committing.
+
+**If it's discovered that production code was written before, or without, a preceding failing
+test** — mid-session, in review, by the pre-commit hook, however it surfaces — **the fix is to
+discard the whole diff and redo it properly**, test first. Do not backfill tests onto code that
+already exists to make the rule look satisfied after the fact: a test written against working code
+tends to just confirm what the code already does, not drive the design or catch what the code
+missed. `git diff` to see what's uncommitted, stash or note anything worth keeping conceptually,
+then `git checkout -- <files>` (or `git reset` for staged-but-uncommitted work) and start over from
+a failing test. This applies to every agent working in this repo, not just whoever wrote the
+original rule.
+
+**Scoping exception:** pure resource/config/manifest changes with no meaningful unit-testable
+behavior (an XML theme parent, a manifest permission, a Gradle version bump) don't get a red step —
+there's nothing to fail. These are instead covered by the static-analysis gate (lint/detekt) acting
+as the regression check. If you're unsure whether something falls in this bucket, default to
+writing the test.
+
+## Quality gate — every commit
+
+Enforced by `.githooks/pre-commit` (see below; run `git config core.hooksPath .githooks` once per
+clone to activate it — this is a manual one-time step per checkout, not automatic, since hooks
+aren't cloned by default):
+
+1. **Build** — `./gradlew compileDebugKotlin` must succeed.
+2. **Tests** — `./gradlew testDebugUnitTest` must pass in full.
+3. **Static analysis / code-quality audit** — `./gradlew detekt lintDebug` must both come back
+ clean: zero lint errors (warnings are fine to accumulate but should trend down, not up — see
+ "periodic checks"), zero new detekt findings beyond the tracked baseline.
+4. **Coverage must not decrease** — Jacoco line coverage over `app/src/main/java` excluding
+ `ui/**` (Compose screens are impractical to unit-test for coverage and would make the metric
+ meaningless — see `app/build.gradle.kts` Jacoco exclusions) is compared against
+ `.coverage-baseline`. A commit that drops it fails.
+ - **Justifiable exceptions exist** — e.g. deleting dead code that happened to be covered,
+ where the deletion is the point (percentage can dip when the denominator shrinks even though
+ real risk went down). These aren't silently allowed: set
+ `COVERAGE_EXCEPTION="reason for the drop"` when committing, which the hook logs and lets
+ through instead of blocking. Don't reach for this to avoid writing a test you were supposed to
+ write — it's for cases where the drop itself is the correct outcome.
+ - A commit that *raises* coverage automatically updates `.coverage-baseline` as part of the
+ hook.
+5. **WebView JavaScript justification** — any new or changed `javaScriptEnabled = true` /
+ `setJavaScriptEnabled(true)` site must carry a `@SuppressLint("SetJavaScriptEnabled")` plus an
+ inline comment explaining why (matches the existing pattern in `ReaderExtractor.kt`,
+ `LinkReaderScreen.kt`, `BrowserScreen.kt`). This app's whole security posture around untrusted
+ article content rests on JS being off almost everywhere — a new JS-enabled surface is exactly
+ the kind of thing that should never land silently.
+6. **Manifest permission diff** — if `AndroidManifest.xml`'s `<uses-permission>` list changed, the
+ hook prints a warning requiring the commit message to say why. Doesn't block (permissions
+ sometimes need to land before the feature using them), but makes it impossible to miss in review.
+7. **Secret scan** — a lightweight grep for API-key-shaped strings and common credential patterns
+ across the staged diff. Blocks on a hit; override only if you're certain it's a false positive
+ (e.g. a test fixture with an obviously-fake key).
+8. **Schema-migration test requirement** — if `AppDatabase.kt`'s `version = N` changed, the diff
+ must also touch a migration test file (Room's `MigrationTestHelper`, run against a real
+ pre-migration DB). No mechanism currently exists for this (see Task: Room migration tests below)
+ — until it does, this check is a documented rule, not yet hook-enforced.
+
+Never bypass this hook with `--no-verify` without asking first, same as the general git-safety
+rules — if a check is wrong or too strict for a specific case, fix the check or use the documented
+exception mechanism, don't route around it silently.
+
+## Periodic checks (not per-commit — too heavy, or need a human/agent read, not a mechanical gate)
+
+- **Dependency freshness** — `./gradlew lintDebug` already reports outdated dependency versions via
+ its `GradleDependency` check (no extra plugin needed — the `com.github.ben-manes.versions` /
+ `io.github.ben-manes.versions` plugin was tried during setup and reproducibly broke
+ `kspDebugKotlin` in this environment, so it's deliberately not used here). Batch-upgrade
+ periodically from those findings rather than drip-updating one dependency per unrelated commit.
+- **Dead-code / reachability sweep** — the kind of audit that found the orphaned `/tabs/meals`-style
+ routes on doot and the dead DAO methods here: re-run after a significant feature batch, not every
+ commit. Trace every nav route, DAO method, and setting to a real caller.
+- **Room migration correctness** — beyond the per-commit "a test exists" check above, periodically
+ verify the full migration chain (v1→latest) runs clean against a realistic seeded DB, not just the
+ latest single-step migration in isolation.
+- **Detekt/lint baseline review** — baselines exist so new code doesn't inherit old debt as a blocker,
+ but a stale baseline just hides debt forever. Periodically review what's in it and pay some down.
+
+## Why these specific checks
+
+Every check above traces back to something that actually happened in this codebase, not a generic
+checklist:
+- The lint zero-errors gate exists because `themes.xml`'s theme parent silently required API 29
+ against a minSdk of 26 — a real crash risk on real devices that sat unnoticed until an audit found
+ it.
+- The WebView-JS-justification check exists because this app's core security property (JS off for
+ untrusted article content) depends entirely on that staying true at every callsite, and one
+ existing site (`ReaderWebView.kt`) already didn't carry the same justification pattern as the
+ other three.
+- The dead-code sweep exists because three DAO methods (`ArticleDao.delete`,
+ `FilterRuleDao.getAll`, `ArticleDao.markAllReadForFeed`) were superseded by newer methods and left
+ behind, undetected until an audit.
+- The migration-test requirement exists because `AppDatabase` is on its 11th schema version with
+ zero migration tests, several of which are one-off hardcoded data repairs — exactly the kind of
+ change most likely to corrupt a real, non-reproducible production DB if wrong.