patchwarden 0.4.0 → 0.6.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/PatchWarden.cmd +51 -0
- package/README.en.md +1022 -0
- package/README.md +897 -358
- package/dist/assessments/agentAssessor.d.ts +15 -0
- package/dist/assessments/agentAssessor.js +293 -0
- package/dist/assessments/assessmentStore.d.ts +133 -0
- package/dist/assessments/assessmentStore.js +238 -0
- package/dist/assessments/confirmCli.d.ts +9 -0
- package/dist/assessments/confirmCli.js +26 -0
- package/dist/config.d.ts +22 -1
- package/dist/config.js +156 -3
- package/dist/direct/directAudit.d.ts +23 -0
- package/dist/direct/directAudit.js +309 -0
- package/dist/direct/directGuards.d.ts +20 -0
- package/dist/direct/directGuards.js +137 -0
- package/dist/direct/directPatch.d.ts +17 -0
- package/dist/direct/directPatch.js +113 -0
- package/dist/direct/directSessionStore.d.ts +63 -0
- package/dist/direct/directSessionStore.js +192 -0
- package/dist/direct/directVerification.d.ts +12 -0
- package/dist/direct/directVerification.js +96 -0
- package/dist/doctor.js +73 -5
- package/dist/logging.d.ts +52 -0
- package/dist/logging.js +123 -0
- package/dist/runner/agentInvocation.d.ts +23 -0
- package/dist/runner/agentInvocation.js +106 -0
- package/dist/runner/changeCapture.d.ts +70 -0
- package/dist/runner/changeCapture.js +278 -6
- package/dist/runner/runTask.js +271 -65
- package/dist/runner/simpleProcess.d.ts +21 -0
- package/dist/runner/simpleProcess.js +206 -0
- package/dist/runner/watch.js +17 -2
- package/dist/security/commandGuard.d.ts +2 -1
- package/dist/security/commandGuard.js +25 -3
- package/dist/security/riskEngine.d.ts +27 -0
- package/dist/security/riskEngine.js +118 -0
- package/dist/security/runtimeGuard.d.ts +6 -0
- package/dist/security/runtimeGuard.js +28 -0
- package/dist/smoke-test.js +1353 -7
- package/dist/test/unit/android-doctor.test.d.ts +1 -0
- package/dist/test/unit/android-doctor.test.js +118 -0
- package/dist/test/unit/chinese-path.test.d.ts +1 -0
- package/dist/test/unit/chinese-path.test.js +91 -0
- package/dist/test/unit/command-guard.test.d.ts +1 -0
- package/dist/test/unit/command-guard.test.js +160 -0
- package/dist/test/unit/direct-guards.test.d.ts +1 -0
- package/dist/test/unit/direct-guards.test.js +213 -0
- package/dist/test/unit/logging.test.d.ts +1 -0
- package/dist/test/unit/logging.test.js +275 -0
- package/dist/test/unit/path-guard.test.d.ts +1 -0
- package/dist/test/unit/path-guard.test.js +109 -0
- package/dist/test/unit/safe-status.test.d.ts +1 -0
- package/dist/test/unit/safe-status.test.js +165 -0
- package/dist/test/unit/sensitive-guard.test.d.ts +1 -0
- package/dist/test/unit/sensitive-guard.test.js +104 -0
- package/dist/test/unit/sync-file.test.d.ts +1 -0
- package/dist/test/unit/sync-file.test.js +154 -0
- package/dist/test/unit/watcher-status.test.d.ts +1 -0
- package/dist/test/unit/watcher-status.test.js +169 -0
- package/dist/tools/androidDoctor.d.ts +38 -0
- package/dist/tools/androidDoctor.js +391 -0
- package/dist/tools/applyPatch.d.ts +16 -0
- package/dist/tools/applyPatch.js +41 -0
- package/dist/tools/auditSession.d.ts +5 -0
- package/dist/tools/auditSession.js +12 -0
- package/dist/tools/auditTask.d.ts +7 -0
- package/dist/tools/auditTask.js +105 -7
- package/dist/tools/createDirectSession.d.ts +14 -0
- package/dist/tools/createDirectSession.js +54 -0
- package/dist/tools/createTask.d.ts +48 -1
- package/dist/tools/createTask.js +298 -47
- package/dist/tools/finalizeDirectSession.d.ts +19 -0
- package/dist/tools/finalizeDirectSession.js +84 -0
- package/dist/tools/getTaskSummary.d.ts +50 -0
- package/dist/tools/getTaskSummary.js +79 -2
- package/dist/tools/healthCheck.d.ts +27 -12
- package/dist/tools/healthCheck.js +44 -6
- package/dist/tools/readWorkspaceFile.d.ts +7 -1
- package/dist/tools/readWorkspaceFile.js +48 -2
- package/dist/tools/registry.js +270 -19
- package/dist/tools/runVerification.d.ts +16 -0
- package/dist/tools/runVerification.js +32 -0
- package/dist/tools/safeStatus.d.ts +19 -0
- package/dist/tools/safeStatus.js +72 -0
- package/dist/tools/savePlan.d.ts +1 -0
- package/dist/tools/savePlan.js +38 -7
- package/dist/tools/searchWorkspace.d.ts +19 -0
- package/dist/tools/searchWorkspace.js +205 -0
- package/dist/tools/syncFile.d.ts +18 -0
- package/dist/tools/syncFile.js +65 -0
- package/dist/tools/taskOutputs.d.ts +2 -2
- package/dist/tools/taskTemplates.js +2 -1
- package/dist/tools/toolCatalog.d.ts +4 -3
- package/dist/tools/toolCatalog.js +34 -5
- package/dist/tools/waitForTask.d.ts +2 -2
- package/dist/tools/waitForTask.js +1 -1
- package/dist/version.d.ts +2 -2
- package/dist/version.js +2 -2
- package/dist/watcherStatus.d.ts +1 -0
- package/dist/watcherStatus.js +96 -4
- package/docs/chatgpt-usage.md +101 -0
- package/docs/performance-notes.md +55 -0
- package/docs/release-checklist.md +14 -0
- package/docs/release-v0.6.0.md +71 -0
- package/docs/release-v0.6.1.md +75 -0
- package/examples/config.example.json +5 -0
- package/examples/openai-tunnel/README.md +11 -4
- package/examples/openai-tunnel/chatgpt-test-prompt.md +18 -14
- package/examples/openai-tunnel/tunnel-client.example.yaml +8 -0
- package/package.json +13 -8
- package/scripts/brand-check.js +58 -12
- package/scripts/control-smoke.js +206 -0
- package/scripts/http-mcp-smoke.js +10 -2
- package/scripts/launchers/Check-PatchWarden-Health.cmd +6 -0
- package/scripts/launchers/Reset-PatchWarden-Tunnel-Key.cmd +6 -0
- package/scripts/launchers/Restart-PatchWarden.cmd +6 -0
- package/scripts/launchers/Start-PatchWarden-Direct-Tunnel.cmd +7 -0
- package/scripts/launchers/Start-PatchWarden-Tunnel.cmd +7 -0
- package/scripts/lifecycle-smoke.js +402 -8
- package/scripts/manage-patchwarden.ps1 +639 -0
- package/scripts/mcp-manifest-check.js +123 -58
- package/scripts/mcp-smoke.js +160 -2
- package/scripts/pack-clean.js +160 -5
- package/scripts/package-manifest-check.js +78 -0
- package/scripts/patchwarden-mcp-direct.cmd +7 -0
- package/scripts/patchwarden-mcp-stdio.cmd +1 -1
- package/scripts/restart-patchwarden.ps1 +5 -4
- package/scripts/start-patchwarden-tunnel.ps1 +261 -30
- package/scripts/tunnel-supervisor-smoke.js +36 -4
- package/scripts/unit-tests.js +36 -0
- package/scripts/watcher-supervisor-smoke.js +10 -6
- package/src/assessments/agentAssessor.ts +324 -0
- package/src/assessments/assessmentStore.ts +426 -0
- package/src/assessments/confirmCli.ts +29 -0
- package/src/config.ts +178 -4
- package/src/direct/directAudit.ts +400 -0
- package/src/direct/directGuards.ts +279 -0
- package/src/direct/directPatch.ts +258 -0
- package/src/direct/directSessionStore.ts +345 -0
- package/src/direct/directVerification.ts +138 -0
- package/src/doctor.ts +103 -7
- package/src/logging.ts +152 -0
- package/src/runner/agentInvocation.ts +125 -0
- package/src/runner/changeCapture.ts +352 -6
- package/src/runner/runTask.ts +279 -63
- package/src/runner/simpleProcess.ts +223 -0
- package/src/runner/watch.ts +18 -2
- package/src/security/commandGuard.ts +46 -4
- package/src/security/riskEngine.ts +160 -0
- package/src/security/runtimeGuard.ts +41 -0
- package/src/smoke-test.ts +1291 -4
- package/src/test/unit/android-doctor.test.ts +158 -0
- package/src/test/unit/chinese-path.test.ts +106 -0
- package/src/test/unit/command-guard.test.ts +221 -0
- package/src/test/unit/direct-guards.test.ts +297 -0
- package/src/test/unit/logging.test.ts +325 -0
- package/src/test/unit/path-guard.test.ts +150 -0
- package/src/test/unit/safe-status.test.ts +187 -0
- package/src/test/unit/sensitive-guard.test.ts +124 -0
- package/src/test/unit/sync-file.test.ts +231 -0
- package/src/test/unit/watcher-status.test.ts +190 -0
- package/src/tools/androidDoctor.ts +424 -0
- package/src/tools/applyPatch.ts +86 -0
- package/src/tools/auditSession.ts +28 -0
- package/src/tools/auditTask.ts +111 -7
- package/src/tools/createDirectSession.ts +113 -0
- package/src/tools/createTask.ts +405 -55
- package/src/tools/finalizeDirectSession.ts +144 -0
- package/src/tools/getTaskSummary.ts +133 -2
- package/src/tools/healthCheck.ts +45 -6
- package/src/tools/readWorkspaceFile.ts +85 -2
- package/src/tools/registry.ts +305 -19
- package/src/tools/runVerification.ts +58 -0
- package/src/tools/safeStatus.ts +96 -0
- package/src/tools/savePlan.ts +57 -7
- package/src/tools/searchWorkspace.ts +275 -0
- package/src/tools/syncFile.ts +122 -0
- package/src/tools/taskTemplates.ts +2 -1
- package/src/tools/toolCatalog.ts +37 -6
- package/src/tools/waitForTask.ts +3 -3
- package/src/version.ts +2 -2
- package/src/watcherStatus.ts +101 -4
- package/tsconfig.json +18 -17
- package/Check-PatchWarden-Health.cmd +0 -6
- package/Reset-PatchWarden-Tunnel-Key.cmd +0 -6
- package/Restart-PatchWarden.cmd +0 -19
- package/Start-PatchWarden-Tunnel.cmd +0 -7
- package/docs/release-v0.3.0.md +0 -43
- package/docs/release-v0.4.0.md +0 -74
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { lstatSync, readFileSync, readdirSync, writeFileSync, } from "node:fs";
|
|
3
|
-
import { join, relative, resolve } from "node:path";
|
|
3
|
+
import { join, relative, resolve, isAbsolute } from "node:path";
|
|
4
4
|
import { spawnSync } from "node:child_process";
|
|
5
5
|
import { isSensitivePath } from "../security/sensitiveGuard.js";
|
|
6
6
|
const MAX_HASH_BYTES = 5 * 1024 * 1024;
|
|
@@ -13,11 +13,51 @@ export function captureRepoSnapshot(repoPath) {
|
|
|
13
13
|
let head = null;
|
|
14
14
|
let status = "";
|
|
15
15
|
let paths = [];
|
|
16
|
+
const trackedPaths = new Set();
|
|
17
|
+
const ignoredPaths = new Set();
|
|
18
|
+
const dirtyPaths = new Set();
|
|
16
19
|
if (isGit) {
|
|
17
20
|
const headResult = runGit(repoPath, ["rev-parse", "HEAD"]);
|
|
18
21
|
if (headResult.status === 0)
|
|
19
22
|
head = headResult.stdout.trim() || null;
|
|
20
23
|
status = runGit(repoPath, ["status", "--porcelain=v1", "-uall"]).stdout.trimEnd();
|
|
24
|
+
// Parse git status --porcelain to collect all dirty paths
|
|
25
|
+
for (const line of status.split("\n")) {
|
|
26
|
+
if (line.length < 4)
|
|
27
|
+
continue;
|
|
28
|
+
const st = line.slice(0, 2); // XY status codes
|
|
29
|
+
const rawPath = line.slice(3);
|
|
30
|
+
// M=modified, A=added, D=deleted, ?=untracked, R=renamed, !=ignored
|
|
31
|
+
if (/[MAD\?R]/.test(st)) {
|
|
32
|
+
if (st.includes("R")) {
|
|
33
|
+
// Rename: rawPath is "oldname -> newname"
|
|
34
|
+
const parts = rawPath.split(" -> ");
|
|
35
|
+
if (parts.length === 2) {
|
|
36
|
+
dirtyPaths.add(normalizePath(parts[0]));
|
|
37
|
+
dirtyPaths.add(normalizePath(parts[1]));
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
dirtyPaths.add(normalizePath(rawPath));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
dirtyPaths.add(normalizePath(rawPath));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const tracked = runGit(repoPath, ["ls-files", "-z"]);
|
|
49
|
+
if (tracked.status === 0) {
|
|
50
|
+
for (const path of tracked.stdout.split("\0").filter(Boolean))
|
|
51
|
+
trackedPaths.add(normalizePath(path));
|
|
52
|
+
}
|
|
53
|
+
const ignored = runGit(repoPath, ["ls-files", "-o", "-i", "--exclude-standard", "-z"]);
|
|
54
|
+
if (ignored.status === 0) {
|
|
55
|
+
for (const path of ignored.stdout.split("\0").filter(Boolean))
|
|
56
|
+
ignoredPaths.add(normalizePath(path));
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
warnings.push("git ignored-file discovery failed; ignored classification may be incomplete");
|
|
60
|
+
}
|
|
21
61
|
const listed = runGit(repoPath, ["ls-files", "-co", "--exclude-standard", "-z"]);
|
|
22
62
|
if (listed.status === 0) {
|
|
23
63
|
paths = [...new Set([
|
|
@@ -40,7 +80,7 @@ export function captureRepoSnapshot(repoPath) {
|
|
|
40
80
|
}
|
|
41
81
|
const files = {};
|
|
42
82
|
for (const inputPath of paths.sort()) {
|
|
43
|
-
const normalized = inputPath
|
|
83
|
+
const normalized = normalizePath(inputPath);
|
|
44
84
|
if (!normalized || normalized.startsWith(".patchwarden/") || isSensitivePath(normalized))
|
|
45
85
|
continue;
|
|
46
86
|
const absolutePath = resolve(repoPath, inputPath);
|
|
@@ -51,7 +91,12 @@ export function captureRepoSnapshot(repoPath) {
|
|
|
51
91
|
const sha256 = stat.size <= MAX_HASH_BYTES
|
|
52
92
|
? createHash("sha256").update(readFileSync(absolutePath)).digest("hex")
|
|
53
93
|
: `large-file:${stat.size}:${Math.trunc(stat.mtimeMs)}`;
|
|
54
|
-
files[normalized] = {
|
|
94
|
+
files[normalized] = {
|
|
95
|
+
size: stat.size,
|
|
96
|
+
sha256,
|
|
97
|
+
tracked: trackedPaths.has(normalized),
|
|
98
|
+
ignored: !trackedPaths.has(normalized) && ignoredPaths.has(normalized),
|
|
99
|
+
};
|
|
55
100
|
}
|
|
56
101
|
catch {
|
|
57
102
|
warnings.push(`could not fingerprint: ${normalized}`);
|
|
@@ -64,6 +109,7 @@ export function captureRepoSnapshot(repoPath) {
|
|
|
64
109
|
status,
|
|
65
110
|
workspace_dirty: status.trim().length > 0,
|
|
66
111
|
files,
|
|
112
|
+
dirty_paths: [...dirtyPaths],
|
|
67
113
|
warnings,
|
|
68
114
|
};
|
|
69
115
|
}
|
|
@@ -72,6 +118,7 @@ export function writeSnapshot(taskDir, filename, snapshot) {
|
|
|
72
118
|
}
|
|
73
119
|
export function buildChangeArtifacts(repoPath, before, after) {
|
|
74
120
|
const changedFiles = compareSnapshots(before, after);
|
|
121
|
+
const artifactHygiene = classifyArtifactHygiene(changedFiles);
|
|
75
122
|
const sections = [];
|
|
76
123
|
const scopedPaths = [...new Set(changedFiles.flatMap((file) => file.old_path ? [file.old_path, file.path] : [file.path]))];
|
|
77
124
|
if (before.is_git && after.is_git && scopedPaths.length > 0) {
|
|
@@ -122,6 +169,7 @@ export function buildChangeArtifacts(repoPath, before, after) {
|
|
|
122
169
|
? "Git could not produce a textual patch for the changed files; hash evidence remains available."
|
|
123
170
|
: "Repository is not a Git worktree; only bounded hash evidence is available.")
|
|
124
171
|
: null,
|
|
172
|
+
artifact_hygiene: artifactHygiene,
|
|
125
173
|
};
|
|
126
174
|
}
|
|
127
175
|
function buildFileStats(repoPath, before, after, changedFiles) {
|
|
@@ -168,13 +216,13 @@ export function compareSnapshots(before, after) {
|
|
|
168
216
|
const left = before.files[path];
|
|
169
217
|
const right = after.files[path];
|
|
170
218
|
if (!left && right) {
|
|
171
|
-
changed.push(
|
|
219
|
+
changed.push(classifyChangedFile(path, "added", null, right));
|
|
172
220
|
}
|
|
173
221
|
else if (left && !right) {
|
|
174
|
-
changed.push(
|
|
222
|
+
changed.push(classifyChangedFile(path, "deleted", left, null));
|
|
175
223
|
}
|
|
176
224
|
else if (left.sha256 !== right.sha256) {
|
|
177
|
-
changed.push(
|
|
225
|
+
changed.push(classifyChangedFile(path, "modified", left, right));
|
|
178
226
|
}
|
|
179
227
|
}
|
|
180
228
|
const deletedByHash = new Map();
|
|
@@ -198,11 +246,235 @@ export function compareSnapshots(before, after) {
|
|
|
198
246
|
change: "renamed",
|
|
199
247
|
before_sha256: source.before_sha256,
|
|
200
248
|
after_sha256: file.after_sha256,
|
|
249
|
+
tracked: file.tracked || source.tracked,
|
|
250
|
+
ignored: file.ignored,
|
|
251
|
+
kind: classifyPathKind(file.path),
|
|
201
252
|
});
|
|
202
253
|
}
|
|
203
254
|
return [...changed.filter((item) => !consumed.has(item)), ...renamed]
|
|
204
255
|
.sort((left, right) => left.path.localeCompare(right.path));
|
|
205
256
|
}
|
|
257
|
+
export function emptyArtifactHygiene() {
|
|
258
|
+
return {
|
|
259
|
+
counts: {
|
|
260
|
+
source_changes: 0,
|
|
261
|
+
tracked_build_artifacts: 0,
|
|
262
|
+
ignored_untracked_artifacts: 0,
|
|
263
|
+
runtime_generated_files: 0,
|
|
264
|
+
suspicious_changes: 0,
|
|
265
|
+
},
|
|
266
|
+
source_changes: [],
|
|
267
|
+
tracked_build_artifacts: [],
|
|
268
|
+
ignored_untracked_artifacts: [],
|
|
269
|
+
runtime_generated_files: [],
|
|
270
|
+
suspicious_changes: [],
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Extract files that are dirty in the workspace but outside the target repo.
|
|
275
|
+
* Used to establish a baseline before task execution.
|
|
276
|
+
*/
|
|
277
|
+
export function extractExternalDirtyFiles(workspaceSnapshot, repoPath, workspaceRoot) {
|
|
278
|
+
const dirtyFiles = [];
|
|
279
|
+
const dirtyPathSet = new Set(workspaceSnapshot.dirty_paths);
|
|
280
|
+
for (const [path, fingerprint] of Object.entries(workspaceSnapshot.files)) {
|
|
281
|
+
const absolutePath = resolve(workspaceRoot, path);
|
|
282
|
+
const rel = relative(repoPath, absolutePath);
|
|
283
|
+
// If the path is outside repoPath (starts with .. or is absolute)
|
|
284
|
+
if (rel.startsWith("..") || isAbsolute(rel)) {
|
|
285
|
+
// A file is "external dirty" if:
|
|
286
|
+
// 1. Git reports it as dirty (modified/added/deleted/untracked) via dirty_paths, OR
|
|
287
|
+
// 2. It's not tracked by git (untracked file), OR
|
|
288
|
+
// 3. It's explicitly ignored
|
|
289
|
+
const isDirty = dirtyPathSet.has(path);
|
|
290
|
+
const isUntracked = !fingerprint.tracked;
|
|
291
|
+
const isIgnored = fingerprint.ignored;
|
|
292
|
+
if (isDirty || isUntracked || isIgnored) {
|
|
293
|
+
dirtyFiles.push({
|
|
294
|
+
path,
|
|
295
|
+
change: isDirty ? "modified" : "added",
|
|
296
|
+
before_sha256: fingerprint.sha256,
|
|
297
|
+
after_sha256: null,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return dirtyFiles;
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Compare external dirty files between baseline and post-task snapshots.
|
|
306
|
+
* Returns files that are NEW (not present in baseline) or CHANGED
|
|
307
|
+
* (same path but different sha256, meaning the task modified them).
|
|
308
|
+
*/
|
|
309
|
+
export function findNewExternalDirtyFiles(baseline, current) {
|
|
310
|
+
const baselineMap = new Map(baseline.map((f) => [f.path, f]));
|
|
311
|
+
return current.filter((f) => {
|
|
312
|
+
const baselineFile = baselineMap.get(f.path);
|
|
313
|
+
if (!baselineFile)
|
|
314
|
+
return true; // New path — definitely new
|
|
315
|
+
// Same path but content changed during task execution
|
|
316
|
+
if (baselineFile.before_sha256 !== f.before_sha256)
|
|
317
|
+
return true;
|
|
318
|
+
return false;
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
export function buildArtifactManifest(changedFiles, repoPath, taskId) {
|
|
322
|
+
const entries = [];
|
|
323
|
+
for (const file of changedFiles) {
|
|
324
|
+
if (file.kind !== "build_artifact")
|
|
325
|
+
continue;
|
|
326
|
+
const absolutePath = resolve(repoPath, file.path);
|
|
327
|
+
let size = 0;
|
|
328
|
+
let sha256 = file.after_sha256 || "unknown";
|
|
329
|
+
try {
|
|
330
|
+
const stat = lstatSync(absolutePath);
|
|
331
|
+
if (stat.isFile()) {
|
|
332
|
+
size = stat.size;
|
|
333
|
+
if (size <= MAX_HASH_BYTES) {
|
|
334
|
+
sha256 = createHash("sha256").update(readFileSync(absolutePath)).digest("hex");
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
catch {
|
|
339
|
+
// File may have been deleted
|
|
340
|
+
}
|
|
341
|
+
entries.push({
|
|
342
|
+
path: file.path,
|
|
343
|
+
type: classifyArtifactType(file.path),
|
|
344
|
+
size,
|
|
345
|
+
sha256,
|
|
346
|
+
generated_by: "task_execution",
|
|
347
|
+
created_at: new Date().toISOString(),
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
return {
|
|
351
|
+
task_id: taskId || null,
|
|
352
|
+
generated_at: new Date().toISOString(),
|
|
353
|
+
artifacts: entries,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
function classifyArtifactType(path) {
|
|
357
|
+
const normalized = normalizePath(path).toLowerCase();
|
|
358
|
+
const basename = normalized.split("/").pop() || "";
|
|
359
|
+
if (basename.endsWith(".exe"))
|
|
360
|
+
return "windows_exe";
|
|
361
|
+
if (basename.endsWith(".apk"))
|
|
362
|
+
return "android_apk";
|
|
363
|
+
if (basename.endsWith(".zip"))
|
|
364
|
+
return "zip";
|
|
365
|
+
if (basename.endsWith(".asar"))
|
|
366
|
+
return "asar";
|
|
367
|
+
if (basename.endsWith(".dll"))
|
|
368
|
+
return "dll";
|
|
369
|
+
if (basename.endsWith(".pak"))
|
|
370
|
+
return "pak";
|
|
371
|
+
return "release_directory_file";
|
|
372
|
+
}
|
|
373
|
+
export function groupChangedFiles(changedFiles) {
|
|
374
|
+
const groups = {
|
|
375
|
+
source_changes: [],
|
|
376
|
+
docs_changes: [],
|
|
377
|
+
config_changes: [],
|
|
378
|
+
test_changes: [],
|
|
379
|
+
release_artifacts: [],
|
|
380
|
+
runtime_generated_files: [],
|
|
381
|
+
};
|
|
382
|
+
for (const file of changedFiles) {
|
|
383
|
+
const normalized = normalizePath(file.path).toLowerCase();
|
|
384
|
+
const parts = normalized.split("/");
|
|
385
|
+
const basename = parts[parts.length - 1] || "";
|
|
386
|
+
// Check for docs
|
|
387
|
+
if (parts.some((p) => p === "docs") || /\.(md|rst|txt)$/.test(basename)) {
|
|
388
|
+
groups.docs_changes.push(file);
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
// Check for config
|
|
392
|
+
if (basename === "package.json" || basename === "tsconfig.json" || basename === ".gitignore" ||
|
|
393
|
+
basename.startsWith(".config") || basename.endsWith(".config.js") || basename.endsWith(".config.ts")) {
|
|
394
|
+
groups.config_changes.push(file);
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
// Check for test files
|
|
398
|
+
if (basename.includes(".test.") || basename.includes(".spec.") || parts.some((p) => p === "test" || p === "tests" || p === "__tests__")) {
|
|
399
|
+
groups.test_changes.push(file);
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
// Check for build artifacts / release
|
|
403
|
+
if (file.kind === "build_artifact") {
|
|
404
|
+
groups.release_artifacts.push(file);
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
// Check for runtime generated
|
|
408
|
+
if (file.kind === "runtime_generated") {
|
|
409
|
+
groups.runtime_generated_files.push(file);
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
// Default: source changes
|
|
413
|
+
groups.source_changes.push(file);
|
|
414
|
+
}
|
|
415
|
+
return groups;
|
|
416
|
+
}
|
|
417
|
+
function classifyChangedFile(path, change, before, after) {
|
|
418
|
+
return {
|
|
419
|
+
path,
|
|
420
|
+
change,
|
|
421
|
+
before_sha256: before?.sha256 || null,
|
|
422
|
+
after_sha256: after?.sha256 || null,
|
|
423
|
+
tracked: Boolean(after?.tracked || before?.tracked),
|
|
424
|
+
ignored: Boolean(after?.ignored ?? before?.ignored),
|
|
425
|
+
kind: classifyPathKind(path),
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
function classifyArtifactHygiene(changes) {
|
|
429
|
+
const hygiene = emptyArtifactHygiene();
|
|
430
|
+
const entries = changes.map((change) => ({
|
|
431
|
+
path: change.path,
|
|
432
|
+
change: change.change,
|
|
433
|
+
tracked: change.tracked,
|
|
434
|
+
ignored: change.ignored,
|
|
435
|
+
kind: change.kind,
|
|
436
|
+
reason: classificationReason(change),
|
|
437
|
+
}));
|
|
438
|
+
hygiene.source_changes = entries.filter((entry) => entry.kind === "source" && !entry.ignored);
|
|
439
|
+
hygiene.tracked_build_artifacts = entries.filter((entry) => entry.kind === "build_artifact" && entry.tracked);
|
|
440
|
+
hygiene.ignored_untracked_artifacts = entries.filter((entry) => entry.ignored && !entry.tracked);
|
|
441
|
+
hygiene.runtime_generated_files = entries.filter((entry) => entry.kind === "runtime_generated");
|
|
442
|
+
hygiene.suspicious_changes = entries.filter((entry) => (entry.kind === "build_artifact" || entry.kind === "runtime_generated") && !entry.ignored);
|
|
443
|
+
hygiene.counts = {
|
|
444
|
+
source_changes: hygiene.source_changes.length,
|
|
445
|
+
tracked_build_artifacts: hygiene.tracked_build_artifacts.length,
|
|
446
|
+
ignored_untracked_artifacts: hygiene.ignored_untracked_artifacts.length,
|
|
447
|
+
runtime_generated_files: hygiene.runtime_generated_files.length,
|
|
448
|
+
suspicious_changes: hygiene.suspicious_changes.length,
|
|
449
|
+
};
|
|
450
|
+
return hygiene;
|
|
451
|
+
}
|
|
452
|
+
function classifyPathKind(path) {
|
|
453
|
+
const normalized = normalizePath(path).toLowerCase();
|
|
454
|
+
const parts = normalized.split("/");
|
|
455
|
+
const basename = parts[parts.length - 1] || "";
|
|
456
|
+
if (basename === "sync-store.json" || /\.(log|tmp|temp|pid)$/.test(basename))
|
|
457
|
+
return "runtime_generated";
|
|
458
|
+
if (parts.some((part) => ["dist", "release", "build", "out", "coverage", ".next"].includes(part)))
|
|
459
|
+
return "build_artifact";
|
|
460
|
+
if (/\.(exe|dll|pak|bin|zip|tgz|tar\.gz)$/.test(basename))
|
|
461
|
+
return "build_artifact";
|
|
462
|
+
return "source";
|
|
463
|
+
}
|
|
464
|
+
function classificationReason(change) {
|
|
465
|
+
if (change.ignored)
|
|
466
|
+
return "untracked path is ignored by repository Git rules";
|
|
467
|
+
if (change.kind === "build_artifact" && change.tracked)
|
|
468
|
+
return "artifact-like path is tracked by Git and requires review";
|
|
469
|
+
if (change.kind === "build_artifact")
|
|
470
|
+
return "artifact-like path is not ignored and requires review";
|
|
471
|
+
if (change.kind === "runtime_generated")
|
|
472
|
+
return "runtime-generated path is not ignored and requires review";
|
|
473
|
+
return change.tracked ? "tracked source change" : "untracked source change";
|
|
474
|
+
}
|
|
475
|
+
function normalizePath(value) {
|
|
476
|
+
return value.replace(/\\/g, "/");
|
|
477
|
+
}
|
|
206
478
|
function walkWorkspace(root) {
|
|
207
479
|
const result = [];
|
|
208
480
|
const visit = (directory) => {
|