Files
crewli/dev-docs/WS-3-LINT-BASELINE-AUDIT-2026-04-29.md
bert.hausmans f44bb969c9 docs: WS-3 session 1b-i lint baseline audit report
Categorises remaining apps/app eslint problems after the Tier 1 +
Tier 2 autofix and whitespace cleanup. Six buckets:
- X. Tier 3 deferred (vite.config.ts, themeConfig.ts, vitest.config.ts) — 134
- A. Trivial-fix (second-pass autofix residue + unused imports) — 42
- B. Type safety (mostly no-explicit-any in vendored Vuexy code) — 34
- C. Style preference / rule-config — 8
- D. Vuetify / Vuexy idiom (ml-/pl- restricted-class) — 5
- E. Bug-shaped (security, isAxiosError, missing-return, fire-and-forget) — 8
Sum check: 134 + 42 + 34 + 8 + 5 + 8 = 231 ✓

Five open questions for Bert + Claude Chat decisions before 1b-ii:
- Should src/@core/** + src/@layouts/** be in ignorePatterns?
- vue/no-restricted-class regex scope (RTL-aware vs ml/pl only)?
- Stance on vue/prefer-true-attribute-shorthand?
- Bucket E.5 axios fire-and-forget pattern (void / .catch / disable)?
- Decoupling pnpm lint from --fix?

Lint baseline: 1451 → 231 this session, with Tier 3 (134) +
non-fixable (66) + second-pass-residue (31) deferred to 1b-ii.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 11:15:16 +02:00

522 lines
21 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.
# WS-3 Lint baseline audit — apps/app
**Date:** 2026-04-29
**Source:** `pnpm exec eslint . -c .eslintrc.cjs --ext .ts,.js,.cjs,.vue,.tsx,.jsx --no-fix`
after WS-3 session 1b-i Tasks 1, 2, 3 (Tier 1 + Tier 2 autofix +
trailing-whitespace cleanup).
## Counts
| Stage | Total | Errors | Warnings | Fixable |
|-------|-------|--------|----------|---------|
| Pre-1b-i (main, dd8430f) | 1451 | 919 | 532 | 1370 |
| Post-Tier 1 (47bd533) | 422 | 408 | 14 | 354 |
| Post-Tier 2 (a7eaf0f) | 246 | 232 | 14 | 180 |
| Post-whitespace (4976b4e) | 231 | 217 | 14 | 165 |
Reduction: **1451 → 231** mechanical, **with Tier 3 deferred**.
The 165 still-fixable items break down as:
- **134 in Tier 3 files** (vite.config.ts, themeConfig.ts,
vitest.config.ts) — explicitly excluded from this session per
the risk-tier policy.
- **31 in Tier 1/2 paths** — second-pass-autofix residue (mainly
24 `indent` errors in one file, plus `lines-around-comment`
edge cases on Vue SFC `<script>`-tag-adjacent comments).
## Bucket summary
| Bucket | Count | Recommended disposition |
|--------|------:|-------------------------|
| X. Tier 3 deferred | 134 | 1b-ii, hand-reviewed per file |
| A. Trivial-fix | 42 | 1b-ii, mechanical (second-pass --fix or simple delete) |
| B. Type safety | 34 | 1b-ii, per-instance review |
| C. Style / rule-config | 8 | 1b-ii, rule-level decisions |
| D. Vuetify / Vuexy idiom | 5 | 1b-ii, per-pattern overrides |
| E. Bug-shaped | 8 | Investigate as bugs |
| **Sum** | **231** | |
Sum check: X(134) + A(42) + B(34) + C(8) + D(5) + E(8) = **231**
(matches total).
---
## Bucket X — Tier 3 deferred (134 items, all fixable)
All in three configuration files, all autofix-mechanical. Excluded
from this session per the risk-tier policy because the autofix could
in principle alter Vite plugin order, theme-token resolution, or
vitest setup-hook ordering.
```
apps/app/vite.config.ts — 91 items
58 @typescript-eslint/quotes
15 semi
15 @typescript-eslint/semi
2 arrow-parens
1 curly
apps/app/themeConfig.ts — 42 items
28 @typescript-eslint/quotes
7 semi
7 @typescript-eslint/semi
apps/app/vitest.config.ts — 1 item
1 lines-around-comment
```
**Recommendation for 1b-ii:** per-file diff review before applying
`--fix`. Focus areas:
- `vite.config.ts`: confirm no plugin-order changes (the autofix is
pure quotes/semi/arrow-parens; in principle no semantic effect).
Smoke `pnpm dev` and `pnpm build` after.
- `themeConfig.ts`: confirm no theme-token reordering. Visually
check the running app afterwards (login + dashboard).
- `vitest.config.ts`: trivial — single `lines-around-comment` fix.
---
## Bucket A — Trivial-fix (42 items)
### A.1 Second-pass autofix residue (31 items, all fixable)
Items where Tier 1 / Tier 2 `--fix` ran but ESLint's default 10-pass
cap left a cascade of dependent fixes unresolved. Re-running `--fix`
on the same file resolves them (verified in Task 3 on
`DefaultLayoutWithVerticalNav.vue`).
```
By rule:
24 indent (all in useTimeSlotDropdown.ts)
3 vue/max-attributes-per-line (App.vue — outside Tier 1 glob)
3 lines-around-comment (PortalLayout/PublicLayout/AppKpiCard — see A.2)
1 (no rule) (VNodeRenderer.tsx — eslint-disable comment that's now unused)
By file:
24 apps/app/src/composables/useTimeSlotDropdown.ts
3 apps/app/src/App.vue
1 apps/app/src/@layouts/components/VNodeRenderer.tsx
1 apps/app/src/components/AppKpiCard.vue
1 apps/app/src/layouts/PortalLayout.vue
1 apps/app/src/layouts/PublicLayout.vue
```
**Recommendation for 1b-ii:**
- Re-run `pnpm exec eslint <file> --fix` on each (single command per
file is safest) and verify `git diff --check` clean afterwards.
- For App.vue: it's at `src/App.vue`, missed by Tier 1's
`src/{components,pages,layouts,views}/**/*.vue` glob. Fix by
including `src/App.vue` in the Tier 1 paths in the next pass.
- For VNodeRenderer.tsx (`src/@layouts/`): vendored Vuexy code; the
fix is just removing an `eslint-disable` directive that's no longer
needed. Low risk.
### A.2 Vue-SFC `lines-around-comment` autofix gap (3 of the 31 above)
`PortalLayout.vue`, `PublicLayout.vue`, and `AppKpiCard.vue` show
`error Expected line before comment lines-around-comment` at line 2:1
because the comment block sits immediately after `<script setup
lang="ts">` on line 1. The rule has a fixer and the files were inside
the Tier 1 glob, but the fix did not apply — likely a vue-eslint-parser
boundary quirk (the script-tag opening doesn't register as a "block
start" the way a `{` does for `allowBlockStart: true`).
**Recommendation for 1b-ii:** either (a) hand-add a blank line before
the comment in each file, or (b) extend `.eslintrc.cjs` with an
overrides block that disables `lines-around-comment` for `*.vue` files
(or sets `allowBlockStart: true` more aggressively).
### A.3 Unused-imports / unused-vars (10 items, manual)
Trivial deletes. Each item below: file:line, the unused identifier,
and recommended action.
```
apps/app/src/components/layout/OrganisationSwitcher.vue:30:10
'toggleMenu' is defined but never used.
→ Delete the destructured `toggleMenu` from the line 30 destructure.
(Both unused-imports/no-unused-vars and @typescript-eslint/no-unused-vars
flag the same identifier — count of 2 above is the same identifier.)
apps/app/src/components/sections/CreateShiftDialog.vue:40:9
'scenario' is assigned a value but never used.
→ Delete the `const scenario = ...` line.
(Two-rule overlap — same identifier.)
apps/app/src/pages/events/[id]/time-slots/index.vue:242:27
'event' is defined but never used. (vue/no-unused-vars)
→ Replace with `_event` in the slot scope, or remove from destructure.
apps/app/src/pages/organisation/companies.vue:8:7
'authStore' is assigned a value but never used.
→ Delete `const authStore = useAuthStore()` on line 8.
(Two-rule overlap.)
apps/app/src/pages/platform/activity-log/index.vue:11:7
'searchDebounced' is assigned a value but never used.
→ Delete the line. (Two-rule overlap.)
apps/app/src/components/persons/PersonDetailPanel.vue:77:58
Unnecessary { after 'if' condition. (curly)
→ Strip the redundant brace block (one-statement if).
```
Note on the rule overlap: `unused-imports/no-unused-vars` and
`@typescript-eslint/no-unused-vars` both fire on the same identifier
in 4 of the 5 cases. Removing the identifier will resolve both per
report — the **42 Bucket A total** correctly counts these as 2 items
each (matching ESLint's actual emit). Effective unique deletes: 6.
---
## Bucket B — Type safety (34 items)
### B.1 `@typescript-eslint/no-explicit-any` (23 items)
Per CLAUDE.md frontend rule and `.eslintrc.cjs` line 62, this is
**erroneously committed** to `error` for the project. Yet 23 instances
exist, predominantly in `src/@core/` and `src/@layouts/` (the vendored
Vuexy code) and a few in our own files.
**Distribution:**
- `src/@core/**`: 13 (DropZone.vue, I18n.vue, Notifications.vue,
AppDateTimePicker.vue, createUrl.ts, useCookie.ts ×4,
apexCharConfig.ts ×3, types.ts)
- `src/@layouts/**`: 7 (index.ts ×1, types.ts ×6)
- `src/layouts/blank.vue`: 1 (line 9 — `ref<any>`)
- `src/layouts/default.vue`: 1 (line 21 — `ref<any>`)
- `src/layouts/components/NavSearchBar.vue`: 1
The 13 + 7 = 20 inside `@core` and `@layouts` are vendored Vuexy
template code. CLAUDE.md says: "Never: TypeScript `any` type" — but
this rule was clearly intended for OUR code, not the vendored copy
of Vuexy that we don't maintain.
**Recommendation for 1b-ii — judgment call needed:**
- **Option a** (preferred for honesty): add `src/@core/**` and
`src/@layouts/**` to `ignorePatterns` in `.eslintrc.cjs`. This is
the same pragma we already apply to `src/plugins/iconify/*.js`
(vendored). Reduces the 23 down to 3 (blank.vue, default.vue,
NavSearchBar.vue) — all `ref<any>` for the `AppLoadingIndicator`
template ref. Those 3 should be properly typed.
- **Option b**: keep the rule on, narrow the 20 vendored items to
proper types one by one. This is real work and risks breaking the
vendored components' contract.
- **Option c**: per-file `// eslint-disable-next-line` markers on the
vendored `any`s. Noisy but localised.
The `.eslintrc.cjs` already says (lines 60-62):
```
// Project rule (CLAUDE.md frontend rules): no `any`. Override the
// Vuexy reference (which sets this off) — Crewli's stricter posture.
'@typescript-eslint/no-explicit-any': 'error',
```
So the choice was deliberate when the rule was set. Option a is the
cleanest re-statement: "the ban applies to our code, not vendored."
### B.2 `@typescript-eslint/no-use-before-define` (7 items)
All in `src/components/shifts/ShiftDetailPanel.vue` (lines 249-261).
The pattern: handler functions defined at the top of `<script setup>`
reference `cancellingAssignment` and `isCancelDialogOpen` refs
declared further down in the file.
```
apps/app/src/components/shifts/ShiftDetailPanel.vue:249:3
'cancellingAssignment' was used before it was defined.
[...6 more in lines 250-261]
```
This is idiomatic Composition API: declare reactive state at the top,
then handlers, then watchers. The rule fires because handler
*functions* are hoisted but the `ref(...)` call results aren't.
**Recommendation for 1b-ii:** either (a) reorder the file to declare
the refs before the functions that reference them (mechanical, same
file only), or (b) add an override clause to `.eslintrc.cjs` allowing
this pattern in `<script setup>`. Option a is simpler and one-file.
### B.3 `@typescript-eslint/no-shadow` (1 item)
```
apps/app/src/stores/useImpersonationStore.ts:119:11
'stored' is already declared in the upper scope on line 18 column 9.
```
Local `stored` shadowing an outer `stored` — usually fine but
defensive practice is to rename. **Recommendation:** rename the
inner `stored` to `storedSnapshot` or similar.
### B.4 `camelcase` (3 items, same identifier)
```
apps/app/src/composables/api/useFormSchemas.ts:97:30
apps/app/src/composables/api/useFormSchemas.ts:99:17
apps/app/src/composables/api/useFormSchemas.ts:99:36
Identifier 'confirmed_name' is not in camel case.
```
Context (lines 95-100):
```ts
return useMutation({
mutationFn: async ({ id, confirmed_name }: { id: string; confirmed_name?: string }) => {
await apiClient.delete(`/organisations/${orgId.value}/forms/schemas/${id}`, {
params: confirmed_name ? { confirmed_name } : undefined,
})
},
```
`confirmed_name` is an **API query parameter** that the backend reads
in snake_case. CLAUDE.md says: "DB columns: snake_case" and
"TypeScript / JS variables: camelCase". The boundary is at the API
layer — the query-param key MUST be `confirmed_name` (snake) on the
wire. The local TypeScript variable `confirmed_name` matches the
wire format for symmetry (rather than `confirmedName` aliased).
**Recommendation for 1b-ii — judgment call:** rename the local
variable to `confirmedName` and keep the wire key snake_case
(`{ confirmed_name: confirmedName }`). Three rule violations resolved
with no behaviour change.
---
## Bucket C — Style / rule-config (8 items)
```
apps/app/src/components/form-failures/DismissFailureDialog.vue:43:3
sonarjs/prefer-single-boolean-return
"Replace this if-then-else flow by a single return statement."
apps/app/src/components/form-failures/FormFailureDetail.vue:44:5
no-void
"Expected 'undefined' and instead saw 'void'."
apps/app/src/components/sections/AssignShiftDialog.vue:41:3
sonarjs/prefer-single-boolean-return
apps/app/src/components/sections/SectionsShiftsPanel.vue:333:11
vue/prefer-true-attribute-shorthand
"Boolean prop with 'true' value should be written in shorthand form."
apps/app/src/components/shifts/AssignPersonDialog.vue:120:5
sonarjs/no-collapsible-if
"Merge this if statement with the nested one."
apps/app/src/components/shifts/AssignPersonDialog.vue:125:5
sonarjs/no-collapsible-if
apps/app/src/pages/events/[id]/settings/registration-fields.vue:335:13
vue/prefer-true-attribute-shorthand
apps/app/src/stores/useImpersonationStore.ts:105:12
sonarjs/no-collapsible-if
```
All readable-style preferences. **Recommendation for 1b-ii:** apply
the suggested rewrite per item; all are local single-file edits with
no behaviour change.
---
## Bucket D — Vuetify / Vuexy idiom (5 items)
```
apps/app/src/components/persons/PersonDetailPanel.vue:272:25
vue/no-restricted-class — "'ml-1' class is not allowed."
apps/app/src/components/sections/SectionsShiftsPanel.vue:357:25
vue/no-restricted-class — "'ml-1' class is not allowed."
apps/app/src/components/shifts/AssignPersonDialog.vue:461:29
vue/no-restricted-class — "'pl-4' class is not allowed."
apps/app/src/components/shifts/AssignPersonDialog.vue:475:23
vue/no-restricted-class — "'ml-auto' class is not allowed."
apps/app/src/components/shifts/AssignPersonDialog.vue:500:27
vue/no-restricted-class — "'ml-1' class is not allowed."
```
The rule (`.eslintrc.cjs` line 176) bans `m{l,r}-` and `p{l,r}-`
Tailwind utility-class names because the project is Vuetify-only
("Use Vuetify utility class … never custom CSS" per CLAUDE.md).
Vuetify's spacing utilities are `ms-*`, `me-*`, `ps-*`, `pe-*` (start/
end LTR-aware), or `mx-*`/`px-*` for both sides.
**Recommendation for 1b-ii:** rewrite the 5 occurrences:
- `ml-1``ms-1`
- `pl-4``ps-4`
- `ml-auto``ms-auto`
Vuetify supports both `m{l,r,s,e}-N` directly; the project preference
per the rule is the start/end form. Mechanical 5-line change across
3 files.
---
## Bucket E — Bug-shaped (8 items)
### E.1 `import/no-named-as-default-member` × 2 — `EventTabsNav.vue`
```
apps/app/src/components/events/EventTabsNav.vue:53:8
apps/app/src/components/events/EventTabsNav.vue:76:9
"Caution: `axios` also has a named export `isAxiosError`. Check if
you meant to write `import { isAxiosError } from 'axios'` instead."
```
The file accesses `axios.isAxiosError(...)` via the default export
namespace; ESLint flags that there's a named export of the same name.
Both forms work, but the named import is the modern axios pattern and
narrower (no full default-axios import overhead).
**Recommendation for 1b-ii:** change `import axios from 'axios'` to
`import { isAxiosError } from 'axios'` (or add the named import
alongside the default). Two-line edit. Zero behaviour change.
### E.2 `vue/component-api-style` × 1 — vitest spec
```
apps/app/src/composables/api/__tests__/useFormFailures.spec.ts:38:5
"Options API is not allowed in your project. `render` function is
part of the Options API. Use Composition API instead."
```
A test mock uses Options-API `render` function. CLAUDE.md frontend
rules: "Always `<script setup lang='ts'>` — never the Options API".
**Recommendation for 1b-ii:** rewrite the mock as Composition API,
or add a per-spec-file override.
### E.3 `vue/return-in-computed-property` × 1 — useTimeSlotDropdown
```
apps/app/src/composables/useTimeSlotDropdown.ts:80:32
"Expected to return a value in computed function."
```
A `computed` callback is missing a return path on at least one
branch — a real bug-class. **Recommendation for 1b-ii:** read line 80
of `useTimeSlotDropdown.ts` and ensure every code path returns.
### E.4 `vue/no-template-target-blank` × 1 — security
```
apps/app/src/pages/organisation/index.vue:342:21
"Using target='_blank' without rel='noopener noreferrer' is a
security risk."
```
External anchor with `target="_blank"` and no `rel="noopener"`
classic reverse-tabnabbing exposure. **Recommendation:** add
`rel="noopener noreferrer"` to the anchor.
### E.5 `promise/no-promise-in-callback` × 3 — `axios.ts`
```
apps/app/src/lib/axios.ts:42:12
apps/app/src/lib/axios.ts:61:7
apps/app/src/lib/axios.ts:73:7
"Avoid using promises inside of callbacks."
```
In each case the response interceptor reaches into a dynamic `import()`
to lazy-load a store, then chains `.then()`. Pattern:
```ts
import('@/stores/useImpersonationStore').then(({ useImpersonationStore }) => {
const impersonationStore = useImpersonationStore()
impersonationStore.clearState()
window.location.href = '/platform'
})
```
The lint rule fires because the interceptor itself is a callback and
the promise inside isn't returned/awaited. In practice the
interceptor doesn't need to wait for the store dynamic-import (the
side effect — set window.location — is fire-and-forget). It's not a
real bug, but the rule is correctly identifying that errors inside
those `.then()` blocks would be silently swallowed.
**Recommendation for 1b-ii — judgment call:**
- **Option a:** wrap each in a `void` to mark the fire-and-forget
intent: `void import('@/stores/...').then(...)`. Tells ESLint and
future-readers that the promise discard is intentional.
- **Option b:** add `.catch(err => console.error(...))` to each so
errors aren't swallowed.
- **Option c:** disable the rule for `lib/axios.ts` only — but loses
defence everywhere else.
Option b is most defensive; option a is most honest about current
behaviour. Both are 3-line single-file edits.
---
## Action plan for session 1b-ii
| Step | Bucket | Effort | Risk | Notes |
|------|--------|--------|------|-------|
| 1. Tier 3 hand-review + autofix | X | 1-2h | medium | Smoke `pnpm dev` + `pnpm build` after vite.config.ts |
| 2. Bucket A second-pass autofix on Tier 1/2 paths | A.1 | trivial | low | Rerun `--fix` on the 6 listed files; verify `git diff --check` |
| 3. Bucket A unused-imports/vars manual deletes | A.3 | trivial | low | 6 unique deletes |
| 4. Bucket A `lines-around-comment` Vue SFC fix | A.2 | trivial | low | Either hand-add blank lines or override rule for `*.vue` |
| 5. Bucket B `no-explicit-any` ignore-patterns decision | B.1 | trivial config | low | Likely add @core/** + @layouts/** to ignorePatterns |
| 6. Bucket B `no-explicit-any` real fixes (3 in our code) | B.1 | low | low | blank.vue / default.vue / NavSearchBar.vue refs |
| 7. Bucket B `no-use-before-define` reorder | B.2 | low | low | Reorder ShiftDetailPanel.vue script setup |
| 8. Bucket B `no-shadow` rename | B.3 | trivial | low | useImpersonationStore.ts |
| 9. Bucket B `camelcase` rename | B.4 | low | low | useFormSchemas.ts — 3-line edit |
| 10. Bucket C style fixes | C | low | low | 8 mechanical rewrites |
| 11. Bucket D Vuetify class renames | D | low | low | ml-/pl-/etc → ms-/ps-/etc, 5 occurrences |
| 12. Bucket E.1 isAxiosError import | E.1 | trivial | low | One file, two lines |
| 13. Bucket E.2 spec rewrite to Composition API | E.2 | low | low | Test only |
| 14. Bucket E.3 fix missing return in computed | E.3 | low | medium | Real semantic bug — verify with smoke |
| 15. Bucket E.4 security: add rel="noopener" | E.4 | trivial | low | One attribute add |
| 16. Bucket E.5 axios fire-and-forget pattern | E.5 | low | low | 3 spots, design choice between options a/b/c |
After 1b-ii completes, the lint count should drop from 231 to a
small number (single digits, mostly Bucket-B-judgment-call leftovers
if any — depending on the @core/@layouts ignorePatterns decision).
---
## Open questions for Bert + Claude Chat
1. **Vendored Vuexy code under `src/@core/` and `src/@layouts/`**
should it be included in `.eslintrc.cjs`'s `ignorePatterns`? 20 of
the 23 `no-explicit-any` items would silently disappear, plus a
small number of other rules. The trade-off: future drift in those
directories goes uncaught. Counter-trade-off: spending time
correcting `any`s in code we copy-pasted is low value and high
churn.
2. **`vue/no-restricted-class` regex `/^(p|m)(l|r)-/`** — the rule
bans both `mr-` and `ml-` (and `pr-`, `pl-`). Vuetify supports
`mr-N` natively (it's not just a Tailwind name); the migration
to `me-N` is for LTR/RTL flexibility. Confirm this is the
intended ban, or relax it to `pl/ml` only if RTL isn't a
project goal.
3. **Bucket C `vue/prefer-true-attribute-shorthand`** — the rule
prefers `<comp prop>` over `<comp :prop="true">` for boolean
props. Some teams find the explicit form clearer when it
distinguishes from omitted. Confirm preference.
4. **Bucket E.5 axios callback-promises** — pick option a/b/c per
above before 1b-ii starts. Option b (`.catch`) is the most
defensive but introduces error-handling code; option a (`void`)
is the most honest about today's behaviour.
5. **`pnpm lint` script in `package.json`** — currently aliased to
`eslint . --fix`. After 1b-ii lands a clean baseline, consider
splitting into `pnpm lint` (no-fix) and `pnpm lint:fix` (with
--fix). The current aliasing is what made the original 105
reference number wrong (someone ran `pnpm lint` and reported
the post-fix remainder).
---
## Artifacts
- `/tmp/eslint-pre.json` — pre-1b-i baseline (1451 problems)
- `/tmp/eslint-current.json` — post-Task-3 state (231 problems)
Both retained on the dev machine for 1b-ii reference. Not committed.