mirror of
https://github.com/kuhyx/todo-app.git
synced 2026-11-10 19:27:01 +01:00
Adopt the four markdown namespaces and gate them
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:
parent
fca5deebe6
commit
59ee407d80
38
.github/workflows/md-naming.yml
vendored
Normal file
38
.github/workflows/md-naming.yml
vendored
Normal 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
|
||||
@ -50,3 +50,13 @@ repos:
|
||||
name: file length <= 250 lines
|
||||
entry: scripts/check_file_length.sh
|
||||
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
172
AGENTS.md
Normal 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
172
CLAUDE.md
@ -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.
|
||||
44
MIGRATION.md
44
MIGRATION.md
@ -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.
|
||||
@ -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
|
||||
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.
|
||||
|
||||
REMOVE ME AFTER FINISH
|
||||
@ -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/advancedMode`). If you add a synced preference, follow
|
||||
`reconcileWithRemote`/`adopt`; if local-only, skip that path entirely.
|
||||
|
||||
REMOVE ME AFTER FINISH
|
||||
@ -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
|
||||
# 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.
|
||||
version: 1.0.26+1
|
||||
version: 1.0.27+1
|
||||
|
||||
environment:
|
||||
sdk: ^3.12.2
|
||||
|
||||
@ -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
32
scripts/check_md_naming.sh
Executable 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 "$@"
|
||||
Loading…
Reference in New Issue
Block a user