@principal-ai/subsystems-core 0.29.0 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -110
- package/dist/index.d.ts +7 -75
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -205
- package/dist/index.js.map +1 -1
- package/dist/node.d.ts +5 -61
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +9 -131
- package/dist/node.js.map +1 -1
- package/dist/types/index.d.ts +3 -348
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +3 -19
- package/dist/types/index.js.map +1 -1
- package/dist/types/subsystem-model.d.ts +34 -3
- package/dist/types/subsystem-model.d.ts.map +1 -1
- package/dist/types/subsystem-model.js.map +1 -1
- package/package.json +7 -24
- package/schemas/subsystem-model.schema.json +52 -1
- package/src/index.ts +7 -580
- package/src/node.ts +21 -262
- package/src/types/index.ts +3 -456
- package/src/types/subsystem-model.ts +39 -3
- package/dist/ConfigurationLoader.d.ts +0 -76
- package/dist/ConfigurationLoader.d.ts.map +0 -1
- package/dist/ConfigurationLoader.js +0 -148
- package/dist/ConfigurationLoader.js.map +0 -1
- package/dist/ConfigurationValidator.d.ts +0 -31
- package/dist/ConfigurationValidator.d.ts.map +0 -1
- package/dist/ConfigurationValidator.js +0 -239
- package/dist/ConfigurationValidator.js.map +0 -1
- package/dist/EventProcessor.d.ts +0 -49
- package/dist/EventProcessor.d.ts.map +0 -1
- package/dist/EventProcessor.js +0 -220
- package/dist/EventProcessor.js.map +0 -1
- package/dist/EventRecorderService.d.ts +0 -305
- package/dist/EventRecorderService.d.ts.map +0 -1
- package/dist/EventRecorderService.js +0 -467
- package/dist/EventRecorderService.js.map +0 -1
- package/dist/LibraryLoader.d.ts +0 -65
- package/dist/LibraryLoader.d.ts.map +0 -1
- package/dist/LibraryLoader.js +0 -197
- package/dist/LibraryLoader.js.map +0 -1
- package/dist/PathBasedEventProcessor.d.ts +0 -77
- package/dist/PathBasedEventProcessor.d.ts.map +0 -1
- package/dist/PathBasedEventProcessor.js +0 -136
- package/dist/PathBasedEventProcessor.js.map +0 -1
- package/dist/SessionManager.d.ts +0 -194
- package/dist/SessionManager.d.ts.map +0 -1
- package/dist/SessionManager.js +0 -302
- package/dist/SessionManager.js.map +0 -1
- package/dist/ValidationEngine.d.ts +0 -31
- package/dist/ValidationEngine.d.ts.map +0 -1
- package/dist/ValidationEngine.js +0 -162
- package/dist/ValidationEngine.js.map +0 -1
- package/dist/auxiliary/AuxiliaryManifestValidator.d.ts +0 -41
- package/dist/auxiliary/AuxiliaryManifestValidator.d.ts.map +0 -1
- package/dist/auxiliary/AuxiliaryManifestValidator.js +0 -100
- package/dist/auxiliary/AuxiliaryManifestValidator.js.map +0 -1
- package/dist/auxiliary/index.d.ts +0 -9
- package/dist/auxiliary/index.d.ts.map +0 -1
- package/dist/auxiliary/index.js +0 -14
- package/dist/auxiliary/index.js.map +0 -1
- package/dist/auxiliary/validateAreaScopeDisjoint.d.ts +0 -24
- package/dist/auxiliary/validateAreaScopeDisjoint.d.ts.map +0 -1
- package/dist/auxiliary/validateAreaScopeDisjoint.js +0 -61
- package/dist/auxiliary/validateAreaScopeDisjoint.js.map +0 -1
- package/dist/cli/codegen.d.ts +0 -41
- package/dist/cli/codegen.d.ts.map +0 -1
- package/dist/cli/codegen.js +0 -257
- package/dist/cli/codegen.js.map +0 -1
- package/dist/codegen/type-generator.d.ts +0 -123
- package/dist/codegen/type-generator.d.ts.map +0 -1
- package/dist/codegen/type-generator.js +0 -433
- package/dist/codegen/type-generator.js.map +0 -1
- package/dist/codegen/usage-example.d.ts +0 -103
- package/dist/codegen/usage-example.d.ts.map +0 -1
- package/dist/codegen/usage-example.js +0 -258
- package/dist/codegen/usage-example.js.map +0 -1
- package/dist/dashboard/DashboardValidator.d.ts +0 -85
- package/dist/dashboard/DashboardValidator.d.ts.map +0 -1
- package/dist/dashboard/DashboardValidator.js +0 -775
- package/dist/dashboard/DashboardValidator.js.map +0 -1
- package/dist/dashboard/index.d.ts +0 -6
- package/dist/dashboard/index.d.ts.map +0 -1
- package/dist/dashboard/index.js +0 -10
- package/dist/dashboard/index.js.map +0 -1
- package/dist/discovery/CanvasDiscovery.d.ts +0 -160
- package/dist/discovery/CanvasDiscovery.d.ts.map +0 -1
- package/dist/discovery/CanvasDiscovery.js +0 -865
- package/dist/discovery/CanvasDiscovery.js.map +0 -1
- package/dist/discovery/CanvasFileManifest.d.ts +0 -86
- package/dist/discovery/CanvasFileManifest.d.ts.map +0 -1
- package/dist/discovery/CanvasFileManifest.js +0 -338
- package/dist/discovery/CanvasFileManifest.js.map +0 -1
- package/dist/discovery/LibraryDiscovery.d.ts +0 -108
- package/dist/discovery/LibraryDiscovery.d.ts.map +0 -1
- package/dist/discovery/LibraryDiscovery.js +0 -263
- package/dist/discovery/LibraryDiscovery.js.map +0 -1
- package/dist/discovery/types.d.ts +0 -403
- package/dist/discovery/types.d.ts.map +0 -1
- package/dist/discovery/types.js +0 -6
- package/dist/discovery/types.js.map +0 -1
- package/dist/events/EventsCanvasValidator.d.ts +0 -139
- package/dist/events/EventsCanvasValidator.d.ts.map +0 -1
- package/dist/events/EventsCanvasValidator.js +0 -281
- package/dist/events/EventsCanvasValidator.js.map +0 -1
- package/dist/events/NamespacePathIndex.d.ts +0 -61
- package/dist/events/NamespacePathIndex.d.ts.map +0 -1
- package/dist/events/NamespacePathIndex.js +0 -74
- package/dist/events/NamespacePathIndex.js.map +0 -1
- package/dist/events/OtelEventPathsValidator.d.ts +0 -75
- package/dist/events/OtelEventPathsValidator.d.ts.map +0 -1
- package/dist/events/OtelEventPathsValidator.js +0 -126
- package/dist/events/OtelEventPathsValidator.js.map +0 -1
- package/dist/events/ScopeEventsValidator.d.ts +0 -68
- package/dist/events/ScopeEventsValidator.d.ts.map +0 -1
- package/dist/events/ScopeEventsValidator.js +0 -91
- package/dist/events/ScopeEventsValidator.js.map +0 -1
- package/dist/events/index.d.ts +0 -9
- package/dist/events/index.d.ts.map +0 -1
- package/dist/events/index.js +0 -12
- package/dist/events/index.js.map +0 -1
- package/dist/events/path-helpers.d.ts +0 -33
- package/dist/events/path-helpers.d.ts.map +0 -1
- package/dist/events/path-helpers.js +0 -59
- package/dist/events/path-helpers.js.map +0 -1
- package/dist/execution/ExecutionLoader.d.ts +0 -135
- package/dist/execution/ExecutionLoader.d.ts.map +0 -1
- package/dist/execution/ExecutionLoader.js +0 -266
- package/dist/execution/ExecutionLoader.js.map +0 -1
- package/dist/execution/ExecutionValidator.d.ts +0 -94
- package/dist/execution/ExecutionValidator.d.ts.map +0 -1
- package/dist/execution/ExecutionValidator.js +0 -503
- package/dist/execution/ExecutionValidator.js.map +0 -1
- package/dist/generated/graph-converter-execution.types.d.ts +0 -177
- package/dist/generated/graph-converter-execution.types.d.ts.map +0 -1
- package/dist/generated/graph-converter-execution.types.js +0 -12
- package/dist/generated/graph-converter-execution.types.js.map +0 -1
- package/dist/helpers/GraphInstrumentationHelper.d.ts +0 -93
- package/dist/helpers/GraphInstrumentationHelper.d.ts.map +0 -1
- package/dist/helpers/GraphInstrumentationHelper.js +0 -252
- package/dist/helpers/GraphInstrumentationHelper.js.map +0 -1
- package/dist/matchers/SpanMatcher.d.ts +0 -107
- package/dist/matchers/SpanMatcher.d.ts.map +0 -1
- package/dist/matchers/SpanMatcher.js +0 -274
- package/dist/matchers/SpanMatcher.js.map +0 -1
- package/dist/orchestration/TraceOrchestrator.d.ts +0 -71
- package/dist/orchestration/TraceOrchestrator.d.ts.map +0 -1
- package/dist/orchestration/TraceOrchestrator.js +0 -416
- package/dist/orchestration/TraceOrchestrator.js.map +0 -1
- package/dist/parsers/OtlpTraceParser.d.ts +0 -131
- package/dist/parsers/OtlpTraceParser.d.ts.map +0 -1
- package/dist/parsers/OtlpTraceParser.js +0 -310
- package/dist/parsers/OtlpTraceParser.js.map +0 -1
- package/dist/registry/CompositeRegistry.d.ts +0 -71
- package/dist/registry/CompositeRegistry.d.ts.map +0 -1
- package/dist/registry/CompositeRegistry.js +0 -113
- package/dist/registry/CompositeRegistry.js.map +0 -1
- package/dist/registry/EventRegistry.d.ts +0 -79
- package/dist/registry/EventRegistry.d.ts.map +0 -1
- package/dist/registry/EventRegistry.js +0 -165
- package/dist/registry/EventRegistry.js.map +0 -1
- package/dist/registry/LocalRegistry.d.ts +0 -111
- package/dist/registry/LocalRegistry.d.ts.map +0 -1
- package/dist/registry/LocalRegistry.js +0 -335
- package/dist/registry/LocalRegistry.js.map +0 -1
- package/dist/registry/RemoteRegistry.d.ts +0 -122
- package/dist/registry/RemoteRegistry.d.ts.map +0 -1
- package/dist/registry/RemoteRegistry.js +0 -399
- package/dist/registry/RemoteRegistry.js.map +0 -1
- package/dist/registry/VersionRegistry.d.ts +0 -51
- package/dist/registry/VersionRegistry.d.ts.map +0 -1
- package/dist/registry/VersionRegistry.js +0 -105
- package/dist/registry/VersionRegistry.js.map +0 -1
- package/dist/rules/config.d.ts +0 -57
- package/dist/rules/config.d.ts.map +0 -1
- package/dist/rules/config.js +0 -382
- package/dist/rules/config.js.map +0 -1
- package/dist/rules/engine.d.ts +0 -70
- package/dist/rules/engine.d.ts.map +0 -1
- package/dist/rules/engine.js +0 -373
- package/dist/rules/engine.js.map +0 -1
- package/dist/rules/implementations/connection-type-references.d.ts +0 -7
- package/dist/rules/implementations/connection-type-references.d.ts.map +0 -1
- package/dist/rules/implementations/connection-type-references.js +0 -107
- package/dist/rules/implementations/connection-type-references.js.map +0 -1
- package/dist/rules/implementations/dead-end-states.d.ts +0 -17
- package/dist/rules/implementations/dead-end-states.d.ts.map +0 -1
- package/dist/rules/implementations/dead-end-states.js +0 -75
- package/dist/rules/implementations/dead-end-states.js.map +0 -1
- package/dist/rules/implementations/index.d.ts +0 -23
- package/dist/rules/implementations/index.d.ts.map +0 -1
- package/dist/rules/implementations/index.js +0 -73
- package/dist/rules/implementations/index.js.map +0 -1
- package/dist/rules/implementations/minimum-node-sources.d.ts +0 -24
- package/dist/rules/implementations/minimum-node-sources.d.ts.map +0 -1
- package/dist/rules/implementations/minimum-node-sources.js +0 -59
- package/dist/rules/implementations/minimum-node-sources.js.map +0 -1
- package/dist/rules/implementations/no-unknown-fields.d.ts +0 -7
- package/dist/rules/implementations/no-unknown-fields.d.ts.map +0 -1
- package/dist/rules/implementations/no-unknown-fields.js +0 -216
- package/dist/rules/implementations/no-unknown-fields.js.map +0 -1
- package/dist/rules/implementations/orphaned-edge-types.d.ts +0 -7
- package/dist/rules/implementations/orphaned-edge-types.d.ts.map +0 -1
- package/dist/rules/implementations/orphaned-edge-types.js +0 -50
- package/dist/rules/implementations/orphaned-edge-types.js.map +0 -1
- package/dist/rules/implementations/orphaned-node-types.d.ts +0 -7
- package/dist/rules/implementations/orphaned-node-types.d.ts.map +0 -1
- package/dist/rules/implementations/orphaned-node-types.js +0 -53
- package/dist/rules/implementations/orphaned-node-types.js.map +0 -1
- package/dist/rules/implementations/required-metadata.d.ts +0 -7
- package/dist/rules/implementations/required-metadata.d.ts.map +0 -1
- package/dist/rules/implementations/required-metadata.js +0 -48
- package/dist/rules/implementations/required-metadata.js.map +0 -1
- package/dist/rules/implementations/state-transition-references.d.ts +0 -7
- package/dist/rules/implementations/state-transition-references.d.ts.map +0 -1
- package/dist/rules/implementations/state-transition-references.js +0 -138
- package/dist/rules/implementations/state-transition-references.js.map +0 -1
- package/dist/rules/implementations/unreachable-states.d.ts +0 -7
- package/dist/rules/implementations/unreachable-states.d.ts.map +0 -1
- package/dist/rules/implementations/unreachable-states.js +0 -83
- package/dist/rules/implementations/unreachable-states.js.map +0 -1
- package/dist/rules/implementations/valid-action-patterns.d.ts +0 -17
- package/dist/rules/implementations/valid-action-patterns.d.ts.map +0 -1
- package/dist/rules/implementations/valid-action-patterns.js +0 -112
- package/dist/rules/implementations/valid-action-patterns.js.map +0 -1
- package/dist/rules/implementations/valid-color-format.d.ts +0 -7
- package/dist/rules/implementations/valid-color-format.d.ts.map +0 -1
- package/dist/rules/implementations/valid-color-format.js +0 -94
- package/dist/rules/implementations/valid-color-format.js.map +0 -1
- package/dist/rules/implementations/valid-edge-types.d.ts +0 -7
- package/dist/rules/implementations/valid-edge-types.d.ts.map +0 -1
- package/dist/rules/implementations/valid-edge-types.js +0 -248
- package/dist/rules/implementations/valid-edge-types.js.map +0 -1
- package/dist/rules/implementations/valid-node-types.d.ts +0 -7
- package/dist/rules/implementations/valid-node-types.d.ts.map +0 -1
- package/dist/rules/implementations/valid-node-types.js +0 -187
- package/dist/rules/implementations/valid-node-types.js.map +0 -1
- package/dist/rules/index.d.ts +0 -28
- package/dist/rules/index.d.ts.map +0 -1
- package/dist/rules/index.js +0 -70
- package/dist/rules/index.js.map +0 -1
- package/dist/rules/types.d.ts +0 -309
- package/dist/rules/types.d.ts.map +0 -1
- package/dist/rules/types.js +0 -40
- package/dist/rules/types.js.map +0 -1
- package/dist/scopes/ScopePathIndex.d.ts +0 -56
- package/dist/scopes/ScopePathIndex.d.ts.map +0 -1
- package/dist/scopes/ScopePathIndex.js +0 -67
- package/dist/scopes/ScopePathIndex.js.map +0 -1
- package/dist/scopes/ScopesCanvasValidator.d.ts +0 -76
- package/dist/scopes/ScopesCanvasValidator.d.ts.map +0 -1
- package/dist/scopes/ScopesCanvasValidator.js +0 -221
- package/dist/scopes/ScopesCanvasValidator.js.map +0 -1
- package/dist/scopes/index.d.ts +0 -12
- package/dist/scopes/index.d.ts.map +0 -1
- package/dist/scopes/index.js +0 -30
- package/dist/scopes/index.js.map +0 -1
- package/dist/scopes/utils.d.ts +0 -85
- package/dist/scopes/utils.d.ts.map +0 -1
- package/dist/scopes/utils.js +0 -143
- package/dist/scopes/utils.js.map +0 -1
- package/dist/scopes/validateScopeNamespaceNesting.d.ts +0 -38
- package/dist/scopes/validateScopeNamespaceNesting.d.ts.map +0 -1
- package/dist/scopes/validateScopeNamespaceNesting.js +0 -69
- package/dist/scopes/validateScopeNamespaceNesting.js.map +0 -1
- package/dist/spans/index.d.ts +0 -12
- package/dist/spans/index.d.ts.map +0 -1
- package/dist/spans/index.js +0 -25
- package/dist/spans/index.js.map +0 -1
- package/dist/spans/utils.d.ts +0 -70
- package/dist/spans/utils.d.ts.map +0 -1
- package/dist/spans/utils.js +0 -205
- package/dist/spans/utils.js.map +0 -1
- package/dist/storyboard/builder.d.ts +0 -135
- package/dist/storyboard/builder.d.ts.map +0 -1
- package/dist/storyboard/builder.js +0 -235
- package/dist/storyboard/builder.js.map +0 -1
- package/dist/storyboard/index.d.ts +0 -9
- package/dist/storyboard/index.d.ts.map +0 -1
- package/dist/storyboard/index.js +0 -18
- package/dist/storyboard/index.js.map +0 -1
- package/dist/storyboard/types.d.ts +0 -101
- package/dist/storyboard/types.d.ts.map +0 -1
- package/dist/storyboard/types.js +0 -10
- package/dist/storyboard/types.js.map +0 -1
- package/dist/telemetry/coverage.d.ts +0 -92
- package/dist/telemetry/coverage.d.ts.map +0 -1
- package/dist/telemetry/coverage.js +0 -335
- package/dist/telemetry/coverage.js.map +0 -1
- package/dist/telemetry/event-validator.d.ts +0 -86
- package/dist/telemetry/event-validator.d.ts.map +0 -1
- package/dist/telemetry/event-validator.js +0 -257
- package/dist/telemetry/event-validator.js.map +0 -1
- package/dist/types/audit.d.ts +0 -236
- package/dist/types/audit.d.ts.map +0 -1
- package/dist/types/audit.js +0 -53
- package/dist/types/audit.js.map +0 -1
- package/dist/types/auxiliary.d.ts +0 -54
- package/dist/types/auxiliary.d.ts.map +0 -1
- package/dist/types/auxiliary.js +0 -27
- package/dist/types/auxiliary.js.map +0 -1
- package/dist/types/canvas-scope.d.ts +0 -156
- package/dist/types/canvas-scope.d.ts.map +0 -1
- package/dist/types/canvas-scope.js +0 -62
- package/dist/types/canvas-scope.js.map +0 -1
- package/dist/types/canvas.d.ts +0 -1138
- package/dist/types/canvas.d.ts.map +0 -1
- package/dist/types/canvas.js +0 -178
- package/dist/types/canvas.js.map +0 -1
- package/dist/types/dashboard.d.ts +0 -170
- package/dist/types/dashboard.d.ts.map +0 -1
- package/dist/types/dashboard.js +0 -14
- package/dist/types/dashboard.js.map +0 -1
- package/dist/types/library.d.ts +0 -227
- package/dist/types/library.d.ts.map +0 -1
- package/dist/types/library.js +0 -16
- package/dist/types/library.js.map +0 -1
- package/dist/types/otel.d.ts +0 -465
- package/dist/types/otel.d.ts.map +0 -1
- package/dist/types/otel.js +0 -163
- package/dist/types/otel.js.map +0 -1
- package/dist/types/path-based-config.d.ts +0 -235
- package/dist/types/path-based-config.d.ts.map +0 -1
- package/dist/types/path-based-config.js +0 -11
- package/dist/types/path-based-config.js.map +0 -1
- package/dist/types/registered-trace.d.ts +0 -337
- package/dist/types/registered-trace.d.ts.map +0 -1
- package/dist/types/registered-trace.js +0 -16
- package/dist/types/registered-trace.js.map +0 -1
- package/dist/types/resource-match.d.ts +0 -207
- package/dist/types/resource-match.d.ts.map +0 -1
- package/dist/types/resource-match.js +0 -74
- package/dist/types/resource-match.js.map +0 -1
- package/dist/types/version-registry.d.ts +0 -137
- package/dist/types/version-registry.d.ts.map +0 -1
- package/dist/types/version-registry.js +0 -10
- package/dist/types/version-registry.js.map +0 -1
- package/dist/utils/CanvasConverter.d.ts +0 -148
- package/dist/utils/CanvasConverter.d.ts.map +0 -1
- package/dist/utils/CanvasConverter.js +0 -530
- package/dist/utils/CanvasConverter.js.map +0 -1
- package/dist/utils/GraphConverter.d.ts +0 -18
- package/dist/utils/GraphConverter.d.ts.map +0 -1
- package/dist/utils/GraphConverter.js +0 -80
- package/dist/utils/GraphConverter.js.map +0 -1
- package/dist/utils/PathMatcher.d.ts +0 -55
- package/dist/utils/PathMatcher.d.ts.map +0 -1
- package/dist/utils/PathMatcher.js +0 -178
- package/dist/utils/PathMatcher.js.map +0 -1
- package/dist/utils/TraceToCanvas.d.ts +0 -83
- package/dist/utils/TraceToCanvas.d.ts.map +0 -1
- package/dist/utils/TraceToCanvas.js +0 -336
- package/dist/utils/TraceToCanvas.js.map +0 -1
- package/dist/utils/YamlParser.d.ts +0 -36
- package/dist/utils/YamlParser.d.ts.map +0 -1
- package/dist/utils/YamlParser.js +0 -92
- package/dist/utils/YamlParser.js.map +0 -1
- package/dist/utils/registeredTraceHelpers.d.ts +0 -76
- package/dist/utils/registeredTraceHelpers.d.ts.map +0 -1
- package/dist/utils/registeredTraceHelpers.js +0 -203
- package/dist/utils/registeredTraceHelpers.js.map +0 -1
- package/dist/utils/traceAggregation.d.ts +0 -25
- package/dist/utils/traceAggregation.d.ts.map +0 -1
- package/dist/utils/traceAggregation.js +0 -140
- package/dist/utils/traceAggregation.js.map +0 -1
- package/dist/validation/OtelCanvasValidator.d.ts +0 -40
- package/dist/validation/OtelCanvasValidator.d.ts.map +0 -1
- package/dist/validation/OtelCanvasValidator.js +0 -59
- package/dist/validation/OtelCanvasValidator.js.map +0 -1
- package/dist/validation/index.d.ts +0 -8
- package/dist/validation/index.d.ts.map +0 -1
- package/dist/validation/index.js +0 -13
- package/dist/validation/index.js.map +0 -1
- package/dist/validation/libraryValidation.d.ts +0 -53
- package/dist/validation/libraryValidation.d.ts.map +0 -1
- package/dist/validation/libraryValidation.js +0 -115
- package/dist/validation/libraryValidation.js.map +0 -1
- package/dist/workflow/edge-derivation.d.ts +0 -111
- package/dist/workflow/edge-derivation.d.ts.map +0 -1
- package/dist/workflow/edge-derivation.js +0 -174
- package/dist/workflow/edge-derivation.js.map +0 -1
- package/dist/workflow/edge-validation.d.ts +0 -170
- package/dist/workflow/edge-validation.d.ts.map +0 -1
- package/dist/workflow/edge-validation.js +0 -332
- package/dist/workflow/edge-validation.js.map +0 -1
- package/dist/workflow/example.d.ts +0 -11
- package/dist/workflow/example.d.ts.map +0 -1
- package/dist/workflow/example.js +0 -310
- package/dist/workflow/example.js.map +0 -1
- package/dist/workflow/index.d.ts +0 -20
- package/dist/workflow/index.d.ts.map +0 -1
- package/dist/workflow/index.js +0 -49
- package/dist/workflow/index.js.map +0 -1
- package/dist/workflow/scenario-matcher.d.ts +0 -63
- package/dist/workflow/scenario-matcher.d.ts.map +0 -1
- package/dist/workflow/scenario-matcher.js +0 -215
- package/dist/workflow/scenario-matcher.js.map +0 -1
- package/dist/workflow/template-parser.d.ts +0 -73
- package/dist/workflow/template-parser.d.ts.map +0 -1
- package/dist/workflow/template-parser.js +0 -280
- package/dist/workflow/template-parser.js.map +0 -1
- package/dist/workflow/template-renderer.d.ts +0 -45
- package/dist/workflow/template-renderer.d.ts.map +0 -1
- package/dist/workflow/template-renderer.js +0 -401
- package/dist/workflow/template-renderer.js.map +0 -1
- package/dist/workflow/types.d.ts +0 -393
- package/dist/workflow/types.d.ts.map +0 -1
- package/dist/workflow/types.js +0 -40
- package/dist/workflow/types.js.map +0 -1
- package/dist/workflow/validator.d.ts +0 -363
- package/dist/workflow/validator.d.ts.map +0 -1
- package/dist/workflow/validator.js +0 -2283
- package/dist/workflow/validator.js.map +0 -1
- package/schemas/auxiliary.manifest.schema.json +0 -56
- package/src/ConfigurationLoader.test.ts +0 -466
- package/src/ConfigurationLoader.ts +0 -184
- package/src/ConfigurationValidator.test.ts +0 -200
- package/src/ConfigurationValidator.ts +0 -273
- package/src/EventProcessor.test.ts +0 -405
- package/src/EventProcessor.ts +0 -256
- package/src/EventRecorderService.test.ts +0 -543
- package/src/EventRecorderService.ts +0 -741
- package/src/LibraryLoader.ts +0 -194
- package/src/PathBasedEventProcessor.test.ts +0 -336
- package/src/PathBasedEventProcessor.ts +0 -196
- package/src/SessionManager.test.ts +0 -424
- package/src/SessionManager.ts +0 -471
- package/src/ValidationEngine.test.ts +0 -520
- package/src/ValidationEngine.ts +0 -200
- package/src/auxiliary/AuxiliaryManifestValidator.test.ts +0 -93
- package/src/auxiliary/AuxiliaryManifestValidator.ts +0 -132
- package/src/auxiliary/index.ts +0 -18
- package/src/auxiliary/validateAreaScopeDisjoint.test.ts +0 -85
- package/src/auxiliary/validateAreaScopeDisjoint.ts +0 -72
- package/src/cli/codegen.ts +0 -286
- package/src/codegen/type-generator.test.ts +0 -219
- package/src/codegen/type-generator.ts +0 -560
- package/src/codegen/usage-example.ts +0 -357
- package/src/dashboard/DashboardValidator.ts +0 -904
- package/src/dashboard/index.ts +0 -14
- package/src/discovery/CanvasDiscovery.test.ts +0 -691
- package/src/discovery/CanvasDiscovery.ts +0 -1114
- package/src/discovery/CanvasFileManifest.test.ts +0 -521
- package/src/discovery/CanvasFileManifest.ts +0 -419
- package/src/discovery/LibraryDiscovery.test.ts +0 -329
- package/src/discovery/LibraryDiscovery.ts +0 -386
- package/src/discovery/types.ts +0 -453
- package/src/events/EventsCanvasValidator.test.ts +0 -191
- package/src/events/EventsCanvasValidator.ts +0 -428
- package/src/events/NamespacePathIndex.test.ts +0 -97
- package/src/events/NamespacePathIndex.ts +0 -100
- package/src/events/OtelEventPathsValidator.test.ts +0 -243
- package/src/events/OtelEventPathsValidator.ts +0 -191
- package/src/events/ScopeEventsValidator.test.ts +0 -173
- package/src/events/ScopeEventsValidator.ts +0 -158
- package/src/events/index.ts +0 -27
- package/src/events/path-helpers.ts +0 -53
- package/src/execution/ExecutionLoader.test.ts +0 -378
- package/src/execution/ExecutionLoader.ts +0 -331
- package/src/execution/ExecutionValidator.test.ts +0 -503
- package/src/execution/ExecutionValidator.ts +0 -617
- package/src/generated/graph-converter-execution.types.ts +0 -193
- package/src/helpers/GraphInstrumentationHelper.test.ts +0 -364
- package/src/helpers/GraphInstrumentationHelper.ts +0 -323
- package/src/matchers/SpanMatcher.test.ts +0 -1105
- package/src/matchers/SpanMatcher.ts +0 -381
- package/src/orchestration/TraceOrchestrator.ts +0 -560
- package/src/orchestration/__tests__/TraceOrchestrator.test.ts +0 -1098
- package/src/parsers/OtlpTraceParser.ts +0 -401
- package/src/parsers/__tests__/OtlpTraceParser.test.ts +0 -197
- package/src/registry/CompositeRegistry.ts +0 -140
- package/src/registry/EventRegistry.test.ts +0 -387
- package/src/registry/EventRegistry.ts +0 -191
- package/src/registry/LocalRegistry.ts +0 -416
- package/src/registry/RemoteRegistry.ts +0 -499
- package/src/registry/VersionRegistry.test.ts +0 -223
- package/src/registry/VersionRegistry.ts +0 -121
- package/src/rules/config.test.ts +0 -277
- package/src/rules/config.ts +0 -450
- package/src/rules/engine.test.ts +0 -331
- package/src/rules/engine.ts +0 -448
- package/src/rules/implementations/connection-type-references.ts +0 -117
- package/src/rules/implementations/dead-end-states.ts +0 -101
- package/src/rules/implementations/index.ts +0 -66
- package/src/rules/implementations/minimum-node-sources.ts +0 -84
- package/src/rules/implementations/no-unknown-fields.ts +0 -372
- package/src/rules/implementations/orphaned-edge-types.ts +0 -55
- package/src/rules/implementations/orphaned-node-types.ts +0 -58
- package/src/rules/implementations/required-metadata.ts +0 -52
- package/src/rules/implementations/state-transition-references.ts +0 -155
- package/src/rules/implementations/unreachable-states.ts +0 -94
- package/src/rules/implementations/valid-action-patterns.ts +0 -140
- package/src/rules/implementations/valid-color-format.ts +0 -152
- package/src/rules/implementations/valid-edge-types.ts +0 -273
- package/src/rules/implementations/valid-node-types.ts +0 -200
- package/src/rules/index.ts +0 -92
- package/src/rules/types.ts +0 -426
- package/src/scopes/ScopePathIndex.test.ts +0 -94
- package/src/scopes/ScopePathIndex.ts +0 -89
- package/src/scopes/ScopesCanvasValidator.test.ts +0 -127
- package/src/scopes/ScopesCanvasValidator.ts +0 -305
- package/src/scopes/__tests__/utils.test.ts +0 -383
- package/src/scopes/index.ts +0 -42
- package/src/scopes/utils.ts +0 -182
- package/src/scopes/validateScopeNamespaceNesting.test.ts +0 -127
- package/src/scopes/validateScopeNamespaceNesting.ts +0 -88
- package/src/spans/index.ts +0 -24
- package/src/spans/utils.ts +0 -249
- package/src/storyboard/builder.ts +0 -308
- package/src/storyboard/index.ts +0 -26
- package/src/storyboard/types.ts +0 -117
- package/src/telemetry/coverage.ts +0 -480
- package/src/telemetry/event-validator.ts +0 -312
- package/src/types/audit.ts +0 -314
- package/src/types/auxiliary.ts +0 -63
- package/src/types/canvas-scope.ts +0 -190
- package/src/types/canvas.test.ts +0 -199
- package/src/types/canvas.ts +0 -1395
- package/src/types/dashboard.ts +0 -257
- package/src/types/library.ts +0 -283
- package/src/types/otel.test.ts +0 -219
- package/src/types/otel.ts +0 -620
- package/src/types/path-based-config.ts +0 -285
- package/src/types/registered-trace.ts +0 -432
- package/src/types/resource-match.ts +0 -263
- package/src/types/version-registry.ts +0 -150
- package/src/utils/CanvasConverter.ts +0 -670
- package/src/utils/GraphConverter.test.ts +0 -322
- package/src/utils/GraphConverter.ts +0 -84
- package/src/utils/PathMatcher.test.ts +0 -139
- package/src/utils/PathMatcher.ts +0 -185
- package/src/utils/TraceToCanvas.test.ts +0 -310
- package/src/utils/TraceToCanvas.ts +0 -510
- package/src/utils/YamlParser.ts +0 -77
- package/src/utils/registeredTraceHelpers.ts +0 -251
- package/src/utils/traceAggregation.test.ts +0 -385
- package/src/utils/traceAggregation.ts +0 -173
- package/src/validation/OtelCanvasValidator.ts +0 -86
- package/src/validation/index.ts +0 -17
- package/src/validation/libraryValidation.ts +0 -206
- package/src/workflow/README.md +0 -408
- package/src/workflow/__tests__/edge-derivation.test.ts +0 -449
- package/src/workflow/__tests__/edge-validation.test.ts +0 -417
- package/src/workflow/__tests__/scenario-matcher.test.ts +0 -225
- package/src/workflow/__tests__/template-parser.test.ts +0 -276
- package/src/workflow/__tests__/template-renderer.test.ts +0 -509
- package/src/workflow/__tests__/types.test.ts +0 -187
- package/src/workflow/__tests__/validator.test.ts +0 -3225
- package/src/workflow/edge-derivation.ts +0 -265
- package/src/workflow/edge-validation.ts +0 -503
- package/src/workflow/example.ts +0 -330
- package/src/workflow/index.ts +0 -86
- package/src/workflow/scenario-matcher.ts +0 -253
- package/src/workflow/template-parser.ts +0 -350
- package/src/workflow/template-renderer.ts +0 -453
- package/src/workflow/types.ts +0 -511
- package/src/workflow/validator.ts +0 -2737
|
@@ -1,2283 +0,0 @@
|
|
|
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
|