Contributing to the framework¶
This guide says where new code, skills, schemas and integrations go in cheme-as-code, and which
rules keep the package usable from the CLI, the viewer API, agents and downstream applications
alike. Read architecture.md for the reasons behind the rules. The repository
conventions in AGENTS.md (Python 3.12+, uv, ruff line length 100, pytest) apply throughout.
Contribution terms¶
By submitting a contribution to this repository, you agree to make it available under the public license applicable to that part of the repository. You also grant a non-exclusive, worldwide, perpetual, irrevocable, royalty-free license to use, reproduce, modify, distribute, and sublicense your contribution under other terms. You retain ownership of your contribution.
Where things go¶
Work through this list from the top. The first match is the place.
- An engineering method (a calculation, a rule that produces findings, a loader, a
generator) goes in the domain that owns the subject:
domains/<d>/<module>.py. Use the domain table in architecture.md; if two domains seem to fit, take the one the information flows from (a balance belongs toprocess, even ifassessmentreports it). - A schema of a file in a plant repository goes next to the code that reads it:
domains/<d>/schemas.py(ordomains/<d>/schemas/<name>.pywhen a domain has several), built onfoundation.schema_base.Strict; a file every repository has (plant.yaml, checks, status, review records) goes infoundation/schemas/. Re-export the model from the cataloguechemeascode.schemas(andLIST_SCHEMAS/OTHER_SCHEMASwhencheme schemashould list it). Domain code imports the owner module, not the catalogue. Add optional fields only: an existing plant repository must stay valid without edits. - Something every domain needs (findings, YAML line numbers, workspace paths, git queries,
review records, provenance) goes in
foundation/, but only when it holds no engineering knowledge. Keep foundation small; when in doubt, keep the code in its domain. - Composition over the whole workspace (validate everything, evaluate gates, sign-off,
status, change impact) goes in
project/. - A wrapper over an external program or library (git configuration, thermo, resvg, a
simulator, a CAD exchange format) goes in
integrations/, with the optional import inside the function. Domain code calls the wrapper; it does not import the library itself. - A command goes in
interfaces/cli/, an endpoint ininterfaces/api/serve.py. Both only parse input, call one function of a domain or ofproject, and format the result. If you are writing an engineering rule in an interface, move it down first. - A skill goes with the capability it drives:
domains/<d>/skills/<name>/SKILL.md(workspace-wide skills:project/skills/<name>/), withscripts/,references/orexamples/beside it where useful. See "Adding or changing a skill". - Project template files (what
cheme initwrites besides the skills) go intemplates/project/. A file whose name starts withdot-is written as.<rest>.
A domain without code keeps only its README.md scope document. Do not add empty classes,
placeholder functions or tools that return invented results; add the first real function
instead.
Import rules¶
interfaces -> project -> domains -> foundation
| | |
+-------------+------------+--> integrations -> foundation
templates -> foundation
foundationimports no domain,project, interface, integration or template.- A domain imports
foundation,integrationsand the domains architecture.md allows it. Feedback in the other direction (a safety finding that sends the team back to chemistry) travels through files and review records, not imports. - Typer only in
interfaces/cli, FastAPI and uvicorn only ininterfaces/api. foundation/__init__.pyand everydomains/<d>/__init__.pyimport nothing.
tests/test_boundaries.py checks every import, including those inside functions, and imports the
basic modules in a fresh interpreter to make sure no optional dependency loads. A rule the code
still breaks is listed in its EXCEPTIONS with the reason; the test fails once an exception is
no longer needed, so remove it when you fix the cause.
Optional dependencies¶
- Base dependencies are what
import chemeascodeand a plaincheme validateneed. Anything heavy (thermodynamics, simulators, CAD, ML models, web servers) goes into an extra inpyproject.toml(todayprops,serve,png,units,chemistry,lca). - Import an optional library inside the function that uses it, never at module level, and
only from
integrations/or the domain function that needs it. When it is missing, return a finding or raise an error that names the extra (pip install 'cheme-as-code[props]'). - Add the library to
LAZY_ONLYandOPTIONALintests/test_boundaries.py. - Agents and LLMs are never a dependency of a deterministic function. A skill may call an agent workflow; a domain function may not.
The pattern: one implementation, called from Python and the CLI¶
cheme process streams is the worked example
(src/chemeascode/domains/process/streams.py, tests/test_stream_report.py):
- The function lives in the domain and takes explicit inputs:
stream_report(root: Path, case: str | None = None) -> StreamReport. It never reads the current directory, never prints, never exits. - The result is structured: a dataclass with the values and their units, per-value
provenance (
given,assumed,calculated), the findings (Finding, each with file, line, rule id and hint) and aSourceRevision(git commit, dirty inputs, sha256 per input file, fromfoundation.provenance), so a caller can check later that the result still applies. Input problems are findings; only a wrong call (no plant repository, an undeclared case) raises. - The command is a thin wrapper: it finds the workspace, calls the function, maps exceptions to exit codes and formats the result. Keep existing output byte-identical when refactoring a command onto a function: snapshot it before, compare after.
- Tests compare the CLI output with the function's result, call the function from another working directory, and import it in a subprocess to show it needs neither Typer nor FastAPI.
A worker on a project snapshot uses the same functions: check out or export the revision into a
directory, call the function with that directory, return the result with its source.
Provenance and change impact¶
- An artifact is identified by its path from the repository root. A source revision is a commit
sha or
sha256:<hex>of a file (foundation.provenance.file_fingerprint). - A result computed from files should carry a
SourceRevision(foundation.provenance.source_revision(root, paths)). - A relationship that the existing mechanisms do not record goes into an optional
basis: [{path, revision, relation}]list, withrelationone ofderived-from,represents,analyses,depends-on. Review records have it today. project/impact.pyturns changed sources intocheme.review-neededfindings at level info. A new artifact kind with recorded sources adds a collector there. Never regenerate, rewrite or declare anything invalid from it.
Adding or changing a skill¶
- Put it in
domains/<d>/skills/<name>/SKILL.md(orproject/skills/<name>/). Skill names are unique across the package;templates.install.bundled_skills()refuses duplicates. - Keep the frontmatter (
name,description) and use the seven sections, in this order: Purpose and when to use, Required inputs and missing inputs, Workflow and tools, Validation and failure handling, Outputs and provenance, Required human judgment, References. - A skill describes judgment and workflow and calls
chemecommands. Shared engineering algorithms belong in a domain function, not in a skill script. - Run
uv run python scripts/build_skill_manifest.pyanduv run python tests/test_init_golden.py > tests/data/init-manifest.txt, and commit both outputs.tests/test_skills.pyfails while the manifest lacks the current hash, so thatcheme skills statuscan tell an outdated installed copy from a locally changed one. cheme initandcheme skills updatenever replace a locally modified skill without--force; keep it that way.
Moving a module¶
There is no compatibility layer for internal modules: a move changes the import path. Moving a module on the documented Python API (ADR 0032) is a breaking change and needs a new major version, or the new path first and the removal in the next major.
git mvthe file, sogit log --followkeeps its history. Change only import lines.- Update every reference to the old path:
src/, tests, skills (SKILL.md, scripts),templates/project/,examples/,scripts/and the docs. Do not add a shim or alias module;tests/test_old_paths.pychecks that the removed top-level names stay removed. - Add a row to migration-table.md and a CHANGELOG entry, since plant repositories' generator scripts may import the old path.
- Anything under
domains/process/*.pychangesgenerator_fingerprint(). Rebuild the examples (uv run --directory examples/<example> cheme process build) and commit the sheets; onlygenerator_sha256changes.
Releases and versions¶
cheme-as-code follows semantic versioning from 1.0.0 (ADR 0032). A release sets version in
pyproject.toml (then uv lock), adds a dated CHANGELOG section (with "Breaking" first when
there is one, and its migration), and is tagged on main after the merge with an annotated
vX.Y.Z tag and a GitHub release whose notes are that CHANGELOG section. A breaking change to
the public interface the ADR lists needs a new major version.
Architecture decisions¶
A change that alters the architecture (layers, module ownership, import rules, base versus
optional dependencies, a new out-of-process worker), a public file format or schema, the agent
interface (commands, findings, skills) or a contract with another ChemE-as-Code repository (the
viewer API, the project template, the process library) adds a new Architecture Decision Record in
docs/adr/, or supersedes an existing one, in the same pull request. Copy
docs/adr/template.md, add the record to the index in the ADR overview and to nav in
mkdocs.yml. An accepted ADR is not rewritten: a new one supersedes it. ADRs record decisions
about this software; a plant's engineering decisions belong in that plant's DESIGN.md.
Tests¶
- Tests live in
tests/, one file per feature or module, and import the modules from their current paths. - Test behaviour, not folder layout: an output, a finding, a refusal, an import that must not happen.
tests/test_init_golden.pypins whatcheme initwrites. Regenerate the manifest only for an intended change and say so in the commit message.- A test that reads Rich help or error text strips ANSI codes first
(
re.sub(r"\x1b\[[0-9;]*m", "", output)): CI forces coloured output, so a test that passes locally can fail there. - Run
uv run pytest -qanduv run ruff check . && uv run ruff format --check .before every commit. - Catch the exceptions you expect. An
except Exceptionis for a reporting boundary (a file, pyDEXPI or a third-party library that can fail in any way, reported as a finding or an error) and carries# noqa: BLE001 - <why>. Everysubprocess.runpassescheck=; anoqanames the rule it silences, and ruff (RUF100) flags one that silences nothing.
Web databases and prediction services¶
An adapter to a web database or a service goes in integrations/<name>.py and takes an
integrations.http.HttpClient argument; the parsing of a page is a separate function of the
HTML, tested on a saved snippet. A service endpoint is a required argument: no default host,
no environment variable, no key read at import. Tests never touch the network; a live test is
skipped unless CHEME_NETWORK_TESTS=1. The domain function chooses between sources and
returns the source with every value.
Research and paper projects¶
Paper-specific experiments, manuscripts, datasets and results do not belong in this repository. A research project is its own repository that depends on the package:
[project]
dependencies = ["cheme-as-code[props] @ git+https://<host>/<org>/core@<tag-or-sha>"]
Pin a tag or commit, so that results stay reproducible, and record that pin with the results.
Import the domain functions (chemeascode.domains.process.streams.stream_report, ...) rather than
the CLI. When a method from a research project turns out to be generally useful, contribute it
here as a domain function with tests, following this guide, and keep the experiment in the
research repository.
Code from other repositories comes in only when its licence allows it under AGPL-3.0-or-later; companion-inventory.md lists candidate components and their status.
Documentation site¶
The documentation site is built with MkDocs Material from mkdocs.yml and docs/ (dependency
group docs). Preview it with live reload, or build it as CI does:
uv sync --all-extras --group docs
uv run --group docs mkdocs serve # http://127.0.0.1:8000, rebuilds on changes
uv run --group docs mkdocs build --strict # site/, fails on any warning
Most guide pages contain no copy of their text: a comment such as
<!-- readme: ### process: BFD, PFD, stream table and properties --> includes that section of
REFERENCE.md, the full reference (or, with include:, of another repository file such as the project template's
STAGES.md) when the site is built (docs/hooks/includes.py). Change REFERENCE.md, not the page;
renaming a heading there that a page includes fails the strict build. The CLI, schema and serve
API reference pages are not files at all: docs/hooks/reference.py generates them from the
Typer app, the cheme schema catalogue and the OpenAPI document of cheme serve on every build.
A new page must be added to nav in mkdocs.yml.