---
milestone: v1.0
audited: 2026-05-30
status: gaps_found
scores:
  requirements: 48/54
  phases_verified: 2/5
  integration_blockers: 3
  integration_warnings: 6
  flows_complete: 2/4
gaps:
  requirements:
    - id: "SHARE-02"
      status: "partial"
      phase: "4"
      claimed_by_plans: ["04-01-PLAN.md", "04-04-PLAN.md"]
      completed_by_plans: ["04-04-SUMMARY.md (Sharing API SHARE-01..05 in commit log)"]
      verification_status: "missing"
      evidence: "Grant/revocation/list work. But GET /api/documents/{id} enforces doc.user_id == current_user.id — share recipients get 404 on metadata, blocking the document detail view."

    - id: "DOC-01"
      status: "partial"
      phase: "4"
      claimed_by_plans: ["04-01-PLAN.md"]
      completed_by_plans: ["04-09-SUMMARY.md (implicit)"]
      verification_status: "missing"
      evidence: "Owners can view document metadata and extracted text. Share recipients who navigate to /document/{id} get 404 because documents.py:542 checks ownership only, not share grants."

    - id: "STORE-06"
      status: "partial"
      phase: "3"
      claimed_by_plans: ["03-02-PLAN.md"]
      completed_by_plans: ["03-02-SUMMARY.md (STORE-06)"]
      verification_status: "missing"
      evidence: "services/storage.delete_document() always calls MinIOBackend.delete_object() regardless of doc.storage_backend, then decrements MinIO quota. Cloud-stored documents never incremented MinIO quota (D-11), so deletion incorrectly decrements it. Actual cloud provider files are not deleted on user-initiated document delete — they become orphaned."

    - id: "SEC-09"
      status: "partial"
      phase: "4"
      claimed_by_plans: ["04-07-PLAN.md", "05-05-PLAN.md"]
      completed_by_plans: ["04-07-SUMMARY.md (SEC-09 MinIO cleanup)", "05-05-SUMMARY.md (SEC-09 cloud cleanup)"]
      verification_status: "partial — Phase 5 VERIFICATION.md confirms admin delete path; user delete path unverified"
      evidence: "Admin-initiated delete (admin.py lines 522–546) correctly purges CloudConnection rows before MinIO cleanup. User-initiated document delete (services/storage.delete_document) does not call get_storage_backend_for_document — cloud provider files are orphaned when a user deletes a cloud-stored document."

    - id: "ADMIN-06"
      status: "partial"
      phase: "4"
      claimed_by_plans: ["04-06-PLAN.md"]
      completed_by_plans: ["04-06-SUMMARY.md (audit.py)", "04-02-SUMMARY.md (GIN index, audit-logs bucket)"]
      verification_status: "missing"
      evidence: "GET /api/admin/audit-log JSON viewer works end-to-end. GET /api/admin/audit-log/export: AuditLogTab.vue uses window.location.href for the export button, which does not send the Authorization: Bearer header. get_current_admin requires HTTPBearer — export always returns 403."

    - id: "CLOUD-03"
      status: "partial"
      phase: "5"
      claimed_by_plans: ["05-05-PLAN.md", "05-09-PLAN.md"]
      completed_by_plans: ["05-05-SUMMARY.md (CLOUD-03)", "05-06-SUMMARY.md (CLOUD-03)"]
      verification_status: "Phase 5 VERIFICATION.md: SATISFIED"
      evidence: "PATCH /api/users/me/default-storage endpoint exists and is registered in main.py. updateDefaultStorage() is defined in client.js. However, no Vue component imports or calls updateDefaultStorage(). No UI selector for default backend in SettingsCloudTab.vue or any other component. Default storage (minio) can only be changed via direct API call, not through the UI."

  integration:
    - blocker: "SHARE-02 / DOC-01 — Share recipient document detail blocked"
      description: "documents.py line 542: `if doc is None or doc.user_id != current_user.id: raise HTTPException(404)`. Recipients with valid share records cannot access GET /api/documents/{id} — they see 404 despite being able to stream /content."

    - blocker: "STORE-06 / SEC-09 — Cloud document delete corrupts MinIO quota and orphans cloud files"
      description: "services/storage.delete_document() calls self._backend().delete_object(doc.object_key) where _backend() always returns MinIOBackend. Cloud-stored documents: (1) MinIO delete_object silently fails (NoSuchKey); (2) MinIO quota decremented even though no quota was charged at upload; (3) actual file in Google Drive / OneDrive / Nextcloud / WebDAV is never deleted."

    - blocker: "ADMIN-06 — Audit log CSV export always returns 403"
      description: "AuditLogTab.vue line 191: `window.location.href = '/api/admin/audit-log/export?${params}'`. Browser navigation strips Authorization: Bearer header. Backend requires get_current_admin (HTTPBearer). All admin CSV export clicks result in 403."

  flows:
    - name: "Recipient views shared document"
      breaks_at: "GET /api/documents/{id} — ownership check excludes recipients"
      affected_requirements: ["SHARE-02", "DOC-01"]

    - name: "User deletes cloud document"
      breaks_at: "services/storage.delete_document() — MinIO backend hardcoded"
      affected_requirements: ["STORE-06", "SEC-09"]

    - name: "Admin exports audit log"
      breaks_at: "AuditLogTab.vue — window.location.href drops Bearer token"
      affected_requirements: ["ADMIN-06"]

    - name: "User selects default cloud storage backend"
      breaks_at: "No UI component calls updateDefaultStorage()"
      affected_requirements: ["CLOUD-03"]

