DISTILL Methodology: Acceptance Test Creation
This skill provides the acceptance designer's methodology for creating acceptance tests. The orchestrator controls the overall flow (agent dispatch, review gate, handoff) -- this skill focuses on HOW to create good acceptance tests.
ADR-025 (2026-05-07) — DISTILL is canonical AT author
DISTILL produces ALL acceptance tests as scaffolded RED (skip/pending markers). DELIVER's 3-phase cycle (RED / GREEN / COMMIT, per ADR-025) does NOT re-author ATs in RED — it only unskips the scaffolds and writes PBT unit tests. Wave separation: DISTILL = "what should the system do" (ATs), DELIVER = "how" (PBT unit + impl). The pre-DELIVER fail-for-right-reason gate (described in this skill) becomes the RED phase entry/exit gate in DELIVER per ADR-025 D2.
Output Tiers (per D2)
Provenance: feature lean-wave-documentation — D2 (schema-typed sections), D10 (one-line expansion descriptions). Tier-1 [REF] sections (always emitted) + Tier-2 EXPANSION CATALOG items (lazy, on-demand) are the two output bands. The .feature file remains the SSOT for scenarios; the wave-delta sections are pointers + structured summaries. Full contract: nWave/skills/nw-density-resolution-contract/SKILL.md.
Tier-1 [REF] — always emitted
Under ## Wave: DISTILL / [REF] <Section> headings:
- Scenario list with tags — table of scenario titles + tags (
@walking_skeleton,@US-N,@real-io,@in-memory,@error,@property) - WS strategy — A/B/C/D selection per Mandate 5 with one-line justification
- Adapter coverage table — per Mandate 6, every driven adapter mapped to at least one
@real-ioscenario - Scaffolds — list of RED-ready scaffold files created (per Mandate 7) with
__SCAFFOLD__markers - Test placement —
tests/{path}/directory choice with one-line precedent justification - Driving Adapter coverage — every CLI/endpoint/hook in DESIGN mapped to at least one subprocess/HTTP/hook scenario
- Pre-requisites — DESIGN driving ports + DEVOPS environment matrix the scenarios depend on
Tier-2 EXPANSION CATALOG — lazy, on-demand (per D10)
Rendered under ## Wave: DISTILL / [WHY|HOW] <Section> only when requested via --expand <id> (DDD-2), the wave-end menu (expansion_prompt = "ask"), mode = "full" auto-expansion, or an ad-hoc user request mid-session.
| Expansion ID | Tier label | One-line description |
|---|---|---|
scenario-alternatives-considered | [WHY] | Alternative scenario phrasings weighed and rejected (Gherkin variants, tag schemes) |
fixture-design-discussion | [WHY] | Why these tmp_path/conftest fixtures, why these scopes, what they cannot model |
edge-case-enumeration | [WHY] | Full edge-case taxonomy: empty/null/boundary/concurrency/timeout/permission |
error-path-rationale | [WHY] | Why each @error scenario was chosen and what failure mode it surfaces |
tagging-cookbook | [HOW] | Cookbook for tag application: @property, @requires_external, @walking_skeleton |
scaffold-authoring-recipes | [HOW] | Per-language scaffold recipes (Python, TS, Go, Rust, Java) with marker conventions |
pbt-strategy-notes | [WHY] | Property-based testing strategies for invariants surfaced by the feature |
expansion-catalog-rationale | [WHY] | Why this set of expansions, why these defaults, why D10 enforces one-line descriptions |
domain-language-fact-to-step-table | [HOW] | Soft gate: agent proposes fact→step-name pairs for user review before committing step-method names to code |
policy-bootstrap-template | [HOW] | docs/architecture/atdd-infrastructure-policy.md bootstrap snippet emitted on first DISTILL in a project |
tier-b-state-machine-template | [HOW] | State-machine PBT skeleton for Tier B in-memory journey testing (Mandate 10) |
Density resolution (per D12)
Call resolve_density(global_config) from scripts/shared/density_config.py after reading ~/.nwave/global-config.json (missing/malformed = empty dict). Returns mode ("lean" | "full") + expansion_prompt ("ask" | "always-skip" | "always-expand" | "smart") per the D12 cascade (resolver-internal, DDD-5 — do NOT replicate locally). Branch on density.mode for what to emit; branch on density.expansion_prompt at wave end for menu behaviour. Full cascade detail, branch semantics, ad-hoc override workflow: nWave/skills/nw-density-resolution-contract/SKILL.md.
Telemetry (per D4 + DDD-6)
Every expansion choice emits a DocumentationDensityEvent (dataclass at src/des/domain/telemetry/documentation_density_event.py) via event.to_audit_event() → JsonlAuditLogWriter().log_event(...). Schema fields per D4: feature_id, wave, expansion_id, choice, timestamp. For this wave the schema declares "wave": "DISTILL". Use helper scripts/shared/telemetry.py:write_density_event(...) — do NOT write JSONL directly.
Wave-specific signal: DELIVER consuming a lean DISTILL feature-delta — downstream --expand for fixture-design or edge-case enumeration indicates the [REF] baseline plus the .feature file was insufficient for the crafter. Full emission rules: nWave/skills/nw-density-resolution-contract/SKILL.md.
Feature-Delta Schema (US-01, US-02)
Provenance: unified-feature-delta US-01 (scaffold command) and US-02 (E1+E2 validator rules).
Every feature-delta.md is a Markdown document with ## Wave: <NAME> sections. The canonical table format must be used in every ### [REF] Inherited commitments block.
Scaffold command
nwave-ai init-scaffold --feature <feature-name>
Creates docs/feature/<feature-name>/feature-delta.md with three pre-populated wave sections (DISCUSS, DESIGN, DISTILL), each containing a ready-to-fill commitments table. The scaffold passes the E1+E2 validator immediately.
Canonical table format
Every ### [REF] Inherited commitments block MUST have exactly four columns in this order:
## Wave: DISCUSS
### [REF] Inherited commitments
| Origin | Commitment | DDD | Impact |
|--------|------------|-----|--------|
| n/a | <commitment text> | n/a | <impact text> |
Column semantics:
- Origin: wave and row reference of the upstream commitment (e.g.,
DISCUSS#row1) orn/afor root commitments - Commitment: the specific commitment inherited or newly introduced in this wave
- DDD: Design Decision Document reference that authorizes any change (e.g.,
DDD-3) orn/a/(none)when not applicable - Impact: substantive description (>=10 words or a consequence verb from the verb list) of the commitment's effect on the system
Validator rules (E1+E2)
- E1 (SectionPresent): every
## Wave: <NAME>heading must match the canonical pattern. Known wave names: DISCOVER, DISCUSS, DESIGN, DEVOPS, DISTILL, DELIVER. Near-misses get a did-you-mean suggestion. - E2 (ColumnsPresent): every
### [REF] Inherited commitmentsblock must have a header row with the four required columns (Origin, Commitment, DDD, Impact) in any order, case-insensitive.
Incremental authoring
Sections for waves not yet authored may be omitted entirely. The validator does not require all six wave sections to be present. An incremental feature-delta with only DISCUSS is valid. Missing future-wave sections are never flagged.
Acceptance Criteria: Port-to-Port Principle
Every AC MUST name the driving port (entry point) through which the behavior is exercised. This enables port-to-port acceptance tests that make TBU (Tested But Unwired) defects structurally impossible.
Each AC includes:
- Observable outcome: what the user/system sees
- Driving port: the entry point that triggers the behavior (service, handler, endpoint, CLI command)
Without the driving port, a crafter can write correct code that is never wired into the system.
Features: "When user {action} via {driving_port}, {observable_outcome}" Bug fixes: "When {trigger}, {modified_code_path} produces {correct_outcome} instead of {current_broken_