Skip to content
DarkPrint
What a blueprint isSpecification · 4 of 4
Layers 02 and 03 of 03

The node card (YAML)

One YAML file per node: what the step is, what it is told to do, which model and tools it may use, what arrives and what must never arrive. The validator accepts the same fields as JSON. A published version is never edited in place, so a change means a new file and a new version number.

One card, seven rows
  • modelclaude-sonnet-5 reaches The model this step runs on, named the way the provider names it.A stylesheet on the graph is a default for the nodes that name no model; a line here outranks it. A reader can still point the run at something else.
  • toolsnone reaches Capabilities it may reach for: a shell, a search index, a browser.The card names a capability rather than a vendor, so a graph says what it touches and never what you bought.
  • mcpfilesystem reaches An MCP server this step may talk to, by the name it is registered under on the machine that runs the graph.Two nodes naming the same server have the same reach; two naming different servers do not. Reach is declared per card, so a graph says what each node may touch rather than what the whole system may.
  • skillskills/code-builder.md reaches A written procedure it follows. A pointer only.Nothing here reads what this path points at, so no skill document travels in the folder. Each blueprint’s README lists the ones you supply yourself.
  • cannotacceptance-criteria cannot be What must never arrive. The validator enforces it against every incoming edge, no matter which node draws the edge.Data types from the vocabulary, and nothing else. Writing one here is what turns a stated rule into a checked one.
  • will_notread the checks the work will be run against cannot be What the card promises, in the author’s own sentences. It reaches whoever runs the node, and the model that is handed the specification at run time.The prohibitions nothing can check, kept apart from the ones the validator can, so a reader can tell them apart without running anything.
  • risk_markersnone declared reaches The blast radius, declared. The card states what this step could break if it goes wrong.Risk-marker terms from the vocabulary, and nothing else. Writing one here puts the blast radius in the file, where the validator can hold the author to a word the vocabulary defines.
One node, line by line

A node is a card, and the card is checkable

This is code-builder@1.0.0 as the archive stores it. Nine places on the card decide what the node is, what it does, the brief it is handed, which model it runs, what it may reach, what arrives and what must never arrive. The last of those is why whoever writes the code never reads the tests the work is judged against. Pick one on the right and the lines it is about light up on the left.

code-builder@1.0.051 lines, as the archive stores them
id: code-builder
name: Code Builder
type: agent
phase: implementation
action: >-
Work through the build brief and emit the source it describes, adding nothing the brief does
not ask for.
spec: >-
A build brief arrives with the run: an ordered list of steps, each naming the file or module
it touches and what should exist once it is done. Work through the steps in order and write
the source they describe, staying inside the files each step names and adding no behaviour
nobody asked for. Emit the finished source on `build` as one complete, compiling change, with
no commentary and no summary of what you did. The brief is all you get, and that is the point
do not go looking for a test suite, do not reason about how the result will be checked, and
do not tune anything toward a check you imagine exists.
model: claude-sonnet-5
agent: Builder
tools: []
mcp:
- filesystem
skill: skills/code-builder.md
inputs:
- name: brief
type: plan
description: The ordered build steps the run was instantiated with, the only thing this node sees.
outputs:
- name: build
type: code
description: One complete, compiling change implementing the brief, with no commentary attached.
dependencies: []
cannot:
- acceptance-criteria
will_not:
- read the checks the work will be run against
risk_markers: []
notes: >-
Doc 1 §3.2: isolation is not only an absent edge, it is the absence of the content from the
spec. This card is the reference for what that looks like, the spec above names no criterion,
quotes no threshold and paraphrases nothing the planner wrote, so the similarity half of the
`criteria-leak` check stays quiet on it as well as the topological half. Measured against
`spec-planner@1.0.0`, the 3-gram Jaccard similarity is 0.0356, against a configured threshold
of 0.35. `acceptance-criteria` in `cannot` names a data type in the ontology, so the resolver
enforces it: draw an edge that carries the criteria into this node and the bundle fails with
`bundle/prohibition-violated`. The absent edge is a rule the engine holds the graph to rather
than a convention the author remembered.
version: 1.0.0
author: autogen

The reference

Every field, and what holds it

The ids under a field are the curated core, spelled as a card has to spell them.

  • an error code refuses the bundle
  • a warning is reported and the bundle loads
  • free text is shown and checked by nothing
id · version

A lowercase hyphenated id, optionally namespaced, and the card's own semantic version. A graph pins a node to the pair as id@version.

card/bad-idcard/bad-versionrefuses the bundle
name · action · agent · notes · author · provenance

Prose for whoever opens the card, carried into the download. provenance says where the card came from when its author says so, and most cards leave it out.

free text
type

One node-type term, and the only field that says what kind of actor the node is. The autonomy reading asks two things of it: whether the type is a kind of human-in-the-loop, and whether it decides which other nodes run. The exporter draws the node's shape from it. A shell-tool with no command in params.tool_command is reported as card/missing-field, and the bundle still loads. Three of the curated values are categories: human-in-the-loop, evaluative and orchestration. The validator accepts a category, and the exporter has no shape for one and falls back to box, so write the concrete subtype.

