In this section

Purpose

A machine is one or more capability strands wired into data-flow graphs and identified together as one anchor-realized value. Machine notation is the textual encoding of such a machine.

It is used for:

  1. Stable identifiers: A canonical string by which two machine values can be compared, stored, and looked up.
  2. Round-trip serialization: Persist a machine to text, parse it back, get an equivalent machine.
  3. Human authoring: A compact, readable form a user can write or edit directly.

This document describes the language. The dispatch and ranking semantics for individual caps are defined in Dispatch and Ranking; machine notation builds on top of them.


Layers

The system distinguishes three layers:

Layer Type Anchor commitment? Cap-arg assignment? Inputs from?
Planner Strand no no media URN patterns
Anchored Machine (this document) yes yes (resolved) anchor URNs
Concrete MachineRun yes yes actual input files

A planner Strand is a linear sequence of cap steps with no commitment to which data positions are inputs and which are intermediate. A Machine is the anchor-realized form: every internal data position is identified, every cap arg is bound to its source position, and the input/output anchors are committed. A MachineRun binds those anchors to actual input files at execution time.


Surface forms

Two equally valid statement forms exist. Both can be freely mixed in the same program.

Bracketed

Each statement is wrapped in [...]. Inside a bracketed statement the cap URN reads until the closing ], so line breaks between statements are insignificant, and multiple statements may share a line:

[extract cap:in="media:document;ext=pdf";extract;out="media:txt;enc=utf-8"]
[doc -> extract -> text]

Line-based

One statement per line, no brackets. In this form a cap URN reads until the newline, so line breaks terminate statements:

extract cap:in="media:document;ext=pdf";extract;out="media:txt;enc=utf-8"
doc -> extract -> text

The bracketed form is the canonical serialization (one-line, suitable for use as an identifier). The line-based form is intended for human reading and editing.


Statement kinds

A program is a sequence of two kinds of statements: headers and wirings.

Headers

A header binds an alias to something that can stand in a wiring’s step position: a cap, or another machine.

<alias> <cap-urn>
<alias> machine "<reference>"

Examples:

extract cap:in="media:document;ext=pdf";extract;out="media:txt;enc=utf-8"
embed cap:in="media:enc=utf-8";embed;out="media:embedding-vector;fmt=json;record"
clean machine "clean.machine"

The cap URN is parsed by CapUrn::from_string (see Cap URN structure). A machine reference is an opaque string the host resolves (see Machines in step position). A header is consumed by the wirings that name its alias.

Wirings

A wiring is one invocation: what it is given, the step, and what is taken from it.

<sources> -> <step> -> <targets>
Part Forms
sources one node alias; or a group ( member, ... ), which may be empty: ()
member node, or name = node, or name = constant
step a header alias or a registered cap alias, optionally followed by a policy: fetch{retry=3}
targets one node alias; or a group ( target, ... ), which may be empty: ()
target node or name = node

Examples:

doc -> extract -> text
(thumbnail, model_spec) -> describe -> description
(path, benchmark=bench) -> compare -> result
(prices, window=20, statistic="mean") -> rolling -> smooth
(reference="fred/UNRATE") -> fetch{retry=3} -> series
() -> now -> stamp
report -> publish -> ()
obs -> regress -> (fit, residuals=res)
item -> route -> (yes=kept, no=dropped)
  • A node is a data position. Two references to one node name are one position.
  • A name is the name of one of the step’s args (in sources) or outputs (in targets), as its definition declares it. Names are labels local to that definition: they select an arg or an output and carry no other meaning. Nothing about an arg’s or an output’s media URN can be read from its name, and the reverse.
  • A constant is a JSON value: a string, a number, true, false, null, an array or an object. A constant is always named. true, false and null are therefore reserved: no node may bear one of those names. Text that is JSON by its grammar and still says no value is refused as a parse error: a number no float holds (1e999), and a string with an escape for half of a surrogate pair without the other half ("\ud800"). The same holds for the reference of a machine header.
  • There is no per-item marker and no gather marker. Both are derived from cardinality (see Iteration).

The grammar is defined in capdag-rs/src/machine/machine.pest. Whitespace and line breaks between statements are insignificant in the bracketed form; in the line-based form, line breaks terminate statements.


Aliases and node names

Aliases and node names are opaque labels. The parser does not extract any meaning from them beyond their role as program-local names: two references to the same name refer to the same thing, two different names refer to different things. Conventions like extract, doc, text are for the user’s benefit only, and the canonical machine produced by the parser does not embed them.

The serializer produces canonical aliases as edge_<i> and canonical node names as n<i>, where <i> is a global counter across all strands. Two semantically-equivalent machines produce byte-identical canonical notation.

