codeloop-mcp-server 0.1.50 → 0.1.52

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 (71) hide show
  1. package/dist/auth/critical_floors.d.ts.map +1 -1
  2. package/dist/auth/critical_floors.js +8 -0
  3. package/dist/auth/critical_floors.js.map +1 -1
  4. package/dist/evidence/anti_rationalisation.d.ts +34 -0
  5. package/dist/evidence/anti_rationalisation.d.ts.map +1 -0
  6. package/dist/evidence/anti_rationalisation.js +85 -0
  7. package/dist/evidence/anti_rationalisation.js.map +1 -0
  8. package/dist/evidence/change_coverage.d.ts +59 -0
  9. package/dist/evidence/change_coverage.d.ts.map +1 -0
  10. package/dist/evidence/change_coverage.js +422 -0
  11. package/dist/evidence/change_coverage.js.map +1 -0
  12. package/dist/evidence/change_manifest.d.ts +94 -0
  13. package/dist/evidence/change_manifest.d.ts.map +1 -0
  14. package/dist/evidence/change_manifest.js +830 -0
  15. package/dist/evidence/change_manifest.js.map +1 -0
  16. package/dist/evidence/loop_state.d.ts +53 -0
  17. package/dist/evidence/loop_state.d.ts.map +1 -0
  18. package/dist/evidence/loop_state.js +147 -0
  19. package/dist/evidence/loop_state.js.map +1 -0
  20. package/dist/evidence/verify_staleness.d.ts +9 -0
  21. package/dist/evidence/verify_staleness.d.ts.map +1 -0
  22. package/dist/evidence/verify_staleness.js +180 -0
  23. package/dist/evidence/verify_staleness.js.map +1 -0
  24. package/dist/index.d.ts +1 -1
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +374 -19
  27. package/dist/index.js.map +1 -1
  28. package/dist/runners/empty_state_detector.d.ts +33 -0
  29. package/dist/runners/empty_state_detector.d.ts.map +1 -0
  30. package/dist/runners/empty_state_detector.js +304 -0
  31. package/dist/runners/empty_state_detector.js.map +1 -0
  32. package/dist/runners/maestro.d.ts +13 -0
  33. package/dist/runners/maestro.d.ts.map +1 -1
  34. package/dist/runners/maestro.js +37 -1
  35. package/dist/runners/maestro.js.map +1 -1
  36. package/dist/runners/modal_detector.d.ts +60 -0
  37. package/dist/runners/modal_detector.d.ts.map +1 -0
  38. package/dist/runners/modal_detector.js +160 -0
  39. package/dist/runners/modal_detector.js.map +1 -0
  40. package/dist/runners/python_tests.d.ts +26 -0
  41. package/dist/runners/python_tests.d.ts.map +1 -0
  42. package/dist/runners/python_tests.js +181 -0
  43. package/dist/runners/python_tests.js.map +1 -0
  44. package/dist/runners/rust_tests.d.ts +28 -0
  45. package/dist/runners/rust_tests.d.ts.map +1 -0
  46. package/dist/runners/rust_tests.js +76 -0
  47. package/dist/runners/rust_tests.js.map +1 -0
  48. package/dist/tools/c7_slug.d.ts +14 -0
  49. package/dist/tools/c7_slug.d.ts.map +1 -0
  50. package/dist/tools/c7_slug.js +21 -0
  51. package/dist/tools/c7_slug.js.map +1 -0
  52. package/dist/tools/diagnose.d.ts.map +1 -1
  53. package/dist/tools/diagnose.js +13 -0
  54. package/dist/tools/diagnose.js.map +1 -1
  55. package/dist/tools/gate_check.d.ts +2 -1
  56. package/dist/tools/gate_check.d.ts.map +1 -1
  57. package/dist/tools/gate_check.js +74 -32
  58. package/dist/tools/gate_check.js.map +1 -1
  59. package/dist/tools/is_ui_project.d.ts +23 -0
  60. package/dist/tools/is_ui_project.d.ts.map +1 -0
  61. package/dist/tools/is_ui_project.js +42 -0
  62. package/dist/tools/is_ui_project.js.map +1 -0
  63. package/dist/tools/plan_change_journey.d.ts +41 -0
  64. package/dist/tools/plan_change_journey.d.ts.map +1 -0
  65. package/dist/tools/plan_change_journey.js +131 -0
  66. package/dist/tools/plan_change_journey.js.map +1 -0
  67. package/dist/tools/verify.d.ts +28 -0
  68. package/dist/tools/verify.d.ts.map +1 -1
  69. package/dist/tools/verify.js +272 -8
  70. package/dist/tools/verify.js.map +1 -1
  71. package/package.json +1 -1
