@principal-ai/subsystems-core 0.29.0 → 0.31.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 (558) hide show
  1. package/README.md +15 -110
  2. package/dist/index.d.ts +7 -75
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +21 -205
  5. package/dist/index.js.map +1 -1
  6. package/dist/node.d.ts +5 -61
  7. package/dist/node.d.ts.map +1 -1
  8. package/dist/node.js +9 -131
  9. package/dist/node.js.map +1 -1
  10. package/dist/types/index.d.ts +3 -348
  11. package/dist/types/index.d.ts.map +1 -1
  12. package/dist/types/index.js +3 -19
  13. package/dist/types/index.js.map +1 -1
  14. package/dist/types/subsystem-model.d.ts +34 -3
  15. package/dist/types/subsystem-model.d.ts.map +1 -1
  16. package/dist/types/subsystem-model.js.map +1 -1
  17. package/package.json +7 -24
  18. package/schemas/subsystem-model.schema.json +52 -1
  19. package/src/index.ts +7 -580
  20. package/src/node.ts +21 -262
  21. package/src/types/index.ts +3 -456
  22. package/src/types/subsystem-model.ts +39 -3
  23. package/dist/ConfigurationLoader.d.ts +0 -76
  24. package/dist/ConfigurationLoader.d.ts.map +0 -1
  25. package/dist/ConfigurationLoader.js +0 -148
  26. package/dist/ConfigurationLoader.js.map +0 -1
  27. package/dist/ConfigurationValidator.d.ts +0 -31
  28. package/dist/ConfigurationValidator.d.ts.map +0 -1
  29. package/dist/ConfigurationValidator.js +0 -239
  30. package/dist/ConfigurationValidator.js.map +0 -1
  31. package/dist/EventProcessor.d.ts +0 -49
  32. package/dist/EventProcessor.d.ts.map +0 -1
  33. package/dist/EventProcessor.js +0 -220
  34. package/dist/EventProcessor.js.map +0 -1
  35. package/dist/EventRecorderService.d.ts +0 -305
  36. package/dist/EventRecorderService.d.ts.map +0 -1
  37. package/dist/EventRecorderService.js +0 -467
  38. package/dist/EventRecorderService.js.map +0 -1
  39. package/dist/LibraryLoader.d.ts +0 -65
  40. package/dist/LibraryLoader.d.ts.map +0 -1
  41. package/dist/LibraryLoader.js +0 -197
  42. package/dist/LibraryLoader.js.map +0 -1
  43. package/dist/PathBasedEventProcessor.d.ts +0 -77
  44. package/dist/PathBasedEventProcessor.d.ts.map +0 -1
  45. package/dist/PathBasedEventProcessor.js +0 -136
  46. package/dist/PathBasedEventProcessor.js.map +0 -1
  47. package/dist/SessionManager.d.ts +0 -194
  48. package/dist/SessionManager.d.ts.map +0 -1
  49. package/dist/SessionManager.js +0 -302
  50. package/dist/SessionManager.js.map +0 -1
  51. package/dist/ValidationEngine.d.ts +0 -31
  52. package/dist/ValidationEngine.d.ts.map +0 -1
  53. package/dist/ValidationEngine.js +0 -162
  54. package/dist/ValidationEngine.js.map +0 -1
  55. package/dist/auxiliary/AuxiliaryManifestValidator.d.ts +0 -41
  56. package/dist/auxiliary/AuxiliaryManifestValidator.d.ts.map +0 -1
  57. package/dist/auxiliary/AuxiliaryManifestValidator.js +0 -100
  58. package/dist/auxiliary/AuxiliaryManifestValidator.js.map +0 -1
  59. package/dist/auxiliary/index.d.ts +0 -9
  60. package/dist/auxiliary/index.d.ts.map +0 -1
  61. package/dist/auxiliary/index.js +0 -14
  62. package/dist/auxiliary/index.js.map +0 -1
  63. package/dist/auxiliary/validateAreaScopeDisjoint.d.ts +0 -24
  64. package/dist/auxiliary/validateAreaScopeDisjoint.d.ts.map +0 -1
  65. package/dist/auxiliary/validateAreaScopeDisjoint.js +0 -61
  66. package/dist/auxiliary/validateAreaScopeDisjoint.js.map +0 -1
  67. package/dist/cli/codegen.d.ts +0 -41
  68. package/dist/cli/codegen.d.ts.map +0 -1
  69. package/dist/cli/codegen.js +0 -257
  70. package/dist/cli/codegen.js.map +0 -1
  71. package/dist/codegen/type-generator.d.ts +0 -123
  72. package/dist/codegen/type-generator.d.ts.map +0 -1
  73. package/dist/codegen/type-generator.js +0 -433
  74. package/dist/codegen/type-generator.js.map +0 -1
  75. package/dist/codegen/usage-example.d.ts +0 -103
  76. package/dist/codegen/usage-example.d.ts.map +0 -1
  77. package/dist/codegen/usage-example.js +0 -258
  78. package/dist/codegen/usage-example.js.map +0 -1
  79. package/dist/dashboard/DashboardValidator.d.ts +0 -85
  80. package/dist/dashboard/DashboardValidator.d.ts.map +0 -1
  81. package/dist/dashboard/DashboardValidator.js +0 -775
  82. package/dist/dashboard/DashboardValidator.js.map +0 -1
  83. package/dist/dashboard/index.d.ts +0 -6
  84. package/dist/dashboard/index.d.ts.map +0 -1
  85. package/dist/dashboard/index.js +0 -10
  86. package/dist/dashboard/index.js.map +0 -1
  87. package/dist/discovery/CanvasDiscovery.d.ts +0 -160
  88. package/dist/discovery/CanvasDiscovery.d.ts.map +0 -1
  89. package/dist/discovery/CanvasDiscovery.js +0 -865
  90. package/dist/discovery/CanvasDiscovery.js.map +0 -1
  91. package/dist/discovery/CanvasFileManifest.d.ts +0 -86
  92. package/dist/discovery/CanvasFileManifest.d.ts.map +0 -1
  93. package/dist/discovery/CanvasFileManifest.js +0 -338
  94. package/dist/discovery/CanvasFileManifest.js.map +0 -1
  95. package/dist/discovery/LibraryDiscovery.d.ts +0 -108
  96. package/dist/discovery/LibraryDiscovery.d.ts.map +0 -1
  97. package/dist/discovery/LibraryDiscovery.js +0 -263
  98. package/dist/discovery/LibraryDiscovery.js.map +0 -1
  99. package/dist/discovery/types.d.ts +0 -403
  100. package/dist/discovery/types.d.ts.map +0 -1
  101. package/dist/discovery/types.js +0 -6
  102. package/dist/discovery/types.js.map +0 -1
  103. package/dist/events/EventsCanvasValidator.d.ts +0 -139
  104. package/dist/events/EventsCanvasValidator.d.ts.map +0 -1
  105. package/dist/events/EventsCanvasValidator.js +0 -281
  106. package/dist/events/EventsCanvasValidator.js.map +0 -1
  107. package/dist/events/NamespacePathIndex.d.ts +0 -61
  108. package/dist/events/NamespacePathIndex.d.ts.map +0 -1
  109. package/dist/events/NamespacePathIndex.js +0 -74
  110. package/dist/events/NamespacePathIndex.js.map +0 -1
  111. package/dist/events/OtelEventPathsValidator.d.ts +0 -75
  112. package/dist/events/OtelEventPathsValidator.d.ts.map +0 -1
  113. package/dist/events/OtelEventPathsValidator.js +0 -126
  114. package/dist/events/OtelEventPathsValidator.js.map +0 -1
  115. package/dist/events/ScopeEventsValidator.d.ts +0 -68
  116. package/dist/events/ScopeEventsValidator.d.ts.map +0 -1
  117. package/dist/events/ScopeEventsValidator.js +0 -91
  118. package/dist/events/ScopeEventsValidator.js.map +0 -1
  119. package/dist/events/index.d.ts +0 -9
  120. package/dist/events/index.d.ts.map +0 -1
  121. package/dist/events/index.js +0 -12
  122. package/dist/events/index.js.map +0 -1
  123. package/dist/events/path-helpers.d.ts +0 -33
  124. package/dist/events/path-helpers.d.ts.map +0 -1
  125. package/dist/events/path-helpers.js +0 -59
  126. package/dist/events/path-helpers.js.map +0 -1
  127. package/dist/execution/ExecutionLoader.d.ts +0 -135
  128. package/dist/execution/ExecutionLoader.d.ts.map +0 -1
  129. package/dist/execution/ExecutionLoader.js +0 -266
  130. package/dist/execution/ExecutionLoader.js.map +0 -1
  131. package/dist/execution/ExecutionValidator.d.ts +0 -94
  132. package/dist/execution/ExecutionValidator.d.ts.map +0 -1
  133. package/dist/execution/ExecutionValidator.js +0 -503
  134. package/dist/execution/ExecutionValidator.js.map +0 -1
  135. package/dist/generated/graph-converter-execution.types.d.ts +0 -177
  136. package/dist/generated/graph-converter-execution.types.d.ts.map +0 -1
  137. package/dist/generated/graph-converter-execution.types.js +0 -12
  138. package/dist/generated/graph-converter-execution.types.js.map +0 -1
  139. package/dist/helpers/GraphInstrumentationHelper.d.ts +0 -93
  140. package/dist/helpers/GraphInstrumentationHelper.d.ts.map +0 -1
  141. package/dist/helpers/GraphInstrumentationHelper.js +0 -252
  142. package/dist/helpers/GraphInstrumentationHelper.js.map +0 -1
  143. package/dist/matchers/SpanMatcher.d.ts +0 -107
  144. package/dist/matchers/SpanMatcher.d.ts.map +0 -1
  145. package/dist/matchers/SpanMatcher.js +0 -274
  146. package/dist/matchers/SpanMatcher.js.map +0 -1
  147. package/dist/orchestration/TraceOrchestrator.d.ts +0 -71
  148. package/dist/orchestration/TraceOrchestrator.d.ts.map +0 -1
  149. package/dist/orchestration/TraceOrchestrator.js +0 -416
  150. package/dist/orchestration/TraceOrchestrator.js.map +0 -1
  151. package/dist/parsers/OtlpTraceParser.d.ts +0 -131
  152. package/dist/parsers/OtlpTraceParser.d.ts.map +0 -1
  153. package/dist/parsers/OtlpTraceParser.js +0 -310
  154. package/dist/parsers/OtlpTraceParser.js.map +0 -1
  155. package/dist/registry/CompositeRegistry.d.ts +0 -71
  156. package/dist/registry/CompositeRegistry.d.ts.map +0 -1
  157. package/dist/registry/CompositeRegistry.js +0 -113
  158. package/dist/registry/CompositeRegistry.js.map +0 -1
  159. package/dist/registry/EventRegistry.d.ts +0 -79
  160. package/dist/registry/EventRegistry.d.ts.map +0 -1
  161. package/dist/registry/EventRegistry.js +0 -165
  162. package/dist/registry/EventRegistry.js.map +0 -1
  163. package/dist/registry/LocalRegistry.d.ts +0 -111
  164. package/dist/registry/LocalRegistry.d.ts.map +0 -1
  165. package/dist/registry/LocalRegistry.js +0 -335
  166. package/dist/registry/LocalRegistry.js.map +0 -1
  167. package/dist/registry/RemoteRegistry.d.ts +0 -122
  168. package/dist/registry/RemoteRegistry.d.ts.map +0 -1
  169. package/dist/registry/RemoteRegistry.js +0 -399
  170. package/dist/registry/RemoteRegistry.js.map +0 -1
  171. package/dist/registry/VersionRegistry.d.ts +0 -51
  172. package/dist/registry/VersionRegistry.d.ts.map +0 -1
  173. package/dist/registry/VersionRegistry.js +0 -105
  174. package/dist/registry/VersionRegistry.js.map +0 -1
  175. package/dist/rules/config.d.ts +0 -57
  176. package/dist/rules/config.d.ts.map +0 -1
  177. package/dist/rules/config.js +0 -382
  178. package/dist/rules/config.js.map +0 -1
  179. package/dist/rules/engine.d.ts +0 -70
  180. package/dist/rules/engine.d.ts.map +0 -1
  181. package/dist/rules/engine.js +0 -373
  182. package/dist/rules/engine.js.map +0 -1
  183. package/dist/rules/implementations/connection-type-references.d.ts +0 -7
  184. package/dist/rules/implementations/connection-type-references.d.ts.map +0 -1
  185. package/dist/rules/implementations/connection-type-references.js +0 -107
  186. package/dist/rules/implementations/connection-type-references.js.map +0 -1
  187. package/dist/rules/implementations/dead-end-states.d.ts +0 -17
  188. package/dist/rules/implementations/dead-end-states.d.ts.map +0 -1
  189. package/dist/rules/implementations/dead-end-states.js +0 -75
  190. package/dist/rules/implementations/dead-end-states.js.map +0 -1
  191. package/dist/rules/implementations/index.d.ts +0 -23
  192. package/dist/rules/implementations/index.d.ts.map +0 -1
  193. package/dist/rules/implementations/index.js +0 -73
  194. package/dist/rules/implementations/index.js.map +0 -1
  195. package/dist/rules/implementations/minimum-node-sources.d.ts +0 -24
  196. package/dist/rules/implementations/minimum-node-sources.d.ts.map +0 -1
  197. package/dist/rules/implementations/minimum-node-sources.js +0 -59
  198. package/dist/rules/implementations/minimum-node-sources.js.map +0 -1
  199. package/dist/rules/implementations/no-unknown-fields.d.ts +0 -7
  200. package/dist/rules/implementations/no-unknown-fields.d.ts.map +0 -1
  201. package/dist/rules/implementations/no-unknown-fields.js +0 -216
  202. package/dist/rules/implementations/no-unknown-fields.js.map +0 -1
  203. package/dist/rules/implementations/orphaned-edge-types.d.ts +0 -7
  204. package/dist/rules/implementations/orphaned-edge-types.d.ts.map +0 -1
  205. package/dist/rules/implementations/orphaned-edge-types.js +0 -50
  206. package/dist/rules/implementations/orphaned-edge-types.js.map +0 -1
  207. package/dist/rules/implementations/orphaned-node-types.d.ts +0 -7
  208. package/dist/rules/implementations/orphaned-node-types.d.ts.map +0 -1
  209. package/dist/rules/implementations/orphaned-node-types.js +0 -53
  210. package/dist/rules/implementations/orphaned-node-types.js.map +0 -1
  211. package/dist/rules/implementations/required-metadata.d.ts +0 -7
  212. package/dist/rules/implementations/required-metadata.d.ts.map +0 -1
  213. package/dist/rules/implementations/required-metadata.js +0 -48
  214. package/dist/rules/implementations/required-metadata.js.map +0 -1
  215. package/dist/rules/implementations/state-transition-references.d.ts +0 -7
  216. package/dist/rules/implementations/state-transition-references.d.ts.map +0 -1
  217. package/dist/rules/implementations/state-transition-references.js +0 -138
  218. package/dist/rules/implementations/state-transition-references.js.map +0 -1
  219. package/dist/rules/implementations/unreachable-states.d.ts +0 -7
  220. package/dist/rules/implementations/unreachable-states.d.ts.map +0 -1
  221. package/dist/rules/implementations/unreachable-states.js +0 -83
  222. package/dist/rules/implementations/unreachable-states.js.map +0 -1
  223. package/dist/rules/implementations/valid-action-patterns.d.ts +0 -17
  224. package/dist/rules/implementations/valid-action-patterns.d.ts.map +0 -1
  225. package/dist/rules/implementations/valid-action-patterns.js +0 -112
  226. package/dist/rules/implementations/valid-action-patterns.js.map +0 -1
  227. package/dist/rules/implementations/valid-color-format.d.ts +0 -7
  228. package/dist/rules/implementations/valid-color-format.d.ts.map +0 -1
  229. package/dist/rules/implementations/valid-color-format.js +0 -94
  230. package/dist/rules/implementations/valid-color-format.js.map +0 -1
  231. package/dist/rules/implementations/valid-edge-types.d.ts +0 -7
  232. package/dist/rules/implementations/valid-edge-types.d.ts.map +0 -1
  233. package/dist/rules/implementations/valid-edge-types.js +0 -248
  234. package/dist/rules/implementations/valid-edge-types.js.map +0 -1
  235. package/dist/rules/implementations/valid-node-types.d.ts +0 -7
  236. package/dist/rules/implementations/valid-node-types.d.ts.map +0 -1
  237. package/dist/rules/implementations/valid-node-types.js +0 -187
  238. package/dist/rules/implementations/valid-node-types.js.map +0 -1
  239. package/dist/rules/index.d.ts +0 -28
  240. package/dist/rules/index.d.ts.map +0 -1
  241. package/dist/rules/index.js +0 -70
  242. package/dist/rules/index.js.map +0 -1
  243. package/dist/rules/types.d.ts +0 -309
  244. package/dist/rules/types.d.ts.map +0 -1
  245. package/dist/rules/types.js +0 -40
  246. package/dist/rules/types.js.map +0 -1
  247. package/dist/scopes/ScopePathIndex.d.ts +0 -56
  248. package/dist/scopes/ScopePathIndex.d.ts.map +0 -1
  249. package/dist/scopes/ScopePathIndex.js +0 -67
  250. package/dist/scopes/ScopePathIndex.js.map +0 -1
  251. package/dist/scopes/ScopesCanvasValidator.d.ts +0 -76
  252. package/dist/scopes/ScopesCanvasValidator.d.ts.map +0 -1
  253. package/dist/scopes/ScopesCanvasValidator.js +0 -221
  254. package/dist/scopes/ScopesCanvasValidator.js.map +0 -1
  255. package/dist/scopes/index.d.ts +0 -12
  256. package/dist/scopes/index.d.ts.map +0 -1
  257. package/dist/scopes/index.js +0 -30
  258. package/dist/scopes/index.js.map +0 -1
  259. package/dist/scopes/utils.d.ts +0 -85
  260. package/dist/scopes/utils.d.ts.map +0 -1
  261. package/dist/scopes/utils.js +0 -143
  262. package/dist/scopes/utils.js.map +0 -1
  263. package/dist/scopes/validateScopeNamespaceNesting.d.ts +0 -38
  264. package/dist/scopes/validateScopeNamespaceNesting.d.ts.map +0 -1
  265. package/dist/scopes/validateScopeNamespaceNesting.js +0 -69
  266. package/dist/scopes/validateScopeNamespaceNesting.js.map +0 -1
  267. package/dist/spans/index.d.ts +0 -12
  268. package/dist/spans/index.d.ts.map +0 -1
  269. package/dist/spans/index.js +0 -25
  270. package/dist/spans/index.js.map +0 -1
  271. package/dist/spans/utils.d.ts +0 -70
  272. package/dist/spans/utils.d.ts.map +0 -1
  273. package/dist/spans/utils.js +0 -205
  274. package/dist/spans/utils.js.map +0 -1
  275. package/dist/storyboard/builder.d.ts +0 -135
  276. package/dist/storyboard/builder.d.ts.map +0 -1
  277. package/dist/storyboard/builder.js +0 -235
  278. package/dist/storyboard/builder.js.map +0 -1
  279. package/dist/storyboard/index.d.ts +0 -9
  280. package/dist/storyboard/index.d.ts.map +0 -1
  281. package/dist/storyboard/index.js +0 -18
  282. package/dist/storyboard/index.js.map +0 -1
  283. package/dist/storyboard/types.d.ts +0 -101
  284. package/dist/storyboard/types.d.ts.map +0 -1
  285. package/dist/storyboard/types.js +0 -10
  286. package/dist/storyboard/types.js.map +0 -1
  287. package/dist/telemetry/coverage.d.ts +0 -92
  288. package/dist/telemetry/coverage.d.ts.map +0 -1
  289. package/dist/telemetry/coverage.js +0 -335
  290. package/dist/telemetry/coverage.js.map +0 -1
  291. package/dist/telemetry/event-validator.d.ts +0 -86
  292. package/dist/telemetry/event-validator.d.ts.map +0 -1
  293. package/dist/telemetry/event-validator.js +0 -257
  294. package/dist/telemetry/event-validator.js.map +0 -1
  295. package/dist/types/audit.d.ts +0 -236
  296. package/dist/types/audit.d.ts.map +0 -1
  297. package/dist/types/audit.js +0 -53
  298. package/dist/types/audit.js.map +0 -1
  299. package/dist/types/auxiliary.d.ts +0 -54
  300. package/dist/types/auxiliary.d.ts.map +0 -1
  301. package/dist/types/auxiliary.js +0 -27
  302. package/dist/types/auxiliary.js.map +0 -1
  303. package/dist/types/canvas-scope.d.ts +0 -156
  304. package/dist/types/canvas-scope.d.ts.map +0 -1
  305. package/dist/types/canvas-scope.js +0 -62
  306. package/dist/types/canvas-scope.js.map +0 -1
  307. package/dist/types/canvas.d.ts +0 -1138
  308. package/dist/types/canvas.d.ts.map +0 -1
  309. package/dist/types/canvas.js +0 -178
  310. package/dist/types/canvas.js.map +0 -1
  311. package/dist/types/dashboard.d.ts +0 -170
  312. package/dist/types/dashboard.d.ts.map +0 -1
  313. package/dist/types/dashboard.js +0 -14
  314. package/dist/types/dashboard.js.map +0 -1
  315. package/dist/types/library.d.ts +0 -227
  316. package/dist/types/library.d.ts.map +0 -1
  317. package/dist/types/library.js +0 -16
  318. package/dist/types/library.js.map +0 -1
  319. package/dist/types/otel.d.ts +0 -465
  320. package/dist/types/otel.d.ts.map +0 -1
  321. package/dist/types/otel.js +0 -163
  322. package/dist/types/otel.js.map +0 -1
  323. package/dist/types/path-based-config.d.ts +0 -235
  324. package/dist/types/path-based-config.d.ts.map +0 -1
  325. package/dist/types/path-based-config.js +0 -11
  326. package/dist/types/path-based-config.js.map +0 -1
  327. package/dist/types/registered-trace.d.ts +0 -337
  328. package/dist/types/registered-trace.d.ts.map +0 -1
  329. package/dist/types/registered-trace.js +0 -16
  330. package/dist/types/registered-trace.js.map +0 -1
  331. package/dist/types/resource-match.d.ts +0 -207
  332. package/dist/types/resource-match.d.ts.map +0 -1
  333. package/dist/types/resource-match.js +0 -74
  334. package/dist/types/resource-match.js.map +0 -1
  335. package/dist/types/version-registry.d.ts +0 -137
  336. package/dist/types/version-registry.d.ts.map +0 -1
  337. package/dist/types/version-registry.js +0 -10
  338. package/dist/types/version-registry.js.map +0 -1
  339. package/dist/utils/CanvasConverter.d.ts +0 -148
  340. package/dist/utils/CanvasConverter.d.ts.map +0 -1
  341. package/dist/utils/CanvasConverter.js +0 -530
  342. package/dist/utils/CanvasConverter.js.map +0 -1
  343. package/dist/utils/GraphConverter.d.ts +0 -18
  344. package/dist/utils/GraphConverter.d.ts.map +0 -1
  345. package/dist/utils/GraphConverter.js +0 -80
  346. package/dist/utils/GraphConverter.js.map +0 -1
  347. package/dist/utils/PathMatcher.d.ts +0 -55
  348. package/dist/utils/PathMatcher.d.ts.map +0 -1
  349. package/dist/utils/PathMatcher.js +0 -178
  350. package/dist/utils/PathMatcher.js.map +0 -1
  351. package/dist/utils/TraceToCanvas.d.ts +0 -83
  352. package/dist/utils/TraceToCanvas.d.ts.map +0 -1
  353. package/dist/utils/TraceToCanvas.js +0 -336
  354. package/dist/utils/TraceToCanvas.js.map +0 -1
  355. package/dist/utils/YamlParser.d.ts +0 -36
  356. package/dist/utils/YamlParser.d.ts.map +0 -1
  357. package/dist/utils/YamlParser.js +0 -92
  358. package/dist/utils/YamlParser.js.map +0 -1
  359. package/dist/utils/registeredTraceHelpers.d.ts +0 -76
  360. package/dist/utils/registeredTraceHelpers.d.ts.map +0 -1
  361. package/dist/utils/registeredTraceHelpers.js +0 -203
  362. package/dist/utils/registeredTraceHelpers.js.map +0 -1
  363. package/dist/utils/traceAggregation.d.ts +0 -25
  364. package/dist/utils/traceAggregation.d.ts.map +0 -1
  365. package/dist/utils/traceAggregation.js +0 -140
  366. package/dist/utils/traceAggregation.js.map +0 -1
  367. package/dist/validation/OtelCanvasValidator.d.ts +0 -40
  368. package/dist/validation/OtelCanvasValidator.d.ts.map +0 -1
  369. package/dist/validation/OtelCanvasValidator.js +0 -59
  370. package/dist/validation/OtelCanvasValidator.js.map +0 -1
  371. package/dist/validation/index.d.ts +0 -8
  372. package/dist/validation/index.d.ts.map +0 -1
  373. package/dist/validation/index.js +0 -13
  374. package/dist/validation/index.js.map +0 -1
  375. package/dist/validation/libraryValidation.d.ts +0 -53
  376. package/dist/validation/libraryValidation.d.ts.map +0 -1
  377. package/dist/validation/libraryValidation.js +0 -115
  378. package/dist/validation/libraryValidation.js.map +0 -1
  379. package/dist/workflow/edge-derivation.d.ts +0 -111
  380. package/dist/workflow/edge-derivation.d.ts.map +0 -1
  381. package/dist/workflow/edge-derivation.js +0 -174
  382. package/dist/workflow/edge-derivation.js.map +0 -1
  383. package/dist/workflow/edge-validation.d.ts +0 -170
  384. package/dist/workflow/edge-validation.d.ts.map +0 -1
  385. package/dist/workflow/edge-validation.js +0 -332
  386. package/dist/workflow/edge-validation.js.map +0 -1
  387. package/dist/workflow/example.d.ts +0 -11
  388. package/dist/workflow/example.d.ts.map +0 -1
  389. package/dist/workflow/example.js +0 -310
  390. package/dist/workflow/example.js.map +0 -1
  391. package/dist/workflow/index.d.ts +0 -20
  392. package/dist/workflow/index.d.ts.map +0 -1
  393. package/dist/workflow/index.js +0 -49
  394. package/dist/workflow/index.js.map +0 -1
  395. package/dist/workflow/scenario-matcher.d.ts +0 -63
  396. package/dist/workflow/scenario-matcher.d.ts.map +0 -1
  397. package/dist/workflow/scenario-matcher.js +0 -215
  398. package/dist/workflow/scenario-matcher.js.map +0 -1
  399. package/dist/workflow/template-parser.d.ts +0 -73
  400. package/dist/workflow/template-parser.d.ts.map +0 -1
  401. package/dist/workflow/template-parser.js +0 -280
  402. package/dist/workflow/template-parser.js.map +0 -1
  403. package/dist/workflow/template-renderer.d.ts +0 -45
  404. package/dist/workflow/template-renderer.d.ts.map +0 -1
  405. package/dist/workflow/template-renderer.js +0 -401
  406. package/dist/workflow/template-renderer.js.map +0 -1
  407. package/dist/workflow/types.d.ts +0 -393
  408. package/dist/workflow/types.d.ts.map +0 -1
  409. package/dist/workflow/types.js +0 -40
  410. package/dist/workflow/types.js.map +0 -1
  411. package/dist/workflow/validator.d.ts +0 -363
  412. package/dist/workflow/validator.d.ts.map +0 -1
  413. package/dist/workflow/validator.js +0 -2283
  414. package/dist/workflow/validator.js.map +0 -1
  415. package/schemas/auxiliary.manifest.schema.json +0 -56
  416. package/src/ConfigurationLoader.test.ts +0 -466
  417. package/src/ConfigurationLoader.ts +0 -184
  418. package/src/ConfigurationValidator.test.ts +0 -200
  419. package/src/ConfigurationValidator.ts +0 -273
  420. package/src/EventProcessor.test.ts +0 -405
  421. package/src/EventProcessor.ts +0 -256
  422. package/src/EventRecorderService.test.ts +0 -543
  423. package/src/EventRecorderService.ts +0 -741
  424. package/src/LibraryLoader.ts +0 -194
  425. package/src/PathBasedEventProcessor.test.ts +0 -336
  426. package/src/PathBasedEventProcessor.ts +0 -196
  427. package/src/SessionManager.test.ts +0 -424
  428. package/src/SessionManager.ts +0 -471
  429. package/src/ValidationEngine.test.ts +0 -520
  430. package/src/ValidationEngine.ts +0 -200
  431. package/src/auxiliary/AuxiliaryManifestValidator.test.ts +0 -93
  432. package/src/auxiliary/AuxiliaryManifestValidator.ts +0 -132
  433. package/src/auxiliary/index.ts +0 -18
  434. package/src/auxiliary/validateAreaScopeDisjoint.test.ts +0 -85
  435. package/src/auxiliary/validateAreaScopeDisjoint.ts +0 -72
  436. package/src/cli/codegen.ts +0 -286
  437. package/src/codegen/type-generator.test.ts +0 -219
  438. package/src/codegen/type-generator.ts +0 -560
  439. package/src/codegen/usage-example.ts +0 -357
  440. package/src/dashboard/DashboardValidator.ts +0 -904
  441. package/src/dashboard/index.ts +0 -14
  442. package/src/discovery/CanvasDiscovery.test.ts +0 -691
  443. package/src/discovery/CanvasDiscovery.ts +0 -1114
  444. package/src/discovery/CanvasFileManifest.test.ts +0 -521
  445. package/src/discovery/CanvasFileManifest.ts +0 -419
  446. package/src/discovery/LibraryDiscovery.test.ts +0 -329
  447. package/src/discovery/LibraryDiscovery.ts +0 -386
  448. package/src/discovery/types.ts +0 -453
  449. package/src/events/EventsCanvasValidator.test.ts +0 -191
  450. package/src/events/EventsCanvasValidator.ts +0 -428
  451. package/src/events/NamespacePathIndex.test.ts +0 -97
  452. package/src/events/NamespacePathIndex.ts +0 -100
  453. package/src/events/OtelEventPathsValidator.test.ts +0 -243
  454. package/src/events/OtelEventPathsValidator.ts +0 -191
  455. package/src/events/ScopeEventsValidator.test.ts +0 -173
  456. package/src/events/ScopeEventsValidator.ts +0 -158
  457. package/src/events/index.ts +0 -27
  458. package/src/events/path-helpers.ts +0 -53
  459. package/src/execution/ExecutionLoader.test.ts +0 -378
  460. package/src/execution/ExecutionLoader.ts +0 -331
  461. package/src/execution/ExecutionValidator.test.ts +0 -503
  462. package/src/execution/ExecutionValidator.ts +0 -617
  463. package/src/generated/graph-converter-execution.types.ts +0 -193
  464. package/src/helpers/GraphInstrumentationHelper.test.ts +0 -364
  465. package/src/helpers/GraphInstrumentationHelper.ts +0 -323
  466. package/src/matchers/SpanMatcher.test.ts +0 -1105
  467. package/src/matchers/SpanMatcher.ts +0 -381
  468. package/src/orchestration/TraceOrchestrator.ts +0 -560
  469. package/src/orchestration/__tests__/TraceOrchestrator.test.ts +0 -1098
  470. package/src/parsers/OtlpTraceParser.ts +0 -401
  471. package/src/parsers/__tests__/OtlpTraceParser.test.ts +0 -197
  472. package/src/registry/CompositeRegistry.ts +0 -140
  473. package/src/registry/EventRegistry.test.ts +0 -387
  474. package/src/registry/EventRegistry.ts +0 -191
  475. package/src/registry/LocalRegistry.ts +0 -416
  476. package/src/registry/RemoteRegistry.ts +0 -499
  477. package/src/registry/VersionRegistry.test.ts +0 -223
  478. package/src/registry/VersionRegistry.ts +0 -121
  479. package/src/rules/config.test.ts +0 -277
  480. package/src/rules/config.ts +0 -450
  481. package/src/rules/engine.test.ts +0 -331
  482. package/src/rules/engine.ts +0 -448
  483. package/src/rules/implementations/connection-type-references.ts +0 -117
  484. package/src/rules/implementations/dead-end-states.ts +0 -101
  485. package/src/rules/implementations/index.ts +0 -66
  486. package/src/rules/implementations/minimum-node-sources.ts +0 -84
  487. package/src/rules/implementations/no-unknown-fields.ts +0 -372
  488. package/src/rules/implementations/orphaned-edge-types.ts +0 -55
  489. package/src/rules/implementations/orphaned-node-types.ts +0 -58
  490. package/src/rules/implementations/required-metadata.ts +0 -52
  491. package/src/rules/implementations/state-transition-references.ts +0 -155
  492. package/src/rules/implementations/unreachable-states.ts +0 -94
  493. package/src/rules/implementations/valid-action-patterns.ts +0 -140
  494. package/src/rules/implementations/valid-color-format.ts +0 -152
  495. package/src/rules/implementations/valid-edge-types.ts +0 -273
  496. package/src/rules/implementations/valid-node-types.ts +0 -200
  497. package/src/rules/index.ts +0 -92
  498. package/src/rules/types.ts +0 -426
  499. package/src/scopes/ScopePathIndex.test.ts +0 -94
  500. package/src/scopes/ScopePathIndex.ts +0 -89
  501. package/src/scopes/ScopesCanvasValidator.test.ts +0 -127
  502. package/src/scopes/ScopesCanvasValidator.ts +0 -305
  503. package/src/scopes/__tests__/utils.test.ts +0 -383
  504. package/src/scopes/index.ts +0 -42
  505. package/src/scopes/utils.ts +0 -182
  506. package/src/scopes/validateScopeNamespaceNesting.test.ts +0 -127
  507. package/src/scopes/validateScopeNamespaceNesting.ts +0 -88
  508. package/src/spans/index.ts +0 -24
  509. package/src/spans/utils.ts +0 -249
  510. package/src/storyboard/builder.ts +0 -308
  511. package/src/storyboard/index.ts +0 -26
  512. package/src/storyboard/types.ts +0 -117
  513. package/src/telemetry/coverage.ts +0 -480
  514. package/src/telemetry/event-validator.ts +0 -312
  515. package/src/types/audit.ts +0 -314
  516. package/src/types/auxiliary.ts +0 -63
  517. package/src/types/canvas-scope.ts +0 -190
  518. package/src/types/canvas.test.ts +0 -199
  519. package/src/types/canvas.ts +0 -1395
  520. package/src/types/dashboard.ts +0 -257
  521. package/src/types/library.ts +0 -283
  522. package/src/types/otel.test.ts +0 -219
  523. package/src/types/otel.ts +0 -620
  524. package/src/types/path-based-config.ts +0 -285
  525. package/src/types/registered-trace.ts +0 -432
  526. package/src/types/resource-match.ts +0 -263
  527. package/src/types/version-registry.ts +0 -150
  528. package/src/utils/CanvasConverter.ts +0 -670
  529. package/src/utils/GraphConverter.test.ts +0 -322
  530. package/src/utils/GraphConverter.ts +0 -84
  531. package/src/utils/PathMatcher.test.ts +0 -139
  532. package/src/utils/PathMatcher.ts +0 -185
  533. package/src/utils/TraceToCanvas.test.ts +0 -310
  534. package/src/utils/TraceToCanvas.ts +0 -510
  535. package/src/utils/YamlParser.ts +0 -77
  536. package/src/utils/registeredTraceHelpers.ts +0 -251
  537. package/src/utils/traceAggregation.test.ts +0 -385
  538. package/src/utils/traceAggregation.ts +0 -173
  539. package/src/validation/OtelCanvasValidator.ts +0 -86
  540. package/src/validation/index.ts +0 -17
  541. package/src/validation/libraryValidation.ts +0 -206
  542. package/src/workflow/README.md +0 -408
  543. package/src/workflow/__tests__/edge-derivation.test.ts +0 -449
  544. package/src/workflow/__tests__/edge-validation.test.ts +0 -417
  545. package/src/workflow/__tests__/scenario-matcher.test.ts +0 -225
  546. package/src/workflow/__tests__/template-parser.test.ts +0 -276
  547. package/src/workflow/__tests__/template-renderer.test.ts +0 -509
  548. package/src/workflow/__tests__/types.test.ts +0 -187
  549. package/src/workflow/__tests__/validator.test.ts +0 -3225
  550. package/src/workflow/edge-derivation.ts +0 -265
  551. package/src/workflow/edge-validation.ts +0 -503
  552. package/src/workflow/example.ts +0 -330
  553. package/src/workflow/index.ts +0 -86
  554. package/src/workflow/scenario-matcher.ts +0 -253
  555. package/src/workflow/template-parser.ts +0 -350
  556. package/src/workflow/template-renderer.ts +0 -453
  557. package/src/workflow/types.ts +0 -511
  558. package/src/workflow/validator.ts +0 -2737
