Skip to content

Stages and gates

A plant advances through the stages listed in plant.yaml (the template: concept, basic, detail). Each stage has gates in checks/checks.yaml; cheme check evaluates them and cheme check --write signs a stage off in checks/status.json.

The project stage is one of four axes, kept apart: it is neither the calculation maturity cheme maturity evaluates, nor the design maturity a process-library manifest claims, nor a review status; see the glossary.

The template's STAGES.md defines what each stage requires:

Stages

The stages are listed in plant.yaml; the gates of each are in checks/checks.yaml. A gate on P&ID content is kind: on-sheet (tags and classes found in the model), never a grep over the XML: DEXPI builds printed tags such as AIA-3103 from several attributes, so grep finds ids, not tags.

A stage is reached when its sign-off (cheme check --stage <stage> --write, an entry in checks/status.json) is on the default branch. Stage work is done on a branch stage/<stage>-<topic>; the sign-off is its last commit, and the branch is merged by a human or the reviewer, not by its author (see AGENTS.md). A sign-off left on an unmerged branch has not taken effect.

concept: Concept and feasibility

Design basis agreed (assumed values marked ASSUMED), block flow (process/flowsheet.yaml, the generated BFD, balances evaluable and clean), main equipment identified, the equipment, line and instrument lists and DESIGN.md started (gates design-basis, concept-lists, assumptions-marked, process-concept, process-balances; process-links-concept reports unresolved links). The plot plan: the site, its areas, roads and pipe racks, every equipment-list item placed and the spacing criteria declared as separations: (gate layout-plot-plan, skill layout-yaml). Economics: the decision the next stage needs stated, an AACE class 5 capital estimate, the prices of the main feeds and products with their sources, and a screening TEA showing which premises dominate (estimate/tea.yaml, skill tea).

basic: Basic engineering

PFD generated with no refinement pending and no stale calculated values (process-basic), every flowsheet link resolved (process-links), P&IDs complete and valid, equipment and line lists complete, design review done. The 3D layout (gate layout-3d): layout/plant-model.yaml built from the current inputs and committed, every piped nozzle positioned (from the P&ID side or an override), every elevated item mounted on a building or structure level, every process line of DN 25 and above routed or skipped with a reason (utilities follow at detail), no clash or headroom finding left unaccepted, zones declared (layout/zones.yaml, skill area-classification), and a layout review (topic layout, gate layout-review) that cites the rendered plot plan and at least two sections (skills pipe-routing, structures-access, review). The material take-off (estimate/mto.csv, skill mto-estimate) is rewritten from the committed model (gate mto). Economics: a class 4 capital estimate (equipment costed from datasheet sizes, OSBL and owner's cost stated), every flow priced or listed free, the TEA's uncertainty and break-even values reviewed against the decision (gate tea, skill tea).

detail: Detail engineering

Datasheets (one equipment/datasheets/<TAG>.yaml per equipment-list item, format in equipment/datasheets/README.md, cheme schema datasheet; gate datasheets), instrument list, HAZOP actions closed. Every line routed, utilities and small bore included, still without clashes (gate layout-detail). Every motor and instrument has a cable routed in trays from its room, no tray over its fill, power and instrument trays kept apart (electrical/electrical-list.yaml, skill electrical-cabling; gate electrical). Every clad building is ventilated: air handling units, rooms with their air changes, ducts routed inside the building within their velocity limits, louvres large enough (layout/hvac.yaml, skill hvac-ducts; gate hvac). Where the plant documents its control: every loop and sequence complete and every control reference resolved (control/loops.yaml, control/sequences.yaml, process/narrative.md, control/philosophy.md; skills control-loops, control-sequences, control-narrative; gate control-complete). Economics: a class 3 estimate from quotations and the take-off, the TEA current with it, ready for the investment decision.

check

cheme check [--stage S [--only-stage]] [--write [--review RECORD] [--by NAME]] evaluates the gates of checks/checks.yaml and exits 1 when one fails. --stage S runs the gates of every stage up to S in plant.yaml order (concept and basic for basic); --only-stage only S's. A gate whose stage: is not a stage of plant.yaml (a typo such as Concept) is never left out: it fails in every run, and cheme validate reports it (cheme.check-stage-unknown, error), as it does a check id listed twice (cheme.check-duplicate-id, error). It only prints, except with --write, which signs off stage S (default: plant.yaml's stage) in checks/status.json:

