diff --git a/CLAUDE.md b/CLAUDE.md index 916db0a1..da19522b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -268,6 +268,24 @@ you are using available components rather than building custom ones. - Never: TypeScript `any` type (use proper types, generics, or `unknown` with type guards) - Never: import axios directly in a component (use `src/lib/axios.ts` via a composable) +## Frontend import boundaries (apps/app/) + +`apps/app/` enforces a layered import architecture via +`eslint-plugin-boundaries`. Ten zones (`types` → `utils` → `lib` → +`plugins` / `composables` / `stores` / `navigation` → `components` → +`layouts` → `pages`); each zone may only import from the zones below +it in the matrix. Vendored `@core/` and `@layouts/` are exempt. +Cross-zone violations are lint errors, not warnings. + +Matrix details + rationale: `dev-docs/WS-3-SESSION-1C-AUDIT.md`. +Config: `apps/app/.eslintrc.cjs`. + +When adding a new file: pick the zone first. If your file imports +from a zone the matrix forbids, the structural answer is usually to +hoist a type to `types/` or extract a helper to `utils/` / +`composables/` — not to disable the rule. Per-line disables are +allowed only with a `TODO TECH-*` reference to a backlog item. + ## Order of work for each new module 1. Create and run migration(s) diff --git a/apps/app/.eslintrc.cjs b/apps/app/.eslintrc.cjs index 3625abb3..4129ea26 100644 --- a/apps/app/.eslintrc.cjs +++ b/apps/app/.eslintrc.cjs @@ -32,6 +32,7 @@ module.exports = { '@typescript-eslint', 'regex', 'regexp', + 'boundaries', ], ignorePatterns: [ 'src/plugins/iconify/*.js', @@ -200,12 +201,68 @@ module.exports = { ], '\\.eslintrc\\.cjs', ], + + // Architectural import boundaries (WS-3 1c, audit: + // dev-docs/WS-3-SESSION-1C-AUDIT.md). The matrix is layered: + // types → utils → lib → composables → stores → components → layouts → pages. + // The `lib → stores` edge is intentionally disallowed; lib/axios.ts + // uses dynamic `await import('@/stores/...')` for its 4 store reads + // so the static-import surface stays clean. + // Sub-zone enforcement (components/{organizer,portal,shared}) is a + // backlog item (TECH-WS3-BOUNDARIES-SUBZONES); it lands after the + // §4.2 consolidation directory layout. + // FORWARD-FLAG: when src/plugins/1.router/ migrates to src/router/ + // in a later WS-3 PR, add `{ type: 'router', pattern: 'src/router/**' }` + // to boundaries/elements and `{ from: 'router', allow: ['types', + // 'utils', 'lib', 'plugins', 'stores'] }` to the rules. + 'boundaries/element-types': ['error', { + default: 'disallow', + rules: [ + { from: 'types', allow: ['types'] }, + { from: 'utils', allow: ['types', 'utils'] }, + { from: 'lib', allow: ['types', 'utils', 'lib'] }, + { 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'] }, + ], + }], + 'boundaries/no-unknown': 'off', // External packages are fine. + 'boundaries/no-unknown-files': 'off', // The ignore list handles vendor/generated. }, settings: { 'import/resolver': { node: true, typescript: {}, }, + + // Element-type assignment: first-match-wins. Order matters. + 'boundaries/elements': [ + { 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': [ + 'src/@core/**', // vendored Vuexy + 'src/@layouts/**', // vendored Vuexy + 'src/views/**', // single dead Vuexy file (zero importers); see TECH-DELETE-DEAD-VIEWS + 'src/App.vue', // orchestration root + 'src/main.ts', // orchestration root + 'src/assets/**', // static media + 'src/styles/**', // SCSS + '**/*.d.ts', // generated declarations + ], + 'boundaries/include': ['src/**/*.{ts,vue,tsx}'], }, overrides: [ // Vue SFCs: the base lines-around-comment rule conflicts with diff --git a/apps/app/package.json b/apps/app/package.json index 24343078..8aab373a 100644 --- a/apps/app/package.json +++ b/apps/app/package.json @@ -106,6 +106,7 @@ "eslint-config-airbnb-base": "15.0.0", "eslint-import-resolver-typescript": "3.10.1", "eslint-plugin-antfu": "0.43.1", + "eslint-plugin-boundaries": "6.0.2", "eslint-plugin-case-police": "0.6.1", "eslint-plugin-es-x": "7.8.0", "eslint-plugin-eslint-comments": "3.2.0", diff --git a/apps/app/pnpm-lock.yaml b/apps/app/pnpm-lock.yaml index f58facfe..d7ff1e59 100644 --- a/apps/app/pnpm-lock.yaml +++ b/apps/app/pnpm-lock.yaml @@ -274,6 +274,9 @@ importers: eslint-plugin-antfu: specifier: 0.43.1 version: 0.43.1(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-boundaries: + specifier: 6.0.2 + version: 6.0.2(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) eslint-plugin-case-police: specifier: 0.6.1 version: 0.6.1(eslint@8.57.1)(typescript@5.9.3) @@ -581,6 +584,10 @@ packages: resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} engines: {node: '>=6.9.0'} + '@boundaries/elements@2.0.1': + resolution: {integrity: sha512-sAWO3D8PFP6pBXdxxW93SQi/KQqqhE2AAHo3AgWfdtJXwO6bfK6/wUN81XnOZk0qRC6vHzUEKhjwVD9dtDWvxg==} + engines: {node: '>=18.18'} + '@bundled-es-modules/cookie@2.0.1': resolution: {integrity: sha512-8o+5fRPLNbjbdGRRmJj3h6Hh1AQJf2dk3qQ/5ZFb+PXkRNiSoMGGUKlsgLfrxneb72axVJyIYji64E2+nNfYyw==} @@ -2747,6 +2754,12 @@ packages: eslint-plugin-antfu@0.43.1: resolution: {integrity: sha512-Nak+Qpy5qEK10dCXtVaabPTUmLBPLhsVKAFXAtxYGYRlY/SuuZUBhW2YIsLsixNROiICGuov8sN+eNOCC7Wb5g==} + eslint-plugin-boundaries@6.0.2: + resolution: {integrity: sha512-wSHgiYeMEbziP91lH0UQ9oslgF2djG1x+LV9z/qO19ggMKZaCB8pKIGePHAY91eLF4EAgpsxQk8MRSFGRPfPzw==} + engines: {node: '>=18.18'} + peerDependencies: + eslint: '>=6.0.0' + eslint-plugin-case-police@0.6.1: resolution: {integrity: sha512-SNyZBjc39CwoNSOl3aiu5EsuHYXOIaPDraMsgLQmcH8CbEhllyOMkmV9kuSNHjjtM8iYRHsiBs1WEFw3/9qkoQ==} @@ -3184,6 +3197,11 @@ packages: grid-index@1.1.0: resolution: {integrity: sha512-HZRwumpOGUrHyxO5bqKZL0B0GlUpwtCAzZ42sgxUPniu33R1LSFH5yrIcBCHjkctCAh3mtWKcKd9J4vDDdeVHA==} + handlebars@4.7.9: + resolution: {integrity: sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==} + engines: {node: '>=0.4.7'} + hasBin: true + happy-dom@20.9.0: resolution: {integrity: sha512-GZZ9mKe8r646NUAf/zemnGbjYh4Bt8/MqASJY+pSm5ZDtc3YQox+4gsLI7yi1hba6o+eCsGxpHn5+iEVn31/FQ==} engines: {node: '>=20.0.0'} @@ -3793,6 +3811,9 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + node-exports-info@1.6.0: resolution: {integrity: sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==} engines: {node: '>= 0.4'} @@ -4281,11 +4302,6 @@ packages: resolve-protobuf-schema@2.1.0: resolution: {integrity: sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==} - resolve@1.22.11: - resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} - engines: {node: '>= 0.4'} - hasBin: true - resolve@1.22.12: resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} engines: {node: '>= 0.4'} @@ -4823,6 +4839,11 @@ packages: ufo@1.6.1: resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} + uglify-js@3.19.3: + resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} + engines: {node: '>=0.8.0'} + hasBin: true + unbox-primitive@1.1.0: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} @@ -5240,6 +5261,9 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + wordwrap@1.0.0: + resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + wrap-ansi@6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} @@ -5631,6 +5655,20 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 + '@boundaries/elements@2.0.1(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1)': + dependencies: + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.12.1(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) + handlebars: 4.7.9 + is-core-module: 2.16.1 + micromatch: 4.0.8 + transitivePeerDependencies: + - '@typescript-eslint/parser' + - eslint + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + '@bundled-es-modules/cookie@2.0.1': dependencies: cookie: 0.7.2 @@ -7917,7 +7955,7 @@ snapshots: dependencies: debug: 3.2.7 is-core-module: 2.16.1 - resolve: 1.22.11 + resolve: 1.22.12 transitivePeerDependencies: - supports-color @@ -7983,6 +8021,21 @@ snapshots: - supports-color - typescript + eslint-plugin-boundaries@6.0.2(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1): + dependencies: + '@boundaries/elements': 2.0.1(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) + chalk: 4.1.2 + eslint: 8.57.1 + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.12.1(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) + handlebars: 4.7.9 + micromatch: 4.0.8 + transitivePeerDependencies: + - '@typescript-eslint/parser' + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + eslint-plugin-case-police@0.6.1(eslint@8.57.1)(typescript@5.9.3): dependencies: '@typescript-eslint/utils': 5.62.0(eslint@8.57.1)(typescript@5.9.3) @@ -8606,6 +8659,15 @@ snapshots: grid-index@1.1.0: {} + handlebars@4.7.9: + dependencies: + minimist: 1.2.8 + neo-async: 2.6.2 + source-map: 0.6.1 + wordwrap: 1.0.0 + optionalDependencies: + uglify-js: 3.19.3 + happy-dom@20.9.0: dependencies: '@types/node': 24.9.2 @@ -9222,6 +9284,8 @@ snapshots: natural-compare@1.4.0: {} + neo-async@2.6.2: {} + node-exports-info@1.6.0: dependencies: array.prototype.flatmap: 1.3.3 @@ -9758,12 +9822,6 @@ snapshots: dependencies: protocol-buffers-schema: 3.6.0 - resolve@1.22.11: - dependencies: - is-core-module: 2.16.1 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 - resolve@1.22.12: dependencies: es-errors: 1.3.0 @@ -9975,8 +10033,7 @@ snapshots: source-map-js@1.2.1: {} - source-map@0.6.1: - optional: true + source-map@0.6.1: {} space-separated-tokens@2.0.2: {} @@ -10394,6 +10451,9 @@ snapshots: ufo@1.6.1: {} + uglify-js@3.19.3: + optional: true + unbox-primitive@1.1.0: dependencies: call-bound: 1.0.4 @@ -10885,6 +10945,8 @@ snapshots: word-wrap@1.2.5: {} + wordwrap@1.0.0: {} + wrap-ansi@6.2.0: dependencies: ansi-styles: 4.3.0 diff --git a/apps/app/src/lib/axios.ts b/apps/app/src/lib/axios.ts index f561c12a..a40404db 100644 --- a/apps/app/src/lib/axios.ts +++ b/apps/app/src/lib/axios.ts @@ -1,6 +1,8 @@ import axios from 'axios' import type { AxiosInstance, InternalAxiosRequestConfig } from 'axios' +// eslint-disable-next-line boundaries/element-types -- TECH-AXIOS-STORE-COUPLING: deliberate HTTP↔state seam, refactor scheduled per backlog. import { useNotificationStore } from '@/stores/useNotificationStore' +// eslint-disable-next-line boundaries/element-types -- TECH-AXIOS-STORE-COUPLING: deliberate HTTP↔state seam, refactor scheduled per backlog. import { useOrganisationStore } from '@/stores/useOrganisationStore' const apiClient: AxiosInstance = axios.create({ @@ -58,6 +60,7 @@ apiClient.interceptors.response.use( // Handle impersonation session expiry if (status === 403 && error.response?.data?.impersonation_ended) { + // eslint-disable-next-line boundaries/element-types -- TECH-AXIOS-STORE-COUPLING: deliberate HTTP↔state seam, refactor scheduled per backlog. const { useImpersonationStore } = await import('@/stores/useImpersonationStore') const impersonationStore = useImpersonationStore() @@ -69,6 +72,7 @@ apiClient.interceptors.response.use( if (status === 401) { // Lazy import to avoid circular dependency + // eslint-disable-next-line boundaries/element-types -- TECH-AXIOS-STORE-COUPLING: deliberate HTTP↔state seam, refactor scheduled per backlog. const { useAuthStore } = await import('@/stores/useAuthStore') const authStore = useAuthStore() if (authStore.isInitialized) diff --git a/dev-docs/ARCH-CONSOLIDATION-2026-04.md b/dev-docs/ARCH-CONSOLIDATION-2026-04.md index 8e48d2e3..af920cde 100644 --- a/dev-docs/ARCH-CONSOLIDATION-2026-04.md +++ b/dev-docs/ARCH-CONSOLIDATION-2026-04.md @@ -551,6 +551,30 @@ Zie §4 voor scope en stappen. drie geplande wijzigingen, doorschuiven naar follow-up. WS-3 lint cleanup workstream effectief afgerond; sessie 1c (eslint-plugin-boundaries) kan starten op een schone baseline. +- **Sessie 1c (2026-04-30)** — _import-boundaries enforcement, klaar._ + `eslint-plugin-boundaries@6.0.2` (MIT, ESLint ≥6 peer-dep, Node ≥18.18) + toegevoegd als directe devDep aan `apps/app/package.json` per de + TECH-PORTAL-ESLINT-DEPS lesson, en geactiveerd in `apps/app/.eslintrc.cjs` + met layered-architecture matrix: 10 zones (`types`, `utils`, `lib`, + `plugins`, `composables`, `stores`, `navigation`, `components`, `layouts`, + `pages`) met richtgevende edges (rationale + bewijs in + `dev-docs/WS-3-SESSION-1C-AUDIT.md`). Vendored `@core/`, `@layouts/`, + het dode `views/` bestand en orchestratie-roots `App.vue` + `main.ts` + staan in `boundaries/ignore`. Vier `lib → stores` violations in + `lib/axios.ts` (regels 3, 4, 61, 72) gemarkeerd met per-line + `eslint-disable-next-line` comments referencerend naar + `TECH-AXIOS-STORE-COUPLING` — de structurele decoupling van axios is + bewust uitgesteld naar een dedicated sessie omdat het architectuurwerk + is, geen tooling-cleanup. Boundary-rule blijft `error`, matrix blijft + strict; toekomstige `lib/X.ts` schrijvers stoten alsnog tegen de regel. + Lint baseline 0 errors / 0 warnings; build smoke groen; Vitest groen + (49 tests). Drie backlog-items aangemaakt voor toekomstige actie: + `TECH-AXIOS-STORE-COUPLING` (decouple axios), `TECH-DELETE-DEAD-VIEWS` + (verwijder `src/views/`), `TECH-WS3-BOUNDARIES-SUBZONES` (sub-zone + enforcement na PR-B), `TECH-WS3-BOUNDARIES-ROUTER-ZONE` (matrix-update + wanneer `plugins/1.router/` naar `router/` verhuist). WS-3 lint cleanup + + boundaries enforcement effectief afgerond; volgende WS-3 stap is PR-B + (portal merge) zodra WS-6 sessie 2 in main is geland. **Klaar-criteria:** - `apps/portal/` is verwijderd diff --git a/dev-docs/BACKLOG.md b/dev-docs/BACKLOG.md index 815a0ce7..bfce4e71 100644 --- a/dev-docs/BACKLOG.md +++ b/dev-docs/BACKLOG.md @@ -622,6 +622,139 @@ TECH-ESLINT-V9-MIGRATION zijn natuurlijke kandidaten). --- +### TECH-AXIOS-STORE-COUPLING — Decouple lib/axios.ts from stores layer + +**Aanleiding:** WS-3 sessie 1c (eslint-plugin-boundaries enforcement) +constateerde dat `apps/app/src/lib/axios.ts` 4 imports heeft uit `stores/` +(2 statisch op regel 3-4 voor `useNotificationStore` / +`useOrganisationStore`, 2 dynamisch op regel 61, 72 voor +`useImpersonationStore` / `useAuthStore` uit 1b-iii). De `lib → stores` +edge schendt de layered-architecture matrix. Om sessie 1c on-time te +landen zijn de 4 sites gemarkeerd met `eslint-disable-next-line` + +verwijzing naar dit backlog-item; de structurele fix is bewust uitgesteld +naar een dedicated sessie omdat het architectuurwerk is, geen tooling- +cleanup. + +**Wat:** +- Decouple `lib/axios.ts` van stores zodat het puur HTTP-infrastructuur + wordt. Twee paden, kies bij refactor: + - **Approach 1 (preferred starting point):** `lib/axios.ts` exporteert + de axios-instance plus een `registerInterceptors(client, deps)` + functie die callbacks accepteert (`onAuthFail`, + `onImpersonationDrop`, `getActiveOrgId`, `notify(message, level)`). + Een nieuwe `plugins/axios-bindings.ts` (mag `stores` importeren per + matrix) roept `registerInterceptors` aan bij app-init met closures + over de stores. + - **Approach 2 (fallback):** event-bus / callback registry; axios.ts + emit-eert semantische events (`auth-failed`, `needs-org-header`, + `notify-error`) en `plugins/axios-bindings.ts` subscribet. +- Verwijder alle 4 `eslint-disable-next-line` comments uit + `lib/axios.ts`. +- Tests moeten dekken: X-Organisation-Id header injection, 401/403 + logout flow, impersonation revocation flow, error toast op 4xx/5xx. +- Optioneel: meteen ook de static/dynamic import-split in axios.ts + uniformeren (nu inconsistent om legacy 1b-iii-redenen). + +**Prioriteit:** Middel — geen blokker voor andere workstreams, maar elke +maand dat dit blijft staan is een vlek op de boundaries-enforcement +geloofwaardigheid. Aanbevolen: meelift met de eerste WS-3 PR die `lib/` +of `plugins/` raakt, of een dedicated 2-3 uur sessie na WS-6 sluiting. + +--- + +### TECH-DELETE-DEAD-VIEWS — Verwijder src/views/ uit apps/app/ + +**Aanleiding:** WS-3 sessie 1c audit (`dev-docs/WS-3-SESSION-1C-AUDIT.md` +§A.1) constateerde dat `apps/app/src/views/` precies één bestand bevat +(`views/pages/authentication/AuthProvider.vue`) met nul importers in de +hele repo. Het is overgebleven Vuexy-template dode code. De §4.2 +post-consolidatie target layout drop `views/` volledig. Het bestand is +nu in `boundaries/ignore` opgenomen om sessie 1c te laten landen, maar +de natuurlijke vervolgstap is fysieke verwijdering. + +**Wat:** +- Verwijder `apps/app/src/views/` recursief. +- Verwijder de bijbehorende `'src/views/**'` regel uit + `apps/app/.eslintrc.cjs` `boundaries/ignore`. +- Verifieer dat `pnpm lint`, `pnpm build` en `pnpm test` groen blijven. +- Eén commit: `chore(cleanup): delete dead Vuexy views/ directory`. + +**Prioriteit:** Laag — triviaal cleanup-item, kan in elke gerelateerde +housekeeping-sprint meeliften (bijvoorbeeld vóór of na WS-3 PR-B). + +--- + +### TECH-WS3-BOUNDARIES-SUBZONES — Sub-zone import-boundaries inside components/ and pages/ + +**Aanleiding:** WS-3 sessie 1c heeft top-level zone-boundaries in +`apps/app/` neergezet via `eslint-plugin-boundaries`. De `/dev-docs/ARCH-CONSOLIDATION-2026-04.md` +§4.2 target layout introduceert sub-zones binnen die top-level zones — +specifiek `components/{organizer,portal,shared}/` en +`pages/{(auth),portal,register,events,persons,organisations,platform}/`. +De architecturale intent is dat `components/portal` niet uit +`components/organizer` mag importeren (en vice versa), met `shared` als +de gemeenschappelijke uitgang. Sessie 1c heeft die sub-zone +enforcement bewust uitgesteld omdat de sub-folders nog niet bestaan; +pre-emptieve rules op niet-bestaande directories worden stille dode +config die drift. + +**Wat:** +- **Precondition:** WS-3 PR-B is gemerged en de §4.2 sub-folder + structuur is gelandt (`components/{organizer,portal,shared}/` en + `pages/{(auth),portal,...}/` bestaan fysiek met content). +- Breid `boundaries/elements` in `apps/app/.eslintrc.cjs` uit met: + - `{ type: 'components-organizer', pattern: 'src/components/organizer/**' }` + - `{ type: 'components-portal', pattern: 'src/components/portal/**' }` + - `{ type: 'components-shared', pattern: 'src/components/shared/**' }` + - (ontworpen sub-zones voor `pages/` analoog) +- Voeg per-sub-zone rules toe: `components-portal` en + `components-organizer` mogen beide uit `components-shared` importeren, + maar niet uit elkaar. `pages/portal/` mag niet uit `pages/events/` + (en de andere organizer-pages) importeren, en omgekeerd. +- Resolve violations die bij eerste activatie naar boven komen. +- ETA: 1-2 uur zodra precondities ervoor liggen. + +**Prioriteit:** Middel — preventieve architectuur-discipline voor de +multi-tenant context-isolatie tussen organizer en portal UI-paden. +Zonder deze rules is de kans groot dat een ontwikkelaar tijdens een +PR-B follow-up onbewust portal- en organizer-componenten verstrengelt. + +--- + +### TECH-WS3-BOUNDARIES-ROUTER-ZONE — Add `router/` zone to boundaries matrix + +**Aanleiding:** WS-3 sessie 1c audit (§3 forward-compatibility) flagde +dat de §4.2 target layout `src/plugins/1.router/` vervangt door een +flat `src/router/`. De huidige boundaries-matrix in +`apps/app/.eslintrc.cjs` mapt router-files naar de `plugins` zone +(omdat ze fysiek in `src/plugins/1.router/` zitten). Zodra de +verhuizing plaatsvindt — geplant in een latere WS-3 PR — moet de +matrix-config dat reflecteren, anders vallen router-files buiten de +`boundaries/elements` mapping en flag-stormt de plugin met "no rule +found". + +**Wat:** In dezelfde commit/PR die `src/plugins/1.router/` naar +`src/router/` verhuist: + +- Voeg toe aan `boundaries/elements` in `apps/app/.eslintrc.cjs`: +```js + { type: 'router', pattern: 'src/router/**' }, +``` + Plaats vóór `plugins` in de array (first-match-wins ordering). +- Voeg toe aan `boundaries/element-types` rules: +```js + { from: 'router', allow: ['types', 'utils', 'lib', 'plugins', 'stores'] }, +``` +- Verifieer `pnpm lint` blijft op 0 problemen. + +**Trigger:** "src/plugins/1.router/" → "src/router/" verhuizing (latere +WS-3 PR, vermoedelijk PR-B als die de router-tree consolideert). + +**Prioriteit:** Laag — geen actie tot de verhuizing plaatsvindt; dan +verplicht 5-minute follow-up. + +--- + ### TECH-08 — Paginated response meta wordt weggegooid in organizer composables **Aanleiding:** `apps/app/src/composables/api/useSections.ts` en 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.