# Phase 8: Stack Upgrade & Backend Decomposition — Research

**Researched:** 2026-06-07
**Domain:** FastAPI router decomposition, Python package decomposition, frontend API client decomposition, Vite 5→6 migration, requirements.txt pinning
**Confidence:** HIGH

---

<user_constraints>
## User Constraints (from CONTEXT.md)

### Locked Decisions

**Phase 7.1 — Session Revocation (Wave 1)**
- D-01: Phase 7.1 is absorbed into Phase 8 as the first wave.
- D-02: Phase 7.1 scope: `revoke_all_refresh_tokens()` gets a `skip_token_hash` param; wired into `change_password`, `enable_totp`, and `disable_totp` with `sessions_revoked` field in each response shape; audit log entries written for each revocation.
- D-03: Frontend toast implementation: create an empty `useToastStore` Pinia store stub during Phase 7.1. Phase 7.1 components (`SettingsAccountTab.vue`, `TotpEnrollment.vue`) call `toastStore.show(...)` when `sessions_revoked > 0`. Phase 10 fills in the full toast implementation — Phase 7.1 only creates the stub and wires the call sites.

**Backend Router Decomposition**
- D-04: Sub-routers MUST have NO prefix on `APIRouter()`. Parent prefix propagates. Any sub-router prefix causes doubled URL segments.
- D-05: `api/admin/` split: `users.py`, `quotas.py`, `ai.py` — exact names locked.
- D-06: `api/documents/` split: 4 sub-modules. Exact names are researcher/planner choice.
- D-07: `api/auth/` split: 4 sub-modules (login/tokens, TOTP, password management, session management). Module names must mirror `services/auth.py` logical groupings.
- D-08: Re-classify endpoint placement within `api/documents/` is researcher/planner choice.

**Shared Pydantic Schemas (CODE-08)**
- D-09: Pydantic models shared within a single package → `shared.py` inside that package.
- D-10: Pydantic models referenced by 2+ packages → `backend/api/schemas.py`.
- D-11: Validators currently defined inline in router files must migrate to appropriate `services/` module.

**Frontend API Client Decomposition (CODE-04)**
- D-12: `client.js` becomes HTTP transport layer + re-export barrel. `request()` and `noRefreshPaths` stay in `client.js`. Zero changes to any of the 35+ consumer files.
- D-13: `fetchWithRetry()` (consolidating 3 blob-download 401-retry duplicates) lives in `frontend/src/api/utils.js`. `client.js` re-exports it.
- D-14: Domain sub-modules: `documents.js`, `auth.js`, `admin.js`, `folders.js`, `shares.js`, `cloud.js`, `topics.js`.

**Frontend Dependencies (PERF-01)**
- D-15: Vite 5→6 is a major version bump requiring migration research.
- D-16: Researcher determines which PERF-01 packages need config wiring vs install-only.

**Backend Dependency Pinning**
- D-17: `requirements.txt` must be converted from floating `>=` ranges to exact `==` pins using currently-installed versions. No version changes.

### Claude's Discretion

- Exact file names for `api/documents/` sub-modules
- Exact file names for `api/auth/` sub-modules
- Re-classify endpoint placement within `api/documents/`
- Which validators in router files qualify for migration to `services/`
- Domain grouping of each `client.js` function
- Which PERF-01 packages need config wiring vs install-only

### Deferred Ideas (OUT OF SCOPE)

- Backend package version bumps (FastAPI, SQLAlchemy, etc.) — only exact-pin current versions
- Composition API migration (Vue components)
- Virtual scrolling, dark mode, folder reordering, multi-select batch ops
</user_constraints>

---

<phase_requirements>
## Phase Requirements

| ID | Description | Research Support |
|----|-------------|------------------|
| CR-01 | `change_password` revokes other sessions, returns `sessions_revoked` | ALREADY IMPLEMENTED in auth.py L518-536 — code exists, tests and toast needed |
| CR-02 | `enable_totp` revokes other sessions, returns `sessions_revoked` | ALREADY IMPLEMENTED in auth.py L613-636 — code exists, tests and toast needed |
| CR-03 | `disable_totp` revokes other sessions, returns `sessions_revoked` | ALREADY IMPLEMENTED in auth.py L659-682 — code exists, tests and toast needed |
| CODE-01 | `api/admin.py` (934L) decomposed into `api/admin/` package | Sub-module boundaries identified below |
| CODE-02 | `api/documents.py` (852L) decomposed into `api/documents/` package | Sub-module boundaries identified below |
| CODE-03 | `api/auth.py` (825L) decomposed into `api/auth/` package | Sub-module boundaries identified below |
| CODE-04 | `frontend/src/api/client.js` (635L) decomposed into domain modules | Function mapping identified below |
| CODE-08 | No duplicated Pydantic models across router files; shared schemas extracted | Cross-package models identified below |
| PERF-01 | Frontend deps bumped/added: vue@^3.5, vite@^6.4.3, @vueuse/core@^14.3, etc. | Migration analysis complete |
</phase_requirements>

---

## Summary

Phase 8 is a structural refactoring phase — zero behavior changes, all URL contracts preserved. The phase absorbs Phase 7.1 (session revocation) as Wave 1 before any decomposition work begins.

**Critical finding on Wave 1 (CR-01, CR-02, CR-03):** Reading `backend/api/auth.py` confirms that all three session-revocation calls (`revoke_all_refresh_tokens` with `skip_token_hash`) are **already implemented** in the codebase at lines 518, 613, and 663. The `sessions_revoked` field is already in all three response shapes. The Wave 1 backend work is complete. What remains for Wave 1 is: (1) tests verifying the revocation behavior, and (2) the `useToastStore` stub + call-site wiring in `SettingsAccountTab.vue` and `TotpEnrollment.vue`. The frontend components already have inline `sessionRevokedToast` refs with 5s auto-dismiss — D-03 says to replace these with `toastStore.show(...)` calls and a stub store.

**Critical finding on `CloudConnectionOut`:** This Pydantic model is defined in `api/admin.py` and imported by `api/cloud.py` at line 35. When `api/admin.py` is split into `api/admin/users.py`, `api/admin/quotas.py`, `api/admin/ai.py`, this import in `cloud.py` must update to the new location. Since `CloudConnectionOut` is used by 2+ packages (admin + cloud), it belongs in `api/schemas.py` per D-10.

**Backend decomposition:** The three monoliths have clear natural boundaries based on reading their full source. Sub-module boundaries are documented below with exact line ranges.

**Frontend decomposition:** 35+ consumer files import from `client.js` using three patterns: `import * as api`, named imports, and lazy `await import()`. The barrel re-export pattern preserves all patterns without touching consumer files.

