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>
This commit is contained in:
2026-04-30 23:06:22 +02:00
parent 37dac93da2
commit 403be990f3

View File

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