v0.3 milestone merged to main. Mastery scoring + competency rubrics + verifiable credentials (formative-tier) shipped. 13/13 REQ-IDs covered. Next milestone: v0.4 (operator tier — cohort dashboard + auth + Postgres). ---ci--- project: praxis phase: 2 milestone: v0.3 status: complete milestone_complete: true milestone_merged_to_main: true ---/ci---
31 KiB
Praxis — v0.3 Execution Plan (Mastery Scoring + Competency Rubrics + VC Issuance)
Milestone: v0.3 (Mastery scoring + competency rubrics + verifiable credentials) Phases: 1 execution phase (P1: mastery core + IRT + scenarios + paths + VC issuance) + final phase (P2: review + ship) Ship: v0.1.3 (Phase 0) → v0.1.4 (P1) → v0.1.5 (P2 = v0.3 milestone release) Status: plan (grill-amended — operator tier deferred to v0.4 per GRILL-v0.3.md Axis 2 + Axis 8) Autonomy: full Parallelization: enabled, max 5 concurrent agents Personas active: lead-developer, backend-engineer, data-engineer, security-engineer (frontend-engineer + devops-engineer DEACTIVATED — no UI, no new deploy scripts in v0.3) Date: 2026-08-03
Grill Amendments (binding — per GRILL-v0.3.md)
The grill (GO-WITH-CONDITIONS, 4 MUST) restructured this plan:
- Axis 2 (MUST) — Split the milestone. The operator tier (REQ-DASH-01, REQ-AUTH-01, REQ-MT-01/02 + associated NFRs) is deferred to v0.4. v0.3 is now a clean learner-facing mastery milestone. This restores the original ROADMAP intent (dashboard was v0.8) and avoids the hybrid SQLite+Postgres topology in v0.3.
- Axis 8 (MUST) — VC issuance moves to P1. VC issuance is a learner-facing consequence of mastery (D-048), not an operator feature. Issuer keys are SQLite-backed in v0.3 (Postgres takes over in v0.4 when the operator tier arrives).
- Axis 3 (MUST) — VC interop + key-rotation tests added. TASK-12-07 (external W3C verifier interop) + TASK-12-08 (key-rotation operational drill).
- Axis 4 (MUST) — Three technical-risk fixes. (a) VC labeled
formativein payload + verification + REQ-MAST-03. (b) R-AUTH-01 deferred to v0.4 with the operator surface (no auth in v0.3 → no cookie issue). (c) Evidence-extraction fallback changed from silent-fail-to-zero toscoring_inconclusivewith learner-visible retry signal.
FIX conditions (non-blocking, tracked in VERIFY): re-task SLICE-12/13 (now moot for v0.3 — operator tier deferred), wire VC trigger (resolved — VC now in P1), Postgres-failure semantics (deferred to v0.4), real-LLM smoke test (added to P1 SLICE-08), k-anonymity differencing-attack test (deferred to v0.4), reconciliation drift-correction test (deferred to v0.4), de-escalation weight clarification (static in v0.3 — dynamic re-weighting is a future feature).
Phase Split Rationale (post-grill)
v0.3 is now a single execution phase (P1) + final review/ship (P2):
- P1 (Mastery Core + VC Issuance): rubric engine, IRT, scenario library (≥6 CS scenarios), path engine (6-week), mastery score + gate logic, VC issuer (W3C VC 2.0, Ed25519, SQLite-backed issuer keys, public verification endpoint). All learner-facing. Shippable as
v0.1.4. - P2 (Final): review + audit + milestone ship (
v0.1.5= v0.3 milestone release).
The operator tier (cohort dashboard, auth, Postgres) is v0.4 — a separate milestone with its own phase 0. This keeps v0.3 honest: one milestone, one shippable learner-facing deliverable, no hybrid storage, no operator auth surface.
Deferred to v0.4 (operator tier — per grill Axis 2)
The following REQ-IDs are deferred to v0.4 and removed from v0.3 scope:
- REQ-DASH-01 (cohort dashboard) — was v0.8 on original ROADMAP; v0.4 is still ahead of that but follows the grill's "split the milestone" verdict
- REQ-AUTH-01 (operator auth) — no operator surface in v0.3 → no auth needed
- REQ-MT-01, REQ-MT-02 (operator Postgres, cohort aggregation) — no operator tier in v0.3
- REQ-NFR-DASH-01, REQ-NFR-DASH-02, REQ-NFR-AUTH-01, REQ-NFR-MT-01 — associated NFRs
v0.3 REQ-IDs (post-grill): 13 (REQ-MAST-01/02/03, REQ-SCEN-02/03/04, REQ-PATH-02 + 6 NFRs: REQ-NFR-MAST-01/02, REQ-NFR-VC-01/02, REQ-NFR-IRT-01). REQ-MAST-04 is a principle (accepted).
Phase 1 — Mastery Core (learner-facing mastery layer)
Branch: phase/01-mastery-core → merged to milestone/v0.3-mastery-scoring
Ship: v0.1.4 (patch release, feature milestone type)
REQ-IDs covered: REQ-MAST-01, REQ-MAST-02, REQ-SCEN-02, REQ-SCEN-03, REQ-SCEN-04, REQ-PATH-02, REQ-NFR-MAST-01, REQ-NFR-MAST-02, REQ-NFR-IRT-01
Slices: 8 vertical slices in 4 waves
Total tasks: 38
| Wave | Slices | Parallel slots | Description |
|---|---|---|---|
| 1 | SLICE-01, SLICE-02 | 2 | Rubric schema + scenario library schema (parallel — disjoint file territories) |
| 2 | SLICE-03, SLICE-04, SLICE-05 | 3 | Rubric scoring engine + IRT engine + path engine (parallel — all depend on W1 schemas, disjoint modules) |
| 3 | SLICE-06, SLICE-07 | 2 | Scenario library content (≥6 CS scenarios) + mastery score + gate logic (parallel — SLICE-06 authors scenarios, SLICE-07 wires scoring into session_recorder) |
| 4 | SLICE-08 | 1 | Integration tests + mastery-gate audit log + real-LLM smoke test (depends on all prior) |
| 5 | SLICE-09 | 1 | VC issuer + verification endpoint + interop/rotation tests (depends on SLICE-07 gate-open trigger) |
Wave dependency graph
Wave 1 ────────────────────────────────────────
SLICE-01 (rubric YAML schema + loader)
SLICE-02 (scenario library schema + index + loader)
│
▼
Wave 2 ────────────────────────────────────────
SLICE-03 (rubric scoring engine: evidence extractor + rule scorer) ← depends on SLICE-01
SLICE-04 (IRT engine + theta persistence) ← depends on SLICE-02 (scenario difficulty)
SLICE-05 (path engine: 6-week structure + progression) ← depends on SLICE-02 (scenario library)
│
▼
Wave 3 ────────────────────────────────────────
SLICE-06 (≥6 expert CS scenarios + index.yaml + rubric mapping) ← depends on SLICE-01, SLICE-02
SLICE-07 (mastery score + gate logic + session_recorder hooks) ← depends on SLICE-03, SLICE-04, SLICE-05
│
▼
Wave 4 ────────────────────────────────────────
SLICE-08 (integration tests + mastery-gate audit log in SQLite + real-LLM smoke) ← depends on all prior
│
▼
Wave 5 ────────────────────────────────────────
SLICE-09 (VC issuer: Ed25519 + JCS + Status List + verification + interop + rotation) ← depends on SLICE-07 (gate-open trigger)
Persona load distribution (P1)
| Persona | Tasks | Primary territory |
|---|---|---|
| backend-engineer | 20 | server/mastery/**, server/scenarios/library.py, server/paths/**, server/session_recorder.py extension |
| security-engineer | 8 | server/vc/** (Ed25519 issuer, JCS, Status List, verification endpoint, interop + rotation tests) |
| data-engineer | 6 | db/migrations/0003_mastery.sql (learner_ability, mastery_progress, issuer_keys, issued_credentials, mastery_gate_events tables), db/store.py v0.3 additions |
| lead-developer | 6 | pyproject.toml deps, integration test orchestration, cross-persona coordination |
| frontend-engineer | 0 | DEACTIVATED (no UI in v0.3 — dashboard is v0.4) |
| devops-engineer | 0 | DEACTIVATED (no new deploy scripts) |
Total P1 tasks: 40 (was 38 + 8 VC - 6 rebalanced; +2 grill interop/rotation tests)
SLICE-01: Rubric Schema + Loader (W1)
- Goal: Define the competency rubric YAML format + Pydantic model + loader so scenarios can reference rubric criteria.
- REQ-IDs covered: REQ-MAST-01 (partial — schema only), REQ-NFR-MAST-01 (determinism foundation)
- Wave: 1
- Dependencies: none
- Persona: data-engineer (schema), backend-engineer (loader)
Tasks
TASK-01-01 — Rubric YAML schema definition
- Persona: data-engineer
- File:
rubrics/customer_service.yaml(new — refund/complaint archetype per RESEARCH §6.2) - Content: 4 criteria (empathy 0.35, resolution 0.30, de-escalation 0.20, professionalism 0.15), 5-level anchors each (level 1=fail … 5=mastery/entrustable, per RESEARCH §2), per-archetype weights (D-039 amendment). Professionalism = conjunctive floor ≥2.
TASK-01-02 — Rubric Pydantic model
- Persona: backend-engineer
- File:
server/mastery/rubric_schema.py(new) - Content:
Rubric,RubricCriterion,RubricLevelmodels. Fields: id, skill, criteria[{id, name, weight, levels[{level, anchor, signals[]}]}]. Validate weights sum to 1.0. Validate 5 levels per criterion.
TASK-01-03 — Rubric loader
- Persona: backend-engineer
- File:
server/mastery/rubric_loader.py(new) - Content:
load_rubric(skill: str) -> Rubric— loadsrubrics/<skill>.yaml, parses via Pydantic. Caches in-memory. Validates against schema.
TASK-01-04 — Rubric unit tests
- Persona: backend-engineer
- File:
tests/test_rubric_schema.py(new) - Content: load valid rubric, reject invalid weights, reject missing levels, criterion lookup by id, weight sum validation.
SLICE-02: Scenario Library Schema + Index + Loader (W1)
- Goal: Extend the v0.1 scenario schema (D-018) with rubric mapping + library index manifest + loader for multi-scenario selection.
- REQ-IDs covered: REQ-SCEN-03 (partial — schema), REQ-SCEN-04 (partial — format extension)
- Wave: 1
- Dependencies: none (parallel with SLICE-01 — disjoint files)
- Persona: backend-engineer
Tasks
TASK-02-01 — Extend Scenario schema with rubric mapping + IRT fields
- Persona: backend-engineer
- File:
server/scenarios/schema.py(extend existing) - Content: Add
rubric_criteria: list[{criterion_id, weight, evidence_required}]field toScenario. Addirt_target_p: float = 0.7field (D-035 practice default). Addversion: str(semver, D-036). Addgenerated_from: str | None(AI-variation backref, D-036). Addintent_hash: str | None(structural drift detection). Keep backward compat with v0.1 scenario YAML.
TASK-02-02 — Scenario index manifest
- Persona: backend-engineer
- File:
scenarios/index.yaml(new — slim manifest per RESEARCH §D) - Content: list of {id, path, title, difficulty, failure_mode, rubric_criteria, version, author, generated_from}. ~50 lines/scenario metadata. Updated when scenarios are added.
TASK-02-03 — Scenario library loader
- Persona: backend-engineer
- File:
server/scenarios/library.py(new) - Content:
ScenarioLibraryclass — loadsscenarios/index.yaml, loads individual scenario YAMLs on demand, validates against schema.list_by_path(path),list_by_difficulty(range),get(scenario_id),select_for_theta(theta, path)(IRT-aware selection targeting ~50% or ~70% perirt_target_p). EnforcesMIN_COVERAGE = 2scenarios per rubric criterion (CI check, RESEARCH §D).
TASK-02-04 — Library unit tests
- Persona: backend-engineer
- File:
tests/test_scenario_library.py(new) - Content: load index, list by path, select_for_theta, MIN_COVERAGE validation, reject invalid semver, AI-variation backref validation.
SLICE-03: Rubric Scoring Engine (W2)
- Goal: Implement the deterministic rubric scoring flow: LLM-extracts-evidence, rules-score-evidence (D-038, REQ-NFR-MAST-01).
- REQ-IDs covered: REQ-MAST-01 (scoring logic), REQ-NFR-MAST-01 (determinism)
- Wave: 2
- Dependencies: SLICE-01 (rubric schema)
- Persona: backend-engineer
Tasks
TASK-03-01 — Evidence extractor (LLM, off-voice-path)
- Persona: backend-engineer
- File:
server/mastery/evidence_extractor.py(new) - Content:
async extract_evidence(turns, rubric_criteria) -> list[Evidence]. Calls deepseek-v4-flash:cloud, temp=0, JSON-schema-validated output:[{criterion_id, quote, signals: [...]}]. Critical: fuzzy-match quote against transcript (rapidfuzz or difflib) → reject + re-extract on mismatch (R-MAST-02). Max 2 re-extraction attempts; on final failure, mark scenario asscoring_inconclusive— do NOT count toward gate, do NOT penalize learner, surface 'technical issue, please retry' in the debrief (grill Axis 4 MUST #3 — silent fail-to-zero is unacceptable). Log the failure for operator review.
TASK-03-02 — Rule-based scorer (deterministic)
- Persona: backend-engineer
- File:
server/mastery/rubric_scorer.py(new) - Content:
score(evidence, rubric) -> list[CriterionScore]. Maps signals → 1-5 level per criterion via rubric YAML level anchors (each level has asignals[]list — match evidence signals to level signals). Deterministic — no LLM. Output:[{criterion_id, level, weight, evidence_quote}].
TASK-03-03 — Mastery Score computation (deterministic)
- Persona: backend-engineer
- File:
server/mastery/mastery_score.py(new) - Content:
compute_scenario_score(criterion_scores, rubric) -> ScenarioScore(weighted mean + conjunctive floor: every criterion ≥2, scenario mean ≥3.0 to pass).compute_path_score(passing_scenario_scores) -> PathScore(mean over passing scenarios only).check_gate(path_score, distinct_passed_count) -> bool(≥3 distinct passed AND ≥3.5 — D-032).
TASK-03-04 — Scoring unit tests
- Persona: backend-engineer
- File:
tests/test_rubric_scoring.py(new) - Content: evidence extraction with mocked LLM, quote fuzzy-match rejection, rule-based scoring determinism (same input → same output), conjunctive floor enforcement, gate logic.
TASK-03-05 — Evidence extractor integration test (mocked LLM)
- Persona: backend-engineer
- File:
tests/test_evidence_extractor_integration.py(new) - Content: end-to-end extraction → scoring with a mocked LLM returning canned evidence. Verify JSON schema validation, quote matching, deterministic scoring.
SLICE-04: IRT Engine + Theta Persistence (W2)
- Goal: Implement 1PL/Rasch IRT with Bayesian theta update, persisted to SQLite (D-046, REQ-NFR-IRT-01).
- REQ-IDs covered: REQ-SCEN-02, REQ-NFR-IRT-01
- Wave: 2
- Dependencies: SLICE-02 (scenario difficulty field)
- Persona: backend-engineer (engine), data-engineer (SQLite table)
Tasks
TASK-04-01 — IRT engine
- Persona: backend-engineer
- File:
server/mastery/irt.py(new) - Content:
class IRTEngine:P_success(theta, b) -> float(logistic(θ−b)).update_theta(theta, sigma_sq, outcome, b) -> (new_theta, new_sigma_sq)(Gaussian-approximation Bayesian: θ ← θ + (outcome − P) × σ²/(σ² + 1); σ² shrinks per observation).select_scenario(theta, library, path, target_p) -> Scenario(picks scenario with b closest to θ − logit(target_p)). Cold-start: θ=0, σ²=1; fall back toscenario.difficultyuntil ≥5 observations (R-IRT-01).
TASK-04-02 — Theta persistence (SQLite)
- Persona: data-engineer
- File:
db/migrations/0003_mastery.sql(new — adds learner_ability + mastery_progress tables),db/store.py(extend) - Content:
learner_abilitytable (learner_id, path, theta REAL, sigma_sq REAL, observations INTEGER, updated_at).mastery_progresstable (learner_id, path, current_week INTEGER, scenarios_passed_json TEXT, mastery_score REAL, gate_open bool, updated_at).PraxisStore.get_ability(),set_ability(),get_progress(),set_progress()async methods.
TASK-04-03 — IRT unit tests
- Persona: backend-engineer
- File:
tests/test_irt.py(new) - Content: P_success correctness, theta update convergence, cold-start fallback, select_scenario targeting, sigma_sq shrinkage.
TASK-04-04 — Theta persistence integration test
- Persona: data-engineer
- File:
tests/test_learner_ability_db.py(new) - Content: get/set ability round-trip, get/set progress round-trip, migration idempotency, concurrent writes (aiosqlite).
SLICE-05: Path Engine (W2)
- Goal: Implement the 6-week path structure with mastery gates (D-037, REQ-PATH-02).
- REQ-IDs covered: REQ-PATH-02
- Wave: 2
- Dependencies: SLICE-02 (scenario library — paths reference scenarios)
- Persona: backend-engineer
Tasks
TASK-05-01 — Path YAML schema + Pydantic model
- Persona: backend-engineer
- File:
server/paths/schema.py(new) - Content:
Pathmodel: slug, name, skill, weeks[{week, title, scenario_ids[], gate: {required_scenarios: int, required_score: float}}]. Validate 6 weeks. Validate scenario_ids exist in library.
TASK-05-02 — Customer Service path YAML
- Persona: backend-engineer
- File:
paths/customer_service.yaml(new) - Content: 6 weeks per PRD §6.4. Week 1: basics (refund scenario). Week 2: escalation. Week 3: policy exceptions. Week 4: multi-issue. Week 5: recovery. Week 6: mastery demonstration. Each week references ≥1 scenario from the library (SLICE-06). Gate: ≥3 distinct scenarios passed, score ≥3.5 (D-032).
TASK-05-03 — Path engine (progression logic)
- Persona: backend-engineer
- File:
server/paths/engine.py(new) - Content:
PathEngine:load_path(slug) -> Path.current_week(progress) -> int.check_gate(progress, week) -> bool(delegates to mastery_score.check_gate).advance_week(progress) -> progress(D-048).is_path_complete(progress) -> bool(week 6 gate open).
TASK-05-04 — Path unit tests
- Persona: backend-engineer
- File:
tests/test_path_engine.py(new) - Content: load path, validate 6 weeks, gate check, week advancement, path completion.
SLICE-06: Scenario Library Content (W3)
- Goal: Author ≥6 expert Customer Service scenarios filling the 6-week path (D-047, REQ-SCEN-03).
- REQ-IDs covered: REQ-SCEN-03, REQ-SCEN-04 (expert-authored; AI variations in P2 or later)
- Wave: 3
- Dependencies: SLICE-01 (rubric), SLICE-02 (library schema)
- Persona: lead-developer (content authoring — domain expertise), backend-engineer (validation)
Tasks
TASK-06-01 — Author 6 CS scenarios
- Persona: lead-developer
- Files:
scenarios/customer_service/cs_refund_ca_v01.yaml(exists — extend with rubric mapping),scenarios/customer_service/cs_escalation_ca_v02.yaml(new),scenarios/customer_service/cs_policy_exception_ca_v03.yaml(new),scenarios/customer_service/cs_multi_issue_ca_v04.yaml(new),scenarios/customer_service/cs_recovery_ca_v05.yaml(new),scenarios/customer_service/cs_mastery_demonstration_ca_v06.yaml(new) - Content: Each scenario: extends v0.1 schema with
rubric_criteria(mapped to the 4 CS criteria),irt_target_p(0.7 for practice weeks, 0.5 for mastery-demonstration week 6),version: 1.0.0,author: expert. Difficulty 1-5 across weeks. Failure modes vary (escalates_unresolved, policy_rigid, multi_issue_drop, recovery_missed).
TASK-06-02 — Update index.yaml manifest
- Persona: lead-developer
- File:
scenarios/index.yaml(update) - Content: All 6 scenarios listed with metadata.
MIN_COVERAGE = 2per criterion verified (each of empathy/resolution/de-escalation/professionalism exercised by ≥2 scenarios).
TASK-06-03 — Scenario validation tests
- Persona: backend-engineer
- File:
tests/test_scenario_library_content.py(new) - Content: all 6 scenarios load via schema, rubric_criteria reference valid criterion IDs, MIN_COVERAGE per criterion, semver valid, index.yaml in sync with files.
SLICE-07: Mastery Score + Gate Logic + Session Recorder Hooks (W3)
- Goal: Wire the rubric scoring + IRT + path progression into the session end flow (server/session_recorder.py).
- REQ-IDs covered: REQ-MAST-02, REQ-NFR-MAST-02 (auditability — SQLite log)
- Wave: 3
- Dependencies: SLICE-03 (scoring), SLICE-04 (IRT), SLICE-05 (path)
- Persona: backend-engineer
Tasks
TASK-07-01 — Extend session_recorder.py with mastery hooks
- Persona: backend-engineer
- File:
server/session_recorder.py(extend existing) - Content: After existing
end()logic: (1) callevidence_extractor.extract_evidence(turns, scenario.rubric_criteria), (2)rubric_scorer.score(evidence, rubric), (3)mastery_score.compute_scenario_score(...), (4)irt.update_theta(...), (5)path_engine.check_gate + advance_week, (6) recordmastery_gate_eventin SQLitemastery_gate_eventstable (REQ-NFR-MAST-02 audit), (7) if week-final gate open → callvc_issuer.issue_credential(...)(SLICE-09) — VC issuance is wired here, not in a later phase (grill Axis 8 MUST). All off the voice path (async, after session end). If evidence extraction returnsscoring_inconclusive, skip steps 2-7 and surface retry in debrief.
TASK-07-02 — Mastery gate event SQLite table
- Persona: data-engineer
- File:
db/migrations/0003_mastery.sql(extend),db/store.py(extend) - Content:
mastery_gate_eventstable (id, learner_id, path, week, scenarios_passed_json, rubric_scores_json, mastery_score, gate_opened_at).PraxisStore.record_gate_event()async method.
TASK-07-03 — Mastery integration test (end-to-end scoring flow)
- Persona: backend-engineer
- File:
tests/test_mastery_integration.py(new) - Content: simulate a session with turns → run mastery flow → verify scenario score, theta update, progress advancement, gate event recorded. Mocked LLM for evidence extraction. Verify determinism (same input → same scores).
TASK-07-04 — IRT selection integration (next-scenario recommendation)
- Persona: backend-engineer
- File:
server/scenarios/library.py(extend),tests/test_irt_selection_integration.py(new) - Content:
library.select_for_theta(theta, path)picks the next scenario. Integration test: given a theta and a path, verify the selected scenario targets the right P.
SLICE-08: Integration Tests + Mastery-Gate Audit Log (W4)
- Goal: End-to-end P1 integration tests + verify the mastery-gate audit log is complete and queryable.
- REQ-IDs covered: REQ-NFR-MAST-02 (full auditability)
- Wave: 4
- Dependencies: all prior slices
- Persona: lead-developer (orchestration), backend-engineer (tests)
Tasks
TASK-08-01 — End-to-end P1 smoke test
- Persona: lead-developer
- File:
scripts/test_mastery_e2e.py(new) - Content: simulate 3 sessions across 3 distinct scenarios → verify mastery gate opens after 3 passing scenarios with score ≥3.5. Verify theta converges. Verify progress advances. Verify gate events recorded.
TASK-08-02 — Audit log queryability test
- Persona: backend-engineer
- File:
tests/test_gate_audit_log.py(new) - Content: query mastery_gate_events by learner, by path, by date range. Verify evidence (scenarios_passed, rubric_scores) is persisted and reconstructable.
TASK-08-03 — P1 verification matrix
- Persona: lead-developer
- File:
.ciagent/VERIFY-P1.md(new — pre-verify checklist for the verify stage) - Content: REQ-ID → test mapping. Confirm all P1 REQ-IDs have covering tests.
TASK-08-04 — Real-LLM evidence extraction smoke test (grill Axis 7 FIX #1)
- Persona: backend-engineer
- File:
scripts/test_real_llm_evidence.py(new — staging-gated, requires OLLAMA_API_KEY) - Content: run one real session transcript through the actual deepseek-v4-flash:cloud evidence extractor. Verify output is valid JSON with fuzzy-matching quotes. This runs only in staging (gated by
PRAXIS_RUN_REAL_LLM_TESTS=1env). Mocked-LLM tests stay in CI. Validates that the extraction prompt works, not just the scoring logic.
SLICE-09: VC Issuer + Verification Endpoint + Interop/Rotation Tests (W5)
- Goal: Implement Ed25519-signed W3C VC 2.0 issuance + public verification + Status List revocation, SQLite-backed issuer keys (D-033, D-042, D-043, REQ-MAST-03, REQ-NFR-VC-01, REQ-NFR-VC-02). VC labeled
formativeper grill Axis 4 MUST #1. - REQ-IDs covered: REQ-MAST-03, REQ-NFR-VC-01, REQ-NFR-VC-02
- Wave: 5
- Dependencies: SLICE-07 (gate-open trigger — TASK-07-01 step 7 calls issue_credential)
- Persona: security-engineer (issuer + crypto), data-engineer (SQLite issuer_keys/issued_credentials tables)
Tasks
TASK-09-01 — SQLite issuer keys + issued_credentials tables
- Persona: data-engineer
- File:
db/migrations/0003_mastery.sql(extend),db/store.py(extend) - Content:
issuer_keystable (id, public_key TEXT, private_key_enc BLOB, status TEXT active|superseded, created_at).issued_credentialstable (id, learner_id, vc_payload_json, signature_b64, status active|revoked, issued_at).PraxisStoreasync methods:init_issuer_key(),get_active_signing_key(),get_public_key(key_id),insert_credential(),get_credential(),set_credential_status(). Private key encrypted at rest withPRAXIS_VC_ISSUER_KEYroot key from env (D-042).
TASK-09-02 — Ed25519 issuer key management + VC payload builder + JCS + signing
- Persona: security-engineer
- File:
server/vc/issuer_keys.py(new),server/vc/issuer.py(new) - Content:
init_issuer_key(store, root_key) -> KeyPair— generate Ed25519 (pynacl), encrypt private key, store in SQLite.build_vc_payload(learner_ref, path, scenarios_passed, rubric_score, completed_weeks, evidence) -> dict(W3C VC 2.0:scenariosPassed,rubricScore,completedWeeks: 6,evidence,issuedAt,validUntil: +3y,credentialTier: "formative"per grill Axis 4).canonicalize(payload) -> bytes(JCS via canonicaljson).sign(payload, signing_key) -> str(eddsa-jcs-2022).issue_credential(...) -> str(stores in SQLite).
TASK-09-03 — Bitstring Status List (revocation)
- Persona: security-engineer
- File:
server/vc/status_list.py(new) - Content:
BitstringStatusList— one bitstring per status list, indexed by credential sequence.set_status(credential_idx, revoked),get_status(credential_idx) -> bool. Persisted in SQLite (status_liststable or adjacent to issuer_keys). Revocation latency = next verify call (status list fetched from SQLite on every verification — no cache, REQ-NFR-VC-02).
TASK-09-04 — Public verification endpoint
- Persona: security-engineer
- File:
server/vc/verification.py(new),server/__main__.py(extend — add route) - Content:
GET /vc/verify/<credential_id>— public, unauthenticated (D-043). Fetch credential from SQLite, fetch issuer public key fromverificationMethodURL, validate Ed25519 signature, check status list. Return{valid, status, issuer, credential, mastery, credentialTier: "formative", verifiedAt}. No PII beyond what the credential asserts.
TASK-09-05 — VC unit tests
- Persona: security-engineer
- File:
tests/test_vc_issuer.py(new) - Content: key generation, sign/verify round-trip, tamper detection (flip a byte → verify fails), JCS canonicalization determinism, status list set/get, revocation invalidates verification.
TASK-09-06 — VC integration test (issue → verify round-trip + key rotation)
- Persona: security-engineer
- File:
tests/test_vc_integration.py(new) - Content: issue a credential, GET /vc/verify/ → valid: true, credentialTier: formative. Revoke → GET → valid: false, status: revoked. Tamper payload → verify fails. Key rotation: old VC still verifies against archived public key.
TASK-09-07 — VC interop test (grill Axis 3 MUST #1 — external W3C verifier)
- Persona: security-engineer
- File:
tests/test_vc_interop.py(new — staging-gated, requires external verifier dependency) - Content: verify a Praxis-issued VC against at least one external W3C VC verifier (e.g.,
digitalbazaar/vc-verifieror a JS@digitalcredentials/vcverifier via subprocess). Round-trip self-verification is insufficient for cryptographic claims. This is the grill's binding MUST — custom crypto code without interop verification is an unmitigated liability.
TASK-09-08 — Key-rotation operational drill (grill Axis 3 MUST #2)
- Persona: security-engineer
- File:
tests/test_vc_key_rotation_drill.py(new) - Content: end-to-end operational drill — issue N VCs with key A, rotate to key B (archive A as superseded), issue M VCs with key B, verify all N+M VCs still verify (N against archived key A, M against active key B), revoke one of each, verify revocation. This is the one crypto procedure that, if broken, silently invalidates every credential ever issued.
Final Phase (P2) — Review + Audit + Milestone Ship
Branch: phase/02-final-review-ship → merged to milestone/v0.3-mastery-scoring → merged to main
Ship: v0.1.5 (final patch = v0.3 milestone release)
REQ-IDs covered: all v0.3 REQ-IDs (milestone-complete verification)
Tasks (delegated to ciagent-review + ciagent-audit + ciagent-ship)
- Run branch gate → create
phase/02-final-review-ship ciagent-review— multi-persona review across P1; auto-apply P0 fixes, flag P1+ciagent-audit— reconstruction test, file discipline, branch hygiene, commit disciplineciagent-ship— merge phase/02 → milestone/v0.3 → main; tag v0.1.5; create release with full milestone summary- Update REQUIREMENTS.md (all v0.3 REQ → complete), ROADMAP.md (v0.3 → complete; v0.4 = operator tier)
- Commit:
docs(milestone): complete v0.3-mastery-scoring - Clear checkpoint
REQ-ID Coverage Matrix (post-grill)
| REQ-ID | Phase | Slice(s) | Coverage |
|---|---|---|---|
| REQ-MAST-01 | P1 | SLICE-01, 03 | rubric schema + scoring |
| REQ-MAST-02 | P1 | SLICE-07 | mastery score + gate logic |
| REQ-MAST-03 | P1 | SLICE-09 | VC issuer (formative-tier, SQLite-backed) |
| REQ-MAST-04 | — | — | principle (accepted) |
| REQ-SCEN-02 | P1 | SLICE-04 | IRT dynamic difficulty |
| REQ-SCEN-03 | P1 | SLICE-02, 06 | scenario library |
| REQ-SCEN-04 | P1 | SLICE-02, 06 | expert-authored format + AI variation hooks |
| REQ-PATH-02 | P1 | SLICE-05 | 6-week path structure |
| REQ-NFR-MAST-01 | P1 | SLICE-03 | deterministic scoring |
| REQ-NFR-MAST-02 | P1 | SLICE-07, 09 | gate auditability (SQLite) |
| REQ-NFR-VC-01 | P1 | SLICE-09 | tamper-evidence + interop test (TASK-09-07) |
| REQ-NFR-VC-02 | P1 | SLICE-09 | revocation latency (next verify call) |
| REQ-NFR-IRT-01 | P1 | SLICE-04 | IRT <100ms |
Deferred to v0.4 (operator tier — per grill Axis 2): REQ-DASH-01, REQ-AUTH-01, REQ-MT-01, REQ-MT-02, REQ-NFR-DASH-01, REQ-NFR-DASH-02, REQ-NFR-AUTH-01, REQ-NFR-MT-01.
v0.3 total: 13 REQ-IDs covered (7 functional + 6 NFR). 0 partial. 0 deferred within v0.3. 8 REQ-IDs deferred to v0.4.
Open Questions Deferred to EXECUTE
- R-VC-02 (validUntil): 3-year default, configurable per path. Confirm in SLICE-09.
- R-IRT-01 (cold start): Fall back to scenario.difficulty until ≥5 observations. Confirm in SLICE-04.
- R-MAST-03 (per-archetype weights): Ship refund/complaint weights only in v0.3 (static — dynamic branch-dependent re-weighting is a future feature per grill Axis 9 FIX). Confirm in SLICE-06.
- VC interop test dependency: TASK-09-07 requires an external W3C verifier. Confirm which verifier is available (digitalbazaar/vc-verifier or @digitalcredentials/vc) and whether it runs in CI or staging-only.