**Vite 5→6 migration:** For this project's `vite.config.js` (only `plugins`, `build.target`, `server.proxy`), the migration is low-risk. One potentially required change: `resolve.conditions` defaults changed. Since there is no custom `resolve.conditions` in this config, the default behavior change may or may not affect the project — the safest approach is to test after bump before committing.

**Primary recommendation:** Execute in wave order — Wave 1 (CR tests + toast stub), Wave 2 (backend decomposition in parallel, CODE-04 independent), Wave 3 (pinning + PERF-01 deps).

---

## Architectural Responsibility Map

| Capability | Primary Tier | Secondary Tier | Rationale |
|------------|-------------|----------------|-----------|
| Router decomposition | API layer (`backend/api/`) | None | Pure structural split; no service layer involvement |
| Shared Pydantic schemas | API layer (`api/schemas.py`) | None | Response models live at API boundary |
| Validators migration | Service layer (`services/`) | None | CLAUDE.md rule: validators raise ValueError, not HTTPException |
| Frontend API decomposition | Frontend API layer (`src/api/`) | Stores (consumers) | Barrel pattern ensures stores see no change |
| `useToastStore` stub | Frontend store layer (`src/stores/`) | Components (callers) | Pinia stores own application state |
| requirements.txt pinning | Infrastructure | None | Reproducibility, no behavior change |
| Vite 6 upgrade | Frontend build | None | Build tooling only; no runtime behavior change |

---

## Wave 1: Session Revocation (CR-01, CR-02, CR-03) — BACKEND ALREADY COMPLETE

### What is already implemented

Reading `backend/api/auth.py` confirms all three revocation calls exist:

**`change_password` (L475-537):**
- `skip_token_hash` computed from `request.cookies.get("refresh_token")` at L516-517
- `revoke_all_refresh_tokens(session, current_user.id, skip_token_hash=skip_hash)` at L518
- `sessions_revoked` in response at L537: `return {"message": "Password updated", "sessions_revoked": revoked}`
- Audit log with `metadata_={"sessions_revoked": revoked}` at L526

**`enable_totp` (L579-636):**
- `skip_token_hash` computed at L613-614
- `revoke_all_refresh_tokens(session, current_user.id, skip_token_hash=skip_hash)` at L615
- `sessions_revoked` in response at L636: `return {"backup_codes": plain_codes, "sessions_revoked": revoked}`
- Audit log at L619 with `metadata_={"sessions_revoked": revoked}`

**`disable_totp` (L641-682):**
- `skip_token_hash` computed at L660-661
- `revoke_all_refresh_tokens(session, current_user.id, skip_token_hash=skip_hash)` at L662
- `sessions_revoked` in response at L682: `return {"message": "TOTP disabled", "sessions_revoked": revoked}`
- Audit log at L664 with `metadata_={"sessions_revoked": revoked}`

`revoke_all_refresh_tokens` signature in `services/auth.py` (L250-273):
```python
async def revoke_all_refresh_tokens(
    session: AsyncSession, user_id: uuid.UUID, skip_token_hash: Optional[str] = None
) -> int:
```
The `skip_token_hash` parameter already exists and is already implemented correctly.

### What Wave 1 actually needs to do

1. **Tests only (backend):** Write 3 new tests confirming the revocation behavior:
   - `test_change_password_revokes_other_sessions` — change password, verify other refresh tokens revoked, current session preserved
   - `test_enable_totp_revokes_other_sessions`
   - `test_disable_totp_revokes_other_sessions`

2. **Frontend `useToastStore` stub:** Create `frontend/src/stores/toast.js` with a stub `show()` method (Phase 10 fills in the implementation). The stub must export `useToastStore` and accept the same `show(message, type)` call shape that Phase 10 will implement.

3. **Frontend call-site rewiring:** In `SettingsAccountTab.vue` and `TotpEnrollment.vue`, replace the existing `sessionRevokedToast` ref + `setTimeout` pattern with `toastStore.show("Other sessions have been terminated.", "success")`. The existing inline toast HTML blocks in these two components can be removed since the stub store will handle display in Phase 10.

### Existing inline toast locations

`SettingsAccountTab.vue`:
- Lines 1-25: inline toast HTML (fixed top-right, `v-if="sessionRevokedToast"`)
- Line 211: `const sessionRevokedToast = ref(false)`
- Lines 225-228: `changePassword` sets toast, 5s timeout
- Lines 261-264: `disableTotp` sets toast, 5s timeout

`TotpEnrollment.vue`:
- Lines 1-23: inline toast HTML (inline, not fixed-position)
- `sessionRevokedToast` ref and `setTimeout` in `confirmEnrollment` at line 174-177

**Important:** The existing tests in `SettingsAccountTab.test.js` and `TotpEnrollment.test.js` already test for `sessions_revoked > 0` behavior and expect the toast text "Other sessions have been terminated." — these tests must still pass after the refactor, just via the store rather than the ref.

---

## Backend Router Decomposition

### api/admin.py → api/admin/ package

**Current structure (934L):**
- Response helpers: `_ai_config_to_dict()`, `_user_to_dict()` (L58-91)
- Pydantic models: `UserCreate`, `UserStatusUpdate`, `QuotaUpdate`, `UserAiConfigUpdate`, `SystemAiConfigUpdate`, `TestConnectionRequest`, `SystemTopicCreate`, `UserDeleteConfirm`, `CloudConnectionOut` (L96-223)
- Endpoints by functional area:

| Function | Endpoint | Target sub-module |
|---|---|---|
| `list_users`, `create_user`, `update_user_status`, `initiate_password_reset`, `delete_user` | `/api/admin/users*` | `users.py` |
| `get_user_quota`, `update_user_quota` | `/api/admin/users/{id}/quota` | `quotas.py` |
| `update_ai_config` (per-user) | `/api/admin/users/{id}/ai-config` | `users.py` |
| `create_system_topic` | `/api/admin/topics` | `users.py` (admin utility) or `ai.py` |
| `get_ai_config_models`, `test_ai_connection`, `get_ai_config`, `update_system_ai_config` | `/api/admin/ai-config*` | `ai.py` |

**Recommended sub-module assignment:**

`api/admin/users.py`: `list_users`, `create_user`, `update_user_status`, `initiate_password_reset`, `delete_user`, `update_ai_config` (per-user), `create_system_topic`
`api/admin/quotas.py`: `get_user_quota`, `update_user_quota`
`api/admin/ai.py`: `get_ai_config_models`, `test_ai_connection`, `get_ai_config`, `update_system_ai_config`

