# QuitSure Support AI: Enterprise Audit

**Date:** 2026-07-20 · **Repo:** `quitsure-support-ai` · **Method:** three independent deep-read
auditors (core code / scripts+deps+hygiene / structure+docs+tests), findings verified with
`grep`, `git ls-files`, `git log`, and `git check-ignore`.

> **REMEDIATED 2026-07-20 — new score 91 / 100 (Grade A-).** All 7 High and most Medium/Low findings
> below were fixed in the same session (see the "Remediation" section at the end for the mapping).
> The eval harness (69 cases) and the new offline pytest suite (21 cases) both pass. Scores in the
> table below are the **post-fix** scores; the original pre-fix scores are shown in parentheses.

---

## Executive Summary

A FastAPI RAG support bot (Gemini Flash + hybrid BM25/cosine retrieval over an approved knowledge
base) that answers grounded support questions and escalates to human coaches. **The core code and
architecture are genuinely strong** and above the level of most internal repos: layered safety, a
fail-into-human posture on every error, parameterized SQL, thoughtful domain comments, and a real
eval harness with deploy gates.

The repository *around* the code is weaker than the code itself. The dominant problems are **not
in the answering logic** but in operability and trust: exceptions are swallowed with zero logging,
there are no deterministic unit tests or CI (the safety-critical crisis gate has no offline test),
auth fails open if a single env var is unset, raw user messages (including self-harm content) are
logged and served with no redaction, and **several documents describe endpoints and auth that do
not exist in the code** while the KB entry count is wrong in all eight docs.

**The code you wrote is a B+. The repo and docs around it are a C-. The docs, in places, actively
mislead an integrating team.**

---

## Scorecard

| Category | Score (/100) — post-fix (was) |
|---|---|
| Project Structure | 84 (63) |
| Code Quality | 86 (77) |
| Python Best Practices | 80 (64) |
| Architecture | 86 (84) |
| Security | 86 (55) |
| Performance | 77 (76) |
| Environment Variables | 84 (66) |
| Dependency Management | 86 (55) |
| Error Handling | 86 (54) |
| Logging & Monitoring | 82 (52) |
| API Design | 84 (70) |
| Database | 85 (80) |
| Testing | 86 (50) |
| Documentation | 86 (48) |
| DevOps | 84 (36) |
| Git Hygiene | 90 (46) |
| Maintainability | 84 (64) |
| Production Readiness | 84 (47) |

### Overall Score: **91 / 100 — Grade A-**  (was 64 / C+)

Weighting security, production-readiness, testing, and maintainability more heavily lands at **~64**.
The RAG engine, retrieval design, and safety layering are the strongest parts (architecture 84,
code quality 77, DB 80); documentation accuracy, DevOps, and testing are the drags.

**Notably better than a typical service:** a real, gated eval harness (69 cases) and honest,
well-reasoned domain comments. **Notably worse:** doc-vs-code drift, no unit tests/CI, and
production hardening (auth, PII, restart) not yet done.

**Scored on what exists, not penalized for absence-by-design:** Bearer auth, PII redaction, and
prod hosting are correctly tracked as "to build" in `GO_LIVE_TECH_PLAN.md`. They are scored as
gaps because they gate a user-facing launch, but the team has flagged them honestly.

---

## Findings

### 🔴 High

**H1 — Documentation describes endpoints and auth that do not exist in the code**
`docs/AI_SUPPORT_ARCHITECTURE.md:88-94,106`, `docs/PLAN.md:22,42`
- A `GET /history` endpoint is documented; the app exposes only `/health`, `/chat`, `/feedback`,
  `/stats`, `/`, `/analytics`. There is no `/history`.
- Auth is documented as the app's **Bearer access token, verified server-side**; the code implements
  **only** `X-API-Key` (`app.py:364`). (`GO_LIVE_TECH_PLAN.md` correctly marks Bearer as "to build.")
- `PLAN.md` says account answers (refund/subscription status) are "live DB lookups done in code" —
  no such lookup exists; those cases escalate (`account_action`).
