Files
familysync/.planning/milestones/v1.1-phases/13-real-lint-gate-eslint/13-PATTERNS.md
T
2026-06-18 22:21:38 -04:00

279 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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):
```json
"lint": "pnpm -r --if-present lint",
"typecheck": "pnpm -r typecheck"
```
**Apply the same style** for new scripts:
```json
"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):
```json
"typecheck": "tsc --noEmit"
```
**Pattern to mirror** — same position in the scripts block, same style:
```json
"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):
```json
"typecheck": "tsc --noEmit && tsc --project tsconfig.e2e.json --noEmit"
```
**Pattern to mirror:**
```json
"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):
```yaml
- 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):
```yaml
- 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):
```yaml
# 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/*.ts` — `no-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):
```ts
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:
```ts
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:
```ts
// 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.tsx` — `no-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):
```ts
// (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
### `typecheck` → `lint` 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.