# Goal: implement validator rules for parallel shapes (SPEC §6.9.4)

SPEC §6.9 introduces two new structural shapes — `component`
(fan-out) and `tripleoctagon` (fan-in). The SPEC is committed but
the implementation is not. This task is the first dogfood pass:
implement the **parser additions and validator rules** so
`attractor validate` accepts well-formed parallel-region graphs
and rejects malformed ones with actionable errors.

Engine traversal, worktree manager, and resume logic are
explicitly out of scope — they ship in later impl chunks. After
this task, parallel graphs validate but cannot yet `run`.

## Files to touch

- `src/attractor/workflow/graph.py` — add `NodeKind.FANOUT` and
  `NodeKind.JOIN`; extend `shape_to_kind` to map `"component"` →
  `NodeKind.FANOUT` and `"tripleoctagon"` → `NodeKind.JOIN`.
- `src/attractor/workflow/validate.py` — add validator checks
  for the rules below.
- `tests/test_workflow/` — add a positive and a negative test
  per rule. Mirror the layout of existing validator tests.
- `traceable-reqs.toml` — promote
  `REQ-DOT-PARALLEL-SHAPES.required_stages` from `["doc"]` to
  `["doc", "impl", "unit"]`.

## Rules to implement (SPEC §6.9.4)

1. Every `component` pairs with exactly one `tripleoctagon`
   reachable from all its outgoing branches; every directed path
   leaving the component must reach that join.
2. No path leaves a parallel region except via the matching join.
   A branch may not reach `exit`, a different region's join, or
   any node outside the region.
3. **No nesting in v0.2.** A region's branches may not contain
   another `component`. Error message identifies the inner
   component and the outer region's component.
4. Multiple non-overlapping regions in series are allowed (no-op
   rule — ensure rules 1-3 are computed per-region, not
   graph-globally; add a happy-path test fixture).
5. Branches may contain loops bounded by `max_visits` (no new
   check; existing `_check_bounded_cycles` covers this — add a
   happy-path test fixture only).
6. A `component` rejects `prompt`, `script`, `goal_gate`,
   `class`. Outgoing edge labels (if any) must be unique within
   a single component.
7. A `tripleoctagon`'s incoming edges must be unlabeled
   (validator rejects labels on edges with `dst == tripleoctagon`).

## Definition of done

- All rules above implemented as discrete `_check_*` functions in
  `validate.py`. Each function carries
  `# [impl->REQ-DOT-PARALLEL-SHAPES]` on or directly above it.
- Each rule has at least one positive test (valid graph passes
  `validate()`) and at least one negative test (malformed graph
  raises `ValidationFailed` with the expected error message and
  offending source location). Tests carry
  `# [unit->REQ-DOT-PARALLEL-SHAPES]` on or directly above the
  test function.
- The §6.9.5 verifier example and the §6.10.3 merger example
  BOTH validate clean — add them as workflow fixtures under
  `workflows/` and reference them from a test that asserts they
  pass validate.
- `uv run pytest && uv run ruff check src tests && uv run pyright`
  all pass.
- `uv run traceable-reqs check --json` reports
  `REQ-DOT-PARALLEL-SHAPES` complete on `doc`, `impl`, and
  `unit` stages.

## References

- SPEC.md §6.9 (full section on parallel nodes) — load-bearing
- SPEC.md §6.9.4 (validator rules — the 7-rule list above)
- SPEC.md §6.9.5 (verifier-pattern worked example — happy-path
  fixture)
- SPEC.md §6.10.3 (merger-pattern worked example — happy-path
  fixture)
- `src/attractor/workflow/validate.py` — existing validator
  style reference (Tarjan SCC, edge endpoint check, etc.)
- `traceable-reqs.toml` — REQ-DOT-PARALLEL-SHAPES (doc-only;
  promote in this PR)

## Out of scope

- Engine traversal changes (REQ-EXEC-PARALLEL-FANOUT,
  REQ-EXEC-PARALLEL-JOIN) — later impl chunk.
- Worktree manager changes (REQ-EXEC-PARALLEL-WORKTREE) —
  later impl chunk.
- Resume logic and the `branch_name` field on `NodeCompleted`
  (REQ-EXEC-PARALLEL-RESUME) — later impl chunk.
- Engine-side rejection at run time. After this PR, parallel
  workflows validate but a runtime `EngineError` is acceptable
  (engine catches `NodeKind.FANOUT` / `NodeKind.JOIN` in
  `_dispatch_handler` and raises a "not yet implemented" error).

After this task lands, parallel graphs *validate* — running them
is the next chunk.