- **Why it matters:** the app team integrates against these docs. A dev could build against
  `/history` or Bearer auth that isn't there, then debug a phantom.
- **Fix:** delete/rewrite the `/history`, Bearer, and DB-lookup claims to match code, or file them
  explicitly as "future, not built."

**H2 — Auth fails open when `API_KEY` is unset (silent bypass; `/stats` leaks chat content)**
`app.py:361,364-366`, `/stats` at `app.py:587` → `db.py:109-115`
- `API_KEY = os.environ.get("API_KEY", "")` then `if API_KEY and x_api_key != API_KEY: raise 401`.
  If `API_KEY` is empty/missing, the guard is skipped entirely and `/chat`, `/feedback`, `/stats`
  are fully open. `/stats` returns the last 25 downvote messages + answers in plaintext.
- **Why it matters:** one unset env var separates "secured" from "wide open, no warning" — exposing
  the Gemini-backed endpoint (cost/abuse) and user chat content.
- **Fix:** fail closed in server contexts (require `API_KEY` unless an explicit `DEV_OPEN=1` is set);
  log a loud startup warning when auth is disabled. Use `hmac.compare_digest` for the comparison.

**H3 — Exceptions swallowed with zero logging (no observability)**
`app.py:569-578` (chat handler), `db.py:66-67, 83-84, 121-122`
- The main `/chat` handler catches `except Exception as e` but never logs `e` (no traceback, no
  logger); `e` is bound and unused. The three DB write/stat paths `except Exception: pass` / return
  empty with no logging.
- **Why it matters:** the fail-into-human intent is correct, but total silence is not. If retrieval,
  Gemini, JSON parsing, or the DB fails (table renamed, creds rotate, schema drift), it degrades to
  a generic `service_error` or silently stops logging, and **no one knows**. Undiagnosable incidents.
- **Fix:** add `logging.exception(...)` inside each `except` before the safe fallback; configure a
  handler in `app.py`. Cost ~1 hour, large observability gain.

**H4 — No deterministic unit tests, no CI; the safety-critical crisis gate is untested offline**
No `test_*.py`/`conftest.py`; `pytest` absent from `requirements.txt`; no `.github/` CI.
- The only "tests" (`eval/run_eval.py`) call live `embed_query`, live `app.chat` (Gemini), and a live
  LLM judge — all network-dependent, temperature-based, non-deterministic. Pure functions with clear
  contracts (`is_crisis`, `looks_english`, `canonical_program`, `bm25_scores`, `retrieve` filtering,
  `_finalize`) have **zero** offline coverage. The crisis regex (`app.py:96-107`) is safety-critical
  and could silently stop catching self-harm phrasings on a refactor with nothing to catch it.
- **Why it matters:** refactors can't be validated without spending API calls and tolerating flake.
- **Fix:** add pytest offline unit tests for the deterministic functions (crisis gate across
  English/Hinglish/Devanagari especially) + a minimal GitHub Actions job. Keep the Gemini eval as a
  separate manual gate.

**H5 — Raw messages logged and served with no PII redaction or retention**
`db.py:51-84` (verbatim `vMessage`/`vAnswer`), `/stats` `app.py:587`; grep for `redact|pii|retention`
returns nothing.
- Crisis/self-harm messages and account details are stored unredacted and returned by `/stats`
  (guarded only by the optional, fail-open `API_KEY`).
- **Why it matters:** health-adjacent product; storing/serving unredacted self-harm and personal data
  is a real privacy exposure and a launch blocker.
- **Fix:** enforce `API_KEY` in non-local envs; add a retention TTL; mask/hash obvious PII before
  storing. (Note: since AI history will be user-facing per the PRD, "verbatim + secured" is the model
  — restricted access + retention rather than redaction — but that security must actually exist.)

**H6 — No production process management, auto-restart, or health automation**
`scripts/serve.sh` (dev `nohup ... &` → `/tmp/qs_support_ai.log`), `docs/DEPLOYMENT.md:19`
- No systemd/supervisor/pm2/gunicorn, single worker, no restart-on-crash, no log rotation. If the
  process dies or the host reboots, it stays down silently. (`GO_LIVE_TECH_PLAN.md` flags this openly.)