The only constraint on alias and node names is the lexical form ((ALPHA | "_") (ALNUM | "_" | "-")*) and that no node name may collide with a header alias.

Fabric cap aliases (auto-resolved cap-position names)

A wiring’s cap position (loop_cap) normally names a local header. If the name is not defined by a local header, it is taken to be a fabric cap alias and resolved through the registry: an identifier with no local definition is resolved as an alias before it is declared undefined. The resolved cap URN is used as if a header had defined it:

[doc -> csv2json -> out]

Here csv2json has no header, so it is resolved as a cap alias to its target cap URN (e.g. cap:convert-format;in="media:fmt=csv;list;record";out="media:fmt=json;list;record"). Rules:

  • Local shadows registry. A name defined by a local header is used as written; the registry alias of the same name is never consulted.
  • Cap position requires a cap. An alias whose target is a media URN in cap position is a hard error (MachineSyntaxError::AliasNotACap). Media URNs are implicit in notation, so media aliases are never resolved in a wiring.
  • Still undefined if unknown. A cap-position name that is neither a local header nor a registered cap alias raises MachineSyntaxError::UndefinedAlias, so the alias mechanism never masks a genuinely undefined name.
  • Warm-up. The synchronous resolver consults the in-memory alias cache; the async entry point (parse_machine_with_node_names_async) pre-warms the alias and its target cap before the sync resolver runs. Aliases resolve to canonical cap URNs, so canonical serialization is unaffected (the serializer always emits resolved URNs, never alias names).

Strands and connected components

A Machine consists of one or more MachineStrands. A strand is a maximal connected sub-graph of the wiring graph: two wirings belong to the same strand iff there exists a path through the wiring set, hopping along shared node names, that connects them. The parser computes connected components on the wirings via union-find and produces one MachineStrand per component.

[a -> cap_x -> b]
[b -> cap_y -> c]
[d -> cap_z -> e]

The first two wirings share node b → one strand (a → cap_x → b → cap_y → c). The third wiring shares no node names with the first two → a separate strand (d → cap_z → e).

Crossings are internal to a single strand. What looks like “two strands crossing” via a shared node is, by definition, just one strand with a shared internal node. Two strands within the same machine never share node names.

Strand declaration order matters: strands are listed in the resulting Machine in first-appearance order (the strand whose earliest wiring appears first in the textual input comes first). Strict equivalence compares strands position-by-position.


Binding sources to args

Each cap definition lists its input arguments in args. An arg has:

  • a media_urn, which identifies it: unique within the cap (RULE1), the key of every binding, and the URN its stream is delivered under;
  • a name: required, unique within the cap, a label by which a wiring may select it;
  • the type it accepts: the URN a source must conform to. For the main input this is its stdin source URN (which is the cap URN’s in=); for any other arg it is accepts when the definition gives one, and the arg’s own media_urn otherwise;
  • is_sequence, required, and optionally a default_value.

Every arg can be bound by a wiring. How an arg is supplied on a command line (stdin, position, cli_flag) plays no part in this: those are the CLI’s channels, not the machine’s.

An arg a wiring leaves unbound takes its value at run time: the run’s per-step value for it, else the per-cap setting, else the definition’s default (see Execution). An arg a wiring binds is bound: no run-time value may be supplied for it.

The algorithm

  1. Named members bind first. name = node binds that node to the arg of that name. The node’s URN must conform to the arg’s accepted type. name = constant binds the constant; it must satisfy the validation the arg’s media definition declares. A name may be repeated only for a sequence arg; each repetition adds one member to the gather, in the order written.
  2. Unnamed members are matched by type over the args no named member bound: a minimum-cost one-to-one assignment with a uniqueness check.
    • Cost. For source URN s and accepted type a: if s.conforms_to(a) is false the pair is impossible; otherwise the cost is spec(s) - spec(a), the specificity distance.
    • Uniqueness. The minimum-cost assignment must be unique. Two assignments tied at the minimum fail with AmbiguousMachineNotation, which names the members that have to be named. Position in the group is never a tiebreaker.
    • Gather. When no one-to-one assignment exists, a sequence arg absorbs every unnamed member that conforms to it and can go nowhere else, in the order written.
  3. The main input must be bound unless the cap’s in= is media:void, in which case it has none. This constrains step 2: where an unnamed member can feed the main input, a way of placing the members that leaves the main input out is not an assignment, and does not tie with one that binds it. So series -> compare -> result resolves even though compare has a second arg that accepts a series: the one member is the main input. It is a constraint, not a preference: (a, b) -> compare -> result, where both are series, has two assignments that bind the main input, and is ambiguous.