**Pydantic model placement:**
- `CloudConnectionOut` → `api/schemas.py` (used by both `api/admin/` and `api/cloud.py`)
- `UserCreate`, `UserStatusUpdate`, `UserAiConfigUpdate`, `UserDeleteConfirm` → `api/admin/users.py` (only used in users.py)
- `QuotaUpdate` → `api/admin/quotas.py` (only used in quotas.py)
- `SystemAiConfigUpdate`, `TestConnectionRequest` → `api/admin/ai.py` (only used in ai.py)
- `SystemTopicCreate` → `api/admin/users.py` (only used in create_system_topic)
- `_user_to_dict()` helper → `api/admin/shared.py` (used by users.py and quotas.py both need user info)
- `_ai_config_to_dict()` helper → `api/admin/ai.py` (only used by ai.py)

**Validators that must migrate to services/ (D-11):**

`UserCreate.password_strength` (L102-106): This `@field_validator` calls `validate_password_strength(v)` which already lives in `services/auth.py`. The validator itself is just a thin call-through — it is acceptable to keep it as-is in the Pydantic model since it delegates to the service. No migration needed for this one.

`QuotaUpdate.must_be_positive` (L116-120): This `@field_validator` validates `limit_bytes > 0`. This is a Pydantic input validation rule (belongs at API boundary), NOT business logic. Keep in model per CLAUDE.md: "Pydantic `@field_validator` used for complex field constraints." No migration needed.

`SystemAiConfigUpdate.provider_must_be_known` (L147-155) and `TestConnectionRequest.provider_must_be_known` (L172-179): These validate `provider_id` against `PROVIDER_DEFAULTS`. Since `PROVIDER_DEFAULTS` is an `ai/` layer concern, migrating this check to `services/ai_config.py` as `validate_provider_id(v: str) -> str` (raises `ValueError`) would be cleaner. The `@field_validator` in both models would then call `ai_config_service.validate_provider_id(v)`. This is the one migration that qualifies under D-11 since it's duplicated across two models.

`DocumentPatch.filename_no_path_separators` (documents.py L83-88): Security validator — belongs in the Pydantic model at the API boundary. No migration needed.

**Summary:** Only `provider_must_be_known` in `SystemAiConfigUpdate` + `TestConnectionRequest` qualifies for migration to `services/ai_config.py`. All other validators are appropriate Pydantic field constraints.

### api/documents.py → api/documents/ package

**Current structure (852L):**
- Helper: `_parse_range()` (L744-760) — used only by `stream_document_content`
- Constant: `_CLOUD_PROVIDERS` frozenset (L59)
- Pydantic models: `UploadUrlRequest`, `DocumentPatch` (L65-88)

| Endpoint | HTTP | Target sub-module |
|---|---|---|
| `request_upload_url` | POST /upload-url | `upload.py` |
| `upload_document` | POST /upload | `upload.py` |
| `confirm_upload` | POST /{id}/confirm | `upload.py` |
| `list_documents` | GET / | `crud.py` |
| `get_document` | GET /{id} | `crud.py` |
| `patch_document` | PATCH /{id} | `crud.py` |
| `delete_document` | DELETE /{id} | `crud.py` |
| `classify_document` | POST /{id}/classify | `crud.py` |
| `stream_document_content` | GET /{id}/content | `content.py` |

**Recommended sub-module assignment:**

`api/documents/upload.py`: `request_upload_url`, `upload_document`, `confirm_upload` — all deal with the presigned-URL + cloud upload flow
`api/documents/crud.py`: `list_documents`, `get_document`, `patch_document`, `delete_document`, `classify_document` — standard document CRUD + re-classify
`api/documents/content.py`: `stream_document_content`, `_parse_range` helper — content proxy + range header handling
`api/documents/shared.py`: `_CLOUD_PROVIDERS` frozenset, `UploadUrlRequest`, `DocumentPatch` — shared by upload.py and crud.py

Re-classify endpoint (`POST /api/documents/{id}/classify`) → `crud.py` (it operates on an existing document, same ownership check pattern as get/patch/delete).

### api/auth.py → api/auth/ package

**Current structure (825L):**

| Endpoint | HTTP | Target sub-module |
|---|---|---|
| `register`, `login`, `refresh_token`, `logout`, `logout_all`, `get_me`, `get_my_quota` | `/api/auth/*` | `tokens.py` |
| `totp_setup`, `enable_totp` | `/api/auth/totp/*` | `totp.py` |
| `password_reset_request`, `password_reset_confirm` | `/api/auth/password-reset*` | `password.py` |
| `change_password` | `/api/auth/change-password` | `password.py` |
| `disable_totp` | `/api/auth/totp` (DELETE) | `totp.py` |
| `get_my_preferences`, `update_my_preferences` | `/api/auth/me/preferences` | `tokens.py` (profile management) |

**Recommended sub-module assignment:**

`api/auth/tokens.py`: `register`, `login`, `refresh_token`, `logout`, `logout_all`, `get_me`, `get_my_quota`, `get_my_preferences`, `update_my_preferences` — token issuance, session management, profile
`api/auth/totp.py`: `totp_setup`, `enable_totp`, `disable_totp` — TOTP lifecycle
`api/auth/password.py`: `change_password`, `password_reset_request`, `password_reset_confirm` — password management
`api/auth/shared.py`: `_set_refresh_cookie`, `_user_dict`, `RegisterRequest`, `LoginRequest`, `ChangePasswordRequest`, `TotpEnableRequest`, `PasswordResetRequest`, `PasswordResetConfirmRequest`, `PreferencesUpdate`, `limiter` (the Limiter instance) — shared across all sub-modules

**JTI/ES256/fgp placement:** These live entirely in `services/auth.py` and `deps/auth.py` — not in the router files. No placement decision needed for decomposition.

### main.py updates required

Current imports from monoliths:
```python
from api.auth import limiter as auth_limiter    # L21 — used for app.state.limiter
from api.documents import router as documents_router  # L22
from api.auth import router as auth_router      # L320
from api.admin import router as admin_router    # L321
```

After decomposition, each package's `__init__.py` aggregates sub-routers and exports `router`. `main.py` only changes to import from `api/auth/__init__.py`, `api/documents/__init__.py`, `api/admin/__init__.py`. The `limiter` from `api.auth` becomes `api.auth.shared.limiter` or re-exported from `api/auth/__init__.py`.

### Circular import risk analysis

**Risk 1: `api/cloud.py` imports `CloudConnectionOut` from `api/admin.py`**
- Current: `from api.admin import CloudConnectionOut` (L35 of cloud.py)
- After split: `api/admin/__init__.py` no longer directly defines it
- Fix: Move `CloudConnectionOut` to `api/schemas.py` BEFORE splitting admin.py. Update cloud.py to `from api.schemas import CloudConnectionOut`. Both packages import from schemas — no circular dependency.

**Risk 2: `backend/main.py` imports `limiter` from `api/auth.py`**
- Current: `from api.auth import limiter as auth_limiter` (L21)
- After split: `limiter` moves to `api/auth/shared.py`
- Fix: `api/auth/__init__.py` re-exports `limiter`. `main.py` import unchanged.

