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.
- package/dist/auth/critical_floors.d.ts.map +1 -1
- package/dist/auth/critical_floors.js +8 -0
- package/dist/auth/critical_floors.js.map +1 -1
- package/dist/evidence/anti_rationalisation.d.ts +34 -0
- package/dist/evidence/anti_rationalisation.d.ts.map +1 -0
- package/dist/evidence/anti_rationalisation.js +85 -0
- package/dist/evidence/anti_rationalisation.js.map +1 -0
- package/dist/evidence/change_coverage.d.ts +59 -0
- package/dist/evidence/change_coverage.d.ts.map +1 -0
- package/dist/evidence/change_coverage.js +422 -0
- package/dist/evidence/change_coverage.js.map +1 -0
- package/dist/evidence/change_manifest.d.ts +94 -0
- package/dist/evidence/change_manifest.d.ts.map +1 -0
- package/dist/evidence/change_manifest.js +830 -0
- package/dist/evidence/change_manifest.js.map +1 -0
- package/dist/evidence/loop_state.d.ts +53 -0
- package/dist/evidence/loop_state.d.ts.map +1 -0
- package/dist/evidence/loop_state.js +147 -0
- package/dist/evidence/loop_state.js.map +1 -0
- package/dist/evidence/verify_staleness.d.ts +9 -0
- package/dist/evidence/verify_staleness.d.ts.map +1 -0
- package/dist/evidence/verify_staleness.js +180 -0
- package/dist/evidence/verify_staleness.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +374 -19
- package/dist/index.js.map +1 -1
- package/dist/runners/empty_state_detector.d.ts +33 -0
- package/dist/runners/empty_state_detector.d.ts.map +1 -0
- package/dist/runners/empty_state_detector.js +304 -0
- package/dist/runners/empty_state_detector.js.map +1 -0
- package/dist/runners/maestro.d.ts +13 -0
- package/dist/runners/maestro.d.ts.map +1 -1
- package/dist/runners/maestro.js +37 -1
- package/dist/runners/maestro.js.map +1 -1
- package/dist/runners/modal_detector.d.ts +60 -0
- package/dist/runners/modal_detector.d.ts.map +1 -0
- package/dist/runners/modal_detector.js +160 -0
- package/dist/runners/modal_detector.js.map +1 -0
- package/dist/runners/python_tests.d.ts +26 -0
- package/dist/runners/python_tests.d.ts.map +1 -0
- package/dist/runners/python_tests.js +181 -0
- package/dist/runners/python_tests.js.map +1 -0
- package/dist/runners/rust_tests.d.ts +28 -0
- package/dist/runners/rust_tests.d.ts.map +1 -0
- package/dist/runners/rust_tests.js +76 -0
- package/dist/runners/rust_tests.js.map +1 -0
- package/dist/tools/c7_slug.d.ts +14 -0
- package/dist/tools/c7_slug.d.ts.map +1 -0
- package/dist/tools/c7_slug.js +21 -0
- package/dist/tools/c7_slug.js.map +1 -0
- package/dist/tools/diagnose.d.ts.map +1 -1
- package/dist/tools/diagnose.js +13 -0
- package/dist/tools/diagnose.js.map +1 -1
- package/dist/tools/gate_check.d.ts +2 -1
- package/dist/tools/gate_check.d.ts.map +1 -1
- package/dist/tools/gate_check.js +74 -32
- package/dist/tools/gate_check.js.map +1 -1
- package/dist/tools/is_ui_project.d.ts +23 -0
- package/dist/tools/is_ui_project.d.ts.map +1 -0
- package/dist/tools/is_ui_project.js +42 -0
- package/dist/tools/is_ui_project.js.map +1 -0
- package/dist/tools/plan_change_journey.d.ts +41 -0
- package/dist/tools/plan_change_journey.d.ts.map +1 -0
- package/dist/tools/plan_change_journey.js +131 -0
- package/dist/tools/plan_change_journey.js.map +1 -0
- package/dist/tools/verify.d.ts +28 -0
- package/dist/tools/verify.d.ts.map +1 -1
- package/dist/tools/verify.js +272 -8
- package/dist/tools/verify.js.map +1 -1
- 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
|