agentopology 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/LICENSE +191 -0
  2. package/README.md +450 -0
  3. package/dist/analyzer/__tests__/analyzer.test.d.ts +2 -0
  4. package/dist/analyzer/__tests__/analyzer.test.d.ts.map +1 -0
  5. package/dist/analyzer/__tests__/analyzer.test.js +258 -0
  6. package/dist/analyzer/__tests__/analyzer.test.js.map +1 -0
  7. package/dist/analyzer/index.d.ts +44 -0
  8. package/dist/analyzer/index.d.ts.map +1 -0
  9. package/dist/analyzer/index.js +308 -0
  10. package/dist/analyzer/index.js.map +1 -0
  11. package/dist/bindings/__tests__/bindings.test.d.ts +2 -0
  12. package/dist/bindings/__tests__/bindings.test.d.ts.map +1 -0
  13. package/dist/bindings/__tests__/bindings.test.js +2089 -0
  14. package/dist/bindings/__tests__/bindings.test.js.map +1 -0
  15. package/dist/bindings/claude-code.d.ts +12 -0
  16. package/dist/bindings/claude-code.d.ts.map +1 -0
  17. package/dist/bindings/claude-code.js +2063 -0
  18. package/dist/bindings/claude-code.js.map +1 -0
  19. package/dist/bindings/codex.d.ts +18 -0
  20. package/dist/bindings/codex.d.ts.map +1 -0
  21. package/dist/bindings/codex.js +1633 -0
  22. package/dist/bindings/codex.js.map +1 -0
  23. package/dist/bindings/copilot-cli.d.ts +20 -0
  24. package/dist/bindings/copilot-cli.d.ts.map +1 -0
  25. package/dist/bindings/copilot-cli.js +1515 -0
  26. package/dist/bindings/copilot-cli.js.map +1 -0
  27. package/dist/bindings/cursor.d.ts +17 -0
  28. package/dist/bindings/cursor.d.ts.map +1 -0
  29. package/dist/bindings/cursor.js +1715 -0
  30. package/dist/bindings/cursor.js.map +1 -0
  31. package/dist/bindings/gemini-cli.d.ts +18 -0
  32. package/dist/bindings/gemini-cli.d.ts.map +1 -0
  33. package/dist/bindings/gemini-cli.js +1327 -0
  34. package/dist/bindings/gemini-cli.js.map +1 -0
  35. package/dist/bindings/index.d.ts +20 -0
  36. package/dist/bindings/index.d.ts.map +1 -0
  37. package/dist/bindings/index.js +33 -0
  38. package/dist/bindings/index.js.map +1 -0
  39. package/dist/bindings/kiro.d.ts +17 -0
  40. package/dist/bindings/kiro.d.ts.map +1 -0
  41. package/dist/bindings/kiro.js +1624 -0
  42. package/dist/bindings/kiro.js.map +1 -0
  43. package/dist/bindings/openclaw.d.ts +18 -0
  44. package/dist/bindings/openclaw.d.ts.map +1 -0
  45. package/dist/bindings/openclaw.js +1578 -0
  46. package/dist/bindings/openclaw.js.map +1 -0
  47. package/dist/bindings/types.d.ts +33 -0
  48. package/dist/bindings/types.d.ts.map +1 -0
  49. package/dist/bindings/types.js +23 -0
  50. package/dist/bindings/types.js.map +1 -0
  51. package/dist/cli/index.d.ts +19 -0
  52. package/dist/cli/index.d.ts.map +1 -0
  53. package/dist/cli/index.js +679 -0
  54. package/dist/cli/index.js.map +1 -0
  55. package/dist/docs/__tests__/docs.test.d.ts +2 -0
  56. package/dist/docs/__tests__/docs.test.d.ts.map +1 -0
  57. package/dist/docs/__tests__/docs.test.js +438 -0
  58. package/dist/docs/__tests__/docs.test.js.map +1 -0
  59. package/dist/docs/content.d.ts +16 -0
  60. package/dist/docs/content.d.ts.map +1 -0
  61. package/dist/docs/content.js +3273 -0
  62. package/dist/docs/content.js.map +1 -0
  63. package/dist/docs/index.d.ts +17 -0
  64. package/dist/docs/index.d.ts.map +1 -0
  65. package/dist/docs/index.js +117 -0
  66. package/dist/docs/index.js.map +1 -0
  67. package/dist/exporters/__tests__/exporters.test.d.ts +2 -0
  68. package/dist/exporters/__tests__/exporters.test.d.ts.map +1 -0
  69. package/dist/exporters/__tests__/exporters.test.js +731 -0
  70. package/dist/exporters/__tests__/exporters.test.js.map +1 -0
  71. package/dist/exporters/index.d.ts +13 -0
  72. package/dist/exporters/index.d.ts.map +1 -0
  73. package/dist/exporters/index.js +18 -0
  74. package/dist/exporters/index.js.map +1 -0
  75. package/dist/exporters/json.d.ts +8 -0
  76. package/dist/exporters/json.d.ts.map +1 -0
  77. package/dist/exporters/json.js +25 -0
  78. package/dist/exporters/json.js.map +1 -0
  79. package/dist/exporters/markdown.d.ts +9 -0
  80. package/dist/exporters/markdown.d.ts.map +1 -0
  81. package/dist/exporters/markdown.js +686 -0
  82. package/dist/exporters/markdown.js.map +1 -0
  83. package/dist/exporters/mermaid.d.ts +11 -0
  84. package/dist/exporters/mermaid.d.ts.map +1 -0
  85. package/dist/exporters/mermaid.js +161 -0
  86. package/dist/exporters/mermaid.js.map +1 -0
  87. package/dist/exporters/types.d.ts +22 -0
  88. package/dist/exporters/types.d.ts.map +1 -0
  89. package/dist/exporters/types.js +10 -0
  90. package/dist/exporters/types.js.map +1 -0
  91. package/dist/import/__tests__/import.test.d.ts +2 -0
  92. package/dist/import/__tests__/import.test.d.ts.map +1 -0
  93. package/dist/import/__tests__/import.test.js +781 -0
  94. package/dist/import/__tests__/import.test.js.map +1 -0
  95. package/dist/import/claude-code.d.ts +38 -0
  96. package/dist/import/claude-code.d.ts.map +1 -0
  97. package/dist/import/claude-code.js +897 -0
  98. package/dist/import/claude-code.js.map +1 -0
  99. package/dist/import/index.d.ts +18 -0
  100. package/dist/import/index.d.ts.map +1 -0
  101. package/dist/import/index.js +28 -0
  102. package/dist/import/index.js.map +1 -0
  103. package/dist/import/serializer.d.ts +11 -0
  104. package/dist/import/serializer.d.ts.map +1 -0
  105. package/dist/import/serializer.js +960 -0
  106. package/dist/import/serializer.js.map +1 -0
  107. package/dist/index.d.ts +26 -0
  108. package/dist/index.d.ts.map +1 -0
  109. package/dist/index.js +21 -0
  110. package/dist/index.js.map +1 -0
  111. package/dist/parser/__tests__/parser.test.d.ts +2 -0
  112. package/dist/parser/__tests__/parser.test.d.ts.map +1 -0
  113. package/dist/parser/__tests__/parser.test.js +6514 -0
  114. package/dist/parser/__tests__/parser.test.js.map +1 -0
  115. package/dist/parser/ast.d.ts +708 -0
  116. package/dist/parser/ast.d.ts.map +1 -0
  117. package/dist/parser/ast.js +12 -0
  118. package/dist/parser/ast.js.map +1 -0
  119. package/dist/parser/index.d.ts +253 -0
  120. package/dist/parser/index.d.ts.map +1 -0
  121. package/dist/parser/index.js +1985 -0
  122. package/dist/parser/index.js.map +1 -0
  123. package/dist/parser/lexer.d.ts +100 -0
  124. package/dist/parser/lexer.d.ts.map +1 -0
  125. package/dist/parser/lexer.js +304 -0
  126. package/dist/parser/lexer.js.map +1 -0
  127. package/dist/parser/validator.d.ts +42 -0
  128. package/dist/parser/validator.d.ts.map +1 -0
  129. package/dist/parser/validator.js +2568 -0
  130. package/dist/parser/validator.js.map +1 -0
  131. package/dist/scaffold/__tests__/scaffold.test.d.ts +7 -0
  132. package/dist/scaffold/__tests__/scaffold.test.d.ts.map +1 -0
  133. package/dist/scaffold/__tests__/scaffold.test.js +295 -0
  134. package/dist/scaffold/__tests__/scaffold.test.js.map +1 -0
  135. package/dist/scaffold/incremental.d.ts +43 -0
  136. package/dist/scaffold/incremental.d.ts.map +1 -0
  137. package/dist/scaffold/incremental.js +185 -0
  138. package/dist/scaffold/incremental.js.map +1 -0
  139. package/dist/scaffold/index.d.ts +13 -0
  140. package/dist/scaffold/index.d.ts.map +1 -0
  141. package/dist/scaffold/index.js +13 -0
  142. package/dist/scaffold/index.js.map +1 -0
  143. package/dist/scaffold/manifest.d.ts +30 -0
  144. package/dist/scaffold/manifest.d.ts.map +1 -0
  145. package/dist/scaffold/manifest.js +71 -0
  146. package/dist/scaffold/manifest.js.map +1 -0
  147. package/dist/scaffold/merge.d.ts +52 -0
  148. package/dist/scaffold/merge.d.ts.map +1 -0
  149. package/dist/scaffold/merge.js +134 -0
  150. package/dist/scaffold/merge.js.map +1 -0
  151. package/dist/scaffold/types.d.ts +51 -0
  152. package/dist/scaffold/types.d.ts.map +1 -0
  153. package/dist/scaffold/types.js +10 -0
  154. package/dist/scaffold/types.js.map +1 -0
  155. package/dist/sync/index.d.ts +34 -0
  156. package/dist/sync/index.d.ts.map +1 -0
  157. package/dist/sync/index.js +392 -0
  158. package/dist/sync/index.js.map +1 -0
  159. package/dist/visualizer/index.d.ts +17 -0
  160. package/dist/visualizer/index.d.ts.map +1 -0
  161. package/dist/visualizer/index.js +1976 -0
  162. package/dist/visualizer/index.js.map +1 -0
  163. package/examples/code-review.at +167 -0
  164. package/examples/data-processing.at +296 -0
  165. package/examples/openclaw-assistant.at +142 -0
  166. package/examples/scheduled-monitor.at +193 -0
  167. package/examples/simple-pipeline.at +163 -0
  168. package/package.json +77 -0
  169. package/skill/SKILL.md +403 -0
  170. package/skill/scripts/emit.ts +853 -0
  171. package/skill/scripts/visualize.ts +89 -0
  172. package/spec/grammar.md +1466 -0
  173. package/spec/reserved-keywords.md +147 -0
  174. package/spec/validation.md +535 -0
