vibepro 0.1.0-alpha.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/LICENSE +201 -0
- package/NOTICE +9 -0
- package/README.ja.md +448 -0
- package/README.md +520 -0
- package/agent-instructions/codex/AGENTS.vibepro.md +45 -0
- package/bin/vibepro.js +9 -0
- package/docs/assets/vibepro-header.png +0 -0
- package/package.json +51 -0
- package/skills/vibepro-diagnosis-packages/SKILL.md +133 -0
- package/skills/vibepro-human-review/SKILL.md +73 -0
- package/skills/vibepro-story-refactor/SKILL.md +89 -0
- package/skills/vibepro-workflow/SKILL.md +139 -0
- package/src/agent-harness-map.js +230 -0
- package/src/agent-harness-scanner.js +337 -0
- package/src/agent-review.js +2180 -0
- package/src/api-boundary-scanner.js +452 -0
- package/src/architecture-profiler.js +423 -0
- package/src/authorization-scoring.js +149 -0
- package/src/brainbase-importer.js +534 -0
- package/src/change-risk-classifier.js +195 -0
- package/src/check-packs.js +605 -0
- package/src/checkpoint-manager.js +233 -0
- package/src/cli.js +2213 -0
- package/src/code-quality-scanner.js +310 -0
- package/src/codex-manager.js +143 -0
- package/src/component-style-scanner.js +336 -0
- package/src/coverage-report.js +99 -0
- package/src/database-access-scanner.js +163 -0
- package/src/decision-records.js +315 -0
- package/src/design-modernize.js +1435 -0
- package/src/design-system.js +1732 -0
- package/src/diagnostic-engine.js +1945 -0
- package/src/diagram-requirement-resolver.js +194 -0
- package/src/doctor.js +677 -0
- package/src/environment-graph.js +424 -0
- package/src/execution-state.js +849 -0
- package/src/explore-evidence.js +425 -0
- package/src/flow-design-scanner.js +896 -0
- package/src/flow-verifier.js +887 -0
- package/src/gesture-interaction-scanner.js +330 -0
- package/src/graph-context.js +263 -0
- package/src/graphify-adapter.js +189 -0
- package/src/html-report.js +1035 -0
- package/src/journey-map.js +1299 -0
- package/src/language.js +48 -0
- package/src/lazy-pattern-detector.js +182 -0
- package/src/local-dev-scanner.js +135 -0
- package/src/managed-worktree-gate.js +187 -0
- package/src/managed-worktree.js +766 -0
- package/src/merge-manager.js +501 -0
- package/src/network-contract-scanner.js +442 -0
- package/src/nocodb-story-sync.js +386 -0
- package/src/oss-readiness-scanner.js +417 -0
- package/src/performance-evidence.js +756 -0
- package/src/performance-measurer.js +591 -0
- package/src/pr-manager.js +8220 -0
- package/src/presets.js +682 -0
- package/src/public-discovery-scanner.js +519 -0
- package/src/refactoring-delta-reporter.js +367 -0
- package/src/refactoring-opportunity-generator.js +797 -0
- package/src/regression-risk-scanner.js +146 -0
- package/src/repo-status.js +266 -0
- package/src/report-fingerprint.js +188 -0
- package/src/report-pr-body-prompt-template.md +108 -0
- package/src/report-pr-body-schema.json +95 -0
- package/src/report-store.js +135 -0
- package/src/report-validator.js +192 -0
- package/src/requirement-consistency.js +1066 -0
- package/src/runtime-info.js +134 -0
- package/src/self-dogfood-scanner.js +476 -0
- package/src/session-learning.js +164 -0
- package/src/skills-manager.js +157 -0
- package/src/spec-drift.js +378 -0
- package/src/spec-fingerprint.js +445 -0
- package/src/spec-prompt-template.md +155 -0
- package/src/spec-schema.json +219 -0
- package/src/spec-store.js +258 -0
- package/src/spec-validator.js +459 -0
- package/src/static-site-scanner.js +316 -0
- package/src/story-candidate-generator.js +85 -0
- package/src/story-catalog-generator.js +2813 -0
- package/src/story-html.js +156 -0
- package/src/story-manager.js +2144 -0
- package/src/story-task-generator.js +522 -0
- package/src/task-manager.js +1029 -0
- package/src/terminal-link-scanner.js +238 -0
- package/src/usage-report.js +417 -0
- package/src/verification-evidence.js +284 -0
- package/src/workspace.js +126 -0
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
const SCHEMA_VERSION = '0.1.0';
|
|
2
|
+
|
|
3
|
+
export function buildRefactoringDelta({ beforeEvidence = null, afterEvidence = null, beforeRun = null } = {}) {
|
|
4
|
+
const afterRunId = afterEvidence?.run_id ?? null;
|
|
5
|
+
const beforeRunId = beforeEvidence?.run_id ?? beforeRun?.run_id ?? null;
|
|
6
|
+
if (!beforeEvidence) {
|
|
7
|
+
return {
|
|
8
|
+
schema_version: SCHEMA_VERSION,
|
|
9
|
+
status: 'no_baseline',
|
|
10
|
+
before_run_id: beforeRunId,
|
|
11
|
+
after_run_id: afterRunId,
|
|
12
|
+
summary: emptySummary(),
|
|
13
|
+
items: [],
|
|
14
|
+
top_improvements: [],
|
|
15
|
+
top_regressions: [],
|
|
16
|
+
top_remaining: buildRemainingItems([], afterEvidence)
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const beforeItems = indexOpportunities(beforeEvidence.refactoring_opportunities);
|
|
21
|
+
const afterItems = indexOpportunities(afterEvidence?.refactoring_opportunities);
|
|
22
|
+
if (beforeItems.size === 0 && afterItems.size === 0) {
|
|
23
|
+
return {
|
|
24
|
+
schema_version: SCHEMA_VERSION,
|
|
25
|
+
status: 'no_refactoring_opportunities',
|
|
26
|
+
before_run_id: beforeRunId,
|
|
27
|
+
after_run_id: afterRunId,
|
|
28
|
+
summary: emptySummary(),
|
|
29
|
+
items: [],
|
|
30
|
+
top_improvements: [],
|
|
31
|
+
top_regressions: [],
|
|
32
|
+
top_remaining: []
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const keys = [...new Set([...beforeItems.keys(), ...afterItems.keys()])].sort();
|
|
37
|
+
const items = keys.map((key) => buildDeltaItem(key, beforeItems.get(key), afterItems.get(key)));
|
|
38
|
+
const summary = summarizeItems(items, beforeItems.size, afterItems.size);
|
|
39
|
+
return {
|
|
40
|
+
schema_version: SCHEMA_VERSION,
|
|
41
|
+
status: 'available',
|
|
42
|
+
before_run_id: beforeRunId,
|
|
43
|
+
after_run_id: afterRunId,
|
|
44
|
+
summary,
|
|
45
|
+
items,
|
|
46
|
+
top_improvements: items
|
|
47
|
+
.filter((item) => ['improved', 'removed'].includes(item.status))
|
|
48
|
+
.sort(compareImprovements)
|
|
49
|
+
.slice(0, 10),
|
|
50
|
+
top_regressions: items
|
|
51
|
+
.filter((item) => ['regressed', 'new'].includes(item.status))
|
|
52
|
+
.sort(compareRegressions)
|
|
53
|
+
.slice(0, 10),
|
|
54
|
+
top_remaining: buildRemainingItems(items)
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function renderRefactoringDelta(delta, { limit = 10 } = {}) {
|
|
59
|
+
if (!delta || delta.status === 'no_baseline') {
|
|
60
|
+
return `# VibePro リファクタリング差分
|
|
61
|
+
|
|
62
|
+
- status: no_baseline
|
|
63
|
+
- before: -
|
|
64
|
+
- after: ${delta?.after_run_id ?? '-'}
|
|
65
|
+
|
|
66
|
+
前回の同一Story診断runがないため、差分はまだ算出していません。
|
|
67
|
+
`;
|
|
68
|
+
}
|
|
69
|
+
if (delta.status === 'no_refactoring_opportunities') {
|
|
70
|
+
return `# VibePro リファクタリング差分
|
|
71
|
+
|
|
72
|
+
- status: no_refactoring_opportunities
|
|
73
|
+
- before: ${delta.before_run_id ?? '-'}
|
|
74
|
+
- after: ${delta.after_run_id ?? '-'}
|
|
75
|
+
|
|
76
|
+
比較対象の両runにリファクタリング機会はありません。
|
|
77
|
+
`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const improvements = delta.top_improvements?.slice(0, limit) ?? [];
|
|
81
|
+
const regressions = delta.top_regressions?.slice(0, limit) ?? [];
|
|
82
|
+
const remaining = delta.top_remaining?.slice(0, limit) ?? [];
|
|
83
|
+
return `# VibePro リファクタリング差分
|
|
84
|
+
|
|
85
|
+
| 項目 | 内容 |
|
|
86
|
+
|------|------|
|
|
87
|
+
| Status | ${delta.status} |
|
|
88
|
+
| Before run | ${delta.before_run_id ?? '-'} |
|
|
89
|
+
| After run | ${delta.after_run_id ?? '-'} |
|
|
90
|
+
| Before機会 | ${delta.summary?.total_before ?? 0}件 |
|
|
91
|
+
| After機会 | ${delta.summary?.total_after ?? 0}件 |
|
|
92
|
+
| 改善 | ${delta.summary?.improved ?? 0}件 |
|
|
93
|
+
| 解消 | ${delta.summary?.removed ?? 0}件 |
|
|
94
|
+
| 悪化 | ${delta.summary?.regressed ?? 0}件 |
|
|
95
|
+
| 新規 | ${delta.summary?.new ?? 0}件 |
|
|
96
|
+
|
|
97
|
+
## 改善・解消
|
|
98
|
+
|
|
99
|
+
${renderDeltaTable(improvements)}
|
|
100
|
+
|
|
101
|
+
## 悪化・新規
|
|
102
|
+
|
|
103
|
+
${renderDeltaTable(regressions)}
|
|
104
|
+
|
|
105
|
+
## 残っている上位候補
|
|
106
|
+
|
|
107
|
+
${renderRemainingTable(remaining)}
|
|
108
|
+
`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function renderRefactoringDeltaCompact(delta, { limit = 5 } = {}) {
|
|
112
|
+
if (!delta || delta.status === 'no_baseline') {
|
|
113
|
+
return '- 前回の同一Story診断runがないため、差分は未算出';
|
|
114
|
+
}
|
|
115
|
+
if (delta.status === 'no_refactoring_opportunities') {
|
|
116
|
+
return '- 比較対象の両runにリファクタリング機会なし';
|
|
117
|
+
}
|
|
118
|
+
const lines = [
|
|
119
|
+
`- before: ${delta.before_run_id ?? '-'} / after: ${delta.after_run_id ?? '-'}`,
|
|
120
|
+
`- 改善: ${delta.summary?.improved ?? 0}件 / 解消: ${delta.summary?.removed ?? 0}件 / 悪化: ${delta.summary?.regressed ?? 0}件 / 新規: ${delta.summary?.new ?? 0}件`
|
|
121
|
+
];
|
|
122
|
+
const improvements = delta.top_improvements?.slice(0, limit) ?? [];
|
|
123
|
+
if (improvements.length === 0) {
|
|
124
|
+
lines.push('- 主な改善: なし');
|
|
125
|
+
} else {
|
|
126
|
+
for (const item of improvements) {
|
|
127
|
+
lines.push(`- ${formatDeltaItemLabel(item)}: ${formatCounts(item.before)} -> ${formatCounts(item.after)} (${formatStatus(item.status)})`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const remaining = delta.top_remaining?.slice(0, 3) ?? [];
|
|
131
|
+
if (remaining.length > 0) {
|
|
132
|
+
lines.push('- 次の候補:');
|
|
133
|
+
for (const item of remaining) {
|
|
134
|
+
lines.push(` - ${formatDeltaItemLabel(item)}: ${formatCounts(item.after)} (${item.refactoring_intent ?? '-'})`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return lines.join('\n');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function formatCounts(counts) {
|
|
141
|
+
return `${counts?.target_file_count ?? 0}ファイル / ${counts?.occurrence_count ?? 0}出現`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function indexOpportunities(opportunities) {
|
|
145
|
+
const indexed = new Map();
|
|
146
|
+
for (const opportunity of Array.isArray(opportunities) ? opportunities : []) {
|
|
147
|
+
const key = buildOpportunityKey(opportunity);
|
|
148
|
+
const existing = indexed.get(key);
|
|
149
|
+
if (!existing) {
|
|
150
|
+
indexed.set(key, normalizeOpportunity(opportunity, key));
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
indexed.set(key, mergeOpportunity(existing, opportunity));
|
|
154
|
+
}
|
|
155
|
+
return indexed;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function buildOpportunityKey(opportunity) {
|
|
159
|
+
const source = opportunity?.source ?? 'unknown';
|
|
160
|
+
const refs = opportunity?.evidence_refs ?? {};
|
|
161
|
+
if (source === 'duplicate_query_shape' && refs.signature) return `${source}:${refs.signature}`;
|
|
162
|
+
if (source === 'responsibility_hotspot' && refs.file) return `${source}:${normalizePath(refs.file)}`;
|
|
163
|
+
const title = opportunity?.title ?? 'untitled';
|
|
164
|
+
const files = uniqueStrings(opportunity?.target_files ?? []).join(',');
|
|
165
|
+
return `${source}:${title}:${files}`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function normalizeOpportunity(opportunity, key) {
|
|
169
|
+
const targetFiles = uniqueStrings(opportunity?.target_files ?? []);
|
|
170
|
+
const refs = opportunity?.evidence_refs ?? {};
|
|
171
|
+
const occurrenceCount = Number.isFinite(refs.occurrence_count)
|
|
172
|
+
? refs.occurrence_count
|
|
173
|
+
: Number.isFinite(refs.file_count)
|
|
174
|
+
? refs.file_count
|
|
175
|
+
: targetFiles.length;
|
|
176
|
+
return {
|
|
177
|
+
key,
|
|
178
|
+
source: opportunity?.source ?? null,
|
|
179
|
+
title: opportunity?.title ?? key,
|
|
180
|
+
refactoring_intent: opportunity?.refactoring_intent ?? null,
|
|
181
|
+
finding_id: opportunity?.finding_id ?? null,
|
|
182
|
+
evidence_refs: refs,
|
|
183
|
+
counts: {
|
|
184
|
+
target_file_count: Number.isFinite(opportunity?.target_count) ? opportunity.target_count : targetFiles.length,
|
|
185
|
+
occurrence_count: occurrenceCount,
|
|
186
|
+
rank: opportunity?.rank ?? null,
|
|
187
|
+
score_total: opportunity?.score?.total ?? null
|
|
188
|
+
},
|
|
189
|
+
target_files: targetFiles
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function mergeOpportunity(existing, opportunity) {
|
|
194
|
+
const next = normalizeOpportunity(opportunity, existing.key);
|
|
195
|
+
const targetFiles = uniqueStrings([...existing.target_files, ...next.target_files]);
|
|
196
|
+
return {
|
|
197
|
+
...existing,
|
|
198
|
+
title: existing.title ?? next.title,
|
|
199
|
+
counts: {
|
|
200
|
+
target_file_count: targetFiles.length,
|
|
201
|
+
occurrence_count: existing.counts.occurrence_count + next.counts.occurrence_count,
|
|
202
|
+
rank: existing.counts.rank ?? next.counts.rank,
|
|
203
|
+
score_total: (existing.counts.score_total ?? 0) + (next.counts.score_total ?? 0)
|
|
204
|
+
},
|
|
205
|
+
target_files: targetFiles
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function buildDeltaItem(key, before, after) {
|
|
210
|
+
const beforeCounts = before?.counts ?? zeroCounts();
|
|
211
|
+
const afterCounts = after?.counts ?? zeroCounts();
|
|
212
|
+
const targetFilesBefore = before?.target_files ?? [];
|
|
213
|
+
const targetFilesAfter = after?.target_files ?? [];
|
|
214
|
+
const targetFilesRemoved = targetFilesBefore.filter((file) => !targetFilesAfter.includes(file));
|
|
215
|
+
const targetFilesAdded = targetFilesAfter.filter((file) => !targetFilesBefore.includes(file));
|
|
216
|
+
const occurrenceDelta = afterCounts.occurrence_count - beforeCounts.occurrence_count;
|
|
217
|
+
const targetFileDelta = afterCounts.target_file_count - beforeCounts.target_file_count;
|
|
218
|
+
return {
|
|
219
|
+
key,
|
|
220
|
+
source: after?.source ?? before?.source ?? null,
|
|
221
|
+
title: after?.title ?? before?.title ?? key,
|
|
222
|
+
refactoring_intent: after?.refactoring_intent ?? before?.refactoring_intent ?? null,
|
|
223
|
+
finding_id: after?.finding_id ?? before?.finding_id ?? null,
|
|
224
|
+
before: beforeCounts,
|
|
225
|
+
after: afterCounts,
|
|
226
|
+
occurrence_delta: occurrenceDelta,
|
|
227
|
+
target_file_delta: targetFileDelta,
|
|
228
|
+
target_files_removed: targetFilesRemoved,
|
|
229
|
+
target_files_added: targetFilesAdded,
|
|
230
|
+
target_files_after: targetFilesAfter,
|
|
231
|
+
status: classifyStatus({ before, after, occurrenceDelta, targetFileDelta })
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function classifyStatus({ before, after, occurrenceDelta, targetFileDelta }) {
|
|
236
|
+
if (before && !after) return 'removed';
|
|
237
|
+
if (!before && after) return 'new';
|
|
238
|
+
if (occurrenceDelta < 0 || targetFileDelta < 0) return 'improved';
|
|
239
|
+
if (occurrenceDelta > 0 || targetFileDelta > 0) return 'regressed';
|
|
240
|
+
return 'unchanged';
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function summarizeItems(items, totalBefore, totalAfter) {
|
|
244
|
+
return {
|
|
245
|
+
total_before: totalBefore,
|
|
246
|
+
total_after: totalAfter,
|
|
247
|
+
improved: items.filter((item) => item.status === 'improved').length,
|
|
248
|
+
removed: items.filter((item) => item.status === 'removed').length,
|
|
249
|
+
regressed: items.filter((item) => item.status === 'regressed').length,
|
|
250
|
+
new: items.filter((item) => item.status === 'new').length,
|
|
251
|
+
unchanged: items.filter((item) => item.status === 'unchanged').length
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function emptySummary() {
|
|
256
|
+
return {
|
|
257
|
+
total_before: 0,
|
|
258
|
+
total_after: 0,
|
|
259
|
+
improved: 0,
|
|
260
|
+
removed: 0,
|
|
261
|
+
regressed: 0,
|
|
262
|
+
new: 0,
|
|
263
|
+
unchanged: 0
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function renderDeltaTable(items) {
|
|
268
|
+
if (!Array.isArray(items) || items.length === 0) return '- なし';
|
|
269
|
+
const rows = items.map((item) => (
|
|
270
|
+
`| ${escapeTable(formatDeltaItemLabel(item))} | ${item.refactoring_intent ?? '-'} | ${formatCounts(item.before)} | ${formatCounts(item.after)} | ${item.target_files_removed.join('<br>') || '-'} | ${formatStatus(item.status)} |`
|
|
271
|
+
));
|
|
272
|
+
return `| 対象 | Intent | Before | After | 減ったファイル | Status |
|
|
273
|
+
|------|--------|--------|-------|--------------|--------|
|
|
274
|
+
${rows.join('\n')}`;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function renderRemainingTable(items) {
|
|
278
|
+
if (!Array.isArray(items) || items.length === 0) return '- なし';
|
|
279
|
+
const rows = items.map((item) => (
|
|
280
|
+
`| ${escapeTable(formatDeltaItemLabel(item))} | ${item.refactoring_intent ?? '-'} | ${formatCounts(item.after)} | ${item.target_files_after?.slice(0, 6).join('<br>') || '-'} | ${formatStatus(item.status)} |`
|
|
281
|
+
));
|
|
282
|
+
return `| 対象 | Intent | Current | Files | Delta status |
|
|
283
|
+
|------|--------|---------|-------|--------------|
|
|
284
|
+
${rows.join('\n')}`;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function formatDeltaItemLabel(item) {
|
|
288
|
+
const keyTail = item.key.includes(':') ? item.key.slice(item.key.indexOf(':') + 1) : item.key;
|
|
289
|
+
return item.title && item.title !== item.key ? item.title : keyTail;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function formatStatus(status) {
|
|
293
|
+
return {
|
|
294
|
+
improved: '改善',
|
|
295
|
+
removed: '解消',
|
|
296
|
+
regressed: '悪化',
|
|
297
|
+
new: '新規',
|
|
298
|
+
unchanged: '変化なし'
|
|
299
|
+
}[status] ?? status;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function compareImprovements(a, b) {
|
|
303
|
+
return improvementMagnitude(b) - improvementMagnitude(a)
|
|
304
|
+
|| a.key.localeCompare(b.key);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function compareRegressions(a, b) {
|
|
308
|
+
return regressionMagnitude(b) - regressionMagnitude(a)
|
|
309
|
+
|| a.key.localeCompare(b.key);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function buildRemainingItems(items, afterEvidence = null) {
|
|
313
|
+
if (!Array.isArray(items) || items.length === 0) {
|
|
314
|
+
return [...indexOpportunities(afterEvidence?.refactoring_opportunities).values()]
|
|
315
|
+
.map((item) => ({
|
|
316
|
+
...item,
|
|
317
|
+
after: item.counts,
|
|
318
|
+
status: 'unchanged',
|
|
319
|
+
target_files_after: item.target_files
|
|
320
|
+
}))
|
|
321
|
+
.sort(compareRemaining)
|
|
322
|
+
.slice(0, 10);
|
|
323
|
+
}
|
|
324
|
+
return items
|
|
325
|
+
.filter((item) => (item.after?.target_file_count ?? 0) > 0 || (item.after?.occurrence_count ?? 0) > 0)
|
|
326
|
+
.sort(compareRemaining)
|
|
327
|
+
.slice(0, 10);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function compareRemaining(a, b) {
|
|
331
|
+
const aRank = Number.isFinite(a.after?.rank) ? a.after.rank : Number.MAX_SAFE_INTEGER;
|
|
332
|
+
const bRank = Number.isFinite(b.after?.rank) ? b.after.rank : Number.MAX_SAFE_INTEGER;
|
|
333
|
+
return aRank - bRank
|
|
334
|
+
|| (b.after?.score_total ?? 0) - (a.after?.score_total ?? 0)
|
|
335
|
+
|| (b.after?.occurrence_count ?? 0) - (a.after?.occurrence_count ?? 0)
|
|
336
|
+
|| (b.after?.target_file_count ?? 0) - (a.after?.target_file_count ?? 0)
|
|
337
|
+
|| a.key.localeCompare(b.key);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function improvementMagnitude(item) {
|
|
341
|
+
return Math.max(0, -item.occurrence_delta) + Math.max(0, -item.target_file_delta);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function regressionMagnitude(item) {
|
|
345
|
+
return Math.max(0, item.occurrence_delta) + Math.max(0, item.target_file_delta);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function zeroCounts() {
|
|
349
|
+
return {
|
|
350
|
+
target_file_count: 0,
|
|
351
|
+
occurrence_count: 0,
|
|
352
|
+
rank: null,
|
|
353
|
+
score_total: null
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function uniqueStrings(values) {
|
|
358
|
+
return [...new Set(values.map((value) => normalizePath(value)).filter(Boolean))].sort();
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function normalizePath(value) {
|
|
362
|
+
return String(value ?? '').replace(/\\/g, '/').replace(/^\.\//, '');
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function escapeTable(value) {
|
|
366
|
+
return String(value).replace(/\|/g, '\\|');
|
|
367
|
+
}
|