Files
praxis/.ciagent/PROJECT.md
T
Praxis CI 6ab40c6f25 docs(milestone): merge phase/00 pre-execution → milestone/v0.4-operator-tier
Phase 0 complete — v0.4 operator tier pre-execution artifacts:
- PROJECT.md (v0.4 scope validated, D-050..D-057)
- REQUIREMENTS.md (8 active REQs: REQ-MT-01/02, REQ-AUTH-01, REQ-DASH-01 + 4 NFRs)
- ARCHITECTURE.md (operator Postgres + auth + dashboard + aggregation + VC migration)
- PERSONAS.md (6 active personas — frontend + devops reactivated)
- RESEARCH-v0.4-operator-tier.md (7 domains, 20 risks, confidence 0.70-0.95)
- PLAN-v0.4-operator-tier.md (2 execution phases, 10 slices, 52 tasks, 8/8 REQ)
- GRILL-v0.4.md (proceed-with-conditions, 6 MUST binding decisions)

---ci---
project: praxis
phase: 0
milestone: v0.4
status: complete
requirements:
  covered: []
  partial: []
---/ci---
2026-08-04 00:39:53 +00:00

39 KiB
Raw Blame History

Praxis — Voice-first AI Apprenticeship Platform

Milestone: v0.4 (Operator tier — cohort dashboard, auth, Postgres) Status: phase 0 — specify (active milestone) Autonomy: full Previous milestone: v0.3 (Mastery scoring + competency rubrics + verifiable credentials) — complete, tagged v0.1.5, release #380

Vision

Praxis is a voice-first, AI-tutored skill platform for learners in resource-constrained environments. Instead of courses, videos, and quizzes, learners practice real job scenarios through real-time spoken conversation with AI tutors. The platform treats every learner as an apprentice to a master craftsperson — open the app, talk, do the job, get better at it.

One-line pitch: Praxis turns every smartphone into a master craftsperson that talks to you, challenges you, and helps you get good at your job.

Objective

Build a voice-first AI apprenticeship platform where learners engage in spoken role-play scenarios with AI tutors, receive coaching debriefs, and progress via mastery gates — working on low-cost phones over constrained bandwidth.

v0.3 Scope (Mastery Scoring + Competency Rubrics — complete, retained for context)

v0.3 activated the mastery/assessment layer deferred from v0.1/v0.2 (per D-021, ROADMAP line 53). Learners progress via mastery gates — they move on only when they can do the thing across varied scenarios, scored against a competency rubric. v0.3 introduced a verifiable-credential issuer so mastery is portable. The operator tier (multi-tenant + auth + cohort dashboard) was deferred to v0.4 per GRILL-v0.3.md Axis 2.

v0.3 in scope (activated REQ groups — post-grill):

  • Mastery core (REQ-MAST-01, REQ-MAST-02): competency rubric per skill; Mastery Score updated after each session, requiring varied-scenario success before a mastery gate opens
  • Verifiable credentials (REQ-MAST-03): portable, tamper-evident credentials issued on week-final mastery gate (W3C VC Data Model 2.0, Ed25519, formative-tier, SQLite-backed issuer keys, public verification endpoint)
  • Dynamic difficulty (REQ-SCEN-02): scenario difficulty adjusts to learner performance (item-response-theory-informed)
  • Scenario library (REQ-SCEN-03, REQ-SCEN-04): library tagged by skill/difficulty/failure_mode; expert-authored format extended with rubric mappings + AI-generated variation hooks
  • Path structure (REQ-PATH-02): path-as-job 6-week structure (PRD §6.4) — the progression container mastery gates live in

v0.3 out of scope (deferred to v0.4 per GRILL-v0.3.md Axis 2):

  • REQ-DASH-01 (cohort dashboard) + REQ-AUTH-01 (operator auth) + REQ-MT-01/02 (operator Postgres + aggregation) + 4 NFRs — the operator tier was originally v0.8 on the ROADMAP; pulling it into v0.3 created a 2-milestone program. The grill's binding verdict splits it to v0.4. D-031 (override D-007) is deferred with the operator tier.
  • REQ-PATH-01 (full multi-path launch) — v0.3 ships the Customer Service path only
  • REQ-DASH-02 (full operator-suite dashboard) — later milestone
  • REQ-ASSIST-01..03 (Live Assist) — later milestone
  • REQ-LOWBW-01..03 (WhatsApp/USSD/offline) — later milestone
  • REQ-VOICE-05/06 (multi-language, persona switching) — later milestone
  • Active failure injection (D-009) — D-049 confirms stays off in v0.3
  • Dynamic rubric weight re-weighting on branch outcome — static in v0.3 (grill Axis 9)
  • Traefik proxy / public TLS — deferred from v0.2 (R-AUTH-01 deferred to v0.4 with the operator surface)

Carries forward from v0.2 (already in production):

  • Docker-in-LXC deployment (lxc-deploy.sh, praxis.service, /health :8789)
  • Voice loop (Deepgram Nova-3 + Cartesia + Pipecat + Ollama Cloud)
  • v0.1 scenario (cs_refund_ca_v01.yaml) + guardrails + debrief

