scripts/code-map.mjs
342 lines and 43 functions in DrDustinEdwards/devkit, at commit 505cda4 on 2026-10-24. This file on GitHub.
Functions
| Function | Lines | Complexity | Covered | CRAP | Mutation |
|---|---|---|---|---|---|
| crap | 36 to 39 | 2 | 100% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| loadTypeScript | 42 to 51 | 3 | 70% | OkCRAP 3.2 | No data: mutation testing has not run on this repo yet |
| functionsOf | 59 to 94 | 1 | 100% | OkCRAP 1 | No data: mutation testing has not run on this repo yet |
| isFunction | 64 to 66 | 7 | 100% | WatchCRAP 7 | No data: mutation testing has not run on this repo yet |
| lineOf | 67 to 67 | 1 | 100% | OkCRAP 1 | No data: mutation testing has not run on this repo yet |
| nameOf | 68 to 74 | 11 | 86% | WatchCRAP 11.4 | No data: mutation testing has not run on this repo yet |
| branches | 75 to 87 | 2 | 100% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| walk | 77 to 83 | 13 | 100% | WatchCRAP 13 | No data: mutation testing has not run on this repo yet |
| visit | 88 to 91 | 3 | 100% | OkCRAP 3 | No data: mutation testing has not run on this repo yet |
| specifiersOf | 97 to 107 | 1 | 100% | OkCRAP 1 | No data: mutation testing has not run on this repo yet |
| visit | 100 to 104 | 9 | 100% | WatchCRAP 9 | No data: mutation testing has not run on this repo yet |
| cyclesOf | 110 to 141 | 3 | 100% | OkCRAP 3 | No data: mutation testing has not run on this repo yet |
| connect | 117 to 138 | 10 | 100% | WatchCRAP 10 | No data: mutation testing has not run on this repo yet |
| <anonymous:140> | 140 to 140 | 1 | 100% | OkCRAP 1 | No data: mutation testing has not run on this repo yet |
| innermost | 148 to 152 | 6 | 100% | WatchCRAP 6 | No data: mutation testing has not run on this repo yet |
| sourceOf | 160 to 165 | 3 | 100% | OkCRAP 3 | No data: mutation testing has not run on this repo yet |
| walk | 168 to 174 | 1 | 100% | OkCRAP 1 | No data: mutation testing has not run on this repo yet |
| <anonymous:169> | 169 to 173 | 4 | 100% | OkCRAP 4 | No data: mutation testing has not run on this repo yet |
| lcovCoverage | 180 to 198 | 7 | 100% | WatchCRAP 7 | No data: mutation testing has not run on this repo yet |
| keptReport | 209 to 222 | 8 | 100% | WatchCRAP 8 | No data: mutation testing has not run on this repo yet |
| <anonymous:215> | 215 to 215 | 1 | 100% | OkCRAP 1 | No data: mutation testing has not run on this repo yet |
| <anonymous:216> | 216 to 216 | 1 | 100% | OkCRAP 1 | No data: mutation testing has not run on this repo yet |
| codeMap | 228 to 320 | 8 | 100% | WatchCRAP 8 | No data: mutation testing has not run on this repo yet |
| onUnRecoverableConfigFileDiagnostic | 230 to 230 | 1 | 100% | OkCRAP 1 | No data: mutation testing has not run on this repo yet |
| rel | 232 to 232 | 1 | 100% | OkCRAP 1 | No data: mutation testing has not run on this repo yet |
| <anonymous:235> | 235 to 235 | 5 | 100% | OkCRAP 5 | No data: mutation testing has not run on this repo yet |
| <anonymous:235> | 235 to 235 | 2 | 100% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| <anonymous:235> | 235 to 235 | 1 | 100% | OkCRAP 1 | No data: mutation testing has not run on this repo yet |
| <anonymous:241> | 241 to 306 | 21 | 100% | WatchCRAP 21 | No data: mutation testing has not run on this repo yet |
| <anonymous:253> | 253 to 253 | 1 | 100% | OkCRAP 1 | No data: mutation testing has not run on this repo yet |
| <anonymous:268> | 268 to 268 | 1 | 100% | OkCRAP 1 | No data: mutation testing has not run on this repo yet |
| body | 281 to 281 | 1 | 100% | OkCRAP 1 | No data: mutation testing has not run on this repo yet |
| keyed | 282 to 285 | 1 | 100% | OkCRAP 1 | No data: mutation testing has not run on this repo yet |
| <anonymous:284> | 284 to 284 | 2 | 100% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| <anonymous:287> | 287 to 287 | 1 | 100% | OkCRAP 1 | No data: mutation testing has not run on this repo yet |
| <anonymous:288> | 288 to 292 | 3 | 100% | OkCRAP 3 | No data: mutation testing has not run on this repo yet |
| <anonymous:298> | 298 to 298 | 1 | 100% | OkCRAP 1 | No data: mutation testing has not run on this repo yet |
| <anonymous:300> | 300 to 304 | 8 | 100% | WatchCRAP 8 | No data: mutation testing has not run on this repo yet |
| args | 323 to 331 | 5 | 0% | WatchCRAP 30 | No data: mutation testing has not run on this repo yet |
| json | 336 to 336 | 3 | 0% | WatchCRAP 12 | No data: mutation testing has not run on this repo yet |
| <anonymous:357> | 357 to 357 | 1 | 0% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| <anonymous:358> | 358 to 358 | 1 | 0% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| <anonymous:358> | 358 to 358 | 1 | 100% | OkCRAP 1 | No data: mutation testing has not run on this repo yet |
Imports
It imports no other module of this repo. No module of this repo imports it.
Source
#!/usr/bin/env node
// The code map: one JSON file per public repository saying,
// for every module, how big it is, what it imports and whether it sits in an import cycle, and for every function its
// cyclomatic complexity, its share of statements covered, its CRAP score and, when a Stryker report is given, its mutants
// and mutation score. Capsid publishes it and the explorer at capsid.dustinedwards.info draws it.
//
// Everything is read through the repository's own TypeScript, so there is no dependency here and no second parser:
// imports are resolved by ts.resolveModuleName with the repository's compiler options, and complexity is counted on the
// syntax tree. Coverage is Vitest's (istanbul's) coverage-final.json, or the lcov file `node --test
// --experimental-test-coverage --test-reporter=lcov` writes, and mutation is Stryker's mutation.json, each mapped onto the
// innermost function whose lines hold a statement (an lcov line) or a mutant. A number nobody measured is null, never 0.
//
// TypeScript and JavaScript alike: .ts, .tsx, .js, .mjs and the rest, never a declaration file or a *.test.* or *.spec.*
// file. A repository with no tsconfig is read as plain modules (allowJs, NodeNext). `--include .` maps every folder, and
// `--exclude <prefix>` (repeatable) leaves a path out.
//
// Mutation runs less often than the map (weekly, design D11), so a run's reports are kept as one smaller file, mutation.json,
// beside the map: the commit and day of the run, and per file its source then and each mutant's status and line. A later map
// reads that file, places each mutant in the function it was made in from the source then, and carries the numbers to a
// function whose lines are unchanged since. A function that changed, or is new, is `stale: true` with no numbers, and the
// map names the run it read as `mutated`.
//
// node code-map.mjs --out code-map.json [--tsconfig tsconfig.json] [--include src/]... [--exclude <prefix>]... [--coverage <file>]
// [--mutation <stryker report>]... [--mutation-kept <mutation.json>] [--mutation-out <mutation.json>]
//
// The repository and commit come from GITHUB_REPOSITORY and GITHUB_SHA, or --repo and --sha. code-map.yml carries this file
// byte for byte; after an edit run `node scripts/inline-rollback-guard.mjs`.
import { execFileSync } from "node:child_process";
import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
import { createRequire } from "node:module";
import { join, relative, resolve, sep } from "node:path";
import { pathToFileURL } from "node:url";
/** CRAP = CC² × (1 − coverage)³ + CC (crap4j's formula), rounded to two places. Null when coverage was not measured. */
export function crap(/** @type {number} */ cc, /** @type {number | null} */ coverage) {
if (coverage === null) return null;
return Math.round((cc * cc * (1 - coverage) ** 3 + cc) * 100) / 100;
}
/** The repository's TypeScript, from its own node_modules, so the map parses what its compiler parses. */
export function loadTypeScript(/** @type {string} */ root) {
for (const base of [join(root, "package.json"), import.meta.url]) {
try {
return createRequire(base)("typescript");
} catch {
// Try the next place.
}
}
throw new Error("typescript is not installed in the repository; the code map reads its code through it");
}
/**
* Every function in a source file with its lines and cyclomatic complexity: 1, plus one for each branch (if, ?:, a loop,
* a case, a catch, &&, ||, ?? and their assignments) in its own body, not in a function nested inside it.
* @param {any} ts @param {any} sf
* @returns {{ name: string, line: number, end: number, cc: number }[]}
*/
export function functionsOf(ts, sf) {
/** @type {{ name: string, line: number, end: number, cc: number }[]} */
const out = [];
const K = ts.SyntaxKind;
const LOGICAL = new Set([K.AmpersandAmpersandToken, K.BarBarToken, K.QuestionQuestionToken, K.AmpersandAmpersandEqualsToken, K.BarBarEqualsToken, K.QuestionQuestionEqualsToken]);
const isFunction = (/** @type {any} */ n) =>
ts.isFunctionDeclaration(n) || ts.isFunctionExpression(n) || ts.isArrowFunction(n) || ts.isMethodDeclaration(n) ||
ts.isConstructorDeclaration(n) || ts.isGetAccessorDeclaration(n) || ts.isSetAccessorDeclaration(n);
const lineOf = (/** @type {number} */ pos) => sf.getLineAndCharacterOfPosition(pos).line + 1;
const nameOf = (/** @type {any} */ n) => {
if (ts.isConstructorDeclaration(n)) return "constructor";
if (n.name && (ts.isIdentifier(n.name) || ts.isStringLiteral(n.name) || ts.isPrivateIdentifier(n.name))) return n.name.text;
const p = n.parent;
if (p && (ts.isVariableDeclaration(p) || ts.isPropertyAssignment(p) || ts.isPropertyDeclaration(p)) && ts.isIdentifier(p.name)) return p.name.text;
return `<anonymous:${lineOf(n.getStart(sf))}>`;
};
const branches = (/** @type {any} */ n) => {
let count = 0;
const walk = (/** @type {any} */ c) => {
if (isFunction(c)) return;
if (ts.isIfStatement(c) || ts.isConditionalExpression(c) || ts.isForStatement(c) || ts.isForInStatement(c) ||
ts.isForOfStatement(c) || ts.isWhileStatement(c) || ts.isDoStatement(c) || ts.isCaseClause(c) || ts.isCatchClause(c)) count++;
else if (ts.isBinaryExpression(c) && LOGICAL.has(c.operatorToken.kind)) count++;
ts.forEachChild(c, walk);
};
if (ts.isBlock(n.body)) ts.forEachChild(n.body, walk);
else walk(n.body);
return count;
};
const visit = (/** @type {any} */ n) => {
if (isFunction(n) && n.body) out.push({ name: nameOf(n), line: lineOf(n.getStart(sf)), end: lineOf(n.getEnd()), cc: 1 + branches(n) });
ts.forEachChild(n, visit);
};
visit(sf);
return out;
}
/** The module specifiers a file imports or re-exports, static and dynamic. @param {any} ts @param {any} sf */
export function specifiersOf(ts, sf) {
/** @type {string[]} */
const out = [];
const visit = (/** @type {any} */ n) => {
if ((ts.isImportDeclaration(n) || ts.isExportDeclaration(n)) && n.moduleSpecifier && ts.isStringLiteral(n.moduleSpecifier)) out.push(n.moduleSpecifier.text);
else if (ts.isCallExpression(n) && n.expression.kind === ts.SyntaxKind.ImportKeyword && n.arguments[0] && ts.isStringLiteral(n.arguments[0])) out.push(n.arguments[0].text);
ts.forEachChild(n, visit);
};
visit(sf);
return out;
}
/** Every import cycle (Tarjan's strongly connected components of two or more modules, or one that imports itself). */
export function cyclesOf(/** @type {Map<string, string[]>} */ graph) {
let index = 0;
/** @type {Map<string, number>} */ const idx = new Map();
/** @type {Map<string, number>} */ const low = new Map();
/** @type {string[]} */ const stack = [];
const on = new Set();
/** @type {string[][]} */ const out = [];
const connect = (/** @type {string} */ v) => {
idx.set(v, index);
low.set(v, index++);
stack.push(v);
on.add(v);
for (const w of graph.get(v) ?? []) {
if (!idx.has(w)) {
connect(w);
low.set(v, Math.min(/** @type {number} */ (low.get(v)), /** @type {number} */ (low.get(w))));
} else if (on.has(w)) low.set(v, Math.min(/** @type {number} */ (low.get(v)), /** @type {number} */ (idx.get(w))));
}
if (low.get(v) === idx.get(v)) {
/** @type {string[]} */ const component = [];
let w;
do {
w = /** @type {string} */ (stack.pop());
on.delete(w);
component.push(w);
} while (w !== v);
if (component.length > 1 || (graph.get(v) ?? []).includes(v)) out.push(component.sort());
}
};
for (const v of [...graph.keys()].sort()) if (!idx.has(v)) connect(v);
return out.sort((a, b) => a[0].localeCompare(b[0]));
}
/**
* The innermost function whose lines hold `line`, or undefined.
* @template {{ line: number, end: number }} F
* @param {F[]} fns @param {number} line
*/
function innermost(fns, line) {
let best;
for (const f of fns) if (f.line <= line && line <= f.end && (!best || f.end - f.line < best.end - best.line)) best = f;
return best;
}
/** A module the map reads: source, not a declaration file and not a test. */
const SOURCE = /\.[cm]?[jt]sx?$/;
const DECLARATION = /\.d\.[cm]?ts$/;
const TEST = /\.(test|spec)\.[cm]?[jt]sx?$/;
/** The JavaScript source a hand-written declaration file describes, when it sits beside it: x.d.mts for x.mjs. */
function sourceOf(/** @type {string} */ file) {
const m = /^(.*)\.d\.([cm]?)ts$/.exec(file);
if (!m) return file;
const js = `${m[1]}.${m[2]}js`;
return existsSync(js) ? js : file;
}
/** Every file under a directory, leaving out node_modules and dot-folders. @param {string} dir @returns {string[]} */
function walk(dir) {
return readdirSync(dir).flatMap((name) => {
if (name === "node_modules" || name.startsWith(".")) return [];
const full = join(dir, name);
return statSync(full).isDirectory() ? walk(full) : [full];
});
}
/**
* lcov as istanbul's coverage-final.json shape: per file, each instrumented line (DA) a statement with its hit count.
* Paths are resolved against the repository's root. @param {string} text @param {string} root
*/
export function lcovCoverage(text, root) {
/** @type {Record<string, { statementMap: Record<string, { start: { line: number } }>, s: Record<string, number> }>} */
const out = {};
let file = null;
for (const raw of text.split("\n")) {
const line = raw.trim();
if (line.startsWith("SF:")) {
// Forward slashes, as TypeScript names its files, so the lookup also matches on Windows.
const abs = resolve(root, line.slice(3)).replaceAll("\\", "/");
file = out[abs] ??= { statementMap: {}, s: {} };
} else if (line.startsWith("DA:") && file) {
const [n, hits] = line.slice(3).split(",").map(Number);
const id = String(Object.keys(file.statementMap).length);
file.statementMap[id] = { start: { line: n } };
file.s[id] = hits;
} else if (line === "end_of_record") file = null;
}
return out;
}
/** Stryker's statuses: detected and valid, as its mutation score counts them. */
const DETECTED = new Set(["Killed", "Timeout"]);
const VALID = new Set(["Killed", "Timeout", "Survived", "NoCoverage"]);
/**
* One kept mutation report from a run's Stryker reports (one per chunk or shard): per file its source and each valid
* mutant's status and line, with the run's commit and day.
* @param {any[]} reports @param {string} sha @param {string} date
*/
export function keptReport(reports, sha, date) {
/** @type {Record<string, { source: string | null, mutants: { status: string, location: { start: { line: number } } }[] }>} */
const files = {};
for (const r of reports) {
for (const [path, f] of Object.entries(r?.files ?? {})) {
const mutants = (/** @type {any} */ (f).mutants ?? [])
.filter((/** @type {any} */ m) => VALID.has(m.status))
.map((/** @type {any} */ m) => ({ status: m.status, location: { start: { line: m.location.start.line } } }));
const was = files[path];
files[path] = { source: was?.source ?? /** @type {any} */ (f).source ?? null, mutants: [...(was?.mutants ?? []), ...mutants] };
}
}
return { version: 1, sha, date, files };
}
/**
* The code map of a repository.
* @param {{ ts: any, root: string, tsconfig?: string, include?: string[], exclude?: string[], coverage?: any, mutation?: any, repo: string, sha: string, date: string }} o
*/
export function codeMap({ ts, root, tsconfig = "tsconfig.json", include = ["src/"], exclude = [], coverage = null, mutation = null, repo, sha, date }) {
const parsed = existsSync(resolve(root, tsconfig))
? ts.getParsedCommandLineOfConfigFile(resolve(root, tsconfig), {}, { ...ts.sys, onUnRecoverableConfigFileDiagnostic: (/** @type {any} */ d) => { throw new Error(ts.flattenDiagnosticMessageText(d.messageText, "\n")); } })
: { fileNames: walk(root), options: { allowJs: true, module: ts.ModuleKind.NodeNext, moduleResolution: ts.ModuleResolutionKind.NodeNext, target: ts.ScriptTarget.ES2022 } };
const rel = (/** @type {string} */ abs) => relative(root, abs).split(sep).join("/");
// A package that ships hand-written .d.mts beside its .mjs: the compiler lists the declaration in the source's place.
const files = [...new Set(parsed.fileNames.map(sourceOf))]
.filter((f) => SOURCE.test(f) && !DECLARATION.test(f) && !TEST.test(f) && include.some((p) => p === "." || rel(f).startsWith(p)) && !exclude.some((p) => rel(f).startsWith(p)))
.sort();
const mapped = new Set(files.map(rel));
const host = ts.createCompilerHost(parsed.options);
/** @type {Map<string, string[]>} */
const graph = new Map();
const modules = files.map((abs) => {
const path = rel(abs);
const text = readFileSync(abs, "utf8");
const sf = ts.createSourceFile(abs, text, ts.ScriptTarget.Latest, true);
const imports = new Set();
for (const spec of specifiersOf(ts, sf)) {
const hit = ts.resolveModuleName(spec, abs, parsed.options, host).resolvedModule;
const to = hit && rel(sourceOf(hit.resolvedFileName));
if (to && mapped.has(to)) imports.add(to);
}
graph.set(path, [...imports].sort());
/** @type {{ name: string, line: number, end: number, cc: number, statements: number, covered: number, mutants: number, detected: number, stale?: true }[]} */
const fns = functionsOf(ts, sf).map((f) => ({ ...f, statements: 0, covered: 0, mutants: 0, detected: 0 }));
const cov = coverage?.[abs] ?? coverage?.[path];
if (cov) {
for (const [id, s] of Object.entries(cov.statementMap ?? {})) {
const f = innermost(fns, /** @type {any} */ (s).start.line);
if (!f) continue;
f.statements++;
if ((cov.s?.[id] ?? 0) > 0) f.covered++;
}
}
const mut = mutation?.files?.[path] ?? mutation?.files?.[abs];
if (mut) {
// Each mutant goes to the function it was made in, read from the file as the run saw it, so lines that moved since
// still meet their function.
const then = typeof mut.source === "string" ? functionsOf(ts, ts.createSourceFile(abs, mut.source, ts.ScriptTarget.Latest, true)).map((f) => ({ ...f, mutants: 0, detected: 0 })) : fns;
for (const m of mut.mutants ?? []) {
if (!VALID.has(m.status)) continue;
const f = innermost(then, m.location.start.line);
if (!f) continue;
f.mutants++;
if (DETECTED.has(m.status)) f.detected++;
}
if (then !== fns) {
// A function keeps the run's numbers only when its lines are the same as then: the nth function of a name now
// against the nth then.
const linesThen = mut.source.split("\n");
const linesNow = text.split("\n");
const body = (/** @type {string[]} */ lines, /** @type {{ line: number, end: number }} */ f) => lines.slice(f.line - 1, f.end).join("\n");
const keyed = (/** @type {{ name: string }[]} */ list) => {
/** @type {Map<string, number>} */ const n = new Map();
return list.map((f) => { const i = n.get(f.name) ?? 0; n.set(f.name, i + 1); return `${f.name}#${i}`; });
};
const keysThen = keyed(then);
const byKey = new Map(then.map((f, i) => [keysThen[i], f]));
keyed(fns).forEach((key, i) => {
const was = byKey.get(key);
if (was && body(linesThen, was) === body(linesNow, fns[i])) Object.assign(fns[i], { mutants: was.mutants, detected: was.detected });
else Object.assign(fns[i], { stale: true });
});
}
}
return {
path,
loc: text.split("\n").filter((l) => l.trim() !== "").length,
imports: graph.get(path),
functions: fns.map(({ name, line, end, cc, statements, covered, mutants, detected, stale }) => {
const c = cov ? (statements === 0 ? 1 : Math.round((covered / statements) * 1000) / 1000) : null;
const measured = mut && !stale;
return { name, line, end, cc, coverage: c, crap: crap(cc, c), mutants: measured ? mutants : null, score: measured && mutants > 0 ? Math.round((detected / mutants) * 1000) / 10 : null, ...(stale ? { stale } : {}) };
}),
};
});
const cycles = cyclesOf(graph);
const inCycle = new Set(cycles.flat());
for (const m of modules) if (inCycle.has(m.path)) Object.assign(m, { cycle: true });
return {
version: 1,
repo,
sha,
date,
tools: { typescript: ts.version, coverage: coverage ? "istanbul" : null, mutation: mutation ? "stryker" : null },
...(mutation?.sha && mutation.sha !== sha ? { mutated: { sha: mutation.sha, date: mutation.date } } : {}),
modules,
cycles,
};
}
/** --name value pairs; a repeated name collects every value. @param {string[]} argv */
function args(argv) {
/** @type {Record<string, string[]>} */
const out = {};
for (let i = 0; i < argv.length; i += 2) {
if (!argv[i].startsWith("--") || argv[i + 1] === undefined) throw new Error(`expected --name value, got ${argv[i]}`);
(out[argv[i].slice(2)] ??= []).push(argv[i + 1]);
}
return out;
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
const a = args(process.argv.slice(2));
const root = process.cwd();
const json = (/** @type {string | undefined} */ f) => (f && existsSync(f) ? JSON.parse(readFileSync(f, "utf8")) : null);
for (const f of [a.coverage?.[0], ...(a.mutation ?? []), a["mutation-kept"]?.[0]]) if (f && !existsSync(f)) console.log(`::warning::${f} does not exist, so the map has no numbers from it`);
const sha = a.sha?.[0] ?? process.env.GITHUB_SHA ?? execFileSync("git", ["rev-parse", "HEAD"], { encoding: "utf8" }).trim();
const date = new Date().toISOString().slice(0, 10);
// A fresh run's reports win over a kept one; with neither, the map has no mutation numbers.
const reports = (a.mutation ?? []).map(json).filter(Boolean);
const mutation = reports.length ? keptReport(reports, sha, date) : json(a["mutation-kept"]?.[0]);
if (mutation && a["mutation-out"]?.[0]) writeFileSync(a["mutation-out"][0], `${JSON.stringify(mutation)}\n`);
const map = codeMap({
ts: loadTypeScript(root),
root,
tsconfig: a.tsconfig?.[0],
include: a.include,
exclude: a.exclude,
coverage: a.coverage?.[0] && existsSync(a.coverage[0]) && !a.coverage[0].endsWith(".json") ? lcovCoverage(readFileSync(a.coverage[0], "utf8"), root) : json(a.coverage?.[0]),
mutation,
repo: a.repo?.[0] ?? process.env.GITHUB_REPOSITORY ?? "",
sha,
date,
});
writeFileSync(a.out?.[0] ?? "code-map.json", `${JSON.stringify(map, null, 2)}\n`);
const fns = map.modules.reduce((n, m) => n + m.functions.length, 0);
const stale = map.modules.reduce((n, m) => n + m.functions.filter((f) => f.stale).length, 0);
console.log(`code map: ${map.modules.length} modules, ${fns} functions, ${map.cycles.length} cycles${map.mutated ? `; mutation from ${map.mutated.sha.slice(0, 7)} (${map.mutated.date}), ${stale} functions stale` : ""}`);
}