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>
336 lines
20 KiB
Markdown
336 lines
20 KiB
Markdown
# 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.
|