@principal-ai/subsystems-core 0.29.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 (614) hide show
  1. package/README.md +124 -0
  2. package/dist/ConfigurationLoader.d.ts +76 -0
  3. package/dist/ConfigurationLoader.d.ts.map +1 -0
  4. package/dist/ConfigurationLoader.js +148 -0
  5. package/dist/ConfigurationLoader.js.map +1 -0
  6. package/dist/ConfigurationValidator.d.ts +31 -0
  7. package/dist/ConfigurationValidator.d.ts.map +1 -0
  8. package/dist/ConfigurationValidator.js +239 -0
  9. package/dist/ConfigurationValidator.js.map +1 -0
  10. package/dist/EventProcessor.d.ts +49 -0
  11. package/dist/EventProcessor.d.ts.map +1 -0
  12. package/dist/EventProcessor.js +220 -0
  13. package/dist/EventProcessor.js.map +1 -0
  14. package/dist/EventRecorderService.d.ts +305 -0
  15. package/dist/EventRecorderService.d.ts.map +1 -0
  16. package/dist/EventRecorderService.js +467 -0
  17. package/dist/EventRecorderService.js.map +1 -0
  18. package/dist/LibraryLoader.d.ts +65 -0
  19. package/dist/LibraryLoader.d.ts.map +1 -0
  20. package/dist/LibraryLoader.js +197 -0
  21. package/dist/LibraryLoader.js.map +1 -0
  22. package/dist/PathBasedEventProcessor.d.ts +77 -0
  23. package/dist/PathBasedEventProcessor.d.ts.map +1 -0
  24. package/dist/PathBasedEventProcessor.js +136 -0
  25. package/dist/PathBasedEventProcessor.js.map +1 -0
  26. package/dist/SessionManager.d.ts +194 -0
  27. package/dist/SessionManager.d.ts.map +1 -0
  28. package/dist/SessionManager.js +302 -0
  29. package/dist/SessionManager.js.map +1 -0
  30. package/dist/ValidationEngine.d.ts +31 -0
  31. package/dist/ValidationEngine.d.ts.map +1 -0
  32. package/dist/ValidationEngine.js +162 -0
  33. package/dist/ValidationEngine.js.map +1 -0
  34. package/dist/agent-sessions/fixture.d.ts +80 -0
  35. package/dist/agent-sessions/fixture.d.ts.map +1 -0
  36. package/dist/agent-sessions/fixture.js +130 -0
  37. package/dist/agent-sessions/fixture.js.map +1 -0
  38. package/dist/agent-sessions/index.d.ts +3 -0
  39. package/dist/agent-sessions/index.d.ts.map +1 -0
  40. package/dist/agent-sessions/index.js +6 -0
  41. package/dist/agent-sessions/index.js.map +1 -0
  42. package/dist/auxiliary/AuxiliaryManifestValidator.d.ts +41 -0
  43. package/dist/auxiliary/AuxiliaryManifestValidator.d.ts.map +1 -0
  44. package/dist/auxiliary/AuxiliaryManifestValidator.js +100 -0
  45. package/dist/auxiliary/AuxiliaryManifestValidator.js.map +1 -0
  46. package/dist/auxiliary/index.d.ts +9 -0
  47. package/dist/auxiliary/index.d.ts.map +1 -0
  48. package/dist/auxiliary/index.js +14 -0
  49. package/dist/auxiliary/index.js.map +1 -0
  50. package/dist/auxiliary/validateAreaScopeDisjoint.d.ts +24 -0
  51. package/dist/auxiliary/validateAreaScopeDisjoint.d.ts.map +1 -0
  52. package/dist/auxiliary/validateAreaScopeDisjoint.js +61 -0
  53. package/dist/auxiliary/validateAreaScopeDisjoint.js.map +1 -0
  54. package/dist/cli/codegen.d.ts +41 -0
  55. package/dist/cli/codegen.d.ts.map +1 -0
  56. package/dist/cli/codegen.js +257 -0
  57. package/dist/cli/codegen.js.map +1 -0
  58. package/dist/codegen/type-generator.d.ts +123 -0
  59. package/dist/codegen/type-generator.d.ts.map +1 -0
  60. package/dist/codegen/type-generator.js +433 -0
  61. package/dist/codegen/type-generator.js.map +1 -0
  62. package/dist/codegen/usage-example.d.ts +103 -0
  63. package/dist/codegen/usage-example.d.ts.map +1 -0
  64. package/dist/codegen/usage-example.js +258 -0
  65. package/dist/codegen/usage-example.js.map +1 -0
  66. package/dist/dashboard/DashboardValidator.d.ts +85 -0
  67. package/dist/dashboard/DashboardValidator.d.ts.map +1 -0
  68. package/dist/dashboard/DashboardValidator.js +775 -0
  69. package/dist/dashboard/DashboardValidator.js.map +1 -0
  70. package/dist/dashboard/index.d.ts +6 -0
  71. package/dist/dashboard/index.d.ts.map +1 -0
  72. package/dist/dashboard/index.js +10 -0
  73. package/dist/dashboard/index.js.map +1 -0
  74. package/dist/discovery/CanvasDiscovery.d.ts +160 -0
  75. package/dist/discovery/CanvasDiscovery.d.ts.map +1 -0
  76. package/dist/discovery/CanvasDiscovery.js +865 -0
  77. package/dist/discovery/CanvasDiscovery.js.map +1 -0
  78. package/dist/discovery/CanvasFileManifest.d.ts +86 -0
  79. package/dist/discovery/CanvasFileManifest.d.ts.map +1 -0
  80. package/dist/discovery/CanvasFileManifest.js +338 -0
  81. package/dist/discovery/CanvasFileManifest.js.map +1 -0
  82. package/dist/discovery/LibraryDiscovery.d.ts +108 -0
  83. package/dist/discovery/LibraryDiscovery.d.ts.map +1 -0
  84. package/dist/discovery/LibraryDiscovery.js +263 -0
  85. package/dist/discovery/LibraryDiscovery.js.map +1 -0
  86. package/dist/discovery/types.d.ts +403 -0
  87. package/dist/discovery/types.d.ts.map +1 -0
  88. package/dist/discovery/types.js +6 -0
  89. package/dist/discovery/types.js.map +1 -0
  90. package/dist/events/EventsCanvasValidator.d.ts +139 -0
  91. package/dist/events/EventsCanvasValidator.d.ts.map +1 -0
  92. package/dist/events/EventsCanvasValidator.js +281 -0
  93. package/dist/events/EventsCanvasValidator.js.map +1 -0
  94. package/dist/events/NamespacePathIndex.d.ts +61 -0
  95. package/dist/events/NamespacePathIndex.d.ts.map +1 -0
  96. package/dist/events/NamespacePathIndex.js +74 -0
  97. package/dist/events/NamespacePathIndex.js.map +1 -0
  98. package/dist/events/OtelEventPathsValidator.d.ts +75 -0
  99. package/dist/events/OtelEventPathsValidator.d.ts.map +1 -0
  100. package/dist/events/OtelEventPathsValidator.js +126 -0
  101. package/dist/events/OtelEventPathsValidator.js.map +1 -0
  102. package/dist/events/ScopeEventsValidator.d.ts +68 -0
  103. package/dist/events/ScopeEventsValidator.d.ts.map +1 -0
  104. package/dist/events/ScopeEventsValidator.js +91 -0
  105. package/dist/events/ScopeEventsValidator.js.map +1 -0
  106. package/dist/events/index.d.ts +9 -0
  107. package/dist/events/index.d.ts.map +1 -0
  108. package/dist/events/index.js +12 -0
  109. package/dist/events/index.js.map +1 -0
  110. package/dist/events/path-helpers.d.ts +33 -0
  111. package/dist/events/path-helpers.d.ts.map +1 -0
  112. package/dist/events/path-helpers.js +59 -0
  113. package/dist/events/path-helpers.js.map +1 -0
  114. package/dist/execution/ExecutionLoader.d.ts +135 -0
  115. package/dist/execution/ExecutionLoader.d.ts.map +1 -0
  116. package/dist/execution/ExecutionLoader.js +266 -0
  117. package/dist/execution/ExecutionLoader.js.map +1 -0
  118. package/dist/execution/ExecutionValidator.d.ts +94 -0
  119. package/dist/execution/ExecutionValidator.d.ts.map +1 -0
  120. package/dist/execution/ExecutionValidator.js +503 -0
  121. package/dist/execution/ExecutionValidator.js.map +1 -0
  122. package/dist/generated/graph-converter-execution.types.d.ts +177 -0
  123. package/dist/generated/graph-converter-execution.types.d.ts.map +1 -0
  124. package/dist/generated/graph-converter-execution.types.js +12 -0
  125. package/dist/generated/graph-converter-execution.types.js.map +1 -0
  126. package/dist/helpers/GraphInstrumentationHelper.d.ts +93 -0
  127. package/dist/helpers/GraphInstrumentationHelper.d.ts.map +1 -0
  128. package/dist/helpers/GraphInstrumentationHelper.js +252 -0
  129. package/dist/helpers/GraphInstrumentationHelper.js.map +1 -0
  130. package/dist/index.d.ts +82 -0
  131. package/dist/index.d.ts.map +1 -0
  132. package/dist/index.js +214 -0
  133. package/dist/index.js.map +1 -0
  134. package/dist/matchers/SpanMatcher.d.ts +107 -0
  135. package/dist/matchers/SpanMatcher.d.ts.map +1 -0
  136. package/dist/matchers/SpanMatcher.js +274 -0
  137. package/dist/matchers/SpanMatcher.js.map +1 -0
  138. package/dist/node.d.ts +78 -0
  139. package/dist/node.d.ts.map +1 -0
  140. package/dist/node.js +177 -0
  141. package/dist/node.js.map +1 -0
  142. package/dist/opencode/OpenCodeEventStore.d.ts +25 -0
  143. package/dist/opencode/OpenCodeEventStore.d.ts.map +1 -0
  144. package/dist/opencode/OpenCodeEventStore.js +182 -0
  145. package/dist/opencode/OpenCodeEventStore.js.map +1 -0
  146. package/dist/opencode/agent-sessions.d.ts +46 -0
  147. package/dist/opencode/agent-sessions.d.ts.map +1 -0
  148. package/dist/opencode/agent-sessions.js +302 -0
  149. package/dist/opencode/agent-sessions.js.map +1 -0
  150. package/dist/opencode/index.d.ts +6 -0
  151. package/dist/opencode/index.d.ts.map +1 -0
  152. package/dist/opencode/index.js +15 -0
  153. package/dist/opencode/index.js.map +1 -0
  154. package/dist/opencode/node-path-adapter.d.ts +25 -0
  155. package/dist/opencode/node-path-adapter.d.ts.map +1 -0
  156. package/dist/opencode/node-path-adapter.js +188 -0
  157. package/dist/opencode/node-path-adapter.js.map +1 -0
  158. package/dist/opencode/pipeline.d.ts +42 -0
  159. package/dist/opencode/pipeline.d.ts.map +1 -0
  160. package/dist/opencode/pipeline.js +90 -0
  161. package/dist/opencode/pipeline.js.map +1 -0
  162. package/dist/opencode/types.d.ts +39 -0
  163. package/dist/opencode/types.d.ts.map +1 -0
  164. package/dist/opencode/types.js +3 -0
  165. package/dist/opencode/types.js.map +1 -0
  166. package/dist/orchestration/TraceOrchestrator.d.ts +71 -0
  167. package/dist/orchestration/TraceOrchestrator.d.ts.map +1 -0
  168. package/dist/orchestration/TraceOrchestrator.js +416 -0
  169. package/dist/orchestration/TraceOrchestrator.js.map +1 -0
  170. package/dist/parsers/OtlpTraceParser.d.ts +131 -0
  171. package/dist/parsers/OtlpTraceParser.d.ts.map +1 -0
  172. package/dist/parsers/OtlpTraceParser.js +310 -0
  173. package/dist/parsers/OtlpTraceParser.js.map +1 -0
  174. package/dist/registry/CompositeRegistry.d.ts +71 -0
  175. package/dist/registry/CompositeRegistry.d.ts.map +1 -0
  176. package/dist/registry/CompositeRegistry.js +113 -0
  177. package/dist/registry/CompositeRegistry.js.map +1 -0
  178. package/dist/registry/EventRegistry.d.ts +79 -0
  179. package/dist/registry/EventRegistry.d.ts.map +1 -0
  180. package/dist/registry/EventRegistry.js +165 -0
  181. package/dist/registry/EventRegistry.js.map +1 -0
  182. package/dist/registry/LocalRegistry.d.ts +111 -0
  183. package/dist/registry/LocalRegistry.d.ts.map +1 -0
  184. package/dist/registry/LocalRegistry.js +335 -0
  185. package/dist/registry/LocalRegistry.js.map +1 -0
  186. package/dist/registry/RemoteRegistry.d.ts +122 -0
  187. package/dist/registry/RemoteRegistry.d.ts.map +1 -0
  188. package/dist/registry/RemoteRegistry.js +399 -0
  189. package/dist/registry/RemoteRegistry.js.map +1 -0
  190. package/dist/registry/VersionRegistry.d.ts +51 -0
  191. package/dist/registry/VersionRegistry.d.ts.map +1 -0
  192. package/dist/registry/VersionRegistry.js +105 -0
  193. package/dist/registry/VersionRegistry.js.map +1 -0
  194. package/dist/rules/config.d.ts +57 -0
  195. package/dist/rules/config.d.ts.map +1 -0
  196. package/dist/rules/config.js +382 -0
  197. package/dist/rules/config.js.map +1 -0
  198. package/dist/rules/engine.d.ts +70 -0
  199. package/dist/rules/engine.d.ts.map +1 -0
  200. package/dist/rules/engine.js +373 -0
  201. package/dist/rules/engine.js.map +1 -0
  202. package/dist/rules/implementations/connection-type-references.d.ts +7 -0
  203. package/dist/rules/implementations/connection-type-references.d.ts.map +1 -0
  204. package/dist/rules/implementations/connection-type-references.js +107 -0
  205. package/dist/rules/implementations/connection-type-references.js.map +1 -0
  206. package/dist/rules/implementations/dead-end-states.d.ts +17 -0
  207. package/dist/rules/implementations/dead-end-states.d.ts.map +1 -0
  208. package/dist/rules/implementations/dead-end-states.js +75 -0
  209. package/dist/rules/implementations/dead-end-states.js.map +1 -0
  210. package/dist/rules/implementations/index.d.ts +23 -0
  211. package/dist/rules/implementations/index.d.ts.map +1 -0
  212. package/dist/rules/implementations/index.js +73 -0
  213. package/dist/rules/implementations/index.js.map +1 -0
  214. package/dist/rules/implementations/minimum-node-sources.d.ts +24 -0
  215. package/dist/rules/implementations/minimum-node-sources.d.ts.map +1 -0
  216. package/dist/rules/implementations/minimum-node-sources.js +59 -0
  217. package/dist/rules/implementations/minimum-node-sources.js.map +1 -0
  218. package/dist/rules/implementations/no-unknown-fields.d.ts +7 -0
  219. package/dist/rules/implementations/no-unknown-fields.d.ts.map +1 -0
  220. package/dist/rules/implementations/no-unknown-fields.js +216 -0
  221. package/dist/rules/implementations/no-unknown-fields.js.map +1 -0
  222. package/dist/rules/implementations/orphaned-edge-types.d.ts +7 -0
  223. package/dist/rules/implementations/orphaned-edge-types.d.ts.map +1 -0
  224. package/dist/rules/implementations/orphaned-edge-types.js +50 -0
  225. package/dist/rules/implementations/orphaned-edge-types.js.map +1 -0
  226. package/dist/rules/implementations/orphaned-node-types.d.ts +7 -0
  227. package/dist/rules/implementations/orphaned-node-types.d.ts.map +1 -0
  228. package/dist/rules/implementations/orphaned-node-types.js +53 -0
  229. package/dist/rules/implementations/orphaned-node-types.js.map +1 -0
  230. package/dist/rules/implementations/required-metadata.d.ts +7 -0
  231. package/dist/rules/implementations/required-metadata.d.ts.map +1 -0
  232. package/dist/rules/implementations/required-metadata.js +48 -0
  233. package/dist/rules/implementations/required-metadata.js.map +1 -0
  234. package/dist/rules/implementations/state-transition-references.d.ts +7 -0
  235. package/dist/rules/implementations/state-transition-references.d.ts.map +1 -0
  236. package/dist/rules/implementations/state-transition-references.js +138 -0
  237. package/dist/rules/implementations/state-transition-references.js.map +1 -0
  238. package/dist/rules/implementations/unreachable-states.d.ts +7 -0
  239. package/dist/rules/implementations/unreachable-states.d.ts.map +1 -0
  240. package/dist/rules/implementations/unreachable-states.js +83 -0
  241. package/dist/rules/implementations/unreachable-states.js.map +1 -0
  242. package/dist/rules/implementations/valid-action-patterns.d.ts +17 -0
  243. package/dist/rules/implementations/valid-action-patterns.d.ts.map +1 -0
  244. package/dist/rules/implementations/valid-action-patterns.js +112 -0
  245. package/dist/rules/implementations/valid-action-patterns.js.map +1 -0
  246. package/dist/rules/implementations/valid-color-format.d.ts +7 -0
  247. package/dist/rules/implementations/valid-color-format.d.ts.map +1 -0
  248. package/dist/rules/implementations/valid-color-format.js +94 -0
  249. package/dist/rules/implementations/valid-color-format.js.map +1 -0
  250. package/dist/rules/implementations/valid-edge-types.d.ts +7 -0
  251. package/dist/rules/implementations/valid-edge-types.d.ts.map +1 -0
  252. package/dist/rules/implementations/valid-edge-types.js +248 -0
  253. package/dist/rules/implementations/valid-edge-types.js.map +1 -0
  254. package/dist/rules/implementations/valid-node-types.d.ts +7 -0
  255. package/dist/rules/implementations/valid-node-types.d.ts.map +1 -0
  256. package/dist/rules/implementations/valid-node-types.js +187 -0
  257. package/dist/rules/implementations/valid-node-types.js.map +1 -0
  258. package/dist/rules/index.d.ts +28 -0
  259. package/dist/rules/index.d.ts.map +1 -0
  260. package/dist/rules/index.js +70 -0
  261. package/dist/rules/index.js.map +1 -0
  262. package/dist/rules/types.d.ts +309 -0
  263. package/dist/rules/types.d.ts.map +1 -0
  264. package/dist/rules/types.js +40 -0
  265. package/dist/rules/types.js.map +1 -0
  266. package/dist/scopes/ScopePathIndex.d.ts +56 -0
  267. package/dist/scopes/ScopePathIndex.d.ts.map +1 -0
  268. package/dist/scopes/ScopePathIndex.js +67 -0
  269. package/dist/scopes/ScopePathIndex.js.map +1 -0
  270. package/dist/scopes/ScopesCanvasValidator.d.ts +76 -0
  271. package/dist/scopes/ScopesCanvasValidator.d.ts.map +1 -0
  272. package/dist/scopes/ScopesCanvasValidator.js +221 -0
  273. package/dist/scopes/ScopesCanvasValidator.js.map +1 -0
  274. package/dist/scopes/index.d.ts +12 -0
  275. package/dist/scopes/index.d.ts.map +1 -0
  276. package/dist/scopes/index.js +30 -0
  277. package/dist/scopes/index.js.map +1 -0
  278. package/dist/scopes/utils.d.ts +85 -0
  279. package/dist/scopes/utils.d.ts.map +1 -0
  280. package/dist/scopes/utils.js +143 -0
  281. package/dist/scopes/utils.js.map +1 -0
  282. package/dist/scopes/validateScopeNamespaceNesting.d.ts +38 -0
  283. package/dist/scopes/validateScopeNamespaceNesting.d.ts.map +1 -0
  284. package/dist/scopes/validateScopeNamespaceNesting.js +69 -0
  285. package/dist/scopes/validateScopeNamespaceNesting.js.map +1 -0
  286. package/dist/spans/index.d.ts +12 -0
  287. package/dist/spans/index.d.ts.map +1 -0
  288. package/dist/spans/index.js +25 -0
  289. package/dist/spans/index.js.map +1 -0
  290. package/dist/spans/utils.d.ts +70 -0
  291. package/dist/spans/utils.d.ts.map +1 -0
  292. package/dist/spans/utils.js +205 -0
  293. package/dist/spans/utils.js.map +1 -0
  294. package/dist/storage/topic-types.d.ts +203 -0
  295. package/dist/storage/topic-types.d.ts.map +1 -0
  296. package/dist/storage/topic-types.js +60 -0
  297. package/dist/storage/topic-types.js.map +1 -0
  298. package/dist/storage/topicStore.d.ts +135 -0
  299. package/dist/storage/topicStore.d.ts.map +1 -0
  300. package/dist/storage/topicStore.js +389 -0
  301. package/dist/storage/topicStore.js.map +1 -0
  302. package/dist/storyboard/builder.d.ts +135 -0
  303. package/dist/storyboard/builder.d.ts.map +1 -0
  304. package/dist/storyboard/builder.js +235 -0
  305. package/dist/storyboard/builder.js.map +1 -0
  306. package/dist/storyboard/index.d.ts +9 -0
  307. package/dist/storyboard/index.d.ts.map +1 -0
  308. package/dist/storyboard/index.js +18 -0
  309. package/dist/storyboard/index.js.map +1 -0
  310. package/dist/storyboard/types.d.ts +101 -0
  311. package/dist/storyboard/types.d.ts.map +1 -0
  312. package/dist/storyboard/types.js +10 -0
  313. package/dist/storyboard/types.js.map +1 -0
  314. package/dist/telemetry/coverage.d.ts +92 -0
  315. package/dist/telemetry/coverage.d.ts.map +1 -0
  316. package/dist/telemetry/coverage.js +335 -0
  317. package/dist/telemetry/coverage.js.map +1 -0
  318. package/dist/telemetry/event-validator.d.ts +86 -0
  319. package/dist/telemetry/event-validator.d.ts.map +1 -0
  320. package/dist/telemetry/event-validator.js +257 -0
  321. package/dist/telemetry/event-validator.js.map +1 -0
  322. package/dist/types/audit.d.ts +236 -0
  323. package/dist/types/audit.d.ts.map +1 -0
  324. package/dist/types/audit.js +53 -0
  325. package/dist/types/audit.js.map +1 -0
  326. package/dist/types/auxiliary.d.ts +54 -0
  327. package/dist/types/auxiliary.d.ts.map +1 -0
  328. package/dist/types/auxiliary.js +27 -0
  329. package/dist/types/auxiliary.js.map +1 -0
  330. package/dist/types/canvas-scope.d.ts +156 -0
  331. package/dist/types/canvas-scope.d.ts.map +1 -0
  332. package/dist/types/canvas-scope.js +62 -0
  333. package/dist/types/canvas-scope.js.map +1 -0
  334. package/dist/types/canvas.d.ts +1138 -0
  335. package/dist/types/canvas.d.ts.map +1 -0
  336. package/dist/types/canvas.js +178 -0
  337. package/dist/types/canvas.js.map +1 -0
  338. package/dist/types/dashboard.d.ts +170 -0
  339. package/dist/types/dashboard.d.ts.map +1 -0
  340. package/dist/types/dashboard.js +14 -0
  341. package/dist/types/dashboard.js.map +1 -0
  342. package/dist/types/index.d.ts +352 -0
  343. package/dist/types/index.d.ts.map +1 -0
  344. package/dist/types/index.js +39 -0
  345. package/dist/types/index.js.map +1 -0
  346. package/dist/types/library.d.ts +227 -0
  347. package/dist/types/library.d.ts.map +1 -0
  348. package/dist/types/library.js +16 -0
  349. package/dist/types/library.js.map +1 -0
  350. package/dist/types/otel.d.ts +465 -0
  351. package/dist/types/otel.d.ts.map +1 -0
  352. package/dist/types/otel.js +163 -0
  353. package/dist/types/otel.js.map +1 -0
  354. package/dist/types/path-based-config.d.ts +235 -0
  355. package/dist/types/path-based-config.d.ts.map +1 -0
  356. package/dist/types/path-based-config.js +11 -0
  357. package/dist/types/path-based-config.js.map +1 -0
  358. package/dist/types/registered-trace.d.ts +337 -0
  359. package/dist/types/registered-trace.d.ts.map +1 -0
  360. package/dist/types/registered-trace.js +16 -0
  361. package/dist/types/registered-trace.js.map +1 -0
  362. package/dist/types/resource-match.d.ts +207 -0
  363. package/dist/types/resource-match.d.ts.map +1 -0
  364. package/dist/types/resource-match.js +74 -0
  365. package/dist/types/resource-match.js.map +1 -0
  366. package/dist/types/subsystem-model.d.ts +265 -0
  367. package/dist/types/subsystem-model.d.ts.map +1 -0
  368. package/dist/types/subsystem-model.js +35 -0
  369. package/dist/types/subsystem-model.js.map +1 -0
  370. package/dist/types/version-registry.d.ts +137 -0
  371. package/dist/types/version-registry.d.ts.map +1 -0
  372. package/dist/types/version-registry.js +10 -0
  373. package/dist/types/version-registry.js.map +1 -0
  374. package/dist/utils/CanvasConverter.d.ts +148 -0
  375. package/dist/utils/CanvasConverter.d.ts.map +1 -0
  376. package/dist/utils/CanvasConverter.js +530 -0
  377. package/dist/utils/CanvasConverter.js.map +1 -0
  378. package/dist/utils/GraphConverter.d.ts +18 -0
  379. package/dist/utils/GraphConverter.d.ts.map +1 -0
  380. package/dist/utils/GraphConverter.js +80 -0
  381. package/dist/utils/GraphConverter.js.map +1 -0
  382. package/dist/utils/PathMatcher.d.ts +55 -0
  383. package/dist/utils/PathMatcher.d.ts.map +1 -0
  384. package/dist/utils/PathMatcher.js +178 -0
  385. package/dist/utils/PathMatcher.js.map +1 -0
  386. package/dist/utils/TraceToCanvas.d.ts +83 -0
  387. package/dist/utils/TraceToCanvas.d.ts.map +1 -0
  388. package/dist/utils/TraceToCanvas.js +336 -0
  389. package/dist/utils/TraceToCanvas.js.map +1 -0
  390. package/dist/utils/YamlParser.d.ts +36 -0
  391. package/dist/utils/YamlParser.d.ts.map +1 -0
  392. package/dist/utils/YamlParser.js +92 -0
  393. package/dist/utils/YamlParser.js.map +1 -0
  394. package/dist/utils/registeredTraceHelpers.d.ts +76 -0
  395. package/dist/utils/registeredTraceHelpers.d.ts.map +1 -0
  396. package/dist/utils/registeredTraceHelpers.js +203 -0
  397. package/dist/utils/registeredTraceHelpers.js.map +1 -0
  398. package/dist/utils/traceAggregation.d.ts +25 -0
  399. package/dist/utils/traceAggregation.d.ts.map +1 -0
  400. package/dist/utils/traceAggregation.js +140 -0
  401. package/dist/utils/traceAggregation.js.map +1 -0
  402. package/dist/validation/OtelCanvasValidator.d.ts +40 -0
  403. package/dist/validation/OtelCanvasValidator.d.ts.map +1 -0
  404. package/dist/validation/OtelCanvasValidator.js +59 -0
  405. package/dist/validation/OtelCanvasValidator.js.map +1 -0
  406. package/dist/validation/index.d.ts +8 -0
  407. package/dist/validation/index.d.ts.map +1 -0
  408. package/dist/validation/index.js +13 -0
  409. package/dist/validation/index.js.map +1 -0
  410. package/dist/validation/libraryValidation.d.ts +53 -0
  411. package/dist/validation/libraryValidation.d.ts.map +1 -0
  412. package/dist/validation/libraryValidation.js +115 -0
  413. package/dist/validation/libraryValidation.js.map +1 -0
  414. package/dist/workflow/edge-derivation.d.ts +111 -0
  415. package/dist/workflow/edge-derivation.d.ts.map +1 -0
  416. package/dist/workflow/edge-derivation.js +174 -0
  417. package/dist/workflow/edge-derivation.js.map +1 -0
  418. package/dist/workflow/edge-validation.d.ts +170 -0
  419. package/dist/workflow/edge-validation.d.ts.map +1 -0
  420. package/dist/workflow/edge-validation.js +332 -0
  421. package/dist/workflow/edge-validation.js.map +1 -0
  422. package/dist/workflow/example.d.ts +11 -0
  423. package/dist/workflow/example.d.ts.map +1 -0
  424. package/dist/workflow/example.js +310 -0
  425. package/dist/workflow/example.js.map +1 -0
  426. package/dist/workflow/index.d.ts +20 -0
  427. package/dist/workflow/index.d.ts.map +1 -0
  428. package/dist/workflow/index.js +49 -0
  429. package/dist/workflow/index.js.map +1 -0
  430. package/dist/workflow/scenario-matcher.d.ts +63 -0
  431. package/dist/workflow/scenario-matcher.d.ts.map +1 -0
  432. package/dist/workflow/scenario-matcher.js +215 -0
  433. package/dist/workflow/scenario-matcher.js.map +1 -0
  434. package/dist/workflow/template-parser.d.ts +73 -0
  435. package/dist/workflow/template-parser.d.ts.map +1 -0
  436. package/dist/workflow/template-parser.js +280 -0
  437. package/dist/workflow/template-parser.js.map +1 -0
  438. package/dist/workflow/template-renderer.d.ts +45 -0
  439. package/dist/workflow/template-renderer.d.ts.map +1 -0
  440. package/dist/workflow/template-renderer.js +401 -0
  441. package/dist/workflow/template-renderer.js.map +1 -0
  442. package/dist/workflow/types.d.ts +393 -0
  443. package/dist/workflow/types.d.ts.map +1 -0
  444. package/dist/workflow/types.js +40 -0
  445. package/dist/workflow/types.js.map +1 -0
  446. package/dist/workflow/validator.d.ts +363 -0
  447. package/dist/workflow/validator.d.ts.map +1 -0
  448. package/dist/workflow/validator.js +2283 -0
  449. package/dist/workflow/validator.js.map +1 -0
  450. package/package.json +84 -0
  451. package/schemas/auxiliary.manifest.schema.json +56 -0
  452. package/schemas/subsystem-model.schema.json +767 -0
  453. package/src/ConfigurationLoader.test.ts +466 -0
  454. package/src/ConfigurationLoader.ts +184 -0
  455. package/src/ConfigurationValidator.test.ts +200 -0
  456. package/src/ConfigurationValidator.ts +273 -0
  457. package/src/EventProcessor.test.ts +405 -0
  458. package/src/EventProcessor.ts +256 -0
  459. package/src/EventRecorderService.test.ts +543 -0
  460. package/src/EventRecorderService.ts +741 -0
  461. package/src/LibraryLoader.ts +194 -0
  462. package/src/PathBasedEventProcessor.test.ts +336 -0
  463. package/src/PathBasedEventProcessor.ts +196 -0
  464. package/src/SessionManager.test.ts +424 -0
  465. package/src/SessionManager.ts +471 -0
  466. package/src/ValidationEngine.test.ts +520 -0
  467. package/src/ValidationEngine.ts +200 -0
  468. package/src/agent-sessions/fixture.ts +225 -0
  469. package/src/agent-sessions/index.ts +10 -0
  470. package/src/auxiliary/AuxiliaryManifestValidator.test.ts +93 -0
  471. package/src/auxiliary/AuxiliaryManifestValidator.ts +132 -0
  472. package/src/auxiliary/index.ts +18 -0
  473. package/src/auxiliary/validateAreaScopeDisjoint.test.ts +85 -0
  474. package/src/auxiliary/validateAreaScopeDisjoint.ts +72 -0
  475. package/src/browser-safety.test.ts +170 -0
  476. package/src/cli/codegen.ts +286 -0
  477. package/src/codegen/type-generator.test.ts +219 -0
  478. package/src/codegen/type-generator.ts +560 -0
  479. package/src/codegen/usage-example.ts +357 -0
  480. package/src/dashboard/DashboardValidator.ts +904 -0
  481. package/src/dashboard/index.ts +14 -0
  482. package/src/discovery/CanvasDiscovery.test.ts +691 -0
  483. package/src/discovery/CanvasDiscovery.ts +1114 -0
  484. package/src/discovery/CanvasFileManifest.test.ts +521 -0
  485. package/src/discovery/CanvasFileManifest.ts +419 -0
  486. package/src/discovery/LibraryDiscovery.test.ts +329 -0
  487. package/src/discovery/LibraryDiscovery.ts +386 -0
  488. package/src/discovery/types.ts +453 -0
  489. package/src/events/EventsCanvasValidator.test.ts +191 -0
  490. package/src/events/EventsCanvasValidator.ts +428 -0
  491. package/src/events/NamespacePathIndex.test.ts +97 -0
  492. package/src/events/NamespacePathIndex.ts +100 -0
  493. package/src/events/OtelEventPathsValidator.test.ts +243 -0
  494. package/src/events/OtelEventPathsValidator.ts +191 -0
  495. package/src/events/ScopeEventsValidator.test.ts +173 -0
  496. package/src/events/ScopeEventsValidator.ts +158 -0
  497. package/src/events/index.ts +27 -0
  498. package/src/events/path-helpers.ts +53 -0
  499. package/src/execution/ExecutionLoader.test.ts +378 -0
  500. package/src/execution/ExecutionLoader.ts +331 -0
  501. package/src/execution/ExecutionValidator.test.ts +503 -0
  502. package/src/execution/ExecutionValidator.ts +617 -0
  503. package/src/generated/graph-converter-execution.types.ts +193 -0
  504. package/src/helpers/GraphInstrumentationHelper.test.ts +364 -0
  505. package/src/helpers/GraphInstrumentationHelper.ts +323 -0
  506. package/src/index.ts +587 -0
  507. package/src/matchers/SpanMatcher.test.ts +1105 -0
  508. package/src/matchers/SpanMatcher.ts +381 -0
  509. package/src/node.ts +316 -0
  510. package/src/opencode/EVENT_MAPPING.md +82 -0
  511. package/src/opencode/OpenCodeEventStore.ts +214 -0
  512. package/src/opencode/__fixtures__/event-shapes.json +443 -0
  513. package/src/opencode/__fixtures__/session-events.json +13491 -0
  514. package/src/opencode/agent-sessions.ts +376 -0
  515. package/src/opencode/index.ts +23 -0
  516. package/src/opencode/node-path-adapter.ts +205 -0
  517. package/src/opencode/pipeline.ts +141 -0
  518. package/src/opencode/types.ts +43 -0
  519. package/src/orchestration/TraceOrchestrator.ts +560 -0
  520. package/src/orchestration/__tests__/TraceOrchestrator.test.ts +1098 -0
  521. package/src/parsers/OtlpTraceParser.ts +401 -0
  522. package/src/parsers/__tests__/OtlpTraceParser.test.ts +197 -0
  523. package/src/registry/CompositeRegistry.ts +140 -0
  524. package/src/registry/EventRegistry.test.ts +387 -0
  525. package/src/registry/EventRegistry.ts +191 -0
  526. package/src/registry/LocalRegistry.ts +416 -0
  527. package/src/registry/RemoteRegistry.ts +499 -0
  528. package/src/registry/VersionRegistry.test.ts +223 -0
  529. package/src/registry/VersionRegistry.ts +121 -0
  530. package/src/rules/config.test.ts +277 -0
  531. package/src/rules/config.ts +450 -0
  532. package/src/rules/engine.test.ts +331 -0
  533. package/src/rules/engine.ts +448 -0
  534. package/src/rules/implementations/connection-type-references.ts +117 -0
  535. package/src/rules/implementations/dead-end-states.ts +101 -0
  536. package/src/rules/implementations/index.ts +66 -0
  537. package/src/rules/implementations/minimum-node-sources.ts +84 -0
  538. package/src/rules/implementations/no-unknown-fields.ts +372 -0
  539. package/src/rules/implementations/orphaned-edge-types.ts +55 -0
  540. package/src/rules/implementations/orphaned-node-types.ts +58 -0
  541. package/src/rules/implementations/required-metadata.ts +52 -0
  542. package/src/rules/implementations/state-transition-references.ts +155 -0
  543. package/src/rules/implementations/unreachable-states.ts +94 -0
  544. package/src/rules/implementations/valid-action-patterns.ts +140 -0
  545. package/src/rules/implementations/valid-color-format.ts +152 -0
  546. package/src/rules/implementations/valid-edge-types.ts +273 -0
  547. package/src/rules/implementations/valid-node-types.ts +200 -0
  548. package/src/rules/index.ts +92 -0
  549. package/src/rules/types.ts +426 -0
  550. package/src/scopes/ScopePathIndex.test.ts +94 -0
  551. package/src/scopes/ScopePathIndex.ts +89 -0
  552. package/src/scopes/ScopesCanvasValidator.test.ts +127 -0
  553. package/src/scopes/ScopesCanvasValidator.ts +305 -0
  554. package/src/scopes/__tests__/utils.test.ts +383 -0
  555. package/src/scopes/index.ts +42 -0
  556. package/src/scopes/utils.ts +182 -0
  557. package/src/scopes/validateScopeNamespaceNesting.test.ts +127 -0
  558. package/src/scopes/validateScopeNamespaceNesting.ts +88 -0
  559. package/src/spans/index.ts +24 -0
  560. package/src/spans/utils.ts +249 -0
  561. package/src/storage/topic-types.ts +235 -0
  562. package/src/storage/topicStore.test.ts +193 -0
  563. package/src/storage/topicStore.ts +489 -0
  564. package/src/storyboard/builder.ts +308 -0
  565. package/src/storyboard/index.ts +26 -0
  566. package/src/storyboard/types.ts +117 -0
  567. package/src/telemetry/coverage.ts +480 -0
  568. package/src/telemetry/event-validator.ts +312 -0
  569. package/src/types/audit.ts +314 -0
  570. package/src/types/auxiliary.ts +63 -0
  571. package/src/types/canvas-scope.ts +190 -0
  572. package/src/types/canvas.test.ts +199 -0
  573. package/src/types/canvas.ts +1395 -0
  574. package/src/types/dashboard.ts +257 -0
  575. package/src/types/index.ts +460 -0
  576. package/src/types/library.ts +283 -0
  577. package/src/types/otel.test.ts +219 -0
  578. package/src/types/otel.ts +620 -0
  579. package/src/types/path-based-config.ts +285 -0
  580. package/src/types/registered-trace.ts +432 -0
  581. package/src/types/resource-match.ts +263 -0
  582. package/src/types/subsystem-model.ts +352 -0
  583. package/src/types/version-registry.ts +150 -0
  584. package/src/utils/CanvasConverter.ts +670 -0
  585. package/src/utils/GraphConverter.test.ts +322 -0
  586. package/src/utils/GraphConverter.ts +84 -0
  587. package/src/utils/PathMatcher.test.ts +139 -0
  588. package/src/utils/PathMatcher.ts +185 -0
  589. package/src/utils/TraceToCanvas.test.ts +310 -0
  590. package/src/utils/TraceToCanvas.ts +510 -0
  591. package/src/utils/YamlParser.ts +77 -0
  592. package/src/utils/registeredTraceHelpers.ts +251 -0
  593. package/src/utils/traceAggregation.test.ts +385 -0
  594. package/src/utils/traceAggregation.ts +173 -0
  595. package/src/validation/OtelCanvasValidator.ts +86 -0
  596. package/src/validation/index.ts +17 -0
  597. package/src/validation/libraryValidation.ts +206 -0
  598. package/src/workflow/README.md +408 -0
  599. package/src/workflow/__tests__/edge-derivation.test.ts +449 -0
  600. package/src/workflow/__tests__/edge-validation.test.ts +417 -0
  601. package/src/workflow/__tests__/scenario-matcher.test.ts +225 -0
  602. package/src/workflow/__tests__/template-parser.test.ts +276 -0
  603. package/src/workflow/__tests__/template-renderer.test.ts +509 -0
  604. package/src/workflow/__tests__/types.test.ts +187 -0
  605. package/src/workflow/__tests__/validator.test.ts +3225 -0
  606. package/src/workflow/edge-derivation.ts +265 -0
  607. package/src/workflow/edge-validation.ts +503 -0
  608. package/src/workflow/example.ts +330 -0
  609. package/src/workflow/index.ts +86 -0
  610. package/src/workflow/scenario-matcher.ts +253 -0
  611. package/src/workflow/template-parser.ts +350 -0
  612. package/src/workflow/template-renderer.ts +453 -0
  613. package/src/workflow/types.ts +511 -0
  614. package/src/workflow/validator.ts +2737 -0
