Files
crewli/dev-docs/WS-3-SESSION-1C-AUDIT.md
bert.hausmans 403be990f3 docs(ws3): add session 1c audit report (boundaries plugin)
Phase A deliverable per the WS-3 1c session prompt. Read-only audit
establishing the proposed eslint-plugin-boundaries matrix from
filesystem evidence in apps/app/src/.

Findings summary:
- 14 zones inventoried; views/ contains a single dead Vuexy file
  (zero importers) and is recommended for ignore alongside @core/@layouts.
- Proposed matrix refines the prompt's starting-point with three
  evidence-based additions:
  * composables → stores (2 actual usages: api composables read auth)
  * plugins → stores (1 actual usage: router guards read auth + org)
  * pages → layouts (forward-compat with §4.2 route-meta layout binding)
- Forward-compat verified vs ARCH-CONSOLIDATION-2026-04.md §4.2
  sub-zones — all resolve cleanly under the proposed pattern set.
- Plugin: eslint-plugin-boundaries@6.0.2, MIT, peerDeps eslint>=6
  (compatible with v8.57.1), as direct devDep per TECH-PORTAL-ESLINT-DEPS.
- Estimated violation count: 0 if Q1=yes (allow lib→stores), 2 if Q1=no.

Four open questions for Bert before Phase B sign-off:
- Q1 lib→stores: allow/refactor/extract-seam? (recommendation: allow)
- Q2 views/ ignored: confirm
- Q3 navigation imports scope: keep types+utils as headroom?
- Q4 sub-zone enforcement = backlog (TECH-WS3-BOUNDARIES-SUBZONES)?

No .eslintrc.cjs or package.json edits in this commit. STOP at Phase B
per the prompt — Phase C executes only after Bert's sign-off in chat.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-04-30 23:06:22 +02:00

