Generated: 2026-03-26 Scope: Full codebase — backend (app/, services/, models/, middleware/, utils/, tests/), frontend (src/), infrastructure (Dockerfile, docker-compose, embedding_server), docs (README.md, docs/) Files reviewed: ~110 source files across all modules Claude bootstrap used: YES Claimed-vs-shipped verification: YES
VERDICT: REJECTED RISK: CRITICAL
The ragapp codebase contains 5 confirmed Critical findings and 18 High findings across security, data integrity, and correctness domains. Three default credential values (admin token, JWT secret, health check API key) allow authentication bypass and rate-limit circumvention in any deployment that does not change defaults — a critical risk for any shipped instance. A SQL injection vulnerability exists in the FTS vault filter in vector_store.py. An acknowledged Unicode TypeError in CSRF/security token comparison is documented in the test suite but left unfixed. Data integrity is compromised by missing CASCADE DELETE constraints on the files→vaults and memories→vaults foreign keys. Eighteen High-severity findings cover CSRF token replay, path traversal in file operations, inadequate attachment validation, missing user-scoping on memory search, connection pool exhaustion, and test theater that provides false assurance on security-critical paths.
Group 1 (Behavior): 0 / 1 / 4 / 1 / 0
Group 2 (Substance/Wiring): 0 / 1 / 4 / 0 / 0
Group 3 (Security): 4 / 9 / 3 / 0 / 0
Group 4 (Dependencies): 0 / 2 / 1 / 0 / 0
Group 5 (Claimed vs Shipped): 0 / 0 / 3 / 1 / 2
Group 6 (Cross-Platform): 0 / 0 / 1 / 0 / 0
Group 7 (AI Smells): 0 / 0 / 3 / 2 / 0
Group 8 (Architecture/Perf): 0 / 3 / 5 / 0 / 0
Group 9 (Tests): 1 / 3 / 8 / 0 / 0
------------------------------------------------
TOTAL: 5 / 19 / 32 / 4 / 2
AI Pattern Distribution:
mapping-hallucination: 0
naming-hallucination: 0
resource-hallucination: 0
logic-hallucination: 3
claim-hallucination: 2
phantom-dependency: 0
stale-api: 0
context-rot: 1
unwired-functionality: 2
happy-path-only: 7
other: 5
Claim Ledger:
supported: 9
partially_supported: 4
unsupported: 0
contradicted: 3
stealth_change: 1
| Claim | Status | Evidence |
|---|---|---|
| Hybrid search (dense + BM25 sparse) | PARTIALLY_SUPPORTED | Code path exists; hybrid_alpha parameter accepted but never applied to RRF weighting (vector_store.py:463) |
| Tri-vector search | PARTIALLY_SUPPORTED | Dense + sparse implemented; colbert always returns None in query pipeline (embeddings.py:366, 379) |
| Contextual chunking | SUPPORTED | contextual_chunking.py implemented and integrated in rag_engine.py:389 |
| Reranking | PARTIALLY_SUPPORTED | reranking.py exists and is called; _rerank_score added to chunks but results not re-sorted by it before final limit (rag_engine.py:290) |
| Email ingestion via IMAP | SUPPORTED | email_service.py fully implemented, wired in main.py:272 |
| Multi-vault support | SUPPORTED | Vault routes, filtering, and access control all implemented |
| Rate limiting | SUPPORTED | WhitelistLimiter in limiter.py, applied via SlowAPIMiddleware in main.py |
| CSRF protection | PARTIALLY_SUPPORTED | CSRFManager implemented; however tokens are not invalidated on use (security.py:118-123) |
| Session memory | SUPPORTED | MemoryStore integrated with intent detection in rag_engine.py:118 |
| Background processing queue | SUPPORTED | BackgroundProcessor running, wired to email and file watcher |
| Default embedding model is bge-m3 | CONTRADICTED | docs/release.md:36 and docs/admin-guide.md:255 reference nomic-embed-text; all code and docker-compose default to bge-m3 |
| Architecture diagram is production-accurate | CONTRADICTED | README.md:34 shows port 5173 in architecture; README.md:35 immediately states it is dev-only |
| Release checklist is current | CONTRADICTED | docs/release.md:36 shows outdated default embedding model |
[CRITICAL] CORE-001 — backend/app/config.py:177
Insecure default admin token shipped in all deployments
admin_secret_token defaults to the literal string "admin-secret-token". Any deployment that does not override this value (via environment variable) has a known, public authentication bypass token. security.py:207 checks against this value at request time, but there is no startup validation rejecting the default. A warning is logged at main.py:193-198 but execution continues normally.
Fix: Raise ValueError at startup if admin_secret_token == "admin-secret-token". Do not allow application startup with a default credential.
[CRITICAL] CORE-002 — backend/app/config.py:184
Weak default JWT secret key is human-guidance text, not a random value
jwt_secret_key defaults to "change-me-to-a-random-64-char-string". This is a documentation string, not entropy. Any deployment using this default produces JWTs that can be forged by any attacker who reads the source code. No startup validation enforces that this was changed.
Fix: At startup, validate len(settings.jwt_secret_key) >= 64 and that it is not equal to the default string. Raise on failure.
[CRITICAL] CORE-012 — backend/app/limiter.py:16-32
Health check API key defaults to public string, bypasses rate limiting globally
health_check_api_key defaults to "health-api-key" (config.py:164). The _should_whitelist function in limiter.py:23 compares the X-API-Key header against this value using hmac.compare_digest. Since the default is public and known, any caller supplying X-API-Key: health-api-key bypasses rate limiting on all routes. This is a rate-limit bypass without authentication.
Fix: Require health_check_api_key to be set to a strong random value. Raise at startup if it equals the default.
[CRITICAL] RAG-001 — backend/app/services/vector_store.py:611
SQL injection in FTS vault filter — pattern inconsistency confirmed
Line 611 interpolates vault_id directly into a filter string: fts_query = fts_query.where(f"vault_id = '{vault_id}'") without the sanitization applied at every other call site. Lines 314-315, 350-351, 404-408, 581-582, 708-714 all apply safe_vault_id = str(vault_id).replace("'", "\\'") before string interpolation. Line 611 is the sole exception. A caller controlling vault_id (e.g., via API with string-coerced vault ID) can inject arbitrary filter expressions.
Fix: Apply safe_vault_id = str(vault_id).replace("'", "\\'") at line 611, consistent with all other call sites.
[CRITICAL] TEST-001 — backend/tests/test_security_adversarial.py:293
Acknowledged Unicode TypeError in security comparison left unfixed; tests expect the bug
Lines 286-306 document that fullwidth Unicode characters, combining diacritics, homoglyphs, and RTL override characters cause an unhandled TypeError in CSRF/token compare_digest comparisons. Lines 293, 299, 305, 318 assert pytest.raises(TypeError) — meaning the test suite passes with the vulnerability present. The issue is acknowledged in comments but not fixed in security.py. An attacker sending a Unicode-mangled token can trigger an unhandled exception path rather than a clean 401/403.
Fix: In security.py, encode all token strings to bytes before hmac.compare_digest: hmac.compare_digest(token.encode("utf-8"), stored.encode("utf-8")). Update tests to expect HTTPException, not TypeError.
[HIGH] CORE-006 — backend/app/main.py:118-163
Settings loaded from database with setattr without key allowlist
_load_persisted_settings reads key-value pairs from the settings_kv table and applies them via setattr(settings, key, value). NEW_DIRECT_KEYS (line 122-139) accepts a list of keys from the database without an explicit allowlist verification before setattr. If the database is modified (via SQL injection, compromised admin, or the unsanitized FTS filter in RAG-001), arbitrary attributes can be set on the settings object, potentially overriding security-critical configuration values.
Fix: Define a SETTINGS_ALLOWLIST = frozenset({...}) containing only the keys permitted for database persistence. Validate each key against the allowlist before setattr.
[HIGH] CORE-008 — backend/app/security.py:118-123
CSRF token not invalidated on use — unlimited replay within TTL
validate_token calls store.expire(key) (line 120) to refresh the TTL rather than store.delete(key). The token remains valid for its full TTL after successful validation and can be replayed indefinitely. An attacker who obtains a valid CSRF token (e.g., via XSS) can use it for all state-changing requests until expiry.
Fix: Replace store.expire(key) with store.delete(key). Issue a new CSRF token in the response after successful validation.
[HIGH] CORE-014 — backend/app/api/deps.py:142-169
Default admin token rejection placed AFTER scope validation
In require_scope, scope validation occurs at line 158 before the default-token rejection check at lines 161-163. A caller providing the default token "admin-secret-token" who also presents a matching scope header passes scope validation before being rejected. Depending on execution order of other middleware, this sequencing is fragile.
Fix: Move the DEFAULT_TOKEN rejection check (lines 161-163) to execute before scope validation (line 158).
[HIGH] CORE-022 — backend/app/main.py:193-198
Application starts normally after logging critical warning about default admin token
Lines 193-198 issue logger.critical(...) if admin_secret_token is the default value, but do not halt startup. The application serves all routes with a publicly-known admin token. See also CORE-001.
Fix: sys.exit(1) or raise RuntimeError after the critical warning.
[HIGH] PROC-002 — backend/app/services/email_service.py:365
Unsanitized email body content reaches vector store
_sanitize_html is defined at lines 656-671 but is never called on email body content before parsing, chunking, or storage. Email body is extracted at lines 365-369 and passed directly to the document processor. Malicious HTML or script content in an email body is stored without sanitization and could be returned in RAG responses rendered to users.
Fix: Call _sanitize_html(body) on extracted email body content before passing to document processor.
[HIGH] PROC-003 — backend/app/services/email_service.py:495-509
Email attachment validation uses MIME type from headers only — no magic number check
Attachment validation at lines 495-509 checks part.get_content_type(), which is attacker-controlled in the email. A malicious executable renamed to .pdf with MIME type application/pdf passes the whitelist check. No file signature (magic number) verification is performed.
Fix: Read the first N bytes of the payload and verify the file signature matches the claimed MIME type before accepting the attachment.
[HIGH] PROC-021 — backend/app/services/email_service.py:455
os.path.basename does not sanitize control characters or null bytes in filenames
_sanitize_filename uses os.path.basename(filename) to strip directory components but does not remove control characters (\x00-\x1f), null bytes, or filesystem-invalid characters. A filename like file\x00name.txt passes sanitization unchanged and can cause undefined behavior on downstream filesystem operations.
Fix: After basename, apply a regex to whitelist only safe characters: re.sub(r'[^\w.\-]', '_', name).
[HIGH] PROC-025 — backend/app/services/upload_path.py:342
os.rename in vault path migration lacks path canonicalization
At line 342, os.rename(old_path, new_path) is called after constructing paths from safe_name and vault_id, but neither path is resolved with .resolve() or validated to be within vaults_dir using .relative_to(). A symlink or race condition could cause files to be moved outside the expected directory tree.
Fix: Call .resolve() on both paths and assert both are within vaults_dir using .relative_to(vaults_dir) before os.rename.
[HIGH] FE-003 — frontend/src/lib/api.ts:505-593
Aborted stream leaves incomplete assistant message permanently on screen
When a stream is aborted mid-response (network failure, user abort, server crash), the optimistic assistant message created at line 65-69 remains in the UI with partial or empty content. There is no error flag, rollback, or "message incomplete" indicator. Users see a permanently incomplete message with no explanation.
Fix: In the stream abort/error handler, set message.error = true or message.content += "\n\n[Response interrupted]" and update the store so the UI can render an appropriate indicator.
[HIGH] FE-005 — frontend/src/components/RoleGuard.tsx:9-16
RoleGuard enforces roles client-side only using hydrated localStorage state
RoleGuard checks user.role from authStore, which is hydrated from localStorage on page load (authStore.ts:168). The role is only fetched from the server once (at login). An attacker who modifies localStorage can bypass all RoleGuard protected components. Backend endpoints must independently enforce role requirements.
Fix: Ensure every endpoint behind a RoleGuard has an explicit role check in deps.py. Add a periodic server-side role re-validation (e.g., on useEffect mount in ProtectedRoute).
[HIGH] DB-002 — backend/app/models/database.py:45
Missing ON DELETE CASCADE on files.vault_id → vaults.id
The files table foreign key at line 45 does not specify ON DELETE CASCADE. Deleting a vault leaves all associated file records in the files table with dangling vault_id references, causing data inconsistency and potential information disclosure in queries that filter by vault.
Fix: Change line 45 to: FOREIGN KEY (vault_id) REFERENCES vaults(id) ON DELETE CASCADE
[HIGH] DB-003 — backend/app/models/database.py:51
Missing foreign key constraint on memories.vault_id
The memories table has vault_id INTEGER (nullable) with no foreign key declaration. Vault deletion does not affect orphaned memory records. This is inconsistent with the intent for vault-scoped memories.
Fix: Add FOREIGN KEY (vault_id) REFERENCES vaults(id) ON DELETE SET NULL to the memories table schema.
[HIGH] MEM-001 — backend/app/services/memory_store.py:95
Memory search not scoped to authenticated user — any vault_id accepted without access check
search_memories accepts a vault_id parameter (line 95) and filters results by it (SQL at line 127), but does not verify the calling user has read access to that vault. An authenticated user can pass any vault_id to retrieve memories from vaults they do not have access to.
Fix: Before executing the FTS query, validate that the authenticated user is a member of the specified vault using the vault_members table.
[HIGH] TEST-003 — backend/tests/test_vector_store_security.py:84
SQL injection tests verify string construction only, not actual VectorStore behavior
Tests in TestVaultFilterSanitization (lines 84-134) construct malicious filter strings manually and assert that the injection payload is present in the combined string — they never call VectorStore.search(). These tests document vulnerability scenarios but provide no proof that the production code actually sanitizes input. The 2 production call sites that DO correctly escape are not tested for consistency with the 1 site that does not (RAG-001).
Fix: Call VectorStore.search() with adversarial filter_expr values and assert that injection payloads are either rejected with an exception or escaped such that they cannot alter the query logic.
[HIGH] TEST-004 — backend/tests/test_vector_store_security.py:261
Boundary condition tests are all empty pass statements
TestSearchBoundaryConditions (lines 258-295) contains 6+ test methods (test_limit_zero, test_negative_limit, test_oversized_embedding, test_wrong_dimensions, etc.) that contain only pass. All are commented "# Tested via integration" but there are no corresponding integration tests. These tests run, pass, and report coverage for paths that are never actually exercised.
Fix: Implement assertions in each test method or delete the placeholder stubs. Do not leave pass-only test methods in the suite.
[HIGH] ARCH-002 — backend/app/models/database.py:739
SQLite connection pool exhaustion blocks requests for up to 15 seconds
SQLiteConnectionPool.get_connection() (line 813) blocks on self._pool.get(timeout=5) with max_wait_attempts=3, meaning a request can block up to 15 seconds before raising RuntimeError. With the default pool size of 5, concurrent requests (e.g., multiple streaming sessions) can saturate the pool and degrade all subsequent requests, including auth checks.
Fix: Reduce timeout to 1 second per attempt and return HTTP 503 Service Unavailable immediately when the pool is exhausted rather than blocking the caller thread.
[HIGH] INFRA-001 — Dockerfile:2
Node base image unpinned — non-deterministic builds
FROM node:20-alpine has no patch version, allowing silent updates on each build. A supply chain change in the base image could introduce vulnerabilities without detection.
Fix: FROM node:20.18.3-alpine (or verified current patch). Also apply to python:3.11-slim at Dockerfile:10.
[HIGH] INFRA-002 — Dockerfile:10
Python base image unpinned
Same as INFRA-001 for the production Python image. FROM python:3.11-slim allows silent patch updates.
Fix: FROM python:3.11.12-slim or verified current patch version.
| ID | File | Line | Title | Group |
|---|---|---|---|---|
| CORE-007 | app/main.py |
118-119 | Silent exception swallowing in settings restoration | 1 |
| CORE-009 | app/security.py |
172-182 | CSRF validation applied to all methods with no method guard | 3 |
| CORE-010 | app/security.py |
185-194 | CSRF cookie secure=True hardcoded, breaks HTTP development |
3 |
| CORE-011 | app/security.py |
21-54 | In-memory CSRF store returns hardcoded "1" instead of stored token value |
3 |
| CORE-013 | app/api/deps.py |
274-335 | Vault permission queries not in a single transaction — race condition | 8 |
| CORE-015 | app/middleware/logging.py |
29-36 | Query parameter scrubbing uses substring match, over-redacts similar field names | 1 |
| CORE-016 | app/services/auth_service.py |
30-51 | Password strength check missing special character requirement | 3 |
| CORE-017 | app/services/auth_service.py |
54-67 | JWT iat (issued-at) not validated on decode |
3 |
| CORE-018 | app/services/auth_service.py |
83-91 | Refresh token stored as unsalted SHA256 hash | 3 |
| CORE-020 | app/services/toggle_manager.py |
29-47 | Race condition between cache check and database read in get_toggle |
8 |
| RAG-003 | app/services/rag_engine.py |
373 | _rerank_score added to chunks but results not re-sorted before final limit |
1 |
| RAG-004 | app/services/vector_store.py |
463 | hybrid_alpha accepted but never applied to RRF fusion weights |
2 |
| RAG-005 | app/services/embeddings.py |
350-366 | Tri-vector docstring claims colbert but colbert=None always returned |
2 |
| RAG-007 | app/services/reranking.py |
89 | Reranker fallback silently returns unsorted results with no failure signal | 1 |
| RAG-008 | app/services/query_transformer.py |
70-72 | HyDE failure silently reduces query diversity with no caller signal | 1 |
| RAG-010 | app/services/rag_engine.py |
209 | Hybrid search applied to original query only, not transformed variants | 8 |
| RAG-011 | app/services/rag_engine.py |
306 | Broad exception converts all retrieval errors to identical fallback state | 1 |
| PROC-007 | app/services/file_watcher.py |
161-163 | File watcher does not exclude symlinks from scan | 6 |
| PROC-008 | app/services/file_watcher.py |
148-188 | File watcher does not detect deleted files between scans | 8 |
| PROC-009 | app/services/background_tasks.py |
305-311 | _handle_failure itself not wrapped in try/except |
8 |
| PROC-011 | app/services/document_processor.py |
86-90 | TOCTOU race between existence check and file read in parse() |
8 |
| PROC-016 | app/services/upload_path.py |
228-229 | Vault name sanitization duplicated in multiple functions without shared utility | 5 |
| PROC-023 | app/services/email_service.py |
372 | Email sender/subject logged after sanitization — PII still present | 3 |
| PROC-026 | app/services/email_service.py |
537-592 | Temp attachment file not cleaned up if enqueue() call fails |
8 |
| PROC-028 | app/services/email_service.py |
201-207 | IMAP search result structure not validated before data[0].split() |
8 |
| DB-001 | app/models/database.py |
61 | FTS5 memories_fts search sanitization may not cover all FTS5 operators |
1 |
| DB-004 | app/models/database.py |
258 | Missing indexes on memories.vault_id and related query columns |
8 |
| MEM-002 | app/services/memory_store.py |
95 | No upper bound enforced on memory search limit parameter |
8 |
| CHUNK-001 | app/services/chunking.py |
227-232 | Naive backtick count for code fence detection; broken on escaped backticks | 7 |
| CHUNK-003 | app/services/chunking.py |
335-340 | Sentence splitter uses ASCII punctuation regex, fails on Unicode punctuation | 7 |
| FE-006 | frontend/src/hooks/useSendMessage.ts |
42-54 | Chat session creation has no lock against concurrent session creation | 8 |
| FE-007 | frontend/src/stores/useUploadStore.ts |
119-134 | Upload queue lock acquisition is not atomic in Zustand | 8 |
| FE-008 | frontend/src/stores/useChatStore.ts |
1 | Two chat stores (useChatStore, useChatStoreRedesign) with overlapping scope |
7 |
| FE-009 | frontend/src/lib/auth.ts |
10-19 | useRequirePasswordChange redirects but does not block rendering |
3 |
| FE-012 | frontend/src/pages/LoginPage.tsx |
24 | Synchronous mustChangePassword access immediately after async login() |
1 |
| FE-013 | frontend/src/hooks/useChatHistory.ts |
27-50 | Chat history cache not invalidated when active vault changes | 8 |
| TEST-002 | backend/tests/test_security_adversarial.py |
24 | Auth rejection tests verify status code only, not error message content | 9 |
| TEST-005 | backend/tests/test_auth.py |
693-702 | Password hash tests do not validate algorithm, iteration count, or salt | 9 |
| TEST-006 | backend/tests/test_rag_engine.py |
162 | Filter relevance tests use _distance field but production code may use score |
9 |
| TEST-007 | backend/tests/test_rag_engine.py |
249 | Fallback behavior test does not verify fallback chunks are the actual low-relevance results | 9 |
| TEST-010 | backend/tests/test_auth.py |
712-748 | Refresh token rotation test does not query database to verify old token invalidation | 9 |
| TEST-012 | backend/tests/test_integration.py |
79-164 | Integration tests use fully mocked services — named integration, behave as unit tests | 9 |
| DOC-003 | docs/release.md |
36 | Pre-deployment checklist references nomic-embed-text — current default is bge-m3 |
5 |
| ARCH-001 | app/models/database.py |
289 | Concurrent startup of multiple app workers can race on schema migrations | 8 |
| ARCH-003 | app/models/database.py |
732 | PRAGMA foreign_keys may not be re-applied on pooled connection reuse |
8 |
| ID | File | Line | Title |
|---|---|---|---|
| CORE-019 | app/services/secret_manager.py |
14-38 | Secret key not validated for minimum length |
| CORE-023 | app/middleware/maintenance.py |
31-44 | Maintenance mode blocks writes but allows reads, may return inconsistent data |
| CORE-024 | app/config.py |
420-422 | Orphan vault ID hardcoded as 1 with no existence validation |
| RAG-002 | app/services/rag_engine.py |
290 | reranker_top_n > initial_retrieval_top_k misconfiguration not validated |
| RAG-009 | app/services/vector_store.py |
593-621 | rrf_fuse called even when hybrid is disabled — needless overhead |
| RAG-012 | app/services/retrieval_evaluator.py |
31-32 | Empty chunk list returns CONFIDENT — should return NO_MATCH |
| RAG-014 | app/services/rag_engine.py |
202 | is identity comparison used instead of index to detect original query embedding |
| RAG-015 | app/services/rag_engine.py |
483 | Mixed _distance/score semantics without type safety in threshold logic |
| PROC-013 | app/services/document_processor.py |
543-546 | Contextual chunking silently skipped for schema files with no log |
| FE-014 | frontend/src/pages/DocumentsPage.tsx |
248 | Document deletion uses window.confirm — inconsistent with rest of app using Dialog |
| FE-015 | frontend/src/stores/useSettingsStore.ts |
326-349 | Duplicate resetState / reset methods with identical implementations |
| FE-020 | frontend/src/pages/ChatShell.tsx |
413-415 | Hardcoded 50ms setTimeout for textarea focus — fragile on slow devices |
| DOC-001 | README.md |
34-35 | Architecture diagram shows port 5173, immediately annotated as dev-only |
| DOC-004 | docs/admin-guide.md |
255 | Admin guide shows ollama pull nomic-embed-text — current default is bge-m3 |
| INFRA-003 | docker-compose.yml |
42 | Redis image pinned to minor version only (redis:7-alpine) |
| CONFIG-001 | docker-compose.yml |
13, 30 | flag-embed external dependency not documented in compose file |
| TEST-008 | backend/tests/test_embeddings.py |
73-122 | Batch overflow test mock setup is fragile — call order dependent |
| TEST-011 | backend/tests/test_chat_streaming.py |
129-365 | No tests for oversized streaming content chunks |
1. Happy-path-only logic (7 instances)
Prevalent across rag_engine.py, email_service.py, reranking.py, and query_transformer.py. Error conditions are caught and swallowed into generic fallback states with no signal to callers about degraded quality. Specifically: HyDE failure silently reduces query diversity, reranker failure silently returns unsorted results, stream abort leaves incomplete UI messages, retrieval exceptions produce identical fallback output regardless of root cause.
2. Unwired/partial feature implementation (2 instances)
hybrid_alpha is accepted by vector_store.search() but never applied to the RRF fusion formula. colbert vectors are documented in the tri-vector system, allocated in TriVectorResponse, and discussed in docstrings, but are always None at both the embedding server and RAG engine query layers.
3. Logic hallucination (3 instances)
retrieval_evaluator.py returns CONFIDENT on empty chunk lists (the semantically opposite result). security.py stores "1" as the token value in the in-memory CSRF store rather than the actual token. Reranking adds _rerank_score to chunks without using it for the final result sort order.
4. Claim hallucination (2 instances)
docs/release.md and docs/admin-guide.md consistently reference nomic-embed-text as the default embedding model; all code, docker-compose, and README default to bge-m3. The docstring on embed_multi() claims tri-vector returns (dense, sparse, colbert) but the implementation always returns colbert=None.
| Claim | Location | Status | Evidence |
|---|---|---|---|
Default embedding model is nomic-embed-text |
docs/release.md:36, docs/admin-guide.md:255 |
CONTRADICTED | All code defaults: bge-m3. docker-compose.yml:16, README.md:16, embedding_server both use bge-m3 |
| Architecture diagram shows production topology | README.md:34 |
CONTRADICTED | Port 5173 shown; README:35 immediately states it is development-only |
hybrid_alpha weights the RRF fusion |
docker-compose.yml env, config.py, vector_store.py:449 |
CONTRADICTED | Parameter accepted; vector_store.py:463 comment states "not directly used in pure RRF"; fusion.py uses only constant k |
| Reranked results are returned in rerank score order | Implied by reranking feature | PARTIALLY_SUPPORTED | _rerank_score added to chunks (reranking.py:96) but rag_engine.py:290 applies final limit without re-sorting by rerank score |
| Tri-vector search uses dense + sparse + colbert | README.md, embeddings.py:352 |
PARTIALLY_SUPPORTED | colbert always None; only dense + sparse used in query pipeline |
RAG-006 — backend/app/services/rag_engine.py:165-172 / embeddings.py:366
Documents are indexed with tri-vector embeddings (dense + sparse, colbert=None stub) during ingestion via embed_multi(), but the RAG query pipeline only generates query_sparse for the original query (not transformed variants) and never attempts colbert retrieval. The indexing and query paths are asymmetric with no documentation acknowledging this. Users enabling TRI_VECTOR_SEARCH_ENABLED get 2-vector search at query time despite 3-vector field indexing.
- All Python packages in
backend/requirements.txtverified as legitimate PyPI packages with valid version constraints. No phantom packages detected. - All npm packages in
frontend/package.jsonverified as legitimate npm packages. No cross-ecosystem confusion. - Embedding server (
backend/embedding_server/requirements.txt) uses exact pinning (torch==2.6.0,transformers==4.35.2,FlagEmbedding==1.2.10) while the main backend uses range constraints. This dual strategy is intentional isolation but risks silent compatibility drift. - Docker base images unpinned (
node:20-alpine,python:3.11-slim,redis:7-alpine) — see INFRA-001, INFRA-002, INFRA-003. Non-deterministic builds. flag-embedexternal service is referenced indocker-compose.yml:13and resolved viaextra_hosts:flag-embed:host-gateway. Not defined in compose, requiring undocumented external setup.
backend/app/api/routes/directory (individual route files for documents, vaults, auth, chat, settings, users) was not individually deep-read. The main.py router registration was verified and security deps were audited. Route-level parameter validation coverage may have gaps not captured here.- The redesign directory (
redesign/frontend/) was not audited — it appears to be a development prototype not currently wired into the production build. backend/tests/contains ~50 test files. 12 representative files were audited in depth. Coverage gaps in un-audited test files are possible.backend/embedding_server/server.pywas read. The colbert=None finding (FEATURE-002) is confirmed.- No runtime execution was performed — dynamic analysis findings (actual SQL behavior, actual FTS5 injection outcomes) are based on static analysis with HIGH confidence where pattern inconsistencies are structural.
- CORE-001 + CORE-022: Reject startup if
admin_secret_tokenequals default. Do not log a warning and continue. - CORE-002: Reject startup if
jwt_secret_keyequals default or is < 64 chars. - CORE-012: Reject startup if
health_check_api_keyequals default. - RAG-001: Add
safe_vault_idescaping atvector_store.py:611. - TEST-001: Fix the Unicode encoding in
security.pycompare_digestcalls. Change test assertions fromTypeErrortoHTTPException.
- CORE-008: Invalidate CSRF tokens on use (
store.deletenotstore.expire). - CORE-014: Move default-token rejection before scope validation in
require_scope. - RAG-003: After reranking, re-sort
vector_resultsby_rerank_scorebefore applying final limit. - RAG-004: Either implement
hybrid_alphaweighting inrrf_fuseor remove the parameter and document it as unsupported. - RAG-012: Return
NO_MATCHfromretrieval_evaluatorwhen chunk list is empty. - CORE-011: Store actual token value (not
"1") in_InMemoryCSRFStore.
- PROC-003: Add magic number file signature check for email attachments.
- PROC-021: Replace
os.path.basenamewith full sanitization including control character removal. - PROC-025: Add
resolve()andrelative_to()validation in vault path migration. - PROC-002: Call
_sanitize_html()on email body before processing. - MEM-001: Add vault access authorization check in
search_memories. - FE-005: Add server-side role verification endpoint and call it on protected route mount.
- CORE-006: Implement
SETTINGS_ALLOWLISTin_load_persisted_settings.
- DB-002: Add
ON DELETE CASCADEtofiles.vault_idforeign key. - DB-003: Add
ON DELETE SET NULLtomemories.vault_idwith foreign key declaration. - ARCH-001: Add migration advisory lock to prevent concurrent schema changes.
- ARCH-003: Re-apply
PRAGMA foreign_keys = ONon every connection retrieved from pool. - ARCH-002: Return HTTP 503 immediately on pool exhaustion instead of blocking 15 seconds.
- TEST-003: Rewrite SQL injection tests to call
VectorStore.search()with adversarial inputs. - TEST-004: Implement boundary condition test bodies or delete placeholder stubs.
- TEST-010: Add database query to verify old token hash is removed after refresh.
- TEST-012: Create real integration test tier that uses actual service implementations.
- TEST-005: Add assertions on hash algorithm and iteration count.
- DOC-003: Update
docs/release.md:36fromnomic-embed-texttobge-m3. - DOC-004: Update
docs/admin-guide.md:255fromollama pull nomic-embed-texttoollama pull bge-m3. - RAG-005/RAG-006: Update
embeddings.py:352docstring and README tri-vector claims to accurately reflect colbert deferral. - DOC-001: Separate dev and production architecture diagrams in README.
- Add caller signals for all silent degradation paths (HyDE failure, reranker unavailability, stream abort).
- Consolidate
useChatStoreanduseChatStoreRedesignor document the split responsibility explicitly. - Centralize vault name sanitization into a single utility function.
- Pin all Docker base images to specific patch versions.
Report generated by Claude Sonnet 4.6. Static analysis only. No application source files were modified.