Adopt the four markdown namespaces and gate them
Some checks failed
file-length / file length <= 250 lines (push) Has been cancelled
md-naming / markdown naming convention (push) Has been cancelled
release-apk / release (APK) (push) Has been cancelled

Renames this repo's markdown onto README.md / CLAUDE*.md / DOCS*.md /
TODO*.md so a future session can tell a live task from a finished
record without reading the file to find out.

TODO files carry the literal "REMOVE ME AFTER FINISH" and are deleted
when the work lands. Enforced by the md-naming pre-commit hook and CI
workflow, both delegating to the shared gate in kuhyx/utils so this
repo's idea of a valid name cannot drift from every other repo's.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A47aX5DJwmA3QFn3pkEWi7
This commit is contained in:
Krzysztof Rudnicki 2026-08-24 15:54:35 +02:00
parent fca5deebe6
commit 59ee407d80
19 changed files with 258 additions and 321 deletions

38
.github/workflows/md-naming.yml vendored Normal file
View File

@ -0,0 +1,38 @@
name: md-naming
# The four markdown namespaces, enforced on push as well as at commit time.
# The local pre-commit hook can be skipped (a fresh clone that never ran
# `pre-commit install` has no hooks at all), so the convention needs a second
# gate that no one can forget to install.
#
# Deliberately a SEPARATE workflow from any release job: a badly named doc
# should fail lint, not block publishing an artifact.
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
jobs:
md-naming:
name: markdown naming convention
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# The gate and its exemption list live in the shared utils repo so every
# repo enforces the same namespaces. Checking it out beats vendoring a
# copy here, which is exactly how a gate and a survey start disagreeing.
- name: Check out the shared gate
uses: actions/checkout@v4
with:
repository: kuhyx/utils
path: .utils
- name: Enforce the naming convention
run: bash .utils/scripts/check_md_naming.sh --all
# Warns only: no machine can prove a task is finished, so an old TODO
# gets a nudge, never a failure.
- name: Warn about stale TODOs
run: PYTHONPATH=.utils python3 .utils/md_naming/stale.py

View File

@ -50,3 +50,13 @@ repos:
name: file length <= 250 lines name: file length <= 250 lines
entry: scripts/check_file_length.sh entry: scripts/check_file_length.sh
language: system language: system
# Four markdown namespaces (README / CLAUDE* / DOCS* / TODO*), plus the
# rule that a TODO carries "REMOVE ME AFTER FINISH" and nothing else may.
# Shared gate in ~/utils owns the namespaces and the exemption list, so
# one repo's idea of a valid name cannot drift from every other repo's.
- id: md-naming
name: markdown naming convention
entry: scripts/check_md_naming.sh
language: system
files: \.(md|markdown)$

172
AGENTS.md Normal file
View File

