@boyingliu01/xp-gate 0.15.5 → 0.18.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 (165) hide show
  1. package/adapter-common.sh +0 -0
  2. package/adapters/cpp.sh +0 -0
  3. package/adapters/dart.sh +0 -0
  4. package/adapters/flutter.sh +0 -0
  5. package/adapters/go.sh +0 -0
  6. package/adapters/iac.sh +0 -0
  7. package/adapters/java.sh +0 -0
  8. package/adapters/kotlin.sh +0 -0
  9. package/adapters/objectivec.sh +0 -0
  10. package/adapters/plugins/book299-20132-python/scripts/install-gradle.sh +0 -0
  11. package/adapters/plugins/book299-20132-python/scripts/install-maven.sh +0 -0
  12. package/adapters/plugins/book299-4081-c/scripts/install-gradle.sh +0 -0
  13. package/adapters/plugins/book299-4081-c/scripts/install-maven.sh +0 -0
  14. package/adapters/plugins/book299-4083-javascriptes5/scripts/install-gradle.sh +0 -0
  15. package/adapters/plugins/book299-4083-javascriptes5/scripts/install-maven.sh +0 -0
  16. package/adapters/powershell.sh +0 -0
  17. package/adapters/python.sh +0 -0
  18. package/adapters/shell.sh +0 -0
  19. package/adapters/swift.sh +0 -0
  20. package/adapters/typescript.sh +0 -0
  21. package/bin/xp-gate.js +29 -1
  22. package/gate-3.sh +0 -0
  23. package/gate-4.sh +0 -0
  24. package/gate-7.sh +0 -0
  25. package/gate-8.sh +0 -0
  26. package/gate-9.sh +0 -0
  27. package/hooks/adapter-common.sh +0 -0
  28. package/hooks/gate-3.sh +0 -0
  29. package/hooks/gate-4.sh +0 -0
  30. package/hooks/gate-7.sh +0 -0
  31. package/hooks/gate-8.sh +0 -0
  32. package/hooks/gate-9.sh +0 -0
  33. package/hooks/lib/now-ms.sh +33 -0
  34. package/hooks/pre-commit +63 -47
  35. package/hooks/pre-push +0 -0
  36. package/hooks/sprint-gate.sh +291 -0
  37. package/lib/__tests__/check-version.test.js +122 -0
  38. package/lib/__tests__/doctor.test.js +13 -15
  39. package/lib/__tests__/gate-audit.test.ts +38 -0
  40. package/lib/__tests__/phase-transition.test.js +512 -272
  41. package/lib/__tests__/retro.test.js +131 -0
  42. package/lib/__tests__/sprint-audit.test.js +207 -207
  43. package/lib/__tests__/sprint-status-rework.test.js +184 -0
  44. package/lib/check-version.js +9 -0
  45. package/lib/doctor-tui.js +268 -198
  46. package/lib/doctor.js +15 -3
  47. package/lib/gate-audit.ts +18 -7
  48. package/lib/gate-runner.js +77 -30
  49. package/lib/init.js +10 -0
  50. package/lib/language-tools.js +833 -833
  51. package/lib/mattpocock-license-verdict.json +7 -0
  52. package/lib/phase-transition.js +499 -249
  53. package/lib/retro.js +222 -0
  54. package/lib/shared-utils.js +7 -0
  55. package/lib/sprint-audit.js +235 -235
  56. package/lib/sprint-init.js +207 -0
  57. package/lib/sprint-state-migrator.js +1 -0
  58. package/lib/sprint-status.js +111 -0
  59. package/mock-policy/AGENTS.md +3 -3
  60. package/mutation/AGENTS.md +3 -3
  61. package/package.json +3 -2
  62. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  63. package/plugins/claude-code/bin/delphi-review-guard.sh +0 -0
  64. package/plugins/claude-code/bin/xp-gate-check +0 -0
  65. package/plugins/claude-code/skills/batch-grill-me/SKILL.md +15 -0
  66. package/plugins/claude-code/skills/delphi-review/AGENTS.md +3 -3
  67. package/plugins/claude-code/skills/delphi-review/SKILL.md +59 -4
  68. package/plugins/claude-code/skills/delphi-review/references/requirements.md +390 -0
  69. package/plugins/claude-code/skills/delphi-review/references/round-templates.md +67 -0
  70. package/plugins/claude-code/skills/domain-modeling/ADR-FORMAT.md +64 -0
  71. package/plugins/claude-code/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
  72. package/plugins/claude-code/skills/domain-modeling/SKILL.md +74 -0
  73. package/plugins/claude-code/skills/grill-with-docs/SKILL.md +7 -0
  74. package/plugins/claude-code/skills/grilling/SKILL.md +12 -0
  75. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
  76. package/plugins/claude-code/skills/sprint-flow/SKILL.md +168 -43
  77. package/plugins/claude-code/skills/sprint-flow/references/components/memory.md +1 -1
  78. package/plugins/claude-code/skills/sprint-flow/references/components/middleware.md +1 -1
  79. package/plugins/claude-code/skills/sprint-flow/references/components/skill-invocations.md +21 -20
  80. package/plugins/claude-code/skills/sprint-flow/references/components/tool-descriptions.md +22 -23
  81. package/plugins/claude-code/skills/sprint-flow/references/orchestration-rules.md +14 -12
  82. package/plugins/claude-code/skills/sprint-flow/references/phase-2-design.md +145 -133
  83. package/plugins/claude-code/skills/sprint-flow/references/phase-3-build.md +69 -2
  84. package/plugins/claude-code/skills/sprint-flow/references/phase-4-verify.md +93 -31
  85. package/plugins/claude-code/skills/sprint-flow/references/phase-5-ship.md +99 -14
  86. package/plugins/claude-code/skills/sprint-flow/references/phase-6-close.md +37 -5
  87. package/plugins/claude-code/skills/sprint-flow/references/phase-overview.md +58 -53
  88. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
  89. package/plugins/claude-code/skills/test-specification-alignment/SKILL.md +8 -3
  90. package/plugins/claude-code/skills/to-issues/SKILL.md +1 -1
  91. package/plugins/opencode/index.ts +1 -0
  92. package/plugins/opencode/package.json +1 -1
  93. package/plugins/opencode/skills/batch-grill-me/SKILL.md +15 -0
  94. package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
  95. package/plugins/opencode/skills/delphi-review/SKILL.md +59 -4
  96. package/plugins/opencode/skills/delphi-review/references/requirements.md +390 -0
  97. package/plugins/opencode/skills/delphi-review/references/round-templates.md +67 -0
  98. package/plugins/opencode/skills/domain-modeling/ADR-FORMAT.md +64 -0
  99. package/plugins/opencode/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
  100. package/plugins/opencode/skills/domain-modeling/SKILL.md +74 -0
  101. package/plugins/opencode/skills/grill-with-docs/SKILL.md +7 -0
  102. package/plugins/opencode/skills/grilling/SKILL.md +12 -0
  103. package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
  104. package/plugins/opencode/skills/sprint-flow/SKILL.md +168 -43
  105. package/plugins/opencode/skills/sprint-flow/references/components/memory.md +1 -1
  106. package/plugins/opencode/skills/sprint-flow/references/components/middleware.md +1 -1
  107. package/plugins/opencode/skills/sprint-flow/references/components/skill-invocations.md +21 -20
  108. package/plugins/opencode/skills/sprint-flow/references/components/tool-descriptions.md +22 -23
  109. package/plugins/opencode/skills/sprint-flow/references/orchestration-rules.md +14 -12
  110. package/plugins/opencode/skills/sprint-flow/references/phase-2-design.md +145 -133
  111. package/plugins/opencode/skills/sprint-flow/references/phase-3-build.md +69 -2
  112. package/plugins/opencode/skills/sprint-flow/references/phase-4-verify.md +93 -31
  113. package/plugins/opencode/skills/sprint-flow/references/phase-5-ship.md +99 -14
  114. package/plugins/opencode/skills/sprint-flow/references/phase-6-close.md +37 -5
  115. package/plugins/opencode/skills/sprint-flow/references/phase-overview.md +58 -53
  116. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
  117. package/plugins/opencode/skills/test-specification-alignment/SKILL.md +8 -3
  118. package/plugins/opencode/skills/to-issues/SKILL.md +1 -1
  119. package/plugins/qoder/agents/delphi-architecture.md +48 -48
  120. package/plugins/qoder/agents/delphi-feasibility.md +48 -48
  121. package/plugins/qoder/agents/delphi-technical.md +48 -48
  122. package/plugins/qoder/bin/sprint-flow-guard.sh +0 -0
  123. package/plugins/qoder/bin/xp-gate-check +0 -0
  124. package/plugins/qoder/plugin.json +1 -1
  125. package/plugins/qoder/skills/delphi-review/AGENTS.md +3 -3
  126. package/plugins/qoder/skills/sprint-flow/AGENTS.md +3 -3
  127. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
  128. package/principles/AGENTS.md +3 -3
  129. package/scripts/__tests__/check-mattpocock-license.test.js +229 -0
  130. package/scripts/__tests__/sync-package-content.test.js +50 -0
  131. package/scripts/check-mattpocock-license.cjs +302 -0
  132. package/scripts/delphi-external-review.cjs +432 -432
  133. package/scripts/migrate-audit-anomalies.mjs +78 -0
  134. package/scripts/sync-package-content.js +52 -5
  135. package/skills/batch-grill-me/SKILL.md +15 -0
  136. package/skills/delphi-review/AGENTS.md +3 -3
  137. package/skills/delphi-review/SKILL.md +59 -4
  138. package/skills/delphi-review/references/requirements.md +390 -0
  139. package/skills/delphi-review/references/round-templates.md +67 -0
  140. package/skills/domain-modeling/ADR-FORMAT.md +64 -0
  141. package/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
  142. package/skills/domain-modeling/SKILL.md +74 -0
  143. package/skills/grill-with-docs/SKILL.md +7 -0
  144. package/skills/grilling/SKILL.md +12 -0
  145. package/skills/sprint-flow/AGENTS.md +3 -3
  146. package/skills/sprint-flow/SKILL.md +168 -43
  147. package/skills/sprint-flow/references/components/memory.md +1 -1
  148. package/skills/sprint-flow/references/components/middleware.md +1 -1
  149. package/skills/sprint-flow/references/components/skill-invocations.md +21 -20
  150. package/skills/sprint-flow/references/components/tool-descriptions.md +22 -23
  151. package/skills/sprint-flow/references/orchestration-rules.md +14 -12
  152. package/skills/sprint-flow/references/phase-2-design.md +145 -133
  153. package/skills/sprint-flow/references/phase-3-build.md +69 -2
  154. package/skills/sprint-flow/references/phase-4-verify.md +93 -31
  155. package/skills/sprint-flow/references/phase-5-ship.md +99 -14
  156. package/skills/sprint-flow/references/phase-6-close.md +37 -5
  157. package/skills/sprint-flow/references/phase-overview.md +58 -53
  158. package/skills/test-specification-alignment/AGENTS.md +3 -3
  159. package/skills/test-specification-alignment/SKILL.md +8 -3
  160. package/skills/to-issues/SKILL.md +1 -1
  161. package/adapters/gate-3.sh +0 -68
  162. package/adapters/gate-4.sh +0 -80
  163. package/adapters/gate-7.sh +0 -45
  164. package/adapters/gate-8.sh +0 -56
  165. package/adapters/gate-9.sh +0 -104
