> built 2026-10-13 23:26 UTC from 2eb9059 (main) · reci 0.0.33. Details: build_info.json

# index.html.md

<!-- generated by epythet -->

# reci

Compile declarative CI recipes into fully-wired GitHub Actions workflows with typed data-flow validation.

To install:	`pip install reci`

## Why reci

Writing GitHub Actions workflows by hand means wading through hundreds of lines of
boilerplate: forwarding step outputs to job outputs, threading `needs.X.outputs.Y`
expressions across jobs, and copy-pasting config values that belong in one place.
When something changes you touch a dozen lines across three jobs — and one typo
silently breaks your pipeline.

**reci** lets you write a short recipe that says *what* your CI does (which actions,
in what order, with what config), and the compiler handles *how* — auto-wiring
cross-job outputs, injecting config from `pyproject.toml`, and validating data-flow
before you push.

## Agent skills (recommended)

reci ships with CLI and Python APIs (see [below]()), but the best way to use it
is through **AI agent skills** — structured instructions that let coding assistants
operate reci on your behalf. Instead of memorizing CLI flags and YAML syntax, just
say what you want:

-  *“Set up CI for this project”*
-  *“Migrate my existing workflow to reci”*
-  *“What CI actions make sense here?”*

### Available skills

| Skill          | What it does                                                                       |
|----------------|------------------------------------------------------------------------------------|
| [ci-setup]()   | Examines your project, proposes a CI plan, writes a recipe, compiles to a workflow |
| [ci-migrate]() | Converts an existing GitHub Actions workflow into a reci recipe                    |
| [ci-advisor]() | Interactive discussion about what CI pipeline makes sense for your project         |
| [reci-dev]()   | Guide for contributors — architecture, adding rules, adding adapters               |

### Using the skills

The skills live in `reci/data/skills/`, so they ship inside `pip install reci` and any agent host can install them from the repo with the GitHub CLI (`gh` 2.90+):

```bash
gh skill install i2mint/reci ci-setup
gh skill install i2mint/reci ci-migrate
gh skill install i2mint/reci ci-advisor
```

**Claude Code** — symlink or copy the skills into your project or your personal skills directory (from a checkout, or from the installed package’s `reci/data/skills/`):

```bash
# Make available to all your projects
ln -s /path/to/reci/reci/data/skills/ci-setup ~/.claude/skills/ci-setup
ln -s /path/to/reci/reci/data/skills/ci-migrate ~/.claude/skills/ci-migrate
ln -s /path/to/reci/reci/data/skills/ci-advisor ~/.claude/skills/ci-advisor

# Or just for one project
ln -s /path/to/reci/reci/data/skills/ci-setup my-project/.claude/skills/ci-setup
```

