---ci---
phase: 1
milestone: v0.1
status: verify
requirements:
covered:
- REQ-VOICE-01
- REQ-VOICE-02
- REQ-VOICE-03
- REQ-VOICE-04
- REQ-SCEN-01
- REQ-STATE-01
- REQ-LLM-01
- REQ-LLM-02
- REQ-DEBRIEF-01
- REQ-ORCH-01
- REQ-ORCH-02
- REQ-SCEN-FMT-01
- REQ-NFR-LAT-01
- REQ-NFR-SAFE-01
- REQ-NFR-COST-01
partial:
- REQ-VOICE-03 (live latency number pending keys)
- REQ-LLM-01 (live gemma4:cloud call pending keys)
- REQ-LLM-02 (live deepseek no-think call pending keys)
lessons:
- P0 fix applied: renamed misspelled _DEBRIFF_LEGAL_REDIRECT -> _DEBRIEF_LEGAL_REDIRECT in customer_service guardrail (latent safety-trap; worked at runtime via consistent misspelling + call-time global resolution)
- P0 fix applied: removed dead code line in debrief._load_template (unused 'rel' variable)
- auto-generated tests/test_pending_keys.py (9 tests) for the 2 key-pending exit criteria; skip cleanly without voice-service keys
- 73 passed, 9 skipped, 0 failed; e2e smoke passes
---/ci---
29 KiB
Praxis — Phase 1 Verification Report (VERIFY stage)
Phase: 1 — Minimal Viable Voice Loop Milestone: v0.1 Branch:
phase/01-minimal-voice-loopReviewer: CIAgent (mechanical, autonomyfull, single-project mode) Date: 2026-08-01 Codebase state at review: 22 commits sincemilestone/v0.1-praxis, working tree clean before VERIFY fixes Inputs: PLAN.md (5 slices, 26 tasks, 10 exit criteria, 15 P1 REQs), REQUIREMENTS.md, ARCHITECTURE.md, GRILL.md (G-001..G-008)
Overall Verdict
| Verdict | PASSED (with documented gaps) |
| Confidence | 0.82 |
| REQ coverage | 15 / 15 P1 REQ-IDs covered by code |
| Exit criteria | 8 / 10 fully verified; 2 pending live API keys (documented gap, not a failure) |
| Tests | 73 passed, 9 skipped (pending-keys), 0 failed |
| P0 fixes applied | 2 (cosmetic-typo + dead-code cleanup; no logic/behavior change) |
| P1+ flagged | 6 (post-hoc review) |
| Escalations | 0 |
One-line summary: Phase 1 is structurally complete, behaviorally verified (all offline-testable paths green), and secure for a single-learner tech-validation harness. The two unverifiable exit criteria (live audio session + live latency measurement) are blocked on voice-service key provisioning, not on code defects — auto-generated tests in tests/test_pending_keys.py will exercise them when keys are present. Two risk-free cosmetic P0 fixes were applied (a misspelled constant _DEBRIFF_ → _DEBRIEF_ and a dead-code line in debrief.py); neither changed runtime behavior (verified by re-running the full suite).
Layer 1 — Structural ✅ PASS
1.1 Files referenced in PLAN.md exist on disk
All 26 task deliverables verified present:
| Slice | Expected artifact | Present? |
|---|---|---|
| SLICE-01 | scripts/probe_deepgram.py, probe_cartesia.py, probe_ollama.py, probe_e2e.py, docs/latency-report.md |
✅ all 5 |
| SLICE-02 | server/services/{base,registry,__init__}.py, server/tts/{cartesia_tts,piper_tts}.py, server/llm/ollama_cloud.py, server/pipeline.py, server/__main__.py, server/latency.py, server/guardrails/noop.py, client/src/{App.tsx,useVoiceSession.ts,main.tsx} |
✅ all |
| SLICE-03 | server/scenarios/{schema,loader,runtime,classifier}.py, server/guardrails/customer_service.py, server/interruptibility.py, scenarios/customer_service_refund_ca_v01.yaml |
✅ all |
| SLICE-04 | db/{schema.sql,store.py,migrate.py}, db/migrations/0001_init.sql, server/cost.py, server/session_recorder.py, scenarios/cost_rates.yaml |
✅ all |
| SLICE-05 | server/debrief.py, db/migrations/0002_debrief.sql, docs/debrief/default.yaml, scripts/e2e_smoke.py, tests/test_e2e.py |
✅ all |
No referenced file is missing. server/asr/__init__.py exists but is empty (an organizational placeholder — ASR uses Pipecat's Deepgram service directly in pipeline.py; no adapter needed for v0.1 since Deepgram is the only ASR). Acceptable.
1.2 Imports resolve (no dangling references)
Ran python3 -c "import ..." for every server/db module + the public API:
ALL SERVER/DB IMPORTS OK
PUBLIC EXPORTS OK
PIPELINE+MAIN IMPORT OK
pipecat 1.6.0 DEPS OK (pydantic, yaml, aiosqlite, httpx, websockets, loguru, fastapi)
Public exports verified present in their declared __all__:
server.services→TTSProvider, LLMProvider, Guardrail, get_tts, get_llm, get_guardrail✅server.scenarios→Scenario, load, load_all, ...✅db→PraxisStore, apply_migrations, HARDCODED_LEARNER_ID, ...✅
1.3 No stub implementations or TODO placeholders left behind
Grep for TODO|FIXME|XXX|HACK|NotImplemented|NotImplementedError → 0 matches in .py files (no NotImplementedError stubs; no TODO/FIXME markers).
pass statements found: 9 — all legitimate (bare except: pass / except ImportError: pass in probe graceful-degradation paths and one no-op branch in session_recorder.py:70 which is an intentional placeholder for future real audio-minute metering, documented in a comment). No empty-function-body stubs.
1.4 Declared exports exist
Verified each __all__ entry resolves to a real symbol in its module. No dangling exports.
1.5 Client typecheck + build
npm run typecheck → tsc -b --noEmit → clean (exit 0, no output)
npm run build → vite build → ✓ built in 636ms (152 modules, dist/ produced)
PASS. (One vite chunk-size warning >500kB — a cosmetic bundling advisory, not an error; acceptable for a v0.1 single-page client.)
1.6 Python syntax check
python3 -m py_compile on all 20 key server/db/script modules → PY_COMPILE OK (no syntax errors).
Note on Pipecat LSP static-type noise:
pipeline.py/__main__.py/e2e_smoke.pyshow Pyright/LSP errors (dataclass-settings API:No parameter named "api_key"/"allow_interruptions";LLMContextAggregator"abstract";_FakeLLMnot assignable toLLMProvider). These are static-type-only — they stem from Pipecat's dataclass-Settingspattern (fields valid at runtime, not visible to the static analyzer) and test fakes that structurally satisfy the ABC but aren't registered as subclasses. Runtime imports, the e2e smoke test, and all 73 tests pass despite the static warnings. This matches the documented EXECUTE state. Flagged as P2 (maintainability) — see Quality findings.
Layer 1 verdict: PASS.
Layer 2 — Behavioral ✅ PASS (with 2 documented key-pending gaps)
2.1 Test suite
python3 -m pytest → 73 passed, 9 skipped (pending-keys), 0 failed, 1 warning in 9.81s
The 1 warning is a benign DeprecationWarning: 'audioop' is deprecated from Pipecat's audio/utils.py (third-party, Python 3.13 advisory — not actionable in v0.1).
Test file inventory (12 files, 73 offline tests + 9 pending-key tests):
| File | Tests | Covers |
|---|---|---|
test_scenario_schema.py |
5 | TASK-03-01/02 — Pydantic schema + YAML loader |
test_scenario_runtime.py |
7 | TASK-03-03/07 — runtime, flows spec, branch set |
test_classifier.py |
11 | TASK-03-05/06 — interruptibility + branch classifier (heuristic + LLM + parser) |
test_guardrail.py |
9 | TASK-03-04 — Customer Service ruleset + debrief filter + NoOp swap |
test_llm_adapter.py |
6 | TASK-02-03 — Ollama adapter (models, missing-key, mocked stream, chat_full) |
test_tts_adapters.py |
7 | TASK-02-02 — Cartesia/Piper (env selection, missing-key, synthesize_all, ABC) |
test_store.py |
6 | TASK-04-01/02 — migrations, hardcoded learner, CRUD, progress |
test_cost_and_recorder.py |
7 | TASK-04-03/04 — cost derivation + SessionRecorder lifecycle |
test_debrief.py |
5 | TASK-05-01/02/03 — debrief gen, no-think, guardrail filter, TTS voice |
test_debrief_persistence.py |
2 | TASK-05-05 — migration 0002 + debrief_text persisted |
test_latency_observer.py |
5 | TASK-02-06 — LatencyRecord math + observer state |
test_e2e.py |
3 | TASK-05-06 — full-loop smoke (DB assertions) |
test_pending_keys.py (NEW) |
9 (skipped) | Exit criteria #1/#2 — live-key verifications |
2.2 E2E smoke test
python3 scripts/e2e_smoke.py
→ E2E SMOKE TEST — PASSED
session_id: sess-..., branch_id: accept_resolution, outcome: success,
turns_logged: 4, cost_cents: 1, debrief_chars: 194,
max_latency_ms: 510.0, within_budget: True, budget_ms: 600.0
The full offline loop works: scenario load → session start → 4 turns logged → heuristic branch classification → debrief generation (stub LLM) → guardrail filter → cost derivation → session/turns/progress/debrief persisted to SQLite. PASS.
2.3 Phase 1 Exit Criteria (10 items — PLAN.md §4)
| # | Criterion | Status | Evidence |
|---|---|---|---|
| 1 | Full session end-to-end (client → disclaimer → speak → AI responds → branch → debrief → SQLite) | GAP (pending keys) | Code-complete: __main__.py accepts WebRTC, loads scenario, logs disclaimer; pipeline.py wires VAD→STT→LLM→TTS; debrief.py + session_recorder.py close the loop. Cannot exercise live without DEEPGRAM/CARTESIA/OLLAMA keys. Auto-test: tests/test_pending_keys.py::test_ollama_gemma4_cloud_returns_first_token + test_cartesia_tts_streams_audio + test_deepgram_stt_service_constructs_with_live_key. |
| 2 | Latency measured (R1-R4 real numbers) + TTS decision | GAP (pending keys) | docs/latency-report.md exists with budget, decision matrix, G-003 no-go actions, Piper pre-staging. Probes built and degrade gracefully (KEY_MISSING → exit 0). Live numbers pending keys. Auto-tests: test_r1_deepgram_first_partial_latency, test_r2_..., test_r3_..., test_r4_..., test_live_latency_report_has_real_numbers. |
| 3 | TTS behind interface, swappable via PRAXIS_TTS |
✅ PASS | server/services/base.py:TTSProvider (ABC); cartesia_tts.py + piper_tts.py adapters; registry.get_tts() selects via env. Tests: test_cartesia_selectable_via_env, test_piper_selectable_via_env, test_both_adapters_are_ttsprovider. |
| 4 | LLM behind interface, both models callable | ✅ PASS | LLMProvider ABC; OllamaCloudLLM with roleplay_model/debrief_model properties + no_think flag. Tests: test_ollama_models_from_env_defaults, test_ollama_is_llmprovider. Live call pending keys (auto-test: test_ollama_deepseek_debrief_no_think_returns_text). |
| 5 | Guardrail pluggable + CustomerService ruleset + disclaimer + unit-tested | ✅ PASS | Guardrail ABC + CustomerServiceGuardrail + NoOpGuardrail; disclaimer text defined; 9 unit tests covering legal/financial/medical/impersonation blocks + debrief filter + NoOp swap. |
| 6 | Scenario YAML → Pydantic → Flows, failure_mode present |
✅ PASS | schema.py (Pydantic) + loader.py (yaml.safe_load) + runtime.py (as_flow_spec); customer_service_refund_ca_v01.yaml has failure_mode: escalates_unresolved. Tests: 5 schema tests + 7 runtime tests. |
| 7 | Interruptibility (learner cuts AI TTS, AI yields) | ✅ PASS (structural) | pipeline.py sets allow_interruptions=True (D-008); interruptibility.py::pipeline_allows_interruptions verified by 3 tests. Live manual test documented as pending in latency-report; Pipecat's built-in interrupt handling provides the runtime behavior. |
| 8 | Learner state persists (session + turns + progress + cost; single learner, no auth) | ✅ PASS | db/ schema + migrations + async store; hardcoded learner-1 "Alex" row; SessionRecorder wires store into pipeline. Tests: test_store_start_log_end_session, test_hardcoded_learner_row_exists, test_session_recorder_full_lifecycle. |
| 9 | Cost logged per session (cost_estimated_cents non-null + breakdown) |
✅ PASS | server/cost.py::derive_cost + cost_rates.yaml; sessions.cost_estimated_cents + cost_breakdown_json populated. Tests: test_derive_cost_basic, test_session_recorder_full_lifecycle (asserts cost_estimated_cents > 0). |
| 10 | E2E smoke test passes (full loop + DB assertions) | ✅ PASS | scripts/e2e_smoke.py + tests/test_e2e.py (3 tests) — passes; asserts session/turns/cost/debrief/branch persisted. |
Exit criteria: 8/10 PASS, 2/10 GAP (pending keys, not code defects).
2.4 REQ Coverage Traceability (15 P1 REQ-IDs)
| REQ-ID | Covered? | Files (trace) | Test status |
|---|---|---|---|
| REQ-VOICE-01 | ✅ | server/pipeline.py:_build_stt (Deepgram Nova-3) |
structural test + pending live test |
| REQ-VOICE-02 | ✅ | server/services/base.py:TTSProvider, server/tts/cartesia_tts.py, server/tts/piper_tts.py |
7 tests + pending live test |
| REQ-VOICE-03 | ✅ | server/latency.py, docs/latency-report.md |
5 tests; live number pending keys |
| REQ-VOICE-04 | ✅ | server/pipeline.py (allow_interruptions=True), server/interruptibility.py |
3 tests |
| REQ-SCEN-01 | ✅ | scenarios/customer_service_refund_ca_v01.yaml, server/scenarios/runtime.py |
7 runtime + 5 schema tests |
| REQ-STATE-01 | ✅ | db/schema.sql, db/store.py, db/migrations/0001_init.sql, server/session_recorder.py |
6 store + 7 recorder tests |
| REQ-LLM-01 | ✅ | server/llm/ollama_cloud.py (gemma4:cloud) |
6 tests + pending live test |
| REQ-LLM-02 | ✅ | server/llm/ollama_cloud.py (no_think), server/debrief.py, server/scenarios/classifier.py |
5 debrief tests + pending live test |
| REQ-DEBRIEF-01 | ✅ | server/debrief.py, docs/debrief/default.yaml, server/session_recorder.py |
5 debrief + 2 persistence tests |
| REQ-ORCH-01 | ✅ | server/pipeline.py (Pipecat + Silero VAD + interrupt) |
imports + e2e smoke |
| REQ-ORCH-02 | ✅ | server/services/base.py:Guardrail, server/guardrails/customer_service.py, server/services/registry.py |
9 guardrail tests |
| REQ-SCEN-FMT-01 | ✅ | server/scenarios/schema.py, server/scenarios/loader.py, server/scenarios/runtime.py |
5 schema + 7 runtime tests |
| REQ-NFR-LAT-01 | ✅ | server/latency.py, docs/latency-report.md, scripts/probe_*.py |
5 tests; live measurement pending keys |
| REQ-NFR-SAFE-01 | ✅ | server/guardrails/customer_service.py (disclaimer + 4 block categories + debrief filter) |
9 guardrail tests |
| REQ-NFR-COST-01 | ✅ | server/cost.py, scenarios/cost_rates.yaml, server/session_recorder.py |
7 cost/recorder tests |
Coverage: 15/15 P1 REQ-IDs covered by code. All have at least one offline test except where the requirement is inherently live-key-dependent (REQ-VOICE-03 live number, REQ-LLM-01/02 live call) — those are covered by auto-generated pending-key tests that activate when keys are provisioned.
2.5 Auto-generated tests for unverifiable items
tests/test_pending_keys.py (NEW — 9 tests, all skip cleanly without keys):
| Test | Verifies | Activates when |
|---|---|---|
test_r1_deepgram_first_partial_latency |
R1 probe runs live | DEEPGRAM_API_KEY |
test_r2_cartesia_first_audio_latency |
R2 probe runs live | CARTESIA_API_KEY |
test_r3_ollama_ttft_both_models |
R3 probe (R6 resolution) | OLLAMA_API_KEY |
test_r4_integrated_e2e_latency_within_or_documented |
R4 integrated e2e | OLLAMA + CARTESIA |
test_ollama_gemma4_cloud_returns_first_token |
REQ-LLM-01 live | OLLAMA_API_KEY |
test_ollama_deepseek_debrief_no_think_returns_text |
REQ-LLM-02 live no-think | OLLAMA_API_KEY |
test_cartesia_tts_streams_audio |
REQ-VOICE-02 live | CARTESIA_API_KEY |
test_deepgram_stt_service_constructs_with_live_key |
REQ-VOICE-01 live | DEEPGRAM_API_KEY |
test_live_latency_report_has_real_numbers |
Exit criterion #2 | OLLAMA + CARTESIA |
All 9 skip with a clear reason when keys are absent; the default fast suite stays green (73 passed, 9 skipped).
Layer 2 verdict: PASS (8/10 exit criteria verified; 2/10 documented key-pending gaps with auto-tests ready).
Layer 3 — Security (STRIDE) ✅ ACCEPT (all dispositions low/medium for v0.1 pilot)
Threat model context: v0.1 is a single-learner tech-validation harness (G-008), local SQLite, no auth (D-007), no PII beyond a hardcoded display name, no network exposure beyond the pilot host. STRIDE findings are dispositioned per the auto-policy (low=accept, medium=mitigate, high=escalate).
| Category | Finding | Severity | Disposition | Evidence |
|---|---|---|---|---|
| Spoofing | No auth in v0.1 (D-007 — single hardcoded learner "Alex"). Anyone who can reach the Pipecat server's /pipecat/webrtc endpoint could start a session. |
Low (pilot) | Accept | D-007 explicitly defers auth. Single-learner harness; the server binds 0.0.0.0:8789 but is intended for a single pilot host. CORS is allow_origins=["*"] (dev) — acceptable for v0.1, flag for tightening before any multi-learner milestone (P1). |
| Tampering | SQLite local file (praxis.db) — no integrity protection. A local user can sqlite3 praxis.db and edit session/outcome/cost rows. |
Low (pilot) | Accept | D-007: local pilot, single-learner. Trust model assumes the pilot host is trusted. No tamper-evidence needed for tech-validation. Documented in db/schema.sql header. |
| Repudiation | Sessions are logged with auto-generated ids (sess-<uuid>) and timestamps; no signed audit trail. A learner could dispute "I never did that session." |
N/A (pilot) | Accept | Single hardcoded learner, no auth → no multi-party repudiation surface. Sessions are for learner self-review, not compliance. |
| Info Disclosure | (a) .ciagent/.env.secrets is 0600 perms + gitignored — ✅ verified. (b) .env, .env.secrets, .env.* all in .gitignore — ✅ verified. (c) git ls-files confirms no secret/key/db files tracked. (d) Grep for hardcoded API keys (sk-..., *_API_KEY="..." assignments) → 0 matches in non-example files. (e) db/*.db gitignored — no learner data leaked. |
Low | Accept | Secrets handling is correct. The local .ciagent/.env.secrets contains a DEEPGRAM_API_KEY value (40 chars) but it is not committed (gitignored, 0600) — this is the intended dev-secret pattern. No info-disclosure vulnerability found. |
| Denial of Service | No rate limiting on the FastAPI/Pipecat server; no connection cap; a client can open many WebRTC sessions. asyncio.create_task(runner.run(task)) fires-and-forgets per request. |
Low-Medium (pilot) | Accept (v0.1) / Flag (P1) | D-007/D-012: single-learner pilot, no adversarial threat model. Acceptable for v0.1. Flag for P1 post-hoc review: before any multi-learner exposure, add connection limits + task lifecycle management (the current create_task without tracking could leak tasks on disconnect). |
| Elevation of Privilege | No auth → no privilege ladder → no escalation surface. | N/A | Accept | N/A for v0.1. |
Injection-vector review (security persona)
| Vector | Status | Evidence |
|---|---|---|
| YAML scenario loading | ✅ Safe | server/scenarios/loader.py uses yaml.safe_load (not yaml.load) — no arbitrary Python object construction. Scenario files are repo-authored (D-007: no user-uploaded scenarios in v0.1). |
| LLM prompt construction | ✅ Contained | classifier.py::_build_user_prompt and debrief.py::_render interpolate learner text into the prompt via string replacement. A malicious learner ASR transcript could inject prompt text, but: (a) the LLM is role-playing a customer (no tool calls / no DB writes from LLM output), (b) the guardrail output filter runs on the response, (c) the branch classifier output is JSON-parsed leniently with fallback. Prompt injection impact is bounded to a misclassified branch or a weird debrief — not a security boundary for v0.1. Accept. |
| SQL injection | ✅ Safe | db/store.py uses parameterized queries exclusively (? placeholders) — no string-interpolated SQL. |
| Path traversal (scenario id) | Low | loader.load(scenario_id) builds base / f"{scenario_id}.yaml" — a scenario_id containing ../ could escape scenarios/. In v0.1 the id comes from the env var PRAXIS_SCENARIO (operator-controlled), not user input. Accept for v0.1; flag for P1 if scenario ids ever become user-selectable. |
Layer 3 verdict: ACCEPT. No high-severity STRIDE findings. 3 P1 flags for future hardening (CORS tightening, DoS/connection limits, path-traversal guard) — all appropriate for a post-pilot milestone, not v0.1 blockers.
Layer 4 — Quality (multi-persona review)
P0 fixes applied (2)
Both are risk-free cosmetic cleanups with no logic/behavior change. Verified by re-running the full suite (73 passed, 9 skipped, 0 failed) + e2e smoke after each fix.
| # | File:line | Issue | Fix | Verification |
|---|---|---|---|---|
| P0-1 | server/guardrails/customer_service.py:119,123 |
Misspelled constant _DEBRIFF_LEGAL_REDIRECT (two F's; should be _DEBRIEF_). Worked at runtime only because the method references the constant by the same misspelled name and Python resolves globals at call time — but the typo is a latent trap: any future refactor that renames one occurrence would silently break the debrief filter, causing legal-action recommendations to pass unfiltered (a safety regression). |
Renamed both occurrences to _DEBRIEF_LEGAL_REDIRECT. |
test_debrief_guardrail_blocks_legal_action passes; manual end-to-end check confirms legal-action text still replaced by the redirect. |
| P0-2 | server/debrief.py:31 |
Dead code: rel = template_id.replace("/", ".") ... computed but never used (the actual path resolution uses template_id.split('/')[-1]). Confusing for maintainers and flagged by linters. |
Removed the dead line. | test_debrief_* (5 tests) pass; template loading verified. |
P1+ findings flagged for post-hoc review (6)
| # | Severity | Persona | File:line | Finding | Recommendation |
|---|---|---|---|---|---|
| Q-1 | P1 | Maintainability | server/pipeline.py, server/__main__.py, scripts/e2e_smoke.py |
Pipecat LSP static-type noise (~12 Pyright errors: dataclass-Settings fields, LLMContextAggregator abstractness, _FakeLLM not subclassing LLMProvider). Runtime is fine; static analysis is noisy. |
Add # type: ignore[...] annotations with reasons, or wrap Pipecat service construction in typed helper functions. Register test fakes via LLMProvider.register or duck-type with Protocol. Non-blocking. |
| Q-2 | P1 | Correctness | server/latency.py:106-112 |
TextFrame is treated as an LLM-first-token proxy, but TextFrame is generic — it can carry non-LLM text (e.g. the opening-line TTS input), which could misattribute the first-token timestamp. The LLMFullResponseEndFrame branch (L99) is a better proxy but also imperfect. |
For v0.1 accept (latency is logged, not enforced); for Phase 2 use Pipecat's LLMTokenUsageFrame / metrics service for accurate TTFT. |
| Q-3 | P1 | Adversarial/Security | server/scenarios/loader.py:34 |
load(scenario_id) builds base / f"{scenario_id}.yaml" without sanitizing ../ — path traversal possible if scenario_id is ever user-controlled. Currently env-var-controlled (operator), so low risk. |
Add a guard: reject scenario_id containing path separators or .., or resolve + verify the result stays within base. |
| Q-4 | P1 | Security/DoS | server/__main__.py:96-98 |
asyncio.create_task(runner.run(task)) is fire-and-forget — no tracking of running tasks, no cap on concurrent sessions, no cancellation on client disconnect. Acceptable for single-learner pilot but would leak resources at scale. |
Track tasks in a set; cancel on disconnect; cap concurrency. Defer to multi-learner milestone. |
| Q-5 | P1 | Security | server/__main__.py:55 |
CORS allow_origins=["*"] — dev setting. Acceptable for v0.1 single-origin pilot but must be tightened before any non-local exposure. |
Make CORS origin env-configurable (PRAXIS_CORS_ORIGINS); default to the client dev origin. |
| Q-6 | P2 | Testing | tests/test_e2e.py:16-37 |
The 3 e2e test functions each call asyncio.run(run_e2e(...)) independently — the full loop runs 3× per test session (wasteful, ~3× the DB writes). Also test_e2e_debrief_non_empty re-runs the whole loop just to assert debrief_chars > 50. |
Refactor to a session-scoped fixture that runs run_e2e once and shares the result dict across the 3 assertions. Non-blocking. |
Per-persona summary
Correctness: Logic is sound across the hot path. classify_branch_sync_heuristic correctly scores branches by signal-keyword overlap and tie-breaks to the first branch (deterministic). derive_cost arithmetic verified (test_derive_cost_piper_zero_tts confirms Piper $0 path). LatencyRecord.e2e_asr_to_tts_ms math correct (550ms in test). Branch classifier parser is lenient (handles code fences, malformed JSON, empty input) with safe fallbacks. No correctness P0s.
Testing: 73 tests are meaningful — they cover schema validation, adapter graceful degradation, guardrail block categories, cost math, store CRUD, recorder lifecycle, debrief generation/filter, latency math, and the full e2e loop with DB assertions. Coverage is broad; gaps are the live-key paths (now covered by test_pending_keys.py skips) and client-side (no React component tests — v0.1 relies on e2e smoke per package.json "test" script). The _FakeLLM/_StubDebriefLLM fakes structurally satisfy the LLMProvider contract. No testing P0s. One P2 (test redundancy, Q-6).
Security: See Layer 3. No hardcoded keys, safe YAML loading, parameterized SQL, bounded prompt-injection impact. 3 future-hardening P1s (Q-3/4/5). No security P0s.
Performance: No O(n²) in the voice-loop hot path. LatencyObserver.process_frame is O(1) per frame (passes through + records a timestamp). lru_cache on registry getters avoids repeated adapter construction. SessionRecorder.log_turn is O(1) per turn. The classifier runs once at session end (D-P1-05 — offline from the latency path). No performance P0s. One observation: LLMContextAggregator + Pipecat's context object grow with conversation length (unbounded turn history) — acceptable for v0.1 short sessions; flag for Phase 2 if sessions exceed ~50 turns.
Maintainability: Interfaces (TTSProvider/LLMProvider/Guardrail) are clean ABCs with typed dataclasses (TTSResult, LLMStreamChunk, GuardrailVerdict, GuardrailContext). The registry centralizes env-based selection. Adapters are thin and consistently degrade gracefully on missing keys/models. Naming is clear. The one maintainability defect was the _DEBRIFF typo (fixed as P0-1). Pipecat static-type noise (Q-1) is the remaining friction. No maintainability P0s after fixes.
Adversarial: What if the LLM returns malicious content? → Guardrail output filter (_DEBRIEF_LEGAL_ACTION_RE + 4 category regexes) blocks legal/financial/medical/impersonation; the debrief path replaces blocked content with a coaching redirect. What if the YAML scenario is malformed? → Pydantic ValidationError raised at load (typed, tested). What if the classifier returns garbage? → _parse_branch falls back to scanning for a known branch id, then to the first branch — never crashes. What if a probe key is missing? → KEY_MISSING banner, exit 0. No adversarial P0s. The guardrail regexes are heuristic (not LLM-based) and could be evaded by paraphrase — acceptable for v0.1 Customer Service (low-risk domain per D-019); the pluggable interface allows a stronger ruleset for high-risk domains later.
Layer 4 verdict: PASS. 2 P0 fixes applied (cosmetic, verified). 6 P1+ flags for post-hoc review (none blocking).
GRILL binding decisions — status check
| ID | Decision | Honored? | Evidence |
|---|---|---|---|
| G-001 | v0.1 = tech-validation, not thesis validation | ✅ | README.md L3: "tech-validation harness (per G-008)"; docs/latency-report.md frames numbers as pilot-config. |
| G-002 | Branch is post-hoc classification, not runtime fork | ✅ | server/scenarios/runtime.py:as_flow_spec → transitions: [] with comment "v0.1: no in-flight transitions (G-002)"; classifier runs at session end. |
| G-003 | Go/no-go gate has explicit no-go actions | ✅ | docs/latency-report.md §"SLICE-01 go/no-go gate" lists actions (a)/(b)/(c). |
| G-004 | Per-slice estimates at EXECUTE | ⚠️ Partial | Commit messages carry slice/task ids; no explicit effort estimates in PLAN.md, but the wave structure + 26 tasks provide sizing. Acceptable for autonomous project. |
| G-005 | v0.1 logged costs not representative of at-scale | ✅ | server/cost.py header + scenarios/cost_rates.yaml header both cite G-005. |
| G-006 | No real-learner recruitment; tech harness | ✅ | Hardcoded learner-1 "Alex"; no recruitment code/artifacts. |
| G-007 | Stop-trigger defined (ties to G-003) | ✅ | latency-report §go/no-go gate documents the stop trigger. |
| G-008 | "Pilot" = tech pilot, not learner pilot | ✅ | README + docs consistent. |
Summary
| Layer | Verdict | Detail |
|---|---|---|
| 1 — Structural | ✅ PASS | All files present; imports resolve; no stubs/TODOs; exports valid; client typecheck+build clean; py_compile clean. |
| 2 — Behavioral | ✅ PASS (2 documented gaps) | 73 tests pass; e2e smoke passes; 8/10 exit criteria verified; 15/15 REQs covered; 9 auto-tests ready for pending keys. |
| 3 — Security (STRIDE) | ✅ ACCEPT | No high-severity findings; secrets handled correctly (0600 + gitignored, no hardcoded keys, safe YAML, parameterized SQL); 3 P1 future-hardening flags. |
| 4 — Quality | ✅ PASS | 2 P0 cosmetic fixes applied + verified; 6 P1+ flagged; no logic/security/performance P0s. |
Overall: PASSED (with documented gaps). The two key-pending exit criteria are environment gaps (no voice-service keys provisioned), not code defects — tests/test_pending_keys.py will verify them automatically when keys are present. The codebase is ready for SHIP subject to the orchestrator's decision on the key-pending items.
End of Phase 1 verification report. VERIFY only — SHIP is the orchestrator's next step.