@fabricorg/databricks-bdd 0.3.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +41 -3
  2. package/dist/actions-CuXXKUqe.d.ts +92 -0
  3. package/dist/actions-CxfxhBZh.d.cts +92 -0
  4. package/dist/actions.cjs +14 -0
  5. package/dist/actions.cjs.map +1 -0
  6. package/dist/actions.d.cts +6 -0
  7. package/dist/actions.d.ts +6 -0
  8. package/dist/actions.js +3 -0
  9. package/dist/actions.js.map +1 -0
  10. package/dist/cardinality.cjs +24 -0
  11. package/dist/cardinality.cjs.map +1 -0
  12. package/dist/cardinality.d.cts +19 -0
  13. package/dist/cardinality.d.ts +19 -0
  14. package/dist/cardinality.js +3 -0
  15. package/dist/cardinality.js.map +1 -0
  16. package/dist/{chunk-355WDWP3.js → chunk-3XDMBHFP.js} +101 -3
  17. package/dist/chunk-3XDMBHFP.js.map +1 -0
  18. package/dist/chunk-6JC65RH2.js +11 -0
  19. package/dist/chunk-6JC65RH2.js.map +1 -0
  20. package/dist/chunk-HTKRJLVX.js +70 -0
  21. package/dist/chunk-HTKRJLVX.js.map +1 -0
  22. package/dist/chunk-K5LJ7WSW.js +22 -0
  23. package/dist/chunk-K5LJ7WSW.js.map +1 -0
  24. package/dist/index.cjs +199 -0
  25. package/dist/index.cjs.map +1 -1
  26. package/dist/index.d.cts +24 -65
  27. package/dist/index.d.ts +24 -65
  28. package/dist/index.js +4 -1
  29. package/dist/index.js.map +1 -1
  30. package/dist/scoped-state.cjs +74 -0
  31. package/dist/scoped-state.cjs.map +1 -0
  32. package/dist/scoped-state.d.cts +30 -0
  33. package/dist/scoped-state.d.ts +30 -0
  34. package/dist/scoped-state.js +3 -0
  35. package/dist/scoped-state.js.map +1 -0
  36. package/dist/steps.cjs +177 -3
  37. package/dist/steps.cjs.map +1 -1
  38. package/dist/steps.js +10 -4
  39. package/dist/steps.js.map +1 -1
  40. package/package.json +31 -1
  41. package/dist/chunk-355WDWP3.js.map +0 -1