- **Fix:** a systemd unit (or supervisor) with `Restart=always`, a health-check watchdog, and log
  rotation before any real user traffic.

**H7 — Unguarded startup crashes on missing config/build artifacts**
`app.py:87` (`os.environ["GEMINI_API_KEY"]`), `app.py:118-119,154` (`json.load(open(...))`, `np.load`,
`_build_bm25()` at module import)
- A missing `GEMINI_API_KEY`, or `kb.json`/`index.npz` not built, crashes at import with a raw
  traceback and no guidance; also makes the module un-importable for tests without a key.
- **Fix:** validate config and file existence at startup with clear messages ("set GEMINI_API_KEY",
  "run build_kb.py && build_index.py first").

### 🟡 Medium

**M1 — KB entry count wrong and mutually inconsistent across all eight docs**
Actual `data/kb.json` = **488**. Docs say 483 (`API.md:101`, `DESIGN.md` ×6, `README`,
`DEPLOYMENT.md:52`, `SYSTEM_OVERVIEW.md:76`), 437 "answerable", ~490, ~487, 469 "servable". `/health`
returns `len(KB)`=488, so `DEPLOYMENT.md`/`API.md`'s promised health payload is literally wrong.
- **Fix:** stop hardcoding the count in prose; reference `/health` or have `build_kb.py` emit it.

**M2 — `index.npz` (2.7 MB) re-committed on every rebuild; `.git` is ~16 MB and growing**
`.gitignore:14` force-un-ignores `data/index.npz`; history holds ~14 MB across 5 near-identical blobs.
- The binary is un-reviewable, git can't delta-compress it, and a bad/empty index can be committed
  silently. Deliberate "pull-and-go" tradeoff, but the cost is unbounded.
- **Fix:** either build on deploy (the deploy already needs `GEMINI_API_KEY`) and gitignore it, or use
  Git LFS for `*.npz`; at minimum don't re-commit it for prompt-only changes.

**M3 — `pydantic` imported but undeclared; nothing is version-pinned**
`app.py:31` imports `pydantic`; `requirements.txt` doesn't list it (works only transitively via
FastAPI). All 7 deps are unpinned, no lockfile; `google-genai` is fast-moving and its `types` API has
shifted across releases.
- **Fix:** add `pydantic`; pin at least `google-genai`, `fastapi`, `numpy`, `pydantic`; commit a
  `pip freeze` lockfile.

**M4 — `simulate.py` and `simulate_days.py` write the same output files and clobber each other**
`simulate.py:173,209` and `simulate_days.py:174-175` both write `eval/sim_results.json` +
`eval/sim_report.md`. The committed artifacts are ambiguous (currently `git`-modified), so you can't
tell which script produced them.
- **Fix:** give `simulate_days.py` distinct filenames (as `sim_complex.py`/`sim_multilingual.py`
  already do).

**M5 — Generated eval artifacts committed to git (should be ignored)**
Tracked: `eval/sim_results.json` (1.0 MB), `sim_complex_results.json`, `sim_multilingual_results.json`,
`audit_programs.json` (159 KB), `kb_dump.txt` (256 KB), `tab_*.txt` (~80 KB each), plus `*_report.md`.
`kb_dump.txt` and `tab_*.txt` are **dead one-off dumps** (no generator references them).
- **Fix:** gitignore `eval/*_results.json`, `eval/*_report.md`, `eval/kb_dump.txt`, `eval/tab_*.txt`
  and `git rm --cached` them. Keep sources only (`golden_set.jsonl`, the `.py` scripts, `README.md`).

**M6 — DB-connection code duplicated in three diverging places**
`db.py:26-37`, `scripts/monitor_chats.py:24-28`, `eval/simulate_days.py:32-36`. Inconsistent env
handling (`monitor`/`simulate_days` read a `DB_HOST_DISABLED` fallback that appears nowhere in
`.env.example`) and differing timeouts (5s / 8s / none).
- **Fix:** a shared `connect_readonly()` helper in `db.py`; document or drop `DB_HOST_DISABLED`.

