12 KiB
FocusFlow Logging Rules
These instructions are for adding logging calls to the FocusFlow codebase. The goal is to add useful diagnostics without moving logging policy into individual services, controllers, repositories, or widgets.
Hard Requirements
-
Use the shared logger only. Do not create service-specific logger classes, log enums, log entries, helper sinks, caller-capture code, stack-frame parsing, or message-formatting code in feature files.
-
Call logging directly from feature code. Correct examples:
logger.debug(() => 'Applying flexible quick action. taskId=${task.id}'); logger.warn(() => 'Duplicate operation ignored. operationId=$operationId'); logger.error(() => 'SQLite bootstrap failed. code=${failure.code.name}');Incorrect examples:
if (logger.wouldWrite(FocusFlowLogLevel.debug)) { logger.debug('Applying action. taskId=${task.id}'); }final message = 'Expensive state dump. tasks=${tasks.map(...).join(',')}'; logger.finest(message); -
Lazy messages are required for anything with interpolation, collection formatting, object dumps, date formatting, joins, maps, or stack-related values. Pass a closure:
logger.debug(() => '...'). The logger will not evaluate the closure unless that level is enabled. -
Do not manually capture caller file, method, line, or stack traces for normal log calls. The logging layer automatically captures caller information when the configured level requires it:
finestconfiguration captures caller information for every written log.fine,finer, orfinestconfiguration captures caller information forwarnanderrorlogs.debug,info,warn, orerrorconfiguration does not capture caller information for normal lower-detail logs.
-
If a caught exception already provides an error or stack trace, pass it to the logger. Do not create
StackTrace.currentjust to satisfy logging.} on Object catch (error, stackTrace) { logger.error( () => 'Startup failed.', error: error, stackTrace: stackTrace, ); } -
Do not change runtime behavior to add logging. Logging must not affect return values, operation IDs, scheduling results, persistence results, widget state, retry behavior, or exception behavior.
-
Preserve existing SPDX headers, library docs, Dartdoc, imports, and part-file structure. New Dart files need SPDX metadata and Dartdoc, but this pass should not need new Dart files.
-
Use ASCII only unless the edited file already requires non-ASCII.
Import Rules
Use the shared scheduler logger from:
import 'package:scheduler_core/scheduler_core.dart' show logger;
If a file already has a local logger name or a conflict, use an alias:
import 'package:scheduler_core/scheduler_core.dart' as scheduler_core;
scheduler_core.logger.info(() => '...');
Inside packages/scheduler_core/lib/src/..., prefer existing local library
patterns. Some scheduler core files are part of files. A part of file cannot
add imports. For those files, add the import to the parent library file instead.
Example: flexible_task_action_service.dart is a part of
task_actions.dart, so task_actions.dart imports the logging layer and the
part file can call logger.debug(...).
Do not import the Flutter app file logger into scheduler core or package code.
The app file logger is only the configured sink. Feature code should use the
shared logger.
Message Shape
Prefer concise, structured text:
logger.debug(() => 'Scheduling backlog item. taskId=$taskId '
'durationMinutes=$durationMinutes windowStart=${window.start.toIso8601String()}');
Use:
- A short action phrase first.
- Stable key names in
key=valueform. - IDs, enum names, dates, counts, status, outcome codes, and operation IDs.
toIso8601String()for dates when logging dates.
Avoid:
- Full user-authored task titles, descriptions, notes, or free-form content at
info,debug,fine,warn, orerror. - Large object dumps outside
finest. - Vague messages like
Something went wrong. - Messages that blame the user.
- Duplicate logs for the same event at multiple levels.
Level Rules
finest
Use for extremely granular diagnostics only.
Good uses:
- Full input or output dumps after level gating.
- Raw decoded payloads or document maps when debugging mapping/migration.
- Complete task lists, scheduling inputs, scheduling results, or repository state snapshots.
- Per-candidate loop details only when needed to reconstruct a scheduling decision.
Do not use for:
- Normal app flow.
- High-level operation starts or finishes.
- Any message that would be useful in normal debugging at
debug.
Notes:
finestconfiguration automatically prepends caller information for every written log.- Always use a lazy closure.
finer
Use for intra-method minor actions and small state changes.
Good uses:
- Branches selected inside a larger operation.
- Resolved operation IDs, generated IDs, or normalized inputs.
- A candidate accepted/rejected inside a scheduling method.
- A repository deciding which query path to use.
- A controller clearing or preserving local selection as a secondary state change.
Do not use for:
- Method entry/exit in every method.
- Normal high-level actions that belong at
debug. - Large data dumps that belong at
finest.
fine
Use for secondary detail and "possible issue" logging.
Good uses:
- Extra detail that explains a
warnorerrorpath. - Detailed result summaries, notice lists, change lists, or stack traces when already caught.
- A handled unexpected or non-typical value that could indicate a real issue, but the code safely handled it.
Possible issue rule:
- A "possible issue" is an unexpected or non-typical value that was handled but may indicate bad upstream state or a missed assumption.
- Only use this when there is a real reason to suspect an issue.
- Start the message with
Possible issue:when using this concept.
Good possible issue example:
logger.fine(() => 'Possible issue: completion transition did not apply. '
'taskId=${task.id} outcome=${transition.outcomeCode.name}');
Do not use fine for:
- Normal control flow.
- Expected empty states.
- User choices such as canceling, closing a modal, or choosing no date.
- Anything that should clearly be a handled issue at
warn.
debug
Use for high-level debug flow and useful state values.
Good uses:
- A command/action starts with important input IDs and state.
- A scheduling operation finishes with outcome, change count, notice count.
- A task is pushed and the earliest candidate time or destination used.
- A controller submits an action and refreshes data.
- A repository save/load operation begins or finishes with IDs and counts.
Do not use for:
- Healthy app lifecycle milestones that belong at
info. - Handled failures that belong at
warn. - Extremely detailed dumps that belong at
finest. - Per-frame or every-build Flutter widget logs.
info
Use for very high-level healthy lifecycle checkpoints.
Good uses:
- App startup started/completed.
- Persistent runtime opened/closed.
- SQLite connected or persistence backend opened.
- Backup/export started/completed.
- A major recovery routine completed with a compact summary.
Do not use for:
- Per-task operations.
- UI taps.
- Scheduler internal decisions.
- Warnings, handled issues, or noisy repeated operations.
warn
Use when something did go wrong, but the app handled it and kept going.
Good uses:
- Duplicate operation ignored.
- Task not found and a typed no-op/not-found result is returned.
- Invalid task state handled with a typed result.
- Config file unreadable and defaults are used.
- Recovery performed a fallback and continued.
- Repository compare-and-set conflict handled without corrupting state.
Do not use for:
- Expected user choices.
- Empty query results.
- Normal no-op behavior that is part of the happy path.
- "Possible issues" that are only unusual and safely handled; those are
fine.
error
Use when state may be corrupted, startup cannot continue, persistence may be in a bad state, data may require review, or an invariant is being violated.
Good uses:
- Throwing because a service received an impossible or invalid task type.
- Startup/bootstrap failure.
- Repository commit failure.
- Migration or mapping failure that prevents safe loading.
- Backup encryption/decryption failure.
- Unhandled command failure requiring review.
Do not use for:
- Handled typed no-op results.
- Validation failures that are expected user input paths.
- Missing optional configuration.
- Anything recovered cleanly without review; use
warn.
Where Logging Belongs
Add logs to imperative boundaries and domain operations:
main.dartand app composition/open/close paths.- Controllers that submit commands, refresh data, or handle failures.
- Application use cases.
- Scheduling services and engines.
- Repository and persistence adapters.
- Backup/export/import operations.
- Recovery, migration, mapping, and command orchestration.
Avoid logs in:
- Pure data classes, enums, constants, token files, and simple value objects.
copyWith, equality, validation constructors, and simple formatting helpers unless they throw or handle a real issue.- Flutter
buildmethods, painters, layout helpers, and frequently called visual functions unless there is a rare handled error. Build methods can run often; logging there can make logs unusable.
For loops:
- Prefer one
debugsummary before/after the loop. - Use
finerfor important branch decisions inside the loop only when useful. - Use
finestfor per-item dumps only when the details are essential.
For caught exceptions:
warnif recovered and state is safe.errorif state may be bad, operation failed, or review is needed.- Pass
error:andstackTrace:if already available.
Performance Rules
The logger already gates work by level. Call sites should rely on that.
Good:
logger.finest(() => 'Scheduling input dump. input=$input tasks=${input.tasks}');
Bad:
final dump = 'Scheduling input dump. input=$input tasks=${input.tasks}';
logger.finest(dump);
Do not add logger.wouldWrite(...) checks around normal calls. If message
creation is expensive, put that expensive work inside the closure. Only consider
wouldWrite for rare cases where a large temporary structure must be built
before a logger call and cannot reasonably be built inside the closure.
Validation Rules
After adding logs:
-
Run formatter on edited Dart files.
-
Run package analysis and tests for touched packages.
-
At minimum for scheduler core changes:
cd packages/scheduler_core dart analyze dart test -
At minimum for Flutter app changes:
cd apps/focus_flow_flutter dart analyze flutter test -
Do not introduce new analyzer warnings. Existing unrelated info-level lints may remain, but do not add new ones.
-
Logging changes must not change functional test expectations except tests that directly assert logging behavior.
Zip Replacement Instructions
This zip is intended to be edited externally and then extracted over the repo
root. It contains this rules file at the top level and source files under their
repo-relative paths, such as apps/... and packages/....
When returning edited files:
- Keep the same paths.
- Do not rename files.
- Do not remove files from the archive unless they are intentionally deleted in the repository.
- Do not add generated files, build output,
.dart_tool, or dependency caches. - Preserve the shared logging layer design.