@@ -0,0 +1,70 @@
1
+ // src/scoped-state.ts
2
+ var SharedStateRegistry = class {
3
+ runValues = /* @__PURE__ */ new Map();
4
+ featureValues = /* @__PURE__ */ new Map();
5
+ run() {
6
+ return this.runValues;
7
+ }
8
+ feature(featureUri) {
9
+ if (!featureUri) throw new Error("Feature state requires a feature URI");
10
+ let values = this.featureValues.get(featureUri);
11
+ if (!values) {
12
+ values = /* @__PURE__ */ new Map();
13
+ this.featureValues.set(featureUri, values);
14
+ }
15
+ return values;
16
+ }
17
+ clear() {
18
+ this.runValues.clear();
19
+ this.featureValues.clear();
20
+ }
21
+ };
22
+ var ScopedState = class {
23
+ constructor(runValues, featureValues) {
24
+ this.runValues = runValues;
25
+ this.featureValues = featureValues;
26
+ }
27
+ runValues;
28
+ featureValues;
29
+ scenarioValues = /* @__PURE__ */ new Map();
30
+ set(scope, key, value) {
31
+ this.values(scope).set(validateKey(key), value);
32
+ return value;
33
+ }
34
+ get(key) {
35
+ const validKey = validateKey(key);
36
+ if (this.scenarioValues.has(validKey)) return this.scenarioValues.get(validKey);
37
+ if (this.featureValues.has(validKey)) return this.featureValues.get(validKey);
38
+ return this.runValues.get(validKey);
39
+ }
40
+ require(key) {
41
+ const value = this.get(key);
42
+ if (value === void 0) throw new Error(`Required BDD state '${key}' is not set`);
43
+ return value;
44
+ }
45
+ has(key) {
46
+ const validKey = validateKey(key);
47
+ return this.scenarioValues.has(validKey) || this.featureValues.has(validKey) || this.runValues.has(validKey);
48
+ }
49
+ delete(scope, key) {
50
+ return this.values(scope).delete(validateKey(key));
51
+ }
52
+ clearScenario() {
53
+ this.scenarioValues.clear();
54
+ }
55
+ values(scope) {
56
+ if (scope === "run") return this.runValues;
57
+ if (scope === "feature") return this.featureValues;
58
+ return this.scenarioValues;
59
+ }
60
+ };
61
+ function validateKey(key) {
62
+ const trimmed = key.trim();
63
+ if (!trimmed) throw new Error("BDD state key must not be empty");
64
+ return trimmed;
65
+ }
66
+ var sharedState = new SharedStateRegistry();
67
+
68
+ export { ScopedState, SharedStateRegistry, sharedState };
69
+ //# sourceMappingURL=chunk-HTKRJLVX.js.map
70
+ //# sourceMappingURL=chunk-HTKRJLVX.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/scoped-state.ts"],"names":[],"mappings":"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","file":"chunk-HTKRJLVX.js","sourcesContent":["export type StateScope = 'run' | 'feature' | 'scenario';\n\ntype StateValues = Map<string, unknown>;\n\n/**\n * Process-local state backing Behave-style run and feature context layers.\n * Scenario values stay on the scenario World and are never shared.\n */\nexport class SharedStateRegistry {\n private readonly runValues: StateValues = new Map();\n private readonly featureValues = new Map<string, StateValues>();\n\n run(): StateValues {\n return this.runValues;\n }\n\n feature(featureUri: string): StateValues {\n if (!featureUri) throw new Error('Feature state requires a feature URI');\n let values = this.featureValues.get(featureUri);\n if (!values) {\n values = new Map();\n this.featureValues.set(featureUri, values);\n }\n return values;\n }\n\n clear(): void {\n this.runValues.clear();\n this.featureValues.clear();\n }\n}\n\n/** Layered state lookup: scenario overrides feature, which overrides run. */\nexport class ScopedState {\n private readonly scenarioValues: StateValues = new Map();\n\n constructor(\n private readonly runValues: StateValues,\n private readonly featureValues: StateValues,\n ) {}\n\n set<T>(scope: StateScope, key: string, value: T): T {\n this.values(scope).set(validateKey(key), value);\n return value;\n }\n\n get<T>(key: string): T | undefined {\n const validKey = validateKey(key);\n if (this.scenarioValues.has(validKey)) return this.scenarioValues.get(validKey) as T;\n if (this.featureValues.has(validKey)) return this.featureValues.get(validKey) as T;\n return this.runValues.get(validKey) as T | undefined;\n }\n\n require<T>(key: string): T {\n const value = this.get<T>(key);\n if (value === undefined) throw new Error(`Required BDD state '${key}' is not set`);\n return value;\n }\n\n has(key: string): boolean {\n const validKey = validateKey(key);\n return (\n this.scenarioValues.has(validKey) ||\n this.featureValues.has(validKey) ||\n this.runValues.has(validKey)\n );\n }\n\n delete(scope: StateScope, key: string): boolean {\n return this.values(scope).delete(validateKey(key));\n }\n\n clearScenario(): void {\n this.scenarioValues.clear();\n }\n\n private values(scope: StateScope): StateValues {\n if (scope === 'run') return this.runValues;\n if (scope === 'feature') return this.featureValues;\n return this.scenarioValues;\n }\n}\n\nfunction validateKey(key: string): string {\n const trimmed = key.trim();\n if (!trimmed) throw new Error('BDD state key must not be empty');\n return trimmed;\n}\n\nexport const sharedState = new SharedStateRegistry();\n"]}
@@ -0,0 +1,22 @@
1
+ // src/cardinality.ts
2
+ function parseCardinalityField(value, cardinality, transform, options = {}) {
3
+ const fieldName = options.fieldName ?? "field";
4
+ const trimmed = value.trim();
5
+ const items = trimmed ? trimmed.split(options.separator ?? /\s*,\s*/).map((item) => item.trim()) : [];
6
+ if (items.some((item) => !item)) {
7
+ throw new Error(`${fieldName} contains an empty value`);
8
+ }
9
+ if (cardinality === "?" && items.length > 1) {
10
+ throw new Error(`${fieldName} expects zero or one value, received ${items.length}`);
11
+ }
12
+ if (cardinality === "+" && items.length === 0) {
13
+ throw new Error(`${fieldName} expects one or more values`);
14
+ }
15
+ const parsed = items.map(transform);
16
+ if (cardinality === "?") return parsed[0];
17
+ return parsed;
18
+ }
19
+
20
+ export { parseCardinalityField };
21
+ //# sourceMappingURL=chunk-K5LJ7WSW.js.map
22
+ //# sourceMappingURL=chunk-K5LJ7WSW.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cardinality.ts"],"names":[],"mappings":";AAmBO,SAAS,sBACd,KAAA,EACA,WAAA,EACA,SAAA,EACA,OAAA,GAA8B,EAAC,EACN;AACzB,EAAA,MAAM,SAAA,GAAY,QAAQ,SAAA,IAAa,OAAA;AACvC,EAAA,MAAM,OAAA,GAAU,MAAM,IAAA,EAAK;AAC3B,EAAA,MAAM,KAAA,GAAQ,OAAA,GACV,OAAA,CAAQ,KAAA,CAAM,QAAQ,SAAA,IAAa,SAAS,CAAA,CAAE,GAAA,CAAI,CAAC,IAAA,KAAS,IAAA,CAAK,IAAA,EAAM,IACvE,EAAC;AACL,EAAA,IAAI,MAAM,IAAA,CAAK,CAAC,IAAA,KAAS,CAAC,IAAI,CAAA,EAAG;AAC/B,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,EAAG,SAAS,CAAA,wBAAA,CAA0B,CAAA;AAAA,EACxD;AAEA,EAAA,IAAI,WAAA,KAAgB,GAAA,IAAO,KAAA,CAAM,MAAA,GAAS,CAAA,EAAG;AAC3C,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,EAAG,SAAS,CAAA,qCAAA,EAAwC,KAAA,CAAM,MAAM,CAAA,CAAE,CAAA;AAAA,EACpF;AACA,EAAA,IAAI,WAAA,KAAgB,GAAA,IAAO,KAAA,CAAM,MAAA,KAAW,CAAA,EAAG;AAC7C,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,EAAG,SAAS,CAAA,2BAAA,CAA6B,CAAA;AAAA,EAC3D;AAEA,EAAA,MAAM,MAAA,GAAS,KAAA,CAAM,GAAA,CAAI,SAAS,CAAA;AAClC,EAAA,IAAI,WAAA,KAAgB,GAAA,EAAK,OAAO,MAAA,CAAO,CAAC,CAAA;AACxC,EAAA,OAAO,MAAA;AACT","file":"chunk-K5LJ7WSW.js","sourcesContent":["/** Cardinalities supported by Behave's cfparse fields. */\nexport type FieldCardinality = '?' | '*' | '+';\n\nexport interface CardinalityOptions {\n /** Delimiter between values. Defaults to a comma with optional whitespace. */\n separator?: string | RegExp;\n /** Human-readable field name used in validation errors. */\n fieldName?: string;\n}\n\nexport type CardinalityResult<T, C extends FieldCardinality> = C extends '?' ? T | undefined : T[];\n\n/**\n * Parse a Cucumber string parameter with cfparse-compatible cardinality.\n *\n * Use `?` for zero-or-one, `*` for zero-or-more, and `+` for one-or-more.\n * This keeps the capability in TypeScript while allowing standard Cucumber\n * Expressions and readable comma-separated parameters.\n */\nexport function parseCardinalityField<T, C extends FieldCardinality>(\n value: string,\n cardinality: C,\n transform: (item: string, index: number) => T,\n options: CardinalityOptions = {},\n): CardinalityResult<T, C> {\n const fieldName = options.fieldName ?? 'field';\n const trimmed = value.trim();\n const items = trimmed\n ? trimmed.split(options.separator ?? /\\s*,\\s*/).map((item) => item.trim())\n : [];\n if (items.some((item) => !item)) {\n throw new Error(`${fieldName} contains an empty value`);\n }\n\n if (cardinality === '?' && items.length > 1) {\n throw new Error(`${fieldName} expects zero or one value, received ${items.length}`);\n }\n if (cardinality === '+' && items.length === 0) {\n throw new Error(`${fieldName} expects one or more values`);\n }\n\n const parsed = items.map(transform);\n if (cardinality === '?') return parsed[0] as CardinalityResult<T, C>;\n return parsed as CardinalityResult<T, C>;\n}\n"]}
package/dist/index.cjs CHANGED
@@ -11,6 +11,14 @@ var util = require('util');
11
11
  var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