@@ -0,0 +1,3225 @@
1
+ /**
2
+ * Tests for workflow template validator
3
+ */
4
+
5
+ import { WorkflowValidator } from '../validator';
6
+ import type { WorkflowTemplate, WorkflowValidationContext } from '../validator';
7
+ import type { ExtendedCanvas } from '../../types/canvas';
8
+ import type { ComponentLibrary } from '../../types/library';
9
+ import { EventRegistry } from '../../registry/EventRegistry';
10
+ import { NodeFileSystemAdapter } from '@principal-ai/repository-abstraction/node';
11
+ import { mkdtempSync, writeFileSync, rmSync } from 'fs';
12
+ import { join } from 'path';
13
+ import { tmpdir } from 'os';
14
+
15
+ describe('WorkflowValidator', () => {
16
+ let validator: WorkflowValidator;
17
+ let tempDir: string;
18
+
19
+ beforeEach(() => {
20
+ validator = new WorkflowValidator(new NodeFileSystemAdapter());
21
+ // Create a temporary directory for test files
22
+ tempDir = mkdtempSync(join(tmpdir(), 'workflow-validator-test-'));
23
+ });
24
+
25
+ afterEach(() => {
26
+ // Clean up temp directory
27
+ if (tempDir) {
28
+ rmSync(tempDir, { recursive: true, force: true });
29
+ }
30
+ });
31
+
32
+ // ============================================================================
33
+ // Helper Functions
34
+ // ============================================================================
35
+
36
+ function createValidWorkflow(): WorkflowTemplate {
37
+ return {
38
+ version: '1.0.0',
39
+ canvas: 'test.otel.canvas',
40
+ name: 'Test Workflow',
41
+ description: 'A test workflow template',
42
+ spanPattern: 'test.execution',
43
+ scenarioSelection: 'first-match',
44
+ scenarios: [
45
+ {
46
+ id: 'test-passed',
47
+ priority: 1,
48
+ description: 'Test passed successfully',
49
+ template: {
50
+ introduction: 'Test passed',
51
+ events: {
52
+ 'test.started': 'Started test',
53
+ 'test.complete': 'Test completed',
54
+ },
55
+ summary: 'Success',
56
+ },
57
+ },
58
+ {
59
+ id: 'test-failed',
60
+ priority: 2,
61
+ description: 'Test failed',
62
+ template: {
63
+ introduction: 'Test failed',
64
+ events: {
65
+ 'test.started': 'Test started',
66
+ 'test.failed': 'Test failed',
67
+ },
68
+ },
69
+ },
70
+ ],
71
+ };
72
+ }
73
+
74
+ function createValidCanvas(): ExtendedCanvas {
75
+ return {
76
+ nodes: [
77
+ {
78
+ id: 'test-started',
79
+ type: 'otel-event',
80
+ x: 0,
81
+ y: 0,
82
+ width: 200,
83
+ height: 100,
84
+ eventRef: 'test.started',
85
+ },
86
+ {
87
+ id: 'test-complete',
88
+ type: 'otel-event',
89
+ x: 250,
90
+ y: 0,
91
+ width: 200,
92
+ height: 100,
93
+ eventRef: 'test.complete',
94
+ },
95
+ {
96
+ id: 'test-error',
97
+ type: 'otel-event',
98
+ x: 500,
99
+ y: 0,
100
+ width: 200,
101
+ height: 100,
102
+ eventRef: 'test.error',
103
+ },
104
+ ],
105
+ edges: [
106
+ {
107
+ id: 'edge1',
108
+ fromNode: 'test-started',
109
+ toNode: 'test-complete',
110
+ fromSide: 'right',
111
+ toSide: 'left',
112
+ },
113
+ {
114
+ id: 'edge2',
115
+ fromNode: 'test-started',
116
+ toNode: 'test-error',
117
+ fromSide: 'right',
118
+ toSide: 'left',
119
+ },
120
+ ],
121
+ pv: {
122
+ version: '1.0.0',
123
+ name: 'Test Canvas',
124
+ markdown: 'test.md',
125
+ },
126
+ };
127
+ }
128
+
129
+ function createContext(
130
+ workflow: Partial<WorkflowTemplate>,
131
+ options: {
132
+ canvas?: ExtendedCanvas;
133
+ canvasPath?: string;
134
+ } = {}
135
+ ): WorkflowValidationContext {
136
+ const fullWorkflow = {
137
+ ...createValidWorkflow(),
138
+ ...workflow,
139
+ } as WorkflowTemplate;
140
+
141
+ return {
142
+ workflow: fullWorkflow,
143
+ workflowPath: 'test.workflow.json',
144
+ canvas: options.canvas,
145
+ canvasPath: options.canvasPath,
146
+ basePath: tempDir,
147
+ };
148
+ }
149
+
150
+ // ============================================================================
151
+ // Schema Validation Tests
152
+ // ============================================================================
153
+
154
+ describe('checkSchema', () => {
155
+ it('should pass for valid workflow template', async () => {
156
+ // Create canvas file so canvas-exists check passes
157
+ const canvasPath = join(tempDir, 'test.otel.canvas');
158
+ const markdownPath = join(tempDir, 'test.md');
159
+ writeFileSync(canvasPath, JSON.stringify(createValidCanvas()));
160
+ writeFileSync(markdownPath, '# Test Documentation');
161
+
162
+ const context = createContext({}, { canvasPath });
163
+ const result = await validator.validate(context);
164
+
165
+ expect(result.errorCount).toBe(0);
166
+ expect(result.warningCount).toBe(0);
167
+ });
168
+
169
+ it('should flag missing version', async () => {
170
+ const context = createContext({ version: undefined as unknown });
171
+ const result = await validator.validate(context);
172
+
173
+ expect(result.errorCount).toBeGreaterThan(0);
174
+ expect(result.violations).toContainEqual(
175
+ expect.objectContaining({
176
+ ruleId: 'workflow-schema-valid',
177
+ path: 'version',
178
+ message: expect.stringContaining('Missing required field "version"'),
179
+ })
180
+ );
181
+ });
182
+
183
+ it('should flag invalid semver version', async () => {
184
+ const context = createContext({ version: 'not-a-version' });
185
+ const result = await validator.validate(context);
186
+
187
+ expect(result.errorCount).toBeGreaterThan(0);
188
+ expect(result.violations).toContainEqual(
189
+ expect.objectContaining({
190
+ ruleId: 'workflow-schema-valid',
191
+ path: 'version',
192
+ message: expect.stringContaining('Invalid version format'),
193
+ })
194
+ );
195
+ });
196
+
197
+ it('should flag missing canvas reference', async () => {
198
+ const context = createContext({ canvas: undefined as unknown });
199
+ const result = await validator.validate(context);
200
+
201
+ expect(result.errorCount).toBeGreaterThan(0);
202
+ expect(result.violations).toContainEqual(
203
+ expect.objectContaining({
204
+ ruleId: 'workflow-schema-valid',
205
+ path: 'canvas',
206
+ message: expect.stringContaining('Missing required field "canvas"'),
207
+ })
208
+ );
209
+ });
210
+
211
+ it('should flag missing name', async () => {
212
+ const context = createContext({ name: undefined as unknown });
213
+ const result = await validator.validate(context);
214
+
215
+ expect(result.errorCount).toBeGreaterThan(0);
216
+ expect(result.violations).toContainEqual(
217
+ expect.objectContaining({
218
+ ruleId: 'workflow-schema-valid',
219
+ path: 'name',
220
+ message: expect.stringContaining('Missing required field "name"'),
221
+ })
222
+ );
223
+ });
224
+
225
+ it('should flag missing description', async () => {
226
+ const context = createContext({ description: undefined as unknown });
227
+ const result = await validator.validate(context);
228
+
229
+ expect(result.errorCount).toBeGreaterThan(0);
230
+ expect(result.violations).toContainEqual(
231
+ expect.objectContaining({
232
+ ruleId: 'workflow-schema-valid',
233
+ path: 'description',
234
+ })
235
+ );
236
+ });
237
+
238
+ it('should flag invalid scenarioSelection', async () => {
239
+ const context = createContext({ scenarioSelection: 'invalid' as unknown });
240
+ const result = await validator.validate(context);
241
+
242
+ expect(result.errorCount).toBeGreaterThan(0);
243
+ expect(result.violations).toContainEqual(
244
+ expect.objectContaining({
245
+ ruleId: 'workflow-schema-valid',
246
+ path: 'scenarioSelection',
247
+ })
248
+ );
249
+ });
250
+
251
+ it('should flag empty scenarios array', async () => {
252
+ const context = createContext({ scenarios: [] });
253
+ const result = await validator.validate(context);
254
+
255
+ expect(result.errorCount).toBeGreaterThan(0);
256
+ expect(result.violations).toContainEqual(
257
+ expect.objectContaining({
258
+ ruleId: 'workflow-schema-valid',
259
+ path: 'scenarios',
260
+ message: expect.stringContaining('empty'),
261
+ })
262
+ );
263
+ });
264
+ });
265
+
266
+ // ============================================================================
267
+ // Canvas Existence Tests
268
+ // ============================================================================
269
+
270
+ describe('checkCanvasExists', () => {
271
+ it('should pass when canvas file exists', async () => {
272
+ // Create a canvas file in temp directory
273
+ const canvasPath = join(tempDir, 'test.otel.canvas');
274
+ const markdownPath = join(tempDir, 'test.md');
275
+ writeFileSync(canvasPath, JSON.stringify(createValidCanvas()));
276
+ writeFileSync(markdownPath, '# Test Documentation');
277
+
278
+ const context = createContext(
279
+ { canvas: 'test.otel.canvas' },
280
+ { canvasPath }
281
+ );
282
+ const result = await validator.validate(context);
283
+
284
+ const canvasViolations = result.violations.filter(
285
+ (v) => v.ruleId === 'workflow-canvas-exists'
286
+ );
287
+ expect(canvasViolations).toHaveLength(0);
288
+ });
289
+
290
+ it('should flag when canvas file does not exist', async () => {
291
+ const context = createContext(
292
+ { canvas: 'nonexistent.otel.canvas' },
293
+ { canvasPath: join(tempDir, 'nonexistent.otel.canvas') }
294
+ );
295
+ const result = await validator.validate(context);
296
+
297
+ expect(result.errorCount).toBeGreaterThan(0);
298
+ expect(result.violations).toContainEqual(
299
+ expect.objectContaining({
300
+ ruleId: 'workflow-canvas-exists',
301
+ message: expect.stringContaining('does not exist'),
302
+ })
303
+ );
304
+ });
305
+ });
306
+
307
+ // ============================================================================
308
+ // Implementation Files Existence Tests (workflow-files-exist)
309
+ // ============================================================================
310
+
311
+ describe('workflow-files-exist', () => {
312
+ // Create the referenced canvas so unrelated canvas-exists violations stay silent.
313
+ function setupCanvas(): string {
314
+ const canvasPath = join(tempDir, 'test.otel.canvas');
315
+ writeFileSync(canvasPath, JSON.stringify(createValidCanvas()));
316
+ writeFileSync(join(tempDir, 'test.md'), '# Test');
317
+ return canvasPath;
318
+ }
319
+
320
+ it('should pass when status is implemented and all files exist', async () => {
321
+ const canvasPath = setupCanvas();
322
+ writeFileSync(join(tempDir, 'impl.ts'), '// impl');
323
+
324
+ const context = createContext(
325
+ { status: 'implemented', scope: 'test-scope', files: ['impl.ts'] },
326
+ { canvasPath }
327
+ );
328
+ const result = await validator.validate(context);
329
+
330
+ const fileViolations = result.violations.filter(
331
+ (v) => v.ruleId === 'workflow-files-exist'
332
+ );
333
+ expect(fileViolations).toHaveLength(0);
334
+ });
335
+
336
+ it('should flag each missing file when status is implemented', async () => {
337
+ const canvasPath = setupCanvas();
338
+
339
+ const context = createContext(
340
+ {
341
+ status: 'implemented',
342
+ scope: 'test-scope',
343
+ files: ['missing-one.ts', 'missing-two.ts'],
344
+ },
345
+ { canvasPath }
346
+ );
347
+ const result = await validator.validate(context);
348
+
349
+ const fileViolations = result.violations.filter(
350
+ (v) => v.ruleId === 'workflow-files-exist'
351
+ );
352
+ expect(fileViolations).toHaveLength(2);
353
+ expect(fileViolations).toContainEqual(
354
+ expect.objectContaining({
355
+ ruleId: 'workflow-files-exist',
356
+ severity: 'error',
357
+ path: 'files',
358
+ message: expect.stringContaining('missing-one.ts'),
359
+ })
360
+ );
361
+ expect(fileViolations).toContainEqual(
362
+ expect.objectContaining({
363
+ ruleId: 'workflow-files-exist',
364
+ message: expect.stringContaining('missing-two.ts'),
365
+ })
366
+ );
367
+ });
368
+
369
+ it('should flag only the missing files when some exist and some do not', async () => {
370
+ const canvasPath = setupCanvas();
371
+ writeFileSync(join(tempDir, 'present.ts'), '// present');
372
+
373
+ const context = createContext(
374
+ {
375
+ status: 'implemented',
376
+ scope: 'test-scope',
377
+ files: ['present.ts', 'absent.ts'],
378
+ },
379
+ { canvasPath }
380
+ );
381
+ const result = await validator.validate(context);
382
+
383
+ const fileViolations = result.violations.filter(
384
+ (v) => v.ruleId === 'workflow-files-exist'
385
+ );
386
+ expect(fileViolations).toHaveLength(1);
387
+ expect(fileViolations[0].message).toContain('absent.ts');
388
+ });
389
+
390
+ it('should not check file existence when status is draft', async () => {
391
+ const canvasPath = setupCanvas();
392
+
393
+ const context = createContext(
394
+ { status: 'draft', files: ['does-not-exist.ts'] },
395
+ { canvasPath }
396
+ );
397
+ const result = await validator.validate(context);
398
+
399
+ const fileViolations = result.violations.filter(
400
+ (v) => v.ruleId === 'workflow-files-exist'
401
+ );
402
+ expect(fileViolations).toHaveLength(0);
403
+ });
404
+
405
+ it('should not check file existence when status is approved', async () => {
406
+ const canvasPath = setupCanvas();
407
+
408
+ const context = createContext(
409
+ {
410
+ status: 'approved',
411
+ scope: 'test-scope',
412
+ files: ['does-not-exist.ts'],
413
+ },
414
+ { canvasPath }
415
+ );
416
+ const result = await validator.validate(context);
417
+
418
+ const fileViolations = result.violations.filter(
419
+ (v) => v.ruleId === 'workflow-files-exist'
420
+ );
421
+ expect(fileViolations).toHaveLength(0);
422
+ });
423
+ });
424
+
425
+ // ============================================================================
426
+ // Scenario Validation Tests
427
+ // ============================================================================
428
+
429
+ describe('checkScenarios', () => {
430
+ it('should pass for valid scenarios', async () => {
431
+ const context = createContext({});
432
+ const result = await validator.validate(context);
433
+
434
+ const scenarioViolations = result.violations.filter(
435
+ (v) => v.ruleId === 'workflow-scenario-valid'
436
+ );
437
+ expect(scenarioViolations).toHaveLength(0);
438
+ });
439
+
440
+ it('should flag missing scenario ID', async () => {
441
+ const context = createContext({
442
+ scenarios: [
443
+ {
444
+ id: undefined as unknown,
445
+ priority: 1,
446
+ description: 'Test',
447
+ template: { introduction: 'Test' },
448
+ },
449
+ ],
450
+ });
451
+ const result = await validator.validate(context);
452
+
453
+ expect(result.errorCount).toBeGreaterThan(0);
454
+ expect(result.violations).toContainEqual(
455
+ expect.objectContaining({
456
+ ruleId: 'workflow-scenario-valid',
457
+ message: expect.stringContaining('missing required "id"'),
458
+ })
459
+ );
460
+ });
461
+
462
+ it('should flag duplicate scenario IDs', async () => {
463
+ const context = createContext({
464
+ scenarios: [
465
+ {
466
+ id: 'duplicate',
467
+ priority: 1,
468
+ description: 'First',
469
+ template: { introduction: 'First' },
470
+ },
471
+ {
472
+ id: 'duplicate',
473
+ priority: 2,
474
+ description: 'Second',
475
+ template: { introduction: 'Second' },
476
+ },
477
+ ],
478
+ });
479
+ const result = await validator.validate(context);
480
+
481
+ expect(result.errorCount).toBeGreaterThan(0);
482
+ expect(result.violations).toContainEqual(
483
+ expect.objectContaining({
484
+ ruleId: 'workflow-scenario-valid',
485
+ message: expect.stringContaining('Duplicate scenario ID'),
486
+ })
487
+ );
488
+ });
489
+
490
+ it('should flag missing priority', async () => {
491
+ const context = createContext({
492
+ scenarios: [
493
+ {
494
+ id: 'test',
495
+ priority: undefined as unknown,
496
+ description: 'Test',
497
+ template: { introduction: 'Test' },
498
+ },
499
+ ],
500
+ });
501
+ const result = await validator.validate(context);
502
+
503
+ expect(result.errorCount).toBeGreaterThan(0);
504
+ expect(result.violations).toContainEqual(
505
+ expect.objectContaining({
506
+ ruleId: 'workflow-scenario-valid',
507
+ message: expect.stringContaining('missing required "priority"'),
508
+ })
509
+ );
510
+ });
511
+
512
+ it('should flag negative priority', async () => {
513
+ const context = createContext({
514
+ scenarios: [
515
+ {
516
+ id: 'test',
517
+ priority: -1,
518
+ description: 'Test',
519
+ template: { introduction: 'Test' },
520
+ },
521
+ ],
522
+ });
523
+ const result = await validator.validate(context);
524
+
525
+ expect(result.errorCount).toBeGreaterThan(0);
526
+ expect(result.violations).toContainEqual(
527
+ expect.objectContaining({
528
+ ruleId: 'workflow-scenario-valid',
529
+ message: expect.stringContaining('non-negative'),
530
+ })
531
+ );
532
+ });
533
+
534
+ it('should flag duplicate priorities', async () => {
535
+ const context = createContext({
536
+ scenarios: [
537
+ {
538
+ id: 'first',
539
+ priority: 1,
540
+ description: 'First',
541
+ template: { introduction: 'First' },
542
+ },
543
+ {
544
+ id: 'second',
545
+ priority: 1,
546
+ description: 'Second',
547
+ template: { introduction: 'Second' },
548
+ },
549
+ ],
550
+ });
551
+ const result = await validator.validate(context);
552
+
553
+ expect(result.errorCount).toBeGreaterThan(0);
554
+ expect(result.violations).toContainEqual(
555
+ expect.objectContaining({
556
+ ruleId: 'workflow-scenario-valid',
557
+ message: expect.stringContaining('Duplicate priority'),
558
+ })
559
+ );
560
+ });
561
+
562
+ it('should flag missing description', async () => {
563
+ const context = createContext({
564
+ scenarios: [
565
+ {
566
+ id: 'test',
567
+ priority: 1,
568
+ description: undefined as unknown,
569
+ template: { introduction: 'Test' },
570
+ },
571
+ ],
572
+ });
573
+ const result = await validator.validate(context);
574
+
575
+ expect(result.errorCount).toBeGreaterThan(0);
576
+ expect(result.violations).toContainEqual(
577
+ expect.objectContaining({
578
+ ruleId: 'workflow-scenario-valid',
579
+ message: expect.stringContaining('missing required "description"'),
580
+ })
581
+ );
582
+ });
583
+
584
+ it('should flag missing template', async () => {
585
+ const context = createContext({
586
+ scenarios: [
587
+ {
588
+ id: 'test',
589
+ priority: 1,
590
+ description: 'Test',
591
+ template: undefined as unknown,
592
+ },
593
+ ],
594
+ });
595
+ const result = await validator.validate(context);
596
+
597
+ expect(result.errorCount).toBeGreaterThan(0);
598
+ expect(result.violations).toContainEqual(
599
+ expect.objectContaining({
600
+ ruleId: 'workflow-scenario-valid',
601
+ message: expect.stringContaining('missing required "template"'),
602
+ })
603
+ );
604
+ });
605
+ });
606
+
607
+ // ============================================================================
608
+ // Template Syntax Tests
609
+ // ============================================================================
610
+
611
+ describe('checkTemplateSyntax', () => {
612
+ it('should pass for valid template strings', async () => {
613
+ const context = createContext({
614
+ scenarios: [
615
+ {
616
+ id: 'test',
617
+ priority: 1,
618
+ description: 'Test',
619
+ template: {
620
+ introduction: 'Started {test.name}',
621
+ events: {
622
+ 'test.started': 'Test {test.id} started',
623
+ 'test.complete': '{result.status === "passed" ? "✅ Passed" : "❌ Failed"}',
624
+ },
625
+ summary: 'Completed in {duration}ms',
626
+ },
627
+ },
628
+ ],
629
+ });
630
+ const result = await validator.validate(context);
631
+
632
+ const syntaxViolations = result.violations.filter(
633
+ (v) => v.ruleId === 'workflow-template-syntax'
634
+ );
635
+ expect(syntaxViolations).toHaveLength(0);
636
+ });
637
+
638
+ it('should flag unbalanced braces (missing closing)', async () => {
639
+ const context = createContext({
640
+ scenarios: [
641
+ {
642
+ id: 'test',
643
+ priority: 1,
644
+ description: 'Test',
645
+ template: {
646
+ introduction: 'Test {test.name',
647
+ },
648
+ },
649
+ ],
650
+ });
651
+ const result = await validator.validate(context);
652
+
653
+ expect(result.errorCount).toBeGreaterThan(0);
654
+ expect(result.violations).toContainEqual(
655
+ expect.objectContaining({
656
+ ruleId: 'workflow-template-syntax',
657
+ message: expect.stringContaining('Unbalanced braces'),
658
+ })
659
+ );
660
+ });
661
+
662
+ it('should flag unbalanced braces (extra closing)', async () => {
663
+ const context = createContext({
664
+ scenarios: [
665
+ {
666
+ id: 'test',
667
+ priority: 1,
668
+ description: 'Test',
669
+ template: {
670
+ introduction: 'Test {test.name}}',
671
+ },
672
+ },
673
+ ],
674
+ });
675
+ const result = await validator.validate(context);
676
+
677
+ expect(result.errorCount).toBeGreaterThan(0);
678
+ expect(result.violations).toContainEqual(
679
+ expect.objectContaining({
680
+ ruleId: 'workflow-template-syntax',
681
+ message: expect.stringContaining('Unbalanced braces'),
682
+ })
683
+ );
684
+ });
685
+
686
+ it('should flag incomplete conditional expression', async () => {
687
+ const context = createContext({
688
+ scenarios: [
689
+ {
690
+ id: 'test',
691
+ priority: 1,
692
+ description: 'Test',
693
+ template: {
694
+ introduction: '{result.passed ? "Success"}',
695
+ },
696
+ },
697
+ ],
698
+ });
699
+ const result = await validator.validate(context);
700
+
701
+ expect(result.errorCount).toBeGreaterThan(0);
702
+ expect(result.violations).toContainEqual(
703
+ expect.objectContaining({
704
+ ruleId: 'workflow-template-syntax',
705
+ message: expect.stringContaining('Incomplete conditional'),
706
+ })
707
+ );
708
+ });
709
+
710
+ it('should handle quotes in template strings', async () => {
711
+ const context = createContext({
712
+ scenarios: [
713
+ {
714
+ id: 'test',
715
+ priority: 1,
716
+ description: 'Test',
717
+ template: {
718
+ introduction: '{test.status === "passed" ? "Success" : "Failed"}',
719
+ },
720
+ },
721
+ ],
722
+ });
723
+ const result = await validator.validate(context);
724
+
725
+ const syntaxViolations = result.violations.filter(
726
+ (v) => v.ruleId === 'workflow-template-syntax'
727
+ );
728
+ expect(syntaxViolations).toHaveLength(0);
729
+ });
730
+
731
+ it('should validate event templates', async () => {
732
+ const context = createContext({
733
+ scenarios: [
734
+ {
735
+ id: 'test',
736
+ priority: 1,
737
+ description: 'Test',
738
+ template: {
739
+ events: {
740
+ 'test.started': 'Started {test.name',
741
+ 'test.complete': 'Completed',
742
+ },
743
+ },
744
+ },
745
+ ],
746
+ });
747
+ const result = await validator.validate(context);
748
+
749
+ expect(result.errorCount).toBeGreaterThan(0);
750
+ expect(result.violations).toContainEqual(
751
+ expect.objectContaining({
752
+ ruleId: 'workflow-template-syntax',
753
+ path: expect.stringContaining('events.test.started'),
754
+ })
755
+ );
756
+ });
757
+
758
+ it('should flag Handlebars {{#if}} conditionals', async () => {
759
+ const context = createContext({
760
+ scenarios: [
761
+ {
762
+ id: 'test',
763
+ priority: 1,
764
+ description: 'Test',
765
+ template: {
766
+ introduction: '{{#if hasError}}Error occurred{{/if}}',
767
+ },
768
+ },
769
+ ],
770
+ });
771
+ const result = await validator.validate(context);
772
+
773
+ expect(result.errorCount).toBeGreaterThan(0);
774
+ expect(result.violations).toContainEqual(
775
+ expect.objectContaining({
776
+ ruleId: 'workflow-template-conditional',
777
+ message: expect.stringContaining('Conditional syntax detected'),
778
+ suggestion: expect.stringContaining('Split into separate scenarios'),
779
+ })
780
+ );
781
+ });
782
+
783
+ it('should flag Handlebars {{#unless}} conditionals', async () => {
784
+ const context = createContext({
785
+ scenarios: [
786
+ {
787
+ id: 'test',
788
+ priority: 1,
789
+ description: 'Test',
790
+ template: {
791
+ summary: '{{#unless success}}Failed{{/unless}}',
792
+ },
793
+ },
794
+ ],
795
+ });
796
+ const result = await validator.validate(context);
797
+
798
+ expect(result.errorCount).toBeGreaterThan(0);
799
+ expect(result.violations).toContainEqual(
800
+ expect.objectContaining({
801
+ ruleId: 'workflow-template-conditional',
802
+ message: expect.stringContaining('{{#unless'),
803
+ })
804
+ );
805
+ });
806
+
807
+ it('should flag Handlebars {{#each}} iteration', async () => {
808
+ const context = createContext({
809
+ scenarios: [
810
+ {
811
+ id: 'test',
812
+ priority: 1,
813
+ description: 'Test',
814
+ template: {
815
+ introduction: '{{#each items}}{{name}}{{/each}}',
816
+ },
817
+ },
818
+ ],
819
+ });
820
+ const result = await validator.validate(context);
821
+
822
+ expect(result.errorCount).toBeGreaterThan(0);
823
+ expect(result.violations).toContainEqual(
824
+ expect.objectContaining({
825
+ ruleId: 'workflow-template-conditional',
826
+ message: expect.stringContaining('{{#each'),
827
+ })
828
+ );
829
+ });
830
+
831
+ it('should flag Handlebars {{else}} clause', async () => {
832
+ const context = createContext({
833
+ scenarios: [
834
+ {
835
+ id: 'test',
836
+ priority: 1,
837
+ description: 'Test',
838
+ template: {
839
+ events: {
840
+ 'test.started': '{{#if fast}}Quick{{else}}Slow{{/if}}',
841
+ },
842
+ },
843
+ },
844
+ ],
845
+ });
846
+ const result = await validator.validate(context);
847
+
848
+ expect(result.errorCount).toBeGreaterThan(0);
849
+ expect(result.violations).toContainEqual(
850
+ expect.objectContaining({
851
+ ruleId: 'workflow-template-conditional',
852
+ })
853
+ );
854
+ });
855
+
856
+ it('should allow simple variable interpolation without conditionals', async () => {
857
+ const context = createContext({
858
+ scenarios: [
859
+ {
860
+ id: 'test',
861
+ priority: 1,
862
+ description: 'Test',
863
+ template: {
864
+ introduction: 'Test {{name}} completed in {{duration}}ms',
865
+ events: {
866
+ 'test.started': 'Started {{testName}}',
867
+ },
868
+ summary: 'Result: {{status}}',
869
+ },
870
+ },
871
+ ],
872
+ });
873
+ const result = await validator.validate(context);
874
+
875
+ const conditionalViolations = result.violations.filter(
876
+ (v) => v.ruleId === 'workflow-template-conditional'
877
+ );
878
+ expect(conditionalViolations).toHaveLength(0);
879
+ });
880
+ });
881
+
882
+ // ============================================================================
883
+ // Emoji Validation Tests
884
+ // ============================================================================
885
+
886
+ describe('checkTemplateEmojis', () => {
887
+ it('should flag emojis in event templates', async () => {
888
+ const context = createContext({
889
+ scenarios: [
890
+ {
891
+ id: 'test',
892
+ priority: 1,
893
+ description: 'Test',
894
+ template: {
895
+ events: {
896
+ 'test.started': '🚀 Starting test',
897
+ },
898
+ },
899
+ },
900
+ ],
901
+ });
902
+ const result = await validator.validate(context);
903
+
904
+ expect(result.errorCount).toBeGreaterThan(0);
905
+ expect(result.violations).toContainEqual(
906
+ expect.objectContaining({
907
+ ruleId: 'workflow-template-emoji',
908
+ message: expect.stringContaining('emoji'),
909
+ suggestion: expect.stringContaining('Remove emoji'),
910
+ })
911
+ );
912
+ });
913
+
914
+ it('should flag emojis in introduction', async () => {
915
+ const context = createContext({
916
+ scenarios: [
917
+ {
918
+ id: 'test',
919
+ priority: 1,
920
+ description: 'Test',
921
+ template: {
922
+ introduction: '✅ Test Started',
923
+ events: {
924
+ 'test.started': 'Started',
925
+ },
926
+ },
927
+ },
928
+ ],
929
+ });
930
+ const result = await validator.validate(context);
931
+
932
+ expect(result.violations).toContainEqual(
933
+ expect.objectContaining({
934
+ ruleId: 'workflow-template-emoji',
935
+ })
936
+ );
937
+ });
938
+
939
+ it('should flag emojis in summary', async () => {
940
+ const context = createContext({
941
+ scenarios: [
942
+ {
943
+ id: 'test',
944
+ priority: 1,
945
+ description: 'Test',
946
+ template: {
947
+ events: {
948
+ 'test.started': 'Started',
949
+ },
950
+ summary: '❌ Failed',
951
+ },
952
+ },
953
+ ],
954
+ });
955
+ const result = await validator.validate(context);
956
+
957
+ expect(result.violations).toContainEqual(
958
+ expect.objectContaining({
959
+ ruleId: 'workflow-template-emoji',
960
+ })
961
+ );
962
+ });
963
+
964
+ it('should flag multiple different emojis', async () => {
965
+ const context = createContext({
966
+ scenarios: [
967
+ {
968
+ id: 'test',
969
+ priority: 1,
970
+ description: 'Test',
971
+ template: {
972
+ introduction: '🔄 Loading...',
973
+ events: {
974
+ 'test.started': '▶️ Started',
975
+ 'test.complete': '✅ Done',
976
+ },
977
+ summary: '🎉 Success!',
978
+ },
979
+ },
980
+ ],
981
+ });
982
+ const result = await validator.validate(context);
983
+
984
+ const emojiViolations = result.violations.filter(
985
+ (v) => v.ruleId === 'workflow-template-emoji'
986
+ );
987
+ expect(emojiViolations.length).toBeGreaterThan(0);
988
+ });
989
+
990
+ it('should allow templates without emojis', async () => {
991
+ const context = createContext({
992
+ scenarios: [
993
+ {
994
+ id: 'test',
995
+ priority: 1,
996
+ description: 'Test',
997
+ template: {
998
+ introduction: '[START] Test Beginning',
999
+ events: {
1000
+ 'test.started': 'Started: {{name}}',
1001
+ 'test.complete': 'Completed in {{duration}}ms',
1002
+ },
1003
+ summary: '[DONE] Test finished successfully',
1004
+ },
1005
+ },
1006
+ ],
1007
+ });
1008
+ const result = await validator.validate(context);
1009
+
1010
+ const emojiViolations = result.violations.filter(
1011
+ (v) => v.ruleId === 'workflow-template-emoji'
1012
+ );
1013
+ expect(emojiViolations).toHaveLength(0);
1014
+ });
1015
+
1016
+ it('should allow special characters that are not emojis', async () => {
1017
+ const context = createContext({
1018
+ scenarios: [
1019
+ {
1020
+ id: 'test',
1021
+ priority: 1,
1022
+ description: 'Test',
1023
+ template: {
1024
+ introduction: '>>> Test Output <<<',
1025
+ events: {
1026
+ 'test.started': '* Started @ {{timestamp}}',
1027
+ 'test.complete': '# Complete - Duration: {{duration}}ms',
1028
+ },
1029
+ summary: '=== Summary ===',
1030
+ },
1031
+ },
1032
+ ],
1033
+ });
1034
+ const result = await validator.validate(context);
1035
+
1036
+ const emojiViolations = result.violations.filter(
1037
+ (v) => v.ruleId === 'workflow-template-emoji'
1038
+ );
1039
+ expect(emojiViolations).toHaveLength(0);
1040
+ });
1041
+ });
1042
+
1043
+ // ============================================================================
1044
+ // Formatting Options Tests
1045
+ // ============================================================================
1046
+
1047
+ describe('checkFormattingOptions', () => {
1048
+ it('should pass for valid formatting options', async () => {
1049
+ const context = createContext({
1050
+ formatting: {
1051
+ indentPerLevel: ' ',
1052
+ timestampFormat: 'HH:mm:ss.SSS',
1053
+ showTimestamps: true,
1054
+ showDuration: true,
1055
+ showSpanIds: false,
1056
+ showAttributes: 'matched',
1057
+ },
1058
+ });
1059
+ const result = await validator.validate(context);
1060
+
1061
+ const formattingViolations = result.violations.filter(
1062
+ (v) => v.ruleId === 'workflow-formatting-options'
1063
+ );
1064
+ expect(formattingViolations).toHaveLength(0);
1065
+ });
1066
+
1067
+ it('should flag invalid showAttributes value', async () => {
1068
+ const context = createContext({
1069
+ formatting: {
1070
+ showAttributes: 'invalid' as unknown,
1071
+ },
1072
+ });
1073
+ const result = await validator.validate(context);
1074
+
1075
+ expect(result.warningCount).toBeGreaterThan(0);
1076
+ expect(result.violations).toContainEqual(
1077
+ expect.objectContaining({
1078
+ ruleId: 'workflow-formatting-options',
1079
+ severity: 'warn',
1080
+ message: expect.stringContaining('Invalid showAttributes'),
1081
+ })
1082
+ );
1083
+ });
1084
+
1085
+ it('should accept valid showAttributes values', async () => {
1086
+ const values = ['none', 'matched', 'all'];
1087
+
1088
+ for (const value of values) {
1089
+ const context = createContext({
1090
+ formatting: {
1091
+ showAttributes: value as unknown,
1092
+ },
1093
+ });
1094
+ const result = await validator.validate(context);
1095
+
1096
+ const formattingViolations = result.violations.filter(
1097
+ (v) => v.ruleId === 'workflow-formatting-options' && v.path === 'formatting.showAttributes'
1098
+ );
1099
+ expect(formattingViolations).toHaveLength(0);
1100
+ }
1101
+ });
1102
+ });
1103
+
1104
+ // ============================================================================
1105
+ // Aggregate Results Tests
1106
+ // ============================================================================
1107
+
1108
+ describe('aggregateResults', () => {
1109
+ it('should correctly count errors and warnings', async () => {
1110
+ const context = createContext({
1111
+ version: undefined as unknown,
1112
+ name: undefined as unknown,
1113
+ formatting: {
1114
+ showAttributes: 'invalid' as unknown,
1115
+ },
1116
+ });
1117
+ const result = await validator.validate(context);
1118
+
1119
+ expect(result.errorCount).toBeGreaterThan(0);
1120
+ expect(result.warningCount).toBeGreaterThan(0);
1121
+ expect(result.violations.length).toBe(result.errorCount + result.warningCount);
1122
+ });
1123
+
1124
+ it('should count fixable violations', async () => {
1125
+ // Currently no violations are fixable, but test the infrastructure
1126
+ const context = createContext({});
1127
+ const result = await validator.validate(context);
1128
+
1129
+ const fixable = result.violations.filter((v) => v.fixable);
1130
+ expect(result.fixableCount).toBe(fixable.length);
1131
+ });
1132
+ });
1133
+
1134
+ // ============================================================================
1135
+ // Integration Tests
1136
+ // ============================================================================
1137
+
1138
+ describe('integration tests', () => {
1139
+ it('should validate a complete valid workflow template', async () => {
1140
+ const canvasPath = join(tempDir, 'test.otel.canvas');
1141
+ const markdownPath = join(tempDir, 'test.md');
1142
+ writeFileSync(canvasPath, JSON.stringify(createValidCanvas()));
1143
+ writeFileSync(markdownPath, '# Test Documentation');
1144
+
1145
+ const workflow: WorkflowTemplate = {
1146
+ version: '1.0.0',
1147
+ canvas: 'test.otel.canvas',
1148
+ name: 'Complete Test Workflow',
1149
+ description: 'A complete workflow template for testing',
1150
+ spanPattern: 'test.integration',
1151
+ scenarioSelection: 'first-match',
1152
+ scenarios: [
1153
+ {
1154
+ id: 'error',
1155
+ priority: 1,
1156
+ description: 'Error scenario',
1157
+ template: {
1158
+ introduction: '[ERROR] Error Occurred',
1159
+ events: {
1160
+ 'test.started': 'Started: {test.name}',
1161
+ 'test.error': 'Error: {error.message}',
1162
+ },
1163
+ summary: '[FAILED]',
1164
+ },
1165
+ },
1166
+ {
1167
+ id: 'success',
1168
+ priority: 2,
1169
+ description: 'Success scenario',
1170
+ template: {
1171
+ introduction: '[SUCCESS] Test Passed',
1172
+ events: {
1173
+ 'test.started': 'Started: {test.name}',
1174
+ 'test.complete': 'Completed in {duration}ms',
1175
+ },
1176
+ summary: '[SUCCESS]',
1177
+ },
1178
+ },
1179
+ ],
1180
+ formatting: {
1181
+ indentPerLevel: ' ',
1182
+ showTimestamps: false,
1183
+ showDuration: true,
1184
+ showAttributes: 'matched',
1185
+ },
1186
+ };
1187
+
1188
+ const context: WorkflowValidationContext = {
1189
+ workflow,
1190
+ workflowPath: 'test.workflow.json',
1191
+ canvas: createValidCanvas(),
1192
+ canvasPath,
1193
+ basePath: tempDir,
1194
+ };
1195
+
1196
+ const result = await validator.validate(context);
1197
+
1198
+ expect(result.errorCount).toBe(0);
1199
+ expect(result.warningCount).toBe(0);
1200
+ expect(result.violations).toHaveLength(0);
1201
+ });
1202
+
1203
+ it('should collect multiple violations from different rules', async () => {
1204
+ const workflow = {
1205
+ version: 'invalid-version',
1206
+ canvas: 'nonexistent.canvas',
1207
+ name: '',
1208
+ scenarios: [
1209
+ {
1210
+ id: 'test',
1211
+ priority: -1,
1212
+ template: {
1213
+ introduction: '{unclosed',
1214
+ },
1215
+ },
1216
+ ],
1217
+ formatting: {
1218
+ showAttributes: 'invalid',
1219
+ },
1220
+ } as unknown as WorkflowTemplate;
1221
+
1222
+ const context: WorkflowValidationContext = {
1223
+ workflow,
1224
+ workflowPath: 'test.workflow.json',
1225
+ basePath: tempDir,
1226
+ };
1227
+
1228
+ const result = await validator.validate(context);
1229
+
1230
+ expect(result.violations.length).toBeGreaterThan(5);
1231
+
1232
+ const ruleIds = new Set(result.violations.map((v) => v.ruleId));
1233
+ expect(ruleIds.size).toBeGreaterThan(3); // Multiple different rules triggered
1234
+ });
1235
+ });
1236
+
1237
+ // ============================================================================
1238
+ // EventRegistry Enhanced Error Messages Tests
1239
+ // ============================================================================
1240
+
1241
+ describe('checkEventReferences with EventRegistry', () => {
1242
+ function createLibrary(eventSchemas: Record<string, { description: string; attributes: Record<string, unknown> }>): ComponentLibrary {
1243
+ return {
1244
+ version: '1.0.0',
1245
+ name: 'Test Library',
1246
+ nodeComponents: {},
1247
+ edgeComponents: {},
1248
+ eventSchemas,
1249
+ };
1250
+ }
1251
+
1252
+ function createCanvasWithEvents(events: string[]): ExtendedCanvas {
1253
+ return {
1254
+ nodes: events.map((eventName, i) => ({
1255
+ id: `node-${i}`,
1256
+ type: 'text' as const,
1257
+ x: i * 100,
1258
+ y: 0,
1259
+ width: 100,
1260
+ height: 50,
1261
+ pv: {
1262
+ eventRef: eventName,
1263
+ },
1264
+ })),
1265
+ edges: [],
1266
+ pv: {
1267
+ version: '1.0.0',
1268
+ name: 'Test Canvas',
1269
+ markdown: 'test.md',
1270
+ },
1271
+ };
1272
+ }
1273
+
1274
+ it('should show library suggestion when event exists in library but not canvas', async () => {
1275
+ // Create library with the event
1276
+ const library = createLibrary({
1277
+ 'cleanup.started': { description: 'Cleanup started', attributes: {} },
1278
+ });
1279
+
1280
+ // Create a canvas WITHOUT the event
1281
+ const canvas = createCanvasWithEvents(['other.event']);
1282
+
1283
+ // Create another canvas that HAS the event (for the registry)
1284
+ const otherCanvas = createCanvasWithEvents(['cleanup.started']);
1285
+
1286
+ // Build registry with library
1287
+ const canvases = new Map<string, ExtendedCanvas>();
1288
+ canvases.set('other.otel.canvas', otherCanvas);
1289
+ const eventRegistry = EventRegistry.build(library, canvases, 'library.yaml');
1290
+
1291
+ // Workflow references an event not in its canvas
1292
+ const workflow: WorkflowTemplate = {
1293
+ version: '1.0.0',
1294
+ canvas: 'test.otel.canvas',
1295
+ name: 'Test Workflow',
1296
+ description: 'Test',
1297
+ mode: 'span-tree',
1298
+ scenarioSelection: 'first-match',
1299
+ scenarios: [
1300
+ {
1301
+ id: 'default',
1302
+ priority: 1,
1303
+ description: 'Default',
1304
+ template: {
1305
+ events: {
1306
+ 'cleanup.started': 'Cleanup started', // Not in canvas
1307
+ },
1308
+ },
1309
+ },
1310
+ ],
1311
+ };
1312
+
1313
+ const context: WorkflowValidationContext = {
1314
+ workflow,
1315
+ workflowPath: 'test.workflow.json',
1316
+ canvas,
1317
+ canvasPath: join(tempDir, 'test.otel.canvas'),
1318
+ basePath: tempDir,
1319
+ eventRegistry,
1320
+ };
1321
+
1322
+ const result = await validator.validate(context);
1323
+
1324
+ // Should have an error about the missing event
1325
+ const eventViolation = result.violations.find(
1326
+ v => v.ruleId === 'workflow-event-sync' && v.message.includes('cleanup.started')
1327
+ );
1328
+
1329
+ expect(eventViolation).toBeDefined();
1330
+ // Should suggest using eventRef since it's in the library
1331
+ expect(eventViolation!.message).toContain('available in library');
1332
+ expect(eventViolation!.suggestion).toContain('eventRef');
1333
+ });
1334
+
1335
+ it('should show canvas location when event exists in another canvas but not library', async () => {
1336
+ // No library events
1337
+ const canvases = new Map<string, ExtendedCanvas>();
1338
+
1339
+ // Create another canvas that has the event we're looking for
1340
+ const cleanupCanvas = createCanvasWithEvents(['cleanup.started']);
1341
+ canvases.set('cleanup-operations.otel.canvas', cleanupCanvas);
1342
+
1343
+ // Build registry without library
1344
+ const eventRegistry = EventRegistry.build(undefined, canvases);
1345
+
1346
+ // Create a canvas WITHOUT the event
1347
+ const canvas = createCanvasWithEvents(['other.event']);
1348
+
1349
+ // Workflow references an event not in its canvas
1350
+ const workflow: WorkflowTemplate = {
1351
+ version: '1.0.0',
1352
+ canvas: 'test.otel.canvas',
1353
+ name: 'Test Workflow',
1354
+ description: 'Test',
1355
+ mode: 'span-tree',
1356
+ scenarioSelection: 'first-match',
1357
+ scenarios: [
1358
+ {
1359
+ id: 'default',
1360
+ priority: 1,
1361
+ description: 'Default',
1362
+ template: {
1363
+ events: {
1364
+ 'cleanup.started': 'Cleanup started', // Not in canvas but in another canvas
1365
+ },
1366
+ },
1367
+ },
1368
+ ],
1369
+ };
1370
+
1371
+ const context: WorkflowValidationContext = {
1372
+ workflow,
1373
+ workflowPath: 'test.workflow.json',
1374
+ canvas,
1375
+ canvasPath: join(tempDir, 'test.otel.canvas'),
1376
+ basePath: tempDir,
1377
+ eventRegistry,
1378
+ };
1379
+
1380
+ const result = await validator.validate(context);
1381
+
1382
+ // Should have an error about the missing event
1383
+ const eventViolation = result.violations.find(
1384
+ v => v.ruleId === 'workflow-event-sync' && v.message.includes('cleanup.started')
1385
+ );
1386
+
1387
+ expect(eventViolation).toBeDefined();
1388
+ // Should mention where the event was found
1389
+ expect(eventViolation!.message).toContain('cleanup-operations.otel.canvas');
1390
+ // Should suggest adding to library
1391
+ expect(eventViolation!.suggestion).toContain('library.yaml');
1392
+ });
1393
+
1394
+ it('should show generic error when event not found anywhere', async () => {
1395
+ // Empty registry
1396
+ const eventRegistry = EventRegistry.build(undefined, new Map());
1397
+
1398
+ // Create a canvas without the event
1399
+ const canvas = createCanvasWithEvents(['other.event']);
1400
+
1401
+ // Workflow references an event that doesn't exist anywhere
1402
+ const workflow: WorkflowTemplate = {
1403
+ version: '1.0.0',
1404
+ canvas: 'test.otel.canvas',
1405
+ name: 'Test Workflow',
1406
+ description: 'Test',
1407
+ mode: 'span-tree',
1408
+ scenarioSelection: 'first-match',
1409
+ scenarios: [
1410
+ {
1411
+ id: 'default',
1412
+ priority: 1,
1413
+ description: 'Default',
1414
+ template: {
1415
+ events: {
1416
+ 'nonexistent.event': 'Does not exist anywhere',
1417
+ },
1418
+ },
1419
+ },
1420
+ ],
1421
+ };
1422
+
1423
+ const context: WorkflowValidationContext = {
1424
+ workflow,
1425
+ workflowPath: 'test.workflow.json',
1426
+ canvas,
1427
+ canvasPath: join(tempDir, 'test.otel.canvas'),
1428
+ basePath: tempDir,
1429
+ eventRegistry,
1430
+ };
1431
+
1432
+ const result = await validator.validate(context);
1433
+
1434
+ // Should have an error about the missing event
1435
+ const eventViolation = result.violations.find(
1436
+ v => v.ruleId === 'workflow-event-sync' && v.message.includes('nonexistent.event')
1437
+ );
1438
+
1439
+ expect(eventViolation).toBeDefined();
1440
+ // Should show the standard error message
1441
+ expect(eventViolation!.message).toContain('not defined in canvas');
1442
+ });
1443
+
1444
+ it('should work without eventRegistry (backward compatible)', async () => {
1445
+ // Create a canvas without the event
1446
+ const canvas = createCanvasWithEvents(['other.event']);
1447
+
1448
+ // Workflow references an event not in canvas
1449
+ const workflow: WorkflowTemplate = {
1450
+ version: '1.0.0',
1451
+ canvas: 'test.otel.canvas',
1452
+ name: 'Test Workflow',
1453
+ description: 'Test',
1454
+ mode: 'span-tree',
1455
+ scenarioSelection: 'first-match',
1456
+ scenarios: [
1457
+ {
1458
+ id: 'default',
1459
+ priority: 1,
1460
+ description: 'Default',
1461
+ template: {
1462
+ events: {
1463
+ 'missing.event': 'Not in canvas',
1464
+ },
1465
+ },
1466
+ },
1467
+ ],
1468
+ };
1469
+
1470
+ // No eventRegistry provided
1471
+ const context: WorkflowValidationContext = {
1472
+ workflow,
1473
+ workflowPath: 'test.workflow.json',
1474
+ canvas,
1475
+ canvasPath: join(tempDir, 'test.otel.canvas'),
1476
+ basePath: tempDir,
1477
+ // eventRegistry: undefined
1478
+ };
1479
+
1480
+ const result = await validator.validate(context);
1481
+
1482
+ // Should still report the error, just without enhanced suggestions
1483
+ const eventViolation = result.violations.find(
1484
+ v => v.ruleId === 'workflow-event-sync' && v.message.includes('missing.event')
1485
+ );
1486
+
1487
+ expect(eventViolation).toBeDefined();
1488
+ expect(eventViolation!.message).toContain('not defined in canvas');
1489
+ });
1490
+ });
1491
+
1492
+ // ============================================================================
1493
+ // Event Connectivity Validation Tests
1494
+ // ============================================================================
1495
+
1496
+ describe('checkEventConnectivity', () => {
1497
+ it('should pass when all events are connected', async () => {
1498
+ const workflow: WorkflowTemplate = {
1499
+ version: '1.0.0',
1500
+ name: 'Test',
1501
+ description: 'Test workflow',
1502
+ mode: 'span-tree',
1503
+ canvas: 'test.otel.canvas',
1504
+ scenarios: [{
1505
+ id: 'test',
1506
+ priority: 1,
1507
+ description: 'Test scenario',
1508
+ template: {
1509
+ events: {
1510
+ 'event.started': 'Started',
1511
+ 'event.processing': 'Processing',
1512
+ 'event.complete': 'Complete'
1513
+ }
1514
+ }
1515
+ }]
1516
+ };
1517
+
1518
+ const canvas: ExtendedCanvas = {
1519
+ nodes: [
1520
+ { id: 'n1', type: 'otel-event', x: 0, y: 0, width: 100, height: 50, event: { name: 'event.started', attributes: {} } },
1521
+ { id: 'n2', type: 'otel-event', x: 150, y: 0, width: 100, height: 50, event: { name: 'event.processing', attributes: {} } },
1522
+ { id: 'n3', type: 'otel-event', x: 300, y: 0, width: 100, height: 50, event: { name: 'event.complete', attributes: {} } }
1523
+ ],
1524
+ edges: [
1525
+ { id: 'e1', fromNode: 'n1', toNode: 'n2' },
1526
+ { id: 'e2', fromNode: 'n2', toNode: 'n3' }
1527
+ ],
1528
+ pv: {
1529
+ version: '1.0.0',
1530
+ name: 'Test Canvas',
1531
+ markdown: 'test.md'
1532
+ }
1533
+ };
1534
+
1535
+ const context: WorkflowValidationContext = {
1536
+ workflow,
1537
+ workflowPath: 'test.workflow.json',
1538
+ canvas,
1539
+ basePath: tempDir
1540
+ };
1541
+
1542
+ const result = await validator.validate(context);
1543
+ const connectivityViolations = result.violations.filter(v => v.ruleId === 'workflow-event-connectivity');
1544
+ expect(connectivityViolations).toHaveLength(0);
1545
+ });
1546
+
1547
+ it('should error when events are disconnected', async () => {
1548
+ const workflow: WorkflowTemplate = {
1549
+ version: '1.0.0',
1550
+ name: 'Test',
1551
+ description: 'Test workflow',
1552
+ mode: 'span-tree',
1553
+ canvas: 'test.otel.canvas',
1554
+ scenarios: [{
1555
+ id: 'test',
1556
+ priority: 1,
1557
+ description: 'Test scenario',
1558
+ template: {
1559
+ events: {
1560
+ 'event.started': 'Started',
1561
+ 'event.complete': 'Complete',
1562
+ 'event.error': 'Error' // Disconnected - no path from started/complete to error
1563
+ }
1564
+ }
1565
+ }]
1566
+ };
1567
+
1568
+ const canvas: ExtendedCanvas = {
1569
+ nodes: [
1570
+ { id: 'n1', type: 'otel-event', x: 0, y: 0, width: 100, height: 50, event: { name: 'event.started', attributes: {} } },
1571
+ { id: 'n2', type: 'otel-event', x: 150, y: 0, width: 100, height: 50, event: { name: 'event.complete', attributes: {} } },
1572
+ { id: 'n3', type: 'otel-event', x: 0, y: 100, width: 100, height: 50, event: { name: 'event.error', attributes: {} } }
1573
+ ],
1574
+ edges: [
1575
+ { id: 'e1', fromNode: 'n1', toNode: 'n2' }
1576
+ // n3 is isolated - no edges connecting it to n1 or n2
1577
+ ],
1578
+ pv: {
1579
+ version: '1.0.0',
1580
+ name: 'Test Canvas',
1581
+ markdown: 'test.md'
1582
+ }
1583
+ };
1584
+
1585
+ const context: WorkflowValidationContext = {
1586
+ workflow,
1587
+ workflowPath: 'test.workflow.json',
1588
+ canvas,
1589
+ basePath: tempDir
1590
+ };
1591
+
1592
+ const result = await validator.validate(context);
1593
+ const connectivityViolations = result.violations.filter(v => v.ruleId === 'workflow-event-connectivity');
1594
+ expect(connectivityViolations).toHaveLength(1);
1595
+ expect(connectivityViolations[0].severity).toBe('error');
1596
+ expect(connectivityViolations[0].message).toContain('disconnected');
1597
+ });
1598
+
1599
+ it('should error when canvas has no edges but workflow has multi-event scenarios', async () => {
1600
+ const workflow: WorkflowTemplate = {
1601
+ version: '1.0.0',
1602
+ name: 'Test',
1603
+ description: 'Test workflow',
1604
+ mode: 'span-tree',
1605
+ canvas: 'test.otel.canvas',
1606
+ scenarios: [{
1607
+ id: 'test',
1608
+ priority: 1,
1609
+ description: 'Test scenario',
1610
+ template: {
1611
+ events: {
1612
+ 'event.started': 'Started',
1613
+ 'event.complete': 'Complete'
1614
+ }
1615
+ }
1616
+ }]
1617
+ };
1618
+
1619
+ const canvas: ExtendedCanvas = {
1620
+ nodes: [
1621
+ { id: 'n1', type: 'otel-event', x: 0, y: 0, width: 100, height: 50, event: { name: 'event.started', attributes: {} } },
1622
+ { id: 'n2', type: 'otel-event', x: 150, y: 0, width: 100, height: 50, event: { name: 'event.complete', attributes: {} } }
1623
+ ],
1624
+ edges: [], // No edges defined
1625
+ pv: {
1626
+ version: '1.0.0',
1627
+ name: 'Test Canvas',
1628
+ markdown: 'test.md'
1629
+ }
1630
+ };
1631
+
1632
+ const context: WorkflowValidationContext = {
1633
+ workflow,
1634
+ workflowPath: 'test.workflow.json',
1635
+ canvas,
1636
+ basePath: tempDir
1637
+ };
1638
+
1639
+ const result = await validator.validate(context);
1640
+ // Should produce connectivity violation when canvas has no edges but scenario has multiple events
1641
+ const connectivityViolations = result.violations.filter(v => v.ruleId === 'workflow-event-connectivity');
1642
+ expect(connectivityViolations).toHaveLength(1);
1643
+ expect(connectivityViolations[0].severity).toBe('error');
1644
+ expect(connectivityViolations[0].message).toContain('has no edges');
1645
+ });
1646
+
1647
+ it('should skip check when scenario has less than 2 events', async () => {
1648
+ const workflow: WorkflowTemplate = {
1649
+ version: '1.0.0',
1650
+ name: 'Test',
1651
+ description: 'Test workflow',
1652
+ mode: 'span-tree',
1653
+ canvas: 'test.otel.canvas',
1654
+ scenarios: [{
1655
+ id: 'test',
1656
+ priority: 1,
1657
+ description: 'Test scenario',
1658
+ template: {
1659
+ events: {
1660
+ 'event.started': 'Started'
1661
+ }
1662
+ }
1663
+ }]
1664
+ };
1665
+
1666
+ const canvas: ExtendedCanvas = {
1667
+ nodes: [
1668
+ { id: 'n1', type: 'otel-event', x: 0, y: 0, width: 100, height: 50, event: { name: 'event.started', attributes: {} } }
1669
+ ],
1670
+ edges: [],
1671
+ pv: {
1672
+ version: '1.0.0',
1673
+ name: 'Test Canvas',
1674
+ markdown: 'test.md'
1675
+ }
1676
+ };
1677
+
1678
+ const context: WorkflowValidationContext = {
1679
+ workflow,
1680
+ workflowPath: 'test.workflow.json',
1681
+ canvas,
1682
+ basePath: tempDir
1683
+ };
1684
+
1685
+ const result = await validator.validate(context);
1686
+ const connectivityViolations = result.violations.filter(v => v.ruleId === 'workflow-event-connectivity');
1687
+ expect(connectivityViolations).toHaveLength(0);
1688
+ });
1689
+
1690
+ it('should handle events connected through intermediate nodes', async () => {
1691
+ const workflow: WorkflowTemplate = {
1692
+ version: '1.0.0',
1693
+ name: 'Test',
1694
+ description: 'Test workflow',
1695
+ mode: 'span-tree',
1696
+ canvas: 'test.otel.canvas',
1697
+ scenarios: [{
1698
+ id: 'test',
1699
+ priority: 1,
1700
+ description: 'Test scenario',
1701
+ template: {
1702
+ events: {
1703
+ 'event.start': 'Start',
1704
+ 'event.end': 'End'
1705
+ }
1706
+ }
1707
+ }]
1708
+ };
1709
+
1710
+ const canvas: ExtendedCanvas = {
1711
+ nodes: [
1712
+ { id: 'n1', type: 'otel-event', x: 0, y: 0, width: 100, height: 50, event: { name: 'event.start', attributes: {} } },
1713
+ { id: 'n2', type: 'otel-event', x: 150, y: 0, width: 100, height: 50, event: { name: 'event.middle1', attributes: {} } },
1714
+ { id: 'n3', type: 'otel-event', x: 300, y: 0, width: 100, height: 50, event: { name: 'event.middle2', attributes: {} } },
1715
+ { id: 'n4', type: 'otel-event', x: 450, y: 0, width: 100, height: 50, event: { name: 'event.end', attributes: {} } }
1716
+ ],
1717
+ edges: [
1718
+ { id: 'e1', fromNode: 'n1', toNode: 'n2' },
1719
+ { id: 'e2', fromNode: 'n2', toNode: 'n3' },
1720
+ { id: 'e3', fromNode: 'n3', toNode: 'n4' }
1721
+ ],
1722
+ pv: {
1723
+ version: '1.0.0',
1724
+ name: 'Test Canvas',
1725
+ markdown: 'test.md'
1726
+ }
1727
+ };
1728
+
1729
+ const context: WorkflowValidationContext = {
1730
+ workflow,
1731
+ workflowPath: 'test.workflow.json',
1732
+ canvas,
1733
+ basePath: tempDir
1734
+ };
1735
+
1736
+ const result = await validator.validate(context);
1737
+ const connectivityViolations = result.violations.filter(v => v.ruleId === 'workflow-event-connectivity');
1738
+ // Should pass because events are connected through intermediate nodes
1739
+ expect(connectivityViolations).toHaveLength(0);
1740
+ });
1741
+ });
1742
+
1743
+ // ============================================================================
1744
+ // Subset Validation Tests
1745
+ // ============================================================================
1746
+
1747
+ describe('checkScenarioSubsets', () => {
1748
+ it('should pass when scenarios have mutually exclusive event sets', async () => {
1749
+ const workflow: WorkflowTemplate = {
1750
+ version: '1.0.0',
1751
+ name: 'Test',
1752
+ description: 'Test workflow',
1753
+ mode: 'span-tree',
1754
+ canvas: 'test.otel.canvas',
1755
+ scenarioSelection: 'first-match',
1756
+ scenarios: [
1757
+ {
1758
+ id: 'success',
1759
+ priority: 1,
1760
+ description: 'Success scenario',
1761
+ template: {
1762
+ events: {
1763
+ 'payment.authorized': 'Payment authorized',
1764
+ 'order.confirmed': 'Order confirmed'
1765
+ }
1766
+ }
1767
+ },
1768
+ {
1769
+ id: 'failure',
1770
+ priority: 2,
1771
+ description: 'Failure scenario',
1772
+ template: {
1773
+ events: {
1774
+ 'payment.declined': 'Payment declined',
1775
+ 'error.logged': 'Error logged'
1776
+ }
1777
+ }
1778
+ }
1779
+ ]
1780
+ };
1781
+
1782
+ const context: WorkflowValidationContext = {
1783
+ workflow,
1784
+ workflowPath: 'test.workflow.json',
1785
+ basePath: tempDir
1786
+ };
1787
+
1788
+ const result = await validator.validate(context);
1789
+ const subsetViolations = result.violations.filter(v => v.ruleId === 'workflow-scenario-subset');
1790
+ expect(subsetViolations).toHaveLength(0);
1791
+ });
1792
+
1793
+ it('should pass when scenarios have overlapping but not subset event sets', async () => {
1794
+ const workflow: WorkflowTemplate = {
1795
+ version: '1.0.0',
1796
+ name: 'Test',
1797
+ description: 'Test workflow',
1798
+ mode: 'span-tree',
1799
+ canvas: 'test.otel.canvas',
1800
+ scenarioSelection: 'first-match',
1801
+ scenarios: [
1802
+ {
1803
+ id: 'google-oauth',
1804
+ priority: 1,
1805
+ description: 'Google OAuth',
1806
+ template: {
1807
+ events: {
1808
+ 'oauth.google': 'Google auth',
1809
+ 'oauth.complete': 'OAuth complete',
1810
+ 'user.created': 'User created'
1811
+ }
1812
+ }
1813
+ },
1814
+ {
1815
+ id: 'email-password',
1816
+ priority: 2,
1817
+ description: 'Email/password signup',
1818
+ template: {
1819
+ events: {
1820
+ 'email.verified': 'Email verified',
1821
+ 'password.set': 'Password set',
1822
+ 'user.created': 'User created'
1823
+ }
1824
+ }
1825
+ }
1826
+ ]
1827
+ };
1828
+
1829
+ const context: WorkflowValidationContext = {
1830
+ workflow,
1831
+ workflowPath: 'test.workflow.json',
1832
+ basePath: tempDir
1833
+ };
1834
+
1835
+ const result = await validator.validate(context);
1836
+ const subsetViolations = result.violations.filter(v => v.ruleId === 'workflow-scenario-subset');
1837
+ expect(subsetViolations).toHaveLength(0);
1838
+ });
1839
+
1840
+ it('should error when one scenario is a strict subset of another', async () => {
1841
+ const workflow: WorkflowTemplate = {
1842
+ version: '1.0.0',
1843
+ name: 'Test',
1844
+ description: 'Test workflow',
1845
+ mode: 'span-tree',
1846
+ canvas: 'test.otel.canvas',
1847
+ scenarioSelection: 'first-match',
1848
+ scenarios: [
1849
+ {
1850
+ id: 'basic-checkout',
1851
+ priority: 1,
1852
+ description: 'Basic checkout',
1853
+ template: {
1854
+ events: {
1855
+ 'cart.viewed': 'Cart viewed',
1856
+ 'checkout.started': 'Checkout started'
1857
+ }
1858
+ }
1859
+ },
1860
+ {
1861
+ id: 'complete-checkout',
1862
+ priority: 2,
1863
+ description: 'Complete checkout',
1864
+ template: {
1865
+ events: {
1866
+ 'cart.viewed': 'Cart viewed',
1867
+ 'checkout.started': 'Checkout started',
1868
+ 'payment.complete': 'Payment complete'
1869
+ }
1870
+ }
1871
+ }
1872
+ ]
1873
+ };
1874
+
1875
+ const context: WorkflowValidationContext = {
1876
+ workflow,
1877
+ workflowPath: 'test.workflow.json',
1878
+ basePath: tempDir
1879
+ };
1880
+
1881
+ const result = await validator.validate(context);
1882
+ const subsetViolations = result.violations.filter(v => v.ruleId === 'workflow-scenario-subset');
1883
+
1884
+ expect(subsetViolations).toHaveLength(1);
1885
+ expect(subsetViolations[0].severity).toBe('error');
1886
+ expect(subsetViolations[0].message).toContain('basic-checkout');
1887
+ expect(subsetViolations[0].message).toContain('strict subset');
1888
+ expect(subsetViolations[0].message).toContain('complete-checkout');
1889
+ expect(subsetViolations[0].impact).toContain('ambiguous');
1890
+ expect(subsetViolations[0].suggestion).toContain('Merge into one scenario');
1891
+ expect(subsetViolations[0].suggestion).toContain('mutually exclusive');
1892
+ });
1893
+
1894
+ it('should detect reverse subset relationship', async () => {
1895
+ const workflow: WorkflowTemplate = {
1896
+ version: '1.0.0',
1897
+ name: 'Test',
1898
+ description: 'Test workflow',
1899
+ mode: 'span-tree',
1900
+ canvas: 'test.otel.canvas',
1901
+ scenarioSelection: 'first-match',
1902
+ scenarios: [
1903
+ {
1904
+ id: 'complete-flow',
1905
+ priority: 1,
1906
+ description: 'Complete flow',
1907
+ template: {
1908
+ events: {
1909
+ 'step.one': 'Step 1',
1910
+ 'step.two': 'Step 2',
1911
+ 'step.three': 'Step 3'
1912
+ }
1913
+ }
1914
+ },
1915
+ {
1916
+ id: 'partial-flow',
1917
+ priority: 2,
1918
+ description: 'Partial flow',
1919
+ template: {
1920
+ events: {
1921
+ 'step.one': 'Step 1',
1922
+ 'step.two': 'Step 2'
1923
+ }
1924
+ }
1925
+ }
1926
+ ]
1927
+ };
1928
+
1929
+ const context: WorkflowValidationContext = {
1930
+ workflow,
1931
+ workflowPath: 'test.workflow.json',
1932
+ basePath: tempDir
1933
+ };
1934
+
1935
+ const result = await validator.validate(context);
1936
+ const subsetViolations = result.violations.filter(v => v.ruleId === 'workflow-scenario-subset');
1937
+
1938
+ expect(subsetViolations).toHaveLength(1);
1939
+ expect(subsetViolations[0].message).toContain('partial-flow');
1940
+ expect(subsetViolations[0].message).toContain('strict subset');
1941
+ expect(subsetViolations[0].message).toContain('complete-flow');
1942
+ });
1943
+
1944
+ it('should pass when scenarios have same number of events but different events', async () => {
1945
+ const workflow: WorkflowTemplate = {
1946
+ version: '1.0.0',
1947
+ name: 'Test',
1948
+ description: 'Test workflow',
1949
+ mode: 'span-tree',
1950
+ canvas: 'test.otel.canvas',
1951
+ scenarioSelection: 'first-match',
1952
+ scenarios: [
1953
+ {
1954
+ id: 'scenario-a',
1955
+ priority: 1,
1956
+ description: 'Scenario A',
1957
+ template: {
1958
+ events: {
1959
+ 'event.a': 'Event A',
1960
+ 'event.b': 'Event B'
1961
+ }
1962
+ }
1963
+ },
1964
+ {
1965
+ id: 'scenario-b',
1966
+ priority: 2,
1967
+ description: 'Scenario B',
1968
+ template: {
1969
+ events: {
1970
+ 'event.c': 'Event C',
1971
+ 'event.d': 'Event D'
1972
+ }
1973
+ }
1974
+ }
1975
+ ]
1976
+ };
1977
+
1978
+ const context: WorkflowValidationContext = {
1979
+ workflow,
1980
+ workflowPath: 'test.workflow.json',
1981
+ basePath: tempDir
1982
+ };
1983
+
1984
+ const result = await validator.validate(context);
1985
+ const subsetViolations = result.violations.filter(v => v.ruleId === 'workflow-scenario-subset');
1986
+ expect(subsetViolations).toHaveLength(0);
1987
+ });
1988
+
1989
+ it('should pass when workflow has only one scenario', async () => {
1990
+ const workflow: WorkflowTemplate = {
1991
+ version: '1.0.0',
1992
+ name: 'Test',
1993
+ description: 'Test workflow',
1994
+ mode: 'span-tree',
1995
+ canvas: 'test.otel.canvas',
1996
+ scenarioSelection: 'first-match',
1997
+ scenarios: [
1998
+ {
1999
+ id: 'only-scenario',
2000
+ priority: 1,
2001
+ description: 'Only scenario',
2002
+ template: {
2003
+ events: {
2004
+ 'event.one': 'Event 1'
2005
+ }
2006
+ }
2007
+ }
2008
+ ]
2009
+ };
2010
+
2011
+ const context: WorkflowValidationContext = {
2012
+ workflow,
2013
+ workflowPath: 'test.workflow.json',
2014
+ basePath: tempDir
2015
+ };
2016
+
2017
+ const result = await validator.validate(context);
2018
+ const subsetViolations = result.violations.filter(v => v.ruleId === 'workflow-scenario-subset');
2019
+ expect(subsetViolations).toHaveLength(0);
2020
+ });
2021
+
2022
+ it('should handle scenarios with empty event sets', async () => {
2023
+ const workflow: WorkflowTemplate = {
2024
+ version: '1.0.0',
2025
+ name: 'Test',
2026
+ description: 'Test workflow',
2027
+ mode: 'span-tree',
2028
+ canvas: 'test.otel.canvas',
2029
+ scenarioSelection: 'first-match',
2030
+ scenarios: [
2031
+ {
2032
+ id: 'empty-events',
2033
+ priority: 1,
2034
+ description: 'Empty events',
2035
+ template: {
2036
+ introduction: 'Test',
2037
+ events: {}
2038
+ }
2039
+ },
2040
+ {
2041
+ id: 'with-events',
2042
+ priority: 2,
2043
+ description: 'With events',
2044
+ template: {
2045
+ events: {
2046
+ 'event.one': 'Event 1'
2047
+ }
2048
+ }
2049
+ }
2050
+ ]
2051
+ };
2052
+
2053
+ const context: WorkflowValidationContext = {
2054
+ workflow,
2055
+ workflowPath: 'test.workflow.json',
2056
+ basePath: tempDir
2057
+ };
2058
+
2059
+ const result = await validator.validate(context);
2060
+ const subsetViolations = result.violations.filter(v => v.ruleId === 'workflow-scenario-subset');
2061
+
2062
+ // Empty set is a strict subset of any non-empty set
2063
+ expect(subsetViolations).toHaveLength(1);
2064
+ expect(subsetViolations[0].message).toContain('empty-events');
2065
+ expect(subsetViolations[0].message).toContain('strict subset');
2066
+ });
2067
+
2068
+ it('should detect multiple subset violations in a workflow', async () => {
2069
+ const workflow: WorkflowTemplate = {
2070
+ version: '1.0.0',
2071
+ name: 'Test',
2072
+ description: 'Test workflow',
2073
+ mode: 'span-tree',
2074
+ canvas: 'test.otel.canvas',
2075
+ scenarioSelection: 'first-match',
2076
+ scenarios: [
2077
+ {
2078
+ id: 'minimal',
2079
+ priority: 1,
2080
+ description: 'Minimal',
2081
+ template: {
2082
+ events: {
2083
+ 'event.a': 'A'
2084
+ }
2085
+ }
2086
+ },
2087
+ {
2088
+ id: 'medium',
2089
+ priority: 2,
2090
+ description: 'Medium',
2091
+ template: {
2092
+ events: {
2093
+ 'event.a': 'A',
2094
+ 'event.b': 'B'
2095
+ }
2096
+ }
2097
+ },
2098
+ {
2099
+ id: 'complete',
2100
+ priority: 3,
2101
+ description: 'Complete',
2102
+ template: {
2103
+ events: {
2104
+ 'event.a': 'A',
2105
+ 'event.b': 'B',
2106
+ 'event.c': 'C'
2107
+ }
2108
+ }
2109
+ }
2110
+ ]
2111
+ };
2112
+
2113
+ const context: WorkflowValidationContext = {
2114
+ workflow,
2115
+ workflowPath: 'test.workflow.json',
2116
+ basePath: tempDir
2117
+ };
2118
+
2119
+ const result = await validator.validate(context);
2120
+ const subsetViolations = result.violations.filter(v => v.ruleId === 'workflow-scenario-subset');
2121
+
2122
+ // Should detect:
2123
+ // 1. minimal ⊂ medium
2124
+ // 2. minimal ⊂ complete
2125
+ // 3. medium ⊂ complete
2126
+ expect(subsetViolations.length).toBeGreaterThanOrEqual(3);
2127
+ });
2128
+
2129
+ it('should provide helpful fix suggestions in error messages', async () => {
2130
+ const workflow: WorkflowTemplate = {
2131
+ version: '1.0.0',
2132
+ name: 'Test',
2133
+ description: 'Test workflow',
2134
+ mode: 'span-tree',
2135
+ canvas: 'test.otel.canvas',
2136
+ scenarioSelection: 'first-match',
2137
+ scenarios: [
2138
+ {
2139
+ id: 'abandoned',
2140
+ priority: 1,
2141
+ description: 'Abandoned cart',
2142
+ template: {
2143
+ events: {
2144
+ 'cart.created': 'Cart created',
2145
+ 'items.added': 'Items added'
2146
+ }
2147
+ }
2148
+ },
2149
+ {
2150
+ id: 'completed',
2151
+ priority: 2,
2152
+ description: 'Completed purchase',
2153
+ template: {
2154
+ events: {
2155
+ 'cart.created': 'Cart created',
2156
+ 'items.added': 'Items added',
2157
+ 'checkout.complete': 'Checkout complete'
2158
+ }
2159
+ }
2160
+ }
2161
+ ]
2162
+ };
2163
+
2164
+ const context: WorkflowValidationContext = {
2165
+ workflow,
2166
+ workflowPath: 'test.workflow.json',
2167
+ basePath: tempDir
2168
+ };
2169
+
2170
+ const result = await validator.validate(context);
2171
+ const subsetViolations = result.violations.filter(v => v.ruleId === 'workflow-scenario-subset');
2172
+
2173
+ expect(subsetViolations).toHaveLength(1);
2174
+
2175
+ const violation = subsetViolations[0];
2176
+ expect(violation.suggestion).toContain('cart.created');
2177
+ expect(violation.suggestion).toContain('items.added');
2178
+ expect(violation.suggestion).toContain('checkout.complete');
2179
+ expect(violation.suggestion).toContain('Merge into one scenario');
2180
+ expect(violation.suggestion).toContain('template conditionals');
2181
+ expect(violation.suggestion).toContain('mutually exclusive');
2182
+ expect(violation.suggestion).toContain('distinguishing events');
2183
+ });
2184
+
2185
+ it('should error when two scenarios have identical event sets', async () => {
2186
+ const workflow: WorkflowTemplate = {
2187
+ version: '1.0.0',
2188
+ name: 'Test',
2189
+ description: 'Test workflow',
2190
+ mode: 'span-tree',
2191
+ canvas: 'test.otel.canvas',
2192
+ spanPattern: 'test.*',
2193
+ scenarios: [
2194
+ {
2195
+ id: 'scenario-a',
2196
+ priority: 1,
2197
+ description: 'First scenario',
2198
+ template: {
2199
+ summary: 'Test A',
2200
+ events: {
2201
+ 'event.start': 'Started',
2202
+ 'event.complete': 'Completed'
2203
+ }
2204
+ }
2205
+ },
2206
+ {
2207
+ id: 'scenario-b',
2208
+ priority: 2,
2209
+ description: 'Second scenario with same events',
2210
+ template: {
2211
+ summary: 'Test B',
2212
+ events: {
2213
+ 'event.start': 'Started',
2214
+ 'event.complete': 'Completed'
2215
+ }
2216
+ }
2217
+ }
2218
+ ]
2219
+ };
2220
+
2221
+ const context: WorkflowValidationContext = {
2222
+ workflow,
2223
+ workflowPath: 'test.workflow.json',
2224
+ basePath: tempDir
2225
+ };
2226
+
2227
+ const result = await validator.validate(context);
2228
+ const identicalViolations = result.violations.filter(v => v.ruleId === 'workflow-scenario-identical');
2229
+
2230
+ expect(identicalViolations).toHaveLength(1);
2231
+ expect(identicalViolations[0].severity).toBe('error');
2232
+ expect(identicalViolations[0].message).toContain('scenario-a');
2233
+ expect(identicalViolations[0].message).toContain('scenario-b');
2234
+ expect(identicalViolations[0].message).toContain('identical event sets');
2235
+ expect(identicalViolations[0].impact).toContain('arbitrary');
2236
+ expect(identicalViolations[0].suggestion).toContain('Merge into a single scenario');
2237
+ });
2238
+
2239
+ it('should pass when scenarios have same size but different events', async () => {
2240
+ const workflow: WorkflowTemplate = {
2241
+ version: '1.0.0',
2242
+ name: 'Test',
2243
+ description: 'Test workflow',
2244
+ mode: 'span-tree',
2245
+ canvas: 'test.otel.canvas',
2246
+ spanPattern: 'test.*',
2247
+ scenarios: [
2248
+ {
2249
+ id: 'oauth-flow',
2250
+ priority: 1,
2251
+ description: 'OAuth login',
2252
+ template: {
2253
+ summary: 'OAuth',
2254
+ events: {
2255
+ 'oauth.start': 'Started',
2256
+ 'oauth.complete': 'Completed'
2257
+ }
2258
+ }
2259
+ },
2260
+ {
2261
+ id: 'password-flow',
2262
+ priority: 2,
2263
+ description: 'Password login',
2264
+ template: {
2265
+ summary: 'Password',
2266
+ events: {
2267
+ 'password.start': 'Started',
2268
+ 'password.complete': 'Completed'
2269
+ }
2270
+ }
2271
+ }
2272
+ ]
2273
+ };
2274
+
2275
+ const context: WorkflowValidationContext = {
2276
+ workflow,
2277
+ workflowPath: 'test.workflow.json',
2278
+ basePath: tempDir
2279
+ };
2280
+
2281
+ const result = await validator.validate(context);
2282
+ const identicalViolations = result.violations.filter(v => v.ruleId === 'workflow-scenario-identical');
2283
+ const subsetViolations = result.violations.filter(v => v.ruleId === 'workflow-scenario-subset');
2284
+
2285
+ expect(identicalViolations).toHaveLength(0);
2286
+ expect(subsetViolations).toHaveLength(0);
2287
+ });
2288
+ });
2289
+
2290
+ // ============================================================================
2291
+ // Span Pattern Validation Tests
2292
+ // ============================================================================
2293
+
2294
+ describe('validateSpanPatterns (CLI-level validation)', () => {
2295
+ it('should pass when all workflows have unique spanPatterns', () => {
2296
+ const workflows = [
2297
+ {
2298
+ workflow: {
2299
+ version: '1.0.0',
2300
+ canvas: 'test.otel.canvas',
2301
+ name: 'Payment Workflow',
2302
+ description: 'Payment processing',
2303
+ spanPattern: 'payment.authorize',
2304
+ scenarioSelection: 'first-match' as const,
2305
+ scenarios: [],
2306
+ },
2307
+ workflowPath: 'workflows/payment.workflow.json',
2308
+ },
2309
+ {
2310
+ workflow: {
2311
+ version: '1.0.0',
2312
+ canvas: 'test.otel.canvas',
2313
+ name: 'Inventory Workflow',
2314
+ description: 'Inventory management',
2315
+ spanPattern: 'inventory.reserve',
2316
+ scenarioSelection: 'first-match' as const,
2317
+ scenarios: [],
2318
+ },
2319
+ workflowPath: 'workflows/inventory.workflow.json',
2320
+ },
2321
+ ];
2322
+
2323
+ const violations = WorkflowValidator.validateSpanPatterns(workflows);
2324
+ expect(violations).toHaveLength(0);
2325
+ });
2326
+
2327
+ it('should error when workflows have duplicate spanPatterns', () => {
2328
+ const workflows = [
2329
+ {
2330
+ workflow: {
2331
+ version: '1.0.0',
2332
+ canvas: 'test1.otel.canvas',
2333
+ name: 'Payment Workflow 1',
2334
+ description: 'Payment processing',
2335
+ spanPattern: 'payment.authorize',
2336
+ scenarioSelection: 'first-match' as const,
2337
+ scenarios: [],
2338
+ },
2339
+ workflowPath: 'workflows/payment1.workflow.json',
2340
+ },
2341
+ {
2342
+ workflow: {
2343
+ version: '1.0.0',
2344
+ canvas: 'test2.otel.canvas',
2345
+ name: 'Payment Workflow 2',
2346
+ description: 'Payment processing alternative',
2347
+ spanPattern: 'payment.authorize',
2348
+ scenarioSelection: 'first-match' as const,
2349
+ scenarios: [],
2350
+ },
2351
+ workflowPath: 'workflows/payment2.workflow.json',
2352
+ },
2353
+ ];
2354
+
2355
+ const violations = WorkflowValidator.validateSpanPatterns(workflows);
2356
+
2357
+ expect(violations).toHaveLength(2); // One for each workflow
2358
+ expect(violations[0].ruleId).toBe('workflow-span-pattern-duplicate');
2359
+ expect(violations[0].severity).toBe('error');
2360
+ expect(violations[0].message).toContain('payment.authorize');
2361
+ expect(violations[0].message).toContain('Duplicate spanPattern');
2362
+ expect(violations[0].suggestion).toContain('payment2.workflow.json');
2363
+ });
2364
+
2365
+ it('should detect multiple duplicates across many workflows', () => {
2366
+ const workflows = [
2367
+ {
2368
+ workflow: {
2369
+ version: '1.0.0',
2370
+ canvas: 'test.otel.canvas',
2371
+ name: 'Workflow A1',
2372
+ description: 'Test',
2373
+ spanPattern: 'span.a',
2374
+ scenarioSelection: 'first-match' as const,
2375
+ scenarios: [],
2376
+ },
2377
+ workflowPath: 'a1.workflow.json',
2378
+ },
2379
+ {
2380
+ workflow: {
2381
+ version: '1.0.0',
2382
+ canvas: 'test.otel.canvas',
2383
+ name: 'Workflow A2',
2384
+ description: 'Test',
2385
+ spanPattern: 'span.a',
2386
+ scenarioSelection: 'first-match' as const,
2387
+ scenarios: [],
2388
+ },
2389
+ workflowPath: 'a2.workflow.json',
2390
+ },
2391
+ {
2392
+ workflow: {
2393
+ version: '1.0.0',
2394
+ canvas: 'test.otel.canvas',
2395
+ name: 'Workflow B',
2396
+ description: 'Test',
2397
+ spanPattern: 'span.b',
2398
+ scenarioSelection: 'first-match' as const,
2399
+ scenarios: [],
2400
+ },
2401
+ workflowPath: 'b.workflow.json',
2402
+ },
2403
+ {
2404
+ workflow: {
2405
+ version: '1.0.0',
2406
+ canvas: 'test.otel.canvas',
2407
+ name: 'Workflow C1',
2408
+ description: 'Test',
2409
+ spanPattern: 'span.c',
2410
+ scenarioSelection: 'first-match' as const,
2411
+ scenarios: [],
2412
+ },
2413
+ workflowPath: 'c1.workflow.json',
2414
+ },
2415
+ {
2416
+ workflow: {
2417
+ version: '1.0.0',
2418
+ canvas: 'test.otel.canvas',
2419
+ name: 'Workflow C2',
2420
+ description: 'Test',
2421
+ spanPattern: 'span.c',
2422
+ scenarioSelection: 'first-match' as const,
2423
+ scenarios: [],
2424
+ },
2425
+ workflowPath: 'c2.workflow.json',
2426
+ },
2427
+ ];
2428
+
2429
+ const violations = WorkflowValidator.validateSpanPatterns(workflows);
2430
+
2431
+ // Should have violations for: a1, a2, c1, c2 (4 workflows with duplicates)
2432
+ expect(violations.length).toBe(4);
2433
+
2434
+ // Check that span.b has no violations
2435
+ const spanBViolations = violations.filter(v => v.file === 'b.workflow.json');
2436
+ expect(spanBViolations).toHaveLength(0);
2437
+ });
2438
+
2439
+ it('should handle workflows without spanPattern gracefully', () => {
2440
+ const workflows = [
2441
+ {
2442
+ workflow: {
2443
+ version: '1.0.0',
2444
+ canvas: 'test.otel.canvas',
2445
+ name: 'Valid Workflow',
2446
+ description: 'Test',
2447
+ spanPattern: 'payment.authorize',
2448
+ scenarioSelection: 'first-match' as const,
2449
+ scenarios: [],
2450
+ },
2451
+ workflowPath: 'valid.workflow.json',
2452
+ },
2453
+ {
2454
+ workflow: {
2455
+ version: '1.0.0',
2456
+ canvas: 'test.otel.canvas',
2457
+ name: 'Invalid Workflow',
2458
+ description: 'Test',
2459
+ // spanPattern missing - will be caught by individual validation
2460
+ scenarioSelection: 'first-match' as const,
2461
+ scenarios: [],
2462
+ } as Partial<WorkflowTemplate> as WorkflowTemplate,
2463
+ workflowPath: 'invalid.workflow.json',
2464
+ },
2465
+ ];
2466
+
2467
+ const violations = WorkflowValidator.validateSpanPatterns(workflows);
2468
+
2469
+ // Should not error - just skip the workflow without spanPattern
2470
+ expect(violations).toHaveLength(0);
2471
+ });
2472
+ });
2473
+
2474
+ // ============================================================================
2475
+ // Span Pattern Schema Validation Tests
2476
+ // ============================================================================
2477
+
2478
+ describe('checkSchema - spanPattern', () => {
2479
+ it('should error when spanPattern is missing', async () => {
2480
+ const context = createContext({ spanPattern: undefined });
2481
+ const result = await validator.validate(context);
2482
+
2483
+ const spanPatternViolations = result.violations.filter(
2484
+ v => v.ruleId === 'workflow-schema-valid' && v.path === 'spanPattern'
2485
+ );
2486
+
2487
+ expect(spanPatternViolations).toHaveLength(1);
2488
+ expect(spanPatternViolations[0].message).toContain('Missing required field "spanPattern"');
2489
+ expect(spanPatternViolations[0].suggestion).toContain('exact span name');
2490
+ });
2491
+
2492
+ it('should error when spanPattern is empty string', async () => {
2493
+ const context = createContext({ spanPattern: '' });
2494
+ const result = await validator.validate(context);
2495
+
2496
+ const spanPatternViolations = result.violations.filter(
2497
+ v => v.ruleId === 'workflow-schema-valid' && v.path === 'spanPattern'
2498
+ );
2499
+
2500
+ expect(spanPatternViolations).toHaveLength(1);
2501
+ expect(spanPatternViolations[0].message).toContain('Missing required field');
2502
+ });
2503
+
2504
+ it('should pass when spanPattern is a valid string', async () => {
2505
+ const canvasPath = join(tempDir, 'test.otel.canvas');
2506
+ writeFileSync(canvasPath, JSON.stringify(createValidCanvas()));
2507
+
2508
+ const context = createContext({ spanPattern: 'payment.authorize' }, { canvasPath });
2509
+ const result = await validator.validate(context);
2510
+
2511
+ const spanPatternViolations = result.violations.filter(
2512
+ v => v.ruleId === 'workflow-schema-valid' && v.path === 'spanPattern'
2513
+ );
2514
+
2515
+ expect(spanPatternViolations).toHaveLength(0);
2516
+ });
2517
+
2518
+ it('should allow parentheses in spanPattern (Next.js style span names)', async () => {
2519
+ const canvasPath = join(tempDir, 'test.otel.canvas');
2520
+ writeFileSync(canvasPath, JSON.stringify(createValidCanvas()));
2521
+
2522
+ // Next.js creates span names like "executing api route (app) /api/auth/me/route"
2523
+ const context = createContext({
2524
+ spanPattern: 'executing api route (app) /api/auth/me/route'
2525
+ }, { canvasPath });
2526
+ const result = await validator.validate(context);
2527
+
2528
+ const spanPatternViolations = result.violations.filter(
2529
+ v => v.ruleId === 'workflow-span-pattern-exact' && v.path === 'spanPattern'
2530
+ );
2531
+
2532
+ expect(spanPatternViolations).toHaveLength(0);
2533
+ });
2534
+
2535
+ it('should error when spanPattern contains wildcard *', async () => {
2536
+ const context = createContext({ spanPattern: 'payment.*' });
2537
+ const result = await validator.validate(context);
2538
+
2539
+ const spanPatternViolations = result.violations.filter(
2540
+ v => v.ruleId === 'workflow-span-pattern-exact' && v.path === 'spanPattern'
2541
+ );
2542
+
2543
+ expect(spanPatternViolations).toHaveLength(1);
2544
+ expect(spanPatternViolations[0].message).toContain('wildcard');
2545
+ });
2546
+
2547
+ it('should error when spanPattern contains regex special characters', async () => {
2548
+ const regexPatterns = [
2549
+ 'payment[type]', // square brackets
2550
+ 'payment{1,2}', // curly braces (quantifier)
2551
+ 'payment^start', // caret
2552
+ 'payment$end', // dollar
2553
+ 'payment|refund', // pipe (alternation)
2554
+ 'payment\\d+', // backslash
2555
+ 'payment+', // plus
2556
+ 'payment?', // question mark
2557
+ ];
2558
+
2559
+ for (const pattern of regexPatterns) {
2560
+ const context = createContext({ spanPattern: pattern });
2561
+ const result = await validator.validate(context);
2562
+
2563
+ const spanPatternViolations = result.violations.filter(
2564
+ v => v.ruleId === 'workflow-span-pattern-exact' && v.path === 'spanPattern'
2565
+ );
2566
+
2567
+ expect(spanPatternViolations).toHaveLength(1);
2568
+ expect(spanPatternViolations[0].message).toContain('regex special characters');
2569
+ }
2570
+ });
2571
+ });
2572
+
2573
+ // ============================================================================
2574
+ // Event Attribute Requirements
2575
+ // ============================================================================
2576
+
2577
+ describe('Event Attribute Requirements', () => {
2578
+ it('should error when attribute is not used in any template', async () => {
2579
+ const workflow: WorkflowTemplate = {
2580
+ version: '1.0.0',
2581
+ name: 'Test',
2582
+ description: 'Test workflow',
2583
+ mode: 'span-tree',
2584
+ canvas: 'test.otel.canvas',
2585
+ scenarios: [{
2586
+ id: 'test',
2587
+ priority: 1,
2588
+ description: 'Test scenario',
2589
+ template: {
2590
+ events: {
2591
+ 'order.completed': 'Order {{orderId}} completed' // total is not used
2592
+ }
2593
+ }
2594
+ }]
2595
+ };
2596
+
2597
+ const canvas: ExtendedCanvas = {
2598
+ nodes: [
2599
+ {
2600
+ id: 'n1',
2601
+ type: 'otel-event',
2602
+ x: 0,
2603
+ y: 0,
2604
+ width: 100,
2605
+ height: 50,
2606
+ event: {
2607
+ name: 'order.completed',
2608
+ description: 'Order completion event',
2609
+ attributes: {
2610
+ orderId: { type: 'string' },
2611
+ total: { type: 'number' } // not used in template
2612
+ }
2613
+ }
2614
+ }
2615
+ ],
2616
+ edges: [],
2617
+ pv: { version: '1.0.0', name: 'Test Canvas', markdown: 'test.md' }
2618
+ };
2619
+
2620
+ const context: WorkflowValidationContext = {
2621
+ workflow,
2622
+ workflowPath: 'test.workflow.json',
2623
+ canvas,
2624
+ basePath: tempDir
2625
+ };
2626
+
2627
+ const result = await validator.validate(context);
2628
+ const violations = result.violations.filter(v => v.ruleId === 'workflow-attribute-unused');
2629
+
2630
+ expect(violations).toHaveLength(1);
2631
+ expect(violations[0].message).toContain('order.completed');
2632
+ expect(violations[0].message).toContain('total');
2633
+ expect(violations[0].message).toContain('not used in any template');
2634
+ });
2635
+
2636
+ it('should pass when all attributes are used', async () => {
2637
+ const workflow: WorkflowTemplate = {
2638
+ version: '1.0.0',
2639
+ name: 'Test',
2640
+ description: 'Test workflow',
2641
+ mode: 'span-tree',
2642
+ canvas: 'test.otel.canvas',
2643
+ scenarios: [{
2644
+ id: 'test',
2645
+ priority: 1,
2646
+ description: 'Test scenario',
2647
+ template: {
2648
+ events: {
2649
+ 'order.completed': 'Order {{orderId}} for {{total}}'
2650
+ }
2651
+ }
2652
+ }]
2653
+ };
2654
+
2655
+ const canvas: ExtendedCanvas = {
2656
+ nodes: [
2657
+ {
2658
+ id: 'n1',
2659
+ type: 'otel-event',
2660
+ x: 0,
2661
+ y: 0,
2662
+ width: 100,
2663
+ height: 50,
2664
+ event: {
2665
+ name: 'order.completed',
2666
+ description: 'Order completion event',
2667
+ attributes: {
2668
+ orderId: { type: 'string' },
2669
+ total: { type: 'number' }
2670
+ }
2671
+ }
2672
+ }
2673
+ ],
2674
+ edges: [],
2675
+ pv: { version: '1.0.0', name: 'Test Canvas', markdown: 'test.md' }
2676
+ };
2677
+
2678
+ const context: WorkflowValidationContext = {
2679
+ workflow,
2680
+ workflowPath: 'test.workflow.json',
2681
+ canvas,
2682
+ basePath: tempDir
2683
+ };
2684
+
2685
+ const result = await validator.validate(context);
2686
+ const violations = result.violations.filter(v => v.ruleId === 'workflow-attribute-unused');
2687
+
2688
+ expect(violations).toHaveLength(0);
2689
+ });
2690
+
2691
+ it('should consider nested property access as using the parent attribute', async () => {
2692
+ const workflow: WorkflowTemplate = {
2693
+ version: '1.0.0',
2694
+ name: 'Test',
2695
+ description: 'Test workflow',
2696
+ mode: 'span-tree',
2697
+ canvas: 'test.otel.canvas',
2698
+ scenarios: [{
2699
+ id: 'test',
2700
+ priority: 1,
2701
+ description: 'Test scenario',
2702
+ template: {
2703
+ events: {
2704
+ 'config.loaded': 'Loaded {{config.timeout}} ms timeout'
2705
+ }
2706
+ }
2707
+ }]
2708
+ };
2709
+
2710
+ const canvas: ExtendedCanvas = {
2711
+ nodes: [
2712
+ {
2713
+ id: 'n1',
2714
+ type: 'otel-event',
2715
+ x: 0,
2716
+ y: 0,
2717
+ width: 100,
2718
+ height: 50,
2719
+ event: {
2720
+ name: 'config.loaded',
2721
+ description: 'Configuration loaded',
2722
+ attributes: {
2723
+ config: { type: 'object' } // used via nested path {{config.timeout}}
2724
+ }
2725
+ }
2726
+ }
2727
+ ],
2728
+ edges: [],
2729
+ pv: { version: '1.0.0', name: 'Test Canvas', markdown: 'test.md' }
2730
+ };
2731
+
2732
+ const context: WorkflowValidationContext = {
2733
+ workflow,
2734
+ workflowPath: 'test.workflow.json',
2735
+ canvas,
2736
+ basePath: tempDir
2737
+ };
2738
+
2739
+ const result = await validator.validate(context);
2740
+ const violations = result.violations.filter(v => v.ruleId === 'workflow-attribute-unused');
2741
+
2742
+ expect(violations).toHaveLength(0);
2743
+ });
2744
+
2745
+ it('should check library event schemas via eventRef', async () => {
2746
+ const workflow: WorkflowTemplate = {
2747
+ version: '1.0.0',
2748
+ name: 'Test',
2749
+ description: 'Test workflow',
2750
+ mode: 'span-tree',
2751
+ canvas: 'test.otel.canvas',
2752
+ scenarios: [{
2753
+ id: 'test',
2754
+ priority: 1,
2755
+ description: 'Test scenario',
2756
+ template: {
2757
+ events: {
2758
+ 'user.signup': 'User {{email}} signed up' // name is not used
2759
+ }
2760
+ }
2761
+ }]
2762
+ };
2763
+
2764
+ const canvas: ExtendedCanvas = {
2765
+ nodes: [
2766
+ {
2767
+ id: 'n1',
2768
+ type: 'otel-event',
2769
+ x: 0,
2770
+ y: 0,
2771
+ width: 100,
2772
+ height: 50,
2773
+ eventRef: 'user.signup'
2774
+ }
2775
+ ],
2776
+ edges: [],
2777
+ pv: { version: '1.0.0', name: 'Test Canvas', markdown: 'test.md' }
2778
+ };
2779
+
2780
+ const library: ComponentLibrary = {
2781
+ version: '1.0.0',
2782
+ eventSchemas: {
2783
+ 'user.signup': {
2784
+ description: 'User signup event',
2785
+ attributes: {
2786
+ email: { type: 'string' },
2787
+ name: { type: 'string' } // not used in template
2788
+ }
2789
+ }
2790
+ }
2791
+ };
2792
+
2793
+ const canvases = new Map<string, ExtendedCanvas>();
2794
+ canvases.set('test.otel.canvas', canvas);
2795
+ const eventRegistry = EventRegistry.build(library, canvases, 'library.yaml');
2796
+
2797
+ const context: WorkflowValidationContext = {
2798
+ workflow,
2799
+ workflowPath: 'test.workflow.json',
2800
+ canvas,
2801
+ basePath: tempDir,
2802
+ eventRegistry
2803
+ };
2804
+
2805
+ const result = await validator.validate(context);
2806
+ const violations = result.violations.filter(v => v.ruleId === 'workflow-attribute-unused');
2807
+
2808
+ expect(violations).toHaveLength(1);
2809
+ expect(violations[0].message).toContain('user.signup');
2810
+ expect(violations[0].message).toContain('name');
2811
+ });
2812
+
2813
+ it('should not validate attributes for events not included in workflow (disconnected subgraph support)', async () => {
2814
+ // This test verifies the fix for Issue 3: Conflicting Validation Rules for Shared Canvases with Disconnected Subgraphs
2815
+ // A canvas may have multiple disconnected flows, and workflows may focus on only one flow.
2816
+ // We should NOT report "attribute not used" errors for events that aren't even part of the workflow.
2817
+
2818
+ const workflow: WorkflowTemplate = {
2819
+ version: '1.0.0',
2820
+ name: 'Navigation Flow',
2821
+ description: 'Workflow focusing only on navigation events (subgraph 1)',
2822
+ mode: 'span-tree',
2823
+ canvas: 'multi-flow.otel.canvas',
2824
+ scenarios: [{
2825
+ id: 'navigation',
2826
+ priority: 1,
2827
+ description: 'Navigation flow',
2828
+ template: {
2829
+ events: {
2830
+ // Only uses events from the navigation subgraph
2831
+ 'panel.opened': 'Panel {{panelName}} opened',
2832
+ 'panel.closed': 'Panel {{panelName}} closed'
2833
+ }
2834
+ }
2835
+ }]
2836
+ };
2837
+
2838
+ // Canvas with TWO disconnected subgraphs:
2839
+ // Subgraph 1 (navigation): panel.opened -> panel.closed
2840
+ // Subgraph 2 (tool execution): tool.started -> tool.completed
2841
+ const canvas: ExtendedCanvas = {
2842
+ nodes: [
2843
+ // Subgraph 1: Navigation (included in workflow)
2844
+ {
2845
+ id: 'nav1',
2846
+ type: 'otel-event',
2847
+ x: 0,
2848
+ y: 0,
2849
+ width: 100,
2850
+ height: 50,
2851
+ event: {
2852
+ name: 'panel.opened',
2853
+ description: 'Panel opened event',
2854
+ attributes: {
2855
+ panelName: { type: 'string' }
2856
+ }
2857
+ }
2858
+ },
2859
+ {
2860
+ id: 'nav2',
2861
+ type: 'otel-event',
2862
+ x: 200,
2863
+ y: 0,
2864
+ width: 100,
2865
+ height: 50,
2866
+ event: {
2867
+ name: 'panel.closed',
2868
+ description: 'Panel closed event',
2869
+ attributes: {
2870
+ panelName: { type: 'string' }
2871
+ }
2872
+ }
2873
+ },
2874
+ // Subgraph 2: Tool execution (NOT included in workflow)
2875
+ {
2876
+ id: 'tool1',
2877
+ type: 'otel-event',
2878
+ x: 0,
2879
+ y: 200,
2880
+ width: 100,
2881
+ height: 50,
2882
+ event: {
2883
+ name: 'tool.started',
2884
+ description: 'Tool started event',
2885
+ attributes: {
2886
+ toolName: { type: 'string' },
2887
+ toolArgs: { type: 'string' } // These attributes are NOT used in the workflow
2888
+ }
2889
+ }
2890
+ },
2891
+ {
2892
+ id: 'tool2',
2893
+ type: 'otel-event',
2894
+ x: 200,
2895
+ y: 200,
2896
+ width: 100,
2897
+ height: 50,
2898
+ event: {
2899
+ name: 'tool.completed',
2900
+ description: 'Tool completed event',
2901
+ attributes: {
2902
+ toolName: { type: 'string' },
2903
+ result: { type: 'string' } // These attributes are NOT used in the workflow
2904
+ }
2905
+ }
2906
+ }
2907
+ ],
2908
+ edges: [
2909
+ // Subgraph 1 edges
2910
+ { id: 'e1', fromNode: 'nav1', fromSide: 'right', toNode: 'nav2', toSide: 'left' },
2911
+ // Subgraph 2 edges (disconnected from subgraph 1)
2912
+ { id: 'e2', fromNode: 'tool1', fromSide: 'right', toNode: 'tool2', toSide: 'left' }
2913
+ ],
2914
+ pv: { version: '1.0.0', name: 'Multi-Flow Canvas', markdown: 'test.md' }
2915
+ };
2916
+
2917
+ const context: WorkflowValidationContext = {
2918
+ workflow,
2919
+ workflowPath: 'navigation.workflow.json',
2920
+ canvas,
2921
+ basePath: tempDir
2922
+ };
2923
+
2924
+ const result = await validator.validate(context);
2925
+ const attributeViolations = result.violations.filter(v => v.ruleId === 'workflow-attribute-unused');
2926
+ const connectivityViolations = result.violations.filter(v => v.ruleId === 'workflow-event-connectivity');
2927
+
2928
+ // Should NOT have any "attribute not used" errors for tool.started or tool.completed
2929
+ // because those events are not part of this workflow
2930
+ expect(attributeViolations).toHaveLength(0);
2931
+
2932
+ // Should NOT have connectivity errors because the workflow only uses connected events
2933
+ expect(connectivityViolations).toHaveLength(0);
2934
+ });
2935
+ });
2936
+
2937
+ describe('workflow-template-attribute-not-in-schema', () => {
2938
+ it('should error when template references attribute not in event schema', async () => {
2939
+ const workflow: WorkflowTemplate = {
2940
+ version: '1.0.0',
2941
+ name: 'Test',
2942
+ description: 'Test workflow',
2943
+ mode: 'span-tree',
2944
+ canvas: 'test.otel.canvas',
2945
+ scenarios: [{
2946
+ id: 'test',
2947
+ priority: 1,
2948
+ description: 'Test scenario',
2949
+ template: {
2950
+ events: {
2951
+ // Template references context.projectRoot which is NOT in the schema
2952
+ 'task.delete.error': 'Task {{input.taskId}} not found in {{context.projectRoot}}'
2953
+ }
2954
+ }
2955
+ }]
2956
+ };
2957
+
2958
+ const canvas: ExtendedCanvas = {
2959
+ nodes: [
2960
+ {
2961
+ id: 'n1',
2962
+ type: 'otel-event',
2963
+ x: 0,
2964
+ y: 0,
2965
+ width: 100,
2966
+ height: 50,
2967
+ event: {
2968
+ name: 'task.delete.error',
2969
+ description: 'Task deletion error',
2970
+ attributes: {
2971
+ 'error.type': { type: 'string' },
2972
+ 'error.message': { type: 'string' },
2973
+ 'input.taskId': { type: 'string' }
2974
+ // Note: context.projectRoot is NOT defined here
2975
+ }
2976
+ }
2977
+ }
2978
+ ],
2979
+ edges: [],
2980
+ pv: { version: '1.0.0', name: 'Test Canvas', markdown: 'test.md' }
2981
+ };
2982
+
2983
+ const context: WorkflowValidationContext = {
2984
+ workflow,
2985
+ workflowPath: 'test.workflow.json',
2986
+ canvas,
2987
+ basePath: tempDir
2988
+ };
2989
+
2990
+ const result = await validator.validate(context);
2991
+ const violations = result.violations.filter(v => v.ruleId === 'workflow-template-attribute-not-in-schema');
2992
+
2993
+ expect(violations).toHaveLength(1);
2994
+ expect(violations[0].message).toContain('context.projectRoot');
2995
+ expect(violations[0].message).toContain('not defined in the event schema');
2996
+ expect(violations[0].message).toContain('task.delete.error');
2997
+ });
2998
+
2999
+ it('should pass when all template attributes are in event schema', async () => {
3000
+ const workflow: WorkflowTemplate = {
3001
+ version: '1.0.0',
3002
+ name: 'Test',
3003
+ description: 'Test workflow',
3004
+ mode: 'span-tree',
3005
+ canvas: 'test.otel.canvas',
3006
+ scenarios: [{
3007
+ id: 'test',
3008
+ priority: 1,
3009
+ description: 'Test scenario',
3010
+ template: {
3011
+ events: {
3012
+ 'task.create.complete': 'Created task {{output.taskId}} in {{duration.ms}}ms'
3013
+ }
3014
+ }
3015
+ }]
3016
+ };
3017
+
3018
+ const canvas: ExtendedCanvas = {
3019
+ nodes: [
3020
+ {
3021
+ id: 'n1',
3022
+ type: 'otel-event',
3023
+ x: 0,
3024
+ y: 0,
3025
+ width: 100,
3026
+ height: 50,
3027
+ event: {
3028
+ name: 'task.create.complete',
3029
+ description: 'Task creation complete',
3030
+ attributes: {
3031
+ 'output.taskId': { type: 'string' },
3032
+ 'duration.ms': { type: 'number' }
3033
+ }
3034
+ }
3035
+ }
3036
+ ],
3037
+ edges: [],
3038
+ pv: { version: '1.0.0', name: 'Test Canvas', markdown: 'test.md' }
3039
+ };
3040
+
3041
+ const context: WorkflowValidationContext = {
3042
+ workflow,
3043
+ workflowPath: 'test.workflow.json',
3044
+ canvas,
3045
+ basePath: tempDir
3046
+ };
3047
+
3048
+ const result = await validator.validate(context);
3049
+ const violations = result.violations.filter(v => v.ruleId === 'workflow-template-attribute-not-in-schema');
3050
+
3051
+ expect(violations).toHaveLength(0);
3052
+ });
3053
+
3054
+ it('should error for multiple undefined attributes', async () => {
3055
+ const workflow: WorkflowTemplate = {
3056
+ version: '1.0.0',
3057
+ name: 'Test',
3058
+ description: 'Test workflow',
3059
+ mode: 'span-tree',
3060
+ canvas: 'test.otel.canvas',
3061
+ scenarios: [{
3062
+ id: 'test',
3063
+ priority: 1,
3064
+ description: 'Test scenario',
3065
+ template: {
3066
+ events: {
3067
+ 'task.delete.error': 'Task {{input.taskId}} not found in {{context.projectRoot}} ({{context.taskCount}} tasks)'
3068
+ }
3069
+ }
3070
+ }]
3071
+ };
3072
+
3073
+ const canvas: ExtendedCanvas = {
3074
+ nodes: [
3075
+ {
3076
+ id: 'n1',
3077
+ type: 'otel-event',
3078
+ x: 0,
3079
+ y: 0,
3080
+ width: 100,
3081
+ height: 50,
3082
+ event: {
3083
+ name: 'task.delete.error',
3084
+ description: 'Task deletion error',
3085
+ attributes: {
3086
+ 'input.taskId': { type: 'string' }
3087
+ // context.projectRoot and context.taskCount are NOT defined
3088
+ }
3089
+ }
3090
+ }
3091
+ ],
3092
+ edges: [],
3093
+ pv: { version: '1.0.0', name: 'Test Canvas', markdown: 'test.md' }
3094
+ };
3095
+
3096
+ const context: WorkflowValidationContext = {
3097
+ workflow,
3098
+ workflowPath: 'test.workflow.json',
3099
+ canvas,
3100
+ basePath: tempDir
3101
+ };
3102
+
3103
+ const result = await validator.validate(context);
3104
+ const violations = result.violations.filter(v => v.ruleId === 'workflow-template-attribute-not-in-schema');
3105
+
3106
+ expect(violations).toHaveLength(2);
3107
+ expect(violations.some(v => v.message.includes('context.projectRoot'))).toBe(true);
3108
+ expect(violations.some(v => v.message.includes('context.taskCount'))).toBe(true);
3109
+ });
3110
+
3111
+ it('should handle nested attribute access when parent is defined', async () => {
3112
+ const workflow: WorkflowTemplate = {
3113
+ version: '1.0.0',
3114
+ name: 'Test',
3115
+ description: 'Test workflow',
3116
+ mode: 'span-tree',
3117
+ canvas: 'test.otel.canvas',
3118
+ scenarios: [{
3119
+ id: 'test',
3120
+ priority: 1,
3121
+ description: 'Test scenario',
3122
+ template: {
3123
+ events: {
3124
+ // Accessing nested path input.taskId when input.taskId is defined
3125
+ 'task.update.started': 'Updating task {{input.taskId}}'
3126
+ }
3127
+ }
3128
+ }]
3129
+ };
3130
+
3131
+ const canvas: ExtendedCanvas = {
3132
+ nodes: [
3133
+ {
3134
+ id: 'n1',
3135
+ type: 'otel-event',
3136
+ x: 0,
3137
+ y: 0,
3138
+ width: 100,
3139
+ height: 50,
3140
+ event: {
3141
+ name: 'task.update.started',
3142
+ description: 'Task update started',
3143
+ attributes: {
3144
+ 'input.taskId': { type: 'string' },
3145
+ 'input.hasTitle': { type: 'boolean' }
3146
+ }
3147
+ }
3148
+ }
3149
+ ],
3150
+ edges: [],
3151
+ pv: { version: '1.0.0', name: 'Test Canvas', markdown: 'test.md' }
3152
+ };
3153
+
3154
+ const context: WorkflowValidationContext = {
3155
+ workflow,
3156
+ workflowPath: 'test.workflow.json',
3157
+ canvas,
3158
+ basePath: tempDir
3159
+ };
3160
+
3161
+ const result = await validator.validate(context);
3162
+ const violations = result.violations.filter(v => v.ruleId === 'workflow-template-attribute-not-in-schema');
3163
+
3164
+ expect(violations).toHaveLength(0);
3165
+ });
3166
+
3167
+ it('should suggest similar attributes when typo detected', async () => {
3168
+ const workflow: WorkflowTemplate = {
3169
+ version: '1.0.0',
3170
+ name: 'Test',
3171
+ description: 'Test workflow',
3172
+ mode: 'span-tree',
3173
+ canvas: 'test.otel.canvas',
3174
+ scenarios: [{
3175
+ id: 'test',
3176
+ priority: 1,
3177
+ description: 'Test scenario',
3178
+ template: {
3179
+ events: {
3180
+ // Typo: input.takId instead of input.taskId
3181
+ 'task.delete.started': 'Deleting task {{input.takId}}'
3182
+ }
3183
+ }
3184
+ }]
3185
+ };
3186
+
3187
+ const canvas: ExtendedCanvas = {
3188
+ nodes: [
3189
+ {
3190
+ id: 'n1',
3191
+ type: 'otel-event',
3192
+ x: 0,
3193
+ y: 0,
3194
+ width: 100,
3195
+ height: 50,
3196
+ event: {
3197
+ name: 'task.delete.started',
3198
+ description: 'Task deletion started',
3199
+ attributes: {
3200
+ 'input.taskId': { type: 'string' }
3201
+ }
3202
+ }
3203
+ }
3204
+ ],
3205
+ edges: [],
3206
+ pv: { version: '1.0.0', name: 'Test Canvas', markdown: 'test.md' }
3207
+ };
3208
+
3209
+ const context: WorkflowValidationContext = {
3210
+ workflow,
3211
+ workflowPath: 'test.workflow.json',
3212
+ canvas,
3213
+ basePath: tempDir
3214
+ };
3215
+
3216
+ const result = await validator.validate(context);
3217
+ const violations = result.violations.filter(v => v.ruleId === 'workflow-template-attribute-not-in-schema');
3218
+
3219
+ expect(violations).toHaveLength(1);
3220
+ expect(violations[0].message).toContain('input.takId');
3221
+ // Should suggest the similar attribute
3222
+ expect(violations[0].suggestion).toContain('input.taskId');
3223
+ });
3224
+ });
3225
+ });