@@ -163,6 +163,43 @@ describe('check-version.js — REQ-001-01', () => {
163
163
  // Verify clearCache handles null cachePath gracefully
164
164
  expect(() => mod.clearCache()).not.toThrow();
165
165
  });
166
+
167
+ // AC-001-05-A: writeCache validates semver before persisting
168
+ it('rejects invalid version: null → cache NOT written', () => {
169
+ const cpPath = require('path').join(tmpHome, '.xp-gate', 'version-cache.json');
170
+ expect(fs.existsSync(cpPath)).toBe(false);
171
+ });
172
+
173
+ it('rejects invalid version: empty string → cache NOT written', () => {
174
+ const cpPath = require('path').join(tmpHome, '.xp-gate', 'version-cache.json');
175
+ expect(fs.existsSync(cpPath)).toBe(false);
176
+ });
177
+
178
+ it('rejects invalid version: "not-a-version" → cache NOT written', () => {
179
+ const cpPath = require('path').join(tmpHome, '.xp-gate', 'version-cache.json');
180
+ expect(fs.existsSync(cpPath)).toBe(false);
181
+ });
182
+
183
+ it('accepts valid version: "0.18.0" → cache written', () => {
184
+ // Simulate what writeCache does — write valid semver, then read back
185
+ const cpDir = require('path').join(tmpHome, '.xp-gate');
186
+ const cpFile = require('path').join(cpDir, 'version-cache.json');
187
+ fs.mkdirSync(cpDir, { recursive: true });
188
+ fs.writeFileSync(cpFile, JSON.stringify({ ts: Date.now(), version: '0.18.0', publishedAt: '' }));
189
+ expect(fs.existsSync(cpFile)).toBe(true);
190
+ const cached = JSON.parse(fs.readFileSync(cpFile, 'utf8'));
191
+ expect(cached.version).toBe('0.18.0');
192
+ });
193
+
194
+ it('accepts valid version: "1.0.0" → cache written', () => {
195
+ const cpDir = require('path').join(tmpHome, '.xp-gate');
196
+ const cpFile = require('path').join(cpDir, 'version-cache.json');
197
+ fs.mkdirSync(cpDir, { recursive: true });
198
+ fs.writeFileSync(cpFile, JSON.stringify({ ts: Date.now(), version: '1.0.0', publishedAt: '' }));
199
+ expect(fs.existsSync(cpFile)).toBe(true);
200
+ const cached = JSON.parse(fs.readFileSync(cpFile, 'utf8'));
201
+ expect(cached.version).toBe('1.0.0');
202
+ });
166
203
  });