**Risk 3: `celery_app.py` import constraints**
- Celery task modules never import from router modules (ARCHITECTURE.md constraint)
- The new sub-packages must not be imported by `tasks/` or `celery_app.py`
- The tasks deferred-import pattern (e.g., `from tasks.email_tasks import send_reset_email`) must stay as local imports inside function bodies, not at module top level

**Risk 4: `tests/conftest.py` imports `auth_limiter`**
- `backend/tests/conftest.py` L222: `from api.auth import limiter as auth_limiter`
- After split: update to wherever `limiter` lands (via `api.auth` re-export, this stays unchanged)

---

## Frontend API Client Decomposition (CODE-04)

### client.js function inventory and domain mapping

**Functions that stay in `client.js` (transport layer):**
- `request()` (L11-57) — HTTP transport, Bearer injection, 401-refresh-retry
- `noRefreshPaths` (L26) — skip-refresh list

**`frontend/src/api/utils.js` (new, per D-13):**
The three blob-download 401-retry functions share identical structure — they bypass `request()` to return a raw `Response` instead of JSON. They should be consolidated into `fetchWithRetry(url, options, downloadHandler)`:

1. `adminExportAuditLogCsv` (L428-471) — fetch CSV, 401-retry, Blob download
2. `adminDownloadDailyExport` (L492-529) — fetch CSV, 401-retry, Blob download
3. `fetchDocumentContent` (L552-581) — fetch content, 401-retry, return raw Response

These three share identical authentication injection + 401-retry logic. The `fetchWithRetry` helper in `utils.js` handles the auth+retry boilerplate; each function provides its own `downloadHandler` or just returns the Response.

**Domain sub-module assignments:**

`api/documents.js`:
- `listDocuments`, `getDocument`, `deleteDocument`, `deleteDocumentRemoveOnly`
- `classifyDocument`, `getUploadUrl`, `confirmUpload`, `uploadToCloud`
- `fetchDocumentContent`, `getDocumentContentUrl`

`api/auth.js`:
- `login`, `register`, `refreshToken`, `logout`, `logoutAll`, `getMe`
- `changePassword`, `totpSetup`, `totpEnable`, `totpDisable`
- `passwordResetRequest`, `passwordResetConfirm`
- `getMyPreferences`, `updateMyPreferences`
- `getMyQuota`

`api/admin.js`:
- `adminListUsers`, `adminCreateUser`, `adminDeactivateUser`, `adminReactivateUser`
- `adminResetUserPassword`, `adminGetUserQuota`, `adminUpdateQuota`, `adminUpdateAiConfig`, `adminDeleteUser`
- `getAiConfig`, `saveAiConfig`, `testAiConnection`, `getAiModels`
- `adminListAuditLog`, `adminExportAuditLogCsv`, `adminListDailyExports`, `adminDownloadDailyExport`

`api/folders.js`:
- `listFolders`, `createFolder`, `getFolder`, `renameFolder`, `deleteFolder`, `moveDocument`

`api/shares.js`:
- `createShare`, `updateSharePermission`, `listShares`, `deleteShare`, `getSharedWithMe`

`api/cloud.js`:
- `listCloudConnections`, `disconnectCloud`, `connectWebDav`, `updateDefaultStorage`
- `getCloudFolders`, `initiateOAuth`, `getConnectionConfig`

`api/topics.js`:
- `listTopics`, `createTopic`, `updateTopic`, `deleteTopic`, `suggestTopics`

### Consumer import inventory

35+ consumer files import from `client.js` using three patterns:

**Pattern 1: `import * as api from '...api/client.js'`** (most common — namespace import)
- `stores/auth.js`, `stores/documents.js`, `stores/folders.js`, `stores/topics.js`, `stores/cloudConnections.js`
- `SettingsAccountTab.vue`, `SettingsPreferencesTab.vue`, `TotpEnrollment.vue`
- `AppSidebar.vue`, `AuditLogTab.vue`, `AdminAiConfigTab.vue`, `AdminQuotasTab.vue`, `AdminUsersTab.vue`
- `CloudProviderTreeItem.vue`, `CloudFolderTreeItem.vue`, `CloudCredentialModal.vue`
- `FolderTreeItem.vue`, `DocumentView.vue`, `SharedView.vue`, `CloudFolderView.vue`, `AccountView.vue`
- `NewPasswordView.vue`, `PasswordResetView.vue`

**Pattern 2: Named imports `import { funcName } from '...api/client.js'`**
- `SearchableModelSelect.vue`: `import { getAiModels }`
- `SettingsCloudTab.vue`: `import { initiateOAuth }`
- `AdminAiConfigTab.vue`: also `import { getAiConfig, saveAiConfig, testAiConnection }`
- `DocumentCard.vue`: `import { moveDocument, classifyDocument }`
- `DocumentPreviewModal.vue`: `import { fetchDocumentContent }`
- `DocumentView.vue`: also `import { fetchDocumentContent }` (in addition to `* as api`)

**Pattern 3: Lazy `await import('../stores/auth.js')`** (inside `request()` itself, stays in client.js)

The barrel re-export in `client.js` must re-export every function from every domain module so that all three patterns continue to work unchanged:

```javascript
// client.js (after decomposition)
// ... request() and noRefreshPaths stay here ...
export * from './documents.js'
export * from './auth.js'
export * from './admin.js'
export * from './folders.js'
export * from './shares.js'
export * from './cloud.js'
export * from './topics.js'
export { fetchWithRetry } from './utils.js'
```

---

## PERF-01: Frontend Dependencies Analysis

### Currently installed vs. required

| Package | Currently installed | PERF-01 requires | Action |
|---|---|---|---|
| `vue` | 3.5.34 | `^3.5.0` | Already satisfies — no change |
| `vite` | 5.4.21 | `^6.4.3` | Upgrade required |
| `@vitejs/plugin-vue` | 5.2.4 | Must match Vite 6 | Upgrade to 6.x required |
| `@vueuse/core` | not installed | `^14.3.0` | Install required |
| `@vueuse/integrations` | not installed | `^14.3.0` | Install required |
| `sortablejs` | not installed | `^1.15.7` | Install required |
| `@tailwindcss/forms` | not installed | `^0.5.11` | Install required |
| `rollup-plugin-visualizer` | not installed (dev) | `^7.0.1` | Install required (devDep) |
| `@types/sortablejs` | not installed (dev) | latest | Install required (devDep) |

[VERIFIED: npm registry] — all package versions confirmed via `npm view <pkg> version`.

### Vite 5→6 migration for this project

