@runcofactor/cli 0.10.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 (150) hide show
  1. package/README.md +125 -0
  2. package/dist/api-catalog-GFGCLVS4.js +401 -0
  3. package/dist/api-catalog-GFGCLVS4.js.map +1 -0
  4. package/dist/attachment-SJC7D7FY.js +276 -0
  5. package/dist/attachment-SJC7D7FY.js.map +1 -0
  6. package/dist/auth-EN6NQCJY.js +31 -0
  7. package/dist/auth-EN6NQCJY.js.map +1 -0
  8. package/dist/bin.js +7 -0
  9. package/dist/bin.js.map +1 -0
  10. package/dist/chunk-3CZU7XCS.js +272 -0
  11. package/dist/chunk-3CZU7XCS.js.map +1 -0
  12. package/dist/chunk-4NRDONSC.js +1048 -0
  13. package/dist/chunk-4NRDONSC.js.map +1 -0
  14. package/dist/chunk-4PNOV7OV.js +51980 -0
  15. package/dist/chunk-4PNOV7OV.js.map +1 -0
  16. package/dist/chunk-52IO7ACJ.js +2941 -0
  17. package/dist/chunk-52IO7ACJ.js.map +1 -0
  18. package/dist/chunk-6LEZCKKV.js +663 -0
  19. package/dist/chunk-6LEZCKKV.js.map +1 -0
  20. package/dist/chunk-A4OFTUVV.js +170 -0
  21. package/dist/chunk-A4OFTUVV.js.map +1 -0
  22. package/dist/chunk-B2VISN2V.js +2459 -0
  23. package/dist/chunk-B2VISN2V.js.map +1 -0
  24. package/dist/chunk-CLAWT7BJ.js +271 -0
  25. package/dist/chunk-CLAWT7BJ.js.map +1 -0
  26. package/dist/chunk-GKY2KXO5.js +281 -0
  27. package/dist/chunk-GKY2KXO5.js.map +1 -0
  28. package/dist/chunk-I4QLI3JO.js +413 -0
  29. package/dist/chunk-I4QLI3JO.js.map +1 -0
  30. package/dist/chunk-IMPZAX6L.js +180 -0
  31. package/dist/chunk-IMPZAX6L.js.map +1 -0
  32. package/dist/chunk-J6SH7GQT.js +241 -0
  33. package/dist/chunk-J6SH7GQT.js.map +1 -0
  34. package/dist/chunk-JDBCZ3Z2.js +298 -0
  35. package/dist/chunk-JDBCZ3Z2.js.map +1 -0
  36. package/dist/chunk-JICSYFRJ.js +46 -0
  37. package/dist/chunk-JICSYFRJ.js.map +1 -0
  38. package/dist/chunk-JWHYXEA2.js +32 -0
  39. package/dist/chunk-JWHYXEA2.js.map +1 -0
  40. package/dist/chunk-N7HFALX3.js +88 -0
  41. package/dist/chunk-N7HFALX3.js.map +1 -0
  42. package/dist/chunk-Q4BO4Z5V.js +178 -0
  43. package/dist/chunk-Q4BO4Z5V.js.map +1 -0
  44. package/dist/chunk-QFDOWABX.js +195 -0
  45. package/dist/chunk-QFDOWABX.js.map +1 -0
  46. package/dist/chunk-SJJW77WN.js +605 -0
  47. package/dist/chunk-SJJW77WN.js.map +1 -0
  48. package/dist/chunk-VKVL7WBN.js +50 -0
  49. package/dist/chunk-VKVL7WBN.js.map +1 -0
  50. package/dist/chunk-WOHL4FTB.js +353 -0
  51. package/dist/chunk-WOHL4FTB.js.map +1 -0
  52. package/dist/chunk-XQXVG7RA.js +426 -0
  53. package/dist/chunk-XQXVG7RA.js.map +1 -0
  54. package/dist/chunk-ZD3KHESI.js +94 -0
  55. package/dist/chunk-ZD3KHESI.js.map +1 -0
  56. package/dist/credential-COLTBV6D.js +450 -0
  57. package/dist/credential-COLTBV6D.js.map +1 -0
  58. package/dist/credential-template-EEPYFG7R.js +291 -0
  59. package/dist/credential-template-EEPYFG7R.js.map +1 -0
  60. package/dist/debug-workflow-KOVH5GOP.js +259 -0
  61. package/dist/debug-workflow-KOVH5GOP.js.map +1 -0
  62. package/dist/dist-44PZR7BQ.js +423 -0
  63. package/dist/dist-44PZR7BQ.js.map +1 -0
  64. package/dist/docs-3SSORHB5.js +366 -0
  65. package/dist/docs-3SSORHB5.js.map +1 -0
  66. package/dist/doctor-OTSIWV6G.js +132 -0
  67. package/dist/doctor-OTSIWV6G.js.map +1 -0
  68. package/dist/feature-flag-T2LRZME3.js +283 -0
  69. package/dist/feature-flag-T2LRZME3.js.map +1 -0
  70. package/dist/file-type-S3UUBVCT.js +4205 -0
  71. package/dist/file-type-S3UUBVCT.js.map +1 -0
  72. package/dist/index.d.ts +3 -0
  73. package/dist/index.js +982 -0
  74. package/dist/index.js.map +1 -0
  75. package/dist/init-TGWRECT2.js +208 -0
  76. package/dist/init-TGWRECT2.js.map +1 -0
  77. package/dist/install-DXSNWZKO.js +295 -0
  78. package/dist/install-DXSNWZKO.js.map +1 -0
  79. package/dist/issues-XIZOPX6T.js +564 -0
  80. package/dist/issues-XIZOPX6T.js.map +1 -0
  81. package/dist/knobs-NBINZIUB.js +295 -0
  82. package/dist/knobs-NBINZIUB.js.map +1 -0
  83. package/dist/list-3TDHK637.js +59 -0
  84. package/dist/list-3TDHK637.js.map +1 -0
  85. package/dist/local-HICVD6PU.js +100 -0
  86. package/dist/local-HICVD6PU.js.map +1 -0
  87. package/dist/login-UOIUXVUK.js +315 -0
  88. package/dist/login-UOIUXVUK.js.map +1 -0
  89. package/dist/mcp-54IJEOUA.js +1956 -0
  90. package/dist/mcp-54IJEOUA.js.map +1 -0
  91. package/dist/pdf-extract-ABWV3L5X.js +13 -0
  92. package/dist/pdf-extract-ABWV3L5X.js.map +1 -0
  93. package/dist/pdfjs-DQ2YA5S3.js +41561 -0
  94. package/dist/pdfjs-DQ2YA5S3.js.map +1 -0
  95. package/dist/property-UXLOLPTD.js +701 -0
  96. package/dist/property-UXLOLPTD.js.map +1 -0
  97. package/dist/property-skill-guide.md +189 -0
  98. package/dist/psql-BYUUNLGG.js +115 -0
  99. package/dist/psql-BYUUNLGG.js.map +1 -0
  100. package/dist/record-4TR2UIIS.js +233 -0
  101. package/dist/record-4TR2UIIS.js.map +1 -0
  102. package/dist/redis-35VBUYW7.js +116 -0
  103. package/dist/redis-35VBUYW7.js.map +1 -0
  104. package/dist/release-OOLTUBUT.js +354 -0
  105. package/dist/release-OOLTUBUT.js.map +1 -0
  106. package/dist/sandbox-XGSUU6G7.js +2046 -0
  107. package/dist/sandbox-XGSUU6G7.js.map +1 -0
  108. package/dist/scenarios-GWLJEGHA.js +280 -0
  109. package/dist/scenarios-GWLJEGHA.js.map +1 -0
  110. package/dist/session-MYFNZPRP.js +2414 -0
  111. package/dist/session-MYFNZPRP.js.map +1 -0
  112. package/dist/skill-KM34LDMO.js +1839 -0
  113. package/dist/skill-KM34LDMO.js.map +1 -0
  114. package/dist/skills/debug-run/SKILL.md +262 -0
  115. package/dist/skills/debug-run/history/1.0.0.md +244 -0
  116. package/dist/skills/debug-run/history/1.0.1.md +245 -0
  117. package/dist/skills/debug-run/history/1.0.2.md +254 -0
  118. package/dist/skills/debug-run/history/1.1.0.md +262 -0
  119. package/dist/skills/debug-run/version.json +3 -0
  120. package/dist/spec-RA3CFEKG.js +951 -0
  121. package/dist/spec-RA3CFEKG.js.map +1 -0
  122. package/dist/src-LB4HBTJQ.js +1516 -0
  123. package/dist/src-LB4HBTJQ.js.map +1 -0
  124. package/dist/src-N3TPKGTY.js +244 -0
  125. package/dist/src-N3TPKGTY.js.map +1 -0
  126. package/dist/src-UVDX5UP6.js +17 -0
  127. package/dist/src-UVDX5UP6.js.map +1 -0
  128. package/dist/start-PSEQSQA3.js +151 -0
  129. package/dist/start-PSEQSQA3.js.map +1 -0
  130. package/dist/tag-YY6YAABQ.js +22 -0
  131. package/dist/tag-YY6YAABQ.js.map +1 -0
  132. package/dist/task-IQC3YR5K.js +922 -0
  133. package/dist/task-IQC3YR5K.js.map +1 -0
  134. package/dist/test-suite-DZNJFXDW.js +394 -0
  135. package/dist/test-suite-DZNJFXDW.js.map +1 -0
  136. package/dist/trace-HXIW7U2D.js +14 -0
  137. package/dist/trace-HXIW7U2D.js.map +1 -0
  138. package/dist/triage-ORG45R5W.js +16 -0
  139. package/dist/triage-ORG45R5W.js.map +1 -0
  140. package/dist/upgrade-L2VYMP2V.js +61 -0
  141. package/dist/upgrade-L2VYMP2V.js.map +1 -0
  142. package/dist/workflow-XYFH5LTP.js +3708 -0
  143. package/dist/workflow-XYFH5LTP.js.map +1 -0
  144. package/dist/workflow-cache-NQJQFWMV.js +253 -0
  145. package/dist/workflow-cache-NQJQFWMV.js.map +1 -0
  146. package/dist/workflow-debug-BGD4UIXU.js +927 -0
  147. package/dist/workflow-debug-BGD4UIXU.js.map +1 -0
  148. package/dist/workflow-proposals-MTUDX46O.js +382 -0
  149. package/dist/workflow-proposals-MTUDX46O.js.map +1 -0
  150. package/package.json +61 -0
