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>
20 KiB
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)
'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 |
|---|---|---|
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 ofuseNotificationStoreline 3 anduseOrganisationStoreline 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-Idheader fromuseOrganisationStore). - 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 → storesin the matrix. Pragmatic; matches current code. The boundaries rule reflects reality:libandstoresare both infrastructure layers andlib/axios.tsis 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-justifiedlib → storeimport in any other lib file. Mitigation: keep the rule; add a comment inaxios.tsdocumenting it's the deliberate exception. - B. Disallow
lib → stores; convert the 2 static imports to dynamic. Refactorlib/axios.tslines 3-4 to remove the static imports andawait import('@/stores/...')inside each interceptor callback (matching the auth flow's pattern). Two-file edit (justaxios.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 (theuseOrganisationStorecall is on every API request, not just error paths). - C. Disallow
lib → stores; extract the seam to a new zone. Make a thinlib/http-bindings.ts(or move axios fully to a newservices/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/andpages/(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: extendboundaries/elementswith{ 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:
- The proposed matrix in §2 is acceptable (or amend it).
- Q1 (lib → stores): A, B, or C?
- Q2 (views/ ignored): yes/no?
- Q3 (navigation imports): keep as proposed (
types, utils) or tighten? - 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 lintexit 0,pnpm buildsucceeds,pnpm testremains 49 passed, no file inapps/portal//api//packages//docs/modified.