Then invoke with `/ci-setup`, `/ci-migrate`, or `/ci-advisor` in Claude Code.
See the [Claude Code skills docs](https://docs.anthropic.com/en/docs/claude-code/skills) for details.

**Other agents** — the skills are plain Markdown files following the open
[Agent Skills standard](https://agentskills.org). For Cursor, Copilot, Windsurf,
and other tools, see the standard’s
[integration guide](https://agentskills.org/integrations).

## Quick start

```python
from reci import (
    parse_recipe_string,
    compile_recipe,
    dump_workflow,
    ActionSpec,
    InputSpec,
    OutputSpec,
)

recipe = parse_recipe_string("""
name: CI
on: [push, pull_request]

jobs:
  test:
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '${{ config.python_version }}'
      - id: run_tests
        run: pytest
""")

# Action specs can be fetched from GitHub or provided manually
specs = {
    "actions/checkout@v4": ActionSpec(ref="actions/checkout@v4"),
    "actions/setup-python@v5": ActionSpec(
        ref="actions/setup-python@v5",
        inputs={"python_version": InputSpec(name="python_version", default="3.x")},
    ),
}

config = {"python_version": "3.12"}
workflow = compile_recipe(recipe, specs, config=config)
print(dump_workflow(workflow))
```

The compiler:

- Injects a `setup` job that exports config values as job outputs
- Rewrites `${{ config.* }}` to `${{ needs.setup.outputs.* }}`
- Auto-wires `needs:` edges from data-flow dependencies
- Generates the cross-job output forwarding ceremony (step output -> job output -> `needs` consumption)

## CLI

```bash
# Inspect an action's input/output contract
reci inspect actions/setup-python@v5

# Compile a recipe to workflow YAML
reci compile recipe.yml --output .github/workflows/ci.yml

# Validate a recipe + config
reci validate --recipe recipe.yml --format cli

# Scaffold a config skeleton from a recipe
reci scaffold recipe.yml
```

## Five-level input resolution

For each action input, the compiler resolves its value using this precedence:

1. **Explicit `with:`** in the recipe — used verbatim
2. **Upstream output match** — `${{ steps.<id>.outputs.<name> }}`
3. **Config value** (scoped `action__key`, then shared `key`) — `${{ needs.setup.outputs.<key> }}`
4. **Default from action.yml** — omitted (GitHub uses the default)
5. **Required + no source** — validation error

## Validation

ESLint-style severity (`error`/`warning`/`info`) with ruff-style rule prefixes:

| Rule    | Category   | What it catches                                                                                                                                           |
|---------|------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------|
| DAG001  | Structure  | Cycle detected                                                                                                                                            |
| DAG002  | Structure  | Duplicate output name in a job                                                                                                                            |
| DAG005  | Structure  | Data flow between jobs makes their `needs` form a cycle                                                                                                   |
| DAG006  | Structure  | `needs:` names a job that does not exist (`setup` exists only with config)                                                                                |
| DAG007  | Structure  | A recipe job is named `setup` while config is supplied                                                                                                    |
| FLOW001 | Data flow  | Required input has no source                                                                                                                              |
| FLOW002 | Data flow  | Ambiguous wiring (multiple upstream matches, in a job or across jobs)                                                                                     |
| FLOW006 | Data flow  | Job output consumed downstream comes from a matrix job (non-deterministic) or a job with `if:` (skipped consumer)                                         |
| FLOW007 | Data flow  | `steps.<id>.outputs.<n>` or `needs.<job>.outputs.<n>` reference that resolves to nothing (unknown or not-yet-run step, undeclared output, job not needed) |
| CONF001 | Config     | Required config key missing                                                                                                                               |
| CONF004 | Config     | `${{ config.X }}` names a key that is not in the config, or sits where GitHub has no `needs` context (workflow `env`, job `permissions`)                  |
| CONF005 | Config     | A config key is not a valid output id, or a value holds characters (CR, NUL, C0/C1 controls, U+2028/9) the setup job cannot export unchanged              |
| PURE001 | Purity     | `run:` step breaks typed contract                                                                                                                         |
| ACT002  | Action     | Referenced action not found                                                                                                                               |

## Config adapters

Read CI config from your project file of choice:

| Adapter        | Section          | Library              |
|----------------|------------------|----------------------|
| `pyproject`    | `[tool.ci]`      | tomlkit (round-trip) |
| `wads`         | `[tool.wads.ci]` | tomlkit              |
| `package-json` | `"ci"` key       | json                 |
| `yaml`         | `.ci.yml`        | ruamel.yaml          |

## The recipe format

A recipe looks like a GitHub Actions workflow with reci extensions:

- **`${{ config.* }}`** — references to config values (resolved from pyproject.toml etc.)
- **`bind:`** — input renaming (`bind: {tag_name: version}` wires upstream output `version` to input `tag_name`)
- **`outputs:`** on `run:` steps — manual output annotation for untyped steps

```yaml
name: Python CI
on: [push, pull_request]

jobs:
  test:
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '${{ config.python_version }}'
      - run: pytest

  publish:
    needs: [test]
    steps:
      - uses: actions/checkout@v4
      - id: bump
        uses: i2mint/isee/actions/bump-version-number@master
      - uses: i2mint/wads/actions/git-tag@master
        bind:
          tag_name: version  # wire bump's "version" output to git-tag's "tag_name" input
```

<p class="epythet-aggregates">This documentation as a single file: <a href="reci.md">reci.md</a> (Markdown, for agents).</p>


# _autosummary/reci.action_spec.html.md

# reci.action_spec

Action contract model — parsed from `action.yml` metadata.

An [`ActionSpec`](_autosummary/reci.action_spec.html.md#reci.action_spec.ActionSpec) captures the typed input/output contract of a GitHub
Action so that downstream modules (graph, compiler, validation) can reason
about data flow without fetching YAML at every step.

### Functions

| [`action_local_name`](_autosummary/reci.action_spec.html.md#reci.action_spec.action_local_name)(ref)                       | Extract the last path segment of an action ref, normalized.                                                        |
|-----------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------|
| [`action_spec_from_declaration`](_autosummary/reci.action_spec.html.md#reci.action_spec.action_spec_from_declaration)(ref, \*[, ...]) | Build an [`ActionSpec`](_autosummary/reci.action_spec.html.md#reci.action_spec.ActionSpec) from inline recipe declarations.              |
| [`action_spec_from_ref`](_autosummary/reci.action_spec.html.md#reci.action_spec.action_spec_from_ref)(ref)                    | Fetch action.yml from GitHub and return an [`ActionSpec`](_autosummary/reci.action_spec.html.md#reci.action_spec.ActionSpec).            |
| [`fetch_action_yml`](_autosummary/reci.action_spec.html.md#reci.action_spec.fetch_action_yml)(ref)                        | Fetch and parse an `action.yml` from GitHub.                                                                       |
| [`normalize_name`](_autosummary/reci.action_spec.html.md#reci.action_spec.normalize_name)(name)                         | Canonical form: lowercase, hyphens/spaces → underscores.                                                           |
| [`parse_action_spec`](_autosummary/reci.action_spec.html.md#reci.action_spec.parse_action_spec)(ref, \*, action_yml_data)  | Build an [`ActionSpec`](_autosummary/reci.action_spec.html.md#reci.action_spec.ActionSpec) from a ref string and parsed action.yml data. |

### Classes

| [`ActionSpec`](_autosummary/reci.action_spec.html.md#reci.action_spec.ActionSpec)(ref[, inputs, outputs, composite])   | The typed contract of a single GitHub Action.                                                            |
|--------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------|
| [`InputSpec`](_autosummary/reci.action_spec.html.md#reci.action_spec.InputSpec)(name[, required, default, ...])       | One declared input of a GitHub Action.                                                                   |
| [`OutputSpec`](_autosummary/reci.action_spec.html.md#reci.action_spec.OutputSpec)(name[, description, original_name])  | One declared output of a GitHub Action (see [`InputSpec`](_autosummary/reci.action_spec.html.md#reci.action_spec.InputSpec)). |

### Exceptions

| [`ActionFetchError`](_autosummary/reci.action_spec.html.md#reci.action_spec.ActionFetchError)   | Raised when an action.yml cannot be retrieved.   |
|---------------------------------------------------------------------|--------------------------------------------------|

### *exception* reci.action_spec.ActionFetchError

Bases: [`Exception`](https://docs.python.org/3/builtins/exceptions.html#Exception)

Raised when an action.yml cannot be retrieved.

### *class* reci.action_spec.ActionSpec(ref, inputs=<factory>, outputs=<factory>, composite=False)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

The typed contract of a single GitHub Action.

*inputs* and *outputs* are keyed by **normalized** name.

#### composite *: [bool](https://docs.python.org/3/builtins/functions.html#bool)* *= False*

`runs.using: composite`.  Only a composite action must declare every
output it sets; a JavaScript or Docker action may set undeclared ones.

### *class* reci.action_spec.InputSpec(name, required=False, default=None, description='', original_name='')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One declared input of a GitHub Action.

*name* is normalized (used for matching); *original_name* is the spelling
in `action.yml` (used when emitting, since GitHub does not map `_` to
`-`).

#### *property* emitted_name *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

The spelling to write into a workflow.

### *class* reci.action_spec.OutputSpec(name, description='', original_name='')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One declared output of a GitHub Action (see [`InputSpec`](_autosummary/reci.action_spec.html.md#reci.action_spec.InputSpec)).

#### *property* emitted_name *: [str](https://docs.python.org/3/builtins/stdtypes.html#str)*

The spelling to write into a workflow.

### reci.action_spec.action_local_name(ref)

Extract the last path segment of an action ref, normalized.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> action_local_name('i2mint/wads/actions/run-tests@master')
'run_tests'
>>> action_local_name('actions/checkout@v4')
'checkout'
```

### reci.action_spec.action_spec_from_declaration(ref, , inputs=None, outputs=None)

Build an [`ActionSpec`](_autosummary/reci.action_spec.html.md#reci.action_spec.ActionSpec) from inline recipe declarations.

Used when the recipe manually annotates inputs/outputs for a `run:`
step or overrides the fetched metadata.

* **Return type:**
  [`ActionSpec`](_autosummary/reci.action_spec.html.md#reci.action_spec.ActionSpec)

### reci.action_spec.action_spec_from_ref(ref)

Fetch action.yml from GitHub and return an [`ActionSpec`](_autosummary/reci.action_spec.html.md#reci.action_spec.ActionSpec).

* **Return type:**
  [`ActionSpec`](_autosummary/reci.action_spec.html.md#reci.action_spec.ActionSpec)

### reci.action_spec.fetch_action_yml(ref)

Fetch and parse an `action.yml` from GitHub.

Tries the raw URL first, then a fallback with `action.yaml`.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### reci.action_spec.normalize_name(name)

Canonical form: lowercase, hyphens/spaces → underscores.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> normalize_name('python-version')
'python_version'
>>> normalize_name('Root Dir')
'root_dir'
```

### reci.action_spec.parse_action_spec(ref, , action_yml_data)

Build an [`ActionSpec`](_autosummary/reci.action_spec.html.md#reci.action_spec.ActionSpec) from a ref string and parsed action.yml data.

* **Return type:**
  [`ActionSpec`](_autosummary/reci.action_spec.html.md#reci.action_spec.ActionSpec)


# _autosummary/reci.adapters.base.html.md

# reci.adapters.base

ConfigAdapter protocol — interface for reading/writing CI config.

### Classes

| [`ConfigAdapter`](_autosummary/reci.adapters.base.html.md#reci.adapters.base.ConfigAdapter)(\*args, \*\*kwargs)   | Protocol for pluggable CI config I/O.   |
|--------------------------------------------------------------------------------------|-----------------------------------------|

### *class* reci.adapters.base.ConfigAdapter(\*args, \*\*kwargs)

Bases: [`Protocol`](https://docs.python.org/3/library/typing.html#typing.Protocol)

Protocol for pluggable CI config I/O.

#### default_path()

Return the default filename for this adapter.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

#### read(path)

Read config from a file and return a flat dict.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

#### write(path, config)

Write config to a file, preserving existing content.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)


# _autosummary/reci.adapters.html.md

# reci.adapters

Config adapter registry.

### Functions

| [`detect_adapter`](_autosummary/reci.adapters.html.md#reci.adapters.detect_adapter)(project_root)   | Auto-detect the config adapter from project files.   |
|---------------------------------------------------------------------------------|------------------------------------------------------|
| [`get_adapter`](_autosummary/reci.adapters.html.md#reci.adapters.get_adapter)(name)              | Get a config adapter by name.                        |

### reci.adapters.detect_adapter(project_root)

Auto-detect the config adapter from project files.

* **Return type:**
  [`ConfigAdapter`](_autosummary/reci.adapters.base.html.md#reci.adapters.base.ConfigAdapter) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### reci.adapters.get_adapter(name)

Get a config adapter by name.

* **Return type:**
  [`ConfigAdapter`](_autosummary/reci.adapters.base.html.md#reci.adapters.base.ConfigAdapter)

### Modules

| [`base`](_autosummary/reci.adapters.base.html.md#module-reci.adapters.base)                 | ConfigAdapter protocol — interface for reading/writing CI config.            |
|-------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------|
| [`package_json`](_autosummary/reci.adapters.package_json.html.md#module-reci.adapters.package_json) | PackageJsonAdapter — reads `"ci"` key from package.json.                     |
| [`pyproject`](_autosummary/reci.adapters.pyproject.html.md#module-reci.adapters.pyproject)       | PyprojectTomlAdapter — reads `[tool.ci]` via tomlkit, round-trip preserving. |
| [`wads`](_autosummary/reci.adapters.wads.html.md#module-reci.adapters.wads)                 | WadsAdapter — reads `[tool.wads.ci]` for backward compatibility.             |
| [`yaml_adapter`](_autosummary/reci.adapters.yaml_adapter.html.md#module-reci.adapters.yaml_adapter) | YamlAdapter — reads `.ci.yml` via ruamel.yaml.                               |


# _autosummary/reci.adapters.package_json.html.md

# reci.adapters.package_json

PackageJsonAdapter — reads `"ci"` key from package.json.

### Classes

| [`PackageJsonAdapter`](_autosummary/reci.adapters.package_json.html.md#reci.adapters.package_json.PackageJsonAdapter)()   | Read/write CI config from `"ci"` key in package.json.   |
|-------------------------------------------------------------------------|---------------------------------------------------------|

### *class* reci.adapters.package_json.PackageJsonAdapter

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Read/write CI config from `"ci"` key in package.json.


# _autosummary/reci.adapters.pyproject.html.md

# reci.adapters.pyproject

PyprojectTomlAdapter — reads `[tool.ci]` via tomlkit, round-trip preserving.

### Classes

| [`PyprojectTomlAdapter`](_autosummary/reci.adapters.pyproject.html.md#reci.adapters.pyproject.PyprojectTomlAdapter)()   | Read/write CI config from `[tool.ci]` in pyproject.toml.   |
|---------------------------------------------------------------------------|------------------------------------------------------------|

### *class* reci.adapters.pyproject.PyprojectTomlAdapter

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Read/write CI config from `[tool.ci]` in pyproject.toml.


# _autosummary/reci.adapters.wads.html.md

# reci.adapters.wads

WadsAdapter — reads `[tool.wads.ci]` for backward compatibility.

### Classes

| [`WadsAdapter`](_autosummary/reci.adapters.wads.html.md#reci.adapters.wads.WadsAdapter)()   | Read/write CI config from `[tool.wads.ci]` in pyproject.toml.   |
|------------------------------------------------------------------|-----------------------------------------------------------------|

### *class* reci.adapters.wads.WadsAdapter

Bases: [`PyprojectTomlAdapter`](_autosummary/reci.adapters.pyproject.html.md#reci.adapters.pyproject.PyprojectTomlAdapter)

Read/write CI config from `[tool.wads.ci]` in pyproject.toml.


# _autosummary/reci.adapters.yaml_adapter.html.md

# reci.adapters.yaml_adapter

YamlAdapter — reads `.ci.yml` via ruamel.yaml.

### Classes

| [`YamlAdapter`](_autosummary/reci.adapters.yaml_adapter.html.md#reci.adapters.yaml_adapter.YamlAdapter)()   | Read/write CI config from a `.ci.yml` file.   |
|------------------------------------------------------------------|-----------------------------------------------|

### *class* reci.adapters.yaml_adapter.YamlAdapter

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Read/write CI config from a `.ci.yml` file.


# _autosummary/reci.compiler.html.md

# reci.compiler

Compiler — recipe + action specs + config → GitHub Actions workflow YAML.

The compiler resolves data-flow wiring using a five-level precedence, injects
a `setup` job that exports config values as job outputs, auto-generates the
cross-job output forwarding ceremony, and adds `needs:` edges from data-flow
dependencies.

### Functions

| [`compile_recipe`](_autosummary/reci.compiler.html.md#reci.compiler.compile_recipe)(recipe, action_specs, \*[, config])   | Compile a recipe into a GitHub Actions workflow dict.                        |
|-------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------|
| [`describe_cycle_edges`](_autosummary/reci.compiler.html.md#reci.compiler.describe_cycle_edges)(cycle, edges)                   | The data-flow reads that justify each `consumer -> producer` hop of *cycle*. |
| [`detect_cross_job_edges`](_autosummary/reci.compiler.html.md#reci.compiler.detect_cross_job_edges)(graph, config)                | Find data-flow edges between steps in different jobs.                        |

### Exceptions

| [`CompilationError`](_autosummary/reci.compiler.html.md#reci.compiler.CompilationError)   | Raised when compilation fails.   |
|---------------------------------------------------------------------|----------------------------------|

### *exception* reci.compiler.CompilationError

Bases: [`Exception`](https://docs.python.org/3/builtins/exceptions.html#Exception)

Raised when compilation fails.

### reci.compiler.compile_recipe(recipe, action_specs, , config=None)

Compile a recipe into a GitHub Actions workflow dict.

* **Parameters:**
  * **recipe** ([`Recipe`](_autosummary/reci.recipe.html.md#reci.recipe.Recipe)) – Parsed recipe.
  * **action_specs** ([`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`ActionSpec`](_autosummary/reci.action_spec.html.md#reci.action_spec.ActionSpec)]) – Action specs keyed by ref string.
  * **config** ([`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)] | [`None`](https://docs.python.org/3/builtins/constants.html#None)) – Flat config dict (optional).  If non-empty, a `setup` job is
    injected, jobs depend on it, and config references are wired.
    Cross-job data-flow wiring happens either way.
* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### reci.compiler.describe_cycle_edges(cycle, edges)

The data-flow reads that justify each `consumer -> producer` hop of *cycle*.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

### reci.compiler.detect_cross_job_edges(graph, config)

Find data-flow edges between steps in different jobs.

Public: the validation rules use the same detection as the compiler.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`CrossJobEdge`](_autosummary/reci.graph.html.md#reci.graph.CrossJobEdge)]


# _autosummary/reci.config.html.md

# reci.config

Config resolution — flat dict with scoped overrides.

Resolution order for input `root_dir` of action `run-tests`:

```default
1. ``run_tests__root_dir`` in config → scoped override wins
2. ``root_dir`` in config → shared default
3. ``default`` from action.yml → action default
4. If required and none found → validation error (FLOW001)
```

### Functions

| [`collect_required_config_keys`](_autosummary/reci.config.html.md#reci.config.collect_required_config_keys)(recipe, ...)         | Scan a recipe and return config keys needed by its actions.   |
|----------------------------------------------------------------------------------------------------|---------------------------------------------------------------|
| [`flatten_config`](_autosummary/reci.config.html.md#reci.config.flatten_config)(nested, \*[, prefix])              | Convert a nested dict to a flat dict with `__` separators.    |
| [`resolve_config_for_input`](_autosummary/reci.config.html.md#reci.config.resolve_config_for_input)(input_name, ref, config) | Find a config value for an action input.                      |

### reci.config.collect_required_config_keys(recipe, action_specs)

Scan a recipe and return config keys needed by its actions.

Returns `{key: required}` where *required* is True when the input
has `required=True` and no default.  Inputs given by an explicit
`with:` or fed by an upstream step output (`bind:` honoured) need no
config key.  Keys the recipe references as `${{ config.X }}` are required.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`bool`](https://docs.python.org/3/builtins/functions.html#bool)]

### reci.config.flatten_config(nested, , prefix='')

Convert a nested dict to a flat dict with `__` separators.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)]

```pycon
>>> flatten_config({'testing': {'python_versions': ['3.10']}})
{'testing__python_versions': ['3.10']}
```

### reci.config.resolve_config_for_input(input_name, ref, config)

Find a config value for an action input.

Returns `(config_key, value)` or `None`.

* **Return type:**
  [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`Any`](https://docs.python.org/3/library/typing.html#typing.Any)] | [`None`](https://docs.python.org/3/builtins/constants.html#None)


# _autosummary/reci.graph.html.md

# reci.graph

Recipe DAG — computational graph built on [`graphlib`](https://docs.python.org/3/library/graphlib.html#module-graphlib).

[`RecipeGraph`](_autosummary/reci.graph.html.md#reci.graph.RecipeGraph) wraps [`graphlib.TopologicalSorter`](https://docs.python.org/3/library/graphlib.html#graphlib.TopologicalSorter) to model
the two-level structure of GitHub Actions workflows: jobs containing
sequential steps, with inter-job parallelism governed by `needs:`
edges and data-flow dependencies.

### Functions

| [`find_cycle`](_autosummary/reci.graph.html.md#reci.graph.find_cycle)(deps)   | A dependency cycle in `{node: nodes it depends on}`, or `None`.   |
|---------------------------------------------------------------------|-------------------------------------------------------------------|

### Classes

| [`ActionNode`](_autosummary/reci.graph.html.md#reci.graph.ActionNode)(step_id, job[, ref, action_spec, ...])   | A single step in the recipe graph.              |
|------------------------------------------------------------------------------------------------------|-------------------------------------------------|
| [`CrossJobEdge`](_autosummary/reci.graph.html.md#reci.graph.CrossJobEdge)(source_job, source_step_id, ...)       | Data-flow edge between steps in different jobs. |
| [`RecipeGraph`](_autosummary/reci.graph.html.md#reci.graph.RecipeGraph)()                                       | The computational DAG of a recipe.              |

### *class* reci.graph.ActionNode(step_id, job, ref=None, action_spec=None, with_=<factory>, bind=<factory>, if_=None, declared_outputs=<factory>, run=None, name=None, env=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A single step in the recipe graph.

Uses `step_id` + `job` as its identity (the graph keys these as
`"{job}.{step_id}"` strings for `TopologicalSorter`).

#### *property* input_specs *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [InputSpec](_autosummary/reci.action_spec.html.md#reci.action_spec.InputSpec)]*

steps.

* **Type:**
  Input specs from the action, or empty for run

#### *property* output_names *: [list](https://docs.python.org/3/builtins/stdtypes.html#list)[[str](https://docs.python.org/3/builtins/stdtypes.html#str)]*

All normalized output names this node produces.

#### *property* output_spellings *: [dict](https://docs.python.org/3/builtins/stdtypes.html#dict)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), [str](https://docs.python.org/3/builtins/stdtypes.html#str)]*

`{normalized output name: spelling to emit}` for this node.

Declared (recipe) outputs come first; the action’s own `action.yml`
spelling wins when both name the same output.

### *class* reci.graph.CrossJobEdge(source_job, source_step_id, source_output, target_job, target_step_id, target_input, alternatives=())

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Data-flow edge between steps in different jobs.

#### alternatives *: [tuple](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[str](https://docs.python.org/3/builtins/stdtypes.html#str), ...]* *= ()*

other jobs that also produce the output; the first producer in recipe
order (`source_job`) is the one used

### *class* reci.graph.RecipeGraph

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

The computational DAG of a recipe.

Nodes are identified by `"{job}.{step_id}"` strings.  The actual
[`ActionNode`](_autosummary/reci.graph.html.md#reci.graph.ActionNode) objects are stored in `_nodes`.

#### execution_waves()

Return parallel execution waves (topological generations).

Each wave is a tuple of node keys that can execute concurrently.
Raises `CycleError` if the graph has cycles.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`...`](https://docs.python.org/3/builtins/constants.html#Ellipsis)]]

#### *classmethod* from_recipe(recipe, action_specs)

Build a graph from a parsed recipe and fetched action specs.

*action_specs* is keyed by action ref (e.g. `"actions/checkout@v4"`).

* **Return type:**
  [`RecipeGraph`](_autosummary/reci.graph.html.md#reci.graph.RecipeGraph)

#### job_needs(job_id)

The job ids *job_id* lists in its explicit `needs:`.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

#### upstream_output_names(node, , other_jobs=True)

Normalized output names *node* can be fed from.

Outputs of earlier steps in its own job, plus (unless *other_jobs* is
false) outputs of any step in another job, which the compiler forwards
through `needs`.

* **Return type:**
  [`set`](https://docs.python.org/3/builtins/stdtypes.html#set)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

#### validate(config=None)

Run all validation rules and return a ValidationReport.

Imported lazily to avoid circular imports (validation depends on graph).

### reci.graph.find_cycle(deps)

A dependency cycle in `{node: nodes it depends on}`, or `None`.

The cycle is returned as a closed path following the dependency direction:
each node depends on the next, and the first node is repeated at the end.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)] | [`None`](https://docs.python.org/3/builtins/constants.html#None)

```pycon
>>> find_cycle({'a': ['b'], 'b': ['a']})
['a', 'b', 'a']
>>> find_cycle({'a': ['b'], 'b': ['c'], 'c': ['a']})
['a', 'b', 'c', 'a']
>>> find_cycle({'a': ['b'], 'b': []}) is None
True
```


# _autosummary/reci.html.md

# reci

reci — Compile declarative CI recipes into GitHub Actions workflows.

### Modules

| [`action_spec`](_autosummary/reci.action_spec.html.md#module-reci.action_spec)     | Action contract model — parsed from `action.yml` metadata.                                                               |
|------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------|
| [`adapters`](_autosummary/reci.adapters.html.md#module-reci.adapters)           | Config adapter registry.                                                                                                 |
| [`compiler`](_autosummary/reci.compiler.html.md#module-reci.compiler)           | Compiler — recipe + action specs + config → GitHub Actions workflow YAML.                                                |
| [`config`](_autosummary/reci.config.html.md#module-reci.config)               | Config resolution — flat dict with scoped overrides.                                                                     |
| [`graph`](_autosummary/reci.graph.html.md#module-reci.graph)                 | Recipe DAG — computational graph built on [`graphlib`](https://docs.python.org/3/library/graphlib.html#module-graphlib). |
| [`recipe`](_autosummary/reci.recipe.html.md#module-reci.recipe)               | Recipe model — declarative CI pipeline description.                                                                      |
| [`recipe_checks`](_autosummary/reci.recipe_checks.html.md#module-reci.recipe_checks) | Whole-recipe checks that need the recipe text and the config, not the step graph.                                        |
| [`validation`](_autosummary/reci.validation.html.md#module-reci.validation)       | Validation system — ESLint-style severity, ruff-style rule prefixes.                                                     |
| [`yaml_gen`](_autosummary/reci.yaml_gen.html.md#module-reci.yaml_gen)           | YAML I/O wrapper using ruamel.yaml (YAML 1.2).                                                                           |


# _autosummary/reci.recipe.html.md

# reci.recipe

Recipe model — declarative CI pipeline description.

A recipe is a YAML file that looks like a GitHub Actions workflow but with
reci extensions: `${{ config.* }}` references, `bind:` mappings for
input renaming, and `outputs:` annotations on `run:` steps.

### Functions

| [`parse_recipe`](_autosummary/reci.recipe.html.md#reci.recipe.parse_recipe)(path)        | Load a recipe YAML and return a [`Recipe`](_autosummary/reci.recipe.html.md#reci.recipe.Recipe).   |
|----------------------------------------------------------------------------|--------------------------------------------------------------------------------------------|
| [`parse_recipe_string`](_autosummary/reci.recipe.html.md#reci.recipe.parse_recipe_string)(text) | Parse a recipe from a YAML string (useful for testing).                                    |

### Classes

| [`JobSpec`](_autosummary/reci.recipe.html.md#reci.recipe.JobSpec)(id[, steps, needs, if_, runs_on, ...])   | One job inside a recipe.   |
|---------------------------------------------------------------------------------------------------|----------------------------|
| [`Recipe`](_autosummary/reci.recipe.html.md#reci.recipe.Recipe)(name, on[, jobs, env, defaults, extra])   | A complete reci recipe.    |
| [`StepSpec`](_autosummary/reci.recipe.html.md#reci.recipe.StepSpec)(id[, uses, run, with_, if_, name, ...]) | One step inside a job.     |

### Exceptions

| [`RecipeParseError`](_autosummary/reci.recipe.html.md#reci.recipe.RecipeParseError)   | Raised when a recipe YAML is structurally invalid.   |
|---------------------------------------------------------------------|------------------------------------------------------|

### *class* reci.recipe.JobSpec(id, steps=<factory>, needs=<factory>, if_=None, runs_on='ubuntu-latest', strategy=None, permissions=None, continue_on_error=False, env=<factory>, extra=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One job inside a recipe.

### *class* reci.recipe.Recipe(name, on, jobs=<factory>, env=<factory>, defaults=None, extra=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A complete reci recipe.

### *exception* reci.recipe.RecipeParseError

Bases: [`Exception`](https://docs.python.org/3/builtins/exceptions.html#Exception)

Raised when a recipe YAML is structurally invalid.

### *class* reci.recipe.StepSpec(id, uses=None, run=None, with_=<factory>, if_=None, name=None, outputs=<factory>, bind=<factory>, env=<factory>, continue_on_error=False, extra=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One step inside a job.

### reci.recipe.parse_recipe(path)

Load a recipe YAML and return a [`Recipe`](_autosummary/reci.recipe.html.md#reci.recipe.Recipe).

* **Return type:**
  [`Recipe`](_autosummary/reci.recipe.html.md#reci.recipe.Recipe)

### reci.recipe.parse_recipe_string(text)

Parse a recipe from a YAML string (useful for testing).

* **Return type:**
  [`Recipe`](_autosummary/reci.recipe.html.md#reci.recipe.Recipe)


# _autosummary/reci.recipe_checks.html.md

# reci.recipe_checks

Whole-recipe checks that need the recipe text and the config, not the step graph.

Shared by the compiler (which refuses to emit a workflow with these problems)
and the validation rules (which report them), so both always agree.

### Module Attributes

| [`SETUP_JOB_ID`](_autosummary/reci.recipe_checks.html.md#reci.recipe_checks.SETUP_JOB_ID)   | The generated job that exports config values; recipe jobs may not use the id when config is supplied.   |
|-----------------------------------------------------------------|---------------------------------------------------------------------------------------------------------|

### Functions

| [`as_expression`](_autosummary/reci.recipe_checks.html.md#reci.recipe_checks.as_expression)(text)                  | An `if:` value as a `${{ }}` expression: GitHub evaluates it either way.             |
|---------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------|
| [`config_names`](_autosummary/reci.recipe_checks.html.md#reci.recipe_checks.config_names)(text)                   | The config keys referenced in *text*'s <br/><br/>```<br/>``<br/>```<br/><br/>${{ .   |
| [`config_references`](_autosummary/reci.recipe_checks.html.md#reci.recipe_checks.config_references)(recipe)            | `(location, key, needs_context_available)` for each `${{ config.key }}`.             |
| [`expression_texts`](_autosummary/reci.recipe_checks.html.md#reci.recipe_checks.expression_texts)(text)               | The body of each `${{ }}` in *text*, string literals blanked.                        |
| [`find_recipe_problems`](_autosummary/reci.recipe_checks.html.md#reci.recipe_checks.find_recipe_problems)(recipe, config) | Problems that would make the compiled workflow invalid.                              |
| [`serialize_config_value`](_autosummary/reci.recipe_checks.html.md#reci.recipe_checks.serialize_config_value)(value)        | A config value as the text of a `$GITHUB_OUTPUT` entry.                              |
| [`sub_config_refs`](_autosummary/reci.recipe_checks.html.md#reci.recipe_checks.sub_config_refs)(text, repl)          | Substitute every `config.<key>` inside a <br/><br/>```<br/>``<br/>```<br/><br/>${{ . |
| [`unsupported_config_refs`](_autosummary/reci.recipe_checks.html.md#reci.recipe_checks.unsupported_config_refs)(text)        | The uses of the `config` context in *text* that reci cannot rewrite.                 |

### Classes

| [`RecipeProblem`](_autosummary/reci.recipe_checks.html.md#reci.recipe_checks.RecipeProblem)(rule_id, message[, location])   | One problem found by [`find_recipe_problems()`](_autosummary/reci.recipe_checks.html.md#reci.recipe_checks.find_recipe_problems).   |
|------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------|

### *class* reci.recipe_checks.RecipeProblem(rule_id, message, location=None)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One problem found by [`find_recipe_problems()`](_autosummary/reci.recipe_checks.html.md#reci.recipe_checks.find_recipe_problems).

### reci.recipe_checks.SETUP_JOB_ID *= 'setup'*

The generated job that exports config values; recipe jobs may not use the id
when config is supplied.

### reci.recipe_checks.as_expression(text)

An `if:` value as a `${{ }}` expression: GitHub evaluates it either way.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> as_expression("config.flag"), as_expression("${{ config.flag }}")
('${{ config.flag }}', '${{ config.flag }}')
```

### reci.recipe_checks.config_names(text)

The config keys referenced in *text*’s `${{ ... }}` expressions, in order.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

```pycon
>>> config_names("${{ config.a }} ${{ !config.b && 'config.c' }} config.d")
['a', 'b']
```

### reci.recipe_checks.config_references(recipe)

`(location, key, needs_context_available)` for each `${{ config.key }}`.

Duplicates within one location are reported once.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`str`](https://docs.python.org/3/builtins/stdtypes.html#str), [`bool`](https://docs.python.org/3/builtins/functions.html#bool)]]

### reci.recipe_checks.expression_texts(text)

The body of each `${{ }}` in *text*, string literals blanked.

A literal survives only as the index of a bracket access (`steps['p']`),
so a name inside a message or comparison (`'steps.p.outputs.v'`) is never
taken for a reference.

* **Return type:**
  [`Iterator`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterator)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

```pycon
>>> list(expression_texts("${{ steps['p'].outputs.v == 'steps.q.outputs.w' }} x"))
[" steps['p'].outputs.v == '' "]
```

### reci.recipe_checks.find_recipe_problems(recipe, config)

Problems that would make the compiled workflow invalid.

* `CONF004` — `${{ config.X }}` where `X` is not a config key (the
  rewritten `needs.setup.outputs.X` would be empty or the setup job absent),
  or that sits where the `needs` context is unavailable.
* `DAG006` — `needs:` naming a job that does not exist (`setup` exists
  only when config is supplied).
* `CONF005` — a config key that is not a valid output id, or a string value
  holding characters the setup job cannot export unchanged.
* `DAG007` — a recipe job named `setup` while config is supplied: it
  would replace the generated setup job.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`RecipeProblem`](_autosummary/reci.recipe_checks.html.md#reci.recipe_checks.RecipeProblem)]

### reci.recipe_checks.serialize_config_value(value)

A config value as the text of a `$GITHUB_OUTPUT` entry.

Lists, mappings and tuples become JSON (dates inside them as ISO text); a
boolean `true` / `false`; a number as the author wrote it (a TOML or
YAML `3.10` stays `3.10`, not `3.1`).  `None`, bytes and other
objects have no faithful text and raise [`TypeError`](https://docs.python.org/3/builtins/exceptions.html#TypeError).

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> serialize_config_value({"d": datetime.date(2024, 1, 2), "t": (1, 2)})
'{"d": "2024-01-02", "t": [1, 2]}'
>>> serialize_config_value(True), serialize_config_value(7)
('true', '7')
>>> serialize_config_value(None)
Traceback (most recent call last):
...
TypeError: None (null) has no text form; give the key a value or remove it
```

### reci.recipe_checks.sub_config_refs(text, repl)

Substitute every `config.<key>` inside a `${{ ... }}` in *text*.

*repl* receives the match (`group(1)` is the key).  Only expression code is
touched, never string literals inside an expression (`'config.json'`) and
never text outside `${{ }}` (`cat config.json`).  An expression that
changes is re-emitted as `${{ <code> }}`; the others are left verbatim.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> sub_config_refs("${{ fromJson(config.v) }} cat config.json",
...                 lambda m: "NEEDS." + m.group(1))
'${{ fromJson(NEEDS.v) }} cat config.json'
```

### reci.recipe_checks.unsupported_config_refs(text)

The uses of the `config` context in *text* that reci cannot rewrite.

Only `config.<key>` maps to a setup-job output.  Nested access
(`config.x.y`), bracket access (`config['x']`) and the whole context
(`toJSON(config)`, `config.*`) have no counterpart, and would reach
GitHub unrewritten, where `config` is no context.

* **Return type:**
  [`list`](https://docs.python.org/3/builtins/stdtypes.html#list)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]

```pycon
>>> unsupported_config_refs("${{ config.a }} ${{ config.b.c }} ${{ config['d'] }}")
['config.b.c', 'config[']
>>> unsupported_config_refs("${{ toJSON(config) }} ${{ 'config' }} ${{ config.a }}")
['config']
```


# _autosummary/reci.validation.formatters.html.md

# reci.validation.formatters

Output formatters for validation reports.

### Functions

| [`format_cli`](_autosummary/reci.validation.formatters.html.md#reci.validation.formatters.format_cli)(report)                | Human-readable CLI output with color-coded severity.   |
|------------------------------------------------------------------------------------|--------------------------------------------------------|
| [`format_github_annotations`](_autosummary/reci.validation.formatters.html.md#reci.validation.formatters.format_github_annotations)(report) | GitHub Actions annotation format.                      |
| [`format_json`](_autosummary/reci.validation.formatters.html.md#reci.validation.formatters.format_json)(report)               | JSON output: list of finding dicts.                    |

### reci.validation.formatters.format_cli(report)

Human-readable CLI output with color-coded severity.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### reci.validation.formatters.format_github_annotations(report)

GitHub Actions annotation format.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### reci.validation.formatters.format_json(report)

JSON output: list of finding dicts.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)


# _autosummary/reci.validation.html.md

# reci.validation

Validation system — ESLint-style severity, ruff-style rule prefixes.

### Modules

| [`formatters`](_autosummary/reci.validation.formatters.html.md#module-reci.validation.formatters)   | Output formatters for validation reports.   |
|-------------------------------------------------------------------------------------------------|---------------------------------------------|
| [`report`](_autosummary/reci.validation.report.html.md#module-reci.validation.report)           | Core validation data structures.            |
| [`rules`](_autosummary/reci.validation.rules.html.md#module-reci.validation.rules)             | Validation rule registry and runner.        |


# _autosummary/reci.validation.report.html.md

# reci.validation.report

Core validation data structures.

### Classes

| [`Finding`](_autosummary/reci.validation.report.html.md#reci.validation.report.Finding)(rule_id, severity, message[, ...])   | One validation finding.            |
|-----------------------------------------------------------------------------------------------|------------------------------------|
| [`Severity`](_autosummary/reci.validation.report.html.md#reci.validation.report.Severity)(\*values)                           | ESLint-style three-level severity. |
| [`ValidationReport`](_autosummary/reci.validation.report.html.md#reci.validation.report.ValidationReport)([findings])                 | Collection of validation findings. |

### *class* reci.validation.report.Finding(rule_id, severity, message, location=None, suggestion=None, fixable=False)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

One validation finding.

### *class* reci.validation.report.Severity(\*values)

Bases: [`Enum`](https://docs.python.org/3/library/enum.html#enum.Enum)

ESLint-style three-level severity.

### *class* reci.validation.report.ValidationReport(findings=<factory>)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Collection of validation findings.


# _autosummary/reci.validation.rules.action.html.md

# reci.validation.rules.action

Action metadata validation rules (ACT001–ACT003).

### Functions

| [`check_action_not_found`](_autosummary/reci.validation.rules.action.html.md#reci.validation.rules.action.check_action_not_found)(graph, config)   | Referenced action has no ActionSpec (could not be fetched or parsed).   |
|------------------------------------------------------------------------------------------|-------------------------------------------------------------------------|

### reci.validation.rules.action.check_action_not_found(graph, config)

Referenced action has no ActionSpec (could not be fetched or parsed).

* **Return type:**
  [`Iterable`](https://docs.python.org/3/library/typing.html#typing.Iterable)[[`Finding`](_autosummary/reci.validation.report.html.md#reci.validation.report.Finding)]


# _autosummary/reci.validation.rules.config_rules.html.md

# reci.validation.rules.config_rules

Config validation rules (CONF001–CONF003).

### Functions

| [`check_exportable_config`](_autosummary/reci.validation.rules.config_rules.html.md#reci.validation.rules.config_rules.check_exportable_config)(graph, config)           | Config keys must be valid output ids; values must survive $GITHUB_OUTPUT.   |
|---------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------|
| [`check_missing_required_config`](_autosummary/reci.validation.rules.config_rules.html.md#reci.validation.rules.config_rules.check_missing_required_config)(graph, config)     | Config keys that are required but not present.                              |
| [`check_undefined_config_references`](_autosummary/reci.validation.rules.config_rules.html.md#reci.validation.rules.config_rules.check_undefined_config_references)(graph, config) | `${{ config.X }}` where `X` is not a config key.                            |
| [`check_unused_config`](_autosummary/reci.validation.rules.config_rules.html.md#reci.validation.rules.config_rules.check_unused_config)(graph, config)               | Config keys present but not referenced by any action.                       |

### reci.validation.rules.config_rules.check_exportable_config(graph, config)

Config keys must be valid output ids; values must survive $GITHUB_OUTPUT.

* **Return type:**
  [`Iterable`](https://docs.python.org/3/library/typing.html#typing.Iterable)[[`Finding`](_autosummary/reci.validation.report.html.md#reci.validation.report.Finding)]

### reci.validation.rules.config_rules.check_missing_required_config(graph, config)

Config keys that are required but not present.

* **Return type:**
  [`Iterable`](https://docs.python.org/3/library/typing.html#typing.Iterable)[[`Finding`](_autosummary/reci.validation.report.html.md#reci.validation.report.Finding)]

### reci.validation.rules.config_rules.check_undefined_config_references(graph, config)

`${{ config.X }}` where `X` is not a config key.

* **Return type:**
  [`Iterable`](https://docs.python.org/3/library/typing.html#typing.Iterable)[[`Finding`](_autosummary/reci.validation.report.html.md#reci.validation.report.Finding)]

### reci.validation.rules.config_rules.check_unused_config(graph, config)

Config keys present but not referenced by any action.

* **Return type:**
  [`Iterable`](https://docs.python.org/3/library/typing.html#typing.Iterable)[[`Finding`](_autosummary/reci.validation.report.html.md#reci.validation.report.Finding)]


# _autosummary/reci.validation.rules.dag.html.md

# reci.validation.rules.dag

DAG structure validation rules (DAG001–DAG004).

### Functions

| [`check_cycles`](_autosummary/reci.validation.rules.dag.html.md#reci.validation.rules.dag.check_cycles)(graph, config)                 | Detect cycles in the dependency graph.                                           |
|----------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------|
| [`check_data_flow_needs_cycles`](_autosummary/reci.validation.rules.dag.html.md#reci.validation.rules.dag.check_data_flow_needs_cycles)(graph, config) | Data flow between jobs must not make their `needs` form a cycle.                 |
| [`check_duplicate_outputs`](_autosummary/reci.validation.rules.dag.html.md#reci.validation.rules.dag.check_duplicate_outputs)(graph, config)      | Flag duplicate output names within a job (at the job output level).              |
| [`check_isolated_nodes`](_autosummary/reci.validation.rules.dag.html.md#reci.validation.rules.dag.check_isolated_nodes)(graph, config)         | Flag nodes with no data connections (no outputs consumed, no inputs from graph). |
| [`check_needs_unknown_job`](_autosummary/reci.validation.rules.dag.html.md#reci.validation.rules.dag.check_needs_unknown_job)(graph, config)      | `needs:` names a job that does not exist.                                        |
| [`check_reserved_setup_job`](_autosummary/reci.validation.rules.dag.html.md#reci.validation.rules.dag.check_reserved_setup_job)(graph, config)     | A recipe job uses the id of the generated `setup` job.                           |

### reci.validation.rules.dag.check_cycles(graph, config)

Detect cycles in the dependency graph.

* **Return type:**
  [`Iterable`](https://docs.python.org/3/library/typing.html#typing.Iterable)[[`Finding`](_autosummary/reci.validation.report.html.md#reci.validation.report.Finding)]

### reci.validation.rules.dag.check_data_flow_needs_cycles(graph, config)

Data flow between jobs must not make their `needs` form a cycle.

The compiler adds a `needs` edge from every consumer job to the job that
produces an output it reads.  Combined with explicit `needs:` (or with the
reverse flow) that can close a loop, which GitHub rejects.  A cycle made of
explicit `needs:` alone is DAG001’s.

* **Return type:**
  [`Iterable`](https://docs.python.org/3/library/typing.html#typing.Iterable)[[`Finding`](_autosummary/reci.validation.report.html.md#reci.validation.report.Finding)]

### reci.validation.rules.dag.check_duplicate_outputs(graph, config)

Flag duplicate output names within a job (at the job output level).

* **Return type:**
  [`Iterable`](https://docs.python.org/3/library/typing.html#typing.Iterable)[[`Finding`](_autosummary/reci.validation.report.html.md#reci.validation.report.Finding)]

### reci.validation.rules.dag.check_isolated_nodes(graph, config)

Flag nodes with no data connections (no outputs consumed, no inputs from graph).

* **Return type:**
  [`Iterable`](https://docs.python.org/3/library/typing.html#typing.Iterable)[[`Finding`](_autosummary/reci.validation.report.html.md#reci.validation.report.Finding)]

### reci.validation.rules.dag.check_needs_unknown_job(graph, config)

`needs:` names a job that does not exist.

* **Return type:**
  [`Iterable`](https://docs.python.org/3/library/typing.html#typing.Iterable)[[`Finding`](_autosummary/reci.validation.report.html.md#reci.validation.report.Finding)]

### reci.validation.rules.dag.check_reserved_setup_job(graph, config)

A recipe job uses the id of the generated `setup` job.

* **Return type:**
  [`Iterable`](https://docs.python.org/3/library/typing.html#typing.Iterable)[[`Finding`](_autosummary/reci.validation.report.html.md#reci.validation.report.Finding)]


# _autosummary/reci.validation.rules.flow.html.md

# reci.validation.rules.flow

Data-flow validation rules (FLOW001–FLOW007).

### Functions

| [`check_ambiguous_wiring`](_autosummary/reci.validation.rules.flow.html.md#reci.validation.rules.flow.check_ambiguous_wiring)(graph, config)          | An input name matches multiple upstream outputs, in a job or across jobs.        |
|-------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------|
| [`check_boolean_coercion`](_autosummary/reci.validation.rules.flow.html.md#reci.validation.rules.flow.check_boolean_coercion)(graph, config)          | A string output is wired to a likely-boolean input.                              |
| [`check_matrix_output`](_autosummary/reci.validation.rules.flow.html.md#reci.validation.rules.flow.check_matrix_output)(graph, config)             | A job output consumed by another job comes from a matrix or conditional job.     |
| [`check_output_references`](_autosummary/reci.validation.rules.flow.html.md#reci.validation.rules.flow.check_output_references)(graph, config)         | A `steps.*.outputs.*` or `needs.*.outputs.*` reference that resolves to nothing. |
| [`check_unsourced_required_inputs`](_autosummary/reci.validation.rules.flow.html.md#reci.validation.rules.flow.check_unsourced_required_inputs)(graph, config) | Required input has no source at any level.                                       |
| [`check_unused_outputs`](_autosummary/reci.validation.rules.flow.html.md#reci.validation.rules.flow.check_unused_outputs)(graph, config)            | An output is declared but never consumed downstream.                             |

### reci.validation.rules.flow.check_ambiguous_wiring(graph, config)

An input name matches multiple upstream outputs, in a job or across jobs.

Within a job the nearest preceding step wins.  Across jobs there is no
nearer: the first producing job in recipe order is used.

* **Return type:**
  [`Iterable`](https://docs.python.org/3/library/typing.html#typing.Iterable)[[`Finding`](_autosummary/reci.validation.report.html.md#reci.validation.report.Finding)]

### reci.validation.rules.flow.check_boolean_coercion(graph, config)

A string output is wired to a likely-boolean input.

* **Return type:**
  [`Iterable`](https://docs.python.org/3/library/typing.html#typing.Iterable)[[`Finding`](_autosummary/reci.validation.report.html.md#reci.validation.report.Finding)]

### reci.validation.rules.flow.check_matrix_output(graph, config)

A job output consumed by another job comes from a matrix or conditional job.

A matrix job has one output value for all its legs: whichever leg finishes
last wins.  A job with an `if:` that is false is skipped, and so is every
job that `needs` it (unless it uses `always()` or similar).

* **Return type:**
  [`Iterable`](https://docs.python.org/3/library/typing.html#typing.Iterable)[[`Finding`](_autosummary/reci.validation.report.html.md#reci.validation.report.Finding)]

### reci.validation.rules.flow.check_output_references(graph, config)

A `steps.*.outputs.*` or `needs.*.outputs.*` reference that resolves to nothing.

* `steps.<id>.outputs.<name>` naming a step that does not run before the
  reader, or an output the step’s action does not declare (`-` and `_`
  are different characters to GitHub).
* `needs.<job>.outputs.<name>` naming a job that is not among the job’s
  `needs` (explicit or inferred from data flow), or an output that job
  neither declares in `outputs:` nor has forwarded by the compiler.

* **Return type:**
  [`Iterable`](https://docs.python.org/3/library/typing.html#typing.Iterable)[[`Finding`](_autosummary/reci.validation.report.html.md#reci.validation.report.Finding)]

### reci.validation.rules.flow.check_unsourced_required_inputs(graph, config)

Required input has no source at any level.

* **Return type:**
  [`Iterable`](https://docs.python.org/3/library/typing.html#typing.Iterable)[[`Finding`](_autosummary/reci.validation.report.html.md#reci.validation.report.Finding)]

### reci.validation.rules.flow.check_unused_outputs(graph, config)

An output is declared but never consumed downstream.

Consumed means: matched by an input of a later step (by name, honouring
`bind:`), or read explicitly as `steps.<id>.outputs.<name>` (dot or
bracket syntax; ids and names compared as GitHub does: ignoring case, but
`-` and `_` differ) inside a `${{ }}` of a *later* step of the same
job: its `with:`, `env:`, `run:`, `if:`, `name:` or other keys, or
in a job `outputs:` entry (a job’s other fields cannot read `steps`).
`needs.` references are not counted: only the compiler’s own forwarding and
the job `outputs:` the recipe declares make them resolve.

* **Return type:**
  [`Iterable`](https://docs.python.org/3/library/typing.html#typing.Iterable)[[`Finding`](_autosummary/reci.validation.report.html.md#reci.validation.report.Finding)]


# _autosummary/reci.validation.rules.html.md

# reci.validation.rules

Validation rule registry and runner.

### Functions

| [`register_rule`](_autosummary/reci.validation.rules.html.md#reci.validation.rules.register_rule)(rule_id)                  | Decorator to register a validation rule function.    |
|------------------------------------------------------------------------------------------|------------------------------------------------------|
| [`run_all_rules`](_autosummary/reci.validation.rules.html.md#reci.validation.rules.run_all_rules)(graph, config, \*[, ...]) | Run all registered rules and return a merged report. |

### reci.validation.rules.register_rule(rule_id)

Decorator to register a validation rule function.

### reci.validation.rules.run_all_rules(graph, config, , severity_overrides=None, disabled_rules=None)

Run all registered rules and return a merged report.

* **Return type:**
  [`ValidationReport`](_autosummary/reci.validation.report.html.md#reci.validation.report.ValidationReport)

### Modules

| [`action`](_autosummary/reci.validation.rules.action.html.md#module-reci.validation.rules.action)             | Action metadata validation rules (ACT001–ACT003).   |
|---------------------------------------------------------------------------------------------------------|-----------------------------------------------------|
| [`config_rules`](_autosummary/reci.validation.rules.config_rules.html.md#module-reci.validation.rules.config_rules) | Config validation rules (CONF001–CONF003).          |
| [`dag`](_autosummary/reci.validation.rules.dag.html.md#module-reci.validation.rules.dag)                   | DAG structure validation rules (DAG001–DAG004).     |
| [`flow`](_autosummary/reci.validation.rules.flow.html.md#module-reci.validation.rules.flow)                 | Data-flow validation rules (FLOW001–FLOW007).       |
| [`purity`](_autosummary/reci.validation.rules.purity.html.md#module-reci.validation.rules.purity)             | Purity validation rules (PURE001–PURE003).          |


# _autosummary/reci.validation.rules.purity.html.md

# reci.validation.rules.purity

Purity validation rules (PURE001–PURE003).

### Functions

| [`check_run_step_outputs`](_autosummary/reci.validation.rules.purity.html.md#reci.validation.rules.purity.check_run_step_outputs)(graph, config)   | A run: step writes to $GITHUB_OUTPUT.              |
|------------------------------------------------------------------------------------------|----------------------------------------------------|
| [`check_run_steps`](_autosummary/reci.validation.rules.purity.html.md#reci.validation.rules.purity.check_run_steps)(graph, config)          | Any run: step breaks the typed data-flow contract. |

### reci.validation.rules.purity.check_run_step_outputs(graph, config)

A run: step writes to $GITHUB_OUTPUT.

* **Return type:**
  [`Iterable`](https://docs.python.org/3/library/typing.html#typing.Iterable)[[`Finding`](_autosummary/reci.validation.report.html.md#reci.validation.report.Finding)]

### reci.validation.rules.purity.check_run_steps(graph, config)

Any run: step breaks the typed data-flow contract.

* **Return type:**
  [`Iterable`](https://docs.python.org/3/library/typing.html#typing.Iterable)[[`Finding`](_autosummary/reci.validation.report.html.md#reci.validation.report.Finding)]


# _autosummary/reci.yaml_gen.html.md

# reci.yaml_gen

YAML I/O wrapper using ruamel.yaml (YAML 1.2).

Handles the critical `on:` key (PyYAML treats it as boolean True),
wraps `${{ }}` expressions in SingleQuotedScalarString, and uses
LiteralScalarString for multiline `run:` blocks.

### Functions

| [`dump_workflow`](_autosummary/reci.yaml_gen.html.md#reci.yaml_gen.dump_workflow)(workflow, \*[, stream])   | Serialize a workflow dict to GitHub Actions YAML.                 |
|------------------------------------------------------------------------------------------|-------------------------------------------------------------------|
| [`load_yaml`](_autosummary/reci.yaml_gen.html.md#reci.yaml_gen.load_yaml)(path)                         | Load a YAML file and return a plain dict.                         |
| [`load_yaml_string`](_autosummary/reci.yaml_gen.html.md#reci.yaml_gen.load_yaml_string)(text)                  | Load YAML from a string and return a plain dict.                  |
| [`scalar_source_text`](_autosummary/reci.yaml_gen.html.md#reci.yaml_gen.scalar_source_text)(value)               | A YAML number as written in the source, not as Python renders it. |

### reci.yaml_gen.dump_workflow(workflow, , stream=None)

Serialize a workflow dict to GitHub Actions YAML.

If *stream* is `None`, returns the YAML string.  Otherwise writes to
*stream* and returns `None`.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### reci.yaml_gen.load_yaml(path)

Load a YAML file and return a plain dict.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### reci.yaml_gen.load_yaml_string(text)

Load YAML from a string and return a plain dict.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### reci.yaml_gen.scalar_source_text(value)

A YAML number as written in the source, not as Python renders it.

ruamel keeps the spelling of what it loads, so `3.10` stays `3.10`
(`str()` gives `3.1`), `0x1F` stays `0x1F` and `1e3` stays `1e3`.
Anchors and tags are not part of the spelling.  When ruamel’s text would be
a different number (negative hex, underscored floats), `str()` is used.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

```pycon
>>> scalar_source_text(load_yaml_string("v: 3.10")["v"])
'3.10'
>>> scalar_source_text(load_yaml_string("v: &a 0x1F")["v"])
'0x1F'
>>> scalar_source_text(7)
'7'
```


# about-this-build.html.md

<!-- generated by epythet -->

# About this build

This documentation was built on **2026-10-13 23:26 UTC** from commit <a href="https://github.com/i2mint/reci/commit/2eb9059106424a59f610e1c3c82a0f5ad0435428"><code>2eb9059</code></a> on branch <code>main</code>, for **reci 0.0.33** (from <code>pyproject.toml</code>).

#### WARNING
The documentation and the package may be misaligned:

- The documented version (0.0.33) is ahead of the latest release on PyPI (0.0.32): these docs describe unreleased code.

## Source

|                     |                                                                                                                                                    |
|---------------------|----------------------------------------------------------------------------------------------------------------------------------------------------|
| Commit              | <a href="https://github.com/i2mint/reci/commit/2eb9059106424a59f610e1c3c82a0f5ad0435428"><code>2eb9059106424a59f610e1c3c82a0f5ad0435428</code></a> |
| Branch              | <code>main</code>                                                                                                                                  |
| Tags at this commit | <code>0.0.33</code>                                                                                                                                |
| Working tree        | clean                                                                                                                                              |
| Remote              | <code>https://github.com/i2mint/reci</code>                                                                                                        |

## Continuous integration

|              |                                                                                            |
|--------------|--------------------------------------------------------------------------------------------|
| Repository   | <code>i2mint/reci</code>                                                                   |
| Run          | <a href="https://github.com/i2mint/reci/actions/runs/38484053545">38484053545</a>          |
| Ref          | <code>refs/heads/main</code>                                                               |
| Event commit | <code>07f5bebe7e3b3b4999b10a7dc5fa7a1be1cb6ccb</code> (in the history of the built commit) |

## Tools

|          |         |
|----------|---------|
| epythet  | 0.2.17  |
| Sphinx   | 9.1.0   |
| docutils | 0.22.4  |
| Python   | 3.12.15 |

## Configuration as resolved

|               |                                                                   |
|---------------|-------------------------------------------------------------------|
| theme         | <code>auto</code> (Sphinx theme <code>sphinxawesome_theme</code>) |
| accent        | <code>#753c86</code>                                              |
| api_generator | <code>autosummary</code>                                          |
| ignore        | <code>tests/</code>, <code>scrap/</code>, <code>examples/</code>  |
| agent_outputs | <code>true</code>                                                 |
| aggregates    | <code>md</code>                                                   |
| ai_artifacts  | <code>true</code>                                                 |

## Package on PyPI

Latest release: <a href="https://pypi.org/project/reci/0.0.32/">0.0.32</a>, older than the documented version (0.0.33).

## Reproduce

```bash
git clone https://github.com/i2mint/reci && cd reci
git checkout 2eb9059106424a59f610e1c3c82a0f5ad0435428
pip install "epythet==0.2.17"
epythet quickstart . --ignore tests/ scrap/ examples/
```

The same data, for machines: <a href="build_info.json"><code>build_info.json</code></a> (schema version 1).


# ai-agents.html.md

<!-- generated by epythet -->

# For AI agents

`reci` ships artifacts for coding agents alongside its code. This page lists
them, says where each lives in the repository, and points at the
machine-readable copies of this documentation.

## Skills

Skills are folders holding a `SKILL.md` (the [Agent Skills](https://agentskills.io) format): a description that tells an agent when to use it and a body with the procedure. Install one into your agent with `gh skill` (any host: `--agent claude-code`, `copilot`, `cursor`, `codex`, `gemini`), or use the copy bundled in the wheel.

### `ci-advisor`

Discuss and plan what CI/CD actions, jobs, and recipes make sense for a project. Use this skill when the user wants to talk through their CI strategy, asks “what CI actions should I use”, “what recipes make sense for this project”, “help me plan my CI pipeline”, “what should my CI do”, or wants advice on testing, linting, publishing, deployment, or code quality automation. Also trigger when the user asks about specific GitHub Actions, CI best practices, or trade-offs between different CI approaches — even if they haven’t mentioned reci yet.

```bash
gh skill install i2mint/reci ci-advisor --agent claude-code
```

Source: [`reci/data/skills/ci-advisor`](https://github.com/i2mint/reci/tree/HEAD/reci/data/skills/ci-advisor) (bundled with the pip package).

### `ci-migrate`

Migrate an existing GitHub Actions workflow to a reci recipe. Use this skill when the user wants to convert their current CI workflow to reci, says things like “migrate my CI to reci”, “convert my workflow to a recipe”, “refactor my GitHub Actions”, or “I want to use reci for my existing CI”. Also trigger when the user has an existing .github/workflows/ directory and asks about reci — they probably want a migration, not a fresh setup.

```bash
gh skill install i2mint/reci ci-migrate --agent claude-code
```

Source: [`reci/data/skills/ci-migrate`](https://github.com/i2mint/reci/tree/HEAD/reci/data/skills/ci-migrate) (bundled with the pip package).

### `ci-setup`

Create a new CI/CD pipeline for a project using reci recipes. Use this skill whenever the user wants to set up CI, create a GitHub Actions workflow, add continuous integration to their project, or says things like “make a CI setup for me”, “set up CI”, “add CI/CD”, “create a workflow”, or “I need GitHub Actions for this project”. Also trigger when the user asks about setting up testing, linting, publishing, or deployment pipelines — even if they don’t mention “reci” or “CI” explicitly.

```bash
gh skill install i2mint/reci ci-setup --agent claude-code
```

Source: [`reci/data/skills/ci-setup`](https://github.com/i2mint/reci/tree/HEAD/reci/data/skills/ci-setup) (bundled with the pip package).

### `reci-dev`

Guide for contributing to the reci codebase. Use this skill when the user wants to add features, fix bugs, add validation rules, add config adapters, or understand reci’s architecture. Trigger when the user says things like “I want to add a rule”, “how does the compiler work”, “I want to contribute to reci”, or asks about reci internals.

```bash
gh skill install i2mint/reci reci-dev --agent claude-code
```

Source: [`reci/data/skills/reci-dev`](https://github.com/i2mint/reci/tree/HEAD/reci/data/skills/reci-dev) (bundled with the pip package).

The bundled skills are also on disk after `pip install reci`, under the package’s `data/skills/` directory; link them into an agent without network access with `skill link-skills <that directory>`.

## Instruction files

Files agents read before working in this repository.

- [`.claude/CLAUDE.md`](https://github.com/i2mint/reci/tree/HEAD/.claude/CLAUDE.md): read by Claude Code

## Machine-readable documentation

This site publishes the same documentation in forms that fit an agent’s context window:

- [`llms.txt`](https://i2mint.github.io/reci/llms.txt): an index of every page with a one-line description ([llms.txt](https://llmstxt.org) format)
- [`reci.md`](https://i2mint.github.io/reci/reci.md): the whole documentation as one Markdown file
- `<page>.html.md`: a rendered Markdown twin of every page, advertised from each page’s `<head>` with `<link rel="alternate" type="text/markdown">`
- [`objects.inv`](https://i2mint.github.io/reci/objects.inv): the Sphinx inventory: a symbol-to-URL index (`sphobjinv convert plain objects.inv -`)


# api.html.md

# API reference

| [`reci`](_autosummary/reci.html.md#module-reci)   | reci — Compile declarative CI recipes into GitHub Actions workflows.   |
|---------------------------------------------------------------------|------------------------------------------------------------------------|


