@aqa-pulse/cli 0.1.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 (37) hide show
  1. package/README.md +43 -0
  2. package/bin/aqa-pulse.js +49 -0
  3. package/dist/backend/generate-source-facts.d.ts +2 -0
  4. package/dist/backend/generate-source-facts.js +607 -0
  5. package/dist/backend/merge-reports.d.ts +19 -0
  6. package/dist/backend/merge-reports.js +314 -0
  7. package/dist/backend/upload-report-artifacts.d.ts +9 -0
  8. package/dist/backend/upload-report-artifacts.js +772 -0
  9. package/dist/backend/upload-report.d.ts +13 -0
  10. package/dist/backend/upload-report.js +338 -0
  11. package/dist/dashboard-utils.d.ts +437 -0
  12. package/dist/dashboard-utils.js +2627 -0
  13. package/dist/history-utils.d.ts +72 -0
  14. package/dist/history-utils.js +267 -0
  15. package/dist/shared/business-assumptions.d.ts +14 -0
  16. package/dist/shared/business-assumptions.js +61 -0
  17. package/dist/shared/dashboard-helpers.d.ts +63 -0
  18. package/dist/shared/dashboard-helpers.js +429 -0
  19. package/dist/shared/dashboard-metric-info.d.ts +61 -0
  20. package/dist/shared/dashboard-metric-info.js +15 -0
  21. package/dist/shared/error-utils.d.ts +1 -0
  22. package/dist/shared/error-utils.js +6 -0
  23. package/dist/shared/formatting.d.ts +3 -0
  24. package/dist/shared/formatting.js +42 -0
  25. package/dist/shared/i18n/ru.d.ts +558 -0
  26. package/dist/shared/i18n/ru.js +577 -0
  27. package/dist/shared/metric-info.d.ts +5 -0
  28. package/dist/shared/metric-info.js +210 -0
  29. package/dist/shared/navigation.d.ts +31 -0
  30. package/dist/shared/navigation.js +99 -0
  31. package/dist/shared/test-history-helpers.d.ts +51 -0
  32. package/dist/shared/test-history-helpers.js +294 -0
  33. package/dist/shared/test-history-metric-info.d.ts +17 -0
  34. package/dist/shared/test-history-metric-info.js +20 -0
  35. package/dist/shared/text-utils.d.ts +2 -0
  36. package/dist/shared/text-utils.js +15 -0
  37. package/package.json +37 -0
