Commit Graph

15 Commits

Author SHA1 Message Date
a874e09b65 Cut the llm-design-spec template from 12 sections to 7
Audited the template against 514 real Claude sessions (1503 top-level
prompts) joined to the 49 notes in BACKLOG.md. Sections were judged on
whether the agent actually read them, not on fill rate — four of them
said "leave blank if none" in their own helper, so a low fill rate was
the section working correctly.

Measured over the 22 note/session pairs:
  estimate  filled 22, referenced  1 (4%)
  done      filled 22, quoted back 4 (18%)
  refs      filled 14, URLs fetched 5 (35%)
  tech      the agent opened pubspec.yaml/CLAUDE.md anyway in 54%
  ask       100% ask rate with it, 92% without — no measurable effect

Kept: title, what, where, must, done. Dropped: tech (into `where`),
ask, nice and never (into `must` as `optional:` / `must not:` lines),
depends, estimate. Renamed `refs` -> `read first`. Added `verify`, the
only addition: "test it yourself, on the phone" was the single most
repeated correction in the corpus, and 3 of the 7 filled `never`
sections were already smuggling it in.

Fixes a latent data-loss bug found while testing this. parse() treats
an unknown `## heading` as content on purpose (values may contain
subheadings), so a legacy `## tech` block was folded into the preceding
section's value AND the note still reported conforming — opening any
old note in the stepper would have swallowed it on the next save.
NoteTemplate.retiredLabels now makes those notes explicitly
non-conforming, so they open raw and untouched.

tool/migrate_backlog.dart converts an old export (export -> migrate ->
import). It is idempotent and gated: it throws if a rewritten note does
not conform or if any source line vanishes. All 49 notes migrated on
the desktop and merged to the phone (0 new, 25 updated, 24 unchanged).

Also formats the few files dart format had never been run over, and
extracts a local in frame_stats_io.dart so the reflowed line stays
under 80 chars.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-26 22:03:35 +02:00
c37a9ef118 Add the desktop wrapper, and stop the test suite eating BACKLOG.md
The desktop app is a browser now, so it cannot write files. This adds the other
half: a loopback HTTP server that serves the web build and owns the two files
the browser cannot touch -- `~/todo/BACKLOG.md`, which the user's tooling and
the todo MCP server read, and a disk copy of the note log so a wiped Chrome
profile is not a total-loss event for unsynced notes.

Notable details:

- Binds to 127.0.0.1 only. The endpoints overwrite files in the user's home
  with no authentication, so a routable bind would let anything on the network
  rewrite the backlog.
- Serves explicit content types. CanvasKit refuses a `.wasm` labelled anything
  else and the app then renders nothing at all.
- Mirroring to disk is debounced. The store persists on every keystroke, so
  mirroring eagerly meant an HTTP request plus a whole-log write per character;
  IndexedDB still gets every keystroke, only the durability copy waits.

Separately, this fixes a pre-existing destructive bug found while testing:
`settings_screen_test` did real file I/O and overwrote the user's canonical
`~/todo/BACKLOG.md` with a single fake note on *every* `flutter test` run. It
went unnoticed because the app rewrites the file whenever it launches. The
export destination is now injectable and the test redirects it to a temp dir;
verified that a full suite run leaves the real file byte-identical.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 13:33:25 +02:00
66143bdfe3 Make the app compile and run for web, behind platform seams
First step of moving the desktop app off Flutter's Linux embedder, which tops
out at ~20fps at 4K (see docs/desktop-performance-findings.md). Android is
unaffected and keeps every existing code path.

The blocker was that `dart:io` was imported by note_repository, capture_screen
and settings_screen. On web it compiles to a stub and then throws at runtime --
the first web build rendered a blank white window. So each `dart:io` use now
sits behind a conditional export, with the shared logic hoisted into pure Dart:

