qa360 2.2.12 → 2.2.14

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.
@@ -32,16 +32,28 @@ export class OllamaProvider {
32
32
  }
33
33
  async isAvailable() {
34
34
  try {
35
- // Use timeout with AbortSignal - wrap for compatibility
35
+ // Use a longer timeout (15 seconds) to accommodate slower systems
36
+ // Ollama can take time to respond, especially on first run or with many models
36
37
  const controller = new AbortController();
37
- const timeoutId = setTimeout(() => controller.abort(), 5000);
38
+ const timeoutId = setTimeout(() => controller.abort(), 15000);
38
39
  const response = await fetch(`${this.baseUrl}/api/tags`, {
39
40
  signal: controller.signal,
40
41
  });
41
42
  clearTimeout(timeoutId);
42
43
  return response.ok;
43
44
  }
44
- catch {
45
+ catch (error) {
46
+ // Log the error for debugging (will be visible in verbose mode)
47
+ if (error instanceof Error) {
48
+ if (error.name === 'AbortError') {
49
+ // Timeout - Ollama is slow to respond
50
+ console.debug(`[Ollama] Connection to ${this.baseUrl} timed out after 15s`);
51
+ }
52
+ else {
53
+ // Other error (ECONNREFUSED, ENOTFOUND, etc.)
54
+ console.debug(`[Ollama] Connection error: ${error.message}`);
55
+ }
56
+ }
45
57
  return false;
46
58
  }
47
59
  }
@@ -50,9 +50,12 @@ export function generateSelector(element) {
50
50
  if (className && !isGenericClass(className)) {
51
51
  const classes = className.split(' ').filter(c => !isGenericClass(c));
52
52
  if (classes.length > 0) {
53
- const classSelector = classes.map(c => `.${escapeCss(c)}`).join('');
54
- // Qualify with tag if available
55
- return tagName ? `${tagName}${classSelector}` : classSelector;
53
+ // Smart class selection: prioritize meaningful classes
54
+ const meaningfulClasses = selectMeaningfulClasses(classes);
55
+ const classSelector = meaningfulClasses.map(c => `.${escapeCss(c)}`).join('');
56
+ // Limit total selector length
57
+ const qualifiedSelector = tagName ? `${tagName}${classSelector}` : classSelector;
58
+ return truncateSelector(qualifiedSelector, 200);
56
59
  }
57
60
  }
58
61
  // 7. Tag + text content (for buttons, links)
@@ -169,6 +172,72 @@ function isSelectorStable(selector) {
169
172
  function escapeCss(str) {
170
173
  return str.replace(/(["\\])/g, '\\$1').replace(/"/g, '\\"');
171
174
  }
175
+ /**
176
+ * Select the most meaningful classes from a class list
177
+ * Prioritizes: unique identifiers, component names, action-oriented classes
178
+ * Deprioritizes: utility classes (spacing, colors, sizing)
179
+ */
180
+ function selectMeaningfulClasses(classes) {
181
+ // Tailwind and other utility class patterns to deprioritize
182
+ const utilityPatterns = [
183
+ /^(p|m|px|py|pt|pb|pl|pr)-/, // spacing
184
+ /^(mt|mb|ml|mr)-/, // margin spacing
185
+ /^(text|bg|border)-/, // colors
186
+ /^(w|h)-/, // sizing
187
+ /^(flex|grid|block|inline)/, // display
188
+ /^(justify|items|self)/, // flexbox/grid
189
+ /^(rounded|shadow|opacity)/, // styling
190
+ /^gap-/, // flex gap
191
+ /^(line-)?height-/, // height
192
+ /^(font|weight|size)/, // typography
193
+ ];
194
+ // Score each class by "meaningfulness"
195
+ const scored = classes.map(cls => {
196
+ let score = 50; // base score
197
+ // Bonus: component-like names (contain meaningful words)
198
+ if (/[A-Z][a-z]+/.test(cls))
199
+ score += 30; // CamelCase components
200
+ if (/button|btn|input|form|card|modal|nav|header|footer|sidebar|menu|dropdown/i.test(cls))
201
+ score += 25;
202
+ // Bonus: contains numbers (likely unique)
203
+ if (/\d+/.test(cls))
204
+ score += 15;
205
+ // Penalty: utility classes
206
+ for (const pattern of utilityPatterns) {
207
+ if (pattern.test(cls)) {
208
+ score -= 40;
209
+ break;
210
+ }
211
+ }
212
+ return { class: cls, score };
213
+ });
214
+ // Sort by score (descending) and take top 5
215
+ scored.sort((a, b) => b.score - a.score);
216
+ return scored.slice(0, 5).map(s => s.class);
217
+ }
218
+ /**
219
+ * Truncate selector to max length while keeping it valid
220
+ * Preserves the most specific parts
221
+ */
222
+ function truncateSelector(selector, maxLength) {
223
+ if (selector.length <= maxLength)
224
+ return selector;
225
+ // For class-based selectors, remove classes from the middle
226
+ const classMatch = selector.match(/^(\w+)((\.[\w-]+)+)$/);
227
+ if (classMatch) {
228
+ const tag = classMatch[1];
229
+ const classes = classMatch[2].split('.').filter(c => c); // Remove empty strings
230
+ // Keep first 2 and last 2 classes (most specific)
231
+ if (classes.length > 4) {
232
+ const kept = [...classes.slice(0, 2), ...classes.slice(-2)];
233
+ const truncated = `${tag}.${kept.join('.')}`;
234
+ if (truncated.length <= maxLength)
235
+ return truncated;
236
+ }
237
+ }
238
+ // Final fallback: just truncate (may not be valid CSS but prevents overflow)
239
+ return selector.substring(0, maxLength - 3) + '...';
240
+ }
172
241
  /**
173
242
  * Optimize selector for resiliency
174
243
  */
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * QA360 Crawler Pack Generator
3
3
  *
4
- * Crawls a website and generates a complete pack.yml with E2E tests
4
+ * Crawls a website and generates a complete pack.yml in v2 format
5
5
  */
6
6
  import type { CrawlOptions, CrawlResult } from '../crawler/index.js';
7
7
  /**
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * QA360 Crawler Pack Generator
3
3
  *
4
- * Crawls a website and generates a complete pack.yml with E2E tests
4
+ * Crawls a website and generates a complete pack.yml in v2 format
5
5
  */
6
6
  import { writeFileSync, mkdirSync, existsSync } from 'fs';
7
7
  import { resolve, dirname } from 'path';
@@ -56,165 +56,53 @@ export async function generatePackFromCrawl(options) {
56
56
  }
57
57
  }
58
58
  /**
59
- * Generate pack.yml YAML from crawl result
59
+ * Generate pack.yml YAML from crawl result (v2 format)
60
60
  */
61
61
  function generatePackFromCrawlResult(crawlResult, options) {
62
62
  const { siteMap, userJourneys, forms } = crawlResult;
63
63
  const packName = options.packName || `${new URL(options.baseUrl).hostname.replace(/\./g, '-')}-tests`;
64
- // Build gates array
65
- const gates = ['ui'];
66
- if (options.includeA11y)
67
- gates.push('a11y');
68
- // Build UI tests from user journeys
69
- const uiTests = userJourneys.map((journey, index) => ({
70
- name: options.journeyNames?.[journey.name] || journey.name,
71
- description: journey.description,
72
- path: new URL(journey.entryPoint).pathname,
73
- steps: journey.steps.map((step, stepIndex) => journeyStepToUiTestStep(step, stepIndex)),
74
- }));
75
- // Also add form-based tests
76
- for (const form of forms) {
77
- if (form.purpose !== 'login' && form.purpose !== 'signup') {
78
- uiTests.push({
79
- name: `${form.purpose.charAt(0).toUpperCase() + form.purpose.slice(1)} Form Test`,
80
- description: `Test the ${form.purpose} form`,
81
- path: siteMap.pages.find(p => p.elements.forms.includes(form))?.path || '/',
82
- steps: generateFormTestSteps(form),
83
- });
84
- }
85
- }
86
- // Generate YAML
87
- let yaml = `# QA360 Pack - Generated by crawler
64
+ // Build full URLs for pages
65
+ const baseUrl = options.baseUrl.replace(/\/$/, '');
66
+ const pages = siteMap.pages.map(p => `${baseUrl}${p.path.startsWith('/') ? '' : '/'}${p.path}`);
67
+ // Generate YAML (v2 format)
68
+ let yaml = `# QA360 Pack v2 - Generated by crawler
88
69
  # Source: ${options.baseUrl}
89
70
  # Generated: ${new Date().toISOString()}
90
71
 
91
- version: 1
72
+ version: 2
92
73
  name: "${packName}"
93
74
  description: "Auto-generated E2E tests for ${new URL(options.baseUrl).hostname}"
94
75
 
95
- gates:
96
- ${gates.map(g => ` - ${g}`).join('\n')}
97
-
98
- targets:
99
- web:
100
- baseUrl: "${options.baseUrl}"
101
- headless: true
102
- screenshot: "only-on-fail"
103
- video: "retain-on-fail"
104
-
105
- # Pages discovered by crawler
106
- pages:
107
- ${siteMap.pages.map(p => ` - "${p.path}"`).join('\n')}
76
+ execution:
77
+ default_timeout: 30000
78
+ default_retries: 2
79
+ on_failure: continue
108
80
 
109
- # E2E UI tests generated from discovered user journeys
110
- uiTests:
111
- ${uiTests.map(test => generateUiTestYaml(test)).join('\n')}
81
+ gates:
82
+ ui:
83
+ adapter: playwright-ui
84
+ enabled: true
85
+ config:
86
+ baseUrl: "${options.baseUrl}"
87
+ pages:
88
+ ${pages.map(p => ` - "${p}"`).join('\n')}
112
89
  `;
113
- if (siteMap.metadata.pagesCrawled > 0) {
90
+ // Add a11y gate if requested
91
+ if (options.includeA11y) {
114
92
  yaml += `
115
- budgets:
116
- a11y_min: 80
117
- perf_p95_ms: ${Math.round(siteMap.metadata.avgLoadTime * 1.5)}
93
+ a11y:
94
+ adapter: playwright-ui
95
+ enabled: true
96
+ config:
97
+ baseUrl: "${options.baseUrl}"
98
+ pages:
99
+ ${pages.map(p => ` - "${p}"`).join('\n')}
100
+ budgets:
101
+ a11y_min: 80
118
102
  `;
119
103
  }
120
104
  return yaml;
121
105
  }
122
- /**
123
- * Convert journey step to UI test step
124
- */
125
- function journeyStepToUiTestStep(step, index) {
126
- const uiStep = {
127
- order: index + 1,
128
- action: step.action,
129
- description: step.description,
130
- selector: step.selector,
131
- value: step.value,
132
- wait: 500,
133
- };
134
- if (step.expected) {
135
- uiStep.expected = {};
136
- if (step.expected.url)
137
- uiStep.expected.url = step.expected.url;
138
- if (step.expected.visible)
139
- uiStep.expected.visible = step.expected.visible;
140
- }
141
- return uiStep;
142
- }
143
- /**
144
- * Generate test steps for a form
145
- */
146
- function generateFormTestSteps(form) {
147
- const steps = [];
148
- // Navigate to form
149
- steps.push({
150
- order: 1,
151
- action: 'navigate',
152
- description: 'Navigate to form',
153
- value: form.selector,
154
- });
155
- // Fill form fields
156
- for (const field of form.fields) {
157
- if (field.required) {
158
- const step = {
159
- order: steps.length + 1,
160
- action: field.inputType === 'select-one' ? 'select' : 'fill',
161
- description: `Fill ${field.name || field.inputType}`,
162
- selector: field.selector,
163
- };
164
- if (field.inputType === 'select-one' && field.options && field.options.length > 0) {
165
- step.value = field.options[0];
166
- }
167
- else if (field.inputType === 'email') {
168
- step.value = 'test@example.com';
169
- }
170
- else if (field.inputType === 'tel') {
171
- step.value = '+1234567890';
172
- }
173
- else if (field.inputType === 'checkbox') {
174
- step.action = 'check';
175
- }
176
- else {
177
- step.value = 'Test value';
178
- }
179
- steps.push(step);
180
- }
181
- }
182
- // Submit form
183
- if (form.submitButton) {
184
- steps.push({
185
- order: steps.length + 1,
186
- action: 'click',
187
- description: 'Submit form',
188
- selector: form.submitButton.selector,
189
- wait: 1000,
190
- });
191
- }
192
- return steps;
193
- }
194
- /**
195
- * Generate YAML for a UI test definition
196
- */
197
- function generateUiTestYaml(test) {
198
- const steps = test.steps.map(step => {
199
- let yaml = ` - action: ${step.action}`;
200
- if (step.description)
201
- yaml += `\n description: "${step.description}"`;
202
- if (step.selector)
203
- yaml += `\n selector: "${step.selector}"`;
204
- if (step.value)
205
- yaml += `\n value: "${step.value}"`;
206
- if (step.expected)
207
- yaml += `\n expected: ${JSON.stringify(step.expected)}`;
208
- if (step.wait)
209
- yaml += `\n wait: ${step.wait}`;
210
- return yaml;
211
- }).join('\n');
212
- return ` - name: "${test.name}"
213
- description: "${test.description || ''}"
214
- path: "${test.path || '/'}"
215
- steps:
216
- ${steps}`;
217
- }
218
106
  /**
219
107
  * Quick crawl - generates pack without detailed analysis
220
108
  */
@@ -22,8 +22,8 @@ export class PackValidator {
22
22
  const __dirname = dirname(__filename);
23
23
  // Check multiple paths to handle both development and bundled npm environments
24
24
  // From cli/dist/core/pack/validator.js, we need to reach core/schemas/pack.schema.json
25
- const path1 = join(__dirname, '../../schemas/pack.schema.json'); // bundled (cli/dist/core/schemas/)
26
- const path2 = join(__dirname, '../../../../core/schemas/pack.schema.json'); // npm package (core/schemas/)
25
+ const path1 = join(__dirname, '../../schemas/pack.schema.json'); // bundled (cli/dist/core/schemas/)
26
+ const path2 = join(__dirname, '../../../../core/schemas/pack.schema.json'); // npm package (core/schemas/)
27
27
  let schemaPath = existsSync(path1) ? path1 : path2;
28
28
  // Final fallback: throw error if no schema found
29
29
  if (!existsSync(schemaPath)) {
@@ -41,12 +41,17 @@ export declare class PackMigrator {
41
41
  * Migrate targets to gate configs
42
42
  */
43
43
  private migrateTargetsToGates;
44
+ /**
45
+ * Transform relative page paths to full URLs
46
+ */
47
+ private transformPagesToFullUrls;
44
48
  /**
45
49
  * Find a secret in environment variables by trying common names
46
50
  */
47
51
  private findSecretInEnv;
48
52
  /**
49
53
  * Sanitize gate name for v2 format
54
+ * Handles both strings and objects (from YAML parsing edge cases)
50
55
  */
51
56
  private sanitizeGateName;
52
57
  /**
@@ -395,14 +395,57 @@ export class PackMigrator {
395
395
  gate.config.baseUrl = v1Pack.targets.api.baseUrl;
396
396
  gate.config.smoke = v1Pack.targets.api.smoke;
397
397
  }
398
- // Web target
399
- if (v1Pack.targets.web && v2Pack.gates['ui']) {
400
- const gate = v2Pack.gates['ui'];
401
- gate.config = gate.config || {};
402
- gate.config.baseUrl = v1Pack.targets.web.baseUrl;
403
- gate.config.pages = v1Pack.targets.web.pages;
398
+ // Web target - migrate to both ui and a11y gates
399
+ if (v1Pack.targets.web) {
400
+ const baseUrl = v1Pack.targets.web.baseUrl;
401
+ const pages = this.transformPagesToFullUrls(v1Pack.targets.web.pages || [], baseUrl);
402
+ // ui gate
403
+ if (v2Pack.gates['ui']) {
404
+ const gate = v2Pack.gates['ui'];
405
+ gate.config = gate.config || {};
406
+ gate.config.baseUrl = baseUrl;
407
+ gate.config.pages = pages;
408
+ }
409
+ // a11y gate - also needs pages and baseUrl
410
+ if (v2Pack.gates['a11y']) {
411
+ const gate = v2Pack.gates['a11y'];
412
+ gate.config = gate.config || {};
413
+ gate.config.baseUrl = baseUrl;
414
+ gate.config.pages = pages;
415
+ }
404
416
  }
405
417
  }
418
+ /**
419
+ * Transform relative page paths to full URLs
420
+ */
421
+ transformPagesToFullUrls(pages, baseUrl) {
422
+ if (!pages || pages.length === 0)
423
+ return [];
424
+ if (!baseUrl)
425
+ return pages;
426
+ const base = baseUrl.replace(/\/$/, '');
427
+ return pages.map((page) => {
428
+ // Handle string pages (e.g., "/", "/products")
429
+ if (typeof page === 'string') {
430
+ if (page.startsWith('http')) {
431
+ return page; // Already a full URL
432
+ }
433
+ return `${base}${page.startsWith('/') ? '' : '/'}${page}`;
434
+ }
435
+ // Handle object pages (e.g., { url: "/", ... })
436
+ if (page && typeof page === 'object' && page.url) {
437
+ const url = page.url;
438
+ if (url.startsWith('http')) {
439
+ return page; // Already a full URL
440
+ }
441
+ return {
442
+ ...page,
443
+ url: `${base}${url.startsWith('/') ? '' : '/'}${url}`
444
+ };
445
+ }
446
+ return page;
447
+ });
448
+ }
406
449
  /**
407
450
  * Find a secret in environment variables by trying common names
408
451
  */
@@ -424,8 +467,33 @@ export class PackMigrator {
424
467
  }
425
468
  /**
426
469
  * Sanitize gate name for v2 format
470
+ * Handles both strings and objects (from YAML parsing edge cases)
427
471
  */
428
472
  sanitizeGateName(gate) {
473
+ // Handle edge case where gate might be an object from YAML parsing
474
+ if (typeof gate !== 'string') {
475
+ // If gate is an object, try to extract a name
476
+ if (gate && typeof gate === 'object') {
477
+ // Try common property names
478
+ const possibleName = gate.name || gate.id || gate.gate;
479
+ if (typeof possibleName === 'string') {
480
+ gate = possibleName;
481
+ }
482
+ else {
483
+ // Fallback: stringify the object keys
484
+ const keys = Object.keys(gate);
485
+ if (keys.length === 1) {
486
+ gate = keys[0];
487
+ }
488
+ else {
489
+ throw new Error(`Invalid gate format: expected string, got object with keys: ${keys.join(', ')}`);
490
+ }
491
+ }
492
+ }
493
+ else {
494
+ throw new Error(`Invalid gate format: expected string, got ${typeof gate}`);
495
+ }
496
+ }
429
497
  // Map v1 gate names to v2 conventions
430
498
  const nameMap = {
431
499
  'api_smoke': 'smoke',
@@ -370,7 +370,8 @@ export class PackValidatorV2 {
370
370
  // Validate auth profile reference
371
371
  if (gate.auth) {
372
372
  // Will be validated against profiles in auth section
373
- if (!gate.adapter?.includes('api') && !gate.adapter?.includes('ui')) {
373
+ const adapter = typeof gate.adapter === 'string' ? gate.adapter : '';
374
+ if (adapter && !adapter.includes('api') && !adapter.includes('ui')) {
374
375
  warnings.push({
375
376
  code: 'QP2V019',
376
377
  message: `Auth profile specified but adapter may not support auth`,
@@ -593,7 +594,7 @@ export class PackValidatorV2 {
593
594
  return { errors, warnings };
594
595
  }
595
596
  // Check for API gate with api auth but no auth profile
596
- const apiGates = Object.entries(pack.gates).filter(([_, g]) => g.adapter?.includes('api'));
597
+ const apiGates = Object.entries(pack.gates).filter(([_, g]) => typeof g.adapter === 'string' && g.adapter.includes('api'));
597
598
  if (apiGates.length > 0 && !pack.auth?.api && !pack.auth?.profiles) {
598
599
  warnings.push({
599
600
  code: 'QP2V034',
@@ -603,7 +604,7 @@ export class PackValidatorV2 {
603
604
  });
604
605
  }
605
606
  // Check for UI gate with ui auth but no auth profile
606
- const uiGates = Object.entries(pack.gates).filter(([_, g]) => g.adapter?.includes('ui'));
607
+ const uiGates = Object.entries(pack.gates).filter(([_, g]) => typeof g.adapter === 'string' && g.adapter.includes('ui'));
607
608
  if (uiGates.length > 0 && !pack.auth?.ui && !pack.auth?.profiles) {
608
609
  warnings.push({
609
610
  code: 'QP2V035',
@@ -881,10 +881,21 @@ export class Phase3Runner {
881
881
  * Run UI gate (includes basic accessibility)
882
882
  */
883
883
  async runUiGate() {
884
- const target = this.getTargetWeb();
884
+ let target = this.getTargetWeb();
885
885
  if (!target) {
886
886
  throw new Error('UI gate requires targets.web configuration or gate config with baseUrl');
887
887
  }
888
+ // Transform relative pages to full URLs for v1 packs
889
+ if (target.pages && target.baseUrl && !this.isPackV2(this.pack)) {
890
+ const baseUrl = target.baseUrl.replace(/\/$/, '');
891
+ target.pages = target.pages.map((p) => {
892
+ const pageStr = typeof p === 'object' && p.url ? p.url : p;
893
+ if (typeof pageStr === 'string' && !pageStr.startsWith('http')) {
894
+ return `${baseUrl}${pageStr.startsWith('/') ? '' : '/'}${pageStr}`;
895
+ }
896
+ return p;
897
+ });
898
+ }
888
899
  const credentials = await this.getCredentialsForGate('ui');
889
900
  const adapter = new PlaywrightUiAdapter();
890
901
  return await adapter.runSmokeTests({
package/cli/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qa360",
3
- "version": "2.2.12",
3
+ "version": "2.2.14",
4
4
  "description": "QA360 Proof CLI - Quality as Cryptographic Proof",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qa360",
3
- "version": "2.2.12",
3
+ "version": "2.2.14",
4
4
  "description": "Transform software testing into verifiable, signed, and traceable proofs",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@9.12.2",
@@ -1,101 +0,0 @@
1
- /**
2
- * Coverage Analyzer
3
- *
4
- * Analyzes coverage data to provide insights, trends, and recommendations.
5
- */
6
- import type { FileCoverage, CoverageMetrics, CoverageResult, CoverageTrend, CoverageGap, CoverageComparison, CoverageThreshold, CoverageType, CoverageReport } from './types.js';
7
- /**
8
- * Historical coverage data point
9
- */
10
- interface HistoricalCoverage {
11
- runId: string;
12
- timestamp: number;
13
- metrics: CoverageMetrics;
14
- }
15
- /**
16
- * Coverage Analyzer class
17
- */
18
- export declare class CoverageAnalyzer {
19
- private history;
20
- /**
21
- * Analyze coverage and generate insights
22
- */
23
- analyze(result: CoverageResult, threshold?: CoverageThreshold): CoverageReport;
24
- /**
25
- * Check if coverage meets thresholds
26
- */
27
- checkThresholds(metrics: CoverageMetrics, threshold?: CoverageThreshold): boolean;
28
- /**
29
- * Check if a single file meets thresholds
30
- */
31
- checkFileThresholds(file: FileCoverage, threshold?: CoverageThreshold): boolean;
32
- /**
33
- * Find coverage gaps
34
- */
35
- findGaps(files: Record<string, FileCoverage>, threshold?: CoverageThreshold): CoverageGap[];
36
- /**
37
- * Calculate priority for covering a file
38
- */
39
- private calculatePriority;
40
- /**
41
- * Estimate effort to cover a file
42
- */
43
- private estimateEffort;
44
- /**
45
- * Generate test suggestions for a file
46
- */
47
- private generateSuggestions;
48
- /**
49
- * Group consecutive numbers into ranges
50
- */
51
- private groupConsecutiveNumbers;
52
- /**
53
- * Get top and bottom files by coverage
54
- */
55
- getTopFiles(files: Record<string, FileCoverage>, limit?: number): Array<{
56
- path: string;
57
- coverage: number;
58
- type: 'best' | 'worst';
59
- }>;
60
- /**
61
- * Compare two coverage results
62
- */
63
- compare(baseResult: CoverageResult, compareResult: CoverageResult): CoverageComparison;
64
- /**
65
- * Add historical coverage data
66
- */
67
- addHistory(key: string, data: HistoricalCoverage): void;
68
- /**
69
- * Get coverage trends
70
- */
71
- getTrends(key: string, type?: CoverageType, limit?: number): CoverageTrend[];
72
- /**
73
- * Calculate trend direction
74
- */
75
- getTrendDirection(trends: CoverageTrend[]): 'improving' | 'stable' | 'declining';
76
- /**
77
- * Predict future coverage based on trends
78
- */
79
- predictCoverage(key: string, type: CoverageType | undefined, targetCoverage: number): {
80
- predictedReach: number | null;
81
- projectedCoverage: number;
82
- confidence: 'high' | 'medium' | 'low';
83
- };
84
- /**
85
- * Generate coverage summary text
86
- */
87
- generateSummary(metrics: CoverageMetrics): string;
88
- /**
89
- * Format coverage percentage with color indicator
90
- */
91
- formatCoverage(percentage: number, threshold?: number): string;
92
- /**
93
- * Clear history
94
- */
95
- clearHistory(key?: string): void;
96
- }
97
- /**
98
- * Create a coverage analyzer
99
- */
100
- export declare function createCoverageAnalyzer(): CoverageAnalyzer;
101
- export {};