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:
335
dev-docs/WS-3-SESSION-1C-AUDIT.md
Normal file
335
dev-docs/WS-3-SESSION-1C-AUDIT.md
Normal 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.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.
|
||||
Reference in New Issue
Block a user