@@ -1,2737 +0,0 @@
1
- /**
2
- * Workflow Template Validator
3
- * Validates .workflow.json files against their corresponding .otel.canvas files
4
- */
5
-
6
- import type { FileSystemAdapter } from '@principal-ai/repository-abstraction';
7
- import type { WorkflowTemplate, WorkflowScenario, ScenarioTemplate } from './types';
8
- import { getEventTemplateString } from './types';
9
- import type { ExtendedCanvas, OtelEventNode } from '../types/canvas';
10
- import { isOtelEventNode } from '../types/canvas';
11
- import { resolve, basename } from 'path';
12
- import type { EventRegistry } from '../registry/EventRegistry';
13
- import type { IExportTraceServiceRequest } from '@opentelemetry/otlp-transformer/build/src/trace/internal-types';
14
-
15
- // Type for deprecated scenario format (for migration detection)
16
- interface DeprecatedScenario extends WorkflowScenario {
17
- condition?: unknown;
18
- }
19
-
20
- // ============================================================================
21
- // Validation Types
22
- // ============================================================================
23
-
24
- export interface WorkflowValidationContext {
25
- /** The workflow template being validated */
26
- workflow: WorkflowTemplate;
27
-
28
- /** Path to the workflow file */
29
- workflowPath: string;
30
-
31
- /** The canvas file (if found) */
32
- canvas?: ExtendedCanvas;
33
-
34
- /** Path to the canvas file */
35
- canvasPath?: string;
36
-
37
- /** Base path for resolving relative paths */
38
- basePath: string;
39
-
40
- /** Raw workflow content for line number lookup */
41
- rawContent?: string;
42
-
43
- /** Execution data for validating attribute references (optional) */
44
- executionData?: {
45
- /** Aggregated attributes available in templates */
46
- aggregates: Record<string, unknown>;
47
- /** Attributes grouped by event name */
48
- eventAttributes: Map<string, Record<string, unknown>>;
49
- };
50
-
51
- /**
52
- * Co-located execution files for validating template completeness.
53
- * Array of paths to .otel.json files in the same directory as the workflow.
54
- */
55
- executionFiles?: string[];
56
-
57
- /**
58
- * Optional: Events used across all workflows that reference this canvas.
59
- * When provided, coverage warnings are only emitted for canvas events
60
- * that are NOT in this set (i.e., truly unused across all workflows).
61
- * This enables multi-workflow canvas patterns where different workflows
62
- * cover different subsets of canvas events.
63
- */
64
- allWorkflowEvents?: Set<string>;
65
-
66
- /**
67
- * Optional: Registry of all events across the project.
68
- * When provided, enables enhanced error messages that show where
69
- * missing events are defined (in library or other canvases).
70
- */
71
- eventRegistry?: EventRegistry;
72
-
73
- /**
74
- * Optional: List of owned instrumentation scopes from library.yaml.
75
- * When provided, validates that workflow scope is in this list.
76
- */
77
- ownedScopes?: string[];
78
- }
79
-
80
- export interface WorkflowViolation {
81
- /** Rule ID that detected this violation */
82
- ruleId: string;
83
-
84
- /** Severity level */
85
- severity: 'error' | 'warn';
86
-
87
- /** File path */
88
- file: string;
89
-
90
- /** Line number (1-indexed) */
91
- line?: number;
92
-
93
- /** JSON path to the problematic field */
94
- path?: string;
95
-
96
- /** Clear error message */
97
- message: string;
98
-
99
- /** Explanation of impact */
100
- impact: string;
101
-
102
- /** Suggestion for fixing */
103
- suggestion?: string;
104
-
105
- /** Whether this can be auto-fixed */
106
- fixable: boolean;
107
- }
108
-
109
- export interface WorkflowValidationResult {
110
- /** All violations found */
111
- violations: WorkflowViolation[];
112
-
113
- /** Count of errors */
114
- errorCount: number;
115
-
116
- /** Count of warnings */
117
- warningCount: number;
118
-
119
- /** Count of fixable violations */
120
- fixableCount: number;
121
- }
122
-
123
- // ============================================================================
124
- // Validator Implementation
125
- // ============================================================================
126
-
127
- export class WorkflowValidator {
128
- constructor(private fsAdapter: FileSystemAdapter) {}
129
-
130
- /**
131
- * Validate a workflow template
132
- */
133
- async validate(
134
- context: WorkflowValidationContext
135
- ): Promise<WorkflowValidationResult> {
136
- const violations: WorkflowViolation[] = [];
137
-
138
- // Run all validation rules
139
- violations.push(...(await this.checkSchema(context)));
140
- violations.push(...(await this.checkCanvasExists(context)));
141
- violations.push(...this.checkCanvasNodeLabels(context));
142
- violations.push(...this.checkCanvasCrossReference(context));
143
- violations.push(...this.checkDeprecatedFields(context));
144
- violations.push(...this.checkScenarios(context));
145
- violations.push(...this.checkScenarioSubsets(context));
146
-
147
- // Check event name syntax BEFORE checking event references
148
- // This ensures we catch unsupported syntax before trying to match events
149
- violations.push(...this.checkEventNameSyntax(context));
150
-
151
- // Only run canvas-dependent checks if canvas was loaded
152
- if (context.canvas) {
153
- violations.push(...this.checkEventReferences(context));
154
- violations.push(...this.checkAttributeReferences(context));
155
- violations.push(...this.checkEventConnectivity(context));
156
- violations.push(...this.checkEventAttributeRequirements(context));
157
- violations.push(...this.checkTemplateAttributesDefinedInSchema(context));
158
- violations.push(...this.checkScenarioScopeConsistency(context));
159
- }
160
-
161
- violations.push(...this.checkTemplateSyntax(context));
162
- violations.push(...this.checkConflictingAttributePaths(context));
163
- violations.push(...this.checkFormattingOptions(context));
164
-
165
- // Check execution data completeness if execution files are provided
166
- if (context.executionFiles && context.executionFiles.length > 0) {
167
- violations.push(...(await this.checkExecutionDataCompleteness(context)));
168
- }
169
-
170
- return this.aggregateResults(violations);
171
- }
172
-
173
- /**
174
- * Validate multiple workflows for duplicate spanPatterns
175
- *
176
- * This is a CLI-level validation that checks for conflicts across workflows.
177
- * Call this method after validating individual workflows.
178
- *
179
- * @param workflows Array of workflow templates with their file paths
180
- * @returns Array of violations for duplicate spanPatterns
181
- */
182
- static validateSpanPatterns(
183
- workflows: Array<{ workflow: WorkflowTemplate; workflowPath: string }>
184
- ): WorkflowViolation[] {
185
- const violations: WorkflowViolation[] = [];
186
- const spanPatternMap = new Map<string, string[]>(); // spanPattern -> workflow paths
187
-
188
- // Collect all spanPatterns
189
- for (const { workflow, workflowPath } of workflows) {
190
- if (!workflow.spanPattern) {
191
- continue; // Will be caught by individual validation
192
- }
193
-
194
- if (!spanPatternMap.has(workflow.spanPattern)) {
195
- spanPatternMap.set(workflow.spanPattern, []);
196
- }
197
- spanPatternMap.get(workflow.spanPattern)!.push(workflowPath);
198
- }
199
-
200
- // Check for duplicates
201
- for (const [spanPattern, paths] of spanPatternMap.entries()) {
202
- if (paths.length > 1) {
203
- // Duplicate spanPattern found
204
- for (const path of paths) {
205
- const otherPaths = paths.filter(p => p !== path);
206
- violations.push({
207
- ruleId: 'workflow-span-pattern-duplicate',
208
- severity: 'error',
209
- file: path,
210
- path: 'spanPattern',
211
- message: `Duplicate spanPattern "${spanPattern}" found in multiple workflows`,
212
- impact: 'Multiple workflows cannot match the same span - this creates ambiguous workflow selection',
213
- suggestion: `This spanPattern is also used in:\n${otherPaths.map(p => ` - ${p}`).join('\n')}\n\nEach workflow must have a unique spanPattern. Consider:\n - Using different span names (e.g., "payment.authorize" vs "payment.refund")\n - Merging these workflows into one with multiple scenarios`,
214
- fixable: false,
215
- });
216
- }
217
- }
218
- }
219
-
220
- return violations;
221
- }
222
-
223
- /**
224
- * Check schema validity (required fields, valid values)
225
- */
226
- private async checkSchema(context: WorkflowValidationContext): Promise<WorkflowViolation[]> {
227
- const violations: WorkflowViolation[] = [];
228
- const { workflow, workflowPath } = context;
229
-
230
- // Valid fields at the workflow root level
231
- const validWorkflowFields = [
232
- 'version',
233
- 'canvas',
234
- 'name',
235
- 'description',
236
- 'spanPattern',
237
- 'scope',
238
- 'files',
239
- 'status',
240
- 'scenarioSelection',
241
- 'showLogsPerSpan',
242
- 'scenarios',
243
- 'formatting',
244
- ];
245
-
246
- // Check for unknown fields at workflow level
247
- const workflowRecord = workflow as unknown as Record<string, unknown>;
248
- const workflowKeys = Object.keys(workflowRecord);
249
-
250
- for (const key of workflowKeys) {
251
- if (!validWorkflowFields.includes(key)) {
252
- violations.push({
253
- ruleId: 'workflow-unknown-field',
254
- severity: 'error',
255
- file: workflowPath,
256
- path: key,
257
- message: `Unknown workflow field "${key}"`,
258
- impact: 'This field will be ignored and may indicate a misunderstanding of the schema',
259
- suggestion: `Valid workflow fields are: ${validWorkflowFields.join(', ')}. Remove the "${key}" field or check for typos.`,
260
- fixable: false,
261
- });
262
- }
263
- }
264
-
265
- // Check version
266
- if (!workflow.version) {
267
- violations.push({
268
- ruleId: 'workflow-schema-valid',
269
- severity: 'error',
270
- file: workflowPath,
271
- path: 'version',
272
- message: 'Missing required field "version"',
273
- impact: 'Cannot determine template version for compatibility',
274
- suggestion: 'Add a version field (e.g., "1.0.0")',
275
- fixable: false,
276
- });
277
- } else if (!this.isValidSemver(workflow.version)) {
278
- violations.push({
279
- ruleId: 'workflow-schema-valid',
280
- severity: 'error',
281
- file: workflowPath,
282
- path: 'version',
283
- message: `Invalid version format: "${workflow.version}"`,
284
- impact: 'Version must follow semver format',
285
- suggestion: 'Use semver format like "1.0.0"',
286
- fixable: false,
287
- });
288
- }
289
-
290
- // Check canvas reference
291
- if (!workflow.canvas) {
292
- violations.push({
293
- ruleId: 'workflow-schema-valid',
294
- severity: 'error',
295
- file: workflowPath,
296
- path: 'canvas',
297
- message: 'Missing required field "canvas"',
298
- impact: 'Cannot determine which canvas this workflow belongs to',
299
- suggestion: 'Add a canvas field pointing to an .otel.canvas file',
300
- fixable: false,
301
- });
302
- }
303
-
304
- // Check name
305
- if (!workflow.name) {
306
- violations.push({
307
- ruleId: 'workflow-schema-valid',
308
- severity: 'error',
309
- file: workflowPath,
310
- path: 'name',
311
- message: 'Missing required field "name"',
312
- impact: 'Cannot identify this workflow template',
313
- suggestion: 'Add a human-readable name',
314
- fixable: false,
315
- });
316
- }
317
-
318
- // Check description
319
- if (!workflow.description) {
320
- violations.push({
321
- ruleId: 'workflow-schema-valid',
322
- severity: 'error',
323
- file: workflowPath,
324
- path: 'description',
325
- message: 'Missing required field "description"',
326
- impact: 'Cannot understand the purpose of this workflow',
327
- suggestion: 'Add a description explaining what this workflow shows',
328
- fixable: false,
329
- });
330
- }
331
-
332
- // Check spanPattern
333
- if (!workflow.spanPattern) {
334
- violations.push({
335
- ruleId: 'workflow-schema-valid',
336
- severity: 'error',
337
- file: workflowPath,
338
- path: 'spanPattern',
339
- message: 'Missing required field "spanPattern"',
340
- impact: 'Cannot determine which spans this workflow applies to',
341
- suggestion: 'Add a spanPattern field specifying the exact span name (e.g., "payment.authorize")',
342
- fixable: false,
343
- });
344
- } else if (typeof workflow.spanPattern !== 'string' || workflow.spanPattern.trim() === '') {
345
- violations.push({
346
- ruleId: 'workflow-schema-valid',
347
- severity: 'error',
348
- file: workflowPath,
349
- path: 'spanPattern',
350
- message: 'spanPattern must be a non-empty string',
351
- impact: 'Cannot match spans with invalid pattern',
352
- suggestion: 'Provide a valid span name (e.g., "payment.authorize", "checkout.process")',
353
- fixable: false,
354
- });
355
- } else if (workflow.spanPattern.includes('*')) {
356
- // Reject glob/wildcard patterns - only exact match is supported
357
- violations.push({
358
- ruleId: 'workflow-span-pattern-exact',
359
- severity: 'error',
360
- file: workflowPath,
361
- path: 'spanPattern',
362
- message: `spanPattern contains wildcard "*" which is not supported: "${workflow.spanPattern}"`,
363
- impact: 'Glob patterns create ambiguity when multiple workflows could match the same span',
364
- suggestion: 'Use an exact span name instead (e.g., "payment.authorize" not "payment.*")',
365
- fixable: false,
366
- });
367
- } else if (/[[\]{}^$|\\+?]/.test(workflow.spanPattern)) {
368
- // Reject regex special characters (except . which is common in span names, and () which appear in framework span names like Next.js)
369
- violations.push({
370
- ruleId: 'workflow-span-pattern-exact',
371
- severity: 'error',
372
- file: workflowPath,
373
- path: 'spanPattern',
374
- message: `spanPattern contains regex special characters which are not supported: "${workflow.spanPattern}"`,
375
- impact: 'Regex patterns are not supported - only exact span name matching is used',
376
- suggestion: 'Use an exact span name that matches your instrumented span (e.g., "GET /api/auth/me/route")',
377
- fixable: false,
378
- });
379
- }
380
-
381
- // Check scope against owned-scopes
382
- if (workflow.scope && context.ownedScopes && context.ownedScopes.length > 0) {
383
- if (!context.ownedScopes.includes(workflow.scope)) {
384
- violations.push({
385
- ruleId: 'workflow-scope-owned',
386
- severity: 'error',
387
- file: workflowPath,
388
- path: 'scope',
389
- message: `Scope "${workflow.scope}" is not in owned-scopes list`,
390
- impact: 'Spans from this scope will not be matched - the scope must be declared in library.yaml owned-scopes',
391
- suggestion: `Either:\n - Add "${workflow.scope}" to owned-scopes in library.yaml\n - Change scope to one of: ${context.ownedScopes.join(', ')}\n - Remove scope if using a different tracer`,
392
- fixable: false,
393
- });
394
- }
395
- }
396
-
397
- // Check status value
398
- const validStatuses = ['draft', 'approved', 'implemented'];
399
- if (workflow.status && !validStatuses.includes(workflow.status)) {
400
- violations.push({
401
- ruleId: 'workflow-schema-valid',
402
- severity: 'error',
403
- file: workflowPath,
404
- path: 'status',
405
- message: `Invalid status: "${workflow.status}"`,
406
- impact: 'Status must be a valid lifecycle value',
407
- suggestion: `Use one of: ${validStatuses.join(', ')}`,
408
- fixable: false,
409
- });
410
- }
411
-
412
- // Check files requirement based on status
413
- const status = workflow.status || 'draft';
414
- if ((status === 'approved' || status === 'implemented') && (!workflow.files || workflow.files.length === 0)) {
415
- violations.push({
416
- ruleId: 'workflow-files-required',
417
- severity: 'error',
418
- file: workflowPath,
419
- path: 'files',
420
- message: `Status "${status}" requires files to be specified`,
421
- impact: 'Cannot validate implementation without knowing which files contain the span instrumentation',
422
- suggestion: 'Add a "files" array with the paths where this span is created (e.g., ["src/app/api/auth/me/route.ts"])',
423
- fixable: false,
424
- });
425
- }
426
-
427
- // Check files exist when status is implemented
428
- if (status === 'implemented' && workflow.files && workflow.files.length > 0) {
429
- for (const file of workflow.files) {
430
- const filePath = resolve(context.basePath, file);
431
- if (!(await this.fsAdapter.exists(filePath))) {
432
- violations.push({
433
- ruleId: 'workflow-files-exist',
434
- severity: 'error',
435
- file: workflowPath,
436
- path: 'files',
437
- message: `Implementation file not found: ${file}`,
438
- impact: 'Workflow is marked as implemented but the source file does not exist',
439
- suggestion: `Either:\n - Create the file at ${file}\n - Update the files array with the correct path\n - Change status to "approved" or "draft"`,
440
- fixable: false,
441
- });
442
- }
443
- }
444
- }
445
-
446
- // Check scope requirement based on status
447
- if ((status === 'approved' || status === 'implemented') && !workflow.scope) {
448
- violations.push({
449
- ruleId: 'workflow-scope-required',
450
- severity: 'error',
451
- file: workflowPath,
452
- path: 'scope',
453
- message: `Status "${status}" requires scope to be specified`,
454
- impact: 'Cannot validate instrumentation scope without knowing which tracer emits this span',
455
- suggestion: 'Add a "scope" field with the instrumentation scope name (e.g., "terminal-activity", "auth")',
456
- fixable: false,
457
- });
458
- }
459
-
460
- // Check scenarioSelection
461
- const validSelections = ['first-match', 'manual'];
462
- if (workflow.scenarioSelection && !validSelections.includes(workflow.scenarioSelection)) {
463
- violations.push({
464
- ruleId: 'workflow-schema-valid',
465
- severity: 'error',
466
- file: workflowPath,
467
- path: 'scenarioSelection',
468
- message: `Invalid scenarioSelection: "${workflow.scenarioSelection}"`,
469
- impact: 'Scenario selection must be a valid type',
470
- suggestion: `Use one of: ${validSelections.join(', ')}`,
471
- fixable: false,
472
- });
473
- }
474
-
475
- // Check scenarios array
476
- if (!workflow.scenarios || !Array.isArray(workflow.scenarios)) {
477
- violations.push({
478
- ruleId: 'workflow-schema-valid',
479
- severity: 'error',
480
- file: workflowPath,
481
- path: 'scenarios',
482
- message: 'Missing or invalid "scenarios" field',
483
- impact: 'Cannot generate workflows without scenarios',
484
- suggestion: 'Add a scenarios array with at least one scenario',
485
- fixable: false,
486
- });
487
- } else if (workflow.scenarios.length === 0) {
488
- violations.push({
489
- ruleId: 'workflow-schema-valid',
490
- severity: 'error',
491
- file: workflowPath,
492
- path: 'scenarios',
493
- message: 'Scenarios array is empty',
494
- impact: 'Cannot generate workflows without scenarios',
495
- suggestion: 'Add at least one scenario definition',
496
- fixable: false,
497
- });
498
- }
499
-
500
- return violations;
501
- }
502
-
503
- /**
504
- * Check that the referenced canvas file exists
505
- */
506
- private async checkCanvasExists(context: WorkflowValidationContext): Promise<WorkflowViolation[]> {
507
- const violations: WorkflowViolation[] = [];
508
- const { workflow, workflowPath, basePath, canvasPath } = context;
509
-
510
- if (!workflow.canvas) {
511
- // Already flagged by checkSchema
512
- return violations;
513
- }
514
-
515
- // Resolve canvas path
516
- const resolvedPath = canvasPath || resolve(basePath, workflow.canvas);
517
-
518
- if (!(await this.fsAdapter.exists(resolvedPath))) {
519
- violations.push({
520
- ruleId: 'workflow-canvas-exists',
521
- severity: 'error',
522
- file: workflowPath,
523
- path: 'canvas',
524
- message: `Referenced canvas file does not exist: ${workflow.canvas}`,
525
- impact: 'Cannot validate event references without the canvas',
526
- suggestion: `Canvas paths must be relative to the repository root, not the workflow file. Use a path like ".principal-views/your-storyboard/canvas.otel.canvas" instead of relative paths like "../canvas.otel.canvas"`,
527
- fixable: false,
528
- });
529
- }
530
-
531
- return violations;
532
- }
533
-
534
- /**
535
- * Check OTEL node labels are meaningful (not equal to their IDs).
536
- * Labels should be human-readable display names, not technical identifiers.
537
- */
538
- private checkCanvasNodeLabels(context: WorkflowValidationContext): WorkflowViolation[] {
539
- const violations: WorkflowViolation[] = [];
540
- const { canvas, canvasPath } = context;
541
-
542
- if (!canvas?.nodes || !canvasPath) {
543
- return violations;
544
- }
545
-
546
- // OTEL node types that require meaningful labels
547
- const otelNodeTypes = ['otel-event', 'otel-span-convention', 'otel-scope', 'otel-resource', 'otel-boundary'];
548
-
549
- for (const node of canvas.nodes) {
550
- // Check if it's an OTEL node type
551
- if (!otelNodeTypes.includes(node.type)) {
552
- continue;
553
- }
554
-
555
- // Get label from OTEL node (top-level field)
556
- const label = 'label' in node ? (node as { label?: string }).label : undefined;
557
-
558
- // Skip if no label defined
559
- if (!label) {
560
- continue;
561
- }
562
-
563
- // Check if label equals ID
564
- if (label === node.id) {
565
- violations.push({
566
- ruleId: 'canvas-node-label-meaningful',
567
- severity: 'warn',
568
- file: canvasPath,
569
- path: `nodes[${node.id}].label`,
570
- message: `Node "${node.id}" has label identical to its ID`,
571
- impact: 'Labels should be human-readable display names, not technical identifiers',
572
- suggestion: `Change the label to a human-readable name like "${this.idToHumanReadable(node.id)}"`,
573
- fixable: true,
574
- });
575
- }
576
- }
577
-
578
- return violations;
579
- }
580
-
581
- /**
582
- * Convert a dot-separated ID to a human-readable label.
583
- * E.g., "multi-canvas-panel.render" → "Multi Canvas Panel Render"
584
- */
585
- private idToHumanReadable(id: string): string {
586
- return id
587
- .split(/[-.]/)
588
- .map(word => word.charAt(0).toUpperCase() + word.slice(1))
589
- .join(' ');
590
- }
591
-
592
- /**
593
- * Check if workflow references a canvas from a different storyboard folder.
594
- * Workflows should be co-located with their canvas in the same storyboard.
595
- */
596
- private checkCanvasCrossReference(context: WorkflowValidationContext): WorkflowViolation[] {
597
- const violations: WorkflowViolation[] = [];
598
- const { workflow, workflowPath } = context;
599
-
600
- if (!workflow.canvas) {
601
- return violations;
602
- }
603
-
604
- // Extract storyboard folder from workflow path
605
- // e.g., ".principal-views/task-management/task-workflow/task-complete.workflow.json"
606
- // -> storyboard is "task-management"
607
- const workflowStoryboard = this.extractStoryboardName(workflowPath);
608
-
609
- // Extract storyboard folder from canvas path
610
- // e.g., ".principal-views/cleanup-operations/cleanup-operations.otel.canvas"
611
- // -> storyboard is "cleanup-operations"
612
- const canvasStoryboard = this.extractStoryboardName(workflow.canvas);
613
-
614
- if (workflowStoryboard && canvasStoryboard && workflowStoryboard !== canvasStoryboard) {
615
- violations.push({
616
- ruleId: 'workflow-canvas-cross-reference',
617
- severity: 'error',
618
- file: workflowPath,
619
- path: 'canvas',
620
- message: `Workflow in "${workflowStoryboard}" references canvas from different storyboard "${canvasStoryboard}"`,
621
- impact: 'Cross-referencing canvases across storyboards makes it difficult to understand which workflows cover a canvas and fragments the storyboard organization',
622
- suggestion: `Create a canvas in the "${workflowStoryboard}" storyboard with the events this workflow needs. ` +
623
- `Duplicating events across canvases is acceptable - each storyboard should be self-contained. ` +
624
- `Move or copy the relevant event nodes to ".principal-views/${workflowStoryboard}/${workflowStoryboard}.otel.canvas"`,
625
- fixable: false,
626
- });
627
- }
628
-
629
- return violations;
630
- }
631
-
632
- /**
633
- * Extract storyboard name from a path within .principal-views
634
- * e.g., ".principal-views/task-management/task-workflow/file.json" -> "task-management"
635
- * e.g., ".principal-views/cleanup-operations/cleanup-operations.otel.canvas" -> "cleanup-operations"
636
- */
637
- private extractStoryboardName(filePath: string): string | null {
638
- const parts = filePath.split('/');
639
- const pvIndex = parts.indexOf('.principal-views');
640
-
641
- if (pvIndex === -1 || parts.length < pvIndex + 2) {
642
- return null;
643
- }
644
-
645
- // The storyboard name is the folder immediately after .principal-views
646
- return parts[pvIndex + 1];
647
- }
648
-
649
- /**
650
- * Check for deprecated fields (condition, condition.requires, etc.)
651
- */
652
- private checkDeprecatedFields(context: WorkflowValidationContext): WorkflowViolation[] {
653
- const violations: WorkflowViolation[] = [];
654
- const { workflow, workflowPath } = context;
655
-
656
- if (!workflow.scenarios || workflow.scenarios.length === 0) {
657
- return violations;
658
- }
659
-
660
- workflow.scenarios.forEach((scenario, idx) => {
661
- // Check if scenario has a 'condition' field
662
- const deprecatedScenario = scenario as DeprecatedScenario;
663
- if (deprecatedScenario.condition) {
664
- const condition = deprecatedScenario.condition as Record<string, unknown>;
665
-
666
- // Build list of what was in condition
667
- const conditionFields: string[] = [];
668
- if (condition.requires) conditionFields.push('requires');
669
- if (condition.excludes) conditionFields.push('excludes');
670
- if (condition.assertions) conditionFields.push('assertions');
671
- if (condition.default) conditionFields.push('default');
672
- if (condition.any) conditionFields.push('any');
673
-
674
- const fieldsDescription = conditionFields.length > 0
675
- ? ` (${conditionFields.join(', ')})`
676
- : '';
677
-
678
- violations.push({
679
- ruleId: 'workflow-deprecated-condition',
680
- severity: 'error',
681
- file: workflowPath,
682
- path: `scenarios[${idx}].condition`,
683
- message: `The "condition" field is no longer supported${fieldsDescription}`,
684
- impact: 'Required events are now automatically derived from template.events keys',
685
- suggestion: `Remove the "condition" field. Required events are inferred from template.events.\n\n` +
686
- `Migration:\n` +
687
- ` Before:\n` +
688
- ` {\n` +
689
- ` "condition": { "requires": ["event.a", "event.b"] },\n` +
690
- ` "template": { "events": { "event.a": "...", "event.b": "..." }}\n` +
691
- ` }\n\n` +
692
- ` After:\n` +
693
- ` {\n` +
694
- ` "template": { "events": { "event.a": "...", "event.b": "..." }}\n` +
695
- ` }\n\n` +
696
- `Simply remove the "condition" field - the same events should already be defined in "template.events".`,
697
- fixable: true,
698
- });
699
- }
700
- });
701
-
702
- return violations;
703
- }
704
-
705
- /**
706
- * Check that events referenced in templates exist in the canvas
707
- *
708
- * Note: This is currently a placeholder as canvas files don't yet define event schemas.
709
- * In the future, when canvas files include OTEL event schema definitions,
710
- * this will validate event references.
711
- */
712
- private checkEventReferences(context: WorkflowValidationContext): WorkflowViolation[] {
713
- const violations: WorkflowViolation[] = [];
714
- const { workflow, workflowPath, canvas } = context;
715
-
716
- if (!canvas || !canvas.nodes) {
717
- return violations;
718
- }
719
-
720
- // Extract all event names from canvas nodes (otel-event nodes only)
721
- const canvasEvents = new Set<string>();
722
- for (const node of canvas.nodes) {
723
- // Only process otel-event nodes
724
- if (isOtelEventNode(node)) {
725
- const otelEventNode = node as OtelEventNode;
726
- if (otelEventNode.event?.name) {
727
- canvasEvents.add(otelEventNode.event.name);
728
- } else if (otelEventNode.eventRef) {
729
- canvasEvents.add(otelEventNode.eventRef);
730
- }
731
- }
732
- // Legacy pv.event/pv.eventRef format is no longer supported - use otel-event nodes
733
- }
734
-
735
- // Extract all event names from workflow scenarios (from template.events)
736
- const workflowEvents = new Set<string>();
737
- for (const scenario of workflow.scenarios) {
738
- // From template.events
739
- if (scenario.template?.events) {
740
- for (const eventName of Object.keys(scenario.template.events)) {
741
- // Skip wildcard patterns
742
- if (!eventName.includes('*')) {
743
- workflowEvents.add(eventName);
744
- }
745
- }
746
- }
747
- }
748
-
749
- // Check for workflow events not in canvas
750
- for (const eventName of Array.from(workflowEvents)) {
751
- if (!canvasEvents.has(eventName)) {
752
- // Try to find this event elsewhere using the registry
753
- const eventSources = context.eventRegistry?.findEvent(eventName) ?? [];
754
-
755
- let message = `Workflow references event "${eventName}" which is not defined in canvas`;
756
- let suggestion = `Add event "${eventName}" to a node in ${workflow.canvas} or remove it from the workflow`;
757
-
758
- if (eventSources.length > 0) {
759
- // Event found elsewhere - provide helpful guidance
760
- const librarySources = eventSources.filter(s => s.type === 'library');
761
- const canvasSources = eventSources.filter(s => s.type === 'canvas');
762
-
763
- if (librarySources.length > 0) {
764
- // Event is in library - suggest using eventRef
765
- message = `Event "${eventName}" not found in canvas but is available in library`;
766
- suggestion = `Add a node with eventRef: "${eventName}" to ${workflow.canvas}`;
767
- } else if (canvasSources.length > 0) {
768
- // Event is in another canvas - suggest adding to library
769
- const canvasNames = canvasSources.map(s => basename(s.path)).join(', ');
770
- message = `Event "${eventName}" not found in canvas. Found in: ${canvasNames}`;
771
- suggestion = `Add "${eventName}" to library.yaml eventSchemas and use eventRef in ${workflow.canvas}`;
772
- }
773
- }
774
-
775
- violations.push({
776
- ruleId: 'workflow-event-sync',
777
- severity: 'error',
778
- file: workflowPath,
779
- path: 'events',
780
- message,
781
- impact: 'This event will never highlight a canvas node and may never match',
782
- suggestion,
783
- fixable: false,
784
- });
785
- }
786
- }
787
-
788
- // Check for canvas events not in workflow (warning only)
789
- // If allWorkflowEvents is provided, check against the combined set of all workflows
790
- // for this canvas. Otherwise, check against just this workflow's events.
791
- const eventsToCheckAgainst = context.allWorkflowEvents ?? workflowEvents;
792
-
793
- for (const eventName of Array.from(canvasEvents)) {
794
- if (!eventsToCheckAgainst.has(eventName)) {
795
- violations.push({
796
- ruleId: 'workflow-event-coverage',
797
- severity: 'error',
798
- file: workflowPath,
799
- path: 'events',
800
- message: context.allWorkflowEvents
801
- ? `Canvas defines event "${eventName}" which is not used in any workflow for this canvas`
802
- : `Canvas defines event "${eventName}" which is not used in this workflow scenario`,
803
- impact: 'This canvas node may never be highlighted during workflow playback',
804
- suggestion: `Add event "${eventName}" to a scenario's template.events`,
805
- fixable: false,
806
- });
807
- }
808
- }
809
-
810
- return violations;
811
- }
812
-
813
- /**
814
- * Check that scenarios are well-formed
815
- */
816
- private checkScenarios(context: WorkflowValidationContext): WorkflowViolation[] {
817
- const violations: WorkflowViolation[] = [];
818
- const { workflow, workflowPath } = context;
819
-
820
- if (!workflow.scenarios || workflow.scenarios.length === 0) {
821
- return violations; // Already flagged by checkSchema
822
- }
823
-
824
- const scenarioIds = new Set<string>();
825
- const priorities = new Set<number>();
826
-
827
- // Valid fields for scenario objects
828
- const validScenarioFields = ['id', 'priority', 'description', 'template', 'outcomeType', 'filterDefault'];
829
- const validOutcomeTypes = ['expected', 'expected-issue', 'unknown-issue'];
830
-
831
- workflow.scenarios.forEach((scenario, idx) => {
832
- // Check for unknown fields at scenario level
833
- const scenarioRecord = scenario as unknown as Record<string, unknown>;
834
- const scenarioKeys = Object.keys(scenarioRecord);
835
-
836
- for (const key of scenarioKeys) {
837
- if (!validScenarioFields.includes(key)) {
838
- violations.push({
839
- ruleId: 'workflow-scenario-unknown-field',
840
- severity: 'error',
841
- file: workflowPath,
842
- path: `scenarios[${idx}].${key}`,
843
- message: `Unknown scenario field "${key}"`,
844
- impact: 'This field will be ignored and may indicate a misunderstanding of the schema',
845
- suggestion: `Valid scenario fields are: ${validScenarioFields.join(', ')}. ` +
846
- (key === 'match' || key === 'excludeEvents'
847
- ? 'Scenario matching is based solely on template.events keys - there is no separate match configuration.'
848
- : key === 'condition' || key === 'requires'
849
- ? 'Required events are automatically derived from template.events keys.'
850
- : `Remove the "${key}" field or check for typos.`),
851
- fixable: false,
852
- });
853
- }
854
- }
855
- // Check for required fields
856
- if (!scenario.id) {
857
- violations.push({
858
- ruleId: 'workflow-scenario-valid',
859
- severity: 'error',
860
- file: workflowPath,
861
- path: `scenarios[${idx}].id`,
862
- message: 'Scenario is missing required "id" field',
863
- impact: 'Cannot identify this scenario',
864
- suggestion: 'Add a unique ID for this scenario',
865
- fixable: false,
866
- });
867
- } else {
868
- // Check for duplicate IDs
869
- if (scenarioIds.has(scenario.id)) {
870
- violations.push({
871
- ruleId: 'workflow-scenario-valid',
872
- severity: 'error',
873
- file: workflowPath,
874
- path: `scenarios[${idx}].id`,
875
- message: `Duplicate scenario ID: "${scenario.id}"`,
876
- impact: 'Scenario IDs must be unique',
877
- suggestion: 'Use a unique identifier for each scenario',
878
- fixable: false,
879
- });
880
- }
881
- scenarioIds.add(scenario.id);
882
- }
883
-
884
- // Check priority
885
- if (scenario.priority === undefined || scenario.priority === null) {
886
- violations.push({
887
- ruleId: 'workflow-scenario-valid',
888
- severity: 'error',
889
- file: workflowPath,
890
- path: `scenarios[${idx}].priority`,
891
- message: 'Scenario is missing required "priority" field',
892
- impact: 'Cannot determine scenario selection order',
893
- suggestion: 'Add a priority (lower number = higher priority)',
894
- fixable: false,
895
- });
896
- } else {
897
- if (scenario.priority < 0) {
898
- violations.push({
899
- ruleId: 'workflow-scenario-valid',
900
- severity: 'error',
901
- file: workflowPath,
902
- path: `scenarios[${idx}].priority`,
903
- message: 'Priority must be a non-negative number',
904
- impact: 'Invalid priority value',
905
- suggestion: 'Use a positive integer (1 = highest priority)',
906
- fixable: false,
907
- });
908
- }
909
-
910
- // Check for duplicate priorities
911
- if (priorities.has(scenario.priority)) {
912
- violations.push({
913
- ruleId: 'workflow-scenario-valid',
914
- severity: 'error',
915
- file: workflowPath,
916
- path: `scenarios[${idx}].priority`,
917
- message: `Duplicate priority: ${scenario.priority}`,
918
- impact: 'Priorities must be unique to determine selection order',
919
- suggestion: 'Assign unique priority values to each scenario',
920
- fixable: false,
921
- });
922
- }
923
- priorities.add(scenario.priority);
924
- }
925
-
926
- // Check description
927
- if (!scenario.description) {
928
- violations.push({
929
- ruleId: 'workflow-scenario-valid',
930
- severity: 'error',
931
- file: workflowPath,
932
- path: `scenarios[${idx}].description`,
933
- message: 'Scenario is missing required "description" field',
934
- impact: 'Cannot understand what this scenario represents',
935
- suggestion: 'Add a description explaining what this scenario represents',
936
- fixable: false,
937
- });
938
- }
939
-
940
- // Check outcomeType if present
941
- if (scenario.outcomeType !== undefined && !validOutcomeTypes.includes(scenario.outcomeType)) {
942
- violations.push({
943
- ruleId: 'workflow-scenario-valid',
944
- severity: 'error',
945
- file: workflowPath,
946
- path: `scenarios[${idx}].outcomeType`,
947
- message: `Invalid outcomeType: "${scenario.outcomeType}"`,
948
- impact: 'The scenario outcome type will not be recognized',
949
- suggestion: `Valid outcomeType values are: ${validOutcomeTypes.join(', ')}`,
950
- fixable: false,
951
- });
952
- }
953
-
954
- // Check filterDefault if present (should be boolean)
955
- if (scenario.filterDefault !== undefined && typeof scenario.filterDefault !== 'boolean') {
956
- violations.push({
957
- ruleId: 'workflow-scenario-valid',
958
- severity: 'error',
959
- file: workflowPath,
960
- path: `scenarios[${idx}].filterDefault`,
961
- message: `filterDefault must be a boolean, got: ${typeof scenario.filterDefault}`,
962
- impact: 'The filter default state will not be applied correctly',
963
- suggestion: 'Set filterDefault to true or false',
964
- fixable: false,
965
- });
966
- }
967
-
968
- // Check template
969
- if (!scenario.template) {
970
- violations.push({
971
- ruleId: 'workflow-scenario-valid',
972
- severity: 'error',
973
- file: workflowPath,
974
- path: `scenarios[${idx}].template`,
975
- message: 'Scenario is missing required "template" field',
976
- impact: 'Cannot render workflow without a template',
977
- suggestion: 'Add a template with introduction, events, or flow',
978
- fixable: false,
979
- });
980
- } else {
981
- // Validate template structure
982
- violations.push(...this.checkTemplateStructure(scenario.template, workflowPath, idx));
983
- }
984
- });
985
-
986
- return violations;
987
- }
988
-
989
- /**
990
- * Check for subset relationships between scenarios
991
- *
992
- * Ensures no scenario's event set is a strict subset of another scenario's event set.
993
- * This prevents ambiguous matching where a trace could match multiple scenarios.
994
- */
995
- private checkScenarioSubsets(context: WorkflowValidationContext): WorkflowViolation[] {
996
- const violations: WorkflowViolation[] = [];
997
- const { workflow, workflowPath } = context;
998
-
999
- if (!workflow.scenarios || workflow.scenarios.length < 2) {
1000
- return violations; // Need at least 2 scenarios to check subsets
1001
- }
1002
-
1003
- const scenarios = workflow.scenarios;
1004
-
1005
- for (let i = 0; i < scenarios.length; i++) {
1006
- for (let j = i + 1; j < scenarios.length; j++) {
1007
- const eventsA = new Set(Object.keys(scenarios[i].template?.events || {}));
1008
- const eventsB = new Set(Object.keys(scenarios[j].template?.events || {}));
1009
-
1010
- // Check if A is a strict subset of B
1011
- if (this.isStrictSubset(eventsA, eventsB)) {
1012
- violations.push({
1013
- ruleId: 'workflow-scenario-subset',
1014
- severity: 'error',
1015
- file: workflowPath,
1016
- path: `scenarios[${i}]`,
1017
- message: `Scenario "${scenarios[i].id}" is a strict subset of "${scenarios[j].id}"`,
1018
- impact: 'A trace with all events from both scenarios will match both, causing ambiguous scenario selection',
1019
- suggestion: this.generateSubsetFixSuggestion(scenarios[i], scenarios[j]),
1020
- fixable: false,
1021
- });
1022
- }
1023
-
1024
- // Check if B is a strict subset of A
1025
- if (this.isStrictSubset(eventsB, eventsA)) {
1026
- violations.push({
1027
- ruleId: 'workflow-scenario-subset',
1028
- severity: 'error',
1029
- file: workflowPath,
1030
- path: `scenarios[${j}]`,
1031
- message: `Scenario "${scenarios[j].id}" is a strict subset of "${scenarios[i].id}"`,
1032
- impact: 'A trace with all events from both scenarios will match both, causing ambiguous scenario selection',
1033
- suggestion: this.generateSubsetFixSuggestion(scenarios[j], scenarios[i]),
1034
- fixable: false,
1035
- });
1036
- }
1037
-
1038
- // Check if A and B have identical event sets
1039
- if (this.areIdenticalSets(eventsA, eventsB)) {
1040
- violations.push({
1041
- ruleId: 'workflow-scenario-identical',
1042
- severity: 'error',
1043
- file: workflowPath,
1044
- path: `scenarios[${i}], scenarios[${j}]`,
1045
- message: `Scenarios "${scenarios[i].id}" and "${scenarios[j].id}" have identical event sets`,
1046
- impact: 'Both scenarios will match the same traces, making scenario selection arbitrary based on priority alone',
1047
- suggestion: this.generateIdenticalFixSuggestion(scenarios[i], scenarios[j]),
1048
- fixable: false,
1049
- });
1050
- }
1051
- }
1052
- }
1053
-
1054
- return violations;
1055
- }
1056
-
1057
- /**
1058
- * Check if set A is a strict subset of set B
1059
- *
1060
- * A is a strict subset of B if:
1061
- * 1. All elements of A are in B
1062
- * 2. A has fewer elements than B
1063
- */
1064
- private isStrictSubset(A: Set<string>, B: Set<string>): boolean {
1065
- // A must have fewer elements than B
1066
- if (A.size >= B.size) {
1067
- return false;
1068
- }
1069
-
1070
- // All elements of A must be in B
1071
- for (const item of A) {
1072
- if (!B.has(item)) {
1073
- return false;
1074
- }
1075
- }
1076
-
1077
- return true;
1078
- }
1079
-
1080
- /**
1081
- * Check if two sets have identical elements
1082
- *
1083
- * Two sets are identical if they have the same size and all elements match.
1084
- */
1085
- private areIdenticalSets(A: Set<string>, B: Set<string>): boolean {
1086
- if (A.size !== B.size) {
1087
- return false;
1088
- }
1089
-
1090
- for (const item of A) {
1091
- if (!B.has(item)) {
1092
- return false;
1093
- }
1094
- }
1095
-
1096
- return true;
1097
- }
1098
-
1099
- /**
1100
- * Generate helpful suggestion for fixing identical event sets
1101
- */
1102
- private generateIdenticalFixSuggestion(
1103
- scenarioA: WorkflowScenario,
1104
- scenarioB: WorkflowScenario
1105
- ): string {
1106
- const events = Object.keys(scenarioA.template?.events || {});
1107
-
1108
- return (
1109
- `Both scenarios have events: [${events.join(', ')}]\n\n` +
1110
- `Recommended fixes:\n\n` +
1111
- `1. Merge into a single scenario with template conditionals for any differences:\n` +
1112
- ` Scenarios with identical events represent the same execution path.\n` +
1113
- ` Use template conditionals or attribute-based logic to handle variations.\n\n` +
1114
- `2. If they represent genuinely different paths, add distinguishing events:\n` +
1115
- ` - Add an event unique to "${scenarioA.id}" (e.g., "${scenarioA.id}.marker")\n` +
1116
- ` - Add an event unique to "${scenarioB.id}" (e.g., "${scenarioB.id}.marker")\n\n` +
1117
- `3. If the difference is attribute-based, use template variables:\n` +
1118
- ` Instead of separate scenarios, use {{attribute}} in templates to show variations.`
1119
- );
1120
- }
1121
-
1122
- /**
1123
- * Generate helpful suggestion for fixing subset relationship
1124
- */
1125
- private generateSubsetFixSuggestion(
1126
- subsetScenario: WorkflowScenario,
1127
- supersetScenario: WorkflowScenario
1128
- ): string {
1129
- const subsetEvents = Object.keys(subsetScenario.template?.events || {});
1130
- const supersetEvents = Object.keys(supersetScenario.template?.events || {});
1131
- const extraEvents = supersetEvents.filter(e => !subsetEvents.includes(e));
1132
-
1133
- return (
1134
- `Scenario "${subsetScenario.id}" events: [${subsetEvents.join(', ')}]\n` +
1135
- `Scenario "${supersetScenario.id}" events: [${supersetEvents.join(', ')}]\n\n` +
1136
- `Recommended fixes:\n\n` +
1137
- `1. Merge into one scenario with template conditionals:\n` +
1138
- ` {\n` +
1139
- ` "id": "${subsetScenario.id}",\n` +
1140
- ` "template": {\n` +
1141
- ` "events": { ${subsetEvents.map(e => `"${e}": "..."`).join(', ')} },\n` +
1142
- ` "flow": [\n` +
1143
- ` "Base flow steps...",\n` +
1144
- ` {{#if ${extraEvents[0]}}}Additional step...{{/if}}\n` +
1145
- ` ]\n` +
1146
- ` }\n` +
1147
- ` }\n\n` +
1148
- `2. Make them mutually exclusive by adding distinguishing events:\n` +
1149
- ` - Add an event to "${subsetScenario.id}" that distinguishes it (e.g., "conversion.timeout", "conversion.abandoned")\n` +
1150
- ` - Or ensure "${supersetScenario.id}" has events that never co-occur with "${subsetScenario.id}"`
1151
- );
1152
- }
1153
-
1154
- /**
1155
- * Check that all events in a scenario come from nodes with the same scope
1156
- *
1157
- * This ensures scenarios don't accidentally span multiple instrumentation boundaries.
1158
- * Cross-scope workflows should be explicitly designed and documented.
1159
- */
1160
- private checkScenarioScopeConsistency(context: WorkflowValidationContext): WorkflowViolation[] {
1161
- const violations: WorkflowViolation[] = [];
1162
- const { workflow, workflowPath, canvas } = context;
1163
-
1164
- if (!canvas || !workflow.scenarios) {
1165
- return violations;
1166
- }
1167
-
1168
- // Build a map of event name -> scope from canvas nodes (otel-event nodes only)
1169
- const eventToScope = new Map<string, string | undefined>();
1170
- const eventToNodeId = new Map<string, string>();
1171
-
1172
- for (const node of canvas.nodes || []) {
1173
- // Only process otel-event nodes
1174
- if (isOtelEventNode(node)) {
1175
- const otelEventNode = node as OtelEventNode;
1176
- const eventName = otelEventNode.event?.name || otelEventNode.eventRef;
1177
- const scope = otelEventNode.otel?.scope;
1178
-
1179
- if (eventName) {
1180
- eventToScope.set(eventName, scope);
1181
- eventToNodeId.set(eventName, node.id);
1182
- }
1183
- }
1184
- // Legacy pv.event/pv.eventRef format is no longer supported
1185
- }
1186
-
1187
- // Check each scenario for scope consistency
1188
- for (let idx = 0; idx < workflow.scenarios.length; idx++) {
1189
- const scenario = workflow.scenarios[idx];
1190
- const templateEvents = Object.keys(scenario.template?.events || {});
1191
-
1192
- if (templateEvents.length === 0) {
1193
- continue;
1194
- }
1195
-
1196
- // Collect scopes for all events in this scenario
1197
- const scopesInScenario = new Map<string, string[]>(); // scope -> event names
1198
-
1199
- for (const eventName of templateEvents) {
1200
- // Skip wildcard events
1201
- if (eventName === '*') continue;
1202
-
1203
- const scope = eventToScope.get(eventName);
1204
- const scopeKey = scope || '__undefined__';
1205
-
1206
- if (!scopesInScenario.has(scopeKey)) {
1207
- scopesInScenario.set(scopeKey, []);
1208
- }
1209
- scopesInScenario.get(scopeKey)!.push(eventName);
1210
- }
1211
-
1212
- // Check if there are multiple scopes
1213
- const definedScopes = Array.from(scopesInScenario.keys()).filter(s => s !== '__undefined__');
1214
- const undefinedEvents = scopesInScenario.get('__undefined__') || [];
1215
-
1216
- if (definedScopes.length > 1) {
1217
- // Multiple scopes detected - this is a cross-scope scenario
1218
- const scopeBreakdown = definedScopes
1219
- .map(scope => ` - ${scope}: ${scopesInScenario.get(scope)!.join(', ')}`)
1220
- .join('\n');
1221
-
1222
- violations.push({
1223
- ruleId: 'workflow-scenario-cross-scope',
1224
- severity: 'warn',
1225
- file: workflowPath,
1226
- path: `scenarios[${idx}]`,
1227
- message: `Scenario "${scenario.id}" spans multiple instrumentation scopes`,
1228
- impact: 'This scenario crosses instrumentation boundaries, which may indicate:\n' +
1229
- ' - A workflow that should be split into separate scope-specific workflows\n' +
1230
- ' - A legitimate cross-scope operation that needs documentation',
1231
- suggestion: `Scopes detected:\n${scopeBreakdown}\n\n` +
1232
- 'Consider:\n' +
1233
- ' 1. Split into separate workflows per scope\n' +
1234
- ' 2. If intentional, document the cross-scope nature in the workflow description\n' +
1235
- ' 3. Ensure all nodes have pv.otel.scope defined for accurate tracking',
1236
- fixable: false,
1237
- });
1238
- }
1239
-
1240
- // Warn about events with undefined scopes (only if some events have scopes)
1241
- if (undefinedEvents.length > 0 && definedScopes.length > 0) {
1242
- violations.push({
1243
- ruleId: 'workflow-scenario-scope-missing',
1244
- severity: 'warn',
1245
- file: workflowPath,
1246
- path: `scenarios[${idx}]`,
1247
- message: `Scenario "${scenario.id}" has events without scope defined`,
1248
- impact: 'Cannot determine if these events belong to the same instrumentation boundary',
1249
- suggestion: `Events missing scope: ${undefinedEvents.join(', ')}\n` +
1250
- `Add pv.otel.scope to these nodes in the canvas to enable scope consistency checking`,
1251
- fixable: false,
1252
- });
1253
- }
1254
-
1255
- // Check if node scopes are in owned-scopes list
1256
- if (context.ownedScopes && context.ownedScopes.length > 0) {
1257
- const unknownScopes = definedScopes.filter(scope => !context.ownedScopes!.includes(scope));
1258
- if (unknownScopes.length > 0) {
1259
- const eventsPerScope = unknownScopes
1260
- .map(scope => ` - ${scope}: ${scopesInScenario.get(scope)!.join(', ')}`)
1261
- .join('\n');
1262
-
1263
- violations.push({
1264
- ruleId: 'workflow-node-scope-not-owned',
1265
- severity: 'error',
1266
- file: workflowPath,
1267
- path: `scenarios[${idx}]`,
1268
- message: `Scenario "${scenario.id}" uses scopes not declared in library.yaml owned-scopes`,
1269
- impact: 'Spans from these scopes will not be matched - scopes must be declared in library.yaml',
1270
- suggestion: `Unknown scopes:\n${eventsPerScope}\n\n` +
1271
- `Either:\n` +
1272
- ` - Add these scopes to owned-scopes in library.yaml\n` +
1273
- ` - Update the nodes to use a declared scope: ${context.ownedScopes.join(', ')}`,
1274
- fixable: false,
1275
- });
1276
- }
1277
- }
1278
- }
1279
-
1280
- return violations;
1281
- }
1282
-
1283
- /**
1284
- * Check that template uses valid fields (not legacy format)
1285
- */
1286
- private checkTemplateStructure(
1287
- template: unknown,
1288
- file: string,
1289
- scenarioIdx: number
1290
- ): WorkflowViolation[] {
1291
- const violations: WorkflowViolation[] = [];
1292
- const validFields = ['introduction', 'events', 'logs', 'summary', 'span', 'children'];
1293
-
1294
- // Type guard: ensure template is an object
1295
- if (typeof template !== 'object' || template === null) {
1296
- return violations;
1297
- }
1298
-
1299
- const templateRecord = template as Record<string, unknown>;
1300
- const templateKeys = Object.keys(templateRecord);
1301
-
1302
- // Check that events field is present and is an object
1303
- if (!templateRecord.events) {
1304
- violations.push({
1305
- ruleId: 'workflow-template-structure',
1306
- severity: 'error',
1307
- file,
1308
- path: `scenarios[${scenarioIdx}].template`,
1309
- message: 'Template is missing required "events" field',
1310
- impact: 'Template must specify how to render each event type',
1311
- suggestion: 'Add "events: { eventName: template }" to map event names to templates',
1312
- fixable: false,
1313
- });
1314
- } else if (typeof templateRecord.events !== 'object' || Array.isArray(templateRecord.events)) {
1315
- violations.push({
1316
- ruleId: 'workflow-template-structure',
1317
- severity: 'error',
1318
- file,
1319
- path: `scenarios[${scenarioIdx}].template.events`,
1320
- message: 'Template "events" field must be an object',
1321
- impact: 'Events will not render correctly',
1322
- suggestion: 'Use object format: { "event.name": "template string" }',
1323
- fixable: false,
1324
- });
1325
- } else if (typeof templateRecord.events === 'object' && templateRecord.events !== null && Object.keys(templateRecord.events).length === 0) {
1326
- violations.push({
1327
- ruleId: 'workflow-template-structure',
1328
- severity: 'error',
1329
- file,
1330
- path: `scenarios[${scenarioIdx}].template.events`,
1331
- message: 'Template "events" field must not be empty',
1332
- impact: 'No events will be rendered in this scenario',
1333
- suggestion: 'Add at least one event template: { "event.name": "template string" }',
1334
- fixable: false,
1335
- });
1336
- }
1337
-
1338
- // Check for invalid/legacy fields
1339
- for (const key of templateKeys) {
1340
- if (!validFields.includes(key)) {
1341
- // Check for common legacy format fields
1342
- if (key === 'steps') {
1343
- violations.push({
1344
- ruleId: 'workflow-template-structure',
1345
- severity: 'error',
1346
- file,
1347
- path: `scenarios[${scenarioIdx}].template.${key}`,
1348
- message: `Invalid template field "${key}" (legacy format detected)`,
1349
- impact: 'Template will not render - "steps" field is not supported',
1350
- suggestion: 'Use "events: { eventName: template }" to map event names to templates',
1351
- fixable: false,
1352
- });
1353
- } else if (key === 'details') {
1354
- violations.push({
1355
- ruleId: 'workflow-template-structure',
1356
- severity: 'error',
1357
- file,
1358
- path: `scenarios[${scenarioIdx}].template.${key}`,
1359
- message: `Invalid template field "${key}" (legacy format detected)`,
1360
- impact: 'Template will not render - "details" field is not supported',
1361
- suggestion: 'Remove "details" field - use template variables in "events" or "summary" instead',
1362
- fixable: false,
1363
- });
1364
- } else {
1365
- violations.push({
1366
- ruleId: 'workflow-template-structure',
1367
- severity: 'error',
1368
- file,
1369
- path: `scenarios[${scenarioIdx}].template.${key}`,
1370
- message: `Unknown template field "${key}"`,
1371
- impact: 'This field will be ignored and may cause unexpected behavior',
1372
- suggestion: `Valid fields are: ${validFields.join(', ')}`,
1373
- fixable: false,
1374
- });
1375
- }
1376
- }
1377
- }
1378
-
1379
- return violations;
1380
- }
1381
-
1382
- /**
1383
- * Check that event names don't use attribute filter syntax
1384
- */
1385
- private checkEventNameSyntax(context: WorkflowValidationContext): WorkflowViolation[] {
1386
- const violations: WorkflowViolation[] = [];
1387
- const { workflow, workflowPath } = context;
1388
-
1389
- workflow.scenarios.forEach((scenario, scenarioIdx) => {
1390
- // Check template.events
1391
- if (scenario.template?.events) {
1392
- Object.keys(scenario.template.events).forEach((eventName) => {
1393
- if (eventName.includes('[') && eventName.includes(']')) {
1394
- violations.push({
1395
- ruleId: 'workflow-event-name-syntax',
1396
- severity: 'error',
1397
- file: workflowPath,
1398
- path: `scenarios[${scenarioIdx}].template.events["${eventName}"]`,
1399
- message: `Event name uses unsupported [attribute=value] syntax: "${eventName}"`,
1400
- impact: 'Attribute filter syntax is not supported - template will never render',
1401
- suggestion: `Use a distinct event name instead (e.g., "${this.extractBaseEventName(eventName)}.${this.extractAttributeValue(eventName)}")`,
1402
- fixable: false,
1403
- });
1404
- }
1405
- });
1406
- }
1407
- });
1408
-
1409
- return violations;
1410
- }
1411
-
1412
- /**
1413
- * Check template syntax (balanced braces, valid expressions)
1414
- */
1415
- private checkTemplateSyntax(context: WorkflowValidationContext): WorkflowViolation[] {
1416
- const violations: WorkflowViolation[] = [];
1417
- const { workflow, workflowPath } = context;
1418
-
1419
- workflow.scenarios.forEach((scenario, scenarioIdx) => {
1420
- if (!scenario.template) {
1421
- return;
1422
- }
1423
-
1424
- const template = scenario.template;
1425
-
1426
- // Check introduction
1427
- if (template.introduction) {
1428
- violations.push(...this.validateTemplateString(
1429
- template.introduction,
1430
- workflowPath,
1431
- `scenarios[${scenarioIdx}].template.introduction`
1432
- ));
1433
- }
1434
-
1435
- // Check summary
1436
- if (template.summary) {
1437
- violations.push(...this.validateTemplateString(
1438
- template.summary,
1439
- workflowPath,
1440
- `scenarios[${scenarioIdx}].template.summary`
1441
- ));
1442
- }
1443
-
1444
- // Check event templates
1445
- if (template.events) {
1446
- Object.entries(template.events).forEach(([eventName, templateEntry]) => {
1447
- const templateStr = getEventTemplateString(templateEntry);
1448
- violations.push(...this.validateTemplateString(
1449
- templateStr,
1450
- workflowPath,
1451
- `scenarios[${scenarioIdx}].template.events.${eventName}`
1452
- ));
1453
- });
1454
- }
1455
-
1456
- // Check log templates
1457
- if (template.logs) {
1458
- Object.entries(template.logs).forEach(([severity, templateStr]) => {
1459
- if (typeof templateStr === 'string') {
1460
- violations.push(...this.validateTemplateString(
1461
- templateStr,
1462
- workflowPath,
1463
- `scenarios[${scenarioIdx}].template.logs.${severity}`
1464
- ));
1465
- }
1466
- });
1467
- }
1468
- });
1469
-
1470
- return violations;
1471
- }
1472
-
1473
- /**
1474
- * Validate a single template string
1475
- */
1476
- private validateTemplateString(
1477
- templateStr: string,
1478
- file: string,
1479
- path: string
1480
- ): WorkflowViolation[] {
1481
- const violations: WorkflowViolation[] = [];
1482
-
1483
- // Check for balanced braces
1484
- let braceDepth = 0;
1485
- let inQuote = false;
1486
- let quoteChar = '';
1487
-
1488
- for (let i = 0; i < templateStr.length; i++) {
1489
- const char = templateStr[i];
1490
- const prevChar = i > 0 ? templateStr[i - 1] : '';
1491
-
1492
- // Track quotes
1493
- if ((char === "'" || char === '"') && prevChar !== '\\') {
1494
- if (!inQuote) {
1495
- inQuote = true;
1496
- quoteChar = char;
1497
- } else if (char === quoteChar) {
1498
- inQuote = false;
1499
- quoteChar = '';
1500
- }
1501
- }
1502
-
1503
- // Track braces (only outside of quotes)
1504
- if (!inQuote) {
1505
- if (char === '{') {
1506
- braceDepth++;
1507
- } else if (char === '}') {
1508
- braceDepth--;
1509
- if (braceDepth < 0) {
1510
- violations.push({
1511
- ruleId: 'workflow-template-syntax',
1512
- severity: 'error',
1513
- file,
1514
- path,
1515
- message: 'Unbalanced braces: closing } without opening {',
1516
- impact: 'Template will fail to render',
1517
- suggestion: 'Ensure all {{variables}} and {expressions} have matching braces',
1518
- fixable: false,
1519
- });
1520
- break;
1521
- }
1522
- }
1523
- }
1524
- }
1525
-
1526
- if (braceDepth > 0) {
1527
- violations.push({
1528
- ruleId: 'workflow-template-syntax',
1529
- severity: 'error',
1530
- file,
1531
- path,
1532
- message: 'Unbalanced braces: missing closing }',
1533
- impact: 'Template will fail to render',
1534
- suggestion: 'Ensure all {{variables}} and {expressions} have matching braces',
1535
- fixable: false,
1536
- });
1537
- }
1538
-
1539
- // Check for incomplete conditional expressions (? without :)
1540
- const conditionalPattern = /\{[^}]*\?[^}]*\}/g;
1541
- const conditionals = templateStr.match(conditionalPattern) || [];
1542
-
1543
- conditionals.forEach((expr) => {
1544
- // Simple check: if has ? but no :, it's incomplete
1545
- const questionCount = (expr.match(/\?/g) || []).length;
1546
- const colonCount = (expr.match(/:/g) || []).length;
1547
-
1548
- if (questionCount > colonCount) {
1549
- violations.push({
1550
- ruleId: 'workflow-template-syntax',
1551
- severity: 'error',
1552
- file,
1553
- path,
1554
- message: `Incomplete conditional expression: ${expr}`,
1555
- impact: 'Template will fail to render',
1556
- suggestion: 'Use Handlebars syntax: {{#if condition}}true{{else}}false{{/if}}',
1557
- fixable: false,
1558
- });
1559
- }
1560
- });
1561
-
1562
- // Check for Handlebars conditionals - these are not allowed in scenarios
1563
- // A scenario must represent a single deterministic trace, not multiple conditional paths
1564
- const handlebarsConditionalPattern = /\{\{#(if|unless|each)\b[^}]*\}\}|\{\{else\}\}/g;
1565
- const handlebarsConditionals = templateStr.match(handlebarsConditionalPattern) || [];
1566
-
1567
- if (handlebarsConditionals.length > 0) {
1568
- const firstMatch = handlebarsConditionals[0];
1569
- violations.push({
1570
- ruleId: 'workflow-template-conditional',
1571
- severity: 'error',
1572
- file,
1573
- path,
1574
- message: `Conditional syntax detected: ${firstMatch}`,
1575
- impact: 'Scenarios must represent a single deterministic trace. Conditionals introduce ambiguity about which events should be present.',
1576
- suggestion: 'Split into separate scenarios for each conditional path. Each scenario should represent one specific trace shape.',
1577
- fixable: false,
1578
- });
1579
- }
1580
-
1581
- // Check for emojis - templates should be plain text without emoji characters
1582
- const emojiPattern = /[\u{1F300}-\u{1F9FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]|[\u{1F600}-\u{1F64F}]|[\u{1F680}-\u{1F6FF}]|[\u{1F1E0}-\u{1F1FF}]|[\u{2300}-\u{23FF}]|[\u{2B50}-\u{2B55}]|[\u{200D}]|[\u{FE0F}]/gu;
1583
- const emojiMatches = templateStr.match(emojiPattern);
1584
-
1585
- if (emojiMatches && emojiMatches.length > 0) {
1586
- const uniqueEmojis = [...new Set(emojiMatches)].slice(0, 5);
1587
- violations.push({
1588
- ruleId: 'workflow-template-emoji',
1589
- severity: 'error',
1590
- file,
1591
- path,
1592
- message: `Template contains emoji characters: ${uniqueEmojis.join(' ')}`,
1593
- impact: 'Emojis in templates can cause rendering issues and inconsistent display across platforms',
1594
- suggestion: 'Remove emoji characters and use plain text descriptions instead',
1595
- fixable: true,
1596
- });
1597
- }
1598
-
1599
- return violations;
1600
- }
1601
-
1602
- /**
1603
- * Check for conflicting attribute paths in templates
1604
- *
1605
- * Detects when one attribute path is a prefix of another, which causes
1606
- * rendering issues. For example:
1607
- * - `git.branch` (expects string value)
1608
- * - `git.branch.source` (expects git.branch to be an object)
1609
- *
1610
- * When both are used, the nested object conversion will clobber the string
1611
- * value, resulting in `[object Object]` being rendered.
1612
- */
1613
- private checkConflictingAttributePaths(context: WorkflowValidationContext): WorkflowViolation[] {
1614
- const violations: WorkflowViolation[] = [];
1615
- const { workflow, workflowPath, canvas, canvasPath } = context;
1616
-
1617
- // Helper function to find conflicts in a set of paths
1618
- const findConflicts = (paths: Set<string>): Map<string, string[]> => {
1619
- const pathArray = Array.from(paths);
1620
- const conflicts = new Map<string, string[]>();
1621
-
1622
- for (let i = 0; i < pathArray.length; i++) {
1623
- for (let j = 0; j < pathArray.length; j++) {
1624
- if (i === j) continue;
1625
-
1626
- const pathA = pathArray[i];
1627
- const pathB = pathArray[j];
1628
-
1629
- // Check if pathA is a prefix of pathB (pathA.something)
1630
- if (pathB.startsWith(pathA + '.')) {
1631
- if (!conflicts.has(pathA)) {
1632
- conflicts.set(pathA, []);
1633
- }
1634
- if (!conflicts.get(pathA)!.includes(pathB)) {
1635
- conflicts.get(pathA)!.push(pathB);
1636
- }
1637
- }
1638
- }
1639
- }
1640
-
1641
- return conflicts;
1642
- };
1643
-
1644
- // 1. Check canvas event schema attributes for conflicts
1645
- if (canvas?.nodes) {
1646
- for (const node of canvas.nodes) {
1647
- // Only check nodes that have event schemas (not text nodes)
1648
- if (!('event' in node) || !node.event) continue;
1649
- const eventSchema = node.event as { name?: string; attributes?: Record<string, unknown> };
1650
- if (eventSchema.attributes && typeof eventSchema.attributes === 'object') {
1651
- const attributeNames = new Set<string>(Object.keys(eventSchema.attributes));
1652
- const conflicts = findConflicts(attributeNames);
1653
-
1654
- for (const [parent, children] of conflicts.entries()) {
1655
- violations.push({
1656
- ruleId: 'canvas-event-attribute-conflict',
1657
- severity: 'error',
1658
- file: canvasPath || workflowPath,
1659
- path: `nodes[${node.id}].event.attributes`,
1660
- message: `Conflicting attribute paths in event "${eventSchema.name || 'unknown'}": "${parent}" conflicts with ${children.map(c => `"${c}"`).join(', ')}`,
1661
- impact: `When rendered in workflow templates, "${parent}" will become "[object Object]" because nested paths require it to be an object`,
1662
- suggestion: `Rename attributes to avoid conflicts:\n` +
1663
- ` - "${parent}" → "${parent}Value" or "${parent.replace(/\./g, '_')}"\n` +
1664
- ` - Or use a different naming scheme that doesn't nest (e.g., "${parent.split('.').slice(0, -1).join('.')}_${parent.split('.').pop()}")`,
1665
- fixable: false,
1666
- });
1667
- }
1668
- }
1669
- }
1670
- }
1671
-
1672
- // 2. Check each scenario's template variables for conflicts
1673
- workflow.scenarios.forEach((scenario, scenarioIdx) => {
1674
- if (!scenario.template) return;
1675
-
1676
- // Collect all attribute paths from this scenario's templates
1677
- const allPaths = new Set<string>();
1678
-
1679
- // From introduction
1680
- if (scenario.template.introduction) {
1681
- this.extractAttributeReferences(scenario.template.introduction).forEach(p => allPaths.add(p));
1682
- }
1683
-
1684
- // From summary
1685
- if (scenario.template.summary) {
1686
- this.extractAttributeReferences(scenario.template.summary).forEach(p => allPaths.add(p));
1687
- }
1688
-
1689
- // From event templates
1690
- if (scenario.template.events) {
1691
- Object.values(scenario.template.events).forEach(templateEntry => {
1692
- const templateStr = getEventTemplateString(templateEntry);
1693
- this.extractAttributeReferences(templateStr).forEach(p => allPaths.add(p));
1694
- });
1695
- }
1696
-
1697
- // Check for conflicts
1698
- const conflicts = findConflicts(allPaths);
1699
-
1700
- // Report violations
1701
- for (const [parent, children] of conflicts.entries()) {
1702
- violations.push({
1703
- ruleId: 'workflow-attribute-path-conflict',
1704
- severity: 'error',
1705
- file: workflowPath,
1706
- path: `scenarios[${scenarioIdx}].template`,
1707
- message: `Conflicting attribute paths: "{{${parent}}}" conflicts with ${children.map(c => `"{{${c}}}"`).join(', ')}`,
1708
- impact: `"{{${parent}}}" will render as "[object Object]" because nested paths require it to be an object, not a primitive value`,
1709
- suggestion: `Options:\n` +
1710
- ` 1. Remove "{{${parent}}}" and only use the nested paths\n` +
1711
- ` 2. Rename attributes to avoid conflicts (e.g., "${parent}" → "${parent}_value" or "${parent}.value")\n` +
1712
- ` 3. Use different attribute names that don't share a common prefix`,
1713
- fixable: false,
1714
- });
1715
- }
1716
- });
1717
-
1718
- return violations;
1719
- }
1720
-
1721
- /**
1722
- * Check attribute references against execution data
1723
- *
1724
- * Validates that:
1725
- * - Attributes referenced in templates exist in execution data
1726
- * - Object attributes are accessed via properties (not used directly)
1727
- * - Attribute names are correct (catches typos)
1728
- */
1729
- private checkAttributeReferences(context: WorkflowValidationContext): WorkflowViolation[] {
1730
- const violations: WorkflowViolation[] = [];
1731
- const { workflow, workflowPath, executionData } = context;
1732
-
1733
- // Skip if no execution data provided
1734
- if (!executionData) {
1735
- return violations;
1736
- }
1737
-
1738
- const { aggregates, eventAttributes } = executionData;
1739
-
1740
- // Check each scenario's template
1741
- for (const scenario of workflow.scenarios) {
1742
- const scenarioPath = `scenarios[${scenario.id}]`;
1743
-
1744
- // Check introduction template
1745
- if (scenario.template.introduction) {
1746
- const attrs = this.extractAttributeReferences(scenario.template.introduction);
1747
- violations.push(
1748
- ...this.validateAttributes(
1749
- attrs,
1750
- aggregates,
1751
- null, // introduction doesn't have specific event context
1752
- workflowPath,
1753
- `${scenarioPath}.template.introduction`
1754
- )
1755
- );
1756
- }
1757
-
1758
- // Check event templates
1759
- if (scenario.template.events) {
1760
- for (const [eventName, eventTemplateEntry] of Object.entries(scenario.template.events)) {
1761
- const eventTemplate = getEventTemplateString(eventTemplateEntry);
1762
- const attrs = this.extractAttributeReferences(eventTemplate);
1763
- const eventAttrs = eventAttributes.get(eventName);
1764
-
1765
- violations.push(
1766
- ...this.validateAttributes(
1767
- attrs,
1768
- aggregates,
1769
- eventAttrs || null,
1770
- workflowPath,
1771
- `${scenarioPath}.template.events.${eventName}`,
1772
- eventName
1773
- )
1774
- );
1775
- }
1776
- }
1777
-
1778
- // Check summary template
1779
- if (scenario.template.summary) {
1780
- const attrs = this.extractAttributeReferences(scenario.template.summary);
1781
- violations.push(
1782
- ...this.validateAttributes(
1783
- attrs,
1784
- aggregates,
1785
- null, // summary uses global aggregates
1786
- workflowPath,
1787
- `${scenarioPath}.template.summary`
1788
- )
1789
- );
1790
- }
1791
- }
1792
-
1793
- return violations;
1794
- }
1795
-
1796
- /**
1797
- * Validate a list of attribute references against available data
1798
- *
1799
- * @param attributes - Attribute paths to validate
1800
- * @param aggregates - Global aggregate attributes
1801
- * @param eventAttributes - Event-specific attributes (if validating event template)
1802
- * @param file - File path for violation reporting
1803
- * @param path - JSON path for violation reporting
1804
- * @param eventName - Event name (if validating event template)
1805
- * @returns Array of violations found
1806
- */
1807
- private validateAttributes(
1808
- attributes: string[],
1809
- aggregates: Record<string, unknown>,
1810
- eventAttributes: Record<string, unknown> | null,
1811
- file: string,
1812
- path: string,
1813
- eventName?: string
1814
- ): WorkflowViolation[] {
1815
- const violations: WorkflowViolation[] = [];
1816
-
1817
- for (const attr of attributes) {
1818
- // Check if attribute exists in global aggregates
1819
- const globalValue = aggregates[attr];
1820
- const eventValue = eventAttributes?.[attr];
1821
-
1822
- // Attribute doesn't exist anywhere
1823
- if (globalValue === undefined && eventValue === undefined) {
1824
- // Try to find similar attributes for helpful suggestions
1825
- const allKeys = [
1826
- ...Object.keys(aggregates),
1827
- ...(eventAttributes ? Object.keys(eventAttributes) : []),
1828
- ];
1829
- const similar = this.findSimilarAttributes(attr, allKeys);
1830
-
1831
- violations.push({
1832
- ruleId: 'workflow-attribute-undefined',
1833
- severity: 'warn',
1834
- file,
1835
- path,
1836
- message: eventName
1837
- ? `Attribute "{{${attr}}}" not found in event "${eventName}" or global aggregates`
1838
- : `Attribute "{{${attr}}}" not found in execution data`,
1839
- impact: 'Template will render as empty or "undefined"',
1840
- suggestion: similar.length > 0 ? `Did you mean: ${similar.join(', ')}?` : undefined,
1841
- fixable: false,
1842
- });
1843
- continue;
1844
- }
1845
-
1846
- // Check if object is used directly (should use property access)
1847
- const value = eventValue !== undefined ? eventValue : globalValue;
1848
- if (this.isObjectType(value)) {
1849
- const objectKeys = Object.keys(value as Record<string, unknown>);
1850
- const suggestions = objectKeys.slice(0, 3).map((k) => `{{${attr}.${k}}}`);
1851
-
1852
- violations.push({
1853
- ruleId: 'workflow-attribute-object',
1854
- severity: 'warn',
1855
- file,
1856
- path,
1857
- message: `Attribute "{{${attr}}}" is an object and will render as "[object Object]"`,
1858
- impact: 'Template will show "[object Object]" instead of useful data',
1859
- suggestion: `Access a property instead: ${suggestions.join(', ')}`,
1860
- fixable: false,
1861
- });
1862
- }
1863
- }
1864
-
1865
- return violations;
1866
- }
1867
-
1868
- /**
1869
- * Check event attribute requirements:
1870
- * All attributes defined in event schemas must be used in at least one template.
1871
- *
1872
- * This ensures all defined attributes have a purpose and are displayed to users.
1873
- */
1874
- private checkEventAttributeRequirements(context: WorkflowValidationContext): WorkflowViolation[] {
1875
- const violations: WorkflowViolation[] = [];
1876
- const { workflow, workflowPath, canvas, canvasPath } = context;
1877
-
1878
- if (!canvas?.nodes) {
1879
- return violations;
1880
- }
1881
-
1882
- // Collect all event names referenced in workflow scenarios
1883
- // Only validate attributes for events that are INCLUDED in the workflow
1884
- const workflowEventNames = new Set<string>();
1885
- for (const scenario of workflow.scenarios) {
1886
- if (!scenario.template?.events) continue;
1887
- for (const eventName of Object.keys(scenario.template.events)) {
1888
- workflowEventNames.add(eventName);
1889
- }
1890
- }
1891
-
1892
- // Collect all attribute references from workflow templates by event name
1893
- const templateAttributesByEvent = new Map<string, Set<string>>();
1894
-
1895
- for (const scenario of workflow.scenarios) {
1896
- if (!scenario.template?.events) continue;
1897
-
1898
- for (const [eventName, eventTemplateEntry] of Object.entries(scenario.template.events)) {
1899
- const eventTemplate = getEventTemplateString(eventTemplateEntry);
1900
- const attrs = this.extractAttributeReferences(eventTemplate);
1901
- if (!templateAttributesByEvent.has(eventName)) {
1902
- templateAttributesByEvent.set(eventName, new Set());
1903
- }
1904
- const eventAttrs = templateAttributesByEvent.get(eventName)!;
1905
- attrs.forEach((attr) => eventAttrs.add(attr));
1906
- }
1907
- }
1908
-
1909
- // Check each canvas node's event schema (only for events included in the workflow)
1910
- // Only otel-event nodes are supported
1911
- for (const node of canvas.nodes) {
1912
- let eventName: string | undefined;
1913
- let eventSchema: { attributes?: Record<string, unknown> } | undefined;
1914
- let schemaSource: 'inline' | 'library' = 'inline';
1915
-
1916
- // Only process otel-event nodes
1917
- if (isOtelEventNode(node)) {
1918
- const otelEventNode = node as OtelEventNode;
1919
- if (otelEventNode.event?.name) {
1920
- eventName = otelEventNode.event.name;
1921
- eventSchema = otelEventNode.event as { attributes?: Record<string, unknown> };
1922
- } else if (otelEventNode.eventRef) {
1923
- eventName = otelEventNode.eventRef;
1924
- schemaSource = 'library';
1925
- const sources = context.eventRegistry?.findEvent(eventName) ?? [];
1926
- const librarySource = sources.find((s) => s.type === 'library' && s.eventSchema);
1927
- if (librarySource?.eventSchema) {
1928
- eventSchema = librarySource.eventSchema;
1929
- }
1930
- }
1931
- }
1932
- // Legacy pv.event/pv.eventRef format is no longer supported
1933
-
1934
- if (!eventName || !eventSchema?.attributes) {
1935
- continue; // No schema to validate
1936
- }
1937
-
1938
- // Skip events not included in this workflow
1939
- // This allows canvases with disconnected subgraphs where different workflows focus on different flows
1940
- if (!workflowEventNames.has(eventName)) {
1941
- continue;
1942
- }
1943
-
1944
- // Get all defined attributes
1945
- const definedAttrs = Object.keys(eventSchema.attributes);
1946
-
1947
- // All attributes must be used in templates
1948
- const usedAttrs = templateAttributesByEvent.get(eventName) ?? new Set<string>();
1949
-
1950
- for (const attr of definedAttrs) {
1951
- // Check if attribute is used (direct match or as prefix of a nested path)
1952
- const isUsed = Array.from(usedAttrs).some(
1953
- (usedAttr) => usedAttr === attr || usedAttr.startsWith(attr + '.')
1954
- );
1955
-
1956
- if (!isUsed) {
1957
- violations.push({
1958
- ruleId: 'workflow-attribute-unused',
1959
- severity: 'error',
1960
- file: canvasPath || workflowPath,
1961
- path: schemaSource === 'inline' ? `nodes[${node.id}].event.attributes.${attr}` : `library.eventSchemas.${eventName}.attributes.${attr}`,
1962
- message: `Event "${eventName}" defines attribute "${attr}" that is not used in any template`,
1963
- impact: 'This attribute is defined but never used in any template',
1964
- suggestion: `Use {{${attr}}} in a template for event "${eventName}", or remove the attribute if it's not needed`,
1965
- fixable: false,
1966
- });
1967
- }
1968
- }
1969
- }
1970
-
1971
- return violations;
1972
- }
1973
-
1974
- /**
1975
- * Check that template attributes are defined in the canvas event schema.
1976
- *
1977
- * This validates the reverse of checkEventAttributeRequirements:
1978
- * - checkEventAttributeRequirements: schema defines attr not used in template → error
1979
- * - checkTemplateAttributesDefinedInSchema: template uses attr not in schema → error
1980
- *
1981
- * This catches cases where:
1982
- * - Template references {{context.projectRoot}} but schema doesn't define it
1983
- * - Typos in attribute names (e.g., {{contex.projectRoot}})
1984
- * - Attributes emitted by instrumentation but not documented in schema
1985
- */
1986
- private checkTemplateAttributesDefinedInSchema(context: WorkflowValidationContext): WorkflowViolation[] {
1987
- const violations: WorkflowViolation[] = [];
1988
- const { workflow, workflowPath, canvas, canvasPath } = context;
1989
-
1990
- if (!canvas?.nodes) {
1991
- return violations;
1992
- }
1993
-
1994
- // Build a map of event name -> schema attributes (including nested paths)
1995
- // Only otel-event nodes are supported
1996
- const eventSchemaAttributes = new Map<string, Set<string>>();
1997
-
1998
- for (const node of canvas.nodes) {
1999
- let eventName: string | undefined;
2000
- let eventSchema: { attributes?: Record<string, unknown> } | undefined;
2001
-
2002
- // Only process otel-event nodes
2003
- if (isOtelEventNode(node)) {
2004
- const otelEventNode = node as OtelEventNode;
2005
- if (otelEventNode.event?.name) {
2006
- eventName = otelEventNode.event.name;
2007
- eventSchema = otelEventNode.event as { attributes?: Record<string, unknown> };
2008
- } else if (otelEventNode.eventRef) {
2009
- eventName = otelEventNode.eventRef;
2010
- const sources = context.eventRegistry?.findEvent(eventName) ?? [];
2011
- const librarySource = sources.find((s) => s.type === 'library' && s.eventSchema);
2012
- if (librarySource?.eventSchema) {
2013
- eventSchema = librarySource.eventSchema;
2014
- }
2015
- }
2016
- }
2017
- // Legacy pv.event/pv.eventRef format is no longer supported
2018
-
2019
- if (!eventName) continue;
2020
-
2021
- // Collect all attribute paths from schema (exact paths only, not parents)
2022
- const schemaAttrs = new Set<string>();
2023
- if (eventSchema?.attributes) {
2024
- for (const attrName of Object.keys(eventSchema.attributes)) {
2025
- schemaAttrs.add(attrName);
2026
- }
2027
- }
2028
-
2029
- eventSchemaAttributes.set(eventName, schemaAttrs);
2030
- }
2031
-
2032
- // Check each scenario's event templates
2033
- for (let scenarioIdx = 0; scenarioIdx < workflow.scenarios.length; scenarioIdx++) {
2034
- const scenario = workflow.scenarios[scenarioIdx];
2035
- if (!scenario.template?.events) continue;
2036
-
2037
- for (const [eventName, eventTemplateEntry] of Object.entries(scenario.template.events)) {
2038
- // Skip wildcard patterns
2039
- if (eventName.includes('*')) continue;
2040
-
2041
- const schemaAttrs = eventSchemaAttributes.get(eventName);
2042
-
2043
- // If event has no schema, we can't validate (already flagged by checkEventReferences)
2044
- if (!schemaAttrs) continue;
2045
-
2046
- // Extract attribute references from this template
2047
- const eventTemplate = getEventTemplateString(eventTemplateEntry);
2048
- const templateAttrs = this.extractAttributeReferences(eventTemplate);
2049
-
2050
- for (const attr of templateAttrs) {
2051
- // Check if attribute is defined in schema:
2052
- // 1. Exact match: {{input.taskId}} with schema "input.taskId"
2053
- // 2. Parent access: {{input}} with schema "input.taskId" (accessing parent object)
2054
- // 3. NOT: {{input.takId}} with schema "input.taskId" (typo - different leaf)
2055
- const isDefinedInSchema =
2056
- schemaAttrs.has(attr) || // Exact match
2057
- Array.from(schemaAttrs).some(schemaAttr => schemaAttr.startsWith(attr + '.')); // attr is parent of schema attr
2058
-
2059
- if (!isDefinedInSchema) {
2060
- // Find similar attributes for suggestions
2061
- const similar = this.findSimilarAttributes(attr, Array.from(schemaAttrs));
2062
-
2063
- violations.push({
2064
- ruleId: 'workflow-template-attribute-not-in-schema',
2065
- severity: 'error',
2066
- file: workflowPath,
2067
- path: `scenarios[${scenarioIdx}].template.events["${eventName}"]`,
2068
- message: `Template references "{{${attr}}}" but it is not defined in the event schema for "${eventName}"`,
2069
- impact: 'This attribute may render as empty or cause unexpected behavior. The schema should document all attributes used in templates.',
2070
- suggestion: similar.length > 0
2071
- ? `Did you mean: ${similar.map(s => `{{${s}}}`).join(', ')}? Or add "${attr}" to the event schema in ${canvasPath || 'the canvas file'}.`
2072
- : `Add "${attr}" to the event schema for "${eventName}" in ${canvasPath || 'the canvas file'}, or remove it from the template if not needed.`,
2073
- fixable: false,
2074
- });
2075
- }
2076
- }
2077
- }
2078
- }
2079
-
2080
- return violations;
2081
- }
2082
-
2083
- /**
2084
- * Check if all events in scenario templates are connected through canvas edges
2085
- */
2086
- private checkEventConnectivity(context: WorkflowValidationContext): WorkflowViolation[] {
2087
- const violations: WorkflowViolation[] = [];
2088
-
2089
- if (!context.canvas) {
2090
- return violations; // Can't validate without canvas
2091
- }
2092
-
2093
- // Check if canvas has edges - if not, error for workflows with multi-event scenarios
2094
- if (!context.canvas.edges || context.canvas.edges.length === 0) {
2095
- const { workflow } = context;
2096
-
2097
- for (let i = 0; i < workflow.scenarios.length; i++) {
2098
- const scenario = workflow.scenarios[i];
2099
-
2100
- if (!scenario.template?.events) {
2101
- continue;
2102
- }
2103
-
2104
- const eventNames = Object.keys(scenario.template.events);
2105
-
2106
- if (eventNames.length >= 2) {
2107
- violations.push({
2108
- ruleId: 'workflow-event-connectivity',
2109
- severity: 'error',
2110
- file: context.workflowPath,
2111
- path: `scenarios[${i}]`,
2112
- message: `Scenario "${scenario.id}" has ${eventNames.length} events but the canvas has no edges`,
2113
- impact: 'Events will appear as isolated nodes in workflow visualizations, making it unclear how execution flows between them',
2114
- suggestion: 'Add edges to the canvas to connect the events in this scenario',
2115
- fixable: false,
2116
- });
2117
- }
2118
- }
2119
-
2120
- return violations;
2121
- }
2122
-
2123
- const { workflow, canvas } = context;
2124
-
2125
- // Build event name → node ID mapping (otel-event nodes only)
2126
- const eventToNodeId = new Map<string, string>();
2127
- for (const node of canvas.nodes || []) {
2128
- if (isOtelEventNode(node)) {
2129
- const otelEventNode = node as OtelEventNode;
2130
- const eventName = otelEventNode.event?.name || otelEventNode.eventRef;
2131
- if (eventName) {
2132
- eventToNodeId.set(eventName, node.id);
2133
- }
2134
- }
2135
- // Legacy pv.event/pv.eventRef format is no longer supported
2136
- }
2137
-
2138
- // Build adjacency graph from edges
2139
- const adjacency = new Map<string, Set<string>>();
2140
- for (const edge of canvas.edges!) {
2141
- if (!adjacency.has(edge.fromNode)) {
2142
- adjacency.set(edge.fromNode, new Set());
2143
- }
2144
- if (!adjacency.has(edge.toNode)) {
2145
- adjacency.set(edge.toNode, new Set());
2146
- }
2147
- adjacency.get(edge.fromNode)!.add(edge.toNode);
2148
- adjacency.get(edge.toNode)!.add(edge.fromNode); // Treat as undirected for connectivity
2149
- }
2150
-
2151
- // Check each scenario
2152
- for (let i = 0; i < workflow.scenarios.length; i++) {
2153
- const scenario = workflow.scenarios[i];
2154
-
2155
- if (!scenario.template?.events) {
2156
- continue;
2157
- }
2158
-
2159
- const eventNames = Object.keys(scenario.template.events);
2160
- const nodeIds = eventNames
2161
- .map(name => eventToNodeId.get(name))
2162
- .filter((id): id is string => id !== undefined);
2163
-
2164
- if (nodeIds.length < 2) {
2165
- continue; // Need at least 2 nodes to check connectivity
2166
- }
2167
-
2168
- // Check if all nodes are in the same connected component
2169
- const disconnected = this.findDisconnectedNodes(nodeIds, adjacency);
2170
-
2171
- if (disconnected.length > 0) {
2172
- violations.push({
2173
- ruleId: 'workflow-event-connectivity',
2174
- severity: 'error',
2175
- file: context.workflowPath,
2176
- path: `scenarios[${i}].template.events`,
2177
- message: `Scenario "${scenario.id}" has ${disconnected.length} disconnected event(s)`,
2178
- impact: 'Events will appear as isolated nodes in workflow visualizations, making it unclear how execution flows between them',
2179
- suggestion: `Add intermediate events to connect the flow. Disconnected events: ${disconnected.map(id => {
2180
- const node = canvas.nodes?.find(n => n.id === id);
2181
- if (!node) return id;
2182
- if (isOtelEventNode(node)) {
2183
- const otelEventNode = node as OtelEventNode;
2184
- return otelEventNode.event?.name || otelEventNode.eventRef || id;
2185
- }
2186
- return id;
2187
- }).join(', ')}`,
2188
- fixable: false,
2189
- });
2190
- }
2191
- }
2192
-
2193
- return violations;
2194
- }
2195
-
2196
- /**
2197
- * Find nodes that are not connected to the main component
2198
- */
2199
- private findDisconnectedNodes(
2200
- nodeIds: string[],
2201
- adjacency: Map<string, Set<string>>
2202
- ): string[] {
2203
- if (nodeIds.length === 0) return [];
2204
-
2205
- // BFS from first node - traverse ALL nodes to find connectivity
2206
- const visited = new Set<string>();
2207
- const queue = [nodeIds[0]];
2208
-
2209
- while (queue.length > 0) {
2210
- const current = queue.shift()!;
2211
- if (visited.has(current)) continue;
2212
- visited.add(current);
2213
-
2214
- const neighbors = adjacency.get(current) || new Set();
2215
- for (const neighbor of neighbors) {
2216
- if (!visited.has(neighbor)) {
2217
- queue.push(neighbor);
2218
- }
2219
- }
2220
- }
2221
-
2222
- // Return nodes from nodeIds that were not visited (disconnected)
2223
- return nodeIds.filter(id => !visited.has(id));
2224
- }
2225
-
2226
- /**
2227
- * Find similar attribute names for helpful suggestions
2228
- *
2229
- * Uses simple string similarity (Levenshtein-like) to find typos
2230
- *
2231
- * @param target - The attribute being searched for
2232
- * @param available - Available attribute names
2233
- * @returns Array of similar attribute names (max 3)
2234
- */
2235
- private findSimilarAttributes(target: string, available: string[]): string[] {
2236
- const similar: Array<{ attr: string; score: number }> = [];
2237
-
2238
- for (const attr of available) {
2239
- // Check for prefix match
2240
- if (attr.startsWith(target) || target.startsWith(attr)) {
2241
- similar.push({ attr, score: 10 });
2242
- continue;
2243
- }
2244
-
2245
- // Check for substring match
2246
- if (attr.includes(target) || target.includes(attr)) {
2247
- similar.push({ attr, score: 5 });
2248
- continue;
2249
- }
2250
-
2251
- // Check for similar structure (same number of dots)
2252
- const targetParts = target.split('.');
2253
- const attrParts = attr.split('.');
2254
- if (targetParts.length === attrParts.length) {
2255
- // Check if any parts match
2256
- const matchingParts = targetParts.filter((p, i) => p === attrParts[i]).length;
2257
- if (matchingParts > 0) {
2258
- similar.push({ attr, score: matchingParts });
2259
- }
2260
- }
2261
- }
2262
-
2263
- // Sort by score and return top 3
2264
- return similar
2265
- .sort((a, b) => b.score - a.score)
2266
- .slice(0, 3)
2267
- .map((s) => s.attr);
2268
- }
2269
-
2270
- /**
2271
- * Check formatting options
2272
- */
2273
- private checkFormattingOptions(context: WorkflowValidationContext): WorkflowViolation[] {
2274
- const violations: WorkflowViolation[] = [];
2275
- const { workflow, workflowPath } = context;
2276
-
2277
- if (!workflow.formatting) {
2278
- return violations;
2279
- }
2280
-
2281
- // Check showAttributes
2282
- if (workflow.formatting.showAttributes) {
2283
- const validValues = ['none', 'matched', 'all'];
2284
- if (!validValues.includes(workflow.formatting.showAttributes)) {
2285
- violations.push({
2286
- ruleId: 'workflow-formatting-options',
2287
- severity: 'warn',
2288
- file: workflowPath,
2289
- path: 'formatting.showAttributes',
2290
- message: `Invalid showAttributes value: "${workflow.formatting.showAttributes}"`,
2291
- impact: 'May not display attributes correctly',
2292
- suggestion: `Use one of: ${validValues.join(', ')}`,
2293
- fixable: false,
2294
- });
2295
- }
2296
- }
2297
-
2298
- return violations;
2299
- }
2300
-
2301
- /**
2302
- * Check execution data completeness
2303
- *
2304
- * Validates that co-located execution files contain the events and attributes
2305
- * that workflow templates reference.
2306
- */
2307
- private async checkExecutionDataCompleteness(context: WorkflowValidationContext): Promise<WorkflowViolation[]> {
2308
- const violations: WorkflowViolation[] = [];
2309
- const { workflow, workflowPath, executionFiles } = context;
2310
-
2311
- if (!executionFiles || executionFiles.length === 0) {
2312
- return violations;
2313
- }
2314
-
2315
- // Load and parse all execution files
2316
- const executions: Array<{path: string; data: IExportTraceServiceRequest}> = [];
2317
- for (const execPath of executionFiles) {
2318
- try {
2319
- const content = await this.fsAdapter.readFile(execPath);
2320
- const data = JSON.parse(content) as IExportTraceServiceRequest;
2321
- executions.push({ path: execPath, data });
2322
- } catch (error) {
2323
- // Skip files that can't be loaded/parsed
2324
- continue;
2325
- }
2326
- }
2327
-
2328
- if (executions.length === 0) {
2329
- return violations;
2330
- }
2331
-
2332
- // Check for multiple traces in single files (anti-pattern)
2333
- for (const { path, data } of executions) {
2334
- const traceIds = new Set<string>();
2335
-
2336
- data.resourceSpans?.forEach((rs) => {
2337
- rs.scopeSpans?.forEach((ss) => {
2338
- ss.spans?.forEach((span) => {
2339
- if (span.traceId) {
2340
- traceIds.add(typeof span.traceId === 'string' ? span.traceId : Buffer.from(span.traceId).toString('hex'));
2341
- }
2342
- });
2343
- });
2344
- });
2345
-
2346
- if (traceIds.size > 1) {
2347
- const fileName = path.split('/').pop() || path;
2348
- violations.push({
2349
- ruleId: 'workflow-execution-multiple-traces',
2350
- severity: 'warn',
2351
- file: path,
2352
- message: `Execution file contains ${traceIds.size} traces - should contain only one trace per file`,
2353
- impact: 'Cannot establish clear trace-to-scenario association, makes debugging harder',
2354
- suggestion: `Split ${fileName} into ${traceIds.size} separate files, one per test case (e.g., success.otel.json, error.otel.json)`,
2355
- fixable: false,
2356
- });
2357
- }
2358
- }
2359
-
2360
- // For each scenario, check if execution data can satisfy the template
2361
- workflow.scenarios.forEach((scenario, scenarioIdx) => {
2362
- if (!scenario.template) return;
2363
-
2364
- // Extract all template variables from this scenario
2365
- const templateVars = new Set<string>();
2366
- const eventTemplates = new Map<string, Set<string>>();
2367
-
2368
- // Extract variables from summary
2369
- if (scenario.template.summary) {
2370
- this.extractTemplateVariables(scenario.template.summary).forEach(v => templateVars.add(v));
2371
- }
2372
-
2373
- // Extract variables from introduction
2374
- if (scenario.template.introduction) {
2375
- this.extractTemplateVariables(scenario.template.introduction).forEach(v => templateVars.add(v));
2376
- }
2377
-
2378
- // Extract variables from event templates
2379
- if (scenario.template.events) {
2380
- Object.entries(scenario.template.events).forEach(([eventName, templateEntry]) => {
2381
- const template = getEventTemplateString(templateEntry);
2382
- const vars = this.extractTemplateVariables(template);
2383
- if (!eventTemplates.has(eventName)) {
2384
- eventTemplates.set(eventName, new Set());
2385
- }
2386
- vars.forEach(v => {
2387
- templateVars.add(v);
2388
- eventTemplates.get(eventName)!.add(v);
2389
- });
2390
- });
2391
- }
2392
-
2393
- // Check if ANY execution file has the data needed
2394
- const hasCompleteData = executions.some(({ data }) => {
2395
- return this.executionHasTemplateData(data, scenario.template, templateVars, eventTemplates);
2396
- });
2397
-
2398
- if (!hasCompleteData && templateVars.size > 0) {
2399
- const missingInfo = this.findMissingTemplateData(executions, scenario.template, eventTemplates);
2400
-
2401
- violations.push({
2402
- ruleId: 'workflow-execution-data-incomplete',
2403
- severity: 'warn',
2404
- file: workflowPath,
2405
- path: `scenarios[${scenarioIdx}].template`,
2406
- message: `Template references data not found in co-located execution files`,
2407
- impact: 'Template variables will not resolve when viewing executions',
2408
- suggestion: missingInfo.length > 0
2409
- ? `Missing: ${missingInfo.slice(0, 3).join(', ')}${missingInfo.length > 3 ? ` and ${missingInfo.length - 3} more` : ''}`
2410
- : 'Ensure execution files contain the events and attributes referenced in templates',
2411
- fixable: false,
2412
- });
2413
- }
2414
- });
2415
-
2416
- return violations;
2417
- }
2418
-
2419
- /**
2420
- * Extract template variable references from a template string
2421
- * Matches {{variableName}} and {{@span.attributeName}} patterns
2422
- */
2423
- private extractTemplateVariables(template: string): string[] {
2424
- const vars: string[] = [];
2425
- // Match {{variableName}}, {{object.property}}, or {{@span.attributeName}}
2426
- // but not {{#if}} {{/if}} {{else}}
2427
- const pattern = /\{\{(?!\s*[#/])\s*(@?[a-zA-Z_][a-zA-Z0-9._]*)\s*\}\}/g;
2428
- let match;
2429
-
2430
- while ((match = pattern.exec(template)) !== null) {
2431
- const varName = match[1];
2432
- // Skip Handlebars helpers and keywords
2433
- if (!['this', 'else', 'each', 'if', 'unless', 'with'].includes(varName)) {
2434
- // Skip @span.* variables - they reference span attributes which are
2435
- // validated separately and don't need to be in event attributes
2436
- if (!varName.startsWith('@span.')) {
2437
- vars.push(varName);
2438
- }
2439
- }
2440
- }
2441
-
2442
- return vars;
2443
- }
2444
-
2445
- /**
2446
- * Check if execution data contains the template data needed
2447
- */
2448
- private executionHasTemplateData(
2449
- execution: IExportTraceServiceRequest,
2450
- template: ScenarioTemplate,
2451
- templateVars: Set<string>,
2452
- eventTemplates: Map<string, Set<string>>
2453
- ): boolean {
2454
- // Collect all events and their attributes from the execution
2455
- const executionEvents = new Map<string, Set<string>>();
2456
-
2457
- execution.resourceSpans?.forEach((rs) => {
2458
- rs.scopeSpans?.forEach((ss) => {
2459
- ss.spans?.forEach((span) => {
2460
- // Add span events
2461
- span.events?.forEach((event) => {
2462
- const eventName = event.name;
2463
- if (!executionEvents.has(eventName)) {
2464
- executionEvents.set(eventName, new Set());
2465
- }
2466
- // Add event attributes
2467
- event.attributes?.forEach((attr) => {
2468
- executionEvents.get(eventName)!.add(attr.key);
2469
- });
2470
- });
2471
-
2472
- // Also check span-level attributes (available as aggregates)
2473
- if (span.name) {
2474
- if (!executionEvents.has(span.name)) {
2475
- executionEvents.set(span.name, new Set());
2476
- }
2477
- span.attributes?.forEach((attr) => {
2478
- executionEvents.get(span.name)!.add(attr.key);
2479
- });
2480
- }
2481
- });
2482
- });
2483
- });
2484
-
2485
- // Check if the execution has the events referenced in templates
2486
- let hasAllData = true;
2487
-
2488
- for (const [eventName, requiredVars] of eventTemplates.entries()) {
2489
- // Check if this event exists in execution
2490
- const eventData = executionEvents.get(eventName);
2491
- if (!eventData) {
2492
- // Event doesn't exist - this is incomplete
2493
- hasAllData = false;
2494
- break;
2495
- }
2496
-
2497
- // Check if event has all required attributes
2498
- for (const varName of requiredVars) {
2499
- const baseVar = varName.split('.')[0]; // Handle nested properties
2500
- if (!eventData.has(baseVar)) {
2501
- hasAllData = false;
2502
- break;
2503
- }
2504
- }
2505
-
2506
- if (!hasAllData) break;
2507
- }
2508
-
2509
- return hasAllData;
2510
- }
2511
-
2512
- /**
2513
- * Find what specific data is missing from executions
2514
- */
2515
- private findMissingTemplateData(
2516
- executions: Array<{path: string; data: IExportTraceServiceRequest}>,
2517
- template: ScenarioTemplate,
2518
- eventTemplates: Map<string, Set<string>>
2519
- ): string[] {
2520
- const missing: string[] = [];
2521
- const allEvents = new Set<string>();
2522
-
2523
- // Collect all events from all executions
2524
- executions.forEach(({ data }) => {
2525
- data.resourceSpans?.forEach((rs) => {
2526
- rs.scopeSpans?.forEach((ss) => {
2527
- ss.spans?.forEach((span) => {
2528
- span.events?.forEach((event) => {
2529
- allEvents.add(event.name);
2530
- });
2531
- if (span.name) {
2532
- allEvents.add(span.name);
2533
- }
2534
- });
2535
- });
2536
- });
2537
- });
2538
-
2539
- // Check what's missing
2540
- for (const [eventName, requiredVars] of eventTemplates.entries()) {
2541
- if (!allEvents.has(eventName)) {
2542
- missing.push(`event "${eventName}"`);
2543
- } else {
2544
- // Event exists, check attributes
2545
- requiredVars.forEach(varName => {
2546
- missing.push(`attribute "${varName}" in event "${eventName}"`);
2547
- });
2548
- }
2549
- }
2550
-
2551
- return missing;
2552
- }
2553
-
2554
- /**
2555
- * Aggregate violations into result
2556
- */
2557
- private aggregateResults(violations: WorkflowViolation[]): WorkflowValidationResult {
2558
- let errorCount = 0;
2559
- let warningCount = 0;
2560
- let fixableCount = 0;
2561
-
2562
- violations.forEach((v) => {
2563
- if (v.severity === 'error') {
2564
- errorCount++;
2565
- } else {
2566
- warningCount++;
2567
- }
2568
- if (v.fixable) {
2569
- fixableCount++;
2570
- }
2571
- });
2572
-
2573
- return {
2574
- violations,
2575
- errorCount,
2576
- warningCount,
2577
- fixableCount,
2578
- };
2579
- }
2580
-
2581
- // ============================================================================
2582
- // Helper Methods
2583
- // ============================================================================
2584
-
2585
- /**
2586
- * Check if a version string is valid semver
2587
- */
2588
- private isValidSemver(version: string): boolean {
2589
- const semverPattern = /^\d+\.\d+\.\d+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$/;
2590
- return semverPattern.test(version);
2591
- }
2592
-
2593
- /**
2594
- * Check if an event name matches any available event (supports globs)
2595
- */
2596
- private matchesEventPattern(eventName: string, availableEvents: string[]): boolean {
2597
- // Exact match only
2598
- return availableEvents.includes(eventName);
2599
- }
2600
-
2601
- /**
2602
- * Extract base event name from event pattern
2603
- *
2604
- * Examples:
2605
- * - "installation.started" -> "installation.started"
2606
- * - "installation.progress[stage=skills_discovered]" -> "installation.progress"
2607
- * - "error.*[severity=high]" -> "error.*"
2608
- */
2609
- private extractBaseEventName(eventPattern: string): string {
2610
- const bracketIndex = eventPattern.indexOf('[');
2611
- if (bracketIndex === -1) {
2612
- return eventPattern;
2613
- }
2614
- return eventPattern.substring(0, bracketIndex);
2615
- }
2616
-
2617
- /**
2618
- * Extract attribute value from event pattern for suggestion
2619
- *
2620
- * Examples:
2621
- * - "installation.progress[stage=skills_discovered]" -> "skills_discovered"
2622
- * - "error.*[severity=high]" -> "high"
2623
- * - "installation.started" -> ""
2624
- */
2625
- private extractAttributeValue(eventPattern: string): string {
2626
- const match = eventPattern.match(/\[.*?=(.*?)\]/);
2627
- if (!match) {
2628
- return '';
2629
- }
2630
- return match[1];
2631
- }
2632
-
2633
- /**
2634
- * Extract attribute references from Handlebars template
2635
- *
2636
- * Parses template strings like:
2637
- * - "{{source}}" -> ["source"]
2638
- * - "{{source.url}}" -> ["source.url"]
2639
- * - "{{#if options.global}}" -> ["options.global"]
2640
- * - "{{#if (eq install.mode 'symlink')}}" -> ["install.mode"]
2641
- *
2642
- * @param template - Handlebars template string
2643
- * @returns Array of attribute paths referenced in the template
2644
- */
2645
- private extractAttributeReferences(template: string): string[] {
2646
- const attributes = new Set<string>();
2647
-
2648
- // Match all Handlebars expressions: {{...}}
2649
- const expressionPattern = /\{\{([^}]+)\}\}/g;
2650
- let match;
2651
-
2652
- while ((match = expressionPattern.exec(template)) !== null) {
2653
- const expression = match[1].trim();
2654
-
2655
- // Skip block helpers closing tags
2656
- if (expression.startsWith('/')) {
2657
- continue;
2658
- }
2659
-
2660
- // Handle block helpers: #if, #each, #unless, etc.
2661
- if (expression.startsWith('#')) {
2662
- // Extract the condition/expression after the helper
2663
- const helperMatch = expression.match(/^#\w+\s+(.+)$/);
2664
- if (helperMatch) {
2665
- this.extractAttributesFromExpression(helperMatch[1], attributes);
2666
- }
2667
- continue;
2668
- }
2669
-
2670
- // Handle regular expressions
2671
- this.extractAttributesFromExpression(expression, attributes);
2672
- }
2673
-
2674
- return Array.from(attributes);
2675
- }
2676
-
2677
- /**
2678
- * Extract attribute references from a single Handlebars expression
2679
- *
2680
- * Handles:
2681
- * - Simple references: source.url
2682
- * - Helper calls: (eq install.mode 'symlink')
2683
- * - Nested expressions
2684
- *
2685
- * @param expression - The expression to parse
2686
- * @param attributes - Set to add found attributes to
2687
- */
2688
- private extractAttributesFromExpression(expression: string, attributes: Set<string>): void {
2689
- // Remove helper parentheses: (eq install.mode 'symlink') -> eq install.mode 'symlink'
2690
- const cleaned = expression.replace(/^\(|\)$/g, '').trim();
2691
-
2692
- // Split on spaces to handle helper arguments
2693
- const parts = cleaned.split(/\s+/);
2694
-
2695
- for (const part of parts) {
2696
- // Skip helper names, string literals, numbers, and boolean literals
2697
- if (
2698
- part.match(/^(if|unless|each|with|eq|ne|lt|gt|lte|gte|and|or|not)$/) ||
2699
- part.match(/^['"].*['"]$/) ||
2700
- part.match(/^\d+$/) ||
2701
- part.match(/^(true|false|null|undefined)$/)
2702
- ) {
2703
- continue;
2704
- }
2705
-
2706
- // Remove any remaining quotes or parentheses
2707
- const cleanPart = part.replace(/['"()]/g, '');
2708
-
2709
- // If it looks like an attribute path (contains letters/dots/underscores)
2710
- if (cleanPart && cleanPart.match(/^[a-zA-Z_][a-zA-Z0-9_.]*$/)) {
2711
- attributes.add(cleanPart);
2712
- }
2713
- }
2714
- }
2715
-
2716
- /**
2717
- * Check if an attribute is an object type
2718
- *
2719
- * @param value - The attribute value to check
2720
- * @returns true if value is a plain object (not array, not null)
2721
- */
2722
- private isObjectType(value: unknown): boolean {
2723
- return (
2724
- typeof value === 'object' &&
2725
- value !== null &&
2726
- !Array.isArray(value) &&
2727
- !(value instanceof Date)
2728
- );
2729
- }
2730
- }
2731
-
2732
- /**
2733
- * Create a validator instance
2734
- */
2735
- export function createWorkflowValidator(fsAdapter: FileSystemAdapter): WorkflowValidator {
2736
- return new WorkflowValidator(fsAdapter);
2737
- }