# Nw Distill Port Treatment Policy

> Port-to-port acceptance criteria + the Architecture of Reference (port-class → test treatment) + the Project Infrastructure Policy (concrete mechanism per port) + the walking-skeleton canonical definition and not-applicable exemptions. Consult while classifying a port's test treatment and the concrete mechanism for this codebase.

- Skill: `nwave-ai/nw-distill-port-treatment-policy` (Agent Skill)
- Install (CLI): `npx skillmds@latest add nwave-ai/nw-distill-port-treatment-policy`
- Raw SKILL.md: https://api.skillmd.com/api/skills/nwave-ai/nw-distill-port-treatment-policy/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: nWave-ai (https://skillmd.com/u/nwave-ai)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/nwave-ai/nw-distill-port-treatment-policy

---


# DISTILL Port Treatment + Infrastructure Policy (KNOWLEDGE)

**Kind**: KNOWLEDGE (reference). No forced sequence — consulted on its trigger.

**Trigger**: you are classifying a port → test treatment (real vs fake) and choosing the concrete mechanism for THIS codebase (Testcontainers vs dedicated env vs in-memory; which fake class), or determining walking-skeleton applicability. Composed by `nw-distill`.

## Reasoning Mandate (Caveman)

Verdict-first, tables over prose, evidence-dense, zero narrative. Depth comes from rigor, not padding. State the conclusion, then the supporting evidence; never bury the verdict under exposition.

## Acceptance Criteria: Port-to-Port Principle

Every AC MUST name the driving port (entry point) exercising the behavior → port-to-port ATs → TBU (Tested But Unwired) defects structurally impossible.

Each AC:
1. **Observable outcome**: what user/system sees
2. **Driving port**: entry point triggering the behavior (service, handler, endpoint, CLI command)

No driving port → crafter can write correct code 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_behavior}"

## Architecture of Reference (ports & adapters — project-level defaults)

Three port classes, each with default test treatment. Table = PROJECT-LEVEL: decided once per project (DESIGN of first feature, or framework adoption). NOT renegotiated per feature. Agent applies defaults; per-feature decision = MECHANISM (Project Infrastructure Policy below), not treatment.

| Port type | Examples | Default in test |
|---|---|---|
| **Driving** (entry point) | HTTP API, CLI, in-process call, hook | **In-process via the `OutputPort`** — drive the real entry (`cli main(argv)` / application-service method) IN-PROCESS with a fake output port, no interpreter fork. **subprocess-e2e ONLY for `@walking_skeleton`** (ONE scenario per FEATURE that proves the installed artifact is wired). |
| **Driven internal** (shared state) | Repository, read model, application cache | **In-memory fake by DEFAULT** (config-switch row below); a config parameter selects the real/prod-like mechanism (Testcontainers / dedicated env) for the CI-or-local prod-like run declared in the project Infrastructure Policy. |
| **Driven external / non-deterministic** | Clock, email, SMS, push, payment, LLM, third-party API | Fake/stub with output capture (so a `Then` can observe the side effect) |

Replaces the earlier per-feature Walking Skeleton Strategy A/B/C/D choice. Decision is structural — port CLASS implies port TREATMENT; per-project Infrastructure Policy specializes the mechanism per treatment.

Port unclassifiable by agent → ask the user with a soft prompt. Do NOT improvise the classification.

### The inverted Driving default — in-process, not subprocess (the speed default)

The Driving default INVERTS the old "CLI = subprocess runner" assumption. A new acceptance test drives the entry point **in-process** by default: it calls the real `cli main(argv)` (or an application-service method) directly, threading a fake `OutputPort` that captures the terminal output a `Then` asserts on. No `subprocess.run([sys.executable, ...])` fork — the same Mandate-13 driving-port semantics, 10–100× faster. (Language-agnostic: the rule is "call the shipped entry in-process with a captured output sink"; the `cli main(argv)` form is the Python illustration.)

**subprocess-e2e is reserved for `@walking_skeleton` — ONE per independently shippable value slice.** That scenario proves the installed artifact is wired end-to-end. Every other AT defaults to in-process/in-memory. Wiring coverage beyond it comes from the independent Examiner exercising charter observables through the real surface and the final whole-slice verification; never multiply E2E scenarios to imitate confidence.

### The "CLI = e2e by construction" caveat is DISSOLVED

The old caveat held that a CLI command can only be tested end-to-end (subprocess) because the terminal is intrinsic to the command. That conflation dissolves under the hexagonal cure: **the terminal is an external system behind the `OutputPort`.** The CLI surface SPLITS into two:

| Facet | What it proves | Default treatment |
|---|---|---|
| **output-content / behaviour** | the command computes + emits the right output | **in-process** via the `OutputPort` (captured, asserted on the fake's buffer) |
| **terminal-wiring** | the installed binary actually reaches a real terminal | **exactly one `@walking_skeleton`** per FEATURE (subprocess-e2e) |

The content facet (the bulk of the ATs) drives in-process; the wiring facet is the single WS. A delivery therefore needs at most ONE subprocess AT, not one per scenario, slice or command. Route it through the primary user journey; the Examiner and final whole-delivery verification cover the remaining observable wiring.

### Config-switch — in-memory local default / testcontainers prod-like

Driven-internal ports default to an **in-memory fake** (fast, local). A config parameter selects the prod-like mechanism (Testcontainers / dedicated env) for a CI-or-local run that exercises the real adapter. The switch resolves from the project Infrastructure Policy (`docs/architecture/atdd-infrastructure-policy.md`) per port:

- **`@in-memory`** (default) — the in-memory double; runs everywhere, no container runtime needed.
- **`@real-io` / prod-like** — Testcontainers or a dedicated env; opt-in via the config parameter. When no container runtime is present the prod-like leg degrades-LOUD to INDETERMINATE (`@requires_external`, skip-if-absent) — never silent-pass, never a hard-block on a container-less target. The gate asserts the switch EXISTS and the in-memory path runs; the prod-like path is exercised when the runtime is available.

This keeps the local loop fast (in-memory) while guaranteeing the prod-like path is reachable and exercised where the infrastructure exists.

### Shared-provisioning default for `@real-io` — provision ONCE, isolate PER TEST

**THE RULE: the real adapter's provisioning + teardown (container start/stop, real subprocess/repo/filesystem setup) happens ONCE per test session (or module, if the project's runner cannot share across modules) — never once per test, never once per test class.** Test independence is a SEPARATE, cheap concern layered on top of that one shared instance: a unique namespace/schema/branch/directory per test, or a transaction-and-rollback boundary, or an explicit reset-between-tests step — never re-provisioning the whole real resource per test.

Motivating evidence (this codebase, measured 2026-07-19/20): subprocess-spawning `@real-io` tests that provisioned their own real repo or process per test accounted for **519s = 35% of total CPU in 19% of tests**. Another delivery check leaked orphaned real-postgres clusters because teardown was repeated per test. Both failures share one cause: per-test provisioning of a real resource.

Applying the rule:
- **Author the fixture at session/module scope**, not function scope: the real container/repo/process comes up once, is handed to every test that needs it, and comes down once at the end (or is explicitly reaped on every exit path, including a killed run — never assume the happy-path teardown always runs).
- **Isolate with the CHEAPEST mechanism that keeps tests independent** for that adapter: a fresh schema/table-prefix per test on one shared database, a fresh branch/worktree-relative-path per test on one shared repo clone, a fresh temp-subdirectory per test on one shared filesystem root. The Project Infrastructure Policy's `Note` column records which isolation mechanism was chosen, alongside the shared-provisioning mechanism — e.g. `Testcontainers.MongoDb, ONE instance per session, per-test db name` replaces the older per-test-class-fresh-instance framing.
- **Never let one test's isolation leak into another's** — the isolation mechanism is the thing under scrutiny in review, not the provisioning (provisioning is deliberately NOT re-verified per test, since it is identical for all of them by construction).
- **Degrade-LOUD on shared-instance failure**: if the ONE shared instance becomes unusable mid-session (killed, corrupted), every dependent test must fail informatively (what broke, which test broke it if determinable) — never silently skip or silently re-provision without saying so.

This generalizes the git-pollution-guard fix (`tests/conftest.py:_common_git_dir_is_shared`, 2026-07-20): the failure mode there was the INVERSE of this rule violated on the CONSUMING side — a guard fixture assumed it was the sole writer of a real `.git`, when in fact many linked worktrees legitimately shared it, and its per-test "restore" clobbered siblings' real work. The lesson is symmetric: whichever side owns a SHARED real resource must know it is shared, and neither side may treat per-test churn on it as safe to no-questions-asked reset.

## Project Infrastructure Policy

Architecture of Reference fixes **port class → test treatment** defaults. Project Infrastructure Policy specializes them with the **concrete mechanism** for THIS codebase. **Once per project, not per feature**.

### File location and structure

Lives at `docs/architecture/atdd-infrastructure-policy.md` (project-local). Three tables, one per port class, columns: `Port | Mechanism | Note`.

```markdown
# ATDD Infrastructure Policy

## Driving
| Port | Mechanism | Note |
|---|---|---|
| HTTP API | WebApplicationFactory<Program> | |
| CLI | subprocess from tmp_path | |

## Driven internal (real)
| Port | Mechanism | Note |
|---|---|---|
| IUserRepository (MongoDB) | Testcontainers.MongoDb, ONE instance per session, per-test db name | |

## Driven external / non-deterministic (fake)
| Port | Fake | Note |
|---|---|---|
| IClock | FakeClock | manual advance |
| IEmailSender | FakeEmailSender | in-memory capture |
```

