Skip to content

P&IDs and DEXPI

P&ID sheets are DEXPI 2.0.1 XML files in pid/, loaded through pyDEXPI. cheme validates them (and the lists against them), keeps them in canonical form, renders them and compares them between git revisions. Agents and people edit the XML directly; see Editing and generating sheets for the text helpers and generator scripts.

validate

A P&ID is checked in layers: well-formed XML, a lossless DEXPI 2.0.1 load by pyDEXPI, pyDEXPI's validation rules, then cheme-as-code's document rules (unique ids, an id on every object, every plant item drawn) and model rules:

rule level finds
cheme.segment-ends error a segment's Source/Target Item/Node that is not where its pipes and items start / end, or a node that is not its item's
cheme.dn-mismatch warning a pipe joining two nodes of different numerical DN (reducers too: a reducer changes DN between its own nodes, which no pipe joins)
cheme.label-stale warning a Text whose TextTemplate (with an AttributeRepresentation) renders other than its cached text, or cannot be rendered (an attribute the object does not have)
cheme.duplicate-tag error two piping components (PipingComponentNumber or PositionNumber with a plant-wide tag such as HV-4146; the segment-local C1 of the DEXPI reference does not count) or two instruments (printed tag) with the same tag, on one sheet or across the sheets
cheme.label-literal warning / info a drawn Text of literal text only (no template, or only LiteralText fragments) whose text contradicts (warning) or repeats (info) the data of the object its drawing represents: its DN or its nodes' DNs (DN = 25/50, a nozzle's 80, a reducer's 80/50), a physical quantity with its unit (6 barg against SetPressureHigh), or a string attribute
cheme.template-metadata warning a MetaData value, the <Model name/uri> or the diagram Name that is still template data: pyDEXPI's sample title block (A. P. Prover, Oil & Gas, Inc., Dexpi City, P3.1415, 2016 dates, name="example", DEXPI Example C01, ...) or the placeholders of the template sheet (cheme-template, TEMPLATE)
cheme.titleblock-approval warning ApproverName, ApprovalDateRepresentation or status (ApprovalDescription) "approved" while checks/status.json records no sign-off of the current stage (every recorded gate of the stage passing, or a per-stage entry); CheckerName while reviews/ holds no review record
cheme.titleblock-sheet warning SheetNumber (the sheet id, or its number: 1 for P-01) or DrawingName (neither containing the other) disagreeing with the sheet's entry in plant.yaml
cheme.line-through-label warning a pipe or signal line running through a text box, its own line number or DN label included (it must reach 0.3 mm into the box)
cheme.line-crossing warning two pipes, or a pipe and a signal line, crossing more than 0.5 mm from either line's ends (a branch ending on a line, a tee or a measuring line's tap is no crossing); one finding per pair of lines, with the points
cheme.label-overlap warning two text boxes overlapping by more than 0.25 mm both ways, or a text box that a symbol outline of an object it does not belong to reaches 0.4 mm into, or an equipment text (its tag) that the outline of the item's own symbol reaches 0.4 mm into (a tag inside its vessel is fine, also over the internals drawn inside a column; its nozzles do not count)
cheme.label-far-from-owner warning a label more than 15 mm from the object it represents (a literal note starting with an instrument's tag, e.g. LALL-4108 / TSLL-4114 trip ..., belongs next to that instrument; LALL-4108 is a function of loop LIA-4108); or a label 4 mm or more from its own object and less than half as far from another of the same kind (equipment, valve, instrument, line), so that it reads as the other's
cheme.text-outside-frame warning a drawn text running more than 0.5 mm outside the inside of the sheet's border
cheme.title-text-overflow warning a title block text more than 2 mm wider than its cell (e.g. a long sheet title from plant.yaml)
cheme.line-through-symbol warning a pipe or signal line running through an instrument bubble, or the body of an equipment item, it does not end on (a line ending at a nozzle, or with an end inside the body at an internal or a tap, does not count)
cheme.line-through-valve warning a signal or measuring line running through the body of a valve, a pipe of another segment running through a valve, or any line running through an actuator's head (a control valve's signal drawn up through the valve to an actuator on top, an actuator drawn up the pipe on a vertical valve); one finding per line and valve, the actuator's valve named (from its ActuatingSystem). The pipes of the segment the valve sits on, the actuator's stem, a line with an end inside the outline (a signal line into the actuator's centre) and a line meeting the outline within 1 mm of its end (a signal line ending on the actuator's top) do not count; the line must run 0.3 mm into the outline's box
cheme.line-on-pipe warning a signal or measuring line drawn on top of a pipe: a horizontal or vertical stretch within 0.3 mm of a pipe's stretch, along it for more than 1 mm (a signal line run down the pipe to an actuator drawn on it); one finding per line and piping line
cheme.line-not-orthogonal info a signal or measuring line with a slanted stretch, 2 mm or longer and more than 1 degree off horizontal and vertical (a measuring line drawn straight from its tap to the bubble); the short legs of a hop over another line do not count
cheme.drawing-not-checked info the SVG export failed, so the drawing rules did not run