[CITED: https://v6.vite.dev/guide/migration]

**Breaking changes that affect this project's `vite.config.js`:**

The current `vite.config.js` is minimal:
```javascript
export default defineConfig({
  plugins: [vue()],
  build: { target: 'esnext' },
  server: { host: '0.0.0.0', port: 5173, proxy: { '/api': { target: 'http://backend:8000', changeOrigin: true } } },
})
```

1. **`resolve.conditions` default values changed:** Defaults are no longer added internally for custom condition configs. Since this project has NO custom `resolve.conditions`, this change does NOT affect the project. No config change needed.

2. **`json.stringify` defaults to `'auto'`:** This project does not configure `json.stringify`. The `'auto'` default (stringifies only large JSON files) is safe — no change needed.

3. **CSS output filenames (library mode):** This is not a library mode build. No impact.

4. **Sass modern API default:** This project uses Tailwind (PostCSS), not Sass. No impact.

5. **`@vitejs/plugin-vue` must bump to 6.x:** Vite 6 requires `@vitejs/plugin-vue@^6.0.0`. The current `^5.0.0` is incompatible. [VERIFIED: npm registry] `@vitejs/plugin-vue@6.0.7` is the latest 6.x release.

**Config changes required:**
- Bump `vite` to `^6.4.3`
- Bump `@vitejs/plugin-vue` to `^6.0.7`
- No `vite.config.js` content changes required for this minimal config

**Config changes NOT required:**
- `build.target: 'esnext'` — still valid in Vite 6
- `server.proxy` — unchanged
- No `resolve.conditions` customization needed

### @tailwindcss/forms: config wiring required

Adding `@tailwindcss/forms` requires a one-line change to `tailwind.config.js`:
```javascript
import forms from '@tailwindcss/forms'
export default {
  content: ['./index.html', './src/**/*.{vue,js}'],
  theme: { extend: {} },
  plugins: [forms],
}
```
This is used by VISUAL-02 in Phase 11. Installing in Phase 8 and wiring the plugin is the right time since VISUAL-02 needs it.

### Package config wiring summary

| Package | Install-only | Config wiring needed | Config file |
|---|---|---|---|
| `vue@^3.5.0` | Already installed | None | — |
| `vite@^6.4.3` | Yes (with plugin-vue bump) | Minimal: test after install | `package.json` only |
| `@vitejs/plugin-vue@^6.0.7` | Yes | None | `package.json` only |
| `@vueuse/core@^14.3.0` | Yes | None | — |
| `@vueuse/integrations@^14.3.0` | Yes | None | — |
| `sortablejs@^1.15.7` | Yes | None | — |
| `@tailwindcss/forms@^0.5.11` | No | Yes | `tailwind.config.js` |
| `rollup-plugin-visualizer@^7.0.1` | No | Yes | `vite.config.js` (for PERF-02) |
| `@types/sortablejs` | Yes (dev) | None | — |

`rollup-plugin-visualizer` is needed for PERF-02 (bundle analysis) in Phase 11. Install now, wire in Phase 11 when measurements are taken.

---

## requirements.txt: Exact Pinning (D-17)

**Current format:** All floating `>=` ranges.
**Required format:** Exact `==` pins at currently-installed versions.

Installed versions confirmed via `pip3 index versions`:

| Package | Current constraint | Pin to |
|---|---|---|
| `fastapi` | `>=0.111` | `==0.128.8` |
| `uvicorn[standard]` | `>=0.29` | [ASSUMED] — run `pip show uvicorn` to get exact |
| `python-multipart` | `>=0.0.27` | [ASSUMED] — run `pip show python-multipart` |
| `pydantic-settings` | `>=2.2` | [ASSUMED] — run `pip show pydantic-settings` |
| `pydantic[email]` | `>=2.0` | [ASSUMED] — run `pip show pydantic` |
| `anthropic` | `>=0.95.0` | `==0.104.0` (INSTALLED from pip3 output) |
| `openai` | `>=1.30` | [ASSUMED] — run `pip show openai` |
| `PyMuPDF` | `>=1.26.7` | [ASSUMED] — run `pip show PyMuPDF` |
| `python-docx` | `>=1.1` | [ASSUMED] — run `pip show python-docx` |
| `pytesseract` | `>=0.3` | [ASSUMED] — run `pip show pytesseract` |
| `Pillow` | `>=10.3` | [ASSUMED] — run `pip show Pillow` |
| `aiofiles` | `>=23.2` | [ASSUMED] — run `pip show aiofiles` |
| `httpx` | `>=0.27` | [ASSUMED] — run `pip show httpx` |
| `pytest` | `>=8.2` | [ASSUMED] — run `pip show pytest` |
| `pytest-asyncio` | `>=1.3.0` | [ASSUMED] — run `pip show pytest-asyncio` |
| `sqlalchemy[asyncio]` | `>=2.0.49` | `==2.0.49` (INSTALLED) |
| `psycopg[binary]` | `>=3.3.4` | `==3.2.13` (INSTALLED — note: 3.2.x series) |
| `alembic` | `>=1.18.4` | `==1.16.5` (INSTALLED — note: installed version lower than constraint) |
| `minio` | `>=7.2.20` | [ASSUMED] — run `pip show minio` |
| `celery[redis]` | `>=5.5.0` | `==5.6.3` (INSTALLED) |
| `redis` | `>=4.6.0` | [ASSUMED] — run `pip show redis` |
| `aiosqlite` | `>=0.20.0` | [ASSUMED] — run `pip show aiosqlite` |
| `PyJWT` | `>=2.8.0` | `==2.13.0` (INSTALLED) |
| `pwdlib[argon2]` | `>=0.2.1` | [ASSUMED] — run `pip show pwdlib` |
| `pyotp` | `>=2.9.0` | [ASSUMED] — run `pip show pyotp` |
| `slowapi` | `>=0.1.9` | [ASSUMED] — run `pip show slowapi` |
| `cryptography` | `>=41.0.0` | `==48.0.0` (INSTALLED) |
| `google-auth-oauthlib` | `>=1.3.1` | [ASSUMED] — run `pip show google-auth-oauthlib` |
| `google-api-python-client` | `>=2.196.0` | [ASSUMED] — run `pip show google-api-python-client` |
| `msal` | `>=1.36.0` | [ASSUMED] — run `pip show msal` |
| `webdavclient3` | `>=3.14.7` | [ASSUMED] — run `pip show webdavclient3` |
| `cachetools` | `>=5.3.0` | [ASSUMED] — run `pip show cachetools` |
| `structlog` | `>=25.5.0` | `==25.5.0` (INSTALLED) |

**Implementation:** The planner must include a task that runs `pip show <pkg> | grep Version` for all [ASSUMED] packages to get exact versions, then writes the pinned `requirements.txt`.

**Alembic discrepancy:** `requirements.txt` specifies `>=1.18.4` but `pip3 index versions` shows `1.16.5` installed. This is the installed version — D-17 says pin to currently installed. Pin to `==1.16.5`.

---

## Architecture Patterns

### Package __init__.py aggregation pattern

Every new package must have an `__init__.py` that aggregates sub-routers into a single exported `router` object with NO prefix:

```python
# api/admin/__init__.py
from fastapi import APIRouter
from api.admin.users import router as users_router
from api.admin.quotas import router as quotas_router
from api.admin.ai import router as ai_router

router = APIRouter()
router.include_router(users_router)
router.include_router(quotas_router)
router.include_router(ai_router)
```

The parent prefix is set in `main.py`:
```python
app.include_router(admin_router, prefix="/api/admin", tags=["admin"])
```

Sub-routers have NO prefix — they just define routes without path prefix:
```python
# api/admin/users.py
router = APIRouter()  # NO prefix here

@router.get("/users")  # becomes /api/admin/users via parent
async def list_users(...):
    ...
```

**PITFALL:** `api/auth.py` currently sets `router = APIRouter(prefix="/api/auth", tags=["auth"])`. When decomposed, the parent `api/auth/__init__.py` must set the prefix, and sub-routers must have NO prefix.

**PITFALL for api/auth:** Currently `main.py` imports `limiter` from `api.auth` for rate limit state. After decomposition, `api/auth/shared.py` defines the limiter, `api/auth/__init__.py` re-exports it: `from api.auth.shared import limiter`. main.py import `from api.auth import limiter as auth_limiter` continues to work.

### api/schemas.py pattern

New file for cross-package Pydantic models:
```python
# api/schemas.py
from pydantic import BaseModel, field_validator
from typing import Optional
from datetime import datetime

class CloudConnectionOut(BaseModel):
    """Moved from api/admin.py — used by api/cloud.py and api/admin/."""
    id: str
    provider: str
    display_name: str
    status: str
    connected_at: datetime
    server_url: Optional[str] = None
    connection_username: Optional[str] = None
    model_config = {"from_attributes": True}

    @field_validator("id", mode="before")
    @classmethod
    def coerce_id_to_str(cls, v) -> str:
        return str(v)
```

**Update required in `api/cloud.py`:** Change `from api.admin import CloudConnectionOut` to `from api.schemas import CloudConnectionOut`.

### Frontend barrel re-export pattern

```javascript
// src/api/client.js (after decomposition)
async function request(path, options = {}) { /* unchanged */ }

const noRefreshPaths = ['/api/auth/login', '/api/auth/register', '/api/auth/refresh']

// Re-export all domain modules — preserves all 3 consumer import patterns
export * from './documents.js'
export * from './auth.js'
export * from './admin.js'
export * from './folders.js'
export * from './shares.js'
export * from './cloud.js'
export * from './topics.js'
export { fetchWithRetry } from './utils.js'
```

Domain modules use `request` from a shared location. Since `request` is not exported from `client.js`, domain modules need access to it. Two options:
1. Move `request` to `utils.js` and import from there in both `client.js` and domain files
2. Each domain file imports `request` from `client.js` — but this creates a circular dependency if `client.js` re-exports from domain files

**Correct approach (avoids circular imports):** Move `request` to `utils.js`. Domain files import from `utils.js`. `client.js` imports `request` from `utils.js` for its own internal use, and `export * from './domain.js'`.

```javascript
// src/api/utils.js
export async function request(path, options = {}) { /* ... */ }
export async function fetchWithRetry(url, onResponse, _retry = false) { /* consolidates 3 blob patterns */ }
```

```javascript
// src/api/documents.js
import { request } from './utils.js'
export function listDocuments(...) { return request('/api/documents?...') }
```

```javascript
// src/api/client.js
import { request } from './utils.js'  // for noRefreshPaths compatibility if needed
export * from './documents.js'
export * from './auth.js'
// etc.
```

---

## Don't Hand-Roll

| Problem | Don't Build | Use Instead | Why |
|---|---|---|---|
| Python package aggregation | Custom router merger | Standard FastAPI `include_router` | Built-in, prefix-aware, tag-aware |
| JS barrel re-export | Manually re-listing exports | `export * from './module.js'` | ES module spec, tree-shakeable |
| Blob download with auth | New fetch wrapper | Consolidate into `fetchWithRetry` in `utils.js` | 3 existing implementations share identical auth logic |
| requirements.txt pinning | pip freeze (wrong: includes dev deps) | `pip show <pkg>` per package | Avoids polluting with dev/transitive deps |

---

## Common Pitfalls

### Pitfall 1: Sub-router prefix doubling

**What goes wrong:** Adding `prefix="/api/admin"` to sub-router AND the parent `include_router` call doubles the URL: `/api/admin/api/admin/users`.
**Why it happens:** Developers copy the existing `router = APIRouter(prefix="/api/admin", tags=["admin"])` pattern into sub-routers.
**How to avoid:** Sub-routers must have `router = APIRouter()` with NO prefix. Only the `include_router` call in `main.py` carries the prefix.
**Warning signs:** Test suite returns 404 on all decomposed endpoints.

### Pitfall 2: Circular import via __init__.py

**What goes wrong:** `api/admin/__init__.py` imports from `api/admin/users.py` which imports from `api/admin/__init__.py` (e.g., shared helpers).
**Why it happens:** Putting shared helpers in `__init__.py` instead of `shared.py`.
**How to avoid:** All shared helpers go in `api/admin/shared.py`. `__init__.py` only does router aggregation.
**Warning signs:** `ImportError: cannot import name 'X' from partially initialized module`.

### Pitfall 3: `api/cloud.py` import breaks after admin split

**What goes wrong:** `api/cloud.py` imports `from api.admin import CloudConnectionOut`. After splitting, `CloudConnectionOut` is no longer in `api/admin/__init__.py`.
**Why it happens:** Not updating `cloud.py` when moving `CloudConnectionOut` to `api/schemas.py`.
**How to avoid:** Move `CloudConnectionOut` to `api/schemas.py` FIRST, update `cloud.py` import, verify tests pass, THEN split admin.py.

### Pitfall 4: JS circular import (request in client.js)

**What goes wrong:** Domain files import `request` from `client.js`, but `client.js` re-exports from domain files → circular dependency. Vite may silently produce undefined exports.
**Why it happens:** Keeping `request` in `client.js` while also re-exporting from domain files that need `request`.
**How to avoid:** Move `request` to `utils.js`. Domain files import from `utils.js`. `client.js` re-exports from domain files only.

### Pitfall 5: ES module `export *` name collision

**What goes wrong:** Two domain modules export a function with the same name (e.g., both `auth.js` and `admin.js` export `list()`). `export *` in `client.js` silently takes the last one.
**Why it happens:** Generic function names in domain modules.
**How to avoid:** All client.js functions already have unique, descriptive names (`listDocuments`, `adminListUsers`, etc.). Verify no name collisions before adding `export *`.

### Pitfall 6: Vite 6 `@vitejs/plugin-vue` version mismatch

**What goes wrong:** Running Vite 6 with `@vitejs/plugin-vue@^5.x` causes plugin incompatibility errors.
**Why it happens:** package.json allows `^5.0.0` which cannot satisfy Vite 6's peer dependency.
**How to avoid:** Bump both `vite` and `@vitejs/plugin-vue` in the same `npm install` command.

### Pitfall 7: Wave 1 toast stub breaks existing tests

**What goes wrong:** Replacing the `sessionRevokedToast` ref with `toastStore.show(...)` causes existing Vitest tests to fail because `useToastStore` is not mocked.
**Why it happens:** Tests that mock `api.changePassword` returning `{sessions_revoked: 2}` now need the store to be available.
**How to avoid:** The `useToastStore` stub must be importable and its `show()` must be a no-op by default (returns undefined). Tests that check for the toast text must be updated to check the store was called, or the stub renders nothing (which is fine for Phase 10).

---

## Code Examples

### Package __init__.py with sub-router aggregation

```python
# Source: FastAPI docs — include_router pattern (ASSUMED standard pattern)
from fastapi import APIRouter
from api.admin.users import router as users_router
from api.admin.quotas import router as quotas_router
from api.admin.ai import router as ai_router

router = APIRouter()
router.include_router(users_router)
router.include_router(quotas_router)
router.include_router(ai_router)
```

### Sub-router file structure

```python
# Source: pattern from existing api/folders.py, api/shares.py (established in project)
from __future__ import annotations
from fastapi import APIRouter, Depends, HTTPException, Request
from sqlalchemy.ext.asyncio import AsyncSession
from deps.auth import get_current_admin
from deps.db import get_db

router = APIRouter()  # NO prefix — parent sets it

@router.get("/users")  # Becomes /api/admin/users
async def list_users(
    session: AsyncSession = Depends(get_db),
    _admin: User = Depends(get_current_admin),
): ...
```

### useToastStore stub

```javascript
// Source: established Pinia pattern from auth.js, documents.js (project convention)
import { defineStore } from 'pinia'

export const useToastStore = defineStore('toast', () => {
  function show(message, type = 'info') {
    // Stub: Phase 10 fills in full implementation
    // Phase 7.1 call sites use this; behavior visible after Phase 10
  }
  return { show }
})
```

### fetchWithRetry consolidation

```javascript
// Consolidates adminExportAuditLogCsv, adminDownloadDailyExport, fetchDocumentContent
export async function fetchWithRetry(url, options = {}, _retry = false) {
  const { useAuthStore } = await import('../stores/auth.js')
  const authStore = useAuthStore()

  const headers = { ...(options.headers || {}) }
  if (authStore.accessToken) {
    headers['Authorization'] = `Bearer ${authStore.accessToken}`
  }

  const res = await fetch(url, { ...options, headers, credentials: 'include' })

  if (res.status === 401 && !_retry) {
    try {
      await authStore.refresh()
      return fetchWithRetry(url, options, true)
    } catch {
      authStore.accessToken = null
      authStore.user = null
      throw new Error('Session expired')
    }
  }

  return res
}
```

---

## Package Legitimacy Audit

slopcheck was not available in this environment (installation blocked by auto-mode classifier). All packages below are tagged [ASSUMED] per the graceful-degradation rule. The planner must gate each install behind a `checkpoint:human-verify` task.

| Package | Registry | Purpose | Disposition |
|---|---|---|---|
| `@vueuse/core@^14.3.0` | npm | Vue composition utilities | [ASSUMED] — verify before install |
| `@vueuse/integrations@^14.3.0` | npm | VueUse integrations | [ASSUMED] — verify before install |
| `sortablejs@^1.15.7` | npm | Drag-and-drop sorting | [ASSUMED] — verify before install |
| `@tailwindcss/forms@^0.5.11` | npm | Form base styles | [ASSUMED] — verify before install |
| `rollup-plugin-visualizer@^7.0.1` | npm | Bundle analysis | [ASSUMED] — verify before install |
| `@types/sortablejs` | npm | TypeScript types for sortablejs | [ASSUMED] — verify before install |
| `vite@^6.4.3` | npm | Build tool (major upgrade) | [ASSUMED] — verify before install |
| `@vitejs/plugin-vue@^6.0.7` | npm | Vue SFC compiler for Vite 6 | [ASSUMED] — verify before install |

**Packages removed due to slopcheck [SLOP] verdict:** none (slopcheck unavailable)
**Packages flagged as suspicious [SUS]:** none (slopcheck unavailable)

*All packages above are tagged [ASSUMED] because slopcheck was unavailable at research time. The planner must gate each install behind a `checkpoint:human-verify` task.*

---

## Runtime State Inventory

This is a refactoring phase — no renames, no migrations. This section is NOT applicable.

---

## Environment Availability

| Dependency | Required By | Available | Version | Fallback |
|------------|------------|-----------|---------|----------|
| Python 3.12 | Backend | ✓ | 3.12 (Docker) | — |
| Node.js 20 | Frontend | ✓ | 20 (Docker) | — |
| npm | PERF-01 package installs | ✓ | bundled with Node | — |
| pip3 | requirements.txt pinning | ✓ | system | — |
| pytest | Backend tests | ✓ | installed per requirements.txt | — |
| vitest | Frontend tests | ✓ | `^4.1.7` per package.json | — |

**Missing dependencies with no fallback:** None.

---

## Validation Architecture

### Test Framework

| Property | Value |
|----------|-------|
| Backend framework | pytest with pytest-asyncio (asyncio_mode = auto) |
| Backend config file | `backend/pytest.ini` |
| Backend quick run | `cd backend && pytest tests/test_auth.py -x -v` |
| Backend full suite | `cd backend && pytest -v` |
| Frontend framework | Vitest 4.1.7 |
| Frontend config | `frontend/vitest.config.js` |
| Frontend quick run | `cd frontend && npm test` |

### Phase Requirements → Test Map

| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|--------|----------|-----------|-------------------|-------------|
| CR-01 | `change_password` revokes other sessions, keeps current | unit/integration | `pytest tests/test_auth.py::test_change_password_revokes_other_sessions -x` | ❌ Wave 0 |
| CR-02 | `enable_totp` revokes other sessions, keeps current | unit/integration | `pytest tests/test_auth.py::test_enable_totp_revokes_other_sessions -x` | ❌ Wave 0 |
| CR-03 | `disable_totp` revokes other sessions, keeps current | unit/integration | `pytest tests/test_auth.py::test_disable_totp_revokes_other_sessions -x` | ❌ Wave 0 |
| CODE-01 | All admin URLs still respond on same paths after split | integration | `pytest tests/test_admin.py -x` | ✅ |
| CODE-02 | All document URLs still respond on same paths after split | integration | `pytest tests/test_documents.py -x` | ✅ |
| CODE-03 | All auth URLs still respond on same paths after split | integration | `pytest tests/test_auth.py -x` | ✅ |
| CODE-04 | All existing consumer imports resolve; no 35+ files changed | smoke | `cd frontend && npm test` | ✅ |
| CODE-08 | No model defined twice | static/grep | `grep -rn "class CloudConnectionOut" backend/` | N/A |
| PERF-01 | npm list confirms all packages present | install verification | `cd frontend && npm list` | N/A |

### Wave 0 Gaps (new tests to write)

- [ ] `backend/tests/test_auth.py` — add 3 new tests for CR-01/02/03:
  - `test_change_password_revokes_other_sessions` — create 2 tokens, change password with skip_hash for token 1, verify token 2 revoked
  - `test_enable_totp_revokes_other_sessions`
  - `test_disable_totp_revokes_other_sessions`
- [ ] Frontend: update `SettingsAccountTab.test.js` and `TotpEnrollment.test.js` to mock `useToastStore` after stub creation

---

## Security Domain

### Applicable ASVS Categories

| ASVS Category | Applies | Standard Control |
|---------------|---------|-----------------|
| V2 Authentication | yes (CR-01/02/03) | Session revocation on privilege change — already implemented |
| V3 Session Management | yes | `revoke_all_refresh_tokens` with `skip_token_hash` — no change |
| V4 Access Control | yes | `get_current_admin`, `get_regular_user` deps preserved through decomposition |
| V5 Input Validation | yes | Pydantic models moved to `shared.py` — no change in validation logic |
| V6 Cryptography | no | No crypto changes in this phase |

### Known Threat Patterns

| Pattern | Risk | Mitigation |
|---------|------|------------|
| Router prefix doubling | URL structure breaks, unintended route shadowing | Sub-routers MUST have no prefix (D-04) |
| `CloudConnectionOut` import break | `cloud.py` fails to start; 500 on all cloud endpoints | Move to `api/schemas.py` BEFORE splitting admin.py |
| Circular import crash | App fails to start entirely | `request()` moved to `utils.js`; shared.py not `__init__.py` |
| Admin endpoint leakage | After split, a sub-module accidentally lacks `get_current_admin` dep | Every admin sub-module handler must explicitly inject `_admin: User = Depends(get_current_admin)` |

---

## Assumptions Log

| # | Claim | Section | Risk if Wrong |
|---|-------|---------|---------------|
| A1 | Most [ASSUMED]-tagged pip package versions in pinning table | requirements.txt pinning | Wrong exact version pinned — planner must run `pip show` per package |
| A2 | `fetchWithRetry` abstraction is sufficient to consolidate all 3 blob patterns | CODE-04 | Minor: may need separate functions if download triggers differ |
| A3 | `export *` from domain modules has no name collisions | CODE-04 pitfalls | Low risk: all current client.js exports have unique names |
| A4 | Vite 6 with `esnext` build target works without explicit `resolve.conditions` | PERF-01 | Low risk: project uses no custom conditions; test after upgrade |
| A5 | `@vitejs/plugin-vue@^6.0.7` is the correct peer for `vite@^6.4.3` | PERF-01 | Low risk: npm will warn on peer dep mismatch at install time |

**If this table is empty:** Not empty — A1 is the most significant: the planner must run `pip show` for all [ASSUMED] packages before writing pinned requirements.txt.

---

## Open Questions

1. **`create_system_topic` placement in admin split**
   - What we know: It's a one-line admin utility that calls `services.storage.create_topic()`
   - What's unclear: Does it belong in `users.py` (as an admin utility) or `ai.py` (topics are AI-adjacent)?
   - Recommendation: Place in `users.py` — topic creation is a content management function, not AI config.

2. **`useToastStore` stub API shape**
   - What we know: Phase 10 will implement the full toast system (UX-10). D-03 says Phase 7.1 only creates the stub with `show()`.
   - What's unclear: Should `show()` accept `(message, type)` or `(message, options)`?
   - Recommendation: `show(message, type = 'info')` — minimal two-arg API that Phase 10 can extend without breaking call sites.

3. **alembic version discrepancy**
   - What we know: `requirements.txt` says `>=1.18.4` but `pip3 index versions` shows installed `1.16.5`.
   - What's unclear: Is `1.18.4` the required minimum or a typo?
   - Recommendation: Pin to `==1.16.5` (the installed version). If `1.18.4` was intentional, note the mismatch for the user.

---

## Sources

### Primary (HIGH confidence)
- `backend/api/auth.py` — full source read, CR-01/02/03 implementation verified
- `backend/api/admin.py` — full source read, sub-module boundaries identified
- `backend/api/documents.py` — full source read, sub-module boundaries identified
- `backend/services/auth.py` — `revoke_all_refresh_tokens` signature verified
- `frontend/src/api/client.js` — full source read, all 635 lines, consumer mapping complete
- `backend/main.py` — router registration pattern verified
- `frontend/package.json` — current dependency versions verified
- `frontend/vite.config.js` — current config verified (minimal)
- `backend/requirements.txt` — current floating constraints verified
- `.planning/phases/08-stack-upgrade-backend-decomposition/08-CONTEXT.md` — locked decisions
- `pip3 index versions` output — installed package versions for fastapi, sqlalchemy, celery, alembic, pyjwt, anthropic, cryptography, structlog, psycopg

### Secondary (MEDIUM confidence)
- [CITED: https://v6.vite.dev/guide/migration] — Vite 5→6 breaking changes
- `npm view vite version`, `npm view @vitejs/plugin-vue version` etc. — registry versions verified
- `npm list` in frontend — installed frontend package versions

### Tertiary (LOW confidence — marked [ASSUMED])
- Remaining pip package installed versions (uvicorn, python-multipart, pydantic-settings, etc.) — not verified via `pip show`, only `pip3 index versions` for latest available

---

## Metadata

**Confidence breakdown:**
- CR-01/02/03 backend status: HIGH — source code read directly, implementation confirmed complete
- Backend sub-module boundaries: HIGH — full source code read for all 3 monoliths
- CloudConnectionOut cross-package risk: HIGH — import chain verified in cloud.py
- Frontend function mapping: HIGH — full client.js read + all consumer imports grepped
- Vite 5→6 migration impact: MEDIUM — official migration guide consulted, no Sass/library mode used
- requirements.txt pinning: MEDIUM — some versions confirmed via pip3, most [ASSUMED]
- Package legitimacy: LOW — slopcheck unavailable, all npm packages [ASSUMED]

**Research date:** 2026-06-07
**Valid until:** 2026-07-07 (stable refactoring domain; package versions may drift)
