* docs(sync): fix stale schema docs, add bump policy, rescope Task 6 * docs(sync): add severity-triage and schema-bump rules to AGENTS.md * docs(sync): fix review findings in schema compat docs
13 KiB
AGENTS.md
Guidance for AI agents working in this repository. Super Productivity is a todo and time-tracking app on Angular + Electron + Capacitor.
Product principles
From the project manifesto (Deep Work, Your Way), kept to what changes a build decision — weigh them on every feature, and surface the leaner path when a request fights them:
- Avoid feature creep: prefer the smallest change that solves the real problem. New UI, settings, and sync surface are permanent costs, so extend existing building blocks before adding new ones, and let a feature ship only if it makes users faster, not busier. When scope outgrows the problem, propose the leaner option rather than silently building the larger one — it's still the user's call. Scope guard: this is a personal deep-work tool, not a team-management or reporting product.
- Less noise, more depth: reject constant alerts, vanity dashboards, streaks, and dopamine loops. Opt-in reminders and notifications are core to the app, but anything attention-grabbing ships off by default and stays quiet (flow, not friction).
- Adapt, don't impose: people plan, track, and reflect differently, so ship new behavior as building blocks. Prefer one calm default over a new toggle; add a setting only when real workflows genuinely diverge, never to dodge a default decision (don't build it → calm default → opt-in setting).
- Privacy & offline first: no analytics, tracking, or telemetry (see Project rules → Privacy). Core task and time tracking must work fully offline; sync and online integrations are optional layers that degrade gracefully, never prerequisites.
Required reading per task
- Styling changes →
docs/styling-guide.md - User-facing functionality changes →
docs/documentation-guide.md - Sync, op-log, vector clocks →
docs/sync-and-op-log/ - Effects/reducers/bulk-dispatch touching synced state →
docs/sync-and-op-log/contributor-sync-model.md - E2E tests →
e2e/CLAUDE.md - Load-bearing decisions →
ARCHITECTURE-DECISIONS.md
Core commands
ALWAYS run npm run checkFile <filepath> on every .ts or .scss file you modify before reporting work as done.
npm run checkFile <filepath> # prettier + lint a single file
npm run prettier # multi-file format
npm run lint # multi-file lint
npm test # all unit tests (Jasmine/Karma, .spec.ts co-located)
npm run test:file <filepath> # single spec
npm run e2e # all E2E (Playwright, slow)
npm run e2e:file <path> -- --retries=0 # single E2E (~20s/test); add --grep "name" for one test
npm start # Electron dev
ng serve # web dev (or npm run startFrontend)
npm run dist # production build (all platforms available locally)
Run the full SuperSync and WebDAV E2E suites via GitHub Actions: manually dispatch E2E Tests (Scheduled) for your branch. This should be preferred over running the full suites locally; the workflow provides dedicated WebDAV and sharded SuperSync jobs. The optional grep input filters the SuperSync job only.
For local SuperSync E2E (docker-compose) and the full E2E reference, see e2e/CLAUDE.md.
Project rules
- Translations: UI strings go through
T/TranslateService. Edit onlyen.json; never other locales. - Privacy: no analytics or tracking — user data stays local unless explicitly synced.
- Dependencies: PRs must not add new packages to the root project's
dependenciesordevDependencies; use platform APIs, existing packages, or a small in-repo implementation instead. Dependencies scoped to an individual plugin are allowed when they are necessary and remain isolated to that plugin. - Electron: check
IS_ELECTRONbefore using Electron-specific APIs. - Templates: plain HTML, minimal CSS/classes, Angular Material sparingly. See
docs/styling-guide.md. - Styling review: do not locally restyle Angular Material or shared
src/app/ui/components for one-off context needs. This includes overriding button styles via.mat-*,.mdc-*,button[mat-*], or component internals in local SCSS. Prefer existing inputs/classes/tokens; if a variant must exist, make it reusable or add it to the shared style layer. - Strict TypeScript: no
any(useunknownif truly unknown). - State: never mutate NgRx state — return new objects in reducers. Prefer Signals to Observables.
- Tests: add unit tests for new services and state logic.
- Service size cap: no service may exceed 1200 lines (physical lines — blanks and comments count), lint-enforced via
max-lineson**/*.service.ts; specs are exempt. Split by responsibility before crossing the line — extract collaborators, move pure logic to utils orpackages/— and never grow a service past it. A new service over the cap fails lint. The pre-existing offenders (sync/op-log/plugin/task services) are grandfathered to warnings ineslint.config.js: that list may only shrink — never add to it — and they are debt to pay down when touched, not a precedent to extend. - Code review: when reviewing new features, always double-check the potential long-term costs and risks a change introduces — maintenance burden, hard-to-reverse choices (data shapes, public/plugin APIs, sync formats), locked-in dependencies/abstractions, and footguns that only surface at scale or across synced clients — not just whether the immediate diff is correct.
- Task component is a hot path: every change to
src/app/features/tasks/task/task.component.*(rendered once per task in long, scrollable lists) must be double-checked for negative performance impact — avoid function/getter calls in the template, extra change-detection work, and uncleaned subscriptions; verify against a large task list.
Sync-correctness rules
Touched on most state-related PRs. Read the linked source/doc for full reasoning before editing. Rules 1–3 and 6 are one invariant — one user intent = one op; replayed/remote ops must not re-trigger effects — fully explained in docs/sync-and-op-log/contributor-sync-model.md.
Every change to the sync system is high-risk: a subtle bug can silently corrupt or lose user data across devices and is hard to recover from. Carefully check each change for correctness and possible failure modes (replay determinism, concurrent/remote edits, vector-clock conflicts) and call out the risks before reporting work as done.
- Effects inject
LOCAL_ACTIONS, neverActions(ALL_ACTIONSonly for the op-log capture effect; remote archive side effects →ArchiveOperationHandler, notALL_ACTIONS). Lint-enforced (no-actions-in-effects). → contributor-sync-model.md,src/app/util/local-actions.token.ts. - Prefer action-based effects; a selector-based effect needs
skipDuringSyncWindow(). Lint-enforced (require-hydration-guard). → contributor-sync-model.md. - Multi-entity change = meta-reducer, not an effect fan-out (one reducer pass = one op). → contributor-sync-model.md,
src/app/root-store/meta/task-shared-meta-reducers/. - Logical clock: route "what day is this?" through
DateService(getLogicalTodayDate,isToday,todayStr). Pure reducers/selectors takestartOfNextDayDiffMsas an arg and callisTodayWithOffsetfor replay determinism. The rawDateService.startOfNextDayDiffisprivate; usegetStartOfNextDayDiffMs()at service boundaries. TODAY_TAG('TODAY') is virtual — never add totask.tagIds; membership comes fromtask.dueWithTimeortask.dueDay.TODAY_TAG.taskIdsonly stores ordering. →ARCHITECTURE-DECISIONS.mdDecision #2.- Bulk dispatch loop:
await new Promise(r => setTimeout(r, 0))after the loop (else 50+ rapid dispatches lose state). → contributor-sync-model.md,OperationApplierService.applyOperations(). SYNC_IMPORT/BACKUP_IMPORTreplace state and intentionally drop concurrent ops (CONCURRENT or LESS_THAN by vector clock) — by design, not a bug. →SyncImportFilterService.- Vector clocks:
MAX_VECTOR_CLOCK_SIZE = 20. Server prunes after conflict detection, before storage. →docs/sync-and-op-log/vector-clocks.md. - Logging:
Log.log({ id: task.id }), neverLog.log(task)orLog.log(title)— log history is exportable, never log user content. - A schema bump never protects the released fleet. v17.0.0–v18.14.0 clients apply ops up to schema 5 UNMIGRATED (their old
+3skip band) and, at schema ≥ 6, block them but still advance the server cursor — those ops are skipped permanently, even after updating. Only post-v18.14.0 receivers block newer ops safely. So new op semantics MUST degrade gracefully on older clients (LwwUpdatePayloadenvelope pattern); a change old clients would misapply must not ship behind a bump alone. →packages/shared-schema/src/schema-version.ts, operation-log-architecture.md §A.7.11 "Bump Policy".
Judging sync severity
Rules for triage — how to decide whether a sync bug is real and how bad it is. Each one below is here because getting it wrong already produced a confidently wrong conclusion.
masterships to real users. "It's only on master" never downgrades severity. Every master push auto-publishes to the Play internal track (.github/workflows/build-android.yml,tracks: internal+status: completed→ testers' phones auto-update within minutes, on their real data).ghcr.io/super-productivity/supersync:latestis master and has no release-tagged build at all — it is the default inpackages/super-sync-server/docker-compose.yml, so self-hosters ondocker compose pullrun master HEAD. Snapedgeis also published from every master push. Only desktop/web/F-Droid/Play-production/Snap-stable are release-gated.- Never infer "shipped" from dates or the latest tag — prove it. Use
git merge-base --is-ancestor <commit> v<tag>/git tag --contains <commit>. Tags are cut from a point in time, and sync features routinely land just after: #8874's disjoint-field merge landed ~24h after v18.14.0 was tagged and is in no release, so whole-entity-LWW field loss (rename dies when another device marks the task done, #9095) is live in every shipped version. - "Restores released behavior" ≠ safe. The released behavior can be the bug. #9061 froze the disjoint merge on exactly that reasoning and silently re-armed shipped data loss (#9095). A freeze/revert needs the same "what breaks for users?" analysis as a feature.
- Users do report sync bugs — in non-technical words. There is no
synclabel. Keyword-greppingsync/op-log/conflictundercounts by ~50×. Search what users actually write: lost, disappeared, gone, missing, duplicate, reverted, old version, overwritten, reset, not syncing (#7892 "all data deleted overnight"; #8107 user rebuilt lost projects from memory; #7549 done tasks resurrecting). ~53 user-reported sync/data-loss issues from 44 authors in 90 days ≈ one every 2 days. And silent data loss is structurally under-reported — absence of reports is never evidence of absence. - Audit-generated findings are low-precision, not low-yield — verify them, don't dismiss them.
~89% of sync fixes since v18.14.0 repaired code present in the release, yet ~97% of the self-filed
sync issues carried no reproduction. So both failure modes are live: do not close an unreproduced
finding as speculation (#8960/#9073/#8751/#9040 had no repro and were all real and shipped), and
do not fix one blind — the fix must carry a test that fails without it, and you must confirm
the fix actually fires on a real op (#9045 shipped an
entityIdssecurity check that never fired; #9025 was self-retracted as "not a live data-loss bug"). The reproduction gates the fix, not belief.
Anti-patterns
| Avoid | Do instead |
|---|---|
any type |
proper types, unknown if truly unknown |
| Direct DOM access | Angular bindings, viewChild() |
| Side effects in constructors | async pipe or toSignal |
| Subscribing without cleanup | takeUntilDestroyed() or async pipe |
NgModules for new code |
standalone components |
| Re-declaring Material theme styles | existing theme variables |
One-off .mat-*, .mdc-*, button[mat-*], or shared component overrides |
reusable inputs, tokens, or shared styles |