@pfdsl/core 0.0.1
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/LICENSE +21 -0
- package/README.md +150 -0
- package/dist/index.d.ts +432 -0
- package/dist/index.js +2386 -0
- package/package.json +35 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2386 @@
|
|
|
1
|
+
// src/audit.ts
|
|
2
|
+
function auditGraph(edges, nodeKinds, artifactMeta) {
|
|
3
|
+
const produced = /* @__PURE__ */ new Set();
|
|
4
|
+
const consumed = /* @__PURE__ */ new Set();
|
|
5
|
+
for (const e of edges) {
|
|
6
|
+
if (e.kind === "output") {
|
|
7
|
+
produced.add(e.artifact);
|
|
8
|
+
} else if (e.kind === "input") {
|
|
9
|
+
consumed.add(e.artifact);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
const artifacts = [];
|
|
13
|
+
for (const [id, kind] of nodeKinds) {
|
|
14
|
+
if (kind === "artifact") artifacts.push(id);
|
|
15
|
+
}
|
|
16
|
+
for (const a of [...produced, ...consumed]) {
|
|
17
|
+
if (!nodeKinds.has(a)) artifacts.push(a);
|
|
18
|
+
}
|
|
19
|
+
const terminals = [...new Set(artifacts)].filter(
|
|
20
|
+
(a) => produced.has(a) && !consumed.has(a) && !artifactMeta?.[a]?.externalStakeholders?.length
|
|
21
|
+
);
|
|
22
|
+
const externalInputs = [...new Set(artifacts)].filter(
|
|
23
|
+
(a) => consumed.has(a) && !produced.has(a)
|
|
24
|
+
);
|
|
25
|
+
return { terminals, externalInputs };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// src/formatter.ts
|
|
29
|
+
function splitBodyIntoSegments(body) {
|
|
30
|
+
if (body === "") return [];
|
|
31
|
+
const lines = body.split("\n");
|
|
32
|
+
const hasTrailingNewline = body.endsWith("\n");
|
|
33
|
+
const realLines = hasTrailingNewline ? lines.slice(0, -1) : lines;
|
|
34
|
+
const segments = [];
|
|
35
|
+
let current = null;
|
|
36
|
+
for (const line of realLines) {
|
|
37
|
+
const isComment = line === "" || line.startsWith("#");
|
|
38
|
+
const kind = isComment ? "comment" : "edges";
|
|
39
|
+
if (current && current.kind === kind) {
|
|
40
|
+
current.text += `${line}
|
|
41
|
+
`;
|
|
42
|
+
} else {
|
|
43
|
+
if (current) segments.push(current);
|
|
44
|
+
current = { kind, text: `${line}
|
|
45
|
+
` };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (current) segments.push(current);
|
|
49
|
+
return segments;
|
|
50
|
+
}
|
|
51
|
+
function formatEdges(sortedEdges, sortedIsolated = []) {
|
|
52
|
+
const lines = [];
|
|
53
|
+
for (const e of sortedEdges) {
|
|
54
|
+
if (e.kind === "input") lines.push(`${e.artifact} >> ${e.process}`);
|
|
55
|
+
else if (e.kind === "feedback")
|
|
56
|
+
lines.push(`${e.artifact} >>? ${e.process}`);
|
|
57
|
+
else lines.push(`${e.process} -> ${e.artifact}`);
|
|
58
|
+
}
|
|
59
|
+
for (const id of sortedIsolated) lines.push(id);
|
|
60
|
+
if (lines.length === 0) return "";
|
|
61
|
+
return `${lines.join("\n")}
|
|
62
|
+
`;
|
|
63
|
+
}
|
|
64
|
+
function formatAsFlows(sortedEdges, sortedIsolated = []) {
|
|
65
|
+
const byProcess = /* @__PURE__ */ new Map();
|
|
66
|
+
for (const e of sortedEdges) {
|
|
67
|
+
let entry = byProcess.get(e.process);
|
|
68
|
+
if (!entry) {
|
|
69
|
+
entry = { inputs: [], outputs: [], feedbacks: [] };
|
|
70
|
+
byProcess.set(e.process, entry);
|
|
71
|
+
}
|
|
72
|
+
if (e.kind === "input") entry.inputs.push(e.artifact);
|
|
73
|
+
else if (e.kind === "output") entry.outputs.push(e.artifact);
|
|
74
|
+
else entry.feedbacks.push(e.artifact);
|
|
75
|
+
}
|
|
76
|
+
const rankProxy = /* @__PURE__ */ new Map();
|
|
77
|
+
const offset = sortedEdges.length;
|
|
78
|
+
for (let i = 0; i < sortedEdges.length; i++) {
|
|
79
|
+
const e = sortedEdges[i];
|
|
80
|
+
if (e.kind !== "input" && !rankProxy.has(e.process)) {
|
|
81
|
+
rankProxy.set(e.process, i);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
for (let i = 0; i < sortedEdges.length; i++) {
|
|
85
|
+
const e = sortedEdges[i];
|
|
86
|
+
if (e.kind === "input" && !rankProxy.has(e.process)) {
|
|
87
|
+
rankProxy.set(e.process, i + offset);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const processOrder = [...byProcess.keys()].sort((a, b) => {
|
|
91
|
+
const diff = (rankProxy.get(a) ?? offset * 2) - (rankProxy.get(b) ?? offset * 2);
|
|
92
|
+
return diff !== 0 ? diff : a.localeCompare(b);
|
|
93
|
+
});
|
|
94
|
+
const lines = [];
|
|
95
|
+
for (const proc of processOrder) {
|
|
96
|
+
const { inputs, outputs, feedbacks } = byProcess.get(proc);
|
|
97
|
+
for (const fb of feedbacks) lines.push(`${fb} >>? ${proc}`);
|
|
98
|
+
if (inputs.length === 0 && outputs.length === 0) continue;
|
|
99
|
+
const fmtIds = (ids) => ids.length === 1 ? ids[0] : `[${ids.join(", ")}]`;
|
|
100
|
+
let stmt = inputs.length > 0 ? `${fmtIds(inputs)} >> ${proc}` : proc;
|
|
101
|
+
if (outputs.length > 0) stmt += ` -> ${fmtIds(outputs)}`;
|
|
102
|
+
lines.push(stmt);
|
|
103
|
+
}
|
|
104
|
+
for (const id of sortedIsolated) lines.push(id);
|
|
105
|
+
if (lines.length === 0) return "";
|
|
106
|
+
return `${lines.join("\n")}
|
|
107
|
+
`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// src/frontmatter.ts
|
|
111
|
+
import { parse as parseYaml } from "yaml";
|
|
112
|
+
function findFrontmatterNodeRanges(source) {
|
|
113
|
+
const result = /* @__PURE__ */ new Map();
|
|
114
|
+
const { bodyStartLine } = loadFrontmatter(source);
|
|
115
|
+
const fmEndLine = bodyStartLine - 1;
|
|
116
|
+
const lines = source.split("\n");
|
|
117
|
+
let inNodeSection = false;
|
|
118
|
+
for (let i = 0; i < fmEndLine && i < lines.length; i++) {
|
|
119
|
+
const line = lines[i];
|
|
120
|
+
if (line === void 0) continue;
|
|
121
|
+
if (/^\S/.test(line)) {
|
|
122
|
+
inNodeSection = line.startsWith("artifact:") || line.startsWith("process:");
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (!inNodeSection) continue;
|
|
126
|
+
const m = /^( {2})(\S[^:]*)\s*:/.exec(line);
|
|
127
|
+
if (!m) continue;
|
|
128
|
+
const id = m[2] ?? "";
|
|
129
|
+
if (!id) continue;
|
|
130
|
+
const lineNum = i + 1;
|
|
131
|
+
const col = 3;
|
|
132
|
+
result.set(id, {
|
|
133
|
+
start: { line: lineNum, column: col, offset: 0 },
|
|
134
|
+
end: { line: lineNum, column: col + id.length, offset: 0 }
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
function loadFrontmatter(source) {
|
|
140
|
+
if (!source.startsWith("---")) {
|
|
141
|
+
return {
|
|
142
|
+
frontmatter: null,
|
|
143
|
+
body: source,
|
|
144
|
+
bodyStartLine: 1,
|
|
145
|
+
diagnostics: []
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
const firstNl = source.indexOf("\n");
|
|
149
|
+
let closingLineStart = -1;
|
|
150
|
+
let closingLineEnd = -1;
|
|
151
|
+
let lineNum = 2;
|
|
152
|
+
if (firstNl !== -1) {
|
|
153
|
+
let lineStart = firstNl + 1;
|
|
154
|
+
while (lineStart <= source.length) {
|
|
155
|
+
const nl = source.indexOf("\n", lineStart);
|
|
156
|
+
const lineEnd = nl === -1 ? source.length : nl;
|
|
157
|
+
if (source.slice(lineStart, lineEnd).trimEnd() === "---") {
|
|
158
|
+
closingLineStart = lineStart;
|
|
159
|
+
closingLineEnd = lineEnd;
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
if (nl === -1) break;
|
|
163
|
+
lineStart = nl + 1;
|
|
164
|
+
lineNum++;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (closingLineStart === -1) {
|
|
168
|
+
const diag = {
|
|
169
|
+
severity: "error",
|
|
170
|
+
code: "FM001",
|
|
171
|
+
message: "Unclosed front matter: missing closing ---",
|
|
172
|
+
range: {
|
|
173
|
+
start: { line: 1, column: 1, offset: 0 },
|
|
174
|
+
end: { line: 1, column: 4, offset: 3 }
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
return {
|
|
178
|
+
frontmatter: null,
|
|
179
|
+
body: source,
|
|
180
|
+
bodyStartLine: 1,
|
|
181
|
+
diagnostics: [diag]
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
const yamlText = closingLineStart > firstNl + 1 ? source.slice(firstNl + 1, closingLineStart - 1) : "";
|
|
185
|
+
const body = closingLineEnd === source.length ? "" : source.slice(closingLineEnd + 1);
|
|
186
|
+
const bodyStartLine = lineNum + 1;
|
|
187
|
+
const diagnostics = [];
|
|
188
|
+
let frontmatter = null;
|
|
189
|
+
try {
|
|
190
|
+
const parsed = parseYaml(yamlText);
|
|
191
|
+
if (parsed != null && typeof parsed === "object") {
|
|
192
|
+
frontmatter = parsed;
|
|
193
|
+
}
|
|
194
|
+
} catch (e) {
|
|
195
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
196
|
+
diagnostics.push({
|
|
197
|
+
severity: "error",
|
|
198
|
+
code: "FM002",
|
|
199
|
+
message: `Invalid YAML in front matter: ${msg}`,
|
|
200
|
+
range: {
|
|
201
|
+
start: { line: 2, column: 1, offset: 4 },
|
|
202
|
+
end: { line: 2, column: 1, offset: 4 }
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
return { frontmatter, body, bodyStartLine, diagnostics };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// src/graph.ts
|
|
210
|
+
function buildGraph(edges, nodeKinds) {
|
|
211
|
+
const nodes = new Map(nodeKinds);
|
|
212
|
+
const primaryEdges = [];
|
|
213
|
+
const feedbackEdges = [];
|
|
214
|
+
for (const edge of edges) {
|
|
215
|
+
if (edge.kind === "input") {
|
|
216
|
+
if (!nodes.has(edge.artifact)) nodes.set(edge.artifact, "artifact");
|
|
217
|
+
if (!nodes.has(edge.process)) nodes.set(edge.process, "process");
|
|
218
|
+
primaryEdges.push({
|
|
219
|
+
from: edge.artifact,
|
|
220
|
+
to: edge.process,
|
|
221
|
+
kind: "input"
|
|
222
|
+
});
|
|
223
|
+
} else if (edge.kind === "output") {
|
|
224
|
+
if (!nodes.has(edge.process)) nodes.set(edge.process, "process");
|
|
225
|
+
if (!nodes.has(edge.artifact)) nodes.set(edge.artifact, "artifact");
|
|
226
|
+
primaryEdges.push({
|
|
227
|
+
from: edge.process,
|
|
228
|
+
to: edge.artifact,
|
|
229
|
+
kind: "output"
|
|
230
|
+
});
|
|
231
|
+
} else {
|
|
232
|
+
if (!nodes.has(edge.artifact)) nodes.set(edge.artifact, "artifact");
|
|
233
|
+
if (!nodes.has(edge.process)) nodes.set(edge.process, "process");
|
|
234
|
+
feedbackEdges.push({ artifact: edge.artifact, process: edge.process });
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return { nodes, primaryEdges, feedbackEdges };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// src/lexer.ts
|
|
241
|
+
var ID_PATTERN = /[\p{L}\p{N}_][\p{L}\p{N}_-]*/u;
|
|
242
|
+
var BARE_ID_RE = /[\p{L}\p{N}]/u;
|
|
243
|
+
function lex(source) {
|
|
244
|
+
const tokens = [];
|
|
245
|
+
const diagnostics = [];
|
|
246
|
+
let pos = 0;
|
|
247
|
+
let line = 1;
|
|
248
|
+
let column = 1;
|
|
249
|
+
function currentPos() {
|
|
250
|
+
return { line, column, offset: pos };
|
|
251
|
+
}
|
|
252
|
+
function advance(count = 1) {
|
|
253
|
+
let result = "";
|
|
254
|
+
for (let i = 0; i < count; i++) {
|
|
255
|
+
if (pos >= source.length) break;
|
|
256
|
+
const cp = source.codePointAt(pos);
|
|
257
|
+
const charLen = cp > 65535 ? 2 : 1;
|
|
258
|
+
const ch = source.slice(pos, pos + charLen);
|
|
259
|
+
result += ch;
|
|
260
|
+
if (ch === "\n") {
|
|
261
|
+
line++;
|
|
262
|
+
column = 1;
|
|
263
|
+
} else {
|
|
264
|
+
column++;
|
|
265
|
+
}
|
|
266
|
+
pos += charLen;
|
|
267
|
+
}
|
|
268
|
+
return result;
|
|
269
|
+
}
|
|
270
|
+
function peek(offset = 0) {
|
|
271
|
+
let p = pos;
|
|
272
|
+
for (let i = 0; i < offset; i++) {
|
|
273
|
+
if (p >= source.length) return "";
|
|
274
|
+
p += source.codePointAt(p) > 65535 ? 2 : 1;
|
|
275
|
+
}
|
|
276
|
+
if (p >= source.length) return "";
|
|
277
|
+
const cp = source.codePointAt(p);
|
|
278
|
+
return cp > 65535 ? source.slice(p, p + 2) : source[p];
|
|
279
|
+
}
|
|
280
|
+
function makeToken(type, value, raw, start, end) {
|
|
281
|
+
return { type, value, raw, start, end };
|
|
282
|
+
}
|
|
283
|
+
while (pos < source.length) {
|
|
284
|
+
const ch = peek();
|
|
285
|
+
if (ch === " " || ch === " " || ch === "\r") {
|
|
286
|
+
advance();
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
if (ch === "\n") {
|
|
290
|
+
const start = currentPos();
|
|
291
|
+
advance();
|
|
292
|
+
tokens.push(makeToken("NEWLINE", "\n", "\n", start, currentPos()));
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
if (ch === "#") {
|
|
296
|
+
const start = currentPos();
|
|
297
|
+
let raw = "";
|
|
298
|
+
while (pos < source.length && peek() !== "\n") raw += advance();
|
|
299
|
+
tokens.push(makeToken("COMMENT", raw, raw, start, currentPos()));
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
if (ch === ">" && peek(1) === ">" && peek(2) === "?") {
|
|
303
|
+
const start = currentPos();
|
|
304
|
+
advance(3);
|
|
305
|
+
tokens.push(
|
|
306
|
+
makeToken("ARROW_FEEDBACK", ">>?", ">>?", start, currentPos())
|
|
307
|
+
);
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
if (ch === ">" && peek(1) === ">") {
|
|
311
|
+
const start = currentPos();
|
|
312
|
+
advance(2);
|
|
313
|
+
tokens.push(makeToken("ARROW_INPUT", ">>", ">>", start, currentPos()));
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
if (ch === "-" && peek(1) === ">") {
|
|
317
|
+
const start = currentPos();
|
|
318
|
+
advance(2);
|
|
319
|
+
tokens.push(makeToken("ARROW_OUTPUT", "->", "->", start, currentPos()));
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
if (ch === "[") {
|
|
323
|
+
const s = currentPos();
|
|
324
|
+
advance();
|
|
325
|
+
tokens.push(makeToken("LBRACKET", "[", "[", s, currentPos()));
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
if (ch === "]") {
|
|
329
|
+
const s = currentPos();
|
|
330
|
+
advance();
|
|
331
|
+
tokens.push(makeToken("RBRACKET", "]", "]", s, currentPos()));
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
if (ch === ",") {
|
|
335
|
+
const s = currentPos();
|
|
336
|
+
advance();
|
|
337
|
+
tokens.push(makeToken("COMMA", ",", ",", s, currentPos()));
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
if (ch === ";") {
|
|
341
|
+
const s = currentPos();
|
|
342
|
+
advance();
|
|
343
|
+
tokens.push(makeToken("SEMICOLON", ";", ";", s, currentPos()));
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
if (ch === '"') {
|
|
347
|
+
const start = currentPos();
|
|
348
|
+
advance();
|
|
349
|
+
let value = "";
|
|
350
|
+
let raw = '"';
|
|
351
|
+
let closed = false;
|
|
352
|
+
while (pos < source.length) {
|
|
353
|
+
const c = peek();
|
|
354
|
+
if (c === '"') {
|
|
355
|
+
advance();
|
|
356
|
+
raw += '"';
|
|
357
|
+
closed = true;
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
if (c === "\n") break;
|
|
361
|
+
if (c === "\\") {
|
|
362
|
+
advance();
|
|
363
|
+
raw += "\\";
|
|
364
|
+
const esc = peek();
|
|
365
|
+
if (esc === '"') {
|
|
366
|
+
value += '"';
|
|
367
|
+
raw += '"';
|
|
368
|
+
advance();
|
|
369
|
+
} else if (esc === "\\") {
|
|
370
|
+
value += "\\";
|
|
371
|
+
raw += "\\";
|
|
372
|
+
advance();
|
|
373
|
+
} else if (esc === "n") {
|
|
374
|
+
value += "\n";
|
|
375
|
+
raw += "n";
|
|
376
|
+
advance();
|
|
377
|
+
} else if (esc === "t") {
|
|
378
|
+
value += " ";
|
|
379
|
+
raw += "t";
|
|
380
|
+
advance();
|
|
381
|
+
} else {
|
|
382
|
+
value += `\\${esc}`;
|
|
383
|
+
raw += esc;
|
|
384
|
+
advance();
|
|
385
|
+
}
|
|
386
|
+
} else {
|
|
387
|
+
value += c;
|
|
388
|
+
raw += c;
|
|
389
|
+
advance();
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
if (!closed) {
|
|
393
|
+
diagnostics.push({
|
|
394
|
+
severity: "error",
|
|
395
|
+
code: "L001",
|
|
396
|
+
message: "Unclosed quoted identifier",
|
|
397
|
+
range: { start, end: currentPos() }
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
tokens.push(makeToken("ID", value, raw, start, currentPos()));
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
if (isBareIdChar(ch)) {
|
|
404
|
+
const start = currentPos();
|
|
405
|
+
let value = "";
|
|
406
|
+
while (pos < source.length) {
|
|
407
|
+
const c = peek();
|
|
408
|
+
if (c === ">" && peek(1) === ">") break;
|
|
409
|
+
if (c === "-" && peek(1) === ">") break;
|
|
410
|
+
if (!isBareIdChar(c)) break;
|
|
411
|
+
value += c;
|
|
412
|
+
advance();
|
|
413
|
+
}
|
|
414
|
+
if (value.length > 0) {
|
|
415
|
+
tokens.push(makeToken("ID", value, value, start, currentPos()));
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
const errStart = currentPos();
|
|
420
|
+
const unknown = advance();
|
|
421
|
+
diagnostics.push({
|
|
422
|
+
severity: "error",
|
|
423
|
+
code: "L002",
|
|
424
|
+
message: `Unexpected character: ${JSON.stringify(unknown)}`,
|
|
425
|
+
range: { start: errStart, end: currentPos() }
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
tokens.push(makeToken("EOF", "", "", currentPos(), currentPos()));
|
|
429
|
+
return { tokens, diagnostics };
|
|
430
|
+
}
|
|
431
|
+
function isBareIdChar(ch) {
|
|
432
|
+
if (ch === "_" || ch === "-") return true;
|
|
433
|
+
return BARE_ID_RE.test(ch);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// src/position.ts
|
|
437
|
+
function zeroPos() {
|
|
438
|
+
return { line: 1, column: 1, offset: 0 };
|
|
439
|
+
}
|
|
440
|
+
function zeroRange() {
|
|
441
|
+
return { start: zeroPos(), end: zeroPos() };
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// src/normalizer.ts
|
|
445
|
+
function edgeKey(edge) {
|
|
446
|
+
return edge.kind === "output" ? `output\0${edge.process}\0${edge.artifact}` : `${edge.kind}\0${edge.artifact}\0${edge.process}`;
|
|
447
|
+
}
|
|
448
|
+
function normalize(doc, fm) {
|
|
449
|
+
const diagnostics = [];
|
|
450
|
+
const rawEdges = [];
|
|
451
|
+
const seenEdges = /* @__PURE__ */ new Set();
|
|
452
|
+
const nodeKinds = /* @__PURE__ */ new Map();
|
|
453
|
+
const declaredNodes = /* @__PURE__ */ new Set();
|
|
454
|
+
const edgeNodes = /* @__PURE__ */ new Set();
|
|
455
|
+
for (const id of Object.keys(fm?.artifact ?? {})) {
|
|
456
|
+
nodeKinds.set(id, "artifact");
|
|
457
|
+
}
|
|
458
|
+
for (const id of Object.keys(fm?.group ?? {})) {
|
|
459
|
+
if (!nodeKinds.has(id)) nodeKinds.set(id, "group");
|
|
460
|
+
}
|
|
461
|
+
for (const id of Object.keys(fm?.process ?? {})) {
|
|
462
|
+
if (nodeKinds.has(id)) {
|
|
463
|
+
diagnostics.push({
|
|
464
|
+
severity: "error",
|
|
465
|
+
code: "N001",
|
|
466
|
+
message: `'${id}' declared as both artifact and process in front matter`,
|
|
467
|
+
range: zeroRange()
|
|
468
|
+
});
|
|
469
|
+
} else {
|
|
470
|
+
nodeKinds.set(id, "process");
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
function inferKind(id, kind) {
|
|
474
|
+
const existing = nodeKinds.get(id);
|
|
475
|
+
if (existing === void 0) {
|
|
476
|
+
nodeKinds.set(id, kind);
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
if (existing !== kind) {
|
|
480
|
+
diagnostics.push({
|
|
481
|
+
severity: "error",
|
|
482
|
+
code: "N002",
|
|
483
|
+
message: `'${id}' used as both artifact and process`,
|
|
484
|
+
range: zeroRange()
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
function addEdge(edge) {
|
|
489
|
+
const key = edgeKey(edge);
|
|
490
|
+
if (seenEdges.has(key)) {
|
|
491
|
+
diagnostics.push({
|
|
492
|
+
severity: "warning",
|
|
493
|
+
code: "N003",
|
|
494
|
+
message: "Duplicate edge",
|
|
495
|
+
range: zeroRange()
|
|
496
|
+
});
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
seenEdges.add(key);
|
|
500
|
+
rawEdges.push(edge);
|
|
501
|
+
if (edge.kind === "output") {
|
|
502
|
+
edgeNodes.add(edge.process);
|
|
503
|
+
edgeNodes.add(edge.artifact);
|
|
504
|
+
} else {
|
|
505
|
+
edgeNodes.add(edge.artifact);
|
|
506
|
+
edgeNodes.add(edge.process);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
function ids(expr) {
|
|
510
|
+
return expr.ids.map((i) => i.value);
|
|
511
|
+
}
|
|
512
|
+
function addEdgesFor(kind, artifactIds, proc) {
|
|
513
|
+
inferKind(proc, "process");
|
|
514
|
+
for (const a of artifactIds) {
|
|
515
|
+
inferKind(a, "artifact");
|
|
516
|
+
addEdge(
|
|
517
|
+
kind === "output" ? { kind, process: proc, artifact: a } : { kind, artifact: a, process: proc }
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
function processStmt(stmt) {
|
|
522
|
+
switch (stmt.type) {
|
|
523
|
+
case "chain": {
|
|
524
|
+
let currentArtifacts = ids(stmt.head);
|
|
525
|
+
for (const seg of stmt.segments) {
|
|
526
|
+
const proc = seg.process.value;
|
|
527
|
+
addEdgesFor(
|
|
528
|
+
seg.op === ">>" ? "input" : "feedback",
|
|
529
|
+
currentArtifacts,
|
|
530
|
+
proc
|
|
531
|
+
);
|
|
532
|
+
if (seg.output !== null) {
|
|
533
|
+
const outArtifacts = ids(seg.output);
|
|
534
|
+
addEdgesFor("output", outArtifacts, proc);
|
|
535
|
+
currentArtifacts = outArtifacts;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
break;
|
|
539
|
+
}
|
|
540
|
+
case "input-edge":
|
|
541
|
+
addEdgesFor("input", ids(stmt.artifact), stmt.process.value);
|
|
542
|
+
break;
|
|
543
|
+
case "feedback-edge":
|
|
544
|
+
addEdgesFor("feedback", ids(stmt.artifact), stmt.process.value);
|
|
545
|
+
break;
|
|
546
|
+
case "output-edge":
|
|
547
|
+
addEdgesFor("output", ids(stmt.artifact), stmt.process.value);
|
|
548
|
+
break;
|
|
549
|
+
case "node-decl": {
|
|
550
|
+
const id = stmt.id.value;
|
|
551
|
+
declaredNodes.add(id);
|
|
552
|
+
if (!nodeKinds.has(id)) nodeKinds.set(id, "artifact");
|
|
553
|
+
break;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
for (const stmt of doc.statements) processStmt(stmt);
|
|
558
|
+
const isolatedNodes = /* @__PURE__ */ new Set();
|
|
559
|
+
for (const id of declaredNodes) {
|
|
560
|
+
if (!edgeNodes.has(id)) isolatedNodes.add(id);
|
|
561
|
+
}
|
|
562
|
+
for (const [id, kind] of nodeKinds) {
|
|
563
|
+
if (kind !== "group" && !edgeNodes.has(id) && !isolatedNodes.has(id))
|
|
564
|
+
isolatedNodes.add(id);
|
|
565
|
+
}
|
|
566
|
+
return { edges: rawEdges, nodeKinds, isolatedNodes, diagnostics };
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// src/parser.ts
|
|
570
|
+
function parseTokens(tokens) {
|
|
571
|
+
const diagnostics = [];
|
|
572
|
+
let pos = 0;
|
|
573
|
+
function peek(offset = 0) {
|
|
574
|
+
const idx = pos + offset;
|
|
575
|
+
return tokens[Math.min(idx, tokens.length - 1)];
|
|
576
|
+
}
|
|
577
|
+
function advance() {
|
|
578
|
+
const t = tokens[pos];
|
|
579
|
+
if (pos < tokens.length - 1) pos++;
|
|
580
|
+
return t;
|
|
581
|
+
}
|
|
582
|
+
function skipSeparators() {
|
|
583
|
+
while (peek().type === "NEWLINE" || peek().type === "SEMICOLON" || peek().type === "COMMENT")
|
|
584
|
+
advance();
|
|
585
|
+
}
|
|
586
|
+
function scanContinuation(start) {
|
|
587
|
+
let i = start;
|
|
588
|
+
let consecutive = 0;
|
|
589
|
+
let maxConsecutive = 0;
|
|
590
|
+
while (i < tokens.length) {
|
|
591
|
+
const t = tokens[i]?.type;
|
|
592
|
+
if (t === "NEWLINE") {
|
|
593
|
+
consecutive++;
|
|
594
|
+
if (consecutive > maxConsecutive) maxConsecutive = consecutive;
|
|
595
|
+
} else if (t === "COMMENT") {
|
|
596
|
+
consecutive = 0;
|
|
597
|
+
} else {
|
|
598
|
+
break;
|
|
599
|
+
}
|
|
600
|
+
i++;
|
|
601
|
+
}
|
|
602
|
+
return { next: i, blankLine: maxConsecutive > 1 };
|
|
603
|
+
}
|
|
604
|
+
function tryContinuation(...ops) {
|
|
605
|
+
const { next, blankLine } = scanContinuation(pos);
|
|
606
|
+
if (next > pos && next < tokens.length && !blankLine && ops.includes(tokens[next].type)) {
|
|
607
|
+
pos = next;
|
|
608
|
+
return true;
|
|
609
|
+
}
|
|
610
|
+
return false;
|
|
611
|
+
}
|
|
612
|
+
function isOutputEdgeStart() {
|
|
613
|
+
if (peek().type !== "ID") return false;
|
|
614
|
+
const { next, blankLine } = scanContinuation(pos + 1);
|
|
615
|
+
return !blankLine && next < tokens.length && tokens[next]?.type === "ARROW_OUTPUT";
|
|
616
|
+
}
|
|
617
|
+
function isNodeDeclStart() {
|
|
618
|
+
if (peek().type !== "ID") return false;
|
|
619
|
+
const afterId = pos + 1;
|
|
620
|
+
if (afterId >= tokens.length) return true;
|
|
621
|
+
const t = tokens[afterId]?.type;
|
|
622
|
+
if (t === "ARROW_INPUT" || t === "ARROW_FEEDBACK" || t === "ARROW_OUTPUT")
|
|
623
|
+
return false;
|
|
624
|
+
if (t === "NEWLINE" || t === "COMMENT") {
|
|
625
|
+
const { next, blankLine } = scanContinuation(afterId);
|
|
626
|
+
if (!blankLine && next < tokens.length) {
|
|
627
|
+
const nt = tokens[next]?.type;
|
|
628
|
+
if (nt === "ARROW_INPUT" || nt === "ARROW_FEEDBACK" || nt === "ARROW_OUTPUT")
|
|
629
|
+
return false;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
return t === "NEWLINE" || t === "SEMICOLON" || t === "EOF" || t === "COMMENT";
|
|
633
|
+
}
|
|
634
|
+
function parseId() {
|
|
635
|
+
const t = peek();
|
|
636
|
+
if (t.type !== "ID") return null;
|
|
637
|
+
advance();
|
|
638
|
+
return {
|
|
639
|
+
type: "id",
|
|
640
|
+
value: t.value,
|
|
641
|
+
raw: t.raw,
|
|
642
|
+
start: t.start,
|
|
643
|
+
end: t.end
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
function parseArtifactExpr() {
|
|
647
|
+
const t = peek();
|
|
648
|
+
if (t.type === "LBRACKET") {
|
|
649
|
+
const start = t.start;
|
|
650
|
+
advance();
|
|
651
|
+
skipSeparators();
|
|
652
|
+
const ids = [];
|
|
653
|
+
const first = parseId();
|
|
654
|
+
if (!first) {
|
|
655
|
+
diagnostics.push({
|
|
656
|
+
severity: "error",
|
|
657
|
+
code: "P002",
|
|
658
|
+
message: "Expected identifier in artifact set",
|
|
659
|
+
range: { start: peek().start, end: peek().end }
|
|
660
|
+
});
|
|
661
|
+
while (peek().type !== "RBRACKET" && peek().type !== "EOF") advance();
|
|
662
|
+
const end = peek().end;
|
|
663
|
+
if (peek().type === "RBRACKET") advance();
|
|
664
|
+
return { type: "artifact-expr", ids: [], start, end };
|
|
665
|
+
}
|
|
666
|
+
ids.push(first);
|
|
667
|
+
while (peek().type === "COMMA") {
|
|
668
|
+
advance();
|
|
669
|
+
skipSeparators();
|
|
670
|
+
const id2 = parseId();
|
|
671
|
+
if (!id2) {
|
|
672
|
+
diagnostics.push({
|
|
673
|
+
severity: "error",
|
|
674
|
+
code: "P003",
|
|
675
|
+
message: "Expected identifier after comma in set",
|
|
676
|
+
range: { start: peek().start, end: peek().end }
|
|
677
|
+
});
|
|
678
|
+
break;
|
|
679
|
+
}
|
|
680
|
+
ids.push(id2);
|
|
681
|
+
}
|
|
682
|
+
skipSeparators();
|
|
683
|
+
const rb = peek();
|
|
684
|
+
if (rb.type === "RBRACKET") {
|
|
685
|
+
advance();
|
|
686
|
+
return { type: "artifact-expr", ids, start, end: rb.end };
|
|
687
|
+
}
|
|
688
|
+
diagnostics.push({
|
|
689
|
+
severity: "error",
|
|
690
|
+
code: "P011",
|
|
691
|
+
message: "Expected ] to close artifact set",
|
|
692
|
+
range: { start: rb.start, end: rb.end }
|
|
693
|
+
});
|
|
694
|
+
return { type: "artifact-expr", ids, start, end: rb.start };
|
|
695
|
+
}
|
|
696
|
+
const id = parseId();
|
|
697
|
+
if (!id) return null;
|
|
698
|
+
return { type: "artifact-expr", ids: [id], start: id.start, end: id.end };
|
|
699
|
+
}
|
|
700
|
+
function skipToStatementEnd() {
|
|
701
|
+
while (peek().type !== "NEWLINE" && peek().type !== "SEMICOLON" && peek().type !== "EOF")
|
|
702
|
+
advance();
|
|
703
|
+
}
|
|
704
|
+
function parseStatement() {
|
|
705
|
+
if (isNodeDeclStart()) {
|
|
706
|
+
const id = parseId();
|
|
707
|
+
return {
|
|
708
|
+
type: "node-decl",
|
|
709
|
+
id,
|
|
710
|
+
start: id.start,
|
|
711
|
+
end: id.end
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
if (isOutputEdgeStart()) {
|
|
715
|
+
const processId2 = parseId();
|
|
716
|
+
tryContinuation("ARROW_OUTPUT");
|
|
717
|
+
advance();
|
|
718
|
+
const artifact = parseArtifactExpr();
|
|
719
|
+
if (!artifact) {
|
|
720
|
+
diagnostics.push({
|
|
721
|
+
severity: "error",
|
|
722
|
+
code: "P004",
|
|
723
|
+
message: "Expected artifact expression after ->",
|
|
724
|
+
range: { start: peek().start, end: peek().end }
|
|
725
|
+
});
|
|
726
|
+
return null;
|
|
727
|
+
}
|
|
728
|
+
return {
|
|
729
|
+
type: "output-edge",
|
|
730
|
+
process: processId2,
|
|
731
|
+
artifact,
|
|
732
|
+
start: processId2.start,
|
|
733
|
+
end: artifact.end
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
const head = parseArtifactExpr();
|
|
737
|
+
if (!head) {
|
|
738
|
+
diagnostics.push({
|
|
739
|
+
severity: "error",
|
|
740
|
+
code: "P001",
|
|
741
|
+
message: `Unexpected token: ${peek().type} (${peek().raw})`,
|
|
742
|
+
range: { start: peek().start, end: peek().end }
|
|
743
|
+
});
|
|
744
|
+
return null;
|
|
745
|
+
}
|
|
746
|
+
tryContinuation("ARROW_INPUT", "ARROW_FEEDBACK");
|
|
747
|
+
const opToken = peek();
|
|
748
|
+
if (opToken.type !== "ARROW_INPUT" && opToken.type !== "ARROW_FEEDBACK") {
|
|
749
|
+
diagnostics.push({
|
|
750
|
+
severity: "error",
|
|
751
|
+
code: "P005",
|
|
752
|
+
message: `Expected >> or >>? after artifact, got ${opToken.type}`,
|
|
753
|
+
range: { start: opToken.start, end: opToken.end }
|
|
754
|
+
});
|
|
755
|
+
return null;
|
|
756
|
+
}
|
|
757
|
+
const op = opToken.type === "ARROW_INPUT" ? ">>" : ">>?";
|
|
758
|
+
advance();
|
|
759
|
+
const processId = parseId();
|
|
760
|
+
if (!processId) {
|
|
761
|
+
diagnostics.push({
|
|
762
|
+
severity: "error",
|
|
763
|
+
code: "P006",
|
|
764
|
+
message: "Expected process identifier",
|
|
765
|
+
range: { start: peek().start, end: peek().end }
|
|
766
|
+
});
|
|
767
|
+
return null;
|
|
768
|
+
}
|
|
769
|
+
tryContinuation("ARROW_OUTPUT");
|
|
770
|
+
if (peek().type !== "ARROW_OUTPUT") {
|
|
771
|
+
if (op === ">>") {
|
|
772
|
+
return {
|
|
773
|
+
type: "input-edge",
|
|
774
|
+
artifact: head,
|
|
775
|
+
process: processId,
|
|
776
|
+
start: head.start,
|
|
777
|
+
end: processId.end
|
|
778
|
+
};
|
|
779
|
+
} else {
|
|
780
|
+
return {
|
|
781
|
+
type: "feedback-edge",
|
|
782
|
+
artifact: head,
|
|
783
|
+
process: processId,
|
|
784
|
+
start: head.start,
|
|
785
|
+
end: processId.end
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
advance();
|
|
790
|
+
const firstOutput = parseArtifactExpr();
|
|
791
|
+
if (!firstOutput) {
|
|
792
|
+
diagnostics.push({
|
|
793
|
+
severity: "error",
|
|
794
|
+
code: "P007",
|
|
795
|
+
message: "Expected artifact expression after -> in chain",
|
|
796
|
+
range: { start: peek().start, end: peek().end }
|
|
797
|
+
});
|
|
798
|
+
return null;
|
|
799
|
+
}
|
|
800
|
+
const segments = [
|
|
801
|
+
{ op, process: processId, output: firstOutput }
|
|
802
|
+
];
|
|
803
|
+
while (true) {
|
|
804
|
+
tryContinuation("ARROW_INPUT", "ARROW_FEEDBACK");
|
|
805
|
+
if (peek().type !== "ARROW_INPUT" && peek().type !== "ARROW_FEEDBACK")
|
|
806
|
+
break;
|
|
807
|
+
const segOp = peek().type === "ARROW_INPUT" ? ">>" : ">>?";
|
|
808
|
+
advance();
|
|
809
|
+
const segProcess = parseId();
|
|
810
|
+
if (!segProcess) {
|
|
811
|
+
diagnostics.push({
|
|
812
|
+
severity: "error",
|
|
813
|
+
code: "P008",
|
|
814
|
+
message: "Expected process identifier in chain continuation",
|
|
815
|
+
range: { start: peek().start, end: peek().end }
|
|
816
|
+
});
|
|
817
|
+
skipToStatementEnd();
|
|
818
|
+
break;
|
|
819
|
+
}
|
|
820
|
+
tryContinuation("ARROW_OUTPUT");
|
|
821
|
+
if (peek().type !== "ARROW_OUTPUT") {
|
|
822
|
+
segments.push({ op: segOp, process: segProcess, output: null });
|
|
823
|
+
break;
|
|
824
|
+
}
|
|
825
|
+
advance();
|
|
826
|
+
const segOutput = parseArtifactExpr();
|
|
827
|
+
if (!segOutput) {
|
|
828
|
+
diagnostics.push({
|
|
829
|
+
severity: "error",
|
|
830
|
+
code: "P010",
|
|
831
|
+
message: "Expected artifact expression in chain continuation",
|
|
832
|
+
range: { start: peek().start, end: peek().end }
|
|
833
|
+
});
|
|
834
|
+
skipToStatementEnd();
|
|
835
|
+
break;
|
|
836
|
+
}
|
|
837
|
+
segments.push({ op: segOp, process: segProcess, output: segOutput });
|
|
838
|
+
}
|
|
839
|
+
const last = segments[segments.length - 1];
|
|
840
|
+
return {
|
|
841
|
+
type: "chain",
|
|
842
|
+
head,
|
|
843
|
+
segments,
|
|
844
|
+
start: head.start,
|
|
845
|
+
end: last.output?.end ?? last.process.end
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
const statements = [];
|
|
849
|
+
skipSeparators();
|
|
850
|
+
while (peek().type !== "EOF") {
|
|
851
|
+
const stmt = parseStatement();
|
|
852
|
+
if (stmt) {
|
|
853
|
+
statements.push(stmt);
|
|
854
|
+
} else {
|
|
855
|
+
skipToStatementEnd();
|
|
856
|
+
}
|
|
857
|
+
skipSeparators();
|
|
858
|
+
}
|
|
859
|
+
return { document: { type: "document", statements }, diagnostics };
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
// src/sorter.ts
|
|
863
|
+
function sortIsolated(isolatedNodes) {
|
|
864
|
+
return [...isolatedNodes].sort();
|
|
865
|
+
}
|
|
866
|
+
function sortEdges(edges, graph) {
|
|
867
|
+
const parent = /* @__PURE__ */ new Map();
|
|
868
|
+
function find(x) {
|
|
869
|
+
if (!parent.has(x)) parent.set(x, x);
|
|
870
|
+
const p = parent.get(x) ?? x;
|
|
871
|
+
if (p === x) return x;
|
|
872
|
+
const root = find(p);
|
|
873
|
+
parent.set(x, root);
|
|
874
|
+
return root;
|
|
875
|
+
}
|
|
876
|
+
function union(x, y) {
|
|
877
|
+
const rx = find(x), ry = find(y);
|
|
878
|
+
if (rx !== ry) parent.set(rx, ry);
|
|
879
|
+
}
|
|
880
|
+
for (const e of graph.primaryEdges) union(e.from, e.to);
|
|
881
|
+
const componentMin = /* @__PURE__ */ new Map();
|
|
882
|
+
for (const nodeId of graph.nodes.keys()) {
|
|
883
|
+
const root = find(nodeId);
|
|
884
|
+
const cur = componentMin.get(root);
|
|
885
|
+
if (cur === void 0 || nodeId < cur) componentMin.set(root, nodeId);
|
|
886
|
+
}
|
|
887
|
+
function componentKey(nodeId) {
|
|
888
|
+
return componentMin.get(find(nodeId)) ?? nodeId;
|
|
889
|
+
}
|
|
890
|
+
const inDegree = /* @__PURE__ */ new Map();
|
|
891
|
+
const adjacency = /* @__PURE__ */ new Map();
|
|
892
|
+
for (const id of graph.nodes.keys()) {
|
|
893
|
+
inDegree.set(id, 0);
|
|
894
|
+
adjacency.set(id, []);
|
|
895
|
+
}
|
|
896
|
+
for (const e of graph.primaryEdges) {
|
|
897
|
+
inDegree.set(e.to, (inDegree.get(e.to) ?? 0) + 1);
|
|
898
|
+
adjacency.get(e.from)?.push(e.to);
|
|
899
|
+
}
|
|
900
|
+
const ranks = /* @__PURE__ */ new Map();
|
|
901
|
+
const queue = [];
|
|
902
|
+
for (const [id, deg] of inDegree) {
|
|
903
|
+
if (deg === 0) {
|
|
904
|
+
ranks.set(id, 0);
|
|
905
|
+
queue.push(id);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
for (let head = 0; head < queue.length; head++) {
|
|
909
|
+
const u = queue[head];
|
|
910
|
+
const ru = ranks.get(u);
|
|
911
|
+
for (const v of adjacency.get(u)) {
|
|
912
|
+
const rv = ranks.get(v) ?? -1;
|
|
913
|
+
if (ru + 1 > rv) ranks.set(v, ru + 1);
|
|
914
|
+
const remaining = inDegree.get(v) - 1;
|
|
915
|
+
inDegree.set(v, remaining);
|
|
916
|
+
if (remaining === 0) queue.push(v);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
for (const id of graph.nodes.keys()) {
|
|
920
|
+
if (!ranks.has(id)) ranks.set(id, 0);
|
|
921
|
+
}
|
|
922
|
+
function edgeRank(e) {
|
|
923
|
+
if (e.kind === "input") return ranks.get(e.artifact) ?? 0;
|
|
924
|
+
if (e.kind === "feedback") return ranks.get(e.process) ?? 0;
|
|
925
|
+
return ranks.get(e.process) ?? 0;
|
|
926
|
+
}
|
|
927
|
+
function edgeKindOrder(e) {
|
|
928
|
+
if (e.kind === "input") return 0;
|
|
929
|
+
if (e.kind === "feedback") return 1;
|
|
930
|
+
return 2;
|
|
931
|
+
}
|
|
932
|
+
function edgeLexKey(e) {
|
|
933
|
+
return e.kind === "output" ? `${e.process}\0${e.artifact}` : `${e.artifact}\0${e.process}`;
|
|
934
|
+
}
|
|
935
|
+
const compKeys = /* @__PURE__ */ new Map();
|
|
936
|
+
for (const e of edges) {
|
|
937
|
+
const ref = e.kind === "output" ? e.process : e.artifact;
|
|
938
|
+
compKeys.set(e, componentKey(ref));
|
|
939
|
+
}
|
|
940
|
+
return [...edges].sort((a, b) => {
|
|
941
|
+
const ck = compKeys.get(a).localeCompare(compKeys.get(b));
|
|
942
|
+
if (ck !== 0) return ck;
|
|
943
|
+
const rk = edgeRank(a) - edgeRank(b);
|
|
944
|
+
if (rk !== 0) return rk;
|
|
945
|
+
const kk = edgeKindOrder(a) - edgeKindOrder(b);
|
|
946
|
+
if (kk !== 0) return kk;
|
|
947
|
+
return edgeLexKey(a).localeCompare(edgeLexKey(b));
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// src/types/frontmatter.ts
|
|
952
|
+
var STATUS_VALUES = ["done", "wip", "todo", "blocked"];
|
|
953
|
+
var STYLE_ATTRS = [
|
|
954
|
+
"fillcolor",
|
|
955
|
+
"color",
|
|
956
|
+
"fontcolor",
|
|
957
|
+
"style",
|
|
958
|
+
"penwidth"
|
|
959
|
+
];
|
|
960
|
+
|
|
961
|
+
// src/validator.ts
|
|
962
|
+
var STATUS_SET = new Set(STATUS_VALUES);
|
|
963
|
+
var STYLE_ATTR_SET = new Set(STYLE_ATTRS);
|
|
964
|
+
function validate(edges, nodeKinds, fm, options) {
|
|
965
|
+
const diagnostics = [];
|
|
966
|
+
const nodeRanges = options?.source ? findFrontmatterNodeRanges(options.source) : /* @__PURE__ */ new Map();
|
|
967
|
+
const artifactGenerators = /* @__PURE__ */ new Map();
|
|
968
|
+
for (const e of edges) {
|
|
969
|
+
if (e.kind === "output") {
|
|
970
|
+
const gens = artifactGenerators.get(e.artifact) ?? [];
|
|
971
|
+
gens.push(e.process);
|
|
972
|
+
artifactGenerators.set(e.artifact, gens);
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
for (const [artifact, processes] of artifactGenerators) {
|
|
976
|
+
if (processes.length > 1) {
|
|
977
|
+
diagnostics.push({
|
|
978
|
+
severity: "error",
|
|
979
|
+
code: "V001",
|
|
980
|
+
message: `'${artifact}' generated by multiple processes: ${processes.join(", ")}`,
|
|
981
|
+
range: zeroRange()
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
const processInputCount = /* @__PURE__ */ new Map();
|
|
986
|
+
const processOutputCount = /* @__PURE__ */ new Map();
|
|
987
|
+
for (const e of edges) {
|
|
988
|
+
if (!processInputCount.has(e.process)) processInputCount.set(e.process, 0);
|
|
989
|
+
if (!processOutputCount.has(e.process))
|
|
990
|
+
processOutputCount.set(e.process, 0);
|
|
991
|
+
if (e.kind === "input" || e.kind === "feedback") {
|
|
992
|
+
processInputCount.set(
|
|
993
|
+
e.process,
|
|
994
|
+
(processInputCount.get(e.process) ?? 0) + 1
|
|
995
|
+
);
|
|
996
|
+
} else {
|
|
997
|
+
processOutputCount.set(
|
|
998
|
+
e.process,
|
|
999
|
+
(processOutputCount.get(e.process) ?? 0) + 1
|
|
1000
|
+
);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
for (const [id, count] of processInputCount) {
|
|
1004
|
+
if (count === 0)
|
|
1005
|
+
diagnostics.push({
|
|
1006
|
+
severity: "error",
|
|
1007
|
+
code: "V002",
|
|
1008
|
+
message: `Process '${id}' has no inputs`,
|
|
1009
|
+
range: zeroRange()
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
for (const [id, count] of processOutputCount) {
|
|
1013
|
+
if (count === 0)
|
|
1014
|
+
diagnostics.push({
|
|
1015
|
+
severity: "error",
|
|
1016
|
+
code: "V003",
|
|
1017
|
+
message: `Process '${id}' has no outputs`,
|
|
1018
|
+
range: zeroRange()
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1021
|
+
for (const pid of Object.keys(fm?.process ?? {})) {
|
|
1022
|
+
if (!processInputCount.has(pid)) {
|
|
1023
|
+
diagnostics.push({
|
|
1024
|
+
severity: "error",
|
|
1025
|
+
code: "V020",
|
|
1026
|
+
message: `Process '${pid}' is declared but has no edges (orphaned process)`,
|
|
1027
|
+
range: zeroRange()
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
const artifactMeta = fm?.artifact ?? {};
|
|
1032
|
+
const nodesWithEdges = /* @__PURE__ */ new Set();
|
|
1033
|
+
for (const e of edges) {
|
|
1034
|
+
if (e.kind === "input" || e.kind === "feedback") {
|
|
1035
|
+
nodesWithEdges.add(e.artifact);
|
|
1036
|
+
nodesWithEdges.add(e.process);
|
|
1037
|
+
} else {
|
|
1038
|
+
nodesWithEdges.add(e.process);
|
|
1039
|
+
nodesWithEdges.add(e.artifact);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
for (const [artifactId, meta] of Object.entries(artifactMeta)) {
|
|
1043
|
+
for (const partId of meta.parts ?? []) {
|
|
1044
|
+
if (nodeKinds.get(partId) === "process") {
|
|
1045
|
+
diagnostics.push({
|
|
1046
|
+
severity: "error",
|
|
1047
|
+
code: "V004",
|
|
1048
|
+
message: `Parts member '${partId}' of '${artifactId}' is a process`,
|
|
1049
|
+
range: zeroRange()
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
if (partId === artifactId) {
|
|
1053
|
+
diagnostics.push({
|
|
1054
|
+
severity: "error",
|
|
1055
|
+
code: "V005",
|
|
1056
|
+
message: `'${artifactId}' cannot include itself in parts`,
|
|
1057
|
+
range: zeroRange()
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1060
|
+
if (!nodesWithEdges.has(partId) && nodeKinds.get(partId) !== "process") {
|
|
1061
|
+
diagnostics.push({
|
|
1062
|
+
severity: "warning",
|
|
1063
|
+
code: "W001",
|
|
1064
|
+
message: `Parts member '${partId}' of '${artifactId}' has no edges`,
|
|
1065
|
+
range: zeroRange()
|
|
1066
|
+
});
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
if (meta.status !== void 0 && !STATUS_SET.has(meta.status)) {
|
|
1070
|
+
diagnostics.push({
|
|
1071
|
+
severity: "error",
|
|
1072
|
+
code: "V007",
|
|
1073
|
+
message: `Invalid status '${meta.status}' on artifact '${artifactId}'. Allowed: ${STATUS_VALUES.join(", ")}`,
|
|
1074
|
+
range: zeroRange()
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
const checkIndices = (entries, kind) => {
|
|
1079
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1080
|
+
for (const [id, meta] of Object.entries(entries)) {
|
|
1081
|
+
const idx = meta.index;
|
|
1082
|
+
if (idx === void 0) continue;
|
|
1083
|
+
if (typeof idx !== "number" || !Number.isInteger(idx) || idx < 1) {
|
|
1084
|
+
diagnostics.push({
|
|
1085
|
+
severity: "error",
|
|
1086
|
+
code: "V029",
|
|
1087
|
+
message: `Invalid index '${String(idx)}' on ${kind} '${id}'. Must be a positive integer`,
|
|
1088
|
+
range: nodeRanges.get(id) ?? zeroRange()
|
|
1089
|
+
});
|
|
1090
|
+
continue;
|
|
1091
|
+
}
|
|
1092
|
+
const prev = seen.get(idx);
|
|
1093
|
+
if (prev !== void 0) {
|
|
1094
|
+
diagnostics.push({
|
|
1095
|
+
severity: "warning",
|
|
1096
|
+
code: "W004",
|
|
1097
|
+
message: `Duplicate index ${idx} on ${kind} '${id}' (also on '${prev}')`,
|
|
1098
|
+
range: nodeRanges.get(id) ?? zeroRange()
|
|
1099
|
+
});
|
|
1100
|
+
} else {
|
|
1101
|
+
seen.set(idx, id);
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
};
|
|
1105
|
+
checkIndices(fm?.artifact ?? {}, "artifact");
|
|
1106
|
+
checkIndices(fm?.process ?? {}, "process");
|
|
1107
|
+
for (const [key, style] of Object.entries(fm?.statusStyles ?? {})) {
|
|
1108
|
+
if (!STATUS_SET.has(key)) {
|
|
1109
|
+
diagnostics.push({
|
|
1110
|
+
severity: "error",
|
|
1111
|
+
code: "V008",
|
|
1112
|
+
message: `Invalid statusStyles key '${key}'. Allowed: ${STATUS_VALUES.join(", ")}`,
|
|
1113
|
+
range: zeroRange()
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
for (const attr of Object.keys(style ?? {})) {
|
|
1117
|
+
if (!STYLE_ATTR_SET.has(attr)) {
|
|
1118
|
+
diagnostics.push({
|
|
1119
|
+
severity: "error",
|
|
1120
|
+
code: "V009",
|
|
1121
|
+
message: `Invalid style attribute '${attr}' in statusStyles.${key}. Allowed: ${STYLE_ATTRS.join(", ")}`,
|
|
1122
|
+
range: zeroRange()
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
for (const [key, meta] of Object.entries(fm?.tag ?? {})) {
|
|
1128
|
+
for (const attr of Object.keys(meta?.style ?? {})) {
|
|
1129
|
+
if (!STYLE_ATTR_SET.has(attr)) {
|
|
1130
|
+
diagnostics.push({
|
|
1131
|
+
severity: "error",
|
|
1132
|
+
code: "V009",
|
|
1133
|
+
message: `Invalid style attribute '${attr}' in tag.${key}.style. Allowed: ${STYLE_ATTRS.join(", ")}`,
|
|
1134
|
+
range: zeroRange()
|
|
1135
|
+
});
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1140
|
+
const inStack = /* @__PURE__ */ new Set();
|
|
1141
|
+
const path = [];
|
|
1142
|
+
function detectCycle(id) {
|
|
1143
|
+
if (inStack.has(id)) {
|
|
1144
|
+
diagnostics.push({
|
|
1145
|
+
severity: "error",
|
|
1146
|
+
code: "V006",
|
|
1147
|
+
message: `Cycle in parts: ${[...path, id].join(" \u2192 ")}`,
|
|
1148
|
+
range: zeroRange()
|
|
1149
|
+
});
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
if (visited.has(id)) return;
|
|
1153
|
+
visited.add(id);
|
|
1154
|
+
inStack.add(id);
|
|
1155
|
+
path.push(id);
|
|
1156
|
+
for (const part of artifactMeta[id]?.parts ?? []) detectCycle(part);
|
|
1157
|
+
path.pop();
|
|
1158
|
+
inStack.delete(id);
|
|
1159
|
+
}
|
|
1160
|
+
for (const id of Object.keys(artifactMeta)) detectCycle(id);
|
|
1161
|
+
const primaryAdj = /* @__PURE__ */ new Map();
|
|
1162
|
+
for (const e of edges) {
|
|
1163
|
+
if (e.kind === "input") {
|
|
1164
|
+
const adj = primaryAdj.get(e.artifact) ?? [];
|
|
1165
|
+
adj.push(e.process);
|
|
1166
|
+
primaryAdj.set(e.artifact, adj);
|
|
1167
|
+
} else if (e.kind === "output") {
|
|
1168
|
+
const adj = primaryAdj.get(e.process) ?? [];
|
|
1169
|
+
adj.push(e.artifact);
|
|
1170
|
+
primaryAdj.set(e.process, adj);
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
{
|
|
1174
|
+
let dfsV0102 = function(id) {
|
|
1175
|
+
if (color.get(id) === "gray") return true;
|
|
1176
|
+
if (color.get(id) === "black") return false;
|
|
1177
|
+
color.set(id, "gray");
|
|
1178
|
+
for (const neighbor of primaryAdj.get(id) ?? []) {
|
|
1179
|
+
if (dfsV0102(neighbor)) {
|
|
1180
|
+
if (!cycleReported) {
|
|
1181
|
+
cycleReported = true;
|
|
1182
|
+
diagnostics.push({
|
|
1183
|
+
severity: "error",
|
|
1184
|
+
code: "V010",
|
|
1185
|
+
message: `Primary graph contains a cycle involving '${id}' \u2192 '${neighbor}'`,
|
|
1186
|
+
range: zeroRange()
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1189
|
+
color.set(id, "black");
|
|
1190
|
+
return false;
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
color.set(id, "black");
|
|
1194
|
+
return false;
|
|
1195
|
+
};
|
|
1196
|
+
var dfsV010 = dfsV0102;
|
|
1197
|
+
const color = /* @__PURE__ */ new Map();
|
|
1198
|
+
const allNodes = /* @__PURE__ */ new Set();
|
|
1199
|
+
for (const e of edges) {
|
|
1200
|
+
if (e.kind !== "feedback") {
|
|
1201
|
+
allNodes.add(e.kind === "output" ? e.process : e.artifact);
|
|
1202
|
+
allNodes.add(e.kind === "output" ? e.artifact : e.process);
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
for (const n of allNodes) color.set(n, "white");
|
|
1206
|
+
let cycleReported = false;
|
|
1207
|
+
for (const n of allNodes) {
|
|
1208
|
+
if (color.get(n) === "white") dfsV0102(n);
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
if (options?.strict) {
|
|
1212
|
+
let primaryReachable2 = function(startProcess) {
|
|
1213
|
+
const reachable = /* @__PURE__ */ new Set();
|
|
1214
|
+
const queue = [startProcess];
|
|
1215
|
+
while (queue.length > 0) {
|
|
1216
|
+
const node = queue.shift();
|
|
1217
|
+
for (const neighbor of primaryAdj.get(node) ?? []) {
|
|
1218
|
+
if (!reachable.has(neighbor)) {
|
|
1219
|
+
reachable.add(neighbor);
|
|
1220
|
+
queue.push(neighbor);
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
return reachable;
|
|
1225
|
+
};
|
|
1226
|
+
var primaryReachable = primaryReachable2;
|
|
1227
|
+
for (const e of edges) {
|
|
1228
|
+
if (e.kind === "feedback") {
|
|
1229
|
+
const reachable = primaryReachable2(e.process);
|
|
1230
|
+
if (!reachable.has(e.artifact)) {
|
|
1231
|
+
diagnostics.push({
|
|
1232
|
+
severity: "error",
|
|
1233
|
+
code: "V011",
|
|
1234
|
+
message: `Feedback artifact '${e.artifact}' is not reachable from process '${e.process}' in the primary graph`,
|
|
1235
|
+
range: zeroRange()
|
|
1236
|
+
});
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
const processMeta = fm?.process ?? {};
|
|
1242
|
+
for (const [pid, meta] of Object.entries(processMeta)) {
|
|
1243
|
+
const m = meta;
|
|
1244
|
+
if (m.criteria !== void 0) {
|
|
1245
|
+
diagnostics.push({
|
|
1246
|
+
severity: "error",
|
|
1247
|
+
code: "V012",
|
|
1248
|
+
message: `'criteria' is not allowed on process '${pid}'`,
|
|
1249
|
+
range: zeroRange()
|
|
1250
|
+
});
|
|
1251
|
+
}
|
|
1252
|
+
if (m.location !== void 0) {
|
|
1253
|
+
diagnostics.push({
|
|
1254
|
+
severity: "error",
|
|
1255
|
+
code: "V013",
|
|
1256
|
+
message: `'location' is not allowed on process '${pid}'`,
|
|
1257
|
+
range: zeroRange()
|
|
1258
|
+
});
|
|
1259
|
+
}
|
|
1260
|
+
if (m.revises !== void 0) {
|
|
1261
|
+
diagnostics.push({
|
|
1262
|
+
severity: "error",
|
|
1263
|
+
code: "V015",
|
|
1264
|
+
message: `'revises' is not allowed on process '${pid}'`,
|
|
1265
|
+
range: zeroRange()
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
for (const [aid, meta] of Object.entries(artifactMeta)) {
|
|
1270
|
+
if (meta.criteria === void 0) {
|
|
1271
|
+
diagnostics.push({
|
|
1272
|
+
severity: options?.strict ? "error" : "warning",
|
|
1273
|
+
code: "W002",
|
|
1274
|
+
message: `Artifact '${aid}' has no 'criteria' field`,
|
|
1275
|
+
range: nodeRanges.get(aid) ?? zeroRange()
|
|
1276
|
+
});
|
|
1277
|
+
}
|
|
1278
|
+
if (meta.command !== void 0) {
|
|
1279
|
+
diagnostics.push({
|
|
1280
|
+
severity: "error",
|
|
1281
|
+
code: "V014",
|
|
1282
|
+
message: `'command' is not allowed on artifact '${aid}'`,
|
|
1283
|
+
range: zeroRange()
|
|
1284
|
+
});
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
const revisesTargets = /* @__PURE__ */ new Map();
|
|
1288
|
+
for (const [aid, meta] of Object.entries(artifactMeta)) {
|
|
1289
|
+
const target = meta.revises;
|
|
1290
|
+
if (target === void 0 || target === null) continue;
|
|
1291
|
+
if (typeof target !== "string") {
|
|
1292
|
+
diagnostics.push({
|
|
1293
|
+
severity: "error",
|
|
1294
|
+
code: "V016",
|
|
1295
|
+
message: `'revises' on artifact '${aid}' must be a string, got ${typeof target}`,
|
|
1296
|
+
range: zeroRange()
|
|
1297
|
+
});
|
|
1298
|
+
continue;
|
|
1299
|
+
}
|
|
1300
|
+
if (target === aid) {
|
|
1301
|
+
diagnostics.push({
|
|
1302
|
+
severity: "error",
|
|
1303
|
+
code: "V017",
|
|
1304
|
+
message: `Artifact '${aid}' revises itself`,
|
|
1305
|
+
range: zeroRange()
|
|
1306
|
+
});
|
|
1307
|
+
continue;
|
|
1308
|
+
}
|
|
1309
|
+
if (artifactMeta[target] === void 0) {
|
|
1310
|
+
diagnostics.push({
|
|
1311
|
+
severity: "error",
|
|
1312
|
+
code: "V016",
|
|
1313
|
+
message: `'revises' target '${target}' of artifact '${aid}' not found in this file`,
|
|
1314
|
+
range: zeroRange()
|
|
1315
|
+
});
|
|
1316
|
+
continue;
|
|
1317
|
+
}
|
|
1318
|
+
revisesTargets.set(aid, target);
|
|
1319
|
+
}
|
|
1320
|
+
const revisedBy = /* @__PURE__ */ new Map();
|
|
1321
|
+
for (const [aid, target] of revisesTargets) {
|
|
1322
|
+
const arr = revisedBy.get(target) ?? [];
|
|
1323
|
+
arr.push(aid);
|
|
1324
|
+
revisedBy.set(target, arr);
|
|
1325
|
+
}
|
|
1326
|
+
for (const [target, revisors] of revisedBy) {
|
|
1327
|
+
if (revisors.length > 1) {
|
|
1328
|
+
diagnostics.push({
|
|
1329
|
+
severity: "error",
|
|
1330
|
+
code: "V018",
|
|
1331
|
+
message: `Artifact '${target}' is revised by multiple artifacts: ${revisors.join(", ")}`,
|
|
1332
|
+
range: zeroRange()
|
|
1333
|
+
});
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
{
|
|
1337
|
+
let dfsRevises2 = function(id) {
|
|
1338
|
+
if (color.get(id) === "gray") return true;
|
|
1339
|
+
if (color.get(id) === "black") return false;
|
|
1340
|
+
color.set(id, "gray");
|
|
1341
|
+
const target = revisesTargets.get(id);
|
|
1342
|
+
if (target !== void 0 && dfsRevises2(target)) {
|
|
1343
|
+
diagnostics.push({
|
|
1344
|
+
severity: "error",
|
|
1345
|
+
code: "V019",
|
|
1346
|
+
message: `Cycle detected in 'revises' chain involving '${id}'`,
|
|
1347
|
+
range: zeroRange()
|
|
1348
|
+
});
|
|
1349
|
+
color.set(id, "black");
|
|
1350
|
+
return false;
|
|
1351
|
+
}
|
|
1352
|
+
color.set(id, "black");
|
|
1353
|
+
return false;
|
|
1354
|
+
};
|
|
1355
|
+
var dfsRevises = dfsRevises2;
|
|
1356
|
+
const color = /* @__PURE__ */ new Map();
|
|
1357
|
+
for (const id of Object.keys(artifactMeta)) color.set(id, "white");
|
|
1358
|
+
for (const id of Object.keys(artifactMeta)) {
|
|
1359
|
+
if (color.get(id) === "white") dfsRevises2(id);
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
{
|
|
1363
|
+
const processInputs = /* @__PURE__ */ new Map();
|
|
1364
|
+
const processOutputs = /* @__PURE__ */ new Map();
|
|
1365
|
+
for (const e of edges) {
|
|
1366
|
+
if (e.kind === "input") {
|
|
1367
|
+
const arr = processInputs.get(e.process) ?? [];
|
|
1368
|
+
arr.push(e.artifact);
|
|
1369
|
+
processInputs.set(e.process, arr);
|
|
1370
|
+
} else if (e.kind === "output") {
|
|
1371
|
+
const arr = processOutputs.get(e.process) ?? [];
|
|
1372
|
+
arr.push(e.artifact);
|
|
1373
|
+
processOutputs.set(e.process, arr);
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
for (const [pid, outputs] of processOutputs) {
|
|
1377
|
+
const hasDoneOutput = outputs.some(
|
|
1378
|
+
(aid) => artifactMeta[aid]?.status === "done"
|
|
1379
|
+
);
|
|
1380
|
+
if (!hasDoneOutput) continue;
|
|
1381
|
+
for (const aid of processInputs.get(pid) ?? []) {
|
|
1382
|
+
const status = artifactMeta[aid]?.status;
|
|
1383
|
+
if (status !== void 0 && status !== "done") {
|
|
1384
|
+
diagnostics.push({
|
|
1385
|
+
severity: "warning",
|
|
1386
|
+
code: "W003",
|
|
1387
|
+
message: `Process '${pid}' outputs a 'done' artifact but input '${aid}' has status '${status}'`,
|
|
1388
|
+
range: zeroRange()
|
|
1389
|
+
});
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
for (const [id, meta] of Object.entries(fm?.artifact ?? {})) {
|
|
1395
|
+
if (meta.subflow !== void 0) {
|
|
1396
|
+
diagnostics.push({
|
|
1397
|
+
severity: "error",
|
|
1398
|
+
code: "V023",
|
|
1399
|
+
message: `'subflow' is not allowed on artifact '${id}'`,
|
|
1400
|
+
range: zeroRange()
|
|
1401
|
+
});
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
for (const [pid, meta] of Object.entries(fm?.process ?? {})) {
|
|
1405
|
+
if (meta.boundary !== void 0 && meta.subflow === void 0) {
|
|
1406
|
+
diagnostics.push({
|
|
1407
|
+
severity: "error",
|
|
1408
|
+
code: "V024",
|
|
1409
|
+
message: `'boundary' on process '${pid}' requires 'subflow'`,
|
|
1410
|
+
range: zeroRange()
|
|
1411
|
+
});
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
{
|
|
1415
|
+
let dfsGroup2 = function(id) {
|
|
1416
|
+
color.set(id, "gray");
|
|
1417
|
+
const parent = groupMeta[id]?.parent;
|
|
1418
|
+
if (parent !== void 0) {
|
|
1419
|
+
if (color.get(parent) === "gray") {
|
|
1420
|
+
diagnostics.push({
|
|
1421
|
+
severity: "error",
|
|
1422
|
+
code: "V025",
|
|
1423
|
+
message: `Cycle detected in 'parent' chain involving group '${id}' \u2192 '${parent}'`,
|
|
1424
|
+
range: zeroRange()
|
|
1425
|
+
});
|
|
1426
|
+
color.set(id, "black");
|
|
1427
|
+
return true;
|
|
1428
|
+
}
|
|
1429
|
+
if (color.get(parent) === "white") {
|
|
1430
|
+
if (dfsGroup2(parent)) {
|
|
1431
|
+
color.set(id, "black");
|
|
1432
|
+
return true;
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
color.set(id, "black");
|
|
1437
|
+
return false;
|
|
1438
|
+
};
|
|
1439
|
+
var dfsGroup = dfsGroup2;
|
|
1440
|
+
const groupMeta = fm?.group ?? {};
|
|
1441
|
+
const color = /* @__PURE__ */ new Map();
|
|
1442
|
+
for (const id of Object.keys(groupMeta)) color.set(id, "white");
|
|
1443
|
+
for (const id of Object.keys(groupMeta)) {
|
|
1444
|
+
if (color.get(id) === "white") dfsGroup2(id);
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
return diagnostics;
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
// src/meta.ts
|
|
1451
|
+
function resolveMeta(fm, kind, id) {
|
|
1452
|
+
if (!fm) return void 0;
|
|
1453
|
+
if (kind === "artifact") return fm.artifact?.[id];
|
|
1454
|
+
if (kind === "group") return fm.group?.[id];
|
|
1455
|
+
return fm.process?.[id];
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
// src/diff.ts
|
|
1459
|
+
function edgeKey2(from, to) {
|
|
1460
|
+
return `${from} -> ${to}`;
|
|
1461
|
+
}
|
|
1462
|
+
function setDiff(lhs, rhs) {
|
|
1463
|
+
return [...rhs].filter((x) => !lhs.has(x)).sort();
|
|
1464
|
+
}
|
|
1465
|
+
function stableStringify(value) {
|
|
1466
|
+
if (value === void 0) return "undefined";
|
|
1467
|
+
if (value === null) return "null";
|
|
1468
|
+
if (typeof value !== "object") return JSON.stringify(value);
|
|
1469
|
+
if (Array.isArray(value)) {
|
|
1470
|
+
return `[${value.map(stableStringify).join(",")}]`;
|
|
1471
|
+
}
|
|
1472
|
+
const keys = Object.keys(value).sort();
|
|
1473
|
+
const pairs = keys.map((k) => {
|
|
1474
|
+
const v = value[k];
|
|
1475
|
+
if (v === void 0) return null;
|
|
1476
|
+
return `${JSON.stringify(k)}:${stableStringify(v)}`;
|
|
1477
|
+
}).filter((p) => p !== null);
|
|
1478
|
+
return `{${pairs.join(",")}}`;
|
|
1479
|
+
}
|
|
1480
|
+
function diffGraphs(a, b, fmA, fmB) {
|
|
1481
|
+
const aNodes = new Set(a.nodes.keys());
|
|
1482
|
+
const bNodes = new Set(b.nodes.keys());
|
|
1483
|
+
const aEdges = new Set(a.primaryEdges.map((e) => edgeKey2(e.from, e.to)));
|
|
1484
|
+
const bEdges = new Set(b.primaryEdges.map((e) => edgeKey2(e.from, e.to)));
|
|
1485
|
+
const aFb = new Set(
|
|
1486
|
+
a.feedbackEdges.map((e) => edgeKey2(e.artifact, e.process))
|
|
1487
|
+
);
|
|
1488
|
+
const bFb = new Set(
|
|
1489
|
+
b.feedbackEdges.map((e) => edgeKey2(e.artifact, e.process))
|
|
1490
|
+
);
|
|
1491
|
+
const commonIds = [...aNodes].filter((id) => bNodes.has(id));
|
|
1492
|
+
const changedNodes = [];
|
|
1493
|
+
for (const id of commonIds) {
|
|
1494
|
+
const kindA = a.nodes.get(id);
|
|
1495
|
+
const kindB = b.nodes.get(id);
|
|
1496
|
+
if (kindA !== kindB) {
|
|
1497
|
+
changedNodes.push(id);
|
|
1498
|
+
continue;
|
|
1499
|
+
}
|
|
1500
|
+
if (fmA != null && fmB != null && kindB != null) {
|
|
1501
|
+
const metaA = resolveMeta(fmA, kindB, id);
|
|
1502
|
+
const metaB = resolveMeta(fmB, kindB, id);
|
|
1503
|
+
if (stableStringify(metaA) !== stableStringify(metaB)) {
|
|
1504
|
+
changedNodes.push(id);
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
changedNodes.sort();
|
|
1509
|
+
return {
|
|
1510
|
+
addedNodes: setDiff(aNodes, bNodes),
|
|
1511
|
+
removedNodes: setDiff(bNodes, aNodes),
|
|
1512
|
+
changedNodes,
|
|
1513
|
+
addedEdges: setDiff(aEdges, bEdges),
|
|
1514
|
+
removedEdges: setDiff(bEdges, aEdges),
|
|
1515
|
+
addedFeedback: setDiff(aFb, bFb),
|
|
1516
|
+
removedFeedback: setDiff(bFb, aFb)
|
|
1517
|
+
};
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
// src/reindex.ts
|
|
1521
|
+
function reindex(source, opts = {}) {
|
|
1522
|
+
const { edges, graph, nodeKinds, frontmatter, diagnostics } = analyze(source);
|
|
1523
|
+
if (diagnostics.some((d) => d.severity === "error")) {
|
|
1524
|
+
return { output: source, changes: [], diagnostics };
|
|
1525
|
+
}
|
|
1526
|
+
const kindOf = (id) => nodeKinds.get(id) ?? "artifact";
|
|
1527
|
+
const existingIndex = (id) => {
|
|
1528
|
+
const meta = kindOf(id) === "process" ? frontmatter?.process?.[id] : frontmatter?.artifact?.[id];
|
|
1529
|
+
return typeof meta?.index === "number" ? meta.index : void 0;
|
|
1530
|
+
};
|
|
1531
|
+
const order = [];
|
|
1532
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1533
|
+
const push = (id) => {
|
|
1534
|
+
if (!seen.has(id)) {
|
|
1535
|
+
seen.add(id);
|
|
1536
|
+
order.push(id);
|
|
1537
|
+
}
|
|
1538
|
+
};
|
|
1539
|
+
for (const e of sortEdges(edges, graph)) {
|
|
1540
|
+
if (e.kind === "input") {
|
|
1541
|
+
push(e.artifact);
|
|
1542
|
+
push(e.process);
|
|
1543
|
+
} else if (e.kind === "output") {
|
|
1544
|
+
push(e.process);
|
|
1545
|
+
push(e.artifact);
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
const remaining = /* @__PURE__ */ new Set([
|
|
1549
|
+
...graph.nodes.keys(),
|
|
1550
|
+
...Object.keys(frontmatter?.artifact ?? {}),
|
|
1551
|
+
...Object.keys(frontmatter?.process ?? {})
|
|
1552
|
+
]);
|
|
1553
|
+
for (const id of [...remaining].sort()) push(id);
|
|
1554
|
+
const assigned = /* @__PURE__ */ new Map();
|
|
1555
|
+
if (opts.renumber) {
|
|
1556
|
+
const counter = {
|
|
1557
|
+
artifact: 0,
|
|
1558
|
+
process: 0,
|
|
1559
|
+
group: 0
|
|
1560
|
+
};
|
|
1561
|
+
for (const id of order) {
|
|
1562
|
+
const kind = kindOf(id);
|
|
1563
|
+
counter[kind] += 1;
|
|
1564
|
+
assigned.set(id, counter[kind]);
|
|
1565
|
+
}
|
|
1566
|
+
} else {
|
|
1567
|
+
const next = {
|
|
1568
|
+
artifact: 0,
|
|
1569
|
+
process: 0,
|
|
1570
|
+
group: 0
|
|
1571
|
+
};
|
|
1572
|
+
for (const id of order) {
|
|
1573
|
+
const cur = existingIndex(id);
|
|
1574
|
+
if (cur !== void 0) next[kindOf(id)] = Math.max(next[kindOf(id)], cur);
|
|
1575
|
+
}
|
|
1576
|
+
for (const id of order) {
|
|
1577
|
+
const cur = existingIndex(id);
|
|
1578
|
+
if (cur !== void 0) {
|
|
1579
|
+
assigned.set(id, cur);
|
|
1580
|
+
continue;
|
|
1581
|
+
}
|
|
1582
|
+
const kind = kindOf(id);
|
|
1583
|
+
next[kind] += 1;
|
|
1584
|
+
assigned.set(id, next[kind]);
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
const changes = [];
|
|
1588
|
+
const writes = [];
|
|
1589
|
+
for (const id of order) {
|
|
1590
|
+
const to = assigned.get(id);
|
|
1591
|
+
const from = existingIndex(id) ?? null;
|
|
1592
|
+
if (from === to) continue;
|
|
1593
|
+
const kind = kindOf(id);
|
|
1594
|
+
changes.push({ kind, id, from, to });
|
|
1595
|
+
writes.push({ kind, id, value: to });
|
|
1596
|
+
}
|
|
1597
|
+
const output = writes.length ? applyWrites(source, writes) : source;
|
|
1598
|
+
return { output, changes, diagnostics };
|
|
1599
|
+
}
|
|
1600
|
+
function applyWrites(source, writes) {
|
|
1601
|
+
const lines = source.split("\n");
|
|
1602
|
+
const trailingNewline = source.endsWith("\n");
|
|
1603
|
+
let open = -1;
|
|
1604
|
+
let close = -1;
|
|
1605
|
+
if (lines[0]?.trim() === "---") {
|
|
1606
|
+
open = 0;
|
|
1607
|
+
for (let i = 1; i < lines.length; i++) {
|
|
1608
|
+
if (lines[i]?.trim() === "---") {
|
|
1609
|
+
close = i;
|
|
1610
|
+
break;
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
if (open === -1 || close === -1) {
|
|
1615
|
+
const fm = buildFrontmatter(writes);
|
|
1616
|
+
return `${fm}${source}`;
|
|
1617
|
+
}
|
|
1618
|
+
const yaml = lines.slice(open + 1, close);
|
|
1619
|
+
for (const w of writes) setIndex(yaml, w);
|
|
1620
|
+
const rebuilt = [...lines.slice(0, open + 1), ...yaml, ...lines.slice(close)];
|
|
1621
|
+
let result = rebuilt.join("\n");
|
|
1622
|
+
if (trailingNewline && !result.endsWith("\n")) result += "\n";
|
|
1623
|
+
return result;
|
|
1624
|
+
}
|
|
1625
|
+
function buildFrontmatter(writes) {
|
|
1626
|
+
const lines = ["---"];
|
|
1627
|
+
for (const section of ["artifact", "process"]) {
|
|
1628
|
+
const ws = writes.filter((w) => w.kind === section);
|
|
1629
|
+
if (ws.length === 0) continue;
|
|
1630
|
+
lines.push(`${section}:`);
|
|
1631
|
+
for (const w of ws) {
|
|
1632
|
+
lines.push(` ${w.id}:`);
|
|
1633
|
+
lines.push(` index: ${w.value}`);
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
lines.push("---", "");
|
|
1637
|
+
return lines.join("\n");
|
|
1638
|
+
}
|
|
1639
|
+
var indentOf = (line) => line.length - line.trimStart().length;
|
|
1640
|
+
function setIndex(yaml, w) {
|
|
1641
|
+
const section = w.kind;
|
|
1642
|
+
let sectionStart = -1;
|
|
1643
|
+
for (let i = 0; i < yaml.length; i++) {
|
|
1644
|
+
const line = yaml[i];
|
|
1645
|
+
if (/^[^\s#]/.test(line) && line.replace(/:\s*$/, "") === section) {
|
|
1646
|
+
sectionStart = i;
|
|
1647
|
+
break;
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
if (sectionStart === -1) {
|
|
1651
|
+
yaml.push(`${section}:`, ` ${w.id}:`, ` index: ${w.value}`);
|
|
1652
|
+
return;
|
|
1653
|
+
}
|
|
1654
|
+
let sectionEnd = yaml.length;
|
|
1655
|
+
for (let i = sectionStart + 1; i < yaml.length; i++) {
|
|
1656
|
+
const line = yaml[i];
|
|
1657
|
+
if (line.trim() !== "" && /^[^\s#]/.test(line)) {
|
|
1658
|
+
sectionEnd = i;
|
|
1659
|
+
break;
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
let sectionIndent = 2;
|
|
1663
|
+
for (let i = sectionStart + 1; i < sectionEnd; i++) {
|
|
1664
|
+
const line = yaml[i];
|
|
1665
|
+
if (line.trim() !== "" && !line.trimStart().startsWith("#")) {
|
|
1666
|
+
sectionIndent = indentOf(line);
|
|
1667
|
+
break;
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
const keyRe = new RegExp(`^(\\s+)${escapeRe(w.id)}:(.*)$`);
|
|
1671
|
+
let nodeLine = -1;
|
|
1672
|
+
let nodeIndent = sectionIndent;
|
|
1673
|
+
let nodeRest = "";
|
|
1674
|
+
for (let i = sectionStart + 1; i < sectionEnd; i++) {
|
|
1675
|
+
const m = keyRe.exec(yaml[i]);
|
|
1676
|
+
if (m && m[1].length === sectionIndent) {
|
|
1677
|
+
nodeLine = i;
|
|
1678
|
+
nodeIndent = m[1].length;
|
|
1679
|
+
nodeRest = m[2];
|
|
1680
|
+
break;
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
const pad = (n) => " ".repeat(n);
|
|
1684
|
+
if (nodeLine === -1) {
|
|
1685
|
+
const block = [
|
|
1686
|
+
`${pad(sectionIndent)}${w.id}:`,
|
|
1687
|
+
`${pad(sectionIndent * 2)}index: ${w.value}`
|
|
1688
|
+
];
|
|
1689
|
+
yaml.splice(sectionEnd, 0, ...block);
|
|
1690
|
+
return;
|
|
1691
|
+
}
|
|
1692
|
+
if (nodeRest.trimStart().startsWith("{")) {
|
|
1693
|
+
const span = innerBraceSpan(nodeRest);
|
|
1694
|
+
if (span) {
|
|
1695
|
+
const prefix = yaml[nodeLine].slice(
|
|
1696
|
+
0,
|
|
1697
|
+
yaml[nodeLine].length - nodeRest.length
|
|
1698
|
+
);
|
|
1699
|
+
const before = nodeRest.slice(0, span.open);
|
|
1700
|
+
const after = nodeRest.slice(span.close + 1);
|
|
1701
|
+
const inner = nodeRest.slice(span.open + 1, span.close).trim();
|
|
1702
|
+
let merged;
|
|
1703
|
+
if (/\bindex:\s*\d/.test(inner)) {
|
|
1704
|
+
merged = inner.replace(/(\bindex:\s*)\d+/, `$1${w.value}`);
|
|
1705
|
+
} else {
|
|
1706
|
+
merged = inner === "" ? `index: ${w.value}` : `index: ${w.value}, ${inner}`;
|
|
1707
|
+
}
|
|
1708
|
+
yaml[nodeLine] = `${prefix}${before}{ ${merged} }${after}`;
|
|
1709
|
+
return;
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
const childIndent = nodeIndent + sectionIndent;
|
|
1713
|
+
for (let i = nodeLine + 1; i < sectionEnd; i++) {
|
|
1714
|
+
const line = yaml[i];
|
|
1715
|
+
if (line.trim() === "") continue;
|
|
1716
|
+
if (indentOf(line) <= nodeIndent) break;
|
|
1717
|
+
if (/^\s*index:\s*/.test(line)) {
|
|
1718
|
+
yaml[i] = line.replace(/^(\s*index:\s*)\d+/, `$1${w.value}`);
|
|
1719
|
+
return;
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
yaml.splice(nodeLine + 1, 0, `${pad(childIndent)}index: ${w.value}`);
|
|
1723
|
+
}
|
|
1724
|
+
function escapeRe(s) {
|
|
1725
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1726
|
+
}
|
|
1727
|
+
function innerBraceSpan(s) {
|
|
1728
|
+
const open = s.indexOf("{");
|
|
1729
|
+
if (open === -1) return null;
|
|
1730
|
+
let depth = 0;
|
|
1731
|
+
for (let i = open; i < s.length; i++) {
|
|
1732
|
+
if (s[i] === "{") depth++;
|
|
1733
|
+
else if (s[i] === "}" && --depth === 0) return { open, close: i };
|
|
1734
|
+
}
|
|
1735
|
+
return null;
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
// src/sort.ts
|
|
1739
|
+
var indentOf2 = (line) => line.length - line.trimStart().length;
|
|
1740
|
+
function extractNodeId(line) {
|
|
1741
|
+
const m = /^\s+(\S[^:]*?)\s*:/.exec(line);
|
|
1742
|
+
return m?.[1] ?? "";
|
|
1743
|
+
}
|
|
1744
|
+
function extractBlocksAndGaps(sectionLines, childIndent) {
|
|
1745
|
+
const blocks = [];
|
|
1746
|
+
const gaps = [];
|
|
1747
|
+
let currentGap = [];
|
|
1748
|
+
let pendingComments = [];
|
|
1749
|
+
let i = 0;
|
|
1750
|
+
while (i < sectionLines.length) {
|
|
1751
|
+
const line = sectionLines[i];
|
|
1752
|
+
const trimmed = line.trim();
|
|
1753
|
+
if (trimmed === "") {
|
|
1754
|
+
if (pendingComments.length > 0) {
|
|
1755
|
+
currentGap.push(...pendingComments);
|
|
1756
|
+
pendingComments = [];
|
|
1757
|
+
}
|
|
1758
|
+
currentGap.push(line);
|
|
1759
|
+
i++;
|
|
1760
|
+
continue;
|
|
1761
|
+
}
|
|
1762
|
+
if (trimmed.startsWith("#")) {
|
|
1763
|
+
pendingComments.push(line);
|
|
1764
|
+
i++;
|
|
1765
|
+
continue;
|
|
1766
|
+
}
|
|
1767
|
+
if (indentOf2(line) === childIndent) {
|
|
1768
|
+
gaps.push([...currentGap]);
|
|
1769
|
+
currentGap = [];
|
|
1770
|
+
const blockLines = [...pendingComments, line];
|
|
1771
|
+
pendingComments = [];
|
|
1772
|
+
i++;
|
|
1773
|
+
while (i < sectionLines.length) {
|
|
1774
|
+
const childLine = sectionLines[i];
|
|
1775
|
+
if (childLine.trim() === "") break;
|
|
1776
|
+
if (indentOf2(childLine) <= childIndent) break;
|
|
1777
|
+
blockLines.push(childLine);
|
|
1778
|
+
i++;
|
|
1779
|
+
}
|
|
1780
|
+
blocks.push({ id: extractNodeId(line), lines: blockLines });
|
|
1781
|
+
continue;
|
|
1782
|
+
}
|
|
1783
|
+
if (pendingComments.length > 0) {
|
|
1784
|
+
currentGap.push(...pendingComments);
|
|
1785
|
+
pendingComments = [];
|
|
1786
|
+
}
|
|
1787
|
+
currentGap.push(line);
|
|
1788
|
+
i++;
|
|
1789
|
+
}
|
|
1790
|
+
const trailingGap = [...currentGap, ...pendingComments];
|
|
1791
|
+
gaps.push(trailingGap);
|
|
1792
|
+
return { blocks, gaps };
|
|
1793
|
+
}
|
|
1794
|
+
function sort(source, opts) {
|
|
1795
|
+
const { edges, graph, nodeKinds, frontmatter, diagnostics } = analyze(source);
|
|
1796
|
+
if (diagnostics.some((d) => d.severity === "error")) {
|
|
1797
|
+
return { output: source, changed: false, diagnostics };
|
|
1798
|
+
}
|
|
1799
|
+
const topoOrder = /* @__PURE__ */ new Map();
|
|
1800
|
+
if (opts.by.includes("topological")) {
|
|
1801
|
+
const order = [];
|
|
1802
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1803
|
+
const push = (id) => {
|
|
1804
|
+
if (!seen.has(id)) {
|
|
1805
|
+
seen.add(id);
|
|
1806
|
+
order.push(id);
|
|
1807
|
+
}
|
|
1808
|
+
};
|
|
1809
|
+
for (const e of sortEdges(edges, graph)) {
|
|
1810
|
+
if (e.kind === "input") {
|
|
1811
|
+
push(e.artifact);
|
|
1812
|
+
push(e.process);
|
|
1813
|
+
} else if (e.kind === "output") {
|
|
1814
|
+
push(e.process);
|
|
1815
|
+
push(e.artifact);
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
const remaining = /* @__PURE__ */ new Set([
|
|
1819
|
+
...graph.nodes.keys(),
|
|
1820
|
+
...Object.keys(frontmatter?.artifact ?? {}),
|
|
1821
|
+
...Object.keys(frontmatter?.process ?? {})
|
|
1822
|
+
]);
|
|
1823
|
+
for (const id of [...remaining].sort()) push(id);
|
|
1824
|
+
for (const [rank, id] of order.entries()) topoOrder.set(id, rank);
|
|
1825
|
+
}
|
|
1826
|
+
const kindOf = (id) => nodeKinds.get(id) ?? "artifact";
|
|
1827
|
+
const getGroup = (id) => {
|
|
1828
|
+
const kind = kindOf(id);
|
|
1829
|
+
const meta = kind === "artifact" ? frontmatter?.artifact?.[id] : frontmatter?.process?.[id];
|
|
1830
|
+
return typeof meta?.group === "string" ? meta.group : null;
|
|
1831
|
+
};
|
|
1832
|
+
const getSortValue = (id, key) => {
|
|
1833
|
+
const kind = kindOf(id);
|
|
1834
|
+
const meta = kind === "artifact" ? frontmatter?.artifact?.[id] : frontmatter?.process?.[id];
|
|
1835
|
+
switch (key) {
|
|
1836
|
+
case "index":
|
|
1837
|
+
return typeof meta?.index === "number" ? meta.index : Number.MAX_SAFE_INTEGER;
|
|
1838
|
+
case "topological":
|
|
1839
|
+
return topoOrder.get(id) ?? Number.MAX_SAFE_INTEGER;
|
|
1840
|
+
case "id":
|
|
1841
|
+
return id;
|
|
1842
|
+
}
|
|
1843
|
+
};
|
|
1844
|
+
const lines = source.split("\n");
|
|
1845
|
+
const trailingNewline = source.endsWith("\n");
|
|
1846
|
+
let fmOpen = -1;
|
|
1847
|
+
let fmClose = -1;
|
|
1848
|
+
if (lines[0]?.trim() === "---") {
|
|
1849
|
+
fmOpen = 0;
|
|
1850
|
+
for (let i = 1; i < lines.length; i++) {
|
|
1851
|
+
if (lines[i]?.trim() === "---") {
|
|
1852
|
+
fmClose = i;
|
|
1853
|
+
break;
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
if (fmOpen === -1 || fmClose === -1) {
|
|
1858
|
+
return { output: source, changed: false, diagnostics };
|
|
1859
|
+
}
|
|
1860
|
+
const yaml = lines.slice(fmOpen + 1, fmClose);
|
|
1861
|
+
const sectionMods = [];
|
|
1862
|
+
for (const section of ["artifact", "process"]) {
|
|
1863
|
+
let sectionStart = -1;
|
|
1864
|
+
for (let i = 0; i < yaml.length; i++) {
|
|
1865
|
+
const line = yaml[i];
|
|
1866
|
+
if (/^[^\s#]/.test(line) && line.replace(/\s*:.*$/, "") === section) {
|
|
1867
|
+
sectionStart = i;
|
|
1868
|
+
break;
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
if (sectionStart === -1) continue;
|
|
1872
|
+
let sectionEnd = yaml.length;
|
|
1873
|
+
for (let i = sectionStart + 1; i < yaml.length; i++) {
|
|
1874
|
+
const line = yaml[i];
|
|
1875
|
+
if (line.trim() !== "" && /^[^\s#]/.test(line)) {
|
|
1876
|
+
sectionEnd = i;
|
|
1877
|
+
break;
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
const sectionContent = yaml.slice(sectionStart + 1, sectionEnd);
|
|
1881
|
+
let childIndent = 2;
|
|
1882
|
+
for (const line of sectionContent) {
|
|
1883
|
+
if (line.trim() !== "" && !line.trimStart().startsWith("#")) {
|
|
1884
|
+
childIndent = indentOf2(line);
|
|
1885
|
+
break;
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
const { blocks, gaps } = extractBlocksAndGaps(sectionContent, childIndent);
|
|
1889
|
+
if (blocks.length === 0) continue;
|
|
1890
|
+
const indexed = blocks.map((block, idx) => ({ block, idx }));
|
|
1891
|
+
indexed.sort((a, b) => {
|
|
1892
|
+
for (const key of opts.by) {
|
|
1893
|
+
let cmp;
|
|
1894
|
+
if (key === "group") {
|
|
1895
|
+
const ga = getGroup(a.block.id);
|
|
1896
|
+
const gb = getGroup(b.block.id);
|
|
1897
|
+
if (ga === null && gb === null) cmp = 0;
|
|
1898
|
+
else if (ga === null) cmp = 1;
|
|
1899
|
+
else if (gb === null) cmp = -1;
|
|
1900
|
+
else cmp = ga.localeCompare(gb);
|
|
1901
|
+
} else {
|
|
1902
|
+
const va = getSortValue(a.block.id, key);
|
|
1903
|
+
const vb = getSortValue(b.block.id, key);
|
|
1904
|
+
if (typeof va === "number" && typeof vb === "number") {
|
|
1905
|
+
cmp = va - vb;
|
|
1906
|
+
} else {
|
|
1907
|
+
cmp = String(va).localeCompare(String(vb));
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
if (cmp !== 0) return cmp;
|
|
1911
|
+
}
|
|
1912
|
+
return a.idx - b.idx;
|
|
1913
|
+
});
|
|
1914
|
+
const orderChanged = indexed.some((x, i) => x.idx !== i);
|
|
1915
|
+
if (!orderChanged) continue;
|
|
1916
|
+
const sortedBlocks = indexed.map((x) => x.block);
|
|
1917
|
+
const newLines = [];
|
|
1918
|
+
for (let j = 0; j < sortedBlocks.length; j++) {
|
|
1919
|
+
newLines.push(...gaps[j]);
|
|
1920
|
+
newLines.push(...sortedBlocks[j].lines);
|
|
1921
|
+
}
|
|
1922
|
+
newLines.push(...gaps[sortedBlocks.length]);
|
|
1923
|
+
sectionMods.push({
|
|
1924
|
+
yamlStart: sectionStart + 1,
|
|
1925
|
+
yamlEnd: sectionEnd,
|
|
1926
|
+
newLines
|
|
1927
|
+
});
|
|
1928
|
+
}
|
|
1929
|
+
if (sectionMods.length === 0) {
|
|
1930
|
+
return { output: source, changed: false, diagnostics };
|
|
1931
|
+
}
|
|
1932
|
+
const mutableYaml = [...yaml];
|
|
1933
|
+
for (const mod of [...sectionMods].reverse()) {
|
|
1934
|
+
mutableYaml.splice(
|
|
1935
|
+
mod.yamlStart,
|
|
1936
|
+
mod.yamlEnd - mod.yamlStart,
|
|
1937
|
+
...mod.newLines
|
|
1938
|
+
);
|
|
1939
|
+
}
|
|
1940
|
+
const rebuilt = [
|
|
1941
|
+
...lines.slice(0, fmOpen + 1),
|
|
1942
|
+
...mutableYaml,
|
|
1943
|
+
...lines.slice(fmClose)
|
|
1944
|
+
];
|
|
1945
|
+
let result = rebuilt.join("\n");
|
|
1946
|
+
if (trailingNewline && !result.endsWith("\n")) result += "\n";
|
|
1947
|
+
return { output: result, changed: true, diagnostics };
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
// src/multifile.ts
|
|
1951
|
+
import { dirname, resolve } from "path";
|
|
1952
|
+
function resolveRefPath(fromFile, ref) {
|
|
1953
|
+
if (ref.includes("://")) return { ok: false, reason: "url" };
|
|
1954
|
+
if (ref.startsWith("/")) return { ok: false, reason: "absolute" };
|
|
1955
|
+
return { ok: true, path: resolve(dirname(fromFile), ref) };
|
|
1956
|
+
}
|
|
1957
|
+
function collectExtendsRefs(fm) {
|
|
1958
|
+
const ext = fm.extends;
|
|
1959
|
+
if (ext === void 0) return [];
|
|
1960
|
+
return Array.isArray(ext) ? [...ext] : [ext];
|
|
1961
|
+
}
|
|
1962
|
+
function collectSubflowRefs(fm) {
|
|
1963
|
+
const refs = [];
|
|
1964
|
+
for (const [process, meta] of Object.entries(fm.process ?? {})) {
|
|
1965
|
+
if (typeof meta.subflow === "string") {
|
|
1966
|
+
refs.push({ process, ref: meta.subflow });
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
return refs;
|
|
1970
|
+
}
|
|
1971
|
+
function loadSubflowGraph(entryPath, load) {
|
|
1972
|
+
const docs = /* @__PURE__ */ new Map();
|
|
1973
|
+
const diagnostics = [];
|
|
1974
|
+
const stack = /* @__PURE__ */ new Set();
|
|
1975
|
+
function visit(path) {
|
|
1976
|
+
if (stack.has(path)) {
|
|
1977
|
+
diagnostics.push({
|
|
1978
|
+
severity: "error",
|
|
1979
|
+
code: "V022",
|
|
1980
|
+
message: `circular subflow reference: ${path}`,
|
|
1981
|
+
range: zeroRange()
|
|
1982
|
+
});
|
|
1983
|
+
return;
|
|
1984
|
+
}
|
|
1985
|
+
if (docs.has(path)) return;
|
|
1986
|
+
const doc = load(path);
|
|
1987
|
+
if (doc === null) {
|
|
1988
|
+
diagnostics.push({
|
|
1989
|
+
severity: "error",
|
|
1990
|
+
code: "V021",
|
|
1991
|
+
message: `subflow file not found: ${path}`,
|
|
1992
|
+
range: zeroRange()
|
|
1993
|
+
});
|
|
1994
|
+
return;
|
|
1995
|
+
}
|
|
1996
|
+
docs.set(path, doc);
|
|
1997
|
+
stack.add(path);
|
|
1998
|
+
for (const { ref } of collectSubflowRefs(doc.frontmatter ?? {})) {
|
|
1999
|
+
const resolved = resolveRefPath(path, ref);
|
|
2000
|
+
if (!resolved.ok) {
|
|
2001
|
+
diagnostics.push({
|
|
2002
|
+
severity: "error",
|
|
2003
|
+
code: "V021",
|
|
2004
|
+
message: `invalid subflow path (${resolved.reason}): ${ref}`,
|
|
2005
|
+
range: zeroRange()
|
|
2006
|
+
});
|
|
2007
|
+
continue;
|
|
2008
|
+
}
|
|
2009
|
+
visit(resolved.path);
|
|
2010
|
+
}
|
|
2011
|
+
stack.delete(path);
|
|
2012
|
+
}
|
|
2013
|
+
visit(entryPath);
|
|
2014
|
+
return { docs, diagnostics };
|
|
2015
|
+
}
|
|
2016
|
+
function computeOpenInputs(edges) {
|
|
2017
|
+
const all = /* @__PURE__ */ new Set();
|
|
2018
|
+
const produced = /* @__PURE__ */ new Set();
|
|
2019
|
+
for (const e of edges) {
|
|
2020
|
+
all.add(e.artifact);
|
|
2021
|
+
if (e.kind === "output") produced.add(e.artifact);
|
|
2022
|
+
}
|
|
2023
|
+
return new Set([...all].filter((a) => !produced.has(a)));
|
|
2024
|
+
}
|
|
2025
|
+
function computeTerminals(edges) {
|
|
2026
|
+
const all = /* @__PURE__ */ new Set();
|
|
2027
|
+
const consumed = /* @__PURE__ */ new Set();
|
|
2028
|
+
for (const e of edges) {
|
|
2029
|
+
all.add(e.artifact);
|
|
2030
|
+
if (e.kind === "input" || e.kind === "feedback") consumed.add(e.artifact);
|
|
2031
|
+
}
|
|
2032
|
+
return new Set([...all].filter((a) => !consumed.has(a)));
|
|
2033
|
+
}
|
|
2034
|
+
function validateSubflowBoundary(ctx) {
|
|
2035
|
+
const {
|
|
2036
|
+
processId,
|
|
2037
|
+
parentNormalInputs,
|
|
2038
|
+
parentOutputs,
|
|
2039
|
+
boundaryMap,
|
|
2040
|
+
childOpenInputs,
|
|
2041
|
+
childTerminals
|
|
2042
|
+
} = ctx;
|
|
2043
|
+
const diagnostics = [];
|
|
2044
|
+
const parentBoundary = /* @__PURE__ */ new Set([...parentNormalInputs, ...parentOutputs]);
|
|
2045
|
+
const childBoundary = /* @__PURE__ */ new Set([...childOpenInputs, ...childTerminals]);
|
|
2046
|
+
let hasC1orC2Error = false;
|
|
2047
|
+
for (const key of Object.keys(boundaryMap)) {
|
|
2048
|
+
if (!parentBoundary.has(key)) {
|
|
2049
|
+
hasC1orC2Error = true;
|
|
2050
|
+
diagnostics.push({
|
|
2051
|
+
severity: "error",
|
|
2052
|
+
code: "V025",
|
|
2053
|
+
message: `boundary key '${key}' on process '${processId}' is not a parent boundary artifact`,
|
|
2054
|
+
range: zeroRange()
|
|
2055
|
+
});
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
for (const val of Object.values(boundaryMap)) {
|
|
2059
|
+
if (!childBoundary.has(val)) {
|
|
2060
|
+
hasC1orC2Error = true;
|
|
2061
|
+
diagnostics.push({
|
|
2062
|
+
severity: "error",
|
|
2063
|
+
code: "V025",
|
|
2064
|
+
message: `boundary value '${val}' on process '${processId}' is not a child boundary artifact`,
|
|
2065
|
+
range: zeroRange()
|
|
2066
|
+
});
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
if (hasC1orC2Error) return diagnostics;
|
|
2070
|
+
const effective = /* @__PURE__ */ new Map();
|
|
2071
|
+
for (const p of parentBoundary) {
|
|
2072
|
+
effective.set(p, boundaryMap[p] ?? p);
|
|
2073
|
+
}
|
|
2074
|
+
const childToParent = /* @__PURE__ */ new Map();
|
|
2075
|
+
for (const [p, c] of effective) {
|
|
2076
|
+
if (childToParent.has(c)) {
|
|
2077
|
+
diagnostics.push({
|
|
2078
|
+
severity: "error",
|
|
2079
|
+
code: "V025",
|
|
2080
|
+
message: `boundary map for process '${processId}' is not injective: multiple parent IDs map to child '${c}'`,
|
|
2081
|
+
range: zeroRange()
|
|
2082
|
+
});
|
|
2083
|
+
} else {
|
|
2084
|
+
childToParent.set(c, p);
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
for (const p of parentNormalInputs) {
|
|
2088
|
+
const c = effective.get(p);
|
|
2089
|
+
if (childTerminals.has(c) && !childOpenInputs.has(c)) {
|
|
2090
|
+
diagnostics.push({
|
|
2091
|
+
severity: "error",
|
|
2092
|
+
code: "V025",
|
|
2093
|
+
message: `boundary maps input '${p}' to terminal '${c}' (side mismatch) on process '${processId}'`,
|
|
2094
|
+
range: zeroRange()
|
|
2095
|
+
});
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
for (const p of parentOutputs) {
|
|
2099
|
+
const c = effective.get(p);
|
|
2100
|
+
if (childOpenInputs.has(c) && !childTerminals.has(c)) {
|
|
2101
|
+
diagnostics.push({
|
|
2102
|
+
severity: "error",
|
|
2103
|
+
code: "V025",
|
|
2104
|
+
message: `boundary maps output '${p}' to open input '${c}' (side mismatch) on process '${processId}'`,
|
|
2105
|
+
range: zeroRange()
|
|
2106
|
+
});
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
if (diagnostics.length > 0) return diagnostics;
|
|
2110
|
+
const mappedInputs = new Set(
|
|
2111
|
+
[...parentNormalInputs].map((p) => effective.get(p))
|
|
2112
|
+
);
|
|
2113
|
+
if (!setsEqual(mappedInputs, childOpenInputs)) {
|
|
2114
|
+
diagnostics.push({
|
|
2115
|
+
severity: "error",
|
|
2116
|
+
code: "V025",
|
|
2117
|
+
message: `subflow boundary mismatch on process '${processId}': parent inputs ${JSON.stringify([...mappedInputs].sort())} \u2260 child open inputs ${JSON.stringify([...childOpenInputs].sort())}`,
|
|
2118
|
+
range: zeroRange()
|
|
2119
|
+
});
|
|
2120
|
+
}
|
|
2121
|
+
const mappedOutputs = new Set(
|
|
2122
|
+
[...parentOutputs].map((p) => effective.get(p))
|
|
2123
|
+
);
|
|
2124
|
+
if (!setsEqual(mappedOutputs, childTerminals)) {
|
|
2125
|
+
diagnostics.push({
|
|
2126
|
+
severity: "error",
|
|
2127
|
+
code: "V025",
|
|
2128
|
+
message: `subflow boundary mismatch on process '${processId}': parent outputs ${JSON.stringify([...mappedOutputs].sort())} \u2260 child terminals ${JSON.stringify([...childTerminals].sort())}`,
|
|
2129
|
+
range: zeroRange()
|
|
2130
|
+
});
|
|
2131
|
+
}
|
|
2132
|
+
return diagnostics;
|
|
2133
|
+
}
|
|
2134
|
+
function setsEqual(a, b) {
|
|
2135
|
+
if (a.size !== b.size) return false;
|
|
2136
|
+
for (const x of a) if (!b.has(x)) return false;
|
|
2137
|
+
return true;
|
|
2138
|
+
}
|
|
2139
|
+
function loadExtendsChain(entryPath, load) {
|
|
2140
|
+
const docs = /* @__PURE__ */ new Map();
|
|
2141
|
+
const diagnostics = [];
|
|
2142
|
+
const stack = /* @__PURE__ */ new Set();
|
|
2143
|
+
function visit(path) {
|
|
2144
|
+
if (stack.has(path)) {
|
|
2145
|
+
diagnostics.push({
|
|
2146
|
+
severity: "error",
|
|
2147
|
+
code: "V027",
|
|
2148
|
+
message: `circular extends reference: ${path}`,
|
|
2149
|
+
range: zeroRange()
|
|
2150
|
+
});
|
|
2151
|
+
return;
|
|
2152
|
+
}
|
|
2153
|
+
if (docs.has(path)) return;
|
|
2154
|
+
const doc = load(path);
|
|
2155
|
+
if (doc === null) {
|
|
2156
|
+
diagnostics.push({
|
|
2157
|
+
severity: "error",
|
|
2158
|
+
code: "V026",
|
|
2159
|
+
message: `extends file not found: ${path}`,
|
|
2160
|
+
range: zeroRange()
|
|
2161
|
+
});
|
|
2162
|
+
return;
|
|
2163
|
+
}
|
|
2164
|
+
docs.set(path, doc);
|
|
2165
|
+
stack.add(path);
|
|
2166
|
+
for (const ref of collectExtendsRefs(doc.frontmatter ?? {})) {
|
|
2167
|
+
const resolved = resolveRefPath(path, ref);
|
|
2168
|
+
if (!resolved.ok) {
|
|
2169
|
+
diagnostics.push({
|
|
2170
|
+
severity: "error",
|
|
2171
|
+
code: "V026",
|
|
2172
|
+
message: `invalid extends path (${resolved.reason}): ${ref}`,
|
|
2173
|
+
range: zeroRange()
|
|
2174
|
+
});
|
|
2175
|
+
continue;
|
|
2176
|
+
}
|
|
2177
|
+
visit(resolved.path);
|
|
2178
|
+
}
|
|
2179
|
+
stack.delete(path);
|
|
2180
|
+
}
|
|
2181
|
+
visit(entryPath);
|
|
2182
|
+
return { docs, diagnostics };
|
|
2183
|
+
}
|
|
2184
|
+
var PRESET_ALLOWED_KEYS = /* @__PURE__ */ new Set([
|
|
2185
|
+
"extends",
|
|
2186
|
+
"statusStyles",
|
|
2187
|
+
"tag",
|
|
2188
|
+
"group"
|
|
2189
|
+
]);
|
|
2190
|
+
function validatePresetKeys(path, fm) {
|
|
2191
|
+
if (fm === null) return [];
|
|
2192
|
+
const diagnostics = [];
|
|
2193
|
+
for (const key of Object.keys(fm)) {
|
|
2194
|
+
if (!PRESET_ALLOWED_KEYS.has(key)) {
|
|
2195
|
+
diagnostics.push({
|
|
2196
|
+
severity: "error",
|
|
2197
|
+
code: "V028",
|
|
2198
|
+
message: `preset '${path}' contains non-presentation key '${key}'`,
|
|
2199
|
+
range: zeroRange()
|
|
2200
|
+
});
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
return diagnostics;
|
|
2204
|
+
}
|
|
2205
|
+
function resolvePresentation(chain) {
|
|
2206
|
+
let statusStyles;
|
|
2207
|
+
let tag;
|
|
2208
|
+
let group;
|
|
2209
|
+
for (const { fm } of chain) {
|
|
2210
|
+
if (fm === null) continue;
|
|
2211
|
+
if (fm.statusStyles !== void 0) {
|
|
2212
|
+
if (statusStyles === void 0) {
|
|
2213
|
+
statusStyles = {};
|
|
2214
|
+
}
|
|
2215
|
+
for (const [status, nodeStyle] of Object.entries(fm.statusStyles)) {
|
|
2216
|
+
if (nodeStyle === void 0) continue;
|
|
2217
|
+
const existing = statusStyles[status] ?? {};
|
|
2218
|
+
statusStyles[status] = {
|
|
2219
|
+
...existing,
|
|
2220
|
+
...nodeStyle
|
|
2221
|
+
};
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
if (fm.tag !== void 0) {
|
|
2225
|
+
if (tag === void 0) {
|
|
2226
|
+
tag = {};
|
|
2227
|
+
}
|
|
2228
|
+
for (const [id, tagMeta] of Object.entries(fm.tag)) {
|
|
2229
|
+
if (tagMeta === void 0) continue;
|
|
2230
|
+
const existing = tag[id] ?? {};
|
|
2231
|
+
const { style: newStyle, ...otherFields } = tagMeta;
|
|
2232
|
+
const { style: existingStyle, ...existingOther } = existing;
|
|
2233
|
+
const mergedStyle = newStyle !== void 0 || existingStyle !== void 0 ? { ...existingStyle, ...newStyle } : void 0;
|
|
2234
|
+
tag[id] = {
|
|
2235
|
+
...existingOther,
|
|
2236
|
+
...otherFields,
|
|
2237
|
+
...mergedStyle !== void 0 ? { style: mergedStyle } : {}
|
|
2238
|
+
};
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
if (fm.group !== void 0) {
|
|
2242
|
+
if (group === void 0) {
|
|
2243
|
+
group = {};
|
|
2244
|
+
}
|
|
2245
|
+
for (const [id, groupMeta] of Object.entries(fm.group)) {
|
|
2246
|
+
if (groupMeta === void 0) continue;
|
|
2247
|
+
group[id] = { ...group[id] ?? {}, ...groupMeta };
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2251
|
+
return { statusStyles, tag, group };
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
// src/index.ts
|
|
2255
|
+
function parse(source) {
|
|
2256
|
+
const {
|
|
2257
|
+
frontmatter,
|
|
2258
|
+
body,
|
|
2259
|
+
diagnostics: fmDiags,
|
|
2260
|
+
bodyStartLine
|
|
2261
|
+
} = loadFrontmatter(source);
|
|
2262
|
+
const { tokens: rawTokens, diagnostics: lexDiags } = lex(body);
|
|
2263
|
+
const lineOffset = bodyStartLine - 1;
|
|
2264
|
+
const tokens = lineOffset > 0 ? rawTokens.map((t) => ({
|
|
2265
|
+
...t,
|
|
2266
|
+
start: { ...t.start, line: t.start.line + lineOffset },
|
|
2267
|
+
end: { ...t.end, line: t.end.line + lineOffset }
|
|
2268
|
+
})) : rawTokens;
|
|
2269
|
+
const { document, diagnostics: parseDiags } = parseTokens(tokens);
|
|
2270
|
+
return {
|
|
2271
|
+
document,
|
|
2272
|
+
frontmatter,
|
|
2273
|
+
bodyStartLine,
|
|
2274
|
+
diagnostics: [...fmDiags, ...lexDiags, ...parseDiags]
|
|
2275
|
+
};
|
|
2276
|
+
}
|
|
2277
|
+
function hasErrors(diags) {
|
|
2278
|
+
return diags.some((d) => d.severity === "error");
|
|
2279
|
+
}
|
|
2280
|
+
function analyze(source, opts = {}) {
|
|
2281
|
+
const {
|
|
2282
|
+
document,
|
|
2283
|
+
frontmatter,
|
|
2284
|
+
bodyStartLine,
|
|
2285
|
+
diagnostics: parseDiags
|
|
2286
|
+
} = parse(source);
|
|
2287
|
+
const {
|
|
2288
|
+
edges,
|
|
2289
|
+
nodeKinds,
|
|
2290
|
+
isolatedNodes,
|
|
2291
|
+
diagnostics: normDiags
|
|
2292
|
+
} = normalize(document, frontmatter);
|
|
2293
|
+
const valOpts = { source };
|
|
2294
|
+
if (opts.strict) valOpts.strict = true;
|
|
2295
|
+
const valDiags = validate(edges, nodeKinds, frontmatter, valOpts);
|
|
2296
|
+
const graph = buildGraph(edges, nodeKinds);
|
|
2297
|
+
return {
|
|
2298
|
+
document,
|
|
2299
|
+
frontmatter,
|
|
2300
|
+
bodyStartLine,
|
|
2301
|
+
edges,
|
|
2302
|
+
nodeKinds,
|
|
2303
|
+
isolatedNodes,
|
|
2304
|
+
graph,
|
|
2305
|
+
diagnostics: [...parseDiags, ...normDiags, ...valDiags]
|
|
2306
|
+
};
|
|
2307
|
+
}
|
|
2308
|
+
function format(source, opts = {}) {
|
|
2309
|
+
const {
|
|
2310
|
+
frontmatter,
|
|
2311
|
+
body,
|
|
2312
|
+
diagnostics: fmDiags,
|
|
2313
|
+
bodyStartLine
|
|
2314
|
+
} = loadFrontmatter(source);
|
|
2315
|
+
const { tokens: rawTokens2, diagnostics: lexDiags } = lex(body);
|
|
2316
|
+
const lineOffset = bodyStartLine - 1;
|
|
2317
|
+
const tokens = lineOffset > 0 ? rawTokens2.map((t) => ({
|
|
2318
|
+
...t,
|
|
2319
|
+
start: { ...t.start, line: t.start.line + lineOffset },
|
|
2320
|
+
end: { ...t.end, line: t.end.line + lineOffset }
|
|
2321
|
+
})) : rawTokens2;
|
|
2322
|
+
const { document, diagnostics: parseDiags } = parseTokens(tokens);
|
|
2323
|
+
const {
|
|
2324
|
+
edges,
|
|
2325
|
+
nodeKinds,
|
|
2326
|
+
diagnostics: normDiags
|
|
2327
|
+
} = normalize(document, frontmatter);
|
|
2328
|
+
const valDiags = opts.skipValidation ? [] : validate(edges, nodeKinds, frontmatter, { source });
|
|
2329
|
+
const frontmatterSection = source.slice(0, source.length - body.length);
|
|
2330
|
+
const segments = splitBodyIntoSegments(body);
|
|
2331
|
+
const formattedBody = segments.map((seg) => {
|
|
2332
|
+
if (seg.kind === "comment") return seg.text;
|
|
2333
|
+
const { tokens: segToks } = lex(seg.text);
|
|
2334
|
+
const { document: segDoc } = parseTokens(segToks);
|
|
2335
|
+
const { edges: segEdges, isolatedNodes: segIsolated } = normalize(
|
|
2336
|
+
segDoc,
|
|
2337
|
+
frontmatter
|
|
2338
|
+
);
|
|
2339
|
+
const segGraph = buildGraph(segEdges, nodeKinds);
|
|
2340
|
+
const segSorted = sortEdges(segEdges, segGraph);
|
|
2341
|
+
const segIso = sortIsolated(segIsolated);
|
|
2342
|
+
return opts.style === "flows" ? formatAsFlows(segSorted, segIso) : formatEdges(segSorted, segIso);
|
|
2343
|
+
}).join("");
|
|
2344
|
+
return {
|
|
2345
|
+
output: frontmatterSection + formattedBody,
|
|
2346
|
+
diagnostics: [
|
|
2347
|
+
...fmDiags,
|
|
2348
|
+
...lexDiags,
|
|
2349
|
+
...parseDiags,
|
|
2350
|
+
...normDiags,
|
|
2351
|
+
...valDiags
|
|
2352
|
+
]
|
|
2353
|
+
};
|
|
2354
|
+
}
|
|
2355
|
+
export {
|
|
2356
|
+
ID_PATTERN,
|
|
2357
|
+
STATUS_VALUES,
|
|
2358
|
+
STYLE_ATTRS,
|
|
2359
|
+
analyze,
|
|
2360
|
+
auditGraph,
|
|
2361
|
+
buildGraph,
|
|
2362
|
+
collectExtendsRefs,
|
|
2363
|
+
collectSubflowRefs,
|
|
2364
|
+
computeOpenInputs,
|
|
2365
|
+
computeTerminals,
|
|
2366
|
+
diffGraphs,
|
|
2367
|
+
format,
|
|
2368
|
+
formatAsFlows,
|
|
2369
|
+
formatEdges,
|
|
2370
|
+
hasErrors,
|
|
2371
|
+
loadExtendsChain,
|
|
2372
|
+
loadFrontmatter,
|
|
2373
|
+
loadSubflowGraph,
|
|
2374
|
+
normalize as normalizeDocument,
|
|
2375
|
+
parse,
|
|
2376
|
+
reindex,
|
|
2377
|
+
resolveMeta,
|
|
2378
|
+
resolvePresentation,
|
|
2379
|
+
resolveRefPath,
|
|
2380
|
+
sort,
|
|
2381
|
+
sortEdges,
|
|
2382
|
+
sortIsolated,
|
|
2383
|
+
validate as validateGraph,
|
|
2384
|
+
validatePresetKeys,
|
|
2385
|
+
validateSubflowBoundary
|
|
2386
|
+
};
|