Notechondria

Version: 0.1.52 Build Date: 2026-04-21T12:00

What's Changed

AGENTS.md §1.5: codified 1000-LOC hard ceiling

User flagged the urgent TODO that app_shell.dart grows unchecked and introduced a rule: no code file may exceed 1000 lines. Codified as a hard bullet under AGENTS.md §1.5 Style defaults alongside the existing soft ~500-LOC suggestion:

Hard ceiling: 1000 LOC per code file. No code file should have any reason to exceed 1000 lines. When an existing file grows past 1000 LOC, split it in the same commit that would take it over — by extracting cohesive sub-modules (partials, helper files, or a new class), not by chopping arbitrarily at line 999. If you genuinely cannot find a clean split and must temporarily exceed 1000 LOC, document the reason in the same commit message. Line-count audits should focus on application source; do not count migrations, autogenerated code, or vendored dependencies.

Proof of pattern: recycle-bin extracted to core/local_trash.dart

All three apps had ~250 lines of duplicated recycle-bin code (0.1.51) sitting in app_shell.dart. Moved each into a new lib/core/local_trash.dart partial containing a single extension on _AppShellState:

extension _AppShellLocalTrashX on _AppShellState {
  Future<void> _persistLocalTrashedDrafts() async { ... }
  Future<void> _moveDraftToLocalTrash(...) async { ... }
  Future<ActionFeedback> _restoreTrashedDraft(...) async { ... }
  Future<void> _openLocalRecycleBinDialog() async { ... }
  // …
}

Dart's private-to-library scoping means the extension can touch _localTrashedDrafts, _localDrafts, _log, _showMessage etc. transparently. The one snag: setState is @protected, so extensions can't call it even within the same library. Fix: tiny void _trashRefresh() wrapper left behind on _AppShellState that calls setState(() {}) on behalf of the extension. One line of coupling for a clean 250-line extraction.

Per-app LOC deltas

FileBeforeAfterΔ
editor_app/lib/app_shell.dart54585211-247
planner_app/lib/app_shell.dart40943861-233
portal_app/lib/app_shell.dart39923760-232
new: editor_app/lib/core/local_trash.dart0274+274
new: planner_app/lib/core/local_trash.dart0249+249
new: portal_app/lib/core/local_trash.dart0249+249

None of the three app_shell.dart files are under 1000 LOC yet — this round is the first step, not the completion. See TODO.md for the remaining split backlog (auth flow, sync loops, archive export/import, sidebar categories, settings section builders, note-editor helpers, client.dart HTTP plumbing).

Files Changed

New

Modified

Verification

  • editor_app: flutter analyze 56 issues (+1 informational use_string_in_part_of_directives for the new partial); smoke test passes.
  • planner_app: flutter analyze 70 issues (+1 same); smoke test passes.
  • portal_app: flutter analyze 68 issues (same; +1 absorbed by -1 from now-referenced symbols); smoke test passes.

Notes / follow-ups

  • Dart part files + extension private access is the right pattern for splitting _AppShellState without inheritance. Subclassing State would force a constructor rewrite and break createState(); extensions avoid that entirely.
  • client.dart resists the same pattern because extensions can't satisfy interface members. A base-class split is the only path, and it reduces each file by ~200 LOC which isn't enough on its own for the biggest clients. Flagged in TODO.md.
  • Cross-app duplication across the three local_trash.dart files is ~250 LOC × 3 = 750 LOC of parallel code. A shared module in notechondria_shared would dedupe, but the extension targets _AppShellState which is private per-app. Would require either (a) exposing a small TrashHost mixin on the shared side that each _AppShellState implements, or (b) changing the extension to target a public interface. Not a blocker; revisit if the bin surface grows.