@blzsky/variant 0.2.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.
- package/LICENSE +20 -0
- package/README.md +177 -0
- package/dist/build-2LO7GD2S.js +63 -0
- package/dist/check-ZTKGGF4M.js +44 -0
- package/dist/chunk-3E5LBU3O.js +180 -0
- package/dist/chunk-4LLK2S4R.js +630 -0
- package/dist/chunk-6J4XDG44.js +14 -0
- package/dist/chunk-CXNXZLDG.js +13 -0
- package/dist/chunk-DRMASVYN.js +25 -0
- package/dist/chunk-GDSZFIHM.js +29 -0
- package/dist/chunk-ISRF5F3Q.js +68 -0
- package/dist/chunk-KSHWC2XN.js +78 -0
- package/dist/chunk-LJDDS4RR.js +91 -0
- package/dist/chunk-LXKLCLL6.js +91 -0
- package/dist/chunk-MZZOE7L4.js +26 -0
- package/dist/chunk-NSNZOH5P.js +31 -0
- package/dist/chunk-O4LZKXNZ.js +301 -0
- package/dist/chunk-RGD422OM.js +108 -0
- package/dist/chunk-RVXUCJYA.js +81 -0
- package/dist/chunk-SWPVR2DF.js +468 -0
- package/dist/chunk-V22IPJ7A.js +64 -0
- package/dist/chunk-V5CTGXMM.js +256 -0
- package/dist/chunk-VTJ67TT5.js +238 -0
- package/dist/chunk-XTYVVUWW.js +279 -0
- package/dist/chunk-XUBWCTJZ.js +100 -0
- package/dist/chunk-YYGCSKIQ.js +29 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +225 -0
- package/dist/diff-GFI6RAEA.js +60 -0
- package/dist/doctor-GH4RZVEH.js +127 -0
- package/dist/env-QORDD6FQ.js +24 -0
- package/dist/impact-AF453PLK.js +702 -0
- package/dist/index.d.ts +56 -0
- package/dist/index.js +7 -0
- package/dist/init-KJIUNGAN.js +158 -0
- package/dist/insight-DZ5NQY6D.js +29 -0
- package/dist/pr-UFZRZX6S.js +152 -0
- package/dist/run-6PNH4QRA.js +40 -0
- package/dist/symbol-graph-YYUEMZGR.js +22 -0
- package/dist/tsconfig-paths-E3UEDGXZ.js +10 -0
- package/dist/workspace-X5FUBTY3.js +186 -0
- package/package.json +86 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// src/core/execution/concurrency.ts
|
|
2
|
+
async function mapLimit(items, limit, fn, stopOnError = true) {
|
|
3
|
+
const results = new Array(items.length);
|
|
4
|
+
let next = 0;
|
|
5
|
+
let firstError = null;
|
|
6
|
+
let stopped = false;
|
|
7
|
+
const worker = async () => {
|
|
8
|
+
while (true) {
|
|
9
|
+
if (stopped) return;
|
|
10
|
+
const i = next++;
|
|
11
|
+
if (i >= items.length) return;
|
|
12
|
+
try {
|
|
13
|
+
results[i] = await fn(items[i], i);
|
|
14
|
+
} catch (err) {
|
|
15
|
+
if (firstError === null) firstError = err;
|
|
16
|
+
if (stopOnError) stopped = true;
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
const workerCount = Math.max(1, Math.min(limit, items.length));
|
|
22
|
+
await Promise.all(Array.from({ length: workerCount }, worker));
|
|
23
|
+
if (firstError !== null) throw firstError;
|
|
24
|
+
return results;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export {
|
|
28
|
+
mapLimit
|
|
29
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getColors
|
|
3
|
+
} from "./chunk-YYGCSKIQ.js";
|
|
4
|
+
import {
|
|
5
|
+
lines
|
|
6
|
+
} from "./chunk-6J4XDG44.js";
|
|
7
|
+
import {
|
|
8
|
+
getPrinter
|
|
9
|
+
} from "./chunk-V22IPJ7A.js";
|
|
10
|
+
|
|
11
|
+
// src/cli/render/insight.ts
|
|
12
|
+
function statusLabel(result) {
|
|
13
|
+
const colors = getColors();
|
|
14
|
+
if (result.skipped) return colors.dim("SKIP");
|
|
15
|
+
return result.cacheHit ? colors.green("HIT") : colors.red("MISS");
|
|
16
|
+
}
|
|
17
|
+
function renderInsights(summary, printer = getPrinter()) {
|
|
18
|
+
const colors = getColors();
|
|
19
|
+
const results = summary.lastRunResults;
|
|
20
|
+
if (results.length === 0) {
|
|
21
|
+
const entries = Object.entries(summary.cachedStats);
|
|
22
|
+
if (entries.length === 0) {
|
|
23
|
+
lines(printer, "No cached task data yet. Run a task first.");
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
lines(printer, "", "Cached task history:");
|
|
27
|
+
for (const [task, entry] of entries) {
|
|
28
|
+
const duration = entry.lastDurationMs !== void 0 ? `${entry.lastDurationMs}ms` : "unknown";
|
|
29
|
+
const ran = new Date(entry.lastRun).toLocaleString();
|
|
30
|
+
lines(
|
|
31
|
+
printer,
|
|
32
|
+
` ${task.padEnd(20)} last run: ${ran} duration: ${duration}`
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const nameWidth = Math.max(...results.map((r) => r.task.length), 4);
|
|
38
|
+
const header = `${"TASK".padEnd(nameWidth)} ${"DURATION".padEnd(10)} STATUS`;
|
|
39
|
+
lines(printer, "", colors.bold(header), "-".repeat(header.length));
|
|
40
|
+
for (const result of results) {
|
|
41
|
+
const duration = result.skipped || result.cacheHit ? "-" : `${result.durationMs}ms`;
|
|
42
|
+
lines(
|
|
43
|
+
printer,
|
|
44
|
+
`${result.task.padEnd(nameWidth)} ${duration.padEnd(10)} ${statusLabel(result)}`
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
const percent = (summary.cacheHitRate * 100).toFixed(0);
|
|
48
|
+
lines(
|
|
49
|
+
printer,
|
|
50
|
+
"",
|
|
51
|
+
colors.dim(
|
|
52
|
+
`Total: ${summary.totalDurationMs}ms Cache hit rate: ${percent}%`
|
|
53
|
+
)
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
function renderEnv(pm, runtime, framework, printer = getPrinter()) {
|
|
57
|
+
lines(
|
|
58
|
+
printer,
|
|
59
|
+
`Package Manager : ${pm}`,
|
|
60
|
+
`Runtime : ${runtime.primary} (fallback: ${runtime.fallback})`,
|
|
61
|
+
`Framework : ${framework ?? "none detected"}`
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export {
|
|
66
|
+
renderInsights,
|
|
67
|
+
renderEnv
|
|
68
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// src/core/graph/tsconfig-paths.ts
|
|
2
|
+
import { access } from "fs/promises";
|
|
3
|
+
import path from "path";
|
|
4
|
+
var MAX_WILDCARDS = 1;
|
|
5
|
+
function toPathAliases(cwd, paths, baseUrl) {
|
|
6
|
+
if (paths === void 0) return [];
|
|
7
|
+
const base = baseUrl ?? cwd;
|
|
8
|
+
const aliases = [];
|
|
9
|
+
for (const [pattern, rawTargets] of Object.entries(paths)) {
|
|
10
|
+
const stars = pattern.split("*").length - 1;
|
|
11
|
+
if (stars > MAX_WILDCARDS) continue;
|
|
12
|
+
const wildcard = stars === 1;
|
|
13
|
+
const [prefix = "", suffix = ""] = wildcard ? pattern.split("*") : [pattern, ""];
|
|
14
|
+
const targets = [];
|
|
15
|
+
for (const target of rawTargets) {
|
|
16
|
+
if (target.split("*").length - 1 > MAX_WILDCARDS) continue;
|
|
17
|
+
const absolute = path.resolve(base, target);
|
|
18
|
+
const relative = path.relative(cwd, absolute).replace(/\\/g, "/");
|
|
19
|
+
if (relative === "" || relative.startsWith("../")) continue;
|
|
20
|
+
targets.push(relative);
|
|
21
|
+
}
|
|
22
|
+
if (targets.length > 0) {
|
|
23
|
+
aliases.push({ prefix, suffix, wildcard, targets });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
aliases.sort((a, b) => b.prefix.length - a.prefix.length);
|
|
27
|
+
return aliases;
|
|
28
|
+
}
|
|
29
|
+
function matchPathAlias(spec, aliases) {
|
|
30
|
+
const bases = [];
|
|
31
|
+
for (const alias of aliases) {
|
|
32
|
+
if (!alias.wildcard) {
|
|
33
|
+
if (spec === alias.prefix) bases.push(...alias.targets);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (spec.length < alias.prefix.length + alias.suffix.length || !spec.startsWith(alias.prefix) || !spec.endsWith(alias.suffix)) {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const captured = spec.slice(
|
|
40
|
+
alias.prefix.length,
|
|
41
|
+
spec.length - alias.suffix.length
|
|
42
|
+
);
|
|
43
|
+
for (const target of alias.targets) {
|
|
44
|
+
bases.push(target.replace("*", captured));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return bases;
|
|
48
|
+
}
|
|
49
|
+
async function readPathAliases(cwd, tsconfigName = "tsconfig.json") {
|
|
50
|
+
const configPath = path.join(cwd, tsconfigName);
|
|
51
|
+
try {
|
|
52
|
+
await access(configPath);
|
|
53
|
+
} catch {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
const { ts } = await import("ts-morph");
|
|
57
|
+
const read = ts.readConfigFile(configPath, (file) => ts.sys.readFile(file));
|
|
58
|
+
if (read.error !== void 0 || read.config === void 0) return [];
|
|
59
|
+
const parsed = ts.parseJsonConfigFileContent(
|
|
60
|
+
read.config,
|
|
61
|
+
{
|
|
62
|
+
...ts.sys,
|
|
63
|
+
// Enumerating every file in the workspace is pure waste: only
|
|
64
|
+
// `compilerOptions` is wanted, and a large repo makes this the most
|
|
65
|
+
// expensive call in the command.
|
|
66
|
+
readDirectory: () => [],
|
|
67
|
+
useCaseSensitiveFileNames: ts.sys.useCaseSensitiveFileNames
|
|
68
|
+
},
|
|
69
|
+
cwd
|
|
70
|
+
);
|
|
71
|
+
return toPathAliases(cwd, parsed.options.paths, parsed.options.baseUrl);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export {
|
|
75
|
+
toPathAliases,
|
|
76
|
+
matchPathAlias,
|
|
77
|
+
readPathAliases
|
|
78
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import {
|
|
2
|
+
collectExportedSurface,
|
|
3
|
+
significantText
|
|
4
|
+
} from "./chunk-O4LZKXNZ.js";
|
|
5
|
+
|
|
6
|
+
// src/core/semantic/differ.ts
|
|
7
|
+
function createClassifier() {
|
|
8
|
+
let host;
|
|
9
|
+
let nextId = 0;
|
|
10
|
+
return async (input) => {
|
|
11
|
+
host ??= (async () => {
|
|
12
|
+
const tsm2 = await import("ts-morph");
|
|
13
|
+
return {
|
|
14
|
+
tsm: tsm2,
|
|
15
|
+
project: new tsm2.Project({ useInMemoryFileSystem: true })
|
|
16
|
+
};
|
|
17
|
+
})();
|
|
18
|
+
const { tsm, project } = await host;
|
|
19
|
+
const id = nextId++;
|
|
20
|
+
const beforeFile = project.createSourceFile(
|
|
21
|
+
`__before_${id}.ts`,
|
|
22
|
+
input.before,
|
|
23
|
+
{ overwrite: true }
|
|
24
|
+
);
|
|
25
|
+
const afterFile = project.createSourceFile(
|
|
26
|
+
`__after_${id}.ts`,
|
|
27
|
+
input.after,
|
|
28
|
+
{ overwrite: true }
|
|
29
|
+
);
|
|
30
|
+
try {
|
|
31
|
+
return compareSurfaces(input, beforeFile, afterFile, tsm);
|
|
32
|
+
} finally {
|
|
33
|
+
project.removeSourceFile(beforeFile);
|
|
34
|
+
project.removeSourceFile(afterFile);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
var defaultClassifier;
|
|
39
|
+
async function classifyChange(input) {
|
|
40
|
+
defaultClassifier ??= createClassifier();
|
|
41
|
+
return defaultClassifier(input);
|
|
42
|
+
}
|
|
43
|
+
function compareSurfaces(input, beforeFile, afterFile, tsm) {
|
|
44
|
+
const beforeApi = collectExportedSurface(beforeFile, tsm);
|
|
45
|
+
const afterApi = collectExportedSurface(afterFile, tsm);
|
|
46
|
+
const added = [];
|
|
47
|
+
const removed = [];
|
|
48
|
+
const changed = [];
|
|
49
|
+
for (const [name, after] of afterApi.symbols) {
|
|
50
|
+
const before = beforeApi.symbols.get(name);
|
|
51
|
+
if (before === void 0) {
|
|
52
|
+
added.push(name);
|
|
53
|
+
} else if (before.signature !== after.signature) {
|
|
54
|
+
changed.push({
|
|
55
|
+
name,
|
|
56
|
+
kind: before.typeSpace && after.typeSpace ? "type" : "signature"
|
|
57
|
+
});
|
|
58
|
+
} else if (before.body !== after.body) {
|
|
59
|
+
changed.push({ name, kind: "body" });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
for (const name of beforeApi.symbols.keys()) {
|
|
63
|
+
if (!afterApi.symbols.has(name)) removed.push(name);
|
|
64
|
+
}
|
|
65
|
+
const surfaceChanged = added.length > 0 || removed.length > 0 || changed.some((c) => c.kind !== "body");
|
|
66
|
+
let classification;
|
|
67
|
+
if (surfaceChanged) {
|
|
68
|
+
classification = "breaking";
|
|
69
|
+
} else if (changed.length > 0 || significantText(input.before, tsm) !== significantText(input.after, tsm)) {
|
|
70
|
+
classification = "internal";
|
|
71
|
+
} else {
|
|
72
|
+
classification = "non-impacting";
|
|
73
|
+
}
|
|
74
|
+
const confidenceNotes = [.../* @__PURE__ */ new Set([...beforeApi.notes, ...afterApi.notes])];
|
|
75
|
+
const confidence = Math.max(
|
|
76
|
+
0.3,
|
|
77
|
+
Math.round((1 - confidenceNotes.length * 0.15) * 100) / 100
|
|
78
|
+
);
|
|
79
|
+
return {
|
|
80
|
+
filePath: input.filePath,
|
|
81
|
+
classification,
|
|
82
|
+
exportedSymbols: { added, removed, changed },
|
|
83
|
+
confidence,
|
|
84
|
+
confidenceNotes
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export {
|
|
89
|
+
createClassifier,
|
|
90
|
+
classifyChange
|
|
91
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CacheError
|
|
3
|
+
} from "./chunk-RVXUCJYA.js";
|
|
4
|
+
|
|
5
|
+
// src/core/cache/store.ts
|
|
6
|
+
import { mkdirSync, renameSync, unlinkSync, writeFileSync } from "fs";
|
|
7
|
+
import { mkdir, readFile, rename, unlink, writeFile } from "fs/promises";
|
|
8
|
+
import path from "path";
|
|
9
|
+
var CACHE_FILENAME = "cache.json";
|
|
10
|
+
async function readCache(cacheDir) {
|
|
11
|
+
const filePath = path.join(cacheDir, CACHE_FILENAME);
|
|
12
|
+
try {
|
|
13
|
+
const raw = await readFile(filePath, "utf8");
|
|
14
|
+
return JSON.parse(raw);
|
|
15
|
+
} catch (err) {
|
|
16
|
+
if (err.code === "ENOENT") return { tasks: {} };
|
|
17
|
+
throw new CacheError(
|
|
18
|
+
`Failed to read cache at "${filePath}": ${String(err)}`,
|
|
19
|
+
{ cause: err }
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
async function writeCache(cacheDir, cache) {
|
|
24
|
+
const filePath = path.join(cacheDir, CACHE_FILENAME);
|
|
25
|
+
const tempPath = temporaryPathFor(filePath);
|
|
26
|
+
try {
|
|
27
|
+
await mkdir(cacheDir, { recursive: true });
|
|
28
|
+
await writeFile(tempPath, JSON.stringify(cache));
|
|
29
|
+
await rename(tempPath, filePath);
|
|
30
|
+
} catch (err) {
|
|
31
|
+
await unlink(tempPath).catch(() => {
|
|
32
|
+
});
|
|
33
|
+
throw new CacheError(
|
|
34
|
+
`Failed to write cache to "${cacheDir}": ${String(err)}`,
|
|
35
|
+
{ cause: err }
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function writeCacheSync(cacheDir, cache) {
|
|
40
|
+
const filePath = path.join(cacheDir, CACHE_FILENAME);
|
|
41
|
+
const tempPath = temporaryPathFor(filePath);
|
|
42
|
+
try {
|
|
43
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
44
|
+
writeFileSync(tempPath, JSON.stringify(cache));
|
|
45
|
+
renameSync(tempPath, filePath);
|
|
46
|
+
} catch (err) {
|
|
47
|
+
try {
|
|
48
|
+
unlinkSync(tempPath);
|
|
49
|
+
} catch {
|
|
50
|
+
}
|
|
51
|
+
throw new CacheError(
|
|
52
|
+
`Failed to write cache to "${cacheDir}": ${String(err)}`,
|
|
53
|
+
{ cause: err }
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
var temporaryCounter = 0;
|
|
58
|
+
function temporaryPathFor(filePath) {
|
|
59
|
+
return `${filePath}.${process.pid}.${temporaryCounter++}.tmp`;
|
|
60
|
+
}
|
|
61
|
+
function evictStaleEntries(cache, ttlDays) {
|
|
62
|
+
const cutoff = Date.now() - ttlDays * 24 * 60 * 60 * 1e3;
|
|
63
|
+
const tasks = {};
|
|
64
|
+
for (const [name, entry] of Object.entries(cache.tasks)) {
|
|
65
|
+
if (entry.lastRun >= cutoff) {
|
|
66
|
+
tasks[name] = entry;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return { tasks };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// src/core/insight/analyzer.ts
|
|
73
|
+
function computeInsights(results, cache) {
|
|
74
|
+
const totalDurationMs = results.reduce((sum, r) => sum + r.durationMs, 0);
|
|
75
|
+
const hits = results.filter((r) => r.cacheHit).length;
|
|
76
|
+
const cacheHitRate = results.length === 0 ? 0 : hits / results.length;
|
|
77
|
+
return {
|
|
78
|
+
lastRunResults: results,
|
|
79
|
+
cachedStats: cache.tasks,
|
|
80
|
+
totalDurationMs,
|
|
81
|
+
cacheHitRate
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export {
|
|
86
|
+
readCache,
|
|
87
|
+
writeCache,
|
|
88
|
+
writeCacheSync,
|
|
89
|
+
evictStaleEntries,
|
|
90
|
+
computeInsights
|
|
91
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import {
|
|
2
|
+
packageForFile
|
|
3
|
+
} from "./chunk-3E5LBU3O.js";
|
|
4
|
+
import {
|
|
5
|
+
listChangedFiles
|
|
6
|
+
} from "./chunk-XUBWCTJZ.js";
|
|
7
|
+
|
|
8
|
+
// src/core/cache/git-diff.ts
|
|
9
|
+
import path from "path";
|
|
10
|
+
async function getChangedFiles(options) {
|
|
11
|
+
const { cwd, baseRef = "HEAD~1" } = options;
|
|
12
|
+
return listChangedFiles(cwd, baseRef);
|
|
13
|
+
}
|
|
14
|
+
function changedFilesToPackages(files, graph, cwd) {
|
|
15
|
+
const out = /* @__PURE__ */ new Set();
|
|
16
|
+
for (const file of files) {
|
|
17
|
+
const owner = packageForFile(path.resolve(cwd, file), graph);
|
|
18
|
+
if (owner !== null) out.add(owner.name);
|
|
19
|
+
}
|
|
20
|
+
return out;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export {
|
|
24
|
+
getChangedFiles,
|
|
25
|
+
changedFilesToPackages
|
|
26
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import {
|
|
2
|
+
classifyChange
|
|
3
|
+
} from "./chunk-LJDDS4RR.js";
|
|
4
|
+
import {
|
|
5
|
+
readFileAtRef
|
|
6
|
+
} from "./chunk-XUBWCTJZ.js";
|
|
7
|
+
|
|
8
|
+
// src/core/semantic/file-change.ts
|
|
9
|
+
import { readFile } from "fs/promises";
|
|
10
|
+
import path from "path";
|
|
11
|
+
var DEFAULT_DIFF_BASE_REF = "HEAD~1";
|
|
12
|
+
function toWorkspaceRelative(cwd, filePath) {
|
|
13
|
+
return path.relative(cwd, path.resolve(cwd, filePath)).replace(/\\/g, "/");
|
|
14
|
+
}
|
|
15
|
+
async function classifyFileAgainstRef(cwd, relPath, baseRef) {
|
|
16
|
+
const [before, after] = await Promise.all([
|
|
17
|
+
readFileAtRef(cwd, baseRef, relPath),
|
|
18
|
+
readFile(path.resolve(cwd, relPath), "utf8").catch(() => null)
|
|
19
|
+
]);
|
|
20
|
+
return classifyChange({
|
|
21
|
+
filePath: relPath,
|
|
22
|
+
before: before ?? "",
|
|
23
|
+
after: after ?? ""
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export {
|
|
28
|
+
DEFAULT_DIFF_BASE_REF,
|
|
29
|
+
toWorkspaceRelative,
|
|
30
|
+
classifyFileAgainstRef
|
|
31
|
+
};
|