qa360 1.4.5 → 2.0.1

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 (209) hide show
  1. package/README.md +1 -1
  2. package/dist/commands/ai.d.ts +41 -0
  3. package/dist/commands/ai.js +499 -0
  4. package/dist/commands/ask.js +12 -12
  5. package/dist/commands/coverage.d.ts +8 -0
  6. package/dist/commands/coverage.js +252 -0
  7. package/dist/commands/explain.d.ts +27 -0
  8. package/dist/commands/explain.js +630 -0
  9. package/dist/commands/flakiness.d.ts +73 -0
  10. package/dist/commands/flakiness.js +435 -0
  11. package/dist/commands/generate.d.ts +66 -0
  12. package/dist/commands/generate.js +438 -0
  13. package/dist/commands/init.d.ts +56 -9
  14. package/dist/commands/init.js +217 -10
  15. package/dist/commands/monitor.d.ts +27 -0
  16. package/dist/commands/monitor.js +225 -0
  17. package/dist/commands/ollama.d.ts +40 -0
  18. package/dist/commands/ollama.js +301 -0
  19. package/dist/commands/pack.d.ts +37 -9
  20. package/dist/commands/pack.js +240 -141
  21. package/dist/commands/regression.d.ts +8 -0
  22. package/dist/commands/regression.js +340 -0
  23. package/dist/commands/repair.d.ts +26 -0
  24. package/dist/commands/repair.js +307 -0
  25. package/dist/commands/retry.d.ts +43 -0
  26. package/dist/commands/retry.js +275 -0
  27. package/dist/commands/run.d.ts +8 -3
  28. package/dist/commands/run.js +45 -31
  29. package/dist/commands/slo.d.ts +8 -0
  30. package/dist/commands/slo.js +327 -0
  31. package/dist/core/adapters/playwright-native-api.d.ts +183 -0
  32. package/dist/core/adapters/playwright-native-api.js +461 -0
  33. package/dist/core/adapters/playwright-ui.d.ts +7 -0
  34. package/dist/core/adapters/playwright-ui.js +29 -1
  35. package/dist/core/ai/anthropic-provider.d.ts +50 -0
  36. package/dist/core/ai/anthropic-provider.js +211 -0
  37. package/dist/core/ai/deepseek-provider.d.ts +81 -0
  38. package/dist/core/ai/deepseek-provider.js +254 -0
  39. package/dist/core/ai/index.d.ts +60 -0
  40. package/dist/core/ai/index.js +18 -0
  41. package/dist/core/ai/llm-client.d.ts +45 -0
  42. package/dist/core/ai/llm-client.js +7 -0
  43. package/dist/core/ai/mock-provider.d.ts +49 -0
  44. package/dist/core/ai/mock-provider.js +121 -0
  45. package/dist/core/ai/ollama-provider.d.ts +78 -0
  46. package/dist/core/ai/ollama-provider.js +192 -0
  47. package/dist/core/ai/openai-provider.d.ts +48 -0
  48. package/dist/core/ai/openai-provider.js +188 -0
  49. package/dist/core/ai/provider-factory.d.ts +160 -0
  50. package/dist/core/ai/provider-factory.js +269 -0
  51. package/dist/core/auth/api-key-provider.d.ts +16 -0
  52. package/dist/core/auth/api-key-provider.js +63 -0
  53. package/dist/core/auth/aws-iam-provider.d.ts +35 -0
  54. package/dist/core/auth/aws-iam-provider.js +177 -0
  55. package/dist/core/auth/azure-ad-provider.d.ts +15 -0
  56. package/dist/core/auth/azure-ad-provider.js +99 -0
  57. package/dist/core/auth/basic-auth-provider.d.ts +26 -0
  58. package/dist/core/auth/basic-auth-provider.js +111 -0
  59. package/dist/core/auth/gcp-adc-provider.d.ts +27 -0
  60. package/dist/core/auth/gcp-adc-provider.js +126 -0
  61. package/dist/core/auth/index.d.ts +238 -0
  62. package/dist/core/auth/index.js +82 -0
  63. package/dist/core/auth/jwt-provider.d.ts +19 -0
  64. package/dist/core/auth/jwt-provider.js +160 -0
  65. package/dist/core/auth/manager.d.ts +84 -0
  66. package/dist/core/auth/manager.js +230 -0
  67. package/dist/core/auth/oauth2-provider.d.ts +17 -0
  68. package/dist/core/auth/oauth2-provider.js +114 -0
  69. package/dist/core/auth/totp-provider.d.ts +31 -0
  70. package/dist/core/auth/totp-provider.js +134 -0
  71. package/dist/core/auth/ui-login-provider.d.ts +26 -0
  72. package/dist/core/auth/ui-login-provider.js +198 -0
  73. package/dist/core/cache/index.d.ts +7 -0
  74. package/dist/core/cache/index.js +6 -0
  75. package/dist/core/cache/lru-cache.d.ts +203 -0
  76. package/dist/core/cache/lru-cache.js +397 -0
  77. package/dist/core/coverage/analyzer.d.ts +101 -0
  78. package/dist/core/coverage/analyzer.js +415 -0
  79. package/dist/core/coverage/collector.d.ts +74 -0
  80. package/dist/core/coverage/collector.js +459 -0
  81. package/dist/core/coverage/config.d.ts +37 -0
  82. package/dist/core/coverage/config.js +156 -0
  83. package/dist/core/coverage/index.d.ts +11 -0
  84. package/dist/core/coverage/index.js +15 -0
  85. package/dist/core/coverage/types.d.ts +267 -0
  86. package/dist/core/coverage/types.js +6 -0
  87. package/dist/core/coverage/vault.d.ts +95 -0
  88. package/dist/core/coverage/vault.js +405 -0
  89. package/dist/core/dashboard/assets.d.ts +6 -0
  90. package/dist/core/dashboard/assets.js +690 -0
  91. package/dist/core/dashboard/index.d.ts +6 -0
  92. package/dist/core/dashboard/index.js +5 -0
  93. package/dist/core/dashboard/server.d.ts +72 -0
  94. package/dist/core/dashboard/server.js +354 -0
  95. package/dist/core/dashboard/types.d.ts +70 -0
  96. package/dist/core/dashboard/types.js +5 -0
  97. package/dist/core/discoverer/index.d.ts +115 -0
  98. package/dist/core/discoverer/index.js +250 -0
  99. package/dist/core/flakiness/index.d.ts +228 -0
  100. package/dist/core/flakiness/index.js +384 -0
  101. package/dist/core/generation/code-formatter.d.ts +111 -0
  102. package/dist/core/generation/code-formatter.js +307 -0
  103. package/dist/core/generation/code-generator.d.ts +144 -0
  104. package/dist/core/generation/code-generator.js +293 -0
  105. package/dist/core/generation/generator.d.ts +40 -0
  106. package/dist/core/generation/generator.js +76 -0
  107. package/dist/core/generation/index.d.ts +30 -0
  108. package/dist/core/generation/index.js +28 -0
  109. package/dist/core/generation/pack-generator.d.ts +107 -0
  110. package/dist/core/generation/pack-generator.js +416 -0
  111. package/dist/core/generation/prompt-builder.d.ts +132 -0
  112. package/dist/core/generation/prompt-builder.js +672 -0
  113. package/dist/core/generation/source-analyzer.d.ts +213 -0
  114. package/dist/core/generation/source-analyzer.js +657 -0
  115. package/dist/core/generation/test-optimizer.d.ts +117 -0
  116. package/dist/core/generation/test-optimizer.js +328 -0
  117. package/dist/core/generation/types.d.ts +214 -0
  118. package/dist/core/generation/types.js +4 -0
  119. package/dist/core/index.d.ts +23 -1
  120. package/dist/core/index.js +39 -0
  121. package/dist/core/pack/validator.js +31 -1
  122. package/dist/core/pack-v2/index.d.ts +9 -0
  123. package/dist/core/pack-v2/index.js +8 -0
  124. package/dist/core/pack-v2/loader.d.ts +62 -0
  125. package/dist/core/pack-v2/loader.js +231 -0
  126. package/dist/core/pack-v2/migrator.d.ts +56 -0
  127. package/dist/core/pack-v2/migrator.js +455 -0
  128. package/dist/core/pack-v2/validator.d.ts +61 -0
  129. package/dist/core/pack-v2/validator.js +577 -0
  130. package/dist/core/regression/detector.d.ts +107 -0
  131. package/dist/core/regression/detector.js +497 -0
  132. package/dist/core/regression/index.d.ts +9 -0
  133. package/dist/core/regression/index.js +11 -0
  134. package/dist/core/regression/trend-analyzer.d.ts +102 -0
  135. package/dist/core/regression/trend-analyzer.js +345 -0
  136. package/dist/core/regression/types.d.ts +222 -0
  137. package/dist/core/regression/types.js +7 -0
  138. package/dist/core/regression/vault.d.ts +87 -0
  139. package/dist/core/regression/vault.js +289 -0
  140. package/dist/core/repair/engine/fixer.d.ts +24 -0
  141. package/dist/core/repair/engine/fixer.js +226 -0
  142. package/dist/core/repair/engine/suggestion-engine.d.ts +18 -0
  143. package/dist/core/repair/engine/suggestion-engine.js +187 -0
  144. package/dist/core/repair/index.d.ts +10 -0
  145. package/dist/core/repair/index.js +13 -0
  146. package/dist/core/repair/repairer.d.ts +90 -0
  147. package/dist/core/repair/repairer.js +284 -0
  148. package/dist/core/repair/types.d.ts +91 -0
  149. package/dist/core/repair/types.js +6 -0
  150. package/dist/core/repair/utils/error-analyzer.d.ts +28 -0
  151. package/dist/core/repair/utils/error-analyzer.js +264 -0
  152. package/dist/core/retry/flakiness-integration.d.ts +60 -0
  153. package/dist/core/retry/flakiness-integration.js +228 -0
  154. package/dist/core/retry/index.d.ts +14 -0
  155. package/dist/core/retry/index.js +16 -0
  156. package/dist/core/retry/retry-engine.d.ts +80 -0
  157. package/dist/core/retry/retry-engine.js +296 -0
  158. package/dist/core/retry/types.d.ts +178 -0
  159. package/dist/core/retry/types.js +52 -0
  160. package/dist/core/retry/vault.d.ts +77 -0
  161. package/dist/core/retry/vault.js +304 -0
  162. package/dist/core/runner/e2e-helpers.d.ts +102 -0
  163. package/dist/core/runner/e2e-helpers.js +153 -0
  164. package/dist/core/runner/phase3-runner.d.ts +101 -2
  165. package/dist/core/runner/phase3-runner.js +559 -24
  166. package/dist/core/self-healing/assertion-healer.d.ts +97 -0
  167. package/dist/core/self-healing/assertion-healer.js +371 -0
  168. package/dist/core/self-healing/engine.d.ts +122 -0
  169. package/dist/core/self-healing/engine.js +538 -0
  170. package/dist/core/self-healing/index.d.ts +10 -0
  171. package/dist/core/self-healing/index.js +11 -0
  172. package/dist/core/self-healing/selector-healer.d.ts +103 -0
  173. package/dist/core/self-healing/selector-healer.js +372 -0
  174. package/dist/core/self-healing/types.d.ts +152 -0
  175. package/dist/core/self-healing/types.js +6 -0
  176. package/dist/core/slo/config.d.ts +107 -0
  177. package/dist/core/slo/config.js +360 -0
  178. package/dist/core/slo/index.d.ts +11 -0
  179. package/dist/core/slo/index.js +15 -0
  180. package/dist/core/slo/sli-calculator.d.ts +92 -0
  181. package/dist/core/slo/sli-calculator.js +364 -0
  182. package/dist/core/slo/slo-tracker.d.ts +148 -0
  183. package/dist/core/slo/slo-tracker.js +379 -0
  184. package/dist/core/slo/types.d.ts +281 -0
  185. package/dist/core/slo/types.js +7 -0
  186. package/dist/core/slo/vault.d.ts +102 -0
  187. package/dist/core/slo/vault.js +427 -0
  188. package/dist/core/tui/index.d.ts +7 -0
  189. package/dist/core/tui/index.js +6 -0
  190. package/dist/core/tui/monitor.d.ts +92 -0
  191. package/dist/core/tui/monitor.js +271 -0
  192. package/dist/core/tui/renderer.d.ts +33 -0
  193. package/dist/core/tui/renderer.js +218 -0
  194. package/dist/core/tui/types.d.ts +63 -0
  195. package/dist/core/tui/types.js +5 -0
  196. package/dist/core/types/pack-v2.d.ts +425 -0
  197. package/dist/core/types/pack-v2.js +8 -0
  198. package/dist/core/vault/index.d.ts +116 -0
  199. package/dist/core/vault/index.js +400 -5
  200. package/dist/core/watch/index.d.ts +7 -0
  201. package/dist/core/watch/index.js +6 -0
  202. package/dist/core/watch/watch-mode.d.ts +213 -0
  203. package/dist/core/watch/watch-mode.js +389 -0
  204. package/dist/index.js +68 -68
  205. package/dist/utils/config.d.ts +5 -0
  206. package/dist/utils/config.js +136 -0
  207. package/package.json +5 -1
  208. package/dist/core/adapters/playwright-api.d.ts +0 -82
  209. package/dist/core/adapters/playwright-api.js +0 -264