{
  "stages": {
    "basic": {
      "stage": "basic", "commit": "<sha of the checked tree>", "date": "2026-10-18T14:02:11Z",
      "signed_by": "A. Engineer", "branch": "stage/basic-closeout", "default_branch": "main",
      "review": "reviews/2026-10-18-basic.md", "review_commit": "<sha>",
      "reviewer": "...", "reviewer_kind": "human", "stages": ["concept", "basic"],
      "checks": [{"id": "pids-valid", "title": "...", "stage": "basic", "status": "pass", "detail": ""}]
    }
  },
  "checks": ["... the gates of the latest sign-off, as 0.3 wrote them"]
}

Each stage has its own entry; signing off basic keeps the concept entry. --write writes nothing and exits 1 when the working tree has changes other than checks/status.json, when any gate fails, when no signer is known (--by, else git config user.name), when the --review record has no valid front matter or is the signer's own (reviewer_kind: self, or the reviewer is the signer) without an approved_by naming someone else, or when a critical or high finding of any review record is open or is closed by a commit that does not exist or is not an ancestor of HEAD, or when the --review record has cheme.review-* warnings. A finding is as severe as the most severe rating any record gives it: a later record that carries it forward without a severity, or with a lower one, does not lower it (close, accept or supersede it instead); an open critical or high row whose id is not <record>#<n> blocks on its own. cheme status and GET /api/status show the same severity. Signed off on a branch other than the default, it says to merge: a stage counts as signed off only once the sign-off is on the default branch. Commit checks/status.json as the last commit of the stage. cheme schema status prints the file's schema.

kind passes when
validate no cheme validate finding at fail_on (error, the default; warning; info) or above, among the rules (ids, * matches; default all), that neither the gate's accept: [{id, reason}] nor the repository-wide accept: list names (errors are never accepted); a gate that cheme.generated-sheet-drift could fail runs the P&ID generators, as cheme validate does, and fails on cheme.generator-failed (a generator that cannot run) as on drift: a rule that matches the drift (cheme.generated-* included) matches it too
files every glob of files matches a file inside the repository
command the shell command, run at the repository root, exits 0
on-sheet the tags are drawn on the sheet (or each of sheets; neither: any sheet), and at least min_count (1) items of each of classes
datasheets every equipment-list item has a valid equipment/datasheets/<TAG>.yaml (cheme schema datasheet; an empty list fails)
layout-model the 3D layout meets each item of require: site, areas, roads, racks, placed (every equipment-list item), separations (declared in the site plan), model-current (layout/plant-model.yaml built from the current inputs), nozzles (no unknown override, no piped nozzle placed by default), mounted (every item above grade on a building or structure level, or on another item), routed (every line of DN min_dn (25) and above, not matched by exclude_lines / exclude_fluids, routed or skipped with a reason), clashes (no model finding of rules, default cheme.layout-clash-*, -headroom and -buried-cover, unaccepted), zones (declared), drawings (a review record of topic, default layout, cites the plot plan and sections (2) sections)
review a review record for the topic (or among the files globs) has valid front matter, a reviewer kind in require_kind (default [human, agent]: a self review does not pass), a reviewed_commit that is an ancestor of HEAD, and (current: true, the default) no file of its scope changed since that commit, committed or not (when git cannot tell, the record is not current)

The template's drawing-clean gate (stage basic) fails while a drawing finding is neither fixed nor accepted; the detail of a failing validate gate counts what failed per rule and names the first finding's id:

- id: drawing-clean
  title: No line through a label or valve, along a pipe or slanted, no crossing, overlapping or
    misplaced label left unaccepted
  stage: basic
  kind: validate
  fail_on: info          # cheme.line-not-orthogonal is an info finding
  rules: [cheme.line-through-label, cheme.line-crossing, cheme.label-overlap,
          cheme.label-far-from-owner, cheme.line-through-valve, cheme.line-on-pipe,
          cheme.line-not-orthogonal]