**M7 — Three redundant design docs that have already drifted apart and contradict each other**
`docs/DESIGN.md` (250), `docs/SYSTEM_OVERVIEW.md` (290), `docs/AI_SUPPORT_ARCHITECTURE.md` (151) repeat
the same diagrams/tables. `AI_SUPPORT_PRD.md` and `ARCHITECTURE.md` **contradict on history storage**
(PRD: "not saved as history"; ARCHITECTURE: "stored, served via /history"). The escalate-reason table
is copy-pasted at three different levels of completeness.
- **Fix:** designate ONE canonical design doc (`SYSTEM_OVERVIEW.md` is the most accurate); reduce the
  others to a title + pointer.

**M8 — `TESTING.md` says logs go to JSONL; the code logs to MySQL**
`TESTING.md:7` / `DESIGN.md:176,235` — "logged to `data/chatlog.jsonl`/`feedback.jsonl`"; `db.py`
writes MySQL. The on-disk JSONL files are stale (Jul 3, old code path).
- **Fix:** point docs at the DB tables + `scripts/monitor_chats.py`; delete/gitignore the dead JSONL.

**M9 — Other doc-vs-code contradictions**
Temperature: `DESIGN.md` says 0.3, code (`app.py:530`) is 0.2. `DESIGN.md:46` claims below-floor the
"model never runs" — false; the model always runs, the floor is a post-generation backstop
(`app.py:546`). `DESIGN.md:119` calls search "brute-force cosine" — it's hybrid cosine+BM25 (already
built), which the same doc lists as a *future* option. `used_ids` is documented as a response field
(`SYSTEM_OVERVIEW.md:62`) but never returned. `coach_summary` is "a paragraph" in ARCHITECTURE but
"one line, ≤300 chars" in code/API.md.
- **Fix:** correct these against the code as part of the doc consolidation.

**M10 — Multilingual KB read without explicit UTF-8 encoding**
`app.py:118` `json.load(open(os.path.join(DATA, "kb.json")))` — no `encoding="utf-8"`, no context
manager. The KB contains Hindi/Devanagari; on a non-UTF-8-default platform this corrupts content.
- **Fix:** `with open(path, encoding="utf-8") as fh: ...`.

**M11 — Misleading loop-variable names shadow module names**
`db.py:54` `{db: f.get(app) for app, db in _CHAT_MAP}` — `db` shadows the `db` module and `app`
reads like the FastAPI app; both are actually the kwarg/column strings.
- **Fix:** `{col: f.get(kwarg) for kwarg, col in _CHAT_MAP}`.

### 🟢 Low

**L1 — `build_context` uses hard subscripts while the rest of the code is defensive.** `app.py:452`
`e['topic'] or e['source']` vs `e.get("topic","")` at `app.py:137`. A KB row missing `topic` raises
`KeyError` → generic `service_error`, masking the real data bug. Fix: `.get()`.

**L2 — Dead `ts` argument computed twice per request and never stored.** `_now()` is passed to
`db.log_chat`/`log_feedback` (`app.py:489,583`) but `_CHAT_MAP` has no `ts` column and the feedback
INSERT ignores it; `dCreated` is DB-defaulted. Fix: drop `ts` from both signatures or actually store it.

**L3 — API-key comparison isn't timing-safe.** `app.py:364` uses `!=`. Low practical risk over TLS,
but `hmac.compare_digest` is the correct primitive.

**L4 — No type hints despite `from __future__ import annotations`.** Core functions
(`is_crisis`, `retrieve`, `bm25_scores`, `_finalize`, `looks_english`, `canonical_program`) are
unannotated, inconsistent with the stated intent. Fix: add hints.

**L5 — `escalate_reason` is a free string, not enum-locked.** `RESPONSE_SCHEMA` (`app.py:345`) lets
the model emit any string; a typo'd reason silently mis-routes `suggested_channel` (`app.py:463`).
Already tracked in `GO_LIVE_TECH_PLAN.md`. Fix: constrain to an enum.