167
204
 
168
205
  // ──────────────────────────────────────────
@@ -253,6 +290,91 @@ describe('check-version.js — REQ-001-01', () => {
253
290
  // Without network: null. With network (npm registry proxy): object.
254
291
  expect(r === null || (typeof r.latest === 'string')).toBe(true);
255
292
  });
293
+
294
+ // AC-001-02-A: writeCache rejects non-semver versions from registry response
295
+ function evictCache() {
296
+ const resolved = require.resolve('../check-version');
297
+ const libPrefix = resolved.replace(/check-version\.js$/, '');
298
+ Object.keys(require.cache).forEach(key => {
299
+ if (key.startsWith(libPrefix)) delete require.cache[key];
300
+ });
301
+ delete require.cache[resolved];
302
+ }
303
+
304
+ function withMockedHttpsBox(latestVersion, fn) {
305
+ const fsm = require('fs');
306
+ const osm = require('os');
307
+ const cpPath = require('path').join(osm.homedir(), '.xp-gate', 'version-cache.json');
308
+ if (fsm.existsSync(cpPath)) {
309
+ try { fsm.unlinkSync(cpPath); } catch { }
310
+ }
311
+ evictCache();
312
+ const https = require('https');
313
+ const saved = https.get;
314
+ const body = JSON.stringify({ latest: latestVersion });
315
+ https.get = (_url, _opts, cb) => {
316
+ const callback = typeof _opts === 'function' ? _opts : cb;
317
+ if (!callback) return { on: () => undefined, destroy: () => undefined };
318
+ const mockRes = {
319
+ statusCode: 200,
320
+ on: (evt, handler) => {
321
+ if (evt === 'data') handler(body);
322
+ if (evt === 'end') handler();
323
+ return mockRes;
324
+ },
325
+ };
326
+ callback(mockRes);
327
+ return { on: () => undefined, destroy: () => undefined };
328
+ };
329
+ try {
330
+ const m = require('../check-version');
331
+ return fn(m, cpPath);
332
+ } finally {
333
+ https.get = saved;
334
+ }
335
+ }
336
+
337
+ it('rejects null version from registry — cache NOT written', async () => {
338
+ await withMockedHttpsBox(null, async (m, cpPath) => {
339
+ const r = await m.getRemoteVersion('@test/pkg');
340
+ // null latest is rejected by getRemoteVersion itself (typeof !== 'string')
341
+ // before even reaching writeCache
342
+ expect(r).toBeNull();
343
+ expect(require('fs').existsSync(cpPath)).toBe(false);
344
+ });
345
+ });
346
+
347
+ it('rejects empty version from registry — cache NOT written', async () => {
348
+ await withMockedHttpsBox('', async (m, cpPath) => {
349
+ const r = await m.getRemoteVersion('@test/pkg');
350
+ // getRemoteVersion passes empty string through (it IS a string),
351
+ // but writeCache's semver validation rejects it
352
+ expect(r).not.toBeNull();
353
+ expect(r.latest).toBe('');
354
+ expect(require('fs').existsSync(cpPath)).toBe(false);
355
+ });
356
+ });
357
+
358
+ it('rejects non-semver version from registry — cache NOT written', async () => {
359
+ await withMockedHttpsBox('not-a-version', async (m, cpPath) => {
360
+ const r = await m.getRemoteVersion('@test/pkg');
361
+ // getRemoteVersion passes it through, but writeCache rejects it
362
+ expect(r).not.toBeNull();
363
+ expect(r.latest).toBe('not-a-version');
364
+ expect(require('fs').existsSync(cpPath)).toBe(false);
365
+ });
366
+ });
367
+
368
+ it('accepts valid semver from registry — cache written', async () => {
369
+ await withMockedHttpsBox('0.18.0', async (m, cpPath) => {
370
+ const r = await m.getRemoteVersion('@test/pkg');
371
+ expect(r).not.toBeNull();
372
+ expect(r.latest).toBe('0.18.0');
373
+ expect(require('fs').existsSync(cpPath)).toBe(true);
374
+ const cached = JSON.parse(require('fs').readFileSync(cpPath, 'utf8'));
375
+ expect(cached.version).toBe('0.18.0');
376
+ });
377
+ });
256
378
  });
