focus-flow/LOGGING_RULES.md

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

  1. 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.

  2. 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);
    
  3. 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.

  4. 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:

    • finest configuration captures caller information for every written log.
    • fine, finer, or finest configuration captures caller information for warn and error logs.
    • debug, info, warn, or error configuration does not capture caller information for normal lower-detail logs.
  5. If a caught exception already provides an error or stack trace, pass it to the logger. Do not create StackTrace.current just to satisfy logging.

    } on Object catch (error, stackTrace) {
      logger.error(
        () => 'Startup failed.',
        error: error,
        stackTrace: stackTrace,
      );
    }
    
  6. 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.

  7. 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.

  8. 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=value form.
  • 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, or error.
  • 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:

  • finest configuration 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 warn or error path.
  • 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.dart and 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 build methods, 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 debug summary before/after the loop.
  • Use finer for important branch decisions inside the loop only when useful.
  • Use finest for per-item dumps only when the details are essential.

For caught exceptions:

  • warn if recovered and state is safe.
  • error if state may be bad, operation failed, or review is needed.
  • Pass error: and stackTrace: 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:

  1. Run formatter on edited Dart files.

  2. Run package analysis and tests for touched packages.

  3. At minimum for scheduler core changes:

    cd packages/scheduler_core
    dart analyze
    dart test
    
  4. At minimum for Flutter app changes:

    cd apps/focus_flow_flutter
    dart analyze
    flutter test
    
  5. Do not introduce new analyzer warnings. Existing unrelated info-level lints may remain, but do not add new ones.

  6. 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.