tech_debt:
  - phase: "02-users-authentication"
    items:
      - "Phase 2 VERIFICATION.md status=gaps_found (gap: admin JWT → 403 on documents). Gap was closed by Phase 3 (get_regular_user dep added to all /api/documents/* handlers) but no re-verification was run for Phase 2."

  - phase: "01-infrastructure-foundation"
    items:
      - "No VERIFICATION.md — phase marked complete but never formally verified."

  - phase: "03-document-migration-multi-user-isolation"
    items:
      - "No VERIFICATION.md — phase marked complete but never formally verified."

  - phase: "04-folders-sharing-quotas-document-ux"
    items:
      - "No VERIFICATION.md — phase marked complete but never formally verified."

  - phase: "04-folders-sharing-quotas-document-ux"
    items:
      - "SharedView.vue renders share.document?.created_at and size_bytes, but /api/shares/received returns a flat object (no nested document key) — date/size lines never render for recipients."

  - phase: "03-document-migration-multi-user-isolation"
    items:
      - "Document.user_id ORM column has nullable=True (models.py) but DB has NOT NULL constraint (migration 0003). ORM divergence from actual schema."

  - phase: "05-cloud-storage-backends"
    items:
      - "cloud.py module docstring claims all endpoints use get_regular_user but OAuth callback intentionally omits it (state-token auth). Misleading, though behavior is correct."
      - "CLOUD-05 REQUIRES_REAUTH transitions work for OAuth providers (Google Drive, OneDrive). Nextcloud/WebDAV credential failures produce generic 502 — no REQUIRES_REAUTH state transition. Requirement is OAuth-specific so this is spec-compliant, but a user experience gap."
      - "_doc_to_dict() omits storage_backend and folder_id — document list response cannot distinguish cloud vs local docs."

  - phase: "all"
    items:
      - "REQUIREMENTS.md checkboxes are stale: many implemented requirements still show [ ]. Last updated 2026-05-21, before any execution."
      - "CLAUDE.md specifies ES256 (ECDSA P-256) JWT algorithm, email_hmac deterministic index, and fgp token fingerprint binding. None are implemented — HS256 used, email stored in plaintext, no fingerprint claim. These are v2 hardening items outside the 54 formal v1 REQ-IDs."

nyquist:
  compliant_phases: ["01-infrastructure-foundation", "05-cloud-storage-backends"]
  partial_phases: ["03-document-migration-multi-user-isolation", "04-folders-sharing-quotas-document-ux"]
  missing_phases: ["02-users-authentication"]
  overall: partial
  delta_2026-05-30: "Phase 1 upgraded from partial → compliant after gsd-validate-phase 1: STORE-07 concurrent test added, test_confirm_endpoint unblocked, alembic tests moved to manual-only."
---

# DocuVault v1.0 — Milestone Audit Report

**Audited:** 2026-05-30
**Status:** ⚠ gaps_found
**Score:** 48/54 requirements satisfied (89%)

---

## Executive Summary

All 5 phases executed and marked complete. Phase 5 was formally verified (7/7 truths confirmed). Phase 2 has a VERIFICATION.md with one gap that was closed by Phase 3. Phases 1, 3, and 4 have no VERIFICATION.md.

The integration checker found **3 blockers** and **6 warnings** through cross-phase wiring analysis. Six requirements are partially unsatisfied.

---

## Requirements Coverage (3-Source Cross-Reference)

*Sources: VERIFICATION.md (where present) + SUMMARY.md requirements-completed frontmatter + REQUIREMENTS.md traceability + codebase verification*

### Phase 1 — Infrastructure Foundation (3/3 satisfied)