336 lines
20 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 Session 1c — Audit (Phase A)
**Date:** 2026-04-30
**Branch:** `ws3-session-1c-boundaries`
**Scope:** `apps/app/` only. `apps/portal/` is consolidated in WS-3 PR-B and gets boundaries config there.
**Plugin:** `eslint-plugin-boundaries` (architectural import boundaries via ESLint).
**Lint baseline at start:** 0 problems (per `pnpm lint` exit 0).
This document is the Phase A deliverable per the 1c session prompt. It
establishes the proposed boundaries matrix from filesystem evidence and
flags open questions for Bert before Phase C (implementation) starts.
## 1. Zone inventory (A.1)
Verified directly from `apps/app/src/`. Counts are `*.{ts,vue,tsx}`
files only, excluding `*.d.ts`.
| Zone | Files | Representative path | Responsibility |
| -------------- | ----: | ------------------------------------------------------- | ---------------------------------------------------------------------------- |
| `@core/` | 57 | (vendored Vuexy) | Vuexy reference code. Already in `ignorePatterns`. |
| `@layouts/` | 21 | (vendored Vuexy layout primitives) | Vuexy layout helpers. Already in `ignorePatterns`. |
| `assets/` | 0 | (images / svg only) | Static media. No `.ts`/`.vue` files. |
| `components/` | 79 | `components/AppLoadingIndicator.vue` | Vue components. Sub-folders by domain (account-settings, events, shifts, …).|
| `composables/` | 24 | `composables/useTimeSlotDropdown.ts` | Reusable Composition API logic. Includes `composables/api/*` (TanStack Query).|
| `layouts/` | 16 | `layouts/PublicLayout.vue` | Page-shell layouts. Includes `layouts/components/*` (navbar / footer pieces).|
| `lib/` | 5 | `lib/query-client.ts` | Low-level glue: `axios.ts`, `apiErrors.ts`, `query-client.ts`, helpers. |
| `navigation/` | 2 | `navigation/horizontal/index.ts` | Pure declarative menu config. No imports. |
| `pages/` | 35 | `pages/index.vue` | Route pages (file-based via `unplugin-vue-router`). |
| `plugins/` | 11 | `plugins/webfontloader.ts` | Vue plugin wiring (router guards, vuetify init, iconify, pinia, …). |
| `stores/` | 6 | `stores/useImpersonationStore.ts` | Pinia stores: auth, impersonation, notification, organisation, sectionsUi, shiftDetail. |
| `styles/` | 0 | (SCSS only) | Stylesheets. No `.ts`/`.vue` files. |
| `types/` | 19 | `types/formSchema.ts` | Pure TypeScript types and DTOs. |
| `utils/` | 3 | `utils/deviceFingerprint.ts` | Pure helpers: constants, paginationMeta, deviceFingerprint. |
| `views/` | 1 | `views/pages/authentication/AuthProvider.vue` | **Single dead Vuexy file** — zero importers in repo (verified via grep). |
Auto-generated `*.d.ts` files at `apps/app/` root and inside `src/`:
- `auto-imports.d.ts`, `components.d.ts`, `env.d.ts`, `shims.d.ts`,
`typed-router.d.ts` (apps/app root).
- `src/reset.d.ts` (manually-written, single line: `import '@total-typescript/ts-reset'`).
All `*.d.ts` are already excluded from lint via `ignorePatterns: ['*.d.ts']`.
The `boundaries/include` glob `src/**/*.{ts,vue,tsx}` (no `.d.ts`)
will exclude them automatically too.
Orchestration roots (special — must be in `boundaries/ignore`):
- `src/App.vue` (imports stores, @core helpers; the app root).
- `src/main.ts` (createApp + plugin registration).
## 2. Proposed boundaries matrix (A.2)
Refined from the prompt's starting-point table based on actual import
patterns observed in the codebase. Direction: **rows may import from
columns**.
| from \\ to | types | utils | lib | plugins | composables | stores | navigation | components | layouts | pages |
| -------------- | :---: | :---: | :-: | :-----: | :---------: | :----: | :--------: | :--------: | :-----: | :---: |
| `types` | ✓ | | | | | | | | | |
| `utils` | ✓ | ✓ | | | | | | | | |
| `lib` | ✓ | ✓ | ✓ | | | ?¹ | | | | |
| `plugins` | ✓ | ✓ | ✓ | ✓ | | ✓² | | | | |
| `composables` | ✓ | ✓ | ✓ | | ✓ | ✓³ | | | | |
| `stores` | ✓ | ✓ | ✓ | | ✓ | ✓ | | | | |
| `navigation` | ✓ | ✓ | | | | | ✓ | | | |
| `components` | ✓ | ✓ | ✓ | | ✓ | ✓ | | ✓ | | |
| `layouts` | ✓ | ✓ | ✓ | | ✓ | ✓ | ✓ | ✓ | ✓ | |
| `pages` | ✓ | ✓ | ✓ | | ✓ | ✓ | ✓ | ✓ | ✓ | |
`✓` on the diagonal = peer imports allowed (e.g. one component may import another).
External packages (`vue`, `vuetify`, `pinia`, `@tanstack/vue-query`,
`vue-router`, `@vueuse/*`, etc.) and the vendored zones (`@core/**`,
`@layouts/**`) are importable from anywhere. Encoded by leaving them
out of `boundaries/elements` and setting `boundaries/no-unknown: 'off'`.
### Diff vs prompt's starting-point matrix, with rationale
| Change | Rationale |
|---|---|
| `composables` may import `stores` (✓³) — **added** | Two API composables read auth state: `composables/api/useOrganisations.ts:4` and `composables/api/useAuth.ts:3` both import `useAuthStore`. Standard Composition-API pattern (composable = stateful logic that may read store-backed sources). Disallowing this would force a non-trivial refactor with no architectural benefit. |
| `plugins` may import `stores` (✓²) — **added** | Router guard reads auth + organisation: `plugins/1.router/guards.ts:2-3`. Auth guard genuinely needs the auth store. Disallowing forces a contortion (e.g. dynamic-import dance) with no benefit. |
| `pages` is allowed to import `layouts` (✓) — **added** | The post-consolidation layout uses route meta to bind `OrganizerLayout`/`PortalLayout`/`PublicLayout` per page; some pages may also reference layout types. Cheap, harmless, forward-compatible with §4.2. |
| `lib` may import `stores` (?¹) — **OPEN QUESTION** | `lib/axios.ts` currently has 2 static imports of stores: `useNotificationStore` (toast on errors) and `useOrganisationStore` (active-org header). See open question Q1 below. |
| `views` zone removed — **dropped** | A.1 found `views/` contains a single dead Vuexy file (`AuthProvider.vue`) with zero importers. Treat as vendored / ignore alongside `@core` and `@layouts`. The §4.2 target layout drops `views/` entirely. |
| `pages` may import peer `pages`**disallow** | 0 actual cases today (verified). Cross-page imports indicate routing logic missing or shared component not yet extracted; should fail loudly. Aligns with prompt's starting matrix. |
### `boundaries/elements` config (proposed)
```js
'boundaries/elements': [
// Order matters: first match wins.
{ type: 'types', pattern: 'src/types/**' },
{ type: 'utils', pattern: 'src/utils/**' },
{ type: 'lib', pattern: 'src/lib/**' },
{ type: 'plugins', pattern: 'src/plugins/**' },
{ type: 'composables', pattern: 'src/composables/**' },
{ type: 'stores', pattern: 'src/stores/**' },
{ type: 'navigation', pattern: 'src/navigation/**' },
{ type: 'components', pattern: 'src/components/**' },
{ type: 'layouts', pattern: 'src/layouts/**' },
{ type: 'pages', pattern: 'src/pages/**' },
],
```
### `boundaries/ignore` (proposed)
```js
'boundaries/ignore': [
'src/@core/**', // vendored Vuexy
'src/@layouts/**', // vendored Vuexy
'src/views/**', // single dead file (A.1)
'src/App.vue', // orchestration root
'src/main.ts', // orchestration root
'src/assets/**', // static media
'src/styles/**', // SCSS
'**/*.d.ts', // generated declarations
],
```
### `boundaries/element-types` rules (proposed, for Phase C)
```js
'boundaries/element-types': ['error', {
default: 'disallow',
rules: [
{ from: 'types', allow: ['types'] },
{ from: 'utils', allow: ['types', 'utils'] },
{ from: 'lib', allow: ['types', 'utils', 'lib' /*, 'stores' if Q1=yes */] },
{ from: 'plugins', allow: ['types', 'utils', 'lib', 'plugins', 'stores'] },
{ from: 'composables', allow: ['types', 'utils', 'lib', 'composables', 'stores'] },
{ from: 'stores', allow: ['types', 'utils', 'lib', 'composables', 'stores'] },
{ from: 'navigation', allow: ['types', 'utils', 'navigation'] },
{ from: 'components', allow: ['types', 'utils', 'lib', 'composables', 'stores', 'components'] },
{ from: 'layouts', allow: ['types', 'utils', 'lib', 'composables', 'stores', 'navigation', 'components', 'layouts'] },
{ from: 'pages', allow: ['types', 'utils', 'lib', 'composables', 'stores', 'navigation', 'components', 'layouts'] },
],
}],
```
## 3. Forward-compatibility check vs §4.2 (A.3)
The post-consolidation target layout (`/dev-docs/ARCH-CONSOLIDATION-2026-04.md`
§4.2) introduces sub-folders inside several existing zones. Verifying
the proposed config doesn't flag those as violations the moment they
land:
| §4.2 sub-zone | Resolves under proposed config to | Forward-compat? |
|-----------------------------------------------------------|-----------------------------------|:---------------:|
| `pages/(auth)/`, `pages/portal/`, `pages/register/`, `pages/events/`, `pages/persons/`, `pages/organisations/`, `pages/platform/` | `pages` (pattern `src/pages/**`) | ✓ |
| `components/{organizer,portal,shared}/` | `components` (pattern `src/components/**`) | ✓ |
| `composables/forms/` (replaces `packages/form-schema/`) | `composables` (pattern `src/composables/**`) | ✓ |
| `layouts/OrganizerLayout.vue` / `PortalLayout.vue` / `PublicLayout.vue` | `layouts` (already exists post-1a) | ✓ |
| `router/index.ts`, `router/routes.ts` (new top-level zone) | NOT in current config — see note below | n/a |
| `plugins/theme.ts` | `plugins` (pattern `src/plugins/**`) | ✓ |
**Note on the new `router/` zone.** The §4.2 target replaces
`src/plugins/1.router/` with a flat `src/router/`. When that move
happens (in a later WS-3 PR), `.eslintrc.cjs` will need a new entry:
```js
{ type: 'router', pattern: 'src/router/**' },
```
…with the rule `{ from: 'router', allow: ['types', 'utils', 'lib', 'plugins', 'stores'] }`.
**Not adding pre-emptively in 1c** because the directory doesn't exist
yet — pre-emptive rules become silent dead config that drifts. Will be
added in the same PR that moves the router files. Flagging here for
visibility.
**Sub-zone enforcement** (e.g. "components/portal must not import from
components/organizer") is **explicitly out of scope** for 1c per the
prompt and §4.2 layout-target. Backlogged below.
## 4. Plugin selection + install plan (A.4)
| Field | Value |
|---|---|
| Package | `eslint-plugin-boundaries` |
| Latest version | **6.0.2** |
| License | MIT (permissive ✓) |
| `peerDependencies` | `eslint: '>=6.0.0'` — compatible with our `eslint@8.57.1` ✓ |
| `engines` | `node: '>=18.18'` — compatible with local `v22.22.1` ✓ |
| Install style | exact pin (e.g. `"eslint-plugin-boundaries": "6.0.2"`), matching the rest of `apps/app/package.json` `eslint-plugin-*` entries |
| Install command | `pnpm add -D eslint-plugin-boundaries@6.0.2` from `apps/app/` |
| Direct devDep? | **YES** — same pattern as the 14 existing `eslint-plugin-*` entries; satisfies the `TECH-PORTAL-ESLINT-DEPS` lesson (Cursor's ESLint extension uses strict module resolution and silently fails on plugins reachable only via pnpm hoisting) |
## 5. Estimated violation count + top offenders (A.5)
`pnpm add --no-save` is not supported (pnpm rejects the flag), so the
fallback static-grep approach from the prompt was used. Static greps
covered all matrix-relevant cross-zone import directions.
**Under the proposed matrix** (Q1 = "yes, allow `lib → stores`"):
| Direction probed | Count | Notes |
|---|---:|---|
| `components``pages` | 0 | clean |
| `composables``components` | 0 | clean |
| `stores``components` | 0 | clean |
| `lib``utils` | 0 | (utils currently empty of consumers) |
| `utils` → any internal | 0 | true leaf zone |
| `types` → non-`types` | 0 | only peer-types imports |
| `components``layouts` | 0 | clean |
| `pages` → peer `pages` | 0 | clean |
| `lib``stores` | 2 | **the open-question case** — see Q1 |
| `composables``stores` | 2 | allowed by proposed matrix (rationale §2) |
| `plugins``stores` | 2 | allowed by proposed matrix (rationale §2) |
| Cross-zone relative imports (`../../`, `../zone/`) | 0 | All cross-zone imports use the `@/` alias |
**Final estimate**:
- If Q1 answer is **"yes"** (allow `lib → stores`): **0 violations**.
Phase C ships clean with no code moves.
- If Q1 answer is **"no"** (disallow): **2 violations**, both in
`apps/app/src/lib/axios.ts` (the static imports of
`useNotificationStore` line 3 and `useOrganisationStore` line 4).
Phase C must refactor — see Q1 below for two refactor sketches.
The codebase is exceptionally clean re: architectural boundaries.
Earlier WS-3 lint cleanup (1b-i/ii/iii) and the natural Composition-
API patterns kept the directional graph mostly acyclic. The only
non-trivial decision is the `lib ↔ stores` cross-cut.
## 6. Open questions for Bert
### Q1. `lib → stores`: allow, or refactor `lib/axios.ts`?
**Context.** `apps/app/src/lib/axios.ts` has two **static** imports of
stores (lines 3-4):
```ts
import { useNotificationStore } from '@/stores/useNotificationStore'
import { useOrganisationStore } from '@/stores/useOrganisationStore'
```
These are used in:
- The request interceptor (sets `X-Organisation-Id` header from
`useOrganisationStore`).
- The response interceptor (calls `notificationStore.show(...)` for
toast on 403/404/422/503/5xx/!response).
The same file already uses **dynamic** `await import('@/stores/...')`
for the auth/impersonation flows (the 1b-iii fix). So the pattern
"axios.ts reaches into stores" is already partially established —
just inconsistently.
**Three valid resolutions:**
- **A. Allow `lib → stores` in the matrix.** Pragmatic; matches
current code. The boundaries rule reflects reality: `lib` and
`stores` are both infrastructure layers and `lib/axios.ts` is the
cross-cutting HTTP-↔-state seam. **Phase C ships clean.**
Trade-off: the layered model becomes less strict — a future contributor
could put a less-justified `lib → store` import in any other lib
file. Mitigation: keep the rule; add a comment in `axios.ts`
documenting it's the deliberate exception.
- **B. Disallow `lib → stores`; convert the 2 static imports to
dynamic.** Refactor `lib/axios.ts` lines 3-4 to remove the static
imports and `await import('@/stores/...')` inside each interceptor
callback (matching the auth flow's pattern). Two-file edit (just
`axios.ts`). Resolves the boundary violation without a comment-as-
exception. Trade-off: every interceptor invocation pays the dynamic-
import cost (cached after first call, so amortized to zero). Slightly
uglier code (the `useOrganisationStore` call is on every API request,
not just error paths).
- **C. Disallow `lib → stores`; extract the seam to a new zone.**
Make a thin `lib/http-bindings.ts` (or move axios fully to a new
`services/` layer that's allowed to import stores). More invasive;
not warranted for two imports.
**Recommendation: A.** Keep the boundary loose for `lib → stores`,
ship clean, document the intent in the matrix's rule comment. The
strict-layering prize isn't worth the dynamic-import dance for code
that's been stable for months. If a future code-review sees a NEW
`lib/X.ts` that imports a store without the same axios-level
justification, the reviewer flags it; the lint rule isn't the only
gate.
### Q2. `views/` — confirm: ignore (treat as vendored)?
`views/` contains exactly one file (`views/pages/authentication/AuthProvider.vue`)
which has zero importers in the repo. It's Vuexy-template dead code.
The §4.2 target layout drops `views/` entirely. **Recommendation:
add `src/views/**` to `boundaries/ignore`.** Confirm.
(Tangent: a future cleanup PR could just delete `src/views/` outright.
Not in scope for 1c. Could be a `chore:` follow-up — flagging for
backlog awareness, not asking for sign-off here.)
### Q3. `navigation` allowed to import from where?
`navigation/horizontal/index.ts` and `navigation/vertical/index.ts`
currently import nothing — they are pure declarative menu data. The
proposed matrix allows `navigation → types, utils`. This is forward-
defensive: a future nav config might need a route-name type from
`types/`. **Confirm or tighten to "navigation imports nothing".**
(Default recommendation: keep `types, utils`. Cost = zero, headroom = real.)
### Q4. Sub-zone enforcement scope for the consolidation sprint
§4.2 introduces `components/{organizer,portal,shared}` and
`pages/{(auth),portal,register,...}`. The intent: enforce
"components/portal must not import from components/organizer" and
similar tenant-isolation rules. **Confirmed out of scope for 1c per
the prompt** — flagging for an explicit "yes, this is a backlog item
for after PR-B" sign-off. Proposed backlog entry:
> **TECH-WS3-BOUNDARIES-SUBZONES** — Sub-zone import boundaries
> inside `components/` and `pages/` (organizer ⛔ portal, shared ✓)
> for the post-consolidation §4.2 layout. Preconditions: WS-3 PR-B
> consolidation merged and the §4.2 sub-folder structure landed.
> Approach: extend `boundaries/elements` with `{ type: 'components-organizer', pattern: 'src/components/organizer/**' }`
> etc., and add per-sub-zone rules. ETA: 1-2h once preconditions met.
## 7. STOP — handoff to Phase B
This audit is read-only. No `.eslintrc.cjs` edit. No `package.json`
edit. No code edits.
**To proceed to Phase C**, Bert needs to confirm:
1. The proposed matrix in §2 is acceptable (or amend it).
2. Q1 (lib → stores): A, B, or C?
3. Q2 (views/ ignored): yes/no?
4. Q3 (navigation imports): keep as proposed (`types, utils`) or tighten?
5. Q4 (sub-zone enforcement = backlog item, not 1c work): confirm.
Once Bert signs off in chat, Phase C executes per the prompt's C.1C.7
sequence. Expected outcome:
- 1 commit: `chore(deps): add eslint-plugin-boundaries to apps/app`.
- 1 commit: `chore(tooling): enable eslint-plugin-boundaries in apps/app`.
- 01 commit: `refactor(apps/app): resolve N boundary violations`
(only if Q1 = B or C; otherwise omitted).
- 1 commit: `docs(ws3): record session 1c completion (boundaries enforcement)`.
- Acceptance: `pnpm lint` exit 0, `pnpm build` succeeds, `pnpm test`
remains 49 passed, no file in `apps/portal/`/`api/`/`packages/`/
`docs/` modified.