Commit Graph

8 Commits

Author SHA1 Message Date
9d638fb919 Seed a Firebase session for todo as well
todo used to be grouped with the Flutter-only apps that authenticate in-app
on the phone and therefore need no desktop session. That stopped being true
when it grew a desktop wrapper (lib/desktop/wrapper_server.dart) with its own
Firebase REST client, which needs a seeded session exactly like the Python
daemons do.

Both files the new comment cites were confirmed present today. The credential
cache at ~/.config/todo/firebase_auth.json has the same shape as every other
entry -- FirebaseCredentials.fromJson in crdt_sync_dart parses the same
{id_token, refresh_token, expires_at} that credential_store_for writes -- so
the only thing that was missing is the DEFAULT_APPS entry.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
2026-08-22 16:17:06 +02:00
4c734c1cea crdt-sync: clear the ruff 0.15.2 findings CI reported
My local pre-commit cache holds three ruff versions and I had been resolving
`ls ... | head -1`, which picks 0.14.5 — CI pins 0.15.2, and the newer version
flags eight things the older one does not (unused imports left behind in
_syncargs.py and tool/_oauth_callback.py, import ordering, an empty
TYPE_CHECKING block). Re-ran against the pinned binary; `ruff check` is now
"All checks passed!" under the same version CI uses.

259 tests at 100% branch coverage, `pre-commit run --all-files` clean for this
config, and `python3 -m tool.google_id_token --help` still runs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
2026-08-22 15:56:08 +02:00
9f271b0d37 Fix the cap hook in CI, and the re-exports its --all-files run exposed
The hook entry was an absolute /home/kuhy/utils path, copied from a config
written for ~/.claude where it only ever ran locally. On an Actions runner
that is exit code 127 — "No such file or directory" — so gatelock-precommit
and crdt-sync-precommit failed on every commit of this refactor while the
hook silently checked nothing. Now repo-relative, matching the sibling hooks
(`entry: scripts/check_subproject_scope.sh`): pre-commit always runs from the
git root, so it resolves the same in both places.

Fixing that made the CI configs runnable locally, and `--all-files` then found
what per-file runs had missed — ruff pruning imports that exist *for* their
re-export. `import crdt_sync` was broken three separate ways (FileSyncStateStore,
encode_key, FileCredentialStore). Each affected module now states an explicit
__all__ so the autofixer cannot take them again, and the two files that pushed
past the cap once __all__ was added are split further:

  _firebase.py 260 -> 247 + _fberrors 25
  _sync.py     257 -> 188 + _syncargs 91

Test patch targets followed the code they patch: gatelock._outputs.shutil ->
_xrandr.shutil, gatelock._arbiter.Path -> _claims.Path.

gatelock and crdt-sync both pass `pre-commit run --all-files` in full;
389 and 259 tests at 100% branch coverage; the root gate exits 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
2026-08-22 15:52:59 +02:00
3589d166cd crdt-sync: split five source files — package now fully under the cap
Some checks are pending
crdt-sync pre-commit / pre-commit (push) Waiting to run
crdt-sync tests / test (3.10) (push) Waiting to run
crdt-sync tests / test (3.11) (push) Waiting to run
crdt-sync tests / test (3.12) (push) Waiting to run
gatelock pre-commit / pre-commit (push) Waiting to run
gatelock tests / test (3.10) (push) Waiting to run
gatelock tests / test (3.11) (push) Waiting to run
gatelock tests / test (3.12) (push) Waiting to run
palette drift / palette (push) Waiting to run
_sync.py         456 -> 245 + _syncstate 131 + _pull 106 + _revisions 34
  _firebase_auth   412 -> 242 + _credentials 146 + _autherrors 69
  _firebase.py     336 -> 250 + _fbkeys 100
  google_id_token  316 -> 232 + tool/_oauth_callback 105
  _github.py       255 -> 238 + _gherrors 30

revision_of became its own leaf module because both the push and pull halves
need it; without that the two would have imported each other. _fbkeys holds
the RTDB key escaping — the escape tables moved with the functions that read
them, since the pair has to round-trip exactly or a device silently stops
seeing its own writes.

crdt-sync/tool/ is outside the coverage source, so the split there has no test
net: verified by running it the documented way instead
(`python3 -m tool.google_id_token --help` prints its usage, and
`from tool.google_id_token import TokenError, fetch_id_token` — what
seed_session.py does — still resolves).

An AST pass over each pair caught what the suite would not have: _is_revoked
needs the error type, so it stayed with it rather than following the
credentials; _TERMINAL_AUTH_REASONS and _CONSENT_TIMEOUT_SECONDS had to be
carried to their new callers.

259 passed at 100% branch coverage. crdt-sync/tool/seed_session.py and
firebase-debug.log are pre-existing local changes and are deliberately not in
this commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
2026-08-22 15:30:52 +02:00
2005c40baa Group sync_log's 11 arguments into three bundles
Some checks are pending
crdt-sync pre-commit / pre-commit (push) Waiting to run
crdt-sync tests / test (3.10) (push) Waiting to run
crdt-sync tests / test (3.11) (push) Waiting to run
crdt-sync tests / test (3.12) (push) Waiting to run
palette drift / palette (push) Waiting to run
BREAKING: sync_log's flat keyword arguments are replaced by SyncTarget,
LogCodec and RevisionTracking. Callers change from eleven loose keywords to
four positional arguments; every consumer repo is updated in the same pass.

Eleven parameters on one function was the last real pylint finding in the
package, and the grouping is the fix rather than a workaround: the three
bundles are the three things a tick actually needs -- where to sync, how to
serialize, and whether to track revisions -- and each documents its own
fields. SyncTarget.own_ids also moves the "this device plus its pre-migration
id" rule onto the type that owns it, instead of recomputing it inline.

