| D-M4-12-0 |
2026-10-15 |
Base int/m1-wave24 at 2a72cca, branch feat/M4-12-local-store in its own worktree; not merged locally and not pushed, the PR lane lands it. Brief and spec were read from main; the brief relies on no core rule or D-W ruling that the base lacks, so nothing is ported first. The base names the chapters in German (15-client-architektur.md, 14-suche.md, 03-datenmodell.md, 04-sync-protokoll.md); the text this ticket adds is English. Names that differ from the brief (D-W-100, D-W-114): the removed-target kind is RemovedTargetKind with TargetRef (AccessTargetKind is a type of api_client, which local_store may not import, 15 3.1); SyncStateDao also has markSnapshot and resetCursor; the file entity is FileMeta (D-M4-08-7). |
Instructions of the launcher; merged names win over the brief. |
| D-M4-12-1 |
2026-10-15 |
Cipher chacha20 (ChaCha20-Poly1305, the SQLite3MultipleCiphers default); the key is set as the first statement in the raw form PRAGMA key = "x'<64 hex>'". The SQLite3MultipleCiphers documentation ("Raw key data (without key derivation)", configuration of SQL pragmas) states that a 64-digit hex key in this form is used directly as 32 bytes for the sqleet ChaCha20 scheme, without a key derivation; PRAGMA hexkey would derive. The opener then checks SELECT count(*) FROM sqlite_master (wrong key: LocalStoreKeyError without the key), PRAGMA cipher and FTS5; the hex text lives only in the setup call. |
Design note 2; a random 256-bit key needs no derivation, and the check fails closed on a library without encryption. |
| D-M4-12-2 |
2026-10-15 |
hooks: user_defines: sqlite3: source: sqlite3mc stands in the root pubspec.yaml. Verified with the pinned SDK (Flutter 3.47.7): with the define at the root, PRAGMA cipher reports chacha20 and sqlite3mc_version() reports SQLite3 Multiple Ciphers 2.6.1 (test/encryption_test.dart). |
Build hooks read user defines only from the workspace root. |
| D-M4-12-3 |
2026-10-15 |
The version rule is one SQL statement per mirror upsert: INSERT ... ON CONFLICT DO UPDATE ... WHERE excluded.version > <table>.version, returning whether a row changed; rows without resource.version are written with put after decideApply. Server times are stored as text with milliseconds and UTC (store_date_time_values_as_text), local times (last_accessed_at, outbox times) as epoch milliseconds. |
Design note 3; one statement cannot race with a second writer, and the text form round-trips the entities unchanged. |
| D-M4-12-4 |
2026-10-15 |
Removal deletes everything below the targets in one transaction and keeps outbox entries (they fail with 404 and keep their text, 04 8.3 item 4); file metadata goes only when no remaining message attaches the file. The threads of a removed channel come from every table that carries the channel (threads, messages, message_attachments, runs, read_states), and the files scoped to it (files.scope_channel_id) go with their cache rows when no message attaches them, because a thread row or an attachment link may be gone while content stays (code review and red team of M4-12). The cleanup deletes each thread in its own transaction. |
04 8.3; a long cleanup must not block the sync. |
| D-M4-12-5 |
2026-10-15 |
search_index is FTS5 with the columns kind, target_id, thread_id, channel_id, space_id (all UNINDEXED) and body, tokenizer unicode61 remove_diacritics 2, no stemmer; queries take each word as a quoted prefix term (FTS5 operators are plain text), ordered by bm25, with a snippet marked by the control characters U+0002 and U+0003. An absent space is stored as an empty text. |
Design note 5; 14 6 accepts other word stems than the server, and quoting keeps user input from breaking the query syntax. |
| D-M4-12-6 |
2026-10-15 |
MigrationRules is the helper of every later step (discardCache drops and recreates mirror tables and clears the cursor and the epoch; keepRows migrates preserved tables with alterTable); version 1 has no step, so onUpgrade refuses any other version until version 2 introduces stepByStep. test/drift/survival_test.dart checks the stored schema of the current version, migrates every pair of stored versions (from <= to) with fixture rows in outbox, drafts and share_inbox, fails when a stored version has no fixture rows, and runs the encrypted round trip of version 1. |
Design note 4 and 15 6.2; with one version drift generates no migration test, so the survival test is the test M4-36 extends. |
| D-M4-12-7 |
2026-10-15 |
make codegen-check runs dart run build_runner build and dart run drift_dev make-migrations in every Dart member whose build.yaml configures drift_dev, with tools/ci/codegen_check.py (the snapshot comparison of the Pigeon step) instead of a temporary copy; .pre-commit-config.yaml excludes drift_schemas/*.json from the end-of-file fixer. |
drift writes the schema file without a final newline and make-migrations refuses a stored schema that differs by one byte; the snapshot comparison ignores the ignored build outputs and needs no copy of the workspace. |
| D-M4-12-8 |
2026-10-15 |
host_key_pins is not in schema version 1; M4-36 adds it with its own schema version, adds it to MigrationRules.preservedTables and extends the survival test. |
Fixed requirement 10 (specification first). |
| D-M4-12-9 |
2026-10-15 |
License: SQLite3MultipleCiphers 2.6.1 is MIT (its repository's LICENSE), SQLite is public domain; the sqlite3 hook bundles a prebuilt library checked by sha256, not sources, so the check is the upstream license file and the version the library reports; make licenses covers the Dart package sqlite3 (MIT). |
Design note 8; bundled native code is outside the lock files the gate reads. |
| D-M4-12-10 |
2026-10-17 |
The ticket was ported from its integration base onto main with only its own commits; the English docs and spec of main win (the two paragraphs of 15 section 6 and the section of docs/quality/licenses.md sit in the English chapters, the code comments point to 15-client-architecture.md and 14-search.md). The drift codegen skip is named codegen-drift-dart, and the Pigeon block of the base is not part of this ticket. |
tools/ci/tests/test_make_server_gates.py on main drops Dart skips by the substring -dart (the base matched any dart); the Pigeon check belongs to M4-10. |
| D-M4-12-11 |
2026-10-17 |
The drift table definitions under packages/<member>/lib/src/tables/ leave the Dart coverage sum (tools/ci/lcov_filter.py) and the Sonar coverage measure (sonar.coverage.exclusions) like generated code; they stay in lint and analysis. test/round_trip_test.dart adds round trips of the instance, principal, agent, command and personal tables. |
make coverage measured 58.3 % for local_store on main (the builder's make -k check never reached coverage because test stopped first): the 341 lines of table getters only feed the generator, and the code that runs is the generated database.g.dart. Without them and with the new tests the member is above quality.coverage.dart_min_percent. |