@brunosps00/dev-workflow 0.13.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +9 -3
  2. package/package.json +1 -1
  3. package/scaffold/en/commands/dw-bugfix.md +2 -1
  4. package/scaffold/en/commands/dw-code-review.md +1 -0
  5. package/scaffold/en/commands/dw-create-tasks.md +6 -0
  6. package/scaffold/en/commands/dw-deps-audit.md +1 -1
  7. package/scaffold/en/commands/dw-fix-qa.md +1 -1
  8. package/scaffold/en/commands/dw-functional-doc.md +1 -1
  9. package/scaffold/en/commands/dw-help.md +1 -1
  10. package/scaffold/en/commands/dw-redesign-ui.md +1 -1
  11. package/scaffold/en/commands/dw-run-qa.md +2 -1
  12. package/scaffold/en/commands/dw-run-task.md +1 -1
  13. package/scaffold/pt-br/commands/dw-bugfix.md +2 -1
  14. package/scaffold/pt-br/commands/dw-code-review.md +1 -0
  15. package/scaffold/pt-br/commands/dw-create-tasks.md +6 -0
  16. package/scaffold/pt-br/commands/dw-deps-audit.md +1 -1
  17. package/scaffold/pt-br/commands/dw-fix-qa.md +1 -1
  18. package/scaffold/pt-br/commands/dw-functional-doc.md +1 -1
  19. package/scaffold/pt-br/commands/dw-help.md +1 -1
  20. package/scaffold/pt-br/commands/dw-redesign-ui.md +1 -1
  21. package/scaffold/pt-br/commands/dw-run-qa.md +2 -1
  22. package/scaffold/pt-br/commands/dw-run-task.md +1 -1
  23. package/scaffold/skills/dw-incident-response/SKILL.md +164 -0
  24. package/scaffold/skills/dw-incident-response/references/blameless-discipline.md +126 -0
  25. package/scaffold/skills/dw-incident-response/references/communication-templates.md +107 -0
  26. package/scaffold/skills/dw-incident-response/references/postmortem-template.md +133 -0
  27. package/scaffold/skills/dw-incident-response/references/runbook-templates.md +169 -0
  28. package/scaffold/skills/dw-incident-response/references/severity-and-triage.md +186 -0
  29. package/scaffold/skills/dw-llm-eval/SKILL.md +148 -0
  30. package/scaffold/skills/dw-llm-eval/references/agent-eval.md +252 -0
  31. package/scaffold/skills/dw-llm-eval/references/judge-calibration.md +169 -0
  32. package/scaffold/skills/dw-llm-eval/references/oracle-ladder.md +171 -0
  33. package/scaffold/skills/dw-llm-eval/references/rag-metrics.md +186 -0
  34. package/scaffold/skills/dw-llm-eval/references/reference-dataset.md +190 -0
  35. package/scaffold/skills/dw-testing-discipline/SKILL.md +99 -76
  36. package/scaffold/skills/dw-testing-discipline/references/agent-guardrails.md +170 -0
  37. package/scaffold/skills/dw-testing-discipline/references/anti-patterns.md +6 -6
  38. package/scaffold/skills/dw-testing-discipline/references/core-rules.md +128 -0
  39. package/scaffold/skills/dw-testing-discipline/references/playwright-recipes.md +2 -2
  40. package/scaffold/skills/dw-ui-discipline/SKILL.md +101 -79
  41. package/scaffold/skills/dw-ui-discipline/references/hard-gate.md +93 -73
  42. package/scaffold/skills/dw-ui-discipline/references/visual-slop.md +152 -0
  43. package/scaffold/skills/dw-testing-discipline/references/ai-agent-gates.md +0 -170
  44. package/scaffold/skills/dw-testing-discipline/references/iron-laws.md +0 -128
  45. package/scaffold/skills/dw-ui-discipline/references/anti-slop.md +0 -162
  46. /package/scaffold/skills/dw-testing-discipline/references/{positive-patterns.md → patterns.md} +0 -0