`Note` column optional — one-line clarification only.

### Apply-if-exists / write-if-absent

| Case | Action |
|---|---|
| File exists (default `--policy=inherit`) | read policy, apply recorded decisions; no renegotiation for ports already in table |
| In-scope port missing from policy | soft prompt per missing port (`which mechanism for {port}?`) → **append row to policy** before generating scenarios; policy grows by accretion |
| File absent | create empty skeleton, three section headers (`policy-bootstrap-template` expansion); treat every in-scope port as missing (row above) |

Edited in place. No per-row versioning — git history = audit trail.

### `--policy=fresh` flag

`--policy=fresh`: ignore existing file this run; every in-scope port = missing (soft prompt per port); on completion rewrite file from scratch with newly agreed decisions. `fresh` = major refactors (stack swap, test-strategy overhaul). Otherwise `inherit`.

### Relationship to the Architecture of Reference

Architecture of Reference: "what kind of treatment for this port class?" (real vs fake). Project Policy: "which concrete implementation for that treatment?" (Testcontainers vs dedicated env, which fake class).

Policy CANNOT override port-class defaults: a driven-internal port cannot become fake via policy. Policy records **mechanism** per default treatment only.

### Expansion `policy-bootstrap-template`

Emitted on first DISTILL in a project (file absent):

```markdown
# ATDD Infrastructure Policy

Per `nw-distill-port-treatment-policy`. One file per project. Apply-if-exists; write-if-absent; rewrite with `--policy=fresh`. Git history is the audit trail.

## Driving
| Port | Mechanism | Note |
|---|---|---|

## Driven internal (real)
| Port | Mechanism | Note |
|---|---|---|

## Driven external / non-deterministic (fake)
| Port | Fake | Note |
|---|---|---|
```

## Walking Skeleton Strategy + canonical definition

The 4-way per-feature choice (Strategy A/B/C/D) is REPLACED by two structural decisions: the Architecture of Reference (port-class treatment, once per project) + the Project Infrastructure Policy (concrete mechanism, once per project). Per-feature, DISTILL reads policy (`--policy=inherit` default), appends missing rows, or rewrites (`--policy=fresh`).

### Canonical definition (Cockburn / GOOS)

A walking skeleton is the **thinnest slice of real functionality that runs end-to-end** — from the real entry point through the real architectural components to a terminal on real data — and which can be **automatically built, deployed, and tested** (Cockburn, *Crystal Clear*, paraphrase). In GOOS (Freeman & Pryce) the consequence is operative: **the first acceptance test IS the walking skeleton**, written outside-in before any production code. Because the test drives the real entry point and asserts a real terminal effect, fixture-theater is impossible **by construction** (cross-ref the protocol-driver contract in `nw-test-design-mandates-composition-contract`, the SSOT for the tautological-test anti-pattern).

Default: **every feature is applicable** — it has a real entry point and a real terminal. Only three honest cases exempt a feature, and the declared exemption is **verified by inspecting the feature's changeset** (filesystem inspection of changed files, NOT a mechanical git invocation — gates depend only on Python + filesystem):

| # | "Not applicable" case | Condition | Delta check |
|---|---|---|---|
| 1 | Pure library, no entry point | adds only importable functions/types; no CLI/HTTP/hook/composition entry | no new driving adapter or `__main__`/subcommand in delta |
| 2 | Internal refactor, no new public interface | behavior-preserving restructure; existing ATs cover the surface | no net-new public symbol reachable from an entry point |
| 3 | Config-or-docs-only | change is markdown/yaml/config; no executable behavior path | delta touches only `*.md` / `*.yaml` / config, zero `src/` behavior |

Declaring not-applicable requires naming the case (1/2/3); a delta that contradicts the declaration (e.g. a new subcommand under case 1) is a BLOCKER. Reference: `docs/research/walking-skeleton-atdd-best-practices-2026-06-12.md` (findings A1–A6).

Survives from the old section:

- **Walking-skeleton SCENARIO**: still required (unless a case 1/2/3 exemption is declared and delta-verified). One per feature, tagged `@walking_skeleton @driving_port`, closing the end-to-end loop through the production composition root. Litmus: a non-technical stakeholder confirms "yes, that is what users need."
- **Tagging convention** (unchanged):
  - `@real-io` — scenario uses real adapters (driving + driven-internal per Architecture of Reference)
  - `@in-memory` — scenario uses in-memory doubles (Tier B state-machine PBT, or in-memory acceptance per Mandate 10)
  - `@requires_external` — scenario needs an external system not in the project policy; skip if absent
  - Walking-skeleton scenarios MUST carry `@walking_skeleton @driving_port` and use the production composition root.

