deepclause-pi 0.1.5 → 0.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.
Files changed (49) hide show
  1. package/README.md +36 -0
  2. package/dist/diagram/extract.d.ts +5 -0
  3. package/dist/diagram/extract.js +701 -0
  4. package/dist/diagram/grade.d.ts +41 -0
  5. package/dist/diagram/grade.js +70 -0
  6. package/dist/diagram/validate.d.ts +36 -0
  7. package/dist/diagram/validate.js +148 -0
  8. package/dist/diagram/viewer.d.ts +36 -0
  9. package/dist/diagram/viewer.js +99 -0
  10. package/dist/diagram/workspace.d.ts +24 -0
  11. package/dist/diagram/workspace.js +106 -0
  12. package/dist/index.d.ts +8 -1
  13. package/dist/index.js +462 -17
  14. package/dist/model.d.ts +16 -0
  15. package/dist/model.js +28 -0
  16. package/dist/planner.d.ts +27 -2
  17. package/dist/planner.js +109 -4
  18. package/dist/runtime.d.ts +15 -1
  19. package/dist/runtime.js +116 -3
  20. package/dist/workspace.d.ts +3 -0
  21. package/dist/workspace.js +20 -0
  22. package/docs/DIAGRAM_INTEGRATION_PROPOSAL.md +154 -0
  23. package/docs/SPECKIT.md +222 -0
  24. package/docs/SPEC_LAYER_PROPOSAL.md +1893 -0
  25. package/package.json +1 -1
  26. package/src/assets/AGENTS.md +82 -0
  27. package/src/assets/apply.dml +188 -0
  28. package/src/assets/spec_apply.dml +20 -0
  29. package/src/assets/spec_archive.dml +11 -0
  30. package/src/assets/spec_coverage.dml +26 -0
  31. package/src/assets/spec_graph.dml +12 -0
  32. package/src/assets/spec_merge.dml +10 -0
  33. package/src/assets/spec_query.dml +10 -0
  34. package/src/assets/spec_scaffold.dml +10 -0
  35. package/src/assets/spec_status.dml +7 -0
  36. package/src/assets/spec_validate.dml +9 -0
  37. package/src/assets/specs.dml +991 -0
  38. package/src/assets/vendor/mermaid.min.js +3636 -0
  39. package/src/assets/viewer.template.html +319 -0
  40. package/src/diagram/extract.ts +721 -0
  41. package/src/diagram/grade.ts +104 -0
  42. package/src/diagram/validate.ts +188 -0
  43. package/src/diagram/viewer.ts +144 -0
  44. package/src/diagram/workspace.ts +109 -0
  45. package/src/index.ts +507 -16
  46. package/src/model.ts +46 -0
  47. package/src/planner.ts +123 -3
  48. package/src/runtime.ts +117 -2
  49. package/src/workspace.ts +24 -0
