mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-17 16:37:43 +00:00
* docs(sync): add simplification roadmap * docs(sync): strengthen simplification roadmap * docs(sync): simplify sync implementation plan Prioritize deletion, atomic single-tab ownership, and full-sync trigger consolidation while deferring compatibility removals and speculative abstractions. * docs(sync): make simplification roadmap compatibility-safe Narrow deletion work behind persisted-data, provider-eligibility, delivery, and request-cost gates. Keep schema readers and separate unrelated startup and maintenance correctness projects. * docs(sync): fold code-verified review findings into plan * docs(sync): align simplification plan with latest changes * docs(sync): correct trigger-branch claim and add footprint estimate * docs: cap services at 1000 LOC * chore(lint): warn when a service exceeds 1000 lines * chore(lint): raise service size cap to 1200 lines * docs(sync): re-align simplification plan to master6fefd741c5* docs(sync): fold #9054 footprint auth into plan contracts Master advanced 3 commits past the plan's6fefd741c5baseline. Exactly one touches a task surface: #9054 (authenticated LWW project-move footprint, GHSA-8pxh) landed in conflict-resolution.service.ts — the file Task 6 carves up. Contract 14 documented only #9045's decrypt-path check and emphasised the weaker half ("synthetic ops are intentionally exempt; that exemption must survive the disjoint-merge removal"). Read alone, that could bless an op.entityIds read while removing disjoint-merge, reopening the vector one merge removed. #9045's exemption sentence is still accurate at master, so 14 was incomplete rather than wrong. Name the invariant #9054 established: getTaskProjectMoveEntityIds re-derives footprints from the authenticated payload.projectMoveFootprint (LWW) / actionPayload.projectMoveSubTaskIds + actionPayload.task.id (raw TASK_SHARED_UPDATE), never the plaintext envelope, and that one choke point serves the disjoint-merge, local-win and superseded-op callers. Also fence #9054 in §4 and re-align the baseline to7ef7e69e96(#9052 orphan-task navigation and #9056 locale touch no task surface). * chore(lint): make the service size cap enforce on new services 'warn' could not enforce what AGENTS.md asserts. `ng lint` runs the @angular-eslint builder, whose maxWarnings defaults to -1 and is not overridden in angular.json, so warnings never fail a build: a new 1300-line service merged green. The documented flip-to-'error' gate ("once every *.service.ts is under the cap") needed conflict-resolution.service.ts to go 3934 -> 1200, i.e. the whole 7-task simplification plan, so it would realistically never flip. Ratchet instead: 'error' on **/*.service.ts, with the 8 pre-existing offenders grandfathered to non-failing warnings at their real size, so the debt stays visible and the list can only shrink. Verified: a new over-cap service errors (exit 1); all 8 grandfathered warn (exit 0); full `npm run lint:ts` is exit 0 with exactly 8 warnings and 0 errors, which proves the list is complete — a missed offender would fail the build. Every >1200-line @Injectable in the repo is named *.service.ts, so the glob has no coverage hole, and .service.spec.ts is exempt (checked against the 7797-line conflict-resolution spec). Also correct AGENTS.md "1200 LOC": max-lines defaults to skipComments/skipBlankLines false, so the cap counts physical lines.
85 lines
9.8 KiB
Markdown
85 lines
9.8 KiB
Markdown
# 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`](docs/styling-guide.md)
|
||
- User-facing functionality changes → [`docs/documentation-guide.md`](docs/documentation-guide.md)
|
||
- Sync, op-log, vector clocks → [`docs/sync-and-op-log/`](docs/sync-and-op-log/)
|
||
- Effects/reducers/bulk-dispatch touching synced state → [`docs/sync-and-op-log/contributor-sync-model.md`](docs/sync-and-op-log/contributor-sync-model.md)
|
||
- E2E tests → [`e2e/CLAUDE.md`](e2e/CLAUDE.md)
|
||
- Load-bearing decisions → [`ARCHITECTURE-DECISIONS.md`](ARCHITECTURE-DECISIONS.md)
|
||
|
||
## Core commands
|
||
|
||
**ALWAYS run `npm run checkFile <filepath>` on every `.ts` or `.scss` file you modify** before reporting work as done.
|
||
|
||
```bash
|
||
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)`](.github/workflows/e2e-scheduled.yml) 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`](e2e/CLAUDE.md).
|
||
|
||
## Project rules
|
||
|
||
- **Translations:** UI strings go through `T` / `TranslateService`. Edit only `en.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 `dependencies` or `devDependencies`; 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_ELECTRON` before using Electron-specific APIs.
|
||
- **Templates:** plain HTML, minimal CSS/classes, Angular Material sparingly. See [`docs/styling-guide.md`](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` (use `unknown` if 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-lines` on `**/*.service.ts`; specs are exempt. Split by responsibility before crossing the line — extract collaborators, move pure logic to utils or `packages/` — 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 in `eslint.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`](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.
|
||
|
||
1. **Effects inject `LOCAL_ACTIONS`**, never `Actions` (`ALL_ACTIONS` only for the op-log capture effect; remote archive side effects → `ArchiveOperationHandler`, not `ALL_ACTIONS`). Lint-enforced (`no-actions-in-effects`). → [contributor-sync-model.md](docs/sync-and-op-log/contributor-sync-model.md), `src/app/util/local-actions.token.ts`.
|
||
2. **Prefer action-based effects**; a selector-based effect needs `skipDuringSyncWindow()`. Lint-enforced (`require-hydration-guard`). → [contributor-sync-model.md](docs/sync-and-op-log/contributor-sync-model.md).
|
||
3. **Multi-entity change = meta-reducer**, not an effect fan-out (one reducer pass = one op). → [contributor-sync-model.md](docs/sync-and-op-log/contributor-sync-model.md), `src/app/root-store/meta/task-shared-meta-reducers/`.
|
||
4. **Logical clock:** route "what day is this?" through `DateService` (`getLogicalTodayDate`, `isToday`, `todayStr`). Pure reducers/selectors take `startOfNextDayDiffMs` as an arg and call `isTodayWithOffset` for replay determinism. The raw `DateService.startOfNextDayDiff` is `private`; use `getStartOfNextDayDiffMs()` at service boundaries.
|
||
5. **`TODAY_TAG` (`'TODAY'`) is virtual** — never add to `task.tagIds`; membership comes from `task.dueWithTime` or `task.dueDay`. `TODAY_TAG.taskIds` only stores ordering. → `ARCHITECTURE-DECISIONS.md` Decision #2.
|
||
6. **Bulk dispatch loop:** `await new Promise(r => setTimeout(r, 0))` after the loop (else 50+ rapid dispatches lose state). → [contributor-sync-model.md](docs/sync-and-op-log/contributor-sync-model.md), `OperationApplierService.applyOperations()`.
|
||
7. **`SYNC_IMPORT` / `BACKUP_IMPORT`** replace state and intentionally drop concurrent ops (CONCURRENT or LESS_THAN by vector clock) — by design, not a bug. → `SyncImportFilterService`.
|
||
8. **Vector clocks:** `MAX_VECTOR_CLOCK_SIZE = 20`. Server prunes after conflict detection, before storage. → `docs/sync-and-op-log/vector-clocks.md`.
|
||
9. **Logging:** `Log.log({ id: task.id })`, never `Log.log(task)` or `Log.log(title)` — log history is exportable, never log user content.
|
||
|
||
## 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 |
|