The result is a list of bindings (arg media URN, source), where a source is a node or a constant, sorted by arg media URN and, within one arg, in gather order.

Naming a member is never required where step 2 has one answer. (thumbnail, model_spec) -> describe -> text needs no names: the two members conform to different args. (path, bench) -> compare -> result, where both are series and compare has two args that accept a series, is ambiguous, and (path, benchmark=bench) is how it is said.

Outputs

A cap definition lists its outputs. An output has a media_urn that identifies it (unique within the cap), a name (required, unique within the cap), is_sequence, streaming, whether it is optional, and what it carries: its own URN, or the main input’s runtime type unchanged. Exactly one output is the main output: the one whose URN is the cap URN’s out=; its type follows the cap URN’s effect (Cap URN structure).

In targets, an unnamed node takes the main output and name = node takes the output of that name. At most one target is unnamed; an output is taken at most once per wiring; an output of media:void cannot be taken. An output no target takes is discarded. () takes none.

A target node’s URN is what its output carries.

The URN of a node

Every node has one media URN, derived from the wirings that touch it. Where a member stands in its group never decides it.

  • A node some step produces has the URN of what that output carries.
  • A node nothing produces is an input of the machine, and takes its URN from where it is consumed:
    • as a named member, the accepted type of the arg it names;
    • as an unnamed member, the accepted type of the step’s main input. A wiring may hold at most one unnamed member that nothing produces and that no other wiring gives a URN, and only while no named member has bound the main input. Otherwise the wiring fails with UntypedMachineInput, naming the members that have to be named: nothing says which arg an unnamed input of the machine is for, except that a step’s one data input is its main input.
  • A step’s main input also refines what feeds it. When every unnamed member of a wiring already has a URN, and exactly one of them is on the specialization chain of the type the main input accepts (is_comparable), that member’s node is given that type as well. A consumer that asks for a tighter type than its producer declared states the type of what flows there; the run holds the producer to it, since what it emits must satisfy the arg it is delivered to. Where no member or several are on that chain, nothing is refined and the members are matched as they are.
  • A node given several URNs (by what produces it, by a name, by a main input) must be given URNs on one specialization chain; it has the most specific of them. URNs that are not on one chain fail with NodeMediaConflict.

Each of these is read from the wirings as written, before any of it is applied to another.

The order of the wirings in the text does not change any node’s URN.

Resolution requires the cap registry to look up each cap’s definition. All Machine constructors (from_strand, from_strands, from_string) take &FabricRegistry.


Iteration

Whether an invocation runs once or once per item is derived from cardinality, never written.

A sequence bound to an arg that takes one value is mapped: the step runs once per item. A map is where one or more sequences are taken item by item. Every step runs in exactly one place: the machine itself, or one map; and a map may sit inside another.

  • Steps that map the same sequences in the same place run in one map. Run i of each is given item i, and a step given what two of them produced is given both for the same item. Mapping one sequence from two wirings never makes two independent passes over it.
  • When several members of one wiring are mapped, they are zipped: run i is given item i of each. Their lengths must be equal; unequal lengths fail the run, naming what gave each and the lengths, before the step runs on any item. There is no implicit cross product: a step inside a map that is also given, on an arg that takes one value, a sequence the map does not iterate is not run for every pair. It runs where both sequences are, in the map that zips them.
  • A member that is the item of a map the step is inside is given as that run’s item. A scalar from outside the map, a constant, and a sequence bound to an arg that takes a sequence are given whole to every run. That includes the sequence the map iterates: a step may take the item on one arg and every item on another.
  • Each taken output of a mapped step, read from outside the map, is the sequence of that output over the runs, in item order.
  • A sequence produced inside a map and bound to an arg that takes one value opens a map inside that map. Read from outside both, what the inner map produced is one flat sequence, in order: cardinality has one level.
  • A step that takes a sequence and is given what a map produced folds it, and a fold closes one level: it runs in the place around that map. Given what an inner map produced, it runs once per run of the outer map. So documents, each split into pages, each page read, the readings joined, give one joined value per document.

Where a step runs does not depend on the order the wirings are written in. A set of wirings whose maps would have to contain each other is refused as CyclicMachineStrand.

ForEach and Collect are how a plan realizes this (Planner); neither appears in notation.


Absent values and conditional flow

An output declared optional may not fire: the step ends without producing it. Its node is then absent for that run (or for that item of a map). Absent is not empty and is not an error.

  • A step with a required arg bound to an absent node is skipped: it does not run, and every output it would have produced is absent.
  • An arg that is not required and is bound to an absent node takes its default, or is omitted when it has none.
  • A gather drops its absent members. With none left it delivers an empty sequence.
  • In a map, an item whose step is skipped contributes nothing to the collected sequences. A later zip against the unfiltered sequence therefore fails on length.
  • A machine output that is absent at the end of a run is reported as not produced.