on-sheet asks the loaded model, not the XML text: a tag is the one the P&ID prints (TagName, the instrument tag AIA-3103 built from category, letters and number, a piping component's first PipingComponentNumber / PositionNumber / PipingComponentName that is not a segment-local C1, a LineNumber), so renaming ids changes nothing and an id or substring that merely looks like the tag does not pass. A class matches itself, its DEXPI superclasses and classes whose name ends with it (SafetyValve matches SpringLoadedGlobeSafetyValve). With connected: true each item must also be connected: equipment with a pipe at a nozzle, an instrument function with a measuring or signal line (or a sensing location), a piping component on a segment with a pipe or the segment's end at it. A failing gate names what is missing, e.g. P-03: AIA-9999 not drawn; FV-3111 drawn but not connected.

A review record is reviews/<YYYY-MM-DD>-<topic>.md starting with YAML front matter (cheme schema review):

---
topic: o2-safety
reviewed_commit: 521473c0d3...      # the commit whose files were reviewed
scope: [pid/P-03.xml, pid/P-04.xml, DESIGN.md]
reviewer: A. Engineer
reviewer_kind: human                # human | agent | self (the author's own review)
---

A passing review gate names the record and its commit, (agent-reviewed only) when the reviewer is not a human, and its evidence (path, reviewed_commit, reviewer, reviewer_kind) goes into checks/status.json. A failing one names the record and why: no front matter, a field missing, the reviewer kind, not an ancestor of HEAD, or changed since <commit>: pid/P-04.xml. cheme validate checks every reviews/*.md (but README.md):

rule level finds
cheme.review-front-matter warning / info front matter that is not YAML or misses a field (warning); a record without front matter (info: no review gate accepts it)
cheme.review-unknown-commit warning a reviewed_commit that is not a commit of the repository
cheme.review-scope-missing warning / info a scope path that does not exist (warning); one written with a leading / (warning) or ./ (info), which is read from the repository root (/pid/P-01.xml as pid/P-01.xml, / and ./ as ., the whole repository)
cheme.review-finding-id warning a finding id that is not <record file stem>#<n> (an earlier record's id under Previous findings), or names no record
cheme.review-finding-status warning / info a status other than open, closed <commit> (an existing commit), accepted: <reason>, superseded by <id>; a severity other than critical, high, medium, low (info); an open finding without a severity (info); an open finding carried forward with no or a lower severity than the critical or high an earlier record gave it (warning: it still counts as that)
cheme.review-previous-missing warning the newest (or an uncommitted) record does not list, under ## Previous findings, a finding of an earlier record of overlapping scope that is still open

A findings table is any Markdown table with an id and a status column. A finding's status is the one in the latest record listing it (by date, then the commit that added the record); records are snapshots, and the next record carries open findings forward with their new status.

- id: hydrocarbon-on-pid
  title: Sump hydrocarbon analyzer and LOX purge drawn
  stage: basic
  kind: on-sheet
  sheet: P-03
  tags: [AIA-3103, FIC-3111, FV-3111]
  classes: [SafetyValve]
  connected: true

status

cheme status [--json] [--rev REV] [--no-gates] summarizes where the repository stands, read-only: the stage; per sheet whether it is drawn, empty (an empty frame), missing (declared in plant.yaml, no file yet) or not-loaded, what it draws and which items the lists assign to it (sheet:) that it does not draw; the next sheet to draw; validation totals by level and rule, and the not-on-pid findings by the sheet their list entry names (unassigned without sheet:); every stage's gates (evaluated, not written; --no-gates skips them, as they may run commands); the sign-offs of checks/status.json, flagged when the signed commit is not on the default branch; the open review findings; and git branch, head, uncommitted paths (null in --json when git cannot tell) and how far the branch is ahead of / behind the default branch. --rev summarizes a revision, checked out in a temporary worktree that is removed afterwards. --json has the keys plant, sheets (id, file, state, drawn, assigned, missing), next_sheet, validation, gates, signoffs, legacy_status (a 0.3 status.json), reviews and git, documented in chemeascode.project.status; a script can take the next sheet to draw from next_sheet. GET /api/status returns it (without the gates).