Files
familysync/.planning/phases/13-real-lint-gate-eslint/13-PATTERNS.md
T
Lucas Berger 982438dc10 style(13-03): apply Prettier formatting across repo
Mechanical reformat — no logic changes. 398 files changed, 19125
insertions(+), 16457 deletions(-). Prettier 3.8.4 with .prettierrc
(singleQuote:true, semi:true, tabWidth:2, trailingComma:all,
printWidth:100). Isolated per D-13-08 for reviewability.
2026-06-11 20:35:18 -04:00

13 KiB
Raw Blame History

Phase 13: Real Lint Gate (ESLint) - Pattern Map

Mapped: 2026-06-11 Files analyzed: 8 (3 new config files + 3 modified package.json/ci.yml + 2 source-fix files) Analogs found: 5 / 8 (3 config files are greenfield with no in-repo analog)


File Classification

New/Modified File Role Data Flow Closest Analog Match Quality
eslint.config.js (root) config none greenfield
.prettierrc (root) config none greenfield
.prettierignore (root) config none greenfield
package.json (root) config existing lint/typecheck scripts (lines 11-12) exact
apps/api/package.json config existing typecheck script (line 13) exact
apps/pwa/package.json config existing typecheck script (line 11) exact
.gitea/workflows/ci.yml config existing Lint + Typecheck steps (lines 31-36) exact
apps/api/src/broker/*.ts (violation fixes) service batch existing setInterval+.catch pattern in same files exact
apps/pwa/src/components/EventForm.tsx (violation fix) component request-response existing eslint-disable-next-line + justification comment at line 274 exact

Pattern Assignments

eslint.config.js (root) — GREENFIELD

No analog exists in the codebase. Use the full skeleton from RESEARCH.md Pattern 1 verbatim. Key constraints from the codebase:

  • Root package.json has "type": "module" absent (checked: it does NOT have type: module). However both apps/api and apps/pwa do ("type": "module"). The flat config file must be eslint.config.js (ESM); since root has no type: module, use .mjs extension OR add "type": "module" to root package.json alongside the new scripts. Prefer eslint.config.js + add "type": "module" to root — the root has no CJS code to break.
  • apps/api/tsconfig.json excludes "tests" — add apps/api/tests/**/*.ts to the disableTypeChecked override block alongside the named config files.
  • All three worker files (outboxWorker.ts, poller.ts, reminderScheduler.ts) use setInterval(() => { runX().catch(...) }, N) — the .catch() chain correctly handles the promise. If no-floating-promises fires on the .catch() return value itself, wrap with void: void runX().catch(...).

Config files that need disableTypeChecked override (confirmed outside all tsconfig include arrays):

  • apps/api/drizzle.config.ts
  • apps/api/vitest.config.ts
  • apps/pwa/vite.config.ts
  • apps/pwa/vitest.config.ts
  • apps/pwa/playwright.config.ts
  • apps/api/tests/**/*.ts (excluded from apps/api/tsconfig.json)

.prettierrc / .prettierignore — GREENFIELD

No analog exists. Use RESEARCH.md Prettier Configuration section verbatim.

Codebase style observation: all existing source files use single quotes (confirmed by broker file imports like import { z } from 'zod'). Set "singleQuote": true.


package.json (root) — adding format / format:check scripts

Analog: the existing lint and typecheck script wiring in the same file.

Existing pattern (/home/luc/Projects/familysync/package.json, lines 11-12):

"lint": "pnpm -r --if-present lint",
"typecheck": "pnpm -r typecheck"

Apply the same style for new scripts:

"format":       "prettier --write .",
"format:check": "prettier --check ."

Note: root package.json currently has no "type": "module". Adding it is required so eslint.config.js (ESM) is parsed as ESM by Node. Root has no index.js or other CJS entry points that would break.


apps/api/package.json — adding lint script

Analog: the existing typecheck script in the same file (/home/luc/Projects/familysync/apps/api/package.json, line 13):

"typecheck": "tsc --noEmit"

Pattern to mirror — same position in the scripts block, same style:

"lint": "eslint src/ tests/ --max-warnings 0"

Mirror pattern: single command, no wrapper, scope by directory. The tests/ directory is listed here even though it is excluded from the tsconfig — the ESLint config will apply disableTypeChecked for those files so the linter won't fail on project-inclusion errors.


apps/pwa/package.json — adding lint script

Analog: the existing typecheck script in the same file (/home/luc/Projects/familysync/apps/pwa/package.json, line 11):

"typecheck": "tsc --noEmit && tsc --project tsconfig.e2e.json --noEmit"

Pattern to mirror:

"lint": "eslint src/ e2e/ --max-warnings 0"

The e2e/ directory is covered by tsconfig.e2e.json, which projectService: true discovers automatically — no special override needed for e2e specs.


.gitea/workflows/ci.yml — adding Format check step

Analog: the existing Lint and Typecheck steps in the fast-checks job (/home/luc/Projects/familysync/.gitea/workflows/ci.yml, lines 31-36):

- name: Lint
  run: pnpm lint

- name: Typecheck
  run: pnpm typecheck

New step — insert between Lint and Typecheck (per RESEARCH ordering: lint → format:check → typecheck → tests):

- name: Lint
  run: pnpm lint

- name: Format check
  run: pnpm format:check

- name: Typecheck
  run: pnpm typecheck

Also remove the now-stale comment above the Lint step (lines 28-30):

# lint is currently a no-op: no package defines a `lint` script and ESLint is
# not installed. `pnpm -r lint` prints ERR_PNPM_RECURSIVE_RUN_NO_SCRIPT but
# exits 0, so this step passes. Wiring lint is out of this phase's scope.

