@principal-ai/subsystems-core 0.29.0

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