darkprint 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,206 @@
1
+ # Pre-flight
2
+
3
+ The checklist you walk your own output against **before** you run the validator, so that
4
+ nothing it prints surprises you. It is not a substitute for running it: `SKILL.md`'s
5
+ "After writing" names the three ways, and you take the first one available.
6
+
7
+ Work down it in order. Anything marked **error** means the bundle will not load and will
8
+ not publish, and you fix it. Anything marked *warning* the author will see, on the upload
9
+ page or in the validator's output, so **predict it out loud** before they do: an author
10
+ surprised by the upload screen has been failed by the interview.
11
+
12
+ ---
13
+
14
+ ## 1. The DOT parses
15
+
16
+ - [ ] `digraph`, not `graph`. Every edge `->`, never `--`. `dot/not-directed` **error**
17
+ - [ ] one graph in the file. *`attractor/multiple-graphs`, `dot/unsupported`*
18
+ - [ ] not `strict digraph`. *`attractor/strict-graph`*
19
+ - [ ] `//` or `/* */` comments only. *`attractor/hash-comment`*
20
+ - [ ] attributes comma-separated. *`attractor/attr-separator`*
21
+ - [ ] node ids match `[A-Za-z_][A-Za-z0-9_]*`, unquoted, not a statement keyword.
22
+ *`attractor/bad-node-id`, `attractor/quoted-node-id`*
23
+ - [ ] no node named `start`, `Start`, `exit` or `end`. **No diagnostic**: the exporter renames
24
+ such a node and records the original in `dp_node`, so the compiled file stops saying
25
+ what the topology says. Check by eye
26
+ - [ ] **no `type=` on any node.** *`attractor/reserved-attribute`*, and it is the only reserved
27
+ node attribute the linter reports
28
+ - [ ] no other reserved Attractor node attribute either (`prompt`, `max_retries`, `llm_model`,
29
+ `label`, `shape`, `class`, `timeout`, `goal_gate`, `fidelity`, `thread_id`,
30
+ `retry_target`, `fallback_retry_target`, `llm_provider`, `reasoning_effort`,
31
+ `auto_status`, `allow_partial`). **No diagnostic**: each is written straight into the
32
+ compiled file and configures the run silently. Grep for them yourself
33
+ - [ ] no node declared twice with different attributes. *`dot/duplicate-node`*
34
+ - [ ] no self-loops. *`dot/self-loop`*
35
+
36
+ ## 2. Every node finds its card
37
+
38
+ - [ ] every node carries `card="<id>@<MAJOR.MINOR.PATCH>"`. `bundle/unpinned-card` **error**
39
+ - [ ] every pin names a card file the bundle actually carries. `bundle/missing-card` **error**
40
+ - [ ] every card file is instantiated by some node. *`bundle/orphan-card`*
41
+ - [ ] no two card files declare the same `id@version` with different content.
42
+ `bundle/digest-mismatch` **error**
43
+ - [ ] a card reused from the registry is byte for byte the file the registry serves for that
44
+ `id@version`. Same check, same error, on publish
45
+ - [ ] exactly one `.dot`; `blueprint.yaml` at the root is the manifest; no card named
46
+ `blueprint.yaml` or `extensions.yaml`
47
+
48
+ ## 3. Every card validates
49
+
50
+ - [ ] YAML parses. `card/parse-error` **error**. The one that bites in practice: a plain
51
+ scalar containing `": "` is read as a nested mapping. `description: Where it failed: the
52
+ line and the text` fails with *"nested mappings are not allowed in compact mappings"*.
53
+ **Quote any value containing a colon-space**, or write it as a `>-` folded block
54
+ - [ ] `id` matches `^(?:ns/)?[a-z0-9]+(-[a-z0-9]+)*$`. `card/bad-id` **error**
55
+ - [ ] `name`, `action`, `spec` present and non-blank; `inputs` and `outputs` **present**, even
56
+ when empty. `card/missing-field` **error**
57
+ - [ ] `version` is full semver. `card/bad-version` **error**
58
+ - [ ] `type` is one concrete `node-type`, never `human-in-the-loop`, `evaluative` or
59
+ `orchestration`. `card/unknown-term`, `card/wrong-term-kind` **error**
60
+ - [ ] every node where a person acts carries a `type` subsumed by `human-in-the-loop`, and
61
+ no node where nobody acts does. `type` is the only field that says it. Writing a
62
+ `requires_human` key is `card/retired-field`, a **warning**, and it is ignored
63
+ - [ ] every `shell-tool` carries a non-blank `params.tool_command`. Missing is
64
+ *`card/missing-field`*, blank is *`card/bad-type`*, and either way the node fails on its
65
+ first execution
66
+ - [ ] every `phase` entry is one of the five, never namespaced, never repeated.
67
+ `card/unknown-phase`, `card/namespaced-phase` **error**, *`card/duplicate-phase`*
68
+ - [ ] every `tools` entry is a `tool` term; every `risk_markers` entry is a `risk-marker` term
69
+ - [ ] port names unique within their side. `card/duplicate-port` **error**
70
+ - [ ] `required:` only on inputs
71
+ - [ ] every `spec` over 40 characters, and written as an instruction rather than a label.
72
+ *`card/spec-too-thin`*
73
+ - [ ] the card names no vocabulary version. There is one vocabulary and every card is read
74
+ against it, so writing an `ontology_version` key is `card/retired-field`, a
75
+ **warning**, and it is ignored
76
+ - [ ] no key outside the accepted set (`references/card-schema.md`). A typo is an `info` and
77
+ is **silently ignored**, so check the spelling of `risk_markers` and `will_not` by eye
78
+
79
+ ## 4. Every edge carries something
80
+
81
+ - [ ] no edge out of a node whose `outputs` is `[]`, and no edge into a node whose `inputs` is
82
+ `[]`. `bundle/port-mismatch` **error**, *"carries no data"*
83
+ - [ ] every `out=` / `in=` pin names a port the card actually declares.
84
+ `bundle/port-mismatch` **error**
85
+ - [ ] every edge has at least one compatible pair. Source narrower than target, or equal, or
86
+ either side `any`. `bundle/type-mismatch` **error**
87
+ - [ ] every edge where more than one pair would fit is **pinned**. *`bundle/port-ambiguous`*,
88
+ and an unpinned ambiguous edge means the prohibition check is reasoning about the
89
+ resolver's declaration-order guess rather than about your intent
90
+
91
+ ## 5. Every fork is guarded
92
+
93
+ - [ ] every node with two or more outgoing edges has a `condition` on every arm, or a
94
+ `weight` the author chose on purpose. **No diagnostic**: an unguarded fork is legal
95
+ and the runner decides it by the spelling of the target ids, so a loop whose fix arm
96
+ sorts before its ship arm never ships. Read `references/dot-and-attractor.md`, Part 2
97
+ - [ ] every `condition` uses only the keys `outcome`, `preferred_label` or `context.<path>`,
98
+ the operators `=` and `!=`, and `&&` between clauses. *`attractor/condition-syntax`*
99
+ here, and an error to the runner, which refuses the whole pipeline over it
100
+ - [ ] no `condition=""`. It is no condition at all, and the edge falls back to the order
101
+ above without anything saying so
102
+ - [ ] the check's success arm is `outcome=success` and its other arm is `outcome!=success`,
103
+ so a verdict that is neither lands on the arm that does not ship
104
+
105
+ ## 6. The prohibitions
106
+
107
+ - [ ] every `cannot` entry names a `data-type` term id. A sentence here is
108
+ `card/unknown-term` **error** and the card does not load
109
+ - [ ] every `will_not` entry is a sentence. A `data-type` here is
110
+ *`card/prohibition-misfiled`*, and it means a rule the resolver could have enforced
111
+ is sitting where nothing reads it
112
+ - [ ] no edge carries into a node a type its `cannot` refuses. `bundle/prohibition-violated`
113
+ **error**. Two directions to keep straight:
114
+ - **subsumption**: `cannot: [structured]` refuses an incoming `acceptance-criteria`;
115
+ `cannot: [acceptance-criteria]` does **not** refuse an incoming `structured`;
116
+ - **the carrier**: with no `out=` pin, the carriers are *every output of the source
117
+ card*, so the edge is refused if any of them is prohibited. With an `out=` pin, only
118
+ that one port counts, so a pinned edge can slip past a prohibition the unpinned one
119
+ would have tripped. It still gets charged by the analyzer, which reads the graph at
120
+ node level; `cannot` is the tripwire, not the whole guard
121
+ - [ ] no output typed `any` upstream of a node with a narrower prohibition. `any` never
122
+ violates anything and the whole mechanism goes inert
123
+
124
+ ## 7. Dependencies
125
+
126
+ - [ ] every `dependencies` entry has a matching incoming edge. `bundle/missing-dependency`
127
+ **error**
128
+ - [ ] every incoming edge is listed in the target card's `dependencies`.
129
+ *`bundle/undeclared-dependency`*
130
+ - [ ] entries name the supplier's **card id** or its **DOT node id**; either satisfies the check
131
+
132
+ ## 8. Structure
133
+
134
+ - [ ] at least one node with no incoming edge. *`bundle/no-entry`*
135
+ - [ ] at least one node with no outgoing edge, and it declares `outputs: []`. *`bundle/no-exit`*
136
+ - [ ] every node reachable from a source. *`bundle/unreachable-node`*
137
+ - [ ] the graph is not empty. *`analysis/empty-graph`*
138
+
139
+ ## 9. The checks that decide the security level
140
+
141
+ - [ ] **at least one node typed `validation`.** Without it the generator set is empty and the
142
+ criteria check does not run. *`analysis/criteria-leak-unanchored`* when a producer
143
+ exists, silence when neither leg does, and a 4 is silence either way
144
+ - [ ] **at least one output port typed `acceptance-criteria`.** Without it the producer set is
145
+ empty, same outcome
146
+ - [ ] **no edge from the criteria producer into any node whose work is judged, whatever port
147
+ that edge carries.** The topological walk reads the graph at node level. A brief that
148
+ needs to reach the builder arrives with the run, not over an edge
149
+ - [ ] the criteria producer does not also emit the artefact the judge reads. One node writing
150
+ both is charged off the declarations alone
151
+ - [ ] every generator's `spec` shares under 0.35 3-gram Jaccard with the producer's, names no
152
+ criterion and quotes no threshold. *`analysis/criteria-leak-suspected`*
153
+ - [ ] no `params` key matching `/criteri/i` naming something nothing in the graph produces.
154
+ *`analysis/criteria-out-of-band`*
155
+ - [ ] every cycle has `max_retries` as a **top-level** key of `params` on one member, holding
156
+ a non-negative integer that counts the attempts **after** the first. `max_iterations`
157
+ and `maxIterations` are accepted aliases for the same number. Nested, it is not read,
158
+ and the cycle takes `unbounded-loop` on every member
159
+ - [ ] every node with `web-search`, `http-fetch`, `sql` or `ci` in `tools` either has no
160
+ successors or hands to `validation` nodes only. Otherwise
161
+ `unvalidated-external-access` is inferred, −1.0, whether or not it is declared
162
+ - [ ] you have **not** inserted a validation node purely to silence a marker
163
+
164
+ ## 10. The manifest
165
+
166
+ - [ ] `blueprint.yaml` carries `slug`, `title`, `summary`, `description`, `category` and
167
+ `tags`, and `slug` is the folder name and the name the author creates at `/new`
168
+ - [ ] `summary` is one sentence, because it becomes the compiled pipeline's `goal`
169
+
170
+ ## 11. Predict the validator
171
+
172
+ Write out, for the author, the warnings you expect and why each is intended. The common
173
+ honest ones:
174
+
175
+ | warning | when it is fine |
176
+ |---|---|
177
+ | `analysis/criteria-relayed-through-judge` | the fixer sits downstream of the judge that holds the criteria. The engine cannot tell `judge → fixer → judge` from `judge → builder → judge` and declines to decide. Charges nothing |
178
+ | `bundle/undeclared-dependency` | never fine. Fix it |
179
+ | `bundle/no-entry` / `bundle/no-exit` | never fine in a first emit. A blueprint with no source and no sink is a loop with no way in |
180
+ | `bundle/port-ambiguous` | never fine. Pin the edge |
181
+ | `attractor/condition-syntax` | never fine. The runner refuses the whole pipeline over it |
182
+
183
+ And name the two figures the author will read. The autonomy class is read twice off `type`
184
+ and the weaker reading is the one that lands in a band: the share of all nodes not subsumed
185
+ by `human-in-the-loop`, and the same share taken over the **control points** only (the nodes
186
+ under `evaluative` or `orchestration`, plus `human-gate`), which decides a band only from two
187
+ control points up. A graph with one `human-gate` among its two control points reads 0.5 on
188
+ the second axis and lands a band lower than the first axis alone would say. The bands are in
189
+ `references/ontology.md`. The security level is 4 minus the sum of the distinct markers,
190
+ each charged once.
191
+
192
+ ## 12. Re-emitting
193
+
194
+ If this is a second pass over a bundle that already exists:
195
+
196
+ - [ ] a card whose content changed has a **new version**, and the old file is **deleted**, not
197
+ left beside it. `bundle/digest-mismatch` **error**, *`bundle/orphan-card`*
198
+ - [ ] the bump is large enough. `card/version-bump-too-small` **error**. The full table is in
199
+ `references/card-schema.md`; the rows that bite in a second pass are these. **Major**:
200
+ a port removed, renamed or retyped, an input made required or a required input added,
201
+ `type` or `id` changed, a `cannot` entry added, a `will_not` entry withdrawn. **Minor**:
202
+ `spec`, `model` or `skill` changed, an optional port added, a tool, MCP server, param
203
+ key, risk marker or dependency added, a phase added or dropped, a `cannot` withdrawn, a
204
+ `will_not` stated. **Patch**: wording, a param's value, a reorder, anything withdrawn
205
+ from `tools`, `mcp`, `risk_markers`, `dependencies` or `params`
206
+ - [ ] every DOT pin updated to the new version
@@ -0,0 +1,178 @@
1
+ # Writing the prose
2
+
3
+ The author decides the shape. You write the words. Four card fields carry prose (`action`,
4
+ `spec`, port `description`, `notes`), and two files beside the cards do (`blueprint.yaml`,
5
+ `README.md`).
6
+
7
+ ---
8
+
9
+ ## `action`
10
+
11
+ One sentence, imperative, naming the operation and the artefact. It is the line a reader
12
+ scans; it is not the instruction the agent receives.
13
+
14
+ > Turn the incoming request into two separate artefacts: an ordered brief, and the acceptance
15
+ > criteria the finished work will be judged against.
16
+
17
+ If it contains an "and" naming two different artefacts *and* the node emits both, check
18
+ against the criteria rule below before you keep it.
19
+
20
+ ---
21
+
22
+ ## `spec`, the one that matters
23
+
24
+ **`spec` is compiled into the node's `prompt`.** It is delivered verbatim to an agent that
25
+ does not see the rest of the graph, so it has to be self-sufficient, and it has to respect
26
+ the isolation the topology declares, because an absent edge with the criteria paraphrased into
27
+ the prose is a false isolation. The engine measures that; it does not take your word for it.
28
+
29
+ Aim for **150 to 400 words**. Under 40 characters is `card/spec-too-thin`. Under three words is
30
+ excluded from the similarity comparison entirely, so a placeholder card is both useless and
31
+ unchecked.
32
+
33
+ Write it as:
34
+
35
+ 1. **What arrives.** Name the input ports and say what is in them.
36
+ 2. **What to do**, in the order it is done.
37
+ 3. **What to emit, on which port**, and in what form.
38
+ 4. **What not to do**: the boundaries from Q0.5 and Q4.3, in the second person, the same
39
+ sentences that went into `will_not`.
40
+
41
+ For a `shell-tool`, the `spec` still says what arrives and what is emitted; the command in
42
+ `params.tool_command` is what runs. For a node inside a cycle, the `spec` states the total
43
+ number of attempts in words, and `params.max_retries` holds that total minus one.
44
+
45
+ ### The rule you cannot break
46
+
47
+ A node whose work is judged must not paraphrase the criteria producer's `spec`. The engine
48
+ computes 3-gram Jaccard similarity between the two and warns above **0.35**
49
+ (`analysis/criteria-leak-suspected`).
50
+
51
+ Concretely, in a generator's `spec`:
52
+
53
+ - name no criterion,
54
+ - quote no threshold,
55
+ - do not restate the planner's sentences in your own words,
56
+ - do not describe how the work will be checked.
57
+
58
+ The starter blueprint's builder spec measures **0.0356** against its planner's. That is what
59
+ independent prose looks like. If you find yourself writing "make sure it passes the checks",
60
+ you have just written the leak into the file.
61
+
62
+ The reason, stated to the author when they push back: whoever writes the work must not see the
63
+ acceptance tests, because a node that sees them writes toward them, and the check stops
64
+ measuring the work and starts measuring the aim.
65
+
66
+ ### Do not write one house style and vary it per node
67
+
68
+ Six specs sharing a third of their trigrams cross the threshold against each other. Write each
69
+ one about its own job, in its own words.
70
+
71
+ ---
72
+
73
+ ## Port `description`
74
+
75
+ One line. What is in it, and who it came from, not what the receiving node should do with it.
76
+
77
+ > The ordered build steps the run was instantiated with, the only thing this node sees.
78
+
79
+ A one-line description is where the YAML trap lands, because a colon is the natural way to
80
+ write one. A plain scalar containing `": "` is parsed as a nested mapping and the whole card
81
+ fails with `card/parse-error`. Quote it, or fold it:
82
+
83
+ ```yaml
84
+ description: Where it failed: the line and the text # BREAKS the card
85
+ description: "Where it failed: the line and the text" # fine
86
+ ```
87
+
88
+ ---
89
+
90
+ ## `notes`
91
+
92
+ Optional, and absence carries no judgement. Use it for the thing a reader would otherwise
93
+ misread as an oversight: the edge you did not draw, the marker you deliberately did not
94
+ declare and why, the cap that is load-bearing twice over.
95
+
96
+ This is the right place to record a judgement call that could have gone the other way, with
97
+ what it would cost if it did:
98
+
99
+ > No `irreversible-action`, deliberately: this node writes one tagged artefact to the run's own
100
+ > release target, sends nothing to a third party and deletes nothing. If a reviewer decides a
101
+ > release target counts as publication, the marker costs this blueprint 1.5 and takes its
102
+ > security level from 4 to 3.
103
+
104
+ ---
105
+
106
+ ## `blueprint.yaml`
107
+
108
+ The manifest, derived from the interview (SKILL.md, Q5.3) and shown back before it is
109
+ written:
110
+
111
+ ```yaml
112
+ slug: help-centre-line
113
+ title: Help Centre Article Line
114
+ summary: >-
115
+ Outline, draft and check help-centre articles against acceptance criteria the drafter
116
+ never sees, with one human approval before anything publishes.
117
+ description: |-
118
+ One paragraph per node in the order a run visits them, in the author's words from Q2.1,
119
+ ending with the edge that is deliberately absent and why.
120
+ category: Content
121
+ tags:
122
+ - help-centre
123
+ - editorial
124
+ - isolation
125
+ - human-gate
126
+ ```
127
+
128
+ `summary` is one sentence, because it becomes the compiled pipeline's `goal`: the one line a
129
+ runner reads about the whole graph. `slug` is the folder name, the name the author creates
130
+ at `/new`, and the graph name in card-id grammar. Nothing else goes in the file; the
131
+ registry fills in the author and the dates when it publishes.
132
+
133
+ ---
134
+
135
+ ## `README.md`
136
+
137
+ For a person opening the folder. Written from the answers, not from a template with the
138
+ blanks filled, and in the shape the registry writes for every published bundle, so a folder
139
+ that came from this skill and a folder downloaded from the site read the same way:
140
+
141
+ 1. **What the blueprint does**, in the author's own sentence from Q0.1, and the blueprint it
142
+ started from if Phase 1 found one.
143
+ 2. **The node table**: DOT id, card `id@version`, type, phase, one line each.
144
+ 3. **The edges, including the ones deliberately absent**, with a line per absence saying why,
145
+ and the guard on every fork. This is the section that earns the file.
146
+ 4. **What is in the folder**: `topology.dot`, `cards/`, `blueprint.yaml`, and
147
+ `ontology/extensions.yaml` when there is one.
148
+ 5. **What the validator said**: which validator ran (`darkprint validate`, the
149
+ `/api/validate/bundle` route, or the upload page), the autonomy class and the security
150
+ level it computed, and each warning with the reason it is intended. Never claim the bundle
151
+ validated cleanly without naming which of the three said so.
152
+ 6. **What these files leave to the runner**: that ports, `cannot`, `will_not` and
153
+ `risk_markers` are enforced when the bundle is validated and by nothing in the compiled
154
+ pipeline, and that every Attractor attribute a card has no field for (goal gates, timeouts,
155
+ the retry policy above `max_retries`) falls to the runner's own default.
156
+ 7. **How to check it again**: drop the folder on `https://www.darkprint.io/upload`.
157
+
158
+ ---
159
+
160
+ ## Re-emitting an existing bundle
161
+
162
+ A published card version is never edited in place.
163
+
164
+ - Content changed ⇒ new version, old file **deleted**, DOT pin updated.
165
+ - The bump has to be at least what the engine infers. Under-bumping is
166
+ `card/version-bump-too-small`, an error. The full table is in `references/card-schema.md`.
167
+ The rows that catch people: a port removed, renamed or retyped, a required input added, a
168
+ `cannot` entry added or a `will_not` entry withdrawn are all **major**; `spec`, `model` and
169
+ a new tool, param key, risk marker or dependency are **minor**; a param's *value*, wording
170
+ and reorders are **patch**.
171
+ - Leaving the superseded file in `cards/` is `bundle/orphan-card` at best and
172
+ `bundle/digest-mismatch` at worst.
173
+ - A card reused from the registry is never re-emitted by you at all. If it needs to change,
174
+ it is a new card with a new id, or the author publishes the change under their own name
175
+ and the DOT pins that.
176
+ - When a card's identity is a parameter (the starter encodes its debugger's attempt count in
177
+ the minor version) keep that convention. Two caps behind one pin would be two contents
178
+ behind one reference.
@@ -0,0 +1,84 @@
1
+ # cards/<card-id>@<version>.yaml
2
+ #
3
+ # The wire format is snake_case. Every key the validator accepts is listed in
4
+ # references/card-schema.md; anything outside that set is silently ignored, so a typo in
5
+ # `risk_markers` does nothing rather than failing loudly.
6
+ #
7
+ # This is the skeleton for a node instructed by prose (`agent`, `tool`, `validation`,
8
+ # `decision`, `human-gate`, `human-input`). A node that IS a command is `shell-tool` and
9
+ # has its own skeleton in `shell-tool-card.yaml`, because it owes `params.tool_command`.
10
+ #
11
+ # NEVER name a card file `blueprint.yaml` or `extensions.yaml`. Those names are claimed by
12
+ # the manifest and the local vocabulary, and a card called either disappears from the bundle.
13
+ #
14
+ # YAML trap, hit every time: a plain scalar containing ": " is parsed as a nested mapping and
15
+ # the whole card fails with card/parse-error. Quote it, or use a `>-` folded block.
16
+ # description: Where it failed: the line and the text <- BREAKS the card
17
+ # description: "Where it failed: the line and the text" <- fine
18
+
19
+ # ---- identity ----
20
+ id: card-id # ^(?:namespace/)?[a-z0-9]+(-[a-z0-9]+)*$ lowercase, hyphens, no underscores
21
+ name: Human Readable Name
22
+ type: agent # exactly one node-type term. Never an abstract category.
23
+ phase: implementation # optional and repeatable; omit it entirely when the node sits in none of the five
24
+
25
+ # ---- behaviour ----
26
+ action: >-
27
+ One imperative sentence naming the operation and the artefact.
28
+ spec: >-
29
+ 150-400 words. This is compiled into the node's `prompt` and handed verbatim to an agent
30
+ that does not see the rest of the graph, so it must be self-sufficient: what arrives, what
31
+ to do in order, what to emit on which port and in what form, and what not to do. If this
32
+ node's work is judged, it must name no criterion, quote no threshold and paraphrase nothing
33
+ the criteria producer wrote. The engine measures the overlap and warns above 0.35.
34
+ model: claude-sonnet-5 # optional. A default the runner can override, not a binding.
35
+ agent: Role Name # optional
36
+ tools: [] # tool terms. web-search / http-fetch / sql / ci infer a risk marker.
37
+ mcp: [] # installed server names. Free text, checked against nothing.
38
+ skill: skills/card-id.md # optional pointer; nothing in the engine reads what it points at
39
+ params:
40
+ # The iteration cap, when this node sits in a cycle. TOP LEVEL only: nested it is not read,
41
+ # and the whole cycle takes -1.5 with no obvious cause. It counts the attempts AFTER the
42
+ # first, so 2 means three runs in all. Say the total in `spec` and make the two agree.
43
+ # `max_iterations` and `maxIterations` are accepted aliases and mean the same number.
44
+ max_retries: 2
45
+
46
+ # ---- interfaces ----
47
+ inputs: # must be PRESENT. Write [] when the node needs nothing.
48
+ - name: brief
49
+ type: plan # a data-type term. Do not reach for `any`.
50
+ description: What is in it and who it came from.
51
+ outputs: # must be PRESENT. [] is how a sink is declared.
52
+ - name: build
53
+ type: code
54
+ description: What is handed over.
55
+ dependencies: # the card ids or DOT node ids of the suppliers, derived from the edges
56
+ - upstream-card-id
57
+ cannot: # data-type term ids ONLY. Anything else is card/unknown-term.
58
+ # ENFORCED: the resolver refuses any incoming edge whose carrier is that type or anything
59
+ # narrower, with bundle/prohibition-violated. On an unpinned edge the carriers are EVERY
60
+ # output of the source card; with `out=` only that port.
61
+ - acceptance-criteria
62
+ will_not: # your own sentences. Nothing checks them, and nothing can.
63
+ # Addressed to whoever reads the card and to the agent instantiated from it. Putting a
64
+ # data-type here is card/prohibition-misfiled, a warning: the card loads and no edge is
65
+ # ever checked against the rule you meant to state.
66
+ - read the checks the work will be run against
67
+
68
+ # ---- evaluation metadata ----
69
+ # Whether a person acts here is not a field. It is `type`: human-gate and human-input are
70
+ # subsumed by human-in-the-loop and every other type is not. Writing `requires_human` here
71
+ # is card/retired-field, a warning, and the key is ignored.
72
+ risk_markers: [] # risk-marker terms. unbounded-loop, unvalidated-external-access and
73
+ # criteria-leak are inferred from the graph whether declared or not.
74
+ notes: >-
75
+ Optional. The place to record the thing a reader would otherwise misread as an oversight:
76
+ the edge that is deliberately absent, the marker deliberately not declared and what it would
77
+ cost if the judgement went the other way.
78
+
79
+ # ---- service fields ----
80
+ version: 1.0.0 # full semver. A published version is never edited in place.
81
+ author: your-handle # optional
82
+ # No ontology_version. There is one vocabulary and every card is read against it. The
83
+ # vocabulary carries no version of its own. Writing the key is card/retired-field, a
84
+ # warning.
@@ -0,0 +1,52 @@
1
+ # cards/<card-id>@<version>.yaml, for a node that IS a command.
2
+ #
3
+ # `type: shell-tool` is a kind of `tool`, so every rule about tool nodes still holds, and it
4
+ # compiles to the Attractor handler that runs `params.tool_command` and fails the node before
5
+ # doing anything else when the command is missing or blank. A missing command is
6
+ # card/missing-field and a blank one is card/bad-type, both warnings, both meaning the node
7
+ # resolves and then fails on its first execution. A node instructed by prose alone is
8
+ # `type: tool`, and takes the skeleton in `card.yaml`.
9
+ #
10
+ # The same YAML trap as every card: quote any value containing ": ".
11
+
12
+ # ---- identity ----
13
+ id: test-runner
14
+ name: Test Runner
15
+ type: shell-tool
16
+ phase: testing
17
+
18
+ # ---- behaviour ----
19
+ action: >-
20
+ Run the project's test suite against the build and emit the runner's own report.
21
+ spec: >-
22
+ The build arrives on the `build` port as a checked-out tree. Run the command below from
23
+ its root and capture everything it prints. Emit the whole output on the `report` port,
24
+ exit status first, with nothing summarised and nothing removed. Do not modify the tree,
25
+ do not install anything, and do not retry a command that failed.
26
+ tools: [shell] # the permission the command needs, as a tool term
27
+ params:
28
+ # The exact line the runner executes. Fixed in advance, so it is not arbitrary code
29
+ # execution: declare that marker only when the command runs something decided at run
30
+ # time, such as a script the previous node wrote.
31
+ tool_command: npm test
32
+
33
+ # ---- interfaces ----
34
+ inputs:
35
+ - name: build
36
+ type: code
37
+ description: The tree to test, as the builder handed it over.
38
+ outputs:
39
+ - name: report
40
+ type: report
41
+ description: The runner's own output, exit status included.
42
+ dependencies:
43
+ - code-builder
44
+ cannot: []
45
+ will_not:
46
+ - modify the tree under test
47
+
48
+ # ---- evaluation metadata ----
49
+ risk_markers: []
50
+
51
+ # ---- service fields ----
52
+ version: 1.0.0
@@ -0,0 +1,66 @@
1
+ // A skeleton, not a form to fill in blindly. Every line here is a decision the interview
2
+ // has already made. Delete what does not apply.
3
+ //
4
+ // Comments are `//` or `/* */`. A `#` comment trips `attractor/hash-comment`.
5
+ // Attributes are COMMA-separated. Node ids match [A-Za-z_][A-Za-z0-9_]* and are never quoted.
6
+ // NEVER write `type=` on a node: that is Attractor's handler override, not DarkPrint's
7
+ // ontology type, which lives in the card.
8
+
9
+ digraph blueprint_name {
10
+ rankdir=LR;
11
+ node [shape=box, style=rounded];
12
+
13
+ /* ---- nodes: the id is the instance, the card is the definition ----
14
+ Always pin `card="<id>@<MAJOR.MINOR.PATCH>"`. The bare `version=` fallback makes the node
15
+ id double as the card id, which only works for a single lowercase word. */
16
+
17
+ plan_step [card="plan-step@1.0.0"];
18
+ build_step [card="build-step@1.0.0"];
19
+ check_step [card="check-step@1.0.0"];
20
+ fix_step [card="fix-step@1.0.0"];
21
+ ship_step [card="ship-step@1.0.0"];
22
+
23
+ /* ---- edges: pin the ports whenever more than one pair would fit ----
24
+ `out=` names a port on the source card, `in=` a port on the target card. Neither is
25
+ reserved by Attractor, so both ride along harmlessly in the compiled pipeline. */
26
+
27
+ plan_step -> check_step [label="acceptance criteria", out="criteria", in="criteria"];
28
+ build_step -> check_step [label="build", out="build", in="build"];
29
+
30
+ /* ---- THE FORK, and the guard on each arm ----
31
+ `check_step` has two exits. A runner picks one in a fixed order: a `condition` that
32
+ holds beats every unguarded edge; then an edge whose `label` matches the label the node
33
+ asked for; then a next id the node suggested; then the higher `weight`; and on a tie,
34
+ the target id that sorts first. Two bare edges out of one node are therefore a branch
35
+ decided by the spelling of the node names. `fix_step` sorts before `ship_step`, so an
36
+ unguarded version of this loop repairs until the cap is spent and never ships.
37
+
38
+ `outcome` is set by the runner from the node's own status on every pass. One key against
39
+ its own negation is the only total split the guard grammar can make: it has `=` and
40
+ `!=`, clauses joined by `&&`, and nothing else. */
41
+ check_step -> fix_step [label="failure evidence", style=dashed, condition="outcome!=success", out="evidence", in="evidence"];
42
+ check_step -> ship_step [label="approved", condition="outcome=success", out="approved", in="release"];
43
+
44
+ // The loop returns to the CHECK, never to the builder: the work already done is preserved
45
+ // and the builder stays isolated from every fact about the failures for the whole run.
46
+ // The cap is a TOP-LEVEL `params.max_retries` on one member, counting the attempts after
47
+ // the first: `max_retries: 2` is three runs in all.
48
+ fix_step -> check_step [label="patch", out="patch", in="build"];
49
+
50
+ /* ---- THE EDGE THAT IS NOT HERE ----
51
+ Nothing runs from `plan_step` to `build_step`. The criteria reach the node that JUDGES the
52
+ work and never the node that PRODUCES it.
53
+
54
+ The topological half of the `criteria-leak` check reads the graph at NODE level: any edge
55
+ at all from the criteria producer into a judged node establishes the marker, whichever
56
+ port that edge carries. So the build brief is handed to `build_step` when the graph is
57
+ instantiated, as a source node with an input port and no incoming edge, rather than
58
+ over an edge from the planner.
59
+
60
+ The other end of the same rule is written in the card: `build-step` lists
61
+ `acceptance-criteria` under `cannot`, so drawing that edge unpinned fails the bundle with
62
+ `bundle/prohibition-violated` rather than loading quietly and scoring 2. Pinning it to
63
+ `out="brief"` would satisfy the prohibition, because the carrier is then that one port,
64
+ and the analyzer would still charge criteria-leak, because its walk reads the graph at
65
+ node level. Two guards, and they answer different questions. */
66
+ }