Compare commits
2 Commits
fix/673-ge
...
fix/123
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b4a70d5fe | ||
|
|
bc02ca54ec |
124
GENOME.md
124
GENOME.md
@@ -1,124 +0,0 @@
|
||||
# GENOME.md — the-door
|
||||
|
||||
> Codebase analysis generated 2026-04-13. Crisis intervention web app — a door that's always open.
|
||||
|
||||
## Project Overview
|
||||
|
||||
the-door is a single-URL crisis intervention web app. A man at 3am can talk to Timmy. No login. No signup. No tracking. Just a door that's always open.
|
||||
|
||||
**Mission**: Stand between a broken man and a machine that would tell him to die.
|
||||
|
||||
48 files. Static HTML frontend (<25KB, works on 3G). Python crisis detection backend. Safety-critical — a broken deployment could prevent someone from reaching the 988 Lifeline.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Browser → nginx (SSL) → index.html → /api/* proxy → Hermes Gateway
|
||||
↓
|
||||
crisis/detect.py
|
||||
↓
|
||||
988 Lifeline overlay
|
||||
```
|
||||
|
||||
## Entry Points
|
||||
|
||||
- **index.html** — The entire frontend. One file. <25KB. Works on 3G.
|
||||
- **system-prompt.txt** — Crisis-aware system prompt for the AI.
|
||||
- **deploy/deploy.sh** — Deployment script for VPS.
|
||||
- **deploy/playbook.yml** — Ansible playbook for deployment.
|
||||
- **crisis/detect.py** — Core crisis detection module (canonical).
|
||||
- **crisis_detector.py** — Legacy class API wrapper around detect.py.
|
||||
- **crisis_responder.py** — Response formatting for crisis levels.
|
||||
|
||||
## Data Flow
|
||||
|
||||
```
|
||||
User message → browser
|
||||
↓
|
||||
index.html → client-side crisis keyword scan
|
||||
↓
|
||||
/api/chat → Hermes Gateway
|
||||
↓
|
||||
system-prompt.txt → injected into AI system prompt
|
||||
↓
|
||||
crisis/detect.py → 5-tier classification (NONE/LOW/MEDIUM/HIGH/CRITICAL)
|
||||
↓
|
||||
crisis/response.py → appropriate response with 988 Lifeline info
|
||||
↓
|
||||
Response → browser → crisis overlay if HIGH/CRITICAL
|
||||
```
|
||||
|
||||
## Key Abstractions
|
||||
|
||||
### Crisis Detection (crisis/detect.py)
|
||||
Canonical detection module. Regex-based keyword matching across 4 tiers:
|
||||
- CRITICAL: immediate self-harm risk (single match triggers)
|
||||
- HIGH: strong despair signals (single match triggers)
|
||||
- MEDIUM: distress signals (requires 2+ indicators)
|
||||
- LOW: emotional difficulty (single match)
|
||||
|
||||
Design principles:
|
||||
- Never computes the value of a human life
|
||||
- Never suggests death is a solution
|
||||
- Always errs on side of higher risk
|
||||
|
||||
### Crisis Profiles (crisis/profiles.py)
|
||||
Compassion profiles that shape AI response tone based on crisis level.
|
||||
|
||||
### Session Tracker (crisis/session_tracker.py)
|
||||
Tracks crisis interactions across sessions. Persistent state for ongoing support.
|
||||
|
||||
### Gateway (crisis/gateway.py)
|
||||
HTTP gateway for crisis detection API. Endpoints for scanning text and getting responses.
|
||||
|
||||
### Offline Fallback (crisis-offline.html, sw.js)
|
||||
Service worker caches crisis resources. When network is down, users still see 988 Lifeline info and crisis resources.
|
||||
|
||||
## File Types
|
||||
|
||||
| Type | Count | Purpose |
|
||||
|------|-------|---------|
|
||||
| .py | 16 | Crisis detection, response, tests |
|
||||
| .html | 4 | Frontend, offline fallback, tests |
|
||||
| .yml | 2 | CI workflows |
|
||||
| .sh | 2 | Health check, service restart |
|
||||
| .md | 5 | Documentation, safety audits |
|
||||
|
||||
## Test Coverage
|
||||
|
||||
### Existing Tests
|
||||
- test_crisis_overlay_focus_trap.py — Accessibility: focus trap in crisis overlay
|
||||
- test_dying_detection_deprecation.py — Legacy API deprecation
|
||||
- test_false_positive_fixes.py — Crisis detection false positive resistance
|
||||
- test_service_worker_offline.py — Offline fallback verification
|
||||
- test_session_tracker.py — Session tracking persistence
|
||||
- crisis/test_rescue.py — Rescue flow testing
|
||||
- crisis/tests.py — Core crisis detection tests
|
||||
|
||||
### Coverage Gaps
|
||||
- No integration tests for full browser → API → response → overlay flow
|
||||
- No tests for system-prompt.txt injection into AI system prompt
|
||||
- No load tests (what happens at 1000 concurrent crisis users?)
|
||||
- No tests for deploy.sh idempotency
|
||||
|
||||
### Critical paths that need tests:
|
||||
1. **Full crisis flow**: user message → detection → 988 overlay → response
|
||||
2. **Offline fallback**: network down → service worker → cached crisis resources
|
||||
3. **Deploy safety**: deploy.sh doesn't break running service
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- **SAFETY-CRITICAL**: the-door serves users in crisis. Broken deployment could prevent someone from reaching 988 Lifeline.
|
||||
- **PR safety**: the-door PRs NEVER auto-merge. Requires-human label on all PRs. (fleet-ops#183)
|
||||
- **No authentication by design**: no login, no signup, no tracking. Privacy is a safety feature.
|
||||
- **Rate limiting**: deploy/rate-limit.conf prevents abuse while allowing crisis access.
|
||||
- **Offline resilience**: service worker ensures crisis resources available even without network.
|
||||
- **System prompt is safety boundary**: system-prompt.txt defines the AI's crisis behavior. Changes require human review.
|
||||
|
||||
## Design Decisions
|
||||
|
||||
- **Single HTML file**: no build step, no framework, no dependencies. Works on 3G. Loads instantly.
|
||||
- **Client-side detection first**: browser scans for crisis keywords before sending to server. Instant response for critical cases.
|
||||
- **Server-side detection second**: crisis/detect.py provides deeper analysis with tiered classification.
|
||||
- **Offline-first for crisis**: service worker caches crisis resources. Network failure doesn't block access to help.
|
||||
- **No tracking**: privacy protects vulnerable users. No analytics, no cookies, no login.
|
||||
@@ -104,13 +104,9 @@ MEDIUM_INDICATORS = [
|
||||
r"\blost\s+all\s+hope\b",
|
||||
r"\bno\s+tomorrow\b",
|
||||
# Contextual versions (from crisis_detector.py legacy)
|
||||
# Keep only medium-only patterns here; stronger overlaps live in HIGH_INDICATORS.
|
||||
r"\bfeel(?:s|ing)?\s+(?:so\s+)?worthless\b",
|
||||
r"\bfeel(?:s|ing)?\s+(?:so\s+)?hopeless\b",
|
||||
r"\bfeel(?:s|ing)?\s+trapped\b",
|
||||
r"\bfeel(?:s|ing)?\s+desperate\b",
|
||||
r"\bno\s+future\s+(?:for\s+me|ahead|left)\b",
|
||||
r"\bnothing\s+left\s+(?:to\s+(?:live|hope)\s+for|inside)\b",
|
||||
r"\bgive(?:n)?\s*up\s+on\s+myself\b",
|
||||
]
|
||||
|
||||
LOW_INDICATORS = [
|
||||
|
||||
50
tests/test_indicator_dedup.py
Normal file
50
tests/test_indicator_dedup.py
Normal file
@@ -0,0 +1,50 @@
|
||||
"""Regression tests for duplicate crisis indicators cleanup (#123)."""
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from crisis.detect import detect_crisis
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
DETECT_FILE = ROOT / "crisis" / "detect.py"
|
||||
|
||||
|
||||
class TestCrisisIndicatorDedup(unittest.TestCase):
|
||||
def _extract_indicator_list(self, name: str) -> set[str]:
|
||||
text = DETECT_FILE.read_text(encoding="utf-8")
|
||||
match = re.search(rf"{name}\s*=\s*\[(.*?)\n\]", text, re.S)
|
||||
self.assertIsNotNone(match, f"{name} list missing")
|
||||
return {
|
||||
line.strip().rstrip(",")
|
||||
for line in match.group(1).splitlines()
|
||||
if line.strip().startswith('r"')
|
||||
}
|
||||
|
||||
def test_high_and_medium_indicator_lists_do_not_overlap(self):
|
||||
high = self._extract_indicator_list("HIGH_INDICATORS")
|
||||
medium = self._extract_indicator_list("MEDIUM_INDICATORS")
|
||||
overlap = high & medium
|
||||
self.assertEqual(set(), overlap, f"duplicate cross-tier patterns found: {sorted(overlap)}")
|
||||
|
||||
def test_removed_duplicates_still_classify_at_high_tier(self):
|
||||
cases = {
|
||||
"I feel hopeless": "HIGH",
|
||||
"I feel trapped": "HIGH",
|
||||
"I feel desperate": "HIGH",
|
||||
"I have no future ahead": "HIGH",
|
||||
"I have given up on myself": "HIGH",
|
||||
}
|
||||
for text, expected in cases.items():
|
||||
with self.subTest(text=text):
|
||||
result = detect_crisis(text)
|
||||
self.assertEqual(expected, result.level)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user