@@ -0,0 +1,171 @@
1
+ # Oracle ladder — climb deliberately
2
+
3
+ Five rungs ordered by cost (cheap → expensive) and rigor (strict → subjective). Start at the bottom. Every rung up costs an order of magnitude more in latency, money, or calibration effort. Don't reach for an upper rung when a lower one can prove the case.
4
+
5
+ ## Rung 1 — Exact match
6
+
7
+ **What it checks:** the output equals the expected output, byte-for-byte (or after a normalization step like JSON canonicalization).
8
+
9
+ **Use when:**
10
+ - Output is a structured function call: `expect(toolCalls[0]).toEqual({ name: 'search', args: { q: 'invoices' } })`.
11
+ - Output is a classification from a fixed label set: `expect(label).toBe('refund-request')`.
12
+ - Output is a parsed value from a JSON contract: `expect(result.user_id).toBe('u-42')`.
13
+
14
+ **Example:**
15
+
16
+ ```javascript
17
+ test('classifier labels refund requests correctly', async () => {
18
+ const cases = await loadDataset('.dw/eval/datasets/classifier/cases.jsonl');
19
+ for (const c of cases.filter(c => c.expected === 'refund-request')) {
20
+ expect(await classify(c.input)).toBe('refund-request');
21
+ }
22
+ });
23
+ ```
24
+
25
+ **Cost:** ~free.
26
+ **Limitation:** can't handle creative outputs (paragraphs, summaries). Don't try to force-fit.
27
+
28
+ ## Rung 2 — Schema validation
29
+
30
+ **What it checks:** the output matches a structural contract — types, required fields, value ranges. The SHAPE is fixed; specific values can vary.
31
+
32
+ **Use when:**
33
+ - LLM returns structured data with stable schema (JSON, function call args) but variable content.
34
+ - You need to detect "agent returned garbage" without asserting on the exact garbage.
35
+
36
+ **Example:**
37
+
38
+ ```typescript
39
+ import { z } from 'zod';
40
+
41
+ const ResponseSchema = z.object({
42
+ summary: z.string().min(20).max(500),
43
+ citations: z.array(z.object({
44
+ url: z.string().url(),
45
+ page: z.number().int().optional(),
46
+ })).min(1),
47
+ confidence: z.number().min(0).max(1),
48
+ });
49
+
50
+ test('summarizer returns valid shape', async () => {
51
+ const result = await summarize(input);
52
+ expect(() => ResponseSchema.parse(result)).not.toThrow();
53
+ });
54
+ ```
55
+
56
+ **Cost:** ~free (schema check is cheap).
57
+ **Limitation:** doesn't tell you if the CONTENT is correct, only that it's the right shape. Pair with another rung.
58
+
59
+ ## Rung 3 — Outcome state
60
+
61
+ **What it checks:** a side effect occurred — DB row was created, file was written, tool was called with valid arguments, ticket was opened. The state of the world matches expectations.
62
+
63
+ **Use when:**
64
+ - Agent has tool access and the GOAL is to change state, not produce prose.
65
+ - RAG answer is supposed to lead to an action (e.g., "user clicked the suggested invoice and reconciled it").
66
+ - The system has observable side effects you can query post-hoc.
67
+
68
+ **Example:**
69
+
70
+ ```javascript
71
+ test('agent files refund request when user asks', async () => {
72
+ await agent.run('I want a refund for order #123');
73
+
74
+ const tickets = await db.tickets.findMany({ where: { order_id: '123' } });
75
+ expect(tickets).toHaveLength(1);
76
+ expect(tickets[0].type).toBe('refund');
77
+ expect(tickets[0].status).toBe('pending');
78
+ });
79
+ ```
80
+
81
+ **Cost:** cheap (1 DB query / API call per assertion).
82
+ **Limitation:** doesn't validate the PROSE the agent produced along the way. If the goal was "answer the user politely AND file the refund," rung 3 catches the action but not the politeness — climb to rung 4 for that.
83
+
84
+ **Key benefit:** catches "ghost actions" — agent claims to have done X but didn't actually do it. Rungs 1-2 trust the agent's word; rung 3 verifies the world.
85
+
86
+ ## Rung 4 — LLM-as-judge
87
+
88
+ **What it checks:** a different model grades the output against a rubric. Used for genuinely subjective quality — helpfulness, tone, faithfulness, completeness.
89
+
90
+ **Mandatory before using:**
91
+ - Calibrated against ≥20 human-graded cases (Spearman ≥0.80) — see `judge-calibration.md`.
92
+ - Different model than the system under test.
93
+ - Structured rubric, not free-form "rate 1-10."
94
+
95
+ **Example:**
96
+
97
+ ```javascript
98
+ test('chat response is faithful to retrieved context', async () => {
99
+ const cases = await loadDataset('.dw/eval/datasets/rag-chat/cases.jsonl');
100
+ const scores = [];
101
+
102
+ for (const c of cases) {
103
+ const answer = await chat(c.input, c.context);
104
+ const judgment = await llmJudge({
105
+ model: 'claude-opus-4-7', // different from system under test (GPT-4)
106
+ rubric: faithfulnessRubric,
107
+ input: c.input,
108
+ context: c.context,
109
+ output: answer,
110
+ });
111
+ scores.push(judgment.score);
112
+ }
113
+
114
+ // 80% of cases must score ≥4 on the 1-5 faithfulness rubric
115
+ const passing = scores.filter(s => s >= 4).length / scores.length;
116
+ expect(passing).toBeGreaterThan(0.8);
117
+ });
118
+ ```
119
+
120
+ **Cost:** medium-to-high (one judge call per case; pay per case at API rates).
121
+ **Limitation:** the judge has bias and drift; without calibration, you're measuring the judge's mood. Re-calibrate every quarter, every model swap, and after rubric changes.
122
+
123
+ ## Rung 5 — Human review
124
+
125
+ **What it checks:** a domain expert scores. The gold standard for the rubrics rung 4 calibrates against.
126
+
127
+ **Use when:**
128
+ - Calibrating LLM-as-judge (rung 4 setup).
129
+ - High-stakes outputs where automation isn't trusted (medical, legal, financial).
130
+ - Edge cases that automated rungs flag as borderline.
131
+
132
+ **Cost:** expensive. Don't scale; sample.
133
+
134
+ **Pattern:**
135
+ - Spot-check 5-10% of LLM-as-judge results randomly each week.
136
+ - Whenever LLM-as-judge score is "borderline" (e.g., 2.5-3.5 on 1-5 scale), kick to human.
137
+ - Full human review only for the calibration dataset and high-stakes edge cases.
138
+
139
+ ## The climbing decision tree
140
+
141
+ ```
142
+ Is the output a fixed-structure value (function call, classification, JSON with stable shape)?
143
+ ├── YES → Rung 1 (exact match) or Rung 2 (schema)
144
+ └── NO → does the output cause an observable side effect (DB write, tool call, ticket opened)?
145
+ ├── YES → Rung 3 (outcome state)
146
+ └── NO → output is subjective (prose, summary, recommendation). Rung 4 required.
147
+ └── Did you calibrate the judge against humans (≥20 cases, Spearman ≥0.80)?
148
+ ├── YES → Rung 4 is valid signal
149
+ └── NO → DO NOT USE Rung 4 yet. Calibrate first via Rung 5.
150
+ ```
151
+
152
+ ## Anti-patterns
153
+
154
+ - **Reaching for Rung 4 first** because "everything else seems hard." Climb the ladder; lower rungs catch loud failures cheaply.
155
+ - **Pretending Rung 4 is calibrated** by running it without checking against humans. Score numbers without calibration are decorative.
156
+ - **Skipping Rung 3 because "we have unit tests"** — unit tests with mocked tools prove the agent CALLED the tool. Rung 3 proves the tool's effect happened.
157
+ - **Mixing rungs in one assertion**: `expect(answer).toBe('Yes, your refund is being processed' /* exact */)` — when the exact text doesn't matter, rung 1 is the wrong tool.
158
+
159
+ ## Combining rungs
160
+
161
+ For a serious AI feature, expect to use 2-3 rungs together:
162
+
163
+ | Feature | Typical rung mix |
164
+ |---------|------------------|
165
+ | Classifier | Rung 1 (label correctness) + Rung 4 (rationale quality, if exposed to user) |
166
+ | RAG chat | Rung 2 (response shape) + Rung 3 (citations are valid URLs/IDs) + Rung 4 (faithfulness) |
167
+ | Agent (filing tickets) | Rung 3 (ticket created with correct fields) + Rung 4 (user-facing message tone) |
168
+ | Summarization | Rung 2 (length, structure) + Rung 4 (faithfulness, completeness) |
169
+ | Tool-use trajectory | Rung 1 (specific tool calls expected) + Rung 4 (intermediate reasoning quality, optional) |
170
+
171
+ The rule: cheap rungs catch the failures that scream; expensive rungs catch the failures that whisper. You need both.
@@ -0,0 +1,186 @@
1
+ # RAG evaluation — three orthogonal metrics
2
+
3
+ Retrieval-augmented generation (RAG) has three failure modes, each requiring its own metric. Measure all three. Measuring only one creates blindspots.
4
+
5
+ ## The three metrics
6
+
7
+ ### 1. Retrieval precision@k
8
+
9
+ **What it measures:** of the top-K chunks retrieved, how many were RELEVANT to the user's query?
10
+
11
+ **How to compute:**
12
+
13
+ ```python
14
+ def precision_at_k(retrieved_chunk_ids, relevant_chunk_ids, k=5):
15
+ top_k = retrieved_chunk_ids[:k]
16
+ relevant_in_top_k = sum(1 for cid in top_k if cid in relevant_chunk_ids)
17
+ return relevant_in_top_k / k
18
+ ```
19
+
20
+ **Reference data needed:** for each test case, the human-labeled set of "chunks that should have been retrieved" — the ground truth.
21
+
22
+ **Target:** depends on K. For k=5, target precision >0.6 (3 of 5 chunks relevant). For k=10, target >0.5.
23
+
24
+ **What it catches:** retrieval is bringing back junk. Chunk embeddings are wrong, the index is stale, the query rewriting is broken.
25
+
26
+ **What it misses:** the LLM may still produce a great answer even from imperfect retrieval — or a hallucinated answer despite perfect retrieval. Pair with metrics #2 and #3.
27
+
28
+ ### 2. Answer faithfulness
29
+
30
+ **What it measures:** does the answer make claims that are SUPPORTED by the retrieved context? Or does it fabricate?
31
+
32
+ **How to compute (rung-4 LLM-as-judge with rubric):**
33
+
34
+ The judge sees: user question + retrieved context + generated answer. Scores 1-5 per the faithfulness rubric (see `judge-calibration.md` for an example).
35
+
36
+ **Reference data needed:** the retrieved context (saved from the run) and the answer. No ground-truth answer required — the judge checks claim-by-claim against the context.
37
+
38
+ **Target:** 80% of cases score ≥4 on the 1-5 scale.
39
+
40
+ **What it catches:** hallucination — the answer says things the context didn't support. This is the #1 failure mode in production RAG.
41
+
42
+ **What it misses:** the answer might be faithful to the retrieved context but the retrieved context might be WRONG. Pair with metric #1.
43
+
44
+ ### 3. Context utilization
45
+
46
+ **What it measures:** did the answer USE the retrieved context, or ignore it and produce a generic / parametric-memory response?
47
+
48
+ **How to compute (heuristic + LLM-as-judge hybrid):**
49
+
50
+ Heuristic part — n-gram overlap or semantic similarity:
51
+ ```python
52
+ def context_overlap(answer, context, n=3):
53
+ answer_ngrams = set(ngrams(answer, n))
54
+ context_ngrams = set(ngrams(context, n))
55
+ if not answer_ngrams:
56
+ return 0
57
+ return len(answer_ngrams & context_ngrams) / len(answer_ngrams)
58
+ ```
59
+
60
+ Judge part — ask if the answer would change materially without the context:
61
+ > "If the retrieved context were removed, would the answer be substantially different? 1 = same as without context (didn't use it), 5 = fully context-grounded."
62
+
63
+ **Target:** 70%+ overlap on substantive answers; judge score ≥4 on 80% of cases.
64
+
65
+ **What it catches:** the answer is faithful to the context (metric #2 passes) but ignores it — the model used its parametric memory instead. This means retrieval is doing nothing.
66
+
67
+ **What it misses:** the answer might use the context but cite it incorrectly. Pair with metric #2.
68
+
69
+ ## Why all three are needed
70
+
71
+ | Metric | Detects | Misses |
72
+ |--------|---------|--------|
73
+ | Retrieval precision@k | Junk in retrieval | Faithfulness; utilization |
74
+ | Answer faithfulness | Hallucination | Retrieval quality; whether context was used |
75
+ | Context utilization | Ignoring retrieval | Hallucination beyond context; retrieval quality |
76
+
77
+ A RAG system can fail in all three independent ways. Measuring only one creates blind spots in the other two.
78
+
79
+ ## Combined metric example
80
+
81
+ ```python
82
+ def evaluate_rag(case):
83
+ retrieved = retrieve(case.query)
84
+ answer = generate(case.query, retrieved)
85
+
86
+ return {
87
+ 'precision_at_5': precision_at_k(
88
+ [c.id for c in retrieved],
89
+ case.relevant_chunk_ids,
90
+ k=5
91
+ ),
92
+ 'faithfulness': llm_judge_faithfulness(
93
+ query=case.query,
94
+ context=retrieved,
95
+ answer=answer
96
+ ),
97
+ 'context_utilization_overlap': context_overlap(answer, retrieved),
98
+ 'context_utilization_judge': llm_judge_utilization(
99
+ query=case.query,
100
+ context=retrieved,
101
+ answer=answer
102
+ ),
103
+ }
104
+ ```
105
+
106
+ Aggregate per-case scores into the per-run summary:
107
+
108
+ ```
109
+ Run 2026-05-12:
110
+ precision@5: 0.68 (target >0.6) ✓
111
+ faithfulness ≥4: 83% (target >80%) ✓
112
+ context utilization: 72% (target >70%) ✓
113
+ Overall: PASS
114
+ ```
115
+
116
+ ## Common RAG failure modes
117
+
118
+ | Symptom | Likely metric that catches it |
119
+ |---------|------------------------------|
120
+ | User says "the bot is making stuff up" | Faithfulness |
121
+ | User says "the bot didn't see my documents" | Context utilization (or retrieval precision) |
122
+ | User says "the bot is bad at finding things" | Retrieval precision@k |
123
+ | User says "the answer is correct but ignores recent updates" | Retrieval recall (precision's partner — different metric) |
124
+ | User says "the bot gives the same generic answer no matter what I ask" | Context utilization |
125
+ | User says "the bot says the doc says X but it doesn't" | Faithfulness |
126
+
127
+ The metric points at the layer to fix. Without it, debugging is guesswork.
128
+
129
+ ## Retrieval recall (the fourth metric, conditional)
130
+
131
+ Precision asks "of what we retrieved, how much was good?" Recall asks "of what was good, how much did we retrieve?"
132
+
133
+ In production RAG with many candidate chunks, recall is often the limiting factor — the right chunk exists in the index but doesn't surface.
134
+
135
+ Compute:
136
+ ```python
137
+ def recall_at_k(retrieved_chunk_ids, relevant_chunk_ids, k=5):
138
+ top_k = set(retrieved_chunk_ids[:k])
139
+ return len(top_k & set(relevant_chunk_ids)) / len(relevant_chunk_ids)
140
+ ```
141
+
142
+ Track recall when:
143
+ - The corpus is large (>1000 chunks per query domain).
144
+ - Users report "the bot can't find things that exist in our docs."
145
+ - You're tuning the retrieval pipeline (chunking strategy, embedding model, search algorithm).
146
+
147
+ Skip recall when:
148
+ - The corpus is small (top-K = ~10% of the corpus; recall is high by default).
149
+ - Precision is the dominant problem.
150
+
151
+ ## Dataset structure for RAG
152
+
153
+ ```json
154
+ {
155
+ "id": "rag-case-001",
156
+ "query": "What's our PTO policy for sabbatical years?",
157
+ "expected": {
158
+ "relevant_chunk_ids": ["chunk-policy-pto-2024", "chunk-policy-sabbatical"],
159
+ "expected_answer_themes": ["accrual rate", "carryover limits", "sabbatical exception"],
160
+ "should_cite": true
161
+ },
162
+ "metadata": {
163
+ "source": "production-2026-04-12-support-thread-S-892",
164
+ "difficulty": "medium",
165
+ "tags": ["pto-policy", "sabbatical", "rare-query"]
166
+ }
167
+ }
168
+ ```
169
+
170
+ The `relevant_chunk_ids` field requires human labeling — domain expert reviews the corpus, identifies which chunks SHOULD surface for that query.
171
+
172
+ ## Anti-patterns
173
+
174
+ - **Measuring only one metric** (usually faithfulness via LLM-as-judge) → blind to retrieval and utilization failures.
175
+ - **No human-labeled relevance** → can't compute precision/recall.
176
+ - **Treating retrieval and generation as one black box** → can't tell which layer regressed.
177
+ - **Eval set drawn only from "easy" queries** → metrics are good in test, terrible in production.
178
+ - **Ignoring recent-information bias** (RAG must use retrieval; parametric memory is stale) → context utilization metric catches this.
179
+
180
+ ## Tooling
181
+
182
+ - **ragas** (open source) implements precision, recall, faithfulness, and other RAG metrics with LLM judges. Use as reference implementation.
183
+ - **Custom implementation** is straightforward — the metrics above are <100 lines of Python each.
184
+ - **LangSmith / Weights & Biases** wrap eval runs with tracking but don't replace the core metrics.
185
+
186
+ The discipline isn't tool choice; it's measuring all three orthogonal dimensions every run.
@@ -0,0 +1,190 @@
1
+ # Reference dataset — 20 from failures beats 200 perfect
2
+
3
+ The dataset is the bedrock. Without one, every "improvement" is anecdote and every regression goes unnoticed until users complain. With one, you can measure change.
4
+
5
+ ## The 20-from-failures principle
6
+
7
+ > 20 unambiguous cases drawn from real production failures beat 200 synthetic perfect cases.
8
+
9
+ Why:
10
+ - Synthetic cases reflect what the team IMAGINED would happen — they cover the cases the team already knows about.
11
+ - Production failures reflect what ACTUALLY happens — they cover blind spots and edge cases.
12
+ - 20 well-curated cases at the right level of difficulty discriminate models better than 200 average-difficulty cases.
13
+
14
+ A 20-case dataset is enough to:
15
+ - Detect regressions of >10% accuracy.
16
+ - Calibrate LLM-as-judge.
17
+ - Run cheaply enough to evaluate on every PR.
18
+
19
+ Scale up only when you've validated that 20 is producing useful signal.
20
+
21
+ ## Where cases come from
22
+
23
+ In rough order of priority:
24
+
25
+ 1. **Real production failures.** User reported a bug, support escalated a case, error logs show an unexpected output. Each becomes a case. Sanitize PII before saving.
26
+ 2. **Edge cases discovered during development.** "Oh, what if the user asks X?" — add it.
27
+ 3. **Adversarial examples.** Inputs designed to trip the system (prompt injection, ambiguity, contradiction). Especially important for chat/RAG.
28
+ 4. **Boundary inputs.** Empty, very long, special characters, mixed languages, unusual encodings.
29
+ 5. **Synthetic — last resort.** Only when no real input exists yet (e.g., pre-launch). Mark them as synthetic; replace as production data arrives.
30
+
31
+ Target distribution: **80% real production-sourced**, 20% adversarial/boundary. Pure-synthetic datasets give pure-synthetic confidence.
32
+
33
+ ## Case structure
34
+
35
+ Each case is one line in `cases.jsonl`:
36
+
37
+ ```json
38
+ {
39
+ "id": "case-001",
40
+ "input": {
41
+ "user_message": "I want to cancel my subscription",
42
+ "user_context": { "tier": "premium", "tenure_months": 18 }
43
+ },
44
+ "expected": {
45
+ "intent": "cancellation_request",
46
+ "should_offer_retention": true,
47
+ "tone_targets": ["empathetic", "non-manipulative"]
48
+ },
49
+ "rubric_criteria": ["faithfulness", "tone", "completeness"],
50
+ "metadata": {
51
+ "source": "production-2026-04-12-ticket-T-1234",
52
+ "added_at": "2026-04-15",
53
+ "added_by": "@bruno",
54
+ "difficulty": "medium",
55
+ "tags": ["cancellation", "retention", "premium-user"]
56
+ }
57
+ }
58
+ ```
59
+
60
+ Fields:
61
+
62
+ - **`id`** — stable identifier (you'll reference it in regression reports).
63
+ - **`input`** — what the system receives. Match the production input shape exactly.
64
+ - **`expected`** — for rungs 1-3, the deterministic expected output or state change. For rung 4, the rubric-target (what a 5/5 answer would do).
65
+ - **`rubric_criteria`** — which rubric dimensions this case exercises.
66
+ - **`metadata.source`** — provenance. Production ticket? Synthetic? Adversarial?
67
+ - **`metadata.difficulty`** — easy/medium/hard. Track score by difficulty bucket.
68
+ - **`metadata.tags`** — for filtering ("show me cases that exercise retention logic").
69
+
70
+ ## Dataset layout
71
+
72
+ ```
73
+ .dw/eval/datasets/<feature-name>/
74
+ ├── README.md # provenance, sample size, last review, change log
75
+ ├── cases.jsonl # the cases themselves
76
+ ├── rubric.md # the rubric used for rung-4 scoring
77
+ ├── runs/
78
+ │ ├── 2026-05-01.jsonl # one line per case with scores from that run
79
+ │ ├── 2026-05-08.jsonl
80
+ │ └── ...
81
+ ├── calibration/
82
+ │ ├── 2026-05-12-human-scores.jsonl
83
+ │ └── spearman-2026-05-12.txt
84
+ └── changelog.md # when cases were added/removed and why
85
+ ```
86
+
87
+ Everything is committed. Datasets evolve with the feature; the git history shows when and why.
88
+
89
+ ## README.md template
90
+
91
+ ```markdown
92
+ # Reference dataset — <feature name>
93
+
94
+ **Purpose:** evaluate <feature> for <quality dimensions>.
95
+
96
+ **Current size:** N cases (X production-sourced, Y adversarial, Z synthetic).
97
+
98
+ **Difficulty distribution:** easy: A, medium: B, hard: C.
99
+
100
+ **Last reviewed:** YYYY-MM-DD.
101
+
102
+ **Maintainers:** @name1, @name2.
103
+
104
+ ## When to expand
105
+
106
+ Add a case when:
107
+ - A new production failure is observed (always — that's the primary signal).
108
+ - A new edge case is identified during development.
109
+ - A new adversarial pattern is discovered (security review, red-team session).
110
+
111
+ Do NOT add cases just to inflate the count. The 20-from-failures principle: quality over quantity.
112
+
113
+ ## When to retire a case
114
+
115
+ Retire (don't delete) when:
116
+ - The behavior the case checked is no longer relevant (feature removed).
117
+ - The case became trivially passing across all model versions (it's no longer discriminating).
118
+
119
+ Move retired cases to `cases-retired.jsonl` with a `retired_reason`.
120
+ ```
121
+
122
+ ## Adding cases from production
123
+
124
+ Process:
125
+
126
+ 1. **Capture the failure** — paste the actual input that failed in `cases-pending.jsonl`.
127
+ 2. **Sanitize PII** — replace names, emails, IDs, account numbers with realistic-but-fake equivalents. NEVER commit real user data.
128
+ 3. **Define expected behavior** — what SHOULD have happened. Get sign-off from a domain expert if subjective.
129
+ 4. **Categorize** — difficulty, tags, rubric criteria.
130
+ 5. **Promote** to `cases.jsonl` after review.
131
+
132
+ ## Sampling for regression runs
133
+
134
+ You don't need to re-run the entire dataset every time. Smart sampling:
135
+
136
+ - **PR-time:** random sample of 30% + all "high difficulty" cases + any case added in the last 30 days. Fast feedback.
137
+ - **Pre-merge to main:** full dataset.
138
+ - **Nightly:** full dataset + judge re-calibration check.
139
+ - **Pre-deploy:** full dataset + manual eyeball on 10 random outputs.
140
+
141
+ ## Detecting drift
142
+
143
+ After each run, compare against the prior run on the SAME cases:
144
+
145
+ ```
146
+ Run 2026-05-08 vs 2026-05-01:
147
+ faithfulness: 4.2 → 3.9 (-0.3) ⚠ regression
148
+ completeness: 4.0 → 4.1 (+0.1)
149
+ tone: 4.5 → 4.4 (-0.1)
150
+ outcome accuracy: 95% → 92% ⚠ regression
151
+ ```
152
+
153
+ Two ways drift happens:
154
+ 1. **Code change degraded quality** — your model swap or prompt tweak hurt something. Bisect.
155
+ 2. **Judge drift** — the LLM-as-judge itself changed (vendor updated the model). Re-calibrate; the "regression" may be the judge, not the system.
156
+
157
+ ## Dataset versioning
158
+
159
+ When the dataset materially changes (cases added/removed in batch, rubric updated), bump a version in the README:
160
+
161
+ ```
162
+ Dataset version: 2.3
163
+ - v2.3 (2026-05-12): added 8 cases from production tickets in last 30 days
164
+ - v2.2 (2026-04-15): retired 3 cases that became trivially passing
165
+ - v2.1 (2026-03-20): rubric updated to add "completeness" criterion
166
+ ```
167
+
168
+ Each run logs which dataset version it ran against. You can't compare a v1 score to a v3 score directly.
169
+
170
+ ## Cost discipline
171
+
172
+ - Keep the dataset SMALL on purpose. 20-50 cases for most features. 100+ only if the feature has many categorically different inputs.
173
+ - Cheap evaluations (rungs 1-3) run on every case every time.
174
+ - Expensive evaluations (rung 4) run on samples — 20-50 random cases — except for full pre-deploy runs.
175
+
176
+ ## Anti-patterns
177
+
178
+ - **Synthetic-only dataset.** No connection to real production. Confidence isn't real.
179
+ - **Dataset grew to 500 cases nobody re-reads.** Half are duplicates; half are no longer discriminating. Audit and prune.
180
+ - **Cases without expected behavior.** "Just look at the output." No measurement possible.
181
+ - **Dataset not committed.** Lives in a notebook; ephemeral; lost when person leaves.
182
+ - **No metadata tracking source.** Can't tell synthetic from real; can't audit dataset quality.
183
+ - **Dataset reused across features.** Each feature has its own dataset; one-size-fits-all is one-size-fits-none.
184
+
185
+ ## Cross-reference
186
+
187
+ - `oracle-ladder.md` — what to assert per case.
188
+ - `judge-calibration.md` — how to make rung-4 judgments meaningful.
189
+ - `rag-metrics.md` — RAG-specific extras for the dataset structure.
190
+ - `agent-eval.md` — agent-specific extras (trajectory matching).