# 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.