@@ -0,0 +1,721 @@
1
+ // Port of deepclause-handbook-md/tools/dml_flow.mjs.
2
+ // Heuristic (not a Prolog parser) DML -> Mermaid extractor. Pure string work,
3
+ // no dependencies and no filesystem access.
4
+
5
+ export interface RenderOptions {
6
+ hideOutput?: boolean;
7
+ }
8
+
9
+ interface Clause {
10
+ args: string;
11
+ body: string;
12
+ }
13
+
14
+ type ClauseMap = Map<string, Clause[]>;
15
+
16
+ interface Goal {
17
+ goal: string;
18
+ sep: string;
19
+ }
20
+
21
+ interface Segment {
22
+ entry: string | null;
23
+ exit: string | null;
24
+ }
25
+
26
+ // ---------- lexing -----------------------------------------------------------
27
+
28
+ function stripComments(src: string): string {
29
+ let out = "";
30
+ let inStr = false;
31
+ let inChar = false;
32
+ let inBlock = false;
33
+ for (let i = 0; i < src.length; i++) {
34
+ const c = src.charAt(i);
35
+ const n = src.charAt(i + 1);
36
+ if (inBlock) {
37
+ if (c === "*" && n === "/") {
38
+ inBlock = false;
39
+ i++;
40
+ }
41
+ continue;
42
+ }
43
+ if (inStr) {
44
+ out += c;
45
+ if (c === "\\") {
46
+ out += n;
47
+ i++;
48
+ } else if (c === '"') inStr = false;
49
+ continue;
50
+ }
51
+ if (inChar) {
52
+ out += c;
53
+ if (c === "\\") {
54
+ out += n;
55
+ i++;
56
+ } else if (c === "'") inChar = false;
57
+ continue;
58
+ }
59
+ if (c === "/" && n === "*") {
60
+ inBlock = true;
61
+ i++;
62
+ continue;
63
+ }
64
+ if (c === "%") {
65
+ while (i < src.length && src.charAt(i) !== "\n") i++;
66
+ out += "\n";
67
+ continue;
68
+ }
69
+ if (c === '"') {
70
+ inStr = true;
71
+ out += c;
72
+ continue;
73
+ }
74
+ if (c === "'") {
75
+ inChar = true;
76
+ out += c;
77
+ continue;
78
+ }
79
+ out += c;
80
+ }
81
+ return out;
82
+ }
83
+
84
+ function splitTop(s: string, seps: string): string[] {
85
+ const parts: string[] = [];
86
+ let depth = 0;
87
+ let cur = "";
88
+ let inStr = false;
89
+ let inChar = false;
90
+ for (let i = 0; i < s.length; i++) {
91
+ const c = s.charAt(i);
92
+ if (inStr) {
93
+ cur += c;
94
+ if (c === "\\") cur += s.charAt(++i);
95
+ else if (c === '"') inStr = false;
96
+ continue;
97
+ }
98
+ if (inChar) {
99
+ cur += c;
100
+ if (c === "\\") cur += s.charAt(++i);
101
+ else if (c === "'") inChar = false;
102
+ continue;
103
+ }
104
+ if (c === '"') {
105
+ inStr = true;
106
+ cur += c;
107
+ continue;
108
+ }
109
+ if (c === "'") {
110
+ inChar = true;
111
+ cur += c;
112
+ continue;
113
+ }
114
+ if ("([{".includes(c)) depth++;
115
+ else if (")]}".includes(c)) depth--;
116
+ if (depth === 0 && seps.includes(c)) {
117
+ parts.push(cur.trim());
118
+ cur = "";
119
+ continue;
120
+ }
121
+ cur += c;
122
+ }
123
+ parts.push(cur.trim());
124
+ return parts;
125
+ }
126
+
127
+ function splitGoals(s: string): Goal[] {
128
+ const out: Goal[] = [];
129
+ let depth = 0;
130
+ let cur = "";
131
+ let inStr = false;
132
+ let inChar = false;
133
+ const push = (sep: string) => {
134
+ const goal = cur.trim();
135
+ if (goal) out.push({ goal, sep });
136
+ cur = "";
137
+ };
138
+ for (let i = 0; i < s.length; i++) {
139
+ const c = s.charAt(i);
140
+ if (inStr) {
141
+ cur += c;
142
+ if (c === "\\") cur += s.charAt(++i);
143
+ else if (c === '"') inStr = false;
144
+ continue;
145
+ }
146
+ if (inChar) {
147
+ cur += c;
148
+ if (c === "\\") cur += s.charAt(++i);
149
+ else if (c === "'") inChar = false;
150
+ continue;
151
+ }
152
+ if (c === '"') {
153
+ inStr = true;
154
+ cur += c;
155
+ continue;
156
+ }
157
+ if (c === "'") {
158
+ inChar = true;
159
+ cur += c;
160
+ continue;
161
+ }
162
+ if ("([{".includes(c)) depth++;
163
+ else if (")]}".includes(c)) depth--;
164
+ if (depth === 0 && (c === "," || c === ";")) {
165
+ push(c);
166
+ continue;
167
+ }
168
+ cur += c;
169
+ }
170
+ push("");
171
+ return out;
172
+ }
173
+
174
+ function stripOuterParens(s: string): string {
175
+ let t = s.trim();
176
+ for (;;) {
177
+ if (!(t.startsWith("(") && t.endsWith(")"))) return t;
178
+ let depth = 0;
179
+ let balanced = true;
180
+ for (let i = 0; i < t.length; i++) {
181
+ const c = t.charAt(i);
182
+ if (c === '"') {
183
+ i++;
184
+ while (i < t.length && t.charAt(i) !== '"') {
185
+ if (t.charAt(i) === "\\") i++;
186
+ i++;
187
+ }
188
+ continue;
189
+ }
190
+ if (c === "'") {
191
+ i++;
192
+ while (i < t.length && t.charAt(i) !== "'") {
193
+ if (t.charAt(i) === "\\") i++;
194
+ i++;
195
+ }
196
+ continue;
197
+ }
198
+ if (c === "(") depth++;
199
+ else if (c === ")") {
200
+ depth--;
201
+ if (depth === 0 && i !== t.length - 1) {
202
+ balanced = false;
203
+ break;
204
+ }
205
+ }
206
+ }
207
+ if (!balanced) return t;
208
+ t = t.slice(1, -1).trim();
209
+ }
210
+ }
211
+
212
+ function headName(goal: string): string | null {
213
+ const m = /^\s*(?:\\\+\s*)?([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/.exec(goal);
214
+ if (m && m[1]) return m[1];
215
+ const m2 = /^\s*(?:\\\+\s*)?([a-zA-Z_][a-zA-Z0-9_]*)\s*$/.exec(goal);
216
+ return m2 && m2[1] ? m2[1] : null;
217
+ }
218
+
219
+ function firstString(goal: string): string {
220
+ const m = /"((?:[^"\\]|\\.)*)"/.exec(goal);
221
+ return m && m[1] ? m[1].replace(/\\n/g, " ").replace(/\s+/g, " ").trim() : "";
222
+ }
223
+
224
+ function firstArgVar(goal: string): string {
225
+ const i = goal.indexOf("(");
226
+ if (i < 0) return "";
227
+ const inside = goal.slice(i + 1, goal.lastIndexOf(")"));
228
+ const arg = (splitTop(inside, ",")[0] ?? "").trim();
229
+ return /^[A-Z_]/.test(arg) ? arg : "";
230
+ }
231
+
232
+ const trunc = (s: string, n = 64): string => (s.length > n ? s.slice(0, n - 1) + "…" : s);
233
+
234
+ // ---------- clause extraction (balanced scanner) -----------------------------
235
+
236
+ function extractClauses(src: string): ClauseMap {
237
+ const clauses: ClauseMap = new Map();
238
+ const isId = (c: string) => /[a-zA-Z0-9_]/.test(c);
239
+ const skipWs = (p: number) => {
240
+ while (p < src.length && /\s/.test(src.charAt(p))) p++;
241
+ return p;
242
+ };
243
+ const readStr = (p: number, q: string) => {
244
+ p++;
245
+ while (p < src.length && src.charAt(p) !== q) {
246
+ if (src.charAt(p) === "\\") p++;
247
+ p++;
248
+ }
249
+ return p + 1;
250
+ };
251
+ let i = 0;
252
+ while (i < src.length) {
253
+ i = skipWs(i);
254
+ if (i >= src.length) break;
255
+ if (!/[a-z]/.test(src.charAt(i))) {
256
+ i++;
257
+ continue;
258
+ }
259
+ let j = i;
260
+ while (j < src.length && isId(src.charAt(j))) j++;
261
+ const name = src.slice(i, j);
262
+ let k = j;
263
+ while (k < src.length && /[ \t]/.test(src.charAt(k))) k++;
264
+ let args = "";
265
+ if (src.charAt(k) === "(") {
266
+ let depth = 0;
267
+ let p = k;
268
+ for (; p < src.length; p++) {
269
+ const c = src.charAt(p);
270
+ if (c === '"') {
271
+ p = readStr(p, '"') - 1;
272
+ continue;
273
+ }
274
+ if (c === "'") {
275
+ p = readStr(p, "'") - 1;
276
+ continue;
277
+ }
278
+ if (c === "(") depth++;
279
+ else if (c === ")") {
280
+ depth--;
281
+ if (depth === 0) {
282
+ p++;
283
+ break;
284
+ }
285
+ }
286
+ }
287
+ args = src.slice(k + 1, Math.max(k + 1, p - 1));
288
+ k = p;
289
+ while (k < src.length && /[ \t]/.test(src.charAt(k))) k++;
290
+ }
291
+ let hasBody = false;
292
+ if (src.charAt(k) === ":" && src.charAt(k + 1) === "-") {
293
+ hasBody = true;
294
+ k += 2;
295
+ }
296
+ let depth = 0;
297
+ let inStr = false;
298
+ let inChar = false;
299
+ let b = k;
300
+ for (; b < src.length; b++) {
301
+ const c = src.charAt(b);
302
+ if (inStr) {
303
+ if (c === "\\") b++;
304
+ else if (c === '"') inStr = false;
305
+ continue;
306
+ }
307
+ if (inChar) {
308
+ if (c === "\\") b++;
309
+ else if (c === "'") inChar = false;
310
+ continue;
311
+ }
312
+ if (c === '"') {
313
+ inStr = true;
314
+ continue;
315
+ }
316
+ if (c === "'") {
317
+ inChar = true;
318
+ continue;
319
+ }
320
+ if ("([{".includes(c)) depth++;
321
+ else if (")]}".includes(c)) depth--;
322
+ else if (c === "." && depth === 0 && /\s|$/.test(src.charAt(b + 1))) break;
323
+ }
324
+ if (!clauses.has(name)) clauses.set(name, []);
325
+ clauses.get(name)!.push({ args, body: hasBody ? src.slice(k, b) : "" });
326
+ i = b + 1;
327
+ }
328
+ return clauses;
329
+ }
330
+
331
+ interface ToolCapability {
332
+ name: string;
333
+ args: string;
334
+ }
335
+
336
+ function extractTools(src: string): ToolCapability[] {
337
+ const tools: ToolCapability[] = [];
338
+ for (const m of src.matchAll(/tool\s*\(\s*([a-z][a-zA-Z0-9_]*)\s*\(([^)]*)\)\s*,\s*"((?:[^"\\]|\\.)*)"\s*\)\s*:-/g)) {
339
+ tools.push({ name: m[1] ?? "", args: (m[2] ?? "").trim() });
340
+ }
341
+ return tools;
342
+ }
343
+
344
+ // ---------- analysis ---------------------------------------------------------
345
+
346
+ const BUILTIN = new Set([
347
+ "format", "get_dict", "length", "findall", "member", "forall", "sub_string",
348
+ "atomics_to_string", "retractall", "assertz", "retract", "true", "is",
349
+ "use_module", "=", "\\=", "=:=", ">", "<", ">=", "=<",
350
+ ]);
351
+
352
+ function agenticPredicates(clauses: ClauseMap): Set<string> {
353
+ const agentic = new Set<string>();
354
+ const prim = (n: string) => ["task", "prompt", "exec", "with_tools"].includes(n);
355
+ const visit = (n: string, seen: Set<string>): boolean => {
356
+ if (agentic.has(n)) return true;
357
+ if (seen.has(n)) return false;
358
+ seen.add(n);
359
+ for (const { body } of clauses.get(n) ?? []) {
360
+ for (const { goal } of splitGoals(body)) {
361
+ const h = headName(stripOuterParens(goal));
362
+ if (h && prim(h)) {
363
+ agentic.add(n);
364
+ return true;
365
+ }
366
+ if (h && clauses.has(h) && !BUILTIN.has(h) && visit(h, seen)) {
367
+ agentic.add(n);
368
+ return true;
369
+ }
370
+ }
371
+ }
372
+ return false;
373
+ };
374
+ for (const n of clauses.keys()) visit(n, new Set());
375
+ return agentic;
376
+ }
377
+
378
+ // ---------- rendering --------------------------------------------------------
379
+
380
+ function parseArrow(goal: string): { cond: string; then: string; else: string } | null {
381
+ const t = stripOuterParens(goal);
382
+ let depth = 0;
383
+ let inStr = false;
384
+ let idx = -1;
385
+ for (let i = 0; i < t.length; i++) {
386
+ const c = t.charAt(i);
387
+ if (inStr) {
388
+ if (c === "\\") i++;
389
+ else if (c === '"') inStr = false;
390
+ continue;
391
+ }
392
+ if (c === '"') {
393
+ inStr = true;
394
+ continue;
395
+ }
396
+ if ("([{".includes(c)) depth++;
397
+ else if (")]}".includes(c)) depth--;
398
+ else if (c === "-" && t.charAt(i + 1) === ">" && depth === 0) {
399
+ idx = i;
400
+ break;
401
+ }
402
+ }
403
+ if (idx < 0) return null;
404
+ const parts = splitTop(t.slice(idx + 2), ";");
405
+ return { cond: t.slice(0, idx).trim(), then: parts[0] ?? "", else: parts[1] ?? "" };
406
+ }
407
+
408
+ function labelText(goal: string, ctx: string, fallback: string): string {
409
+ const s = firstString(goal);
410
+ if (s) return s;
411
+ if (ctx) return `(${ctx})`;
412
+ const v = firstArgVar(goal);
413
+ return v ? `→ ${v}` : fallback;
414
+ }
415
+
416
+ function phaseLabel(goal: string): string | null {
417
+ if (headName(goal) !== "output") return null;
418
+ const s = firstString(goal);
419
+ return /^Phase\s+\d+\s*\/\s*\d+/i.test(s) ? s : null;
420
+ }
421
+
422
+ interface NodeKind {
423
+ shape: [string, string];
424
+ cls: string;
425
+ label: string;
426
+ terminal?: boolean;
427
+ }
428
+
429
+ function kindFor(goal: string, ctx = ""): NodeKind | null {
430
+ const h = headName(goal);
431
+ switch (h) {
432
+ case "task":
433
+ return { shape: ["{{", "}}"], cls: "llm", label: `task<br/>${trunc(labelText(goal, ctx, "task"))}` };
434
+ case "prompt":
435
+ return { shape: ["{{", "}}"], cls: "llm", label: `prompt<br/>${trunc(labelText(goal, ctx, "review"))}` };
436
+ case "exec": {
437
+ const i = goal.indexOf("(");
438
+ const inner = i >= 0 ? goal.slice(i + 1, goal.lastIndexOf(")")) : goal;
439
+ const call = splitTop(inner, ",")[0] ?? "";
440
+ const nm = headName(call) || "host";
441
+ const arg = firstString(call);
442
+ return { shape: ["[/", "/]"], cls: "host", label: `exec<br/>${nm}${arg && nm === "pi_bash" ? ": " + trunc(arg, 28) : ""}` };
443
+ }
444
+ case "output":
445
+ return firstString(goal) ? { shape: ["[", "]"], cls: "out", label: `output: ${trunc(firstString(goal), 56)}` } : null;
446
+ case "system":
447
+ return { shape: ["[", "]"], cls: "det", label: "system prompt" };
448
+ case "answer":
449
+ return { shape: ["([", "])"], cls: "terminal", label: "answer", terminal: true };
450
+ default:
451
+ return null;
452
+ }
453
+ }
454
+
455
+ interface RenderState {
456
+ n: number;
457
+ lines: string[];
458
+ clauses: ClauseMap;
459
+ agentic: Set<string>;
460
+ stack: string[];
461
+ sawAnswer: boolean;
462
+ phaseOpen: boolean;
463
+ }
464
+
465
+ export function renderDml(file: string, src: string, opts: RenderOptions = {}): string {
466
+ const clauses = extractClauses(stripComments(src));
467
+ const agentic = agenticPredicates(clauses);
468
+ const tools = extractTools(src);
469
+ const state: RenderState = {
470
+ n: 0,
471
+ lines: ["flowchart TD"],
472
+ clauses,
473
+ agentic,
474
+ stack: [],
475
+ sawAnswer: false,
476
+ phaseOpen: false,
477
+ };
478
+ const nid = () => `n${++state.n}`;
479
+ const node = (shape: [string, string], label: string, klass: string): string => {
480
+ const id = nid();
481
+ state.lines.push(` ${id}${shape[0]}"${label.replace(/"/g, "'")}"${shape[1]}:::${klass}`);
482
+ return id;
483
+ };
484
+ const edge = (a: string, b: string, label?: string) =>
485
+ state.lines.push(` ${a} ${label ? `-->|${label}|` : "-->"} ${b}`);
486
+
487
+ function renderGoals(goals: Goal[], ctx = "", topLevel = false): Segment {
488
+ let entry: string | null = null;
489
+ let exit: string | null = null;
490
+ for (const { goal } of goals) {
491
+ if (topLevel) {
492
+ const ph = phaseLabel(goal);
493
+ if (ph) {
494
+ if (state.phaseOpen) {
495
+ state.lines.push(" end");
496
+ state.phaseOpen = false;
497
+ }
498
+ state.lines.push(` subgraph ph${++state.n}["${ph.replace(/"/g, "'")}"]`);
499
+ state.lines.push(" direction TB");
500
+ state.phaseOpen = true;
501
+ continue;
502
+ }
503
+ }
504
+ const h = headName(stripOuterParens(goal));
505
+ if (!parseArrow(goal) && h && BUILTIN.has(h)) continue;
506
+ const seg = renderGoal(goal, ctx);
507
+ if (!seg || !seg.entry) continue;
508
+ if (!entry) entry = seg.entry;
509
+ if (exit) edge(exit, seg.entry);
510
+ exit = seg.exit ?? seg.entry;
511
+ }
512
+ return { entry, exit };
513
+ }
514
+
515
+ function renderGoal(rawGoal: string, ctx = ""): Segment | null {
516
+ const goal = stripOuterParens(rawGoal);
517
+ const h = headName(goal);
518
+
519
+ const br = parseArrow(goal);
520
+ if (br) {
521
+ const d = node(["{", "}"], `if ${trunc(br.cond, 46)}`, "det");
522
+ const t = renderGoals(splitGoals(br.then), ctx);
523
+ const e = br.else.trim() ? renderGoals(splitGoals(br.else), ctx) : null;
524
+ const merge = nid();
525
+ state.lines.push(` ${merge}["merge"]:::det`);
526
+ edge(d, t.entry ?? merge, "then");
527
+ edge(d, e?.entry ?? merge, "else");
528
+ if (t.exit) edge(t.exit, merge);
529
+ if (e?.exit) edge(e.exit, merge);
530
+ return { entry: d, exit: merge };
531
+ }
532
+
533
+ if (h === "with_tools") {
534
+ const inside = goal.slice(goal.indexOf("(") + 1, goal.lastIndexOf(")"));
535
+ const parts = splitTop(inside, ",");
536
+ const toolList = (parts[0] || "").replace(/[\[\]]/g, "").replace(/\s+/g, " ").trim();
537
+ const sg = `scope${++state.n}`;
538
+ state.lines.push(` subgraph ${sg}["with_tools: ${trunc(toolList, 60)}"]`);
539
+ state.lines.push(" direction TB");
540
+ const saved = state.lines;
541
+ state.lines = [];
542
+ const inner = renderGoals(splitGoals(parts.slice(1).join(",")), ctx);
543
+ const innerLines = state.lines;
544
+ state.lines = saved;
545
+ for (const l of innerLines) state.lines.push(` ${l}`);
546
+ state.lines.push(" end");
547
+ return inner.entry ? inner : null;
548
+ }
549
+
550
+ const k = kindFor(goal, ctx);
551
+ if (k) {
552
+ if (opts.hideOutput && k.cls === "out") return null;
553
+ if (k.terminal) state.sawAnswer = true;
554
+ return { entry: node(k.shape, k.label, k.cls), exit: null };
555
+ }
556
+
557
+ if (h && clauses.has(h)) {
558
+ const bodies = clauses.get(h)!;
559
+ const isFact = bodies.every((c) => !c.body.trim());
560
+ if (isFact) return null;
561
+ if (agentic.has(h) && !state.stack.includes(h)) {
562
+ state.stack.push(h);
563
+ const seg = renderGoals(splitGoals(bodies[0]?.body ?? ""), h);
564
+ state.stack.pop();
565
+ return seg.entry ? seg : null;
566
+ }
567
+ return { entry: node(["[[", "]]"], `${h}()`, "det"), exit: null };
568
+ }
569
+ return null;
570
+ }
571
+
572
+ const mains = clauses.get("agent_main") ?? [];
573
+ const args0 = (mains[0]?.args ?? "").trim();
574
+ const arity = args0 ? splitTop(args0, ",").filter(Boolean).length : 0;
575
+ const start = node(["([", "])"], `agent_main/${arity}<br/>${basename(file)}`, "start");
576
+ const body = renderGoals(splitGoals(mains[0]?.body ?? ""), "", true);
577
+ if (state.phaseOpen) {
578
+ state.lines.push(" end");
579
+ state.phaseOpen = false;
580
+ }
581
+ if (body.entry) edge(start, body.entry);
582
+ if (!state.sawAnswer) edge(body.exit ?? body.entry ?? start, node(["([", "])"], "done", "terminal"));
583
+ if (mains.length > 1) edge(start, node(["([", "])"], "fallback clause (usage)", "terminal"), "on failure");
584
+
585
+ if (tools.length) {
586
+ state.lines.push(' subgraph CAP["tool/2 capabilities"]');
587
+ state.lines.push(" direction LR");
588
+ tools.forEach((t, i) => state.lines.push(` tool${i}[["${t.name}(${trunc(t.args, 26)})"]]:::tool`));
589
+ state.lines.push(" end");
590
+ }
591
+ state.lines.push(
592
+ " classDef start fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px;",
593
+ " classDef terminal fill:#e3f2fd,stroke:#1565c0;",
594
+ " classDef llm fill:#fff8e1,stroke:#f9a825,stroke-width:2px;",
595
+ " classDef det fill:#eceff1,stroke:#546e7a;",
596
+ " classDef host fill:#f3e5f5,stroke:#6a1b9a;",
597
+ " classDef out fill:#fafafa,stroke:#bdbdbd;",
598
+ " classDef tool fill:#ede7f6,stroke:#4527a0;",
599
+ );
600
+ return state.lines.join("\n");
601
+ }
602
+
603
+ // ---------- sequence view ----------------------------------------------------
604
+
605
+ const clamp = (s: string, n = 72): string =>
606
+ s.replace(/\s+/g, " ").replace(/"/g, "'").trim().slice(0, n);
607
+
608
+ export function renderSequence(file: string, src: string): string {
609
+ const clauses = extractClauses(stripComments(src));
610
+ const agentic = agenticPredicates(clauses);
611
+ const lines = [
612
+ "sequenceDiagram",
613
+ " autonumber",
614
+ " actor U as User",
615
+ " participant M as Agent (LLM)",
616
+ " participant P as Prolog spine",
617
+ " participant E as Tools / Env",
618
+ ];
619
+ const state: { clauses: ClauseMap; agentic: Set<string>; stack: string[]; scope: string[][] } = {
620
+ clauses,
621
+ agentic,
622
+ stack: [],
623
+ scope: [],
624
+ };
625
+
626
+ const execName = (g: string): string => {
627
+ const i = g.indexOf("(");
628
+ const inner = i >= 0 ? g.slice(i + 1, g.lastIndexOf(")")) : g;
629
+ return headName(splitTop(inner, ",")[0] ?? "") || "tool";
630
+ };
631
+
632
+ function taskMsg(g: string, ctx: string): void {
633
+ const scope = state.scope[state.scope.length - 1] || [];
634
+ const label = clamp(firstString(g) || ctx || firstArgVar(g) || "task");
635
+ lines.push(` P->>M: task — ${label}`);
636
+ if (scope.includes("user_feedback")) {
637
+ lines.push(" M->>U: present case / ask");
638
+ lines.push(" U-->>M: ok or corrections");
639
+ lines.push(" M-->>P: confirmed result");
640
+ } else if (scope.length) {
641
+ for (const t of scope) lines.push(` M->>E: ${clamp(t, 30)}(...)`);
642
+ lines.push(" E-->>M: tool results");
643
+ lines.push(" M-->>P: summary");
644
+ } else {
645
+ lines.push(" M-->>P: typed result");
646
+ }
647
+ }
648
+
649
+ function walk(goals: Goal[], ctx = ""): void {
650
+ for (const { goal } of goals) {
651
+ const g = stripOuterParens(goal);
652
+ const h = headName(g);
653
+ const br = parseArrow(g);
654
+ if (br) {
655
+ lines.push(` alt ${clamp(br.cond, 40)}`);
656
+ walk(splitGoals(br.then), ctx);
657
+ if (br.else.trim()) {
658
+ lines.push(" else");
659
+ walk(splitGoals(br.else), ctx);
660
+ }
661
+ lines.push(" end");
662
+ continue;
663
+ }
664
+ if (h === "with_tools") {
665
+ const inside = g.slice(g.indexOf("(") + 1, g.lastIndexOf(")"));
666
+ const parts = splitTop(inside, ",");
667
+ const scope = (parts[0] || "").replace(/[\[\]]/g, "").split(",").map((s) => s.trim()).filter(Boolean);
668
+ state.scope.push(scope);
669
+ walk(splitGoals(parts.slice(1).join(",")), ctx);
670
+ state.scope.pop();
671
+ continue;
672
+ }
673
+ if (!br && h && BUILTIN.has(h)) continue;
674
+ if (h === "system") {
675
+ lines.push(` Note over M: ${clamp(firstString(g) || "role + rules")}`);
676
+ continue;
677
+ }
678
+ if (h === "task") {
679
+ taskMsg(g, ctx);
680
+ continue;
681
+ }
682
+ if (h === "prompt") {
683
+ lines.push(` P->>M: review — ${clamp(firstString(g) || ctx || firstArgVar(g) || "review")}`);
684
+ lines.push(" M-->>P: verdict + reason");
685
+ continue;
686
+ }
687
+ if (h === "exec") {
688
+ lines.push(` P->>E: ${execName(g)}`);
689
+ lines.push(" E-->>P: result");
690
+ continue;
691
+ }
692
+ if (h === "answer") {
693
+ lines.push(" P->>U: final answer");
694
+ continue;
695
+ }
696
+ if (h === "output") {
697
+ const ph = phaseLabel(g);
698
+ if (ph) lines.push(` Note over P: ${clamp(ph)}`);
699
+ continue;
700
+ }
701
+ if (h && clauses.has(h) && agentic.has(h) && !state.stack.includes(h)) {
702
+ state.stack.push(h);
703
+ walk(splitGoals(clauses.get(h)?.[0]?.body ?? ""), h);
704
+ state.stack.pop();
705
+ continue;
706
+ }
707
+ if (h && clauses.has(h)) {
708
+ if (clauses.get(h)!.every((c) => !c.body.trim())) continue;
709
+ lines.push(` P->>P: ${h}()`);
710
+ }
711
+ }
712
+ }
713
+
714
+ walk(splitGoals(clauses.get("agent_main")?.[0]?.body ?? ""));
715
+ return lines.join("\n");
716
+ }
717
+
718
+ function basename(file: string): string {
719
+ const parts = file.split(/[\\/]/);
720
+ return parts[parts.length - 1] || file;
721
+ }