Files
crewli/dev-docs/WS-3-SESSION-1C-AUDIT.md
bert.hausmans 403be990f3 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>
2026-04-30 23:06:22 +02:00

20 KiB
Raw Permalink Blame History

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 pagesdisallow 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)

'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)

'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)

'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:

{ 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
componentspages 0 clean
composablescomponents 0 clean
storescomponents 0 clean
libutils 0 (utils currently empty of consumers)
utils → any internal 0 true leaf zone
types → non-types 0 only peer-types imports
componentslayouts 0 clean
pages → peer pages 0 clean
libstores 2 the open-question case — see Q1
composablesstores 2 allowed by proposed matrix (rationale §2)
pluginsstores 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):

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.