docs(P00): clarify — 8 ambiguities auto-resolved (D-050..D-057, full autonomy)
Postgres conn (D-050), VC key migration (D-051), operator bootstrap (D-052),
dashboard scope (D-053), aggregation timing (D-054), backup (D-055),
session store (D-056), auth enforcement (D-057).
---ci---
project: praxis
phase: 0
milestone: v0.4
status: clarify
decisions:
- id: D-050
decision: "Postgres via asyncpg pool over Docker internal DNS"
rationale: "asyncpg matches FastAPI async; no external port exposure"
confidence: 0.85
- id: D-051
decision: "Fresh VC issuer keypair on first v0.4 boot; v0.3 VCs verifiable via archived key"
rationale: "No re-issuance churn; old credentials stay portable"
confidence: 0.80
- id: D-052
decision: "First operator via CLI script from env-provided credentials"
rationale: "Operators are provisioned not self-serve; no signup UI"
confidence: 0.80
- id: D-053
decision: "Dashboard v0.4 = 3 views (practice, mastery, failure) k-anon ≥10"
rationale: "REQ-DASH-01 names exactly those three; no per-learner drill-down"
confidence: 0.80
- id: D-054
decision: "Aggregation = async fire-and-forget on session end + nightly reconcile"
rationale: "Non-blocking learner path (C-8); nightly fixes in-flight losses"
confidence: 0.80
- id: D-055
decision: "Postgres backup = nightly pg_dump to volume, 7-day retention"
rationale: "Pilot-tier; off-CT replication deferred to later milestone"
confidence: 0.70
- id: D-056
decision: "Auth = signed stateless HMAC cookies, no sessions table"
rationale: "Minimizes DB load; revocation list deferred with multi-instance"
confidence: 0.75
- id: D-057
decision: "Auth = server-side on every /api/operator/* + React guard for UX"
rationale: "Server is authority; client guard is UX only; k-anon = defense in depth"
confidence: 0.85
---/ci---
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"phase": 0,
|
||||
"stage": "specify",
|
||||
"stage": "clarify",
|
||||
"milestone": "v0.4",
|
||||
"phase_role": "pre_execution",
|
||||
"attempts": 0,
|
||||
"updated_at": "2026-08-04T00:30:00Z",
|
||||
"updated_at": "2026-08-04T00:45:00Z",
|
||||
"milestone_complete": false,
|
||||
"milestone_merged_to_main": false,
|
||||
"tag": null,
|
||||
|
||||
@@ -177,6 +177,14 @@ v0.3 activated the mastery/assessment layer deferred from v0.1/v0.2 (per D-021).
|
||||
| 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 | No backups (data loss risk), WAL streaming to a replica (no replica in v0.4), S3 push (no S3 in LXC pilot) |
|
||||
| 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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user