apps/api/src/broker/*.tsno-floating-promises / no-unsafe-* violation fixes

Analog: the existing setInterval worker pattern in the same files. All three workers (outboxWorker.ts, poller.ts, reminderScheduler.ts) already use the same correct structure:

Existing correct pattern (outboxWorker.ts, lines 781-787):

export function startOutboxWorker(): void {
  setInterval(() => {
    runOutboxDrain().catch((err: unknown) => {
      console.error('[outboxWorker] Unhandled runOutboxDrain error:', err);
    });
  }, 15 * 1000);
}

This pattern (inner .catch() inside setInterval arrow) is already correct for no-floating-promises — the promise is handled. However no-floating-promises may fire on the .catch() return value itself (the chained promise is also a Promise). If so, the fix is:

setInterval(() => {
  void runOutboxDrain().catch((err: unknown) => {
    console.error('[outboxWorker] Unhandled runOutboxDrain error:', err);
  });
}, 15 * 1000);

The void operator here is legitimate: it signals intentional fire-and-forget within a setInterval that handles its own scheduling. This is NOT masking a bug.

For no-unsafe-member-access on ical.js in sync.ts (apps/api/src/broker/sync.ts, lines 99-125): the existing pattern uses as string | null casts on getFirstPropertyValue() returns. If the rule fires, the correct response is a targeted disable WITH the existing comment pattern:

// ical.js getFirstPropertyValue returns a union type; cast to ICAL.Time for date handling
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const dtstart = vevent.getFirstPropertyValue('dtstart') as ICAL.Time | null;

The file already has an inline comment at line 99 explaining the cast — the justification comment is already there. The eslint-disable-next-line line is the only addition required per D-13-06.


apps/pwa/src/components/EventForm.tsxno-explicit-any violation fix

Analog: the file already has an eslint-disable-next-line with justification comment at line 274 (the line immediately before the violation):

Existing pattern (EventForm.tsx, lines 271-275):

// (the API expand contract does not expose it in v1 — D-03). We cast to any to
// read it if a future API version adds it, and default to 'none' when not present
// (WR-03 v1 comment: occurrence edits whose recurrence is not in the cache default
// to 'none'; this will be addressed when the occurrence/expand contract is extended).
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const derivedRecurrence = (occurrence as any)?.recurrence as RecurrencePreset | undefined;

The eslint-disable-next-line comment is already present in the source. This file requires no change for the no-explicit-any rule — the suppression is pre-existing and already justified. Verify the comment is on the line immediately before the const derivedRecurrence line and that no other as any casts exist in the file.


Shared Patterns

typechecklint script mirroring

Source: all three package.json files Apply to: apps/api/package.json, apps/pwa/package.json

The typecheck scripts are the canonical pattern to copy for new per-package tool scripts:

  • One command per script
  • No wrapper (pnpm run prefix, extra flags)
  • Directory-scoped (not --ext, not glob patterns)

eslint-disable-next-line with justification comment

Source: apps/pwa/src/components/EventForm.tsx lines 271-275 Apply to: any suppression required in apps/api/src/broker/sync.ts or apps/api/src/broker/expand.ts

Pattern: multi-line comment block explaining WHY the rule is wrong here, immediately followed by // eslint-disable-next-line <rule-name>, immediately followed by the flagged line. No blank lines between comment, disable, and code.

CI step insertion

Source: .gitea/workflows/ci.yml lines 31-38 Apply to: new Format check step

Pattern: - name: Verb noun (title case, imperative verb), run: pnpm <script>. No working-directory override for root scripts. No env: block needed for lint/format.


No Analog Found

File Role Data Flow Reason
eslint.config.js config First ESLint config in the repo; use RESEARCH.md skeleton
.prettierrc config First Prettier config in the repo; use RESEARCH.md defaults
.prettierignore config First Prettier ignore in the repo; use RESEARCH.md content

Metadata

Analog search scope: /home/luc/Projects/familysync/package.json, apps/api/package.json, apps/pwa/package.json, .gitea/workflows/ci.yml, apps/api/src/broker/{outboxWorker,poller,reminderScheduler,sync}.ts, apps/pwa/src/components/EventForm.tsx Files scanned: 10 Pattern extraction date: 2026-06-11


PATTERN MAPPING COMPLETE

Phase: 13 - Real Lint Gate (ESLint) Files classified: 9 Analogs found: 6 / 9

Coverage

  • Files with exact analog: 6 (3 × package.json scripts, ci.yml step, broker setInterval pattern, EventForm disable pattern)
  • Files with role-match analog: 0
  • Files with no analog: 3 (eslint.config.js, .prettierrc, .prettierignore — greenfield)

Key Patterns Identified

  • All package-level tool scripts follow the typecheck pattern: single command, directory-scoped, no wrappers — copy exactly for lint scripts
  • CI steps follow - name: Verb noun / run: pnpm <script> — new Format check step slots between Lint and Typecheck
  • All three broker workers already use setInterval(() => { runX().catch(...) }) correctly; if no-floating-promises fires on the .catch() return, add void before runX() (legitimate signal, not a mask)
  • eslint-disable-next-line suppression pattern is pre-established in EventForm.tsx with a multi-line justification block — use the same structure for any ical.js unsafe-access suppressions in broker files

File Created

/home/luc/Projects/familysync/.planning/phases/13-real-lint-gate-eslint/13-PATTERNS.md

Ready for Planning

Pattern mapping complete. Planner can now reference analog patterns in PLAN.md files.