Internals follow: _pull_remote_logs takes a _PullContext instead of eight
positionals, and the push half splits into _merge_peers/_push, which is what
clears the 21-locals finding.

The two duplicated in-memory fakes merge into one FakeStore in conftest, as
does the no-bulk-read wrapper around it -- the shared half was the fiddly
prefix slicing, which is exactly where two copies drift.

crdt_sync now scores a clean 10.00/10 on pylint. Two suppressions remain,
both scoped and explained at the line: asserting on a private attribute IS
the timeout test, and reversing merge_record's operands IS the commutativity
test.

259 tests, still 100% branch coverage.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCucBhUkHBBHHbsY7bSdGV
2026-08-21 14:22:25 +02:00
5f64a8959b Sign in with Google, and stop trusting a revoked session
Some checks failed
crdt-sync pre-commit / pre-commit (push) Has been cancelled
crdt-sync tests / test (3.10) (push) Has been cancelled
crdt-sync tests / test (3.11) (push) Has been cancelled
crdt-sync tests / test (3.12) (push) Has been cancelled
Two changes to the shared sync credential path, both driven by devices
that reported "Connected" while every sync failed.

Google sign-in (signInWithGoogle, both languages) replaces typing the
sync account's password on a fresh install. The account is linked once
from the PC with tool/link_google.py, which asserts the pinned uid --
without that assertion an unlinked identity is *signed up* rather than
refused, and the new uid is then denied every read and write by the
security rules.

The second change is the one with teeth. hasSession() is a presence
check, so a refresh token the server had revoked still read as a live
session: the desktop todo build showed "Connected" and then failed every
sync with `HTTP 400 (TOKEN_EXPIRED)`. A refresh that fails for a terminal
reason now clears the stored session, so hasSession() answers honestly.

Deliberately narrow: only TOKEN_EXPIRED, USER_DISABLED, USER_NOT_FOUND,
INVALID_REFRESH_TOKEN, INVALID_GRANT_TYPE and MISSING_REFRESH_TOKEN
clear it. A network error or a 5xx must not -- a device that signs
itself out whenever the wifi drops needs a manual sign-in on every
device to recover, which is worse than the stale banner this fixes.
Verified against the live endpoint, not just mocks: has_session flips
True -> False on a real rejection.

crdt_sync_dart 0.9.0 -> 0.10.0; the five apps pin the matching tag.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0192EkoAyFBEWTvAN5vVgmSy
2026-08-11 23:58:02 +02:00
dd6e6d5571 crdt_sync 0.5.0: the shared config and the cutover seam
Everything an app needs to move from GitHub to Firebase without each one
reinventing the wiring.

Python:
- _config.py: FirebaseConfig.load() + firebase_client_for(app) +
  mirror_client_for(app, github). One config file, read in one place.
  Credential caches stay per-app (~/.config/<app>/firebase_auth.json) --
  one file written by several processes is a corruption waiting to happen.
- tool/preflight_firebase.py: validates the config before any data moves.
  Catches a wrong-region databaseUrl (which answers 404 with a correctUrl
  body rather than an error, so it would otherwise pass every check and
  break mid-migration) and confirms the uid in the ID token matches the uid
  the rules pin -- sign_in() returns None and discards localId, so the uid
  is decoded from the JWT.
- tool/interop_seed.py: the missing Python half of the cross-language
  check. interop_check.dart asserts it can see a record written by Python,
  but nothing ever wrote one, so it could only ever fail.

Dart:
- firebase_config.dart: the same config shape, kept free of dart:io so it
  works on web and Android, where the file does not exist at all and
  credentials belong in flutter_secure_storage.
- PersistedSyncStateStore: the revision cache, persisted. Without it a
  phone forgets every peer revision on each cold start and re-downloads
  everything -- the exact traffic the revision scheme exists to avoid, and
  what the free-tier headroom depends on.

Verified live against the real database: preflight passes all five checks,
the Dart smoke test passes end to end, and Python <-> Dart interop is OK.

Python 226 tests at 100% branch coverage; Dart 218 tests, both new files
at 100% line coverage.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SJ9sxEw3ymzXNjL7SHNpJc
2026-08-04 22:59:48 +02:00
cfc4c295e4 crdt_sync: add the one-shot GitHub -> Firebase data migration
Reads through the GitHub API, not a local clone: the checkout on this
machine was five days stale while devices kept syncing, so seeding from it
would have silently rolled back everything written in between. Blobs are
fetched by sha rather than by path, so a concurrent push cannot swap the
content out from under the tree listing.

Writes through the real FirebaseSyncClient so the migration exercises the
same key escaping the apps will, then reads every blob back and compares
bytes before reporting success -- a migration that claimed success without
checking would be indistinguishable from one that dropped a file.

Retries connection resets and 5xx: GitHub drops keep-alive connections
routinely, and a half-completed migration is worse than one that never
started.

Three things are deliberately left behind, and stay in the GitHub repo
(archived, not deleted):
  - todo-sync/changesets/  pre-migration sqlite_crdt format, unread since
    2026-07-17
  - diet-guard-sync/devices/desktop/  a replica that stopped syncing on
    2026-07-22; verified to hold 0 records, 0 tombstones and 0 fields newer
    than both `pc` and `phone`. Carrying it over would cost a full
    re-download every tick forever, since a device that publishes no
    revision can never be skipped
  - READMEs and .gitkeep, which are not sync data

Run against the live database: 16 files, 956,272 bytes, all verified
byte-for-byte, and independently re-checked by listing the namespaces and
parsing records back out.
2026-08-03 18:31:18 +02:00