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.
Files changed (189) hide show
  1. package/PatchWarden.cmd +51 -0
  2. package/README.en.md +1022 -0
  3. package/README.md +897 -358
  4. package/dist/assessments/agentAssessor.d.ts +15 -0
  5. package/dist/assessments/agentAssessor.js +293 -0
  6. package/dist/assessments/assessmentStore.d.ts +133 -0
  7. package/dist/assessments/assessmentStore.js +238 -0
  8. package/dist/assessments/confirmCli.d.ts +9 -0
  9. package/dist/assessments/confirmCli.js +26 -0
  10. package/dist/config.d.ts +22 -1
  11. package/dist/config.js +156 -3
  12. package/dist/direct/directAudit.d.ts +23 -0
  13. package/dist/direct/directAudit.js +309 -0
  14. package/dist/direct/directGuards.d.ts +20 -0
  15. package/dist/direct/directGuards.js +137 -0
  16. package/dist/direct/directPatch.d.ts +17 -0
  17. package/dist/direct/directPatch.js +113 -0
  18. package/dist/direct/directSessionStore.d.ts +63 -0
  19. package/dist/direct/directSessionStore.js +192 -0
  20. package/dist/direct/directVerification.d.ts +12 -0
  21. package/dist/direct/directVerification.js +96 -0
  22. package/dist/doctor.js +73 -5
  23. package/dist/logging.d.ts +52 -0
  24. package/dist/logging.js +123 -0
  25. package/dist/runner/agentInvocation.d.ts +23 -0
  26. package/dist/runner/agentInvocation.js +106 -0
  27. package/dist/runner/changeCapture.d.ts +70 -0
  28. package/dist/runner/changeCapture.js +278 -6
  29. package/dist/runner/runTask.js +271 -65
  30. package/dist/runner/simpleProcess.d.ts +21 -0
  31. package/dist/runner/simpleProcess.js +206 -0
  32. package/dist/runner/watch.js +17 -2
  33. package/dist/security/commandGuard.d.ts +2 -1
  34. package/dist/security/commandGuard.js +25 -3
  35. package/dist/security/riskEngine.d.ts +27 -0
  36. package/dist/security/riskEngine.js +118 -0
  37. package/dist/security/runtimeGuard.d.ts +6 -0
  38. package/dist/security/runtimeGuard.js +28 -0
  39. package/dist/smoke-test.js +1353 -7
  40. package/dist/test/unit/android-doctor.test.d.ts +1 -0
  41. package/dist/test/unit/android-doctor.test.js +118 -0
  42. package/dist/test/unit/chinese-path.test.d.ts +1 -0
  43. package/dist/test/unit/chinese-path.test.js +91 -0
  44. package/dist/test/unit/command-guard.test.d.ts +1 -0
  45. package/dist/test/unit/command-guard.test.js +160 -0
  46. package/dist/test/unit/direct-guards.test.d.ts +1 -0
  47. package/dist/test/unit/direct-guards.test.js +213 -0
  48. package/dist/test/unit/logging.test.d.ts +1 -0
  49. package/dist/test/unit/logging.test.js +275 -0
  50. package/dist/test/unit/path-guard.test.d.ts +1 -0
  51. package/dist/test/unit/path-guard.test.js +109 -0
  52. package/dist/test/unit/safe-status.test.d.ts +1 -0
  53. package/dist/test/unit/safe-status.test.js +165 -0
  54. package/dist/test/unit/sensitive-guard.test.d.ts +1 -0
  55. package/dist/test/unit/sensitive-guard.test.js +104 -0
  56. package/dist/test/unit/sync-file.test.d.ts +1 -0
  57. package/dist/test/unit/sync-file.test.js +154 -0
  58. package/dist/test/unit/watcher-status.test.d.ts +1 -0
  59. package/dist/test/unit/watcher-status.test.js +169 -0
  60. package/dist/tools/androidDoctor.d.ts +38 -0
  61. package/dist/tools/androidDoctor.js +391 -0
  62. package/dist/tools/applyPatch.d.ts +16 -0
  63. package/dist/tools/applyPatch.js +41 -0
  64. package/dist/tools/auditSession.d.ts +5 -0
  65. package/dist/tools/auditSession.js +12 -0
  66. package/dist/tools/auditTask.d.ts +7 -0
  67. package/dist/tools/auditTask.js +105 -7
  68. package/dist/tools/createDirectSession.d.ts +14 -0
  69. package/dist/tools/createDirectSession.js +54 -0
  70. package/dist/tools/createTask.d.ts +48 -1
  71. package/dist/tools/createTask.js +298 -47
  72. package/dist/tools/finalizeDirectSession.d.ts +19 -0
  73. package/dist/tools/finalizeDirectSession.js +84 -0
  74. package/dist/tools/getTaskSummary.d.ts +50 -0
  75. package/dist/tools/getTaskSummary.js +79 -2
  76. package/dist/tools/healthCheck.d.ts +27 -12
  77. package/dist/tools/healthCheck.js +44 -6
  78. package/dist/tools/readWorkspaceFile.d.ts +7 -1
  79. package/dist/tools/readWorkspaceFile.js +48 -2
  80. package/dist/tools/registry.js +270 -19
  81. package/dist/tools/runVerification.d.ts +16 -0
  82. package/dist/tools/runVerification.js +32 -0
  83. package/dist/tools/safeStatus.d.ts +19 -0
  84. package/dist/tools/safeStatus.js +72 -0
  85. package/dist/tools/savePlan.d.ts +1 -0
  86. package/dist/tools/savePlan.js +38 -7
  87. package/dist/tools/searchWorkspace.d.ts +19 -0
  88. package/dist/tools/searchWorkspace.js +205 -0
  89. package/dist/tools/syncFile.d.ts +18 -0
  90. package/dist/tools/syncFile.js +65 -0
  91. package/dist/tools/taskOutputs.d.ts +2 -2
  92. package/dist/tools/taskTemplates.js +2 -1
  93. package/dist/tools/toolCatalog.d.ts +4 -3
  94. package/dist/tools/toolCatalog.js +34 -5
  95. package/dist/tools/waitForTask.d.ts +2 -2
  96. package/dist/tools/waitForTask.js +1 -1
  97. package/dist/version.d.ts +2 -2
  98. package/dist/version.js +2 -2
  99. package/dist/watcherStatus.d.ts +1 -0
  100. package/dist/watcherStatus.js +96 -4
  101. package/docs/chatgpt-usage.md +101 -0
  102. package/docs/performance-notes.md +55 -0
  103. package/docs/release-checklist.md +14 -0
  104. package/docs/release-v0.6.0.md +71 -0
  105. package/docs/release-v0.6.1.md +75 -0
  106. package/examples/config.example.json +5 -0
  107. package/examples/openai-tunnel/README.md +11 -4
  108. package/examples/openai-tunnel/chatgpt-test-prompt.md +18 -14
  109. package/examples/openai-tunnel/tunnel-client.example.yaml +8 -0
  110. package/package.json +13 -8
  111. package/scripts/brand-check.js +58 -12
  112. package/scripts/control-smoke.js +206 -0
  113. package/scripts/http-mcp-smoke.js +10 -2
  114. package/scripts/launchers/Check-PatchWarden-Health.cmd +6 -0
  115. package/scripts/launchers/Reset-PatchWarden-Tunnel-Key.cmd +6 -0
  116. package/scripts/launchers/Restart-PatchWarden.cmd +6 -0
  117. package/scripts/launchers/Start-PatchWarden-Direct-Tunnel.cmd +7 -0
  118. package/scripts/launchers/Start-PatchWarden-Tunnel.cmd +7 -0
  119. package/scripts/lifecycle-smoke.js +402 -8
  120. package/scripts/manage-patchwarden.ps1 +639 -0
  121. package/scripts/mcp-manifest-check.js +123 -58
  122. package/scripts/mcp-smoke.js +160 -2
  123. package/scripts/pack-clean.js +160 -5
  124. package/scripts/package-manifest-check.js +78 -0
  125. package/scripts/patchwarden-mcp-direct.cmd +7 -0
  126. package/scripts/patchwarden-mcp-stdio.cmd +1 -1
  127. package/scripts/restart-patchwarden.ps1 +5 -4
  128. package/scripts/start-patchwarden-tunnel.ps1 +261 -30
  129. package/scripts/tunnel-supervisor-smoke.js +36 -4
  130. package/scripts/unit-tests.js +36 -0
  131. package/scripts/watcher-supervisor-smoke.js +10 -6
  132. package/src/assessments/agentAssessor.ts +324 -0
  133. package/src/assessments/assessmentStore.ts +426 -0
  134. package/src/assessments/confirmCli.ts +29 -0
  135. package/src/config.ts +178 -4
  136. package/src/direct/directAudit.ts +400 -0
  137. package/src/direct/directGuards.ts +279 -0
  138. package/src/direct/directPatch.ts +258 -0
  139. package/src/direct/directSessionStore.ts +345 -0
  140. package/src/direct/directVerification.ts +138 -0
  141. package/src/doctor.ts +103 -7
  142. package/src/logging.ts +152 -0
  143. package/src/runner/agentInvocation.ts +125 -0
  144. package/src/runner/changeCapture.ts +352 -6
  145. package/src/runner/runTask.ts +279 -63
  146. package/src/runner/simpleProcess.ts +223 -0
  147. package/src/runner/watch.ts +18 -2
  148. package/src/security/commandGuard.ts +46 -4
  149. package/src/security/riskEngine.ts +160 -0
  150. package/src/security/runtimeGuard.ts +41 -0
  151. package/src/smoke-test.ts +1291 -4
  152. package/src/test/unit/android-doctor.test.ts +158 -0
  153. package/src/test/unit/chinese-path.test.ts +106 -0
  154. package/src/test/unit/command-guard.test.ts +221 -0
  155. package/src/test/unit/direct-guards.test.ts +297 -0
  156. package/src/test/unit/logging.test.ts +325 -0
  157. package/src/test/unit/path-guard.test.ts +150 -0
  158. package/src/test/unit/safe-status.test.ts +187 -0
  159. package/src/test/unit/sensitive-guard.test.ts +124 -0
  160. package/src/test/unit/sync-file.test.ts +231 -0
  161. package/src/test/unit/watcher-status.test.ts +190 -0
  162. package/src/tools/androidDoctor.ts +424 -0
  163. package/src/tools/applyPatch.ts +86 -0
  164. package/src/tools/auditSession.ts +28 -0
  165. package/src/tools/auditTask.ts +111 -7
  166. package/src/tools/createDirectSession.ts +113 -0
  167. package/src/tools/createTask.ts +405 -55
  168. package/src/tools/finalizeDirectSession.ts +144 -0
  169. package/src/tools/getTaskSummary.ts +133 -2
  170. package/src/tools/healthCheck.ts +45 -6
  171. package/src/tools/readWorkspaceFile.ts +85 -2
  172. package/src/tools/registry.ts +305 -19
  173. package/src/tools/runVerification.ts +58 -0
  174. package/src/tools/safeStatus.ts +96 -0
  175. package/src/tools/savePlan.ts +57 -7
  176. package/src/tools/searchWorkspace.ts +275 -0
  177. package/src/tools/syncFile.ts +122 -0
  178. package/src/tools/taskTemplates.ts +2 -1
  179. package/src/tools/toolCatalog.ts +37 -6
  180. package/src/tools/waitForTask.ts +3 -3
  181. package/src/version.ts +2 -2
  182. package/src/watcherStatus.ts +101 -4
  183. package/tsconfig.json +18 -17
  184. package/Check-PatchWarden-Health.cmd +0 -6
  185. package/Reset-PatchWarden-Tunnel-Key.cmd +0 -6
  186. package/Restart-PatchWarden.cmd +0 -19
  187. package/Start-PatchWarden-Tunnel.cmd +0 -7
  188. package/docs/release-v0.3.0.md +0 -43
  189. 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.replace(/\\/g, "/");
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] = { size: stat.size, sha256 };
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({ path, change: "added", before_sha256: null, after_sha256: right.sha256 });
219
+ changed.push(classifyChangedFile(path, "added", null, right));
172
220
  }
173
221
  else if (left && !right) {
174
- changed.push({ path, change: "deleted", before_sha256: left.sha256, after_sha256: null });
222
+ changed.push(classifyChangedFile(path, "deleted", left, null));
175
223
  }
176
224
  else if (left.sha256 !== right.sha256) {
177
- changed.push({ path, change: "modified", before_sha256: left.sha256, after_sha256: right.sha256 });
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) => {