SPEC v0.12: M8 outcomes (D-081 to D-085), HANDOFF, prompts

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
This commit is contained in:
2026-10-18 14:39:23 +02:00
parent 3d9eea8235
commit 6159e18198
4 changed files with 84 additions and 59 deletions
+30 -22
View File
@@ -3,46 +3,54 @@
Living document, rewritten in place (never a second copy). A new orchestrator session reads CLAUDE.md, then this.
## 1. Where things stand
- `main` at `3a1bd14` (M6b design-system fixes after the M7 wave) plus the SPEC v0.11 docs commit from `agent-spec11` once it lands. History: `5e1be38` plans and prompts, `c885aba` M2c-dev, `76ee957` SPEC v0.3, `fad6fc9` M2b, `fdf2093` M2a, `040bf41` SPEC v0.4, `503cee5` M3, `c28c52b` M5, `982451d` SPEC v0.5, `bd6f10a` M4, `23a2f1e` SPEC v0.6, `ab298ab` M9, `e8ee518` SPEC v0.7, `6b60ef7` M12a, `7fb9f6f` SPEC v0.8, `e499107` M12c, `2e2ebe7` M12b, `d452fa6` UX Kalas categories and tokens, `16c4d01` UX sprint U2, `45eecd1` SPEC v0.9, `bdba9d3` M12d, `3080804` M6, `7bd0aa1` SPEC v0.10, `72f21b2` app debug fakes bindings (temporary), `e6ad858` app release wiring of `:data` (pre-M8), `b9b061a` M7a, `6ac711d` M7e, `082b8dc` M7d, `31760f9` M7b, `1e42f76` M7c, `a54eb7c` M7f, `3a1bd14` M6b.
- Landed: M0 spec, M1 contracts and vectors (`c6df2ab`), M2a (`fdf2093`), M2b (`fad6fc9`), M2c-dev (`c885aba`), M3 (`503cee5`), M4 (`bd6f10a`), M5 (`c28c52b`), M9 (`ab298ab`), M12a (`6b60ef7`), M12b (`2e2ebe7`), M12c (`e499107`), U1 and U2 (`16c4d01`, owner approved), SPEC v0.9 (`45eecd1`), M12d (`bdba9d3`), M6 (`3080804`), SPEC v0.10 (`7bd0aa1`, D-060 to D-068), M7a onboarding (`b9b061a`), M7e analytics (`6ac711d`), M7d settle (`082b8dc`), M7b home and account (`31760f9`), M7c expense (`1e42f76`), M7f settings (`a54eb7c`), M6b `:core:design` fixes (`3a1bd14`).
- With `agent-spec11`: SPEC v0.11, D-069 to D-080 (owner approved in advance). D-069 S-11 caption, legend order, loading = no successful sync yet; D-070 S-12 titles, no error or loading state; D-071 top-level screens draw their own `KvittNavigationBar`, feature argument-key constants, result snackbars on the screen returned to (archive with undo); D-072 new template day = today; D-073 "Du har den senaste versionen" when `UpdateState` stays `None`; D-074 `FieldError.field` names, edited settlement keeps its rows, "Föreslå igen"; D-075 `quickRowCategories` in `:core:design`; D-076 two-line category labels with "…" for long custom names; D-077 long day ranges with the year; D-078 amounts open with two decimals; D-079 new string keys, `strings.csv` 578 rows; D-080 "Synka nu" online only, discard dialogs without body. §11.3 has the list of contract requests for a later contract pass (M12e).
- Both gates exist: Android `tools/gradle-serial.sh check` (from `android/`), server `cargo fmt --check && cargo clippy --all-targets -- -D warnings && cargo test` (from `server/`). Both are green on `main` as far as reported.
- Worktrees in flight under `.claude/worktrees/`: `agent-m8` (M8 app integration, started 2026-10-18; the phone agent; stops at the pairing HUMAN step), `agent-m7g` (M7g feature cleanups in `android/feature/**`, started 2026-10-18), `agent-spec11` (this docs change: SPEC v0.11).
- `main` at `00a14a3` (M8 app integration, second commit) plus the SPEC v0.12 docs commit from `agent-docs-v012` once it lands. History: `5e1be38` plans and prompts, `c885aba` M2c-dev, `76ee957` SPEC v0.3, `fad6fc9` M2b, `fdf2093` M2a, `040bf41` SPEC v0.4, `503cee5` M3, `c28c52b` M5, `982451d` SPEC v0.5, `bd6f10a` M4, `23a2f1e` SPEC v0.6, `ab298ab` M9, `e8ee518` SPEC v0.7, `6b60ef7` M12a, `7fb9f6f` SPEC v0.8, `e499107` M12c, `2e2ebe7` M12b, `d452fa6` UX Kalas categories and tokens, `16c4d01` UX sprint U2, `45eecd1` SPEC v0.9, `bdba9d3` M12d, `3080804` M6, `7bd0aa1` SPEC v0.10, `72f21b2` app debug fakes bindings (temporary, deleted by M8), `e6ad858` app release wiring of `:data` (pre-M8), `b9b061a` M7a, `6ac711d` M7e, `082b8dc` M7d, `31760f9` M7b, `1e42f76` M7c, `a54eb7c` M7f, `3a1bd14` M6b, `d504004` SPEC v0.11, `34a2590` M7g, `369e516` and `00a14a3` M8.
- Landed: M0 spec, M1 contracts and vectors (`c6df2ab`), M2a (`fdf2093`), M2b (`fad6fc9`), M2c-dev (`c885aba`), M3 (`503cee5`), M4 (`bd6f10a`), M5 (`c28c52b`), M9 (`ab298ab`), M12a (`6b60ef7`), M12b (`2e2ebe7`), M12c (`e499107`), U1 and U2 (`16c4d01`, owner approved), M12d (`bdba9d3`), M6 (`3080804`), M7a to M7f, M6b (`3a1bd14`), SPEC v0.11 (`d504004`, D-069 to D-080), M7g feature cleanups (`34a2590`), M8 app integration (`369e516` + `00a14a3`, landed 2026-10-18, gate green: BUILD SUCCESSFUL, 710 actionable tasks).
- M8 outcome (recorded in SPEC v0.12 as D-081 to D-085): both variants bind `:data` through `di/AppDataModule.kt`, debug fakes module gone (D-081, revises D-041); debug network security config allows cleartext everywhere, the AC-2.8 limit is enforced in `SessionRepositoryImpl.normaliseServerUrl`, pinned-root config behind `kvitt.tls.pinnedRoot` (D-082); FCM registration by Firebase Installation ID, firebase-messaging 26.0.0, no server change (D-083); invite links ignored while paired (D-084); generic-notification, deletion and `sync_batch` taps open Home, push in an expedited WorkManager job (D-085). Not yet run: the end-to-end pairing, offline and push test on the phone and the design-spec §2.7 typeface check (phone was not attached; §3 item 1).
- In flight: M12e contract pass (started 2026-10-18; SPEC §11.3 list; proposes D-086 onwards; spec v0.13 next). `agent-docs-v012` (this docs change: SPEC v0.12).
- Both gates exist: Android `tools/gradle-serial.sh check` (from `android/`), server `cargo fmt --check && cargo clippy --all-targets -- -D warnings && cargo test` (from `server/`). Both green on `main` as far as reported.
- The UX sprint (owner's session in `~/orca/workspaces/kvitt/UX-sprint`, branch `UX-sprint`) has landed; nothing is in flight there.
- Importer: the dry run of 2026-10-18 with M12b's mapping reconciled on both real files (Vardag 1746 rows, 0 unmapped; Hemma 37 rows, 0 unmapped). The CSV files stay in `export/` (git-ignored).
- Development stack (M2c-dev): running on the Mac, reachable at `http://192.168.1.210:8080`; database at schema 2 since 2026-10-18 (migrated after a backup). Firebase project `kvitt-1337` with apps `se.hagstedt.kvitt` and `se.hagstedt.kvitt.debug`; service account `kvitt-fcm`; `infra/secrets/fcm-service-account.json` and `infra/secrets/google-services.json` in the main checkout (git-ignored).
- Installed on the owner's phone (SM-S942B): the debug app `se.hagstedt.kvitt.debug` built from `c28c52b`, as of the last report; M8 installs its build.
- Development stack (M2c-dev): running on the Mac, reachable at `http://192.168.1.210:8080`; database at schema 2 since 2026-10-18 (migrated after a backup). Backup `infra/dev/backups/kvitt-20261018T093143Z.sqlite.gz` exists (git-ignored). Its bootstrap pairing code is held by the orchestrator outside the repo and expires 2026-10-21T12:28Z. No scripted partner device exists yet (it needs the admin token, available only after the owner pairs). Firebase project `kvitt-1337` with apps `se.hagstedt.kvitt` and `se.hagstedt.kvitt.debug`; service account `kvitt-fcm`; `infra/secrets/fcm-service-account.json` and `infra/secrets/google-services.json` in the main checkout (git-ignored).
- Installed on the owner's phone (SM-S942B): the debug app `se.hagstedt.kvitt.debug` built from `c28c52b`, as of the last report; the M8 build is not installed yet (phone not attached).
## 2. Open work (one line each, with who owns it: agent or owner)
- Land `agent-spec11` (orchestrator): SPEC v0.11, D-069 to D-080, HANDOFF, `prompts/README.md`, `android/README.md`, PLAN-M8 §3 facts.
- M8 App integration (`agent-m8`, in flight, phone agent). Decision range D-085 to D-089 reserved; renumbered to D-081 onwards on landing (after `agent-spec11`). Spec version v0.12. It wires the §13.2 navigation (D-071), deletes `android/app/src/debug/.../DebugFakesModule.kt` (`72f21b2`), keeps the `:app` requests of M6 (`LocalAvatarPictures`, `ProvideHousehold`, `LocalKvittClock`) and the design-spec §2.7 One UI font check on the S26.
- M7g Feature cleanups (`agent-m7g`, in flight): feature modules after M6b; no phone.
- Later contract pass M12e (not started; SPEC §11.3 list): invite kind in the `pair()` result, admin name in the invite link, `SyncSummary` "last attempt failed", actor on `Rejection`, version name on `Device`, `RecurringRepository.create` without a caller-made `TemplateId`, a result for `UpdateRepository.check()`, untouched accounts in `SettleState`, the D-027 re-stamp in `:data`, the 128 px picture size (owner question).
- Land `agent-docs-v012` (orchestrator): SPEC v0.12, D-081 to D-085, HANDOFF, `prompts/README.md`.
- M8 on-phone test (orchestrator with the owner, §3 item 1): install the M8 debug build, pair against the dev server, script a partner device with the admin token, run the offline and push checks; then the design-spec §2.7 typeface check (One UI font styles on the S26).
- M12e contract pass (in flight): the SPEC §11.3 list, including M8's additions (`PushEvent` to `:core:model`, S-22 licences (OQ-5), detail-screen snackbar hosts, S-7/S-8 snackbar strings as resource ids, launcher icon, the `fid` field on the server).
- Image build for the Pi (M2c-pi or M10): since M4 the arm64 server image needs a C toolchain in the build stage (aws-lc-rs) and CA roots in the distroless image (§10.3); confirm both and one FCM send from the shipped image.
- Later cleanup (any Android plan that owns `android/gradle/**`): remove the unused Vico entries from `libs.versions.toml` (D-063).
- Plans (SPEC §11.2 v0.11), in dependency order; prompt `prompts/<id>.md`:
- M1, M2a, M2b, M2c-dev, M3, M4, M5, M6, M6b, M7a to M7f, M9, M12a, M12b, M12c, M12d, U1, U2: landed.
- Now: M8 and M7g in parallel.
- Next: M2c-pi Infra on the Pi after M8 (owner: answer OQ-3, grant the Pi, Home Assistant Pi, Gitea and Drive allowances at the start, D-023); M10 First release after M2c-pi (owner: keystore per §10.6, D-024); M11 Import history after M9 and M10 (procedure §10.8 "Production run", D-025; each run must reconcile, D-045); M12e contract pass when scheduled.
- Plans (SPEC §11.2 v0.12), in dependency order; prompt `prompts/<id>.md`:
- M1, M2a, M2b, M2c-dev, M3, M4, M5, M6, M6b, M7a to M7g, M8, M9, M12a, M12b, M12c, M12d, U1, U2: landed.
- Now: M12e contract pass.
- Next: M2c-pi Infra on the Pi (blocked on OQ-3; owner: answer OQ-3, grant the Pi, Home Assistant Pi, Gitea and Drive allowances at the start, D-023); M10 First release after M2c-pi (owner: keystore per §10.6, D-024); M11 Import history after M9 and M10 (procedure §10.8 "Production run", D-025; each run must reconcile, D-045).
## 3. HUMAN steps pending (relay in chat; the owner does them tonight, 2026-10-18)
1. M8 pairing and offline test on the phone; relay wording comes from the M8 report.
## 3. HUMAN steps pending (relay in chat)
1. M8 pairing, offline and push test on the phone (the owner does it with the orchestrator; dev server at `http://192.168.1.210:8080`; the orchestrator holds the bootstrap code, valid until 2026-10-21T12:28Z):
1. Connect the phone to home Wi-Fi, plug it in over USB, open Kvitt (debug).
2. On "Para ihop", open "Serveradress" and enter `http://192.168.1.210:8080`.
3. Enter the code, enter "Ditt namn", tap "Para ihop".
4. Land on "Väntar på din partner"; an agent pairs a scripted partner and creates an account. Yes/no: does Home show the account?
5. If Android asks to allow notifications, tap Allow.
6. Airplane mode on. Tap "Lägg till utgift", amount, description, "Spara". Yes/no: does the row show "Väntar på synk"?
7. Airplane mode off, wait about 10 seconds. Yes/no: marker gone?
8. Agent posts an expense as the partner. Yes/no: did a notification appear?
2. Look through the M6 component screenshots (`android/core/design/src/test/screenshots/`); approved in advance, nothing blocks on it.
3. Answer OQ-3 for M2c-pi (§4).
4. Release keystore per SPEC §10.6 for M10, or allow an agent to generate one.
5. Yes/no: the keyword "boende" maps to travel. Yes/no: is the 128 px picture size needed?
5. Yes/no: the keyword "boende" maps to travel. Yes/no: is the 128 px picture size needed? Yes/no: is an open-source licences screen wanted in v1 (OQ-5, default no)?
## 4. Owner questions (asked, unanswered)
- OQ-3 LAN addresses and SSH users of the Kvitt Pi and the Home Assistant Pi; SSH port of the HA Pi; certificate directory there; which SSH add-on (SPEC §12). Needed by M2c-pi only.
- OQ-5 open-source licences screen on S-22 in v1 (SPEC §12; default not in v1).
- "boende" keyword → travel (yes/no); 128 px picture size needed (yes/no) (§3 item 5).
## 5. Paused (waiting on something outside the repo)
- Nothing.
- M2c-pi: OQ-3.
## 6. Numbering
- Decisions: D-001 to D-080 used (D-008 and D-009 unused). D-085 to D-089 reserved by M8 (renumbered to D-081 onwards on landing). Next free: **D-081**.
- Spec version: v0.11 written; next **v0.12**.
- Server schema version **2** (migration 0002, landed with M12b); next **3**. Room schema version **1** (`android/data/schemas/se.hagstedt.kvitt.data.db.KvittDatabase/1.json`); next **2**.
- Decisions: D-001 to D-085 used (D-008 and D-009 unused). Next free: **D-086** (M12e reserved D-086 to D-095).
- Spec version: v0.12 written; next **v0.13** (M12e).
- Server schema version **2** (migration 0002, landed with M12b); next **3**. Room schema version **1** (`android/data/schemas/se.hagstedt.kvitt.data.db.KvittDatabase/1.json`); M12e may propose **2**.
- Sync protocol **1**, push payload **1**.
## 7. How the work is run
+6 -1
View File
@@ -45,7 +45,7 @@ entry and the old one a strike-through plus a pointer. The spec is updated in th
| D-038 | 2026-10-18 | FCM retry is one send plus up to three retries after 1 s, 5 s, 25 s (four sends at most) on 429, 5xx and network errors. On 401 the cached access token is dropped and the message is not retried; other 4xx are not retried. Retries run inside the single sender task, blocking it up to 31 s per device; accepted for two users. | M4 report (`bd6f10a`): "3 attempts with delays 1 s, 5 s, 25 s" was ambiguous (three delays need four sends) and 4xx handling was open. | SPEC §14.6 |
| D-039 | 2026-10-18 | `sync_mutations` pruning deletes rows whose `received_at` is strictly older than 90 days (a row exactly 90 days old is kept); PLAN-M4's "90 days and older" corrected. Seam change to D-015: `tasks::start` returns the `AppState` with the real push queue installed and `serve` builds the router from it; `AppState::new` defaults to `SqlAuditWriter`; `NoopAuditWriter` is removed; `NoopPushQueue` stays for push disabled. | M4 report (`bd6f10a`): "older than 90 days" (D-021) and PLAN-M4 disagreed at the boundary; the push queue must exist before the router is built. | SPEC §6.3, §14.1, §14.4.9, §14.6; PLAN-M4; server/README.md |
| D-040 | 2026-10-17 | A push refused as a whole (400 `invalid_request`, 413 `payload_too_large`, 409 `household_incomplete`, 422 `batch_too_large`) counts as a batch with no progress: the cycle goes on to pull and records `last_error_code`; 5xx and 429 retry with backoff; 401 and 426 stop. (Proposed by M5 as D-030, renumbered.) | M5 report: §14.4.5 covered only network errors, 5xx and 401. | SPEC §14.4.5 |
| D-041 | 2026-10-17 | `:data`'s `DataModule` provides implementations only; the interface bindings live in `DataBindingsModule` (`@DisableInstallInCheck`), which `:app` includes for release so the debug fakes do not duplicate bindings. (M5's D-031, renumbered.) | M5 report: Hilt fails on duplicate bindings when debug binds fakes. | SPEC §6.2, §11.3 (M8); PLAN-M8 |
| D-041 | 2026-10-17 | `:data`'s `DataModule` provides implementations only; the interface bindings live in `DataBindingsModule` (`@DisableInstallInCheck`), which `:app` includes ~~for release so the debug fakes do not duplicate bindings~~ (revised by D-081: `:app` includes it in both variants; the debug fakes module is gone). (M5's D-031, renumbered.) | M5 report: Hilt fails on duplicate bindings when debug binds fakes. | SPEC §6.2, §11.3 (M8); PLAN-M8 |
| D-042 | 2026-10-17 | Room tests run on Robolectric with the framework SQLite driver; migration tests build from the exported schema JSON rather than `room3-testing`. (M5's D-032, renumbered.) | M5 report: no device needed for the gate. | SPEC §6.2 |
| D-043 | 2026-10-17 | Two extra DataStore keys: the pairing status in `auth.preferences_pb`, and the installed version code that received a 426 in `prefs.preferences_pb` (syncing stops until the installed version changes). The token is encrypted with Keystore key `kvitt_token_key`, AES-256-GCM (as §8.2). (M5's D-033, renumbered.) | M5 report. | SPEC §8.2, §14.2.2, §14.4.5 |
| D-044 | 2026-10-18 | Nine Swedish Splitwise category names, found unmapped in the importer rehearsal on the real Swedish export, are added to the §10.8 table and to `server/crates/kvitt-import/data/splitwise_categories.csv` with the key of their English equivalent: Städning → `utilities` (Cleaning); Verktyg - Övrigt → `utilities` (Utilities - Other); Tjänster → `home` (Services); Cykel → `transport` (Bicycle); Flyg → `travel` (Plane); Spel → `entertainment` (Games); Kläder → `shopping` (Clothing); Livet - Övrigt → `other` (Life - Other); Utbildning → `other` (Education). The test `category_table_matches_the_spec` keeps the two in step. | The rehearsal of 2026-10-18 left 21 rows of the Vardag file as `other`, counted unmapped, only because these names were missing from D-003's table. | SPEC §10.8; `server/crates/kvitt-import/data/splitwise_categories.csv` |
@@ -85,3 +85,8 @@ entry and the old one a strike-through plus a pointer. The spec is updated in th
| D-078 | 2026-10-18 | A stored amount opens for editing with both decimals ("1 247,60", "12 400,00"); ratio percent fields keep the trimmed form ("60", "60,5"). Note: `viewerPercentOf` now reads basis points (bug fix, no rule change). | M6b (`3a1bd14`, `AmountInput.of` and `AmountInput.percent`, replacing M7c's own `moneyInput`): money is always shown with two decimals (§9.8), percents are not money. | SPEC §9.6 |
| D-079 | 2026-10-18 | New shared keys `common_ok`, `common_undo`, `editor_split_full_on_you` ("All on you" / "Allt på dig"; `ledger_settlement_label` was D-068). The module keys proposed by M7b to M7f are recorded in `docs/ux/strings.csv` as built: `analytics_granularity`, `analytics_measure`, `analytics_group_by`, `analytics_range_apply`; `settle_row_amount`, `settle_detail_delete`, `settle_detail_sync_now`; `expense_detail_sync_now`; `home_archived_expanded`, `home_archived_collapsed`; `settings_category_archived_snackbar`, `settings_category_unarchived_snackbar`, `settings_recurring_summary_none`, `settings_expanded`, `settings_collapsed`, `settings_partner_missing`. `strings.csv` has 578 rows, header included. | M7b to M7f reports and M6b (`3a1bd14`): §13.4 requires a decisions entry for strings not in the file. | SPEC §13.4; `docs/ux/strings.csv` |
| D-080 | 2026-10-18 | "Synka nu" (the missing states of S-8 and S-10, and S-16) is an online-only action, disabled with "Kräver anslutning" offline. Discard dialogs (S-6, S-7, S-9, S-19) have a title and no body text. | M7c report (`1e42f76`): §9.9 did not list "Sync now", which needs the server; the discard dialogs' body text was not fixed. | SPEC §9.9, §13.3 (S-6, S-7, S-8, S-9, S-10, S-16, S-19) |
| D-081 | 2026-10-18 | Both variants bind the `:core:model` interfaces to `:data` through `android/app/src/main/.../di/AppDataModule.kt`, which includes `DataBindingsModule` (D-041). `DebugFakesModule` (`72f21b2`) and the release-only data module are deleted; no Hilt module lives in `src/debug` or `src/release`. Fakes live only in test source sets and feature previews; `:app`'s unit tests run on a plain `Application`. Revises D-041 ("debug binds the fakes"). (Proposed by M8 as D-085, renumbered.) | M8 report (`369e516`): the debug app must pair against the development server (D-001), so debug needs the real `:data` graph. | SPEC §6.2, §11.2 (M8), §11.3 (M8) |
| D-082 | 2026-10-18 | A network security config cannot express IP ranges, so the debug config (`android/app/src/debug/res/xml/network_security_config.xml`) allows cleartext to every host and the AC-2.8 limit (`localhost`, `10.0.2.2`, private LAN addresses only) is enforced by `SessionRepositoryImpl.normaliseServerUrl`. The release config is in `src/main` (cleartext off, system anchors); the pinned-root config of §10.5.3 is in `src/releasePinnedRoot/res`, added to the release resources only when the Gradle property `kvitt.tls.pinnedRoot` is `true`. Lint's `InsecureBaseConfiguration` warning on the debug config is intended. (M8's D-086, renumbered.) | M8 report (`369e516`): §8.5 asked for cleartext "to the hosts listed in AC-2.8", which the config format cannot list. | SPEC §8.5, §10.5.3, AC-2.8 |
| D-083 | 2026-10-18 | FCM registration by Firebase Installation ID (firebase-messaging 26.0.0 through the Firebase BoM 35.0.2, google-services plugin 4.5.0). `register()` runs whenever the phone is paired and at each app start while paired; `onRegistered(fid)` sends the id as `fcm_token` with E-17; when the device is revoked the app calls `unregister()`. Needs the manifest meta-data `firebase_messaging_installation_id_enabled`. No server change: FCM HTTP v1 `message.token` accepts installation ids during Firebase's transition. Follow-up request for the server (§11.3 M12e list): send the id in the `fid` field. (M8's D-087, renumbered.) | M8 report (`369e516`): firebase-messaging 26 registers an app instance by its Firebase Installation ID; AC-13.7 described a rotating FCM token. | SPEC AC-13.7, §6.2, §14.6, §11.3 |
| D-084 | 2026-10-18 | An invite link (`kvitt://invite` or `https://icehack.asuscomm.com:8443/i/<CODE>`, declared with `autoVerify="false"`) opened while the phone is paired is ignored; the app never re-pairs over live data. (M8's D-088, renumbered.) | M8 report (`369e516`): AC-2.6 did not say what a link does on a paired phone; re-pairing would discard the local copy and its pending changes. | SPEC AC-2.6 |
| D-085 | 2026-10-18 | Push handling: an expedited WorkManager job calls `syncForPush` (waited up to 20 s), then builds the notification text from Room. A tap on the generic notification (sync failed, or entity or actor missing locally) opens Home, not the detail; deletions and `sync_batch` also open Home; an unknown `type` or `v` ≠ `1` only syncs. Channel `activity`; notification id = hash of `entity_id`, `sync_batch` uses id `1`. (M8's D-089, renumbered.) | M8 report (`369e516`): AC-13.6 and §14.6 did not say where a generic notification leads or which fixed id `sync_batch` uses. | SPEC AC-13.6, §14.6 |