- note_repository loses `dart:io`/sqlite entirely. Platform wiring moves to
  repository_factory{,_io,_web}, and the legacy sqlite import moves to its own
  io-only library. The HLC-from-updated_at seeding is preserved verbatim, so
  independently-migrating devices still resolve pre-cutover edits by real edit
  time.
- Notes persist to IndexedDB on web, not localStorage: the log is the primary
  copy of the backlog and localStorage caps at ~5-10MB and is evicted eagerly.
- BACKLOG.md and the explicit Export keep working through a local wrapper
  (added next). A browser cannot write `~/todo/BACKLOG.md`, and the user's
  tooling plus the todo MCP server read that path, so leaving it to rot was not
  an option. Writes are best-effort so the app still works with no wrapper.
- Notes are also mirrored to the wrapper's on-disk copy, so a wiped Chrome
  profile is not a total-loss event for notes that have not reached GitHub yet.

`openRepository` is split so `openRepositoryIn` keeps the migration tests; the
suite stays at 174 green and 100% coverage.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 13:14:03 +02:00
f463e50d67 Switch to very_good_analysis, matching the rest of the Flutter ecosystem
Standardizes on the same stricter lint set + shared analysis_options.yaml
used by diet-guard/app and the new habit_stack app. Fixes all 127 issues
surfaced: dart fix --apply for imports/cascades/sorting, unawaited() on
fire-and-forget Futures, bare catch(e) narrowed to on Exception catch(e)
(matching diet_guard's existing pattern), one documented ignore for a
deliberate TypeError catch around untrusted peer JSON, and missing
public-member doc comments throughout. No behavior change: 174/174 tests
pass, 100% line coverage maintained.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-20 11:05:13 +02:00
7564a6569f Surface sync outcomes so devices can't silently diverge
The desktop and phone split-brained for 5 days (39 vs 30 notes) after the
crdt_sync cutover because one device kept running the pre-migration build
— and nothing in the UI ever said a sync had failed or skipped a peer:

- SyncResult now reports peer files that were listed but couldn't be
  fetched/decoded (skippedFiles) instead of dropping them silently.
- Auto-sync no longer swallows errors: every sync (manual or automatic)
  records a persisted last-sync status shown in a status line under the
  editor, with failures in the error color.
- Desktop actually background-syncs now: focus loss (inactive/hidden,
  debounced 5s) triggers a sync — Flutter Linux rarely delivers the
  `paused` event the old code relied on. `paused` still syncs immediately
  for Android.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K5C5jZ2c4mTaGedK7C1dDv
2026-07-16 17:58:41 +02:00
253088c4ed Use crdt_sync's shared GitHubClient; drop todo's local copy
crdt_sync v0.3.0 now provides canAccessRepo() + deleteFile(), so todo syncs
through the one shared client like the other apps instead of its own copy.
SyncService loses its SHA-tracking (crdt_sync's putFileText self-resolves the
sha); lib/sync/github_client.dart + its test are deleted; capture/settings/
sync_smoke repoint to the shared client. flutter analyze clean, 169 tests at
100% coverage, device-verified (27-note backlog intact, no crash).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019KwUCPESvE5CjCtmUFz8xQ
2026-07-11 23:53:47 +02:00
b9d9ac6521 Migrate notes storage from sqlite_crdt to the shared crdt_sync LogStore
NoteRepository is rewritten on crdt_sync's LogStore: each Note is stored *as*
a Record (per-field LWW + sticky tombstone), so the on-disk log IS the sync
wire format. Its public API is unchanged (upsert/delete/listNotes/watchNotes/
watchCount/importNotes/changes/nodeId), so the UI and most tests are
untouched; only the internals, the two sync methods (getChangeset/merge ->
exportLog/importLog over a Log), and the changeset-specific tests change.
Filtering/sorting now run in Dart over the in-memory log; watchNotes/watchCount
seed synchronously in onListen to avoid a subscription race.

SyncService moves from per-device sqlite changesets to per-device crdt_sync
Logs under a new todo-sync/notes/ path (clean format cutover; old clients
can't misread a new file). todo's own GitHubClient transport is kept.

One-time migration in NoteRepository.open: reads the legacy sqlite DB (kept
untouched as a safety net; the dep stays only for this), seeds every note --
tombstones included -- into the log with each field's HLC derived from its
updated_at (so a pre-cutover edit still wins by real edit time across two
independently-migrating devices), reuses the legacy node id, and sets a
persisted "migrated" flag so it never re-runs.

Verified: flutter analyze + dart format clean; 178 tests at 100% coverage
(incl. migration, tombstone, restore-race, and corrupt-peer cases). Device
verified on dev.kuhy.todo: real backlog of 27 live notes (+5 tombstones)
migrated intact -- confirmed against a pulled sqlite backup, the pulled
LogStore JSON, and the app's own "27 saved" -- notes render with status/
priority/dates, no crash, migration one-time.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019KwUCPESvE5CjCtmUFz8xQ
2026-07-11 23:28:54 +02:00
01411810da Cut per-keystroke O(N) work to keep capture smooth at scale
The capture screen fed the on-disk BACKLOG.md backup off an always-live
watchNotes() subscription, so every keystroke re-queried the whole table,
re-parsed every row, and rebuilt the full Markdown export on the UI
isolate — O(notes) per character. It also fired a whole-screen setState
just to update the "Saved locally" timestamp.

- Add NoteRepository.changes: an O(1) broadcast tick after each write
  (upsert/delete/merge; importNotes transitively). LocalBackup now pulls
  notes via an injected fetcher only when its 2s debounce fires, so the
  query + parse + Markdown export runs at most once per idle window
  instead of on every keystroke. Capture listens to changes, not
  watchNotes; a launch-time schedule() keeps BACKLOG.md fresh.
- Scope the save indicator to a ValueNotifier/ValueListenableBuilder so a
  keystroke updates one line of text, not the AppBar/dropdowns/editor.
- Key list tiles by note id for identity-based reorder diffing.

Measured on a 4K/144Hz desktop the UI-thread build stays <=3.3ms;
coverage held at 100% on all changed files.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 21:51:46 +02:00
9edf603a24 Point notes sync at the consolidated syncs monorepo
todo-sync, diet-guard-sync, and screen-locker-sync were folded into
one private github.com/kuhyx/syncs monorepo, each keeping its own
subdirectory as the Contents-API path prefix. Update the default
owner/repo, the changeset path prefix, and add a persisted-value
migration so an already-installed device that saved the old
'todo-sync' repo name picks up 'syncs' on next load.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-10 14:04:05 +02:00
9841aecd32 Store the GitHub sync token in the OS keystore, migrating off plaintext
The token moved from plaintext SharedPreferences into flutter_secure_storage
(Android Keystore / libsecret). Only the non-secret owner/repo/clientId stay in
prefs. Migration is confirm-before-delete: load() reads the keystore first and
falls back to the legacy plaintext token, migrating it only once a secure write
succeeds; save() likewise keeps writing plaintext if no secret service is
available, so we degrade to — never below — the old behaviour.

157 tests, 100% line coverage, analyze clean. Verified on-device: Settings
stays connected and sync succeeds after the one-time migration.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 22:57:05 +02:00
f5d79a6a57 One-tap GitHub connect via a baked-in OAuth App client id
Previously "Connect GitHub" (OAuth device flow) still required entering an
OAuth App client id and owner/repo — friction that returned on every
reinstall once shared_prefs were wiped.

- Bake the app's own device-flow OAuth App client id in as
  SyncSettings.defaultClientId and default to it in load() (alongside the
  existing kuhyx/todo-sync repo default). A device-flow client id is a
  public identifier, not a secret, so it is safe to commit.
- Settings now leads with a single "Connect GitHub" button; the manual
  client-id / token fields and Test connection move under an "Advanced"
  expander. Result: fresh install (or post-reinstall) is one tap →
  authorize the code in the browser → synced. No tokens, no setup.

Note: an OAuth App authorizes with the classic `repo` scope (all repos),
broader than the prior fine-grained PAT — the trade-off for one-tap
device-flow convenience. 151 tests, 100% line coverage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 22:21:34 +02:00
6db9ee11d0 Auto-export a local Markdown backup and recover from it on launch
Third durability layer beside GitHub auto-sync and Android Auto Backup:
a plain, human/LLM-readable Markdown file kept current on local disk.

- LocalBackup (lib/sync): pure, injectable file IO. scheduleExport()
  debounces writes (a burst of keystrokes → one export); recover() parses
  the file back into notes. Reused NotesMarkdown serializer.
- CaptureScreen wires it: on launch, recover into an *empty* DB only (so a
  stale backup never clobbers existing notes), then keep the backup current
  as notes change. Platform path = ~/todo/BACKLOG.md on desktop (the path
  the user's workflow already reads) or the app documents dir on mobile
  (covered by Android Auto Backup). File IO is injected in tests.
- Added fake_async dev dep to unit-test the debounce with a virtual clock.

151 tests, 100% line coverage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 22:11:08 +02:00
6947757ba0 Reach 100% test coverage (capture-screen sync DI + plugin/clipboard fakes)
- Inject an optional http.Client into CaptureScreen (mirroring SettingsScreen)
  so the configured sync path runs against a MockClient instead of the network;
  capture_screen.dart now 100%.
- Mock the file_selector and url_launcher platform interfaces and the clipboard
  channel so the import flow, _openPage launch, and the device-code dialog's
  error/Cancel/Open paths are exercised deterministically (no hangs, no timers).
- Add unit tests for the remaining fallbacks/defaults: copyWith no-arg paths,
  GitHubApiException.toString, default-constructed clients, empty NoteFilter,
  the v1->v2 status-column migration, and the export/import error branches.
- coverage:ignore the private static-only NotesMarkdown ctor.

101 tests, all green in ~5.5s. Line coverage 96.2% -> 100%.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 17:11:01 +02:00
7f84414c87 Add list filters/sort, status, priority rework, export/import, structured template
Notes list & filtering:
- Text-search filter plus independent date-range filters for both created
  and last-updated (AND-combined), a priority filter, and a new status
  filter. Default view hides Done/Abandoned and renders as "unfiltered"
  (no badge for the default state); fixed badge clipping.
- NoteSort options wired into the list UI; watchCount() for the "N saved".

Status & priority:
- New Status enum (toDo/inProgress/Done/Abandoned) as a settable + filterable
  attribute on every note, with capture-screen dropdown.
- Removed "None" priority: every note is Low/Medium/High, default Medium.
  Schema migration v2->v3 rewrites legacy priority 0 -> Medium.

Export / import:
- NotesMarkdown round-trippable single-file format with HTML-comment markers.
- Settings "Export notes" (mobile share sheet / desktop writes ~/todo/BACKLOG.md)
  and "Import notes" (file picker + safe newer-wins merge by id).

Structured template:
- Every new note pre-fills the richer what/where/must/nice/out/done/depends/
  estimate/refs scaffold.

Tests:
- New fast (~5s), deterministic suite via FakeNoteRepository (no DB timers) and
  injected http/file-selector/url-launcher fakes. 86 tests, 96.2% line coverage
  (note.dart & sync_service.dart at 100%, settings 98.7%). Mobile-only share
  branch excluded via coverage:ignore (unreachable on the Linux test host).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 16:52:59 +02:00
d48bcd24f7 Initial commit: offline-first CRDT notes app (capture + GitHub sync)
Flutter app for Android + Linux desktop. Captures ideas with per-keystroke local autosave to a CRDT-backed SQLite store (sqlite_crdt), and syncs through a private GitHub repo using per-device changeset files (conflict-free last-writer-wins merge). Includes GitHub OAuth device-flow sign-in with PAT fallback, a barebones notes list, and sync settings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 14:20:19 +02:00