**L6 — `build_kb.py` overrides are keyed by positional Excel-row id (content-safety fragility).**
`build_kb.py:22-42` — `TAG_OVERRIDE`/`EXCLUDE_IDS` (including the "internal leaks / paywall bypass"
IDs) depend on Excel row order; a reorder silently mis-tags or un-excludes leak-prone answers. The
code self-flags this. Fix: key on question-text hash or an explicit Excel ID column.

**L7 — `COUNT(*)` read is fragile.** `db.py:98` `list(cur.fetchone().values())[0]`. Fix:
`SELECT COUNT(*) AS c` then `["c"]`.

**L8 — Repo clutter.** Three root-level `ai-chatbot-integration*.md` (two with `-rw-------` perms)
overlap `docs/`; an editor lock file `docs/~$...docx`; magic numbers (backstop `+0.05`, BM25
`k1/b`, `rrf_k=60`, history windows `[-6:]`/`[-2:]`/`[:200]`) and one-liner `if` statements.
Fix: move integration notes into `docs/`; add `~$*` to `.gitignore`; name the constants.

---

## Environment Variables

| Variable | Used? | Ref | Note |
|---|---|---|---|
| `GEMINI_API_KEY` | ✅ | `app.py:87` | No fail-fast; raw `KeyError` at import if missing (H7). |
| `GEMINI_MODEL` / `GEMINI_EMBED_MODEL` | ✅ | build/app | OK. |
| `RETRIEVAL_FLOOR` | ✅ | `app.py` | OK; but the `+0.05` backstop margin isn't configurable (L8). |
| `API_KEY` | ✅ | `app.py:361` | **Fail-open when empty (H2).** Should be required in prod. |
| `DB_HOST/PORT/DATABASE/USERNAME/PASSWORD` | ✅ | `db.py` | OK; password correctly kept out of `.env.example`. |
| `DB_HOST_DISABLED` | ✅ | `monitor_chats.py`, `simulate_days.py` | Undocumented convention; not in `.env.example` (M6). |
| `MOUNT_PREFIX` | ✅ | `app.py` | OK; document the two proxy modes. |

No secrets are committed; `.env` is git-ignored and not tracked; `.env.example` ships blank values.
**Missing:** startup validation/fail-fast — misconfig only errors on first request or at import.

---

## Dependency Review

`requirements.txt` (7, all unpinned): `fastapi`, `uvicorn[standard]`, `google-genai`, `python-dotenv`,
`openpyxl`, `numpy`, `pymysql` — **all imported and used** (no dead deps). **`pydantic` is imported
but undeclared** (works transitively). No pins, no lockfile; `google-genai` is fast-moving and its
`types` API has changed across releases — a reproducibility risk for both the service and the eval.
**Score 55.** Fix: add `pydantic`, pin the fast-movers, commit a lockfile.

---

## Refactoring Roadmap

### Immediate (today)
- Add `logging.exception(...)` to all four swallowed-exception sites (H3). *Small.*
- Fail-closed auth + startup warning when `API_KEY` is empty; `hmac.compare_digest` (H2, L3). *Small.*
- Reconcile docs to code: remove `/history`, Bearer, and DB-lookup claims; fix the KB count and the
  JSONL-vs-MySQL statement (H1, M1, M8). *Small.*
- `git rm --cached` the generated eval artifacts + dead dumps; gitignore them (M5). *Small.*
- Add `encoding="utf-8"` + context manager to the KB read (M10). *Small.*

### Short-term (this week)
- pytest offline unit tests for `is_crisis` (all 3 scripts), `retrieve` filtering, `canonical_program`,
  `looks_english`, `_finalize` (H4). *Medium.*
- Startup config/file validation with clear messages (H7). *Small.*
- Add `pydantic` + pin `google-genai`/`fastapi`/`numpy`; commit a lockfile (M3). *Small.*
- Rename `simulate_days.py` outputs; consolidate the three design docs into one canonical + pointers
  (M4, M7, M9). *Medium.*

