# Per-kind imports + paired-port stereo

Two declarative metadata fields on a kind extend `toBox` with the surrounding-environment bookkeeping the generator needs:

- **`imports?: string[]`** — Faust libraries the kind's `toBox` template depends on, beyond `stdfaust.lib`.
- **`channels: 2`** on ports — marks paired-port stereo (one L + one R port forming a single stereo signal).

Both shipped post-Phase-4 (PR #303 + #284 respectively). Together they let packs use libraries beyond stdfaust + carry stereo signals through the graph without folding to mono at every hop.

## Per-kind imports

Some libfaust-wasm bundled libraries aren't aliased by `stdfaust.lib`. The big three packs hit on:

- **`tubes.lib`** — vacuum-tube triode/pentode stage emulations (`T1_12AX7`, `T2_12AX7`, etc. — symbols bare, no namespace prefix)
- **`tonestacks.lib`** — guitar/bass amp tonestacks (`bassman`, `jcm800`, `vox_ac30`, etc.)
- **`vaeffects.lib`** — VA-modelled effects (`crybaby`, `autowah`)

`faust-vintage` ships 46 kinds across all three. Without imports plumbing, the only resolution path was to add `import("tubes.lib");` etc. to every DSP by hand — fragile + per-DSP.

The **`imports` field** lets a kind declare its dependency:

```json
{
  "kind": "tube_12ax7_t1",
  "imports": ["tubes.lib"],
  "toBox": {
    "outputs": { "out": "prim('T1_12AX7', input('in'))" }
  }
}
```

The Builder's `faustGen` walks all node-kinds in the active graph, collects the **union** of every visited kind's `imports`, dedupes + sorts, and emits one `import("<name>");` line per name in the DSP header right after the default `import("stdfaust.lib");`:

```faust
// Generated from graph — do not edit by hand for now.
import("stdfaust.lib");
import("tonestacks.lib");
import("tubes.lib");
import("vaeffects.lib");

process(in_l, in_r) = …
```

(Sorted alphabetically for diff-stable output across runs and snapshot tests.) The bare names `T1_12AX7`, `jcm800`, `crybaby` resolve at the DSP's symbol scope.

### Rules

- Each entry must end in `.lib`.
- Names must be resolvable by libfaust — either bundled in libfaust-wasm (the three above + `analyzers.lib` / `physmodels.lib` / `quantizers.lib` / etc., all stdfaust-aliased so they don't need `imports`) or mounted via `library.mount` (user-shipped `.lib` files).
- `stdfaust.lib` is always emitted by the header — listing it explicitly is a no-op.
- Skip the field entirely (or `[]`) for kinds whose `toBox` only touches stdfaust-aliased symbols. Most kinds in the `faust` / `faustwave-conventions` / `faust-analysis` / `faust-reverbs` / `faust-physical-models` / `faust-spatial` packs have no `imports` field.

### Shipping your own `.lib` files

Two paths:

1. **`faustLibs` on the pack manifest** — ships inline `.lib` source bytes that the install flow auto-mounts into libfaust's VFS. Use when your pack needs a library NOT already in libfaust-wasm:

```json
{
  "packId": "my-pack",
  "faustLibs": [{ "name": "mycustom.lib", "source": "<full .lib source as string>" }],
  "kinds": [ … ]
}
```

The pack install flow calls `library.mount("mycustom.lib", source)` automatically. Subsequent compiles resolve `import("mycustom.lib")` against your inline source.

2. **User-mounted `.lib`** — for libs the user owns directly (Hub Library install / FaustLibrariesPanel mount). Same effect; the pack's `imports` field works the same way against any name in the VFS, regardless of how it got mounted.

## Paired-port stereo

Phase 4 (PR #284) introduced the **paired-port stereo** pattern. Stereo signals carry as two adjacent ports flagged with `channels: 2` (a marker, not a literal count — the L + R partner is implicit from the next port in declaration order).

### The shape

```json
{
  "inputs": [
    { "id": "in_l", "label": "L", "kind": "audio", "channels": 2 },
    { "id": "in_r", "label": "R", "kind": "audio", "channels": 2 }
  ],
  "outputs": [
    { "id": "out_l", "label": "L", "kind": "audio", "channels": 2 },
    { "id": "out_r", "label": "R", "kind": "audio", "channels": 2 }
  ]
}
```

Each port keeps its own `id` (so `input('in_l')` + `input('in_r')` in the Box-DSL works exactly like the mono case), but the `channels: 2` marker tells the rest of the system:

- **Builder UI**: the dot rendering switches to the white-ring variant (same diameter, thin outer ring) and the cable stroke-width doubles when both halves are wired.
- **Wire validator**: `isPairedHalfPort()` lets the validator surface a "wire the partner too" hint when only one half of a paired pair is connected, vs. plain mono cable.
- **`faustGen`**: dead-port-def gating already skips emit for ports nothing consumes; for paired ports it tracks both halves independently so a stereo path that only uses L stays mono in the emitted source.

### When to use paired-port vs two separate mono ports

| Use paired-port (`channels: 2`) | Use two mono ports |
|---|---|
| The signal is logically **one stereo entity** that should travel as a unit (e.g. a reverb tail, a panned synth's output) | The kind has two **independent mono inputs** (e.g. a mixer with two unrelated sources, a sidechain compressor's main + sidechain) |
| Wrong to use only one half | Each half is meaningful on its own |
| The kind's `toBox` likely calls a `(2, 2)` lib function (`re.jpverb`, `dattorro_rev_default`, …) | Each port goes to a different leaf in the Box AST |

`faust-spatial`'s `mono_to_stereo` is the canonical bridge: ONE mono input, TWO `channels: 2` paired outputs. Use it (or a custom variant) to enter the stereo regime from a mono chain.

### Worked example — Lush Reverb (Stereo)

```json
{
  "kind": "lush_reverb_stereo",
  "inputs": [
    { "id": "in_l", "label": "L", "kind": "audio", "channels": 2 },
    { "id": "in_r", "label": "R", "kind": "audio", "channels": 2 }
  ],
  "outputs": [
    { "id": "out_l", "label": "L", "kind": "audio", "channels": 2 },
    { "id": "out_r", "label": "R", "kind": "audio", "channels": 2 }
  ],
  "toBox": {
    "defs": {
      "jp_pair": "seq(par(input('in_l'), input('in_r')), lib('re', 'jpverb', param('t60'), param('damp'), param('size'), real(0.6), real(0.1), real(2.0), real(1.0), real(1.0), real(1.0), int(200), int(6000)))",
      "jp_l": "seq(def('jp_pair'), par(wire(), cut()))",
      "jp_r": "seq(def('jp_pair'), par(cut(), wire()))"
    },
    "outputs": {
      "out_l": "plus(times(input('in_l'), minus(real(1), param('mix'))), times(def('jp_l'), param('mix')))",
      "out_r": "plus(times(input('in_r'), minus(real(1), param('mix'))), times(def('jp_r'), param('mix')))"
    }
  }
}
```

`re.jpverb` is intrinsically `(2, 2)` — it takes paired stereo in + emits paired stereo out. `par(input('in_l'), input('in_r'))` collects the two mono Boxes into one `(0, 2)` Box; `seq(…, lib('re', 'jpverb', …))` chains them into the reverb. Then `par(wire(), cut())` peels off only L, `par(cut(), wire())` peels off only R, so the per-output dry/wet mix can route per-channel.

## Cross-cutting consequences

Once you mark paired ports with `channels: 2`, the surrounding system honours them:

- The **picker** + Inspector show paired-port kinds with the stereo-cable hint.
- The **wire validator** refuses cable patterns that violate paired-port semantics (e.g. crossing L into R mid-pair).
- The **`faust-spatial` pack** is the bundled case study — three kinds (`mono_to_stereo`, `stereo_widener`, `stereo_panner` etc.) that demonstrate the pattern end-to-end. Read it as a template.

A kind that's stereo internally but exposes a `(1, 2)` shape (one mono in → two mono outs) is **NOT** paired-port. Use `channels: 2` only when both halves are logically a unit.
