.agents/rules.md
These are repository coding and testing conventions. Codex command permission rules belong in .codex/rules/*.rules; see .agents/agent-config.md before adding those.
analysis_options.yaml enforces these non-default rules:
prefer_single_quotes: true: always use single quotes.require_trailing_commas: true: use trailing commas in multi-line argument lists.sort_child_properties_last: true: child: must be the last named parameter.avoid_print: true: do not use print() calls.prefer_const_constructors: true and prefer_const_declarations: true.prefer_final_locals: true and prefer_final_in_for_each: true.always_declare_return_types: true.Generated directories are excluded from analysis:
build/**lib/l10n/intl/**lib/**/generated/**plugins/**Comments are opt-in and reserved for the few places that genuinely need one. Density is the point: every comment that restates the code devalues the comments that carry real information, until readers skim past all of them. A file with three comments that matter is more readable than one with thirty.
// ignore:, // ignore_for_file:,
// coverage:ignore), license and copyright headers, code-generation markers, and comments inside vendored upstream
code such as lib/widgets/open_container.dart.Pick the destination by where the constraint would be violated, not by how important it feels.
.agents/*.md or a .agents/skills/*/SKILL.md. They
are violated from many files, so they must reach every future agent at session start. A comment in one file cannot do
that.lib/common/constant.dart is the model case:
the delay-test concurrency cap is bound to mBatch in core/common.go, and whoever changes that number must see the
constraint on the same screen.Both failure directions are real. Moving a local constraint into .agents/ hides it from the person editing the line;
leaving a repo-wide policy as a comment reaches only the reader of that one file.
Before any of the three, prefer encoding the intent in structure and naming — a named mixin, type, or method that makes the invariant hard to break beats prose that asks the next reader not to break it.
CoreMethodCall/CoreMethodResponse JSON envelope structurally identical across Dart, Go, JNI, and
desktop IPC. Do not double-encode arguments, result, or event batches.core/message.go; bulk backpressure must not
evict delay, loaded-provider, or geo-update state.DesktopCoreLifecycle; do not start/kill FlClashCore from providers, widgets,
managers, or ad hoc exit callbacks. Acquire and release it through a CoreProcessLease.CoreController.close() and platform close() implementations are terminal and idempotent. Application shutdown must
stay centralized in SystemAction/SystemExitCoordinator.ServiceState; do not add a Flutter completion callback that creates a second lifecycle owner.ServiceState and keep ServiceController as the sole binding/run-time owner.BroadcastReceiver.goAsync() path must finish its PendingResult exactly once. A watchdog may release the
broadcast lease, but must not cancel, reverse, or otherwise redefine the service operation.CoreStatusButton's connecting hold must remain local display state. It must not delay or
overwrite coreStatusProvider, and a real failure must bypass/cancel the hold immediately.The core/ directory is excluded from automated coverage accounting. Do not add coverage instrumentation or coverage
collection for code under core/. CI still runs CGO_ENABLED=0 go test . and go vet . to compile/check the Go wrapper;
verify cross-language protocol behavior through shared Dart contract tests under test/core/ and native platform build
checks.
Use CoreController.test(mock) to inject a mocked CoreHandlerInterface. Call CoreController.resetInstance() in tearDown to clean up the singleton between tests.
Register fallback values for freezed params used with any() matchers.
Use ProviderContainer directly for simple Riverpod provider tests. The generated Riverpod update() method takes a callback:
notifier.update((state) => newValue);
When testing freezed models with nested objects, always round-trip through jsonEncode and jsonDecode. Direct fromJson(toJson()) fails for nested freezed types because toJson() stores child objects directly instead of maps.
For async widgets, put visual cleanup in finally when the action may throw. Focused widget tests should cover success,
failure, disposal, and any timer boundary that changes visible state.
Do not manually edit generated files under:
lib/l10n/l10n.dartlib/models/generated/lib/providers/generated/lib/database/generated/lib/l10n/intl/After schema, model, or provider changes, run build generation and include focused tests when behavior changes.