@ -0,0 +1,172 @@
# CLAUDE.md — todo
Offline-first, CRDT-synced notes app. Flutter, targets **Android + web**.
The **desktop app is the web build**, served by a small local wrapper
(`bin/todo_desktop.dart`) and shown in a Chrome `--app` window. Flutter's Linux
embedder was removed: it manages only ~20fps at 4K while the same Dart code in
Chrome sustains ~144fps. See `docs/DOCS-desktop-performance-findings.md` for the
measurements. Do not re-add `linux/` — `flutter create --platforms linux` would
silently bring the slow path back.
Capture an idea instantly (persisted on every keystroke), browse/filter notes,
and sync peer-to-peer through a GitHub repo used as dumb storage.
- Package name: `todo` (Dart SDK `^3.12.2`).
- Git remote: `origin` → `github.com/kuhyx/todo-app`.
- The note **content** also syncs to a separate private repo `kuhyx/syncs`
(under `todo-sync/`) via the in-app GitHub sync (changeset files); that is
data, not this codebase.
## Git workflow (repo-specific — overrides global rules)
- **Never open pull requests. Always commit and work directly on `main` and
`git push` to `origin/main`.** Do not create feature branches for normal work.
- End commit messages with the standard
`Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>` trailer.
## Commands
- Run tests + coverage: `flutter test --coverage` (suite runs in under a minute; keep it
quick — see Testing below).
- Coverage summary: `lcov --list coverage/lcov.info`.
- Static analysis: `flutter analyze` (must be clean before committing).
- Format: `dart format lib/ test/`.
- Run the desktop app: `./run.sh` (builds the web bundle, starts the wrapper,
opens Chrome). Mobile: `flutter run` with a connected Android device.
- The wrapper serves on a **fixed** port 8730 with a **fixed** Chrome profile
dir. Both must stay fixed: the GitHub token (`localStorage`) and the notes
(IndexedDB) are keyed by origin and live in that profile, so changing either
silently logs you out and hides your notes. Because the port cannot move,
`lib/desktop/port_guard.dart` decides what to do when it is already taken:
reap a wrapper with no window attached (so an upgrade can't leave you on old
code), join one that still has a window, and refuse to touch anything else.
- Release APK: `flutter build apk --release` (signs with the debug key; debug
builds are janky — always measure smoothness on a release build).
- Sync smoke test (hits real GitHub, needs a token): `dart run tool/sync_smoke.dart`.
## Architecture
Three layers under `lib/`, each single-purpose:
- `data/` — domain + local storage.
- `note.dart`: `Note` model; `Priority` enum (low/medium/high, **default
medium**, no "none"); `Status` enum (todo/inProgress/done/abandoned).
- `note_repository.dart`: CRDT storage over `sqlite_crdt`. Schema is at
**version 3** with `onCreate`/`onUpgrade` migrations (v1→v2 adds the
`status` column; v2→v3 backfills legacy priority `0`→medium). Also defines
`NoteFilter` (query / priorities / statuses / created+updated date ranges,
AND-combined), `NoteSort`, `watchNotes`/`watchCount` streams, and
`importNotes` (safe newer-wins merge by id).
- `sync/` — GitHub-as-storage sync.
- `sync_service.dart`: each device owns one file
`todo-sync/changesets/<nodeId>.json`
holding its full CRDT changeset. No two devices write the same file ⇒ no
git merge conflicts; convergence is the CRDT layer's job (pull every other
device's changeset and `merge()`, which is commutative + idempotent).
- `github_client.dart`: thin GitHub contents-API client (injectable
`http.Client`). `github_device_auth.dart`: OAuth device flow to mint a
token. `sync_settings.dart`: persisted owner/repo/token/clientId.
- `notes_markdown.dart`: round-trippable single-file export/import format
(HTML-comment `<!-- @note id=… priority=… status=… … -->` markers).
- `ui/` — screens, all take an injected `NoteRepository`.
- `capture_screen.dart`: landing screen; always-focused text box pre-filled
with the structured template; lazy note creation on first keystroke.
- `notes_list_screen.dart`: list + search + sort + filter sheet + per-note
sheet. **Default view hides Done/Abandoned and shows no filter badge**
(looks unfiltered).
- `settings_screen.dart`: GitHub connect/test, device-flow auth, and the
Export/Import backup actions.
- `main.dart`: bootstrap only (`// coverage:ignore-file`) — wires platform DB
paths and `runApp`.
### Note template (default content of every new note)
`lib/data/note_template.dart` is the **single source of truth** — the stepper,
the editor and the tests are all template-driven, so changing that list changes
every surface. A new note pre-fills a title line plus
`what / where / must / done / verify / read first`.
The set was cut from twelve sections to seven in 2026-07 after auditing 514 real
sessions (`docs/DOCS-llm-design-spec-audit.md`): `estimate`, `ask`, `nice`, `tech`,
`never` and `depends` were measurably never read, and `verify` was added because
"test it yourself, on the phone" was the single most repeated correction in the
corpus. Do not re-add a section without evidence that it gets used.
`NoteTemplate.retiredLabels` lists the dropped headings. `parse()` reports any
note containing one as **non-conforming** so it opens in the raw editor — without
that, the stepper would fold a retired section into its neighbour on the next
save. `tool/migrate_backlog.dart` converts an old export (Settings → Export →
migrate → Settings → Import).
## Testing
- **The suite must stay quick and fully green.** It is currently **313 tests at
100% line coverage** (2005 lines), running well under a minute on this
machine. Don't regress either.
(The "~5s" figure this file used to quote had drifted well before the port
guard was added; measure before quoting a new one.)
- Widget tests use `test/fake_note_repository.dart` — a `FakeNoteRepository`
built on `StreamController`s, **not** a real DB. A real `sqlite_crdt` DB
schedules timers that never drain under the widget tester's fake clock
("A Timer is still pending"); the fake avoids that.
- Pitfalls learned the hard way (keep following these):
- **Avoid `pumpAndSettle` when a widget animates forever** — an autofocused
`TextField`'s cursor blink never settles, and an open device-code dialog
keeps a pending poll timer. Use explicit `pump(Duration)` there.
`pumpAndSettle` is fine once nothing is animating (e.g. a route/dialog pop,
or a dialog already showing a static error).
- Inject fakes rather than touching the platform: `MockClient`
(`package:http/testing.dart`) for HTTP; the `file_selector` and
`url_launcher` **platform interfaces** (`MockPlatformInterfaceMixin`) for
the picker/launcher; stub `SystemChannels.platform` for clipboard.
- To exercise the configured sync path, pass a `MockClient` via
`CaptureScreen(httpClient: …)` / `SettingsScreen(httpClient: …)` — the real
`SyncService` then runs without network.
- `getChangeset()` serialises HLCs as `String`; `merge()` needs `Hlc.parse`
and fresh mutable maps (QueryRows are read-only) — see the changeset test.
- Use `// coverage:ignore-line` / `ignore-start`/`ignore-end` only for
genuinely unreachable code (e.g. a private static-only constructor, or a
mobile-only `Platform.isAndroid` branch that can't run on the Linux test
host), with a one-line reason.
## Conventions
- Run `dart format` + `flutter analyze` (clean) before every commit.
- Comments explain intent/trade-offs, not syntax.
- Keep the app buttery-smooth and low on CPU/RAM — it's a quick-capture tool.
- **Every file is at most 250 lines**, source and prose alike. Enforced by a
pre-commit hook (`scripts/check_file_length.sh`, delegating to the shared
gate in `~/utils`) and by `.github/workflows/file-length.yml`, so it fails
the commit rather than being a note anyone can ignore. Install the hook on
a fresh clone with `scripts/install_hooks.sh`.
- **Only non-mutating checks go in `.pre-commit-config.yaml`.** pre-commit
stashes unstaged changes and restores them with `git apply`; a hook that
rewrites a tracked file inside that window makes the restore conflict,
which aborts the commit *and drops the developer's uncommitted edit*. That
is why `bump_patch_version.sh` runs from the wrapper hook that
`install_hooks.sh` writes, after pre-commit has exited and only when it
passed — see the comments in both files. There is deliberately no
baseline and no allowlist — split the file instead. Exemptions (generated,
vendored, data files) live with the checker so the gate and the survey
cannot disagree.
- Where a split would otherwise break callers, keep the original path as a
barrel that re-exports the parts (`note_template.dart`,
`firebase_backend.dart`, `note_repository.dart` all do this). Where a
widget and its private `State` must stay one library, use `part` files
(`note_editor_*.dart`, `capture_screen_*.dart`).
## Flutter/Dart AI rules
Upstream's Flutter/Dart guidance used to be pasted in below this line, which
made 85% of this always-loaded file a document nobody edits. It now lives in
`docs/`, linked rather than inlined so the part loaded on every turn stays
about *this repo*:
- [Language, style and architecture](docs/DOCS-flutter-rules-language.md)
- [State, routing and data](docs/DOCS-flutter-rules-state-and-data.md)
- [Code generation, testing, theming and assets](docs/DOCS-flutter-rules-testing-and-assets.md)
- [UI: theming, layout and overlays](docs/DOCS-flutter-rules-ui.md)
- [Colour, type, documentation and accessibility](docs/DOCS-flutter-rules-design.md)
Sourced from [flutter/flutter docs/rules/rules.md](https://github.com/flutter/flutter/blob/main/docs/rules/rules.md)
via `~/.claude/CLAUDE.md`'s Flutter AI tooling setup. Re-fetch periodically.

172
CLAUDE.md
View File

@ -1,172 +0,0 @@
# CLAUDE.md — todo
Offline-first, CRDT-synced notes app. Flutter, targets **Android + web**.
The **desktop app is the web build**, served by a small local wrapper
(`bin/todo_desktop.dart`) and shown in a Chrome `--app` window. Flutter's Linux
embedder was removed: it manages only ~20fps at 4K while the same Dart code in
Chrome sustains ~144fps. See `docs/desktop-performance-findings.md` for the
measurements. Do not re-add `linux/` — `flutter create --platforms linux` would
silently bring the slow path back.
Capture an idea instantly (persisted on every keystroke), browse/filter notes,
and sync peer-to-peer through a GitHub repo used as dumb storage.
- Package name: `todo` (Dart SDK `^3.12.2`).
- Git remote: `origin` → `github.com/kuhyx/todo-app`.
- The note **content** also syncs to a separate private repo `kuhyx/syncs`
(under `todo-sync/`) via the in-app GitHub sync (changeset files); that is
data, not this codebase.
## Git workflow (repo-specific — overrides global rules)
- **Never open pull requests. Always commit and work directly on `main` and
`git push` to `origin/main`.** Do not create feature branches for normal work.
- End commit messages with the standard
`Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>` trailer.
## Commands
- Run tests + coverage: `flutter test --coverage` (suite runs in under a minute; keep it
quick — see Testing below).
- Coverage summary: `lcov --list coverage/lcov.info`.
- Static analysis: `flutter analyze` (must be clean before committing).
- Format: `dart format lib/ test/`.
- Run the desktop app: `./run.sh` (builds the web bundle, starts the wrapper,
opens Chrome). Mobile: `flutter run` with a connected Android device.
- The wrapper serves on a **fixed** port 8730 with a **fixed** Chrome profile
dir. Both must stay fixed: the GitHub token (`localStorage`) and the notes
(IndexedDB) are keyed by origin and live in that profile, so changing either
silently logs you out and hides your notes. Because the port cannot move,
`lib/desktop/port_guard.dart` decides what to do when it is already taken:
reap a wrapper with no window attached (so an upgrade can't leave you on old
code), join one that still has a window, and refuse to touch anything else.
- Release APK: `flutter build apk --release` (signs with the debug key; debug
builds are janky — always measure smoothness on a release build).
- Sync smoke test (hits real GitHub, needs a token): `dart run tool/sync_smoke.dart`.
## Architecture
Three layers under `lib/`, each single-purpose:
- `data/` — domain + local storage.
- `note.dart`: `Note` model; `Priority` enum (low/medium/high, **default
medium**, no "none"); `Status` enum (todo/inProgress/done/abandoned).
- `note_repository.dart`: CRDT storage over `sqlite_crdt`. Schema is at
**version 3** with `onCreate`/`onUpgrade` migrations (v1→v2 adds the
`status` column; v2→v3 backfills legacy priority `0`→medium). Also defines
`NoteFilter` (query / priorities / statuses / created+updated date ranges,
AND-combined), `NoteSort`, `watchNotes`/`watchCount` streams, and
`importNotes` (safe newer-wins merge by id).
- `sync/` — GitHub-as-storage sync.
- `sync_service.dart`: each device owns one file
`todo-sync/changesets/<nodeId>.json`
holding its full CRDT changeset. No two devices write the same file ⇒ no
git merge conflicts; convergence is the CRDT layer's job (pull every other
device's changeset and `merge()`, which is commutative + idempotent).
- `github_client.dart`: thin GitHub contents-API client (injectable
`http.Client`). `github_device_auth.dart`: OAuth device flow to mint a
token. `sync_settings.dart`: persisted owner/repo/token/clientId.
- `notes_markdown.dart`: round-trippable single-file export/import format
(HTML-comment `<!-- @note id=… priority=… status=… … -->` markers).
- `ui/` — screens, all take an injected `NoteRepository`.
- `capture_screen.dart`: landing screen; always-focused text box pre-filled
with the structured template; lazy note creation on first keystroke.
- `notes_list_screen.dart`: list + search + sort + filter sheet + per-note
sheet. **Default view hides Done/Abandoned and shows no filter badge**
(looks unfiltered).
- `settings_screen.dart`: GitHub connect/test, device-flow auth, and the
Export/Import backup actions.
- `main.dart`: bootstrap only (`// coverage:ignore-file`) — wires platform DB
paths and `runApp`.
### Note template (default content of every new note)
`lib/data/note_template.dart` is the **single source of truth** — the stepper,
the editor and the tests are all template-driven, so changing that list changes
every surface. A new note pre-fills a title line plus
`what / where / must / done / verify / read first`.
The set was cut from twelve sections to seven in 2026-07 after auditing 514 real
sessions (`docs/llm-design-spec-audit.md`): `estimate`, `ask`, `nice`, `tech`,
`never` and `depends` were measurably never read, and `verify` was added because
"test it yourself, on the phone" was the single most repeated correction in the
corpus. Do not re-add a section without evidence that it gets used.
`NoteTemplate.retiredLabels` lists the dropped headings. `parse()` reports any
note containing one as **non-conforming** so it opens in the raw editor — without
that, the stepper would fold a retired section into its neighbour on the next
save. `tool/migrate_backlog.dart` converts an old export (Settings → Export →
migrate → Settings → Import).
## Testing
- **The suite must stay quick and fully green.** It is currently **313 tests at
100% line coverage** (2005 lines), running well under a minute on this
machine. Don't regress either.
(The "~5s" figure this file used to quote had drifted well before the port
guard was added; measure before quoting a new one.)
- Widget tests use `test/fake_note_repository.dart` — a `FakeNoteRepository`
built on `StreamController`s, **not** a real DB. A real `sqlite_crdt` DB
schedules timers that never drain under the widget tester's fake clock
("A Timer is still pending"); the fake avoids that.
- Pitfalls learned the hard way (keep following these):
- **Avoid `pumpAndSettle` when a widget animates forever** — an autofocused
`TextField`'s cursor blink never settles, and an open device-code dialog
keeps a pending poll timer. Use explicit `pump(Duration)` there.
`pumpAndSettle` is fine once nothing is animating (e.g. a route/dialog pop,
or a dialog already showing a static error).
- Inject fakes rather than touching the platform: `MockClient`
(`package:http/testing.dart`) for HTTP; the `file_selector` and
`url_launcher` **platform interfaces** (`MockPlatformInterfaceMixin`) for
the picker/launcher; stub `SystemChannels.platform` for clipboard.
- To exercise the configured sync path, pass a `MockClient` via
`CaptureScreen(httpClient: …)` / `SettingsScreen(httpClient: …)` — the real
`SyncService` then runs without network.
- `getChangeset()` serialises HLCs as `String`; `merge()` needs `Hlc.parse`
and fresh mutable maps (QueryRows are read-only) — see the changeset test.
- Use `// coverage:ignore-line` / `ignore-start`/`ignore-end` only for
genuinely unreachable code (e.g. a private static-only constructor, or a
mobile-only `Platform.isAndroid` branch that can't run on the Linux test
host), with a one-line reason.
## Conventions
- Run `dart format` + `flutter analyze` (clean) before every commit.
- Comments explain intent/trade-offs, not syntax.
- Keep the app buttery-smooth and low on CPU/RAM — it's a quick-capture tool.
- **Every file is at most 250 lines**, source and prose alike. Enforced by a
pre-commit hook (`scripts/check_file_length.sh`, delegating to the shared
gate in `~/utils`) and by `.github/workflows/file-length.yml`, so it fails
the commit rather than being a note anyone can ignore. Install the hook on
a fresh clone with `scripts/install_hooks.sh`.
- **Only non-mutating checks go in `.pre-commit-config.yaml`.** pre-commit
stashes unstaged changes and restores them with `git apply`; a hook that
rewrites a tracked file inside that window makes the restore conflict,
which aborts the commit *and drops the developer's uncommitted edit*. That
is why `bump_patch_version.sh` runs from the wrapper hook that
`install_hooks.sh` writes, after pre-commit has exited and only when it
passed — see the comments in both files. There is deliberately no
baseline and no allowlist — split the file instead. Exemptions (generated,
vendored, data files) live with the checker so the gate and the survey
cannot disagree.
- Where a split would otherwise break callers, keep the original path as a
barrel that re-exports the parts (`note_template.dart`,
`firebase_backend.dart`, `note_repository.dart` all do this). Where a
widget and its private `State` must stay one library, use `part` files
(`note_editor_*.dart`, `capture_screen_*.dart`).
## Flutter/Dart AI rules
Upstream's Flutter/Dart guidance used to be pasted in below this line, which
made 85% of this always-loaded file a document nobody edits. It now lives in
`docs/`, linked rather than inlined so the part loaded on every turn stays
about *this repo*:
- [Language, style and architecture](docs/flutter-rules-language.md)
- [State, routing and data](docs/flutter-rules-state-and-data.md)
- [Code generation, testing, theming and assets](docs/flutter-rules-testing-and-assets.md)
- [UI: theming, layout and overlays](docs/flutter-rules-ui.md)
- [Colour, type, documentation and accessibility](docs/flutter-rules-design.md)
Sourced from [flutter/flutter docs/rules/rules.md](https://github.com/flutter/flutter/blob/main/docs/rules/rules.md)
via `~/.claude/CLAUDE.md`'s Flutter AI tooling setup. Re-fetch periodically.

1
CLAUDE.md Symbolic link
View File

@ -0,0 +1 @@
AGENTS.md

View File

@ -1,44 +0,0 @@
# Migration: `sqlite_crdt` → shared `crdt_sync`
todo's note storage and sync were moved off `sqlite_crdt` onto the shared
[`crdt_sync`](https://github.com/kuhyx/utils/tree/main/crdt_sync_dart) library
(pinned tag `crdt_sync_dart-v0.3.0`), so every companion app now syncs through
one library.
## What changed
- **Storage:** `NoteRepository` is backed by `crdt_sync`'s `LogStore` (a
`Log = Map<String, Record>` persisted as JSON). Each `Note` is stored *as* a
`Record` (per-field last-writer-wins + a sticky tombstone), so the on-disk
log **is** the sync wire format — no separate adapter. Filtering/sorting run
in Dart over the in-memory log.
- **Sync:** `SyncService` pushes each device's `Log` to
`todo-sync/notes/<nodeId>.json` in `kuhyx/syncs` (new dir; the old
`changesets/` format is incompatible, so it's a clean cutover). It uses
`crdt_sync`'s `GitHubClient` + `logToJson`/`logFromJson`.
- **Client:** todo's local `lib/sync/github_client.dart` was deleted; it now
uses `crdt_sync`'s `GitHubClient` (which gained `canAccessRepo()` +
`deleteFile()` in v0.3.0). `putFileText` self-resolves the file sha, so the
old SHA-tracking is gone.
## The one-time on-device migration
`NoteRepository.open()` migrates a pre-existing `sqlite_crdt` DB into the log on
first launch, guarded by a persisted `crdt.migratedFromSqlite` flag. Verified
on-device: a real 27-note backlog (+5 tombstones) migrated intact.
`sqlite_crdt` is **retained as a dependency** only to read that legacy DB during
migration — the old `todo.db` is left untouched as a safety net. Remove the dep
in a later cleanup once every device is known-migrated.
## Surprises / non-obvious decisions
- **Seed HLCs from each note's `updatedAt`, not "now".** Two devices migrate
independently; stamping migration-time "now" would let per-field LWW resolve
a pre-cutover edit arbitrarily. Seeding from `updatedAt` keeps
last-real-edit-wins across the cutover.
- **Reuse the legacy `sqlite_crdt` node id** (persisted in prefs) so a device
keeps its `devices/<id>` identity and devices still converge.
- **`watchNotes`/`watchCount` seed in `onListen`**, not lazily in an `async*`
body — otherwise a write between `listen()` and the first generator tick
swallows the initial snapshot.

View File

@ -87,3 +87,5 @@ per-file values have no source of truth at all).
- The single hardcoded-color violation (`Colors.red`) is also the app's only - The single hardcoded-color violation (`Colors.red`) is also the app's only
spot where a raw `Colors.*` constant appears outside `main.dart`'s seed — spot where a raw `Colors.*` constant appears outside `main.dart`'s seed —
fixing it removes the last non-theme-derived color reference in the app. fixing it removes the last non-theme-derived color reference in the app.
REMOVE ME AFTER FINISH

View File

@ -216,3 +216,5 @@ can go stale independently — but this prompt's done-condition is the phone.
- Settings sync cross-device last-writer-wins through Firebase - Settings sync cross-device last-writer-wins through Firebase
(`settings/advancedMode`). If you add a synced preference, follow (`settings/advancedMode`). If you add a synced preference, follow
`reconcileWithRemote`/`adopt`; if local-only, skip that path entirely. `reconcileWithRemote`/`adopt`; if local-only, skip that path entirely.
REMOVE ME AFTER FINISH

View File

@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts # In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix. # of the product and file versions while build-number is used as the build suffix.
version: 1.0.26+1 version: 1.0.27+1
environment: environment:
sdk: ^3.12.2 sdk: ^3.12.2

View File

@ -1,104 +0,0 @@
# Refactor TODO — enforce the 250-line file cap
> **DONE for this repo (2026-08-14).** Every file is at most 250 lines and
> `bash ~/utils/scripts/check_file_length.sh --all` exits 0. The gate is wired
> into `.pre-commit-config.yaml` and `.github/workflows/file-length.yml`, and
> was verified from a fresh checkout: `scripts/install_hooks.sh` installs it,
> and a staged 260-line file then makes `git commit` fail. 313 tests pass at
> 100% coverage (up from 99.9%). The counts below are the pre-refactor survey,
> kept as the record of what was done — re-run the survey before reusing this
> file here. It is still a valid prompt for **other** repos.
> **This file is a ready-to-use prompt.** Paste it to Claude, or open this repo
> and say "do refactor_claude_todo". It is self-contained: everything needed to
> execute is below. Generated 2026-08-14 from a measured survey of every repo.
## Goal
Every file in this repo must be **at most 250 lines** — source, tests, and
prose (`.md`/`.txt`/`.rst`/`.tex`) alike — and must **stay** that way forever,
enforced by a gate that fails the commit, not by a note anyone can ignore.
Why: a file that cannot be read in one piece forces re-reads and partial edits,
which is the single largest avoidable cost in an LLM-assisted workflow. Aim by
churn, not size alone — refactoring pays where code is read and changed often
(Fowler, *refactoring economic benefit*).
## Scope in this repo
- **19 files** currently exceed 250 lines (of 115 eligible files).
- **8,691 lines** sit in violation; longest file is **925 lines**.
Exempt (do NOT split these):
- generated files — `*.g.dart`, `*.freezed.dart`, `*.gr.dart`, `**/l10n/generated/**`,
anything with a `GENERATED` header
- markup — `.html`, `.css`, `.scss`
- data files — `.json`, `.yaml`, `.csv`, wordlists and other data-ish `.txt`
(mean line length under 25 chars)
## Violations, highest ROI first
ROI = lines x commits in the last year. Work top-down; a long file nobody edits
has near-zero payoff and should not be first.
| lines | commits/yr | kind | file |
|------:|-----------:|:-----|:-----|
| 739 | 19 | code | `lib/ui/capture_screen.dart` |
| 660 | 17 | code | `test/capture_screen_test.dart` |
| 702 | 10 | code | `lib/ui/note_editor.dart` |
| 677 | 9 | code | `test/note_repository_test.dart` |
| 925 | 6 | prose | `CLAUDE.md` |
| 391 | 13 | code | `test/settings_screen_test.dart` |
| 429 | 8 | code | `lib/data/note_repository.dart` |
| 502 | 6 | code | `test/note_editor_test.dart` |
| 356 | 7 | code | `test/sync_service_test.dart` |
| 402 | 5 | code | `test/wrapper_server_test.dart` |
| 256 | 7 | code | `lib/ui/notes_list_screen.dart` |
| 253 | 7 | code | `bin/todo_desktop.dart` |
| 347 | 4 | code | `test/notes_list_screen_test.dart` |
| 336 | 4 | code | `lib/desktop/wrapper_server.dart` |
| 405 | 1 | code | `test/port_guard_test.dart` |
_(4 further files over 250 lines not listed — re-run the survey for the full set.)_
## How to split
- **Python** — extract cohesive helpers into sibling modules; keep the public
API and imports stable.
- **Shell** — split into `lib/*.sh` sourced by a thin entry script. Keep
`set -euo pipefail` in each.
- **Dart / TypeScript** — extract widgets/components into their own files.
- **Tests** — split by test-group into sibling files
(`foo.test.ts` -> `foo.parsing.test.ts`, `foo.render.test.ts`). Coverage must
not drop.
- **Docs** — split into topic files under `docs/` with an index. For an
oversized `CLAUDE.md`, move detail into referenced docs so the
always-loaded part shrinks.
**Do not** game the cap: no one-lining, no deleting tests, no moving code into
an exempt extension, no `# noqa`-style suppressions.
## Make it permanent (required — this is the point)
A refactor without a gate silently regrows. Before this task is done:
1. Wire the shared gate `~/utils/scripts/check_file_length.sh` into this repo's
`.pre-commit-config.yaml` as a local hook. If the repo has no pre-commit
config, add a minimal one.
2. The hook checks **files in the commit** (not the whole tree), so unrelated
commits never break, and it **fails** — exit 1, not a warning.
3. No baseline file and no allowlist. Those are suppressions.
4. If this repo has CI (`.github/workflows`), add the same check there so it
also fails on push.
## Done condition
- `bash ~/utils/scripts/check_file_length.sh --all` from this repo root exits 0.
- The repo's own test suite and coverage bar are still green.
- `pre-commit run --files <changed files>` passes.
- A deliberately over-250-line test file, staged, makes `git commit` **fail**.
- For a deployed daemon/app: the entry point still actually runs.
## Verify
Mobile is the primary platform: verify with an APK on the phone via the `phone-deploy` skill, not on desktop.

32
scripts/check_md_naming.sh Executable file
View File

@ -0,0 +1,32 @@
#!/bin/bash
# ============================================================================
# Fail if any markdown file breaks the shared naming convention.
#
# Thin delegate to the shared gate in ~/utils, which owns the four namespaces
# (README / CLAUDE* / DOCS* / TODO*), the "REMOVE ME AFTER FINISH" marker rule
# and the exemption list. Copying that logic here is what lets one repo's idea
# of a valid name drift from every other repo's -- so this script only locates
# the shared checker and forwards its arguments.
#
# Usage:
# scripts/check_md_naming.sh <file> [<file> ...] # pre-commit passes these
# scripts/check_md_naming.sh --all # whole tree, from cwd
# ============================================================================
set -euo pipefail
readonly SHARED_GATE="${UTILS_ROOT:-$HOME/utils}/scripts/check_md_naming.sh"
main() {
if [[ ! -x "$SHARED_GATE" ]]; then
echo "Error: shared markdown-naming gate not found at $SHARED_GATE" >&2
echo " Clone github.com/kuhyx/utils to ~/utils, or set" >&2
echo " UTILS_ROOT to where it lives." >&2
exit 1
fi
exec bash "$SHARED_GATE" "$@"
}
main "$@"