A step whose policy tolerates failure (Step policy) makes its outputs absent in the same sense when it fails.


Step policy

A wiring’s step may carry a policy in braces. A step with none fails the run when it fails.

ref -> fetch{retry=3} -> series
item -> parse{on_error=absent} -> row
(a, b=c) -> join{retry=2, on_error=absent} -> out
Key Value Meaning
retry a non-negative integer Up to that many further attempts after a failure whose attribution class is retryable (Error handling). Never for input or user.
on_error absent When the step has failed for good, its outputs are absent and the run continues. A cancel is never tolerated.

An unknown key, a key given twice, and a value outside the table are syntax errors. retry on a step with a streaming output is refused at resolution: nothing a retried step emits may be forwarded before an attempt succeeds, and an unbounded output cannot be held back. The policy is part of the machine: two machines that differ in it are not equivalent.


Machines in step position

A header alias machine "<reference>" makes another machine usable as a step.

  • Its inputs are its input anchors and its outputs its output anchors, each selected by the node name it bears in the referenced notation as written.
  • A member binds to an input by name, or unnamed by type: the unnamed members are matched to the inputs no member names, as unnamed members are matched to a cap’s args. A machine has no main input, so an unnamed member that nothing gives a type has nothing to be matched by (UntypedMachineMember); a member that fits no input left is UnmatchedMachineMember, and members that fit in more than one way are AmbiguousMachineMembers.
  • Every input is given exactly one member: a node or a constant. An input given none, or named by two, fails with MachineInputBinding. A named source must conform to the type the input holds (SourceDoesNotConformToMachineInput).
  • A target takes an output by name. A machine has no main output: an unnamed target takes the one output the wiring’s other targets leave, and fails with UnnamedMachineTarget when they leave several or none. An output no target takes is not produced for the including machine; the steps that make it still run.
  • Resolution inlines the referenced machine: its steps become steps of the including machine, each use with node positions of its own. The included machine is resolved alone first, and is written in as it resolved: every binding it made and every type its nodes hold stay as they were, whatever it is wired to. A constant given to an input is the value of every arg that input feeds, and is held to each one’s validation.
  • Each inlined step records the machine steps it came through, outermost first, as alias#use (use counts the wirings that use that alias, from one, in the order written). It is a label for views and reports; equivalence does not read it.
  • A machine that includes itself, directly or through others, fails with CyclicMachineReference, which names every machine on the way round. A machine that is not itself valid fails with InvalidIncludedMachine, carrying its own error. Every machine header is resolved, whether a wiring uses it or not.
  • A reference is an opaque string resolved by the host through one interface (MachineSource): the capdag CLI reads it as a path relative to the file that wrote it; an engine reads it from its machine store. An unresolvable reference fails with UnknownMachineReference; notation read with nothing to resolve references against fails with MachineReferenceWithoutSource.
  • A policy on an included machine is refused (PolicyOnMachine): it would have to mean one thing for every step inside.
  • The canonical notation of a machine is the notation of the inlined whole; it contains no references.

Anchors

Each MachineStrand has two anchor sets:

  • Input anchors: NodeIds (data positions) that no edge in the strand produces as its target. These are the strand’s external inputs: the data the user provides at runtime.
  • Output anchors: NodeIds that no edge in the strand consumes via any binding. These are the strand’s external outputs: the data the strand produces.

Anchors are sorted multisets of MediaUrn (sorted by structural MediaUrn::Ord); they are compared positionally on the sorted form, which is multiset equality.

A node that is both consumed and produced is an internal data position. A node that is neither cannot exist (every node must appear in at least one wiring, where it is either a source or a target). A constant is not a node: it is part of the binding that carries it. A strand whose every step is fed by constants alone has no input anchor.

Anchors are how a host supplies a run’s inputs and reads its outputs. Where types leave one answer the host matches by type; where two anchors have comparable URNs it addresses them by the node names the notation was written with, which every parse reports (parse_machine_with_node_names). Those names are labels of that text and nothing more: canonical notation renames them, and equivalence ignores them.


Strict equivalence

Machine::is_equivalent is the only equivalence relation on machines. It is strict, positional:

  1. Same number of MachineStrands in the strands vec.
  2. For every i, self.strands[i].is_equivalent(&other.strands[i]).

MachineStrand::is_equivalent walks both strands in their canonical edge order (computed by the resolver via Kahn’s topological sort with a structural tiebreaker), comparing edges position-by-position. It builds a NodeId bijection between the two strands on the fly: any inconsistency (the same self-NodeId mapped to two different other-NodeIds, or two NodeIds whose URNs are not is_equivalent) fails the comparison.

