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.
- package/README.md +25 -0
- package/dist/cli.js +15656 -0
- package/package.json +21 -0
- package/skill/darkprint/SKILL.md +688 -0
- package/skill/darkprint/references/card-schema.md +544 -0
- package/skill/darkprint/references/dot-and-attractor.md +300 -0
- package/skill/darkprint/references/live-preview.md +95 -0
- package/skill/darkprint/references/ontology.md +242 -0
- package/skill/darkprint/references/preflight.md +206 -0
- package/skill/darkprint/references/writing-cards.md +178 -0
- package/skill/darkprint/templates/card.yaml +84 -0
- package/skill/darkprint/templates/shell-tool-card.yaml +52 -0
- package/skill/darkprint/templates/topology.dot +66 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
# The DOT, and what a card becomes when it is compiled
|
|
2
|
+
|
|
3
|
+
Three things live here: how to write `topology.dot` so it loads clean, how a fork is
|
|
4
|
+
guarded so a runner takes the arm the author meant, and what happens to a card when the
|
|
5
|
+
bundle is compiled into a pipeline a runner takes. **This skill compiles nothing**, and no
|
|
6
|
+
published bundle folder carries a compiled graph. The third part is background, and an
|
|
7
|
+
author is entitled to know what their `spec` turns into and which parts of their card never
|
|
8
|
+
leave DarkPrint.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Part 1: writing `topology.dot`
|
|
13
|
+
|
|
14
|
+
### The shape
|
|
15
|
+
|
|
16
|
+
```dot
|
|
17
|
+
digraph help_centre_line {
|
|
18
|
+
rankdir=LR;
|
|
19
|
+
node [shape=box, style=rounded];
|
|
20
|
+
|
|
21
|
+
// One statement per node. The id is the instance; the card is the definition.
|
|
22
|
+
outline_writer [card="article-outliner@1.0.0"];
|
|
23
|
+
draft_writer [card="article-drafter@1.0.0"];
|
|
24
|
+
|
|
25
|
+
// Edges carry data. Pin the ports whenever more than one pair would fit.
|
|
26
|
+
outline_writer -> editorial_check [label="acceptance criteria", out="criteria", in="criteria"];
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Node ids
|
|
31
|
+
|
|
32
|
+
`[A-Za-z_][A-Za-z0-9_]*`. No hyphens, no leading digit, never quoted.
|
|
33
|
+
|
|
34
|
+
- A hyphen or a quoted id is `attractor/bad-node-id` or `attractor/quoted-node-id` (warnings,
|
|
35
|
+
visible on `/upload`, and they make the bundle look broken).
|
|
36
|
+
- `digraph`, `edge`, `graph`, `node`, `strict`, `subgraph` are statement keywords and cannot
|
|
37
|
+
open a node statement. The same warning, and the same fix: rename the node.
|
|
38
|
+
- `start`, `Start`, `exit`, `end` are legal here and **no diagnostic warns about them**.
|
|
39
|
+
Attractor resolves those names as the pipeline's entry and exit, so they collide with the
|
|
40
|
+
boundary nodes DarkPrint synthesises when it compiles; the exporter renames such a node
|
|
41
|
+
and records the author's id in a `dp_node` attribute. Avoid them, because the compiled
|
|
42
|
+
file then no longer says what the topology says.
|
|
43
|
+
|
|
44
|
+
The card id grammar is the other one: `^(?:namespace/)?[a-z0-9]+(-[a-z0-9]+)*$`. Lowercase,
|
|
45
|
+
hyphens, no underscores. **The two grammars are incompatible for every multi-word name**, so
|
|
46
|
+
always write the pin explicitly:
|
|
47
|
+
|
|
48
|
+
```dot
|
|
49
|
+
editorial_check [card="editorial-check@1.0.0"];
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`card="id@version"` is canonical and always wins. The bare `version="1.0.0"` fallback makes the
|
|
53
|
+
node id double as the card id, which only works for a single lowercase word. Do not use it.
|
|
54
|
+
An unpinned pointer (`card="solver"`, `card="solver@latest"`, `version="1.x"`) is
|
|
55
|
+
`bundle/unpinned-card`, an **error**.
|
|
56
|
+
|
|
57
|
+
### Attribute syntax
|
|
58
|
+
|
|
59
|
+
Attributes are **comma**-separated. `[a=1; b=2]` and `[a=1 b=2]` are both
|
|
60
|
+
`attractor/attr-separator`.
|
|
61
|
+
|
|
62
|
+
Comments are `//` or `/* … */`. A `#` comment is `attractor/hash-comment`.
|
|
63
|
+
|
|
64
|
+
No string concatenation (`"a" + "b"`), no `<html-like>` literals, and a duration is an integer
|
|
65
|
+
followed by one of `ms s m h d`. Anything else is `attractor/unsupported-value`.
|
|
66
|
+
|
|
67
|
+
### Never write `type=` on a node
|
|
68
|
+
|
|
69
|
+
`type` is a **reserved Attractor node attribute** and means *handler override*. DarkPrint's
|
|
70
|
+
node type is an ontology term inside the YAML card and never a DOT attribute. Writing it is
|
|
71
|
+
`attractor/reserved-attribute`, and the value would be read as something entirely different
|
|
72
|
+
from what you meant.
|
|
73
|
+
|
|
74
|
+
**That is the only reserved node attribute the linter reports.** The rest of Attractor's
|
|
75
|
+
reserved node names (`prompt`, `max_retries`, `llm_model`, `label`, `shape`, `class`,
|
|
76
|
+
`timeout`, `goal_gate`, `fidelity`, `thread_id`, `retry_target`, `fallback_retry_target`,
|
|
77
|
+
`llm_provider`, `reasoning_effort`, `auto_status`, `allow_partial`) pass through the linter
|
|
78
|
+
in silence and are written straight into the compiled file, where they configure the run.
|
|
79
|
+
A `prompt=` on a topology node replaces the card's `spec` at run time with nothing saying
|
|
80
|
+
so. Grep your own DOT for them before you call it finished. The bundle's own node
|
|
81
|
+
attributes are `card`, and optionally `digest` (a prefix of the card digest, checked as
|
|
82
|
+
`bundle/digest-mismatch`).
|
|
83
|
+
|
|
84
|
+
On an **edge**, `label`, `condition`, `weight`, `fidelity`, `thread_id` and `loop_restart` are
|
|
85
|
+
reserved. `label`, `condition` and `weight` are the three this skill writes deliberately, and
|
|
86
|
+
the next section is about them. `out=` and `in=` are not reserved, which is what lets the
|
|
87
|
+
port pins ride along safely.
|
|
88
|
+
|
|
89
|
+
### One graph, one file
|
|
90
|
+
|
|
91
|
+
Only the first graph in the file is read (`attractor/multiple-graphs`, `dot/unsupported`). It
|
|
92
|
+
must be a `digraph`, and every edge `->`; a `graph` or a `--` edge is `dot/not-directed`, an
|
|
93
|
+
**error** that stops the pipeline dead. Do not write `strict digraph`.
|
|
94
|
+
|
|
95
|
+
Only **one** `.dot` file goes in the bundle. The upload page classifies files by name: the
|
|
96
|
+
first `.dot` or `.gv` is the topology and a second one is demoted with a note.
|
|
97
|
+
|
|
98
|
+
### Filenames are load-bearing
|
|
99
|
+
|
|
100
|
+
The upload page, the CLI and the publish route all read roles off filenames, so:
|
|
101
|
+
|
|
102
|
+
- exactly one `topology.dot`;
|
|
103
|
+
- cards under `cards/`, named `<card-id>@<version>.yaml`;
|
|
104
|
+
- `blueprint.yaml` at the root is the manifest (slug, title, summary, description, category,
|
|
105
|
+
tags), and `ontology/extensions.yaml` is the local vocabulary when there is one;
|
|
106
|
+
- **never name a card `blueprint.yaml` or `extensions.yaml`.** A card named either is
|
|
107
|
+
classified as the manifest or the vocabulary, vanishes from the card set, and every node
|
|
108
|
+
pinning it reports `bundle/missing-card`.
|
|
109
|
+
|
|
110
|
+
Nothing in the engine requires a card's filename to match its `id` and `version`; the card's
|
|
111
|
+
own fields are its identity. Matching them is a courtesy to whoever opens the folder, and this
|
|
112
|
+
skill always does it.
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Part 2: the guard on a fork
|
|
117
|
+
|
|
118
|
+
A node with two or more outgoing edges is a fork, and a runner takes exactly one arm. Which
|
|
119
|
+
one is decided in a fixed order, from the Attractor spec's edge selection:
|
|
120
|
+
|
|
121
|
+
1. an edge whose `condition` holds;
|
|
122
|
+
2. an edge whose `label` matches the label the node's own status asked for
|
|
123
|
+
(`preferred_label`);
|
|
124
|
+
3. an edge to a node id the status suggested as next;
|
|
125
|
+
4. the higher `weight`, default 0;
|
|
126
|
+
5. on a tie, **the target node id that sorts first**.
|
|
127
|
+
|
|
128
|
+
So two bare edges out of one node are a branch decided by the spelling of the node names.
|
|
129
|
+
The shipped starter learned this the hard way: while `tester -> debugger` and
|
|
130
|
+
`tester -> deployer` carried a label and nothing else, `debugger` beat `deployer` on the
|
|
131
|
+
fourth character and the factory repaired forever without releasing. Every fork this skill
|
|
132
|
+
writes carries a `condition` on every arm, or a `weight` the author chose on purpose.
|
|
133
|
+
|
|
134
|
+
### The grammar
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
Condition ::= Clause ( '&&' Clause )*
|
|
138
|
+
Clause ::= Key Operator Literal
|
|
139
|
+
Key ::= 'outcome' | 'preferred_label' | 'context.' Path
|
|
140
|
+
Operator ::= '=' | '!='
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
`outcome` is the value a runner sets from the node's own status on every pass, and it is
|
|
144
|
+
the key to use for a verdict. `preferred_label` is the label the node asked for, when a
|
|
145
|
+
node writes one. `context.<path>` reads a value the run has stored under a dotted path.
|
|
146
|
+
|
|
147
|
+
The literal is compared as text, so `outcome=success`, `outcome!=success` and
|
|
148
|
+
`context.retries=2` all parse. Quote a literal with spaces: `preferred_label="ship it"`.
|
|
149
|
+
|
|
150
|
+
What does **not** parse, and what the linter reports as `attractor/condition-syntax`:
|
|
151
|
+
`||`, `!`, `>`, `<`, `>=`, `<=`, `contains`, `matches`, parentheses, a bare key
|
|
152
|
+
(`tests_pass`), a bare literal (`success`), and the empty string. A runner grades that rule
|
|
153
|
+
as an error and refuses the whole pipeline before the first node runs, so a mistyped guard
|
|
154
|
+
is a blueprint that validates and never executes. Note `condition=""` separately: it is not
|
|
155
|
+
a syntax error to a runner but it is NO condition, so the edge falls back to the order above.
|
|
156
|
+
|
|
157
|
+
### The shape this skill writes
|
|
158
|
+
|
|
159
|
+
```dot
|
|
160
|
+
check_step -> fix_step [label="failure evidence", style=dashed, condition="outcome!=success"];
|
|
161
|
+
check_step -> ship_step [label="approved", condition="outcome=success"];
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
One key against its own negation is the only total split the grammar can make: there is no
|
|
165
|
+
disjunction, so `partial_success`, `retry`, `fail` and a verdict the check could not reach
|
|
166
|
+
all land on the arm that does not ship, and exactly one arm is ever eligible. A `decision`
|
|
167
|
+
node, whose whole job is to route, follows the same rule with `context.<path>` keys when the
|
|
168
|
+
route is not a plain verdict, and every arm still carries a guard.
|
|
169
|
+
|
|
170
|
+
`weight` is for a preference among arms that are all allowed: `weight=10` on the arm to
|
|
171
|
+
take when nothing else decides. It is an integer, and the linter checks nothing about it.
|
|
172
|
+
|
|
173
|
+
Nothing in DarkPrint evaluates a guard. A guarded edge counts exactly as much as an
|
|
174
|
+
unguarded one in every risk reading, because the guard runs on somebody else's machine
|
|
175
|
+
against data the engine never sees. `condition="false"` does not remove an edge from the
|
|
176
|
+
criteria walk.
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## Part 3: what a card becomes
|
|
181
|
+
|
|
182
|
+
`darkprint export <dir> --attractor` compiles a resolved bundle into an Attractor DOT and
|
|
183
|
+
writes it to stdout. **This skill does not write that file**, and a published bundle folder
|
|
184
|
+
does not carry one: duplicating the emit rules here would let the two drift, and the
|
|
185
|
+
exporter is the one that gets tested against Attractor's parser on every build.
|
|
186
|
+
|
|
187
|
+
It is documented because an author writing a `spec` should know it is the thing that will be
|
|
188
|
+
handed to the agent verbatim.
|
|
189
|
+
|
|
190
|
+
| the card says | the compiled node says |
|
|
191
|
+
|---|---|
|
|
192
|
+
| `spec` | `prompt`, the payload the agent actually receives |
|
|
193
|
+
| `name` | `label` |
|
|
194
|
+
| `model` | `llm_model`, Attractor's reserved model identifier |
|
|
195
|
+
| `params.max_retries` (or `max_iterations`, or `maxIterations`) | `max_retries` |
|
|
196
|
+
| `params.tool_command`, on a `shell-tool` | `tool_command` |
|
|
197
|
+
| `type` and `phases` | `class`, every name prefixed `dp-` |
|
|
198
|
+
| `type: agent` | `shape=box` → the `codergen` handler |
|
|
199
|
+
| `type: validation` | `shape=box` → the `codergen` handler |
|
|
200
|
+
| `type: tool` | `shape=box` → the `codergen` handler |
|
|
201
|
+
| `type: human-gate` | `shape=hexagon` → the `wait.human` handler |
|
|
202
|
+
| `type: human-input` | `shape=hexagon` → the `wait.human` handler |
|
|
203
|
+
| `type: decision` | `shape=diamond` → the `conditional` handler |
|
|
204
|
+
| `type: shell-tool` | `shape=parallelogram` → the `tool` handler |
|
|
205
|
+
| `type: parallel` | `shape=component` → the `parallel` handler |
|
|
206
|
+
| `type: parallel.fan-in` | `shape=tripleoctagon` → the `parallel.fan_in` handler |
|
|
207
|
+
| `type: manager-loop` | `shape=house` → the `stack.manager_loop` handler |
|
|
208
|
+
| edge `label`, `condition`, `weight` | the same three edge attributes, carried verbatim |
|
|
209
|
+
| the manifest's `summary` and `title` | graph `goal` and `label` |
|
|
210
|
+
|
|
211
|
+
A `__start` and a `__exit` node are **synthesised** rather than borrowed from your graph: your
|
|
212
|
+
entry node is an ordinary card node with a prompt to run, and re-shaping it into a boundary
|
|
213
|
+
would mean that prompt never runs. `card="id@version"` rides along in the compiled file
|
|
214
|
+
untouched, because Attractor ignores attributes it does not reserve.
|
|
215
|
+
|
|
216
|
+
### What the compiled file cannot say
|
|
217
|
+
|
|
218
|
+
The compiled DOT opens with a comment listing every attribute Attractor reads that a
|
|
219
|
+
DarkPrint blueprint has no way to set. That list is derived from the engine rather than
|
|
220
|
+
written by hand, so it is the current answer and not a sentence somebody forgot to update.
|
|
221
|
+
Today it covers goal gates, timeouts, the whole retry policy above `max_retries`, fidelity,
|
|
222
|
+
thread ids, the parallel join policy, the manager-loop controls and the graph-level
|
|
223
|
+
defaults, hooks and model stylesheet. Each one falls back to whatever the runner's own
|
|
224
|
+
default is, and nothing warns anybody: a gate nobody wrote is a gate that never fires. An
|
|
225
|
+
author who needs one adds it to the compiled file by hand, and a later export replaces the
|
|
226
|
+
whole file. The same two lists are printed into every published bundle's `README.md`, under
|
|
227
|
+
"What these files leave to the runner".
|
|
228
|
+
|
|
229
|
+
The other half is what stops at the DarkPrint boundary. A node's `prompt` is everything the
|
|
230
|
+
runner receives from its card, so ports, dependencies, `cannot`, `will_not` and
|
|
231
|
+
`risk_markers` are not enforced by anything in the compiled file. They are enforced by the
|
|
232
|
+
engine, at the moment the bundle is validated, which is a different moment from the moment
|
|
233
|
+
the pipeline runs.
|
|
234
|
+
|
|
235
|
+
Three consequences for how you write a card:
|
|
236
|
+
|
|
237
|
+
1. **`spec` is the prompt.** It is delivered to an agent that does not see the rest of the
|
|
238
|
+
graph, so it has to be self-sufficient, and it must respect the isolation the topology
|
|
239
|
+
declares. An absent edge with the criteria paraphrased into the prose is a false isolation.
|
|
240
|
+
2. **`model` is what the node runs on unless somebody edits the file.** DarkPrint writes
|
|
241
|
+
`llm_model` and writes no `model_stylesheet`, so there is no sheet in the compiled graph
|
|
242
|
+
to override it. Absence is still an answer: a card that names no model emits no
|
|
243
|
+
`llm_model` at all, and the node takes whatever the runner supplies.
|
|
244
|
+
3. **The iteration cap has one home.** Write it as a top-level key of `params`. The security
|
|
245
|
+
analyzer and the exporter read it through the same function, so a cap that reads as
|
|
246
|
+
uncapped would also compile as unbounded. And it is the number Attractor counts, attempts
|
|
247
|
+
after the first, so `max_retries: 2` is three attempts in all.
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## Part 4: reading a pipeline back
|
|
252
|
+
|
|
253
|
+
`darkprint import <pipeline.dot> --as <handle> --out <dir>` is the other direction. It reads
|
|
254
|
+
an Attractor pipeline and writes a **draft** bundle: a `topology.dot` and one card per node,
|
|
255
|
+
in the layout the upload page and `darkprint validate` both accept. The mapping table above
|
|
256
|
+
is run backwards, from the same table, so the two directions cannot drift apart.
|
|
257
|
+
|
|
258
|
+
| the pipeline says | the card says |
|
|
259
|
+
|---|---|
|
|
260
|
+
| `prompt` | `spec` |
|
|
261
|
+
| `label` | `name` |
|
|
262
|
+
| `llm_model` | `model` |
|
|
263
|
+
| `max_retries` | `params.max_iterations`, the first of the three accepted cap keys, holding the same number |
|
|
264
|
+
| `tool_command` | `params.tool_command`, on a `shell-tool` card |
|
|
265
|
+
| `shape` | `type`, through the table above |
|
|
266
|
+
| `class` | which of a shared shape's two types, and the `phase` list |
|
|
267
|
+
| `card="id@version"` | the card's own `id` and `version` |
|
|
268
|
+
|
|
269
|
+
### Both flags are required, and neither has a default
|
|
270
|
+
|
|
271
|
+
`--out` because a bundle is a folder and a folder cannot go down a pipe. `--as` because a
|
|
272
|
+
`prompt` is somebody's writing: every synthesised card carries `author` set to the handle you
|
|
273
|
+
give and `provenance` set to `derived:attractor <the pipeline>`, so a compiled card can be
|
|
274
|
+
told from a written one by reading it or by grepping for the marker. Every card comes out at
|
|
275
|
+
version `0.1.0`. It is a draft and it is meant to be edited before anybody publishes it.
|
|
276
|
+
|
|
277
|
+
### What a round trip keeps, and what it does not
|
|
278
|
+
|
|
279
|
+
`tests/attractor-round-trip.test.ts` runs a corpus of pipelines out and back and asserts that
|
|
280
|
+
every attribute an Attractor runner reads survives unchanged. What it does not keep is
|
|
281
|
+
recorded there too, exactly, rather than left to be discovered:
|
|
282
|
+
|
|
283
|
+
- **Ports, dependencies, `cannot`, `will_not` and `risk_markers` come back empty.** No
|
|
284
|
+
Attractor file has ever carried them, so an import cannot invent them. You write them.
|
|
285
|
+
- **A node with no `prompt` becomes a card with no `spec`.** Attractor allows it; a DarkPrint
|
|
286
|
+
card does not. The import says which node, and the bundle does not resolve until you write
|
|
287
|
+
the missing half.
|
|
288
|
+
- **`shape=box` comes back as `agent` and `shape=hexagon` as `human-gate`** unless the file
|
|
289
|
+
carries the `class` DarkPrint writes. Attractor selects one handler for `agent` and
|
|
290
|
+
`validation` alike, and stores nothing that separates them.
|
|
291
|
+
- **Subgraphs are gone**, and with them the classes derived from a subgraph's label.
|
|
292
|
+
- **Every attribute in the compiled file's own disclosure header is dropped**: goal gates,
|
|
293
|
+
timeouts, the retry policy above `max_retries`, the parallel and manager-loop controls, the
|
|
294
|
+
graph defaults, hooks and the model stylesheet.
|
|
295
|
+
- **An attribute on an edge to the start or the exit is dropped**, because DarkPrint
|
|
296
|
+
synthesises both boundary nodes and derives their wiring rather than storing it.
|
|
297
|
+
|
|
298
|
+
A DarkPrint bundle that goes out through `export` and back through `import` comes home byte
|
|
299
|
+
for byte, the blueprint digest line aside. The losses above are what a *foreign* pipeline
|
|
300
|
+
pays, and they are the price of the two formats not being the same size.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# The live preview
|
|
2
|
+
|
|
3
|
+
An optional window on the interview. When the author opts in, the blueprint-writing skill
|
|
4
|
+
posts the draft to darkprint.io after every phase, and the page at
|
|
5
|
+
`https://www.darkprint.io/tutorial/live/<token>` draws the graph as it takes shape, in the
|
|
6
|
+
same panel a published blueprint page uses. The site runs nothing: it keeps the last draft it
|
|
7
|
+
received and shows it. Every field below is transcribed from `lib/core/tutorial/live.ts`, the
|
|
8
|
+
one contract the skill, the routes and the page share.
|
|
9
|
+
|
|
10
|
+
## The three endpoints
|
|
11
|
+
|
|
12
|
+
| call | body | answers |
|
|
13
|
+
|---|---|---|
|
|
14
|
+
| `POST https://www.darkprint.io/api/tutorial/live` | `{}` | `{ token, url, expiresAt }` |
|
|
15
|
+
| `GET https://www.darkprint.io/api/tutorial/live/<token>` | none | `{ token, revision, updatedAt, expiresAt, draft }`, with `ETag: "<revision>"` and a 304 on `If-None-Match` |
|
|
16
|
+
| `PUT https://www.darkprint.io/api/tutorial/live/<token>` | a `LiveDraft` | `{ revision, updatedAt, expiresAt }` |
|
|
17
|
+
|
|
18
|
+
The token is 32 URL-safe characters, `[A-Za-z0-9_-]{32}`, and anything else is refused
|
|
19
|
+
before the store is asked. A page lives 24 hours from the moment it was opened, and every
|
|
20
|
+
accepted PUT refreshes that. The contract caps one draft at 512 KiB of JSON. A 400 names the
|
|
21
|
+
field that is wrong in one sentence; fix the payload and send again. The page polls the GET
|
|
22
|
+
itself, so the skill never calls it.
|
|
23
|
+
|
|
24
|
+
Open once and remember the token from the answer:
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
curl -fsS -X POST https://www.darkprint.io/api/tutorial/live \
|
|
28
|
+
-H "content-type: application/json" --data '{}'
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
When the author arrived with the URL already, the token is its last path segment and the POST
|
|
32
|
+
is skipped; when that URL's origin is not `https://www.darkprint.io`, use its origin in every
|
|
33
|
+
line here, because the page lives where it was opened. Each shell command may run in a fresh
|
|
34
|
+
shell, so set the variable at the head of every command rather than exporting it once. Then,
|
|
35
|
+
at each boundary, the JSON goes on stdin, so nothing is written to disk before the author has
|
|
36
|
+
said yes to the files:
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
DARKPRINT_LIVE_TOKEN=<token>; \
|
|
40
|
+
curl -fsS -X PUT "https://www.darkprint.io/api/tutorial/live/$DARKPRINT_LIVE_TOKEN" \
|
|
41
|
+
-H "content-type: application/json" --data-binary @- <<'JSON'
|
|
42
|
+
{ "phase": "nodes", "task": "...", "bundle": { "manifest": { ... }, "dot": "...", "cardFiles": { ... } }, "ledger": { ... } }
|
|
43
|
+
JSON
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
A failed PUT, whether the network is down or the answer is a 400, is one line to the author,
|
|
47
|
+
and the interview goes on. The folder is the deliverable; the page is a window on it.
|
|
48
|
+
|
|
49
|
+
## `LiveDraft`, field by field
|
|
50
|
+
|
|
51
|
+
| field | required | what |
|
|
52
|
+
|---|---|---|
|
|
53
|
+
| `phase` | yes | one of `need`, `reuse`, `nodes`, `ports`, `guards`, `risk`, `written`, `enriched`, `published` |
|
|
54
|
+
| `task` | no | the author's one-sentence answer to Q0.1 |
|
|
55
|
+
| `bundle.manifest` | yes | `slug`, `title`, `summary` as strings and `tags` as an array of strings; `description` and `category` ride along once Q5.3 derives them |
|
|
56
|
+
| `bundle.dot` | yes | the DOT so far; `""` before any node has a name |
|
|
57
|
+
| `bundle.cardFiles` | yes | an object keyed `cards/<id>@<version>.yaml` whose values are the YAML text; `{}` before any card is drafted |
|
|
58
|
+
| `ledger` | no | `settled`, `open`, `blocked`: the three lists of posture rule 4, each an array of strings |
|
|
59
|
+
| `hits` | no | every registry hit considered, each `{ kind, ref, title, score }`: `kind` is `blueprint` or `card`, `ref` is `owner/slug` or `id@version`, `score` is the search module's own number |
|
|
60
|
+
| `publishedRef` | no | `owner/slug` once the author has published, so the page can link to it |
|
|
61
|
+
|
|
62
|
+
`hits` comes straight off the search answer. A blueprint hit already carries `ref`, `title`
|
|
63
|
+
and `score`; a card hit carries `name` where the draft wants `title`, so copy `name` into it.
|
|
64
|
+
Send every hit shown to the author, including the ones recommended against; the page is
|
|
65
|
+
meant to show the search happening.
|
|
66
|
+
|
|
67
|
+
**Before Q5.3 has derived the manifest**, send provisional values, because the PUT refuses a
|
|
68
|
+
draft without them: `title` is Q0.1 as a title, `summary` is the Q0.1 sentence, `slug` is the
|
|
69
|
+
title in card-id grammar (`^[a-z0-9]+(-[a-z0-9]+)*$`), `tags` is `[]`. Replace them with the
|
|
70
|
+
derived ones at `risk`.
|
|
71
|
+
|
|
72
|
+
**The bundle may be partial.** The PUT checks the shape of the JSON and nothing else: a draft
|
|
73
|
+
that `darkprint validate` would refuse with `bundle/port-mismatch` is still accepted, and the
|
|
74
|
+
page draws what the engine can resolve and lists the rest as still to settle. Send the DOT as
|
|
75
|
+
soon as the nodes have names, and each card as soon as it exists in memory. The files on the
|
|
76
|
+
author's disk are written when SKILL.md says and not before; the draft is a copy of what is
|
|
77
|
+
settled so far, never a copy of the folder.
|
|
78
|
+
|
|
79
|
+
## The phases, and when to send each
|
|
80
|
+
|
|
81
|
+
| `phase` | send it | the page's label, and what the draft carries by then |
|
|
82
|
+
|---|---|---|
|
|
83
|
+
| `need` | after Q0.6, before searching | "The need": the task sentence and no graph yet |
|
|
84
|
+
| `reuse` | after Phase 1, whether or not anything fit | "Reuse before drawing": the hits considered, with their scores |
|
|
85
|
+
| `nodes` | after Phase 2, once the split is agreed | "The nodes": a DOT with named nodes and no edges |
|
|
86
|
+
| `ports` | after Q3.6, when the ledger closes | "Ports and edges": the derived edges, and the ports still unmatched in `ledger.open` |
|
|
87
|
+
| `guards` | after Q4.7 | "Isolation and guards": the `cannot` entries and the condition on every fork |
|
|
88
|
+
| `risk` | after the show-back, before writing | "Risk and identity": the manifest as derived, the risk markers, the names |
|
|
89
|
+
| `written` | after the files are written and validated | "Folder written": the whole bundle, as it sits on disk |
|
|
90
|
+
| `enriched` | after an enrich merge | "Enriched from the registry": the grown graph, and the hits that fed it |
|
|
91
|
+
| `published` | after the author publishes, with `publishedRef` | "Published to an account": a link to the blueprint's page |
|
|
92
|
+
|
|
93
|
+
The quoted words are `LIVE_PHASE_LABELS` from the contract, the heading the page prints for
|
|
94
|
+
each phase. What follows each is what the draft carries at that point; the page draws
|
|
95
|
+
whatever of it resolves, and a draft sent twice under the same phase replaces the earlier one.
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
GENERATED FILE. Do not edit by hand.
|
|
3
|
+
Rendered from lib/core/ontology/core.ts and lib/core/config.ts by scripts/skill-refs.ts.
|
|
4
|
+
Regenerate with: npm run generate:skill-refs
|
|
5
|
+
scripts/generate-skill-refs.test.ts fails the suite if this file drifts.
|
|
6
|
+
-->
|
|
7
|
+
|
|
8
|
+
# DarkPrint core vocabulary
|
|
9
|
+
|
|
10
|
+
54 terms. The vocabulary carries no version of its own: it names what an
|
|
11
|
+
Attractor node IS, and Attractor fixes those shapes in its own spec.
|
|
12
|
+
|
|
13
|
+
Every card field that names a term is resolved against this list. A term that is not
|
|
14
|
+
here is `card/unknown-term` (error). A term of the wrong kind, a `data-type` in the
|
|
15
|
+
`tools` list or a `tool` in `type`, is `card/wrong-term-kind` (error).
|
|
16
|
+
|
|
17
|
+
A card names no vocabulary version either. There is one vocabulary and every card is
|
|
18
|
+
read against it, so writing `ontology_version:` on a card is `card/retired-field`
|
|
19
|
+
(warning).
|
|
20
|
+
|
|
21
|
+
## phase: the five, closed
|
|
22
|
+
|
|
23
|
+
Optional and repeatable. A card may declare none, one, or several. `phase: []`, or the
|
|
24
|
+
field omitted entirely, is a **complete and correct answer**, and the validator emits
|
|
25
|
+
nothing at all about it: an intake step, a retrieval step and a memory store sit in none
|
|
26
|
+
of the five. Phase coverage is descriptive and nothing scores off it, so never invent a
|
|
27
|
+
phase to fill a strip.
|
|
28
|
+
|
|
29
|
+
This is the one dimension that is never namespaced: `me/triage` as a phase is
|
|
30
|
+
`card/namespaced-phase` (error). Listed in lifecycle order, which is the order coverage
|
|
31
|
+
reports in.
|
|
32
|
+
|
|
33
|
+
| id | label | meaning |
|
|
34
|
+
| --- | --- | --- |
|
|
35
|
+
| `planning` | Planning | From the request to a plan and the acceptance criteria. |
|
|
36
|
+
| `implementation` | Implementation | From the plan to the artefact. |
|
|
37
|
+
| `testing` | Testing | Runs the checks and produces the evidence. |
|
|
38
|
+
| `debugging` | Debugging | From failure evidence to a targeted fix. |
|
|
39
|
+
| `deployment` | Deployment | Release, publication, delivery. |
|
|
40
|
+
|
|
41
|
+
## node-type: what does the job
|
|
42
|
+
|
|
43
|
+
Exactly one per card, in `type`. Three of these are **abstract categories** and a node
|
|
44
|
+
should not be typed with one: they exist so the metrics can ask a subsumption question.
|
|
45
|
+
|
|
46
|
+
`human-gate` and `human-input` are subsumed by `human-in-the-loop` and carry
|
|
47
|
+
`impliesHuman`. Declaring one of them is the whole of how a card says a person acts at
|
|
48
|
+
the node: there is no second field beside `type` to set, and nothing else on the card
|
|
49
|
+
can say otherwise.
|
|
50
|
+
|
|
51
|
+
The `orchestration` branch is control flow: `parallel` splits the run, `parallel.fan-in`
|
|
52
|
+
joins it back, `manager-loop` supervises a sub-run and decides whether it repeats. The
|
|
53
|
+
three are named after the Attractor handlers they compile to, so a bundle's `topology.dot`
|
|
54
|
+
reads the same on both sides.
|
|
55
|
+
|
|
56
|
+
Autonomy is read twice off `type` and the weaker reading is the one that lands in a band.
|
|
57
|
+
How much runs alone: the share of nodes whose `type` is *not* subsumed by
|
|
58
|
+
`human-in-the-loop`. How much of the deciding runs alone: the same share taken over the
|
|
59
|
+
**control points** only, which are the nodes under `evaluative` or `orchestration` plus
|
|
60
|
+
`human-gate`, and it decides a band only from 2 control points up. Bands: > 0.9 closed-loop, ≥ 0.7 conditional, ≥ 0.5 supervised, below that assisted.
|
|
61
|
+
|
|
62
|
+
| id | label | broader | implies human | governs flow | meaning |
|
|
63
|
+
| --- | --- | --- | --- | --- | --- |
|
|
64
|
+
| `agent` | Agent | — | no | no | A model that reasons and produces non-deterministic output. |
|
|
65
|
+
| `decision` | Decision | `evaluative` | no | yes | A conditional switch that evaluates and routes without producing artefacts. |
|
|
66
|
+
| `evaluative` | Evaluative | — | no | yes | The abstract category of nodes that judge or route rather than produce an artefact. |
|
|
67
|
+
| `human-gate` | Human gate | `human-in-the-loop` | yes | yes | A point where a person must approve or reject. |
|
|
68
|
+
| `human-in-the-loop` | Human in the loop | — | no | no | The abstract category of nodes at which a person acts, and the one the autonomy metric interrogates. |
|
|
69
|
+
| `human-input` | Human input | `human-in-the-loop` | yes | no | A point where a person must supply data or content. |
|
|
70
|
+
| `manager-loop` | Manager loop | `orchestration` | no | yes | Supervises a sub-run, polling the work and deciding whether to act on it and whether to go round again until its stop condition holds. |
|
|
71
|
+
| `orchestration` | Orchestration | — | no | yes | The abstract category of nodes that shape the run itself: how many copies of a step exist, when they converge, whether the whole thing repeats. |
|
|
72
|
+
| `parallel` | Parallel | `orchestration` | no | yes | Splits the run into branches that proceed at the same time, producing nothing itself and deciding only how many copies of the work exist. |
|
|
73
|
+
| `parallel.fan-in` | Parallel fan-in | `orchestration` | no | yes | Waits for the branches a parallel node opened and joins them back into one line, deciding when the run continues rather than what it continues with. |
|
|
74
|
+
| `shell-tool` | Shell tool | `tool` | no | no | A tool node whose instruction is a shell command the runner executes directly. |
|
|
75
|
+
| `tool` | Tool | — | no | no | A deterministic operation: running tests, compiling, formatting, calling an API. |
|
|
76
|
+
| `validation` | Validation | `evaluative` | no | yes | Compares an artefact against criteria and produces a verdict with evidence. |
|
|
77
|
+
|
|
78
|
+
**The trap.** `tool`'s own description names running tests, which invites typing the
|
|
79
|
+
test runner `tool`. The `criteria-leak` check defines its generator set as the
|
|
80
|
+
predecessors of nodes typed `validation`; type the judge `tool` and that set is empty,
|
|
81
|
+
the check does not run, and the blueprint scores 4 on security because nothing was
|
|
82
|
+
asked, not because nothing was found. The engine says so with
|
|
83
|
+
`analysis/criteria-leak-unanchored` (warning). Whatever decides the run is finished is
|
|
84
|
+
`validation`.
|
|
85
|
+
|
|
86
|
+
## risk-marker: what it costs
|
|
87
|
+
|
|
88
|
+
Declared in `risk_markers`. The security score starts at 4 and each distinct marker
|
|
89
|
+
present anywhere in the blueprint is charged **once**, however many nodes carry it:
|
|
90
|
+
`clamp(round(4 − Σ weights), 1, 4)`. Weights are read from `lib/core/config.ts`; a locally
|
|
91
|
+
namespaced marker with no weight counts 0
|
|
92
|
+
and does not move the score.
|
|
93
|
+
|
|
94
|
+
Three markers are **inferred** from the graph whether or not any card declares them.
|
|
95
|
+
Declaring one the engine would have inferred anyway changes nothing; failing to declare
|
|
96
|
+
one does not hide it.
|
|
97
|
+
|
|
98
|
+
| id | weight | how it arrives | broader | meaning |
|
|
99
|
+
| --- | --- | --- | --- | --- |
|
|
100
|
+
| `arbitrary-code-execution` | −2.0 | declared | `execution-risk` | The node can run code or shell commands that were not decided in advance. |
|
|
101
|
+
| `criteria-leak` | −2.0 | inferred | `isolation-breach` | The node can see the acceptance criteria its own output will be judged against. |
|
|
102
|
+
| `execution-risk` | — | category | — | The abstract category for markers about running code the blueprint did not fix in advance. |
|
|
103
|
+
| `irreversible-action` | −1.5 | declared | — | The node takes actions that cannot be undone: publishing, sending, deleting. |
|
|
104
|
+
| `isolation-breach` | — | category | — | The abstract category for markers where information or state crosses a boundary the topology was meant to hold. |
|
|
105
|
+
| `secret-access` | −1.0 | declared | — | The node handles credentials, keys or tokens. |
|
|
106
|
+
| `unbounded-loop` | −1.5 | inferred | — | The node sits in a cycle with no iteration cap and no exit condition. |
|
|
107
|
+
| `unchecked-write` | −1.0 | declared | `isolation-breach` | The node writes to disk, a database or a repository with no check upstream. |
|
|
108
|
+
| `unvalidated-external-access` | −1.0 | inferred | — | The node reaches the network, an API or an external resource with no validation node upstream. |
|
|
109
|
+
|
|
110
|
+
How the three inferred ones are found:
|
|
111
|
+
|
|
112
|
+
- `unbounded-loop`: every strongly connected component in the graph, unless some card
|
|
113
|
+
in it declares an iteration cap. The cap is a **top-level** key of `params`, one of
|
|
114
|
+
`max_iterations`, `maxIterations`, `max_retries`, holding a non-negative
|
|
115
|
+
integer (`0` counts). Nested inside another object it is not read, and the cycle takes
|
|
116
|
+
the charge on every member with no obvious cause.
|
|
117
|
+
- `unvalidated-external-access`: a node whose `tools` include anything subsumed by
|
|
118
|
+
`web-search`, `http-fetch`, `sql` or `ci`, which has at least one successor, and at
|
|
119
|
+
least one of those successors is not a `validation` node. `messaging`, `git`,
|
|
120
|
+
`vector-store`, `file-io`, `shell` and `python-sandbox` are deliberately excluded.
|
|
121
|
+
- `criteria-leak`: see below. This is the one the whole design is built around.
|
|
122
|
+
|
|
123
|
+
### criteria-leak, precisely
|
|
124
|
+
|
|
125
|
+
Two sets are computed first. **Producers** are nodes declaring an output port whose type
|
|
126
|
+
is subsumed by `acceptance-criteria`. **Judges** are nodes typed `validation`, and
|
|
127
|
+
**generators** are the predecessors of any judge, closed upward through non-judge nodes.
|
|
128
|
+
The marker fires, at −2.0, when:
|
|
129
|
+
|
|
130
|
+
- **topological**: walking forward from a producer, absorbing at judges, reaches a
|
|
131
|
+
generator. The criteria reach the node whose work is being judged.
|
|
132
|
+
- **declarative**: one node emits both an `acceptance-criteria` port *and* another port
|
|
133
|
+
a directly connected judge reads as the artefact under judgement. One node writing the
|
|
134
|
+
criteria and the work is structurally illegal, off the declarations alone.
|
|
135
|
+
|
|
136
|
+
And it warns without charging when:
|
|
137
|
+
|
|
138
|
+
- **content**: the 3-gram Jaccard similarity between a generator's `spec` and a producer's
|
|
139
|
+
exceeds 0.35
|
|
140
|
+
(`analysis/criteria-leak-suspected`). An absent edge with the criteria paraphrased into
|
|
141
|
+
the prose is a false isolation, and this is the half that catches it. Specs under three
|
|
142
|
+
words are excluded from the comparison entirely.
|
|
143
|
+
- **relayed**: reachable only by walking *through* a judge
|
|
144
|
+
(`analysis/criteria-relayed-through-judge`). The engine cannot tell an endorsed
|
|
145
|
+
`judge → fixer → judge` loop from a forbidden `judge → builder → judge` one, so it
|
|
146
|
+
declines to decide and says which it saw.
|
|
147
|
+
- **out of band**: a `params` key matching `/criteri/i` naming something nothing in the
|
|
148
|
+
graph produces (`analysis/criteria-out-of-band`). Isolation has stopped being a property
|
|
149
|
+
of the topology for that node.
|
|
150
|
+
- **unanchored**: one of the two legs is missing: producers with no generators, or
|
|
151
|
+
generators with no producers (`analysis/criteria-leak-unanchored`). **The check did not
|
|
152
|
+
run.** A 4 in this state is silence, not a pass. Both sets empty is silent by design.
|
|
153
|
+
|
|
154
|
+
## data-type: what an edge carries
|
|
155
|
+
|
|
156
|
+
Every port declares one, in `type`. Compatibility along an edge is directional: a source
|
|
157
|
+
port fits a target port when the types are equal, when either side is `any`, or when the
|
|
158
|
+
**source is narrower** than the target. Never the other way. `code → text` carries;
|
|
159
|
+
`text → code` is `bundle/type-mismatch` (error).
|
|
160
|
+
|
|
161
|
+
**Do not reach for `any`.** It matches everything, which means every edge passes, no
|
|
162
|
+
`cannot` prohibition can be violated, and the criteria check has nothing to anchor on. A
|
|
163
|
+
bundle typed `any` throughout loads perfectly and checks nothing.
|
|
164
|
+
|
|
165
|
+
`acceptance-criteria` is load-bearing: it is the port type the entire `criteria-leak`
|
|
166
|
+
machinery anchors on. Criteria typed `text` or `structured` are criteria the engine
|
|
167
|
+
cannot see.
|
|
168
|
+
|
|
169
|
+
| id | label | broader | meaning |
|
|
170
|
+
| --- | --- | --- | --- |
|
|
171
|
+
| `acceptance-criteria` | Acceptance criteria | `structured` | The conditions an artefact must satisfy, produced in planning and used to judge the result. |
|
|
172
|
+
| `any` | Any | — | The top of the data lattice, which accepts anything and asserts nothing. |
|
|
173
|
+
| `artifact` | Artifact | `binary` | A build output or file bundle produced by the run. |
|
|
174
|
+
| `binary` | Binary | `any` | Opaque bytes the graph moves without reading. |
|
|
175
|
+
| `code` | Code | `text` | Source code in some language, meant to be run or reviewed rather than read as prose. |
|
|
176
|
+
| `event` | Event | `signal` | A notice that something happened, with a name and a time. |
|
|
177
|
+
| `json` | JSON | `structured` | A JSON value, self-describing and machine-parsable. |
|
|
178
|
+
| `markdown` | Markdown | `text` | Text carrying Markdown structure: headings, lists, emphasis. |
|
|
179
|
+
| `plan` | Plan | `structured` | An ordered set of steps a downstream node is expected to carry out. |
|
|
180
|
+
| `report` | Report | `structured` | A finished write-up of what happened or what was found, meant to be read. |
|
|
181
|
+
| `signal` | Signal | `any` | A lightweight message that carries coordination rather than content. |
|
|
182
|
+
| `status` | Status | `signal` | The outcome of a step, in a form the graph can branch on. |
|
|
183
|
+
| `structured` | Structured | `any` | Data with a shape the receiving node can count on. |
|
|
184
|
+
| `table` | Table | `structured` | Rows over a fixed set of columns. |
|
|
185
|
+
| `text` | Text | `any` | Free-form prose with no structure the next node can rely on. |
|
|
186
|
+
|
|
187
|
+
The lattice, as `broader` draws it:
|
|
188
|
+
|
|
189
|
+
```
|
|
190
|
+
any
|
|
191
|
+
binary
|
|
192
|
+
artifact
|
|
193
|
+
signal
|
|
194
|
+
event
|
|
195
|
+
status
|
|
196
|
+
structured
|
|
197
|
+
acceptance-criteria
|
|
198
|
+
json
|
|
199
|
+
plan
|
|
200
|
+
report
|
|
201
|
+
table
|
|
202
|
+
text
|
|
203
|
+
code
|
|
204
|
+
markdown
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## tool: what a node is permitted to do
|
|
208
|
+
|
|
209
|
+
Declared in `tools`. Note the deliberate id collision: `tool` is both a `node-type` and
|
|
210
|
+
the *kind* of these terms. The field a term appears in decides which is meant, so there
|
|
211
|
+
is no ambiguity to resolve: `type: tool` is the node type and `tools: [shell]` is a
|
|
212
|
+
capability.
|
|
213
|
+
|
|
214
|
+
`tools` says what the node is permitted to do. `mcp` says which installed server supplies
|
|
215
|
+
it, is free text, and is checked against nothing, because an MCP server is a process somebody
|
|
216
|
+
installed and the vocabulary has no term for one. A node can carry either without the other.
|
|
217
|
+
|
|
218
|
+
| id | label | broader | meaning |
|
|
219
|
+
| --- | --- | --- | --- |
|
|
220
|
+
| `ci` | CI | `tool-capability` | Triggers or inspects a continuous-integration pipeline. |
|
|
221
|
+
| `file-io` | File I/O | `tool-capability` | Reads and writes files on the local filesystem. |
|
|
222
|
+
| `git` | Git | `tool-capability` | Reads and writes a Git repository: branches, commits, diffs. |
|
|
223
|
+
| `http-fetch` | HTTP fetch | `tool-capability` | Fetches a URL over HTTP and hands back the response. |
|
|
224
|
+
| `human-review` | Human review | `tool-capability` | Routes the work to a person and waits for their verdict. |
|
|
225
|
+
| `messaging` | Messaging | `tool-capability` | Sends messages to a chat or notification channel. |
|
|
226
|
+
| `python-sandbox` | Python sandbox | `tool-capability` | Executes Python in an isolated interpreter. |
|
|
227
|
+
| `shell` | Shell | `tool-capability` | Runs shell commands on the host. |
|
|
228
|
+
| `sql` | SQL | `tool-capability` | Issues SQL statements against a database. |
|
|
229
|
+
| `tool-capability` | Tool capability | — | The root of the tool vocabulary, a capability a node needs from its host. |
|
|
230
|
+
| `vector-store` | Vector store | `tool-capability` | Embeds, stores and retrieves vectors for semantic recall. |
|
|
231
|
+
| `web-search` | Web search | `tool-capability` | Queries a search engine and returns ranked results. |
|
|
232
|
+
|
|
233
|
+
## Local terms
|
|
234
|
+
|
|
235
|
+
`node-type`, `risk-marker`, `data-type` and `tool` accept a locally namespaced term
|
|
236
|
+
(`me/my-term`), which must be rooted in an `extensions.yaml` carried by the bundle.
|
|
237
|
+
`phase` never accepts one.
|
|
238
|
+
|
|
239
|
+
**Do not emit local terms unless the author asks for one and writes the extension.** An
|
|
240
|
+
unrooted local term is silently ignored, which is the worst outcome available: the card
|
|
241
|
+
loads, the field reads as declared, and nothing enforces it.
|
|
242
|
+
|