| REQ-ID | Description | REQUIREMENTS.md | SUMMARY.md | VERIFICATION.md | Status |
|--------|-------------|-----------------|------------|-----------------|--------|
| STORE-01 | PostgreSQL + MinIO migration | [ ] (stale) | 01-03 ✓ | MISSING | ✅ satisfied |
| STORE-02 | MinIO key schema {user_id}/{doc_id}/{uuid4()}{ext} | [ ] (stale) | not claimed (implicit in 01-04) | MISSING | ✅ satisfied (confirmed in minio_backend.py:75) |
| STORE-07 | Stateless backend | [ ] (stale) | 01-03 ✓ | MISSING | ✅ satisfied (no BackgroundTasks, Celery used) |

### Phase 2 — Users & Authentication (20/20 satisfied)

*Note: Phase 2 VERIFICATION.md status=gaps_found (4/5). The gap (admin JWT → 403 on /api/documents/*) was closed in Phase 3 by adding get_regular_user dep. Effectively all Phase 2 requirements are satisfied.*

| REQ-ID | Description | Status |
|--------|-------------|--------|
| AUTH-01 | Register (Argon2 + HIBP) | ✅ satisfied |
| AUTH-02 | JWT session + httpOnly cookie | ✅ satisfied |
| AUTH-03 | TOTP enrollment + backup codes | ✅ satisfied |
| AUTH-04 | Login via TOTP or backup code | ✅ satisfied |
| AUTH-05 | Password reset email | ✅ satisfied |
| AUTH-06 | Sign out all devices | ✅ satisfied |
| AUTH-07 | Refresh token family revocation | ✅ satisfied |
| AUTH-08 | TOTP single-use within window | ✅ satisfied |
| SEC-01 | CSRF (SameSite=Strict + origin validation) | ✅ satisfied |
| SEC-02 | Rate limiting on auth endpoints | ✅ satisfied |
| SEC-03 | Parameterized queries / ORM only | ✅ satisfied |
| SEC-05 | Security headers (CSP, X-Frame-Options, X-Content-Type-Options) | ✅ satisfied |
| SEC-06 | Constant-time comparison for token verification | ✅ satisfied |
| SEC-07 | Admin role dep + admin blocked from doc content | ✅ satisfied (gap closed Phase 3) |
| ADMIN-01 | Admin creates user | ✅ satisfied |
| ADMIN-02 | Admin deactivates user | ✅ satisfied |
| ADMIN-03 | Admin initiates password reset | ✅ satisfied |
| ADMIN-04 | Admin adjusts user quotas | ✅ satisfied |
| ADMIN-05 | Admin assigns AI provider/model | ✅ satisfied |
| ADMIN-07 | No admin impersonation | ✅ satisfied |

### Phase 3 — Document Migration & Multi-User Isolation (8/9 satisfied)

| REQ-ID | Description | Status |
|--------|-------------|--------|
| STORE-03 | Atomic quota enforcement at upload | ✅ satisfied |
| STORE-04 | Quota bar (80%/95% warnings) | ✅ satisfied |
| STORE-05 | Upload rejected at quota limit | ✅ satisfied |
| STORE-06 | Quota decremented on document delete | ⚠️ partial — cloud docs decrement MinIO quota they never incremented; cloud provider file not deleted |
| STORE-08 | BackgroundTasks replaced with Celery | ✅ satisfied |
| SEC-04 | File access via DB lookup only | ✅ satisfied |
| DOC-03 | AI provider/model from admin-assigned DB field | ✅ satisfied |
| DOC-04 | System + per-user topic overrides | ✅ satisfied |
| DOC-05 | Classification uses user's assigned AI config | ✅ satisfied |

### Phase 4 — Folders, Sharing, Quotas & Document UX (11/15 satisfied)

| REQ-ID | Description | Status |
|--------|-------------|--------|
| FOLD-01 | Folder CRUD with count confirmation | ✅ satisfied |
| FOLD-02 | Move documents between folders | ✅ satisfied |
| FOLD-03 | Breadcrumb navigation | ✅ satisfied |
| FOLD-04 | Document list sort | ✅ satisfied |
| FOLD-05 | Full-text search via tsvector | ✅ satisfied |
| SHARE-01 | Share by user handle | ✅ satisfied |
| SHARE-02 | "Shared with me" folder; no quota for recipient | ⚠️ partial — recipient can stream /content but GET /api/documents/{id} returns 404 (ownership-only check) |
| SHARE-03 | View-only default sharing | ✅ satisfied |
| SHARE-04 | Share revocation | ✅ satisfied |
| SHARE-05 | Shared indicator in owner's list | ✅ satisfied |
| SEC-08 | credentials_enc excluded from all serializers | ✅ satisfied |
| SEC-09 | Account deletion purges cloud files | ⚠️ partial — admin delete path correct; user-initiated document delete does not purge cloud provider files |
| ADMIN-06 | Admin audit log viewer | ⚠️ partial — JSON viewer works; CSV export returns 403 (Bearer header dropped by window.location.href) |
| DOC-01 | View document metadata and extracted text | ⚠️ partial — owners: ✅; share recipients: 404 at GET /api/documents/{id} |
| DOC-02 | In-browser PDF preview (bytes proxied, no presigned URLs) | ✅ satisfied |

### Phase 5 — Cloud Storage Backends (6/7 satisfied)

| REQ-ID | Description | Status |
|--------|-------------|--------|
| CLOUD-01 | Connect OneDrive, Google Drive, Nextcloud, WebDAV | ✅ satisfied |
| CLOUD-02 | HKDF per-user credential encryption | ✅ satisfied |
| CLOUD-03 | Local and cloud storage coexist; user selects default | ⚠️ partial — coexist: ✅; select default: API exists but no UI component calls it |
| CLOUD-04 | Connection status display (ACTIVE/REQUIRES_REAUTH/ERROR) | ✅ satisfied |
| CLOUD-05 | invalid_grant transitions to REQUIRES_REAUTH | ✅ satisfied (OAuth providers; WebDAV/Nextcloud don't use OAuth) |
| CLOUD-06 | Disconnect; credentials permanently deleted from DB | ✅ satisfied |
| CLOUD-07 | StorageBackend ABC + factory | ✅ satisfied |

---

## Phase Verification Status

| Phase | VERIFICATION.md | Status | Score | Notes |
|-------|-----------------|--------|-------|-------|
| 01 — Infrastructure Foundation | ❌ MISSING | Unverified | — | No formal verification run |
| 02 — Users & Authentication | ✅ Present | gaps_found (4/5) | 4/5 | Gap closed by Phase 3 (get_regular_user on /api/documents/*) |
| 03 — Document Migration | ❌ MISSING | Unverified | — | No formal verification run |
| 04 — Folders, Sharing, Quotas | ❌ MISSING | Unverified | — | No formal verification run |
| 05 — Cloud Storage Backends | ✅ Present | human_needed | 7/7 | 6 human UAT items (cloud credentials required) |

---

## Nyquist Compliance (Validation Coverage)

| Phase | VALIDATION.md | nyquist_compliant | Action |
|-------|---------------|-------------------|--------|
| 01 — Infrastructure Foundation | ✅ exists (compliant) | ✅ true | — *(validated 2026-05-30)* |
| 02 — Users & Authentication | ❌ missing | — | `/gsd:validate-phase 2` |
| 03 — Document Migration | ✅ exists (draft) | ❌ false | `/gsd:validate-phase 3` |
| 04 — Folders, Sharing, Quotas | ✅ exists (draft) | ❌ false | `/gsd:validate-phase 4` |
| 05 — Cloud Storage Backends | ✅ exists (complete) | ✅ true | — |

---

## Critical Blockers (3)

### BLOCKER-1 — Share Recipient Cannot View Document Metadata

**Affected Requirements:** SHARE-02, DOC-01
**File:** `backend/api/documents.py` line 542
**Root cause:** `if doc is None or doc.user_id != current_user.id: raise HTTPException(404)` — no share-grant check.
**Broken flow:** SharedView.vue → click shared item → DocumentView.vue → `api.getDocument(id)` → 404 for recipient.
**Fix:** Add share-grant check to `get_document()`: if `doc.user_id != current_user.id`, query `Share` table for `(document_id=doc_id, recipient_id=current_user.id)` and allow if found.

### BLOCKER-2 — Cloud Document Delete Corrupts Quota and Orphans Files

**Affected Requirements:** STORE-06, SEC-09
**File:** `backend/services/storage.py` (delete_document function)
**Root cause:** `self._backend().delete_object(doc.object_key)` always uses MinIOBackend regardless of `doc.storage_backend`. Then decrements MinIO quota unconditionally.
**Broken flow:** User uploads to Google Drive (quota=0) → deletes document → `delete_object()` gets NoSuchKey on MinIO (silently swallowed) → quota decremented below actual MinIO usage → actual Google Drive file never deleted.
**Fix:** Use `get_storage_backend_for_document(doc, session)` in `delete_document()` (same pattern as admin delete). Gate quota decrement on `doc.storage_backend == "minio"`.

### BLOCKER-3 — Admin Audit Log CSV Export Always Returns 403

**Affected Requirements:** ADMIN-06
**File:** `frontend/src/components/admin/AuditLogTab.vue` line 191
**Root cause:** `window.location.href = '/api/admin/audit-log/export?${params}'` — browser navigation strips Authorization: Bearer header. `get_current_admin` requires HTTPBearer.
**Broken flow:** Admin clicks "Export CSV" → 403 Forbidden.
**Fix:** Use `fetch()` with `Authorization: Bearer ${accessToken}` header and download the blob via `URL.createObjectURL()`, or pass the access token as a query param (less secure but simple).

---

## Warnings (6)

| # | Severity | Description | Requirement |
|---|----------|-------------|-------------|
| W-1 | Medium | SharedView.vue uses `share.document?.created_at` but /api/shares/received returns flat objects — date/size lines never render | SHARE-02 |
| W-2 | Medium | `updateDefaultStorage()` defined in client.js but never called; no default-backend UI selector exists | CLOUD-03 |
| W-3 | Low | `_doc_to_dict()` omits `storage_backend` and `folder_id` — list response cannot distinguish cloud vs local docs | CLOUD-03 |
| W-4 | Low | `Document.user_id` ORM column has `nullable=True` but DB has `NOT NULL` constraint (migration 0003) — ORM/schema drift | STORE-03 |
| W-5 | Low | cloud.py module docstring says all endpoints use `get_regular_user` but OAuth callback intentionally omits it | — |
| W-6 | Info | CLAUDE.md specifies ES256, email_hmac, fgp fingerprint claim — none implemented (HS256, plaintext email, no fingerprint). Outside 54 v1 REQ-IDs. | v2 scope |

---

## Tech Debt by Phase

**Phase 01:** No VERIFICATION.md written.
**Phase 02:** VERIFICATION.md status=gaps_found; Phase 3 closed the gap but no re-verification was run.
**Phase 03:** No VERIFICATION.md written. Document.user_id ORM nullable divergence.
**Phase 04:** No VERIFICATION.md written. VALIDATION.md in draft state.
**Phase 05:** CLOUD-05 REQUIRES_REAUTH transition not implemented for WebDAV/Nextcloud (spec-compliant; quality gap).
**All:** REQUIREMENTS.md checkboxes not maintained during execution — many satisfied requirements still show `[ ]`.

---

## Integration Wiring Summary

| Connection | Status |
|------------|--------|
| Auth deps (get_regular_user / get_current_admin) on all protected endpoints | ✅ All wired |
| Phase 2 admin gap (admin JWT → 403 on /api/documents/*) | ✅ Closed in Phase 3 |
| Atomic quota at upload (MinIO path) | ✅ Wired |
| Atomic quota decrement at delete (MinIO path only) | ⚠️ Cloud path broken |
| Cloud document content proxy (authenticated fetch) | ✅ Wired |
| Admin delete: cloud cleanup before MinIO before DB | ✅ Wired (SEC-09) |
| User-initiated doc delete: cloud provider cleanup | ❌ Not wired (STORE-06, SEC-09) |
| Share recipient access to /content | ✅ Wired |
| Share recipient access to GET /documents/{id} | ❌ Ownership check blocks recipients |
| Admin audit log JSON viewer | ✅ Wired end-to-end |
| Admin audit log CSV export | ❌ Bearer header dropped |
| Default storage backend selection UI | ❌ Client function orphaned, no UI |
| HKDF credential encryption throughout cloud flows | ✅ Wired |
| All routers registered in main.py | ✅ Confirmed |

---

## Remediation Plan

3 blockers require closure phases (or targeted inline fixes). In priority order:

### Gap 1 — Share recipient metadata access (BLOCKER-1)
Affects: SHARE-02, DOC-01
Effort: Small — add share-grant check to `get_document()` in `documents.py` (~15 lines)

### Gap 2 — Cloud document delete (BLOCKER-2)
Affects: STORE-06, SEC-09
Effort: Medium — refactor `delete_document()` in `services/storage.py` to use `get_storage_backend_for_document()` and conditionally decrement quota (~30 lines)

### Gap 3 — Admin audit log CSV export (BLOCKER-3)
Affects: ADMIN-06
Effort: Small — change `window.location.href` to `fetch()` with Bearer header and blob download in `AuditLogTab.vue` (~20 lines)

These three fixes are small enough to close as a single gap-closure phase or inline as part of `/gsd:complete-milestone v1.0` pre-work.

---

_Audited: 2026-05-30_
_Auditor: Claude (gsd-audit-milestone)_
_Integration checker: gsd-integration-checker_