card/unknown-termcard/wrong-term-kindrefuses the bundle
phase

Where in the lifecycle the node stands: any number of the five. The key takes a single term or a sequence, because both spellings read naturally in YAML. The five are closed and nobody may add one, so a namespaced entry is refused. An empty list is a complete answer rather than a hole, because the phases describe the blueprint rather than every node in it. An intake, a retrieval step or a memory store stands in none of them. An empty list is shown as empty, never as missing.

card/unknown-phasecard/namespaced-phaserefuses the bundle
spec

The instruction handed to the agent when the graph runs.

card/spec-too-thinreported, still loads
model

Written the way the provider writes the identifier. A default rather than a binding: a graph's model_stylesheet sets the model for every node matching a shape. An explicit field here outranks the sheet (Attractor spec §8.5). Whoever runs the blueprint outranks both.

free text
tools

The capabilities the node is permitted to reach for, as tool terms rather than labels. tools says what the node may do and mcp says which process supplies it, and a node can carry either without the other.

card/unknown-termcard/wrong-term-kindrefuses the bundle
mcp

The MCP servers the node needs. The vocabulary has no term for a server and is not going to grow one, so every entry is free text.

free text
skill

The path, inside the folder, of the document that defines the agent's behaviour.

free text
inputs · outputs

The ports, each with a name, a type, a description and, on an input, required. The type is one data-type term, and it is the only part of a port the resolver pairs on. This is what makes an edge checkable at all: an edge holds when the source's output type is the target's input type or a narrower kind of it. The name is the end of an edge rather than a label. A DOT edge writes [out="build", in="brief"] to say which pair of ports it joins, so a name is unique within a side. The description is free text for whoever wires the graph, where a port says the part its type cannot. required is true unless the card says otherwise. On an output it describes nothing, and the validator reports it as card/bad-type against the exact path rather than dropping the key in silence. The bundle still loads.

card/unknown-termcard/wrong-term-kindcard/duplicate-portrefuses the bundle
dependencies

Which cards this one receives from, checked both ways. A declared dependency with no edge into the node is refused. An edge from a card the node does not list is reported as bundle/undeclared-dependency, and the bundle still loads.

bundle/missing-dependencyrefuses the bundle
cannot

Data types the node must never receive: the same data-type terms a port takes, and nothing else. Each entry is a prohibition the resolver enforces, held to the same rule that pairs an edge's ports. An incoming edge able to carry that type fails the whole blueprint. A sentence written here does not resolve, because the validator has no way to hold a graph to a sentence and this is the field it holds graphs to.

card/unknown-termcard/wrong-term-kindbundle/prohibition-violatedrefuses the bundle
will_not

The prohibitions the author states and the validator cannot check: “never opens a shell”, “does not edit the code under test”. Nothing reads them, and they are addressed to whoever runs the node and to the model that is handed the specification at run time. cannot holds the rules the validator enforces and will_not the ones it cannot. Both are legitimate. A reader has to be able to tell which is which without running anything. The one thing checked here is that no data type is written in it by mistake.

card/prohibition-misfiledreported, still loads
risk_markers

What could go wrong if this step misbehaves, as risk-marker terms rather than sentences. Seven core markers carry a weight in DarkPrint's configuration, and Security subtracts each one it finds. A marker coined in a blueprint's own namespace sets its own weight. A marker with no weight, including the two category terms execution-risk and isolation-breach, is declared and never scored.

card/unknown-termcard/wrong-term-kindrefuses the bundle
params

Nested configuration, free in shape, which must survive a JSON round trip. The card's digest is a hash of its JSON form, and a blueprint pins a card by that digest. A value that cannot be serialised is refused where it is written rather than later, when two digests disagree. Two keys are read: max_iterations is the iteration cap, and tool_command is the command a shell-tool runs.

card/bad-typerefuses the bundle
a second version of a card in one blueprint

When a blueprint carries two versions of one card, the newer one must bump its version at least as far as the change requires. Adding to cannot narrows what the node accepts, so it needs a major bump. Changing model needs a minor bump.

card/version-bump-too-smallrefuses the bundle
The checks

What the validator checks

Every code beside a field is a real diagnostic. The validator prints it on the upload page and in the build when the rule is broken, so a code can be grepped for and tripped on purpose, which is what separates a checked rule from a promise.

This message is the validator’s own output rather than text written for this page. It comes from adding one edge, planner -> builder [label="acceptance criteria"];, to the starter blueprint.

errorbundle/prohibition-violated

Card `code-builder@1.0.0` declares that `builder` cannot receive `acceptance-criteria`, and edge `planner -> builder` carries that type.

`planner` declares the output `criteria` (`acceptance-criteria`). Remove the edge, or take `acceptance-criteria` out of `cannot` on `code-builder`. Moving it to `will_not` states the same rule and stops it being checked.

The topology pageshows the same prohibition from the other side: the starter’s DOT file, where the planner -> builder edge is deliberately absent.

Every term

The whole vocabulary, and what names each term

The 54 curated terms the fields draw on, and every term a published blueprint declares in its own namespace, each with its parent and how many cards name it.