v0.4 Scope (Operator Tier — Cohort Dashboard + Auth + Postgres)

v0.4 activates the operator tier deferred from v0.3 per GRILL-v0.3.md Axis 2 (the operator tier was originally v0.8 on this ROADMAP; pulling it into v0.3 created a 2-milestone program disguised as one). The v0.3 mastery/VC/scenario work carries forward unchanged; v0.4 layers the operator surface on top of it.

v0.4 in scope (activated REQ groups — 8 REQs total):

  • Operator-tier Postgres (REQ-MT-01): second Docker service in the existing LXC CT (docker-compose.yml adds postgres), Postgres 16, persistent volume, internal Docker network only (D-040). Separate from learner-local SQLite (D-007 preserved for learner surface). Stores cohort aggregations, operator accounts, issued credentials, mastery-gate audit log.
  • Cohort aggregation pipeline (REQ-MT-02): on-session-end hook + nightly reconciliation job writes k-anonymized aggregates to Postgres from learner sessions (D-045). No raw learner PII in Postgres.
  • Operator auth (REQ-AUTH-01): session-cookie, argon2id passwords, single operator role, login rate-limited (5 attempts/min) (D-041). Cookie: httpOnly, secure, SameSite=Strict, 8h expiry. Protects cohort dashboard + credential issuance.
  • Cohort dashboard (REQ-DASH-01): anonymized cohort view (practice, mastery progression, failure patterns) for training operators — k-anonymity ≥ 10, 7-day aggregation window (D-034). React route under /operator/*, served by the same FastAPI server (new /api/operator/* prefix), reuses v0.2 StaticFiles (D-044). No separate SPA build — same client/dist.
  • NFRs (4): REQ-NFR-AUTH-01 (argon2id + httpOnly + secure + rate-limited), REQ-NFR-MT-01 (Postgres-in-LXC without destabilizing learner service), REQ-NFR-DASH-01 (k-anonymity ≥ 10 enforced — cells < 10 suppressed), REQ-NFR-DASH-02 (freshness ≤ 24h stale).

v0.4 out of scope (still deferred):

  • REQ-PATH-01 (full multi-path launch) — v0.3 ships Customer Service path only, multi-path later
  • REQ-DASH-02 (full operator-suite dashboard) — later milestone (v0.4 ships the foundational cohort view only)
  • REQ-ASSIST-01..03 (Live Assist) — later milestone
  • REQ-LOWBW-01..03 (WhatsApp/USSD/offline) — later milestone
  • REQ-VOICE-05/06 (multi-language, persona switching) — later milestone
  • Learner auth / multi-learner-per-device — operator auth is v0.4; learner auth later
  • RBAC (multiple operator roles) — single operator role in v0.4; RBAC deferred
  • Third-party credential issuers — v0.9 credentialing milestone
  • Differential privacy — k-anonymity ≥ 10 is sufficient for v0.4 scale (D-034)

Carries forward from v0.3 (already in production):

  • Mastery scoring + competency rubrics + IRT dynamic difficulty (v0.3)
  • Verifiable credential issuer (W3C VC 2.0, Ed25519, SQLite-backed) — v0.4 migrates the issuer key store to operator-tier Postgres + secrets (D-042)
  • Scenario library + Customer Service 6-week path (v0.3)
  • Docker-in-LXC deployment (v0.2)
  • Voice loop (Deepgram Nova-3 + Cartesia + Pipecat + Ollama Cloud) (v0.1)

v0.3 Scope (Mastery Scoring + Competency Rubrics — complete)

v0.3 activated the mastery/assessment layer deferred from v0.1/v0.2 (per D-021). Learners progressed via mastery gates — they moved on only when they could do the thing across varied scenarios, scored against a competency rubric. v0.3 shipped competency rubric engine + Mastery Score + scenario library (≥6 CS scenarios) + dynamic difficulty (IRT) + Customer Service 6-week path + verifiable-credential issuer (W3C VC 2.0, Ed25519, SQLite-backed, formative-tier, public verification). All learner-facing. Released as v0.1.5.

v0.2 Scope (Proxmox LXC Deployment — complete)

v0.2 in scope:

  • Docker image (multi-stage: Node builds client/dist, Python runs server + serves dist via FastAPI StaticFiles)
  • scripts/proxmox/ adapted from coreci (api.sh, lxc-deploy, lxc-clone, lxc-config, lxc-start, health-check, rollback, stage-snippet, firstboot-hook, timing)
  • scripts/install-service.sh (systemd unit for docker compose up)
  • Secret wiring: PROXMOX_* sourced from coreci's .env.secrets; GITEA_TOKEN + DEEPGRAM_API_KEY from praxis's secrets
  • Health-check adapted for /health :8789 (praxis's endpoint, not coreci's /healthz :18080)
  • E2E deploy verification against the live Proxmox cluster

v0.2 out of scope (deferred):

  • Mastery scoring, competency rubrics (deferred to v0.3)
  • CARTESIA_API_KEY / OLLAMA_API_KEY provisioning (infrastructure-only; server degrades gracefully per v0.1 design)
  • Traefik proxy / public TLS (pilot = direct bridge IP access)
  • Multi-environment (dev/staging/prod) — single pilot CT
  • vmbr1 private network (pilot uses vmbr0 DHCP)

Product Principles (non-negotiable)

  1. Voice is the primary interface. Text is fallback, not default.
  2. Doing > Knowing. Every session produces observable action, not passive consumption.
  3. One skill, one outcome. Each path is a job someone can get.
  4. Works on a cheap phone, on 2G. Engineering constraints are product features.
  5. The AI is a master, not a chatbot. Personality, standards, opinions.
  6. Mastery gates progression. Move on when you can do the thing.
  7. Failure is the curriculum. AI provokes mistakes, then coaches recovery.

Requirements (summary — see REQUIREMENTS.md for formal REQ-IDs)

  • Voice conversation engine: real-time ASR + streaming TTS, <600ms round-trip, interruptible, persona switching
  • Scenario engine: branching role-plays with failure-injection and dynamic difficulty (v0.1: one scenario)
  • Learner state: progress, session history, mastery accumulation (v0.3: mastery scoring + competency rubrics + verifiable credentials)
  • Scenario engine: branching role-plays with failure-injection and dynamic difficulty (v0.3: dynamic difficulty + scenario library + AI variations)
  • Skill paths: path-as-job 6-week structure (v0.3: Customer Service path structured + mastery gates)
  • Cohort dashboard: anonymized cohort view for training operators (v0.3: multi-tenant + auth + cohort view)
  • LLM foundation: Ollama-hosted open-weights models gemma4:cloud and deepseek-v4-flash:cloud
  • Low-bandwidth surfaces (later milestones)

Constraints

  • C-1 Voice is primary interface; text is fallback only
  • C-2 Must work on $100 Android phone over 2G/3G
  • C-3 Cost ≤ $3/active learner/month (target markets; v0.1 is Canada launch — relaxed for pilot)
  • C-4 Audio-only in v1 (no large video assets)
  • C-5 Open-weights LLM via Ollama catalog — gemma4:cloud + deepseek-v4-flash:cloud
  • C-6 Domain safety guardrails + human-in-the-loop + disclaimers for safety-sensitive domains
  • C-7 Scenarios authored by domain experts + learning designers; AI generates variations only
  • C-8 Latency budget < 600ms end-to-end (ASR → LLM → TTS)

Key Decisions

ID Decision Rationale Confidence Alternatives
D-001 Launch market = Canada (path: Customer Service) User-directed; Canada as initial market for v0.1 pilot. PRD named Kenya — overridden. 0.70 Kenya + Customer Service (PRD default)
D-002 Milestone = v0.1 foundation (v1.0 reserved for working/tested product) User-directed; v0.1 is the foundation slice (Phase 0 + Phase 1 minimal voice loop). v1.0 is a future milestone. 0.90 v1.0 = Phase 0 + Phase 1 (too ambitious for first milestone)
D-003 LLM foundation = Ollama cataloggemma4:cloud + deepseek-v4-flash:cloud User-directed; open-weights via Ollama, two base models for edge/cloud split. Research phase to verify exact catalog IDs. 0.75 Llama-family, Mistral-family, Qwen-family
D-004 Defer monetization model decision to Phase 1 PRD §11.5 explicitly lists this as a Phase 1 decision (B2C paid, B2B per-seat, donor-funded, government). 0.85 Decide now (insufficient data)
D-005 Single-project mode Fresh repo with one project; no multi-project need. 1.00 Multi-project mode
D-006 "One persona" = one voice persona; scenario role-play uses the same TTS voice as mentor (no distinct character voice in v0.1) Minimizes v0.1 surface area; PRD's full persona-switching (REQ-VOICE-06) is deferred. Same voice avoids a second TTS configuration to validate. 0.70 Two voices (mentor + character) — adds TTS config risk
D-007 "Single learner state" = local single hardcoded profile, no auth, no multi-tenant; persisted via SQLite on-device (or local file fallback) v0.1 is a pilot harness, not a production multi-user system. Auth/multi-tenant is a later-milestone concern. SQLite chosen as the default local store; research phase may refine. 0.80 In-memory only (no persistence), server-side Postgres (premature)
D-008 Interruptibility = abort-and-yield (learner speech cuts AI TTS immediately, AI yields the floor, no pause/resume state machine in v0.1) Matches real-conversation semantics per PRD §6.1; pause/resume adds state-machine complexity inappropriate for v0.1. 0.75 Pause/resume state machine
D-009 Failure-injection hook = architecturally present (scenario declares a failure_mode field) but NOT actively provoked in v0.1 sessions v0.1 validates the data model and one scenario's success criteria; provoking failures is a coaching-debrief feature tied to mastery (deferred). Hook present so Phase 2+ can activate it without schema change. 0.70 Active failure injection in v0.1 (couples to deferred mastery engine)
D-010 v0.1 Canada Customer Service scenario = "Angry customer requesting refund on a damaged product" (retail context, single branch point) Concrete, universally recognizable, low safety-risk (non-medical/non-electrical). One branch point (customer escalates vs accepts resolution) keeps scenario runtime minimal while exercising branching. 0.65 "Customer with wrong booking" (hospitality — less universal for Canada pilot)
D-011 Coaching debrief = included in v0.1 as a single end-of-session text+voice summary (not the full PRD §5.1 multi-moment replay) The debrief is part of the core daily loop and cheap to include at a basic level. Full replay/multi-moment coaching is tied to mastery (deferred). 0.70 Exclude debrief entirely (loses core loop identity), full replay (over-scoped)
D-012 v0.1 cost ceiling = no enforced ceiling (pilot); architecture must not bake in assumptions that would prevent meeting ≤$3/learner/month post-pilot C-3 is a target-market constraint. Canada pilot is a foundation/tech-validation milestone, not a unit-economics milestone. Logging actual cost per session is a v0.1 NFR to inform later milestones. 0.85 Enforce $3 ceiling in v0.1 (premature optimization, wrong market)
D-013 ASR = Deepgram Nova-3 streaming (cloud, WebSocket) Research-verified: streaming-native, ~200-300ms first partial, accent-robust for Canadian English, first-class Pipecat integration, Canada data-residency available. Fallback: Groq-hosted Whisper. 0.85 whisper.cpp (breaks <600ms budget), OpenAI Whisper API (batch)
D-014 TTS = Cartesia Sonic (cloud, ~120ms first audio) primary; Piper (self-hosted, ~80ms) fallback behind interface Research-verified: Cartesia #1 on Speech Arena; Piper is open-weights post-pilot ≤$3/learner path. R4 risk: all-cloud path ~670ms — Piper local may be required for production v0.1 latency. 0.80 ElevenLabs (quality but higher latency/cost), Amazon Polly
D-015 Client = React + WebRTC via Pipecat client SDK Research-verified: Pipecat ships React/RN/Swift/Kotlin SDKs; web client = fastest v0.1 iteration, no app-store distribution, upgrades to React Native for Android later. 0.85 Python CLI harness (dev-integration only), native Android Kotlin (premature)
D-016 Transport = WebRTC (UDP, sub-50ms audio); WebSocket dev fallback Research-verified: WebRTC is Pipecat's production transport; adaptive bitrate, UDP. SSE/HTTP rejected (unidirectional/high overhead). 0.85 WebSocket-only (higher audio latency), custom raw HTTP/2
D-017 Orchestration = Pipecat (not custom, not Vocode) Research-verified: 13.8k★, active, integrates Deepgram+Cartesia+Piper+Ollama natively, has VAD/interrupt/Flows for branching. Vocode stale since Nov 2024. Custom orchestration rebuilds solved problems. 0.85 Vocode (stale), custom from scratch
D-018 Scenario format = YAML DSL → Pydantic → Pipecat Flows Research-verified: YAML is human-authorable + diffable + supports comments (critical for learning-designer rationale per C-7); Pydantic gives typed runtime; Pipecat Flows consumes the schema for branching. JSON is wire format only. 0.85 JSON DSL (no comments), code-authored (couples authoring to engineering)
D-019 v0.1 guardrail layer = pluggable interface with Customer Service ruleset implementation Research: v0.1 is low-risk (Customer Service) but architecture must support pluggable guardrails for later high-risk domains (health/electrical). Ruleset: no legal/financial/medical advice, no real-company employee impersonation, stay-in-role, session-start disclaimer audio, no PII beyond hardcoded profile. 0.80 No guardrails (violates C-6), hardcoded non-pluggable rules (blocks future domains)
D-020 LLM access = Ollama Cloud direct API (https://ollama.com/api/chat + OLLAMA_API_KEY) — no local daemon Research-verified: :cloud tags are real Ollama hosted-inference on NVIDIA cloud partners. Direct API eliminates local-daemon deployment dependency. gemma4:cloud (256K ctx) → role-play fast path; deepseek-v4-flash:cloud (1M ctx, no-think mode) → debrief. Self-host gemma4:e4b is the post-pilot cost-reduction path. 0.85 Local Ollama daemon proxy mode (adds deployment dependency)
D-021 v0.2 scope = Proxmox LXC deployment (replaces roadmap's mastery-scoring v0.2) User-directed: deploy praxis into an LXC container hosted on Proxmox, reusing ~/coreci/scripts/proxmox/ methods. Mastery scoring deferred to v0.3. 0.95 v0.2 = mastery scoring (original roadmap), v0.2 = LXC deploy + mastery (too large)
D-022 Artifact = Docker image in LXC (nesting=1) User-directed. Isolates Python/Pipecat deps; coreci's clone script already sets features=nesting=1. Avoids venv/pip first-boot fragility (Pipecat has many native deps). Multi-stage build: Node stage produces client/dist, Python stage runs the server. 0.85 Clone repo + venv + pip (fragile first-boot), sdist tarball (needs build/release step)
D-023 Client serving = FastAPI serves client/dist as StaticFiles User-directed. Single port (8789), simplest pilot — no nginx/caddy. The Docker image bundles the pre-built dist. 0.90 Separate static server (nginx/caddy — more moving parts), client out of scope
D-024 Voice-service keys = infrastructure-only for v0.2 User-directed. Server starts and /health passes even without CARTESIA/OLLAMA keys (v0.1 graceful degradation). Keys provisioned in a later milestone. Only GITEA_TOKEN + DEEPGRAM_API_KEY are in .env.secrets. 0.90 Provision all keys in v0.2 (premature — deploy infra first)
D-025 Image distribution = host-build → pct push tarball (research decision, see RESEARCH.md) The LXC CT may not route to the internet (coreci pattern: host-fetch → pct push). Build the Docker image on the PVE host (Docker available on Proxmox host) and `docker save pct exec -- docker load, or pct push` a tarball. Avoids needing a container registry. 0.75
D-026 Proxmox secrets sourced from ~/coreci/.ciagent/.env.secrets Same Proxmox cluster, same operator. PROXMOX_API_URL/TOKEN/NODE/STORAGE/TEMPLATE_VOLID already provisioned there. Praxis's .env.secrets adds GITEA_TOKEN + DEEPGRAM_API_KEY. The deploy script sources both. 0.90 Duplicate proxmox secrets in praxis (drift risk)
D-027 VMID = auto (fresh allocation via pve_nextid) CLARIFY auto-decide (full autonomy). Don't reuse coreci's fixed PROXMOX_LXC_VMID — praxis gets its own CT on the same cluster. 0.95 Reuse coreci's VMID (collision), hardcode a new fixed VMID (manual allocation)
D-028 Docker installed inside the CT via apt (CT has network via vmbr0 DHCP) CLARIFY auto-decide. Avoids needing Docker on the PVE host. The debian-12 template + nesting=1 supports Docker-in-LXC. firstboot hook runs pct exec to install docker.io + docker-compose-v2. 0.90 Docker on PVE host (extra host dependency), pre-baked template (custom template maintenance)
D-029 Image built inside the CT (clone repo from Gitea, docker build, docker compose up) CLARIFY auto-decide. Self-contained — CT fetches its own source + builds. No image transfer needed. Slower first-boot (~3-5 min for build) but simpler and reproducible. 0.80 Build on PVE host + pct push tarball (host Docker dependency), pre-built image from registry (external dependency)
D-030 CT network = vmbr0 DHCP only (pilot, no vmbr1, no Traefik proxy) CLARIFY auto-decide. v0.2 is infrastructure-only pilot. Direct bridge IP access for health-check. Proxy/TLS deferred to a later milestone. 0.90 vmbr1 + Traefik proxy (over-scoped for pilot)
D-031 v0.3 introduces multi-tenant + authoverrides D-007 for the cohort-dashboard surface REQ-DASH-01 (anonymized cohort view for training operators) requires multi-tenant data. D-007's single-learner/no-auth stance was correct for v0.1/v0.2 pilot but blocks v0.3's cohort dashboard. Resolution: hybrid — learner-local state stays SQLite-on-device (D-007 preserved for learner surface); a new operator-tier Postgres stores cohort aggregations + operator accounts + issued credentials. Learner auth deferred (single-learner-per-device still valid for pilot). Operator auth = session-based, single operator role in v0.3. Research phase to validate Postgres-in-LXC + migration path. 0.75 Full Postgres migration (abandons SQLite pilot work), defer DASH-01 again (scope creep), no auth (insecure)
D-032 Mastery gate = N-of-M varied-scenario success + rubric score ≥ threshold Operationalizes PRD principle 6 ("move on when you can do the thing"). N=3 distinct scenarios, rubric mean ≥ 3.5/5.0 (configurable per path). Research phase to validate rubric model + threshold against competency-based-assessment literature. 0.70 Single-scenario pass (gaming risk), pure rubric score (no variety), pure time-on-task (invalid)
D-033 Verifiable credentials = W3C VC Data Model 2.0, platform-issued (operator key), Ed25519 signatures Research-anticipated: W3C VC 2.0 is the current standard; platform-issued is simplest viable issuer model (no DID method proliferation); Ed25519 is compact + widely supported. Self-issued (learner-side key) rejected — no tamper-evidence authority. Third-party issuer (university/agency) deferred to v0.9 credentialing milestone. Revocation = simple status list (VC Status List v2025). 0.70 Self-issued (no authority), third-party issuer (v0.9 scope), JWT-VC (less mature tooling)
D-034 Cohort anonymization = k-anonymity ≥ 10 + aggregation window ≥ 7 days REQ-DASH-01 operator view must not expose individual learners. k=10 is the conventional minimum for anonymized analytics; 7-day aggregation prevents re-identification via sparse windows. Research phase to validate against differential-privacy literature. Operator sees aggregate progression/failure-patterns only. 0.70 No anonymization (privacy violation), differential privacy (over-engineered for v0.3 scale), k=5 (too weak)
D-035 Dynamic difficulty = IRT-informed (1-parameter Rasch), updated per session REQ-SCEN-02. Item Response Theory (1PL/Rasch) is the simplest well-grounded model: learner ability θ, scenario difficulty b, P(success)=logistic(θ−b). Bayesian update of θ after each session. Avoids 2PL/3PL complexity (discrimination/guessing params — needs more data than v0.3 has). Research phase to validate. 0.70 ELO-like (less theoretically grounded), fixed difficulty steps (no adaptation), 2PL/3PL (data-hungry)
D-036 Scenario library structure = YAML directory + index manifest, tagged by skill/difficulty/failure_mode/rubric Extends D-018's YAML DSL. Library = scenarios/<path>/<scenario>.yaml + scenarios/index.yaml manifest (tagged, versioned). Expert-authored scenarios ship as YAML; AI-generated variations use the same schema with a generated_from backref. Rubric mapping added to scenario schema (each scenario declares which rubric criteria it exercises). 0.80 Database-backed library (premature — YAML is diffable + authorable per C-7), JSON (no comments per D-018), inline in code (couples authoring to engineering)
D-037 Path structure = 6-week job-structured path, JSON + YAML, mastery gates between weeks REQ-PATH-02 (PRD §6.4). Path = paths/<slug>.yaml defining 6 weeks, each week = a set of scenarios + a mastery gate. Gate opens when D-032 mastery condition met. v0.3 ships the Customer Service path fully (6 weeks) with ≥1 scenario per week (library REQ-SCEN-03 fills the rest). 0.75 Free-form progression (no structure), 12-week (too long for pilot), week-as-fixed-time (relax to mastery-paced)
D-038 Rubric scoring path = rule-based final score, LLM-assisted criterion extraction only (REQ-NFR-MAST-01) Final score must be deterministic. LLM (deepseek-v4-flash:cloud no_think) extracts criterion evidence from session turns (which utterance maps to which rubric criterion); a rule function computes the 1-5 score per criterion from the extracted evidence + branch outcome. No LLM in the numeric scoring step. Preserves REQ-NFR-MAST-01 determinism + keeps latency off the voice path. 0.80 Pure-LLM scoring (non-deterministic, violates NFR-MAST-01), pure-rule extraction (rigid — can't handle free-form speech)
D-039 Rubric YAML format = rubrics/<skill>.yaml with criteria, 5-level anchors, per-skill weights Extends D-018's YAML-everywhere stance. One rubric file per skill (v0.3: rubrics/customer_service.yaml). Each criterion has id, name, 5 anchored levels (1=fail … 5=mastery), weight. Scenario YAML maps to rubric criteria via rubric_criteria field (D-036). 0.80 JSON (no comments per D-018), inline in scenario (couples rubric to scenario — rubric is per-skill not per-scenario), DB-backed (premature)
D-040 Operator Postgres deployment = second Docker service in the existing LXC CT (docker-compose.yml adds postgres service) REQ-NFR-MT-01. Reuses v0.2's LXC + Docker-in-LXC. No new CT, no host Postgres. Postgres 16, persistent volume, internal Docker network only (not exposed to bridge). Operator auth + cohort API + VC issuer connect to it. 0.80 Separate CT (over-provisioned for v0.3 scale), host Postgres (PVE host dependency), SQLite for operator (cohort aggregation needs relational + k-anonymity queries — SQLite workable but Postgres is the safer default)
D-041 Operator auth = session-cookie, argon2id passwords, single operator role, login rate-limited (5 attempts/min) REQ-NFR-AUTH-01. Simplest viable auth for v0.3's single operator role. No OAuth/JWT complexity for one role. Cookie: httpOnly, secure, SameSite=Strict, 8h expiry. Rate limit via in-memory counter (single-instance). RBAC deferred (one role). 0.75 JWT (over-engineered for server-side session), OAuth (no IdP yet), basic-auth (insecure), no rate-limit (brute-force risk)
D-042 VC issuer key = Ed25519 keypair in operator-tier secrets (PRAXIS_VC_ISSUER_KEY), generated on first issuer init, not committed REQ-NFR-VC-01. Key generated at first boot if absent, stored in Postgres issuer_keys table encrypted at rest with a root key from secrets. Verification endpoint serves the public key. Rotation = new key + old key marked superseded (not revoked — old VCs still verify against archived public key). 0.70 RSA (larger, slower), KMS-managed (no KMS in LXC), self-signed cert chain (X.509 complexity unjustified for one issuer)
D-043 VC verification endpoint = public, unauthenticated, GET /vc/verify/<credential_id> Third parties (employers/agencies) verify credentials without an account. Returns {valid: bool, status: "active"|"revoked", issuer: "praxis-v0.3", mastery: {...}}. No PII in the verification response beyond what the credential itself asserts. 0.80 Authenticated verification (friction for employers), no public endpoint (credentials not portable), returns full learner PII (privacy violation)
D-044 Cohort dashboard UI = React route under /operator/*, served by the same FastAPI server (new prefix), reuses v0.2 StaticFiles REQ-DASH-01. Frontend-engineer reactivates (PERSONAS.md). Adds /operator React route + /api/operator/* FastAPI endpoints. Auth gate in React + server-side session check. No separate SPA build — same client/dist. 0.75 Separate operator SPA (extra build pipeline), server-rendered HTML (abandons React investment), no UI (operator reads JSON — not a product)
D-045 Cohort aggregation trigger = on-session-end hook + nightly reconciliation job REQ-MT-02. Hook fires after end_session() → writes k-anonymized aggregate to Postgres (incremental). Nightly job (cron in the praxis service) reconciles + recomputes 7-day windows. Hybrid: low-latency updates + correctness guarantee. 0.70 Pure real-time (race-prone), pure nightly (stale, violates NFR-DASH-02 if job lags), CDC/streaming (over-engineered)
D-046 IRT θ persistence = in learner-local SQLite (learner_ability table: learner_id, path, theta, updated_at) REQ-NFR-IRT-01. θ is per-learner-per-path, computed in-process on session end, no LLM call. Stays in SQLite with the rest of learner state (D-007 preserved). Cohort dashboard sees only k-anonymized aggregates of θ, never raw θ. 0.80 Postgres (couples learner state to operator tier — violates D-031 hybrid), in-memory (lost on restart), file-based JSON (no queryability)
D-047 Scenario library minimum for v0.3 = ≥6 expert-authored Customer Service scenarios (one per path week) + AI-generated variations gated by expert review REQ-SCEN-03/04. 6 scenarios give the mastery gate's N=3 varied-scenario condition room (D-032) without being so few that mastery is gameable. AI variations: LLM generates a variation from an expert scenario's schema with generated_from backref; expert reviews + approves before it enters the library. 0.70 3 scenarios (mastery gate N=3 = exactly the minimum — no room for failure-retry variety), 12 scenarios (over-scoped for one milestone), no AI variations (loses REQ-SCEN-04)
D-048 Mastery gate open action = advance learner to next path week + issue VC if week-final gate When D-032 condition met for a week's scenarios: learner progress.current_week advances. If the gate is the final week's gate, a VC is issued (REQ-MAST-03) asserting mastery of the path. Mid-path gates: no VC, just advancement. VCs are path-level, not week-level. 0.75 VC per week (credential spam — devalues the credential), no advancement (mastery gate is decorative), manual advancement (violates autonomy)
D-049 v0.3 activation of D-009 failure-injection = NO — failure-injection stays architecturally present but not provoked in v0.3 D-009 hook stays in the schema. v0.3 mastery scoring scores recovery from naturally-occurring failure branches (the escalate branch in cs_refund_ca_v01), not AI-provoked failures. Active failure injection couples to a "failure-recovery coaching" feature that's a later milestone. v0.3 RESEARCH confirms this — no new failure-injection scenarios authored. 0.80 Activate failure injection in v0.3 (couples mastery scoring to a new feature — scope creep), remove the hook (breaks forward compat)
D-050 Postgres connection from praxis service = asyncpg pool over Docker internal network, service DNS name postgres CLARIFY auto-decide (full autonomy). docker-compose defines a postgres service on an internal bridge network; the praxis service reaches it via postgresql://praxis:${PRAXIS_PG_PASSWORD}@postgres:5432/praxis. asyncpg is the async Pg driver (matches FastAPI async). No external port exposure. Single connection pool (min 1, max 10 — v0.4 scale). 0.85 psycopg2 sync (blocks event loop), external port + host access (security surface), pgbouncer (over-provisioned for v0.4 scale)
D-051 VC issuer key migration = fresh keypair on first v0.4 boot; v0.3 SQLite-issued VCs remain verifiable via archived public key CLARIFY auto-decide. v0.3 stored the Ed25519 issuer key in SQLite (issuer_keys table). v0.4 generates a fresh keypair in Postgres issuer_keys (D-040), marks it active, and archives the v0.3 public key as superseded (not revoked — old VCs still verify against it). The verification endpoint tries the active key first, falls back to superseded keys for older credentials. No re-issuance of v0.3 VCs. 0.80 Re-issue all v0.3 VCs (unnecessary churn, learners hold old credentials), revoke v0.3 key (breaks old VCs), keep SQLite key store (defeats D-031 hybrid)
D-052 Operator account bootstrap = first-run CLI script scripts/create-operator.py creates the initial operator from env-provided credentials CLARIFY auto-decide. No signup UI (operators are provisioned, not self-serve). Script reads PRAXIS_BOOTSTRAP_OPERATOR_USER + PRAXIS_BOOTSTRAP_OPERATOR_PASS from .env.secrets, hashes the password with argon2id, inserts into operators table. Idempotent (no-op if user exists). Subsequent operators added via the same script (run by the operator from the host). RBAC deferred (D-041 single role). 0.80 First-run web wizard (UI surface for a one-time action), hardcoded admin/admin (insecure), SQL insert (no password hashing)
D-053 Cohort dashboard v0.4 scope = 3 views: practice-volume, mastery-progression, failure-patterns — all k-anonymized ≥10, 7-day rolling windows CLARIFY auto-decide. REQ-DASH-01 names "practice, mastery progression, failure patterns" — v0.4 implements exactly those three views, no more. (1) Practice volume: sessions/day per path, anonymized. (2) Mastery progression: % learners at each week, gate-open rate. (3) Failure patterns: top failure modes by frequency, rubric criterion weak-spots. Each view = a /api/operator/<view> endpoint returning pre-aggregated rows from cohort_aggregates; React renders read-only tables + sparkline charts. No filters beyond path + window (no per-learner drill-down — k-anon). 0.80 Full BI dashboard (over-scoped for v0.4), single combined view (loses the three named aspects), per-learner drill-down (violates k-anon)
D-054 Aggregation trigger = async fire-and-forget on session end (non-blocking); nightly reconciliation job at 03:00 CT CLARIFY auto-decide. D-045 named the trigger; this clarifies the semantics. On end_session(), the server enqueues an aggregation task to an in-process asyncio.Task (no Celery/Redis for v0.4 scale) — non-blocking, the session-end response returns immediately. Failures log + the nightly job reconciles (idempotent upsert by window). Nightly job: cron-style asyncio.create_task loop, recomputes all 7-day windows. If the service restarts, the in-flight task is lost but nightly reconciliation covers it. 0.80 Sync on session-end (adds latency to learner path — violates C-8), Celery+Redis (over-provisioned), CDC streaming (over-engineered)
D-055 Postgres backup = nightly pg_dump to a named Docker volume, 7-day retention CLARIFY auto-decide. Postgres data lives on a named Docker volume (pgdata) inside the LXC CT. Nightly cron job runs `pg_dump praxis gzip > /backups/praxis-$(date).sql.gz to a second named volume (pgbackups). 7-day retention (rotates oldest). Operator can pct pull` backups to the PVE host. No streaming replication (single CT, no replica target). This is pilot-tier backup; a later milestone adds off-CT replication. 0.70
D-056 Auth session store = signed stateless cookies (HMAC-SHA256), no server-side session table CLARIFY auto-decide. D-041 said "session-cookie" — clarifying: the cookie is a self-contained signed token (user_id, issued_at, expiry, HMAC). No sessions table in Postgres. Verification = recompute HMAC + check expiry. Logout = client clears cookie (stateless — no server revocation list in v0.4). Rate limit is in-memory (single-instance). This minimizes DB load + simplifies the auth surface. A later milestone adds a revocation list if multi-instance or forced-logout is needed. 0.75 Postgres sessions table (DB load + cleanup job), Redis sessions (extra service), JWT with claims (same idea, more complex tooling)
D-057 Auth enforcement = server-side on every /api/operator/* request + React route guard for UX, never trust the client CLARIFY auto-decide. FastAPI middleware checks the signed cookie on every /api/operator/* request; 401 if missing/invalid/expired. React /operator/* routes check a /api/operator/me call on mount and redirect to /operator/login if 401 — this is UX only, the server is the authority. The cohort dashboard reads only k-anonymized aggregates (D-034) so even an auth bypass leaks no PII (defense in depth). VC issuance endpoints (/api/operator/credentials/*) are also auth-gated. 0.85 Server-only (poor UX — no redirect), React-only (insecure — bypassable), no auth on issuance (credential forgery risk)

Confidence updates from research

ID Before After Reason
D-003 0.75 0.95 Both Ollama model IDs verified in catalog as real, current, cloud-hosted tags
D-007 0.80 0.90 SQLite confirmed appropriate for v0.1 single-learner scale; no evidence favors alternatives

Target Users (v0.3: Canada pilot — Customer Service path)

Persona Description Pain
Aspiring Adebayo → "Aspiring Alex" 1928, Canada. Recent secondary school grad. Smartphone, limited data. Wants a service job. Can't afford vocational school. Needs to actually do the job.
Upskilling Ursula → "Upskilling Uma" 2540, Canada. Retail, hospitality, healthcare. Wants promotion/new role. No time for courses. Learns on the job.
Frontline Felix Customer service / sales / field tech agent, hired recently. Manager has no time to coach. Wants quick on-shift practice.

Success Metrics (Year-1 targets, post-v0.1)

Metric Target Why
Active weekly learners 100k Engagement, not downloads
Sessions per learner / week ≥5 Habit formation
Mastery rate per path ≥40% completion Real learning
Median session length 610 min On-the-go use
Cost / active learner / month ≤$3 Sustainable
Reported job/promotion outcome ≥25% North star
NPS (learner) ≥50 Word-of-mouth growth

Open Questions (for research/clarify phases)

  1. Will learners talk to their phone in public? (earbuds + "no one will know" framing)
  2. How to certify mastery credibly? (employer/agency recognition)
  3. Domain safety minimum HITL for health/electrical scenarios
  4. Voice cloning / impersonation disclosure
  5. Monetization model (deferred to Phase 1)
  6. Skills that should remain out of scope

References

  • PRD v0.1 (this document's source)
  • ARCHITECTURE.md — system architecture
  • ROADMAP.md — phase breakdown
  • REQUIREMENTS.md — formal requirements with REQ-IDs