Files
kite/.planning/milestones/v0.2-phases/08-stack-upgrade-backend-decomposition/08-RESEARCH.md
T
curo1305andClaude Sonnet 4.6 123ae5b29b chore: archive v0.2 phase directories to milestones/v0.2-phases/
Moves phases 08–11 execution artifacts from .planning/phases/ to
.planning/milestones/v0.2-phases/ to keep .planning/phases/ clean
for the next milestone.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 14:34:52 +02:00

50 KiB

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

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:

  • CloudConnectionOutapi/schemas.py (used by both api/admin/ and api/cloud.py)
  • UserCreate, UserStatusUpdate, UserAiConfigUpdate, UserDeleteConfirmapi/admin/users.py (only used in users.py)
  • QuotaUpdateapi/admin/quotas.py (only used in quotas.py)
  • SystemAiConfigUpdate, TestConnectionRequestapi/admin/ai.py (only used in ai.py)
  • SystemTopicCreateapi/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:

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:

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

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:

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:

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

app.include_router(admin_router, prefix="/api/admin", tags=["admin"])

Sub-routers have NO prefix — they just define routes without path prefix:

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

# 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

// 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'.

// src/api/utils.js
export async function request(path, options = {}) { /* ... */ }
export async function fetchWithRetry(url, onResponse, _retry = false) { /* consolidates 3 blob patterns */ }
// src/api/documents.js
import { request } from './utils.js'
export function listDocuments(...) { return request('/api/documents?...') }
// 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

# 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

# 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

// 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

// 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.pyrevoke_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)