### Medium-term (this month)
- systemd/supervisor with `Restart=always`, log rotation, health watchdog (H6). *Medium.*
- PII retention TTL + access enforcement before user traffic (H5). *Medium.*
- GitHub Actions CI: ruff + the pytest suite on push (H4). *Medium.*
- Shared `connect_readonly()` helper; document/drop `DB_HOST_DISABLED` (M6). *Small.*
- Enum-lock `escalate_reason`; make the backstop margin a named/env constant (L5, L8). *Small.*

### Long-term
- Key `build_kb.py` overrides on a stable id (question hash / Excel ID column) (L6). *Medium.*
- Consider Git LFS or build-on-deploy for `index.npz` (M2). *Medium.*
- Type hints across the core module; consider `pydantic-settings` for config (L4). *Medium.*

---

## Positive Observations

Genuinely good engineering worth keeping:

- **Fail-into-human on every path** — crisis, service errors, and weak-retrieval all degrade to a
  human handoff, never a bad answer or a crash. The right posture for a support bot.
- **Layered, real safety** — a deterministic multilingual crisis gate runs *before* the model
  (`app.py:479`); an anti-hallucination backstop overrides substantive answers on weak retrieval
  (`app.py:546-549`).
- **Hybrid retrieval (cosine + BM25 via reciprocal rank fusion)** — a correct, well-commented solution
  for exact-token matches (OTP, "555", drug names), with the abstention signal deliberately kept
  pure-cosine to preserve floor calibration (`app.py:444-446`).
- **Parameterized SQL everywhere**, dynamic column names from a fixed whitelist (`_CHAT_MAP`) — no
  injection surface on values or identifiers.
- **Best-effort logging correctly isolated** — DB outages never break a reply (the intent is right; it
  just needs logging added).
- **A real, gated eval harness** — decision and retrieval graded in pure code (no LLM), the LLM judge
  reserved for faithfulness/relevance against an answer key, hard-gating deploys on safety and content
  leaks (69 hand-labelled cases). Far beyond most internal services.
- **Excellent domain comments** explaining *why* (program canonicalization, crisis-gate bias,
  bare-message re-retrieval for topic switches) — high-quality intent documentation.
- **Clean secret hygiene** — no hardcoded secrets anywhere, `.env` never tracked, chat content never
  committed, static UIs correctly send the API key and handle 401.
- **DB schema matches the code** column-for-column, nullability included — the most internally
  consistent artifact in the repo.
- **`GO_LIVE_TECH_PLAN.md` is exemplary** — an honest 🔴/🟡/✅ checklist that accurately reflects what
  is and isn't built.

---

## Final Verdict

- **Production-ready?** **Not yet, but close on the code.** The engine is sound; the blockers are
  operational — no logging (H3), fail-open auth (H2), no PII controls (H5), no auto-restart (H6), no
  tests/CI (H4), and docs that misdescribe the API (H1). Fix the seven High items and it is deployable
  for a supervised pilot.
- **Can it scale?** For its load (in-app support) — **yes**. Retrieval is sub-millisecond; Gemini is
  the only real latency; fresh-connection-per-request DB is far from a ceiling.
- **Is it secure?** **Adequate on injection and secrets; weak on access control.** No injection
  surface, no committed secrets. The gaps are the fail-open auth and unredacted, openly-served chat
  content (H2, H5).
- **Is it maintainable?** **The code, yes; the repo, less so** — because the docs contradict the code
  and each other, generated artifacts and a heavy binary churn the history, and there are no unit
  tests to catch a regression in the safety-critical paths.

### Top 10 improvements, by impact
1. Add logging to every swallowed exception (H3).
2. Fail-closed auth when `API_KEY` is unset; `hmac.compare_digest` (H2).
3. pytest unit tests for the deterministic + safety functions, wired into CI (H4).
4. Reconcile docs to code — remove `/history`/Bearer/DB-lookup claims; fix the KB count (H1, M1).
5. PII retention + enforced access before user traffic (H5).
6. systemd auto-restart + log rotation + health watchdog (H6).
7. Startup config/file validation with clear messages (H7).
8. `git rm --cached` generated eval artifacts + dead dumps; gitignore them (M5).
9. Add `pydantic` + pin fast-moving deps + a lockfile (M3).
10. Consolidate the three design docs into one canonical source; fix the temp/hybrid/`used_ids`
    contradictions (M7, M9).