cheme.segment-ends reports what pyDEXPI's Mandatory rules pydexpi_segment_connectivity and pydexpi_segment_reference_nodes_and_items_match find, once per segment, on the line of the segment's References; the pyDEXPI rule ids are named in the message, and are not reported separately. The ends are read from how the pipes connect, not from their order in the file (DEXPI 2.0.1 does not order a segment's Connections; pipes listed out of flow order are only pyDEXPI's Recommended pydexpi_segment_linear_order). A segment may end at another node of its end item when that item is one of the segment's own Items: the DEXPI reference P&ID ends a segment at the outlet of its last valve or at the branch of its tee, where the next segment starts. A template that stops short of its text (the reference labels valves 66KL21-80 from PipingComponentName and -) is not stale while the text starts with what the template renders. Labels made of literal text only (e.g. a hand-written DN = 25/50) show no attribute, so label-stale cannot check them; cheme.label-literal compares the values they spell out with the object's data instead. DNs are compared as a set (DN = 25/50 matches nodes DN 25 and DN 50 in either order); a text that is only numbers (80, 80/50) is read as DNs when the object has any. The info findings say which labels must be changed by hand with the data (-q hides them).

Drawing geometry

The drawing rules check the sheet as cheme render draws it: pyDEXPI's SVG export, with every shape's transform applied, so they see what a reader of the rendered sheet sees. Text boxes are estimated from the font size and the characters (Arial widths, the export's fallback font), placed by text-anchor and dominant-baseline (a text-bottom label stands above its y, a middle one is centred on it) and turned with the text. The sheet's border (zone numbers) and title block are not checked. Each finding has the sheet, the ids, a box in sheet mm and a crop (the box plus 10 mm, on the sheet) ready for cheme render --crop; the text output adds [crop X0,Y0,X1,Y1] [id ...]. Before the totals, cheme validate prints one line per sheet with the counts, which a review record quotes:

pid/P-04.xml:1952: warning cheme.line-crossing [Line_GOX4003_S1_P2, Inst_UIC4103_ML]: pipe GOX-4003 (Line_GOX4003_S1_P2) crosses signal line Inst_UIC4103_ML at (121.4,80.0), away from any node -- route one line around the other; ... [crop 110.3,69,132.4,91] [id pid/P-04.xml#line-crossing:Line_GOX4003_S1_P2+Inst_UIC4103_ML]
drawing pid/P-04.xml: 1 line-through-label, 1 line-crossing, 3 label-overlap, 11 label-far-from-owner

They take well under a second per sheet. On the DEXPI reference P&ID they find its three line crossings and two labels its own lines touch.

Tags named in texts

Literal texts (no template) often name other objects: off-page texts (from P-03 LIN-3001) and trip notes (LALL-4108 / TSLL-4114 trip P-4102A/B, XV-4137). With every sheet validated, a cross-check reads their tag-like tokens -- letters, -, digits, e.g. P-4102A, with A/B expanded -- that have the shape (letters and number of digits) of a tag in the lists or sheets, or of an instrument loop:

rule level finds
cheme.label-tag-unknown warning a token that no list entry, drawn tag (TagName, LineNumber, valve number, printed instrument tag) or instrument loop has (LALL-4108 is known when LIA-4108 is), or a sheet number plant.yaml does not declare
cheme.offpage-mismatch warning an off-page connector whose text names a line other than its own that differs only in its code (LN-3001 on LIN-3001), is not on the named sheet, or does not join the connector's line there (a branch's from P-04 GAN-4002 is fine); or names a sheet that does not carry the connector's line
cheme.label-tag info what each such text names and what it is (K-4101 (equipment), LALL-4108 (instrument loop)), as no template follows these names

A note that names an external tag on purpose is accepted like any other finding.

Accepting a finding

Each finding has an id, <file>#<rule without cheme.>:<subject> (--json gives it as id; the drawing rules also print it), e.g. pid/P-03.xml#line-crossing:Line_GOX3001_S1_P1+Line_LOX3001_S1_P1. checks/checks.yaml can accept findings, each with a reason; * and ? match any text:

accept:
  - id: "pid/P-03.xml#line-crossing:Line_GOX3001_S1_P1+Line_LOX3001_S1_P1"
    reason: "D-023: no route around the GOX riser (J. Smith, design review 2026-10-20)"

cheme validate then reports the finding as info, (accepted: <reason>), the drawing line counts it as (+1 accepted), and no gate fails on it. Only warnings and infos can be accepted: an entry (top-level or a gate's) that matches an error leaves it an error, so validate, the pre-commit hook, CI and the gates still fail, and is reported as cheme.accept-error (warning). An entry, top-level or a gate's, that names no finding is cheme.accept-unused (info), so stale entries get removed. The text output prints [id ...] on every finding that can be accepted.

YAML lists are checked against their schemas, then compared with the P&IDs: equipment tags and classes (cheme.equipment-*), the optional dimensions of an equipment item (overall diameter, height or length, width, head, support, weight; source, assumed and fingerprint of sizes a tool calculated, such as cheme process stack, of which the datasheets carry source and assumed) against the sizes its P&ID states where it states them (NominalCapacityVolume, CylinderLength, TubeLength, a chamber's InsideDiameter or NominalDiameter, PlateHeight/PlateWidth: cheme.equipment-dimension-mismatch, warning), line numbers against PipingNetworkSystem LineNumbers and their DN, per segment when the line lists segments (cheme.line-not-listed, line-not-on-pid, line-dn-mismatch), their from/to against the ends of the drawn segments (cheme.line-endpoint-mismatch, warning: a listed tag drawn on a loaded sheet that the line does not reach while it ends at a nozzle of equipment the list does not name; BL, ATM, line numbers and areas are not compared, and nor is an end beyond a tee or an off-page connector), and instrument tags as printed, e.g. PICSA-4712.02 (cheme.instrument-not-listed, instrument-not-on-pid), and, when the optional piping/special-items.yaml (kind piping_items: tag, type, line, sheet, set_pressure_barg, relief_case) exists, tagged piping components by their printed tag (the first of PipingComponentNumber, PositionNumber, PipingComponentName that is not a segment-local number such as the DEXPI reference's C1; a component with only C1 has no tag): cheme.piping-item-not-on-pid, piping-item-not-listed (only safety valves and fittings -- SafetyValveOrFitting subclasses -- and silencers must be listed), piping-item-type-mismatch (the DEXPI class or a superclass), piping-item-line-mismatch. An item of that list may be placed in the layout like equipment. Interlocks in control/cause-effect.yaml (kind interlocks: id, cause: {tag, condition}, effects: [{tag, action}] with action trip, stop, start, close, open, depressurize or alarm, sheet, sil: 1 to 4, or 0 / a) are checked against the lists and sheets: cheme.interlock-unknown-tag (warning, a cause or effect tag no list holds and no sheet draws), cheme.interlock-duplicate, cheme.interlock-duplicate-effect (warning, two effects of one interlock on one tag; the matrix shows both actions), and cheme.instrument-no-interlock for a trip instrument -- function letters with HH or LL (TSLL-4114), or instrument-list notes saying it trips -- that is in no interlock, as its cause or in its logic (info up to the concept stage, a warning from basic on). cheme cause-effect [-o FILE] writes the matrix, causes by effect tags, to build/cause-effect.html and .csv. An entry listed twice is cheme.line-duplicate / instrument-duplicate / equipment-duplicate, and each entry is checked on its own line. Numbers, tags and DNs need no quotes (number: 47121 is read as '47121') and surrounding blanks are ignored.

Sheets in plant.yaml may be status: planned (the default is drafted): declared, not drawn yet. A planned sheet without its file is cheme.sheet-planned (info) instead of cheme.missing-sheet (error); once the file exists it is validated like any other. Missing from the P&IDs is judged per sheet: a listed item whose sheet: names a planned sheet, or one that is missing or did not load, is held back, and so is an item without sheet: while any sheet is missing or did not load, or while plant.yaml stage is the first stage (after the first stage a planned sheet does not hold back items without sheet:). Held items give one info line per list, rule and reason (all their ids in ids); an item assigned to a drafted, loaded sheet stays a warning. The compared: line names planned and not loaded sheets. --ignore RULE[,RULE] drops rules from the report and --only RULE[,RULE] keeps only those (ids or globs such as cheme.*-not-on-pid, repeatable); both apply to --json, --quiet and --fail-on. A pattern that matches cheme.generated-sheet-drift (cheme.generated-* included) also matches cheme.generator-failed, as a gate's rules do. The text output ends with a compared: line naming the sheets, lists and cross-checks run (and those skipped, with the reason); --json has the same under compared. Each finding is one line an agent can act on:

pid/P-01.xml:1043: error dexpi.load [Pipe_7]: Reference 'Nozzle_9' for attribute 'targetNode' not found ...
equipment/equipment-list.yaml:14: warning cheme.equipment-not-on-pid [P-102B]: P-102B is in the equipment list and on no P&ID -- draw it, or remove it from the list

Equipment datasheets, equipment/datasheets/<TAG>.yaml (kind datasheet: service, operating and design conditions, materials, sizing basis, values marked assumed), are checked against their schema and the equipment list: cheme.datasheet-not-listed (warning, a tag not in the list), cheme.datasheet-mismatch (warning, design pressure, temperature or type differ from the list entry) and cheme.datasheet-name (warning, a file not named after its tag). cheme datasheets draft [--tag T] drafts the missing ones from the equipment list, the P&ID nozzles and the flowsheet streams linked to each item: every value with its source, unknowns TBD, draft: true (the datasheets gate, a detail gate, does not count drafts). cheme datasheets fill [--tag T] [--param name=value] [--dry-run] calculates the sizes a draft leaves TBD and writes each with its basis (calculation, stream ids, nozzles, pipes, cited tables; assumed: true when an input is an assumption), never over a value it did not write: exchanger duty (property adapter), LMTD with F and area from a given or typical U (Sinnott, C&R Vol. 6, Table 12.1); pump head from the stream pressures, the nozzle elevations and the Darcy-Weisbach losses along the routed lines, NPSH available and power; compressor stages, power and intercooler duties; Fenske-Underwood-Gilliland stages; vessel volume and residence time. A value calculated from the streams carries their fingerprint, and validate reports it as cheme.datasheet-stale once they change; the sheet's operating names the case and the streams it was filled from, which validate resolves (cheme.contract-ref). See docs/framework/sizing.md and the skill datasheets.

sheet

cheme sheet new <ID> [--title T] [--file F] [--revision R] [--force] writes pid/<ID>.xml from the template frame (.claude/skills/dexpi-xml/snippets/empty-sheet.xml, generated without pyDEXPI's sample data) and fills its title block from plant.yaml: Model name (plant name) and uri, diagram Name (the sheet id), DrawingNumber (<project.number>-<ID>), DrawingName (title), SheetNumber, project number and name, client (EnterpriseName), site, revision (project.revision, default A), creation and modification dates, status "for review"; approver, approval date and checker stay empty. It adds the sheet to plant.yaml sheets: (keeping comments; with an empty list the sheets already in pid/ are listed too) or turns a planned entry drafted. The optional project: block of plant.yaml is {number, name, client, site, revision} (cheme schema plant).

cheme sheet meta <ID> Key=Value ... sets title-block values: a MetaData property (RevisionNumber, ApproverName, ...), a short name (Revision, Status, Title, Approver, Checker, Creator, Client, Site, Project, Number, Sheet, Date, ...), or ModelName, ModelUri, DiagramName. Each title-block text showing a changed attribute is re-rendered with pyDEXPI's label toolkit (what cheme.label-stale checks), and the file is written in fmt form. Without pairs it prints the title block.

fmt

Gives every <Object> without an id a deterministic, readable one (Tank1_Nozzles2, Pipe3_Drawing1) -- pyDEXPI would otherwise mint a random id on every load -- and writes the document back through pyDEXPI's serializer in canonical order, keeping the <Model>'s name and uri. It refuses a document whose load reports a loss -- a loader error, or a warning that content "will be ignored", such as a Data property DEXPI does not define -- and names it; the edit commands then write their change unformatted and say so. XML comments and processing instructions have no place in the DEXPI model and are dropped, each one reported with its line (by cheme fmt, cheme run --fmt, the edit commands, cheme sheet meta and the Python API's Sheet.save(), as a warning).

render

cheme render [PATHS] [--png] [--scale 4] [--crop X0,Y0,X1,Y1] [--rev REV] [-o OUT] draws each sheet with pyDEXPI's SVG export to build/<sheet>.svg (build/<sheet>@<rev>.svg with --rev, which renders the sheet as committed at that revision). With --crop the name ends in the crop, build/<sheet>[@<rev>]_crop-X0-Y0-X1-Y1.svg|png (corners ordered), so a crop never overwrites the full sheet or another crop. --png writes a PNG instead, --scale pixels per sheet millimetre (default 4); it needs the png extra and says so when it is missing. --crop limits the output, SVG or PNG, to a region of the sheet given in millimetres from the top-left corner, Y down. These are the sheet's own XML coordinates (the export does not flip Y), so a Position read from the XML can be used directly. Each drawn object's <g> carries the id of the object it represents. A PNG larger than 100 Mpx is refused (exit 2). A sheet that does not load or draw is reported as <path>: cannot render: <reason>; the other sheets are still rendered and the exit code is 1.

--findings draws the drawing-geometry findings (see validate) on the output as numbered red frames with the rule's name; those checks/checks.yaml's accept: accepts are grey and captioned (accepted). --tiles CxR (e.g. 4x3) renders the whole viewBox, which can include content left of or above the frame, as C columns by R rows of tiles that overlap by 10 %, build/<sheet>[@<rev>]_tile-<row letter><column>.svg|png (_tile-B3 is the second row, third column), at --scale 8 unless given, and writes the index build/<sheet>[@<rev>]_tiles.json (each tile's crop, its file and, with --findings, the findings it shows: number, rule, id and, when accepted, the reason); it prints one line per tile with --crop X0,Y0,X1,Y1 --scale S, which reproduces the tile. --tiles auto picks the grid so a tile is at most 1600 x 1200 px (2 mm text is 16 px at scale 8): 3x3 for an A3 sheet. --tiles with --crop or -o, or a malformed grid, exits 2. Tiles make a visual review cover the whole sheet; they do not replace the drawing rules.

--emphasise-inline draws the valves, inline fittings (reducers, silencers, ...) and the actuators 1.5 times larger about their insertion points, with lines twice as wide, so they can be read on a full-sheet render (a valve at the usual scale 0.4 is 4 x 2 mm). It is a review aid: the XML, the pipes and the labels are unchanged, and the output is named ..._inline.svg|png (build/<sheet>_inline_tile-A1.png with --tiles) so it never stands in for the drawing.

diff

cheme diff [OLD=HEAD] [NEW=working tree] [--path P] [--html OUT] [--json] [--list [--verbose|--flat]] loads two revisions, compares them with pyDEXPI's semantic diff, and writes an HTML page with both sheets side by side: added items green, removed red, modified orange, plus the change list and the review-text diff. It prints the counts per sheet and the report's path. Without --path it compares every pid/*.xml on either side, so a deleted or added sheet is reported too. An unknown revision exits 2 with unknown revision (as does render --rev). A sheet that does not load or draw on either side is named on stderr with a one-line reason (<sheet>: cannot compare: <rev> does not load: <reason>, or cannot draw <rev>: <reason>); the other sheets are still compared, the output (counts, list, JSON, HTML) is still written, and the exit code is 1.

  • --list prints, per changed sheet, a <sheet>: line (with (sheet added) or (sheet removed) for a sheet on one side only) and one line per plant item a change belongs to: <kind> <class> <id> [tag]: N added, M removed, K modified (, L moved when any moved), e.g. removed TubularHeatExchanger TubularHeatExchanger1 [H1008]: 0 added, 132 removed, 0 modified. <kind> is what happened to the item itself, or changed when only its parts or its drawing did. A part (nozzle, node, chamber) belongs to its item, a pipe or segment to its line (piping network system), a valve or tee is an item of its own, an instrument function's parts belong to it, and a drawing element to the item of the object it draws (the title block and frame to the plant model); changes that belong to none are counted under changed (no plant item). Removed items come first, then added, modified and changed.
  • --list --verbose adds, under each item, its changes one per line, indented, as --flat prints them.
  • --list --flat prints one line per changed object, as 0.2 did: <kind> <class> <id> [tag] (<domain>) <attrs> -> drawn as <owner|not drawn>, e.g. modified GlobeValve GlobeValve2 [C1A] (conceptual) pipingComponentNumber -> drawn as GlobeValve2. The owner is the drawn object (a <g id> of the SVG) the change shows on. --flat or --verbose without --list exits 2.
  • --json prints a JSON array with one object per changed sheet, the shape of GET /api/diff without old_svg/new_svg: path, old_rev, new_rev, status, changes (kind, object_type, object_id, tag, domain, detail, item), old_highlight, new_highlight, old_owner, new_owner, items (each plant item a change names: class, tag), projection_diff, error (why the sheet could not be compared or a side not drawn; "" when all went well). No change prints [].

With --json or --list the HTML report is written only when --html names a file.

show

cheme show ID_OR_TAG [--sheet FILE] finds an object in every sheet (or in one) by its XML id or a tag -- TagName, the printed instrument tag (PICSA-4712.02; also without the hyphen or with a space), LineNumber, PipingComponentNumber/PipingComponentName, a function or segment number -- and prints where it is: file:start-end of its <Object> block, its class and tag, its owner chain, the file:start-end of the RepresentationGroup(s) that draw it with the region their points span (at X0,Y0,X1,Y1 mm, sheet mm, Y down: symbol insertion points, line and label positions; add a margin for render --crop) or the drawn object it is part of, the ids it references and the references to it, each with a line number. An object's parts (nozzles, chambers, instrument functions) are listed with what connects to them. For a piping network system (a line number) or segment it also lists the segments, their end items and nodes, the pipes with both ends, and every item's nodes with their DN.

For equipment (or a nozzle), connections: lists each nozzle with the segment and line attached to it and what is at the segment's other end (another equipment's nozzle, a tee on another segment, its own end item, or open), or the instrument sensing at it, or nothing. only for <tag>: lists what exists only for that equipment and dangles without it: the segments reached from its nozzles through items that are not tees, when they reach no other equipment, no off-page connector and no tee branching off them; lines all of whose segments are among them; and the branch tee on another segment they start from. Items on those segments that are part of an instrument chain are named with the cheme show call that prints it.

instrumentation: follows the instrument chains an object is part of, from any member: an instrument function (ProcessInstrumentationFunction) or its parts, an InstrumentationLoopFunction, an ActuatingSystem (or its actuator), a valve its OperatedValveReference names, or an object (or part, such as a nozzle) a ProcessSignalGeneratingFunction senses at. Per function it prints the loop(s) that list it (or none), what it senses and where, its measuring and signal lines (Source -> Target, with their drawings), and each ActuatingFunction with its ActuatingSystem, ControlledActuator and operated valve, each with its block and drawing. Functions joined by a signal line (a cascade) are listed together.

It reads the XML only, so it works on a sheet pyDEXPI cannot load. Nothing found: exit 1 and close ids and tags.

$ cheme show T4750
pid/P-01.xml:3412-3718 Tank Tank1 [T4750]
  owner: PlantModel1 (PlantModel) pid/P-01.xml:8 < Model_EngineeringModel1 (EngineeringModel) pid/P-01.xml:6
  drawn by: Tank1_Drawing1 (RepresentationGroup) pid/P-01.xml:25152-28120 at 171,69.3138,280,291 mm
  references (0):
  referenced by (4):
    pid/P-01.xml:25233 Tank1_Drawing1_Groups2_Elements1_Template1_Fragments1 (AttributeRepresentation) Object
    ...
  parts (9):
    Nozzles: Nozzle5 (ProcessNozzle N1) pid/P-01.xml:3414-3434 <- Pipe4 TargetItem, PipingNetworkSegment3 TargetItem
    ...
  connections:
    Nozzle5 (ProcessNozzle N1): segment PipingNetworkSegment3 (PipingNetworkSegment S1) of line 47123 pid/P-01.xml:449-535; other end: PlateHeatExchanger1 (PlateHeatExchanger H1007) nozzle Nozzle4 (ProcessNozzle N2)
    Nozzle18 (ProcessNozzle N3): nothing
    ...
  only for T4750 (these dangle without it):
    segment PipingNetworkSegment8 (PipingNetworkSegment S2) of line 47125 pid/P-01.xml:979-1016
    ...
    line PipingNetworkSystem5 (PipingNetworkSystem 47125) pid/P-01.xml:889-1039: all its segments
    branch tee PipeTee1 (PipeTee C3) pid/P-01.xml:1231-1285 on PipingNetworkSegment11 (PipingNetworkSegment S3) of line 47126 pid/P-01.xml:1221-1315: its branch serves only this
  instrumentation (1 function(s)):
    ProcessInstrumentationFunction4 (ProcessInstrumentationFunction TICSA-4750.03) pid/P-01.xml:2577-2616 drawn by ProcessInstrumentationFunction4_Drawing1 pid/P-01.xml:16160-16448 at 197.25,57.25,340.5,64.75 mm
      loop: PlantModel1_InstrumentationLoopFunctions4 (InstrumentationLoopFunction 4750.03) pid/P-01.xml:306-311
      actuates: ActuatingFunction3 (ActuatingFunction TV4750.03) pid/P-01.xml:2579-2584
        system: ActuatingSystem3 (ActuatingSystem TV4750.03) pid/P-01.xml:257-285 drawn by ActuatingSystem3_Drawing1 pid/P-01.xml:9941-10111 at 337.5,104.9,340.5,115.5 mm
        ...
        valve: GlobeValve3 (GlobeValve C1) pid/P-01.xml:2348-2401 (OperatedValveReference pid/P-01.xml:275)
      senses: ProcessSignalGeneratingFunction3 (ProcessSignalGeneratingFunction TT4750.03) pid/P-01.xml:2587-2592 at Nozzle17 (InstrumentNozzle N7) pid/P-01.xml:3519
      ...

git diff

cheme git-setup registers a textconv driver for pid/*.xml, so git diff, git log -p and git show print P&IDs as pyDEXPI's review text (one block per object, attributes sorted) instead of XML. Objects are written Class@<id> with the id as written in the file (PipeTee@PipeTee_47123_BypassIn); an object without an id in the file (before cheme fmt) keeps pyDEXPI's stable hash token. The driver and the pre-commit hook call cheme by the absolute path it had when git-setup ran, so they work from clients without cheme on PATH (rerun git-setup after moving the installation). The driver never fails a git command: a file it cannot load is shown as its XML under a (cheme textconv could not load ...) line.