Codegen & the YAML contract

Chemistry’s defining rule: an element is declared in YAML, and a generator compiles that declaration into the code the rest of the platform runs. Nothing about an element type is hand-written in the runtime or the UI. This page is the reference for that contract — the pipeline, the rule that keeps it honest, and why the whole platform can be element-agnostic because of it.

The pipeline

chemistry/elements/**/.triform/*.yaml   →   chemistry/generators/rust/   →   chemistry/generated/
        (the declaration)                    (the codegen binary)              (NEVER edit by hand)

Each element type carries a small set of .triform/*.yaml files — definition.yaml (identity, shell role, runtime wiring), properties.yaml (what you can configure), contract.yaml (ports and wires), ops.yaml (operations and their input schemas), validation.yaml, observability.yaml, and the authored docs.yaml narrative overlay. Running chemistry/generators/build-all.sh reads all of them and regenerates everything under chemistry/generated/ — the Rust types, the JSON schemas, the API routes, the category-dispatch maps, and the docs corpus.

Two rules keep it honest

  • Never edit chemistry/generated/. It is overwritten on every build. You change the YAML (or the generator), regenerate, and commit the source and the generated output together in one change, so the contract and its compiled form never drift apart. A generated file edited by hand is a change that vanishes on the next build.
  • The generator owns policy; the runtime owns mechanism. Physics and the Portal never branch on a specific element type. All element behaviour flows through the generated dispatch — a category lookup, a generated schema, a generated route — not a hand-written if element.kind == "python". To add behaviour, you add it to the YAML or the generator, never by special-casing an element downstream.

This is the lockstep: the declaration and the running code are the same fact in two forms, regenerated together, and that is what lets one URL shape, one run model, and one set of docs sections describe a Python action, a Postgres table, a Slack connector, and a circle identically.

Why this makes the platform element-agnostic

Because every element type is generated from the same YAML shape, the runtime can treat them uniformly. The dispatch layer reads an element’s category from generated metadata and routes accordingly — it never needs to know that python runs in a sandbox and sql runs in Postgres at the level of a hand-written branch; that knowledge lives in the element’s declaration and flows through generated code. The same is true of this documentation: the per-element pages, the operation index, the capabilities catalog, and the topology facts are all generated from the YAML, so the docs and the runtime can never disagree about what an element does.

Add a new element type and you do not touch physics or portal — you write its YAML, run the generator, and the new type appears in the API, the dispatch maps, the docs, and the agent tools at once.

One load-bearing exception

There is exactly one hand-maintained fork of generated output: portal/src/generated/api/ is a hand-edited copy of chemistry/generated/portal-api/ and must not be auto-synced — a warning in build-all.sh marks it. Everything else under chemistry/generated/ is regenerate-only.

Related