@@ -0,0 +1,2459 @@
1
+ import { createRequire as __cr } from "module"; const require = __cr(import.meta.url);
2
+ import {
3
+ InlineAssertionSchema
4
+ } from "./chunk-SJJW77WN.js";
5
+
6
+ // ../canary-spec/src/parse.ts
7
+ import yaml from "yaml";
8
+
9
+ // ../canary-spec/src/grammar.ts
10
+ import { z } from "zod";
11
+ var CredentialRefSchema = z.union([
12
+ z.object({ kind: z.literal("template"), role: z.string().min(1) }),
13
+ z.object({ kind: z.literal("credential"), name: z.string().min(1) }),
14
+ /**
15
+ * `seed-output <ref>` — the journey runs as a credential that was provisioned
16
+ * by an upstream seed workflow. The `ref` is the seed's `dataKey`; the
17
+ * runtime resolves it at suite-prep time via the seed_outputs lookup.
18
+ */
19
+ z.object({ kind: z.literal("seedOutput"), ref: z.string().min(1) })
20
+ ]);
21
+ var FrontmatterSchema = z.object({
22
+ name: z.string().min(1),
23
+ /**
24
+ * Workflow ID this spec is bound to. Absent on first compile; the
25
+ * compiler writes it back into the frontmatter after a successful
26
+ * create. On recompile, the compiler keys the diff on this ID.
27
+ */
28
+ workflowId: z.string().uuid().optional(),
29
+ status: z.enum(["draft", "published"]).optional(),
30
+ flowType: z.enum(["standard", "setup", "seed", "teardown"]).optional(),
31
+ /**
32
+ * Credential the journey runs as. Required for authenticated specs (the
33
+ * common case); optional for unauthenticated flows, which have no leading
34
+ * login node to represent.
35
+ */
36
+ actor: CredentialRefSchema.optional(),
37
+ authenticated: z.boolean().default(true),
38
+ tags: z.array(z.string()).optional(),
39
+ requirementRef: z.string().optional(),
40
+ apiOnly: z.boolean().optional(),
41
+ sandboxTemplate: z.string().optional(),
42
+ environments: z.union([
43
+ z.object({ only: z.array(z.string()).min(1) }),
44
+ z.object({ types: z.array(z.enum(["permanent", "ephemeral", "local"])).min(1) })
45
+ ]).optional(),
46
+ /**
47
+ * Teardown linkage for `flowType: seed` workflows. Two surface forms:
48
+ * teardown: cleanup-demo-org # shorthand, defaults applied
49
+ * teardown:
50
+ * spec: cleanup-demo-org # nested for explicit overrides
51
+ * snapshotVersion: 3 # or 'latest' (default)
52
+ * enabled: false # default true
53
+ * The DB stores teardownWorkflowId/teardownSnapshotVersion/teardownEnabled
54
+ * on the seed row; compile resolves spec name → id and PATCHes the seed,
55
+ * pull reverses (FK → name).
56
+ */
57
+ teardown: z.union([
58
+ z.string().min(1),
59
+ z.object({
60
+ spec: z.string().min(1),
61
+ snapshotVersion: z.union([z.literal("latest"), z.number().int().nonnegative()]).optional(),
62
+ enabled: z.boolean().optional()
63
+ })
64
+ ]).optional()
65
+ }).strict();
66
+ var InputDeclSchema = z.object({
67
+ name: z.string().min(1),
68
+ kind: z.enum(["value", "credential"]).default("value"),
69
+ defaultValue: z.string()
70
+ });
71
+ var ScenarioSchema = z.object({
72
+ name: z.string().min(1),
73
+ description: z.string().optional(),
74
+ isDefault: z.boolean().default(false),
75
+ includeInSuite: z.boolean().default(true),
76
+ skipReason: z.string().optional(),
77
+ values: z.record(z.string(), z.string()).default({}),
78
+ credentials: z.record(z.string(), z.string()).default({})
79
+ });
80
+ var AttachmentDeclSchema = z.object({
81
+ ref: z.string().min(1),
82
+ source: z.string().min(1)
83
+ // path or attachmentId
84
+ });
85
+ var DeclaredOutputSchema = z.object({
86
+ dataKey: z.string().min(1),
87
+ label: z.string().min(1).optional(),
88
+ description: z.string().optional(),
89
+ propertyName: z.string().optional()
90
+ });
91
+ var NodeBase = z.object({
92
+ description: z.string().optional(),
93
+ /**
94
+ * Workflow node UUID — present after first compile, when the compiler
95
+ * writes `<!-- @id <uuid> -->` next to the node's lead-in. Drives the
96
+ * recompile diff.
97
+ */
98
+ nodeId: z.string().uuid().optional(),
99
+ /**
100
+ * Source line index of the lead-in. Used by the writeback step to
101
+ * insert `<!-- @id <uuid> -->` after a CREATE without re-finding the
102
+ * line. Populated by the parser; not user-authored.
103
+ */
104
+ sourceLine: z.number().int().nonnegative().optional()
105
+ });
106
+ var PageUrlSchema = z.string().min(1).refine(
107
+ (s) => {
108
+ if (s.startsWith("/")) return true;
109
+ try {
110
+ const u = new URL(s);
111
+ return u.protocol === "http:" || u.protocol === "https:";
112
+ } catch {
113
+ return false;
114
+ }
115
+ },
116
+ {
117
+ message: "pageUrl must be a root-relative path (e.g. /dashboard) or an absolute http(s) URL"
118
+ }
119
+ );
120
+ var NavigateNodeSchema = NodeBase.extend({
121
+ kind: z.literal("navigate"),
122
+ pageUrl: PageUrlSchema,
123
+ openInNewTab: z.boolean().optional()
124
+ });
125
+ var SwitchRoleNodeSchema = NodeBase.extend({
126
+ kind: z.literal("switchRole"),
127
+ credentialRef: CredentialRefSchema
128
+ });
129
+ var ActionFieldSchema = z.object({
130
+ name: z.string().min(1),
131
+ type: z.string().optional(),
132
+ value: z.string()
133
+ });
134
+ var ActionNodeSchema = NodeBase.extend({
135
+ kind: z.literal("action"),
136
+ title: z.string().min(1),
137
+ fields: z.array(ActionFieldSchema).default([]),
138
+ capture: z.array(z.string()).default([]),
139
+ assertions: z.array(InlineAssertionSchema).default([]),
140
+ captureAudio: z.boolean().optional(),
141
+ captureLabel: z.string().optional(),
142
+ captureExtraMs: z.number().int().nonnegative().optional(),
143
+ alwaysUseAgent: z.boolean().optional()
144
+ });
145
+ var AssertionNodeSchema = NodeBase.extend({
146
+ kind: z.literal("assertion"),
147
+ condition: z.string().min(1),
148
+ severity: z.enum(["warn", "fail"]).default("warn"),
149
+ expectedOutcome: z.string().optional(),
150
+ alwaysUseAgent: z.boolean().optional()
151
+ });
152
+ var SeedNodeSchema = NodeBase.extend({
153
+ kind: z.literal("seed"),
154
+ specName: z.string().min(1),
155
+ version: z.union([z.number().int().nonnegative(), z.literal("latest")]).optional(),
156
+ onFailure: z.enum(["block", "warn"]).optional(),
157
+ mode: z.enum(["recommended", "api", "browser"]).optional(),
158
+ /** Per-request timeout (seconds) for the seed/teardown api_sequence calls (API mode only). */
159
+ apiTimeoutSeconds: z.number().int().positive().optional(),
160
+ scope: z.enum(["inline", "suite"]).optional(),
161
+ runTeardown: z.boolean().optional()
162
+ });
163
+ var SetupNodeSchema = NodeBase.extend({
164
+ kind: z.literal("setup"),
165
+ specName: z.string().min(1),
166
+ version: z.union([z.number().int().nonnegative(), z.literal("latest")]).optional(),
167
+ mode: z.enum(["inline", "suite"]).optional()
168
+ });
169
+ var WaitNodeSchema = NodeBase.extend({
170
+ kind: z.literal("wait"),
171
+ durationSeconds: z.number().int().positive()
172
+ });
173
+ var ConditionNodeSchema = NodeBase.extend({
174
+ kind: z.literal("condition"),
175
+ expression: z.string().min(1),
176
+ hints: z.string().optional(),
177
+ thenBranch: z.lazy(() => z.array(JourneyNodeSchema)),
178
+ elseBranch: z.lazy(() => z.array(JourneyNodeSchema))
179
+ });
180
+ var NotificationNodeSchema = NodeBase.extend({
181
+ kind: z.literal("notification"),
182
+ channel: z.string().min(1),
183
+ to: z.string().min(1),
184
+ subject: z.string().optional(),
185
+ body: z.string().min(1),
186
+ attach: z.string().optional()
187
+ });
188
+ var AudioPlayNodeSchema = NodeBase.extend({
189
+ kind: z.literal("audio"),
190
+ attachmentRef: z.string().min(1),
191
+ loop: z.boolean().optional(),
192
+ waitForCompletion: z.boolean().optional(),
193
+ label: z.string().optional()
194
+ });
195
+ var AudioAssertionNodeSchema = NodeBase.extend({
196
+ kind: z.literal("audioAssertion"),
197
+ label: z.string().min(1),
198
+ severity: z.enum(["warn", "fail"]).default("fail"),
199
+ prompt: z.string().optional(),
200
+ model: z.enum(["gpt-audio", "gemini-2.x"]).optional(),
201
+ minDurationMs: z.number().int().nonnegative().optional(),
202
+ maxDurationMs: z.number().int().nonnegative().optional(),
203
+ minRmsDb: z.number().optional(),
204
+ silenceFractionMax: z.number().min(0).max(1).optional(),
205
+ reference: z.string().optional(),
206
+ envelopeCorrelationMin: z.number().min(-1).max(1).optional()
207
+ });
208
+ var ApiSequenceNodeSchema = NodeBase.extend({
209
+ kind: z.literal("apiSequence"),
210
+ title: z.string().min(1),
211
+ apiEndpointContext: z.string().min(1),
212
+ credentialInputName: z.string().optional(),
213
+ /**
214
+ * Optional sidecar YAML carrying the full ApiSequenceConfig (steps, exports,
215
+ * validations). Path is resolved relative to the .canary.md file. When set,
216
+ * compile POSTs the loaded config to `/api-sequences/save` after the node
217
+ * sync; pull writes the file when an api_sequence row exists. Multiple
218
+ * api_sequence nodes in the same workflow share one sidecar (the DB has a
219
+ * unique index on workflow_id), so the path should typically be identical
220
+ * across nodes — the parser warns when the paths conflict.
221
+ */
222
+ sidecar: z.string().min(1).optional()
223
+ });
224
+ var EndNodeSchema = NodeBase.extend({
225
+ kind: z.literal("end"),
226
+ outcome: z.enum(["pass", "fail"]).default("pass")
227
+ });
228
+ var JourneyNodeSchema = z.union([
229
+ NavigateNodeSchema,
230
+ SwitchRoleNodeSchema,
231
+ ActionNodeSchema,
232
+ AssertionNodeSchema,
233
+ SeedNodeSchema,
234
+ SetupNodeSchema,
235
+ WaitNodeSchema,
236
+ ConditionNodeSchema,
237
+ NotificationNodeSchema,
238
+ AudioPlayNodeSchema,
239
+ AudioAssertionNodeSchema,
240
+ ApiSequenceNodeSchema,
241
+ EndNodeSchema
242
+ ]);
243
+ var SpecAstSchema = z.object({
244
+ frontmatter: FrontmatterSchema,
245
+ spirit: z.string().optional(),
246
+ preflight: z.string().optional(),
247
+ cleanup: z.string().optional(),
248
+ inputs: z.array(InputDeclSchema).default([]),
249
+ scenarios: z.array(ScenarioSchema).default([]),
250
+ attachments: z.array(AttachmentDeclSchema).default([]),
251
+ declaredOutputs: z.array(DeclaredOutputSchema).default([]),
252
+ journey: z.array(JourneyNodeSchema)
253
+ });
254
+
255
+ // ../canary-spec/src/parse.ts
256
+ var FRONTMATTER_FENCE = /^---\s*$/;
257
+ function parseSpec(source) {
258
+ const issues = [];
259
+ const lines = source.split(/\r?\n/);
260
+ const fmEnd = findFrontmatterEnd(lines);
261
+ if (fmEnd === -1) {
262
+ return fail([{ severity: "error", message: "Spec must begin with `---` frontmatter." }]);
263
+ }
264
+ const rawFrontmatter = lines.slice(1, fmEnd).join("\n");
265
+ let frontmatter;
266
+ try {
267
+ const parsed = yaml.parse(rawFrontmatter) ?? {};
268
+ frontmatter = FrontmatterSchema.parse(normalizeActor(parsed));
269
+ } catch (err) {
270
+ return fail([
271
+ {
272
+ severity: "error",
273
+ message: `Frontmatter is invalid: ${err instanceof Error ? err.message : String(err)}`,
274
+ line: 1
275
+ }
276
+ ]);
277
+ }
278
+ const bodyStart = fmEnd + 1;
279
+ const sections = splitH2Sections(lines.slice(bodyStart), bodyStart);
280
+ let spirit;
281
+ let preflight;
282
+ let cleanup;
283
+ let inputs = [];
284
+ let scenarios = [];
285
+ let attachments = [];
286
+ let declaredOutputs = [];
287
+ let journey = [];
288
+ for (const section of sections) {
289
+ const heading = section.heading.toLowerCase();
290
+ if (heading.startsWith("spirit")) spirit = stripBlockquoteOrPlain(section.body).trim() || void 0;
291
+ else if (heading.startsWith("preflight")) preflight = section.body.trim() || void 0;
292
+ else if (heading.startsWith("cleanup")) cleanup = section.body.trim() || void 0;
293
+ else if (heading.startsWith("inputs")) inputs = parseInputs(section.body, section.startLine, issues);
294
+ else if (heading.startsWith("scenarios")) scenarios = parseScenarios(section.body, section.startLine, issues);
295
+ else if (heading.startsWith("attachments")) attachments = parseAttachments(section.body, section.startLine, issues);
296
+ else if (heading.startsWith("declared outputs")) declaredOutputs = parseDeclaredOutputs(section.body, section.startLine, issues);
297
+ else if (heading.startsWith("journey")) journey = parseJourney(section.body, section.startLine, issues);
298
+ else if (heading.startsWith("metadata")) {
299
+ } else {
300
+ issues.push({
301
+ severity: "warn",
302
+ message: `Unknown H2 section "${section.heading}" \u2014 ignored.`,
303
+ line: section.startLine
304
+ });
305
+ }
306
+ }
307
+ if (journey.length === 0) {
308
+ issues.push({
309
+ severity: "error",
310
+ message: "Spec must include a `## Journey` section with at least one node."
311
+ });
312
+ }
313
+ if (issues.some((i) => i.severity === "error")) return { ok: false, issues };
314
+ return {
315
+ ok: true,
316
+ ast: {
317
+ frontmatter,
318
+ spirit,
319
+ preflight,
320
+ cleanup,
321
+ inputs,
322
+ scenarios,
323
+ attachments,
324
+ declaredOutputs,
325
+ journey
326
+ }
327
+ };
328
+ }
329
+ function fail(issues) {
330
+ return { ok: false, issues };
331
+ }
332
+ function findFrontmatterEnd(lines) {
333
+ if (!FRONTMATTER_FENCE.test(lines[0] ?? "")) return -1;
334
+ for (let i = 1; i < lines.length; i++) if (FRONTMATTER_FENCE.test(lines[i])) return i;
335
+ return -1;
336
+ }
337
+ function parseCredentialRef(raw) {
338
+ const trimmed = raw.trim();
339
+ if (trimmed.startsWith("credential ")) {
340
+ return { kind: "credential", name: trimmed.slice("credential ".length).trim() };
341
+ }
342
+ if (trimmed.startsWith("seed-output ")) {
343
+ return { kind: "seedOutput", ref: trimmed.slice("seed-output ".length).trim() };
344
+ }
345
+ const role = trimmed.startsWith("template ") ? trimmed.slice("template ".length).trim() : trimmed;
346
+ return { kind: "template", role };
347
+ }
348
+ function normalizeActor(raw) {
349
+ const a = raw.actor;
350
+ if (typeof a !== "string") return raw;
351
+ return { ...raw, actor: parseCredentialRef(a) };
352
+ }
353
+ function splitH2Sections(lines, offset) {
354
+ const sections = [];
355
+ let current = null;
356
+ for (let i = 0; i < lines.length; i++) {
357
+ const line = lines[i];
358
+ const m = /^##\s+(.+?)\s*$/.exec(line);
359
+ if (m) {
360
+ if (current) sections.push({ heading: current.heading, body: current.body.join("\n"), startLine: current.startLine });
361
+ current = { heading: m[1], body: [], startLine: offset + i + 1 };
362
+ continue;
363
+ }
364
+ if (current) current.body.push(line);
365
+ }
366
+ if (current) sections.push({ heading: current.heading, body: current.body.join("\n"), startLine: current.startLine });
367
+ return sections;
368
+ }
369
+ function stripBlockquoteOrPlain(body) {
370
+ return body.split("\n").map((l) => l.startsWith("> ") ? l.slice(2) : l.startsWith(">") ? l.slice(1) : l).join("\n");
371
+ }
372
+ function parseInputs(body, startLine, issues) {
373
+ const out = [];
374
+ for (const { line, lineNo } of bulletLines(body, startLine)) {
375
+ const credMatch = /^credential\s+([A-Za-z0-9_-]+)\s*:\s*(.+)$/.exec(line);
376
+ if (credMatch) {
377
+ out.push({ name: credMatch[1], kind: "credential", defaultValue: credMatch[2].trim() });
378
+ continue;
379
+ }
380
+ const m = /^([A-Za-z0-9_-]+)\s*:\s*(.+)$/.exec(line);
381
+ if (!m) {
382
+ issues.push({ severity: "warn", message: `Could not parse Inputs entry: "${line}"`, line: lineNo });
383
+ continue;
384
+ }
385
+ out.push({ name: m[1], kind: "value", defaultValue: stripQuotes(m[2].trim()) });
386
+ }
387
+ return out;
388
+ }
389
+ function parseScenarios(body, startLine, issues) {
390
+ const out = [];
391
+ const blocks = splitH3Blocks(body, startLine);
392
+ for (const block of blocks) {
393
+ const headingRaw = block.heading;
394
+ let isDefault = false;
395
+ let includeInSuite = true;
396
+ let skipReason;
397
+ let nameClean = headingRaw;
398
+ const defaultMatch = /\*\(default\)\*/i.exec(headingRaw);
399
+ if (defaultMatch) {
400
+ isDefault = true;
401
+ nameClean = nameClean.replace(defaultMatch[0], "").trim();
402
+ }
403
+ const skipMatch = /\*\(skip(?::\s*([^)]+))?\)\*/i.exec(headingRaw);
404
+ if (skipMatch) {
405
+ includeInSuite = false;
406
+ skipReason = (skipMatch[1] ?? "").trim() || void 0;
407
+ nameClean = nameClean.replace(skipMatch[0], "").trim();
408
+ }
409
+ let bodyYaml = {};
410
+ try {
411
+ const content = block.body.split("\n").map((l) => l.replace(/^-\s+/, "")).join("\n");
412
+ bodyYaml = yaml.parse(content) ?? {};
413
+ } catch (err) {
414
+ issues.push({
415
+ severity: "warn",
416
+ message: `Could not parse scenario "${nameClean}": ${err instanceof Error ? err.message : String(err)}`,
417
+ line: block.startLine
418
+ });
419
+ }
420
+ out.push({
421
+ name: nameClean,
422
+ description: typeof bodyYaml.description === "string" ? bodyYaml.description : void 0,
423
+ isDefault,
424
+ includeInSuite,
425
+ skipReason,
426
+ values: bodyYaml.values ?? {},
427
+ credentials: bodyYaml.credentials ?? {}
428
+ });
429
+ }
430
+ return out;
431
+ }
432
+ function parseAttachments(body, startLine, issues) {
433
+ const out = [];
434
+ for (const { line, lineNo } of bulletLines(body, startLine)) {
435
+ const m = /^([A-Za-z0-9_-]+)\s*:\s*(.+)$/.exec(line);
436
+ if (!m) {
437
+ issues.push({ severity: "warn", message: `Could not parse Attachments entry: "${line}"`, line: lineNo });
438
+ continue;
439
+ }
440
+ out.push({ ref: m[1], source: m[2].trim() });
441
+ }
442
+ return out;
443
+ }
444
+ function parseDeclaredOutputs(body, startLine, issues) {
445
+ const out = [];
446
+ const lines = body.split("\n");
447
+ let i = 0;
448
+ while (i < lines.length) {
449
+ const m = /^-\s+([A-Za-z0-9_-]+)\s*$/.exec(lines[i].trim());
450
+ if (!m) {
451
+ i++;
452
+ continue;
453
+ }
454
+ const dataKey = m[1];
455
+ const block = [];
456
+ i++;
457
+ while (i < lines.length) {
458
+ const raw = lines[i];
459
+ if (/^-\s/.test(raw)) break;
460
+ const sub = /^\s+-\s+(.+)$/.exec(raw);
461
+ if (sub) block.push(sub[1]);
462
+ else if (raw.trim() === "") {
463
+ } else {
464
+ break;
465
+ }
466
+ i++;
467
+ }
468
+ let body2 = {};
469
+ if (block.length > 0) {
470
+ try {
471
+ body2 = yaml.parse(block.join("\n")) ?? {};
472
+ } catch (err) {
473
+ issues.push({
474
+ severity: "warn",
475
+ message: `Could not parse Declared Outputs entry "${dataKey}": ${err instanceof Error ? err.message : String(err)}`,
476
+ line: startLine
477
+ });
478
+ }
479
+ }
480
+ out.push({
481
+ dataKey,
482
+ label: typeof body2.label === "string" ? body2.label : void 0,
483
+ // Spec writes `property: <name>`; the AST uses `propertyName` to make
484
+ // the name-vs-id distinction explicit (sync.ts resolves to id at compile).
485
+ propertyName: typeof body2.property === "string" ? body2.property : void 0,
486
+ description: typeof body2.description === "string" ? body2.description : void 0
487
+ });
488
+ }
489
+ return out;
490
+ }
491
+ function parseJourney(body, startLine, issues) {
492
+ const lines = body.split("\n");
493
+ return parseJourneyBlock(
494
+ lines,
495
+ 0,
496
+ lines.length,
497
+ /* baseIndent */
498
+ 0,
499
+ startLine,
500
+ issues
501
+ ).nodes;
502
+ }
503
+ function parseJourneyBlock(lines, start, end, baseIndent, startLine, issues) {
504
+ const nodes = [];
505
+ let i = start;
506
+ while (i < end) {
507
+ const raw = lines[i];
508
+ const trimmed = raw.trim();
509
+ if (!trimmed) {
510
+ i++;
511
+ continue;
512
+ }
513
+ const indent = leadingSpaces(raw);
514
+ if (indent < baseIndent) break;
515
+ const lineNo = startLine + i;
516
+ const { stripped, nodeId } = extractNodeId(trimmed);
517
+ const leadInMatch = /^\*\*([^*]+)\*\*\s*(.*)$/.exec(stripped);
518
+ if (!leadInMatch) {
519
+ issues.push({
520
+ severity: "warn",
521
+ message: `Unexpected line in Journey: "${trimmed}"`,
522
+ line: lineNo
523
+ });
524
+ i++;
525
+ continue;
526
+ }
527
+ const leadIn = leadInMatch[1].trim();
528
+ const remainder = leadInMatch[2].trim();
529
+ const idMeta = {
530
+ sourceLine: lineNo,
531
+ ...nodeId ? { nodeId } : {}
532
+ };
533
+ const lower = leadIn.toLowerCase();
534
+ if (lower.startsWith("if")) {
535
+ let expression = stripPunctuation(leadIn.replace(/^if\b\s*/i, "") + (remainder ? " " + remainder : "")).trim();
536
+ i++;
537
+ if (!expression) {
538
+ const bq = readBlockquote(lines, i, end);
539
+ if (bq) {
540
+ expression = bq;
541
+ i += countBlockquoteLines(lines, i, end);
542
+ }
543
+ }
544
+ const childIndent = baseIndent + 2;
545
+ let thenBranch = [];
546
+ let elseBranch = [];
547
+ while (i < end) {
548
+ const nextRaw = lines[i];
549
+ const nextTrim = nextRaw.trim();
550
+ if (!nextTrim) {
551
+ i++;
552
+ continue;
553
+ }
554
+ if (leadingSpaces(nextRaw) < baseIndent) break;
555
+ const branch = /^\*\*(then|else):\*\*\s*$/i.exec(nextTrim);
556
+ if (!branch) break;
557
+ i++;
558
+ const sub = parseJourneyBlock(lines, i, end, childIndent, startLine, issues);
559
+ if (branch[1].toLowerCase() === "then") thenBranch = sub.nodes;
560
+ else elseBranch = sub.nodes;
561
+ i = sub.nextIndex;
562
+ }
563
+ nodes.push({ ...idMeta, kind: "condition", expression, thenBranch, elseBranch });
564
+ continue;
565
+ }
566
+ let descScan = i + 1;
567
+ while (descScan < end && !lines[descScan].trim()) descScan++;
568
+ const description = readBlockquote(lines, descScan, end);
569
+ const descLineCount = description ? descScan - (i + 1) + countBlockquoteLines(lines, descScan, end) : 0;
570
+ let consumedMain = 1 + descLineCount;
571
+ if (lower.startsWith("navigate to")) {
572
+ const url = stripPunctuation(remainder);
573
+ nodes.push({ ...idMeta, kind: "navigate", pageUrl: url || (description ?? "").trim() });
574
+ i += consumedMain;
575
+ continue;
576
+ }
577
+ if (lower.startsWith("switch role to")) {
578
+ const stripped2 = leadIn.replace(/^switch role to\s+/i, "").trim();
579
+ const fromLeadIn = stripped2 && stripped2.toLowerCase() !== "switch role to" ? stripped2 : "";
580
+ const rest = fromLeadIn || remainder;
581
+ const credentialRef = parseCredentialRef(stripPunctuation(rest));
582
+ nodes.push({ ...idMeta, kind: "switchRole", credentialRef });
583
+ i += consumedMain;
584
+ continue;
585
+ }
586
+ if (lower.startsWith("wait")) {
587
+ const dur = parseDuration(remainder || leadIn.replace(/^wait\s*/i, ""));
588
+ if (dur === null) {
589
+ issues.push({ severity: "error", message: `Could not parse Wait duration: "${remainder}"`, line: lineNo });
590
+ } else {
591
+ nodes.push({ ...idMeta, kind: "wait", durationSeconds: dur });
592
+ }
593
+ i += consumedMain;
594
+ continue;
595
+ }
596
+ if (lower.startsWith("end")) {
597
+ const outcome = lower.includes("fail") ? "fail" : "pass";
598
+ nodes.push({ ...idMeta, kind: "end", outcome });
599
+ i += consumedMain;
600
+ continue;
601
+ }
602
+ if (lower.startsWith("seed")) {
603
+ const specName = stripPunctuation(remainder.replace(/^[`"]|[`"]$/g, "")) || stripPunctuation(leadIn.replace(/^seed\s*/i, ""));
604
+ const subBullets = readBullets(lines, i + 1 + descLineCount, end, baseIndent);
605
+ consumedMain += subBullets.consumed;
606
+ const opts = parseKvBullets(subBullets.bullets);
607
+ nodes.push({
608
+ ...idMeta,
609
+ kind: "seed",
610
+ specName,
611
+ description,
612
+ version: opts.version === "latest" || opts.version === void 0 ? opts.version : Number(opts.version),
613
+ onFailure: opts.onFailure,
614
+ mode: opts.mode,
615
+ apiTimeoutSeconds: opts.apiTimeoutSeconds === void 0 ? void 0 : Number(opts.apiTimeoutSeconds),
616
+ scope: opts.scope,
617
+ runTeardown: typeof opts.runTeardown === "string" ? opts.runTeardown !== "false" : void 0
618
+ });
619
+ i += consumedMain;
620
+ continue;
621
+ }
622
+ if (lower.startsWith("setup")) {
623
+ const specName = stripPunctuation(remainder.replace(/^[`"]|[`"]$/g, "")) || stripPunctuation(leadIn.replace(/^setup\s*/i, ""));
624
+ const subBullets = readBullets(lines, i + 1 + descLineCount, end, baseIndent);
625
+ consumedMain += subBullets.consumed;
626
+ const opts = parseKvBullets(subBullets.bullets);
627
+ nodes.push({
628
+ ...idMeta,
629
+ kind: "setup",
630
+ specName,
631
+ description,
632
+ version: opts.version === "latest" || opts.version === void 0 ? opts.version : Number(opts.version),
633
+ mode: opts.mode
634
+ });
635
+ i += consumedMain;
636
+ continue;
637
+ }
638
+ if (lower.startsWith("action")) {
639
+ const title = leadIn.replace(/^action\s*[—-]\s*/i, "").trim() || remainder;
640
+ const subBullets = readBullets(lines, i + 1 + descLineCount, end, baseIndent);
641
+ consumedMain += subBullets.consumed;
642
+ const action = parseActionBullets(subBullets.bullets, lineNo, issues);
643
+ nodes.push({
644
+ ...idMeta,
645
+ kind: "action",
646
+ title,
647
+ description,
648
+ ...action
649
+ });
650
+ i += consumedMain;
651
+ continue;
652
+ }
653
+ if (lower.startsWith("assert audio")) {
654
+ const label = leadIn.replace(/^assert audio\s*/i, "").trim() || remainder;
655
+ const subBullets = readBullets(lines, i + 1 + descLineCount, end, baseIndent);
656
+ consumedMain += subBullets.consumed;
657
+ const opts = parseKvBullets(subBullets.bullets);
658
+ nodes.push({
659
+ ...idMeta,
660
+ kind: "audioAssertion",
661
+ label,
662
+ description,
663
+ severity: opts.severity ?? "fail",
664
+ prompt: typeof opts.prompt === "string" ? opts.prompt : void 0,
665
+ model: opts.model,
666
+ minDurationMs: numOpt(opts.minDurationMs),
667
+ maxDurationMs: numOpt(opts.maxDurationMs),
668
+ minRmsDb: numOpt(opts.minRmsDb),
669
+ silenceFractionMax: numOpt(opts.silenceFractionMax),
670
+ reference: typeof opts.reference === "string" ? opts.reference : void 0,
671
+ envelopeCorrelationMin: numOpt(opts.envelopeCorrelationMin)
672
+ });
673
+ i += consumedMain;
674
+ continue;
675
+ }
676
+ if (lower.startsWith("assert")) {
677
+ const condition = leadIn.replace(/^assert\s*/i, "").trim() + (remainder ? " " + remainder : "");
678
+ const subBullets = readBullets(lines, i + 1 + descLineCount, end, baseIndent);
679
+ consumedMain += subBullets.consumed;
680
+ const opts = parseKvBullets(subBullets.bullets);
681
+ nodes.push({
682
+ ...idMeta,
683
+ kind: "assertion",
684
+ condition: stripPunctuation(condition),
685
+ description,
686
+ severity: opts.severity ?? "warn",
687
+ expectedOutcome: typeof opts.expectedOutcome === "string" ? opts.expectedOutcome : void 0,
688
+ alwaysUseAgent: opts.alwaysUseAgent === "true" ? true : void 0
689
+ });
690
+ i += consumedMain;
691
+ continue;
692
+ }
693
+ if (lower.startsWith("play audio")) {
694
+ const ref = leadIn.replace(/^play audio\s*/i, "").trim() || remainder;
695
+ const subBullets = readBullets(lines, i + 1 + descLineCount, end, baseIndent);
696
+ consumedMain += subBullets.consumed;
697
+ const opts = parseKvBullets(subBullets.bullets);
698
+ nodes.push({
699
+ ...idMeta,
700
+ kind: "audio",
701
+ attachmentRef: stripPunctuation(ref),
702
+ description,
703
+ loop: opts.loop === "true" ? true : void 0,
704
+ waitForCompletion: opts.waitForCompletion === "false" ? false : void 0,
705
+ label: typeof opts.label === "string" ? opts.label : void 0
706
+ });
707
+ i += consumedMain;
708
+ continue;
709
+ }
710
+ if (lower.startsWith("notify")) {
711
+ const m = /^notify\s+([A-Za-z0-9_-]+)\s*:?\s*(.*)$/i.exec(leadIn + (remainder ? " " + remainder : ""));
712
+ if (!m) {
713
+ issues.push({ severity: "error", message: `Could not parse Notify lead-in: "${leadIn}"`, line: lineNo });
714
+ } else {
715
+ const subBullets = readBullets(lines, i + 1 + descLineCount, end, baseIndent);
716
+ consumedMain += subBullets.consumed;
717
+ const opts = parseKvBullets(subBullets.bullets);
718
+ nodes.push({
719
+ ...idMeta,
720
+ kind: "notification",
721
+ channel: m[1],
722
+ to: stripPunctuation(stripBackticks(m[2])),
723
+ subject: typeof opts.subject === "string" ? opts.subject : void 0,
724
+ body: description ?? "",
725
+ attach: typeof opts.attach === "string" ? opts.attach : void 0
726
+ });
727
+ }
728
+ i += consumedMain;
729
+ continue;
730
+ }
731
+ if (lower.startsWith("api sequence")) {
732
+ const title = leadIn.replace(/^api sequence\s*[—-]\s*/i, "").trim();
733
+ const subBullets = readBullets(lines, i + 1 + descLineCount, end, baseIndent);
734
+ consumedMain += subBullets.consumed;
735
+ const opts = parseKvBullets(subBullets.bullets);
736
+ nodes.push({
737
+ ...idMeta,
738
+ kind: "apiSequence",
739
+ title,
740
+ apiEndpointContext: description ?? "",
741
+ credentialInputName: typeof opts.credential === "string" ? opts.credential : void 0,
742
+ // Sidecar reference (e.g. `- apiSequence: ./sequences/x.yaml`).
743
+ // Resolved by the CLI at compile/diff time relative to the spec file
744
+ // path; parse.ts has no filesystem context so we just carry the string.
745
+ sidecar: typeof opts.apiSequence === "string" ? opts.apiSequence : void 0
746
+ });
747
+ i += consumedMain;
748
+ continue;
749
+ }
750
+ issues.push({ severity: "error", message: `Unrecognized lead-in: "${leadIn}"`, line: lineNo });
751
+ i++;
752
+ }
753
+ return { nodes, nextIndex: i };
754
+ }
755
+ function leadingSpaces(s) {
756
+ let n = 0;
757
+ while (n < s.length && s[n] === " ") n++;
758
+ return n;
759
+ }
760
+ function stripPunctuation(s) {
761
+ return s.replace(/[`"]/g, "").replace(/\.$/, "").trim();
762
+ }
763
+ function stripBackticks(s) {
764
+ return s.replace(/`/g, "").trim();
765
+ }
766
+ function stripQuotes(s) {
767
+ if (s.startsWith('"') && s.endsWith('"') || s.startsWith("'") && s.endsWith("'")) {
768
+ return s.slice(1, -1);
769
+ }
770
+ return s;
771
+ }
772
+ function parseDuration(s) {
773
+ const m = /^(\d+(?:\.\d+)?)\s*(s|m|h)?\.?$/i.exec(s.trim());
774
+ if (!m) return null;
775
+ const n = Number(m[1]);
776
+ const unit = (m[2] ?? "s").toLowerCase();
777
+ const seconds = unit === "h" ? n * 3600 : unit === "m" ? n * 60 : n;
778
+ return Math.round(seconds);
779
+ }
780
+ function readBlockquote(lines, start, end) {
781
+ if (start >= end) return void 0;
782
+ if (!lines[start].trimStart().startsWith(">")) return void 0;
783
+ const out = [];
784
+ for (let i = start; i < end; i++) {
785
+ const t = lines[i].trimStart();
786
+ if (!t.startsWith(">")) break;
787
+ out.push(t.replace(/^>\s?/, ""));
788
+ }
789
+ return out.join("\n").trim();
790
+ }
791
+ function countBlockquoteLines(lines, start, end) {
792
+ let n = 0;
793
+ for (let i = start; i < end; i++) {
794
+ if (!lines[i].trimStart().startsWith(">")) break;
795
+ n++;
796
+ }
797
+ return n;
798
+ }
799
+ function readBullets(lines, start, end, baseIndent) {
800
+ const bullets = [];
801
+ let i = start;
802
+ while (i < end) {
803
+ const raw = lines[i];
804
+ const trimmed = raw.trim();
805
+ if (!trimmed) {
806
+ i++;
807
+ continue;
808
+ }
809
+ if (!trimmed.startsWith("- ")) break;
810
+ if (leadingSpaces(raw) < baseIndent) break;
811
+ bullets.push(trimmed.slice(2).trim());
812
+ i++;
813
+ }
814
+ return { bullets, consumed: i - start };
815
+ }
816
+ function bulletLines(body, startLine) {
817
+ const out = [];
818
+ const lines = body.split("\n");
819
+ for (let i = 0; i < lines.length; i++) {
820
+ const t = lines[i].trim();
821
+ if (t.startsWith("- ")) out.push({ line: t.slice(2).trim(), lineNo: startLine + i });
822
+ }
823
+ return out;
824
+ }
825
+ function splitH3Blocks(body, startLine) {
826
+ const out = [];
827
+ const lines = body.split("\n");
828
+ let cur = null;
829
+ for (let i = 0; i < lines.length; i++) {
830
+ const m = /^###\s+(.+?)\s*$/.exec(lines[i]);
831
+ if (m) {
832
+ if (cur) out.push({ heading: cur.heading, body: cur.body.join("\n"), startLine: cur.startLine });
833
+ cur = { heading: m[1], body: [], startLine: startLine + i };
834
+ continue;
835
+ }
836
+ if (cur) cur.body.push(lines[i]);
837
+ }
838
+ if (cur) out.push({ heading: cur.heading, body: cur.body.join("\n"), startLine: cur.startLine });
839
+ return out;
840
+ }
841
+ function parseActionBullets(bullets, lineNo, issues) {
842
+ const fields = [];
843
+ const capture = [];
844
+ const assertions = [];
845
+ let captureAudio;
846
+ let captureLabel;
847
+ let captureExtraMs;
848
+ let alwaysUseAgent;
849
+ for (const b of bullets) {
850
+ const fieldM = /^field\s+(?:"([^"]+)"|([A-Za-z0-9_-]+))\s*(?:\(([^)]+)\))?\s*:\s*(.+)$/i.exec(b);
851
+ if (fieldM) {
852
+ fields.push({
853
+ name: fieldM[1] ?? fieldM[2],
854
+ type: fieldM[3],
855
+ value: stripQuotes(fieldM[4].trim())
856
+ });
857
+ continue;
858
+ }
859
+ const capM = /^capture\s*:?\s*(.+)$/i.exec(b);
860
+ if (capM) {
861
+ capture.push(stripQuotes(capM[1].trim()));
862
+ continue;
863
+ }
864
+ const assertM = /^assert\s*\(([^)]+)\)\s*:\s*(.+)$/i.exec(b);
865
+ if (assertM) {
866
+ const flags = assertM[1].split(",").map((s) => s.trim().toLowerCase());
867
+ const severity = flags.includes("fail") ? "fail" : "warn";
868
+ const agentic = flags.includes("agentic") || void 0;
869
+ assertions.push({ condition: stripQuotes(assertM[2].trim()), severity, agentic });
870
+ continue;
871
+ }
872
+ const kv = /^([A-Za-z][A-Za-z0-9]*)\s*:\s*(.+)$/.exec(b);
873
+ if (kv) {
874
+ const key = kv[1];
875
+ const value = kv[2].trim();
876
+ if (key === "captureAudio") captureAudio = value === "true";
877
+ else if (key === "captureLabel") captureLabel = stripQuotes(value);
878
+ else if (key === "captureExtraMs") captureExtraMs = Number(value);
879
+ else if (key === "alwaysUseAgent") alwaysUseAgent = value === "true";
880
+ else issues.push({ severity: "warn", message: `Unknown action option "${key}"`, line: lineNo });
881
+ continue;
882
+ }
883
+ issues.push({ severity: "warn", message: `Could not parse action bullet: "${b}"`, line: lineNo });
884
+ }
885
+ return { fields, capture, assertions, captureAudio, captureLabel, captureExtraMs, alwaysUseAgent };
886
+ }
887
+ function parseKvBullets(bullets) {
888
+ const out = {};
889
+ for (const b of bullets) {
890
+ const m = /^([A-Za-z][A-Za-z0-9_-]*)\s*:\s*(.+)$/.exec(b);
891
+ if (!m) continue;
892
+ out[m[1]] = stripQuotes(m[2].trim());
893
+ }
894
+ return out;
895
+ }
896
+ function numOpt(v) {
897
+ if (v === void 0) return void 0;
898
+ const n = Number(v);
899
+ return Number.isFinite(n) ? n : void 0;
900
+ }
901
+ var NODE_ID_RE = /\s*<!--\s*@id\s+([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\s*-->/i;
902
+ function extractNodeId(line) {
903
+ const m = NODE_ID_RE.exec(line);
904
+ if (!m) return { stripped: line, nodeId: void 0 };
905
+ return { stripped: line.replace(NODE_ID_RE, "").trimEnd(), nodeId: m[1].toLowerCase() };
906
+ }
907
+
908
+ // ../canary-spec/src/lint.ts
909
+ var RESERVED_MACROS = /* @__PURE__ */ new Set([
910
+ "EMAIL",
911
+ "UUID",
912
+ "NOW",
913
+ "RANDOM_STRING",
914
+ "RANDOM_INT",
915
+ "DATE",
916
+ "TODAY",
917
+ "TIMESTAMP",
918
+ "SEQUENCE",
919
+ "FIRST_NAME",
920
+ "LAST_NAME",
921
+ "PHONE",
922
+ "CREDENTIAL"
923
+ ]);
924
+ function lintSpec(ast) {
925
+ const issues = [];
926
+ const hasFailAssertion = anyNode(ast.journey, (n) => {
927
+ if (n.kind === "action") return n.assertions.some((a) => a.severity === "fail");
928
+ if (n.kind === "assertion") return n.severity === "fail";
929
+ if (n.kind === "audioAssertion") return n.severity === "fail";
930
+ return false;
931
+ });
932
+ if (!hasFailAssertion) {
933
+ issues.push({
934
+ severity: "warn",
935
+ message: "Spec has no fail-severity assertions \u2014 the case will pass even if the feature is broken.",
936
+ hint: "Add `- assert (fail): \u2026` under at least one action."
937
+ });
938
+ }
939
+ if (!ast.frontmatter.requirementRef) {
940
+ issues.push({
941
+ severity: "warn",
942
+ message: "`requirementRef` is missing from frontmatter \u2014 untraceable specs rot."
943
+ });
944
+ }
945
+ const declaredInputs = new Set(ast.inputs.map((i) => i.name));
946
+ const promotedVars = /* @__PURE__ */ new Set();
947
+ walkNodes(ast.journey, (n) => {
948
+ if (n.kind === "action") {
949
+ n.capture.forEach((c) => promotedVars.add(c));
950
+ n.fields.forEach((f) => promotedVars.add(f.name));
951
+ }
952
+ });
953
+ walkStrings(ast.journey, (s, ctx) => {
954
+ for (const ref of extractMacroRefs(s)) {
955
+ if (RESERVED_MACROS.has(ref.split("(")[0])) continue;
956
+ if (ref.startsWith("var.")) {
957
+ const name = ref.slice("var.".length);
958
+ if (!promotedVars.has(name)) {
959
+ issues.push({
960
+ severity: "warn",
961
+ message: `\`$var.${name}\` referenced in ${ctx} but not declared via \`- capture:\` or a field.`,
962
+ hint: "If this is authored data, add `- capture: " + name + "` to the action that produces it."
963
+ });
964
+ }
965
+ } else if (!declaredInputs.has(ref)) {
966
+ issues.push({
967
+ severity: "error",
968
+ message: `\`$${ref}\` referenced in ${ctx} but not declared in \`## Inputs\`.`
969
+ });
970
+ }
971
+ }
972
+ });
973
+ const defaults = ast.scenarios.filter((s) => s.isDefault);
974
+ if (defaults.length > 1) {
975
+ issues.push({
976
+ severity: "error",
977
+ message: `Multiple scenarios marked default: ${defaults.map((s) => s.name).join(", ")}.`
978
+ });
979
+ }
980
+ for (const s of ast.scenarios) {
981
+ if (!s.includeInSuite && !s.skipReason) {
982
+ issues.push({
983
+ severity: "warn",
984
+ message: `Scenario "${s.name}" is skipped without a reason. Use *(skip: <ref>)*.`
985
+ });
986
+ }
987
+ }
988
+ for (const s of ast.scenarios) {
989
+ for (const key of Object.keys(s.values)) {
990
+ if (!declaredInputs.has(key)) {
991
+ issues.push({
992
+ severity: "warn",
993
+ message: `Scenario "${s.name}" overrides input "${key}" which is not declared in \`## Inputs\`.`
994
+ });
995
+ }
996
+ }
997
+ }
998
+ const declaredAttachments = new Set(ast.attachments.map((a) => a.ref));
999
+ walkNodes(ast.journey, (n) => {
1000
+ if (n.kind === "audio" && !declaredAttachments.has(n.attachmentRef)) {
1001
+ issues.push({
1002
+ severity: "error",
1003
+ message: `Play audio references unknown attachment "${n.attachmentRef}".`
1004
+ });
1005
+ }
1006
+ if (n.kind === "audioAssertion" && n.reference && !declaredAttachments.has(n.reference)) {
1007
+ issues.push({
1008
+ severity: "error",
1009
+ message: `Audio assertion references unknown attachment "${n.reference}".`
1010
+ });
1011
+ }
1012
+ if (n.kind === "notification" && n.attach && !declaredAttachments.has(n.attach)) {
1013
+ issues.push({
1014
+ severity: "error",
1015
+ message: `Notification attaches unknown attachment "${n.attach}".`
1016
+ });
1017
+ }
1018
+ });
1019
+ walkNodes(ast.journey, (n) => {
1020
+ if (n.kind !== "action") return;
1021
+ const text = (n.description ?? "") + " " + n.title;
1022
+ if (/\bclick\s+(?:the\s+)?["']?\w+/i.test(text) && /,\s*then\s+click/i.test(text)) {
1023
+ issues.push({
1024
+ severity: "warn",
1025
+ message: `Action "${n.title}" reads as click-by-click UI mechanics \u2014 prefer human intent.`
1026
+ });
1027
+ }
1028
+ });
1029
+ return issues;
1030
+ }
1031
+ function anyNode(nodes, pred) {
1032
+ for (const n of nodes) {
1033
+ if (pred(n)) return true;
1034
+ if (n.kind === "condition") {
1035
+ if (anyNode(n.thenBranch, pred) || anyNode(n.elseBranch, pred)) return true;
1036
+ }
1037
+ }
1038
+ return false;
1039
+ }
1040
+ function walkNodes(nodes, visit) {
1041
+ for (const n of nodes) {
1042
+ visit(n);
1043
+ if (n.kind === "condition") {
1044
+ walkNodes(n.thenBranch, visit);
1045
+ walkNodes(n.elseBranch, visit);
1046
+ }
1047
+ }
1048
+ }
1049
+ function walkStrings(nodes, visit) {
1050
+ walkNodes(nodes, (n) => {
1051
+ const ctx = nodeCtx(n);
1052
+ if (n.description) visit(n.description, ctx);
1053
+ if (n.kind === "navigate") visit(n.pageUrl, ctx);
1054
+ if (n.kind === "action") {
1055
+ visit(n.title, ctx);
1056
+ n.fields.forEach((f) => visit(f.value, ctx));
1057
+ n.assertions.forEach((a) => visit(a.condition, ctx));
1058
+ }
1059
+ if (n.kind === "assertion") visit(n.condition, ctx);
1060
+ if (n.kind === "audioAssertion" && n.prompt) visit(n.prompt, ctx);
1061
+ if (n.kind === "notification") {
1062
+ visit(n.to, ctx);
1063
+ visit(n.body, ctx);
1064
+ if (n.subject) visit(n.subject, ctx);
1065
+ }
1066
+ if (n.kind === "condition") visit(n.expression, ctx);
1067
+ });
1068
+ }
1069
+ function nodeCtx(n) {
1070
+ if (n.kind === "action") return `action "${n.title}"`;
1071
+ if (n.kind === "assertion") return `assertion`;
1072
+ if (n.kind === "navigate") return `navigate`;
1073
+ if (n.kind === "condition") return `condition`;
1074
+ if (n.kind === "notification") return `notification`;
1075
+ if (n.kind === "seed") return `seed "${n.specName}"`;
1076
+ if (n.kind === "setup") return `setup "${n.specName}"`;
1077
+ return n.kind;
1078
+ }
1079
+ function extractMacroRefs(s) {
1080
+ const out = [];
1081
+ const re = /\$([A-Za-z_][A-Za-z0-9_.]*)/g;
1082
+ let m;
1083
+ while (m = re.exec(s)) {
1084
+ out.push(m[1].replace(/\.+$/, ""));
1085
+ }
1086
+ return out;
1087
+ }
1088
+
1089
+ // ../canary-spec/src/emit.ts
1090
+ function emitSpec(ast, opts = {}) {
1091
+ const { appendDefaultEnd = true } = opts;
1092
+ const nodes = [];
1093
+ for (const n of ast.journey) emitNode(n, nodes);
1094
+ if (ast.frontmatter.authenticated) {
1095
+ let insertAt = 0;
1096
+ while (insertAt < nodes.length && (nodes[insertAt].nodeType === "seed" || nodes[insertAt].nodeType === "setup")) {
1097
+ insertAt++;
1098
+ }
1099
+ nodes.splice(
1100
+ insertAt,
1101
+ 0,
1102
+ ast.frontmatter.actor ? loginNode(ast.frontmatter.actor) : bareLoginNode()
1103
+ );
1104
+ }
1105
+ const last = nodes[nodes.length - 1];
1106
+ if (appendDefaultEnd && (!last || last.nodeType !== "end")) {
1107
+ nodes.push({
1108
+ nodeType: "end",
1109
+ configJson: { title: "End", description: "Journey complete", outcome: "pass" }
1110
+ });
1111
+ }
1112
+ return {
1113
+ name: ast.frontmatter.name,
1114
+ description: ast.spirit ?? null,
1115
+ preflight: ast.preflight ?? null,
1116
+ cleanup: ast.cleanup ?? null,
1117
+ status: ast.frontmatter.status,
1118
+ flowType: ast.frontmatter.flowType,
1119
+ apiOnly: ast.frontmatter.apiOnly,
1120
+ // Optional fields are emitted as `undefined` when absent so the diff can
1121
+ // tell "spec doesn't constrain this" (no field-update op) from "spec
1122
+ // explicitly wants the field cleared" (handled per-field in diff.ts).
1123
+ tags: ast.frontmatter.tags,
1124
+ environmentConstraints: toEnvironmentConstraints(ast.frontmatter.environments),
1125
+ sandboxTemplate: ast.frontmatter.sandboxTemplate,
1126
+ ...normalizeTeardown(ast.frontmatter.teardown),
1127
+ seedOutputs: ast.declaredOutputs.length > 0 ? ast.declaredOutputs.map((o) => ({
1128
+ dataKey: o.dataKey,
1129
+ ...o.label ? { label: o.label } : {},
1130
+ ...o.description ? { description: o.description } : {},
1131
+ ...o.propertyName ? { propertyName: o.propertyName } : {}
1132
+ })) : void 0,
1133
+ inputsJson: ast.inputs.map((i) => ({
1134
+ kind: i.kind,
1135
+ name: i.name,
1136
+ defaultValue: i.defaultValue
1137
+ })),
1138
+ scenariosJson: ast.scenarios.map((s) => ({
1139
+ id: crypto.randomUUID(),
1140
+ name: s.name,
1141
+ description: s.description,
1142
+ isDefault: s.isDefault || void 0,
1143
+ includeInSuite: s.includeInSuite,
1144
+ // `skipReason` is a spec-source-only field (no DB column). Emit it so the
1145
+ // round-trip diff doesn't flag it as drift, but sync.ts strips it before
1146
+ // PUT — the API doesn't accept it.
1147
+ ...s.skipReason ? { skipReason: s.skipReason } : {},
1148
+ values: Object.keys(s.values).length > 0 ? s.values : void 0,
1149
+ credentials: Object.keys(s.credentials).length > 0 ? s.credentials : void 0
1150
+ })),
1151
+ attachments: ast.attachments,
1152
+ nodes,
1153
+ workflowId: ast.frontmatter.workflowId
1154
+ };
1155
+ }
1156
+ function toEnvironmentConstraints(envs) {
1157
+ if (!envs) return void 0;
1158
+ if ("only" in envs) return { mode: "only", environmentNames: envs.only };
1159
+ return { mode: "envTypes", types: envs.types };
1160
+ }
1161
+ function normalizeTeardown(teardown) {
1162
+ if (!teardown) return {};
1163
+ if (typeof teardown === "string") {
1164
+ return { teardownSpec: teardown, teardownSnapshotVersion: "latest", teardownEnabled: true };
1165
+ }
1166
+ return {
1167
+ teardownSpec: teardown.spec,
1168
+ teardownSnapshotVersion: teardown.snapshotVersion ?? "latest",
1169
+ teardownEnabled: teardown.enabled ?? true
1170
+ };
1171
+ }
1172
+ function bareLoginNode() {
1173
+ return {
1174
+ nodeType: "login",
1175
+ configJson: { title: "Log in", description: "Authenticate the session", performLogin: true }
1176
+ };
1177
+ }
1178
+ function loginNode(actor) {
1179
+ return {
1180
+ nodeType: "login",
1181
+ configJson: loginConfig(actor, { titlePrefix: "Log in as" })
1182
+ };
1183
+ }
1184
+ function loginConfig(ref, opts) {
1185
+ const performLogin = opts.performLogin ?? false;
1186
+ if (ref.kind === "credential") {
1187
+ return {
1188
+ title: `${opts.titlePrefix} ${ref.name}`,
1189
+ description: opts.description ?? `Authenticate using credential "${ref.name}"`,
1190
+ credentialName: ref.name,
1191
+ credentialSource: "static",
1192
+ ...performLogin ? { performLogin: true } : {}
1193
+ };
1194
+ }
1195
+ if (ref.kind === "seedOutput") {
1196
+ return {
1197
+ title: `${opts.titlePrefix} seed-provisioned ${ref.ref}`,
1198
+ description: opts.description ?? `Authenticate using credential from seed output "${ref.ref}"`,
1199
+ credentialSource: "from_seed",
1200
+ // The runtime expects an object ref; sync.ts can enrich with the seed
1201
+ // workflow id once the binding context is known. dataKey is the stable
1202
+ // identifier (matches the seed's declared output key); name mirrors it.
1203
+ seedCredentialRef: { dataKey: ref.ref, name: ref.ref },
1204
+ // from_seed mid-journey switches always perform login; leading login
1205
+ // also performs login on session bootstrap.
1206
+ performLogin: true
1207
+ };
1208
+ }
1209
+ return {
1210
+ title: `${opts.titlePrefix} ${ref.role}`,
1211
+ description: opts.description ?? `Authenticate using credential template "${ref.role}"`,
1212
+ credentialSource: "from_template",
1213
+ credentialTemplateName: ref.role,
1214
+ ...performLogin ? { performLogin: true } : {}
1215
+ };
1216
+ }
1217
+ function emitNode(n, out) {
1218
+ const meta = {
1219
+ ...n.nodeId ? { nodeId: n.nodeId } : {},
1220
+ ...n.sourceLine !== void 0 ? { sourceLine: n.sourceLine } : {}
1221
+ };
1222
+ switch (n.kind) {
1223
+ case "navigate":
1224
+ out.push({
1225
+ ...meta,
1226
+ nodeType: "navigate",
1227
+ configJson: {
1228
+ title: `Go to ${shortPath(n.pageUrl)}`,
1229
+ description: n.description ?? `Navigate to ${n.pageUrl}`,
1230
+ pageUrl: n.pageUrl,
1231
+ ...n.openInNewTab ? { openInNewTab: true } : {}
1232
+ }
1233
+ });
1234
+ return;
1235
+ case "switchRole": {
1236
+ const ref = n.credentialRef;
1237
+ const fallbackDesc = ref.kind === "credential" ? `Re-authenticate as ${ref.name}` : ref.kind === "seedOutput" ? `Re-authenticate as seed-provisioned ${ref.ref}` : `Re-authenticate as ${ref.role}`;
1238
+ out.push({
1239
+ ...meta,
1240
+ nodeType: "login",
1241
+ configJson: loginConfig(ref, {
1242
+ titlePrefix: "Switch role to",
1243
+ description: n.description ?? fallbackDesc,
1244
+ performLogin: true
1245
+ })
1246
+ });
1247
+ return;
1248
+ }
1249
+ case "action": {
1250
+ const fieldEntries = n.fields.map((f) => ({
1251
+ name: f.name,
1252
+ label: humanizeLabel(f.name),
1253
+ type: f.type ?? "text",
1254
+ value: f.value
1255
+ }));
1256
+ const inputValues = fieldEntries.length > 0 ? Object.fromEntries(
1257
+ fieldEntries.filter((f) => f.value !== void 0 && f.value !== "").map((f) => [f.name, f.value])
1258
+ ) : void 0;
1259
+ out.push({
1260
+ ...meta,
1261
+ nodeType: "action",
1262
+ configJson: {
1263
+ customActionTitle: n.title,
1264
+ actionDescription: n.description ?? n.title,
1265
+ ...fieldEntries.length > 0 ? {
1266
+ formFields: fieldEntries,
1267
+ dataRequirements: fieldEntries.map(({ name, label, type }) => ({
1268
+ name,
1269
+ label,
1270
+ type,
1271
+ required: true
1272
+ })),
1273
+ ...inputValues && Object.keys(inputValues).length > 0 ? { inputValues } : {}
1274
+ } : {},
1275
+ ...n.capture.length > 0 ? { declaredOutputs: n.capture } : {},
1276
+ ...n.assertions.length > 0 ? {
1277
+ inlineAssertions: n.assertions.map((a) => ({
1278
+ condition: a.condition,
1279
+ severity: a.severity,
1280
+ ...a.agentic ? { agentic: true } : {}
1281
+ }))
1282
+ } : {},
1283
+ ...n.captureAudio ? {
1284
+ captureAudio: true,
1285
+ ...n.captureLabel ? { captureLabel: n.captureLabel } : {},
1286
+ ...n.captureExtraMs !== void 0 ? { captureExtraMs: n.captureExtraMs } : {}
1287
+ } : {},
1288
+ ...n.alwaysUseAgent ? { alwaysUseAgent: true } : {}
1289
+ }
1290
+ });
1291
+ return;
1292
+ }
1293
+ case "assertion":
1294
+ out.push({
1295
+ ...meta,
1296
+ nodeType: "assertion",
1297
+ configJson: {
1298
+ title: "Assert",
1299
+ description: n.description ?? n.condition,
1300
+ condition: n.condition,
1301
+ severity: n.severity,
1302
+ ...n.expectedOutcome ? { expectedOutcome: n.expectedOutcome } : {},
1303
+ ...n.alwaysUseAgent ? { alwaysUseAgent: true } : {}
1304
+ }
1305
+ });
1306
+ return;
1307
+ case "seed":
1308
+ out.push({
1309
+ ...meta,
1310
+ nodeType: "seed",
1311
+ configJson: {
1312
+ title: `Seed: ${n.specName}`,
1313
+ description: n.description ?? `Run seed workflow "${n.specName}"`,
1314
+ // Compiler resolves spec-name → seedWorkflowId; placeholder until then.
1315
+ seedWorkflowName: n.specName,
1316
+ // Emit the canonical SeedNodeConfig keys the runtime + UI actually
1317
+ // read (seed-deps.ts, SeedNodeConfig.tsx). The `seed`-prefixed names
1318
+ // are only for the AI builder-tool input schemas, which remap them;
1319
+ // here configJson is stored verbatim, so prefixed keys silently
1320
+ // revert to defaults.
1321
+ ...n.version === "latest" || n.version === void 0 ? { snapshotVersion: null } : { snapshotVersion: n.version },
1322
+ ...n.onFailure ? { onFailure: n.onFailure } : {},
1323
+ ...n.mode ? { executionMode: n.mode } : {},
1324
+ ...n.apiTimeoutSeconds ? { apiTimeoutSeconds: n.apiTimeoutSeconds } : {},
1325
+ ...n.scope ? { scope: n.scope } : {},
1326
+ ...n.runTeardown !== void 0 ? { runTeardown: n.runTeardown } : {}
1327
+ }
1328
+ });
1329
+ return;
1330
+ case "setup":
1331
+ out.push({
1332
+ ...meta,
1333
+ nodeType: "setup",
1334
+ configJson: {
1335
+ title: `Setup: ${n.specName}`,
1336
+ description: n.description ?? `Run setup workflow "${n.specName}"`,
1337
+ setupFlowName: n.specName,
1338
+ ...n.version === "latest" || n.version === void 0 ? { snapshotVersion: null } : { snapshotVersion: n.version },
1339
+ ...n.mode ? { executionMode: n.mode } : {}
1340
+ }
1341
+ });
1342
+ return;
1343
+ case "wait":
1344
+ out.push({
1345
+ ...meta,
1346
+ nodeType: "wait",
1347
+ configJson: {
1348
+ title: `Wait ${n.durationSeconds}s`,
1349
+ description: n.description ?? `Pause for ${n.durationSeconds} seconds`,
1350
+ waitDuration: n.durationSeconds,
1351
+ mode: "delay",
1352
+ delaySeconds: n.durationSeconds
1353
+ }
1354
+ });
1355
+ return;
1356
+ case "condition":
1357
+ out.push({
1358
+ ...meta,
1359
+ nodeType: "condition",
1360
+ configJson: {
1361
+ title: "If",
1362
+ description: n.description ?? n.expression,
1363
+ // Canonical runtime key is `conditionDescription`.
1364
+ conditionDescription: n.expression,
1365
+ ...n.hints ? { evaluationHints: n.hints } : {}
1366
+ }
1367
+ });
1368
+ n.thenBranch.forEach((c) => emitNode(c, out));
1369
+ n.elseBranch.forEach((c) => emitNode(c, out));
1370
+ return;
1371
+ case "notification":
1372
+ out.push({
1373
+ ...meta,
1374
+ nodeType: "notification",
1375
+ configJson: {
1376
+ title: `Notify ${n.to}`,
1377
+ description: n.description ?? n.body,
1378
+ channel: n.channel,
1379
+ to: n.to,
1380
+ body: n.body,
1381
+ ...n.subject ? { subject: n.subject } : {},
1382
+ ...n.attach ? { attachmentRef: n.attach } : {}
1383
+ }
1384
+ });
1385
+ return;
1386
+ case "audio":
1387
+ out.push({
1388
+ ...meta,
1389
+ nodeType: "audio",
1390
+ configJson: {
1391
+ title: n.label ?? `Play ${n.attachmentRef}`,
1392
+ description: n.description ?? `Play audio "${n.attachmentRef}"`,
1393
+ attachmentRef: n.attachmentRef,
1394
+ ...n.loop !== void 0 ? { loop: n.loop } : {},
1395
+ ...n.waitForCompletion !== void 0 ? { waitForCompletion: n.waitForCompletion } : {},
1396
+ ...n.label ? { label: n.label } : {}
1397
+ }
1398
+ });
1399
+ return;
1400
+ case "audioAssertion":
1401
+ out.push({
1402
+ ...meta,
1403
+ nodeType: "audio-assertion",
1404
+ configJson: {
1405
+ title: `Assert audio: ${n.label}`,
1406
+ description: n.description ?? n.label,
1407
+ label: n.label,
1408
+ severity: n.severity,
1409
+ ...n.prompt ? { naturalLanguage: { prompt: n.prompt, ...n.model ? { model: n.model } : {} } } : {},
1410
+ ...hasDeterministicAudioAssertion(n) ? {
1411
+ deterministic: {
1412
+ ...n.minDurationMs !== void 0 ? { minDurationMs: n.minDurationMs } : {},
1413
+ ...n.maxDurationMs !== void 0 ? { maxDurationMs: n.maxDurationMs } : {},
1414
+ ...n.minRmsDb !== void 0 ? { minRmsDb: n.minRmsDb } : {},
1415
+ ...n.silenceFractionMax !== void 0 ? { silenceFractionMax: n.silenceFractionMax } : {},
1416
+ ...n.envelopeCorrelationMin !== void 0 ? { envelopeCorrelationMin: n.envelopeCorrelationMin } : {}
1417
+ }
1418
+ } : {},
1419
+ ...n.reference ? { referenceAttachmentRef: n.reference } : {}
1420
+ }
1421
+ });
1422
+ return;
1423
+ case "apiSequence":
1424
+ out.push({
1425
+ ...meta,
1426
+ nodeType: "api_sequence",
1427
+ configJson: {
1428
+ customActionTitle: n.title,
1429
+ description: n.description ?? n.title,
1430
+ apiEndpointContext: n.apiEndpointContext,
1431
+ ...n.credentialInputName ? { credentialInputName: n.credentialInputName } : {},
1432
+ // Sidecar reference is metadata for the CLI sync layer — runtime
1433
+ // ignores unknown configJson keys, and the node-hash projection
1434
+ // drops it (api_sequence stays opaque at node level; the actual DSL
1435
+ // config is diffed via the sidecar load below).
1436
+ ...n.sidecar ? { __apiSequenceSidecar: n.sidecar } : {}
1437
+ }
1438
+ });
1439
+ return;
1440
+ case "end":
1441
+ out.push({
1442
+ ...meta,
1443
+ nodeType: "end",
1444
+ configJson: {
1445
+ title: n.outcome === "fail" ? "End: fail" : "End",
1446
+ description: n.description ?? "Journey complete",
1447
+ outcome: n.outcome
1448
+ }
1449
+ });
1450
+ return;
1451
+ }
1452
+ }
1453
+ function hasDeterministicAudioAssertion(n) {
1454
+ return n.minDurationMs !== void 0 || n.maxDurationMs !== void 0 || n.minRmsDb !== void 0 || n.silenceFractionMax !== void 0 || n.reference !== void 0 || n.envelopeCorrelationMin !== void 0;
1455
+ }
1456
+ function shortPath(url) {
1457
+ if (url.startsWith("/")) return url;
1458
+ try {
1459
+ return new URL(url).pathname || url;
1460
+ } catch {
1461
+ return url;
1462
+ }
1463
+ }
1464
+ function humanizeLabel(name) {
1465
+ return name.replace(/[_-]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
1466
+ }
1467
+
1468
+ // ../canary-spec/src/node-hash.ts
1469
+ import { createHash } from "crypto";
1470
+ function authoredNodeHash(node) {
1471
+ const projection = {
1472
+ nodeType: node.nodeType,
1473
+ ...authoredProjection(node.nodeType, node.configJson)
1474
+ };
1475
+ const stable = JSON.stringify(sortObjectKeys(projection));
1476
+ return createHash("sha256").update(stable).digest("hex").slice(0, 16);
1477
+ }
1478
+ function authoredProjection(nodeType, raw) {
1479
+ const c = raw ?? {};
1480
+ switch (nodeType) {
1481
+ case "login":
1482
+ return { credential: canon(loginCredentialRef(c)) };
1483
+ case "navigate":
1484
+ return {
1485
+ pageUrl: canon(str(c.pageUrl)),
1486
+ openInNewTab: c.openInNewTab === true
1487
+ };
1488
+ case "action":
1489
+ return {
1490
+ title: authoredActionTitle(c),
1491
+ // emit falls back actionDescription → title; trim because the blockquote
1492
+ // round-trip can't preserve leading/trailing whitespace.
1493
+ description: trimText(str(c.actionDescription) ?? str(c.description)) ?? authoredActionTitle(c),
1494
+ // Authorable name/type/value triples — server-assigned labels are
1495
+ // dropped, and value is sourced from inputValues/formFields alike.
1496
+ fields: authoredActionFields(c),
1497
+ captures: arr(c.declaredOutputs).filter((x) => typeof x === "string"),
1498
+ // Drop assertion `id` (random UUID) — only the semantic content matters.
1499
+ assertions: arr(c.inlineAssertions).map((a) => ({
1500
+ condition: str(prop(a, "condition")) ?? "",
1501
+ severity: str(prop(a, "severity")) ?? "fail",
1502
+ agentic: prop(a, "agentic") === true
1503
+ })),
1504
+ captureAudio: c.captureAudio === true,
1505
+ captureLabel: str(c.captureLabel) ?? null,
1506
+ captureExtraMs: num(c.captureExtraMs) ?? null,
1507
+ alwaysUseAgent: c.alwaysUseAgent === true
1508
+ };
1509
+ case "assertion":
1510
+ return {
1511
+ condition: canon(str(c.condition)),
1512
+ description: canon(str(c.description) ?? str(c.condition)),
1513
+ // Legacy `strict:true` is equivalent to severity 'fail' at runtime; map
1514
+ // it so a strict assertion doesn't silently weaken to 'warn'.
1515
+ severity: str(c.severity) ?? (c.strict === true ? "fail" : "warn"),
1516
+ expectedOutcome: str(c.expectedOutcome) ?? null,
1517
+ alwaysUseAgent: c.alwaysUseAgent === true
1518
+ };
1519
+ case "seed":
1520
+ return {
1521
+ specName: canon(str(c.seedWorkflowName)),
1522
+ snapshotVersion: c.snapshotVersion ?? c.seedSnapshotVersion ?? null,
1523
+ onFailure: str(c.onFailure) ?? str(c.seedOnFailure) ?? null,
1524
+ executionMode: str(c.executionMode) ?? str(c.seedExecutionMode) ?? null,
1525
+ scope: str(c.scope) ?? null,
1526
+ runTeardown: typeof c.runTeardown === "boolean" ? c.runTeardown : null
1527
+ };
1528
+ case "setup":
1529
+ return {
1530
+ specName: canon(str(c.setupFlowName)),
1531
+ snapshotVersion: c.snapshotVersion ?? null,
1532
+ mode: str(c.executionMode) ?? null
1533
+ };
1534
+ case "wait":
1535
+ return { delaySeconds: num(c.delaySeconds) ?? num(c.waitDuration) ?? null };
1536
+ case "condition":
1537
+ return {
1538
+ expression: canon(
1539
+ str(c.conditionDescription) ?? str(c.conditionExpression) ?? str(c.description)
1540
+ ),
1541
+ hints: str(c.evaluationHints) ?? null
1542
+ };
1543
+ case "notification":
1544
+ return {
1545
+ channel: str(c.channel) ?? null,
1546
+ to: str(c.to) ?? null,
1547
+ subject: str(c.subject) ?? null,
1548
+ body: str(c.body) ?? null,
1549
+ attach: str(c.attachmentRef) ?? null
1550
+ };
1551
+ case "audio":
1552
+ return {
1553
+ attachmentRef: str(c.attachmentRef) ?? null,
1554
+ loop: c.loop === true,
1555
+ waitForCompletion: c.waitForCompletion !== false,
1556
+ label: str(c.label) ?? null
1557
+ };
1558
+ case "audio-assertion": {
1559
+ const det = obj(c.deterministic);
1560
+ const nl = obj(c.naturalLanguage);
1561
+ return {
1562
+ label: str(c.label) ?? null,
1563
+ severity: str(c.severity) ?? "fail",
1564
+ prompt: str(nl?.prompt) ?? str(c.prompt) ?? null,
1565
+ model: str(nl?.model) ?? str(c.model) ?? null,
1566
+ minDurationMs: num(det?.minDurationMs) ?? num(c.minDurationMs) ?? null,
1567
+ maxDurationMs: num(det?.maxDurationMs) ?? num(c.maxDurationMs) ?? null,
1568
+ minRmsDb: num(det?.minRmsDb) ?? num(c.minRmsDb) ?? null,
1569
+ silenceFractionMax: num(det?.silenceFractionMax) ?? num(c.silenceFractionMax) ?? null,
1570
+ reference: str(c.referenceAttachmentRef) ?? null,
1571
+ envelopeCorrelationMin: num(det?.envelopeCorrelationMin) ?? num(c.envelopeCorrelationMin) ?? null
1572
+ };
1573
+ }
1574
+ case "api_sequence":
1575
+ return {};
1576
+ case "end":
1577
+ return { outcome: str(c.outcome) ?? "pass" };
1578
+ default:
1579
+ return {};
1580
+ }
1581
+ }
1582
+ function authoredActionTitle(raw) {
1583
+ const c = raw ?? {};
1584
+ const explicit = str(c.customActionTitle) ?? str(c.title);
1585
+ if (explicit && explicit.trim()) return explicit.trim();
1586
+ const desc = (str(c.actionDescription) ?? "").replace(/\*/g, "").replace(/\s+/g, " ").trim();
1587
+ if (!desc) return "Action";
1588
+ const sentence = desc.split(/(?<=[.!?])\s/)[0] ?? desc;
1589
+ return sentence.slice(0, 80).trim() || "Action";
1590
+ }
1591
+ function authoredActionFields(raw) {
1592
+ const c = raw ?? {};
1593
+ const inputValues = c.inputValues && typeof c.inputValues === "object" ? c.inputValues : {};
1594
+ const formFields = arr(c.formFields);
1595
+ const formByName = /* @__PURE__ */ new Map();
1596
+ for (const f of formFields) {
1597
+ const n = str(prop(f, "name"));
1598
+ if (n) formByName.set(n, f);
1599
+ }
1600
+ const reqs = arr(c.dataRequirements);
1601
+ const source = reqs.length > 0 ? reqs : formFields;
1602
+ const out = [];
1603
+ for (const s of source) {
1604
+ const name = str(prop(s, "name"));
1605
+ if (!name) continue;
1606
+ const value = str(inputValues[name]) ?? str(prop(formByName.get(name), "value")) ?? "";
1607
+ out.push({ name, type: str(prop(s, "type")) ?? "text", value });
1608
+ }
1609
+ return out;
1610
+ }
1611
+ function loginCredentialRef(raw) {
1612
+ const c = raw ?? {};
1613
+ const seedRef = c.seedCredentialRef;
1614
+ return str(c.credentialTemplateName) ?? str(c.credentialName) ?? str(c.credentialId) ?? str(c.credentialTemplateId) ?? // For from_seed bindings, `dataKey` is the stable identifier (matches the
1615
+ // seed's declared-output key); `name` is a display label that can drift.
1616
+ // Prefer dataKey so spec round-trip (which uses `seed-output <dataKey>`)
1617
+ // hash-matches the live row regardless of label.
1618
+ str(prop(seedRef, "dataKey")) ?? str(prop(seedRef, "name"));
1619
+ }
1620
+ function sortObjectKeys(value) {
1621
+ if (Array.isArray(value)) return value.map(sortObjectKeys);
1622
+ if (value !== null && typeof value === "object") {
1623
+ const obj3 = value;
1624
+ return Object.keys(obj3).sort().reduce((acc, k) => {
1625
+ acc[k] = sortObjectKeys(obj3[k]);
1626
+ return acc;
1627
+ }, {});
1628
+ }
1629
+ return value;
1630
+ }
1631
+ function canon(v) {
1632
+ if (v === void 0) return null;
1633
+ return v.replace(/[`"]/g, "").replace(/\.$/, "").trim();
1634
+ }
1635
+ function trimText(v) {
1636
+ return v === void 0 ? null : v.trim();
1637
+ }
1638
+ function str(v) {
1639
+ return typeof v === "string" ? v : void 0;
1640
+ }
1641
+ function num(v) {
1642
+ return typeof v === "number" && Number.isFinite(v) ? v : void 0;
1643
+ }
1644
+ function arr(v) {
1645
+ return Array.isArray(v) ? v : [];
1646
+ }
1647
+ function prop(v, key) {
1648
+ return v && typeof v === "object" ? v[key] : void 0;
1649
+ }
1650
+ function obj(v) {
1651
+ return v && typeof v === "object" && !Array.isArray(v) ? v : null;
1652
+ }
1653
+
1654
+ // ../canary-spec/src/diff.ts
1655
+ var NodeTypeMutationError = class extends Error {
1656
+ constructor(nodeId, fromType, toType) {
1657
+ super(
1658
+ `Node ${nodeId} cannot change type from "${fromType}" to "${toType}". Remove the @id from the spec and let the diff CREATE a fresh node.`
1659
+ );
1660
+ this.nodeId = nodeId;
1661
+ this.name = "NodeTypeMutationError";
1662
+ }
1663
+ nodeId;
1664
+ };
1665
+ function diffWorkflow(spec, live) {
1666
+ const ops = [];
1667
+ const duplicateSpecIds = [];
1668
+ const orphanedSpecIds = [];
1669
+ const liveById = /* @__PURE__ */ new Map();
1670
+ for (const n of live.nodes) liveById.set(n.id, n);
1671
+ const consumedLiveIds = /* @__PURE__ */ new Set();
1672
+ const seenSpecIds = /* @__PURE__ */ new Set();
1673
+ const pairedOrderIndexes = [];
1674
+ spec.nodes.forEach((specNode, specIndex) => {
1675
+ let pairedLive;
1676
+ if (specNode.nodeId) {
1677
+ if (seenSpecIds.has(specNode.nodeId)) {
1678
+ duplicateSpecIds.push(specNode.nodeId);
1679
+ } else {
1680
+ seenSpecIds.add(specNode.nodeId);
1681
+ const candidate = liveById.get(specNode.nodeId);
1682
+ if (candidate && !consumedLiveIds.has(candidate.id)) {
1683
+ pairedLive = candidate;
1684
+ consumedLiveIds.add(candidate.id);
1685
+ } else {
1686
+ orphanedSpecIds.push(specNode.nodeId);
1687
+ }
1688
+ }
1689
+ } else if (specNode.nodeType === "login") {
1690
+ const liveLogin = live.nodes.find(
1691
+ (n) => n.nodeType === "login" && !consumedLiveIds.has(n.id)
1692
+ );
1693
+ if (liveLogin) {
1694
+ pairedLive = liveLogin;
1695
+ consumedLiveIds.add(liveLogin.id);
1696
+ }
1697
+ }
1698
+ if (pairedLive) {
1699
+ if (pairedLive.nodeType !== specNode.nodeType) {
1700
+ throw new NodeTypeMutationError(pairedLive.id, pairedLive.nodeType, specNode.nodeType);
1701
+ }
1702
+ const configChanged = authoredNodeHash(pairedLive) !== authoredNodeHash({ nodeType: specNode.nodeType, configJson: specNode.configJson });
1703
+ pairedOrderIndexes.push(pairedLive.orderIndex);
1704
+ ops.push({
1705
+ kind: "update",
1706
+ nodeId: pairedLive.id,
1707
+ specIndex,
1708
+ nodeType: specNode.nodeType,
1709
+ configJson: specNode.configJson,
1710
+ orderIndex: specIndex,
1711
+ changes: {
1712
+ // orderChanged is finalized after the pass (needs the full sequence).
1713
+ configChanged,
1714
+ orderChanged: false,
1715
+ fromOrderIndex: pairedLive.orderIndex
1716
+ }
1717
+ });
1718
+ } else {
1719
+ ops.push({
1720
+ kind: "create",
1721
+ specIndex,
1722
+ nodeType: specNode.nodeType,
1723
+ configJson: specNode.configJson,
1724
+ orderIndex: specIndex,
1725
+ sourceLine: specNode.sourceLine
1726
+ });
1727
+ }
1728
+ });
1729
+ const relativeOrderPreserved = pairedOrderIndexes.every(
1730
+ (v, i) => i === 0 || v >= pairedOrderIndexes[i - 1]
1731
+ );
1732
+ if (!relativeOrderPreserved) {
1733
+ for (const op of ops) {
1734
+ if (op.kind === "update") {
1735
+ op.changes.orderChanged = op.changes.fromOrderIndex !== op.orderIndex;
1736
+ }
1737
+ }
1738
+ }
1739
+ for (const liveNode of live.nodes) {
1740
+ if (!consumedLiveIds.has(liveNode.id)) {
1741
+ ops.push({
1742
+ kind: "delete",
1743
+ nodeId: liveNode.id,
1744
+ nodeType: liveNode.nodeType,
1745
+ orderIndex: liveNode.orderIndex
1746
+ });
1747
+ }
1748
+ }
1749
+ const workflowFieldChanges = {};
1750
+ if (spec.description !== live.description) workflowFieldChanges.description = spec.description;
1751
+ if (spec.preflight !== live.preflight) workflowFieldChanges.preflight = spec.preflight;
1752
+ if (spec.cleanup !== live.cleanup) workflowFieldChanges.cleanup = spec.cleanup;
1753
+ if (spec.status !== void 0 && spec.status !== live.status) {
1754
+ workflowFieldChanges.status = spec.status;
1755
+ }
1756
+ if (spec.flowType !== void 0 && spec.flowType !== live.flowType) {
1757
+ workflowFieldChanges.flowType = spec.flowType;
1758
+ }
1759
+ const specApiOnly = spec.apiOnly === true;
1760
+ const liveApiOnly = live.apiOnly === true;
1761
+ if (specApiOnly !== liveApiOnly) workflowFieldChanges.apiOnly = specApiOnly;
1762
+ if (spec.tags !== void 0) {
1763
+ const specSet = [...spec.tags].sort();
1764
+ const liveSet = [...(live.tags ?? []).map((t) => t.name)].sort();
1765
+ if (specSet.length !== liveSet.length || specSet.some((v, i) => v !== liveSet[i])) {
1766
+ workflowFieldChanges.tags = spec.tags;
1767
+ }
1768
+ }
1769
+ if (spec.environmentConstraints !== void 0) {
1770
+ if (!sameEnvConstraints(spec.environmentConstraints, live.environmentConstraints)) {
1771
+ workflowFieldChanges.environmentConstraints = spec.environmentConstraints;
1772
+ }
1773
+ }
1774
+ if (spec.sandboxTemplate !== void 0) {
1775
+ const liveName = live.sandboxTemplate?.name ?? null;
1776
+ if (spec.sandboxTemplate !== liveName) {
1777
+ workflowFieldChanges.sandboxTemplate = spec.sandboxTemplate;
1778
+ }
1779
+ }
1780
+ if (spec.teardownSpec !== void 0) {
1781
+ const liveName = live.teardownWorkflowName ?? null;
1782
+ if (spec.teardownSpec !== liveName) workflowFieldChanges.teardownSpec = spec.teardownSpec;
1783
+ }
1784
+ if (spec.teardownSnapshotVersion !== void 0) {
1785
+ const specVal = spec.teardownSnapshotVersion === "latest" ? null : spec.teardownSnapshotVersion;
1786
+ const liveVal = live.teardownSnapshotVersion ?? null;
1787
+ if (specVal !== liveVal) {
1788
+ workflowFieldChanges.teardownSnapshotVersion = spec.teardownSnapshotVersion;
1789
+ }
1790
+ }
1791
+ if (spec.teardownEnabled !== void 0) {
1792
+ const liveVal = live.teardownEnabled === true;
1793
+ if (spec.teardownEnabled !== liveVal) {
1794
+ workflowFieldChanges.teardownEnabled = spec.teardownEnabled;
1795
+ }
1796
+ }
1797
+ if (spec.seedOutputs !== void 0) {
1798
+ const liveByKey = new Map(
1799
+ (live.seedOutputs ?? []).map((o) => [o.dataKey, o])
1800
+ );
1801
+ const sliced = (live.seedOutputs ?? []).map((o) => ({
1802
+ dataKey: o.dataKey,
1803
+ label: o.displayLabel,
1804
+ description: o.description,
1805
+ propertyName: o.propertyName
1806
+ }));
1807
+ const specSliced = spec.seedOutputs.map((o) => ({
1808
+ dataKey: o.dataKey,
1809
+ label: o.label,
1810
+ description: o.description,
1811
+ propertyName: o.propertyName
1812
+ }));
1813
+ if (!seedOutputsEqual(sliced, specSliced)) {
1814
+ workflowFieldChanges.seedOutputs = spec.seedOutputs.map((o) => {
1815
+ const liveRow = liveByKey.get(o.dataKey);
1816
+ return {
1817
+ dataKey: o.dataKey,
1818
+ label: o.label,
1819
+ description: o.description,
1820
+ propertyName: o.propertyName,
1821
+ ...liveRow?.credentialOutput !== void 0 ? { credentialOutput: liveRow.credentialOutput } : {},
1822
+ ...liveRow?.attributes !== void 0 ? { attributes: liveRow.attributes } : {}
1823
+ };
1824
+ });
1825
+ }
1826
+ }
1827
+ if (!deepEquals(authoredInputs(live.inputsJson), authoredInputs(spec.inputsJson))) {
1828
+ workflowFieldChanges.inputsJson = spec.inputsJson;
1829
+ }
1830
+ if (!deepEquals(authoredScenarios(live.scenariosJson), authoredScenarios(spec.scenariosJson))) {
1831
+ workflowFieldChanges.scenariosJson = spec.scenariosJson;
1832
+ }
1833
+ const nameDrift = spec.name !== live.name ? { from: live.name, to: spec.name } : null;
1834
+ if (nameDrift) workflowFieldChanges.name = spec.name;
1835
+ return { ops, workflowFieldChanges, nameDrift, duplicateSpecIds, orphanedSpecIds };
1836
+ }
1837
+ function authoredInputs(raw) {
1838
+ if (!Array.isArray(raw)) return [];
1839
+ const out = [];
1840
+ for (const i of raw) {
1841
+ if (!i || typeof i !== "object") continue;
1842
+ const rec = i;
1843
+ if (typeof rec.name !== "string") continue;
1844
+ out.push({
1845
+ kind: rec.kind === "credential" ? "credential" : "value",
1846
+ name: rec.name,
1847
+ defaultValue: typeof rec.defaultValue === "string" ? rec.defaultValue : ""
1848
+ });
1849
+ }
1850
+ return out;
1851
+ }
1852
+ function authoredScenarios(raw) {
1853
+ if (!Array.isArray(raw)) return [];
1854
+ const out = [];
1855
+ for (const s of raw) {
1856
+ if (!s || typeof s !== "object") continue;
1857
+ const rec = s;
1858
+ if (typeof rec.name !== "string") continue;
1859
+ out.push({
1860
+ name: rec.name,
1861
+ description: typeof rec.description === "string" ? rec.description : "",
1862
+ isDefault: rec.isDefault === true,
1863
+ includeInSuite: rec.includeInSuite !== false,
1864
+ values: strRecord(rec.values),
1865
+ credentials: strRecord(rec.credentials)
1866
+ });
1867
+ }
1868
+ return out;
1869
+ }
1870
+ function sameEnvConstraints(spec, live) {
1871
+ if (spec === null || spec === void 0) return !live;
1872
+ if (!live) return false;
1873
+ if (spec.mode !== live.mode) return false;
1874
+ if (spec.mode === "only" && live.mode === "only") {
1875
+ const a = [...spec.environmentNames].sort();
1876
+ const b = [...live.environmentNames ?? []].sort();
1877
+ return a.length === b.length && a.every((v, i) => v === b[i]);
1878
+ }
1879
+ if (spec.mode === "envTypes" && live.mode === "envTypes") {
1880
+ const a = [...spec.types].sort();
1881
+ const b = [...live.types].sort();
1882
+ return a.length === b.length && a.every((v, i) => v === b[i]);
1883
+ }
1884
+ return false;
1885
+ }
1886
+ function seedOutputsEqual(a, b) {
1887
+ if (a.length !== b.length) return false;
1888
+ const sortByKey = (rows) => [...rows].sort((x, y) => x.dataKey.localeCompare(y.dataKey));
1889
+ const xa = sortByKey(a);
1890
+ const xb = sortByKey(b);
1891
+ for (let i = 0; i < xa.length; i++) {
1892
+ if (xa[i].dataKey !== xb[i].dataKey) return false;
1893
+ if ((xa[i].label ?? "") !== (xb[i].label ?? "")) return false;
1894
+ if ((xa[i].description ?? "") !== (xb[i].description ?? "")) return false;
1895
+ if ((xa[i].propertyName ?? "") !== (xb[i].propertyName ?? "")) return false;
1896
+ }
1897
+ return true;
1898
+ }
1899
+ function strRecord(v) {
1900
+ const out = {};
1901
+ if (v && typeof v === "object") {
1902
+ for (const [k, val] of Object.entries(v)) {
1903
+ if (typeof val === "string") out[k] = val;
1904
+ }
1905
+ }
1906
+ return out;
1907
+ }
1908
+ function deepEquals(a, b) {
1909
+ if (a === b) return true;
1910
+ if (a === null || b === null) return a === b;
1911
+ if (typeof a !== typeof b) return false;
1912
+ if (typeof a !== "object") return false;
1913
+ if (Array.isArray(a) !== Array.isArray(b)) return false;
1914
+ if (Array.isArray(a) && Array.isArray(b)) {
1915
+ if (a.length !== b.length) return false;
1916
+ for (let i = 0; i < a.length; i++) {
1917
+ if (!deepEquals(a[i], b[i])) return false;
1918
+ }
1919
+ return true;
1920
+ }
1921
+ const ao = a;
1922
+ const bo = b;
1923
+ const ak = Object.keys(ao);
1924
+ const bk = Object.keys(bo);
1925
+ if (ak.length !== bk.length) return false;
1926
+ for (const k of ak) {
1927
+ if (!Object.prototype.hasOwnProperty.call(bo, k)) return false;
1928
+ if (!deepEquals(ao[k], bo[k])) return false;
1929
+ }
1930
+ return true;
1931
+ }
1932
+ function summarize(diff) {
1933
+ let created = 0;
1934
+ let updated = 0;
1935
+ let deleted = 0;
1936
+ let moved = 0;
1937
+ let unchanged = 0;
1938
+ for (const op of diff.ops) {
1939
+ if (op.kind === "create") created++;
1940
+ else if (op.kind === "delete") deleted++;
1941
+ else if (op.kind === "update") {
1942
+ if (op.changes.configChanged) updated++;
1943
+ if (op.changes.orderChanged) moved++;
1944
+ if (!op.changes.configChanged && !op.changes.orderChanged) unchanged++;
1945
+ }
1946
+ }
1947
+ return { created, updated, deleted, moved, unchanged };
1948
+ }
1949
+ function hasAnyChange(diff) {
1950
+ if (diff.ops.some((op) => op.kind !== "update" || op.changes.configChanged || op.changes.orderChanged)) return true;
1951
+ return Object.keys(diff.workflowFieldChanges).length > 0;
1952
+ }
1953
+ function reconcileLinearEdges(finalNodeIds, liveEdges) {
1954
+ const desired = /* @__PURE__ */ new Set();
1955
+ for (let i = 0; i < finalNodeIds.length - 1; i++) {
1956
+ desired.add(`${finalNodeIds[i]}\u2192${finalNodeIds[i + 1]}`);
1957
+ }
1958
+ const ops = [];
1959
+ const haveDefault = /* @__PURE__ */ new Set();
1960
+ for (const e of liveEdges) {
1961
+ if (e.edgeType !== "default") continue;
1962
+ const key = `${e.sourceNodeId}\u2192${e.targetNodeId}`;
1963
+ if (desired.has(key)) {
1964
+ haveDefault.add(key);
1965
+ } else {
1966
+ ops.push({ kind: "delete", edgeId: e.id });
1967
+ }
1968
+ }
1969
+ for (const key of desired) {
1970
+ if (haveDefault.has(key)) continue;
1971
+ const [sourceNodeId, targetNodeId] = key.split("\u2192");
1972
+ ops.push({ kind: "create", sourceNodeId, targetNodeId });
1973
+ }
1974
+ return { ops };
1975
+ }
1976
+
1977
+ // ../canary-spec/src/pull.ts
1978
+ function workflowToSpec(workflow, opts = {}) {
1979
+ const lines = [];
1980
+ const sorted = [...workflow.nodes].sort((a, b) => a.orderIndex - b.orderIndex);
1981
+ let firstNonSeed = 0;
1982
+ while (firstNonSeed < sorted.length && (sorted[firstNonSeed].nodeType === "seed" || sorted[firstNonSeed].nodeType === "setup")) {
1983
+ firstNonSeed++;
1984
+ }
1985
+ const authLogin = sorted[firstNonSeed]?.nodeType === "login" ? sorted[firstNonSeed] : void 0;
1986
+ const authenticated = !!authLogin;
1987
+ const actor = authLogin ? inferActor(authLogin) : null;
1988
+ lines.push("---");
1989
+ lines.push(`name: ${yamlScalar(workflow.name)}`);
1990
+ lines.push(`workflowId: ${workflow.id}`);
1991
+ if (workflow.status === "draft" || workflow.status === "quarantined") {
1992
+ lines.push(`status: ${workflow.status}`);
1993
+ }
1994
+ if (workflow.flowType && workflow.flowType !== "standard") {
1995
+ lines.push(`flowType: ${workflow.flowType}`);
1996
+ }
1997
+ if (actor) lines.push(`actor: ${yamlScalar(actor)}`);
1998
+ lines.push(`authenticated: ${authenticated ? "true" : "false"}`);
1999
+ if (workflow.apiOnly === true) lines.push("apiOnly: true");
2000
+ const tags = (workflow.tags ?? []).map((t) => t.name).filter(Boolean);
2001
+ if (tags.length > 0) lines.push(`tags: [${tags.map(yamlScalar).join(", ")}]`);
2002
+ const envBlock = renderEnvironments(workflow.environmentConstraints);
2003
+ if (envBlock) lines.push(...envBlock);
2004
+ if (workflow.sandboxTemplate?.name) {
2005
+ lines.push(`sandboxTemplate: ${yamlScalar(workflow.sandboxTemplate.name)}`);
2006
+ }
2007
+ const teardownLines = renderTeardown(workflow);
2008
+ if (teardownLines) lines.push(...teardownLines);
2009
+ if (opts.requirementRef) lines.push(`requirementRef: ${yamlScalar(opts.requirementRef)}`);
2010
+ lines.push("---");
2011
+ lines.push("");
2012
+ lines.push(`# ${workflow.name}`);
2013
+ lines.push("");
2014
+ if (workflow.description) {
2015
+ lines.push("## Spirit");
2016
+ lines.push(blockquote(workflow.description));
2017
+ lines.push("");
2018
+ }
2019
+ if (workflow.preflight) {
2020
+ lines.push("## Preflight");
2021
+ lines.push(workflow.preflight.trim());
2022
+ lines.push("");
2023
+ }
2024
+ if (workflow.cleanup) {
2025
+ lines.push("## Cleanup");
2026
+ lines.push(workflow.cleanup.trim());
2027
+ lines.push("");
2028
+ }
2029
+ const inputs = readInputs(workflow.inputsJson);
2030
+ if (inputs.length > 0) {
2031
+ lines.push("## Inputs");
2032
+ for (const i of inputs) {
2033
+ const prefix = i.kind === "credential" ? "credential " : "";
2034
+ lines.push(`- ${prefix}${i.name}: ${inputValue(i.defaultValue)}`);
2035
+ }
2036
+ lines.push("");
2037
+ }
2038
+ const scenarios = readScenarios(workflow.scenariosJson);
2039
+ const skipReasons = opts.preserveScenarioSkipReasons ?? {};
2040
+ if (scenarios.length > 0) {
2041
+ lines.push("## Scenarios");
2042
+ lines.push("");
2043
+ for (const s of scenarios) {
2044
+ let heading = `### ${s.name}`;
2045
+ const skipReason = skipReasons[s.name];
2046
+ if (s.isDefault) heading += " *(default)*";
2047
+ else if (!s.includeInSuite) {
2048
+ heading += skipReason ? ` *(skip: ${skipReason})*` : " *(skip)*";
2049
+ }
2050
+ lines.push(heading);
2051
+ if (s.description) lines.push(`- description: ${s.description}`);
2052
+ if (Object.keys(s.values).length > 0) {
2053
+ lines.push("- values:");
2054
+ for (const [k, v] of Object.entries(s.values)) lines.push(` ${k}: ${yamlScalar(v)}`);
2055
+ }
2056
+ if (Object.keys(s.credentials).length > 0) {
2057
+ lines.push("- credentials:");
2058
+ for (const [k, v] of Object.entries(s.credentials))
2059
+ lines.push(` ${k}: ${yamlScalar(v)}`);
2060
+ }
2061
+ lines.push("");
2062
+ }
2063
+ }
2064
+ const declaredOutputBlock = renderDeclaredOutputs(workflow.seedOutputs);
2065
+ if (declaredOutputBlock) {
2066
+ lines.push("## Declared Outputs");
2067
+ lines.push("");
2068
+ lines.push(...declaredOutputBlock);
2069
+ lines.push("");
2070
+ }
2071
+ lines.push("## Journey");
2072
+ lines.push("");
2073
+ for (const node of sorted) {
2074
+ if (node === authLogin) continue;
2075
+ const block = renderNode(node);
2076
+ if (block.length === 0) continue;
2077
+ block[0] = `${block[0]} <!-- @id ${node.id} -->`;
2078
+ lines.push(...block);
2079
+ lines.push("");
2080
+ }
2081
+ while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
2082
+ lines.push("");
2083
+ return lines.join("\n");
2084
+ }
2085
+ function renderNode(node) {
2086
+ const c = node.configJson ?? {};
2087
+ switch (node.nodeType) {
2088
+ case "login": {
2089
+ return [`**Switch role to ${renderLoginRef(c)}**`];
2090
+ }
2091
+ case "navigate":
2092
+ return [`**Navigate to** \`${str2(c.pageUrl) ?? "/"}\`.`];
2093
+ case "action": {
2094
+ const title = authoredActionTitle(c);
2095
+ const out = [`**Action \u2014 ${title}**`];
2096
+ const desc = str2(c.actionDescription);
2097
+ if (desc && desc !== title) out.push(blockquote(desc));
2098
+ for (const f of authoredActionFields(c)) {
2099
+ const typeSuffix = f.type && f.type !== "text" ? ` (${f.type})` : "";
2100
+ const name = /^[A-Za-z0-9_-]+$/.test(f.name) ? f.name : `"${f.name}"`;
2101
+ out.push(`- field ${name}${typeSuffix}: ${inputValue(f.value)}`);
2102
+ }
2103
+ for (const cap of arr2(c.declaredOutputs)) {
2104
+ if (typeof cap === "string") out.push(`- capture: ${cap}`);
2105
+ }
2106
+ for (const a of arr2(c.inlineAssertions)) {
2107
+ const cond = str2(prop2(a, "condition"));
2108
+ if (!cond) continue;
2109
+ const sev = str2(prop2(a, "severity")) ?? "fail";
2110
+ const flags = prop2(a, "agentic") === true ? `${sev}, agentic` : sev;
2111
+ out.push(`- assert (${flags}): ${cond}`);
2112
+ }
2113
+ if (c.captureAudio === true) {
2114
+ out.push("- captureAudio: true");
2115
+ if (str2(c.captureLabel)) out.push(`- captureLabel: ${str2(c.captureLabel)}`);
2116
+ if (num2(c.captureExtraMs) !== void 0)
2117
+ out.push(`- captureExtraMs: ${num2(c.captureExtraMs)}`);
2118
+ }
2119
+ if (c.alwaysUseAgent === true) out.push("- alwaysUseAgent: true");
2120
+ return out;
2121
+ }
2122
+ case "assertion": {
2123
+ const cond = str2(c.condition) ?? "?";
2124
+ const out = [`**Assert** ${cond}`];
2125
+ const desc = str2(c.description);
2126
+ if (desc && desc !== cond) out.push(blockquote(desc));
2127
+ const severity = str2(c.severity) ?? (c.strict === true ? "fail" : "warn");
2128
+ out.push(`- severity: ${severity}`);
2129
+ if (str2(c.expectedOutcome)) out.push(`- expectedOutcome: ${str2(c.expectedOutcome)}`);
2130
+ if (c.alwaysUseAgent === true) out.push("- alwaysUseAgent: true");
2131
+ return out;
2132
+ }
2133
+ case "seed": {
2134
+ const name = str2(c.seedWorkflowName) ?? "?";
2135
+ const out = [`**Seed** \`${name}\`.`];
2136
+ const snap = c.snapshotVersion ?? c.seedSnapshotVersion;
2137
+ if (typeof snap === "number") out.push(`- version: ${snap}`);
2138
+ const onFailure = str2(c.onFailure) ?? str2(c.seedOnFailure);
2139
+ if (onFailure) out.push(`- onFailure: ${onFailure}`);
2140
+ const mode = str2(c.executionMode) ?? str2(c.seedExecutionMode);
2141
+ if (mode) out.push(`- mode: ${mode}`);
2142
+ if (str2(c.scope)) out.push(`- scope: ${str2(c.scope)}`);
2143
+ if (typeof c.runTeardown === "boolean") out.push(`- runTeardown: ${c.runTeardown}`);
2144
+ return out;
2145
+ }
2146
+ case "setup": {
2147
+ const name = str2(c.setupFlowName) ?? "?";
2148
+ const out = [`**Setup** \`${name}\`.`];
2149
+ if (typeof c.snapshotVersion === "number") out.push(`- version: ${c.snapshotVersion}`);
2150
+ if (str2(c.executionMode)) out.push(`- mode: ${str2(c.executionMode)}`);
2151
+ return out;
2152
+ }
2153
+ case "wait": {
2154
+ const secs = num2(c.delaySeconds) ?? num2(c.waitDuration) ?? 0;
2155
+ return [`**Wait** ${secs}s.`];
2156
+ }
2157
+ case "condition": {
2158
+ const expr = str2(c.conditionDescription) ?? str2(c.conditionExpression) ?? str2(c.description) ?? "?";
2159
+ return expr.includes("\n") ? ["**If**", blockquote(expr)] : [`**If ${expr}**`];
2160
+ }
2161
+ case "notification": {
2162
+ const channel = str2(c.channel) ?? "email";
2163
+ const to = str2(c.to) ?? "?";
2164
+ const out = [`**Notify ${channel}: ${to}**`];
2165
+ if (str2(c.body)) out.push(blockquote(str2(c.body)));
2166
+ if (str2(c.subject)) out.push(`- subject: ${str2(c.subject)}`);
2167
+ if (str2(c.attachmentRef)) out.push(`- attach: ${str2(c.attachmentRef)}`);
2168
+ return out;
2169
+ }
2170
+ case "audio": {
2171
+ const ref = str2(c.attachmentRef) ?? "?";
2172
+ const out = [`**Play audio** \`${ref}\``];
2173
+ if (c.loop === true) out.push("- loop: true");
2174
+ if (c.waitForCompletion === false) out.push("- waitForCompletion: false");
2175
+ if (str2(c.label)) out.push(`- label: ${str2(c.label)}`);
2176
+ return out;
2177
+ }
2178
+ case "audio-assertion": {
2179
+ const det = obj2(c.deterministic);
2180
+ const nl = obj2(c.naturalLanguage);
2181
+ const prompt = str2(nl?.prompt) ?? str2(c.prompt);
2182
+ const model = str2(nl?.model) ?? str2(c.model);
2183
+ const out = [`**Assert audio** ${str2(c.label) ?? "?"}`];
2184
+ out.push(`- severity: ${str2(c.severity) ?? "fail"}`);
2185
+ if (prompt) out.push(`- prompt: ${prompt}`);
2186
+ if (model) out.push(`- model: ${model}`);
2187
+ for (const [cfgKey, bulletKey] of [
2188
+ ["minDurationMs", "minDurationMs"],
2189
+ ["maxDurationMs", "maxDurationMs"],
2190
+ ["minRmsDb", "minRmsDb"],
2191
+ ["silenceFractionMax", "silenceFractionMax"],
2192
+ ["envelopeCorrelationMin", "envelopeCorrelationMin"]
2193
+ ]) {
2194
+ const value = num2(det?.[cfgKey]) ?? num2(c[cfgKey]);
2195
+ if (value !== void 0) out.push(`- ${bulletKey}: ${value}`);
2196
+ }
2197
+ if (str2(c.referenceAttachmentRef)) out.push(`- reference: ${str2(c.referenceAttachmentRef)}`);
2198
+ return out;
2199
+ }
2200
+ case "api_sequence": {
2201
+ const title = str2(c.customActionTitle) ?? str2(c.title) ?? "API sequence";
2202
+ const out = [`**API sequence \u2014 ${title}**`];
2203
+ if (str2(c.apiEndpointContext)) out.push(blockquote(str2(c.apiEndpointContext)));
2204
+ if (str2(c.credentialInputName)) out.push(`- credential: ${str2(c.credentialInputName)}`);
2205
+ if (str2(c.__apiSequenceSidecar)) {
2206
+ out.push(`- apiSequence: ${str2(c.__apiSequenceSidecar)}`);
2207
+ }
2208
+ return out;
2209
+ }
2210
+ case "end":
2211
+ return [str2(c.outcome) === "fail" ? "**End (fail)**." : "**End**."];
2212
+ default:
2213
+ return [];
2214
+ }
2215
+ }
2216
+ function extractPreservedFrontmatter(priorSource) {
2217
+ const result = {};
2218
+ const fmMatch = priorSource.match(/^---\n([\s\S]*?)\n---/);
2219
+ if (fmMatch) {
2220
+ const reqMatch = fmMatch[1].match(/^requirementRef:\s*(.+)$/m);
2221
+ if (reqMatch) {
2222
+ const raw = reqMatch[1].trim();
2223
+ const unquoted = raw.startsWith('"') && raw.endsWith('"') ? JSON.parse(raw) : raw;
2224
+ result.requirementRef = unquoted;
2225
+ }
2226
+ }
2227
+ const skipReasons = {};
2228
+ const re = /^###\s+(.+?)\s+\*\(skip:\s*(.+?)\)\*\s*$/gm;
2229
+ let m;
2230
+ while ((m = re.exec(priorSource)) !== null) {
2231
+ const name = m[1].trim();
2232
+ const reason = m[2].trim();
2233
+ if (name && reason) skipReasons[name] = reason;
2234
+ }
2235
+ if (Object.keys(skipReasons).length > 0) {
2236
+ result.preserveScenarioSkipReasons = skipReasons;
2237
+ }
2238
+ return result;
2239
+ }
2240
+ function renderDeclaredOutputs(outputs) {
2241
+ if (!outputs || outputs.length === 0) return null;
2242
+ const out = [];
2243
+ for (const o of outputs) {
2244
+ out.push(`- ${o.dataKey}`);
2245
+ const label = o.displayLabel ?? o.name;
2246
+ if (label && label !== o.dataKey) out.push(` - label: ${yamlScalar(label)}`);
2247
+ if (o.propertyName) out.push(` - property: ${yamlScalar(o.propertyName)}`);
2248
+ if (o.description) out.push(` - description: ${o.description}`);
2249
+ if (o.credentialOutput !== void 0) {
2250
+ out.push(" <!-- credentialOutput config preserved by the server; edit in UI for now -->");
2251
+ }
2252
+ out.push("");
2253
+ }
2254
+ while (out.length > 0 && out[out.length - 1] === "") out.pop();
2255
+ return out;
2256
+ }
2257
+ function renderTeardown(workflow) {
2258
+ if (!workflow.teardownWorkflowId) return null;
2259
+ const name = workflow.teardownWorkflowName;
2260
+ if (!name) return null;
2261
+ const snap = workflow.teardownSnapshotVersion;
2262
+ const enabled = workflow.teardownEnabled !== false;
2263
+ if ((snap === null || snap === void 0) && enabled) {
2264
+ return [`teardown: ${yamlScalar(name)}`];
2265
+ }
2266
+ const out = ["teardown:", ` spec: ${yamlScalar(name)}`];
2267
+ if (snap !== null && snap !== void 0) out.push(` snapshotVersion: ${snap}`);
2268
+ if (!enabled) out.push(" enabled: false");
2269
+ return out;
2270
+ }
2271
+ function renderEnvironments(constraints) {
2272
+ if (!constraints) return null;
2273
+ if (constraints.mode === "only") {
2274
+ const names = constraints.environmentNames;
2275
+ if (!names || names.length === 0) return null;
2276
+ return ["environments:", ` only: [${names.map(yamlScalar).join(", ")}]`];
2277
+ }
2278
+ return ["environments:", ` types: [${constraints.types.map(yamlScalar).join(", ")}]`];
2279
+ }
2280
+ function renderLoginRef(c) {
2281
+ if (c.credentialSource === "from_seed") {
2282
+ const seedRef = c.seedCredentialRef;
2283
+ const ref = typeof seedRef?.dataKey === "string" && seedRef.dataKey || typeof seedRef?.name === "string" && seedRef.name;
2284
+ if (ref) return `seed-output ${ref}`;
2285
+ }
2286
+ const role = str2(c.credentialTemplateName);
2287
+ if (role) return role;
2288
+ const cred = loginCredentialRef(c);
2289
+ if (cred) return `credential ${cred}`;
2290
+ return "?";
2291
+ }
2292
+ function inferActor(login) {
2293
+ const c = login.configJson ?? {};
2294
+ if (c.credentialSource === "from_seed") {
2295
+ const seedRef = c.seedCredentialRef;
2296
+ const ref = typeof seedRef?.dataKey === "string" && seedRef.dataKey || typeof seedRef?.name === "string" && seedRef.name;
2297
+ if (ref) return `seed-output ${ref}`;
2298
+ }
2299
+ const role = str2(c.credentialTemplateName);
2300
+ if (role) return role;
2301
+ const cred = loginCredentialRef(c);
2302
+ if (cred) return `credential ${cred}`;
2303
+ return null;
2304
+ }
2305
+ function readInputs(raw) {
2306
+ if (!Array.isArray(raw)) return [];
2307
+ const out = [];
2308
+ for (const i of raw) {
2309
+ if (!i || typeof i !== "object") continue;
2310
+ const rec = i;
2311
+ if (typeof rec.name !== "string") continue;
2312
+ out.push({
2313
+ kind: rec.kind === "credential" ? "credential" : "value",
2314
+ name: rec.name,
2315
+ defaultValue: typeof rec.defaultValue === "string" ? rec.defaultValue : ""
2316
+ });
2317
+ }
2318
+ return out;
2319
+ }
2320
+ function readScenarios(raw) {
2321
+ if (!Array.isArray(raw)) return [];
2322
+ const out = [];
2323
+ for (const s of raw) {
2324
+ if (!s || typeof s !== "object") continue;
2325
+ const rec = s;
2326
+ if (typeof rec.name !== "string") continue;
2327
+ out.push({
2328
+ name: rec.name,
2329
+ description: typeof rec.description === "string" ? rec.description : void 0,
2330
+ isDefault: rec.isDefault === true,
2331
+ includeInSuite: rec.includeInSuite !== false,
2332
+ values: strRecord2(rec.values),
2333
+ credentials: strRecord2(rec.credentials)
2334
+ });
2335
+ }
2336
+ return out;
2337
+ }
2338
+ function inputValue(v) {
2339
+ return v === "" ? '""' : v;
2340
+ }
2341
+ function blockquote(text) {
2342
+ return text.trim().split("\n").map((l) => `> ${l}`).join("\n");
2343
+ }
2344
+ function yamlScalar(s) {
2345
+ if (s === "") return '""';
2346
+ if (/^[A-Za-z0-9 _\-./@]+$/.test(s) && !/^\s|\s$/.test(s)) return s;
2347
+ return JSON.stringify(s);
2348
+ }
2349
+ function strRecord2(v) {
2350
+ const out = {};
2351
+ if (v && typeof v === "object") {
2352
+ for (const [k, val] of Object.entries(v)) {
2353
+ if (typeof val === "string") out[k] = val;
2354
+ }
2355
+ }
2356
+ return out;
2357
+ }
2358
+ function str2(v) {
2359
+ return typeof v === "string" ? v : void 0;
2360
+ }
2361
+ function num2(v) {
2362
+ return typeof v === "number" && Number.isFinite(v) ? v : void 0;
2363
+ }
2364
+ function arr2(v) {
2365
+ return Array.isArray(v) ? v : [];
2366
+ }
2367
+ function prop2(v, key) {
2368
+ return v && typeof v === "object" ? v[key] : void 0;
2369
+ }
2370
+ function obj2(v) {
2371
+ return v && typeof v === "object" && !Array.isArray(v) ? v : null;
2372
+ }
2373
+
2374
+ // ../canary-spec/src/api-sequence-file.ts
2375
+ import { z as z2 } from "zod";
2376
+ var ApiSequenceValidationSchema = z2.object({
2377
+ kind: z2.string().min(1).optional()
2378
+ }).passthrough();
2379
+ var ApiSequenceStepSchema = z2.object({
2380
+ stepId: z2.string().min(1).optional(),
2381
+ apiRequestId: z2.string().min(1),
2382
+ credentialId: z2.string().nullable().optional(),
2383
+ method: z2.string().min(1).optional(),
2384
+ urlTemplate: z2.string().optional(),
2385
+ queryParams: z2.array(z2.object({ key: z2.string().min(1), value: z2.string() })).optional(),
2386
+ bodyTemplate: z2.string().nullable().optional(),
2387
+ headersTemplate: z2.record(z2.string(), z2.string()).nullable().optional(),
2388
+ responseVariableName: z2.string().min(1).optional(),
2389
+ expectedStatusCodes: z2.array(z2.number().int()).optional(),
2390
+ description: z2.string().optional(),
2391
+ validations: z2.array(ApiSequenceValidationSchema).optional()
2392
+ });
2393
+ var ApiSequenceExportSchema = z2.object({
2394
+ variableName: z2.string().min(1),
2395
+ stepId: z2.string().min(1),
2396
+ responsePath: z2.string().min(1),
2397
+ source: z2.enum(["request", "response"]).optional(),
2398
+ description: z2.string().optional()
2399
+ });
2400
+ var ApiSequenceFileSchema = z2.object({
2401
+ description: z2.string().optional(),
2402
+ stopOnFailure: z2.boolean().optional(),
2403
+ steps: z2.array(ApiSequenceStepSchema).default([]),
2404
+ exports: z2.array(ApiSequenceExportSchema).default([])
2405
+ });
2406
+
2407
+ // ../canary-spec/src/writeback.ts
2408
+ var FRONTMATTER_FENCE2 = /^---\s*$/;
2409
+ function writeWorkflowId(source, workflowId) {
2410
+ const lines = source.split(/\r?\n/);
2411
+ if (!FRONTMATTER_FENCE2.test(lines[0] ?? "")) return source;
2412
+ const end = findFrontmatterEnd2(lines);
2413
+ if (end === -1) return source;
2414
+ for (let i = 1; i < end; i++) {
2415
+ if (/^workflowId\s*:/.test(lines[i])) return source;
2416
+ }
2417
+ let insertAt = end;
2418
+ for (let i = 1; i < end; i++) {
2419
+ if (/^name\s*:/.test(lines[i])) {
2420
+ insertAt = i + 1;
2421
+ break;
2422
+ }
2423
+ }
2424
+ const out = [...lines];
2425
+ out.splice(insertAt, 0, `workflowId: ${workflowId}`);
2426
+ return out.join("\n");
2427
+ }
2428
+ function writeNodeIds(source, pairs) {
2429
+ const lines = source.split(/\r?\n/);
2430
+ const NODE_ID_RE2 = /<!--\s*@id\s+[0-9a-f-]+\s*-->/i;
2431
+ for (const { sourceLine, nodeId } of pairs) {
2432
+ if (sourceLine < 0 || sourceLine >= lines.length) continue;
2433
+ const line = lines[sourceLine];
2434
+ if (NODE_ID_RE2.test(line)) continue;
2435
+ lines[sourceLine] = `${line.trimEnd()} <!-- @id ${nodeId} -->`;
2436
+ }
2437
+ return lines.join("\n");
2438
+ }
2439
+ function findFrontmatterEnd2(lines) {
2440
+ if (!FRONTMATTER_FENCE2.test(lines[0] ?? "")) return -1;
2441
+ for (let i = 1; i < lines.length; i++) if (FRONTMATTER_FENCE2.test(lines[i])) return i;
2442
+ return -1;
2443
+ }
2444
+
2445
+ export {
2446
+ parseSpec,
2447
+ lintSpec,
2448
+ emitSpec,
2449
+ diffWorkflow,
2450
+ summarize,
2451
+ hasAnyChange,
2452
+ reconcileLinearEdges,
2453
+ workflowToSpec,
2454
+ extractPreservedFrontmatter,
2455
+ ApiSequenceFileSchema,
2456
+ writeWorkflowId,
2457
+ writeNodeIds
2458
+ };
2459
+ //# sourceMappingURL=chunk-B2VISN2V.js.map