@vreko/cli 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +45 -0
- package/dist/CeremonyView-LQS7FTMK.js +134 -0
- package/dist/CeremonyView-LQS7FTMK.js.map +1 -0
- package/dist/InitApp-7K5DTYSW.js +1479 -0
- package/dist/InitApp-7K5DTYSW.js.map +1 -0
- package/dist/SkippedTestDetector-PJSKSOZR.js +7 -0
- package/dist/SkippedTestDetector-PJSKSOZR.js.map +1 -0
- package/dist/TuiApp-FX23XQBK.js +8 -0
- package/dist/TuiApp-FX23XQBK.js.map +1 -0
- package/dist/analysis-ABEO6RTN.js +8 -0
- package/dist/analysis-ABEO6RTN.js.map +1 -0
- package/dist/auth-XNBEBNPY.js +7669 -0
- package/dist/auth-XNBEBNPY.js.map +1 -0
- package/dist/ceremony-M7CXVBVA.js +45 -0
- package/dist/ceremony-M7CXVBVA.js.map +1 -0
- package/dist/chunk-A3QSZJPD.js +3147 -0
- package/dist/chunk-A3QSZJPD.js.map +1 -0
- package/dist/chunk-ASGZ5B6C.js +3969 -0
- package/dist/chunk-ASGZ5B6C.js.map +1 -0
- package/dist/chunk-DMXC2JTC.js +58 -0
- package/dist/chunk-DMXC2JTC.js.map +1 -0
- package/dist/chunk-EEBSK2IH.js +161 -0
- package/dist/chunk-EEBSK2IH.js.map +1 -0
- package/dist/chunk-EWOJGXRX.js +22 -0
- package/dist/chunk-EWOJGXRX.js.map +1 -0
- package/dist/chunk-F7GEJLP7.js +2389 -0
- package/dist/chunk-F7GEJLP7.js.map +1 -0
- package/dist/chunk-GOYL3F4T.js +605 -0
- package/dist/chunk-GOYL3F4T.js.map +1 -0
- package/dist/chunk-GRMRYWYS.js +17 -0
- package/dist/chunk-GRMRYWYS.js.map +1 -0
- package/dist/chunk-GSUGROXB.js +1951 -0
- package/dist/chunk-GSUGROXB.js.map +1 -0
- package/dist/chunk-H7773ONB.js +50 -0
- package/dist/chunk-H7773ONB.js.map +1 -0
- package/dist/chunk-HFQHU5LC.js +445 -0
- package/dist/chunk-HFQHU5LC.js.map +1 -0
- package/dist/chunk-IVHUBLJD.js +318 -0
- package/dist/chunk-IVHUBLJD.js.map +1 -0
- package/dist/chunk-KJWKY4L4.js +14 -0
- package/dist/chunk-KJWKY4L4.js.map +1 -0
- package/dist/chunk-MJVY2XUN.js +1793 -0
- package/dist/chunk-MJVY2XUN.js.map +1 -0
- package/dist/chunk-QWZVCJII.js +1797 -0
- package/dist/chunk-QWZVCJII.js.map +1 -0
- package/dist/chunk-VTSNRV3V.js +3237 -0
- package/dist/chunk-VTSNRV3V.js.map +1 -0
- package/dist/chunk-W5B4GTXR.js +1466 -0
- package/dist/chunk-W5B4GTXR.js.map +1 -0
- package/dist/chunk-WZEZLVOW.js +4995 -0
- package/dist/chunk-WZEZLVOW.js.map +1 -0
- package/dist/chunk-YPTTIXKC.js +199 -0
- package/dist/chunk-YPTTIXKC.js.map +1 -0
- package/dist/chunk-Z55UGM6X.js +6360 -0
- package/dist/chunk-Z55UGM6X.js.map +1 -0
- package/dist/chunk-ZIIRQODJ.js +110 -0
- package/dist/chunk-ZIIRQODJ.js.map +1 -0
- package/dist/chunk-ZSUQ4FMB.js +77 -0
- package/dist/chunk-ZSUQ4FMB.js.map +1 -0
- package/dist/client-JMTSZS3V.js +10 -0
- package/dist/client-JMTSZS3V.js.map +1 -0
- package/dist/deprecated-snap.js +19 -0
- package/dist/deprecated-snap.js.map +1 -0
- package/dist/dist-2KWBZFLA.js +14 -0
- package/dist/dist-2KWBZFLA.js.map +1 -0
- package/dist/dist-5ZYKNNU3.js +7 -0
- package/dist/dist-5ZYKNNU3.js.map +1 -0
- package/dist/dist-CP3RFHPI.js +11 -0
- package/dist/dist-CP3RFHPI.js.map +1 -0
- package/dist/gecko-53ITAGG6.js +56 -0
- package/dist/gecko-53ITAGG6.js.map +1 -0
- package/dist/guards-QAFC64NO.js +7 -0
- package/dist/guards-QAFC64NO.js.map +1 -0
- package/dist/index.js +57785 -0
- package/dist/index.js.map +1 -0
- package/dist/init-command-246JIVXM.js +7 -0
- package/dist/init-command-246JIVXM.js.map +1 -0
- package/dist/init-core-KAI7LCXZ.js +12 -0
- package/dist/init-core-KAI7LCXZ.js.map +1 -0
- package/dist/init-scan-RZNYDTUV.js +1919 -0
- package/dist/init-scan-RZNYDTUV.js.map +1 -0
- package/dist/local-service-adapter-6KNN6WQL.js +8 -0
- package/dist/local-service-adapter-6KNN6WQL.js.map +1 -0
- package/dist/secure-credentials-JXWAQLS2.js +306 -0
- package/dist/secure-credentials-JXWAQLS2.js.map +1 -0
- package/dist/tui-TPJPUS2R.js +111 -0
- package/dist/tui-TPJPUS2R.js.map +1 -0
- package/dist/vreko-dir-O3RLG7PI.js +8 -0
- package/dist/vreko-dir-O3RLG7PI.js.map +1 -0
- package/package.json +132 -0
- package/scripts/check-banned-words.ts +152 -0
- package/scripts/hooks/posttooluse-file-notify.sh +108 -0
- package/scripts/hooks/pretooluse-fragile-guard.sh +82 -0
- package/scripts/post-install-notice.js +24 -0
- package/scripts/postinstall.mjs +84 -0
- package/scripts/preuninstall.mjs +34 -0
- package/scripts/verify-jsx-transform.mjs +55 -0
|
@@ -0,0 +1,1919 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { __name, __require } from './chunk-EWOJGXRX.js';
|
|
3
|
+
import { EventEmitter } from 'events';
|
|
4
|
+
import { execFile } from 'child_process';
|
|
5
|
+
import { promisify } from 'util';
|
|
6
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
7
|
+
import { join } from 'path';
|
|
8
|
+
import { homedir } from 'os';
|
|
9
|
+
|
|
10
|
+
process.env.VREKO_CLI='true';process.env.NODE_NO_WARNINGS='1';
|
|
11
|
+
var __defProp = Object.defineProperty;
|
|
12
|
+
var __name2 = /* @__PURE__ */ __name((target, value) => __defProp(target, "name", {
|
|
13
|
+
value,
|
|
14
|
+
configurable: true
|
|
15
|
+
}), "__name");
|
|
16
|
+
var __require2 = /* @__PURE__ */ ((x) => typeof __require !== "undefined" ? __require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
17
|
+
get: /* @__PURE__ */ __name((a, b) => (typeof __require !== "undefined" ? __require : a)[b], "get")
|
|
18
|
+
}) : x)(function(x) {
|
|
19
|
+
if (typeof __require !== "undefined") return __require.apply(this, arguments);
|
|
20
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
21
|
+
});
|
|
22
|
+
var DiscoveryEmitter = class extends EventEmitter {
|
|
23
|
+
static {
|
|
24
|
+
__name(this, "DiscoveryEmitter");
|
|
25
|
+
}
|
|
26
|
+
static {
|
|
27
|
+
__name2(this, "DiscoveryEmitter");
|
|
28
|
+
}
|
|
29
|
+
emitDiscovery(discovery) {
|
|
30
|
+
if (discovery.confidence > 0.85) {
|
|
31
|
+
this.emit("discovery", discovery);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
function createDiscoveryEmitter() {
|
|
36
|
+
return new DiscoveryEmitter();
|
|
37
|
+
}
|
|
38
|
+
__name(createDiscoveryEmitter, "createDiscoveryEmitter");
|
|
39
|
+
__name2(createDiscoveryEmitter, "createDiscoveryEmitter");
|
|
40
|
+
var execFileAsync = promisify(execFile);
|
|
41
|
+
var ARTIFACT_BASENAMES = /* @__PURE__ */ new Set([
|
|
42
|
+
"pnpm-lock.yaml",
|
|
43
|
+
"yarn.lock",
|
|
44
|
+
"package-lock.json",
|
|
45
|
+
"bun.lockb",
|
|
46
|
+
"Gemfile.lock",
|
|
47
|
+
"Cargo.lock",
|
|
48
|
+
"poetry.lock",
|
|
49
|
+
"go.sum",
|
|
50
|
+
"composer.lock",
|
|
51
|
+
"package.json"
|
|
52
|
+
]);
|
|
53
|
+
var ARTIFACT_BUILD_SEGMENTS = /* @__PURE__ */ new Set([
|
|
54
|
+
"dist",
|
|
55
|
+
".next",
|
|
56
|
+
"__generated__",
|
|
57
|
+
".turbo",
|
|
58
|
+
".cache"
|
|
59
|
+
]);
|
|
60
|
+
function isArtifactFile(filePath) {
|
|
61
|
+
const parts = filePath.split("/");
|
|
62
|
+
const basename = parts[parts.length - 1];
|
|
63
|
+
if (ARTIFACT_BASENAMES.has(basename)) {
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
if (basename.endsWith(".d.ts") || basename.endsWith(".gen.ts") || basename.endsWith(".generated.ts") || basename.endsWith(".gen.js") || basename.endsWith(".generated.js") || basename.endsWith(".min.js") || basename.endsWith(".min.css")) {
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
for (const segment of parts.slice(0, -1)) {
|
|
70
|
+
if (ARTIFACT_BUILD_SEGMENTS.has(segment)) {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
__name(isArtifactFile, "isArtifactFile");
|
|
77
|
+
__name2(isArtifactFile, "isArtifactFile");
|
|
78
|
+
function topLevelDir(filePath) {
|
|
79
|
+
const parts = filePath.split("/");
|
|
80
|
+
return parts.length > 1 ? parts[0] : ".";
|
|
81
|
+
}
|
|
82
|
+
__name(topLevelDir, "topLevelDir");
|
|
83
|
+
__name2(topLevelDir, "topLevelDir");
|
|
84
|
+
function withinHours(a, b, hours) {
|
|
85
|
+
return Math.abs(a.getTime() - b.getTime()) <= hours * 60 * 60 * 1e3;
|
|
86
|
+
}
|
|
87
|
+
__name(withinHours, "withinHours");
|
|
88
|
+
__name2(withinHours, "withinHours");
|
|
89
|
+
async function analyzeGitLog(repoPath, emitter) {
|
|
90
|
+
const signals = {
|
|
91
|
+
avgCommitsPerDay: 0,
|
|
92
|
+
commitFrequencyVariance: 0,
|
|
93
|
+
largeCommitRatio: 0,
|
|
94
|
+
diffusion: {
|
|
95
|
+
avgFilesTouchedPerCommit: 0,
|
|
96
|
+
crossDirectoryDiffusionRate: 0,
|
|
97
|
+
rollbackAdjacentCommitRate: 0,
|
|
98
|
+
largeSessionClusterRate: 0,
|
|
99
|
+
maxSingleCommitSpread: 0
|
|
100
|
+
},
|
|
101
|
+
rollbackCorrelation: {
|
|
102
|
+
resetAfterLargeCommitRate: 0,
|
|
103
|
+
resetAfterBranchSwitchRate: 0,
|
|
104
|
+
resetAfterLateNightRate: 0,
|
|
105
|
+
resetAfterCrossPackageRate: 0,
|
|
106
|
+
branchSpecificRollbackDensity: /* @__PURE__ */ new Map(),
|
|
107
|
+
medianTimeToRecovery: 0,
|
|
108
|
+
topRecoveryTrigger: "large-commit"
|
|
109
|
+
},
|
|
110
|
+
fileChurnRanking: [],
|
|
111
|
+
coChangeGraph: [],
|
|
112
|
+
hotspotFiles: [],
|
|
113
|
+
contributorCount: 0,
|
|
114
|
+
busFactorEstimate: 0,
|
|
115
|
+
mergeConflictFrequency: 0
|
|
116
|
+
};
|
|
117
|
+
try {
|
|
118
|
+
const { stdout: logStdout } = await execFileAsync(
|
|
119
|
+
"git",
|
|
120
|
+
// --no-merges: merge commits produce empty numstat output - file changes
|
|
121
|
+
// were already counted in the original feature commits, so merge commits
|
|
122
|
+
// only add noise and cause fileChurnRanking to appear empty on repos with
|
|
123
|
+
// dense merge-commit histories (e.g. large open-source monorepos on canary branches).
|
|
124
|
+
//
|
|
125
|
+
// Omit --all: scanning every ref (tags, remote branches) on large repos like
|
|
126
|
+
// Next.js (~34k commits) exhausts the 10-second timeout and SIGTERM-kills the
|
|
127
|
+
// child process, causing the catch block to swallow the error and return all-zero
|
|
128
|
+
// signals. HEAD-reachable history is sufficient for behavioral intelligence - the
|
|
129
|
+
// developer's active branch is what matters, not all historical branches.
|
|
130
|
+
[
|
|
131
|
+
"log",
|
|
132
|
+
"--format=COMMIT:%H %aI %P|%s",
|
|
133
|
+
"--numstat",
|
|
134
|
+
"--no-merges",
|
|
135
|
+
"-n",
|
|
136
|
+
"10000"
|
|
137
|
+
],
|
|
138
|
+
{
|
|
139
|
+
cwd: repoPath,
|
|
140
|
+
maxBuffer: 20 * 1024 * 1024,
|
|
141
|
+
// 60 s: gives large monorepos (Next.js-scale, ~34k commits HEAD-reachable)
|
|
142
|
+
// enough headroom while still bounding the scan. The scan result is cached
|
|
143
|
+
// for 24 hours so a one-time 30-60s wait is acceptable on first run.
|
|
144
|
+
timeout: 6e4
|
|
145
|
+
}
|
|
146
|
+
);
|
|
147
|
+
const commits = parseLogOutput(logStdout);
|
|
148
|
+
if (commits.length === 0) {
|
|
149
|
+
return signals;
|
|
150
|
+
}
|
|
151
|
+
const dates = commits.map((c) => c.date.getTime()).sort((a, b) => a - b);
|
|
152
|
+
const timeSpanMs = dates[dates.length - 1] - dates[0];
|
|
153
|
+
const days = Math.max(timeSpanMs / (24 * 60 * 60 * 1e3), 1);
|
|
154
|
+
signals.avgCommitsPerDay = commits.length / days;
|
|
155
|
+
const commitsByDay = /* @__PURE__ */ new Map();
|
|
156
|
+
for (const c of commits) {
|
|
157
|
+
const dayKey = c.date.toISOString().slice(0, 10);
|
|
158
|
+
commitsByDay.set(dayKey, (commitsByDay.get(dayKey) || 0) + 1);
|
|
159
|
+
}
|
|
160
|
+
const dailyCounts = [
|
|
161
|
+
...commitsByDay.values()
|
|
162
|
+
];
|
|
163
|
+
const mean = dailyCounts.reduce((a, b) => a + b, 0) / Math.max(dailyCounts.length, 1);
|
|
164
|
+
signals.commitFrequencyVariance = Math.sqrt(dailyCounts.reduce((sum, c) => sum + (c - mean) ** 2, 0) / Math.max(dailyCounts.length, 1));
|
|
165
|
+
const largeCommits = commits.filter((c) => c.files.length >= 10);
|
|
166
|
+
signals.largeCommitRatio = largeCommits.length / commits.length * 100;
|
|
167
|
+
signals.diffusion = computeDiffusion(commits);
|
|
168
|
+
signals.rollbackCorrelation = computeRollbackCorrelation(commits);
|
|
169
|
+
signals.fileChurnRanking = computeFileChurn(commits);
|
|
170
|
+
signals.coChangeGraph = computeCoChangeGraph(commits);
|
|
171
|
+
signals.hotspotFiles = signals.fileChurnRanking.filter((f) => f.revertCount > 0 || f.changeCount > 5).sort((a, b) => {
|
|
172
|
+
const hasRevertA = a.revertCount > 0 ? 1 : 0;
|
|
173
|
+
const hasRevertB = b.revertCount > 0 ? 1 : 0;
|
|
174
|
+
if (hasRevertB !== hasRevertA) return hasRevertB - hasRevertA;
|
|
175
|
+
if (a.revertCount > 0 && b.revertCount > 0) {
|
|
176
|
+
return b.revertCount / b.changeCount - a.revertCount / a.changeCount;
|
|
177
|
+
}
|
|
178
|
+
return b.changeCount - a.changeCount;
|
|
179
|
+
}).slice(0, 10).map((f) => f.path);
|
|
180
|
+
const { stdout: shortlogOut } = await execFileAsync("git", [
|
|
181
|
+
"shortlog",
|
|
182
|
+
"-sn",
|
|
183
|
+
"--all"
|
|
184
|
+
], {
|
|
185
|
+
cwd: repoPath,
|
|
186
|
+
maxBuffer: 1024 * 1024,
|
|
187
|
+
timeout: 1e4
|
|
188
|
+
});
|
|
189
|
+
const contributors = shortlogOut.split("\n").filter(Boolean);
|
|
190
|
+
signals.contributorCount = contributors.length;
|
|
191
|
+
signals.busFactorEstimate = Math.max(1, Math.min(signals.contributorCount, Math.ceil(signals.contributorCount * 0.3)));
|
|
192
|
+
const mergeCommits = commits.filter((c) => c.parentCount > 1);
|
|
193
|
+
signals.mergeConflictFrequency = commits.length > 0 ? mergeCommits.length / commits.length * 100 : 0;
|
|
194
|
+
if (emitter && signals.coChangeGraph.length > 0) {
|
|
195
|
+
const topPair = signals.coChangeGraph[0];
|
|
196
|
+
if (topPair.confidence > 0.9) {
|
|
197
|
+
emitter.emit("discovery", {
|
|
198
|
+
source: "gitlog",
|
|
199
|
+
confidence: topPair.confidence,
|
|
200
|
+
message: `${topPair.fileA} and ${topPair.fileB} change together ${Math.round(topPair.confidence * 100)}% of the time`,
|
|
201
|
+
detailMessage: "Vreko will track this relationship",
|
|
202
|
+
relatedInsightId: "co-change-instability"
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (emitter && signals.avgCommitsPerDay > 10) {
|
|
207
|
+
emitter.emit("discovery", {
|
|
208
|
+
source: "gitlog",
|
|
209
|
+
confidence: 0.88,
|
|
210
|
+
message: "High commit frequency detected",
|
|
211
|
+
detailMessage: "Vreko can group these logically"
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
} catch (_error) {
|
|
215
|
+
}
|
|
216
|
+
return signals;
|
|
217
|
+
}
|
|
218
|
+
__name(analyzeGitLog, "analyzeGitLog");
|
|
219
|
+
__name2(analyzeGitLog, "analyzeGitLog");
|
|
220
|
+
function parseLogOutput(stdout) {
|
|
221
|
+
const commits = [];
|
|
222
|
+
const lines = stdout.split("\n");
|
|
223
|
+
let current = null;
|
|
224
|
+
for (const line of lines) {
|
|
225
|
+
if (line.startsWith("COMMIT:")) {
|
|
226
|
+
if (current) {
|
|
227
|
+
commits.push(current);
|
|
228
|
+
}
|
|
229
|
+
const rest = line.slice(7);
|
|
230
|
+
const pipeIdx = rest.indexOf("|");
|
|
231
|
+
if (pipeIdx === -1) {
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
const metaPart = rest.slice(0, pipeIdx).trim();
|
|
235
|
+
const subject = rest.slice(pipeIdx + 1);
|
|
236
|
+
const parts = metaPart.split(/\s+/);
|
|
237
|
+
if (parts.length < 2) {
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
const sha = parts[0];
|
|
241
|
+
const dateStr = parts[1];
|
|
242
|
+
const parentHashes = parts.slice(2);
|
|
243
|
+
const date = new Date(dateStr);
|
|
244
|
+
if (Number.isNaN(date.getTime())) {
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
current = {
|
|
248
|
+
sha,
|
|
249
|
+
date,
|
|
250
|
+
parentCount: parentHashes.length,
|
|
251
|
+
files: [],
|
|
252
|
+
isRevert: subject.toLowerCase().startsWith("revert"),
|
|
253
|
+
subject
|
|
254
|
+
};
|
|
255
|
+
} else if (current && line.trim()) {
|
|
256
|
+
const parts = line.split(" ");
|
|
257
|
+
if (parts.length >= 3) {
|
|
258
|
+
const added = parts[0] === "-" ? 0 : Number.parseInt(parts[0], 10) || 0;
|
|
259
|
+
const deleted = parts[1] === "-" ? 0 : Number.parseInt(parts[1], 10) || 0;
|
|
260
|
+
current.files.push({
|
|
261
|
+
path: parts[2],
|
|
262
|
+
added,
|
|
263
|
+
deleted
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (current) {
|
|
269
|
+
commits.push(current);
|
|
270
|
+
}
|
|
271
|
+
return commits;
|
|
272
|
+
}
|
|
273
|
+
__name(parseLogOutput, "parseLogOutput");
|
|
274
|
+
__name2(parseLogOutput, "parseLogOutput");
|
|
275
|
+
function computeDiffusion(commits) {
|
|
276
|
+
if (commits.length === 0) {
|
|
277
|
+
return {
|
|
278
|
+
avgFilesTouchedPerCommit: 0,
|
|
279
|
+
crossDirectoryDiffusionRate: 0,
|
|
280
|
+
rollbackAdjacentCommitRate: 0,
|
|
281
|
+
largeSessionClusterRate: 0,
|
|
282
|
+
maxSingleCommitSpread: 0
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
const fileCounts = commits.map((c) => c.files.length);
|
|
286
|
+
const avgFilesTouchedPerCommit = fileCounts.reduce((a, b) => a + b, 0) / commits.length;
|
|
287
|
+
const crossDirCommits = commits.filter((c) => {
|
|
288
|
+
const dirs = new Set(c.files.map((f) => topLevelDir(f.path)));
|
|
289
|
+
return dirs.size >= 2;
|
|
290
|
+
});
|
|
291
|
+
const crossDirectoryDiffusionRate = crossDirCommits.length / commits.length * 100;
|
|
292
|
+
let rollbackAdjacentCount = 0;
|
|
293
|
+
const sortedByDate = [
|
|
294
|
+
...commits
|
|
295
|
+
].sort((a, b) => a.date.getTime() - b.date.getTime());
|
|
296
|
+
for (let i = 0; i < sortedByDate.length; i++) {
|
|
297
|
+
for (let j = i + 1; j < sortedByDate.length; j++) {
|
|
298
|
+
if (!withinHours(sortedByDate[i].date, sortedByDate[j].date, 6)) {
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
if (sortedByDate[j].isRevert) {
|
|
302
|
+
rollbackAdjacentCount++;
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
const rollbackAdjacentCommitRate = rollbackAdjacentCount / commits.length * 100;
|
|
308
|
+
let clusterCount = 0;
|
|
309
|
+
for (let i = 0; i < sortedByDate.length; i++) {
|
|
310
|
+
let windowEnd = i;
|
|
311
|
+
while (windowEnd + 1 < sortedByDate.length && withinHours(sortedByDate[i].date, sortedByDate[windowEnd + 1].date, 2)) {
|
|
312
|
+
windowEnd++;
|
|
313
|
+
}
|
|
314
|
+
if (windowEnd - i + 1 >= 5) {
|
|
315
|
+
clusterCount++;
|
|
316
|
+
i = windowEnd;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
const largeSessionClusterRate = clusterCount / Math.max(commits.length / 5, 1) * 100;
|
|
320
|
+
const maxSingleCommitSpread = Math.max(...fileCounts, 0);
|
|
321
|
+
return {
|
|
322
|
+
avgFilesTouchedPerCommit,
|
|
323
|
+
crossDirectoryDiffusionRate,
|
|
324
|
+
rollbackAdjacentCommitRate,
|
|
325
|
+
largeSessionClusterRate,
|
|
326
|
+
maxSingleCommitSpread
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
__name(computeDiffusion, "computeDiffusion");
|
|
330
|
+
__name2(computeDiffusion, "computeDiffusion");
|
|
331
|
+
function computeRollbackCorrelation(commits) {
|
|
332
|
+
const result = {
|
|
333
|
+
resetAfterLargeCommitRate: 0,
|
|
334
|
+
resetAfterBranchSwitchRate: 0,
|
|
335
|
+
resetAfterLateNightRate: 0,
|
|
336
|
+
resetAfterCrossPackageRate: 0,
|
|
337
|
+
branchSpecificRollbackDensity: /* @__PURE__ */ new Map(),
|
|
338
|
+
medianTimeToRecovery: 0,
|
|
339
|
+
topRecoveryTrigger: "large-commit"
|
|
340
|
+
};
|
|
341
|
+
const sorted = [
|
|
342
|
+
...commits
|
|
343
|
+
].sort((a, b) => a.date.getTime() - b.date.getTime());
|
|
344
|
+
const reverts = sorted.filter((c) => c.isRevert);
|
|
345
|
+
if (reverts.length === 0) {
|
|
346
|
+
return result;
|
|
347
|
+
}
|
|
348
|
+
let afterLargeCount = 0;
|
|
349
|
+
let afterLateNightCount = 0;
|
|
350
|
+
let afterCrossPackageCount = 0;
|
|
351
|
+
const recoveryTimes = [];
|
|
352
|
+
for (const revert of reverts) {
|
|
353
|
+
const precedingIdx = sorted.findIndex((c) => c.sha === revert.sha) - 1;
|
|
354
|
+
if (precedingIdx < 0) {
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
for (let i = precedingIdx; i >= 0; i--) {
|
|
358
|
+
const preceding = sorted[i];
|
|
359
|
+
if (!withinHours(preceding.date, revert.date, 6)) {
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
const timeDiff = (revert.date.getTime() - preceding.date.getTime()) / (1e3 * 60);
|
|
363
|
+
recoveryTimes.push(timeDiff);
|
|
364
|
+
if (preceding.files.length >= 5) {
|
|
365
|
+
afterLargeCount++;
|
|
366
|
+
}
|
|
367
|
+
if (preceding.date.getHours() >= 22 || preceding.date.getHours() < 5) {
|
|
368
|
+
afterLateNightCount++;
|
|
369
|
+
}
|
|
370
|
+
const dirs = new Set(preceding.files.map((f) => topLevelDir(f.path)));
|
|
371
|
+
if (dirs.size >= 2) {
|
|
372
|
+
afterCrossPackageCount++;
|
|
373
|
+
}
|
|
374
|
+
break;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
const total = Math.max(reverts.length, 1);
|
|
378
|
+
result.resetAfterLargeCommitRate = afterLargeCount / total * 100;
|
|
379
|
+
result.resetAfterLateNightRate = afterLateNightCount / total * 100;
|
|
380
|
+
result.resetAfterCrossPackageRate = afterCrossPackageCount / total * 100;
|
|
381
|
+
if (recoveryTimes.length > 0) {
|
|
382
|
+
recoveryTimes.sort((a, b) => a - b);
|
|
383
|
+
const mid = Math.floor(recoveryTimes.length / 2);
|
|
384
|
+
result.medianTimeToRecovery = recoveryTimes.length % 2 === 0 ? (recoveryTimes[mid - 1] + recoveryTimes[mid]) / 2 : recoveryTimes[mid];
|
|
385
|
+
}
|
|
386
|
+
const triggers = {
|
|
387
|
+
"large-commit": afterLargeCount,
|
|
388
|
+
"late-night": afterLateNightCount,
|
|
389
|
+
"cross-package": afterCrossPackageCount,
|
|
390
|
+
"branch-switch": 0
|
|
391
|
+
};
|
|
392
|
+
result.topRecoveryTrigger = Object.entries(triggers).sort((a, b) => b[1] - a[1])[0][0];
|
|
393
|
+
return result;
|
|
394
|
+
}
|
|
395
|
+
__name(computeRollbackCorrelation, "computeRollbackCorrelation");
|
|
396
|
+
__name2(computeRollbackCorrelation, "computeRollbackCorrelation");
|
|
397
|
+
function computeFileChurn(commits) {
|
|
398
|
+
const churnMap = /* @__PURE__ */ new Map();
|
|
399
|
+
for (const commit of commits) {
|
|
400
|
+
for (const file of commit.files) {
|
|
401
|
+
if (isArtifactFile(file.path)) continue;
|
|
402
|
+
const entry = churnMap.get(file.path) || {
|
|
403
|
+
changeCount: 0,
|
|
404
|
+
totalLines: 0,
|
|
405
|
+
revertCount: 0,
|
|
406
|
+
lastChanged: commit.date
|
|
407
|
+
};
|
|
408
|
+
entry.changeCount++;
|
|
409
|
+
entry.totalLines += file.added + file.deleted;
|
|
410
|
+
if (commit.isRevert) {
|
|
411
|
+
entry.revertCount++;
|
|
412
|
+
}
|
|
413
|
+
if (commit.date > entry.lastChanged) {
|
|
414
|
+
entry.lastChanged = commit.date;
|
|
415
|
+
}
|
|
416
|
+
churnMap.set(file.path, entry);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return [
|
|
420
|
+
...churnMap.entries()
|
|
421
|
+
].map(([path, data]) => ({
|
|
422
|
+
path,
|
|
423
|
+
changeCount: data.changeCount,
|
|
424
|
+
avgLinesChanged: data.changeCount > 0 ? data.totalLines / data.changeCount : 0,
|
|
425
|
+
revertCount: data.revertCount,
|
|
426
|
+
lastChanged: data.lastChanged.toISOString()
|
|
427
|
+
})).sort((a, b) => b.changeCount - a.changeCount).slice(0, 50);
|
|
428
|
+
}
|
|
429
|
+
__name(computeFileChurn, "computeFileChurn");
|
|
430
|
+
__name2(computeFileChurn, "computeFileChurn");
|
|
431
|
+
function computeCoChangeGraph(commits) {
|
|
432
|
+
const pairCounts = /* @__PURE__ */ new Map();
|
|
433
|
+
const fileCounts = /* @__PURE__ */ new Map();
|
|
434
|
+
for (const commit of commits) {
|
|
435
|
+
const paths = commit.files.map((f) => f.path);
|
|
436
|
+
for (const p of paths) {
|
|
437
|
+
fileCounts.set(p, (fileCounts.get(p) || 0) + 1);
|
|
438
|
+
}
|
|
439
|
+
if (paths.length < 2 || paths.length > 20) {
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
for (let i = 0; i < paths.length; i++) {
|
|
443
|
+
for (let j = i + 1; j < paths.length; j++) {
|
|
444
|
+
const key = [
|
|
445
|
+
paths[i],
|
|
446
|
+
paths[j]
|
|
447
|
+
].sort().join("|||");
|
|
448
|
+
pairCounts.set(key, (pairCounts.get(key) || 0) + 1);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
const edges = [];
|
|
453
|
+
for (const [key, count] of pairCounts) {
|
|
454
|
+
if (count < 3) {
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
const [fileA, fileB] = key.split("|||");
|
|
458
|
+
if (isArtifactFile(fileA) && isArtifactFile(fileB)) {
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
const maxChanges = Math.max(fileCounts.get(fileA) || 1, fileCounts.get(fileB) || 1);
|
|
462
|
+
edges.push({
|
|
463
|
+
fileA,
|
|
464
|
+
fileB,
|
|
465
|
+
coChangeCount: count,
|
|
466
|
+
confidence: count / maxChanges
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
return edges.sort((a, b) => b.confidence - a.confidence).slice(0, 20);
|
|
470
|
+
}
|
|
471
|
+
__name(computeCoChangeGraph, "computeCoChangeGraph");
|
|
472
|
+
__name2(computeCoChangeGraph, "computeCoChangeGraph");
|
|
473
|
+
function generateInsights(signals, baseline) {
|
|
474
|
+
const insights = [];
|
|
475
|
+
if (signals.reflog && signals.reflog.resetToHEADCount > 5) {
|
|
476
|
+
const rc = signals.gitlog?.rollbackCorrelation;
|
|
477
|
+
const correlationDetail = rc?.topRecoveryTrigger ? ` Most correlate with ${rc.topRecoveryTrigger.replace(/-/g, " ")} changes.` : "";
|
|
478
|
+
const comparison = baseline ? `${(signals.reflog.resetToHEADCount / Math.max(baseline.baselines.avgResetToHEADRate, 1)).toFixed(1)}x the average for repos like yours` : void 0;
|
|
479
|
+
insights.push({
|
|
480
|
+
id: "high-reset-rate",
|
|
481
|
+
severity: signals.reflog.resetToHEADCount > 15 ? "critical" : "warning",
|
|
482
|
+
observation: `${signals.reflog.resetToHEADCount} recovery events in recent history`,
|
|
483
|
+
whyItMatters: `Frequent resets indicate volatile development sessions.${correlationDetail}`,
|
|
484
|
+
whatWeWillDo: "Increase snapshot density during high-diffusion sessions",
|
|
485
|
+
comparison
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
if (signals.gitlog) {
|
|
489
|
+
const fragileFiles = signals.gitlog.fileChurnRanking.filter((f) => f.changeCount >= 20 && f.revertCount >= 3);
|
|
490
|
+
for (const file of fragileFiles.slice(0, 2)) {
|
|
491
|
+
const isWeaklyTested = signals.structure?.testAdjacency.weaklyTestedHotspots.some((h) => file.path.startsWith(h));
|
|
492
|
+
insights.push({
|
|
493
|
+
id: `fragile-hotspot-${file.path}`,
|
|
494
|
+
severity: "warning",
|
|
495
|
+
observation: `${file.path} changed ${file.changeCount}x, reverted ${file.revertCount}x${isWeaklyTested ? ". Weakly test-adjacent" : ""}`,
|
|
496
|
+
whyItMatters: "High-churn files with reverts are the most likely to need recovery",
|
|
497
|
+
whatWeWillDo: "Watch this file as a fragile hotspot with enhanced monitoring"
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
if (signals.gitlog && signals.gitlog.diffusion.crossDirectoryDiffusionRate > 30) {
|
|
502
|
+
const rc = signals.gitlog.rollbackCorrelation;
|
|
503
|
+
const comparison = baseline ? `${(signals.gitlog.diffusion.crossDirectoryDiffusionRate / Math.max(baseline.baselines.avgCrossDirectoryDiffusionRate, 1)).toFixed(1)}x the average` : void 0;
|
|
504
|
+
insights.push({
|
|
505
|
+
id: "cross-directory-blast",
|
|
506
|
+
severity: "warning",
|
|
507
|
+
observation: `${Math.round(signals.gitlog.diffusion.crossDirectoryDiffusionRate)}% of commits span multiple directories`,
|
|
508
|
+
whyItMatters: `Wide-reaching changes are harder to roll back safely. ${rc.resetAfterCrossPackageRate > 10 ? `${Math.round(rc.resetAfterCrossPackageRate)}% of reverts follow cross-package changes.` : ""}`.trim(),
|
|
509
|
+
whatWeWillDo: "Track cross-directory blast radius and auto-snapshot before high-diffusion commits",
|
|
510
|
+
comparison
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
if (signals.gitlog) {
|
|
514
|
+
const strongPairs = signals.gitlog.coChangeGraph.filter((e) => e.confidence > 0.7);
|
|
515
|
+
if (strongPairs.length > 0) {
|
|
516
|
+
const top = strongPairs[0];
|
|
517
|
+
insights.push({
|
|
518
|
+
id: "co-change-instability",
|
|
519
|
+
severity: "info",
|
|
520
|
+
observation: `${top.fileA} and ${top.fileB} co-change at ${Math.round(top.confidence * 100)}% rate`,
|
|
521
|
+
whyItMatters: "Tightly coupled files that change together may break if one is modified alone",
|
|
522
|
+
whatWeWillDo: "Track co-change relationships and warn if one file changes without the other"
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
if (signals.reflog && signals.reflog.lateNightRatio > 0.2) {
|
|
527
|
+
insights.push({
|
|
528
|
+
id: "temporal-risk",
|
|
529
|
+
severity: "notable",
|
|
530
|
+
observation: `${Math.round(signals.reflog.lateNightRatio * 100)}% of activity occurs during late-night hours`,
|
|
531
|
+
whyItMatters: "Late-night development sessions correlate with higher error rates",
|
|
532
|
+
whatWeWillDo: "Increase snapshot frequency during late-night sessions"
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
if (signals.structure) {
|
|
536
|
+
const weakPackages = signals.structure.testAdjacency.packageTestAdjacency.filter((p) => p.churnScore > 2 && p.testRatio < 0.1);
|
|
537
|
+
if (weakPackages.length > 0) {
|
|
538
|
+
insights.push({
|
|
539
|
+
id: "weak-test-adjacency",
|
|
540
|
+
severity: "notable",
|
|
541
|
+
observation: `${weakPackages.length} high-churn package(s) with minimal test coverage`,
|
|
542
|
+
whyItMatters: "Code that changes frequently without tests is most likely to break unexpectedly",
|
|
543
|
+
whatWeWillDo: "Prioritize snapshot protection for untested hotspots"
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
if (signals.reflog && signals.reflog.branchAbandonmentRate > 30) {
|
|
548
|
+
insights.push({
|
|
549
|
+
id: "branch-abandonment",
|
|
550
|
+
severity: "info",
|
|
551
|
+
observation: `${Math.round(signals.reflog.branchAbandonmentRate)}% of branches are never merged`,
|
|
552
|
+
whyItMatters: "Abandoned branches may indicate experimental work that could benefit from snapshot protection",
|
|
553
|
+
whatWeWillDo: "Track branch lifecycle patterns to identify recovery-prone experiments"
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
if (signals.gitlog && signals.gitlog.busFactorEstimate < 2) {
|
|
557
|
+
const singleAuthorFiles = signals.gitlog.fileChurnRanking.filter((f) => f.changeCount >= 10);
|
|
558
|
+
if (singleAuthorFiles.length > 0) {
|
|
559
|
+
insights.push({
|
|
560
|
+
id: "bus-factor-risk",
|
|
561
|
+
severity: "notable",
|
|
562
|
+
observation: `Low bus factor: ${singleAuthorFiles.length} high-churn files maintained by a single contributor`,
|
|
563
|
+
whyItMatters: "Single-contributor files are at higher risk if that contributor is unavailable",
|
|
564
|
+
whatWeWillDo: "Enhanced snapshot protection for single-contributor hotspots"
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
if (baseline && signals.reflog) {
|
|
569
|
+
if (signals.reflog.forceResetRate > baseline.baselines.avgForceResetRate * 1.5) {
|
|
570
|
+
insights.push({
|
|
571
|
+
id: "above-baseline-resets",
|
|
572
|
+
severity: "info",
|
|
573
|
+
observation: "Force reset rate above typical for your repo type",
|
|
574
|
+
whyItMatters: "Higher-than-average reset frequency suggests more volatile workflows",
|
|
575
|
+
whatWeWillDo: "Calibrate protection levels against aggregate baselines",
|
|
576
|
+
comparison: `${(signals.reflog.forceResetRate / Math.max(baseline.baselines.avgForceResetRate, 0.1)).toFixed(1)}x the average`
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
if (signals.gitlog && signals.topology) {
|
|
581
|
+
const highFanInPaths = new Map(signals.topology.highFanInFiles.map((f) => [
|
|
582
|
+
f.path,
|
|
583
|
+
f.importedByCount
|
|
584
|
+
]));
|
|
585
|
+
for (const hotspotPath of signals.gitlog.hotspotFiles) {
|
|
586
|
+
const importedByCount = highFanInPaths.get(hotspotPath);
|
|
587
|
+
if (importedByCount !== void 0) {
|
|
588
|
+
const churnEntry = signals.gitlog.fileChurnRanking.find((f) => f.path === hotspotPath);
|
|
589
|
+
const changeCount = churnEntry?.changeCount ?? 0;
|
|
590
|
+
const revertCount = churnEntry?.revertCount ?? 0;
|
|
591
|
+
insights.push({
|
|
592
|
+
id: "fused-structural-temporal-hotspot",
|
|
593
|
+
severity: "critical",
|
|
594
|
+
observation: `\`${hotspotPath}\` changed ${changeCount} times, reverted ${revertCount} times, and is imported by ${importedByCount} files`,
|
|
595
|
+
whyItMatters: "This file is both structurally critical (high blast radius) and behaviorally unstable (frequent changes and rollbacks). Changes here cascade across your codebase.",
|
|
596
|
+
whatWeWillDo: "Vreko will apply maximum protection density and provide structural context to AI tools before they modify this file."
|
|
597
|
+
});
|
|
598
|
+
break;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
const locked = selectLockedInsight(signals);
|
|
603
|
+
return {
|
|
604
|
+
insights: insights.slice(0, 7),
|
|
605
|
+
locked
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
__name(generateInsights, "generateInsights");
|
|
609
|
+
__name2(generateInsights, "generateInsights");
|
|
610
|
+
function selectLockedInsight(signals) {
|
|
611
|
+
if (signals.reflog && signals.reflog.lateNightRatio > 0.2) {
|
|
612
|
+
return {
|
|
613
|
+
id: "temporal-risk-windows",
|
|
614
|
+
teaser: "Identify your personal risk windows -- the times and patterns where recovery is most likely",
|
|
615
|
+
requirement: "Requires observed coding sessions to validate temporal patterns",
|
|
616
|
+
unlockCondition: {
|
|
617
|
+
type: "days_observed",
|
|
618
|
+
days: 3
|
|
619
|
+
}
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
if (signals.gitlog && signals.gitlog.busFactorEstimate < 2) {
|
|
623
|
+
return {
|
|
624
|
+
id: "collaboration-risk",
|
|
625
|
+
teaser: "Detect when your coding patterns diverge from your stable baseline",
|
|
626
|
+
requirement: "Requires observed coding sessions to establish baseline patterns",
|
|
627
|
+
unlockCondition: {
|
|
628
|
+
type: "days_observed",
|
|
629
|
+
days: 5
|
|
630
|
+
}
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
return {
|
|
634
|
+
id: "session-risk-windows",
|
|
635
|
+
teaser: "Identify your personal risk windows",
|
|
636
|
+
requirement: "Requires observed coding sessions",
|
|
637
|
+
unlockCondition: {
|
|
638
|
+
type: "days_observed",
|
|
639
|
+
days: 3
|
|
640
|
+
}
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
__name(selectLockedInsight, "selectLockedInsight");
|
|
644
|
+
__name2(selectLockedInsight, "selectLockedInsight");
|
|
645
|
+
var execFileAsync2 = promisify(execFile);
|
|
646
|
+
function parseDateFromLine(line) {
|
|
647
|
+
const match = line.match(/\{([^}]+)\}/);
|
|
648
|
+
if (!match) {
|
|
649
|
+
return null;
|
|
650
|
+
}
|
|
651
|
+
const d = new Date(match[1]);
|
|
652
|
+
return Number.isNaN(d.getTime()) ? null : d;
|
|
653
|
+
}
|
|
654
|
+
__name(parseDateFromLine, "parseDateFromLine");
|
|
655
|
+
__name2(parseDateFromLine, "parseDateFromLine");
|
|
656
|
+
function parseBranchFromLine(line) {
|
|
657
|
+
const match = line.match(/refs\/heads\/([^@]+)@/);
|
|
658
|
+
return match ? match[1] : null;
|
|
659
|
+
}
|
|
660
|
+
__name(parseBranchFromLine, "parseBranchFromLine");
|
|
661
|
+
__name2(parseBranchFromLine, "parseBranchFromLine");
|
|
662
|
+
async function scanReflog(repoPath, emitter) {
|
|
663
|
+
const signals = {
|
|
664
|
+
forceResetRate: 0,
|
|
665
|
+
rebaseFrequency: 0,
|
|
666
|
+
branchAbandonmentRate: 0,
|
|
667
|
+
contextSwitchRate: 0,
|
|
668
|
+
peakActivityWindows: [],
|
|
669
|
+
weekendActivityRatio: 0,
|
|
670
|
+
lateNightRatio: 0,
|
|
671
|
+
resetToHEADCount: 0,
|
|
672
|
+
reflogChurnByBranch: /* @__PURE__ */ new Map(),
|
|
673
|
+
avgTimeBetweenResets: 0
|
|
674
|
+
};
|
|
675
|
+
try {
|
|
676
|
+
const { stdout } = await execFileAsync2("git", [
|
|
677
|
+
"reflog",
|
|
678
|
+
"--all",
|
|
679
|
+
"--date=iso"
|
|
680
|
+
], {
|
|
681
|
+
cwd: repoPath,
|
|
682
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
683
|
+
timeout: 1e4
|
|
684
|
+
});
|
|
685
|
+
const lines = stdout.split("\n").filter(Boolean);
|
|
686
|
+
if (lines.length === 0) {
|
|
687
|
+
return signals;
|
|
688
|
+
}
|
|
689
|
+
const timestamps = [];
|
|
690
|
+
const resetTimestamps = [];
|
|
691
|
+
let resetCount = 0;
|
|
692
|
+
let forceResetCount = 0;
|
|
693
|
+
let rebaseCount = 0;
|
|
694
|
+
let checkoutCount = 0;
|
|
695
|
+
let weekendCount = 0;
|
|
696
|
+
let lateNightCount = 0;
|
|
697
|
+
const hourCounts = new Array(24).fill(0);
|
|
698
|
+
const branchesCreated = /* @__PURE__ */ new Set();
|
|
699
|
+
const branchesMerged = /* @__PURE__ */ new Set();
|
|
700
|
+
const branchChurn = /* @__PURE__ */ new Map();
|
|
701
|
+
for (const line of lines) {
|
|
702
|
+
const date = parseDateFromLine(line);
|
|
703
|
+
if (date) {
|
|
704
|
+
timestamps.push(date);
|
|
705
|
+
const hour = date.getHours();
|
|
706
|
+
hourCounts[hour]++;
|
|
707
|
+
const day = date.getDay();
|
|
708
|
+
if (day === 0 || day === 6) {
|
|
709
|
+
weekendCount++;
|
|
710
|
+
}
|
|
711
|
+
if (hour >= 22 || hour < 5) {
|
|
712
|
+
lateNightCount++;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
const branch = parseBranchFromLine(line);
|
|
716
|
+
if (branch) {
|
|
717
|
+
branchChurn.set(branch, (branchChurn.get(branch) || 0) + 1);
|
|
718
|
+
}
|
|
719
|
+
if (line.includes("reset: moving to HEAD")) {
|
|
720
|
+
resetCount++;
|
|
721
|
+
if (date) {
|
|
722
|
+
resetTimestamps.push(date.getTime());
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
if (line.includes("reset: moving to") && !line.includes("reset: moving to HEAD")) {
|
|
726
|
+
forceResetCount++;
|
|
727
|
+
if (date) {
|
|
728
|
+
resetTimestamps.push(date.getTime());
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
if (line.includes("rebase")) {
|
|
732
|
+
rebaseCount++;
|
|
733
|
+
}
|
|
734
|
+
if (line.includes("checkout: moving from")) {
|
|
735
|
+
checkoutCount++;
|
|
736
|
+
}
|
|
737
|
+
if (line.includes("branch: Created from")) {
|
|
738
|
+
if (branch) {
|
|
739
|
+
branchesCreated.add(branch);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
if (line.includes("merge")) {
|
|
743
|
+
const mergedMatch = line.match(/merge\s+(\S+)/);
|
|
744
|
+
if (mergedMatch) {
|
|
745
|
+
branchesMerged.add(mergedMatch[1].replace(/[:;]$/, ""));
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
const sortedTimestamps = timestamps.map((d) => d.getTime()).sort((a, b) => a - b);
|
|
750
|
+
const timeSpanMs = sortedTimestamps.length > 1 ? sortedTimestamps[sortedTimestamps.length - 1] - sortedTimestamps[0] : 7 * 24 * 60 * 60 * 1e3;
|
|
751
|
+
const weeks = Math.max(timeSpanMs / (7 * 24 * 60 * 60 * 1e3), 1);
|
|
752
|
+
const days = Math.max(timeSpanMs / (24 * 60 * 60 * 1e3), 1);
|
|
753
|
+
signals.resetToHEADCount = resetCount;
|
|
754
|
+
signals.forceResetRate = (forceResetCount + resetCount) / weeks;
|
|
755
|
+
signals.rebaseFrequency = rebaseCount / weeks;
|
|
756
|
+
signals.contextSwitchRate = checkoutCount / days;
|
|
757
|
+
const abandoned = [
|
|
758
|
+
...branchesCreated
|
|
759
|
+
].filter((b) => !branchesMerged.has(b));
|
|
760
|
+
signals.branchAbandonmentRate = branchesCreated.size > 0 ? abandoned.length / branchesCreated.size * 100 : 0;
|
|
761
|
+
signals.weekendActivityRatio = lines.length > 0 ? weekendCount / lines.length : 0;
|
|
762
|
+
signals.lateNightRatio = lines.length > 0 ? lateNightCount / lines.length : 0;
|
|
763
|
+
const meanActivity = lines.length / 24;
|
|
764
|
+
const stddev = Math.sqrt(hourCounts.reduce((sum, c) => sum + (c - meanActivity) ** 2, 0) / 24);
|
|
765
|
+
const threshold = meanActivity + stddev;
|
|
766
|
+
const peakWindows = [];
|
|
767
|
+
let windowStart = null;
|
|
768
|
+
for (let h = 0; h < 24; h++) {
|
|
769
|
+
if (hourCounts[h] > threshold) {
|
|
770
|
+
if (windowStart === null) {
|
|
771
|
+
windowStart = h;
|
|
772
|
+
}
|
|
773
|
+
} else {
|
|
774
|
+
if (windowStart !== null) {
|
|
775
|
+
peakWindows.push({
|
|
776
|
+
startHour: windowStart,
|
|
777
|
+
endHour: h
|
|
778
|
+
});
|
|
779
|
+
windowStart = null;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
if (windowStart !== null) {
|
|
784
|
+
peakWindows.push({
|
|
785
|
+
startHour: windowStart,
|
|
786
|
+
endHour: 24
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
signals.peakActivityWindows = peakWindows;
|
|
790
|
+
signals.reflogChurnByBranch = branchChurn;
|
|
791
|
+
if (resetTimestamps.length > 1) {
|
|
792
|
+
const sorted = resetTimestamps.sort((a, b) => a - b);
|
|
793
|
+
let totalGap = 0;
|
|
794
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
795
|
+
totalGap += sorted[i] - sorted[i - 1];
|
|
796
|
+
}
|
|
797
|
+
signals.avgTimeBetweenResets = totalGap / (sorted.length - 1) / (1e3 * 60);
|
|
798
|
+
}
|
|
799
|
+
if (emitter && resetCount > 5) {
|
|
800
|
+
emitter.emit("discovery", {
|
|
801
|
+
source: "reflog",
|
|
802
|
+
confidence: 0.9,
|
|
803
|
+
message: `Found ${resetCount} reset events in your history`,
|
|
804
|
+
detailMessage: "Vreko will learn your rollback patterns"
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
if (emitter && signals.lateNightRatio > 0.2) {
|
|
808
|
+
emitter.emit("discovery", {
|
|
809
|
+
source: "reflog",
|
|
810
|
+
confidence: 0.88,
|
|
811
|
+
message: `${Math.round(signals.lateNightRatio * 100)}% of git activity happens after 10pm`,
|
|
812
|
+
detailMessage: "Vreko will increase protection during late-night sessions"
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
} catch (_error) {
|
|
816
|
+
}
|
|
817
|
+
return signals;
|
|
818
|
+
}
|
|
819
|
+
__name(scanReflog, "scanReflog");
|
|
820
|
+
__name2(scanReflog, "scanReflog");
|
|
821
|
+
var execFileAsync3 = promisify(execFile);
|
|
822
|
+
function countFiles(dir, predicate, maxDepth = 4, currentDepth = 0) {
|
|
823
|
+
if (currentDepth >= maxDepth || !existsSync(dir)) {
|
|
824
|
+
return 0;
|
|
825
|
+
}
|
|
826
|
+
let count = 0;
|
|
827
|
+
try {
|
|
828
|
+
const entries = readdirSync(dir, {
|
|
829
|
+
withFileTypes: true
|
|
830
|
+
});
|
|
831
|
+
for (const entry of entries) {
|
|
832
|
+
if (entry.name === "node_modules" || entry.name === ".git" || entry.name === "dist" || entry.name === "build") {
|
|
833
|
+
continue;
|
|
834
|
+
}
|
|
835
|
+
if (entry.isFile() && predicate(entry.name)) {
|
|
836
|
+
count++;
|
|
837
|
+
} else if (entry.isDirectory()) {
|
|
838
|
+
count += countFiles(join(dir, entry.name), predicate, maxDepth, currentDepth + 1);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
} catch {
|
|
842
|
+
}
|
|
843
|
+
return count;
|
|
844
|
+
}
|
|
845
|
+
__name(countFiles, "countFiles");
|
|
846
|
+
__name2(countFiles, "countFiles");
|
|
847
|
+
function detectTestFramework(repoPath) {
|
|
848
|
+
if (existsSync(join(repoPath, "vitest.config.ts")) || existsSync(join(repoPath, "vitest.config.js"))) {
|
|
849
|
+
return "vitest";
|
|
850
|
+
}
|
|
851
|
+
if (existsSync(join(repoPath, "jest.config.ts")) || existsSync(join(repoPath, "jest.config.js"))) {
|
|
852
|
+
return "jest";
|
|
853
|
+
}
|
|
854
|
+
if (existsSync(join(repoPath, ".mocharc.yml")) || existsSync(join(repoPath, ".mocharc.json"))) {
|
|
855
|
+
return "mocha";
|
|
856
|
+
}
|
|
857
|
+
if (existsSync(join(repoPath, "pytest.ini")) || existsSync(join(repoPath, "pyproject.toml"))) {
|
|
858
|
+
return "pytest";
|
|
859
|
+
}
|
|
860
|
+
try {
|
|
861
|
+
const pkg = JSON.parse(readFileSync(join(repoPath, "package.json"), "utf-8"));
|
|
862
|
+
const allDeps = {
|
|
863
|
+
...pkg.dependencies,
|
|
864
|
+
...pkg.devDependencies
|
|
865
|
+
};
|
|
866
|
+
if (allDeps.vitest) {
|
|
867
|
+
return "vitest";
|
|
868
|
+
}
|
|
869
|
+
if (allDeps.jest) {
|
|
870
|
+
return "jest";
|
|
871
|
+
}
|
|
872
|
+
if (allDeps.mocha) {
|
|
873
|
+
return "mocha";
|
|
874
|
+
}
|
|
875
|
+
} catch {
|
|
876
|
+
}
|
|
877
|
+
return null;
|
|
878
|
+
}
|
|
879
|
+
__name(detectTestFramework, "detectTestFramework");
|
|
880
|
+
__name2(detectTestFramework, "detectTestFramework");
|
|
881
|
+
function detectFramework(repoPath) {
|
|
882
|
+
if (existsSync(join(repoPath, "next.config.ts")) || existsSync(join(repoPath, "next.config.js")) || existsSync(join(repoPath, "next.config.mjs"))) {
|
|
883
|
+
return "next.js";
|
|
884
|
+
}
|
|
885
|
+
if (existsSync(join(repoPath, "nuxt.config.ts"))) {
|
|
886
|
+
return "nuxt";
|
|
887
|
+
}
|
|
888
|
+
if (existsSync(join(repoPath, "angular.json"))) {
|
|
889
|
+
return "angular";
|
|
890
|
+
}
|
|
891
|
+
if (existsSync(join(repoPath, "svelte.config.js"))) {
|
|
892
|
+
return "svelte";
|
|
893
|
+
}
|
|
894
|
+
if (existsSync(join(repoPath, "vite.config.ts")) || existsSync(join(repoPath, "vite.config.js"))) {
|
|
895
|
+
return "vite";
|
|
896
|
+
}
|
|
897
|
+
try {
|
|
898
|
+
const pkg = JSON.parse(readFileSync(join(repoPath, "package.json"), "utf-8"));
|
|
899
|
+
const allDeps = {
|
|
900
|
+
...pkg.dependencies,
|
|
901
|
+
...pkg.devDependencies
|
|
902
|
+
};
|
|
903
|
+
if (allDeps.next) {
|
|
904
|
+
return "next.js";
|
|
905
|
+
}
|
|
906
|
+
if (allDeps.react) {
|
|
907
|
+
return "react";
|
|
908
|
+
}
|
|
909
|
+
if (allDeps.vue) {
|
|
910
|
+
return "vue";
|
|
911
|
+
}
|
|
912
|
+
if (allDeps.express) {
|
|
913
|
+
return "express";
|
|
914
|
+
}
|
|
915
|
+
if (allDeps.hono) {
|
|
916
|
+
return "hono";
|
|
917
|
+
}
|
|
918
|
+
} catch {
|
|
919
|
+
}
|
|
920
|
+
return null;
|
|
921
|
+
}
|
|
922
|
+
__name(detectFramework, "detectFramework");
|
|
923
|
+
__name2(detectFramework, "detectFramework");
|
|
924
|
+
function detectPackageDirs(repoPath) {
|
|
925
|
+
const dirs = [];
|
|
926
|
+
const candidates = [
|
|
927
|
+
"packages",
|
|
928
|
+
"apps",
|
|
929
|
+
"libs",
|
|
930
|
+
"modules",
|
|
931
|
+
"services"
|
|
932
|
+
];
|
|
933
|
+
for (const candidate of candidates) {
|
|
934
|
+
const candidatePath = join(repoPath, candidate);
|
|
935
|
+
if (!existsSync(candidatePath)) {
|
|
936
|
+
continue;
|
|
937
|
+
}
|
|
938
|
+
try {
|
|
939
|
+
const entries = readdirSync(candidatePath, {
|
|
940
|
+
withFileTypes: true
|
|
941
|
+
});
|
|
942
|
+
for (const entry of entries) {
|
|
943
|
+
if (entry.isDirectory() && existsSync(join(candidatePath, entry.name, "package.json"))) {
|
|
944
|
+
dirs.push(join(candidate, entry.name));
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
} catch {
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
if (dirs.length === 0) {
|
|
951
|
+
dirs.push(".");
|
|
952
|
+
}
|
|
953
|
+
return dirs;
|
|
954
|
+
}
|
|
955
|
+
__name(detectPackageDirs, "detectPackageDirs");
|
|
956
|
+
__name2(detectPackageDirs, "detectPackageDirs");
|
|
957
|
+
function computeTestAdjacency(repoPath, packageDirs, churnFiles) {
|
|
958
|
+
const isTestFile = /* @__PURE__ */ __name2((name) => name.endsWith(".test.ts") || name.endsWith(".test.tsx") || name.endsWith(".test.js") || name.endsWith(".spec.ts") || name.endsWith(".spec.tsx") || name.endsWith(".spec.js"), "isTestFile");
|
|
959
|
+
const isSourceFile = /* @__PURE__ */ __name2((name) => (name.endsWith(".ts") || name.endsWith(".tsx") || name.endsWith(".js") || name.endsWith(".jsx")) && !isTestFile(name) && !name.endsWith(".d.ts") && !name.endsWith(".config.ts") && !name.endsWith(".config.js"), "isSourceFile");
|
|
960
|
+
const packageTestAdjacency = [];
|
|
961
|
+
const churnSet = new Set(churnFiles || []);
|
|
962
|
+
for (const pkgDir of packageDirs) {
|
|
963
|
+
const fullPath = join(repoPath, pkgDir);
|
|
964
|
+
const srcPath = existsSync(join(fullPath, "src")) ? join(fullPath, "src") : fullPath;
|
|
965
|
+
const hasTestDir = existsSync(join(fullPath, "__tests__")) || existsSync(join(fullPath, "test")) || existsSync(join(fullPath, "tests"));
|
|
966
|
+
const testFileCount = countFiles(fullPath, isTestFile);
|
|
967
|
+
const sourceFileCount = countFiles(srcPath, isSourceFile);
|
|
968
|
+
const testRatio = sourceFileCount > 0 ? testFileCount / sourceFileCount : 0;
|
|
969
|
+
let churnScore = 0;
|
|
970
|
+
if (churnSet.size > 0) {
|
|
971
|
+
for (const f of churnSet) {
|
|
972
|
+
if (f.startsWith(pkgDir)) {
|
|
973
|
+
churnScore++;
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
packageTestAdjacency.push({
|
|
978
|
+
packagePath: pkgDir,
|
|
979
|
+
hasTestDir: hasTestDir || testFileCount > 0,
|
|
980
|
+
testFileCount,
|
|
981
|
+
sourceFileCount,
|
|
982
|
+
testRatio,
|
|
983
|
+
churnScore
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
const weaklyTestedHotspots = packageTestAdjacency.filter((p) => p.churnScore > 2 && p.testRatio < 0.3).map((p) => p.packagePath);
|
|
987
|
+
const totalSource = packageTestAdjacency.reduce((s, p) => s + p.sourceFileCount, 0);
|
|
988
|
+
const overallTestAdjacencyScore = totalSource > 0 ? Math.min(100, packageTestAdjacency.reduce((s, p) => s + p.testRatio * p.sourceFileCount, 0) / totalSource * 100) : 0;
|
|
989
|
+
return {
|
|
990
|
+
packageTestAdjacency,
|
|
991
|
+
weaklyTestedHotspots,
|
|
992
|
+
overallTestAdjacencyScore
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
__name(computeTestAdjacency, "computeTestAdjacency");
|
|
996
|
+
__name2(computeTestAdjacency, "computeTestAdjacency");
|
|
997
|
+
function detectSensitiveSurfaces(repoPath) {
|
|
998
|
+
const envPatterns = [
|
|
999
|
+
".env",
|
|
1000
|
+
".env.local",
|
|
1001
|
+
".env.development",
|
|
1002
|
+
".env.production",
|
|
1003
|
+
".env.staging"
|
|
1004
|
+
];
|
|
1005
|
+
const hasEnvFiles = envPatterns.some((p) => existsSync(join(repoPath, p)));
|
|
1006
|
+
const credPatterns = [
|
|
1007
|
+
".npmrc",
|
|
1008
|
+
".pypirc",
|
|
1009
|
+
"docker-compose.yml",
|
|
1010
|
+
"docker-compose.yaml"
|
|
1011
|
+
];
|
|
1012
|
+
const hasCredentialConfigs = credPatterns.some((p) => existsSync(join(repoPath, p)));
|
|
1013
|
+
let hasCISecrets = false;
|
|
1014
|
+
const workflowDir = join(repoPath, ".github", "workflows");
|
|
1015
|
+
if (existsSync(workflowDir)) {
|
|
1016
|
+
try {
|
|
1017
|
+
const files = readdirSync(workflowDir);
|
|
1018
|
+
for (const f of files) {
|
|
1019
|
+
try {
|
|
1020
|
+
const content = readFileSync(join(workflowDir, f), "utf-8");
|
|
1021
|
+
if (content.includes("secrets.")) {
|
|
1022
|
+
hasCISecrets = true;
|
|
1023
|
+
break;
|
|
1024
|
+
}
|
|
1025
|
+
} catch {
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
} catch {
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
const hasInfraState = existsSync(join(repoPath, "terraform.tfstate")) || existsSync(join(repoPath, ".terraform"));
|
|
1032
|
+
let sensitivePathCount = 0;
|
|
1033
|
+
if (hasEnvFiles) {
|
|
1034
|
+
sensitivePathCount += envPatterns.filter((p) => existsSync(join(repoPath, p))).length;
|
|
1035
|
+
}
|
|
1036
|
+
if (hasCredentialConfigs) {
|
|
1037
|
+
sensitivePathCount += credPatterns.filter((p) => existsSync(join(repoPath, p))).length;
|
|
1038
|
+
}
|
|
1039
|
+
if (hasCISecrets) {
|
|
1040
|
+
sensitivePathCount++;
|
|
1041
|
+
}
|
|
1042
|
+
if (hasInfraState) {
|
|
1043
|
+
sensitivePathCount++;
|
|
1044
|
+
}
|
|
1045
|
+
return {
|
|
1046
|
+
hasEnvFiles,
|
|
1047
|
+
hasCredentialConfigs,
|
|
1048
|
+
hasCISecrets,
|
|
1049
|
+
hasInfraState,
|
|
1050
|
+
sensitivePathCount
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
__name(detectSensitiveSurfaces, "detectSensitiveSurfaces");
|
|
1054
|
+
__name2(detectSensitiveSurfaces, "detectSensitiveSurfaces");
|
|
1055
|
+
async function detectStructure(repoPath, emitter) {
|
|
1056
|
+
const isTurbo = existsSync(join(repoPath, "turbo.json"));
|
|
1057
|
+
const isNx = existsSync(join(repoPath, "nx.json"));
|
|
1058
|
+
const isLerna = existsSync(join(repoPath, "lerna.json"));
|
|
1059
|
+
const isPnpmWorkspace = existsSync(join(repoPath, "pnpm-workspace.yaml"));
|
|
1060
|
+
let repoType = "single";
|
|
1061
|
+
if (isTurbo) {
|
|
1062
|
+
repoType = "monorepo-turbo";
|
|
1063
|
+
} else if (isNx) {
|
|
1064
|
+
repoType = "monorepo-nx";
|
|
1065
|
+
} else if (isLerna) {
|
|
1066
|
+
repoType = "monorepo-lerna";
|
|
1067
|
+
} else if (isPnpmWorkspace) {
|
|
1068
|
+
repoType = "polyrepo";
|
|
1069
|
+
}
|
|
1070
|
+
let packageManager = null;
|
|
1071
|
+
if (existsSync(join(repoPath, "pnpm-lock.yaml"))) {
|
|
1072
|
+
packageManager = "pnpm";
|
|
1073
|
+
} else if (existsSync(join(repoPath, "bun.lockb"))) {
|
|
1074
|
+
packageManager = "bun";
|
|
1075
|
+
} else if (existsSync(join(repoPath, "yarn.lock"))) {
|
|
1076
|
+
packageManager = "yarn";
|
|
1077
|
+
} else if (existsSync(join(repoPath, "package-lock.json"))) {
|
|
1078
|
+
packageManager = "npm";
|
|
1079
|
+
}
|
|
1080
|
+
const configExtensions = [
|
|
1081
|
+
".json",
|
|
1082
|
+
".yaml",
|
|
1083
|
+
".yml",
|
|
1084
|
+
".toml",
|
|
1085
|
+
".config.ts",
|
|
1086
|
+
".config.js",
|
|
1087
|
+
".config.mjs"
|
|
1088
|
+
];
|
|
1089
|
+
let configFileCount = 0;
|
|
1090
|
+
try {
|
|
1091
|
+
const rootFiles = readdirSync(repoPath);
|
|
1092
|
+
for (const f of rootFiles) {
|
|
1093
|
+
if (configExtensions.some((ext) => f.endsWith(ext))) {
|
|
1094
|
+
configFileCount++;
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
} catch {
|
|
1098
|
+
}
|
|
1099
|
+
let gitignoreComplexity = 0;
|
|
1100
|
+
try {
|
|
1101
|
+
const gitignore = readFileSync(join(repoPath, ".gitignore"), "utf-8");
|
|
1102
|
+
gitignoreComplexity = gitignore.split("\n").filter((l) => l.trim() && !l.startsWith("#")).length;
|
|
1103
|
+
} catch {
|
|
1104
|
+
}
|
|
1105
|
+
const [ageResult, countResult, branchResult] = await Promise.all([
|
|
1106
|
+
execFileAsync3("git", [
|
|
1107
|
+
"log",
|
|
1108
|
+
"--reverse",
|
|
1109
|
+
"--format=%aI",
|
|
1110
|
+
"-1"
|
|
1111
|
+
], {
|
|
1112
|
+
cwd: repoPath,
|
|
1113
|
+
timeout: 5e3
|
|
1114
|
+
}).catch(() => ({
|
|
1115
|
+
stdout: ""
|
|
1116
|
+
})),
|
|
1117
|
+
execFileAsync3("git", [
|
|
1118
|
+
"rev-list",
|
|
1119
|
+
"--count",
|
|
1120
|
+
"--all"
|
|
1121
|
+
], {
|
|
1122
|
+
cwd: repoPath,
|
|
1123
|
+
timeout: 5e3
|
|
1124
|
+
}).catch(() => ({
|
|
1125
|
+
stdout: "0"
|
|
1126
|
+
})),
|
|
1127
|
+
execFileAsync3("git", [
|
|
1128
|
+
"branch",
|
|
1129
|
+
"-a",
|
|
1130
|
+
"--format=%(refname:short) %(committerdate:iso)"
|
|
1131
|
+
], {
|
|
1132
|
+
cwd: repoPath,
|
|
1133
|
+
timeout: 5e3
|
|
1134
|
+
}).catch(() => ({
|
|
1135
|
+
stdout: ""
|
|
1136
|
+
}))
|
|
1137
|
+
]);
|
|
1138
|
+
let age = 0;
|
|
1139
|
+
if (ageResult.stdout.trim()) {
|
|
1140
|
+
const firstDate = new Date(ageResult.stdout.trim());
|
|
1141
|
+
age = Math.floor((Date.now() - firstDate.getTime()) / (24 * 60 * 60 * 1e3));
|
|
1142
|
+
}
|
|
1143
|
+
const totalCommits = Number.parseInt(countResult.stdout.trim(), 10) || 0;
|
|
1144
|
+
let activeBranches = 0;
|
|
1145
|
+
const thirtyDaysAgo = Date.now() - 30 * 24 * 60 * 60 * 1e3;
|
|
1146
|
+
for (const line of branchResult.stdout.split("\n").filter(Boolean)) {
|
|
1147
|
+
const parts = line.trim().split(/\s+/);
|
|
1148
|
+
if (parts.length >= 2) {
|
|
1149
|
+
const branchDate = new Date(parts.slice(1).join(" "));
|
|
1150
|
+
if (!Number.isNaN(branchDate.getTime()) && branchDate.getTime() > thirtyDaysAgo) {
|
|
1151
|
+
activeBranches++;
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
const packageDirs = detectPackageDirs(repoPath);
|
|
1156
|
+
const testAdjacency = computeTestAdjacency(repoPath, packageDirs);
|
|
1157
|
+
const sensitiveSurfaces = detectSensitiveSurfaces(repoPath);
|
|
1158
|
+
const structure = {
|
|
1159
|
+
repoType,
|
|
1160
|
+
framework: detectFramework(repoPath),
|
|
1161
|
+
packageManager,
|
|
1162
|
+
hasCI: existsSync(join(repoPath, ".github", "workflows")) || existsSync(join(repoPath, ".gitlab-ci.yml")) || existsSync(join(repoPath, ".circleci")),
|
|
1163
|
+
hasDocker: existsSync(join(repoPath, "Dockerfile")) || existsSync(join(repoPath, "docker-compose.yml")),
|
|
1164
|
+
testFramework: detectTestFramework(repoPath),
|
|
1165
|
+
configFileCount,
|
|
1166
|
+
gitignoreComplexity,
|
|
1167
|
+
age,
|
|
1168
|
+
totalCommits,
|
|
1169
|
+
activeBranches,
|
|
1170
|
+
testAdjacency,
|
|
1171
|
+
sensitiveSurfaces
|
|
1172
|
+
};
|
|
1173
|
+
if (emitter && sensitiveSurfaces.hasEnvFiles) {
|
|
1174
|
+
emitter.emit("discovery", {
|
|
1175
|
+
source: "structure",
|
|
1176
|
+
confidence: 0.95,
|
|
1177
|
+
message: "Detected environment variables",
|
|
1178
|
+
detailMessage: "Vreko ignores .env files completely"
|
|
1179
|
+
});
|
|
1180
|
+
}
|
|
1181
|
+
if (emitter && testAdjacency.weaklyTestedHotspots.length > 0) {
|
|
1182
|
+
emitter.emit("discovery", {
|
|
1183
|
+
source: "structure",
|
|
1184
|
+
confidence: 0.87,
|
|
1185
|
+
message: `${testAdjacency.weaklyTestedHotspots.length} package(s) with high churn but low test coverage`,
|
|
1186
|
+
detailMessage: "Vreko will monitor these as fragile zones"
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1189
|
+
return structure;
|
|
1190
|
+
}
|
|
1191
|
+
__name(detectStructure, "detectStructure");
|
|
1192
|
+
__name2(detectStructure, "detectStructure");
|
|
1193
|
+
var execFileAsync4 = promisify(execFile);
|
|
1194
|
+
function writeScanCache(path, cache) {
|
|
1195
|
+
writeFileSync(path, JSON.stringify(cache, null, 2));
|
|
1196
|
+
}
|
|
1197
|
+
__name(writeScanCache, "writeScanCache");
|
|
1198
|
+
__name2(writeScanCache, "writeScanCache");
|
|
1199
|
+
function readScanCache(path) {
|
|
1200
|
+
if (!existsSync(path)) {
|
|
1201
|
+
return null;
|
|
1202
|
+
}
|
|
1203
|
+
try {
|
|
1204
|
+
const data = JSON.parse(readFileSync(path, "utf-8"));
|
|
1205
|
+
if (!data.lastScannedHead || !data.lastScannedAt) {
|
|
1206
|
+
return null;
|
|
1207
|
+
}
|
|
1208
|
+
return data;
|
|
1209
|
+
} catch {
|
|
1210
|
+
return null;
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
__name(readScanCache, "readScanCache");
|
|
1214
|
+
__name2(readScanCache, "readScanCache");
|
|
1215
|
+
async function computeDelta(cache, repoPath) {
|
|
1216
|
+
try {
|
|
1217
|
+
const { stdout: currentHead } = await execFileAsync4("git", [
|
|
1218
|
+
"rev-parse",
|
|
1219
|
+
"HEAD"
|
|
1220
|
+
], {
|
|
1221
|
+
cwd: repoPath,
|
|
1222
|
+
timeout: 5e3
|
|
1223
|
+
});
|
|
1224
|
+
const head = currentHead.trim();
|
|
1225
|
+
if (head === cache.lastScannedHead) {
|
|
1226
|
+
return {
|
|
1227
|
+
needsFullScan: false
|
|
1228
|
+
};
|
|
1229
|
+
}
|
|
1230
|
+
try {
|
|
1231
|
+
await execFileAsync4("git", [
|
|
1232
|
+
"merge-base",
|
|
1233
|
+
"--is-ancestor",
|
|
1234
|
+
cache.lastScannedHead,
|
|
1235
|
+
head
|
|
1236
|
+
], {
|
|
1237
|
+
cwd: repoPath,
|
|
1238
|
+
timeout: 5e3
|
|
1239
|
+
});
|
|
1240
|
+
const { stdout: countOut } = await execFileAsync4("git", [
|
|
1241
|
+
"rev-list",
|
|
1242
|
+
"--count",
|
|
1243
|
+
`${cache.lastScannedHead}..${head}`
|
|
1244
|
+
], {
|
|
1245
|
+
cwd: repoPath,
|
|
1246
|
+
timeout: 5e3
|
|
1247
|
+
});
|
|
1248
|
+
const count = Number.parseInt(countOut.trim(), 10) || 0;
|
|
1249
|
+
return {
|
|
1250
|
+
needsFullScan: false,
|
|
1251
|
+
commitRange: {
|
|
1252
|
+
from: cache.lastScannedHead,
|
|
1253
|
+
to: head,
|
|
1254
|
+
count
|
|
1255
|
+
}
|
|
1256
|
+
};
|
|
1257
|
+
} catch {
|
|
1258
|
+
return {
|
|
1259
|
+
needsFullScan: true
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
1262
|
+
} catch {
|
|
1263
|
+
return {
|
|
1264
|
+
needsFullScan: true
|
|
1265
|
+
};
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
__name(computeDelta, "computeDelta");
|
|
1269
|
+
__name2(computeDelta, "computeDelta");
|
|
1270
|
+
var ScanEventEmitter = class extends EventEmitter {
|
|
1271
|
+
static {
|
|
1272
|
+
__name(this, "ScanEventEmitter");
|
|
1273
|
+
}
|
|
1274
|
+
static {
|
|
1275
|
+
__name2(this, "ScanEventEmitter");
|
|
1276
|
+
}
|
|
1277
|
+
// --- DiscoveryEmitter compatibility ---
|
|
1278
|
+
emitDiscovery(discovery) {
|
|
1279
|
+
if (discovery.confidence > 0.85) {
|
|
1280
|
+
this.emit("discovery", discovery);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
// --- Scan-specific events ---
|
|
1284
|
+
emitProgress(event) {
|
|
1285
|
+
this.emit("progress", event);
|
|
1286
|
+
}
|
|
1287
|
+
emitFinding(event) {
|
|
1288
|
+
this.emit("finding", event);
|
|
1289
|
+
}
|
|
1290
|
+
emitComplete(profile) {
|
|
1291
|
+
this.emit("complete", profile);
|
|
1292
|
+
}
|
|
1293
|
+
onDiscovery(handler) {
|
|
1294
|
+
this.on("discovery", handler);
|
|
1295
|
+
return this;
|
|
1296
|
+
}
|
|
1297
|
+
onProgress(handler) {
|
|
1298
|
+
this.on("progress", handler);
|
|
1299
|
+
return this;
|
|
1300
|
+
}
|
|
1301
|
+
onFinding(handler) {
|
|
1302
|
+
this.on("finding", handler);
|
|
1303
|
+
return this;
|
|
1304
|
+
}
|
|
1305
|
+
onComplete(handler) {
|
|
1306
|
+
this.on("complete", handler);
|
|
1307
|
+
return this;
|
|
1308
|
+
}
|
|
1309
|
+
};
|
|
1310
|
+
function createScanEventEmitter() {
|
|
1311
|
+
return new ScanEventEmitter();
|
|
1312
|
+
}
|
|
1313
|
+
__name(createScanEventEmitter, "createScanEventEmitter");
|
|
1314
|
+
__name2(createScanEventEmitter, "createScanEventEmitter");
|
|
1315
|
+
var ACTIVE_SCAN_LOCKS = /* @__PURE__ */ new Set();
|
|
1316
|
+
var DEFAULT_CACHE_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
1317
|
+
function resolveCachePath(workspaceHash) {
|
|
1318
|
+
const cacheDir = join(homedir(), ".vreko", "cache", "init-scan");
|
|
1319
|
+
mkdirSync(cacheDir, {
|
|
1320
|
+
recursive: true
|
|
1321
|
+
});
|
|
1322
|
+
return join(cacheDir, `${workspaceHash}.json`);
|
|
1323
|
+
}
|
|
1324
|
+
__name(resolveCachePath, "resolveCachePath");
|
|
1325
|
+
__name2(resolveCachePath, "resolveCachePath");
|
|
1326
|
+
function isCacheValid(cache, maxAgeMs = DEFAULT_CACHE_MAX_AGE_MS) {
|
|
1327
|
+
if (!cache) {
|
|
1328
|
+
return false;
|
|
1329
|
+
}
|
|
1330
|
+
const cachedAt = Date.parse(cache.lastScannedAt);
|
|
1331
|
+
if (!Number.isFinite(cachedAt)) {
|
|
1332
|
+
return false;
|
|
1333
|
+
}
|
|
1334
|
+
return Date.now() - cachedAt < maxAgeMs;
|
|
1335
|
+
}
|
|
1336
|
+
__name(isCacheValid, "isCacheValid");
|
|
1337
|
+
__name2(isCacheValid, "isCacheValid");
|
|
1338
|
+
function isScanInProgress(workspaceHash) {
|
|
1339
|
+
return ACTIVE_SCAN_LOCKS.has(workspaceHash);
|
|
1340
|
+
}
|
|
1341
|
+
__name(isScanInProgress, "isScanInProgress");
|
|
1342
|
+
__name2(isScanInProgress, "isScanInProgress");
|
|
1343
|
+
function acquireScanLock(workspaceHash) {
|
|
1344
|
+
if (ACTIVE_SCAN_LOCKS.has(workspaceHash)) {
|
|
1345
|
+
return null;
|
|
1346
|
+
}
|
|
1347
|
+
ACTIVE_SCAN_LOCKS.add(workspaceHash);
|
|
1348
|
+
let released = false;
|
|
1349
|
+
return () => {
|
|
1350
|
+
if (released) {
|
|
1351
|
+
return;
|
|
1352
|
+
}
|
|
1353
|
+
released = true;
|
|
1354
|
+
ACTIVE_SCAN_LOCKS.delete(workspaceHash);
|
|
1355
|
+
};
|
|
1356
|
+
}
|
|
1357
|
+
__name(acquireScanLock, "acquireScanLock");
|
|
1358
|
+
__name2(acquireScanLock, "acquireScanLock");
|
|
1359
|
+
function clamp(value, min = 0, max = 100) {
|
|
1360
|
+
return Math.max(min, Math.min(max, value));
|
|
1361
|
+
}
|
|
1362
|
+
__name(clamp, "clamp");
|
|
1363
|
+
__name2(clamp, "clamp");
|
|
1364
|
+
function computeRecoveryRisk(signals) {
|
|
1365
|
+
const reflog = signals.reflog;
|
|
1366
|
+
const gitlog = signals.gitlog;
|
|
1367
|
+
let score = 0;
|
|
1368
|
+
if (reflog) {
|
|
1369
|
+
score += Math.min(reflog.resetToHEADCount * 3, 40);
|
|
1370
|
+
score += Math.min(reflog.forceResetRate * 5, 15);
|
|
1371
|
+
}
|
|
1372
|
+
if (gitlog) {
|
|
1373
|
+
const rc = gitlog.rollbackCorrelation;
|
|
1374
|
+
score += Math.min(rc.resetAfterLargeCommitRate * 0.3, 15);
|
|
1375
|
+
score += Math.min(rc.resetAfterCrossPackageRate * 0.3, 10);
|
|
1376
|
+
const totalReverts = gitlog.fileChurnRanking.reduce((s, f) => s + f.revertCount, 0);
|
|
1377
|
+
score += Math.min(totalReverts * 2, 20);
|
|
1378
|
+
}
|
|
1379
|
+
return clamp(score);
|
|
1380
|
+
}
|
|
1381
|
+
__name(computeRecoveryRisk, "computeRecoveryRisk");
|
|
1382
|
+
__name2(computeRecoveryRisk, "computeRecoveryRisk");
|
|
1383
|
+
function computeChangeVolatility(signals) {
|
|
1384
|
+
const reflog = signals.reflog;
|
|
1385
|
+
const gitlog = signals.gitlog;
|
|
1386
|
+
let score = 0;
|
|
1387
|
+
if (reflog) {
|
|
1388
|
+
score += Math.min(reflog.forceResetRate * 4, 20);
|
|
1389
|
+
score += Math.min(reflog.rebaseFrequency * 2, 10);
|
|
1390
|
+
score += Math.min(reflog.branchAbandonmentRate * 0.2, 10);
|
|
1391
|
+
}
|
|
1392
|
+
if (gitlog) {
|
|
1393
|
+
const d = gitlog.diffusion;
|
|
1394
|
+
score += Math.min(d.crossDirectoryDiffusionRate * 0.5, 20);
|
|
1395
|
+
score += Math.min(gitlog.largeCommitRatio * 0.4, 15);
|
|
1396
|
+
score += Math.min(d.largeSessionClusterRate * 0.5, 15);
|
|
1397
|
+
score += Math.min(gitlog.commitFrequencyVariance * 2, 10);
|
|
1398
|
+
}
|
|
1399
|
+
return clamp(score);
|
|
1400
|
+
}
|
|
1401
|
+
__name(computeChangeVolatility, "computeChangeVolatility");
|
|
1402
|
+
__name2(computeChangeVolatility, "computeChangeVolatility");
|
|
1403
|
+
function computeWorkflowFragility(signals) {
|
|
1404
|
+
const gitlog = signals.gitlog;
|
|
1405
|
+
const structure = signals.structure;
|
|
1406
|
+
let score = 0;
|
|
1407
|
+
if (gitlog) {
|
|
1408
|
+
const fragileCount = gitlog.fileChurnRanking.filter((f) => f.revertCount > 0 && f.changeCount > 5).length;
|
|
1409
|
+
score += Math.min(fragileCount * 5, 25);
|
|
1410
|
+
const unstablePairs = gitlog.coChangeGraph.filter((e) => e.confidence > 0.7);
|
|
1411
|
+
score += Math.min(unstablePairs.length * 3, 15);
|
|
1412
|
+
score += Math.min(gitlog.hotspotFiles.length * 2, 10);
|
|
1413
|
+
}
|
|
1414
|
+
if (structure) {
|
|
1415
|
+
const testGap = 100 - structure.testAdjacency.overallTestAdjacencyScore;
|
|
1416
|
+
score += Math.min(testGap * 0.3, 20);
|
|
1417
|
+
score += Math.min(structure.testAdjacency.weaklyTestedHotspots.length * 5, 15);
|
|
1418
|
+
score += Math.min(structure.sensitiveSurfaces.sensitivePathCount * 3, 15);
|
|
1419
|
+
}
|
|
1420
|
+
return clamp(score);
|
|
1421
|
+
}
|
|
1422
|
+
__name(computeWorkflowFragility, "computeWorkflowFragility");
|
|
1423
|
+
__name2(computeWorkflowFragility, "computeWorkflowFragility");
|
|
1424
|
+
function computeComplexity(signals) {
|
|
1425
|
+
const structure = signals.structure;
|
|
1426
|
+
if (!structure) {
|
|
1427
|
+
return 0;
|
|
1428
|
+
}
|
|
1429
|
+
let score = 0;
|
|
1430
|
+
score += Math.min(structure.configFileCount * 2, 30);
|
|
1431
|
+
const packageCount = structure.testAdjacency.packageTestAdjacency.length;
|
|
1432
|
+
score += Math.min(packageCount * 3, 30);
|
|
1433
|
+
score += Math.min(structure.gitignoreComplexity, 20);
|
|
1434
|
+
score += structure.repoType.startsWith("monorepo") ? 20 : 0;
|
|
1435
|
+
return clamp(score);
|
|
1436
|
+
}
|
|
1437
|
+
__name(computeComplexity, "computeComplexity");
|
|
1438
|
+
__name2(computeComplexity, "computeComplexity");
|
|
1439
|
+
function computeCollaboration(signals) {
|
|
1440
|
+
const gitlog = signals.gitlog;
|
|
1441
|
+
if (!gitlog) {
|
|
1442
|
+
return 0;
|
|
1443
|
+
}
|
|
1444
|
+
let score = 0;
|
|
1445
|
+
score += Math.min(gitlog.contributorCount * 5, 30);
|
|
1446
|
+
score += Math.min(gitlog.mergeConflictFrequency * 0.5, 20);
|
|
1447
|
+
if (gitlog.busFactorEstimate <= 1) {
|
|
1448
|
+
score += 30;
|
|
1449
|
+
} else if (gitlog.busFactorEstimate <= 2) {
|
|
1450
|
+
score += 15;
|
|
1451
|
+
}
|
|
1452
|
+
return clamp(score);
|
|
1453
|
+
}
|
|
1454
|
+
__name(computeCollaboration, "computeCollaboration");
|
|
1455
|
+
__name2(computeCollaboration, "computeCollaboration");
|
|
1456
|
+
function calculateStructuralRisk(topology) {
|
|
1457
|
+
if (!topology) {
|
|
1458
|
+
return 0;
|
|
1459
|
+
}
|
|
1460
|
+
let score = 0;
|
|
1461
|
+
score += Math.min(30, topology.circularChainCount * 8);
|
|
1462
|
+
const highFanInRatio = topology.highFanInFiles.length / Math.max(topology.moduleCount, 1);
|
|
1463
|
+
score += Math.min(25, highFanInRatio * 500);
|
|
1464
|
+
score += Math.min(20, topology.ruleViolationCount * 3);
|
|
1465
|
+
const orphanRatio = topology.orphanFileCount / Math.max(topology.moduleCount, 1);
|
|
1466
|
+
score += Math.min(15, orphanRatio * 150);
|
|
1467
|
+
const edgeDensity = topology.edgeCount / Math.max(topology.moduleCount, 1);
|
|
1468
|
+
if (edgeDensity > 10) {
|
|
1469
|
+
score += 10;
|
|
1470
|
+
}
|
|
1471
|
+
return Math.min(100, Math.round(score));
|
|
1472
|
+
}
|
|
1473
|
+
__name(calculateStructuralRisk, "calculateStructuralRisk");
|
|
1474
|
+
__name2(calculateStructuralRisk, "calculateStructuralRisk");
|
|
1475
|
+
function computeAiExposure(repoPath, toolIdentity) {
|
|
1476
|
+
if (!repoPath) {
|
|
1477
|
+
return 0;
|
|
1478
|
+
}
|
|
1479
|
+
let score = 0;
|
|
1480
|
+
const aiIndicators = [
|
|
1481
|
+
".cursor",
|
|
1482
|
+
".github/copilot",
|
|
1483
|
+
".codeium",
|
|
1484
|
+
".continue",
|
|
1485
|
+
".aider",
|
|
1486
|
+
".windsurf",
|
|
1487
|
+
".claude",
|
|
1488
|
+
".augment",
|
|
1489
|
+
".roo"
|
|
1490
|
+
];
|
|
1491
|
+
for (const indicator of aiIndicators) {
|
|
1492
|
+
if (existsSync(join(repoPath, indicator))) {
|
|
1493
|
+
score += 15;
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
if (toolIdentity && toolIdentity.confidence >= 0.5) {
|
|
1497
|
+
const TOOL_RISK_MULTIPLIERS = {
|
|
1498
|
+
devin: 1.4,
|
|
1499
|
+
"claude-code": 1.2,
|
|
1500
|
+
cursor: 1,
|
|
1501
|
+
"github-copilot": 0.9,
|
|
1502
|
+
windsurf: 1,
|
|
1503
|
+
augment: 1,
|
|
1504
|
+
cline: 1.1,
|
|
1505
|
+
roocode: 1.1,
|
|
1506
|
+
aider: 1
|
|
1507
|
+
};
|
|
1508
|
+
const multiplier = TOOL_RISK_MULTIPLIERS[toolIdentity.tool] ?? 1;
|
|
1509
|
+
score = Math.round(score * multiplier);
|
|
1510
|
+
}
|
|
1511
|
+
try {
|
|
1512
|
+
const { execSync } = __require2("child_process");
|
|
1513
|
+
const currentBranch = execSync("git rev-parse --abbrev-ref HEAD", {
|
|
1514
|
+
cwd: repoPath,
|
|
1515
|
+
timeout: 5e3,
|
|
1516
|
+
encoding: "utf-8"
|
|
1517
|
+
}).trim();
|
|
1518
|
+
if (/^devin\/\d{10}-/.test(currentBranch)) {
|
|
1519
|
+
score += 10;
|
|
1520
|
+
}
|
|
1521
|
+
} catch {
|
|
1522
|
+
}
|
|
1523
|
+
return clamp(score);
|
|
1524
|
+
}
|
|
1525
|
+
__name(computeAiExposure, "computeAiExposure");
|
|
1526
|
+
__name2(computeAiExposure, "computeAiExposure");
|
|
1527
|
+
function buildTopDrivers(signals) {
|
|
1528
|
+
const drivers = [];
|
|
1529
|
+
if (signals.reflog && signals.reflog.resetToHEADCount > 3) {
|
|
1530
|
+
drivers.push({
|
|
1531
|
+
id: "repeated-recoveries",
|
|
1532
|
+
label: "Repeated recovery events",
|
|
1533
|
+
scoreImpact: Math.min(signals.reflog.resetToHEADCount * 3, 40),
|
|
1534
|
+
evidence: [
|
|
1535
|
+
`${signals.reflog.resetToHEADCount} reset-to-HEAD events detected`,
|
|
1536
|
+
signals.reflog.avgTimeBetweenResets > 0 ? `Average ${Math.round(signals.reflog.avgTimeBetweenResets)} minutes between resets` : "Multiple resets in short timeframe"
|
|
1537
|
+
],
|
|
1538
|
+
protectiveAction: "Increase snapshot density during volatile periods"
|
|
1539
|
+
});
|
|
1540
|
+
}
|
|
1541
|
+
if (signals.gitlog) {
|
|
1542
|
+
const rc = signals.gitlog.rollbackCorrelation;
|
|
1543
|
+
if (rc.resetAfterLargeCommitRate > 20 || rc.resetAfterCrossPackageRate > 20) {
|
|
1544
|
+
drivers.push({
|
|
1545
|
+
id: "rollback-correlation",
|
|
1546
|
+
label: `Recoveries correlate with ${rc.topRecoveryTrigger.replace(/-/g, " ")} changes`,
|
|
1547
|
+
scoreImpact: Math.max(rc.resetAfterLargeCommitRate, rc.resetAfterCrossPackageRate) * 0.3,
|
|
1548
|
+
evidence: [
|
|
1549
|
+
rc.resetAfterLargeCommitRate > 0 ? `${Math.round(rc.resetAfterLargeCommitRate)}% of reverts follow large commits` : "",
|
|
1550
|
+
rc.resetAfterCrossPackageRate > 0 ? `${Math.round(rc.resetAfterCrossPackageRate)}% of reverts follow cross-package changes` : ""
|
|
1551
|
+
].filter(Boolean),
|
|
1552
|
+
protectiveAction: "Auto-snapshot before high-diffusion commits"
|
|
1553
|
+
});
|
|
1554
|
+
}
|
|
1555
|
+
if (signals.gitlog.hotspotFiles.length > 0) {
|
|
1556
|
+
const topChurn = signals.gitlog.fileChurnRanking[0];
|
|
1557
|
+
drivers.push({
|
|
1558
|
+
id: "fragile-hotspots",
|
|
1559
|
+
label: "Fragile file hotspots detected",
|
|
1560
|
+
scoreImpact: Math.min(signals.gitlog.hotspotFiles.length * 5, 25),
|
|
1561
|
+
evidence: [
|
|
1562
|
+
topChurn ? `${topChurn.path} changed ${topChurn.changeCount} times (${topChurn.revertCount} reverts)` : "Multiple high-churn files with reverts"
|
|
1563
|
+
],
|
|
1564
|
+
protectiveAction: "Watch fragile files with enhanced monitoring"
|
|
1565
|
+
});
|
|
1566
|
+
}
|
|
1567
|
+
if (signals.gitlog.diffusion.crossDirectoryDiffusionRate > 20) {
|
|
1568
|
+
drivers.push({
|
|
1569
|
+
id: "cross-directory-blast",
|
|
1570
|
+
label: "High cross-directory change diffusion",
|
|
1571
|
+
scoreImpact: Math.min(signals.gitlog.diffusion.crossDirectoryDiffusionRate * 0.5, 20),
|
|
1572
|
+
evidence: [
|
|
1573
|
+
`${Math.round(signals.gitlog.diffusion.crossDirectoryDiffusionRate)}% of commits span multiple directories`
|
|
1574
|
+
],
|
|
1575
|
+
protectiveAction: "Track cross-directory blast radius"
|
|
1576
|
+
});
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
if (signals.structure?.testAdjacency.weaklyTestedHotspots.length) {
|
|
1580
|
+
drivers.push({
|
|
1581
|
+
id: "weak-test-coverage",
|
|
1582
|
+
label: "Weakly-tested high-churn areas",
|
|
1583
|
+
scoreImpact: Math.min(signals.structure.testAdjacency.weaklyTestedHotspots.length * 5, 15),
|
|
1584
|
+
evidence: signals.structure.testAdjacency.weaklyTestedHotspots.map((p) => `${p} has high churn but low test coverage`),
|
|
1585
|
+
protectiveAction: "Prioritize snapshot protection for untested hotspots"
|
|
1586
|
+
});
|
|
1587
|
+
}
|
|
1588
|
+
return drivers.sort((a, b) => b.scoreImpact - a.scoreImpact).slice(0, 5);
|
|
1589
|
+
}
|
|
1590
|
+
__name(buildTopDrivers, "buildTopDrivers");
|
|
1591
|
+
__name2(buildTopDrivers, "buildTopDrivers");
|
|
1592
|
+
function buildRecommendedConfig(signals, recoveryRisk) {
|
|
1593
|
+
const protectionLevel = recoveryRisk > 70 ? "maximum" : recoveryRisk > 40 ? "enhanced" : "standard";
|
|
1594
|
+
const snapshotFrequency = recoveryRisk > 70 ? "aggressive" : recoveryRisk > 40 ? "balanced" : "conservative";
|
|
1595
|
+
const watchTargets = [];
|
|
1596
|
+
if (signals.gitlog) {
|
|
1597
|
+
for (const file of signals.gitlog.hotspotFiles.slice(0, 5)) {
|
|
1598
|
+
const churnEntry = signals.gitlog.fileChurnRanking.find((f) => f.path === file);
|
|
1599
|
+
watchTargets.push({
|
|
1600
|
+
path: file,
|
|
1601
|
+
fileCount: 1,
|
|
1602
|
+
reason: churnEntry?.revertCount ? "fragile detected" : "high churn"
|
|
1603
|
+
});
|
|
1604
|
+
}
|
|
1605
|
+
for (const edge of signals.gitlog.coChangeGraph.slice(0, 3)) {
|
|
1606
|
+
if (edge.confidence > 0.7) {
|
|
1607
|
+
const dir = edge.fileA.includes("/") ? edge.fileA.substring(0, edge.fileA.lastIndexOf("/")) : ".";
|
|
1608
|
+
if (!watchTargets.some((t) => t.path === dir)) {
|
|
1609
|
+
watchTargets.push({
|
|
1610
|
+
path: dir,
|
|
1611
|
+
fileCount: 2,
|
|
1612
|
+
reason: "co-change patterns"
|
|
1613
|
+
});
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
if (signals.structure) {
|
|
1619
|
+
for (const hotspot of signals.structure.testAdjacency.weaklyTestedHotspots.slice(0, 3)) {
|
|
1620
|
+
if (!watchTargets.some((t) => t.path === hotspot)) {
|
|
1621
|
+
watchTargets.push({
|
|
1622
|
+
path: hotspot,
|
|
1623
|
+
fileCount: 0,
|
|
1624
|
+
reason: "weak test coverage"
|
|
1625
|
+
});
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
if (watchTargets.length === 0 && signals.gitlog?.fileChurnRanking.length) {
|
|
1630
|
+
const topChurned = [
|
|
1631
|
+
...signals.gitlog.fileChurnRanking
|
|
1632
|
+
].filter((f) => f.changeCount > 0).sort((a, b) => b.changeCount - a.changeCount).slice(0, 5);
|
|
1633
|
+
for (const file of topChurned) {
|
|
1634
|
+
watchTargets.push({
|
|
1635
|
+
path: file.path,
|
|
1636
|
+
fileCount: 1,
|
|
1637
|
+
reason: "frequently changed"
|
|
1638
|
+
});
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
const enabledFeatures = [
|
|
1642
|
+
"real-time-protection"
|
|
1643
|
+
];
|
|
1644
|
+
if (recoveryRisk > 50) {
|
|
1645
|
+
enabledFeatures.push("pre-commit-snapshot");
|
|
1646
|
+
}
|
|
1647
|
+
if (signals.gitlog?.coChangeGraph.length) {
|
|
1648
|
+
enabledFeatures.push("co-change-tracking");
|
|
1649
|
+
}
|
|
1650
|
+
return {
|
|
1651
|
+
protectionLevel,
|
|
1652
|
+
snapshotFrequency,
|
|
1653
|
+
watchTargets,
|
|
1654
|
+
enabledFeatures
|
|
1655
|
+
};
|
|
1656
|
+
}
|
|
1657
|
+
__name(buildRecommendedConfig, "buildRecommendedConfig");
|
|
1658
|
+
__name2(buildRecommendedConfig, "buildRecommendedConfig");
|
|
1659
|
+
function riskTier(score) {
|
|
1660
|
+
if (score > 75) {
|
|
1661
|
+
return "high";
|
|
1662
|
+
}
|
|
1663
|
+
if (score > 50) {
|
|
1664
|
+
return "elevated";
|
|
1665
|
+
}
|
|
1666
|
+
if (score > 25) {
|
|
1667
|
+
return "moderate";
|
|
1668
|
+
}
|
|
1669
|
+
return "low";
|
|
1670
|
+
}
|
|
1671
|
+
__name(riskTier, "riskTier");
|
|
1672
|
+
__name2(riskTier, "riskTier");
|
|
1673
|
+
function computeConfidence(signals) {
|
|
1674
|
+
let sources = 0;
|
|
1675
|
+
let weight = 0;
|
|
1676
|
+
if (signals.reflog) {
|
|
1677
|
+
sources++;
|
|
1678
|
+
weight += 0.4;
|
|
1679
|
+
}
|
|
1680
|
+
if (signals.gitlog) {
|
|
1681
|
+
sources++;
|
|
1682
|
+
weight += 0.4;
|
|
1683
|
+
}
|
|
1684
|
+
if (signals.structure) {
|
|
1685
|
+
sources++;
|
|
1686
|
+
weight += 0.2;
|
|
1687
|
+
}
|
|
1688
|
+
return sources === 0 ? 0.1 : weight;
|
|
1689
|
+
}
|
|
1690
|
+
__name(computeConfidence, "computeConfidence");
|
|
1691
|
+
__name2(computeConfidence, "computeConfidence");
|
|
1692
|
+
function buildTopFragileFiles(signals) {
|
|
1693
|
+
if (!signals.gitlog?.fileChurnRanking.length) {
|
|
1694
|
+
return [];
|
|
1695
|
+
}
|
|
1696
|
+
const withReverts = [
|
|
1697
|
+
...signals.gitlog.fileChurnRanking
|
|
1698
|
+
].filter((f) => f.changeCount >= 3 && f.revertCount > 0).sort((a, b) => b.revertCount / b.changeCount - a.revertCount / a.changeCount).slice(0, 20).map((f) => ({
|
|
1699
|
+
path: f.path,
|
|
1700
|
+
changeCount: f.changeCount,
|
|
1701
|
+
revertCount: f.revertCount
|
|
1702
|
+
}));
|
|
1703
|
+
if (withReverts.length > 0) {
|
|
1704
|
+
return withReverts;
|
|
1705
|
+
}
|
|
1706
|
+
return [
|
|
1707
|
+
...signals.gitlog.fileChurnRanking
|
|
1708
|
+
].filter((f) => f.changeCount > 0).sort((a, b) => b.changeCount - a.changeCount).slice(0, 5).map((f) => ({
|
|
1709
|
+
path: f.path,
|
|
1710
|
+
changeCount: f.changeCount,
|
|
1711
|
+
revertCount: 0
|
|
1712
|
+
}));
|
|
1713
|
+
}
|
|
1714
|
+
__name(buildTopFragileFiles, "buildTopFragileFiles");
|
|
1715
|
+
__name2(buildTopFragileFiles, "buildTopFragileFiles");
|
|
1716
|
+
function findTopFragileFile(signals) {
|
|
1717
|
+
if (!signals.gitlog?.fileChurnRanking.length) {
|
|
1718
|
+
return null;
|
|
1719
|
+
}
|
|
1720
|
+
const ranked = [
|
|
1721
|
+
...signals.gitlog.fileChurnRanking
|
|
1722
|
+
].filter((f) => f.changeCount >= 3 && f.revertCount > 0).sort((a, b) => b.revertCount / b.changeCount - a.revertCount / a.changeCount);
|
|
1723
|
+
return ranked.length > 0 ? ranked[0].path : null;
|
|
1724
|
+
}
|
|
1725
|
+
__name(findTopFragileFile, "findTopFragileFile");
|
|
1726
|
+
__name2(findTopFragileFile, "findTopFragileFile");
|
|
1727
|
+
var FRAGILITY_MIN_SAMPLE = 5;
|
|
1728
|
+
function confidenceWeight(changeCount) {
|
|
1729
|
+
return Math.min(changeCount / FRAGILITY_MIN_SAMPLE, 1);
|
|
1730
|
+
}
|
|
1731
|
+
__name(confidenceWeight, "confidenceWeight");
|
|
1732
|
+
__name2(confidenceWeight, "confidenceWeight");
|
|
1733
|
+
function isExcludedFromFragilityRanking(filePath) {
|
|
1734
|
+
return isArtifactFile(filePath);
|
|
1735
|
+
}
|
|
1736
|
+
__name(isExcludedFromFragilityRanking, "isExcludedFromFragilityRanking");
|
|
1737
|
+
__name2(isExcludedFromFragilityRanking, "isExcludedFromFragilityRanking");
|
|
1738
|
+
function buildFragilityArray(signals) {
|
|
1739
|
+
if (!signals.gitlog?.fileChurnRanking.length) {
|
|
1740
|
+
return [];
|
|
1741
|
+
}
|
|
1742
|
+
return signals.gitlog.fileChurnRanking.map((f) => {
|
|
1743
|
+
const excluded = isExcludedFromFragilityRanking(f.path);
|
|
1744
|
+
const revertRate = f.changeCount > 0 ? f.revertCount / f.changeCount : 0;
|
|
1745
|
+
const fragilityScore = excluded ? 0 : revertRate * confidenceWeight(f.changeCount);
|
|
1746
|
+
return {
|
|
1747
|
+
file: f.path,
|
|
1748
|
+
changeCount: f.changeCount,
|
|
1749
|
+
revertCount: f.revertCount,
|
|
1750
|
+
revertRate,
|
|
1751
|
+
fragilityScore,
|
|
1752
|
+
excluded
|
|
1753
|
+
};
|
|
1754
|
+
});
|
|
1755
|
+
}
|
|
1756
|
+
__name(buildFragilityArray, "buildFragilityArray");
|
|
1757
|
+
__name2(buildFragilityArray, "buildFragilityArray");
|
|
1758
|
+
function buildCoChangeArray(signals) {
|
|
1759
|
+
if (!signals.gitlog?.coChangeGraph.length) {
|
|
1760
|
+
return [];
|
|
1761
|
+
}
|
|
1762
|
+
return signals.gitlog.coChangeGraph.map((edge) => ({
|
|
1763
|
+
files: [
|
|
1764
|
+
edge.fileA,
|
|
1765
|
+
edge.fileB
|
|
1766
|
+
],
|
|
1767
|
+
rate: edge.confidence,
|
|
1768
|
+
occurrences: edge.coChangeCount,
|
|
1769
|
+
generated: isArtifactFile(edge.fileA) && isArtifactFile(edge.fileB)
|
|
1770
|
+
}));
|
|
1771
|
+
}
|
|
1772
|
+
__name(buildCoChangeArray, "buildCoChangeArray");
|
|
1773
|
+
__name2(buildCoChangeArray, "buildCoChangeArray");
|
|
1774
|
+
function synthesize(signals, baseline, repoPath) {
|
|
1775
|
+
const { insights, locked } = generateInsights(signals, baseline);
|
|
1776
|
+
const recoveryRisk = computeRecoveryRisk(signals);
|
|
1777
|
+
const changeVolatility = computeChangeVolatility(signals);
|
|
1778
|
+
const workflowFragility = computeWorkflowFragility(signals);
|
|
1779
|
+
return {
|
|
1780
|
+
overallRisk: riskTier(recoveryRisk),
|
|
1781
|
+
confidence: computeConfidence(signals),
|
|
1782
|
+
primary: {
|
|
1783
|
+
recoveryRisk,
|
|
1784
|
+
changeVolatility,
|
|
1785
|
+
workflowFragility
|
|
1786
|
+
},
|
|
1787
|
+
secondary: {
|
|
1788
|
+
complexity: computeComplexity(signals),
|
|
1789
|
+
collaboration: computeCollaboration(signals),
|
|
1790
|
+
aiExposure: computeAiExposure(repoPath, void 0),
|
|
1791
|
+
structuralRisk: calculateStructuralRisk(signals.topology)
|
|
1792
|
+
},
|
|
1793
|
+
topDrivers: buildTopDrivers(signals),
|
|
1794
|
+
insights,
|
|
1795
|
+
lockedInsights: [
|
|
1796
|
+
locked
|
|
1797
|
+
],
|
|
1798
|
+
recommendedConfig: buildRecommendedConfig(signals, recoveryRisk),
|
|
1799
|
+
topFragileFile: findTopFragileFile(signals),
|
|
1800
|
+
topFragileFiles: buildTopFragileFiles(signals),
|
|
1801
|
+
coChange: buildCoChangeArray(signals),
|
|
1802
|
+
fragility: buildFragilityArray(signals)
|
|
1803
|
+
};
|
|
1804
|
+
}
|
|
1805
|
+
__name(synthesize, "synthesize");
|
|
1806
|
+
__name2(synthesize, "synthesize");
|
|
1807
|
+
async function runTopologyScan(provider, repoPath, emitter) {
|
|
1808
|
+
const scanEmitter = emitter;
|
|
1809
|
+
scanEmitter?.emitProgress?.({
|
|
1810
|
+
stage: "topology",
|
|
1811
|
+
progress: 0,
|
|
1812
|
+
message: "Analyzing dependencies..."
|
|
1813
|
+
});
|
|
1814
|
+
try {
|
|
1815
|
+
const result = await provider.scan?.(repoPath);
|
|
1816
|
+
if (!result) {
|
|
1817
|
+
scanEmitter?.emitProgress?.({
|
|
1818
|
+
stage: "topology",
|
|
1819
|
+
progress: 100,
|
|
1820
|
+
message: "Topology scan returned no data"
|
|
1821
|
+
});
|
|
1822
|
+
return null;
|
|
1823
|
+
}
|
|
1824
|
+
for (const file of result.highFanInFiles.slice(0, 3)) {
|
|
1825
|
+
emitter?.emitDiscovery?.({
|
|
1826
|
+
source: "structure",
|
|
1827
|
+
confidence: 0.9,
|
|
1828
|
+
message: `${file.path} is imported by ${file.importedByCount} files`,
|
|
1829
|
+
detailMessage: "Vreko will treat this as a structural hotspot with enhanced blast radius monitoring",
|
|
1830
|
+
relatedInsightId: "fused-structural-temporal-hotspot"
|
|
1831
|
+
});
|
|
1832
|
+
}
|
|
1833
|
+
if (result.circularChainCount > 0) {
|
|
1834
|
+
emitter?.emitDiscovery?.({
|
|
1835
|
+
source: "structure",
|
|
1836
|
+
confidence: 0.95,
|
|
1837
|
+
message: `${result.circularChainCount} circular dependency chain(s) detected`,
|
|
1838
|
+
detailMessage: "Circular dependencies increase change risk - Vreko will monitor these chains closely"
|
|
1839
|
+
});
|
|
1840
|
+
}
|
|
1841
|
+
scanEmitter?.emitProgress?.({
|
|
1842
|
+
stage: "topology",
|
|
1843
|
+
progress: 100,
|
|
1844
|
+
message: `Topology: ${result.moduleCount} modules, ${result.edgeCount} edges in ${result.cruiseDurationMs}ms`
|
|
1845
|
+
});
|
|
1846
|
+
return result;
|
|
1847
|
+
} catch (_err) {
|
|
1848
|
+
scanEmitter?.emitProgress?.({
|
|
1849
|
+
stage: "topology",
|
|
1850
|
+
progress: 100,
|
|
1851
|
+
message: "Topology scan failed (non-fatal)"
|
|
1852
|
+
});
|
|
1853
|
+
return null;
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
__name(runTopologyScan, "runTopologyScan");
|
|
1857
|
+
__name2(runTopologyScan, "runTopologyScan");
|
|
1858
|
+
async function runInitScan({ repoPath, emitter, topologyProvider }) {
|
|
1859
|
+
const scanEmitter = emitter;
|
|
1860
|
+
const [reflogSignals, gitlogSignals, structureSignals, topologySignals] = await Promise.all([
|
|
1861
|
+
scanReflog(repoPath, emitter),
|
|
1862
|
+
analyzeGitLog(repoPath, emitter),
|
|
1863
|
+
detectStructure(repoPath, emitter),
|
|
1864
|
+
topologyProvider?.scan ? runTopologyScan(topologyProvider, repoPath, emitter) : Promise.resolve(null)
|
|
1865
|
+
]);
|
|
1866
|
+
scanEmitter?.emitProgress?.({
|
|
1867
|
+
stage: "synthesis",
|
|
1868
|
+
progress: 0,
|
|
1869
|
+
message: "Synthesizing risk profile..."
|
|
1870
|
+
});
|
|
1871
|
+
const profile = synthesize({
|
|
1872
|
+
reflog: reflogSignals,
|
|
1873
|
+
gitlog: gitlogSignals,
|
|
1874
|
+
structure: structureSignals,
|
|
1875
|
+
topology: topologySignals
|
|
1876
|
+
});
|
|
1877
|
+
scanEmitter?.emitProgress?.({
|
|
1878
|
+
stage: "synthesis",
|
|
1879
|
+
progress: 100,
|
|
1880
|
+
message: "Scan complete"
|
|
1881
|
+
});
|
|
1882
|
+
scanEmitter?.emitComplete?.(profile);
|
|
1883
|
+
return profile;
|
|
1884
|
+
}
|
|
1885
|
+
__name(runInitScan, "runInitScan");
|
|
1886
|
+
__name2(runInitScan, "runInitScan");
|
|
1887
|
+
async function runInitScan2(input, emitter) {
|
|
1888
|
+
if (typeof input !== "string") {
|
|
1889
|
+
return runInitScan({
|
|
1890
|
+
repoPath: input.repoPath,
|
|
1891
|
+
emitter: input.emitter,
|
|
1892
|
+
topologyProvider: input.topologyProvider
|
|
1893
|
+
});
|
|
1894
|
+
}
|
|
1895
|
+
const repoPath = input;
|
|
1896
|
+
const em = emitter ?? createDiscoveryEmitter();
|
|
1897
|
+
const [reflog, gitlog, structure] = await Promise.all([
|
|
1898
|
+
scanReflog(repoPath, em),
|
|
1899
|
+
analyzeGitLog(repoPath, em),
|
|
1900
|
+
detectStructure(repoPath, em)
|
|
1901
|
+
]);
|
|
1902
|
+
const signals = {
|
|
1903
|
+
reflog,
|
|
1904
|
+
gitlog,
|
|
1905
|
+
structure,
|
|
1906
|
+
topology: null
|
|
1907
|
+
};
|
|
1908
|
+
const profile = synthesize(signals, void 0, repoPath);
|
|
1909
|
+
return {
|
|
1910
|
+
profile,
|
|
1911
|
+
emitter: em
|
|
1912
|
+
};
|
|
1913
|
+
}
|
|
1914
|
+
__name(runInitScan2, "runInitScan2");
|
|
1915
|
+
__name2(runInitScan2, "runInitScan");
|
|
1916
|
+
|
|
1917
|
+
export { DiscoveryEmitter, ScanEventEmitter, acquireScanLock, analyzeGitLog, computeDelta, createDiscoveryEmitter, createScanEventEmitter, detectStructure, generateInsights, isCacheValid, isScanInProgress, readScanCache, resolveCachePath, runInitScan2 as runInitScan, scanReflog, synthesize, writeScanCache };
|
|
1918
|
+
//# sourceMappingURL=init-scan-RZNYDTUV.js.map
|
|
1919
|
+
//# sourceMappingURL=init-scan-RZNYDTUV.js.map
|