universal-ast-mapper 1.28.0 → 2.0.0

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 (55) hide show
  1. package/BLUEPRINT.md +230 -230
  2. package/CHANGELOG.md +466 -338
  3. package/README.md +878 -878
  4. package/package.json +48 -47
  5. package/scripts/install-skill.mjs +187 -187
  6. package/dist/analysis.js +0 -134
  7. package/dist/callgraph.js +0 -467
  8. package/dist/check.js +0 -112
  9. package/dist/cli.js +0 -1275
  10. package/dist/complexity.js +0 -98
  11. package/dist/config.js +0 -53
  12. package/dist/contextpack.js +0 -79
  13. package/dist/coupling.js +0 -35
  14. package/dist/crosslang.js +0 -425
  15. package/dist/diskcache.js +0 -97
  16. package/dist/explorer.js +0 -123
  17. package/dist/extractors/c.js +0 -204
  18. package/dist/extractors/common.js +0 -56
  19. package/dist/extractors/cpp.js +0 -272
  20. package/dist/extractors/csharp.js +0 -209
  21. package/dist/extractors/go.js +0 -212
  22. package/dist/extractors/java.js +0 -152
  23. package/dist/extractors/kotlin.js +0 -159
  24. package/dist/extractors/php.js +0 -208
  25. package/dist/extractors/python.js +0 -153
  26. package/dist/extractors/ruby.js +0 -146
  27. package/dist/extractors/rust.js +0 -249
  28. package/dist/extractors/swift.js +0 -192
  29. package/dist/extractors/typescript.js +0 -577
  30. package/dist/gitdiff.js +0 -178
  31. package/dist/graph-analysis.js +0 -279
  32. package/dist/graph.js +0 -165
  33. package/dist/html.js +0 -326
  34. package/dist/index.js +0 -1408
  35. package/dist/layers.js +0 -36
  36. package/dist/modulecoupling.js +0 -0
  37. package/dist/parser.js +0 -84
  38. package/dist/pool.js +0 -114
  39. package/dist/prompts.js +0 -67
  40. package/dist/registry.js +0 -87
  41. package/dist/report.js +0 -232
  42. package/dist/resolver.js +0 -222
  43. package/dist/roots.js +0 -47
  44. package/dist/search.js +0 -68
  45. package/dist/semantic.js +0 -365
  46. package/dist/sfc.js +0 -27
  47. package/dist/skeleton.js +0 -132
  48. package/dist/sourcemap.js +0 -60
  49. package/dist/testmap.js +0 -167
  50. package/dist/tsconfig.js +0 -212
  51. package/dist/typeflow.js +0 -124
  52. package/dist/types.js +0 -5
  53. package/dist/unused-params.js +0 -127
  54. package/dist/worker.js +0 -27
  55. package/dist/workspace.js +0 -330
