Files
minstrel/CLAUDE.md
T
bvandeusen f62e72b2f2 docs: add CLAUDE.md with the Flutter→Android porting discipline
Encodes the hard rule the operator asked for after repeated
divergence: when porting/changing any Android feature that exists in
Flutter, read the Flutter source FIRST and replicate it exactly;
never silently substitute a different design or scope down — verify
the perceived blocker by reading more, and raise it as a question if
genuinely blocked. Points at docs/superpowers/parity-map.md as the
durable feature→source→target→status reference (gitignored, local).
Also captures the standing repo conventions (Forgejo-only, dev→main
flow, no in-task builds, detekt gates) so they survive context
compaction.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 08:12:23 -04:00

3.5 KiB

Minstrel — Claude working instructions

Porting discipline (HARD RULE — overrides default behavior)

The Android native client (android/) is a port of the Flutter client (flutter_client/). The Flutter client is the source of truth for behavior, layout, copy, and data flow. When building or fixing any Android feature that exists in Flutter:

  1. Read the Flutter source FIRST. Open the actual flutter_client/lib/... file(s) for the feature before writing any Kotlin. Do not work from memory, from a summary, or from an assumption about "the shape." If you have not opened the Flutter file in this session, you have not earned the right to write the Android version.

  2. Replicate it exactly. Match the behavior, the layout structure, the section order, the empty/loading/error states, the copy strings, the data sources, and the edge cases. Same number of rows, same fields per row, same tap targets.

  3. Never silently substitute a different design. If the faithful port seems hard, blocked, or impossible, STOP and verify by reading more of the Flutter source + the Android data layer. The blocker is usually wrong — the data or API you think is missing is often already there (e.g. audio_cache_index already carried lastPlayedAt; the offline pool was never actually blocked). If after reading it is genuinely blocked, raise it as a question rather than shipping a scoped-down or alternative design.

  4. Quote the Flutter file when you start a feature. Lead the work with "Here's flutter_client/lib/<path> — here's what it does — here's the Android port," so divergence is caught before code is written, not after.

  5. Keep the parity map current. docs/superpowers/parity-map.md maps every feature → Flutter source path → Android target → status. Re-read the relevant row before porting; update it after. This is the durable reference — rely on it, not on session memory, because context gets compacted on long sessions.

Repo conventions (already in force)

  • No GitHub — Forgejo only. PR/issue ops via the forgejo MCP; CI runs under .forgejo/workflows/. Never use gh or github.com URLs.
  • Git flow: work on dev → PR to protected main → tag release. Never push to main.
  • No in-task tests/builds. Do not run flutter/gradle/npm test/build/ analyze during implementation — CI verifies. Codegen scripts only.
  • CI is operator-side. After git push origin dev, the operator reports the result; don't poll Forgejo.
  • Specs/plans/audits/parity-map live local. docs/superpowers/ is .gitignored — save there for operator review; never commit those.
  • detekt gates CI. Watch the recurring ones: 60-line LongMethod, 11-function-per-file TooManyFunctions (use @file:Suppress with a one-line rationale for Compose-helper density), ReturnCount ≤ 2, MagicNumber, MatchingDeclarationName. Keep lines ≤ 100 chars.

Android port shape (quick reference)

  • Screens: *Screen.kt with the @HiltViewModel inline at the top of the file.
  • Repositories: */data/*Repository.kt. Wire models: models/wire/*Wire.kt. Domain models: models/*.kt.
  • App-lifetime singletons start via the "construct-the-singleton trick" — an @Inject lateinit var in MinstrelApplication whose init {} wires up.
  • Server errors are {"error":{"code":"...","message":"..."}}; surface them through ErrorCopy.fromThrowable(e), never raw e.message.
  • Cross-device reactivity: collect EventsStream.events filtered by kind.