@@ -0,0 +1,731 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { parse } from "../../parser/index.js";
3
+ import { exporters, markdownExporter, mermaidExporter, jsonExporter, } from "../index.js";
4
+ // ---------------------------------------------------------------------------
5
+ // Test topologies
6
+ // ---------------------------------------------------------------------------
7
+ const SIMPLE_PIPELINE = `
8
+ topology simple-pipeline : [pipeline, human-gate] {
9
+ meta {
10
+ version: "1.0.0"
11
+ description: "Research, write, and review"
12
+ }
13
+
14
+ orchestrator {
15
+ model: sonnet
16
+ generates: "commands/create.md"
17
+ handles: [intake, done]
18
+ outputs: {
19
+ topic-type: technical | creative
20
+ }
21
+ }
22
+
23
+ roles {
24
+ researcher: "Gather information and compile research notes"
25
+ writer: "Draft content based on research"
26
+ reviewer: "Review drafts for accuracy"
27
+ }
28
+
29
+ action intake {
30
+ kind: inline
31
+ description: "Parse user request"
32
+ }
33
+
34
+ agent researcher {
35
+ role: researcher
36
+ model: gpt-4o
37
+ permissions: supervised
38
+ phase: 1
39
+ tools: [Read, Grep, Glob, WebSearch]
40
+ writes: ["workspace/research.md"]
41
+ prompt {
42
+ You are a research specialist.
43
+ }
44
+ }
45
+
46
+ agent writer {
47
+ role: writer
48
+ model: sonnet
49
+ permissions: autonomous
50
+ phase: 2
51
+ tools: [Read, Write, Glob]
52
+ reads: ["workspace/research.md"]
53
+ writes: ["workspace/draft.md"]
54
+ outputs: {
55
+ confidence: high | medium | low
56
+ }
57
+ }
58
+
59
+ agent reviewer {
60
+ role: reviewer
61
+ model: opus
62
+ permissions: supervised
63
+ phase: 3
64
+ tools: [Read, Grep, Glob]
65
+ reads: ["workspace/draft.md", "workspace/research.md"]
66
+ writes: ["workspace/review.md"]
67
+ outputs: {
68
+ verdict: approve | revise | reject
69
+ }
70
+ }
71
+
72
+ flow {
73
+ intake -> researcher
74
+ researcher -> writer
75
+ writer -> reviewer
76
+ reviewer -> writer [when reviewer.verdict == revise, max 2]
77
+ reviewer -> researcher [when reviewer.verdict == reject, max 1]
78
+ reviewer -> done [when reviewer.verdict == approve]
79
+ }
80
+
81
+ gates {
82
+ gate quality-check {
83
+ after: writer
84
+ before: reviewer
85
+ run: "scripts/check-quality.sh"
86
+ checks: [grammar, formatting]
87
+ on-fail: bounce-back
88
+ }
89
+ }
90
+
91
+ action done {
92
+ kind: report
93
+ description: "Deliver final content"
94
+ }
95
+
96
+ memory {
97
+ workspace {
98
+ path: "workspace/"
99
+ structure: [research, drafts, reviews]
100
+ }
101
+ }
102
+
103
+ triggers {
104
+ command create {
105
+ pattern: "/create <TOPIC>"
106
+ argument: TOPIC
107
+ }
108
+ }
109
+
110
+ settings {
111
+ allow: ["Read", "Grep", "Glob", "WebSearch"]
112
+ deny: ["Bash(rm -rf *)"]
113
+ }
114
+ }
115
+ `;
116
+ const FAN_OUT_TOPOLOGY = `
117
+ topology code-review : [pipeline, fan-out] {
118
+ meta {
119
+ version: "2.0.0"
120
+ description: "Code review with fan-out"
121
+ }
122
+
123
+ orchestrator {
124
+ model: opus
125
+ handles: [intake, create-report]
126
+ }
127
+
128
+ roles {
129
+ analyzer: "Static analysis"
130
+ security: "Security scanning"
131
+ reviewer: "Code review"
132
+ }
133
+
134
+ action intake {
135
+ kind: external
136
+ source: "github-pr"
137
+ description: "Fetch PR diff"
138
+ }
139
+
140
+ action create-report {
141
+ kind: report
142
+ description: "Final report"
143
+ }
144
+
145
+ agent analyzer {
146
+ role: analyzer
147
+ model: gemini-2.0-flash
148
+ phase: 1
149
+ tools: [Read, Grep]
150
+ behavior: advisory
151
+ outputs: {
152
+ risk-level: low | medium | high
153
+ }
154
+ }
155
+
156
+ agent security-scanner {
157
+ role: security
158
+ model: sonnet
159
+ phase: 1
160
+ tools: [Read, Grep]
161
+ behavior: advisory
162
+ }
163
+
164
+ agent reviewer {
165
+ role: reviewer
166
+ model: opus
167
+ phase: 2
168
+ tools: [Read, Grep, Glob]
169
+ outputs: {
170
+ verdict: approve | reject
171
+ }
172
+ }
173
+
174
+ flow {
175
+ intake -> [analyzer, security-scanner]
176
+ analyzer -> reviewer
177
+ security-scanner -> reviewer
178
+ reviewer -> create-report [when reviewer.verdict == approve]
179
+ reviewer -> create-report [when reviewer.verdict == reject]
180
+ }
181
+
182
+ hooks {
183
+ hook log-findings {
184
+ on: AgentStop
185
+ run: "scripts/log.sh"
186
+ type: command
187
+ }
188
+ }
189
+ }
190
+ `;
191
+ const MINIMAL_TOPOLOGY = `
192
+ topology minimal : [pipeline] {
193
+ meta {
194
+ version: "0.1.0"
195
+ description: "Bare minimum"
196
+ }
197
+ orchestrator {
198
+ model: sonnet
199
+ handles: [start]
200
+ }
201
+ action start {
202
+ kind: inline
203
+ description: "Entry"
204
+ }
205
+ agent worker {
206
+ model: haiku
207
+ phase: 1
208
+ }
209
+ flow {
210
+ start -> worker
211
+ }
212
+ }
213
+ `;
214
+ // ---------------------------------------------------------------------------
215
+ // Registry tests
216
+ // ---------------------------------------------------------------------------
217
+ describe("exporter registry", () => {
218
+ it("has markdown, mermaid, and json exporters", () => {
219
+ expect(exporters).toHaveProperty("markdown");
220
+ expect(exporters).toHaveProperty("mermaid");
221
+ expect(exporters).toHaveProperty("json");
222
+ });
223
+ it("each exporter has required fields", () => {
224
+ for (const exp of Object.values(exporters)) {
225
+ expect(exp.name).toBeTruthy();
226
+ expect(exp.description).toBeTruthy();
227
+ expect(exp.extension).toBeTruthy();
228
+ expect(typeof exp.export).toBe("function");
229
+ }
230
+ });
231
+ });
232
+ // ---------------------------------------------------------------------------
233
+ // Markdown exporter tests
234
+ // ---------------------------------------------------------------------------
235
+ describe("markdownExporter", () => {
236
+ it("has correct metadata", () => {
237
+ expect(markdownExporter.name).toBe("markdown");
238
+ expect(markdownExporter.extension).toBe(".md");
239
+ });
240
+ it("generates a single .md file", () => {
241
+ const ast = parse(SIMPLE_PIPELINE);
242
+ const files = markdownExporter.export(ast);
243
+ expect(files).toHaveLength(1);
244
+ expect(files[0].path).toBe("simple-pipeline.md");
245
+ });
246
+ describe("header section", () => {
247
+ it("includes topology name as title", () => {
248
+ const ast = parse(SIMPLE_PIPELINE);
249
+ const md = markdownExporter.export(ast)[0].content;
250
+ expect(md).toContain("# Simple Pipeline");
251
+ });
252
+ it("includes description as italic", () => {
253
+ const ast = parse(SIMPLE_PIPELINE);
254
+ const md = markdownExporter.export(ast)[0].content;
255
+ expect(md).toContain("*Research, write, and review*");
256
+ });
257
+ it("includes version badge", () => {
258
+ const ast = parse(SIMPLE_PIPELINE);
259
+ const md = markdownExporter.export(ast)[0].content;
260
+ expect(md).toContain("version-1.0.0");
261
+ });
262
+ it("includes pattern badges", () => {
263
+ const ast = parse(SIMPLE_PIPELINE);
264
+ const md = markdownExporter.export(ast)[0].content;
265
+ expect(md).toContain("pattern-pipeline");
266
+ expect(md).toContain("pattern-human-gate");
267
+ });
268
+ });
269
+ describe("overview section", () => {
270
+ it("shows component counts inline", () => {
271
+ const ast = parse(SIMPLE_PIPELINE);
272
+ const md = markdownExporter.export(ast)[0].content;
273
+ expect(md).toContain("At a Glance");
274
+ expect(md).toContain("**3** Agents");
275
+ expect(md).toContain("**2** Actions");
276
+ expect(md).toContain("**1** Gate");
277
+ });
278
+ });
279
+ describe("orchestrator section", () => {
280
+ it("renders orchestrator with model", () => {
281
+ const ast = parse(SIMPLE_PIPELINE);
282
+ const md = markdownExporter.export(ast)[0].content;
283
+ expect(md).toContain("### Orchestrator");
284
+ expect(md).toContain("Model: `sonnet`");
285
+ expect(md).toContain("`intake`");
286
+ });
287
+ it("renders orchestrator outputs", () => {
288
+ const ast = parse(SIMPLE_PIPELINE);
289
+ const md = markdownExporter.export(ast)[0].content;
290
+ expect(md).toContain("`topic-type`");
291
+ expect(md).toContain("`technical`");
292
+ expect(md).toContain("`creative`");
293
+ });
294
+ });
295
+ describe("agents section", () => {
296
+ it("renders each agent", () => {
297
+ const ast = parse(SIMPLE_PIPELINE);
298
+ const md = markdownExporter.export(ast)[0].content;
299
+ expect(md).toContain("#### Researcher");
300
+ expect(md).toContain("#### Writer");
301
+ expect(md).toContain("#### Reviewer");
302
+ });
303
+ it("shows agent model inline with heading", () => {
304
+ const ast = parse(SIMPLE_PIPELINE);
305
+ const md = markdownExporter.export(ast)[0].content;
306
+ expect(md).toContain("#### Researcher `gpt-4o`");
307
+ });
308
+ it("shows agent phase", () => {
309
+ const ast = parse(SIMPLE_PIPELINE);
310
+ const md = markdownExporter.export(ast)[0].content;
311
+ expect(md).toContain("Phase **1**");
312
+ expect(md).toContain("Phase **2**");
313
+ expect(md).toContain("Phase **3**");
314
+ });
315
+ it("shows agent tools", () => {
316
+ const ast = parse(SIMPLE_PIPELINE);
317
+ const md = markdownExporter.export(ast)[0].content;
318
+ expect(md).toContain("`Read`");
319
+ expect(md).toContain("`WebSearch`");
320
+ });
321
+ it("shows reads and writes", () => {
322
+ const ast = parse(SIMPLE_PIPELINE);
323
+ const md = markdownExporter.export(ast)[0].content;
324
+ expect(md).toContain("`workspace/research.md`");
325
+ expect(md).toContain("`workspace/draft.md`");
326
+ });
327
+ it("shows agent outputs", () => {
328
+ const ast = parse(SIMPLE_PIPELINE);
329
+ const md = markdownExporter.export(ast)[0].content;
330
+ expect(md).toContain("`confidence`");
331
+ expect(md).toContain("`high`");
332
+ });
333
+ it("shows inline prompt in collapsible details", () => {
334
+ const ast = parse(SIMPLE_PIPELINE);
335
+ const md = markdownExporter.export(ast)[0].content;
336
+ expect(md).toContain("<details>");
337
+ expect(md).toContain("<summary>Prompt</summary>");
338
+ expect(md).toContain("You are a research specialist.");
339
+ expect(md).toContain("</details>");
340
+ });
341
+ it("renders role description from roles block", () => {
342
+ const ast = parse(SIMPLE_PIPELINE);
343
+ const md = markdownExporter.export(ast)[0].content;
344
+ expect(md).toContain("Gather information and compile research notes");
345
+ });
346
+ it("shows advisory behavior", () => {
347
+ const ast = parse(FAN_OUT_TOPOLOGY);
348
+ const md = markdownExporter.export(ast)[0].content;
349
+ expect(md).toContain("Behavior: `advisory`");
350
+ });
351
+ });
352
+ describe("actions section", () => {
353
+ it("renders actions with kind and description", () => {
354
+ const ast = parse(SIMPLE_PIPELINE);
355
+ const md = markdownExporter.export(ast)[0].content;
356
+ expect(md).toContain("#### Intake `inline`");
357
+ expect(md).toContain("Parse user request");
358
+ });
359
+ it("shows action source", () => {
360
+ const ast = parse(FAN_OUT_TOPOLOGY);
361
+ const md = markdownExporter.export(ast)[0].content;
362
+ expect(md).toContain("Source: `github-pr`");
363
+ });
364
+ });
365
+ describe("gates section", () => {
366
+ it("renders gates with checks", () => {
367
+ const ast = parse(SIMPLE_PIPELINE);
368
+ const md = markdownExporter.export(ast)[0].content;
369
+ expect(md).toContain("#### Quality Check");
370
+ expect(md).toContain("After: `writer`");
371
+ expect(md).toContain("Before: `reviewer`");
372
+ expect(md).toContain("`grammar`");
373
+ expect(md).toContain("`formatting`");
374
+ });
375
+ });
376
+ describe("flow section", () => {
377
+ it("renders flow as code block", () => {
378
+ const ast = parse(SIMPLE_PIPELINE);
379
+ const md = markdownExporter.export(ast)[0].content;
380
+ expect(md).toContain("## Flow");
381
+ expect(md).toContain("intake ───> researcher");
382
+ expect(md).toContain("researcher ───> writer");
383
+ });
384
+ it("shows edge conditions", () => {
385
+ const ast = parse(SIMPLE_PIPELINE);
386
+ const md = markdownExporter.export(ast)[0].content;
387
+ expect(md).toContain("when reviewer.verdict == revise");
388
+ expect(md).toContain("max 2");
389
+ });
390
+ it("shows edge conditions for approve", () => {
391
+ const ast = parse(SIMPLE_PIPELINE);
392
+ const md = markdownExporter.export(ast)[0].content;
393
+ expect(md).toContain("when reviewer.verdict == approve");
394
+ });
395
+ });
396
+ describe("roles section", () => {
397
+ it("renders roles as list", () => {
398
+ const ast = parse(SIMPLE_PIPELINE);
399
+ const md = markdownExporter.export(ast)[0].content;
400
+ expect(md).toContain("## Roles");
401
+ expect(md).toContain("**Researcher**");
402
+ expect(md).toContain("**Writer**");
403
+ expect(md).toContain("**Reviewer**");
404
+ });
405
+ });
406
+ describe("memory section", () => {
407
+ it("renders memory blocks", () => {
408
+ const ast = parse(SIMPLE_PIPELINE);
409
+ const md = markdownExporter.export(ast)[0].content;
410
+ expect(md).toContain("## Memory");
411
+ expect(md).toContain("**Workspace**");
412
+ expect(md).toContain("`workspace/`");
413
+ });
414
+ });
415
+ describe("triggers section", () => {
416
+ it("renders triggers as list", () => {
417
+ const ast = parse(SIMPLE_PIPELINE);
418
+ const md = markdownExporter.export(ast)[0].content;
419
+ expect(md).toContain("## Triggers");
420
+ expect(md).toContain("`create`");
421
+ expect(md).toContain("`/create <TOPIC>`");
422
+ expect(md).toContain("`TOPIC`");
423
+ });
424
+ });
425
+ describe("hooks section", () => {
426
+ it("renders hooks as list", () => {
427
+ const ast = parse(FAN_OUT_TOPOLOGY);
428
+ const md = markdownExporter.export(ast)[0].content;
429
+ expect(md).toContain("## Hooks");
430
+ expect(md).toContain("`log-findings`");
431
+ expect(md).toContain("`AgentStop`");
432
+ });
433
+ });
434
+ describe("settings section", () => {
435
+ it("renders allow/deny lists", () => {
436
+ const ast = parse(SIMPLE_PIPELINE);
437
+ const md = markdownExporter.export(ast)[0].content;
438
+ expect(md).toContain("## Settings");
439
+ expect(md).toContain("**Allow:**");
440
+ });
441
+ });
442
+ describe("footer", () => {
443
+ it("includes AgenTopology link", () => {
444
+ const ast = parse(SIMPLE_PIPELINE);
445
+ const md = markdownExporter.export(ast)[0].content;
446
+ expect(md).toContain("agentopology.com");
447
+ });
448
+ });
449
+ describe("minimal topology", () => {
450
+ it("works with minimal input", () => {
451
+ const ast = parse(MINIMAL_TOPOLOGY);
452
+ const files = markdownExporter.export(ast);
453
+ expect(files).toHaveLength(1);
454
+ expect(files[0].path).toBe("minimal.md");
455
+ const md = files[0].content;
456
+ expect(md).toContain("# Minimal");
457
+ expect(md).toContain("#### Worker");
458
+ });
459
+ });
460
+ });
461
+ // ---------------------------------------------------------------------------
462
+ // Mermaid exporter tests
463
+ // ---------------------------------------------------------------------------
464
+ describe("mermaidExporter", () => {
465
+ it("has correct metadata", () => {
466
+ expect(mermaidExporter.name).toBe("mermaid");
467
+ expect(mermaidExporter.extension).toBe(".mmd");
468
+ });
469
+ it("generates a single .mmd file", () => {
470
+ const ast = parse(SIMPLE_PIPELINE);
471
+ const files = mermaidExporter.export(ast);
472
+ expect(files).toHaveLength(1);
473
+ expect(files[0].path).toBe("simple-pipeline.mmd");
474
+ });
475
+ describe("header", () => {
476
+ it("includes topology name as comment", () => {
477
+ const ast = parse(SIMPLE_PIPELINE);
478
+ const mmd = mermaidExporter.export(ast)[0].content;
479
+ expect(mmd).toContain("%% simple-pipeline v1.0.0");
480
+ });
481
+ it("includes description as comment", () => {
482
+ const ast = parse(SIMPLE_PIPELINE);
483
+ const mmd = mermaidExporter.export(ast)[0].content;
484
+ expect(mmd).toContain("%% Research, write, and review");
485
+ });
486
+ it("starts with flowchart TD", () => {
487
+ const ast = parse(SIMPLE_PIPELINE);
488
+ const mmd = mermaidExporter.export(ast)[0].content;
489
+ expect(mmd).toContain("flowchart TD");
490
+ });
491
+ });
492
+ describe("style classes", () => {
493
+ it("defines all node type classes", () => {
494
+ const ast = parse(SIMPLE_PIPELINE);
495
+ const mmd = mermaidExporter.export(ast)[0].content;
496
+ expect(mmd).toContain("classDef orchestrator");
497
+ expect(mmd).toContain("classDef agent");
498
+ expect(mmd).toContain("classDef action");
499
+ expect(mmd).toContain("classDef gate");
500
+ expect(mmd).toContain("classDef human");
501
+ expect(mmd).toContain("classDef group");
502
+ });
503
+ });
504
+ describe("node shapes", () => {
505
+ it("renders orchestrator as stadium shape", () => {
506
+ const ast = parse(SIMPLE_PIPELINE);
507
+ const mmd = mermaidExporter.export(ast)[0].content;
508
+ expect(mmd).toMatch(/orchestrator\(\[".*"\]\)/);
509
+ });
510
+ it("renders actions as subroutine shape", () => {
511
+ const ast = parse(SIMPLE_PIPELINE);
512
+ const mmd = mermaidExporter.export(ast)[0].content;
513
+ expect(mmd).toMatch(/intake\[\[".*"\]\]/);
514
+ });
515
+ it("renders gates as diamond shape", () => {
516
+ const ast = parse(SIMPLE_PIPELINE);
517
+ const mmd = mermaidExporter.export(ast)[0].content;
518
+ expect(mmd).toMatch(/quality_check\{".*"\}/);
519
+ });
520
+ it("renders agents as rectangles", () => {
521
+ const ast = parse(SIMPLE_PIPELINE);
522
+ const mmd = mermaidExporter.export(ast)[0].content;
523
+ expect(mmd).toMatch(/researcher\[".*"\]/);
524
+ });
525
+ });
526
+ describe("node labels", () => {
527
+ it("includes agent model in label", () => {
528
+ const ast = parse(SIMPLE_PIPELINE);
529
+ const mmd = mermaidExporter.export(ast)[0].content;
530
+ expect(mmd).toContain("Model: gpt-4o");
531
+ expect(mmd).toContain("Model: sonnet");
532
+ expect(mmd).toContain("Model: opus");
533
+ });
534
+ it("includes agent phase in label", () => {
535
+ const ast = parse(SIMPLE_PIPELINE);
536
+ const mmd = mermaidExporter.export(ast)[0].content;
537
+ expect(mmd).toContain("Phase: 1");
538
+ expect(mmd).toContain("Phase: 2");
539
+ expect(mmd).toContain("Phase: 3");
540
+ });
541
+ });
542
+ describe("phase subgraphs", () => {
543
+ it("groups agents by phase", () => {
544
+ const ast = parse(SIMPLE_PIPELINE);
545
+ const mmd = mermaidExporter.export(ast)[0].content;
546
+ expect(mmd).toContain('subgraph Phase_1["Phase 1"]');
547
+ expect(mmd).toContain('subgraph Phase_2["Phase 2"]');
548
+ expect(mmd).toContain('subgraph Phase_3["Phase 3"]');
549
+ });
550
+ it("groups same-phase agents together", () => {
551
+ const ast = parse(FAN_OUT_TOPOLOGY);
552
+ const mmd = mermaidExporter.export(ast)[0].content;
553
+ expect(mmd).toContain('subgraph Phase_1["Phase 1"]');
554
+ const phase1Start = mmd.indexOf('subgraph Phase_1');
555
+ const phase1End = mmd.indexOf("end", phase1Start);
556
+ const phase1Content = mmd.slice(phase1Start, phase1End);
557
+ expect(phase1Content).toContain("analyzer");
558
+ expect(phase1Content).toContain("security_scanner");
559
+ });
560
+ });
561
+ describe("edges", () => {
562
+ it("renders simple edges", () => {
563
+ const ast = parse(SIMPLE_PIPELINE);
564
+ const mmd = mermaidExporter.export(ast)[0].content;
565
+ expect(mmd).toContain("intake --> researcher");
566
+ expect(mmd).toContain("researcher --> writer");
567
+ });
568
+ it("renders conditional edges with annotations", () => {
569
+ const ast = parse(SIMPLE_PIPELINE);
570
+ const mmd = mermaidExporter.export(ast)[0].content;
571
+ expect(mmd).toContain('reviewer -->|"reviewer.verdict == revise, max 2"| writer');
572
+ });
573
+ it("renders edges with only condition", () => {
574
+ const ast = parse(SIMPLE_PIPELINE);
575
+ const mmd = mermaidExporter.export(ast)[0].content;
576
+ expect(mmd).toContain('reviewer -->|"reviewer.verdict == approve"| done');
577
+ });
578
+ });
579
+ describe("hyphenated IDs", () => {
580
+ it("converts hyphens to underscores in IDs", () => {
581
+ const ast = parse(FAN_OUT_TOPOLOGY);
582
+ const mmd = mermaidExporter.export(ast)[0].content;
583
+ expect(mmd).toContain("security_scanner");
584
+ expect(mmd).toContain("create_report");
585
+ expect(mmd).not.toMatch(/\bsecurity-scanner\b.*\[/);
586
+ });
587
+ });
588
+ describe("class assignments", () => {
589
+ it("assigns correct classes to nodes", () => {
590
+ const ast = parse(SIMPLE_PIPELINE);
591
+ const mmd = mermaidExporter.export(ast)[0].content;
592
+ expect(mmd).toContain(":::orchestrator");
593
+ expect(mmd).toContain(":::agent");
594
+ expect(mmd).toContain(":::action");
595
+ expect(mmd).toContain(":::gate");
596
+ });
597
+ });
598
+ describe("minimal topology", () => {
599
+ it("works with minimal input", () => {
600
+ const ast = parse(MINIMAL_TOPOLOGY);
601
+ const files = mermaidExporter.export(ast);
602
+ expect(files).toHaveLength(1);
603
+ expect(files[0].path).toBe("minimal.mmd");
604
+ const mmd = files[0].content;
605
+ expect(mmd).toContain("flowchart TD");
606
+ expect(mmd).toContain("start --> worker");
607
+ });
608
+ });
609
+ });
610
+ // ---------------------------------------------------------------------------
611
+ // Cross-exporter tests
612
+ // ---------------------------------------------------------------------------
613
+ describe("cross-exporter", () => {
614
+ const topologies = [
615
+ { name: "simple-pipeline", source: SIMPLE_PIPELINE },
616
+ { name: "fan-out", source: FAN_OUT_TOPOLOGY },
617
+ { name: "minimal", source: MINIMAL_TOPOLOGY },
618
+ ];
619
+ for (const { name, source } of topologies) {
620
+ it(`both exporters produce non-empty output for ${name}`, () => {
621
+ const ast = parse(source);
622
+ const mdFiles = markdownExporter.export(ast);
623
+ const mmdFiles = mermaidExporter.export(ast);
624
+ expect(mdFiles.length).toBeGreaterThan(0);
625
+ expect(mmdFiles.length).toBeGreaterThan(0);
626
+ for (const file of [...mdFiles, ...mmdFiles]) {
627
+ expect(file.content.length).toBeGreaterThan(0);
628
+ expect(file.path.length).toBeGreaterThan(0);
629
+ }
630
+ });
631
+ }
632
+ it("all nodes appear in both export formats", () => {
633
+ const ast = parse(SIMPLE_PIPELINE);
634
+ const md = markdownExporter.export(ast)[0].content;
635
+ const mmd = mermaidExporter.export(ast)[0].content;
636
+ for (const node of ast.nodes) {
637
+ expect(md.toLowerCase()).toContain(node.id.replace(/-/g, " "));
638
+ expect(mmd).toContain(node.id.replace(/-/g, "_"));
639
+ }
640
+ });
641
+ it("all edges appear in both export formats", () => {
642
+ const ast = parse(SIMPLE_PIPELINE);
643
+ const md = markdownExporter.export(ast)[0].content;
644
+ const mmd = mermaidExporter.export(ast)[0].content;
645
+ for (const edge of ast.edges) {
646
+ expect(md).toContain(edge.from);
647
+ expect(md).toContain(edge.to);
648
+ expect(mmd).toContain(edge.from.replace(/-/g, "_"));
649
+ expect(mmd).toContain(edge.to.replace(/-/g, "_"));
650
+ }
651
+ });
652
+ });
653
+ // ---------------------------------------------------------------------------
654
+ // JSON exporter tests
655
+ // ---------------------------------------------------------------------------
656
+ describe("jsonExporter", () => {
657
+ it("has correct metadata", () => {
658
+ expect(jsonExporter.name).toBe("json");
659
+ expect(jsonExporter.extension).toBe(".json");
660
+ });
661
+ it("produces a single .json file", () => {
662
+ const ast = parse(SIMPLE_PIPELINE);
663
+ const files = jsonExporter.export(ast);
664
+ expect(files).toHaveLength(1);
665
+ expect(files[0].path).toBe("simple-pipeline.json");
666
+ });
667
+ it("produces valid JSON", () => {
668
+ const ast = parse(SIMPLE_PIPELINE);
669
+ const content = jsonExporter.export(ast)[0].content;
670
+ const parsed = JSON.parse(content);
671
+ expect(parsed).toBeDefined();
672
+ expect(parsed.topology.name).toBe("simple-pipeline");
673
+ });
674
+ it("preserves topology fields", () => {
675
+ const ast = parse(SIMPLE_PIPELINE);
676
+ const parsed = JSON.parse(jsonExporter.export(ast)[0].content);
677
+ expect(parsed.topology.version).toBe("1.0.0");
678
+ expect(parsed.topology.description).toBe("Research, write, and review");
679
+ expect(parsed.topology.patterns).toContain("pipeline");
680
+ });
681
+ it("includes nodes", () => {
682
+ const ast = parse(SIMPLE_PIPELINE);
683
+ const parsed = JSON.parse(jsonExporter.export(ast)[0].content);
684
+ const nodeIds = parsed.nodes.map((n) => n.id);
685
+ expect(nodeIds).toContain("researcher");
686
+ expect(nodeIds).toContain("writer");
687
+ expect(nodeIds).toContain("reviewer");
688
+ });
689
+ it("includes edges", () => {
690
+ const ast = parse(SIMPLE_PIPELINE);
691
+ const parsed = JSON.parse(jsonExporter.export(ast)[0].content);
692
+ expect(parsed.edges.length).toBeGreaterThan(0);
693
+ expect(parsed.edges[0]).toHaveProperty("from");
694
+ expect(parsed.edges[0]).toHaveProperty("to");
695
+ });
696
+ it("strips internal _-prefixed fields", () => {
697
+ const ast = parse(SIMPLE_PIPELINE);
698
+ const content = jsonExporter.export(ast)[0].content;
699
+ const parsed = JSON.parse(content);
700
+ for (const key of Object.keys(parsed)) {
701
+ expect(key).not.toMatch(/^_/);
702
+ }
703
+ });
704
+ it("works with fan-out topology", () => {
705
+ const ast = parse(FAN_OUT_TOPOLOGY);
706
+ const files = jsonExporter.export(ast);
707
+ expect(files).toHaveLength(1);
708
+ expect(files[0].path).toBe("code-review.json");
709
+ const parsed = JSON.parse(files[0].content);
710
+ expect(parsed.topology.name).toBe("code-review");
711
+ });
712
+ it("works with minimal topology", () => {
713
+ const ast = parse(MINIMAL_TOPOLOGY);
714
+ const parsed = JSON.parse(jsonExporter.export(ast)[0].content);
715
+ const agents = parsed.nodes.filter((n) => n.id === "worker");
716
+ expect(agents).toHaveLength(1);
717
+ });
718
+ it("includes settings", () => {
719
+ const ast = parse(SIMPLE_PIPELINE);
720
+ const parsed = JSON.parse(jsonExporter.export(ast)[0].content);
721
+ expect(parsed.settings.allow).toContain("Read");
722
+ expect(parsed.settings.deny).toContain("Bash(rm -rf *)");
723
+ });
724
+ it("includes gate nodes", () => {
725
+ const ast = parse(SIMPLE_PIPELINE);
726
+ const parsed = JSON.parse(jsonExporter.export(ast)[0].content);
727
+ const gates = parsed.nodes.filter((n) => n.id === "quality-check");
728
+ expect(gates).toHaveLength(1);
729
+ });
730
+ });
731
+ //# sourceMappingURL=exporters.test.js.map