mustflow 2.85.4 → 2.99.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.
- package/dist/cli/commands/script-pack.js +10 -0
- package/dist/cli/i18n/en.js +183 -0
- package/dist/cli/i18n/es.js +183 -0
- package/dist/cli/i18n/fr.js +183 -0
- package/dist/cli/i18n/hi.js +183 -0
- package/dist/cli/i18n/ko.js +183 -0
- package/dist/cli/i18n/zh.js +183 -0
- package/dist/cli/lib/script-pack-registry.js +284 -1
- package/dist/cli/script-packs/code-change-impact.js +6 -0
- package/dist/cli/script-packs/code-import-cycle.js +193 -0
- package/dist/cli/script-packs/docs-link-integrity.js +145 -0
- package/dist/cli/script-packs/repo-approval-gate.js +100 -0
- package/dist/cli/script-packs/repo-git-ignore-audit.js +119 -0
- package/dist/cli/script-packs/repo-manifest-lock-drift.js +122 -0
- package/dist/cli/script-packs/repo-merge-conflict-scan.js +123 -0
- package/dist/cli/script-packs/repo-skill-route-audit.js +86 -0
- package/dist/cli/script-packs/repo-version-source.js +92 -0
- package/dist/cli/script-packs/test-performance-report.js +247 -0
- package/dist/cli/script-packs/test-regression-selector.js +167 -0
- package/dist/core/change-impact.js +23 -51
- package/dist/core/change-surface-classification.js +198 -0
- package/dist/core/docs-link-integrity.js +443 -0
- package/dist/core/import-cycle.js +152 -0
- package/dist/core/public-json-contracts.js +116 -0
- package/dist/core/repo-approval-gate.js +116 -0
- package/dist/core/repo-git-ignore-audit.js +302 -0
- package/dist/core/repo-manifest-lock-drift.js +321 -0
- package/dist/core/repo-merge-conflict-scan.js +335 -0
- package/dist/core/repo-version-source.js +82 -0
- package/dist/core/script-pack-suggestions.js +77 -1
- package/dist/core/skill-route-audit.js +354 -0
- package/dist/core/test-performance-report.js +697 -0
- package/dist/core/test-regression-selector.js +335 -0
- package/package.json +1 -1
- package/schemas/README.md +40 -2
- package/schemas/change-impact-report.schema.json +35 -1
- package/schemas/import-cycle-report.schema.json +157 -0
- package/schemas/link-integrity-report.schema.json +176 -0
- package/schemas/repo-approval-gate-report.schema.json +115 -0
- package/schemas/repo-git-ignore-audit-report.schema.json +201 -0
- package/schemas/repo-manifest-lock-drift-report.schema.json +202 -0
- package/schemas/repo-merge-conflict-scan-report.schema.json +169 -0
- package/schemas/repo-version-source-report.schema.json +127 -0
- package/schemas/skill-route-audit-report.schema.json +144 -0
- package/schemas/test-performance-report.schema.json +319 -0
- package/schemas/test-regression-selector-report.schema.json +187 -0
- package/templates/default/i18n.toml +66 -18
- package/templates/default/locales/en/.mustflow/skills/INDEX.md +45 -8
- package/templates/default/locales/en/.mustflow/skills/api-access-control-review/SKILL.md +48 -27
- package/templates/default/locales/en/.mustflow/skills/api-failure-triage/SKILL.md +270 -0
- package/templates/default/locales/en/.mustflow/skills/auth-flow-triage/SKILL.md +192 -0
- package/templates/default/locales/en/.mustflow/skills/auth-permission-change/SKILL.md +59 -13
- package/templates/default/locales/en/.mustflow/skills/backend-log-evidence-review/SKILL.md +14 -5
- package/templates/default/locales/en/.mustflow/skills/cache-integrity-review/SKILL.md +30 -15
- package/templates/default/locales/en/.mustflow/skills/change-blast-radius-review/SKILL.md +45 -32
- package/templates/default/locales/en/.mustflow/skills/ci-pipeline-triage/SKILL.md +200 -0
- package/templates/default/locales/en/.mustflow/skills/clarifying-question-gate/SKILL.md +87 -13
- package/templates/default/locales/en/.mustflow/skills/docker-runtime-triage/SKILL.md +191 -0
- package/templates/default/locales/en/.mustflow/skills/go-code-change/SKILL.md +18 -13
- package/templates/default/locales/en/.mustflow/skills/line-ending-hygiene/SKILL.md +18 -10
- package/templates/default/locales/en/.mustflow/skills/llm-hallucination-control-review/SKILL.md +4 -1
- package/templates/default/locales/en/.mustflow/skills/motion-system-contract-review/SKILL.md +155 -0
- package/templates/default/locales/en/.mustflow/skills/next-action-menu/SKILL.md +177 -0
- package/templates/default/locales/en/.mustflow/skills/observability-debuggability-review/SKILL.md +15 -7
- package/templates/default/locales/en/.mustflow/skills/payment-integrity-review/SKILL.md +59 -35
- package/templates/default/locales/en/.mustflow/skills/powershell-code-change/SKILL.md +16 -6
- package/templates/default/locales/en/.mustflow/skills/prompt-contract-quality-review/SKILL.md +4 -1
- package/templates/default/locales/en/.mustflow/skills/python-code-change/SKILL.md +19 -10
- package/templates/default/locales/en/.mustflow/skills/rag-pipeline-triage/SKILL.md +206 -0
- package/templates/default/locales/en/.mustflow/skills/routes.toml +54 -0
- package/templates/default/locales/en/.mustflow/skills/rust-code-change/SKILL.md +10 -4
- package/templates/default/locales/en/.mustflow/skills/search-index-integrity-review/SKILL.md +181 -0
- package/templates/default/locales/en/.mustflow/skills/service-boundary-architecture/SKILL.md +37 -23
- package/templates/default/locales/en/.mustflow/skills/test-suite-performance-review/SKILL.md +9 -0
- package/templates/default/locales/en/.mustflow/skills/typescript-code-change/SKILL.md +14 -9
- package/templates/default/locales/en/.mustflow/skills/vector-search-integrity-review/SKILL.md +209 -0
- package/templates/default/locales/en/.mustflow/skills/version-freshness-check/SKILL.md +16 -14
- package/templates/default/manifest.toml +64 -1
|
@@ -0,0 +1,697 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { readUtf8FileInsideWithoutSymlinks } from './safe-filesystem.js';
|
|
5
|
+
export const TEST_PERFORMANCE_REPORT_PACK_ID = 'test';
|
|
6
|
+
export const TEST_PERFORMANCE_REPORT_SCRIPT_ID = 'performance-report';
|
|
7
|
+
export const TEST_PERFORMANCE_REPORT_SCRIPT_REF = `${TEST_PERFORMANCE_REPORT_PACK_ID}/${TEST_PERFORMANCE_REPORT_SCRIPT_ID}`;
|
|
8
|
+
const LATEST_RUN_RECEIPT_PATH = path.join('.mustflow', 'state', 'runs', 'latest.json');
|
|
9
|
+
const LATEST_RUN_PROFILE_PATH = path.join('.mustflow', 'state', 'runs', 'latest.profile.json');
|
|
10
|
+
const PERFORMANCE_SAMPLES_PATH = path.join('.mustflow', 'state', 'perf', 'samples.json');
|
|
11
|
+
const PERFORMANCE_SUMMARY_PATH = path.join('.mustflow', 'state', 'perf', 'summary.json');
|
|
12
|
+
const DEFAULT_MAX_SAMPLES = 200;
|
|
13
|
+
const DEFAULT_MAX_INTENTS = 30;
|
|
14
|
+
const DEFAULT_MAX_TEST_FILES = 40;
|
|
15
|
+
const DEFAULT_MAX_FINDINGS = 80;
|
|
16
|
+
const DEFAULT_SLOW_SAMPLE_THRESHOLD_MS = 120_000;
|
|
17
|
+
const DEFAULT_HIGH_TIMEOUT_RATIO = 0.75;
|
|
18
|
+
const DEFAULT_PHASE_BOTTLENECK_THRESHOLD_MS = 30_000;
|
|
19
|
+
const DEFAULT_STALE_PROFILE_THRESHOLD_MS = 24 * 60 * 60 * 1000;
|
|
20
|
+
const DEFAULT_LOW_PROFILE_COVERAGE_RATIO = 0.8;
|
|
21
|
+
const MAX_EVIDENCE_BYTES = 512 * 1024;
|
|
22
|
+
const ERROR_CODES = new Set(['test_performance_unreadable_evidence']);
|
|
23
|
+
function normalizeRelativePath(value) {
|
|
24
|
+
return value.replace(/\\/gu, '/');
|
|
25
|
+
}
|
|
26
|
+
function sha256Tagged(value) {
|
|
27
|
+
return `sha256:${createHash('sha256').update(value).digest('hex')}`;
|
|
28
|
+
}
|
|
29
|
+
function isRecord(value) {
|
|
30
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
31
|
+
}
|
|
32
|
+
function isNonNegativeNumber(value) {
|
|
33
|
+
return typeof value === 'number' && Number.isFinite(value) && value >= 0;
|
|
34
|
+
}
|
|
35
|
+
function isHistorySample(value) {
|
|
36
|
+
if (!isRecord(value)) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
return (typeof value.observed_day === 'string' &&
|
|
40
|
+
typeof value.intent === 'string' &&
|
|
41
|
+
isNonNegativeNumber(value.duration_ms) &&
|
|
42
|
+
isNonNegativeNumber(value.timeout_ratio) &&
|
|
43
|
+
(value.status === 'passed' || value.status === 'failed'));
|
|
44
|
+
}
|
|
45
|
+
function isPhaseRecord(value) {
|
|
46
|
+
if (!isRecord(value)) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
return Object.values(value).every(isNonNegativeNumber);
|
|
50
|
+
}
|
|
51
|
+
function isRunReceiptPhase(value) {
|
|
52
|
+
return isRecord(value) && typeof value.name === 'string' && isNonNegativeNumber(value.duration_ms);
|
|
53
|
+
}
|
|
54
|
+
function parseProfileTestFileStatus(value) {
|
|
55
|
+
if (value === 'passed' || value === 'ok' || value === 'success' || value === 0) {
|
|
56
|
+
return 'passed';
|
|
57
|
+
}
|
|
58
|
+
if (value === 'failed' || value === 'failure' || value === 'error') {
|
|
59
|
+
return 'failed';
|
|
60
|
+
}
|
|
61
|
+
if (typeof value === 'number' && Number.isInteger(value) && value > 0) {
|
|
62
|
+
return 'failed';
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
function isReportRunReceiptSelection(value) {
|
|
67
|
+
return (isRecord(value) &&
|
|
68
|
+
typeof value.strategy === 'string' &&
|
|
69
|
+
isNonNegativeNumber(value.changed_file_count) &&
|
|
70
|
+
isNonNegativeNumber(value.selected_target_count) &&
|
|
71
|
+
typeof value.fallback_used === 'boolean');
|
|
72
|
+
}
|
|
73
|
+
function isReportRunReceiptRunner(value) {
|
|
74
|
+
return (isRecord(value) &&
|
|
75
|
+
value.kind === 'local' &&
|
|
76
|
+
typeof value.platform_family === 'string' &&
|
|
77
|
+
typeof value.arch_family === 'string' &&
|
|
78
|
+
(value.runtime === 'node' || value.runtime === 'bun') &&
|
|
79
|
+
isNonNegativeNumber(value.runtime_major));
|
|
80
|
+
}
|
|
81
|
+
function isReportRunReceiptPerformance(value) {
|
|
82
|
+
if (!isRecord(value) || !isRecord(value.result_summary)) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
if (!isNonNegativeNumber(value.duration_ms) || !isNonNegativeNumber(value.timeout_ratio)) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
if (!isReportRunReceiptRunner(value.runner) || typeof value.result_summary.status !== 'string') {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
if (value.phases !== undefined && (!Array.isArray(value.phases) || !value.phases.every(isRunReceiptPhase))) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
if (value.selection !== undefined && !isReportRunReceiptSelection(value.selection)) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
function isReportRunReceipt(value) {
|
|
100
|
+
return (isRecord(value) &&
|
|
101
|
+
value.command === 'run' &&
|
|
102
|
+
typeof value.intent === 'string' &&
|
|
103
|
+
typeof value.finished_at === 'string' &&
|
|
104
|
+
isReportRunReceiptPerformance(value.performance));
|
|
105
|
+
}
|
|
106
|
+
function readJsonEvidence(projectRoot, relativePath, kind, parse, evidenceSources, findings, issues) {
|
|
107
|
+
const normalizedPath = normalizeRelativePath(relativePath);
|
|
108
|
+
const absolutePath = path.join(projectRoot, relativePath);
|
|
109
|
+
if (!existsSync(absolutePath)) {
|
|
110
|
+
evidenceSources.push({ path: normalizedPath, kind, exists: false, readable: false, issue: null });
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
const parsed = JSON.parse(readUtf8FileInsideWithoutSymlinks(projectRoot, absolutePath, { maxBytes: MAX_EVIDENCE_BYTES }));
|
|
115
|
+
const result = parse(parsed);
|
|
116
|
+
if (!result) {
|
|
117
|
+
const message = `${normalizedPath} did not match the expected performance evidence shape.`;
|
|
118
|
+
evidenceSources.push({ path: normalizedPath, kind, exists: true, readable: false, issue: message });
|
|
119
|
+
pushFinding(findings, {
|
|
120
|
+
code: 'test_performance_unreadable_evidence',
|
|
121
|
+
severity: 'high',
|
|
122
|
+
path: normalizedPath,
|
|
123
|
+
message,
|
|
124
|
+
});
|
|
125
|
+
issues.push(message);
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
evidenceSources.push({ path: normalizedPath, kind, exists: true, readable: true, issue: null });
|
|
129
|
+
return result;
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
133
|
+
evidenceSources.push({ path: normalizedPath, kind, exists: true, readable: false, issue: message });
|
|
134
|
+
pushFinding(findings, {
|
|
135
|
+
code: 'test_performance_unreadable_evidence',
|
|
136
|
+
severity: 'high',
|
|
137
|
+
path: normalizedPath,
|
|
138
|
+
message,
|
|
139
|
+
});
|
|
140
|
+
issues.push(`${normalizedPath}: ${message}`);
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function pushFinding(findings, finding, maxFindings = DEFAULT_MAX_FINDINGS) {
|
|
145
|
+
if (findings.length < maxFindings) {
|
|
146
|
+
findings.push(finding);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function percentile(values, percentileValue) {
|
|
150
|
+
if (values.length === 0) {
|
|
151
|
+
return 0;
|
|
152
|
+
}
|
|
153
|
+
const sorted = [...values].sort((left, right) => left - right);
|
|
154
|
+
const index = Math.ceil((percentileValue / 100) * sorted.length) - 1;
|
|
155
|
+
return sorted[Math.max(0, Math.min(sorted.length - 1, index))] ?? 0;
|
|
156
|
+
}
|
|
157
|
+
function groupBy(items, keyFor) {
|
|
158
|
+
const groups = new Map();
|
|
159
|
+
for (const item of items) {
|
|
160
|
+
const key = keyFor(item);
|
|
161
|
+
const group = groups.get(key) ?? [];
|
|
162
|
+
group.push(item);
|
|
163
|
+
groups.set(key, group);
|
|
164
|
+
}
|
|
165
|
+
return groups;
|
|
166
|
+
}
|
|
167
|
+
function toDay(value) {
|
|
168
|
+
return value.slice(0, 10);
|
|
169
|
+
}
|
|
170
|
+
function getRunnerBucket(receipt) {
|
|
171
|
+
const runner = receipt.performance.runner;
|
|
172
|
+
return `${runner.kind}/${runner.platform_family}/${runner.arch_family}/${runner.runtime}@${runner.runtime_major}`;
|
|
173
|
+
}
|
|
174
|
+
function historySampleToReportSample(sample) {
|
|
175
|
+
return {
|
|
176
|
+
source: 'history',
|
|
177
|
+
observed_day: sample.observed_day,
|
|
178
|
+
intent: sample.intent,
|
|
179
|
+
status: sample.status,
|
|
180
|
+
duration_ms: sample.duration_ms,
|
|
181
|
+
timeout_ratio: sample.timeout_ratio,
|
|
182
|
+
runner_bucket: sample.runner_bucket ?? null,
|
|
183
|
+
phase_count: isPhaseRecord(sample.phase_durations_ms) ? Object.keys(sample.phase_durations_ms).length : 0,
|
|
184
|
+
selection_strategy: typeof sample.selection_strategy === 'string' ? sample.selection_strategy : null,
|
|
185
|
+
changed_file_count: typeof sample.changed_file_count === 'number' ? sample.changed_file_count : null,
|
|
186
|
+
selected_target_count: typeof sample.selected_target_count === 'number' ? sample.selected_target_count : null,
|
|
187
|
+
fallback_used: typeof sample.fallback_used === 'boolean' ? sample.fallback_used : null,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
function receiptToReportSample(receipt) {
|
|
191
|
+
const status = receipt.performance.result_summary.status;
|
|
192
|
+
if (status !== 'passed' && status !== 'failed') {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
const selection = receipt.performance.selection;
|
|
196
|
+
return {
|
|
197
|
+
source: 'latest_run',
|
|
198
|
+
observed_day: toDay(receipt.finished_at),
|
|
199
|
+
intent: receipt.intent,
|
|
200
|
+
status,
|
|
201
|
+
duration_ms: receipt.performance.duration_ms,
|
|
202
|
+
timeout_ratio: receipt.performance.timeout_ratio,
|
|
203
|
+
runner_bucket: getRunnerBucket(receipt),
|
|
204
|
+
phase_count: receipt.performance.phases?.length ?? 0,
|
|
205
|
+
selection_strategy: selection?.strategy ?? null,
|
|
206
|
+
changed_file_count: selection?.changed_file_count ?? null,
|
|
207
|
+
selected_target_count: selection?.selected_target_count ?? null,
|
|
208
|
+
fallback_used: selection?.fallback_used ?? null,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
function collectHistoryPhases(samples) {
|
|
212
|
+
const phases = [];
|
|
213
|
+
for (const sample of samples) {
|
|
214
|
+
if (!isPhaseRecord(sample.phase_durations_ms)) {
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
for (const [name, durationMs] of Object.entries(sample.phase_durations_ms)) {
|
|
218
|
+
phases.push({ intent: sample.intent, name, duration_ms: durationMs });
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return phases;
|
|
222
|
+
}
|
|
223
|
+
function collectReceiptPhases(receipt) {
|
|
224
|
+
return (receipt?.performance.phases ?? []).map((phase) => ({
|
|
225
|
+
intent: receipt?.intent ?? 'unknown',
|
|
226
|
+
name: phase.name,
|
|
227
|
+
duration_ms: phase.duration_ms,
|
|
228
|
+
}));
|
|
229
|
+
}
|
|
230
|
+
function summarizePhases(phases) {
|
|
231
|
+
return [...groupBy(phases, (phase) => phase.name).entries()]
|
|
232
|
+
.map(([name, entries]) => {
|
|
233
|
+
const durations = entries.map((entry) => entry.duration_ms);
|
|
234
|
+
return {
|
|
235
|
+
name,
|
|
236
|
+
sample_count: entries.length,
|
|
237
|
+
total_duration_ms: durations.reduce((sum, value) => sum + value, 0),
|
|
238
|
+
max_duration_ms: Math.max(...durations),
|
|
239
|
+
p95_duration_ms: percentile(durations, 95),
|
|
240
|
+
};
|
|
241
|
+
})
|
|
242
|
+
.sort((left, right) => right.p95_duration_ms - left.p95_duration_ms || left.name.localeCompare(right.name));
|
|
243
|
+
}
|
|
244
|
+
function summarizeIntent(intent, samples, phaseSummaries) {
|
|
245
|
+
const durations = samples.map((sample) => sample.duration_ms);
|
|
246
|
+
const selectedTargetCounts = samples
|
|
247
|
+
.map((sample) => sample.selected_target_count)
|
|
248
|
+
.filter((value) => typeof value === 'number');
|
|
249
|
+
return {
|
|
250
|
+
intent,
|
|
251
|
+
sample_count: samples.length,
|
|
252
|
+
success_count: samples.filter((sample) => sample.status === 'passed').length,
|
|
253
|
+
failure_count: samples.filter((sample) => sample.status === 'failed').length,
|
|
254
|
+
fallback_count: samples.filter((sample) => sample.fallback_used === true).length,
|
|
255
|
+
latest_duration_ms: samples.at(-1)?.duration_ms ?? 0,
|
|
256
|
+
min_duration_ms: Math.min(...durations),
|
|
257
|
+
p50_duration_ms: percentile(durations, 50),
|
|
258
|
+
p95_duration_ms: percentile(durations, 95),
|
|
259
|
+
max_duration_ms: Math.max(...durations),
|
|
260
|
+
max_timeout_ratio: Math.max(...samples.map((sample) => sample.timeout_ratio)),
|
|
261
|
+
selected_target_count_max: selectedTargetCounts.length > 0 ? Math.max(...selectedTargetCounts) : null,
|
|
262
|
+
slowest_phase: phaseSummaries[0] ?? null,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
function summarizeIntents(samples, phases, maxIntents) {
|
|
266
|
+
return [...groupBy(samples, (sample) => sample.intent).entries()]
|
|
267
|
+
.map(([intent, intentSamples]) => {
|
|
268
|
+
const intentPhases = phases.filter((phase) => phase.intent === intent);
|
|
269
|
+
return summarizeIntent(intent, intentSamples, summarizePhases(intentPhases));
|
|
270
|
+
})
|
|
271
|
+
.sort((left, right) => right.p95_duration_ms - left.p95_duration_ms || left.intent.localeCompare(right.intent))
|
|
272
|
+
.slice(0, maxIntents);
|
|
273
|
+
}
|
|
274
|
+
function summarizeLatestProfilePhases(profile) {
|
|
275
|
+
if (!isRecord(profile) || !Array.isArray(profile.phases)) {
|
|
276
|
+
return [];
|
|
277
|
+
}
|
|
278
|
+
return profile.phases.filter((phase) => isRunReceiptPhase(phase));
|
|
279
|
+
}
|
|
280
|
+
function summarizeLatestProfileTestFiles(profile, intent, maxTestFiles) {
|
|
281
|
+
if (!isRecord(profile)) {
|
|
282
|
+
return [];
|
|
283
|
+
}
|
|
284
|
+
const entries = Array.isArray(profile.test_files)
|
|
285
|
+
? profile.test_files
|
|
286
|
+
: Array.isArray(profile.files)
|
|
287
|
+
? profile.files
|
|
288
|
+
: Array.isArray(profile.timings)
|
|
289
|
+
? profile.timings
|
|
290
|
+
: [];
|
|
291
|
+
return entries
|
|
292
|
+
.flatMap((entry) => {
|
|
293
|
+
if (!isRecord(entry)) {
|
|
294
|
+
return [];
|
|
295
|
+
}
|
|
296
|
+
const rawPath = typeof entry.path === 'string'
|
|
297
|
+
? entry.path
|
|
298
|
+
: typeof entry.testPath === 'string'
|
|
299
|
+
? entry.testPath
|
|
300
|
+
: typeof entry.file === 'string'
|
|
301
|
+
? entry.file
|
|
302
|
+
: null;
|
|
303
|
+
const rawDuration = isNonNegativeNumber(entry.duration_ms)
|
|
304
|
+
? entry.duration_ms
|
|
305
|
+
: isNonNegativeNumber(entry.durationMs)
|
|
306
|
+
? entry.durationMs
|
|
307
|
+
: null;
|
|
308
|
+
const status = parseProfileTestFileStatus(entry.status ?? entry.exit_code ?? entry.exitCode);
|
|
309
|
+
if (!rawPath || rawDuration === null || !status) {
|
|
310
|
+
return [];
|
|
311
|
+
}
|
|
312
|
+
return [{
|
|
313
|
+
path: normalizeRelativePath(rawPath),
|
|
314
|
+
intent: typeof profile.intent === 'string' ? profile.intent : intent ?? 'latest_profile',
|
|
315
|
+
status,
|
|
316
|
+
duration_ms: rawDuration,
|
|
317
|
+
source: 'latest_profile',
|
|
318
|
+
}];
|
|
319
|
+
})
|
|
320
|
+
.sort((left, right) => right.duration_ms - left.duration_ms || left.path.localeCompare(right.path))
|
|
321
|
+
.slice(0, maxTestFiles);
|
|
322
|
+
}
|
|
323
|
+
function extractLatestProfileGeneratedAt(profile) {
|
|
324
|
+
if (!isRecord(profile) || typeof profile.generated_at !== 'string') {
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
return profile.generated_at;
|
|
328
|
+
}
|
|
329
|
+
function extractLatestProfileAgeMs(profile, nowMs) {
|
|
330
|
+
const generatedAt = extractLatestProfileGeneratedAt(profile);
|
|
331
|
+
if (!generatedAt) {
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
const generatedAtMs = Date.parse(generatedAt);
|
|
335
|
+
if (!Number.isFinite(generatedAtMs)) {
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
return Math.max(0, nowMs - generatedAtMs);
|
|
339
|
+
}
|
|
340
|
+
function extractLatestProfileDeclaredTestFileCount(profile) {
|
|
341
|
+
if (!isRecord(profile) || !isNonNegativeNumber(profile.file_count)) {
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
return Math.floor(profile.file_count);
|
|
345
|
+
}
|
|
346
|
+
function latestProfileTestFileCoverageRatio(visibleTestFileCount, latestProfileTestFileCount, declaredTestFileCount) {
|
|
347
|
+
const denominator = Math.max(declaredTestFileCount ?? 0, latestProfileTestFileCount);
|
|
348
|
+
if (denominator <= 0) {
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
return visibleTestFileCount / denominator;
|
|
352
|
+
}
|
|
353
|
+
function latestProfileActualTestFileCoverageRatio(latestProfileTestFileCount, declaredTestFileCount) {
|
|
354
|
+
if (declaredTestFileCount === null || declaredTestFileCount <= 0) {
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
return latestProfileTestFileCount / declaredTestFileCount;
|
|
358
|
+
}
|
|
359
|
+
function extractRetainedSummaryIntentCount(summary) {
|
|
360
|
+
if (!isRecord(summary) || !isRecord(summary.intents)) {
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
return Object.keys(summary.intents).length;
|
|
364
|
+
}
|
|
365
|
+
function createFindings(samples, phases, testFiles, policy, findings) {
|
|
366
|
+
if (samples.length === 0) {
|
|
367
|
+
pushFinding(findings, {
|
|
368
|
+
code: 'test_performance_no_evidence',
|
|
369
|
+
severity: 'low',
|
|
370
|
+
message: 'No run performance evidence was found under .mustflow/state.',
|
|
371
|
+
}, policy.max_findings);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
for (const sample of samples) {
|
|
375
|
+
if (sample.status === 'failed') {
|
|
376
|
+
pushFinding(findings, {
|
|
377
|
+
code: 'test_performance_previous_failure',
|
|
378
|
+
severity: 'medium',
|
|
379
|
+
intent: sample.intent,
|
|
380
|
+
message: `${sample.intent} has a failed performance sample.`,
|
|
381
|
+
}, policy.max_findings);
|
|
382
|
+
}
|
|
383
|
+
if (sample.duration_ms >= policy.slow_sample_threshold_ms) {
|
|
384
|
+
pushFinding(findings, {
|
|
385
|
+
code: 'test_performance_slow_sample',
|
|
386
|
+
severity: 'medium',
|
|
387
|
+
intent: sample.intent,
|
|
388
|
+
message: `${sample.intent} took ${sample.duration_ms}ms, above the ${policy.slow_sample_threshold_ms}ms threshold.`,
|
|
389
|
+
metric: 'duration_ms',
|
|
390
|
+
actual: sample.duration_ms,
|
|
391
|
+
expected: policy.slow_sample_threshold_ms,
|
|
392
|
+
}, policy.max_findings);
|
|
393
|
+
}
|
|
394
|
+
if (sample.timeout_ratio >= policy.high_timeout_ratio) {
|
|
395
|
+
pushFinding(findings, {
|
|
396
|
+
code: 'test_performance_high_timeout_ratio',
|
|
397
|
+
severity: 'medium',
|
|
398
|
+
intent: sample.intent,
|
|
399
|
+
message: `${sample.intent} consumed ${(sample.timeout_ratio * 100).toFixed(1)}% of its timeout budget.`,
|
|
400
|
+
metric: 'timeout_ratio',
|
|
401
|
+
actual: sample.timeout_ratio,
|
|
402
|
+
expected: policy.high_timeout_ratio,
|
|
403
|
+
}, policy.max_findings);
|
|
404
|
+
}
|
|
405
|
+
if (sample.fallback_used) {
|
|
406
|
+
pushFinding(findings, {
|
|
407
|
+
code: 'test_performance_selection_fallback',
|
|
408
|
+
severity: 'low',
|
|
409
|
+
intent: sample.intent,
|
|
410
|
+
message: `${sample.intent} used a selected-test fallback path.`,
|
|
411
|
+
}, policy.max_findings);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
for (const testFile of testFiles) {
|
|
415
|
+
if (testFile.status === 'failed') {
|
|
416
|
+
pushFinding(findings, {
|
|
417
|
+
code: 'test_performance_previous_failure',
|
|
418
|
+
severity: 'medium',
|
|
419
|
+
path: testFile.path,
|
|
420
|
+
intent: testFile.intent,
|
|
421
|
+
message: `${testFile.path} failed in the latest profiled test run.`,
|
|
422
|
+
}, policy.max_findings);
|
|
423
|
+
}
|
|
424
|
+
if (testFile.duration_ms >= policy.slow_sample_threshold_ms) {
|
|
425
|
+
pushFinding(findings, {
|
|
426
|
+
code: 'test_performance_slow_test_file',
|
|
427
|
+
severity: 'medium',
|
|
428
|
+
path: testFile.path,
|
|
429
|
+
intent: testFile.intent,
|
|
430
|
+
message: `${testFile.path} took ${testFile.duration_ms}ms, above the ${policy.slow_sample_threshold_ms}ms threshold.`,
|
|
431
|
+
metric: 'duration_ms',
|
|
432
|
+
actual: testFile.duration_ms,
|
|
433
|
+
expected: policy.slow_sample_threshold_ms,
|
|
434
|
+
}, policy.max_findings);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
for (const phase of phases) {
|
|
438
|
+
if (phase.p95_duration_ms >= policy.phase_bottleneck_threshold_ms) {
|
|
439
|
+
pushFinding(findings, {
|
|
440
|
+
code: 'test_performance_phase_bottleneck',
|
|
441
|
+
severity: 'medium',
|
|
442
|
+
phase: phase.name,
|
|
443
|
+
message: `${phase.name} phase p95 is ${phase.p95_duration_ms}ms.`,
|
|
444
|
+
metric: 'p95_duration_ms',
|
|
445
|
+
actual: phase.p95_duration_ms,
|
|
446
|
+
expected: policy.phase_bottleneck_threshold_ms,
|
|
447
|
+
}, policy.max_findings);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
function reportStatus(findings) {
|
|
452
|
+
if (findings.some((finding) => ERROR_CODES.has(finding.code))) {
|
|
453
|
+
return 'error';
|
|
454
|
+
}
|
|
455
|
+
return 'passed';
|
|
456
|
+
}
|
|
457
|
+
function hasFinding(findings, code) {
|
|
458
|
+
return findings.some((finding) => finding.code === code);
|
|
459
|
+
}
|
|
460
|
+
function profileTestFilesAreTruncated(testFiles, latestProfileTestFileCount) {
|
|
461
|
+
return latestProfileTestFileCount > testFiles.length;
|
|
462
|
+
}
|
|
463
|
+
function profileTestFilesAreTopHeavy(testFiles, policy) {
|
|
464
|
+
if (testFiles.length < 2) {
|
|
465
|
+
return false;
|
|
466
|
+
}
|
|
467
|
+
const totalDurationMs = testFiles.reduce((sum, testFile) => sum + testFile.duration_ms, 0);
|
|
468
|
+
const slowestDurationMs = testFiles[0]?.duration_ms ?? 0;
|
|
469
|
+
return (totalDurationMs > 0 &&
|
|
470
|
+
slowestDurationMs >= policy.phase_bottleneck_threshold_ms &&
|
|
471
|
+
slowestDurationMs / totalDurationMs >= 0.6);
|
|
472
|
+
}
|
|
473
|
+
function describeSlowestTestFileAction(testFiles, latestProfileTestFileCount) {
|
|
474
|
+
const baseMessage = 'Use the slowest profiled test-file rows to decide whether file-level sharding, fixture reuse, or test splitting is the bottleneck.';
|
|
475
|
+
if (!profileTestFilesAreTruncated(testFiles, latestProfileTestFileCount)) {
|
|
476
|
+
return baseMessage;
|
|
477
|
+
}
|
|
478
|
+
const truncationMessage = `The report shows ${testFiles.length} of ${latestProfileTestFileCount} profiled files; ` +
|
|
479
|
+
'raise --max-test-files when the hidden tail matters.';
|
|
480
|
+
return `${baseMessage} ${truncationMessage}`;
|
|
481
|
+
}
|
|
482
|
+
function pushProfileEvidenceAction(actions, action) {
|
|
483
|
+
const existingIndex = actions.findIndex((candidate) => candidate.code === 'collect_profile_evidence');
|
|
484
|
+
if (existingIndex < 0) {
|
|
485
|
+
actions.push(action);
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
const existing = actions[existingIndex];
|
|
489
|
+
actions[existingIndex] = {
|
|
490
|
+
...existing,
|
|
491
|
+
finding_codes: [...new Set([...existing.finding_codes, ...action.finding_codes])],
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
function createNextActions(samples, findings, testFiles, latestProfileTestFileCount, latestProfileAgeMs, latestProfileActualCoverageRatio, policy) {
|
|
495
|
+
const actions = [];
|
|
496
|
+
if (samples.length === 0 && hasFinding(findings, 'test_performance_no_evidence')) {
|
|
497
|
+
pushProfileEvidenceAction(actions, {
|
|
498
|
+
code: 'collect_profile_evidence',
|
|
499
|
+
message: 'Run a configured profiling intent before changing test scheduling, caching, timeout, or selection policy.',
|
|
500
|
+
command_intent: 'test_related_profile',
|
|
501
|
+
run_hint: 'mf run test_related_profile',
|
|
502
|
+
finding_codes: ['test_performance_no_evidence'],
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
if (samples.length > 0 && latestProfileTestFileCount === 0) {
|
|
506
|
+
pushProfileEvidenceAction(actions, {
|
|
507
|
+
code: 'collect_profile_evidence',
|
|
508
|
+
message: 'Collect test-file profile evidence before changing file-level sharding, fixture reuse, or test splitting policy.',
|
|
509
|
+
command_intent: 'test_related_profile',
|
|
510
|
+
run_hint: 'mf run test_related_profile',
|
|
511
|
+
finding_codes: [],
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
if (samples.length > 0 &&
|
|
515
|
+
latestProfileTestFileCount > 0 &&
|
|
516
|
+
latestProfileActualCoverageRatio !== null &&
|
|
517
|
+
latestProfileActualCoverageRatio < DEFAULT_LOW_PROFILE_COVERAGE_RATIO) {
|
|
518
|
+
pushProfileEvidenceAction(actions, {
|
|
519
|
+
code: 'collect_profile_evidence',
|
|
520
|
+
message: `Latest test profile includes only ${(latestProfileActualCoverageRatio * 100).toFixed(1)}% ` +
|
|
521
|
+
'of declared test files; collect fresh profile evidence before changing scheduling, caching, timeout, or fixture policy.',
|
|
522
|
+
command_intent: 'test_related_profile',
|
|
523
|
+
run_hint: 'mf run test_related_profile',
|
|
524
|
+
finding_codes: [],
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
if (samples.length > 0 &&
|
|
528
|
+
latestProfileTestFileCount > 0 &&
|
|
529
|
+
latestProfileAgeMs !== null &&
|
|
530
|
+
latestProfileAgeMs >= DEFAULT_STALE_PROFILE_THRESHOLD_MS) {
|
|
531
|
+
pushProfileEvidenceAction(actions, {
|
|
532
|
+
code: 'collect_profile_evidence',
|
|
533
|
+
message: 'Latest test profile is older than 24h; collect fresh profile evidence before changing scheduling, caching, timeout, or fixture policy.',
|
|
534
|
+
command_intent: 'test_related_profile',
|
|
535
|
+
run_hint: 'mf run test_related_profile',
|
|
536
|
+
finding_codes: [],
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
if (hasFinding(findings, 'test_performance_previous_failure')) {
|
|
540
|
+
actions.push({
|
|
541
|
+
code: 'investigate_previous_failure',
|
|
542
|
+
message: 'Resolve or classify previous failed test runs before using timing data as optimization evidence.',
|
|
543
|
+
command_intent: null,
|
|
544
|
+
run_hint: null,
|
|
545
|
+
finding_codes: ['test_performance_previous_failure'],
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
const slowFindingCodes = findings
|
|
549
|
+
.filter((finding) => finding.code === 'test_performance_slow_sample' ||
|
|
550
|
+
finding.code === 'test_performance_phase_bottleneck')
|
|
551
|
+
.map((finding) => finding.code);
|
|
552
|
+
if (slowFindingCodes.length > 0) {
|
|
553
|
+
actions.push({
|
|
554
|
+
code: 'inspect_slowest_intents',
|
|
555
|
+
message: 'Use the slowest intent and phase rows to classify discovery, startup, fixture, scheduling, or artifact cost before optimizing.',
|
|
556
|
+
command_intent: null,
|
|
557
|
+
run_hint: null,
|
|
558
|
+
finding_codes: [...new Set(slowFindingCodes)],
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
if (hasFinding(findings, 'test_performance_slow_test_file')) {
|
|
562
|
+
actions.push({
|
|
563
|
+
code: 'inspect_slowest_test_files',
|
|
564
|
+
message: describeSlowestTestFileAction(testFiles, latestProfileTestFileCount),
|
|
565
|
+
command_intent: null,
|
|
566
|
+
run_hint: null,
|
|
567
|
+
finding_codes: ['test_performance_slow_test_file'],
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
else if (profileTestFilesAreTruncated(testFiles, latestProfileTestFileCount) ||
|
|
571
|
+
profileTestFilesAreTopHeavy(testFiles, policy)) {
|
|
572
|
+
actions.push({
|
|
573
|
+
code: 'inspect_slowest_test_files',
|
|
574
|
+
message: describeSlowestTestFileAction(testFiles, latestProfileTestFileCount),
|
|
575
|
+
command_intent: null,
|
|
576
|
+
run_hint: null,
|
|
577
|
+
finding_codes: [],
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
if (hasFinding(findings, 'test_performance_high_timeout_ratio')) {
|
|
581
|
+
actions.push({
|
|
582
|
+
code: 'review_timeout_budget',
|
|
583
|
+
message: 'Review timeout pressure with fresh timing evidence before increasing command timeouts.',
|
|
584
|
+
command_intent: 'test_related_profile',
|
|
585
|
+
run_hint: 'mf run test_related_profile',
|
|
586
|
+
finding_codes: ['test_performance_high_timeout_ratio'],
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
if (hasFinding(findings, 'test_performance_selection_fallback')) {
|
|
590
|
+
actions.push({
|
|
591
|
+
code: 'review_selection_fallback',
|
|
592
|
+
message: 'Inspect selected-test fallback causes before relying on the fast related-test path for this change surface.',
|
|
593
|
+
command_intent: null,
|
|
594
|
+
run_hint: null,
|
|
595
|
+
finding_codes: ['test_performance_selection_fallback'],
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
return actions;
|
|
599
|
+
}
|
|
600
|
+
export function createTestPerformanceReport(projectRoot, options = {}) {
|
|
601
|
+
const root = path.resolve(projectRoot);
|
|
602
|
+
const policy = {
|
|
603
|
+
max_samples: options.maxSamples ?? DEFAULT_MAX_SAMPLES,
|
|
604
|
+
max_intents: options.maxIntents ?? DEFAULT_MAX_INTENTS,
|
|
605
|
+
max_test_files: options.maxTestFiles ?? DEFAULT_MAX_TEST_FILES,
|
|
606
|
+
max_findings: options.maxFindings ?? DEFAULT_MAX_FINDINGS,
|
|
607
|
+
slow_sample_threshold_ms: options.slowSampleThresholdMs ?? DEFAULT_SLOW_SAMPLE_THRESHOLD_MS,
|
|
608
|
+
high_timeout_ratio: options.highTimeoutRatio ?? DEFAULT_HIGH_TIMEOUT_RATIO,
|
|
609
|
+
phase_bottleneck_threshold_ms: options.phaseBottleneckThresholdMs ?? DEFAULT_PHASE_BOTTLENECK_THRESHOLD_MS,
|
|
610
|
+
};
|
|
611
|
+
const evidenceSources = [];
|
|
612
|
+
const findings = [];
|
|
613
|
+
const issues = [];
|
|
614
|
+
const historySamples = readJsonEvidence(root, PERFORMANCE_SAMPLES_PATH, 'performance_samples', (value) => (isRecord(value) && Array.isArray(value.samples) ? value.samples.filter(isHistorySample) : null), evidenceSources, findings, issues) ?? [];
|
|
615
|
+
const retainedSummaryIntentCount = readJsonEvidence(root, PERFORMANCE_SUMMARY_PATH, 'performance_summary', (value) => value, evidenceSources, findings, issues);
|
|
616
|
+
const latestReceipt = readJsonEvidence(root, LATEST_RUN_RECEIPT_PATH, 'latest_run_receipt', (value) => (isReportRunReceipt(value) ? value : null), evidenceSources, findings, issues);
|
|
617
|
+
const latestProfile = readJsonEvidence(root, LATEST_RUN_PROFILE_PATH, 'latest_run_profile', (value) => value, evidenceSources, findings, issues);
|
|
618
|
+
const latestSample = latestReceipt ? receiptToReportSample(latestReceipt) : null;
|
|
619
|
+
const allSamples = [
|
|
620
|
+
...historySamples.map(historySampleToReportSample),
|
|
621
|
+
...(latestSample ? [latestSample] : []),
|
|
622
|
+
];
|
|
623
|
+
const recentSamples = allSamples.slice(Math.max(0, allSamples.length - policy.max_samples));
|
|
624
|
+
const phaseEntries = [
|
|
625
|
+
...collectHistoryPhases(historySamples),
|
|
626
|
+
...collectReceiptPhases(latestReceipt),
|
|
627
|
+
...summarizeLatestProfilePhases(latestProfile).map((phase) => ({
|
|
628
|
+
intent: latestReceipt?.intent ?? 'latest_profile',
|
|
629
|
+
name: phase.name,
|
|
630
|
+
duration_ms: phase.duration_ms,
|
|
631
|
+
})),
|
|
632
|
+
];
|
|
633
|
+
const allTestFiles = summarizeLatestProfileTestFiles(latestProfile, latestReceipt?.intent ?? null, Number.MAX_SAFE_INTEGER);
|
|
634
|
+
const testFiles = allTestFiles.slice(0, policy.max_test_files);
|
|
635
|
+
const latestProfileDeclaredTestFileCount = extractLatestProfileDeclaredTestFileCount(latestProfile);
|
|
636
|
+
const latestProfileActualCoverageRatio = latestProfileActualTestFileCoverageRatio(allTestFiles.length, latestProfileDeclaredTestFileCount);
|
|
637
|
+
const latestProfileCoverageRatio = latestProfileTestFileCoverageRatio(testFiles.length, allTestFiles.length, latestProfileDeclaredTestFileCount);
|
|
638
|
+
const phases = summarizePhases(phaseEntries);
|
|
639
|
+
const intents = summarizeIntents(recentSamples, phaseEntries, policy.max_intents);
|
|
640
|
+
const visibleFindings = findings;
|
|
641
|
+
const nowMs = Date.now();
|
|
642
|
+
const latestProfileGeneratedAt = extractLatestProfileGeneratedAt(latestProfile);
|
|
643
|
+
const latestProfileAgeMs = extractLatestProfileAgeMs(latestProfile, nowMs);
|
|
644
|
+
createFindings(recentSamples, phases, testFiles, policy, visibleFindings);
|
|
645
|
+
const status = reportStatus(visibleFindings);
|
|
646
|
+
const nextActions = createNextActions(recentSamples, visibleFindings, testFiles, allTestFiles.length, latestProfileAgeMs, latestProfileActualCoverageRatio, policy);
|
|
647
|
+
const summary = {
|
|
648
|
+
evidence_source_count: evidenceSources.filter((source) => source.readable).length,
|
|
649
|
+
sample_count: recentSamples.length,
|
|
650
|
+
intent_count: new Set(recentSamples.map((sample) => sample.intent)).size,
|
|
651
|
+
test_file_count: testFiles.length,
|
|
652
|
+
finding_count: visibleFindings.length,
|
|
653
|
+
latest_run_intent: latestReceipt?.intent ?? null,
|
|
654
|
+
latest_run_status: latestReceipt?.performance.result_summary.status ?? null,
|
|
655
|
+
latest_run_duration_ms: latestReceipt?.performance.duration_ms ?? null,
|
|
656
|
+
latest_profile_phase_count: summarizeLatestProfilePhases(latestProfile).length,
|
|
657
|
+
latest_profile_test_file_count: allTestFiles.length,
|
|
658
|
+
latest_profile_declared_test_file_count: latestProfileDeclaredTestFileCount,
|
|
659
|
+
latest_profile_generated_at: latestProfileGeneratedAt,
|
|
660
|
+
latest_profile_age_ms: latestProfileAgeMs,
|
|
661
|
+
latest_profile_test_file_coverage_ratio: latestProfileCoverageRatio,
|
|
662
|
+
latest_profile_test_files_truncated: latestProfileCoverageRatio === null
|
|
663
|
+
? false
|
|
664
|
+
: latestProfileCoverageRatio < 1,
|
|
665
|
+
retained_summary_intent_count: extractRetainedSummaryIntentCount(retainedSummaryIntentCount),
|
|
666
|
+
};
|
|
667
|
+
const hashSummary = {
|
|
668
|
+
...summary,
|
|
669
|
+
latest_profile_age_ms: null,
|
|
670
|
+
};
|
|
671
|
+
return {
|
|
672
|
+
schema_version: '1',
|
|
673
|
+
command: 'script-pack',
|
|
674
|
+
pack_id: TEST_PERFORMANCE_REPORT_PACK_ID,
|
|
675
|
+
script_id: TEST_PERFORMANCE_REPORT_SCRIPT_ID,
|
|
676
|
+
script_ref: TEST_PERFORMANCE_REPORT_SCRIPT_REF,
|
|
677
|
+
action: 'summarize',
|
|
678
|
+
status,
|
|
679
|
+
ok: status === 'passed',
|
|
680
|
+
mustflow_root: root,
|
|
681
|
+
policy,
|
|
682
|
+
input_hash: sha256Tagged(JSON.stringify({ policy, evidenceSources, summary: hashSummary, recentSamples, nextActions })),
|
|
683
|
+
evidence_sources: evidenceSources,
|
|
684
|
+
summary,
|
|
685
|
+
intents,
|
|
686
|
+
phases,
|
|
687
|
+
test_files: testFiles,
|
|
688
|
+
recent_samples: recentSamples,
|
|
689
|
+
truncated: allSamples.length > recentSamples.length ||
|
|
690
|
+
new Set(allSamples.map((sample) => sample.intent)).size > intents.length ||
|
|
691
|
+
allTestFiles.length > testFiles.length ||
|
|
692
|
+
visibleFindings.length >= policy.max_findings,
|
|
693
|
+
findings: visibleFindings,
|
|
694
|
+
next_actions: nextActions,
|
|
695
|
+
issues,
|
|
696
|
+
};
|
|
697
|
+
}
|