create-pathfinder 4.2.0 → 4.3.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/CLAUDE.md +2 -0
- package/package.json +1 -1
- package/skills/learn-codebase/SKILL.md +188 -17
- package/skills/learn-feature/SKILL.md +136 -15
- package/skills/map-system/SKILL.md +293 -0
- package/skills/render-artifact/SKILL.md +187 -0
- package/skills/render-artifact/engine/bin/render.mjs +225 -0
- package/skills/render-artifact/engine/deliver.mjs +197 -0
- package/skills/render-artifact/engine/doctor.mjs +96 -0
- package/skills/render-artifact/engine/examples/diagram.json +223 -0
- package/skills/render-artifact/engine/examples/lesson.json +242 -0
- package/skills/render-artifact/engine/references/determinism.md +71 -0
- package/skills/render-artifact/engine/references/specification.md +149 -0
- package/skills/render-artifact/engine/references/validation.md +268 -0
- package/skills/render-artifact/engine/render/behavior.mjs +128 -0
- package/skills/render-artifact/engine/render/diagram.mjs +342 -0
- package/skills/render-artifact/engine/render/escape.mjs +34 -0
- package/skills/render-artifact/engine/render/graph/behavior.mjs +394 -0
- package/skills/render-artifact/engine/render/graph/draw.mjs +204 -0
- package/skills/render-artifact/engine/render/graph/interaction.mjs +174 -0
- package/skills/render-artifact/engine/render/graph/layout.mjs +698 -0
- package/skills/render-artifact/engine/render/graph/style.mjs +200 -0
- package/skills/render-artifact/engine/render/graph/width.mjs +204 -0
- package/skills/render-artifact/engine/render/index.mjs +50 -0
- package/skills/render-artifact/engine/render/lesson.mjs +294 -0
- package/skills/render-artifact/engine/render/shell.mjs +275 -0
- package/skills/render-artifact/engine/render/theme.mjs +592 -0
- package/skills/render-artifact/engine/schemas/common.schema.json +101 -0
- package/skills/render-artifact/engine/schemas/diagram.schema.json +176 -0
- package/skills/render-artifact/engine/schemas/lesson.schema.json +210 -0
- package/skills/render-artifact/engine/validate/composition.mjs +395 -0
- package/skills/render-artifact/engine/validate/diagnostics.mjs +83 -0
- package/skills/render-artifact/engine/validate/diagram-parts.mjs +68 -0
- package/skills/render-artifact/engine/validate/evidence.mjs +302 -0
- package/skills/render-artifact/engine/validate/index.mjs +132 -0
- package/skills/render-artifact/engine/validate/jsonschema.mjs +312 -0
- package/skills/render-artifact/engine/validate/structural.mjs +241 -0
- package/skills/render-artifact/engine/verification.mjs +76 -0
- package/skills/render-artifact/engine/version.mjs +24 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The semantic index a reader's interactions run on, and the traversal that
|
|
3
|
+
* runs on it.
|
|
4
|
+
*
|
|
5
|
+
* **Interactions read the graph, never the picture.** Downstream of a node is
|
|
6
|
+
* decided by following directed edges the producer authored, not by asking
|
|
7
|
+
* which boxes happen to sit to the right of it or which polylines happen to
|
|
8
|
+
* touch. Geometry is a rendering of the graph and is the wrong thing to infer
|
|
9
|
+
* meaning from: two nodes can be adjacent on the canvas and unrelated in the
|
|
10
|
+
* system, an edge can route past a node it has nothing to do with, and a
|
|
11
|
+
* layout change would silently change what "downstream" meant. So the model
|
|
12
|
+
* below is built from `nodes` and `edges` and consulted by id.
|
|
13
|
+
*
|
|
14
|
+
* **The model is an index, never content.** Ids, labels, adjacency, and each
|
|
15
|
+
* path's exact ordered edge ids — and nothing else. No summary, no detail, no
|
|
16
|
+
* citation, no evidence. Every one of those is already in the document as
|
|
17
|
+
* text, which is what lets the artifact be read with scripting off; putting a
|
|
18
|
+
* second copy in a script would be the one way to make a fact exist only
|
|
19
|
+
* behind an interaction, and that is precisely what the progressive
|
|
20
|
+
* enhancement contract forbids.
|
|
21
|
+
*
|
|
22
|
+
* **Nothing here is producer-authored.** There is no traversal field, no
|
|
23
|
+
* reachable-set field, no adjacency in the specification. All of it is derived,
|
|
24
|
+
* because all of it is derivable: a producer who could hand-write an adjacency
|
|
25
|
+
* list could hand-write one that disagreed with its own edges, and then the
|
|
26
|
+
* diagram and its behaviour would be two different claims.
|
|
27
|
+
*
|
|
28
|
+
* Imports nothing. This is on the render path, which reaches no `node:`
|
|
29
|
+
* builtin by design.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @typedef {object} InteractionModel
|
|
34
|
+
* @property {string[]} nodes node ids, in specification order
|
|
35
|
+
* @property {Record<string,string>} labels node id to its label, for the
|
|
36
|
+
* renderer's own status line. A label is a name, not a claim.
|
|
37
|
+
* @property {Record<string,[string,string][]>} out node id to [edge id, target]
|
|
38
|
+
* @property {Record<string,[string,string][]>} in node id to [edge id, source]
|
|
39
|
+
* @property {Record<string,[string,string]>} edges edge id to [source, target]
|
|
40
|
+
* @property {Record<string,string[]>} paths path id to its authored edge ids,
|
|
41
|
+
* in order, exactly as written
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Build the index for one validated diagram.
|
|
46
|
+
*
|
|
47
|
+
* Adjacency is keyed by node and carries the **edge id** alongside the
|
|
48
|
+
* neighbour, not just the neighbour. That is what keeps two edges joining one
|
|
49
|
+
* pair distinguishable: a traversal that recorded only "from `a` you can reach
|
|
50
|
+
* `b`" would lose which of the two relationships it walked, and highlighting
|
|
51
|
+
* would then light up both — reporting a claim the producer did not make.
|
|
52
|
+
*
|
|
53
|
+
* Ordering is specification order throughout, so the serialized model is a
|
|
54
|
+
* pure function of the specification.
|
|
55
|
+
*
|
|
56
|
+
* @param {object} diagram a `diagram` body that passed validation
|
|
57
|
+
* @returns {InteractionModel}
|
|
58
|
+
*/
|
|
59
|
+
export function interactionModel(diagram) {
|
|
60
|
+
const nodes = diagram.nodes.map((node) => node.id);
|
|
61
|
+
|
|
62
|
+
const labels = {};
|
|
63
|
+
for (const node of diagram.nodes) labels[node.id] = node.label;
|
|
64
|
+
|
|
65
|
+
const out = {};
|
|
66
|
+
const inbound = {};
|
|
67
|
+
for (const id of nodes) {
|
|
68
|
+
out[id] = [];
|
|
69
|
+
inbound[id] = [];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Edge id to its endpoints. This is what makes a path highlight exact: a
|
|
73
|
+
// path names edge ids, and lighting up the nodes a path touches has to start
|
|
74
|
+
// from the edges the producer wrote rather than from any pair of nodes that
|
|
75
|
+
// happen to be joined somehow.
|
|
76
|
+
const edges = {};
|
|
77
|
+
|
|
78
|
+
for (const edge of diagram.edges) {
|
|
79
|
+
// Endpoints resolved by the composition layer before this runs, so an
|
|
80
|
+
// edge naming a node that does not exist cannot reach here. The guards are
|
|
81
|
+
// still here because a model that silently dropped an edge would make
|
|
82
|
+
// traversal quietly wrong rather than loudly broken.
|
|
83
|
+
if (out[edge.from] !== undefined) out[edge.from].push([edge.id, edge.to]);
|
|
84
|
+
if (inbound[edge.to] !== undefined) inbound[edge.to].push([edge.id, edge.from]);
|
|
85
|
+
edges[edge.id] = [edge.from, edge.to];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const paths = {};
|
|
89
|
+
for (const path of diagram.paths ?? []) paths[path.id] = [...path.edges];
|
|
90
|
+
|
|
91
|
+
return { nodes, labels, out, in: inbound, edges, paths };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The traversal, as the source that ships.
|
|
96
|
+
*
|
|
97
|
+
* This is a string rather than a function for one reason: it is emitted into
|
|
98
|
+
* the artifact, and it is also the thing the tests run. Writing it twice —
|
|
99
|
+
* once for the browser and once for Node — would leave two implementations
|
|
100
|
+
* free to disagree about what "downstream" means, and the one that shipped
|
|
101
|
+
* would be the one nobody had tested. The test evaluates this exact text.
|
|
102
|
+
*
|
|
103
|
+
* Breadth-first over the index, with a visited set, which is what makes the
|
|
104
|
+
* three awkward graph shapes safe rather than special-cased:
|
|
105
|
+
*
|
|
106
|
+
* a cycle a node already visited is not queued again, so a ring
|
|
107
|
+
* terminates instead of spinning
|
|
108
|
+
* a self-edge the node is already visited when its own edge is read,
|
|
109
|
+
* so `a -> a` adds the edge and queues nothing
|
|
110
|
+
* repeated edges both are recorded, because the frontier is keyed by node
|
|
111
|
+
* and the result keeps edge ids separately
|
|
112
|
+
*
|
|
113
|
+
* The start node is reported in `nodes` — a reader tracing downstream of a
|
|
114
|
+
* thing is still looking at that thing — and every edge crossed is reported in
|
|
115
|
+
* `edges`, so the caller can light up exactly the relationships walked rather
|
|
116
|
+
* than every relationship between the nodes involved.
|
|
117
|
+
*/
|
|
118
|
+
export const TRAVERSAL_JS = `
|
|
119
|
+
function pfTraverse(model, start, direction) {
|
|
120
|
+
var adjacency = direction === "in" ? model["in"] : model.out;
|
|
121
|
+
if (!adjacency || !adjacency[start]) return { nodes: [start], edges: [] };
|
|
122
|
+
|
|
123
|
+
var seenNodes = Object.create(null);
|
|
124
|
+
var seenEdges = Object.create(null);
|
|
125
|
+
var nodes = [];
|
|
126
|
+
var edges = [];
|
|
127
|
+
var queue = [start];
|
|
128
|
+
|
|
129
|
+
seenNodes[start] = true;
|
|
130
|
+
nodes.push(start);
|
|
131
|
+
|
|
132
|
+
while (queue.length > 0) {
|
|
133
|
+
var current = queue.shift();
|
|
134
|
+
var next = adjacency[current] || [];
|
|
135
|
+
|
|
136
|
+
for (var i = 0; i < next.length; i += 1) {
|
|
137
|
+
var edgeId = next[i][0];
|
|
138
|
+
var neighbour = next[i][1];
|
|
139
|
+
|
|
140
|
+
/* Every edge crossed is recorded, including one that leads somewhere
|
|
141
|
+
already reached. Two edges joining one pair are two claims, and a
|
|
142
|
+
traversal that kept only the first would highlight the wrong one. */
|
|
143
|
+
if (!seenEdges[edgeId]) {
|
|
144
|
+
seenEdges[edgeId] = true;
|
|
145
|
+
edges.push(edgeId);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/* A node already reached is never queued again. This single guard is
|
|
149
|
+
what terminates a cycle and what stops a self-edge looping. */
|
|
150
|
+
if (!seenNodes[neighbour]) {
|
|
151
|
+
seenNodes[neighbour] = true;
|
|
152
|
+
nodes.push(neighbour);
|
|
153
|
+
queue.push(neighbour);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return { nodes: nodes, edges: edges };
|
|
159
|
+
}
|
|
160
|
+
`.trim();
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Serialize the model for the artifact.
|
|
164
|
+
*
|
|
165
|
+
* `JSON.stringify` with no spacing, and with the keys written in the order
|
|
166
|
+
* this module builds them, so the bytes are a function of the specification
|
|
167
|
+
* and this file. The closing-tag guard matters: a producer label containing
|
|
168
|
+
* `</script>` would otherwise end the script element early and put the rest of
|
|
169
|
+
* the model into the document as text. Escaping the slash keeps the JSON
|
|
170
|
+
* identical to the parser and inert to the HTML tokenizer.
|
|
171
|
+
*/
|
|
172
|
+
export function serializeModel(model) {
|
|
173
|
+
return JSON.stringify(model).replace(/<\/script/gi, "<\\/script");
|
|
174
|
+
}
|