Conventions every contributor (human or LLM) follows. Adapted from the x4 project’s guidelines, slimmed to fit a small, read-only Flutter app.
Read first: the design spec and implementation plan in
docs/superpowers/. They explain why the app is shaped the way it is.
A Flutter (Android + iOS) client that shows the daily workout (WOD) for
Vogue Fitness UAE branches. It has no backend — it fetches and parses
the public schedule from vfuae.com/wod/ on-device and caches it locally.
It is a read-only viewer: no booking, no accounts, no notifications.
A single Flutter app package, folder-layered. Layering discipline is kept; the 16-package workspace machinery of larger projects is deliberately not.
lib/
main.dart composition root — builds the dependency graph
app.dart MaterialApp.router + theme
router.dart GoRouter routes
core/ AppFailure, Result, Log — no Flutter, no features
domain/ models (freezed) + WodRepository port
data/ parser, remote source, cache, repository impl, prefs
presentation/
theme/ design system — tokens, typography, ThemeData
<feature>/ one folder per screen: <name>_screen.dart + <name>_cubit.dart
widgets/ shared widgets
test/ mirrors lib/ ; fixtures/ holds the HTML snapshot
Dependency rule — strictly one direction:
presentation → domain → core and data → domain → core.
domain and core never import data, presentation, or package:flutter
beyond what freezed needs. Features depend on the WodRepository port
in domain/, never on a concrete adapter.
Data flow: Screen → Cubit → WodRepository → (RemoteSource | Cache).
Cubit that owns a sealed freezed state union
(loading / data / empty / error). Widgets read state and emit
intents; they contain no business logic.AppFailure, never a raw exception in the UIsealed class AppFailure in lib/core/
(NetworkFailure, ParseFailure, CacheFailure).data/ translate native exceptions into an AppFailure at
the boundary and return Result<T> (Ok / Err) — they do not throw
across layers.AppFailure to user-facing copy (AppFailure.message).printUse Log.d/i/w/e from lib/core/logger.dart. avoid_print is a lint
error.
lib/presentation/theme/. No raw Color(0x…) and no ad-hoc
TextStyle in feature code — add a token if one is missing.program_palette.dart.DESIGN.md for the token vocabulary.freezedDomain models are @freezed. Models that are cached add fromJson/toJson
(json_serializable, explicit_to_json is on — see build.yaml). After
changing a freezed/json model, run dart run build_runner build.
All knowledge of the vfuae.com HTML shape lives in
lib/data/wod_html_parser.dart and nowhere else. If the site changes,
that is the only file to edit. The parser must skip a malformed entry,
never throw and lose the whole feed. Its tests run against the committed
fixture test/fixtures/vfuae_wod_sample.html — keep them deterministic.
analysis_options.yaml extends very_good_analysis with strict-casts,
strict-inference, strict-raw-types, and enforces: avoid_print,
prefer_const_constructors, prefer_final_locals,
require_trailing_commas, unawaited_futures, directives_ordering.
Generated files (*.g.dart, *.freezed.dart) are excluded.
flutter analyze must print No issues found! before every commit.
The repo is public. Nothing in a tracked file or commit message should be something you would not write on a postcard.
Never commit:
android/app/upload-keystore.jks,
android/key.properties, any other *.jks / *.keystore.*.p12, *.mobileprovision, any
exported development or distribution certificate..env, .env.*, anything containing
credentials, API keys, or tokens.build/symbols/. They are emitted at release time and
must be backed up out-of-band (alongside the keystore), never to git.The repo .gitignore covers these paths. Do not add exceptions; if a
build needs a secret value, source it from a file the .gitignore
lists (locally) or from a CI secret store (in CI).
Before pushing, run:
bash tool/lint/no_committed_secrets.sh
It is part of tool/dev/check.sh and looks for the common shapes
(plaintext keystore passwords, AWS / Google / OpenAI key prefixes,
inline PEM private keys, etc.). A clean run prints OK.
If a secret slips through:
git filter-repo (or BFG). A new
commit that just deletes the file does not remove it from the
history that GitHub already has.main is justified.)What we test:
bloc_test.What we do not test: pixel layout, the live network in unit tests (an opt-in test may probe the live site to detect upstream drift).
Practise TDD on the parser, cache, repository, and cubits: failing test first, then the minimal implementation.
docs/superpowers/specs/YYYY-MM-DD-<topic>-design.md.docs/superpowers/plans/YYYY-MM-DD-<topic>-plan.md.bash tool/dev/check.sh must print ALL GREEN (format,
analyze, test) before a PR.Conventional commits: feat(scope): …, fix(scope): …, test(scope): …,
docs: …, chore: …. One logical change per commit. Keep the build green
at every commit.
lib/presentation/<feature>/<feature>_cubit.dart — a Cubit
with a freezed state union; depend on WodRepository (the port).test/presentation/<feature>_cubit_test.dart first (bloc_test).lib/presentation/<feature>/<feature>_screen.dart — wrap in
BlocProvider, render per state, build only from theme tokens and
shared widgets.lib/router.dart.bash tool/dev/check.sh → green → commit.