|
| 1 | +# CLAUDE.md |
| 2 | + |
| 3 | +Guidance for Claude Code (and any contributor) working in this repository. |
| 4 | + |
| 5 | +`n8n_iac` is an infrastructure-as-code SDK for n8n: it models workflows as typed Python, |
| 6 | +compiles them to wire-exact n8n JSON through a deterministic pipeline, validates them offline, |
| 7 | +and deploys them with a Terraform-shaped plan/apply lifecycle. |
| 8 | + |
| 9 | +--- |
| 10 | + |
| 11 | +## Source of Truth |
| 12 | + |
| 13 | +Two specifications define **all** architecture. Code exists to implement them, not to |
| 14 | +reinterpret them. |
| 15 | + |
| 16 | +- [`FABLE_SPEC.md`](FABLE_SPEC.md) — the core engine: wire/authoring Pydantic models, the |
| 17 | + IR/AST compiler (capture → normalize → validate → emit), and the deterministic plan |
| 18 | + validator (rules R1–R10, C1–C3, node rules W1–W4 / H1–H6). |
| 19 | +- [`OPUS_SPEC.md`](OPUS_SPEC.md) — the scaffolding: node classes, the layered REST client, |
| 20 | + the deployer/CLI, and the Pytest suite structure. |
| 21 | + |
| 22 | +**Every change to architectural logic MUST conform strictly to these specs.** If a task |
| 23 | +requires behavior that contradicts a spec, stop and update the spec first (a reviewable |
| 24 | +change) — do not diverge in code. If the two specs disagree, `FABLE_SPEC.md` governs the |
| 25 | +core engine and `OPUS_SPEC.md` governs everything that depends on it. When in doubt, cite the |
| 26 | +governing section (e.g. "FABLE §3.4 C1") in the PR description and the code comment. |
| 27 | + |
| 28 | +--- |
| 29 | + |
| 30 | +## Toolchain & Standards (enforced) |
| 31 | + |
| 32 | +| Concern | Requirement | |
| 33 | +|---|---| |
| 34 | +| **Language** | Python **3.12+**. This is the floor; do not use syntax or stdlib that predates 3.12, and do not lower it. | |
| 35 | +| **Dependency & packaging** | **Poetry** only. All deps, scripts, and tool config live in `pyproject.toml`. Do not add `requirements.txt`, `setup.py`, or `pip install` steps. | |
| 36 | +| **Data modeling** | **Pydantic v2** exclusively. No v1 APIs, no `pydantic.v1` shim. | |
| 37 | +| **Testing** | **Pytest**. No `unittest`, no `nose`. Property tests use Hypothesis (per OPUS §3.3). | |
| 38 | +| **Typing** | **`mypy --strict`** must pass with zero errors. Fully annotate every signature; no implicit `Any`, no untyped `def`. `ruff` also gates the build. | |
| 39 | + |
| 40 | +> **Version note.** OPUS §3.7's CI matrix is Python 3.12 / 3.13. **3.12 is the strict |
| 41 | +> floor** — never reintroduce 3.10/3.11 support or compatibility shims for them. |
| 42 | +
|
| 43 | +--- |
| 44 | + |
| 45 | +## Pydantic v2 rules (non-negotiable) |
| 46 | + |
| 47 | +The wire/authoring split (FABLE §1) is the single most important decision in the engine. |
| 48 | +Preserve it. |
| 49 | + |
| 50 | +- **Wire models** (`n8n_iac.core.wire`) are serialization-exact and derive from a shared |
| 51 | + `StrictBase`: |
| 52 | + |
| 53 | + ```python |
| 54 | + class StrictBase(BaseModel): |
| 55 | + model_config = ConfigDict( |
| 56 | + extra="forbid", # unknown keys on the wire are a bug |
| 57 | + populate_by_name=True, # snake_case in Python, camelCase on the wire |
| 58 | + alias_generator=to_lower_camel, |
| 59 | + validate_assignment=True, |
| 60 | + frozen=True, # immutable post-construction |
| 61 | + ser_json_bytes="base64", |
| 62 | + ) |
| 63 | + ``` |
| 64 | + |
| 65 | + `extra="forbid"` applies to the **envelope and node metadata only**. Node `parameters` |
| 66 | + stay `dict[str, Any]` at the wire layer (FABLE §1.1) — n8n's parameter space is open. |
| 67 | + Parameter strictness lives in the **node schema layer** (FABLE §1.4), never on the wire model. |
| 68 | + |
| 69 | +- **Authoring models** (`n8n_iac.core.dsl`) are the Python-facing classes. They are **never |
| 70 | + serialized directly** — the compiler lowers them to wire models. |
| 71 | + |
| 72 | +- Node classes (OPUS §1.1) **validate nothing themselves**: `**params` is stored raw and |
| 73 | + validated by the compiler against `PARAMS_MODEL`. One validation site, one diagnostic format. |
| 74 | + |
| 75 | +- Pydantic `ValidationError` is **never** raised to the user. The boundary layer converts it |
| 76 | + into `SchemaViolation` diagnostics (FABLE §1.5). |
| 77 | + |
| 78 | +- Version-conditional and discriminated shapes use Pydantic **discriminated unions**, not |
| 79 | + branching code (FABLE §1.4: `Webhook` `multipleMethods`, HTTP Request `authentication`). |
| 80 | + |
| 81 | +--- |
| 82 | + |
| 83 | +## Determinism (a tested property, not an aspiration) |
| 84 | + |
| 85 | +Byte-identical JSON on every machine and every run is a hard requirement (FABLE §2.2, §3.3): |
| 86 | + |
| 87 | +- Never iterate a `dict`/`set` for anything that reaches output; use the canonical order from |
| 88 | + compiler pass b6. |
| 89 | +- Node IDs and `webhookId` are `uuid5`-derived from a required `Workflow.uid` — **never** |
| 90 | + `uuid4`, never random. |
| 91 | +- Diagnostics sort by `(severity, rule_id, node_name, port, index)`; cycles report in |
| 92 | + lexicographically-minimal rotation. |
| 93 | +- `PYTHONHASHSEED` independence is tested (OPUS §3.2 hash-seed matrix). Do not weaken it. |
| 94 | + |
| 95 | +--- |
| 96 | + |
| 97 | +## Testing & coverage |
| 98 | + |
| 99 | +- Suite layout is fixed by OPUS §3.1 (`unit/`, `integration/`, `e2e/`, `property/`, `golden/`, |
| 100 | + `contract/`). Add tests to the matching directory. |
| 101 | +- Coverage target is **100% line and branch** on `n8n_iac/` (`--cov-fail-under=100 |
| 102 | + --cov-branch`, OPUS §3.6). Every `raise` path and both branches of every discriminated |
| 103 | + union / version gate need a test. No blanket `# pragma: no cover`. |
| 104 | +- E2E tests (`@pytest.mark.e2e`) need a real n8n and are excluded by default. |
| 105 | + |
| 106 | +--- |
| 107 | + |
| 108 | +## Common commands |
| 109 | + |
| 110 | +```bash |
| 111 | +poetry install # set up the environment |
| 112 | +poetry run pytest # full suite (e2e excluded by default) |
| 113 | +poetry run pytest tests/unit # fast unit tests |
| 114 | +poetry run pytest -m e2e # opt-in live tests (needs Docker + n8n) |
| 115 | +poetry run pytest --update-golden # regenerate golden files (review the diff!) |
| 116 | +poetry run mypy --strict n8n_iac # strict type check — must be clean |
| 117 | +poetry run ruff check n8n_iac # lint gate |
| 118 | +``` |
| 119 | + |
| 120 | +--- |
| 121 | + |
| 122 | +## Working here |
| 123 | + |
| 124 | +- Match the surrounding code's idiom, naming, and comment density. |
| 125 | +- Do not add API transport logic to the core engine, or planning logic to the transport layer |
| 126 | + — the layering in OPUS §2.1 is strictly downward. |
| 127 | +- When you touch a rule (R*, C*, W*, H*) or a lowering function, update the corresponding |
| 128 | + golden fixture and the targeted mutation test (OPUS §3.3–§3.4). |
0 commit comments