From 403be990f39ef283c79cf2e7b3123aaa4919b3bd Mon Sep 17 00:00:00 2001 From: "bert.hausmans" Date: Thu, 30 Apr 2026 23:06:22 +0200 Subject: [PATCH] docs(ws3): add session 1c audit report (boundaries plugin) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- dev-docs/WS-3-SESSION-1C-AUDIT.md | 335 ++++++++++++++++++++++++++++++ 1 file changed, 335 insertions(+) create mode 100644 dev-docs/WS-3-SESSION-1C-AUDIT.md diff --git a/dev-docs/WS-3-SESSION-1C-AUDIT.md b/dev-docs/WS-3-SESSION-1C-AUDIT.md new file mode 100644 index 00000000..42f5810d --- /dev/null +++ b/dev-docs/WS-3-SESSION-1C-AUDIT.md @@ -0,0 +1,335 @@ +# 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.1–C.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`. +- 0–1 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.