Each edge’s assignment vec is pre-sorted by cap arg media URN (gather members of one arg in the order written), so positional comparison over assignments is canonical. A binding’s source is a node, compared through the bijection, or a constant, compared as JSON values (number by value, object by key set). An edge’s taken outputs are compared by output media URN, and its policy by value. Arg names and output names take no part: they select, they do not identify.

There is no looser variant.


Canonical notation

Two strictly-equivalent Machines produce byte-identical canonical notation, because:

  • Strand order is part of the machine’s equivalence relation (Machine::is_equivalent is positional over strands).
  • Within each strand, canonical edge order is deterministic (Kahn’s algorithm with a structural tiebreaker on (cap_urn, sorted_assignment, taken_outputs, policy, is_loop)).
  • Every wiring is written with both groups, and every binding and every taken output is named: bindings in arg media URN order (the members of a gather in the order written, each under the arg’s name), outputs in output media URN order. Written so, a canonical wiring resolves by step 1 of the algorithm alone: nothing in it is matched by type, so it resolves to the machine it was written from whatever other args its caps declare. (An unnamed main input would be matched again among the args the machine leaves unbound, and could resolve differently.)
  • A constant is written as canonical JSON: no insignificant whitespace, object keys sorted, numbers in their shortest round-tripping form.
  • A policy is written with its keys sorted, and not at all when the step has none.
  • Aliases are edge_<i> from a global counter.
  • Node names are n<i> from a global counter.
  • Cap URNs are written in their canonical to_string form.

A user-authored notation, parsed and then re-serialized, produces canonical notation regardless of what aliases / node names the user wrote. Round-tripping the canonical notation is a fixed point.


From strand to machine

Three ways to build a Machine:

Constructor Input Strand count Crossings?
Machine::from_strand(strand, registry) one planner Strand 1 no
Machine::from_strands(strands, registry) a slice of planner Strands N (one per input) no
Machine::from_string(notation, registry) machine notation text discovered by connected components yes (within a strand)

The two programmatic constructors (from_strand, from_strands) treat each input strand as a self-contained DAG. Even if two input strands have type-compatible URNs internally, from_strands does not join them; that is the contract. Crossings only arise from notation, where the user explicitly shares node names across wirings.

In JavaScript, whose planner answers with paths of the cap fabric rather than strands, machineFromCapPath(path) is the counterpart of from_strand: one strand, each edge a step whose main input is fed by the step before it, read against the definitions of the path’s own caps. A path whose caps do not chain is refused as the same notation would be.

For each Cap step in a planner strand, from_strand builds one resolved edge whose source is the step’s from_spec and whose target is the step’s to_spec. Those boundary URNs are the planner’s resolved runtime endpoints, not merely the cap’s declared in=/out= patterns. ForEach sets is_loop = true on the next cap edge; Collect is elided (cardinality transitions are implicit in the resolved data flow).

Positional interning (planner path)

The planner chains caps by conformance: a cap’s declared input (in=media:enc=utf-8) may be less specific than the preceding cap’s declared output (out=media:page;enc=utf-8). At runtime the more-specific data flows through both. The resolver uses positional interning to collapse these into one shared data position:

  • Each cap step’s source reuses the preceding cap step’s target NodeId iff the two URNs are on the same specialization chain (is_comparable). The more-specific URN wins as the canonical representative.
  • Each cap step’s target always allocates a fresh NodeId.
  • ForEach and Collect preserve the boundary position through the cardinality transition.

This ensures that consecutive caps in a strand share an intermediate node even when their declared URNs differ by specificity.


Failure modes

Building a Machine from notation can fail in several distinct ways:

Error Source Meaning
MachineSyntaxError::Empty parser Input is empty or whitespace-only.
MachineSyntaxError::ParseError parser The text is not notation: a bracket never closed, a wiring or a header of the wrong shape, an unrecognized token.
MachineSyntaxError::DuplicateAlias parser Two header statements define the same alias.
MachineSyntaxError::UndefinedAlias parser A wiring’s cap-position name is neither a local header nor a registered fabric cap alias.
MachineSyntaxError::AliasNotACap parser A wiring’s cap-position name resolves to a fabric alias whose target is a media URN, not a cap.
MachineSyntaxError::NodeAliasCollision parser A wiring uses a node name that is also a header alias.
MachineSyntaxError::InvalidCapUrn parser A cap URN in a header failed to parse.
MachineSyntaxError::InvalidMediaUrn parser A media URN referenced in a cap’s in= or out= spec failed to parse.
MachineSyntaxError::NoEdges parser The notation has headers but no wirings.
MachineAbstractionError::NoCapabilitySteps resolver The strand or wiring set contains no Cap step.
MachineAbstractionError::UnknownCap resolver A cap referenced by a wiring is not in the cap registry’s cache.
MachineAbstractionError::UnmatchedSourceInCapArgs resolver An unnamed member’s URN conforms to no arg left for it.
MachineAbstractionError::AmbiguousMachineNotation resolver The minimum-cost assignment of the unnamed members is not unique. The error names the members to name and the args each could go to.
MachineAbstractionError::CyclicMachineStrand resolver The resolved data-flow graph of a strand contains a cycle.
MachineSyntaxError::UnnamedConstant parser A constant appears as a member without a name.
MachineSyntaxError::ReservedNodeName parser A node is named true, false or null.
MachineSyntaxError::InvalidPolicy parser A step policy has an unknown key, a repeated key, or a value outside the table.
MachineSyntaxError::SeveralUnnamedTargets parser More than one target of a wiring is unnamed.
MachineAbstractionError::UnknownArgName resolver A member names an arg the step does not declare. The error lists the names it does declare.
MachineAbstractionError::UnknownOutputName resolver A target names an output the step does not declare.
MachineAbstractionError::UnknownStrandOutput resolver A planner strand’s step takes, by media URN, an output its cap does not declare as another output. The error lists the ones it does.
MachineAbstractionError::UntypedMachineInput resolver A wiring holds an unnamed member that nothing produces and whose arg nothing decides: a second such member, or one beside a named member that bound the main input. The error names the members to name.
MachineAbstractionError::NodeMediaConflict resolver The wirings that touch a node give it URNs that are not on one specialization chain.
MachineAbstractionError::SourceDoesNotConformToArg resolver A named member’s URN does not conform to the type that arg accepts.
MachineAbstractionError::ConstantFailsValidation resolver A constant does not satisfy the validation of the arg it is bound to.
MachineAbstractionError::ArgBoundTwice resolver A scalar arg is named by two members.
MachineAbstractionError::MainInputUnbound resolver A step whose cap takes an input is given no member for it.
MachineAbstractionError::OutputTakenTwice resolver Two targets of one wiring take the same output.
MachineAbstractionError::VoidOutputTaken resolver A target takes an output of media:void.
MachineAbstractionError::RetryOnStreamingStep resolver A step with retry declares a streaming output.
MachineAbstractionError::PolicyOnMachine resolver A policy is written on an included machine.
MachineAbstractionError::UnknownMachineReference resolver A machine header’s reference does not resolve.
MachineAbstractionError::CyclicMachineReference resolver A machine includes itself, directly or through others. The error names every machine on the way round.
MachineAbstractionError::InvalidIncludedMachine resolver A machine named in a header is not itself a valid machine. The error carries that machine’s own error.
MachineSyntaxError::MachineReferenceWithoutSource parser A machine header is read with nothing to resolve references against.
MachineAbstractionError::UnknownMachineInput resolver A member names an input the machine in step position does not have. The error lists its inputs.
MachineAbstractionError::UnknownMachineOutput resolver A target names an output the machine in step position does not have. The error lists its outputs.
MachineAbstractionError::MachineInputBinding resolver An input of a machine in step position is given no member, or more than one.
MachineAbstractionError::MachineOutputTakenTwice resolver Two targets of one wiring take the same output of a machine.
MachineAbstractionError::UnnamedMachineTarget resolver An unnamed target of a machine step, where the other targets leave several outputs or none.
MachineAbstractionError::UntypedMachineMember resolver An unnamed member of a machine step that nothing gives a type.
MachineAbstractionError::UnmatchedMachineMember resolver An unnamed member of a machine step conforms to no input left for it.
MachineAbstractionError::AmbiguousMachineMembers resolver The unnamed members of a machine step fit its inputs in more than one way. The error names the members to name and the inputs each could go to.
MachineAbstractionError::SourceDoesNotConformToMachineInput resolver A named member’s URN does not conform to the type of the machine input it names.

MachineParseError is the union returned by Machine::from_string and parse_machine; it wraps either a MachineSyntaxError (lexical / grammatical) or a MachineAbstractionError (resolution).


Examples

Linear chain

[extract cap:in="media:document;ext=pdf";extract;out="media:txt;enc=utf-8"]
[embed cap:in="media:enc=utf-8";embed;out="media:embedding-vector;fmt=json;record"]
[doc -> extract -> text]
[text -> embed -> vectors]

One strand. Three nodes (doc, text, vectors). Two edges. Input anchor: the URN bound to doc (= media:document;ext=pdf). Output anchor: the URN bound to vectors.