@@ -0,0 +1,830 @@
1
+ import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync, } from "fs";
2
+ import { dirname, isAbsolute, join, relative, resolve as resolvePath, sep, posix } from "path";
3
+ import { execSync } from "child_process";
4
+ import { homedir } from "os";
5
+ import { readLoopState } from "./loop_state.js";
6
+ import { listRuns, getRunDir, getArtifactsBaseDir } from "./artifacts.js";
7
+ import { loadRunIndex } from "./run_lineage.js";
8
+ /** Source-tree directories we never scan. */
9
+ const EXCLUDE_DIR_NAMES = new Set([
10
+ "node_modules",
11
+ ".git",
12
+ ".vs",
13
+ ".idea",
14
+ ".vscode",
15
+ ".codeloop",
16
+ "artifacts",
17
+ "dist",
18
+ "build",
19
+ "out",
20
+ "bin",
21
+ "obj",
22
+ ".next",
23
+ ".turbo",
24
+ ".cache",
25
+ ".gradle",
26
+ "DerivedData",
27
+ "__pycache__",
28
+ "target",
29
+ ".venv",
30
+ "venv",
31
+ "Pods",
32
+ ]);
33
+ /** File-name patterns we ignore even when their mtime is newer. */
34
+ const EXCLUDE_FILE_PATTERNS = [
35
+ /^\.DS_Store$/,
36
+ /^Thumbs\.db$/,
37
+ /^package-lock\.json$/,
38
+ /^pnpm-lock\.yaml$/,
39
+ /^yarn\.lock$/,
40
+ /^poetry\.lock$/,
41
+ /^Cargo\.lock$/,
42
+ /\.log$/,
43
+ /\.lock$/,
44
+ /\.Designer\.cs$/, // EF migration designer files are auto-generated
45
+ /\.pb\.go$/,
46
+ /\.generated\.\w+$/,
47
+ ];
48
+ /** File extensions whose contents we know how to parse for feature-shape. */
49
+ const PARSEABLE_EXTS = new Set([
50
+ ".xaml",
51
+ ".axaml",
52
+ ".html",
53
+ ".jsx",
54
+ ".tsx",
55
+ ".vue",
56
+ ".cs",
57
+ ".ts",
58
+ ".dart",
59
+ ".swift",
60
+ ".kt",
61
+ ".sql",
62
+ ]);
63
+ function gitAvailable(cwd) {
64
+ if (!existsSync(join(cwd, ".git")))
65
+ return false;
66
+ try {
67
+ execSync("git rev-parse --is-inside-work-tree", {
68
+ cwd,
69
+ stdio: "ignore",
70
+ });
71
+ return true;
72
+ }
73
+ catch {
74
+ return false;
75
+ }
76
+ }
77
+ function gitHeadSha(cwd) {
78
+ try {
79
+ return execSync("git rev-parse HEAD", { cwd, encoding: "utf-8" }).trim();
80
+ }
81
+ catch {
82
+ return null;
83
+ }
84
+ }
85
+ /**
86
+ * Git's well-known SHA for the empty tree. Diffing `EMPTY_TREE..HEAD`
87
+ * universally returns the entire HEAD tree as adds, which is what we
88
+ * want as the absolute fallback when there's no prior verify SHA AND
89
+ * no `HEAD~1` reachable (e.g. a single-commit repo on first verify).
90
+ */
91
+ const EMPTY_TREE_SHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
92
+ function gitDiffNameStatus(cwd, fromSha) {
93
+ const out = [];
94
+ // We try a sequence of ranges from "most precise" to "always-works".
95
+ // The first range that produces output wins. The key invariant: the
96
+ // function MUST return a non-empty list whenever the working tree
97
+ // contains any committed file, because the C3 gate scores against
98
+ // these entries.
99
+ const ranges = [];
100
+ if (fromSha)
101
+ ranges.push(`${fromSha}..HEAD`);
102
+ ranges.push("HEAD~1..HEAD");
103
+ ranges.push(`${EMPTY_TREE_SHA}..HEAD`);
104
+ for (const range of ranges) {
105
+ try {
106
+ const raw = execSync(`git diff --name-status ${range}`, {
107
+ cwd,
108
+ encoding: "utf-8",
109
+ stdio: ["ignore", "pipe", "ignore"],
110
+ });
111
+ for (const line of raw.split("\n")) {
112
+ const m = /^([AMDRCTU])\d*\s+(.+)$/.exec(line.trim());
113
+ if (!m)
114
+ continue;
115
+ const status = m[1];
116
+ const path = m[2].split("\t").pop() || m[2];
117
+ out.push({ status, path });
118
+ }
119
+ if (out.length > 0)
120
+ return out;
121
+ }
122
+ catch {
123
+ /* try next range */
124
+ }
125
+ }
126
+ return out;
127
+ }
128
+ function gitStatusPorcelain(cwd) {
129
+ const out = [];
130
+ try {
131
+ const raw = execSync("git status --porcelain=v1 -uall", {
132
+ cwd,
133
+ encoding: "utf-8",
134
+ stdio: ["ignore", "pipe", "ignore"],
135
+ });
136
+ for (const line of raw.split("\n")) {
137
+ if (!line)
138
+ continue;
139
+ // `XY path` where XY is the index/worktree status. Untracked is `??`.
140
+ const head = line.slice(0, 2);
141
+ const path = line.slice(3).trim();
142
+ if (!path)
143
+ continue;
144
+ let status = "M";
145
+ if (head === "??")
146
+ status = "?";
147
+ else if (head.includes("A"))
148
+ status = "A";
149
+ else if (head.includes("D"))
150
+ status = "D";
151
+ else if (head.includes("R"))
152
+ status = "R";
153
+ else if (head.includes("M"))
154
+ status = "M";
155
+ out.push({ status, path });
156
+ }
157
+ }
158
+ catch {
159
+ /* ignore */
160
+ }
161
+ return out;
162
+ }
163
+ function toFileChange(cwd, e) {
164
+ const rel = e.path.replace(/\\/g, "/");
165
+ return {
166
+ relPath: rel,
167
+ status: e.status,
168
+ absPath: join(cwd, rel.split("/").join(sep)),
169
+ };
170
+ }
171
+ function deduplicate(changes) {
172
+ const seen = new Map();
173
+ for (const c of changes) {
174
+ const existing = seen.get(c.relPath);
175
+ if (!existing) {
176
+ seen.set(c.relPath, c);
177
+ continue;
178
+ }
179
+ // Prefer A (newest authored content) > M > others, so an uncommitted
180
+ // ADD beats a committed M from earlier in the same diff range.
181
+ const rank = {
182
+ A: 5,
183
+ "?": 5,
184
+ M: 4,
185
+ R: 3,
186
+ C: 3,
187
+ T: 2,
188
+ U: 1,
189
+ D: 0,
190
+ };
191
+ if (rank[c.status] >= rank[existing.status]) {
192
+ seen.set(c.relPath, c);
193
+ }
194
+ }
195
+ return [...seen.values()];
196
+ }
197
+ function isExcluded(relPath) {
198
+ const parts = relPath.split("/");
199
+ for (const p of parts) {
200
+ if (EXCLUDE_DIR_NAMES.has(p))
201
+ return true;
202
+ }
203
+ const fname = parts[parts.length - 1] ?? "";
204
+ if (EXCLUDE_FILE_PATTERNS.some((re) => re.test(fname)))
205
+ return true;
206
+ return false;
207
+ }
208
+ function listProjectFiles(cwd, depth = 6) {
209
+ const out = [];
210
+ function walk(dir, d) {
211
+ if (d > depth)
212
+ return;
213
+ let entries;
214
+ try {
215
+ entries = readdirSync(dir, { withFileTypes: true });
216
+ }
217
+ catch {
218
+ return;
219
+ }
220
+ for (const ent of entries) {
221
+ const name = ent.name;
222
+ if (EXCLUDE_DIR_NAMES.has(name))
223
+ continue;
224
+ const full = join(dir, name);
225
+ if (ent.isDirectory()) {
226
+ walk(full, d + 1);
227
+ }
228
+ else if (ent.isFile()) {
229
+ if (EXCLUDE_FILE_PATTERNS.some((re) => re.test(name)))
230
+ continue;
231
+ out.push(full);
232
+ }
233
+ }
234
+ }
235
+ walk(cwd, 0);
236
+ return out;
237
+ }
238
+ function filesNewerThanMs(cwd, sinceMs) {
239
+ const out = [];
240
+ for (const abs of listProjectFiles(cwd)) {
241
+ try {
242
+ const st = statSync(abs);
243
+ if (st.mtimeMs > sinceMs) {
244
+ const rel = relative(cwd, abs).split(sep).join(posix.sep);
245
+ out.push({ relPath: rel, status: "M", absPath: abs });
246
+ }
247
+ }
248
+ catch {
249
+ /* skip unreadable */
250
+ }
251
+ }
252
+ return out;
253
+ }
254
+ function lastVerifyMs(cwd) {
255
+ try {
256
+ const idx = loadRunIndex(getArtifactsBaseDir(cwd));
257
+ let max = 0;
258
+ for (const e of idx.entries) {
259
+ const ts = e.finished_at ?? e.started_at;
260
+ if (!ts)
261
+ continue;
262
+ const ms = Date.parse(ts);
263
+ if (Number.isFinite(ms) && ms > max)
264
+ max = ms;
265
+ }
266
+ return max;
267
+ }
268
+ catch {
269
+ return 0;
270
+ }
271
+ }
272
+ // ─────────────────────────────────────────────────────────────────────
273
+ // Per-file parsers
274
+ // ─────────────────────────────────────────────────────────────────────
275
+ function readSafe(absPath) {
276
+ try {
277
+ return readFileSync(absPath, "utf-8");
278
+ }
279
+ catch {
280
+ return null;
281
+ }
282
+ }
283
+ function classNameForFile(filePath) {
284
+ const base = filePath.split("/").pop() ?? filePath;
285
+ const dot = base.lastIndexOf(".");
286
+ return dot > 0 ? base.slice(0, dot) : base;
287
+ }
288
+ function pushUnique(arr, entry) {
289
+ // Idempotent push — protects against "the same property" emerging
290
+ // from both the diff parser and the structural parser.
291
+ const key = JSON.stringify(entry);
292
+ for (const e of arr) {
293
+ if (JSON.stringify(e) === key)
294
+ return;
295
+ }
296
+ arr.push(entry);
297
+ }
298
+ /** Header / Content / x:Name attribute extractor for a single XAML element. */
299
+ function readXamlAttr(elementText, attr) {
300
+ const re = new RegExp(`${attr}\\s*=\\s*"([^"]+)"`);
301
+ const m = re.exec(elementText);
302
+ return m ? m[1] : null;
303
+ }
304
+ /**
305
+ * Parse XAML / AXAML for added DataGridTextColumn / DataGridCheckBoxColumn /
306
+ * Button / MenuItem / TabItem / TextBox / ComboBox / CheckBox elements.
307
+ * For added files we report every match; for modified files we diff the
308
+ * element list against the old content to surface only adds.
309
+ */
310
+ function parseXaml(filePath, newContent, oldContent, out) {
311
+ const elementRe = /<(DataGridTextColumn|DataGridCheckBoxColumn|DataGridTemplateColumn|DataGridComboBoxColumn|Button|MenuItem|TabItem|TextBox|ComboBox|CheckBox)\b[^/>]*\/?>/g;
312
+ function collectFromText(text) {
313
+ const list = [];
314
+ let m;
315
+ elementRe.lastIndex = 0;
316
+ while ((m = elementRe.exec(text))) {
317
+ const tag = m[1];
318
+ const raw = m[0];
319
+ const header = readXamlAttr(raw, "Header") ??
320
+ readXamlAttr(raw, "Content") ??
321
+ readXamlAttr(raw, "x:Name") ??
322
+ readXamlAttr(raw, "Name") ??
323
+ "";
324
+ if (!header)
325
+ continue;
326
+ list.push({ tag, name: header, raw });
327
+ }
328
+ return list;
329
+ }
330
+ const newEls = collectFromText(newContent);
331
+ const oldEls = oldContent ? collectFromText(oldContent) : [];
332
+ const oldKey = new Set(oldEls.map((e) => `${e.tag}::${e.name}`));
333
+ for (const el of newEls) {
334
+ const k = `${el.tag}::${el.name}`;
335
+ if (oldKey.has(k))
336
+ continue;
337
+ let element;
338
+ if (el.tag.startsWith("DataGrid"))
339
+ element = "datagrid_column";
340
+ else if (el.tag === "Button")
341
+ element = "button";
342
+ else if (el.tag === "MenuItem" || el.tag === "TabItem")
343
+ element = "menu_item";
344
+ else
345
+ element = "input";
346
+ pushUnique(out, {
347
+ kind: "ui_element_added",
348
+ element,
349
+ name: el.name,
350
+ file: filePath,
351
+ });
352
+ }
353
+ // Detect a top-level layout-container restructure: when the OUTERMOST
354
+ // grouping element type changes (e.g. Grid → StackPanel) at the same
355
+ // depth, surface a single layout_restructure entry. This catches the
356
+ // exact Photometry-DB buttons-below-title fix.
357
+ if (oldContent !== null) {
358
+ const newRoot = firstStructuralContainer(newContent);
359
+ const oldRoot = firstStructuralContainer(oldContent);
360
+ if (newRoot && oldRoot && newRoot !== oldRoot) {
361
+ pushUnique(out, {
362
+ kind: "layout_restructure",
363
+ file: filePath,
364
+ from: oldRoot,
365
+ to: newRoot,
366
+ });
367
+ }
368
+ }
369
+ }
370
+ function firstStructuralContainer(text) {
371
+ // Scan for the first <Grid|StackPanel|DockPanel|WrapPanel|Canvas> after
372
+ // the root document element. Used purely for layout-restructure detection.
373
+ const re = /<(Grid|StackPanel|DockPanel|WrapPanel|Canvas|Border|UniformGrid)\b/;
374
+ const m = re.exec(text);
375
+ return m ? m[1] : null;
376
+ }
377
+ function parseHtmlOrJsx(filePath, newContent, oldContent, out) {
378
+ // For HTML / JSX / TSX / Vue we look for added <button>, <a>, <input>,
379
+ // <select>, <textarea> elements with discriminating identifiers.
380
+ const elementRe = /<(button|a|input|select|textarea)\b([^>]*)>(?:([^<]{0,200}?)<\/\1>)?/gi;
381
+ function collect(text) {
382
+ const list = [];
383
+ let m;
384
+ elementRe.lastIndex = 0;
385
+ while ((m = elementRe.exec(text))) {
386
+ const tag = m[1].toLowerCase();
387
+ const attrs = m[2];
388
+ const inner = (m[3] ?? "").trim();
389
+ const aria = readXamlAttr(attrs, "aria-label") ??
390
+ readXamlAttr(attrs, "data-testid") ??
391
+ readXamlAttr(attrs, "id") ??
392
+ readXamlAttr(attrs, "name") ??
393
+ readXamlAttr(attrs, "placeholder") ??
394
+ "";
395
+ const name = aria || inner;
396
+ if (!name)
397
+ continue;
398
+ list.push({ tag, name: name.slice(0, 60) });
399
+ }
400
+ return list;
401
+ }
402
+ const newEls = collect(newContent);
403
+ const oldEls = oldContent ? collect(oldContent) : [];
404
+ const oldKey = new Set(oldEls.map((e) => `${e.tag}::${e.name}`));
405
+ for (const el of newEls) {
406
+ if (oldKey.has(`${el.tag}::${el.name}`))
407
+ continue;
408
+ let element;
409
+ if (el.tag === "button" || el.tag === "a")
410
+ element = "button";
411
+ else
412
+ element = "input";
413
+ pushUnique(out, {
414
+ kind: "ui_element_added",
415
+ element,
416
+ name: el.name,
417
+ file: filePath,
418
+ });
419
+ }
420
+ }
421
+ const CSHARP_PROPERTY_RE = /^\s*public\s+(?:virtual\s+|override\s+|static\s+|readonly\s+)*[\w<>?,\s[\]]+?\s+(\w+)\s*\{\s*get\s*;/;
422
+ const CSHARP_METHOD_RE = /^\s*public\s+(?:virtual\s+|override\s+|static\s+|async\s+)*[\w<>?,\s[\]]+?\s+(\w+)\s*\(/;
423
+ const FEATURE_METHOD_RE = /^(Propagate|On|Update|Add|Delete|Save|Load|Refresh|Handle|Toggle|Open|Close|Submit|Apply)\w*$/;
424
+ function parseCsharp(filePath, newContent, oldContent, out) {
425
+ const className = classNameForFile(filePath);
426
+ function members(text) {
427
+ const properties = [];
428
+ const methods = [];
429
+ for (const line of text.split("\n")) {
430
+ const propMatch = CSHARP_PROPERTY_RE.exec(line);
431
+ if (propMatch) {
432
+ properties.push(propMatch[1]);
433
+ continue;
434
+ }
435
+ const methodMatch = CSHARP_METHOD_RE.exec(line);
436
+ if (methodMatch && FEATURE_METHOD_RE.test(methodMatch[1])) {
437
+ methods.push(methodMatch[1]);
438
+ }
439
+ }
440
+ return { properties, methods };
441
+ }
442
+ const cur = members(newContent);
443
+ const prev = oldContent ? members(oldContent) : { properties: [], methods: [] };
444
+ const prevProps = new Set(prev.properties);
445
+ const prevMethods = new Set(prev.methods);
446
+ for (const p of cur.properties) {
447
+ if (prevProps.has(p))
448
+ continue;
449
+ pushUnique(out, {
450
+ kind: "property_added",
451
+ class: className,
452
+ name: p,
453
+ file: filePath,
454
+ });
455
+ }
456
+ for (const m of cur.methods) {
457
+ if (prevMethods.has(m))
458
+ continue;
459
+ pushUnique(out, {
460
+ kind: "method_added",
461
+ class: className,
462
+ name: m,
463
+ file: filePath,
464
+ });
465
+ }
466
+ }
467
+ function parseTs(filePath, newContent, oldContent, out) {
468
+ const className = classNameForFile(filePath);
469
+ const propRe = /^\s*(?:public\s+|export\s+(?:public\s+)?)?(?:readonly\s+)?(\w+)\s*(?:[:?]\s*[\w<>|[\],\s'"]+)?\s*=\s*(?!\s*function|\s*\()/;
470
+ const methodRe = /^\s*(?:public\s+|export\s+(?:async\s+)?function\s+|async\s+)?(?:async\s+)?(\w+)\s*\(/;
471
+ function members(text) {
472
+ const properties = [];
473
+ const methods = [];
474
+ for (const line of text.split("\n")) {
475
+ const trimmed = line.trim();
476
+ if (trimmed.startsWith("//") || trimmed.startsWith("*"))
477
+ continue;
478
+ const pm = propRe.exec(line);
479
+ if (pm) {
480
+ properties.push(pm[1]);
481
+ continue;
482
+ }
483
+ const mm = methodRe.exec(line);
484
+ if (mm && FEATURE_METHOD_RE.test(mm[1])) {
485
+ methods.push(mm[1]);
486
+ }
487
+ }
488
+ return { properties, methods };
489
+ }
490
+ const cur = members(newContent);
491
+ const prev = oldContent ? members(oldContent) : { properties: [], methods: [] };
492
+ const prevProps = new Set(prev.properties);
493
+ const prevMethods = new Set(prev.methods);
494
+ for (const p of cur.properties) {
495
+ if (prevProps.has(p))
496
+ continue;
497
+ pushUnique(out, { kind: "property_added", class: className, name: p, file: filePath });
498
+ }
499
+ for (const m of cur.methods) {
500
+ if (prevMethods.has(m))
501
+ continue;
502
+ pushUnique(out, { kind: "method_added", class: className, name: m, file: filePath });
503
+ }
504
+ }
505
+ function parseSwiftKtDart(filePath, newContent, oldContent, out) {
506
+ // Light shared regex — Swift `var/let name: Type`, Kotlin `val/var name`,
507
+ // Dart `final Type name` / `Type get name`. We only flag public-looking
508
+ // identifiers (not _-prefixed) and feature-shape methods.
509
+ const className = classNameForFile(filePath);
510
+ const propRe = /^\s*(?:public\s+|open\s+)?(?:final\s+|var\s+|let\s+|val\s+)\s*(\w+)/;
511
+ const methodRe = /^\s*(?:public\s+|open\s+|fun\s+|func\s+|void\s+|Future\b)\s*(\w+)\s*\(/;
512
+ function members(text) {
513
+ const properties = [];
514
+ const methods = [];
515
+ for (const line of text.split("\n")) {
516
+ const trimmed = line.trim();
517
+ if (trimmed.startsWith("//") || trimmed.startsWith("*"))
518
+ continue;
519
+ const pm = propRe.exec(line);
520
+ if (pm && !pm[1].startsWith("_"))
521
+ properties.push(pm[1]);
522
+ const mm = methodRe.exec(line);
523
+ if (mm && FEATURE_METHOD_RE.test(mm[1]))
524
+ methods.push(mm[1]);
525
+ }
526
+ return { properties, methods };
527
+ }
528
+ const cur = members(newContent);
529
+ const prev = oldContent ? members(oldContent) : { properties: [], methods: [] };
530
+ const prevProps = new Set(prev.properties);
531
+ const prevMethods = new Set(prev.methods);
532
+ for (const p of cur.properties) {
533
+ if (prevProps.has(p))
534
+ continue;
535
+ pushUnique(out, { kind: "property_added", class: className, name: p, file: filePath });
536
+ }
537
+ for (const m of cur.methods) {
538
+ if (prevMethods.has(m))
539
+ continue;
540
+ pushUnique(out, { kind: "method_added", class: className, name: m, file: filePath });
541
+ }
542
+ }
543
+ /**
544
+ * Parse EF Core migration C# files for `migrationBuilder.AddColumn` /
545
+ * `CreateTable` calls, plus standalone .sql DDL files.
546
+ */
547
+ function parseMigration(filePath, newContent, out) {
548
+ // EF: migrationBuilder.AddColumn<...>(name: "ProductCode", table: "Products", ...)
549
+ const efAddColRe = /AddColumn\s*<[^>]+>\s*\(\s*name\s*:\s*"([^"]+)"\s*,\s*table\s*:\s*"([^"]+)"/g;
550
+ let m;
551
+ while ((m = efAddColRe.exec(newContent))) {
552
+ pushUnique(out, {
553
+ kind: "migration_column_added",
554
+ table: m[2],
555
+ column: m[1],
556
+ file: filePath,
557
+ });
558
+ }
559
+ const efCreateTableRe = /migrationBuilder\.CreateTable\s*\(\s*name\s*:\s*"([^"]+)"/g;
560
+ while ((m = efCreateTableRe.exec(newContent))) {
561
+ pushUnique(out, { kind: "migration_table_added", table: m[1], file: filePath });
562
+ }
563
+ // Plain SQL DDL — `ALTER TABLE x ADD COLUMN y …` and `CREATE TABLE x …`.
564
+ const sqlAlterRe = /ALTER\s+TABLE\s+["`[]?(\w+)["`\]]?\s+ADD\s+(?:COLUMN\s+)?["`[]?(\w+)["`\]]?/gi;
565
+ while ((m = sqlAlterRe.exec(newContent))) {
566
+ pushUnique(out, {
567
+ kind: "migration_column_added",
568
+ table: m[1],
569
+ column: m[2],
570
+ file: filePath,
571
+ });
572
+ }
573
+ const sqlCreateRe = /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["`[]?(\w+)["`\]]?/gi;
574
+ while ((m = sqlCreateRe.exec(newContent))) {
575
+ pushUnique(out, { kind: "migration_table_added", table: m[1], file: filePath });
576
+ }
577
+ }
578
+ function isMigrationFile(relPath) {
579
+ // EF-style: any .cs under a Migrations folder (Designer.cs is excluded
580
+ // earlier). SQL DDL: any .sql file. We do NOT treat .ts/.js typeorm
581
+ // migration files specifically; the parseTs property/method extractor
582
+ // catches Add* / Create* feature-shaped methods in those.
583
+ if (/(?:^|\/)Migrations\//i.test(relPath) && relPath.endsWith(".cs"))
584
+ return true;
585
+ if (relPath.endsWith(".sql"))
586
+ return true;
587
+ return false;
588
+ }
589
+ function parseFileChange(change, oldGetter, out) {
590
+ if (change.status === "D")
591
+ return;
592
+ const relPosix = change.relPath;
593
+ const ext = (() => {
594
+ const dot = relPosix.lastIndexOf(".");
595
+ return dot > 0 ? relPosix.slice(dot).toLowerCase() : "";
596
+ })();
597
+ if (!PARSEABLE_EXTS.has(ext) && !isMigrationFile(relPosix))
598
+ return;
599
+ const newContent = readSafe(change.absPath);
600
+ if (newContent === null)
601
+ return;
602
+ const oldContent = oldGetter(relPosix);
603
+ if (isMigrationFile(relPosix)) {
604
+ parseMigration(relPosix, newContent, out);
605
+ return;
606
+ }
607
+ switch (ext) {
608
+ case ".xaml":
609
+ case ".axaml":
610
+ parseXaml(relPosix, newContent, oldContent, out);
611
+ return;
612
+ case ".html":
613
+ case ".vue":
614
+ case ".jsx":
615
+ case ".tsx":
616
+ parseHtmlOrJsx(relPosix, newContent, oldContent, out);
617
+ // .tsx also has TS code — extract added properties/methods.
618
+ if (ext === ".tsx" || ext === ".jsx")
619
+ parseTs(relPosix, newContent, oldContent, out);
620
+ return;
621
+ case ".cs":
622
+ parseCsharp(relPosix, newContent, oldContent, out);
623
+ return;
624
+ case ".ts":
625
+ parseTs(relPosix, newContent, oldContent, out);
626
+ return;
627
+ case ".dart":
628
+ case ".swift":
629
+ case ".kt":
630
+ parseSwiftKtDart(relPosix, newContent, oldContent, out);
631
+ return;
632
+ }
633
+ }
634
+ /**
635
+ * Build a ChangeManifest for the project at `cwd`. The result is also
636
+ * written to `<runDir>/change_manifest.json` when `runDir` is provided.
637
+ */
638
+ export function buildChangeManifest(cwd, runDir) {
639
+ const entries = [];
640
+ const changedFiles = [];
641
+ const excluded = [];
642
+ // Track the verified SHA per project so the next run knows where to
643
+ // start the diff. Stored alongside H7's loop-state under the same key.
644
+ let baseline = "mtime";
645
+ let verifiedSha = null;
646
+ const useGit = gitAvailable(cwd);
647
+ let priorSha = null;
648
+ if (useGit) {
649
+ baseline = "git+uncommitted";
650
+ priorSha = readVerifiedSha(cwd);
651
+ verifiedSha = gitHeadSha(cwd);
652
+ const committed = gitDiffNameStatus(cwd, priorSha);
653
+ const uncommitted = gitStatusPorcelain(cwd);
654
+ const merged = deduplicate([
655
+ ...committed.map((e) => toFileChange(cwd, e)),
656
+ ...uncommitted.map((e) => toFileChange(cwd, e)),
657
+ ]);
658
+ const oldContentCache = new Map();
659
+ function gitOldContent(relPath) {
660
+ if (!priorSha)
661
+ return null;
662
+ if (oldContentCache.has(relPath))
663
+ return oldContentCache.get(relPath) ?? null;
664
+ let v = null;
665
+ try {
666
+ v = execSync(`git show ${priorSha}:${quoteForShell(relPath)}`, {
667
+ cwd,
668
+ encoding: "utf-8",
669
+ stdio: ["ignore", "pipe", "ignore"],
670
+ });
671
+ }
672
+ catch {
673
+ v = null;
674
+ }
675
+ oldContentCache.set(relPath, v);
676
+ return v;
677
+ }
678
+ for (const c of merged) {
679
+ if (isExcluded(c.relPath)) {
680
+ excluded.push(c.relPath);
681
+ continue;
682
+ }
683
+ changedFiles.push(c.relPath);
684
+ parseFileChange(c, gitOldContent, entries);
685
+ }
686
+ }
687
+ else {
688
+ baseline = "mtime";
689
+ const sinceMs = lastVerifyMs(cwd);
690
+ const merged = filesNewerThanMs(cwd, sinceMs);
691
+ for (const c of merged) {
692
+ if (isExcluded(c.relPath)) {
693
+ excluded.push(c.relPath);
694
+ continue;
695
+ }
696
+ changedFiles.push(c.relPath);
697
+ // No prior content available outside git — feed null.
698
+ parseFileChange(c, () => null, entries);
699
+ }
700
+ }
701
+ const manifest = {
702
+ schema_version: 1,
703
+ baseline,
704
+ verified_sha: verifiedSha,
705
+ generated_at: new Date().toISOString(),
706
+ changed_files: changedFiles,
707
+ excluded_files: excluded,
708
+ entries,
709
+ };
710
+ if (runDir) {
711
+ try {
712
+ mkdirSync(runDir, { recursive: true });
713
+ writeFileSync(join(runDir, "change_manifest.json"), JSON.stringify(manifest, null, 2), "utf-8");
714
+ }
715
+ catch {
716
+ /* best-effort */
717
+ }
718
+ }
719
+ return manifest;
720
+ }
721
+ function quoteForShell(arg) {
722
+ // We pass refs of the form `<sha>:<path>` to `git show`, so we can't
723
+ // wrap the whole arg in quotes. Just escape spaces — paths with quotes
724
+ // are vanishingly rare in source trees and would have other problems.
725
+ return arg.replace(/(["'\s])/g, "\\$1");
726
+ }
727
+ function shaStatePath() {
728
+ if (process.env.CODELOOP_LOOP_STATE_DIR) {
729
+ return join(process.env.CODELOOP_LOOP_STATE_DIR, "verified-sha.json");
730
+ }
731
+ // Mirror loop_state.ts's homedir fallback. We deliberately use a
732
+ // separate file so the existing loop_state.json schema doesn't grow.
733
+ return join(homedir(), ".codeloop", "verified-sha.json");
734
+ }
735
+ function readShaState() {
736
+ const path = shaStatePath();
737
+ if (!existsSync(path))
738
+ return { schema_version: 1, projects: {} };
739
+ try {
740
+ const parsed = JSON.parse(readFileSync(path, "utf-8"));
741
+ if (parsed && typeof parsed === "object" && parsed.projects) {
742
+ return { schema_version: 1, projects: parsed.projects };
743
+ }
744
+ }
745
+ catch {
746
+ /* fall through */
747
+ }
748
+ return { schema_version: 1, projects: {} };
749
+ }
750
+ function projectKey(cwd) {
751
+ return isAbsolute(cwd) ? cwd : resolvePath(cwd);
752
+ }
753
+ export function readVerifiedSha(cwd) {
754
+ // The H7 loop-state file is the source-of-truth for the rest of the
755
+ // counter machinery. We READ from it via the shared key (resolving
756
+ // the project) so manual `codeloop_doctor --reset-loop-state` doesn't
757
+ // have to know about a second file. Storage lives in its own file.
758
+ void readLoopState; // keep the import — the H7 module side-effects
759
+ const all = readShaState();
760
+ const key = projectKey(cwd);
761
+ return all.projects[key]?.verified_sha ?? null;
762
+ }
763
+ export function writeVerifiedSha(cwd, sha) {
764
+ if (!sha)
765
+ return;
766
+ const all = readShaState();
767
+ const key = projectKey(cwd);
768
+ all.projects[key] = { verified_sha: sha, updated_at: new Date().toISOString() };
769
+ try {
770
+ const path = shaStatePath();
771
+ mkdirSync(dirname(path), { recursive: true });
772
+ writeFileSync(path, JSON.stringify(all, null, 2), "utf-8");
773
+ }
774
+ catch {
775
+ /* best-effort */
776
+ }
777
+ }
778
+ /**
779
+ * Convenience helper used by gate_check / capture_screenshot to load the
780
+ * latest manifest for a run. Returns null when the file is missing.
781
+ */
782
+ export function loadChangeManifest(runDir) {
783
+ const path = join(runDir, "change_manifest.json");
784
+ if (!existsSync(path))
785
+ return null;
786
+ try {
787
+ return JSON.parse(readFileSync(path, "utf-8"));
788
+ }
789
+ catch {
790
+ return null;
791
+ }
792
+ }
793
+ /**
794
+ * Returns the most recent change manifest across all runs in the project,
795
+ * preferring the one tied to `preferredRunId` when present. Used by
796
+ * gate_check / change-coverage to score against the same manifest the
797
+ * verify run produced even when the user re-runs gate_check standalone.
798
+ */
799
+ export function loadMostRecentChangeManifest(cwd, preferredRunId) {
800
+ const baseDir = getArtifactsBaseDir(cwd);
801
+ if (preferredRunId) {
802
+ const m = loadChangeManifest(getRunDir(preferredRunId, baseDir));
803
+ if (m)
804
+ return { manifest: m, runId: preferredRunId };
805
+ }
806
+ for (const rid of listRuns(baseDir)) {
807
+ const m = loadChangeManifest(getRunDir(rid, baseDir));
808
+ if (m)
809
+ return { manifest: m, runId: rid };
810
+ }
811
+ return { manifest: null, runId: null };
812
+ }
813
+ /** Used by C5 + C7 to enumerate the entries an agent must exercise. */
814
+ export function manifestEntryDisplayName(e) {
815
+ switch (e.kind) {
816
+ case "ui_element_added":
817
+ return `${e.element}: "${e.name}"`;
818
+ case "property_added":
819
+ return `${e.class}.${e.name}`;
820
+ case "method_added":
821
+ return `${e.class}.${e.name}()`;
822
+ case "migration_column_added":
823
+ return `${e.table}.${e.column}`;
824
+ case "migration_table_added":
825
+ return `table ${e.table}`;
826
+ case "layout_restructure":
827
+ return `layout ${e.from} → ${e.to} in ${e.file}`;
828
+ }
829
+ }
830
+ //# sourceMappingURL=change_manifest.js.map