257
379
 
258
380
  // ──────────────────────────────────────────
@@ -31,6 +31,12 @@ describe('doctor', () => {
31
31
  originalDetectDepsTools = [...detectDeps.GATE_CLI_TOOLS];
32
32
  detectDeps.GATE_CLI_TOOLS.length = 0;
33
33
  }
34
+ // Mock checkBash to return success by default (tests override as needed)
35
+ detectDeps.checkBash = () => ({
36
+ ok: true,
37
+ path: 'bash',
38
+ version: '5.1.16',
39
+ });
34
40
  });
35
41
 
36
42
  afterAll(() => {
@@ -217,9 +223,6 @@ describe('doctor', () => {
217
223
  if (cmd === 'git --version') {
218
224
  return 'git version 2.39.0\n';
219
225
  }
220
- if (cmd === 'bash --version') {
221
- return 'GNU bash, version 5.1.16\n';
222
- }
223
226
  return '';
224
227
  };
225
228
 
@@ -237,8 +240,6 @@ describe('doctor', () => {
237
240
  callback(null, 'v20.0.0\n', '');
238
241
  } else if (cmd === 'git --version') {
239
242
  callback(null, 'git version 2.39.0\n', '');
240
- } else if (cmd === 'bash --version') {
241
- callback(null, 'GNU bash, version 5.1.16\n', '');
242
243
  } else {
243
244
  callback(null, '', '');
244
245
  }
@@ -401,9 +402,6 @@ describe('doctor', () => {
401
402
  if (cmd === 'git --version') {
402
403
  return 'git version 2.39.0\n';
403
404
  }
404
- if (cmd === 'bash --version') {
405
- return 'GNU bash, version 5.1.16\n';
406
- }
407
405
  return '';
408
406
  };
409
407
  // Async mock matching execSync above
@@ -417,8 +415,6 @@ describe('doctor', () => {
417
415
  callback(null, 'v20.0.0\n', '');
418
416
  } else if (cmd === 'git --version') {
419
417
  callback(null, 'git version 2.39.0\n', '');
420
- } else if (cmd === 'bash --version') {
421
- callback(null, 'GNU bash, version 5.1.16\n', '');
422
418
  } else {
423
419
  callback(null, '', '');
424
420
  }
@@ -502,9 +498,6 @@ describe('doctor', () => {
502
498
  if (cmd === 'git --version') {
503
499
  return 'git version 2.39.0\n';
504
500
  }
505
- if (cmd === 'bash --version') {
506
- return 'GNU bash, version 5.1.16\n';
507
- }
508
501
  return '';
509
502
  };
510
503
  // Async exec mock for execWithTimeout (util.promisify(exec) in doctor.js)
@@ -518,8 +511,6 @@ describe('doctor', () => {
518
511
  callback(null, 'v20.0.0\n', '');
519
512
  } else if (cmd === 'git --version') {
520
513
  callback(null, 'git version 2.39.0\n', '');
521
- } else if (cmd === 'bash --version') {
522
- callback(null, 'GNU bash, version 5.1.16\n', '');
523
514
  } else {
524
515
  callback(null, '', '');
525
516
  }
@@ -569,6 +560,10 @@ describe('doctor', () => {
569
560
  it('detects missing environment dependencies', async () => {
570
561
  setupLocalInstall();
571
562
  seedVersionCache();
563
+ // Make checkBash fail for this test (detect-deps module already loaded + mocked in beforeAll)
564
+ const detectDeps = require('../detect-deps.js');
565
+ const origCheckBash = detectDeps.checkBash;
566
+ detectDeps.checkBash = () => ({ ok: false, message: 'Not found' });
572
567
  mockExecFail();
573
568
  const { doctor } = require('../doctor');
574
569
 
@@ -578,6 +573,9 @@ describe('doctor', () => {
578
573
  expect(logSpy).toHaveBeenCalledWith(
579
574
  expect.stringContaining('Not found')
580
575
  );
576
+
577
+ // Restore
578
+ detectDeps.checkBash = origCheckBash;
581
579
  });
582
580
 
583
581
  // === Issue #186: Version mismatch detection ===
@@ -201,6 +201,44 @@ describe('gate-audit', () => {
201
201
  });
202
202
  });
203
203
 
204
+ describe('duration_anomaly handling', () => {
205
+ it('should exclude duration_anomaly entries from avg_ms calculation', () => {
206
+ const repoRoot = join(TEST_DIR, 'anomaly-avg-test');
207
+ // 2 normal entries: 100ms + 200ms → avg 150ms
208
+ appendAuditEntry(makeEntry({ repo_path: repoRoot, gate_id: 'gate-1', duration_ms: 100 }), repoRoot);
209
+ appendAuditEntry(makeEntry({ repo_path: repoRoot, gate_id: 'gate-1', duration_ms: 200 }), repoRoot);
210
+ // 1 anomalous entry: should be excluded from avg
211
+ appendAuditEntry(
212
+ makeEntry({ repo_path: repoRoot, gate_id: 'gate-1', duration_ms: 1784536915891, duration_anomaly: true }),
213
+ repoRoot,
214
+ );
215
+
216
+ const stats = computeStats(repoRoot);
217
+ const g1 = stats.find(s => s.gate_id === 'gate-1')!;
218
+ // avg should be (100+200)/2 = 150, NOT (100+200+1784536915891)/3
219
+ expect(g1.avg_ms).toBe(150);
220
+ });
221
+
222
+ it('should count anomaly entries separately in stats output', () => {
223
+ const repoRoot = join(TEST_DIR, 'anomaly-count-test');
224
+ appendAuditEntry(makeEntry({ repo_path: repoRoot, gate_id: 'gate-5', duration_ms: 500 }), repoRoot);
225
+ appendAuditEntry(
226
+ makeEntry({ repo_path: repoRoot, gate_id: 'gate-5', duration_ms: 9999999999999, duration_anomaly: true }),
227
+ repoRoot,
228
+ );
229
+ appendAuditEntry(
230
+ makeEntry({ repo_path: repoRoot, gate_id: 'gate-5', duration_ms: 8888888888888, duration_anomaly: true }),
231
+ repoRoot,
232
+ );
233
+
234
+ const stats = computeStats(repoRoot);
235
+ const g5 = stats.find(s => s.gate_id === 'gate-5')!;
236
+ expect(g5.anomaly_count).toBe(2);
237
+ // avg_ms only from the 1 normal entry
238
+ expect(g5.avg_ms).toBe(500);
239
+ });
240
+ });
241
+
204
242
  describe('CLI helpers', () => {
205
243
  it('should parse CLI options and skip dangling flags', () => {
206
244
  expect(parseCliOptions(['--gate-id', 'gate-1', '--passed', 'true', '--dangling'])).toEqual({