Canonical re-serialization, taking extract’s arg to be named document and its output text, and embed’s text and vectors (the names are whatever the definitions give):

[edge_0 cap:in="media:document;ext=pdf";extract;out="media:txt;enc=utf-8"][edge_1 cap:in="media:enc=utf-8";embed;out="media:embedding-vector;fmt=json;record"][(document=n0) -> edge_0 -> (text=n1)][(text=n1) -> edge_1 -> (vectors=n2)]

Strand with iteration

[disbind cap:in="media:document;ext=pdf";disbind;out="media:page;enc=utf-8"]
[make_decision cap:in="media:enc=utf-8";make-decision;out="media:decision;fmt=json;record"]
[doc -> disbind -> pages]
[pages -> make_decision -> decisions]

One strand. disbind declares a sequence output and make_decision consumes a scalar, so the resolver derives is_loop = true on the second edge from cardinality alone, so the cap runs once per item of the sequence. Nothing in the notation authors this. The cap definition declares make_decision’s input as media:enc=utf-8, but the source URN is the more-specific media:page;enc=utf-8; the resolver’s matching accepts the conforming source and emits the binding (media:enc=utf-8, page_node).

Fan-out

[meta cap:in="media:document;ext=pdf";extract-metadata;out="media:file-metadata;fmt=json;record"]
[outline cap:in="media:document;ext=pdf";extract-outline;out="media:document-outline;fmt=json;record"]
[thumb cap:in="media:document;ext=pdf";generate-thumbnail;out="media:ext=png;image;thumbnail"]
[doc -> meta -> metadata]
[doc -> outline -> outline_data]
[doc -> thumb -> thumbnail]

One strand. All three wirings share the source doc, so they are connected (one connected component → one strand). Four nodes total: doc, metadata, outline_data, thumbnail. Three edges. The input anchor is doc’s URN (media:document;ext=pdf); the output anchors are the URNs of metadata, outline_data, and thumbnail.

Fan-in: distinct types into distinct args

[thumb cap:in="media:document;ext=pdf";generate-thumbnail;out="media:ext=png;image;thumbnail"]
[model_dl cap:in="media:model-spec;enc=utf-8";download;out="media:model-spec;enc=utf-8"]
[describe cap:in="media:ext=png;image";describe-image;out="media:image-description;enc=utf-8"]
[doc -> thumb -> thumbnail]
[spec_input -> model_dl -> model_spec]
[(thumbnail, model_spec) -> describe -> description]

describe declares its main input, which accepts media:ext=png;image, and an arg media:model-spec;enc=utf-8 it also offers on the command line as --model-spec. Neither member is named. thumbnail (media:ext=png;image;thumbnail) conforms only to the main input and model_spec only to the model arg, so there is one assignment and the edge has two bindings.

This is the product reading of a group: each member claims a distinct arg. Whenever one such assignment exists, it wins.

Fan-in: one type into two args

[compare cap:in="media:fmt=json;list;record;time-series";compare-to-benchmark;out="media:benchmark-comparison;fmt=json;record"]
[(path, benchmark=bench) -> compare -> result]

compare declares two args that accept a series: its main input, and one named benchmark whose own URN differs and whose accepts is the series type. Unnamed, (path, bench) gives the step two inputs of the machine whose arg nothing decides, and fails with UntypedMachineInput. Were both produced by earlier steps, each would hold a series, which fits either arg at the same cost, and the wiring would fail with AmbiguousMachineNotation, naming both members and the two args each could be for. Naming one member is enough: benchmark=bench binds first, and path has one arg left.

Constants, a source, and two outputs

[(reference="fred/UNRATE", from="2020-01-01") -> fetch_series{retry=3} -> unrate]
[(unrate, window=12, statistic="mean") -> rolling -> smooth]
[(unrate, x=smooth) -> regress -> (fit, residuals=res)]

fetch_series has in=media:void: its group holds constants only, and the strand has no input anchor. regress declares a second output named residuals; the wiring takes both, so the strand has two output anchors beside smooth’s consumer. Its canonical form names every binding and every output it takes:

[(y=n0, x=n1) -> edge_2 -> (fit=n2, residuals=n3)]

A router

[rows -> is_valid -> (yes=good, no=bad)]
[good -> load -> loaded]
[bad -> report{on_error=absent} -> reported]

rows is a sequence and is_valid takes one row, so it runs per row. Each run fires one of its two optional outputs. good and bad are the rows each output fired for, in order; load runs for the first and report for the second.

A machine as a step

clean.machine takes raw and gives rows:

[trim cap:in="media:fmt=csv;list;record";trim;out="media:fmt=csv;list;record;trimmed"]
[dedupe cap:in="media:fmt=csv;list;record;trimmed";dedupe;out="media:fmt=json;list;record;time-series"]
[raw -> trim -> trimmed]
[trimmed -> dedupe -> rows]

A machine beside it uses it twice:

[clean machine "clean.machine"]
[compare cap:in="media:fmt=json;list;record;time-series";compare;out="media:comparison;fmt=json;record"]
[(raw=prices) -> clean -> (rows=series)]
[(raw=index) -> clean -> bench]
[(series, benchmark=bench) -> compare -> result]

Each use is written in as its own two steps: the first as clean#1, the second as clean#2, each with a trimmed node of its own. The second use takes the machine’s only output unnamed. The machine that runs is five steps, with inputs prices and index and output result.

A map inside a map, folded

[split cap:in="media:document;ext=pdf";split;out="media:page;ext=png"]
[read cap:in="media:page;ext=png";read;out="media:enc=utf-8;reading"]
[join cap:in="media:enc=utf-8;reading";join;out="media:enc=utf-8;ext=md"]
[docs -> split -> pages]
[pages -> read -> readings]
[readings -> join -> text]

split gives a sequence of pages and join takes a sequence of readings. Run with docs bound to a sequence of documents, split runs once per document, read once per page of each document in a map inside the document map, and join folds the readings of one document: it runs once per document. text is one value per document; readings, read from outside, is every reading of every document, in order.

Fan-in gather (the implicit Collect)

When no one-to-one assignment exists, a sequence cap-arg (is_sequence = true in the cap definition) absorbs multiple conforming sources:

[extract_pdf cap:in="media:document;ext=pdf";extract;out="media:enc=utf-8;page"]
[extract_md  cap:in="media:ext=md";extract;out="media:enc=utf-8;page"]
[concat      cap:in="media:enc=utf-8";concat;out="media:enc=utf-8;ext=txt"]
[doc_a -> extract_pdf -> t1]
[doc_b -> extract_md  -> t2]
[(t1, t2) -> concat -> combined]

concat declares a single arg whose is_sequence is true. Both t1 and t2 conform to it and cannot claim distinct args, so the resolver gathers them: the edge carries one EdgeAssignmentBinding per source on the same arg, in source-declaration order (which fixes the gathered sequence’s item order). Plan construction synthesizes a real Collect node from the N bindings (the producers wire into the Collect, the Collect feeds the arg as one sequence), and execution concatenates the N scalar producer outputs into one sequence stream.

Like the per-item map, the gather is cardinality-driven, never authored: the notation stays fully abstract (“N producers of a type feed a consumer whose arg is a sequence of that type”), and how many producers actually feed the slot is a property of the wiring, not the grammar. This is the mechanism by which multi-source machines converge: legs from heterogeneous sources meet at a common intermediate type and a sequence-consuming cap folds them.

Rules, enforced hard:

  • The product interpretation takes precedence: sources that can claim distinct args one-to-one do so; the gather only activates when no such assignment exists.
  • Only a sequence arg gathers. Two unnamed members that conform to one scalar arg and to nothing else fail with UnmatchedSourceInCapArgs: a scalar arg takes one value.
  • A gather may also be written by name: (part=t1, part=t2) -> concat -> joined gathers both into the arg named part, in that order, whatever else could have accepted them.
  • Members may be scalar or sequence: the gather flattens deterministically: each member contributes its item(s) to the gathered sequence in binding (source-declaration) order; a scalar member contributes one item, a sequence member its items. This is the batch-plus-single convergence shape (a leg that maps a sequence folds together with a leg that produces one item).
  • The minimum-cost assignment must be unique in whichever phase decides; ties are AmbiguousMachineNotation.
  • The element type of the gathered sequence is the join ∨ (least common generalization, MediaUrn::least_upper_bound) of the members’ runtime media.

Summary

Concept Type Identity rule
Strand (planner) Strand sequence of StrandSteps
Strand (in machine) MachineStrand maximal connected component, with anchors
Machine Machine ordered Vec<MachineStrand>
Edge MachineEdge cap URN + sorted assignment + taken outputs + policy + is_loop
Binding EdgeAssignmentBinding (cap_arg_media_urn, source), a source being a node or a constant; the arg’s name is carried as a label and never compared
Taken output EdgeOutput (output_media_urn, node); the output’s name is carried as a label and never compared
Anchor Vec<MediaUrn> (sorted) multiset of root or leaf URNs

The resolved machine is the canonical anchor-realized form. Two strictly-equivalent machines produce byte-identical canonical notation; the canonical notation is a stable identifier for the machine value.