# Match assembly pipeline

The end-to-end logic the skill follows between getting interview answers and producing a `.usmatch`. This file is loaded on demand — read it when actually composing a match.

## Inputs to the assembly step

After Step 5 of `SKILL.md`, you have:

- `matchName: string`
- `matchDate: ISO YYYY-MM-DD`
- `stageCount: 3 | 4 | 5 | 6`
- `classifierInclusion: 0 | 1 | 2 | 'balanced'`
- `roundCountTarget: 80 | 120 | 150 | 180`
- `character: 'speed' | 'balanced' | 'accuracy' | 'standards'`
- `venue: VenueObj | null`
- `matchDirector: MDObj | null`
- `customStages: [{ name, file, stage }]` — any user-named custom stages already resolved (see `stage_resolution.md`)

## Step A — Decide how many classifiers + custom stages

Effective classifier count from `classifierInclusion`:

```
explicit number (0, 1, 2)  → use directly
'balanced'  → ceil(stageCount / 3) clamped to [1, 2]
  → 3 stages: 1 classifier
  → 4 stages: 2 classifiers
  → 5 stages: 2 classifiers
  → 6 stages: 2 classifiers
```

Stage count breakdown:
- `customStages.length` slots reserved for user-named custom stages
- `effectiveClassifierCount` slots for picks from the bundled classifier index
- Remaining slots (`stageCount - customStages.length - effectiveClassifierCount`) for additional bundled picks (treated the same as classifier picks — there's no separate field-course catalog yet)

If `customStages.length + effectiveClassifierCount > stageCount`, drop the extras: prefer custom stages first, then classifiers. Surface this to the user: *"You asked for 4 stages but named 5 custom stages — using the first 4."*

## Step B — Pick stages from the classifier index

Read `references/classifier_index.md`. Filter by character tags:

```python
# Pseudo-logic
CHARACTER_TAGS = {
  'speed':     {'speed', 'paper+steel', 'steel-heavy', 'transitions'},
  'balanced':  {'balanced', 'paper+steel'},
  'accuracy':  {'accuracy', 'hardcover', 'standards'},
  'standards': {'standards', 'mreload', 'classic'},
}

prefer = CHARACTER_TAGS[character]
candidates = [c for c in catalog if any(tag in c.tags for tag in prefer)]
```

If `candidates` is shorter than the slots needed, expand to the full catalog with a preference weighting (prefer-tag matches go first).

### Round-count balance

Sum the round counts of selected stages. If the total is more than 20% off from `roundCountTarget`:
- **Too high?** Swap one 24-round classifier (CM 99-13, CM 99-46) for a 12-round one.
- **Too low?** Add a 24-round classifier or replace a 6-round (CM 99-62) with a 12-round.

Within ±20% is good enough; don't over-optimize.

### Avoid duplicates

Never pick the same classifier twice in one match. If you've exhausted the unique tagged candidates, expand to neighboring tags.

### Special-case: CM 99-62 (6 rounds)

CM 99-62 ("Bang and Clang") is unusually short. Only include it if:
- Round-count target is `~80` AND stage count is `6` (the short total justifies a quick stage), OR
- The user explicitly asks for it

## Step C — Order the stages

Stage order matters for squad flow. Apply these heuristics in order:

1. **Open with something accessible.** First stage shouldn't be the hardest. Prefer `balanced` or `speed` for slot 1.
2. **Alternate character.** Don't put two `standards` stages back-to-back. Don't put two big-bay stages back-to-back (squad has to walk far).
3. **Save the "showpiece" for slot 3 or 4** (the middle of the match — squad is warmed up but not gassed).
4. **Closing stage** should be enjoyable, not punishing. Avoid ending on `standards` or `accuracy` — finish with `speed` or `balanced` if possible.
5. **Bay logistics.** If the user mentions venue size, try to alternate small-bay and big-bay stages so squads aren't sitting around waiting for one of two big bays to free up.

State the order back to the user:

> *"Order: CM 99-12 (transitions warm-up), CM 99-08 (hardcover accuracy), CM 99-42 (speed + steel showpiece), CM 99-19 (port shooting), CM 99-53 (big steel finish). Sound good?"*

## Step D — Compose the Match JSON

Apply the schema from `references/usmatch_format.md`. Pseudo-code:

```python
import uuid
from datetime import datetime, timezone

iso_date = f"{matchDate}T00:00:00Z"  # UTC midnight on that date

match = {
    "schemaVersion": 1,
    "id": str(uuid.uuid4()).upper(),
    "name": matchName,
    "date": iso_date,
    "stages": [
        {"ref": "CM 99-12"},
        {"ref": "CM 99-08"},
        {"ref": "CM 99-42"},
        {"inline": custom_three_doors_stage_json},  # if user named Three Doors
        {"ref": "CM 99-53"},
    ],
    "notes": match_notes_or_none,
    "venue": venue_or_none,
    "matchDirector": match_director_or_none,
}
```

Set `notes` to `null` (not empty string) if the user didn't provide notes. Same for `venue` and `matchDirector` — `null` if not provided, full object if any field is filled.

## Step E — Validate before driving the editor

Pre-flight checks (mirror the editor's export validation):

```python
assert match["name"].strip(), "match name is required"
assert len(match["stages"]) >= 1, "at least one stage is required"
for entry in match["stages"]:
    has_ref = "ref" in entry and entry["ref"]
    has_inline = "inline" in entry and entry["inline"]
    assert has_ref != has_inline, "each stage Entry needs exactly one of ref/inline"
    if has_inline:
        s = entry["inline"]
        assert s.get("schemaVersion") == 2, "inline stage missing schemaVersion 2"
        assert s.get("name"), "inline stage name is required"
        assert len(s.get("markers", [])) >= 2, "inline stage needs >=2 markers"
```

If any check fails, fix from the skill side (re-resolve a stage, re-prompt the user) — don't push a broken match to the editor.

## Step F — Drive the editor + write the file

See `references/match_builder_api.md` for the preview_eval calls. The two paths:

### Visual review path (default)

1. Inject the composed match into `matchBuilder` via preview_eval
2. Open the Match Builder dialog
3. Screenshot for the user to review
4. On iteration: mutate matchBuilder fields + re-render
5. Call `mbBuildUsmatchPayload()` to read out the final JSON
6. Write the JSON to disk via the `Write` tool with `.usmatch` extension

### Headless path (when the user says "just give me the file")

1. Skip the editor entirely
2. Stringify the composed match
3. Write to disk via the `Write` tool with `.usmatch` extension
4. Tell the user where it landed + how to open it (double-click on Mac if Match Day iOS is paired via Files; AirDrop to iPhone; etc.)

## Step G — Save profiles + ask for iteration

After the file is written:

1. If venue OR matchDirector was filled, offer to save as a profile (see `profile_storage.md`).
2. Ask: *"What would you change?"* Common iterations live in the SKILL.md Step 11.

## Anti-patterns to avoid

- **Don't over-optimize round count.** Within ±20% of target is fine. Swapping stages endlessly to hit exactly the target burns the user's patience.
- **Don't pick the same classifier the user picked last week.** If profiles include a `recentMatches` list (future feature), avoid those. For now, just ask: *"Want to avoid any specific classifiers your squad has shot recently?"*
- **Don't silently include a classifier the user filtered out** by their character choice. If the user picked `speed` and you can only make round-count balance by including `accuracy`, surface the trade-off explicitly.
- **Don't write the .usmatch without telling the user where.** Confirm the save path before writing, especially if the folder doesn't exist yet.