@@ -0,0 +1,429 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.formatCurrency = formatCurrency;
4
+ exports.formatMinutes = formatMinutes;
5
+ exports.formatNullableMinutes = formatNullableMinutes;
6
+ exports.formatDailyRatio = formatDailyRatio;
7
+ exports.formatRunsPerHundred = formatRunsPerHundred;
8
+ exports.formatScore = formatScore;
9
+ exports.formatNullableDays = formatNullableDays;
10
+ exports.formatNullablePercent = formatNullablePercent;
11
+ exports.formatAssumptionValue = formatAssumptionValue;
12
+ exports.formatCommit = formatCommit;
13
+ exports.formatStatusLabel = formatStatusLabel;
14
+ exports.getStatusTone = getStatusTone;
15
+ exports.getScoreTone = getScoreTone;
16
+ exports.getInverseScoreTone = getInverseScoreTone;
17
+ exports.formatDelta = formatDelta;
18
+ exports.formatPerformancePhaseLabel = formatPerformancePhaseLabel;
19
+ exports.getManagerReadinessLabel = getManagerReadinessLabel;
20
+ exports.getManagerRiskLabel = getManagerRiskLabel;
21
+ exports.getManagerChangeLabel = getManagerChangeLabel;
22
+ exports.buildFlakyHistoryInsight = buildFlakyHistoryInsight;
23
+ exports.getFlakyTopTestsEmptyState = getFlakyTopTestsEmptyState;
24
+ exports.getCodeQualityFailureConcentration = getCodeQualityFailureConcentration;
25
+ exports.buildReleaseConfidenceBreakdown = buildReleaseConfidenceBreakdown;
26
+ exports.buildBusinessMetricReadiness = buildBusinessMetricReadiness;
27
+ exports.averageDashboardNumber = averageDashboardNumber;
28
+ exports.getBusinessReadinessStatusLabel = getBusinessReadinessStatusLabel;
29
+ exports.getBusinessAssumptionsState = getBusinessAssumptionsState;
30
+ exports.getBusinessScenarioStatusLabel = getBusinessScenarioStatusLabel;
31
+ exports.getBusinessScenarioStatusHint = getBusinessScenarioStatusHint;
32
+ exports.getBusinessImpactLevel = getBusinessImpactLevel;
33
+ exports.getBusinessImpactClass = getBusinessImpactClass;
34
+ exports.getBusinessImpactLabel = getBusinessImpactLabel;
35
+ exports.getBusinessDriverType = getBusinessDriverType;
36
+ exports.getBusinessBreakdownItemClass = getBusinessBreakdownItemClass;
37
+ exports.getBusinessDriverSignalClass = getBusinessDriverSignalClass;
38
+ exports.getBusinessDriverInsightTitle = getBusinessDriverInsightTitle;
39
+ exports.getBusinessDriverInsightBody = getBusinessDriverInsightBody;
40
+ exports.formatCostShare = formatCostShare;
41
+ exports.formatCostShareWidth = formatCostShareWidth;
42
+ exports.getDashboardScoreTone = getDashboardScoreTone;
43
+ exports.clampDashboardScore = clampDashboardScore;
44
+ exports.roundToOneDigit = roundToOneDigit;
45
+ exports.roundOne = roundOne;
46
+ const ru_1 = require("./i18n/ru");
47
+ const DASHBOARD_TEXT = ru_1.ru.dashboard;
48
+ function formatCurrency(value) {
49
+ if (value === null) {
50
+ return '—';
51
+ }
52
+ return `${value.toFixed(2)} ₽`;
53
+ }
54
+ function formatMinutes(value) {
55
+ return `${value.toFixed(2)} мин`;
56
+ }
57
+ function formatNullableMinutes(value) {
58
+ return value === null ? '—' : formatMinutes(value);
59
+ }
60
+ function formatDailyRatio(value) {
61
+ return `${value.toFixed(2)} / день`;
62
+ }
63
+ function formatRunsPerHundred(value) {
64
+ return `${value.toFixed(1)} / 100 запусков`;
65
+ }
66
+ function formatScore(value) {
67
+ return `${roundOne(value)} / 100`;
68
+ }
69
+ function formatNullableDays(value) {
70
+ return value === null ? '—' : `${value.toFixed(2)} дн`;
71
+ }
72
+ function formatNullablePercent(value) {
73
+ return value === null ? '—' : `${value.toFixed(1)}%`;
74
+ }
75
+ function formatAssumptionValue(value, unit) {
76
+ if (value === null) {
77
+ return DASHBOARD_TEXT.states.notSet;
78
+ }
79
+ return `${value} ${unit}`;
80
+ }
81
+ function formatCommit(commit) {
82
+ return commit ? commit.slice(0, 8) : '—';
83
+ }
84
+ function formatStatusLabel(status, flaky) {
85
+ if (flaky) {
86
+ return DASHBOARD_TEXT.statusLabels.flaky;
87
+ }
88
+ return DASHBOARD_TEXT.statusLabels[status] ?? status;
89
+ }
90
+ function getStatusTone(status, flaky) {
91
+ if (flaky) {
92
+ return 'warn';
93
+ }
94
+ const normalized = status.trim().toLowerCase();
95
+ if (normalized === 'passed') {
96
+ return 'good';
97
+ }
98
+ if (normalized === 'failed' || normalized === 'timedout' || normalized === 'timed out' || normalized === 'interrupted') {
99
+ return 'danger';
100
+ }
101
+ return 'neutral';
102
+ }
103
+ function getScoreTone(value) {
104
+ if (value >= 80) {
105
+ return 'good';
106
+ }
107
+ if (value >= 60) {
108
+ return 'warn';
109
+ }
110
+ return 'danger';
111
+ }
112
+ function getInverseScoreTone(value) {
113
+ if (value >= 70) {
114
+ return 'danger';
115
+ }
116
+ if (value >= 40) {
117
+ return 'warn';
118
+ }
119
+ return 'good';
120
+ }
121
+ function formatDelta(value, label, comparisonMode = 'adjacent') {
122
+ if (value === null) {
123
+ return comparisonMode === 'comparable' ? 'Нет сопоставимого прогона для сравнения' : DASHBOARD_TEXT.states.noPreviousRun;
124
+ }
125
+ if (value === 0) {
126
+ return comparisonMode === 'comparable' ? 'Без изменений к сопоставимому прогону' : DASHBOARD_TEXT.states.noChanges;
127
+ }
128
+ const prefix = value > 0 ? '+' : '';
129
+ return comparisonMode === 'comparable'
130
+ ? `${prefix}${value} ${label} к сопоставимому прогону`
131
+ : `${prefix}${value} ${label}`;
132
+ }
133
+ function formatPerformancePhaseLabel(label) {
134
+ if (label === 'Setup') {
135
+ return 'Подготовка';
136
+ }
137
+ if (label === 'Teardown') {
138
+ return 'Завершение';
139
+ }
140
+ return 'Тесты';
141
+ }
142
+ function getManagerReadinessLabel(level) {
143
+ if (level === 'healthy') {
144
+ return DASHBOARD_TEXT.manager.readinessHealthy;
145
+ }
146
+ if (level === 'warning') {
147
+ return DASHBOARD_TEXT.manager.readinessWarning;
148
+ }
149
+ return DASHBOARD_TEXT.manager.readinessCritical;
150
+ }
151
+ function getManagerRiskLabel(level) {
152
+ if (level === 'healthy') {
153
+ return DASHBOARD_TEXT.manager.riskHealthy;
154
+ }
155
+ if (level === 'warning') {
156
+ return DASHBOARD_TEXT.manager.riskWarning;
157
+ }
158
+ return DASHBOARD_TEXT.manager.riskCritical;
159
+ }
160
+ function getManagerChangeLabel(direction) {
161
+ if (direction === 'improving') {
162
+ return 'Улучшается';
163
+ }
164
+ if (direction === 'regressing') {
165
+ return 'Деградирует';
166
+ }
167
+ return 'Без сдвига';
168
+ }
169
+ function buildFlakyHistoryInsight(summary) {
170
+ const hasHistoricalRanking = summary.flakyAnalytics.topFlakyTests.length > 0;
171
+ const followupLine = hasHistoricalRanking
172
+ ? DASHBOARD_TEXT.flakyInsights.historyReady
173
+ : summary.kpis.flakyTests > 0
174
+ ? DASHBOARD_TEXT.flakyInsights.historyMissing
175
+ : null;
176
+ return {
177
+ tone: hasHistoricalRanking ? 'info' : summary.kpis.flakyTests > 0 ? 'warn' : 'default',
178
+ title: DASHBOARD_TEXT.flakyInsights.title,
179
+ body: [DASHBOARD_TEXT.flakyInsights.historyLine, followupLine].filter(Boolean).join(' '),
180
+ };
181
+ }
182
+ function getFlakyTopTestsEmptyState(summary) {
183
+ if (summary.kpis.flakyTests > 0) {
184
+ return DASHBOARD_TEXT.states.flakyTestsHistoryMissing;
185
+ }
186
+ return DASHBOARD_TEXT.states.flakyTestsEmpty;
187
+ }
188
+ function getCodeQualityFailureConcentration(summary) {
189
+ const unsuccessfulTests = summary.kpis.failedTests + summary.kpis.timedOutTests + summary.kpis.interruptedTests;
190
+ if (unsuccessfulTests === 0) {
191
+ return null;
192
+ }
193
+ const hotspotFailures = summary.topProblematicTests.filter((test) => test.status === 'failed' || test.status === 'timedout' || test.status === 'interrupted').length;
194
+ return roundOne((hotspotFailures / unsuccessfulTests) * 100);
195
+ }
196
+ function buildReleaseConfidenceBreakdown(summary) {
197
+ const passRateValue = clampDashboardScore(summary.kpis.passRate);
198
+ const inverseFlakyValue = clampDashboardScore(100 - summary.kpis.flakyRatio);
199
+ const errorHealthValue = summary.kpis.totalTests === 0
200
+ ? 100
201
+ : clampDashboardScore(100 - ((summary.errorClusters.length / summary.kpis.totalTests) * 100));
202
+ const recentRuns = summary.history.recentRuns.slice(-5);
203
+ const historyConsistencyValue = recentRuns.length > 0
204
+ ? clampDashboardScore(averageDashboardNumber(recentRuns.map((run) => run.passRate - run.flakyRatio)))
205
+ : clampDashboardScore(summary.kpis.passRate - summary.kpis.flakyRatio);
206
+ const componentDefinitions = [
207
+ {
208
+ label: DASHBOARD_TEXT.business.releaseConfidencePassRate,
209
+ rawValue: passRateValue,
210
+ weight: 0.4,
211
+ },
212
+ {
213
+ label: DASHBOARD_TEXT.business.releaseConfidenceFlakyRatio,
214
+ rawValue: inverseFlakyValue,
215
+ weight: 0.3,
216
+ },
217
+ {
218
+ label: DASHBOARD_TEXT.business.releaseConfidenceErrorHealth,
219
+ rawValue: errorHealthValue,
220
+ weight: 0.15,
221
+ },
222
+ {
223
+ label: DASHBOARD_TEXT.business.releaseConfidenceHistoryConsistency,
224
+ rawValue: historyConsistencyValue,
225
+ weight: 0.15,
226
+ },
227
+ ];
228
+ return {
229
+ total: roundOne(componentDefinitions.reduce((total, component) => total + (component.rawValue * component.weight), 0)),
230
+ components: componentDefinitions.map((component) => ({
231
+ label: component.label,
232
+ formula: `${roundOne(component.rawValue)} × ${component.weight} = ${roundOne(component.rawValue * component.weight)}`,
233
+ width: `${roundOne(component.rawValue)}%`,
234
+ tone: getDashboardScoreTone(component.rawValue),
235
+ })),
236
+ };
237
+ }
238
+ function buildBusinessMetricReadiness(summary) {
239
+ const costAssumptionsState = getBusinessAssumptionsState(summary.businessMetrics.costOfFlakiness.assumptions);
240
+ const costStatus = costAssumptionsState === 'empty' ? 'pending' : costAssumptionsState;
241
+ const timeToFixStatus = summary.businessMetrics.timeToFixFlaky.medianDays === null ? 'pending' : 'ready';
242
+ return [
243
+ {
244
+ label: DASHBOARD_TEXT.metrics.timeToDetect,
245
+ status: 'pending',
246
+ statusLabel: DASHBOARD_TEXT.business.readinessPending,
247
+ hint: DASHBOARD_TEXT.business.readinessTimeToDetectHint,
248
+ },
249
+ {
250
+ label: DASHBOARD_TEXT.metrics.timeToFixFlaky,
251
+ status: timeToFixStatus,
252
+ statusLabel: getBusinessReadinessStatusLabel(timeToFixStatus),
253
+ hint: DASHBOARD_TEXT.business.readinessTimeToFixHint,
254
+ },
255
+ {
256
+ label: DASHBOARD_TEXT.metrics.costOfFlakiness,
257
+ status: costStatus,
258
+ statusLabel: getBusinessReadinessStatusLabel(costStatus),
259
+ hint: DASHBOARD_TEXT.business.readinessCostHint,
260
+ },
261
+ {
262
+ label: DASHBOARD_TEXT.metrics.developerFriction,
263
+ status: 'ready',
264
+ statusLabel: DASHBOARD_TEXT.business.readinessReady,
265
+ hint: DASHBOARD_TEXT.business.readinessDeveloperFrictionHint,
266
+ },
267
+ {
268
+ label: DASHBOARD_TEXT.metrics.releaseConfidenceScore,
269
+ status: 'ready',
270
+ statusLabel: DASHBOARD_TEXT.business.readinessReady,
271
+ hint: DASHBOARD_TEXT.business.readinessReleaseConfidenceHint,
272
+ },
273
+ {
274
+ label: DASHBOARD_TEXT.metrics.automationRoi,
275
+ status: 'pending',
276
+ statusLabel: DASHBOARD_TEXT.business.readinessPending,
277
+ hint: DASHBOARD_TEXT.business.readinessAutomationRoiHint,
278
+ },
279
+ ];
280
+ }
281
+ function averageDashboardNumber(values) {
282
+ if (values.length === 0) {
283
+ return 0;
284
+ }
285
+ return values.reduce((total, value) => total + value, 0) / values.length;
286
+ }
287
+ function getBusinessReadinessStatusLabel(status) {
288
+ if (status === 'ready') {
289
+ return DASHBOARD_TEXT.business.readinessReady;
290
+ }
291
+ if (status === 'partial') {
292
+ return DASHBOARD_TEXT.business.readinessPartial;
293
+ }
294
+ return DASHBOARD_TEXT.business.readinessPending;
295
+ }
296
+ function getBusinessAssumptionsState(assumptions) {
297
+ const isCiConfigured = assumptions.ciMinuteCostRub !== null;
298
+ const isDeveloperConfigured = assumptions.developerHourlyCostRub !== null && assumptions.analysisMinutesPerUnstable !== null;
299
+ const hasAnyValue = isCiConfigured || assumptions.developerHourlyCostRub !== null || assumptions.analysisMinutesPerUnstable !== null;
300
+ if (isCiConfigured && isDeveloperConfigured) {
301
+ return 'ready';
302
+ }
303
+ if (hasAnyValue) {
304
+ return 'partial';
305
+ }
306
+ return 'empty';
307
+ }
308
+ function getBusinessScenarioStatusLabel(assumptions) {
309
+ const state = getBusinessAssumptionsState(assumptions);
310
+ if (state === 'ready') {
311
+ return DASHBOARD_TEXT.business.scenarioStatusReady;
312
+ }
313
+ if (state === 'partial') {
314
+ return DASHBOARD_TEXT.business.scenarioStatusPartial;
315
+ }
316
+ return DASHBOARD_TEXT.business.scenarioStatusEmpty;
317
+ }
318
+ function getBusinessScenarioStatusHint(assumptions) {
319
+ const state = getBusinessAssumptionsState(assumptions);
320
+ if (state === 'ready') {
321
+ return DASHBOARD_TEXT.business.scenarioStatusReadyHint;
322
+ }
323
+ if (state === 'partial') {
324
+ return DASHBOARD_TEXT.business.scenarioStatusPartialHint;
325
+ }
326
+ return DASHBOARD_TEXT.business.scenarioStatusEmptyHint;
327
+ }
328
+ function getBusinessImpactLevel(totalCost, costPerDay) {
329
+ if (totalCost === null) {
330
+ return 'unknown';
331
+ }
332
+ if (totalCost >= 50000 || (costPerDay !== null && costPerDay >= 10000)) {
333
+ return 'high';
334
+ }
335
+ if (totalCost >= 15000 || (costPerDay !== null && costPerDay >= 3000)) {
336
+ return 'medium';
337
+ }
338
+ return 'low';
339
+ }
340
+ function getBusinessImpactClass(totalCost, costPerDay) {
341
+ return `impact-${getBusinessImpactLevel(totalCost, costPerDay)}`;
342
+ }
343
+ function getBusinessImpactLabel(totalCost, costPerDay) {
344
+ const impactLevel = getBusinessImpactLevel(totalCost, costPerDay);
345
+ if (impactLevel === 'high') {
346
+ return DASHBOARD_TEXT.business.impactHigh;
347
+ }
348
+ if (impactLevel === 'medium') {
349
+ return DASHBOARD_TEXT.business.impactMedium;
350
+ }
351
+ if (impactLevel === 'low') {
352
+ return DASHBOARD_TEXT.business.impactLow;
353
+ }
354
+ return DASHBOARD_TEXT.business.impactUnknown;
355
+ }
356
+ function getBusinessDriverType(ciCost, developerCost, totalCost) {
357
+ if (totalCost === null || totalCost <= 0) {
358
+ return 'missing';
359
+ }
360
+ const normalizedCiCost = ciCost ?? 0;
361
+ const normalizedDeveloperCost = developerCost ?? 0;
362
+ const delta = Math.abs(normalizedCiCost - normalizedDeveloperCost);
363
+ if (delta <= totalCost * 0.15) {
364
+ return 'balanced';
365
+ }
366
+ return normalizedCiCost > normalizedDeveloperCost ? 'ci' : 'development';
367
+ }
368
+ function getBusinessBreakdownItemClass(ciCost, developerCost, totalCost, target) {
369
+ return getBusinessDriverType(ciCost, developerCost, totalCost) === target ? 'is-dominant' : '';
370
+ }
371
+ function getBusinessDriverSignalClass(ciCost, developerCost, totalCost, target) {
372
+ return getBusinessDriverType(ciCost, developerCost, totalCost) === target ? 'is-dominant' : '';
373
+ }
374
+ function getBusinessDriverInsightTitle(ciCost, developerCost, totalCost) {
375
+ const driverType = getBusinessDriverType(ciCost, developerCost, totalCost);
376
+ if (driverType === 'ci') {
377
+ return DASHBOARD_TEXT.business.topDriverCiTitle;
378
+ }
379
+ if (driverType === 'development') {
380
+ return DASHBOARD_TEXT.business.topDriverDevelopmentTitle;
381
+ }
382
+ if (driverType === 'balanced') {
383
+ return DASHBOARD_TEXT.business.topDriverBalancedTitle;
384
+ }
385
+ return DASHBOARD_TEXT.business.topDriverMissingTitle;
386
+ }
387
+ function getBusinessDriverInsightBody(ciCost, developerCost, totalCost) {
388
+ const driverType = getBusinessDriverType(ciCost, developerCost, totalCost);
389
+ if (driverType === 'ci') {
390
+ return DASHBOARD_TEXT.business.topDriverCiBody;
391
+ }
392
+ if (driverType === 'development') {
393
+ return DASHBOARD_TEXT.business.topDriverDevelopmentBody;
394
+ }
395
+ if (driverType === 'balanced') {
396
+ return DASHBOARD_TEXT.business.topDriverBalancedBody;
397
+ }
398
+ return DASHBOARD_TEXT.business.topDriverMissingBody;
399
+ }
400
+ function formatCostShare(value, total) {
401
+ if (value === null || total === null || total <= 0) {
402
+ return '—';
403
+ }
404
+ return `${((value / total) * 100).toFixed(1)}%`;
405
+ }
406
+ function formatCostShareWidth(value, total) {
407
+ if (value === null || total === null || total <= 0) {
408
+ return '0%';
409
+ }
410
+ return `${Math.max(0, Math.min(100, (value / total) * 100)).toFixed(1)}%`;
411
+ }
412
+ function getDashboardScoreTone(value) {
413
+ if (value >= 80) {
414
+ return '';
415
+ }
416
+ if (value >= 60) {
417
+ return 'warning';
418
+ }
419
+ return 'danger';
420
+ }
421
+ function clampDashboardScore(value) {
422
+ return Math.min(Math.max(value, 0), 100);
423
+ }
424
+ function roundToOneDigit(value) {
425
+ return roundOne(value);
426
+ }
427
+ function roundOne(value) {
428
+ return Math.round(value * 10) / 10;
429
+ }
@@ -0,0 +1,61 @@
1
+ export declare const DASHBOARD_METRIC_DESCRIPTIONS: {
2
+ readonly releaseConfidenceBreakdownTitle: "Сводный прокси-сигнал риска релиза на базе доступной истории: помогает оценить, насколько текущему тестовому сигналу можно доверять перед решением о релизе.";
3
+ readonly managerSummary: "Насколько надёжен текущий тестовый сигнал для решения о релизе, насколько высок риск качества и насколько нестабильность уже влияет на скорость поставки.";
4
+ readonly releaseReadiness: "Позитивный сводный сигнал: показывает, насколько текущий pass rate, flaky ratio и история прогонов дают опору для решения о релизе.";
5
+ readonly qualityRisk: "Сводная оценка риска дефектов и нестабильности: чем выше показатель, тем больше вероятность, что текущее качество требует дополнительной проверки.";
6
+ readonly deliveryRisk: "Сводный сигнал показывает, насколько текущая нестабильность и блокеры влияют на скорость поставки изменений.";
7
+ readonly currentRunTests: "Полный список тестов текущего прогона с разбиением по статусам и отдельным flaky-срезом.";
8
+ readonly signalCoverage: "Показывает, какая доля сигналов уже доступна для risk ranking, root-cause анализа и heuristic/AI слоя.";
9
+ readonly modelReadiness: "Секция собирает ключевые входные сигналы, по которым видно, насколько текущий ingestion готов к heuristic/AI сценариям.";
10
+ readonly passRate: "Доля тестов, завершившихся статусом passed в текущем прогоне.";
11
+ readonly failedTests: "Количество тестов с финальным неуспешным статусом: failed, timed out или interrupted.";
12
+ readonly flakyTests: "Количество flaky-тестов именно в текущем прогоне: тест требовал ретраев или менял статус между попытками.";
13
+ readonly runDuration: "Общая длительность текущего прогона по данным Playwright.";
14
+ readonly medianDuration: "Медианная длительность одного теста в текущем прогоне.";
15
+ readonly errorClusters: "Группы одинаковых или похожих сообщений об ошибках для быстрого анализа.";
16
+ readonly environment: "Окружение запуска: ОС, версии Playwright и Node.js.";
17
+ readonly latestVsPrevious: "Сравнение текущего прогона с предыдущим доступным прогоном в истории.";
18
+ readonly passRateTrend: "История изменения доли успешных тестов по архивным прогонам.";
19
+ readonly statusDistribution: "Распределение финальных статусов тестов в текущем прогоне.";
20
+ readonly recentRuns: "Последние архивные прогоны, попавшие в историю AQA Pulse.";
21
+ readonly notes: "Служебные заметки и выводы, сформированные во время построения отчёта.";
22
+ readonly durationTrend: "Тренд общей длительности прогонов по истории.";
23
+ readonly p95Duration: "95-й перцентиль длительности тестов в текущем срезе. Помогает видеть типичную верхнюю границу без влияния единичных экстремумов.";
24
+ readonly p99Duration: "99-й перцентиль длительности тестов. Показывает хвост самых тяжёлых тестов и помогает искать редкие, но дорогие задержки.";
25
+ readonly topSlowestTests: "Самые медленные тесты текущего среза, требующие оптимизации.";
26
+ readonly phaseBreakdown: "Оценка распределения времени между подготовкой, исполнением тестовых шагов и завершением. Фазы определяются по шагам и служебным названиям в отчёте Playwright, а доли нормализуются по сумме фазового времени.";
27
+ readonly suiteDuration: "Какие наборы тестов суммарно занимают больше всего времени в текущем срезе. Наборы группируются по префиксу title или по имени spec-файла, а доли считаются внутри общего времени всех наборов.";
28
+ readonly durationPerBrowser: "Сколько времени занимают браузеры текущего среза. Если в отчёте есть поле browser, AQA Pulse использует его как есть. Если поля нет, по умолчанию используется Chrome. Доли считаются внутри общего времени по всем браузерам/проектам.";
29
+ readonly flakyTrend: "Тренд количества нестабильных тестов по архивным прогонам.";
30
+ readonly clusterList: "Распределение падений по кластерам ошибок и примеры тестов.";
31
+ readonly problemHotspots: "Количество сценариев, которые прямо сейчас формируют основной backlog по качеству тестового кода и требуют первоочередного разбора.";
32
+ readonly brittlenessProxy: "Прокси-хрупкость тестового набора: опирается на средний исторический flaky score и показывает, насколько часто набор ведёт себя нестабильно по истории.";
33
+ readonly failureConcentration: "Показывает, какая доля текущих неуспешных тестов сосредоточена в небольшом наборе проблемных сценариев. Чем выше доля, тем сильнее сигнал локализован в нескольких тестах.";
34
+ readonly retryDensity: "Плотность лишних повторов по истории: сколько rerun-нагрузки приходится на 100 наблюдавшихся запусков.";
35
+ readonly testSmellScore: "Композитный score по source-анализу Playwright spec-файлов. Учитывает жёсткие паузы, direct locator usage, отсутствие POM-сигнала, слабую step-структуру и хрупкие селекторы.";
36
+ readonly pomCompliance: "Доля тестов, где spec-файл выглядит как consumer page object / screen-model слоя, а не хранит основную locator-логику прямо внутри теста.";
37
+ readonly assertionDensity: "Среднее число expect() на тест по доступным исходникам. Помогает увидеть сценарии, которые делают много действий, но слабо проверяют результат.";
38
+ readonly waitStrategyScore: "Баланс между web-first/assert-based ожиданиями и жёсткими ожиданиями вроде waitForTimeout. Чем выше score, тем устойчивее стратегия ожиданий.";
39
+ readonly stepGranularity: "Среднее число test.step на тест. Метрика показывает, насколько сценарии разбиты на читаемые этапы для отчётов и диагностики.";
40
+ readonly isolationScore: "Эвристика независимости тестов по исходникам: penalizes shared mutable state, heavy beforeAll и serial execution hints.";
41
+ readonly selectorStability: "Оценка устойчивости selector-подхода: role/label/testid дают лучший score, длинные CSS-цепочки и XPath — худший.";
42
+ readonly sourceCoverage: "Какая доля тестов текущего среза была реально сопоставлена с доступными spec-файлами и попала под source-анализ.";
43
+ readonly problematicTests: "Тесты с наибольшей долей падений и высокой вероятностью повторных инцидентов.";
44
+ readonly failureRate: "Доля неуспешных попыток от общего числа запусков теста.";
45
+ readonly topFlakyTests: "Приоритетный список для разбора по архивной истории AQA Pulse: приоритизация строится по flaky-индексу, а не только по текущему запуску. В таблице показывается топ-10 тестов, а расчёт опирается на последние 20 архивных прогонов.";
46
+ readonly flakyScore: "Композитный индекс нестабильности теста по истории прогонов.";
47
+ readonly mtbf: "Среднее время между нестабильными инцидентами теста.";
48
+ readonly unstableRuns: "Количество архивных прогонов, в которых тест был нестабильным.";
49
+ readonly codeQuality: "Плейсхолдер для будущих метрик качества тестового кода и соблюдения практик Playwright.";
50
+ readonly timeToDetect: "Время от падения до уведомления команды. В MVP-плане метрика требует интеграции с каналом алертов, поэтому без него показывается как ожидающая настройки.";
51
+ readonly timeToFixFlaky: "Медианное время от первого нестабильного инцидента до первого стабильного восстановления по доступной истории.";
52
+ readonly ciWasteTime: "Суммарное время, потерянное на лишние ретраи и повторные прогоны в CI по доступной истории.";
53
+ readonly costOfFlakiness: "Оценка потерь от нестабильных тестов на основе стоимости минуты CI и стоимости часа разработки.";
54
+ readonly investigationCost: "Оценка стоимости инженерного разбора нестабильных прогонов на основе assumptions по времени анализа и стоимости часа разработки.";
55
+ readonly developerFriction: "Прокси-нагрузка на команду из-за ретраев, повторных запусков и разбора нестабильных инцидентов.";
56
+ readonly releaseConfidenceScore: "Сводный прокси-сигнал риска релиза на базе доступной истории: помогает оценить, насколько текущему тестовому сигналу можно доверять перед решением о релизе.";
57
+ readonly automationRoi: "Возврат инвестиций в автоматизацию. По формуле из MVP-плана требует данных о сэкономленных ручных часах и стоимости поддержки.";
58
+ readonly configAssumptions: "Параметры сценарного пересчёта стоимости нестабильности.";
59
+ readonly teamMetrics: "Плейсхолдер для будущих командных метрик по ownership и здоровью автотестов.";
60
+ readonly aiMetrics: "Плейсхолдер для будущих AI/ML-метрик и прогнозных моделей.";
61
+ };
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DASHBOARD_METRIC_DESCRIPTIONS = void 0;
4
+ const ru_1 = require("./i18n/ru");
5
+ exports.DASHBOARD_METRIC_DESCRIPTIONS = {
6
+ ...ru_1.ru.dashboard.tooltips,
7
+ releaseConfidenceBreakdownTitle: ru_1.ru.dashboard.tooltips.releaseConfidenceScore,
8
+ managerSummary: ru_1.ru.dashboard.manager.summaryDescription,
9
+ releaseReadiness: 'Позитивный сводный сигнал: показывает, насколько текущий pass rate, flaky ratio и история прогонов дают опору для решения о релизе.',
10
+ qualityRisk: 'Сводная оценка риска дефектов и нестабильности: чем выше показатель, тем больше вероятность, что текущее качество требует дополнительной проверки.',
11
+ deliveryRisk: 'Сводный сигнал показывает, насколько текущая нестабильность и блокеры влияют на скорость поставки изменений.',
12
+ currentRunTests: ru_1.ru.dashboard.testsBrowser.description,
13
+ signalCoverage: 'Показывает, какая доля сигналов уже доступна для risk ranking, root-cause анализа и heuristic/AI слоя.',
14
+ modelReadiness: 'Секция собирает ключевые входные сигналы, по которым видно, насколько текущий ingestion готов к heuristic/AI сценариям.',
15
+ };
@@ -0,0 +1 @@
1
+ export declare function getErrorMessage(error: unknown): string;
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getErrorMessage = getErrorMessage;
4
+ function getErrorMessage(error) {
5
+ return error instanceof Error ? error.message : String(error);
6
+ }
@@ -0,0 +1,3 @@
1
+ export declare function formatPercent(value: number): string;
2
+ export declare function formatDuration(durationMs: number): string;
3
+ export declare function formatDate(timestamp: string | null): string;
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.formatPercent = formatPercent;
4
+ exports.formatDuration = formatDuration;
5
+ exports.formatDate = formatDate;
6
+ function formatPercent(value) {
7
+ return `${value.toFixed(1)}%`;
8
+ }
9
+ function formatDuration(durationMs) {
10
+ if (!Number.isFinite(durationMs) || durationMs <= 0) {
11
+ return '0s';
12
+ }
13
+ const totalSeconds = Math.round(durationMs / 1000);
14
+ const hours = Math.floor(totalSeconds / 3600);
15
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
16
+ const seconds = totalSeconds % 60;
17
+ const parts = [];
18
+ if (hours > 0) {
19
+ parts.push(`${hours}h`);
20
+ }
21
+ if (minutes > 0 || hours > 0) {
22
+ parts.push(`${minutes}m`);
23
+ }
24
+ parts.push(`${seconds}s`);
25
+ return parts.join(' ');
26
+ }
27
+ function formatDate(timestamp) {
28
+ if (!timestamp) {
29
+ return '—';
30
+ }
31
+ const date = new Date(timestamp);
32
+ if (Number.isNaN(date.getTime())) {
33
+ return timestamp;
34
+ }
35
+ return new Intl.DateTimeFormat('ru-RU', {
36
+ year: 'numeric',
37
+ month: 'long',
38
+ day: 'numeric',
39
+ hour: '2-digit',
40
+ minute: '2-digit',
41
+ }).format(date);
42
+ }