@@ -1,13 +1,14 @@
1
1
  /**
2
2
  * QA360 Phase 3 Runner
3
3
  * Orchestrates hooks, adapters, and proof generation
4
+ * Supports both Pack v1 and Pack v2 configurations
4
5
  */
5
6
  import { existsSync, writeFileSync, mkdirSync } from 'fs';
6
7
  import { join } from 'path';
7
8
  import { createHash } from 'crypto';
8
9
  import chalk from 'chalk';
9
10
  import { HooksRunner } from '../hooks/runner.js';
10
- import { PlaywrightApiAdapter } from '../adapters/playwright-api.js';
11
+ import { PlaywrightNativeApiAdapter } from '../adapters/playwright-native-api.js';
11
12
  import { PlaywrightUiAdapter } from '../adapters/playwright-ui.js';
12
13
  import { K6PerfAdapter } from '../adapters/k6-perf.js';
13
14
  import { SemgrepSastAdapter } from '../adapters/semgrep-sast.js';
@@ -15,6 +16,8 @@ import { SecurityRedactor } from '../security/redactor.js';
15
16
  import { initializeKeys, sign } from '../proof/signer.js';
16
17
  import { canonicalize } from '../proof/canonicalize.js';
17
18
  import { EvidenceVault } from '../vault/index.js';
