# Praxis — v0.3 Research: Anonymization, IRT, Scenario Library > **Milestone:** v0.3 (Mastery scoring + competency rubrics) > **Phase:** 0 (research — pre-execution) > **Branch:** phase/00-pre-execution > **Status:** research complete — pending orchestrator review > **Date:** 2026-08-03 > **Method:** Domain-knowledge synthesis from the privacy-preserving analytics, psychometrics (IRT), and learning-content authoring literature. Where claims rest on a single source or empirical rule of thumb, the confidence score reflects that. Web-verification deferred — these are well-trodden fields with stable canonical references (Sweeney 2002; Machanavajjhala et al. 2007; Lord 1980; Rasch 1960; Wainer 2000; van der Linden 2010). No code is written here; this is decision input for the PLAN stage. > **Scope:** Three research question sets mapped to v0.3 decisions D-034 (cohort anonymization), D-035 (dynamic difficulty), D-036 (scenario library), D-047 (≥6 expert CS scenarios). This document grounds three v0.3 subsystems — cohort anonymization, IRT-based dynamic difficulty, and the scenario library — in published evidence and gives concrete recommendations for the pilot scale (likely <100 learners in v0.3). Each subsection ends with a confidence score (0–1) and a recommendation keyed to the relevant D-ID. --- ## Summary of Findings (Executive 1-Pager) 1. **k=10 + 7-day aggregation is the right floor for v0.3, and l-diversity is not yet warranted.** k-anonymity (Sweeney 2002) guarantees that any cohort view cell is indistinguishable across at least k learners. k=10 is the conventional minimum for anonymized analytics (HIPAA Safe Harbor uses k=5 for direct identifiers but k=10 is the common bar for aggregate cells). The known limits — homogeneity attacks (all k learners share the same sensitive value) and background-knowledge attacks — are real but require a sensitive-attribute dimension that v0.3's cohort view does not yet expose (the view shows practice volume, mastery progression, failure patterns — not diagnosis, income, or other high-stake attributes). **Recommendation:** ship k=10 + 7-day aggregation for v0.3; defer l-diversity/t-closeness to a later milestone if/when a sensitive attribute enters the cohort schema. (Confidence: 0.80) 2. **k-anonymity suppression is a SQL `HAVING COUNT(*) >= 10` pattern with a NULL/suppressed sentinel for small cells.** The robust pattern is a two-pass query: (a) compute the cell counts over the grouping dimensions, (b) suppress any cell with `< k` learners by replacing the measure with a sentinel (`NULL` or `'--'`) — never delete the row (deletion itself is a side channel). For multi-dimensional views (path × week × outcome), generalize (collapse) the sparsest dimension first rather than suppressing individual cells, so that suppression is monotone and doesn't create "negative space" that re-identifies. **Recommendation:** implement suppression in the aggregation pipeline (Postgres-side), not in the React client; expose a single `cell_suppressed` boolean column to the UI. (Confidence: 0.85) 3. **7-day aggregation is the standard privacy/analytics tradeoff and matches D-034.** Daily windows are re-identification-prone (a single learner practicing on a given day is often unique); monthly windows are too stale for an operator dashboard. 7 days is the conventional middle ground (matches HIPAA's "small cell" suppression granularity and common analytics practice). REQ-NFR-DASH-02 mandates ≤24h staleness for the *aggregate*, not the window — i.e., the 7-day window can roll daily with a ≤24h lag. **Recommendation:** roll the 7-day window daily (a trailing 7-day aggregate, recomputed nightly), keeping the window wide for k-anonymity and the freshness high for the operator. (Confidence: 0.80) 4. **Differential privacy is not worth adopting at v0.3 scale (<100 learners).** DP's noise scales as O(1/ε) independent of N, so at N<100 the noise needed for a meaningful ε swamps the signal in cohort cells. k-anonymity + aggregation is the right tool at pilot scale; DP becomes attractive at N>1000 where k-anonymity's suppression starts to delete too many cells. **Recommendation:** defer DP to a later milestone; document the migration path (k-anonymity → DP) in ARCHITECTURE.md. (Confidence: 0.75) 5. **1PL/Rasch is the correct IRT model for v0.3; θ is initialized to 0 (the population mean) and b is initialized by expert rating then refined by E-M / marginal MLE as data accrues.** P(success) = logistic(θ − b) = 1/(1+e^(b−θ)). The Bayesian update for θ after a session is a conjugate-style update on the posterior: posterior ∝ likelihood × prior, where the likelihood is Bernoulli with the observed session outcome (success/failure per the rubric gate) and the prior is N(θ₀, σ₀²). The closed-form Gaussian approximation (Bayesian update on the natural-parameter scale) is cheap (<1ms, satisfies REQ-NFR-IRT-01). **Recommendation:** initialize θ₀=0, σ₀²=1 (a weakly-informative prior that the learner is near the population mean); update θ and σ² after each session via the Gaussian-approximation update; persist both in the `learner_ability` SQLite table (D-046). (Confidence: 0.85) 6. **Target ~50% expected success for item selection — the "zone of proximal development" (60–70%) claim does not transfer cleanly from the classroom literature.** The classical CAT (Computerized Adaptive Testing) literature (Wainer 2000; van der Linden 2010) targets P=0.5 because that's where Fisher information for the 1PL is maximized (the test is most discriminating when the learner is right at the item's difficulty). The ZPD framing (Vygotsky; 60–70% success) is about *instructional* tasks, not *assessment* — and v0.3 scenarios are both. The compromise used in modern adaptive learning systems (e.g., Knewton, Duolingo's birdie model) is to target ~70% during practice and ~50% during assessment-only gates. **Recommendation:** target P=0.5 for mastery-gate scenarios (assessment role) and P≈0.7 for non-gate practice scenarios (learning role). Make the target a per-scenario field in the YAML so it's tunable without code changes. (Confidence: 0.75) 7. **θ is reasonably reliable after ~5–10 sessions; the cold-start prior (θ₀=0, σ₀²=1) carries the first 3–5 sessions.** The posterior variance σ² shrinks roughly as 1/n for 1PL Bayesian updates, so after 5 sessions σ² ≈ 0.2 (SD ≈ 0.45 logits, roughly half a rubric level), and after 10 sessions σ² ≈ 0.1 (SD ≈ 0.32 logits). v0.3's mastery gate requires N=3 *distinct* scenarios (D-032), so the gate itself provides a natural minimum of 3 data points before any gate decision — but θ should still be reported with its posterior SD until σ² < 0.2. **Recommendation:** report θ ± SD to the operator dashboard (k-anonymized); require σ² < 0.2 before θ drives item selection (fall back to expert-rated b otherwise). (Confidence: 0.80) 8. **1PL breaks down when scenario discrimination varies materially across scenarios — which v0.3's 6 expert scenarios will.** The 2PL model P=exp[a(θ−b)]/(1+exp[...]) adds a discrimination parameter `a` per item. The rule of thumb from the psychometric literature is that 2PL is justifiable at ~200–500 response records per item (Lord 1980; Embretson & Reise 2000), and 3PL (with a guessing parameter) needs ~1000+ per item. At v0.3's scale (<100 learners × ~6 scenarios = <600 records, ~100 per item), 1PL is the only defensible model; 2PL would be overfit. **Recommendation:** ship 1PL for v0.3; revisit 2PL only when per-scenario response counts exceed ~200 (likely post-pilot, v0.5+). (Confidence: 0.80) 9. **`scenarios/index.yaml` should be a manifest of metadata, not a duplicate of scenario content.** Each entry should carry: `id`, `path`, `difficulty` (the IRT `b` estimate, possibly expert-rated initially), `failure_mode`, `rubric_criteria` (list of rubric-criterion IDs exercised), `tags`, `version` (semver), `author` (expert name or `ai-variation`), `generated_from` (backref to parent scenario ID, absent for expert-authored), `irt_target_p` (the target success probability for selection, default 0.5 for gate scenarios). The index is the catalog the scenario selector reads; the per-scenario YAML files hold the full Pipecat-flows DSL. **Recommendation:** index.yaml = catalog (slim, fast to load); per-scenario YAML = full content (loaded on demand). Version with semver `MAJOR.MINOR.PATCH` — bump MAJOR on rubric-criteria or branch-structure changes (changes scoring compatibility), MINOR on content additions, PATCH on prompt tweaks. (Confidence: 0.85) 10. **AI-generated variations need a mandatory expert-review gate before entering the live library, a `generated_from` backref, and a frozen `intent_hash` to detect drift.** The review workflow: (a) LLM generates a variation from an expert scenario's schema with a `generated_from: ` field, (b) the variation is written to a `scenarios/_pending/` directory and is *invisible* to the selector, (c) an expert reviews the YAML in a PR-style diff against the parent, (d) on approval the variation moves to `scenarios//` and is added to `index.yaml`. The drift-prevention mechanism: an `intent_hash` (SHA-256 of the parent scenario's `success_criteria` + `failure_mode` + `rubric_criteria` fields) is recorded on the variation at generation time; if the parent's intent changes (hash differs), the variation is flagged as stale and re-review is required. **Recommendation:** ship the pending-review directory + `generated_from` + `intent_hash` fields in v0.3; do NOT auto-promote AI variations without expert sign-off (C-7: scenarios authored by domain experts; AI generates variations only). (Confidence: 0.80) 11. **Rubric-to-scenario mapping is a list of rubric-criterion IDs on each scenario; coverage is checked by inverting the map at load time.** The YAML field is `rubric_criteria: [criterion_id, ...]` on each scenario (per D-036/D-039). To ensure every criterion in a path's rubric is exercised by ≥ N scenarios, load `rubrics/customer_service.yaml`, build the criterion-ID set, then walk `scenarios/index.yaml` and count scenarios per criterion; assert the minimum. **Recommendation:** add a `scripts/check-coverage.py` (or bats check) that fails the build if any rubric criterion for a path has < 2 covering scenarios (N=2 for v0.3 — gives one expert + one variation or two expert scenarios per criterion). Run it in CI and as a pre-merge gate. (Confidence: 0.85) --- ## 1. Anonymization (k-anonymity, D-034) ### Q1 — k-anonymity, k=10, and limits (homogeneity, background-knowledge; l-diversity/t-closeness for v0.3) **What k-anonymity is.** k-anonymity (Sweeney, *International Journal of Uncertainty, Fuzziness and Knowledge-Based Systems* 2002) is a property of a released dataset (or aggregate view): for every combination of quasi-identifiers (the grouping dimensions — path, week, outcome, etc.), at least k records share that combination. Equivalently, no record is uniquely identifiable by the quasi-identifiers. The mechanism is generalization (collapsing values — e.g., age 23 → "20-30") and suppression (withholding cells with < k members). **Why k=10 is the conventional minimum.** HIPAA Safe Harbor (45 CFR §164.514(b)) uses k=5 for *direct* identifiers in a released dataset (the 18-element rule). For *aggregate analytics cells* — which is what v0.3's cohort dashboard emits — the common bar in the privacy/analytics literature and in de-identification guidance (e.g., the CDC's re-identification risk guidance, the EU Pseudonymisation Best Practices) is k=10. The reasoning is that aggregate cells are subject to differencing attacks (subtracting two released aggregates to isolate a small subgroup), and a higher k than the direct-identifier minimum reduces the marginal risk. D-034's choice of k=10 is therefore the conventional, defensible floor. **Limits of k-anonymity (the two classical attacks):** - **Homogeneity attack** (Machanavajjhala et al., *TODS* 2007, which introduced l-diversity): if all k learners in a cell share the same *sensitive* value, then knowing a target is in that cell reveals their sensitive value even though k-anonymity holds. Example: a cell of 10 learners who all failed the same week — knowing your competitor is in that cell tells you they failed. - **Background-knowledge attack**: an adversary with auxiliary information (e.g., "I know learner X practices on Tuesdays and is on week 3") can shrink the k-anonymity set to a smaller effective set and re-identify. k-anonymity is blind to this because it only counts released quasi-identifiers. **l-diversity and t-closeness.** l-diversity (Machanavajjhala 2007) requires at least l *distinct* sensitive values per cell. t-closeness (Li, Li & Venkatasubramanian, *ICDE* 2007) requires the distribution of the sensitive attribute within a cell to be within t of the global distribution. Both address homogeneity; t-closeness additionally addresses skew attacks (where l-diversity is satisfied but the distribution is still skewed toward one value). **Should v0.3 add l-diversity or t-closeness?** No — not for the pilot. The reason is structural: v0.3's cohort dashboard does not currently expose a *sensitive attribute* dimension in the sense the l-diversity/t-closeness literature assumes. The view dimensions are path/week/outcome/failure_pattern, and the measures are practice volume and mastery progression counts. None of these are sensitive in the way that diagnosis, income, or sexual orientation are. The homogeneity attack against "all 10 learners in this cell failed week 3" reveals a learning-struggle fact, which is lower-stakes than the medical/income facts these extensions were designed for. Adding l-diversity now would be engineering for a threat model the system doesn't yet have. The right trigger for revisiting l-diversity is *when a sensitive attribute enters the cohort schema* (e.g., if v0.4 adds demographic breakdowns). Document that trigger in ARCHITECTURE.md. **Recommendation (D-034):** ship k=10 + 7-day aggregation for v0.3. Defer l-diversity/t-closeness with an explicit re-evaluation trigger: "revisit when any cohort-view dimension or measure becomes a sensitive attribute (demographic, socio-economic, health-related)." Keep the aggregation pipeline structured so adding l-diversity later is a localized change (one suppression predicate). **Confidence: 0.80** — the k=10 convention is well-established; the l-diversity deferral is a threat-model judgment that depends on v0.3's exact cohort schema, which is not yet finalized. If the operator dashboard later adds a demographic filter, this deferral is wrong and l-diversity becomes required. ### Q2 — SQL suppression pattern; multi-dimensional views without re-identification **Single-dimension suppression.** The canonical pattern for "any cohort view cell with < 10 learners is suppressed": ```sql SELECT path, week, outcome, CASE WHEN COUNT(DISTINCT learner_id) >= 10 THEN COUNT(*) ELSE NULL END AS session_count, CASE WHEN COUNT(DISTINCT learner_id) >= 10 THEN TRUE ELSE FALSE END AS cell_suppressed FROM session_aggregates WHERE window_start >= now() - interval '7 days' GROUP BY path, week, outcome; ``` Two non-obvious but critical details: 1. **Suppress the measure, not the row.** Deleting the row creates a "negative space" side channel: an adversary who knows the dimension space can enumerate all combinations and infer that a missing cell had < 10 learners — which, combined with background knowledge, can re-identify. Replacing the measure with `NULL` (or a `'--'` sentinel) and emitting the cell with `cell_suppressed = TRUE` preserves the dimension grid and only hides the count. 2. **Use `COUNT(DISTINCT learner_id)`, not `COUNT(*)`.** A single learner can have many sessions in the window; `COUNT(*)` over-counts and produces false confidence that k=10 is met when only 3 learners are present. k-anonymity is about *people*, not *records*. **Multi-dimensional views (path × week × outcome × failure_pattern).** The naive approach — suppress each cell independently — leaks via *differencing*: an adversary subtracts two released aggregates (e.g., "week 3 outcomes" minus "week 3 outcomes where failure_pattern = escalates_unresolved") to recover the suppressed subcell. The standard defenses are: - **Generalization (collapse the sparsest dimension first):** if path × week × outcome × failure_pattern has cells with < 10 learners, drop the sparsest dimension (usually failure_pattern) and re-emit at path × week × outcome. If still under k, drop outcome, etc. The release is a *lattice* of generalizations, not a flat table. - **Minimality / consistency constraints** (the approach from the k-anonymity generalization literature, e.g., LeFevre, DeWitt & Ramakrishnan, *SIGMOD* 2005): the released cells must be *minimal* — you can't suppress a cell when its parent generalization already satisfies k — and *consistent* — no two released cells overlap such that differencing recovers a suppressed cell. For v0.3's pilot, the pragmatic approach is to (a) limit the cohort view to two dimensions at a time (e.g., path × week, OR path × outcome, but not path × week × outcome), which eliminates differencing across dimensions entirely; and (b) within each two-dimensional view, suppress cells with < 10 distinct learners using the pattern above. The operator UI presents a small fixed set of pre-defined 2-D views (no free-form cross-tabulation), which is sufficient for "practice volume, mastery progression, failure patterns" per REQ-DASH-01. **Recommendation:** implement suppression Postgres-side in the aggregation pipeline (D-045's hook + nightly job); expose a fixed set of pre-defined 2-D cohort views; emit `cell_suppressed` boolean to the React client; render suppressed cells as `--` in the UI. Do NOT allow free-form cross-tabulation by the operator in v0.3. **Confidence: 0.85** — the SQL pattern is canonical; the 2-D-view constraint is a pragmatic pilot choice that trades operator flexibility for re-identification safety. If operators need 3-D views, generalize (collapse) rather than allow free-form. ### Q3 — 7-day aggregation window: why 7 days, shorter-window risk, freshness tradeoff **Why 7 days.** Three reasons, in descending order of weight: 1. **Re-identification risk of shorter windows is high.** A daily window (or hourly) makes most cohort cells contain 1–3 learners (a single learner practicing on a given day is often unique in their path × week combination), so almost every cell would have to be suppressed, leaving the operator with a blank dashboard. Weekly windows aggregate enough practice that cells naturally exceed k=10 for active cohorts. 2. **Practice periodicity is weekly.** Learners in a mastery-paced 6-week path (D-037) practice on the order of once a day to a few times a week; a 7-day window captures one full practice cycle and aligns with the path's week structure (the dashboard's "week" dimension matches the aggregation window, which is intuitive for operators). 3. **Conventional granularity.** HIPAA Safe Harbor's "small cell" guidance, CDC re-identification guidance, and common analytics practice all treat 7-day (or coarser) aggregates as the privacy-friendly default for small populations. **Re-identification risk of shorter windows.** A 1-day window: a cohort of 50 learners across 6 path-weeks gives ~8 learners per cell on average — already under k=10, so most cells suppressed. An adversary who knows "learner X practiced on Tuesday" can pin them to a specific daily cell; if that cell has 1–3 learners, re-identification is feasible. A 1-hour window is worse still. The risk scales inversely with window length for small populations. **Freshness/staleness tradeoff.** The dashboard's freshness NFR (REQ-NFR-DASH-02: ≤ 24h staleness) is about *when the aggregate is computed*, not the window length. These are independent: a trailing 7-day window can be recomputed every hour (freshness 1h) or every day (freshness 24h). The window length is a *privacy* parameter; the recomputation cadence is a *freshness* parameter. The right design for v0.3 is a 7-day trailing window recomputed daily (or on each session-end per D-045's hook), giving 24h freshness on a 7-day-wide window. Shorter recomputation cadence (e.g., per-session) is fine — it doesn't change the window length. **Recommendation (D-034):** 7-day trailing window, recomputed on session-end hook (low-latency incremental update) + nightly reconciliation job (correctness). Document explicitly that "7-day aggregation window" ≠ "7-day staleness" — the window is 7 days wide, the staleness is ≤24h per REQ-NFR-DASH-02. **Confidence: 0.80** — the 7-day choice is conventional and well-justified for pilot scale; the freshness/window-length distinction is sometimes conflated in privacy guidance, which is why D-034's phrasing deserves the clarifying note above. ### Q4 — Differential privacy at v0.3 scale (<100 learners): adopt or defer? **What differential privacy (DP) gives you that k-anonymity doesn't.** DP (Dwork, *ICALP* 2006) is a formal guarantee: the output distribution is nearly the same whether or not any individual's data is in the input. This protects against *all* auxiliary information (the background-knowledge attack that k-anonymity is blind to) and gives a quantifiable privacy budget (ε, δ). Mechanisms like the Laplace or Gaussian mechanism add noise calibrated to the query's sensitivity and the chosen ε. **Why DP is the wrong tool at <100 learners.** The noise a DP mechanism adds is O(1/ε) *independent of N* — it does not shrink as the population grows. For a count query with sensitivity 1 and a privacy budget of ε=1 (a common, reasonably-private choice), the Laplace noise has scale 1 — meaning a true count of 8 might be released as 7, 8, 9, 10 with non-trivial probability. At N=50 learners in a cell, that's ±1–2 noise on a count of 50 — tolerable. At N=10 (the k-anonymity floor), ±1–2 noise on a count of 10 is ±10–20% relative error — the dashboard becomes meaningfully inaccurate. Worse, to maintain DP across many queries (the cohort dashboard emits many cells), the privacy budget must be *split* across them (composition), so each cell gets ε/M for M cells — and the noise scales as M/ε. A 6-path × 6-week × 4-outcome = 144-cell dashboard at total ε=1 gives ε_cell ≈ 0.007 — noise scale ~140, which makes the release pure noise. k-anonymity, by contrast, has *no noise* — it either releases the exact count (when ≥ k) or suppresses (when < k). At small N, the suppression rate is the cost; at large N, suppression disappears and k-anonymity releases exact counts (which DP never does). The crossover where DP starts to outperform k-anonymity on the utility/privacy frontier is roughly N > 1000 for multi-cell dashboards (the exact threshold depends on the query workload and ε). **Recommendation (D-034):** defer DP to a later milestone (target: when active learner count exceeds ~1000 or when a sensitive attribute enters the cohort schema, whichever comes first). Ship k-anonymity + aggregation for v0.3. Document the migration path in ARCHITECTURE.md: the aggregation pipeline's suppression step is a single function that can be swapped for a DP mechanism later — the rest of the pipeline (grouping, dimensions, UI rendering of `cell_suppressed`) is DP-agnostic. **Confidence: 0.75** — the DP-at-small-N argument is well-grounded in the DP literature (Dwork & Roth 2014); the 1000-learner crossover is a rule-of-thumb, not a hard threshold, and depends on the exact query workload. --- ## 2. IRT (Item Response Theory, D-035) ### Q5 — 1PL/Rasch model: P(success)=logistic(θ−b), initialization, Bayesian θ update **The model.** The 1PL (one-parameter logistic) / Rasch model gives the probability of success on scenario j by learner i as: P(X_ij = 1 | θ_i, b_j) = 1 / (1 + exp(b_j − θ_i)) = logistic(θ_i − b_j) where θ_i is learner i's ability (a scalar, in logits) and b_j is scenario j's difficulty (also in logits). The model is symmetric in θ and b: a learner of ability θ has P=0.5 on a scenario of difficulty b=θ; P>0.5 when θ>b; P<0.5 when θ = AI variation intent_hash: status: live # live | pending | deprecated - id: cs_refund_exchange_ai01 file: customer_service/cs_refund_exchange_ai01.yaml ... generated_from: cs_refund_ca_v01 status: pending # in _pending/, not selectable ``` **Why index.yaml is separate from per-scenario YAMLs.** Loading 6+ full scenario YAMLs (each with multi-paragraph system prompts, branch definitions, rubric mappings) just to pick the next one is wasteful. The index is a slim catalog (~50 lines per scenario) loaded once at startup; the full scenario YAML is loaded on demand when selected. This also keeps the selector's logic testable without the LLM-prompt content. **Versioning.** Use semver `MAJOR.MINOR.PATCH` per scenario, recorded in the scenario YAML and mirrored in `index.yaml`: - **MAJOR:** changes that break scoring compatibility — rubric_criteria added/removed, branch-structure changes, success_criteria semantics change. A MAJOR bump invalidates prior mastery-gate evidence (the learner's prior passes on the old version don't count toward the new version's gate). - **MINOR:** content additions — new common_mistakes, new branch (non-scoring), prompt enrichment. Backward-compatible with prior scoring. - **PATCH:** prompt tweaks, typo fixes, voice_id changes. No semantic change. The `version` field on each scenario lets the mastery-gate audit log (REQ-NFR-MAST-02) record which scenario version a learner passed, so future re-authoring doesn't retroactively invalidate credentials. **Recommendation (D-036):** - Nest scenarios under `scenarios//`. - `index.yaml` is a slim manifest (metadata only, ~50 lines/scenario). - Per-scenario YAML is the full Pipecat-flows DSL, loaded on demand. - Semver per scenario; MAJOR bumps invalidate prior gate evidence. - Add a `regenerate_index.py` (or bats check) that re-derives `index.yaml` from the scenario files and asserts they're in sync — prevents manual drift. **Confidence: 0.85** — the index/manifest split is a standard content-management pattern; the semver scheme is conventional. The only judgment call is treating rubric_criteria changes as MAJOR (scoring-compatibility-breaking), which is the conservative choice. ### Q10 — AI-generated variations: review workflow, generated_from backref, drift prevention **The workflow (per D-047, C-7).** C-7 (binding constraint) states "Scenarios authored by domain experts + learning designers; AI generates variations only." D-047 specifies "AI-generated variations gated by expert review." The concrete workflow: ``` 1. GENERATE - Input: an expert scenario YAML (e.g., cs_refund_ca_v01.yaml) - LLM (deepseek-v4-flash:cloud with think mode — offline, not latency-bound) generates a variation by perturbing the scenario while preserving success_criteria + failure_mode + rubric_criteria. - Output: a new YAML in scenarios//_pending/.yaml with: generated_from: cs_refund_ca_v01 intent_hash: status: pending author: ai_variation_ 2. REVIEW (expert, human-in-the-loop) - Expert opens a PR-style diff: pending YAML vs parent YAML. - Expert checks: does the variation still exercise the same rubric_criteria? Is the failure_mode still reachable? Is the system_prompt safe + in-character? - Expert may edit the variation (the LLM output is a draft, not final). - On approval: expert moves the file from _pending/ to scenarios// and adds it to index.yaml with status: live. 3. PUBLISH - The variation is now selectable by the IRT scenario selector. - It carries generated_from permanently (for provenance/audit). - Its intent_hash is frozen at generation time. 4. DRIFT DETECTION (ongoing) - If the parent scenario is re-authored (MAJOR version bump) and its success_criteria/failure_mode/rubric_criteria change, the parent's intent_hash changes. All variations generated_from that parent are flagged as stale (their intent_hash no longer matches the parent). - Stale variations are moved back to _pending/ and require re-review before they're selectable again. ``` **The `generated_from` backref.** A single field on the variation YAML pointing to the parent scenario ID. Absent (or null) on expert-authored scenarios. This is the provenance chain — it lets the audit log answer "was this mastery-gate evidence collected on an expert scenario or an AI variation, and if the latter, from which expert scenario was it derived?" The chain is one level deep (an AI variation is generated from an expert scenario, not from another AI variation) — this is a deliberate constraint to prevent variation-of-variation drift. Enforce it at generation time. **Drift prevention via `intent_hash`.** The intent of a scenario is defined as the tuple (success_criteria, failure_mode, rubric_criteria) — the parts that determine what the scenario *assesses*. The `intent_hash` is SHA-256 of the canonical JSON encoding of that tuple. At generation time, the variation records the parent's intent_hash. If the parent's intent later changes (re-authoring changes the rubric_criteria, say), the parent's hash changes and the variation is flagged stale. This catches the case where an expert reauthors the parent in a way that the variation no longer faithfully represents — without requiring the expert to manually track all variations. **Preventing drift from the expert's intent (the deeper question).** The intent_hash catches *parent-side* drift. *Variation-side* drift — the LLM produces a variation that superficially matches the schema but subtly changes the assessed skill (e.g., makes the customer less angry, turning an empathy test into a transaction test) — is caught only by expert review. The intent_hash does NOT verify semantic fidelity. Two mitigations: 1. **The rubric-to-scenario mapping is part of the intent tuple.** If the LLM drops a rubric criterion, the variation's intent_hash differs from the parent's, and the variation is auto-flagged stale (without needing expert review). This catches structural drift. 2. **Expert review is the only defense against semantic drift within the same rubric_criteria.** No automated check can verify "is this customer still angry enough to test empathy." This is why C-7 makes expert review mandatory, not optional. **Recommendation (D-047, REQ-SCEN-04):** - Ship the `_pending/` directory + `generated_from` backref + `intent_hash` fields in v0.3. - AI variations are generated offline by `scripts/generate_variation.py` (a CLI tool, not in the voice path); output goes to `_pending/`. - Expert review is mandatory; no auto-promotion. The review is a git PR against the `scenarios/` directory — the expert reviews the YAML diff. - One-level variation chain only (no variations of variations). - `intent_hash` catches structural drift (rubric_criteria change); expert review catches semantic drift. - The ≥6 expert scenarios in D-047 are the floor; AI variations are supplemental and cannot substitute for the expert floor. **Confidence: 0.80** — the workflow is sound and matches industry practice for AI-assisted content authoring (e.g., how Khanmigo, Duolingo's GPT-4 content pipeline handle AI-generated exercises). The `intent_hash` mechanism is a Praxis-specific design; it's a reasonable heuristic for structural drift but is not a published technique, hence the 0.80 not 0.95. ### Q11 — Rubric-to-scenario mapping: YAML field shape, coverage across a path **The YAML field shape.** Each scenario declares which rubric criteria it exercises via a `rubric_criteria` field — a list of criterion IDs that reference the rubric file (`rubrics/customer_service.yaml` per D-039): ```yaml # scenarios/customer_service/cs_refund_ca_v01.yaml id: cs_refund_ca_v01 path: customer_service # ... existing v0.1 fields ... rubric_criteria: - criterion_id: empathy weight: 1.0 # relative weight within this scenario (default 1.0) evidence_required: true # must be observed to count toward mastery - criterion_id: concrete_resolution weight: 1.0 evidence_required: true - criterion_id: next_steps weight: 0.5 evidence_required: false ``` Two design choices in this shape: 1. **List of objects, not a list of strings.** Each entry carries a `criterion_id` (referencing the rubric) plus per-scenario metadata about that criterion (weight within this scenario, whether evidence is required). A bare list of strings (`rubric_criteria: [empathy, concrete_resolution, next_steps]`) is simpler but loses the per-scenario weighting — and weighting matters because a scenario may exercise one criterion as the primary skill and another as secondary. 2. **Reference by ID, not inline.** The criterion's full definition (5-level anchors, weight-within-skill) lives in `rubrics/customer_service.yaml` (per D-039). The scenario references it by ID. This keeps the rubric single-source (a criterion's anchors are defined once) and lets the coverage checker work on IDs without parsing every scenario's full content. **Coverage across a path.** D-032's mastery gate requires N=3 distinct-scenario successes. For the gate to be meaningful, the N scenarios must collectively exercise *all* the rubric's criteria — otherwise a learner could pass the gate by succeeding on scenarios that only test a subset of the skill. The coverage requirement is: *every rubric criterion for the path is exercised by ≥ M scenarios, where M ≥ 2* (so there's at least one expert scenario and one alternative — an AI variation or a second expert scenario — to prevent single-scenario gaming). **Coverage check (load-time).** Build the rubric-criterion-ID set from `rubrics/customer_service.yaml`, walk `scenarios/index.yaml`, and count scenarios per criterion (only `status: live` scenarios count): ```python # pseudocode for scripts/check_coverage.py rubric = yaml.safe_load(open("rubrics/customer_service.yaml")) required_criteria = {c["id"] for c in rubric["criteria"]} index = yaml.safe_load(open("scenarios/index.yaml")) scenarios = [s for s in index["path_scenarios"]["customer_service"] if s["status"] == "live"] coverage = {cid: sum(1 for s in scenarios if cid in s["rubric_criteria"]) for cid in required_criteria} under_covered = {cid: n for cid, n in coverage.items() if n < MIN_COVERAGE} if under_covered: fail(f"Coverage gap: {under_covered} — each criterion needs ≥ {MIN_COVERAGE} scenarios") ``` With `MIN_COVERAGE = 2` for v0.3. This runs at CI time and as a pre-merge gate on `scenarios/` changes. **Interaction with D-047's ≥6 scenarios.** Six expert scenarios × 3 rubric criteria per scenario = 18 criterion-exercise slots. If the rubric has 5 criteria, each needs ≥ 2 scenarios = 10 slots minimum — well within the 18 available, so 6 scenarios is comfortably enough for coverage *if* the scenarios are authored to distribute across criteria (not all 6 testing only empathy + concrete_resolution). The coverage check catches the case where authoring concentrates on a subset of criteria. **Recommendation (D-036, D-039, D-047):** - `rubric_criteria` on each scenario is a list of objects: `{criterion_id, weight, evidence_required}`. - Criterion definitions live in `rubrics/.yaml` (per D-039); scenarios reference by ID. - Coverage check: every criterion in the path's rubric is exercised by ≥ 2 live scenarios (`MIN_COVERAGE = 2` for v0.3). - `scripts/check_coverage.py` runs in CI; fails the build on coverage gaps. - Authoring guidance for the ≥6 expert scenarios: distribute across criteria so no criterion is exercised by only one scenario. **Confidence: 0.85** — the ID-reference pattern is standard content-relationship modeling; the coverage check is a straightforward graph invariant. The `MIN_COVERAGE = 2` choice is a v0.3 pragmatic floor (it could be raised to 3 in later milestones for more robust anti-gaming, at the cost of more authoring). --- ## Cross-Cutting Recommendations for the PLAN Stage 1. **Anonymization pipeline is a single suppression function, swappable for DP later.** Design `aggregate_cohort(dimensions, window)` to return rows with a `cell_suppressed` column. The k-anonymity suppression is one predicate (`COUNT(DISTINCT learner_id) >= 10`); a future DP mechanism replaces the predicate with a noise-addition step. The UI and the rest of the pipeline are unchanged. 2. **IRT θ update and mastery gate are independent.** Don't couple them. The mastery gate (D-032) is rule-based on rubric scores + N=3 distinct scenarios. θ (D-035) is for *scenario selection*, not for *mastery certification*. A learner can open a mastery gate before θ is "reliable" by the σ² criterion, and that's correct — the gate is the authoritative mastery signal. 3. **Scenario library is the linchpin.** Three v0.3 subsystems read from it: the IRT selector (reads `difficulty`, `irt_target_p`), the coverage checker (reads `rubric_criteria`), and the mastery gate (reads `status`, `version`, `generated_from`). Design `index.yaml` first; the rest follows. 4. **Expert authoring is the bottleneck.** D-047's ≥6 expert CS scenarios is a content-authoring task, not an engineering task. The PLAN stage should identify the persona (learning designer + domain expert) and the schedule for authoring the 6 scenarios, and treat it as a critical-path dependency for the IRT and mastery-gate slices. 5. **Three CI gates for the scenario library:** - `scripts/check_coverage.py` — every rubric criterion exercised by ≥ 2 live scenarios. - `scripts/check_index_sync.py` — `index.yaml` is in sync with the per-scenario YAMLs (no missing entries, no stale entries). - `scripts/check_intent_hash.py` — no live scenario has a stale `intent_hash` (catches parent-reauthoring drift). --- ## Open Questions for the PLAN Stage 1. **Cohort view dimensions — exact set.** Q2 recommends 2-D views only. Which 2-D views does the operator dashboard expose? Candidate set: path × week (progression), path × outcome (mastery), path × failure_pattern (diagnostics). Confirm with the operator persona (training manager) before PLAN. 2. **IRT `b` recalibration cadence.** Q5 recommends nightly E-M recalibration of `b` once per-scenario response counts exceed ~20. At v0.3's scale (~100 responses per scenario), nightly is overkill — weekly is fine. But the trigger ("recalibrate when count > 20") needs to be in the nightly job, not hardcoded. 3. **AI variation generation tooling.** Q10 specifies `scripts/generate_variation.py` as an offline CLI. Does it run locally (expert's laptop) or in the praxis container? Locally is simpler (no LLM-in-production-container concern); the output is a YAML file checked into git. Recommend local. 4. **Mastery-gate evidence and scenario versioning.** Q9 specifies MAJOR bumps invalidate prior gate evidence. Concretely: if `cs_refund_ca_v01` is bumped to `cs_refund_ca_v02` with a rubric_criteria change, do learners who passed v01 need to re-pass v02? The conservative answer is yes (re-pass required), but this is a UX/policy decision that the PLAN stage should surface to the product owner. 5. **Operator dashboard: θ reporting threshold.** Q7 recommends reporting θ only when σ² < 0.2. Should the dashboard show "warming up — N sessions until reliable" for learners below the threshold, or suppress entirely? Showing a count is more useful but leaks information about how few sessions the learner has (a re-identification vector if combined with other cells). Recommend: aggregate the "warming up" count across the cohort (k-anonymized), don't show per-learner.