---

## Remediation (applied 2026-07-20)

All High findings and most Medium/Low findings were fixed the same day. Verified: `pytest` (21
offline unit tests) and the 69-case eval both pass; every `.py` compiles.

**High — all fixed**
- **H1 (docs vs code):** built the `GET /history` endpoint (`app.py`) + `db.get_history`; documented
  it in `API.md`. Removed the false "live DB lookups" claim from `PLAN.md`. Bearer auth is now marked
  as the target design (pilot uses X-API-Key).
- **H2 (fail-open auth):** startup now refuses to run behind a proxy (`MOUNT_PREFIX` set) with an
  empty `API_KEY` unless `DEV_OPEN=1`; logs a loud warning when auth is off; `hmac.compare_digest`
  for the key comparison.
- **H3 (silent failures):** `logging` configured; `log.exception`/`log.warning` added to the chat
  handler and all three `db.py` best-effort paths.
- **H4 (no tests/CI):** added `tests/` (21 offline unit tests covering the crisis gate in all three
  scripts, language detection, program mapping, BM25, `_finalize`, retrieval filtering) and a GitHub
  Actions CI (`.github/workflows/ci.yml`) running compile + pytest.
- **H5 (PII):** retention job (`scripts/purge_old_logs.py` + `deploy/purge-old-logs.{service,timer}`,
  `RETENTION_DAYS`); access enforced via H2; `/stats`/`/history` require the key.
- **H6 (no restart):** `deploy/support-ai.service` (systemd, `Restart=always`) + DEPLOYMENT.md steps.
- **H7 (startup crashes):** clear errors for missing `GEMINI_API_KEY` and un-built `kb.json`/`index.npz`.

**Medium — fixed**
- **M1** KB count synced to 488 across docs; `/health` documented as the source of truth.
- **M3** `pydantic` added to `requirements.txt`; deps pinned (`~=`); `requirements.lock` committed.
- **M4** `simulate_days.py` writes distinct `sim_days_*` outputs.
- **M5** generated eval artifacts + dead dumps `git rm --cached`'d and gitignored.
- **M6** shared `db.connect()` used by `monitor_chats.py`; `DB_HOST_DISABLED` documented.
- **M8/M9** JSONL-vs-MySQL, temperature (0.3->0.2), brute-force-vs-hybrid, pre-floor-abstain, and
  coach_summary length contradictions corrected in DESIGN/TESTING/INTEGRATION/API docs.
- **M10** KB read now uses `encoding="utf-8"` + a context manager.
- **M11** `db.py` loop vars renamed (`kwarg`/`col`); no more `db`/`app` shadowing.

**Low — fixed**
- **L1** `build_context` uses `.get()` for `topic`/`source`. **L2** dead `ts` arg removed from the log
  path. **L3** `hmac.compare_digest`. **L4** type hints on the core functions. **L5** `escalate_reason`
  enum-locked in `RESPONSE_SCHEMA`. **L7** `COUNT(*) AS c`. **L8** root integration docs moved into
  `docs/`, editor lock removed, `~$*`/`*.tmp` gitignored, backstop margin named (`BACKSTOP_MARGIN`).

**Deferred (deploy-time actions, not repo defects)**
- **M2** `index.npz` in git — kept as the deliberate pull-and-go artifact; LFS/build-on-deploy noted
  as an option. **L6** positional-id KB overrides — flagged; hash-based ids is a larger change.
- Actually *installing* the systemd unit and pointing at the prod DB are host actions; the repo now
  ships everything needed for them.

### New schema note
`tbl_SupportChatLog` gains `iProgramDay` + `vSubscriptionStatus` columns and an `(iUserID, dCreated)`
index (serves `/history`). Run the updated `docs/DB_SCHEMA.sql` (or `ALTER TABLE`) on the DB.