19
+ import { AuthManager } from '../auth/index.js';
20
+ import { FlakinessDetector, FlakinessCategory, formatFlakinessScore, generateTestId, DEFAULT_FLAKINESS_OPTIONS } from '../flakiness/index.js';
18
21
  export class Phase3Runner {
19
22
  workingDir;
20
23
  pack;
@@ -23,25 +26,165 @@ export class Phase3Runner {
23
26
  hooksRunner;
24
27
  keyPair;
25
28
  vault;
29
+ authManager;
30
+ authCredentialsCache = new Map();
31
+ flakyDetect;
32
+ flakyRuns;
33
+ flakinessDetector;
26
34
  constructor(options) {
27
35
  this.workingDir = options.workingDir;
28
36
  this.pack = options.pack;
29
37
  this.outputDir = options.outputDir || join(this.workingDir, '.qa360', 'runs');
30
38
  this.redactor = SecurityRedactor.forLogs();
39
+ this.authManager = new AuthManager();
40
+ this.flakyDetect = options.flakyDetect || false;
41
+ this.flakyRuns = options.flakyRuns || DEFAULT_FLAKINESS_OPTIONS.consecutiveRuns;
42
+ this.flakinessDetector = new FlakinessDetector({
43
+ consecutiveRuns: this.flakyRuns,
44
+ minRuns: 2,
45
+ enablePatternDetection: true
46
+ });
47
+ // Initialize hooks runner (convert v2 hooks to v1 format if needed)
48
+ const hooks = this.isPackV2(options.pack)
49
+ ? this.convertV2HooksToV1(options.pack.hooks)
50
+ : options.pack.hooks;
31
51
  this.hooksRunner = new HooksRunner({
32
52
  workingDir: this.workingDir,
33
- hooks: this.pack.hooks || {},
34
- execution: this.pack.execution,
53
+ hooks: hooks || {},
54
+ execution: this.isPackV2(options.pack) ? this.convertV2ExecutionToV1(options.pack.execution) : options.pack.execution,
35
55
  redactor: this.redactor
36
56
  });
57
+ // Register auth profiles from pack v2
58
+ if (this.isPackV2(options.pack) && options.pack.auth?.profiles) {
59
+ this.registerAuthProfiles(options.pack.auth.profiles);
60
+ }
61
+ }
62
+ /**
63
+ * Type guard to check if pack is v2
64
+ */
65
+ isPackV2(pack) {
66
+ return pack.version === 2;
67
+ }
68
+ /**
69
+ * Register authentication profiles from pack config
70
+ */
71
+ registerAuthProfiles(profiles) {
72
+ for (const [name, profile] of Object.entries(profiles)) {
73
+ // Convert v2 auth profile to auth module format
74
+ const authConfig = {
75
+ type: profile.type || 'none',
76
+ ...profile.config
77
+ };
78
+ this.authManager.registerProfile(name, authConfig);
79
+ }
80
+ }
81
+ /**
82
+ * Get auth profile name for a specific gate
83
+ */
84
+ getAuthProfileForGate(gateName) {
85
+ if (!this.isPackV2(this.pack) || !this.pack.auth) {
86
+ return undefined;
87
+ }
88
+ const authConfig = this.pack.auth;
89
+ // Check if gate has specific auth override
90
+ const gateConfig = this.pack.gates[gateName];
91
+ if (gateConfig?.auth) {
92
+ return gateConfig.auth;
93
+ }
94
+ // Use default based on gate type
95
+ if (gateName === 'api_smoke' || gateName === 'api') {
96
+ return authConfig.api;
97
+ }
98
+ if (gateName === 'ui' || gateName === 'a11y') {
99
+ return authConfig.ui;
100
+ }
101
+ return undefined;
102
+ }
103
+ /**
104
+ * Authenticate and get credentials for a gate
105
+ */
106
+ async getCredentialsForGate(gateName) {
107
+ const profileName = this.getAuthProfileForGate(gateName);
108
+ if (!profileName) {
109
+ return undefined;
110
+ }
111
+ // Check cache first
112
+ if (this.authCredentialsCache.has(profileName)) {
113
+ return this.authCredentialsCache.get(profileName);
114
+ }
115
+ // Authenticate
116
+ const result = await this.authManager.authenticate(profileName);
117
+ if (result.success && result.credentials) {
118
+ this.authCredentialsCache.set(profileName, result.credentials);
119
+ return result.credentials;
120
+ }
121
+ console.log(chalk.yellow(` ⚠️ Auth failed for profile '${profileName}': ${result.error}`));
122
+ return undefined;
123
+ }
124
+ /**
125
+ * Convert v2 hooks to v1 format
126
+ */
127
+ convertV2HooksToV1(hooks) {
128
+ if (!hooks)
129
+ return undefined;
130
+ // v2 hooks use { type, command, ... } format
131
+ // v1 hooks use { run, timeout } format
132
+ const converted = {
133
+ beforeAll: [],
134
+ afterAll: [],
135
+ beforeEach: [],
136
+ afterEach: []
137
+ };
138
+ for (const [phase, phaseHooks] of Object.entries(hooks)) {
139
+ if (Array.isArray(phaseHooks)) {
140
+ converted[phase] = phaseHooks.map((h) => {
141
+ if (h.type === 'run' || h.type === 'script') {
142
+ return { run: h.command, timeout: h.timeout, cwd: h.cwd, env: h.env };
143
+ }
144
+ if (h.type === 'wait_on') {
145
+ return { run: `npx wait-on ${h.wait_for?.resource || h.command}`, timeout: h.timeout };
146
+ }
147
+ if (h.type === 'docker') {
148
+ return { run: `docker compose ${h.compose?.command || 'up -d'}`, timeout: h.timeout };
149
+ }
150
+ return h;
151
+ });
152
+ }
153
+ }
154
+ return converted;
155
+ }
156
+ /**
157
+ * Convert v2 execution config to v1 format
158
+ */
159
+ convertV2ExecutionToV1(execution) {
160
+ if (!execution)
161
+ return undefined;
162
+ return {
163
+ timeout: execution.default_timeout || execution.timeout,
164
+ max_retries: execution.default_retries || execution.retries,
165
+ on_failure: execution.on_failure || 'continue'
166
+ };
167
+ }
168
+ /**
169
+ * Get gates array from pack (handles v1 and v2)
170
+ */
171
+ getGatesArray() {
172
+ if (this.isPackV2(this.pack)) {
173
+ return Object.keys(this.pack.gates).filter(gateName => {
174
+ const gateConfig = this.pack.gates[gateName];
175
+ return gateConfig?.enabled !== false;
176
+ });
177
+ }
178
+ return this.pack.gates || [];
37
179
  }
38
180
  /**
39
181
  * Execute complete Phase 3 workflow
40
182
  */
41
183
  async run() {
42
184
  const startTime = Date.now();
185
+ const gatesArray = this.getGatesArray();
43
186
  console.log(chalk.bold.blue(`\n🚀 QA360 Phase 3 Runner - ${this.pack.name}`));
44
- console.log(chalk.gray(`Gates: ${this.pack.gates.join(', ')}`));
187
+ console.log(chalk.gray(`Gates: ${gatesArray.join(', ')}`));
45
188
  try {
46
189
  // Ensure output directory exists
47
190
  this.ensureOutputDir();
@@ -65,7 +208,7 @@ export class Phase3Runner {
65
208
  // Execute gates
66
209
  console.log(chalk.blue('\n🎯 Phase 2: Quality Gates'));
67
210
  const gateResults = [];
68
- for (const gate of this.pack.gates) {
211
+ for (const gate of gatesArray) {
69
212
  const gateResult = await this.executeGate(gate);
70
213
  gateResults.push(gateResult);
71
214
  // Check if gate failed and should stop
@@ -74,6 +217,20 @@ export class Phase3Runner {
74
217
  break;
75
218
  }
76
219
  }
220
+ // Flakiness Detection Phase (Vision 2.0)
221
+ let flakinessResults;
222
+ if (this.flakyDetect) {
223
+ console.log(chalk.blue(`\n🎲 Phase 2.5: Flakiness Detection (${this.flakyRuns} consecutive runs)`));
224
+ flakinessResults = await this.detectFlakiness(gateResults);
225
+ // Display flakiness summary
226
+ const unstableCount = flakinessResults.filter(f => f.category === FlakinessCategory.UNSTABLE || f.category === FlakinessCategory.SHAKY).length;
227
+ if (unstableCount > 0) {
228
+ console.log(chalk.yellow(` ⚠️ ${unstableCount} test(s) show flaky behavior`));
229
+ }
230
+ else {
231
+ console.log(chalk.green(' ✅ All tests stable - no flakiness detected'));
232
+ }
233
+ }
77
234
  // Execute afterAll hooks
78
235
  console.log(chalk.blue('\n🔗 Phase 3: Cleanup Hooks'));
79
236
  const afterAllResults = await this.hooksRunner.executeHooks('afterAll');
@@ -98,8 +255,8 @@ export class Phase3Runner {
98
255
  });
99
256
  // Save to Evidence Vault
100
257
  console.log(chalk.blue('\n💾 Phase 5: Evidence Storage'));
101
- const runId = proofPath.split('/').pop()?.replace('-proof.json', '') || 'unknown';
102
- await this.saveToVault(runId, {
258
+ const proofRunId = proofPath.split('/').pop()?.replace('-proof.json', '') || 'unknown';
259
+ const vaultRunId = await this.saveToVault(proofRunId, {
103
260
  success,
104
261
  pack: this.pack,
105
262
  duration,
@@ -112,13 +269,31 @@ export class Phase3Runner {
112
269
  },
113
270
  summary,
114
271
  proofPath
115
- }, proofPath);
272
+ }, proofPath, flakinessResults);
116
273
  // Final summary
117
274
  console.log(chalk.blue('\n📊 Execution Summary'));
118
275
  console.log(` Duration: ${duration}ms`);
119
276
  console.log(` Gates: ${summary.passed}/${summary.total} passed`);
120
277
  console.log(` Trust Score: ${summary.trustScore}%`);
121
278
  console.log(` Proof: ${proofPath}`);
279
+ // Display flakiness details if enabled
280
+ if (flakinessResults && flakinessResults.length > 0) {
281
+ console.log(chalk.blue('\n🎲 Flakiness Scores:'));
282
+ for (const f of flakinessResults) {
283
+ const scoreFormatted = formatFlakinessScore(f.score);
284
+ const categoryMeta = {
285
+ [FlakinessCategory.LEGENDARY]: { emoji: '🟢', color: chalk.green },
286
+ [FlakinessCategory.SOLID]: { emoji: '🟢', color: chalk.green },
287
+ [FlakinessCategory.GOOD]: { emoji: '🟡', color: chalk.yellow },
288
+ [FlakinessCategory.SHAKY]: { emoji: '🟠', color: chalk.hex('#F97316') },
289
+ [FlakinessCategory.UNSTABLE]: { emoji: '🔴', color: chalk.red }
290
+ }[f.category];
291
+ console.log(` ${categoryMeta.color(`${f.testName}: ${scoreFormatted}`)} (${f.successfulRuns}/${f.totalRuns} passes)`);
292
+ if (f.suggestedFix) {
293
+ console.log(chalk.gray(` 💡 ${f.suggestedFix}`));
294
+ }
295
+ }
296
+ }
122
297
  if (success) {
123
298
  console.log(chalk.green('\n✅ All quality gates passed!'));
124
299
  }
@@ -137,7 +312,9 @@ export class Phase3Runner {
137
312
  afterAll: afterAllResults
138
313
  },
139
314
  summary,
140
- proofPath
315
+ flakiness: flakinessResults,
316
+ proofPath,
317
+ runId: vaultRunId
141
318
  };
142
319
  }
143
320
  catch (error) {
@@ -169,9 +346,17 @@ export class Phase3Runner {
169
346
  try {
170
347
  let result;
171
348
  let adapter;
349
+ // Check if this is a v2 pack with dynamic gate configuration
350
+ if (this.isPackV2(this.pack)) {
351
+ const gateConfig = this.pack.gates[gate];
352
+ if (gateConfig) {
353
+ return await this.executeDynamicGate(gate, gateConfig);
354
+ }
355
+ }
356
+ // Legacy v1 gates or predefined gates
172
357
  switch (gate) {
173
358
  case 'api_smoke':
174
- adapter = 'playwright-api';
359
+ adapter = 'playwright-native-api';
175
360
  result = await this.runApiSmokeGate();
176
361
  break;
177
362
  case 'ui':
@@ -231,33 +416,184 @@ export class Phase3Runner {
231
416
  };
232
417
  }
233
418
  }
419
+ /**
420
+ * Execute a dynamic v2 gate
421
+ */
422
+ async executeDynamicGate(gateName, gateConfig) {
423
+ const startTime = Date.now();
424
+ const adapterType = gateConfig.adapter || gateConfig.type;
425
+ if (!adapterType) {
426
+ throw new Error(`Gate '${gateName}' must specify an adapter`);
427
+ }
428
+ // Get auth credentials for this gate
429
+ const credentials = await this.getCredentialsForGate(gateName);
430
+ // Map adapter type to implementation
431
+ let result;
432
+ switch (adapterType) {
433
+ case 'playwright-api':
434
+ result = await this.executePlaywrightApiGate(gateName, gateConfig, credentials);
435
+ break;
436
+ case 'playwright-ui':
437
+ result = await this.executePlaywrightUiGate(gateName, gateConfig, credentials);
438
+ break;
439
+ case 'k6':
440
+ case 'k6-perf':
441
+ result = await this.executeK6PerfGate(gateName, gateConfig);
442
+ break;
443
+ case 'semgrep':
444
+ case 'sast':
445
+ result = await this.executeSemgrepSastGate(gateName, gateConfig);
446
+ break;
447
+ default:
448
+ throw new Error(`Unsupported adapter: '${adapterType}' for gate '${gateName}'`);
449
+ }
450
+ // Set duration
451
+ result.duration = Date.now() - startTime;
452
+ // Log result
453
+ if (result.success) {
454
+ console.log(chalk.green(` ✅ ${gateName} passed (${result.duration}ms)`));
455
+ }
456
+ else {
457
+ console.log(chalk.red(` ❌ ${gateName} failed (${result.duration}ms)`));
458
+ if (result.error) {
459
+ console.log(chalk.red(` 🔍 ${result.error}`));
460
+ }
461
+ }
462
+ return result;
463
+ }
464
+ /**
465
+ * Execute Playwright API gate with v2 config
466
+ * Uses PlaywrightNativeApiAdapter for zero-overhead HTTP testing
467
+ */
468
+ async executePlaywrightApiGate(gateName, gateConfig, credentials) {
469
+ const { PlaywrightNativeApiAdapter } = await import('../adapters/playwright-native-api.js');
470
+ const adapter = new PlaywrightNativeApiAdapter();
471
+ // Transform v2 config to adapter format
472
+ const gateConfigData = gateConfig.config || {};
473
+ const config = {
474
+ target: {
475
+ baseUrl: gateConfigData.baseUrl,
476
+ smoke: gateConfigData.smoke
477
+ },
478
+ budgets: gateConfigData.budgets,
479
+ timeout: gateConfigData.timeout,
480
+ retries: gateConfigData.retries,
481
+ auth: credentials
482
+ };
483
+ const result = await adapter.runSmokeTests(config);
484
+ return {
485
+ gate: gateName,
486
+ success: result.success,
487
+ duration: 0, // Will be set by caller
488
+ adapter: 'playwright-native-api',
489
+ results: result,
490
+ junit: result.junit
491
+ };
492
+ }
493
+ /**
494
+ * Execute Playwright UI gate with v2 config
495
+ */
496
+ async executePlaywrightUiGate(gateName, gateConfig, credentials) {
497
+ const { PlaywrightUiAdapter } = await import('../adapters/playwright-ui.js');
498
+ const adapter = new PlaywrightUiAdapter();
499
+ // Transform v2 config to adapter format
500
+ // v2: { baseUrl, pages } -> adapter expects: { target: { baseUrl, pages } }
501
+ const gateConfigData = gateConfig.config || {};
502
+ const pages = gateConfigData.pages ||
503
+ (gateConfigData.baseUrl ? [gateConfigData.baseUrl] : undefined);
504
+ const config = {
505
+ target: {
506
+ baseUrl: gateConfigData.baseUrl,
507
+ pages: pages
508
+ },
509
+ budgets: gateConfigData.budgets,
510
+ timeout: gateConfigData.timeout,
511
+ auth: credentials
512
+ };
513
+ const result = await adapter.runSmokeTests(config);
514
+ return {
515
+ gate: gateName,
516
+ success: result.success,
517
+ duration: 0, // Will be set by caller
518
+ adapter: 'playwright-ui',
519
+ results: result,
520
+ junit: result.junit
521
+ };
522
+ }
523
+ /**
524
+ * Execute K6 Performance gate with v2 config
525
+ */
526
+ async executeK6PerfGate(gateName, gateConfig) {
527
+ const { K6PerfAdapter } = await import('../adapters/k6-perf.js');
528
+ const adapter = new K6PerfAdapter(this.workingDir);
529
+ const config = {
530
+ baseUrl: gateConfig.config?.baseUrl,
531
+ ...gateConfig.config
532
+ };
533
+ const result = await adapter.runPerfTest(config);
534
+ return {
535
+ gate: gateName,
536
+ success: result.success,
537
+ duration: 0,
538
+ adapter: 'k6',
539
+ results: result,
540
+ junit: result.junit
541
+ };
542
+ }
543
+ /**
544
+ * Execute Semgrep SAST gate with v2 config
545
+ */
546
+ async executeSemgrepSastGate(gateName, gateConfig) {
547
+ const { SemgrepSastAdapter } = await import('../adapters/semgrep-sast.js');
548
+ const adapter = new SemgrepSastAdapter();
549
+ const config = {
550
+ workingDir: this.workingDir,
551
+ ...gateConfig.config
552
+ };
553
+ const result = await adapter.runSastScan(config);
554
+ return {
555
+ gate: gateName,
556
+ success: result.success,
557
+ duration: 0,
558
+ adapter: 'semgrep',
559
+ results: result,
560
+ junit: result.junit
561
+ };
562
+ }
234
563
  /**
235
564
  * Run API smoke gate
565
+ * Uses PlaywrightNativeApiAdapter for zero-overhead HTTP testing
236
566
  */
237
567
  async runApiSmokeGate() {
238
- if (!this.pack.targets?.api) {
239
- throw new Error('API smoke gate requires targets.api configuration');
568
+ const target = this.getTargetApi();
569
+ if (!target) {
570
+ throw new Error('API smoke gate requires targets.api configuration or gate config with baseUrl');
240
571
  }
241
- const adapter = new PlaywrightApiAdapter();
572
+ const credentials = await this.getCredentialsForGate('api_smoke');
573
+ const adapter = new PlaywrightNativeApiAdapter();
242
574
  return await adapter.runSmokeTests({
243
- target: this.pack.targets.api,
244
- budgets: this.pack.budgets,
245
- timeout: this.pack.execution?.timeout || 10000,
246
- retries: this.pack.execution?.max_retries || 1
575
+ target,
576
+ budgets: this.getBudgets(),
577
+ timeout: this.getExecutionTimeout() || 10000,
578
+ retries: this.getExecutionRetries() || 1,
579
+ auth: credentials
247
580
  });
248
581
  }
249
582
  /**
250
583
  * Run UI gate (includes basic accessibility)
251
584
  */
252
585
  async runUiGate() {
253
- if (!this.pack.targets?.web) {
254
- throw new Error('UI gate requires targets.web configuration');
586
+ const target = this.getTargetWeb();
587
+ if (!target) {
588
+ throw new Error('UI gate requires targets.web configuration or gate config with baseUrl');
255
589
  }
590
+ const credentials = await this.getCredentialsForGate('ui');
256
591
  const adapter = new PlaywrightUiAdapter();
257
592
  return await adapter.runSmokeTests({
258
- target: this.pack.targets.web,
259
- budgets: this.pack.budgets,
260
- timeout: this.pack.execution?.timeout || 30000
593
+ target,
594
+ budgets: this.getBudgets(),
595
+ timeout: this.getExecutionTimeout() || 30000,
596
+ auth: credentials
261
597
  });
262
598
  }
263
599
  /**
@@ -415,11 +751,12 @@ export class Phase3Runner {
415
751
  }
416
752
  /**
417
753
  * Save run results to Evidence Vault
754
+ * @returns The vault run ID
418
755
  */
419
- async saveToVault(runId, result, proofPath) {
756
+ async saveToVault(runId, result, proofPath, flakinessResults) {
420
757
  if (!this.vault) {
421
758
  console.log(chalk.yellow(' ⚠️ Vault not initialized, skipping storage'));
422
- return;
759
+ return undefined;
423
760
  }
424
761
  try {
425
762
  // Begin run in vault (returns actual runId used)
@@ -451,11 +788,152 @@ export class Phase3Runner {
451
788
  });
452
789
  }
453
790
  }
791
+ // Record flakiness results if available
792
+ if (flakinessResults && flakinessResults.length > 0) {
793
+ for (const flaky of flakinessResults) {
794
+ // Record unstable/shaky tests as findings
795
+ if (flaky.category === FlakinessCategory.UNSTABLE || flaky.category === FlakinessCategory.SHAKY) {
796
+ await this.vault.recordFinding(vaultRunId, {
797
+ gate: flaky.gate,
798
+ severity: flaky.category === FlakinessCategory.UNSTABLE ? 'high' : 'medium',
799
+ rule: 'flaky-test-detected',
800
+ message: `Test "${flaky.testName}" is ${flaky.category}: ${flaky.score}% reliability (${flaky.successfulRuns}/${flaky.totalRuns} passes)`
801
+ });
802
+ }
803
+ }
804
+ console.log(chalk.green(` 💾 ${flakinessResults.length} flakiness analysis(es) saved`));
805
+ }
454
806
  console.log(chalk.green(' 💾 Run saved to Evidence Vault'));
807
+ return vaultRunId;
455
808
  }
456
809
  catch (error) {
457
810
  console.log(chalk.yellow(` ⚠️ Failed to save to vault: ${error}`));
811
+ return undefined;
812
+ }
813
+ }
814
+ /**
815
+ * Detect flakiness by running tests multiple times consecutively
816
+ * @param gateResults Original gate results from first run
817
+ * @returns Flakiness analysis results
818
+ */
819
+ async detectFlakiness(gateResults) {
820
+ const flakinessMap = new Map();
821
+ const timestamp = Date.now();
822
+ // First, collect results from the initial run
823
+ for (const gateResult of gateResults) {
824
+ for (const [testId, testResult] of this.extractTestResults(gateResult, timestamp)) {
825
+ if (!flakinessMap.has(testId)) {
826
+ flakinessMap.set(testId, []);
827
+ }
828
+ flakinessMap.get(testId).push(testResult);
829
+ }
830
+ }
831
+ // Run additional consecutive runs
832
+ for (let run = 1; run < this.flakyRuns; run++) {
833
+ console.log(chalk.gray(` 🔄 Consecutive run ${run + 1}/${this.flakyRuns}...`));
834
+ for (const gateResult of gateResults) {
835
+ // Re-run all gates to detect flakiness
836
+ try {
837
+ const retryResult = await this.executeGate(gateResult.gate);
838
+ // Extract and store test results
839
+ for (const [testId, testResult] of this.extractTestResults(retryResult, timestamp)) {
840
+ if (!flakinessMap.has(testId)) {
841
+ flakinessMap.set(testId, []);
842
+ }
843
+ flakinessMap.get(testId).push(testResult);
844
+ }
845
+ }
846
+ catch (error) {
847
+ console.log(chalk.yellow(` ⚠️ Failed to re-run ${gateResult.gate}: ${error}`));
848
+ }
849
+ }
850
+ }
851
+ // Analyze all collected results
852
+ return this.flakinessDetector.analyzeAll(flakinessMap);
853
+ }
854
+ /**
855
+ * Extract test results from a gate result
856
+ * Returns a map of testId to TestResult for flakiness tracking
857
+ */
858
+ extractTestResults(gateResult, timestamp) {
859
+ const results = new Map();
860
+ // Extract test results from adapter output
861
+ const adapterResults = gateResult.results;
862
+ if (adapterResults?.results && Array.isArray(adapterResults.results)) {
863
+ // Check for Playwright Native API adapter format
864
+ const firstResult = adapterResults.results[0];
865
+ if (firstResult && 'endpoint' in firstResult && 'method' in firstResult) {
866
+ // PlaywrightNativeApiAdapter format: { endpoint, method, status, success, error, ... }
867
+ for (const test of adapterResults.results) {
868
+ // Extract just the path from full URL for cleaner test names
869
+ const url = new URL(test.endpoint);
870
+ const path = url.pathname + url.search;
871
+ const testName = `${test.method} ${path} -> ${test.status}`;
872
+ const testId = generateTestId(testName, gateResult.gate);
873
+ results.set(testId, {
874
+ testId,
875
+ testName,
876
+ filePath: gateResult.gate,
877
+ gate: gateResult.gate,
878
+ success: test.success,
879
+ durationMs: test.responseTime || 0,
880
+ errorMessage: test.error,
881
+ timestamp: timestamp + Math.random(), // Small offset for ordering
882
+ environment: process.env.NODE_ENV || 'local'
883
+ });
884
+ }
885
+ }
886
+ else {
887
+ // Generic adapter format with results array
888
+ for (const test of adapterResults.results) {
889
+ const testName = test.name || gateResult.gate;
890
+ const testId = generateTestId(testName, gateResult.gate);
891
+ results.set(testId, {
892
+ testId,
893
+ testName,
894
+ filePath: gateResult.gate,
895
+ gate: gateResult.gate,
896
+ success: test.passed || test.status === 'passed',
897
+ durationMs: test.duration || 0,
898
+ errorType: test.error?.type,
899
+ errorMessage: test.error?.message,
900
+ timestamp: timestamp + Math.random(),
901
+ environment: process.env.NODE_ENV || 'local'
902
+ });
903
+ }
904
+ }
905
+ }
906
+ else if (adapterResults?.summary) {
907
+ // Generic adapter with summary only (no detailed results)
908
+ const testId = generateTestId(gateResult.gate, gateResult.gate);
909
+ results.set(testId, {
910
+ testId,
911
+ testName: gateResult.gate,
912
+ filePath: gateResult.gate,
913
+ gate: gateResult.gate,
914
+ success: gateResult.success,
915
+ durationMs: gateResult.duration,
916
+ errorMessage: gateResult.error,
917
+ timestamp,
918
+ environment: process.env.NODE_ENV || 'local'
919
+ });
458
920
  }
921
+ else {
922
+ // Fallback: create single test result from gate
923
+ const testId = generateTestId(gateResult.gate, gateResult.gate);
924
+ results.set(testId, {
925
+ testId,
926
+ testName: gateResult.gate,
927
+ filePath: gateResult.gate,
928
+ gate: gateResult.gate,
929
+ success: gateResult.success,
930
+ durationMs: gateResult.duration,
931
+ errorMessage: gateResult.error,
932
+ timestamp,
933
+ environment: process.env.NODE_ENV || 'local'
934
+ });
935
+ }
936
+ return results;
459
937
  }
460
938
  /**
461
939
  * Generate hash of pack configuration
@@ -466,6 +944,63 @@ export class Phase3Runner {
466
944
  .digest('hex')
467
945
  .substring(0, 16);
468
946
  }
947
+ /**
948
+ * Get API target (v1 or v2 format)
949
+ */
950
+ getTargetApi() {
951
+ if (this.isPackV2(this.pack)) {
952
+ // In v2, target is in the gate config
953
+ const gateConfig = this.pack.gates['api_smoke'] || this.pack.gates['api'];
954
+ if (gateConfig && gateConfig.config?.baseUrl) {
955
+ return gateConfig.config;
956
+ }
957
+ return undefined;
958
+ }
959
+ return this.pack.targets?.api;
960
+ }
961
+ /**
962
+ * Get Web target (v1 or v2 format)
963
+ */
964
+ getTargetWeb() {
965
+ if (this.isPackV2(this.pack)) {
966
+ // In v2, target is in the gate config
967
+ const gateConfig = this.pack.gates['ui'] || this.pack.gates['a11y'];
968
+ if (gateConfig && gateConfig.config?.baseUrl) {
969
+ return gateConfig.config;
970
+ }
971
+ return undefined;
972
+ }
973
+ return this.pack.targets?.web;
974
+ }
975
+ /**
976
+ * Get budgets (v1 or v2 format)
977
+ */
978
+ getBudgets() {
979
+ if (this.isPackV2(this.pack)) {
980
+ // In v2, budgets can be in gate config or global
981
+ // For now, return undefined - budgets are gate-specific in v2
982
+ return undefined;
983
+ }
984
+ return this.pack.budgets;
985
+ }
986
+ /**
987
+ * Get execution timeout (v1 or v2 format)
988
+ */
989
+ getExecutionTimeout() {
990
+ if (this.isPackV2(this.pack)) {
991
+ return this.pack.execution?.default_timeout;
992
+ }
993
+ return this.pack.execution?.timeout;
994
+ }
995
+ /**
996
+ * Get execution retries (v1 or v2 format)
997
+ */
998
+ getExecutionRetries() {
999
+ if (this.isPackV2(this.pack)) {
1000
+ return this.pack.execution?.default_retries;
1001
+ }
1002
+ return this.pack.execution?.max_retries;
1003
+ }
469
1004
  /**
470
1005
  * Ensure output directory exists
471
1006
  */