12
12
  // src/world.ts
13
13
 
14
+ // src/actions.ts
15
+ function defineAction(action) {
16
+ return action;
17
+ }
18
+ async function runAction(world, action, ...args) {
19
+ return await action(world, ...args);
20
+ }
21
+
14
22
  // src/fixtures.ts
15
23
  function isFixtureResult(value) {
16
24
  return typeof value === "object" && value !== null && Object.prototype.hasOwnProperty.call(value, "value") && typeof value.cleanup === "function";
@@ -70,6 +78,73 @@ var SharedFixtureRegistry = class {
70
78
  };
71
79
  var sharedFixtures = new SharedFixtureRegistry();
72
80
 
81
+ // src/scoped-state.ts
82
+ var SharedStateRegistry = class {
83
+ runValues = /* @__PURE__ */ new Map();
84
+ featureValues = /* @__PURE__ */ new Map();
85
+ run() {
86
+ return this.runValues;
87
+ }
88
+ feature(featureUri) {
89
+ if (!featureUri) throw new Error("Feature state requires a feature URI");
90
+ let values = this.featureValues.get(featureUri);
91
+ if (!values) {
92
+ values = /* @__PURE__ */ new Map();
93
+ this.featureValues.set(featureUri, values);
94
+ }
95
+ return values;
96
+ }
97
+ clear() {
98
+ this.runValues.clear();
99
+ this.featureValues.clear();
100
+ }
101
+ };
102
+ var ScopedState = class {
103
+ constructor(runValues, featureValues) {
104
+ this.runValues = runValues;
105
+ this.featureValues = featureValues;
106
+ }
107
+ runValues;
108
+ featureValues;
109
+ scenarioValues = /* @__PURE__ */ new Map();
110
+ set(scope, key, value) {
111
+ this.values(scope).set(validateKey(key), value);
112
+ return value;
113
+ }
114
+ get(key) {
115
+ const validKey = validateKey(key);
116
+ if (this.scenarioValues.has(validKey)) return this.scenarioValues.get(validKey);
117
+ if (this.featureValues.has(validKey)) return this.featureValues.get(validKey);
118
+ return this.runValues.get(validKey);
119
+ }
120
+ require(key) {
121
+ const value = this.get(key);
122
+ if (value === void 0) throw new Error(`Required BDD state '${key}' is not set`);
123
+ return value;
124
+ }
125
+ has(key) {
126
+ const validKey = validateKey(key);
127
+ return this.scenarioValues.has(validKey) || this.featureValues.has(validKey) || this.runValues.has(validKey);
128
+ }
129
+ delete(scope, key) {
130
+ return this.values(scope).delete(validateKey(key));
131
+ }
132
+ clearScenario() {
133
+ this.scenarioValues.clear();
134
+ }
135
+ values(scope) {
136
+ if (scope === "run") return this.runValues;
137
+ if (scope === "feature") return this.featureValues;
138
+ return this.scenarioValues;
139
+ }
140
+ };
141
+ function validateKey(key) {
142
+ const trimmed = key.trim();
143
+ if (!trimmed) throw new Error("BDD state key must not be empty");
144
+ return trimmed;
145
+ }
146
+ var sharedState = new SharedStateRegistry();
147
+
73
148
  // src/world.ts
74
149
  var DatabricksWorld = class extends cucumber.World {
75
150
  profile;
@@ -86,10 +161,13 @@ var DatabricksWorld = class extends cucumber.World {
86
161
  lastSql;
87
162
  lastStatementId;
88
163
  featureUri;
164
+ scenarioName;
165
+ effectiveTags = [];
89
166
  stepOutputCapture;
90
167
  lakebasePool;
91
168
  ctx;
92
169
  activeCtx;
170
+ scopedState;
93
171
  cleanups = [];
94
172
  constructor(options) {
95
173
  super(options);
@@ -124,6 +202,29 @@ var DatabricksWorld = class extends cucumber.World {
124
202
  addCleanup(cleanup) {
125
203
  this.cleanups.push(cleanup);
126
204
  }
205
+ /** Initialize Behave-style feature/scenario metadata and layered state. */
206
+ initializeScenario(featureUri, scenarioName, tags) {
207
+ this.featureUri = featureUri;
208
+ this.scenarioName = scenarioName;
209
+ this.effectiveTags = [...tags];
210
+ this.scopedState = new ScopedState(sharedState.run(), sharedState.feature(featureUri));
211
+ }
212
+ /** Set typed state at run, feature, or scenario scope. */
213
+ setState(scope, key, value) {
214
+ return this.state().set(scope, key, value);
215
+ }
216
+ /** Resolve state using scenario → feature → run precedence. */
217
+ getState(key) {
218
+ return this.state().get(key);
219
+ }
220
+ /** Resolve required layered state or fail with a useful message. */
221
+ requireState(key) {
222
+ return this.state().require(key);
223
+ }
224
+ /** Compose a reusable typed action without reparsing textual Gherkin. */
225
+ runAction(action, ...args) {
226
+ return runAction(this, action, ...args);
227
+ }
127
228
  /** Setup a resource once for this Cucumber run and clean it up in AfterAll. */
128
229
  runFixture(key, setup) {
129
230
  return sharedFixtures.runFixture(key, setup);
@@ -170,8 +271,16 @@ var DatabricksWorld = class extends cucumber.World {
170
271
  errors.push(error);
171
272
  }
172
273
  this.ctx = void 0;
274
+ this.scopedState?.clearScenario();
275
+ this.scopedState = void 0;
173
276
  if (errors.length > 0) throw new AggregateError(errors, "Scenario cleanup failed");
174
277
  }
278
+ state() {
279
+ if (!this.scopedState) {
280
+ throw new Error("BDD scoped state is only available after the scenario Before hook");
281
+ }
282
+ return this.scopedState;
283
+ }
175
284
  };
176
285
 
177
286
  // src/profile.ts
@@ -190,6 +299,68 @@ function liveOnlyReason(tags, profile) {
190
299
  if (blocking.length === 0) return null;
191
300
  return `requires a live workspace (${blocking.join(", ")}); running under DBX_TEST_PROFILE=local`;
192
301
  }
302
+ function capabilitiesFromEnv(profile, env = process.env) {
303
+ const capabilities = { profile };
304
+ const builtins = [
305
+ ["cloud", env.DBX_TEST_CLOUD],
306
+ ["compute", env.DBX_TEST_COMPUTE_MODE ?? env.DBX_TEST_COMPUTE],
307
+ ["stage", env.DBX_TEST_STAGE]
308
+ ];
309
+ for (const [key, value] of builtins) {
310
+ if (value !== void 0 && value !== "") capabilities[key] = value;
311
+ }
312
+ for (const [name, value] of Object.entries(env)) {
313
+ if (!name.startsWith("DBX_TEST_CAPABILITY_") || value === void 0) continue;
314
+ capabilities[normalizeCapabilityKey(name.slice("DBX_TEST_CAPABILITY_".length))] = value;
315
+ }
316
+ return capabilities;
317
+ }
318
+ function capabilityTagReason(tags, capabilities) {
319
+ const normalized = new Map(
320
+ Object.entries(capabilities).map(([key, value]) => [
321
+ normalizeCapabilityKey(key),
322
+ String(value).toLowerCase()
323
+ ])
324
+ );
325
+ for (const tag of tags) {
326
+ const predicate = parseCapabilityTag(tag);
327
+ if (!predicate) continue;
328
+ const actual = normalized.get(normalizeCapabilityKey(predicate.key));
329
+ const matches = predicate.expected ? actual === predicate.expected.toLowerCase() : actual !== void 0 && !["", "0", "false", "no", "off"].includes(actual);
330
+ if (predicate.kind === "requires" && !matches) {
331
+ return `${tag} requires capability ${predicate.key}${predicate.expected ? `=${predicate.expected}` : ""}; actual ${actual ?? "unset"}`;
332
+ }
333
+ if (predicate.kind === "excludes" && matches) {
334
+ return `${tag} excludes capability ${predicate.key}${predicate.expected ? `=${predicate.expected}` : ""}`;
335
+ }
336
+ }
337
+ return null;
338
+ }
339
+ function scenarioSkipReason(tags, profile, env = process.env) {
340
+ return liveOnlyReason(tags, profile) ?? capabilityTagReason(tags, capabilitiesFromEnv(profile, env));
341
+ }
342
+ function parseCapabilityTag(tag) {
343
+ const direct = tag.match(/^@(requires|excludes)\.([^=]+)(?:=(.+))?$/i);
344
+ if (direct?.[1] && direct[2]) {
345
+ return {
346
+ kind: direct[1].toLowerCase(),
347
+ key: direct[2],
348
+ ...direct[3] ? { expected: direct[3] } : {}
349
+ };
350
+ }
351
+ const behave = tag.match(/^@(use|not)\.with_([^=]+)=(.+)$/i);
352
+ if (behave?.[1] && behave[2] && behave[3]) {
353
+ return {
354
+ kind: behave[1].toLowerCase() === "use" ? "requires" : "excludes",
355
+ key: behave[2],
356
+ expected: behave[3]
357
+ };
358
+ }
359
+ return void 0;
360
+ }
361
+ function normalizeCapabilityKey(key) {
362
+ return key.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_");
363
+ }
193
364
 
194
365
  // src/infer.ts
195
366
  var INT_RE = /^-?\d+$/;
@@ -454,6 +625,25 @@ function renderChunks(chunks) {
454
625
  return rendered.trimEnd();
455
626
  }
456
627
 
628
+ // src/cardinality.ts
629
+ function parseCardinalityField(value, cardinality, transform, options = {}) {
630
+ const fieldName = options.fieldName ?? "field";
631
+ const trimmed = value.trim();
632
+ const items = trimmed ? trimmed.split(options.separator ?? /\s*,\s*/).map((item) => item.trim()) : [];
633
+ if (items.some((item) => !item)) {
634
+ throw new Error(`${fieldName} contains an empty value`);
635
+ }
636
+ if (cardinality === "?" && items.length > 1) {
637
+ throw new Error(`${fieldName} expects zero or one value, received ${items.length}`);
638
+ }
639
+ if (cardinality === "+" && items.length === 0) {
640
+ throw new Error(`${fieldName} expects one or more values`);
641
+ }
642
+ const parsed = items.map(transform);
643
+ if (cardinality === "?") return parsed[0];
644
+ return parsed;
645
+ }
646
+
457
647
  Object.defineProperty(exports, "waitForPipelineUpdate", {
458
648
  enumerable: true,
459
649
  get: function () { return databricksTestkit.waitForPipelineUpdate; }
@@ -462,19 +652,28 @@ exports.DatabricksWorld = DatabricksWorld;
462
652
  exports.FAILURE_EVIDENCE_MEDIA_TYPE = FAILURE_EVIDENCE_MEDIA_TYPE;
463
653
  exports.LIVE_ONLY_TAGS = LIVE_ONLY_TAGS;
464
654
  exports.RUN_STATES = RUN_STATES;
655
+ exports.ScopedState = ScopedState;
465
656
  exports.SharedFixtureRegistry = SharedFixtureRegistry;
657
+ exports.SharedStateRegistry = SharedStateRegistry;
658
+ exports.capabilitiesFromEnv = capabilitiesFromEnv;
659
+ exports.capabilityTagReason = capabilityTagReason;
466
660
  exports.coerceMatchRows = coerceMatchRows;
661
+ exports.defineAction = defineAction;
467
662
  exports.inferFixture = inferFixture;
468
663
  exports.liveOnlyReason = liveOnlyReason;
664
+ exports.parseCardinalityField = parseCardinalityField;
469
665
  exports.parseDuration = parseDuration;
470
666
  exports.parseFailureEvidenceAttachment = parseFailureEvidenceAttachment;
471
667
  exports.parseTableIdentifier = parseTableIdentifier;
472
668
  exports.redactSecrets = redactSecrets;
473
669
  exports.resolveJobId = resolveJobId;
474
670
  exports.resolvePipelineId = resolvePipelineId;
671
+ exports.runAction = runAction;
475
672
  exports.runFeatures = runFeatures;
476
673
  exports.runJobToTerminal = runJobToTerminal;
674
+ exports.scenarioSkipReason = scenarioSkipReason;
477
675
  exports.sharedFixtures = sharedFixtures;
676
+ exports.sharedState = sharedState;
478
677
  exports.startPipelineUpdate = startPipelineUpdate;
479
678
  exports.startStepOutputCapture = startStepOutputCapture;
480
679
  //# sourceMappingURL=index.cjs.map