package/dist/callgraph.js DELETED
@@ -1,467 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import { parseSource } from "./parser.js";
4
- import { buildSkeleton } from "./skeleton.js";
5
- import { resolveOptions, loadProjectConfig } from "./config.js";
6
- import { detectLanguage } from "./registry.js";
7
- import { resolveImportPath, resolveAliasedImport, getOrBuildCrossLangIndex } from "./resolver.js";
8
- import { resolveCrossLangTarget } from "./crosslang.js";
9
- const CROSS_LANG = new Set(["java", "csharp", "rust", "go", "kotlin", "c", "cpp", "swift"]);
10
- function pushCall(out, callee, anchor) {
11
- if (callee && anchor)
12
- out.push({ callee, line: anchor.startPosition.row + 1 });
13
- }
14
- function collectCalls(node, out) {
15
- const t = node.type;
16
- // ── call_expression: TS/JS (member_expression) | Python "call" (attribute) |
17
- // Go (selector_expression) | Rust (field_expression, scoped_identifier)
18
- if (t === "call_expression" || t === "call") {
19
- const fn = node.childForFieldName("function");
20
- if (fn) {
21
- let callee = null;
22
- switch (fn.type) {
23
- case "identifier":
24
- callee = fn.text;
25
- break;
26
- case "member_expression":
27
- case "attribute": {
28
- const obj = fn.childForFieldName("object");
29
- const prop = fn.childForFieldName("property") ?? fn.childForFieldName("attribute");
30
- if (prop)
31
- callee = obj ? `${obj.text}.${prop.text}` : prop.text;
32
- break;
33
- }
34
- case "field_expression": {
35
- // Rust: inv.reserve — fields are `value` and `field`
36
- const obj = fn.childForFieldName("value");
37
- const fld = fn.childForFieldName("field");
38
- if (fld)
39
- callee = obj ? `${obj.text}.${fld.text}` : fld.text;
40
- break;
41
- }
42
- case "scoped_identifier":
43
- // Rust: String::from / helpers::format — keep full path
44
- callee = fn.text;
45
- break;
46
- case "selector_expression": {
47
- // Go: pkg.Func
48
- const obj = fn.childForFieldName("operand");
49
- const fld = fn.childForFieldName("field");
50
- if (fld)
51
- callee = obj ? `${obj.text}.${fld.text}` : fld.text;
52
- break;
53
- }
54
- }
55
- pushCall(out, callee, fn);
56
- }
57
- else {
58
- // Kotlin: call_expression has no `function` field — the callee is the
59
- // first named child (a simple_identifier for `Foo(...)` / a bare call,
60
- // or a navigation_expression for `obj.method(...)`).
61
- const callee0 = node.namedChild(0);
62
- if (callee0) {
63
- if (callee0.type === "simple_identifier" || callee0.type === "identifier") {
64
- pushCall(out, callee0.text, callee0);
65
- }
66
- else if (callee0.type === "navigation_expression") {
67
- pushCall(out, callee0.text.replace(/\s+/g, ""), callee0);
68
- }
69
- }
70
- }
71
- }
72
- // ── Java method invocation
73
- else if (t === "method_invocation") {
74
- const name = node.childForFieldName("name");
75
- const obj = node.childForFieldName("object");
76
- if (name)
77
- pushCall(out, obj ? `${obj.text}.${name.text}` : name.text, name);
78
- }
79
- // ── C# invocation expression
80
- else if (t === "invocation_expression") {
81
- const fn = node.childForFieldName("function");
82
- if (fn)
83
- pushCall(out, fn.text, fn);
84
- }
85
- // ── Java + C# constructor call: new Foo(...)
86
- else if (t === "object_creation_expression") {
87
- let typeNode = node.childForFieldName("type");
88
- if (!typeNode) {
89
- for (let i = 0; i < node.namedChildCount; i++) {
90
- const c = node.namedChild(i);
91
- if (c &&
92
- (c.type === "identifier" ||
93
- c.type === "type_identifier" ||
94
- c.type === "scoped_identifier" ||
95
- c.type === "qualified_name" ||
96
- c.type === "generic_type")) {
97
- typeNode = c;
98
- break;
99
- }
100
- }
101
- }
102
- if (typeNode)
103
- pushCall(out, `new ${typeNode.text}`, typeNode);
104
- }
105
- for (let i = 0; i < node.namedChildCount; i++) {
106
- const c = node.namedChild(i);
107
- if (c)
108
- collectCalls(c, out);
109
- }
110
- }
111
- // ─── Function-node finder ─────────────────────────────────────────────────────
112
- const FUNCTION_NODE_TYPES = new Set([
113
- "function_declaration", // TS / JS / Go
114
- "generator_function_declaration",
115
- "method_definition", // TS / JS class member
116
- "method_signature",
117
- "abstract_method_signature",
118
- "function_definition", // Python
119
- "async_function_definition", // Python async
120
- "method_declaration", // Go / Java / C#
121
- "constructor_declaration", // Java / C#
122
- "function_item", // Rust
123
- ]);
124
- function findFunctionNode(root, name) {
125
- function walk(node) {
126
- if (FUNCTION_NODE_TYPES.has(node.type)) {
127
- const named = node.childForFieldName("name");
128
- if (named?.text === name)
129
- return node;
130
- // Kotlin: function_declaration exposes its name as a simple_identifier
131
- // child, not via a `name` field.
132
- if (!named && node.type === "function_declaration") {
133
- const id = node.namedChild(0);
134
- if (id?.type === "simple_identifier" && id.text === name)
135
- return node;
136
- }
137
- }
138
- // const foo = () => ... | const foo = function() ...
139
- if (node.type === "variable_declarator") {
140
- const declName = node.childForFieldName("name")?.text;
141
- const value = node.childForFieldName("value");
142
- if (declName === name &&
143
- value &&
144
- (value.type === "arrow_function" ||
145
- value.type === "function" ||
146
- value.type === "function_expression")) {
147
- return value;
148
- }
149
- }
150
- for (let i = 0; i < node.namedChildCount; i++) {
151
- const c = node.namedChild(i);
152
- if (c) {
153
- const found = walk(c);
154
- if (found)
155
- return found;
156
- }
157
- }
158
- return null;
159
- }
160
- return walk(root);
161
- }
162
- // ─── Destructuring alias tracker (TS/JS only) ─────────────────────────────────
163
- function collectDestructuredAliases(node, importMap) {
164
- const aliases = new Map();
165
- function walk(n) {
166
- if (n.type === "variable_declarator") {
167
- const nameNode = n.childForFieldName("name");
168
- const valueNode = n.childForFieldName("value");
169
- if (nameNode && valueNode && nameNode.type === "object_pattern") {
170
- const baseName = valueNode.text.split(".")[0];
171
- const originRef = importMap.get(baseName);
172
- const origin = originRef?.from ?? aliases.get(baseName);
173
- if (origin) {
174
- for (let i = 0; i < nameNode.namedChildCount; i++) {
175
- const prop = nameNode.namedChild(i);
176
- if (!prop)
177
- continue;
178
- if (prop.type === "shorthand_property_identifier_pattern" ||
179
- prop.type === "shorthand_property_identifier") {
180
- aliases.set(prop.text, origin);
181
- }
182
- if (prop.type === "pair_pattern") {
183
- const val = prop.childForFieldName("value");
184
- if (val)
185
- aliases.set(val.text, origin);
186
- }
187
- }
188
- }
189
- }
190
- }
191
- for (let i = 0; i < n.namedChildCount; i++) {
192
- const c = n.namedChild(i);
193
- if (c)
194
- walk(c);
195
- }
196
- }
197
- walk(node);
198
- return aliases;
199
- }
200
- // ─── Base identifier of a callee expression ───────────────────────────────────
201
- /** Take the leftmost identifier from "obj.method" / "Pkg::func" / "new Foo". */
202
- function baseNameOf(callee) {
203
- let s = callee;
204
- if (s.startsWith("new "))
205
- s = s.slice(4);
206
- return s.split(/::|\./)[0];
207
- }
208
- // ─── Cross-language calledBy scan helper ──────────────────────────────────────
209
- /** Last segment of a member-style callee — "Helper.fmt" -> "fmt", "compute" -> null. */
210
- function memberOf(callee) {
211
- const noNew = callee.startsWith("new ") ? callee.slice(4) : callee;
212
- const parts = noNew.split(/::|\./);
213
- return parts.length > 1 ? parts[parts.length - 1] : null;
214
- }
215
- /**
216
- * Open a file, parse it, and check whether any call expression references
217
- * `funcName` — either as a bare call `funcName(...)` or as the trailing
218
- * member of a qualified call `X.funcName(...)` / `X::funcName(...)`.
219
- * Used for C# / Go reverse calledBy where namespace/package imports do not
220
- * name the called symbol.
221
- */
222
- async function fileCallsSymbol(fileAbs, funcName) {
223
- const lang = detectLanguage(fileAbs);
224
- if (!lang)
225
- return false;
226
- let src;
227
- try {
228
- src = fs.readFileSync(fileAbs, "utf8");
229
- }
230
- catch {
231
- return false;
232
- }
233
- const root = await parseSource(lang.grammar, src);
234
- const calls = [];
235
- collectCalls(root, calls);
236
- for (const c of calls) {
237
- if (c.callee === funcName)
238
- return true;
239
- const m = memberOf(c.callee);
240
- if (m === funcName)
241
- return true;
242
- }
243
- return false;
244
- }
245
- // ─── Public API ───────────────────────────────────────────────────────────────
246
- /** Recursively find the first symbol with the given name and return its decorators. */
247
- function findDecorators(symbols, name) {
248
- for (const s of symbols) {
249
- if (s.name === name && s.decorators && s.decorators.length > 0)
250
- return s.decorators;
251
- const nested = findDecorators(s.children, name);
252
- if (nested)
253
- return nested;
254
- }
255
- return undefined;
256
- }
257
- export async function buildCallGraph(filePath, funcName, root, allSkeletons) {
258
- const langEntry = detectLanguage(filePath);
259
- if (!langEntry)
260
- return null;
261
- const source = fs.readFileSync(filePath, "utf8");
262
- const relPath = path.relative(root, filePath).split(path.sep).join("/");
263
- const rootNode = await parseSource(langEntry.grammar, source);
264
- const funcNode = findFunctionNode(rootNode, funcName);
265
- if (!funcNode)
266
- return null;
267
- const body = funcNode.childForFieldName("body") ?? funcNode;
268
- const rawCalls = [];
269
- collectCalls(body, rawCalls);
270
- const opts = resolveOptions({ detail: "outline", emitHtml: false }, loadProjectConfig(root));
271
- const skel = await buildSkeleton(filePath, relPath, opts);
272
- // localName -> full ImportRef (so cross-lang resolution has the flags it needs)
273
- const importMap = new Map();
274
- for (const imp of skel.imports ?? []) {
275
- if (imp.symbol !== "*" && !imp.isSideEffect) {
276
- importMap.set(imp.alias ?? imp.symbol, imp);
277
- }
278
- }
279
- const localNames = new Set(skel.symbols.map((s) => s.name));
280
- const destructuredAliases = collectDestructuredAliases(body, importMap);
281
- // Build cross-lang index lazily — needed for Java/C#/Rust dispatch.
282
- const isCrossLang = CROSS_LANG.has(skel.language);
283
- const crossIndex = isCrossLang ? await getOrBuildCrossLangIndex(root) : null;
284
- const calls = [];
285
- const seen = new Set();
286
- for (const { callee, line } of rawCalls) {
287
- const key = `${callee}:${line}`;
288
- if (seen.has(key))
289
- continue;
290
- seen.add(key);
291
- const base = baseNameOf(callee);
292
- const importRef = importMap.get(base);
293
- const aliasOrigin = destructuredAliases.get(base);
294
- const call = { callee, line };
295
- if (importRef) {
296
- if (isCrossLang && crossIndex) {
297
- const target = resolveCrossLangTarget(importRef, skel, filePath, root, crossIndex);
298
- if (target) {
299
- if (target.kind === "symbol")
300
- call.calleeFileRel = target.file;
301
- else if (target.files.length > 0)
302
- call.calleeFileRel = target.files[0];
303
- }
304
- else {
305
- call.isExternal = true;
306
- call.calleeFileRel = importRef.from;
307
- }
308
- }
309
- else if (importRef.from.startsWith(".")) {
310
- const resolvedAbs = resolveImportPath(importRef.from, filePath);
311
- if (resolvedAbs) {
312
- call.calleeFileRel = path.relative(root, resolvedAbs).split(path.sep).join("/");
313
- }
314
- }
315
- else {
316
- const aliasAbs = resolveAliasedImport(importRef.from, filePath);
317
- if (aliasAbs) {
318
- call.calleeFileRel = path.relative(root, aliasAbs).split(path.sep).join("/");
319
- }
320
- else {
321
- call.isExternal = true;
322
- call.calleeFileRel = importRef.from;
323
- }
324
- }
325
- }
326
- else if (aliasOrigin) {
327
- // Destructured aliases are TS/JS only (always relative or external).
328
- if (aliasOrigin.startsWith(".")) {
329
- const resolvedAbs = resolveImportPath(aliasOrigin, filePath);
330
- if (resolvedAbs) {
331
- call.calleeFileRel = path.relative(root, resolvedAbs).split(path.sep).join("/");
332
- }
333
- }
334
- else {
335
- const aliasAbs = resolveAliasedImport(aliasOrigin, filePath);
336
- if (aliasAbs) {
337
- call.calleeFileRel = path.relative(root, aliasAbs).split(path.sep).join("/");
338
- }
339
- else {
340
- call.isExternal = true;
341
- call.calleeFileRel = aliasOrigin;
342
- }
343
- }
344
- }
345
- else if (crossIndex && skel.language === "csharp") {
346
- // C# `using App.Models;` makes types visible without naming them.
347
- // Try `<usingNs>.<base>` against the type-by-fqn index.
348
- for (const ns of skel.imports ?? []) {
349
- if (!ns.isNamespaceImport)
350
- continue;
351
- const f = crossIndex.csharpTypes.get(`${ns.from}.${base}`);
352
- if (f && f !== skel.file) {
353
- call.calleeFileRel = f;
354
- break;
355
- }
356
- }
357
- if (!call.calleeFileRel && localNames.has(base))
358
- call.isLocal = true;
359
- }
360
- else if (crossIndex && skel.language === "java") {
361
- // Java wildcard import: `import com.example.*;` doesn't name the type.
362
- for (const wc of skel.imports ?? []) {
363
- if (wc.symbol !== "*")
364
- continue;
365
- const f = crossIndex.javaFqcn.get(`${wc.from}.${base}`);
366
- if (f && f !== skel.file) {
367
- call.calleeFileRel = f;
368
- break;
369
- }
370
- }
371
- if (!call.calleeFileRel && localNames.has(base))
372
- call.isLocal = true;
373
- }
374
- else if (localNames.has(base)) {
375
- call.isLocal = true;
376
- }
377
- calls.push(call);
378
- }
379
- // ── calledBy: who imports this function? ────────────────────────────────
380
- const calledBy = [];
381
- if (allSkeletons) {
382
- for (const otherSkel of allSkeletons) {
383
- if (otherSkel.file === relPath)
384
- continue;
385
- const otherIsCrossLang = CROSS_LANG.has(otherSkel.language);
386
- const otherAbs = path.resolve(root, otherSkel.file);
387
- for (const imp of otherSkel.imports ?? []) {
388
- const importedName = imp.alias ?? imp.symbol;
389
- if (importedName !== funcName && imp.symbol !== funcName)
390
- continue;
391
- if (otherIsCrossLang) {
392
- // Symbol-level cross-lang match only — file/namespace edges are too
393
- // broad to claim "this file calls funcName".
394
- if (!crossIndex)
395
- continue;
396
- const target = resolveCrossLangTarget(imp, otherSkel, otherAbs, root, crossIndex);
397
- if (target && target.kind === "symbol" && target.file === relPath && target.symbol === funcName) {
398
- calledBy.push({ file: otherSkel.file });
399
- break;
400
- }
401
- }
402
- else {
403
- const resolvedAbs = imp.from.startsWith(".")
404
- ? resolveImportPath(imp.from, otherAbs)
405
- : resolveAliasedImport(imp.from, otherAbs);
406
- if (!resolvedAbs)
407
- continue;
408
- const resolvedRel = path.relative(root, resolvedAbs).split(path.sep).join("/");
409
- if (resolvedRel === relPath) {
410
- calledBy.push({ file: otherSkel.file });
411
- break;
412
- }
413
- }
414
- }
415
- }
416
- }
417
- // Extra pass: for C# / Go, the cross-lang resolver gives file-level targets
418
- // (namespace / package) so the loop above misses callers that only show up
419
- // via name-resolution at the call site. Scan candidate files' call sites.
420
- if (allSkeletons &&
421
- crossIndex &&
422
- (skel.language === "csharp" || skel.language === "go")) {
423
- const seenFiles = new Set(calledBy.map((c) => c.file));
424
- for (const otherSkel of allSkeletons) {
425
- if (otherSkel.file === relPath)
426
- continue;
427
- if (otherSkel.language !== skel.language)
428
- continue;
429
- if (seenFiles.has(otherSkel.file))
430
- continue;
431
- const otherAbs = path.resolve(root, otherSkel.file);
432
- // Confirm this other file imports / uses something that resolves to us.
433
- let importsUs = false;
434
- for (const imp of otherSkel.imports ?? []) {
435
- const target = resolveCrossLangTarget(imp, otherSkel, otherAbs, root, crossIndex);
436
- if (!target)
437
- continue;
438
- if (target.kind === "file" && target.files.includes(relPath)) {
439
- importsUs = true;
440
- break;
441
- }
442
- if (target.kind === "symbol" && target.file === relPath) {
443
- importsUs = true;
444
- break;
445
- }
446
- }
447
- if (!importsUs)
448
- continue;
449
- if (await fileCallsSymbol(otherAbs, funcName)) {
450
- calledBy.push({ file: otherSkel.file });
451
- seenFiles.add(otherSkel.file);
452
- }
453
- }
454
- }
455
- const decorators = findDecorators(skel.symbols, funcName);
456
- return {
457
- file: relPath,
458
- function: funcName,
459
- functionRange: {
460
- startLine: funcNode.startPosition.row + 1,
461
- endLine: funcNode.endPosition.row + 1,
462
- },
463
- ...(decorators ? { decorators } : {}),
464
- calls,
465
- calledBy,
466
- };
467
- }
package/dist/check.js DELETED
@@ -1,112 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import { buildReport } from "./report.js";
4
- export const BASELINE_FILENAME = ".ast-map.baseline.json";
5
- export function metricsFromReport(r) {
6
- return {
7
- fileCount: r.fileCount,
8
- symbolCount: r.symbolCount,
9
- cycles: r.cycles.count,
10
- deadExports: r.dead.count,
11
- sdpViolations: r.layerViolations.count,
12
- veryHighComplexity: r.complexity.hotspots.filter((h) => h.complexity > 20).length,
13
- maxComplexity: r.complexity.max,
14
- score: r.score,
15
- grade: r.grade,
16
- };
17
- }
18
- function readBaseline(file) {
19
- try {
20
- const raw = JSON.parse(fs.readFileSync(file, "utf8"));
21
- return raw.metrics ?? null;
22
- }
23
- catch {
24
- return null;
25
- }
26
- }
27
- function checkThresholds(m, t, out) {
28
- const rules = [
29
- ["maxCycles", "cycles", "max"],
30
- ["maxDeadExports", "deadExports", "max"],
31
- ["maxSdpViolations", "sdpViolations", "max"],
32
- ["maxVeryHighComplexity", "veryHighComplexity", "max"],
33
- ["maxComplexity", "maxComplexity", "max"],
34
- ["minScore", "score", "min"],
35
- ];
36
- for (const [tKey, mKey, dir] of rules) {
37
- const limit = t[tKey];
38
- if (limit === undefined)
39
- continue;
40
- const actual = m[mKey];
41
- const bad = dir === "max" ? actual > limit : actual < limit;
42
- if (bad) {
43
- out.push({
44
- kind: "threshold",
45
- metric: mKey,
46
- limit,
47
- actual,
48
- message: `${mKey} is ${actual}, ${dir === "max" ? "exceeds max" : "below min"} ${limit}`,
49
- });
50
- }
51
- }
52
- }
53
- /** Metrics where an increase vs the baseline is a regression. */
54
- const RATCHET_UP = [
55
- "cycles",
56
- "deadExports",
57
- "sdpViolations",
58
- "veryHighComplexity",
59
- ];
60
- function checkBaseline(m, base, out) {
61
- for (const key of RATCHET_UP) {
62
- const was = base[key];
63
- const now = m[key];
64
- if (now > was) {
65
- out.push({
66
- kind: "regression",
67
- metric: key,
68
- limit: was,
69
- actual: now,
70
- message: `${key} regressed: ${was} → ${now} (baseline ratchet)`,
71
- });
72
- }
73
- }
74
- if (m.score < base.score) {
75
- out.push({
76
- kind: "regression",
77
- metric: "score",
78
- limit: base.score,
79
- actual: m.score,
80
- message: `health score regressed: ${base.score} → ${m.score}`,
81
- });
82
- }
83
- }
84
- export async function runQualityGate(absDir, root, opts = {}) {
85
- const report = await buildReport(absDir, root);
86
- const metrics = metricsFromReport(report);
87
- const baselinePath = path.resolve(root, opts.baselinePath ?? BASELINE_FILENAME);
88
- const baseline = readBaseline(baselinePath);
89
- const failures = [];
90
- if (opts.thresholds)
91
- checkThresholds(metrics, opts.thresholds, failures);
92
- if (baseline)
93
- checkBaseline(metrics, baseline, failures);
94
- let baselineUpdated = false;
95
- if (opts.updateBaseline) {
96
- const doc = {
97
- tool: "universal-ast-mapper",
98
- updatedAt: new Date().toISOString(),
99
- metrics,
100
- };
101
- fs.writeFileSync(baselinePath, JSON.stringify(doc, null, 2) + "\n", "utf8");
102
- baselineUpdated = true;
103
- }
104
- return {
105
- passed: failures.length === 0,
106
- metrics,
107
- baseline,
108
- baselinePath,
109
- baselineUpdated,
110
- failures,
111
- };
112
- }