explainer-video 0.0.0-stage → 0.4.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/.claude-plugin/plugin.json +12 -0
- package/LICENSE +21 -0
- package/README.md +152 -2
- package/cordis.patch.yml +7 -0
- package/lib/index.js +35 -0
- package/package.json +56 -4
- package/plugin.json +13 -0
- package/skills/coursework-author/.synced-from +5 -0
- package/skills/coursework-author/SKILL.md +209 -0
- package/skills/craft-codeflow-flows/SKILL.md +116 -0
- package/skills/craft-codeflow-mine/SKILL.md +207 -0
- package/skills/craft-codeflow-plan/SKILL.md +65 -0
- package/skills/craft-codeflow-scene/SKILL.md +168 -0
- package/skills/craft-video-align/SKILL.md +109 -0
- package/skills/craft-video-assemble/SKILL.md +69 -0
- package/skills/craft-video-audio/SKILL.md +59 -0
- package/skills/craft-video-enrich/SKILL.md +46 -0
- package/skills/craft-video-plan/SKILL.md +57 -0
- package/skills/craft-video-publish/SKILL.md +79 -0
- package/skills/craft-video-render/SKILL.md +68 -0
- package/skills/craft-video-scene/SKILL.md +112 -0
- package/skills/craft-video-script/SKILL.md +80 -0
- package/skills/explainer-video-from-codebase/FLOWS.md +577 -0
- package/skills/explainer-video-from-codebase/SKILL.md +659 -0
- package/skills/explainer-video-from-codebase/tools/apply-flows.mjs +531 -0
- package/skills/explainer-video-from-codebase/tools/approve-flows.mjs +56 -0
- package/skills/explainer-video-from-codebase/tools/codemap-md.mjs +116 -0
- package/skills/explainer-video-from-codebase/tools/config/dotenv.mjs +13 -0
- package/skills/explainer-video-from-codebase/tools/config/json.mjs +14 -0
- package/skills/explainer-video-from-codebase/tools/config/redact.mjs +16 -0
- package/skills/explainer-video-from-codebase/tools/config/toml-lite.mjs +13 -0
- package/skills/explainer-video-from-codebase/tools/config/yaml-lite.mjs +18 -0
- package/skills/explainer-video-from-codebase/tools/excerpt-check.mjs +186 -0
- package/skills/explainer-video-from-codebase/tools/excerpt.mjs +116 -0
- package/skills/explainer-video-from-codebase/tools/flow-diagram.mjs +168 -0
- package/skills/explainer-video-from-codebase/tools/flow-plan.mjs +129 -0
- package/skills/explainer-video-from-codebase/tools/flows-md.mjs +87 -0
- package/skills/explainer-video-from-codebase/tools/lang/c.mjs +684 -0
- package/skills/explainer-video-from-codebase/tools/lang/go.mjs +673 -0
- package/skills/explainer-video-from-codebase/tools/lang/python-config.mjs +143 -0
- package/skills/explainer-video-from-codebase/tools/lang/python-deps.mjs +62 -0
- package/skills/explainer-video-from-codebase/tools/lang/python-stdlib.mjs +24 -0
- package/skills/explainer-video-from-codebase/tools/lang/python.mjs +609 -0
- package/skills/explainer-video-from-codebase/tools/lang/typescript.mjs +963 -0
- package/skills/explainer-video-from-codebase/tools/lib/decompose.mjs +102 -0
- package/skills/explainer-video-from-codebase/tools/lib/groups.mjs +77 -0
- package/skills/explainer-video-from-codebase/tools/lib/observe-coverage.mjs +68 -0
- package/skills/explainer-video-from-codebase/tools/lib/observe-v8.mjs +232 -0
- package/skills/explainer-video-from-codebase/tools/lib/observe.mjs +162 -0
- package/skills/explainer-video-from-codebase/tools/lib/redact.mjs +28 -0
- package/skills/explainer-video-from-codebase/tools/lib/sandbox.mjs +97 -0
- package/skills/explainer-video-from-codebase/tools/lib/test-links.mjs +28 -0
- package/skills/explainer-video-from-codebase/tools/lib/toml.mjs +129 -0
- package/skills/explainer-video-from-codebase/tools/lib/trace-c.mjs +95 -0
- package/skills/explainer-video-from-codebase/tools/lib/trace-go.mjs +101 -0
- package/skills/explainer-video-from-codebase/tools/lib/trace-js.mjs +136 -0
- package/skills/explainer-video-from-codebase/tools/lib/tree-sitter.mjs +19 -0
- package/skills/explainer-video-from-codebase/tools/lib/walk.mjs +138 -0
- package/skills/explainer-video-from-codebase/tools/mine.mjs +409 -0
- package/skills/explainer-video-from-codebase/tools/propose.mjs +127 -0
- package/skills/explainer-video-from-codebase/tools/rank.mjs +119 -0
- package/skills/explainer-video-from-codebase/tools/ranking-md.mjs +76 -0
- package/skills/explainer-video-from-codebase/tools/rules/c-handlers.mjs +53 -0
- package/skills/explainer-video-from-codebase/tools/rules/celery-taskiq.mjs +48 -0
- package/skills/explainer-video-from-codebase/tools/rules/click-typer.mjs +6 -0
- package/skills/explainer-video-from-codebase/tools/rules/express.mjs +72 -0
- package/skills/explainer-video-from-codebase/tools/rules/fastapi.mjs +99 -0
- package/skills/explainer-video-from-codebase/tools/rules/go-http.mjs +173 -0
- package/skills/explainer-video-from-codebase/tools/rules/go-test.mjs +108 -0
- package/skills/explainer-video-from-codebase/tools/rules/grpc.mjs +57 -0
- package/skills/explainer-video-from-codebase/tools/rules/gtest.mjs +97 -0
- package/skills/explainer-video-from-codebase/tools/rules/haystack.mjs +55 -0
- package/skills/explainer-video-from-codebase/tools/rules/jest-vitest.mjs +100 -0
- package/skills/explainer-video-from-codebase/tools/rules/js-http-client.mjs +111 -0
- package/skills/explainer-video-from-codebase/tools/rules/nextjs.mjs +53 -0
- package/skills/explainer-video-from-codebase/tools/rules/process-spawn.mjs +49 -0
- package/skills/explainer-video-from-codebase/tools/rules/py-http-client.mjs +26 -0
- package/skills/explainer-video-from-codebase/tools/rules/pytest.mjs +107 -0
- package/skills/explainer-video-from-codebase/tools/rules/react-router.mjs +17 -0
- package/skills/explainer-video-from-codebase/tools/rules/registry.mjs +31 -0
- package/skills/explainer-video-from-codebase/tools/trace/c/fmtrace-sh +14 -0
- package/skills/explainer-video-from-codebase/tools/trace/js/fmtrace-net.cjs +33 -0
- package/skills/explainer-video-from-codebase/tools/trace/js/fmtrace-srcmap.cjs +41 -0
- package/skills/explainer-video-from-codebase/tools/trace/python/fmtrace_plugin.py +87 -0
- package/skills/explainer-video-from-codebase/tools/trace/python/sitecustomize.py +330 -0
- package/skills/explainer-video-from-codebase/tools/trace/sandbox/linux-bwrap.sh +73 -0
- package/skills/explainer-video-from-codebase/tools/trace/sandbox/macos-nonet.sb +41 -0
- package/skills/explainer-video-from-codebase/tools/trace.mjs +577 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-c.txt +21 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-cpp.txt +21 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-go.txt +21 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-javascript.txt +21 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-python.txt +21 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-typescript.txt +21 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/web-tree-sitter.txt +21 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/MANIFEST.json +172 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-c.wasm +0 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-cpp.wasm +0 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-go.wasm +0 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-javascript.wasm +0 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-python.wasm +0 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-tsx.wasm +0 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-typescript.wasm +0 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/web-tree-sitter.js +4081 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/web-tree-sitter.wasm +0 -0
- package/skills/explainer-video-from-coursework/ARCHITECTURE.md +59 -0
- package/skills/explainer-video-from-coursework/PIPELINE.md +343 -0
- package/skills/explainer-video-from-coursework/ROADMAP.md +725 -0
- package/skills/explainer-video-from-coursework/SERIES.md +232 -0
- package/skills/explainer-video-from-coursework/SETUP.md +105 -0
- package/skills/explainer-video-from-coursework/SKILL.md +858 -0
- package/skills/explainer-video-from-coursework/scenes/_pt-chart.js +12 -0
- package/skills/explainer-video-from-coursework/scenes/_pt-equation.js +10 -0
- package/skills/explainer-video-from-coursework/scenes/_pt-hubspoke.js +7 -0
- package/skills/explainer-video-from-coursework/scenes/_pt-loopflow.js +7 -0
- package/skills/explainer-video-from-coursework/scenes/_pt-sequence.js +12 -0
- package/skills/explainer-video-from-coursework/scenes/_pt-timeline.js +11 -0
- package/skills/explainer-video-from-coursework/src/scene-code.html +127 -0
- package/skills/explainer-video-from-coursework/src/scene-code.js +379 -0
- package/skills/explainer-video-from-coursework/src/scene-core.js +1250 -0
- package/skills/explainer-video-from-coursework/src/scene-diagram.html +50 -0
- package/skills/explainer-video-from-coursework/src/scene-svg.html +50 -0
- package/skills/explainer-video-from-coursework/src/timeline.js +297 -0
- package/skills/explainer-video-from-coursework/src/timing-chart.js +160 -0
- package/skills/explainer-video-from-coursework/tools/align.mjs +419 -0
- package/skills/explainer-video-from-coursework/tools/apply-enrichment.mjs +206 -0
- package/skills/explainer-video-from-coursework/tools/assemble.mjs +223 -0
- package/skills/explainer-video-from-coursework/tools/build-audio.mjs +234 -0
- package/skills/explainer-video-from-coursework/tools/chromium.mjs +73 -0
- package/skills/explainer-video-from-coursework/tools/debug-page.mjs +75 -0
- package/skills/explainer-video-from-coursework/tools/env.mjs +30 -0
- package/skills/explainer-video-from-coursework/tools/frame-dedup.mjs +131 -0
- package/skills/explainer-video-from-coursework/tools/key-source.mjs +151 -0
- package/skills/explainer-video-from-coursework/tools/model.mjs +21 -0
- package/skills/explainer-video-from-coursework/tools/naming.mjs +38 -0
- package/skills/explainer-video-from-coursework/tools/onset-audit.mjs +51 -0
- package/skills/explainer-video-from-coursework/tools/pacing-check.mjs +350 -0
- package/skills/explainer-video-from-coursework/tools/planner.mjs +616 -0
- package/skills/explainer-video-from-coursework/tools/publish.mjs +230 -0
- package/skills/explainer-video-from-coursework/tools/render.mjs +205 -0
- package/skills/explainer-video-from-coursework/tools/resolve-beats.mjs +239 -0
- package/skills/explainer-video-from-coursework/tools/script-check.mjs +174 -0
- package/skills/explainer-video-from-coursework/tools/stamp.mjs +71 -0
- package/skills/explainer-video-from-coursework/tools/timing-report.mjs +299 -0
- package/skills/explainer-video-from-coursework/tools/transcribe-gemini.mjs +96 -0
- package/skills/explainer-video-from-coursework/tools/tts-gemini.mjs +394 -0
- package/skills/explainer-video-from-coursework/tools/verify-audio.mjs +88 -0
- package/skills/explainer-video-from-coursework/tools/workdir.mjs +124 -0
- package/skills/explainer-video-from-coursework/vendor/plotly-basic.min.js +8 -0
- package/skills/explainer-video-from-coursework/vendor/plotly-basic.min.js.LICENSE.txt +16 -0
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* =============================================================================
|
|
3
|
+
mine.mjs — parse every file of a repo, put each in a group, and write the
|
|
4
|
+
code map: symbols, typed call edges, entry points, framework rule links,
|
|
5
|
+
tests with their direct links into the code, and config facts.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
node tools/mine.mjs <repo> --out out/<name> [--group 'glob=group' ...]
|
|
9
|
+
[--exclude p,...] [--lang python,typescript,go,c,cpp] [--config-root <dir>]
|
|
10
|
+
|
|
11
|
+
Languages: Python; TypeScript / JavaScript (.ts .tsx .mts .cts .js .jsx .mjs .cjs;
|
|
12
|
+
`--lang typescript` covers both); Go (.go); C (.c, and a .h that holds C); C++ (.cc
|
|
13
|
+
.cpp .cxx .hpp .hh, and a .h that holds C++). Each is mined by its own adapter into
|
|
14
|
+
one code map; an HTTP client's calls link to the routes a backend in any language
|
|
15
|
+
serves.
|
|
16
|
+
|
|
17
|
+
Writes out/<name>/codemap.json and out/<name>/codemap.md. Never writes inside
|
|
18
|
+
the repo, never uses the network, never runs repo code. Config is read only from
|
|
19
|
+
git-tracked files; a value is kept only for a key a dispatch guard reads, and a
|
|
20
|
+
secret-looking one is shown only as set.
|
|
21
|
+
|
|
22
|
+
Exit: 0 ok · 1 bad path, no supported files, or bad flags · 2 self-check failed
|
|
23
|
+
(a definition the parser saw was not extracted, or two collapsed into one symbol)
|
|
24
|
+
========================================================================== */
|
|
25
|
+
import { readdirSync, readFileSync, lstatSync, writeFileSync, mkdirSync, existsSync, realpathSync } from 'node:fs';
|
|
26
|
+
import { join, relative, extname, resolve, sep, dirname, basename } from 'node:path';
|
|
27
|
+
import { createHash } from 'node:crypto';
|
|
28
|
+
import { execFileSync } from 'node:child_process';
|
|
29
|
+
import { loadGrammars } from './lib/tree-sitter.mjs';
|
|
30
|
+
import { PATH_RULES, DEFAULT_RULE, classifyFile, parseOverrides } from './lib/groups.mjs';
|
|
31
|
+
import { newPythonModel, extractPythonFile, resolverFor, resolveCalls, pythonEntries } from './lang/python.mjs';
|
|
32
|
+
import { TS_LANGS, newTsModel, extractTsFile, tsModuleResolver, tsResolverFor, resolveTsCalls, tsEntries } from './lang/typescript.mjs';
|
|
33
|
+
import { newGoModel, extractGoFile, goModules, goResolverFor, resolveGoCalls, linkGoCallbacks, goEntries } from './lang/go.mjs';
|
|
34
|
+
import { C_LANGS, headerLang, newCModel, extractCFile, cResolverFor, resolveCCalls, cEntries } from './lang/c.mjs';
|
|
35
|
+
import { STDLIB } from './lang/python-stdlib.mjs';
|
|
36
|
+
import { DEPENDENCY_FILES, declaredDependencies } from './lang/python-deps.mjs';
|
|
37
|
+
import { classifyDispatch } from './lang/python-config.mjs';
|
|
38
|
+
import * as fastapi from './rules/fastapi.mjs';
|
|
39
|
+
import * as celeryTaskiq from './rules/celery-taskiq.mjs';
|
|
40
|
+
import * as clickTyper from './rules/click-typer.mjs';
|
|
41
|
+
import * as registry from './rules/registry.mjs';
|
|
42
|
+
import * as haystack from './rules/haystack.mjs';
|
|
43
|
+
import * as pyHttp from './rules/py-http-client.mjs';
|
|
44
|
+
import * as spawn from './rules/process-spawn.mjs';
|
|
45
|
+
import * as pytest from './rules/pytest.mjs';
|
|
46
|
+
import * as express from './rules/express.mjs';
|
|
47
|
+
import * as nextjs from './rules/nextjs.mjs';
|
|
48
|
+
import * as reactRouter from './rules/react-router.mjs';
|
|
49
|
+
import * as jsHttp from './rules/js-http-client.mjs';
|
|
50
|
+
import * as jestVitest from './rules/jest-vitest.mjs';
|
|
51
|
+
import * as goHttp from './rules/go-http.mjs';
|
|
52
|
+
import * as grpc from './rules/grpc.mjs';
|
|
53
|
+
import * as goTest from './rules/go-test.mjs';
|
|
54
|
+
import * as cHandlers from './rules/c-handlers.mjs';
|
|
55
|
+
import * as gtest from './rules/gtest.mjs';
|
|
56
|
+
import { directLinks } from './lib/test-links.mjs';
|
|
57
|
+
import * as dotenv from './config/dotenv.mjs';
|
|
58
|
+
import * as yamlLite from './config/yaml-lite.mjs';
|
|
59
|
+
import * as tomlLite from './config/toml-lite.mjs';
|
|
60
|
+
import * as json from './config/json.mjs';
|
|
61
|
+
import { shownValue } from './config/redact.mjs';
|
|
62
|
+
import { renderCodemap } from './codemap-md.mjs';
|
|
63
|
+
|
|
64
|
+
const fail = (code, msg) => { console.error(`[mine] ${msg}`); process.exit(code); };
|
|
65
|
+
|
|
66
|
+
/* ---------------- args ---------------- */
|
|
67
|
+
const argv = process.argv.slice(2);
|
|
68
|
+
const VALUED = new Set(['--out', '--group', '--exclude', '--lang', '--config-root']);
|
|
69
|
+
const flags = new Map(); const bare = [];
|
|
70
|
+
for (let i = 0; i < argv.length; i++) {
|
|
71
|
+
if (VALUED.has(argv[i])) { if (argv[i + 1] == null) fail(1, `${argv[i]} needs a value`); (flags.get(argv[i]) || flags.set(argv[i], []).get(argv[i])).push(argv[++i]); }
|
|
72
|
+
else if (argv[i].startsWith('--')) fail(1, `unknown flag ${argv[i]}`);
|
|
73
|
+
else bare.push(argv[i]);
|
|
74
|
+
}
|
|
75
|
+
if (bare.length !== 1 || !flags.has('--out')) fail(1, 'usage: mine.mjs <repo> --out out/<name> [--group glob=group] [--exclude p,...] [--lang python,typescript,go,c,cpp] [--config-root <dir>]');
|
|
76
|
+
const ROOT = resolve(bare[0]);
|
|
77
|
+
const OUT = resolve(flags.get('--out').at(-1));
|
|
78
|
+
if (!existsSync(ROOT) || !lstatSync(ROOT).isDirectory()) fail(1, `not a directory: ${bare[0]}`);
|
|
79
|
+
// compare real paths so a symlink on either side cannot route --out into the repo
|
|
80
|
+
const realOut = p => existsSync(p) ? realpathSync(p) : join(realOut(dirname(p)), basename(p));
|
|
81
|
+
const REAL_ROOT = realpathSync(ROOT), REAL_OUT = realOut(OUT);
|
|
82
|
+
if (REAL_OUT === REAL_ROOT || REAL_OUT.startsWith(REAL_ROOT + sep)) fail(1, `refusing to write inside the repo: ${OUT}`);
|
|
83
|
+
let overrides;
|
|
84
|
+
try { overrides = parseOverrides((flags.get('--group') || []).flatMap(s => s.split(',')).filter(Boolean)); } catch (e) { fail(1, e.message); }
|
|
85
|
+
const EXCLUDE = (flags.get('--exclude') || []).flatMap(s => s.split(',')).filter(Boolean).map(p => p.replace(/\/+$/, ''));
|
|
86
|
+
const MINED = ['python', 'typescript', 'go', 'c', 'cpp'];
|
|
87
|
+
const LANGS = new Set((flags.get('--lang') || [MINED.join(',')]).flatMap(s => s.split(',')).filter(Boolean).map(l => {
|
|
88
|
+
if (!MINED.includes(l)) fail(1, `--lang ${l}: mined languages are ${MINED.join(', ')} (javascript is mined with typescript)`);
|
|
89
|
+
return l; }));
|
|
90
|
+
const CONFIG_ROOTS = (flags.get('--config-root') || []).map(d => resolve(d));
|
|
91
|
+
for (const d of CONFIG_ROOTS) if (!existsSync(d)) fail(1, `--config-root ${d} does not exist`);
|
|
92
|
+
|
|
93
|
+
/* ---------------- walk ---------------- */
|
|
94
|
+
const SKIP_DIRS = new Set(['.git', 'node_modules', '.venv', 'venv', '__pycache__', 'dist', 'build']);
|
|
95
|
+
const CODE_EXT = { '.py': 'python', '.ts': 'typescript', '.mts': 'typescript', '.cts': 'typescript', '.tsx': 'tsx', '.js': 'javascript', '.jsx': 'javascript', '.mjs': 'javascript',
|
|
96
|
+
'.cjs': 'javascript', '.go': 'go', '.c': 'c', '.h': 'c', '.cc': 'cpp', '.cpp': 'cpp', '.cxx': 'cpp', '.hpp': 'cpp', '.hh': 'cpp' };
|
|
97
|
+
const READERS = [['dotenv', dotenv], ['yaml-lite', yamlLite], ['toml-lite', tomlLite], ['json', json]];
|
|
98
|
+
const excluded = rel => EXCLUDE.some(p => rel === p || rel.startsWith(p + '/'));
|
|
99
|
+
function walk(root, dir, out, skip) {
|
|
100
|
+
for (const name of readdirSync(dir).sort()) {
|
|
101
|
+
if (SKIP_DIRS.has(name)) continue;
|
|
102
|
+
const p = join(dir, name), rel = relative(root, p).split(sep).join('/'), st = lstatSync(p);
|
|
103
|
+
if (st.isSymbolicLink() || skip(rel)) continue;
|
|
104
|
+
if (st.isDirectory()) { if (!name.startsWith('.')) walk(root, p, out, skip); continue; }
|
|
105
|
+
if (DEPENDENCY_FILES.test(rel)) out.deps.push(rel);
|
|
106
|
+
if (name === 'package.json' || name === 'tsconfig.json' || name === 'jsconfig.json') out.jsMeta.push(rel);
|
|
107
|
+
if (name === 'go.mod') out.goMods.push(rel);
|
|
108
|
+
const reader = READERS.find(([, r]) => r.matches(rel));
|
|
109
|
+
if (reader && st.size < 512 * 1024) out.config.push({ rel, reader: reader[0] });
|
|
110
|
+
if (name.startsWith('.')) continue;
|
|
111
|
+
if (CODE_EXT[extname(name)]) out.code.push(rel);
|
|
112
|
+
}
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
const newWalk = () => ({ code: [], config: [], deps: [], jsMeta: [], goMods: [] });
|
|
116
|
+
// git-tracked files under a directory (relative to it); null when it is not in a git work tree
|
|
117
|
+
const trackedFiles = root => {
|
|
118
|
+
try { return new Set(execFileSync('git', ['-c', 'core.fsmonitor=false', '-C', root, 'ls-files', '-z'], { stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: 1 << 28 })
|
|
119
|
+
.toString().split('\0').filter(Boolean)); } catch { return null; }
|
|
120
|
+
};
|
|
121
|
+
const { code, config, deps, jsMeta, goMods } = walk(ROOT, ROOT, newWalk(), excluded);
|
|
122
|
+
const readCache = new Map();
|
|
123
|
+
const readText = rel => readCache.get(rel) ?? (readCache.set(rel, readFileSync(join(ROOT, rel), 'utf8')), readCache.get(rel));
|
|
124
|
+
// a .h header is C++ when it holds C++, else C
|
|
125
|
+
const langOf = rel => extname(rel) === '.h' ? headerLang(readText(rel)) : CODE_EXT[extname(rel)];
|
|
126
|
+
const mined = rel => { const l = langOf(rel); return l === 'python' ? LANGS.has('python') : TS_LANGS.has(l) ? LANGS.has('typescript') : (l === 'go' || C_LANGS.has(l)) && LANGS.has(l); };
|
|
127
|
+
const pyFiles = code.filter(f => CODE_EXT[extname(f)] === 'python' && mined(f));
|
|
128
|
+
const tsFiles = code.filter(f => TS_LANGS.has(CODE_EXT[extname(f)]) && mined(f));
|
|
129
|
+
const goFiles = code.filter(f => CODE_EXT[extname(f)] === 'go' && mined(f));
|
|
130
|
+
const cFiles = code.filter(f => C_LANGS.has(langOf(f)) && mined(f));
|
|
131
|
+
if (!pyFiles.length && !tsFiles.length && !goFiles.length && !cFiles.length) fail(1, `no supported source files under ${ROOT} (supported: ${[...LANGS].join(', ')})`);
|
|
132
|
+
|
|
133
|
+
/* ---------------- parse + extract ---------------- */
|
|
134
|
+
const sha = s => createHash('sha256').update(s).digest('hex');
|
|
135
|
+
const GRAMMARS = ['python', ...(tsFiles.length ? ['typescript', 'tsx', 'javascript'] : []), ...(goFiles.length ? ['go'] : []),
|
|
136
|
+
...['c', 'cpp'].filter(l => cFiles.some(f => langOf(f) === l))];
|
|
137
|
+
const { Parser, grammars, versions } = await loadGrammars(GRAMMARS);
|
|
138
|
+
const parsers = Object.fromEntries(GRAMMARS.map(l => { const p = new Parser(); p.setLanguage(grammars[l]); return [l, p]; }));
|
|
139
|
+
const parser = parsers.python;
|
|
140
|
+
const py = newPythonModel(), ts = newTsModel(), go = newGoModel(), cm = newCModel();
|
|
141
|
+
const files = [];
|
|
142
|
+
const t0 = Date.now();
|
|
143
|
+
for (const rel of code) {
|
|
144
|
+
const lang = langOf(rel);
|
|
145
|
+
const { group, rule } = classifyFile(rel, overrides, lang === 'python' || TS_LANGS.has(lang) || lang === 'go' || C_LANGS.has(lang) ? () => readText(rel) : null, lang);
|
|
146
|
+
const text = readText(rel);
|
|
147
|
+
const f = { path: rel, group, rule, lang, sha256: sha(text) };
|
|
148
|
+
if (mined(rel) && lang === 'python') {
|
|
149
|
+
const tree = parser.parse(text);
|
|
150
|
+
if (tree.rootNode.hasError) f.parseError = true;
|
|
151
|
+
extractPythonFile(py, rel, tree.rootNode, group, text);
|
|
152
|
+
tree.delete();
|
|
153
|
+
} else if (mined(rel) && C_LANGS.has(lang)) {
|
|
154
|
+
const tree = parsers[lang].parse(text);
|
|
155
|
+
if (tree.rootNode.hasError) f.parseError = true;
|
|
156
|
+
extractCFile(cm, rel, tree.rootNode, group, text, lang);
|
|
157
|
+
tree.delete();
|
|
158
|
+
} else if (mined(rel) && lang === 'go') {
|
|
159
|
+
const tree = parsers.go.parse(text);
|
|
160
|
+
if (tree.rootNode.hasError) f.parseError = true;
|
|
161
|
+
extractGoFile(go, rel, tree.rootNode, group, text);
|
|
162
|
+
tree.delete();
|
|
163
|
+
} else if (mined(rel)) {
|
|
164
|
+
const tree = parsers[lang].parse(text);
|
|
165
|
+
if (tree.rootNode.hasError) f.parseError = true;
|
|
166
|
+
extractTsFile(ts, rel, tree.rootNode, group, text, lang);
|
|
167
|
+
tree.delete();
|
|
168
|
+
} else f.unsupported = true;
|
|
169
|
+
files.push(f);
|
|
170
|
+
}
|
|
171
|
+
const parseMs = Date.now() - t0;
|
|
172
|
+
const groupOfFile = new Map(files.map(f => [f.path, f.group]));
|
|
173
|
+
|
|
174
|
+
/* ---------------- resolve: core rules, then framework packs ---------------- */
|
|
175
|
+
fastapi.registerAliases(py);
|
|
176
|
+
const declared = declaredDependencies(deps.map(rel => ({ rel, text: readText(rel) })));
|
|
177
|
+
const R = resolverFor(py, declared.names);
|
|
178
|
+
const tests = pytest.findTests(py, R);
|
|
179
|
+
const fixtures = pytest.findFixtures(py);
|
|
180
|
+
const fixtureBindings = pytest.bindFixtures(py, tests, fixtures);
|
|
181
|
+
resolveCalls(py, R);
|
|
182
|
+
|
|
183
|
+
const prefixOf = fastapi.routePrefixes(py, R);
|
|
184
|
+
const entries = [];
|
|
185
|
+
for (const d of py.decorated) {
|
|
186
|
+
const e = fastapi.routeEntry(py, d, prefixOf) || celeryTaskiq.consumerEntry(py, d) || clickTyper.commandEntry(py, d, entries);
|
|
187
|
+
if (e) entries.push(e);
|
|
188
|
+
}
|
|
189
|
+
entries.push(...pythonEntries(py, R));
|
|
190
|
+
|
|
191
|
+
// TypeScript / JavaScript: resolve, then its routes and UI routes, before any HTTP client is linked to a route
|
|
192
|
+
const symbolOf = id => py.symbols.get(id) || ts.symbols.get(id) || go.symbols.get(id) || cm.symbols.get(id);
|
|
193
|
+
const groupOfId = id => symbolOf(id)?.group || [py, ts, go, cm].map(x => x.modules.get(id.split(':')[0])?.group).find(Boolean);
|
|
194
|
+
ts.groupOf = groupOfId; py.groupOf = groupOfId; go.groupOf = groupOfId; cm.groupOf = groupOfId;
|
|
195
|
+
const TR = tsResolverFor(ts, tsModuleResolver({ files: tsFiles, readText }));
|
|
196
|
+
const exp = express.expressRoutes(ts, TR);
|
|
197
|
+
resolveTsCalls(ts, TR, { skipHanded: exp.claimed });
|
|
198
|
+
const readJson = rel => { try { return JSON.parse(readText(rel)); } catch { return null; } };
|
|
199
|
+
const packageJsons = jsMeta.filter(f => basename(f) === 'package.json').map(rel => ({ rel, dir: dirname(rel) === '.' ? '' : dirname(rel), json: readJson(rel) }));
|
|
200
|
+
const nextRootDirs = tsFiles.length ? nextjs.nextRoots(packageJsons, code) : [];
|
|
201
|
+
// Go: resolve, then its routers and gRPC services, before any HTTP client is linked to a route
|
|
202
|
+
const GR = goResolverFor(go, goModules(goMods.map(rel => ({ rel, text: readText(rel) }))));
|
|
203
|
+
resolveGoCalls(go, GR);
|
|
204
|
+
const goRoutes = goHttp.goRoutes(go, GR);
|
|
205
|
+
linkGoCallbacks(go, GR, { skipHanded: goRoutes.claimed });
|
|
206
|
+
const rpcEntries = grpc.grpcEntries(go, GR);
|
|
207
|
+
// C / C++: handler tables claim the calls that dispatch through them, then every other call resolves
|
|
208
|
+
const CR = cResolverFor(cm, { files: cFiles });
|
|
209
|
+
const cTables = cHandlers.handlerTables(cm, CR);
|
|
210
|
+
resolveCCalls(cm, CR, { skipCalls: cTables.claimed });
|
|
211
|
+
const routeAliases = []; // a handler a second route also reaches: matched by HTTP clients, not a second entry
|
|
212
|
+
for (const e of [...exp.routes, ...nextjs.nextEntries(ts, TR, nextRootDirs), ...reactRouter.uiRoutes(ts, TR), ...tsEntries(ts),
|
|
213
|
+
...goRoutes.routes, ...rpcEntries, ...goEntries(go), ...cTables.entries, ...cEntries(cm, CR)]) {
|
|
214
|
+
const first = entries.find(x => x.id === e.id);
|
|
215
|
+
if (!first) { entries.push(e); continue; }
|
|
216
|
+
if (e.kind === 'http') routeAliases.push(e);
|
|
217
|
+
if (e.path && first.path !== e.path) (first.alsoAt || (first.alsoAt = [])).push(e.method ? `${e.method} ${e.path}` : e.path);
|
|
218
|
+
}
|
|
219
|
+
for (const e of entries) e.group = groupOfFile.get(e.file) || 'prod';
|
|
220
|
+
for (const e of routeAliases) e.group = groupOfFile.get(e.file) || 'prod';
|
|
221
|
+
|
|
222
|
+
const producers = celeryTaskiq.linkProducers(py, R, entries);
|
|
223
|
+
const registryKeys = registry.linkRegistry(py, producers.dispatchers, producers.consumerByKey);
|
|
224
|
+
const depends = fastapi.linkDepends(py, R);
|
|
225
|
+
const pipelines = haystack.linkPipelines(py, R);
|
|
226
|
+
const httpRoutes = [...entries.filter(e => e.kind === 'http'), ...routeAliases];
|
|
227
|
+
const httpCalls = pyHttp.linkHttpCalls(py, httpRoutes);
|
|
228
|
+
const jsHttpCalls = jsHttp.linkHttpCalls(ts, TR, httpRoutes);
|
|
229
|
+
const grpcCalls = grpc.linkGrpcClients(go, GR, entries.filter(e => e.kind === 'rpc'));
|
|
230
|
+
const spawns = spawn.linkSpawns(py, R, entries);
|
|
231
|
+
pytest.linkFixtures(py, fixtureBindings);
|
|
232
|
+
const mocks = pytest.mockTargets(py, R);
|
|
233
|
+
pytest.directLinks(py, tests, fixtures, mocks);
|
|
234
|
+
// jest / vitest: tests, hooks (autouse fixtures), module mocks and spies, then the direct links over every edge
|
|
235
|
+
const jsConfigs = code.filter(f => /(^|\/)(jest|vitest|vite)\.config\.[cm]?[jt]s$/.test(f)).map(rel => ({ rel, text: readText(rel) }));
|
|
236
|
+
for (const p of packageJsons) if (p.json?.jest) jsConfigs.push({ rel: p.rel, text: JSON.stringify(p.json.jest) });
|
|
237
|
+
const setupFiles = jestVitest.configuredSetupFiles(jsConfigs);
|
|
238
|
+
const jsTests = jestVitest.findTests(ts);
|
|
239
|
+
const hooks = jestVitest.findHooks(ts, setupFiles);
|
|
240
|
+
const hookBindings = jestVitest.bindHooks(jsTests, hooks);
|
|
241
|
+
jestVitest.linkHooks(ts, hookBindings);
|
|
242
|
+
const jsMocks = jestVitest.mockTargets(ts, TR, setupFiles);
|
|
243
|
+
// go test: tests and subtests, TestMain as a hook, test doubles standing in for production interfaces
|
|
244
|
+
const goTests = goTest.findGoTests(go, GR);
|
|
245
|
+
const goHooks = goTest.findGoHooks(go, GR);
|
|
246
|
+
const goHookBindings = goTest.bindGoHooks(goTests, goHooks);
|
|
247
|
+
goTest.linkGoHooks(go, goHookBindings);
|
|
248
|
+
const goMocks = goTest.goMockTargets(go, GR, readText);
|
|
249
|
+
// gtest / Catch2 / C test functions: fixtures as hooks, mocks and fakes standing in for production classes
|
|
250
|
+
const cTests = gtest.findCTests(cm);
|
|
251
|
+
const cHooks = gtest.findCHooks(cm, CR, cTests);
|
|
252
|
+
const cHookBindings = gtest.bindCHooks(cm, CR, cTests, cHooks);
|
|
253
|
+
gtest.linkCHooks(cm, cHookBindings);
|
|
254
|
+
const cMocks = gtest.cMockTargets(cm, CR, cHooks);
|
|
255
|
+
const crossEdges = [...py.edges, ...ts.edges, ...go.edges, ...cm.edges];
|
|
256
|
+
directLinks(jsTests, jsMocks, crossEdges, symbolOf);
|
|
257
|
+
directLinks(goTests, goMocks, crossEdges, symbolOf);
|
|
258
|
+
directLinks(cTests, cMocks, crossEdges, symbolOf);
|
|
259
|
+
const allTests = [...tests, ...jsTests, ...goTests, ...cTests], allFixtures = [...fixtures, ...hooks, ...goHooks, ...cHooks],
|
|
260
|
+
allBindings = [...fixtureBindings, ...hookBindings, ...goHookBindings, ...cHookBindings], allMocks = [...mocks, ...jsMocks, ...goMocks, ...cMocks];
|
|
261
|
+
|
|
262
|
+
/* ---------------- config facts ---------------- */
|
|
263
|
+
const configFiles = [], configKV = [];
|
|
264
|
+
let untrackedConfig = 0;
|
|
265
|
+
const notGitWorkTree = [];
|
|
266
|
+
const readConfig = (root, rel, reader, group, label) => {
|
|
267
|
+
let txt; try { txt = readFileSync(join(root, rel), 'utf8'); } catch { return; }
|
|
268
|
+
const facts = READERS.find(([n]) => n === reader)[1].readFacts(txt);
|
|
269
|
+
configFiles.push({ path: label, group, reader, facts: facts.length, sha256: sha(txt) });
|
|
270
|
+
for (const kv of facts) configKV.push({ ...kv, file: label, group });
|
|
271
|
+
};
|
|
272
|
+
const readTracked = (root, cs, groupOf, labelOf) => { const tracked = trackedFiles(root);
|
|
273
|
+
if (!tracked) { notGitWorkTree.push(root); return; }
|
|
274
|
+
for (const c of cs) if (tracked.has(c.rel)) readConfig(root, c.rel, c.reader, groupOf(c.rel), labelOf(c.rel)); else untrackedConfig++; };
|
|
275
|
+
readTracked(ROOT, config, rel => classifyFile(rel, overrides, null).group, rel => rel);
|
|
276
|
+
for (const d of CONFIG_ROOTS) readTracked(d, walk(d, d, newWalk(), () => false).config, () => 'prod', rel => `${d}/${rel}`);
|
|
277
|
+
const factories = Object.fromEntries([...new Set(py.returns.map(r => r.fn))].map(f => [f, R.factoryReturns(f)])
|
|
278
|
+
.filter(([, v]) => v.length > 1 || v.some(x => x.guards.length)));
|
|
279
|
+
const guardParser = new Parser(); guardParser.setLanguage(grammars.python);
|
|
280
|
+
const dispatch = classifyDispatch({ py, factories, entries, configKV, parser: guardParser });
|
|
281
|
+
// env keys a TypeScript guard tests keep their (redacted) values, like the keys a Python dispatch guard reads
|
|
282
|
+
const tsGuardKeys = new Set([...ts.edges.flatMap(e => e.cfg || []).flatMap(g => [...g.matchAll(/(?:process\.env|import\.meta\.env)\.([A-Za-z_]\w*)/g)].map(m => m[1].toUpperCase())),
|
|
283
|
+
...go.edges.flatMap(e => e.cfg || []).flatMap(g => [...g.matchAll(/\bos\.(?:Getenv|LookupEnv)\(\s*"([A-Za-z_]\w*)"/g)].map(m => m[1].toUpperCase()))]);
|
|
284
|
+
const envReads = [...ts.envReads, ...go.envReads, ...cm.envReads];
|
|
285
|
+
for (const g of cm.edges.flatMap(e => e.cfg || [])) for (const m of g.matchAll(/\bgetenv\(\s*"([A-Za-z_]\w*)"/g)) tsGuardKeys.add(m[1].toUpperCase());
|
|
286
|
+
|
|
287
|
+
/* ---------------- self-check ---------------- */
|
|
288
|
+
const KNOWN_PACKS = new Set(['fastapi', 'starlette', 'celery', 'taskiq', 'haystack', 'pytest', 'pytest_asyncio', 'unittest', 'httpx', 'requests', 'aiohttp', 'click', 'typer', 'subprocess', 'asyncio']);
|
|
289
|
+
const FRAMEWORKS = ['flask', 'django', 'rq', 'dramatiq', 'huey', 'arq', 'tornado', 'sanic', 'falcon', 'bottle', 'pyramid', 'quart', 'litestar', 'aiohttp_jinja2',
|
|
290
|
+
'grpc', 'kafka', 'confluent_kafka', 'aiokafka', 'pika', 'aio_pika', 'nameko', 'airflow', 'prefect', 'dagster', 'luigi', 'langchain', 'langgraph',
|
|
291
|
+
'llama_index', 'ray', 'socketio', 'channels', 'graphene', 'strawberry', 'ariadne', 'apscheduler', 'schedule', 'fire', 'docopt'];
|
|
292
|
+
const JS_FRAMEWORKS = ['koa', 'fastify', '@nestjs/core', '@nestjs/common', '@hapi/hapi', 'hono', 'restify', 'vue-router', '@angular/router', '@tanstack/react-router',
|
|
293
|
+
'@remix-run/react', '@sveltejs/kit', 'nuxt', '@trpc/server', '@trpc/client', 'graphql', '@apollo/server', '@apollo/client', 'socket.io', 'bullmq', 'bull', 'kafkajs',
|
|
294
|
+
'amqplib', 'commander', 'yargs', 'mocha', 'superagent', 'got', 'node-fetch', 'undici', 'electron'];
|
|
295
|
+
const GO_FRAMEWORKS = ['github.com/gorilla/mux', 'github.com/gofiber/fiber', 'github.com/beego/beego', 'github.com/kataras/iris', 'github.com/julienschmidt/httprouter',
|
|
296
|
+
'github.com/segmentio/kafka-go', 'github.com/IBM/sarama', 'github.com/Shopify/sarama', 'github.com/nats-io/nats.go', 'github.com/rabbitmq/amqp091-go', 'github.com/streadway/amqp',
|
|
297
|
+
'github.com/hibiken/asynq', 'go.temporal.io/sdk', 'github.com/spf13/cobra', 'github.com/urfave/cli', 'github.com/robfig/cron', 'github.com/99designs/gqlgen',
|
|
298
|
+
'github.com/graph-gophers/graphql-go', 'connectrpc.com/connect', 'github.com/twitchtv/twirp', 'github.com/aws/aws-lambda-go'];
|
|
299
|
+
const goImported = f => [...go.externalImports.keys()].find(p => p === f || p.startsWith(f + '/'));
|
|
300
|
+
const withoutPack = [...FRAMEWORKS.filter(f => py.externalImports.has(f) && !KNOWN_PACKS.has(f)).map(f => ({ name: f, firstSeen: py.externalImports.get(f) })),
|
|
301
|
+
...JS_FRAMEWORKS.filter(f => ts.externalImports.has(f)).map(f => ({ name: f, firstSeen: ts.externalImports.get(f), lang: 'typescript' })),
|
|
302
|
+
...GO_FRAMEWORKS.filter(goImported).map(f => ({ name: f, firstSeen: go.externalImports.get(goImported(f)), lang: 'go' }))];
|
|
303
|
+
const defLinesRaw = pyFiles.reduce((n, f) => n + (readText(f).match(/^[ \t]*(async[ \t]+def|def|class)[ \t]+[A-Za-z_]/gm) || []).length, 0);
|
|
304
|
+
const inRepoCalls = py.callStats.resolved + py.unresolved.inRepo;
|
|
305
|
+
// calls into an import the resolver called external although an in-repo directory with no __init__.py (a namespace
|
|
306
|
+
// package) has its name: Python may find them in the repo, the code map cannot
|
|
307
|
+
const initDirs = new Set(pyFiles.filter(f => /(^|\/)__init__\.py$/.test(f)).map(f => f.split('/').slice(0, -1).join('/')));
|
|
308
|
+
const pyDirs = new Map();
|
|
309
|
+
for (const f of pyFiles) f.split('/').slice(0, -1).forEach((d, i, a) => { const dir = a.slice(0, i + 1).join('/');
|
|
310
|
+
if (!initDirs.has(dir) && !STDLIB.has(d)) (pyDirs.get(d) || pyDirs.set(d, new Set()).get(d)).add(dir); });
|
|
311
|
+
const likeRepoDirs = [...py.externalCalls].filter(([top]) => pyDirs.has(top)).sort()
|
|
312
|
+
.map(([name, calls]) => ({ name, calls, dirs: [...pyDirs.get(name)].sort() }));
|
|
313
|
+
const likeRepoCalls = likeRepoDirs.reduce((n, x) => n + x.calls, 0);
|
|
314
|
+
const definedSymbols = py.symbols.size - new Set(py.mains).size;
|
|
315
|
+
const selfCheck = {
|
|
316
|
+
defNodes: py.defNodes, defsExtracted: py.defsVisited, defScopes: py.defScopes, defLinesRaw, symbols: py.symbols.size,
|
|
317
|
+
filesWithErrors: files.filter(f => f.parseError).map(f => f.path), dependencyFilesNotParsed: declared.unparsed,
|
|
318
|
+
calls: py.callStats, unresolvedByOrigin: py.unresolved,
|
|
319
|
+
inRepoMissRate: inRepoCalls ? Number((py.unresolved.inRepo / inRepoCalls).toFixed(4)) : 0,
|
|
320
|
+
packageRoots: R.packageRoots,
|
|
321
|
+
crossRootImports: [...py.importNotes.cross.values()].sort((a, b) => b.calls - a.calls || (a.name < b.name ? -1 : 1)),
|
|
322
|
+
ambiguousImports: [...py.importNotes.ambiguous.values()].sort((a, b) => b.calls - a.calls || (a.name < b.name ? -1 : 1)),
|
|
323
|
+
externalLikeRepoDirs: { calls: likeRepoCalls, rate: likeRepoCalls ? Number((likeRepoCalls / (inRepoCalls + likeRepoCalls)).toFixed(4)) : 0, names: likeRepoDirs },
|
|
324
|
+
ok: py.defsVisited === py.defNodes && definedSymbols >= py.defScopes && ts.defsVisited === ts.defNodes && go.defsVisited === go.defNodes && cm.defsVisited === cm.defNodes,
|
|
325
|
+
};
|
|
326
|
+
const tsInRepo = ts.callStats.resolved + ts.unresolved.inRepo;
|
|
327
|
+
if (tsFiles.length) selfCheck.typescript = { defNodes: ts.defNodes, defsExtracted: ts.defsVisited, defScopes: ts.defScopes,
|
|
328
|
+
symbols: ts.symbols.size, calls: ts.callStats, unresolvedByOrigin: ts.unresolved,
|
|
329
|
+
inRepoMissRate: tsInRepo ? Number((ts.unresolved.inRepo / tsInRepo).toFixed(4)) : 0,
|
|
330
|
+
unresolvedImports: [...ts.unresolvedImports.values()].sort((a, b) => a.from < b.from ? -1 : a.from > b.from ? 1 : a.spec < b.spec ? -1 : 1) };
|
|
331
|
+
const goInRepo = go.callStats.resolved + go.unresolved.inRepo;
|
|
332
|
+
if (goFiles.length) selfCheck.go = { defNodes: go.defNodes, defsExtracted: go.defsVisited, defScopes: go.defScopes,
|
|
333
|
+
symbols: go.symbols.size, calls: go.callStats, unresolvedByOrigin: go.unresolved,
|
|
334
|
+
inRepoMissRate: goInRepo ? Number((go.unresolved.inRepo / goInRepo).toFixed(4)) : 0,
|
|
335
|
+
modules: goModules(goMods.map(rel => ({ rel, text: readText(rel) }))).map(m => ({ dir: m.dir || '.', module: m.module })) };
|
|
336
|
+
const cInRepo = cm.callStats.resolved + cm.unresolved.inRepo;
|
|
337
|
+
if (cFiles.length) selfCheck.c = { defNodes: cm.defNodes, defsExtracted: cm.defsVisited, defScopes: cm.defScopes,
|
|
338
|
+
symbols: cm.symbols.size, calls: cm.callStats, unresolvedByOrigin: cm.unresolved,
|
|
339
|
+
inRepoMissRate: cInRepo ? Number((cm.unresolved.inRepo / cInRepo).toFixed(4)) : 0,
|
|
340
|
+
unresolvedIncludes: cm.unresolvedIncludes.sort((a, b) => a.from < b.from ? -1 : a.from > b.from ? 1 : a.line - b.line) };
|
|
341
|
+
|
|
342
|
+
/* ---------------- write ---------------- */
|
|
343
|
+
const symbols = [...py.symbols.values(), ...ts.symbols.values(), ...[...go.symbols.values()].map(({ paramTypes, results, pkg, recvType, ...s }) => s),
|
|
344
|
+
...[...cm.symbols.values()].map(({ paramTypes, result, ns, clsQual, ...s }) => s)]
|
|
345
|
+
.map(({ text, ...s }) => ({ ...s, sha256: sha(text) }));
|
|
346
|
+
const edges = [...py.edges, ...ts.edges, ...go.edges, ...cm.edges];
|
|
347
|
+
const byKey = (xs, k) => xs.reduce((a, x) => (a[x[k]] = (a[x[k]] || 0) + 1, a), {});
|
|
348
|
+
const count = (xs, keys) => Object.fromEntries(keys.map(k => [k, xs.filter(x => x.group === k).length]));
|
|
349
|
+
let commit = null;
|
|
350
|
+
try { commit = execFileSync('git', ['-C', ROOT, 'rev-parse', 'HEAD'], { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim(); } catch {}
|
|
351
|
+
const treeHash = 'sha256:' + sha([...files.map(f => `${f.path}\0${f.sha256}`), ...configFiles.map(f => `${f.path}\0${f.sha256}`)].sort().join('\n'));
|
|
352
|
+
const unsupported = files.filter(f => f.unsupported);
|
|
353
|
+
const prodFns = symbols.filter(s => s.group === 'prod' && (s.kind === 'function' || s.kind === 'method'));
|
|
354
|
+
const pinned = new Set(allTests.flatMap(t => t.direct.map(l => l.to)));
|
|
355
|
+
const G = ['prod', 'test', 'eval', 'dev'];
|
|
356
|
+
const codemap = {
|
|
357
|
+
schema: 1, tool: { name: 'mine.mjs', versions }, root: ROOT, commit, treeHash,
|
|
358
|
+
languages: { python: { files: pyFiles.length, parseErrors: files.filter(f => f.parseError && f.lang === 'python').length },
|
|
359
|
+
...(tsFiles.length ? Object.fromEntries(['typescript', 'javascript'].map(l => { const fs = files.filter(f => !f.unsupported && (l === 'javascript' ? f.lang === 'javascript' : f.lang === 'typescript' || f.lang === 'tsx'));
|
|
360
|
+
return [l, { files: fs.length, parseErrors: fs.filter(f => f.parseError).length }]; })) : {}),
|
|
361
|
+
...(goFiles.length ? { go: { files: goFiles.length, parseErrors: files.filter(f => f.parseError && f.lang === 'go').length } } : {}),
|
|
362
|
+
...Object.fromEntries(['c', 'cpp'].map(l => [l, files.filter(f => !f.unsupported && f.lang === l)]).filter(([, fs]) => fs.length)
|
|
363
|
+
.map(([l, fs]) => [l, { files: fs.length, parseErrors: fs.filter(f => f.parseError).length }])) },
|
|
364
|
+
unsupported: { files: unsupported.length, byExt: byKey(unsupported.map(f => ({ ext: extname(f.path) })), 'ext') },
|
|
365
|
+
groups: { rules: [...PATH_RULES.map(r => r.rule), DEFAULT_RULE], overrides: overrides.map(o => `${o.glob}=${o.group}`),
|
|
366
|
+
files: count(files, G), symbols: count(symbols, G), entries: count(entries, G) },
|
|
367
|
+
files, symbols,
|
|
368
|
+
classes: [...py.classes.values(), ...ts.classes.values()].map(k => ({ id: k.id, bases: [...k.bases, ...(k.implementsIds || [])], baseExprs: k.baseExprs, methods: Object.fromEntries(k.methods), decos: k.decos }))
|
|
369
|
+
.concat([...go.classes.values()].map(k => ({ id: k.id, bases: k.bases, baseExprs: k.baseExprs, methods: Object.fromEntries(k.methods), decos: k.decos, ...(k.iface ? { interface: true } : {}) })))
|
|
370
|
+
.concat([...cm.classes.values()].map(k => ({ id: k.id, bases: k.bases, baseExprs: k.baseExprs, methods: Object.fromEntries(k.methods), decos: k.decos }))),
|
|
371
|
+
edges, entries,
|
|
372
|
+
tests: allTests, fixtures: allFixtures, fixtureBindings: allBindings, mocks: allMocks,
|
|
373
|
+
testSummary: { tests: allTests.length, asserting: allTests.filter(t => t.asserts).length, fixtures: allFixtures.length, autouse: allFixtures.filter(f => f.autouse).length,
|
|
374
|
+
fixtureLinks: allBindings.length, mocks: allMocks.length, mocksResolved: allMocks.filter(m => m.target).length,
|
|
375
|
+
sessionMocks: allMocks.filter(m => m.scope === 'session').length, prodFunctions: prodFns.length, prodFunctionsWithDirectLink: prodFns.filter(s => pinned.has(s.id)).length },
|
|
376
|
+
rules: { routes: entries.filter(e => e.kind === 'http').length, depends, taskiq: producers.links, registry: registryKeys, haystack: pipelines,
|
|
377
|
+
pyHttp: httpCalls, spawn: spawns, callbacks: py.edges.filter(e => e.rule === 'py.callback').length,
|
|
378
|
+
...(tsFiles.length ? { jsHttp: jsHttpCalls, uiRoutes: entries.filter(e => e.kind === 'ui-route').length, express: { mounts: exp.mounts }, nextRoots: nextRootDirs,
|
|
379
|
+
routeAliases: routeAliases.map(e => ({ method: e.method, path: e.path, id: e.id, file: e.file, line: e.line, rule: e.rule })),
|
|
380
|
+
setupFiles: [...new Set([...setupFiles.keys(), ...hooks.filter(h => h.setup).map(h => h.file), ...jsMocks.filter(x => x.scope === 'session').map(x => x.conftest)])].sort() } : {}),
|
|
381
|
+
...(goFiles.length ? { go: { routes: goRoutes.routes.length, mounts: goRoutes.mounts, grpc: rpcEntries.length, grpcCalls } } : {}),
|
|
382
|
+
...(cFiles.length ? { c: { tables: cTables.tables, dispatchers: cTables.dispatchers, fnPointer: cm.edges.filter(e => e.rule === 'c.fn-pointer').length } } : {}) },
|
|
383
|
+
dispatchSites: py.dispatchSites, factories,
|
|
384
|
+
config: { files: configFiles, untracked: untrackedConfig, notGitWorkTree,
|
|
385
|
+
settingsFields: py.settingsFields.map(f => ({ ...f, default: shownValue(f.alias || f.name, f.default) })), settingsProps: py.settingsProps,
|
|
386
|
+
kv: configKV.map(({ value, ...kv }) => dispatch.guardKeys.has(kv.key.toUpperCase()) || tsGuardKeys.has(kv.key.toUpperCase()) ? { ...kv, value: shownValue(kv.key, value) } : kv),
|
|
387
|
+
profiles: dispatch.profiles, sites: dispatch.sites,
|
|
388
|
+
...(envReads.length || tsFiles.length ? { envReads: envReads.map(r => r.default != null ? { ...r, default: shownValue(r.key, r.default) } : r) } : {}) },
|
|
389
|
+
frameworksWithoutPack: withoutPack,
|
|
390
|
+
selfCheck,
|
|
391
|
+
};
|
|
392
|
+
mkdirSync(OUT, { recursive: true });
|
|
393
|
+
writeFileSync(join(OUT, 'codemap.json'), JSON.stringify(codemap, null, 1) + '\n');
|
|
394
|
+
writeFileSync(join(OUT, 'codemap.md'), renderCodemap(codemap));
|
|
395
|
+
|
|
396
|
+
const ek = byKey(edges, 'kind'), nk = byKey(entries, 'kind'), tsum = codemap.testSummary;
|
|
397
|
+
const queueBy = byKey(py.edges.filter(e => e.rule === 'registry').map(e => ({ g: e.group })), 'g');
|
|
398
|
+
console.log(`[mine] ${files.length} files (${pyFiles.length} python, ${tsFiles.length} typescript/javascript${goFiles.length ? `, ${goFiles.length} go` : ''}${cFiles.length ? `, ${cFiles.length} c/c++` : ''}, ${unsupported.length} unsupported) parsed in ${parseMs} ms · ${selfCheck.filesWithErrors.length} with syntax errors · ${configFiles.length} config files`);
|
|
399
|
+
console.log(`[mine] files by group ${JSON.stringify(codemap.groups.files)} · entries by group ${JSON.stringify(codemap.groups.entries)}`);
|
|
400
|
+
console.log(`[mine] ${symbols.length} symbols · ${edges.length} edges ${JSON.stringify(ek)}`);
|
|
401
|
+
console.log(`[mine] entries ${JSON.stringify(nk)} · Depends ${depends.length} · registry producers ${JSON.stringify(queueBy)} · haystack ${pipelines.length} · http ${httpCalls.filter(h => h.handler).length}/${httpCalls.length} · spawn ${spawns.length} · callbacks ${codemap.rules.callbacks}`);
|
|
402
|
+
if (tsFiles.length) console.log(`[mine] typescript/javascript: express routes ${exp.routes.length} · next.js roots ${nextRootDirs.length} · ui routes ${codemap.rules.uiRoutes} · js http ${jsHttpCalls.filter(h => h.handler).length}/${jsHttpCalls.length} · tests ${jsTests.length} · hooks ${hooks.length} · mocks ${jsMocks.length} (resolved ${jsMocks.filter(m => m.target).length})`);
|
|
403
|
+
if (goFiles.length) console.log(`[mine] go: routes ${goRoutes.routes.length} · grpc methods ${rpcEntries.length} · grpc client links ${grpcCalls.length} · tests ${goTests.length} · TestMain ${goHooks.length} · test doubles standing in ${goMocks.length} · in-repo miss rate ${(selfCheck.go.inRepoMissRate * 100).toFixed(1)}%`);
|
|
404
|
+
if (cFiles.length) console.log(`[mine] c/c++: handler tables ${cTables.tables.length} (${cTables.entries.length} commands) · dispatchers ${cTables.dispatchers.length} · function-pointer calls ${codemap.rules.c.fnPointer} · tests ${cTests.length} · fixture hooks ${cHooks.length} · mocks ${cMocks.length} · in-repo miss rate ${(selfCheck.c.inRepoMissRate * 100).toFixed(1)}% · ${cm.unresolvedIncludes.length} quoted includes to no file`);
|
|
405
|
+
console.log(`[mine] tests ${tsum.tests} (asserting ${tsum.asserting}) · fixtures ${tsum.fixtures} (autouse ${tsum.autouse}) · fixture links ${tsum.fixtureLinks} · mocks ${tsum.mocks} (resolved ${tsum.mocksResolved}, session-wide ${tsum.sessionMocks}) · prod functions with a direct test link ${tsum.prodFunctionsWithDirectLink}/${tsum.prodFunctions}`);
|
|
406
|
+
for (const d of notGitWorkTree) console.log(`[mine] config not read from ${d}: not a git work tree, so no config file can be proven tracked`);
|
|
407
|
+
console.log(`[mine] config: ${dispatch.sites.length} factory dispatch sites · profiles ${dispatch.profiles.map(p => `${p.name} (${p.group})`).join(', ')}`);
|
|
408
|
+
console.log(`[mine] self-check ${selfCheck.ok ? 'ok' : 'FAILED'}: ${selfCheck.defsExtracted}/${selfCheck.defNodes} definitions extracted · ${definedSymbols} distinct symbols for ${selfCheck.defScopes} distinct definitions · package roots ${selfCheck.packageRoots.join(', ') || 'none'} · calls across package roots ${selfCheck.crossRootImports.reduce((n, x) => n + x.calls, 0)} · calls lost to an ambiguous import ${selfCheck.ambiguousImports.reduce((n, x) => n + x.calls, 0)} · external calls named like repo dirs ${likeRepoCalls} · in-repo miss rate ${(selfCheck.inRepoMissRate * 100).toFixed(1)}%${selfCheck.typescript ? ` · typescript/javascript ${selfCheck.typescript.defsExtracted}/${selfCheck.typescript.defNodes} definitions, in-repo miss rate ${(selfCheck.typescript.inRepoMissRate * 100).toFixed(1)}%, ${selfCheck.typescript.unresolvedImports.length} relative imports to no file` : ''}${selfCheck.go ? ` · go ${selfCheck.go.defsExtracted}/${selfCheck.go.defNodes} definitions, in-repo miss rate ${(selfCheck.go.inRepoMissRate * 100).toFixed(1)}%` : ''}${selfCheck.c ? ` · c/c++ ${selfCheck.c.defsExtracted}/${selfCheck.c.defNodes} definitions, in-repo miss rate ${(selfCheck.c.inRepoMissRate * 100).toFixed(1)}%` : ''} · wrote ${relative(process.cwd(), OUT) || '.'}/codemap.{json,md}`);
|
|
409
|
+
if (!selfCheck.ok) process.exit(2);
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* =============================================================================
|
|
3
|
+
propose.mjs — one candidate flow per prod, eval and dev entry point, walked
|
|
4
|
+
from the code map; plus how far each test's direct links reach.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
node tools/propose.mjs --codemap out/<name>/codemap.json [--fanin 6] [--leafy 5] [--no-iface-fold]
|
|
8
|
+
|
|
9
|
+
Writes candidates.json beside the code map. Each candidate is
|
|
10
|
+
the tree of paths from its entry: every node with how it was reached and why
|
|
11
|
+
it was folded (helper, interface, alternative, uses-prod), cut into segments
|
|
12
|
+
at same-group crossings, with dispatch points, config-guarded branches, the
|
|
13
|
+
test doubles it refused to walk, and for an eval or dev entry the production
|
|
14
|
+
code it exercises. An entry reached through another candidate's same-group
|
|
15
|
+
crossing is a continuation of that candidate. Tests are not entries: each
|
|
16
|
+
test's direct links (from mine) are walked, stopping at what it mocks, to
|
|
17
|
+
record what it exercises. No candidate is chosen here; rank.mjs orders them.
|
|
18
|
+
|
|
19
|
+
Exit: 0 ok · 1 bad flags, unreadable code map, or the code map inside the repo
|
|
20
|
+
· 2 an entry could not be walked (its symbol is not in the code map)
|
|
21
|
+
========================================================================== */
|
|
22
|
+
import { readFileSync, writeFileSync, existsSync, realpathSync } from 'node:fs';
|
|
23
|
+
import { join, resolve, dirname, sep, relative } from 'node:path';
|
|
24
|
+
import { indexCodemap } from './lib/walk.mjs';
|
|
25
|
+
|
|
26
|
+
const fail = (code, msg) => { console.error(`[propose] ${msg}`); process.exit(code); };
|
|
27
|
+
|
|
28
|
+
/* ---------------- args ---------------- */
|
|
29
|
+
const argv = process.argv.slice(2);
|
|
30
|
+
const VALUED = new Set(['--codemap', '--fanin', '--leafy']);
|
|
31
|
+
const flags = new Map();
|
|
32
|
+
for (let i = 0; i < argv.length; i++) {
|
|
33
|
+
if (VALUED.has(argv[i])) { if (argv[i + 1] == null) fail(1, `${argv[i]} needs a value`); flags.set(argv[i], argv[++i]); }
|
|
34
|
+
else if (argv[i] === '--no-iface-fold') flags.set(argv[i], true);
|
|
35
|
+
else fail(1, `unknown argument ${argv[i]}`);
|
|
36
|
+
}
|
|
37
|
+
if (!flags.has('--codemap')) fail(1, 'usage: propose.mjs --codemap out/<name>/codemap.json [--fanin 6] [--leafy 5] [--no-iface-fold]');
|
|
38
|
+
const count = (flag, d) => { const v = flags.has(flag) ? Number(flags.get(flag)) : d; if (!Number.isInteger(v) || v < 0) fail(1, `${flag} must be a whole number`); return v; };
|
|
39
|
+
const params = { fanin: count('--fanin', 6), leafy: count('--leafy', 5), ifaceFold: !flags.has('--no-iface-fold') };
|
|
40
|
+
const CODEMAP = resolve(flags.get('--codemap'));
|
|
41
|
+
let m;
|
|
42
|
+
try { m = JSON.parse(readFileSync(CODEMAP, 'utf8')); } catch (e) { fail(1, `cannot read the code map ${CODEMAP}: ${e.message}`); }
|
|
43
|
+
if (m.schema !== 1 || !Array.isArray(m.symbols) || !Array.isArray(m.edges)) fail(1, `${CODEMAP} is not a mine.mjs code map (schema 1)`);
|
|
44
|
+
const OUT = dirname(CODEMAP);
|
|
45
|
+
if (m.root && existsSync(m.root)) { const root = realpathSync(m.root), o = realpathSync(OUT); if (o === root || o.startsWith(root + sep)) fail(1, `refusing to write inside the repo: ${OUT}`); }
|
|
46
|
+
|
|
47
|
+
/* ---------------- candidates ---------------- */
|
|
48
|
+
const t0 = Date.now();
|
|
49
|
+
const W = indexCodemap(m, params);
|
|
50
|
+
const { sym, G, declCls, entryIds } = W;
|
|
51
|
+
const entryGroup = new Map(m.entries.map(e => [e.id, e.group]));
|
|
52
|
+
const unwalked = m.entries.filter(e => e.group !== 'test' && !sym.has(e.id));
|
|
53
|
+
const cands = m.entries.filter(e => e.group !== 'test' && sym.has(e.id)).map(e => ({ entry: e, group: e.group, ...W.expand(e, { grp: e.group }) }));
|
|
54
|
+
for (const c of cands) {
|
|
55
|
+
// segments: the entry, then every crossing that stays in the candidate's group; a crossing into another group
|
|
56
|
+
// (an eval's HTTP call into a production route) is an `exercises` link, not a segment
|
|
57
|
+
const roots = [{ root: c.entry.id, from: null }, ...c.crossings.filter(x => x.intoGroup === c.group)
|
|
58
|
+
.filter((x, i, a) => a.findIndex(y => y.to === x.to) === i).map(x => ({ root: x.to, from: x.from, kind: x.kind }))];
|
|
59
|
+
c.segments = roots.map(r => { const sub = W.expand({ id: r.root, kind: 'segment' }, { grp: c.group, stopAtCrossing: true });
|
|
60
|
+
return { ...r, own: sub.own.size, folded: sub.folded.size, ownIds: [...sub.own] }; });
|
|
61
|
+
c.exercises = c.crossings.filter(x => x.intoGroup !== c.group && x.intoGroup !== 'ext').map(({ from, to, kind, evidence }) => ({ from, to, kind, evidence }));
|
|
62
|
+
}
|
|
63
|
+
const parentOf = new Map(); // continuation: an entry reached through a same-group crossing of another candidate
|
|
64
|
+
for (const c of cands) for (const x of c.crossings) if (entryIds.has(x.to) && x.to !== c.entry.id && entryGroup.get(x.to) === c.group)
|
|
65
|
+
(parentOf.get(x.to) || parentOf.set(x.to, new Set()).get(x.to)).add(c.entry.id);
|
|
66
|
+
|
|
67
|
+
/* ---------------- tests: direct links (mine's) and what each reaches ---------------- */
|
|
68
|
+
// reach starts at the direct target with the call site's receiver, stops at the test's mocks and at crossings,
|
|
69
|
+
// and counts only expanded code (and helpers): reach, not proof
|
|
70
|
+
const direct = new Map(), exercised = new Map();
|
|
71
|
+
const addTo = (map, k, t) => (map.get(k) || map.set(k, new Set()).get(k)).add(t);
|
|
72
|
+
const testLinks = (m.tests || []).map(t => {
|
|
73
|
+
const mocked = new Set(t.mocked);
|
|
74
|
+
const ex = new Set();
|
|
75
|
+
for (const l of t.direct) {
|
|
76
|
+
addTo(direct, l.to, t.id);
|
|
77
|
+
const w = W.expand({ id: l.to }, { grp: G(l.to), stopAtCrossing: true, stop: mocked, ctx0: l.recvClass || declCls(l.to) });
|
|
78
|
+
for (const n of w.order) if ((!n.folded || n.folded === 'helper') && !n.id.startsWith('ext:')) ex.add(n.id);
|
|
79
|
+
}
|
|
80
|
+
for (const x of ex) addTo(exercised, x, t.id);
|
|
81
|
+
return { test: t.id, file: t.file, line: t.line, asserts: t.asserts, assertLines: t.assertLines, mocked: t.mocked,
|
|
82
|
+
direct: t.direct.map(({ to, via, kind, rule, line }) => ({ to, via, kind, rule, line })), exercised: ex.size, exercisedIds: [...ex].sort() };
|
|
83
|
+
});
|
|
84
|
+
const walkMs = Date.now() - t0;
|
|
85
|
+
|
|
86
|
+
/* ---------------- write ---------------- */
|
|
87
|
+
const nodeIds = new Set(cands.flatMap(c => [...c.ids]));
|
|
88
|
+
for (const id of [...direct.keys(), ...exercised.keys()]) nodeIds.add(id);
|
|
89
|
+
const symbols = Object.fromEntries([...nodeIds].filter(id => sym.has(id)).sort().map(id => { const s = sym.get(id);
|
|
90
|
+
return [id, { group: s.group, kind: s.kind, file: s.file, start: s.start, end: s.end, module: s.module }]; }));
|
|
91
|
+
const brief = c => ({ group: c.group, kind: c.entry.kind, method: c.entry.method, path: c.entry.path, key: c.entry.key, file: c.entry.file, line: c.entry.line,
|
|
92
|
+
continues: parentOf.has(c.entry.id) ? [...parentOf.get(c.entry.id)].sort() : null,
|
|
93
|
+
symbols: c.ids.size, own: c.own.size, folded: c.folded.size, modules: c.modules.size, maxDepth: c.maxDepth });
|
|
94
|
+
const G4 = ['prod', 'eval', 'dev', 'test'];
|
|
95
|
+
const prodFns = m.symbols.filter(s => s.group === 'prod' && (s.kind === 'function' || s.kind === 'method'));
|
|
96
|
+
const allDoubles = new Set(cands.filter(c => c.group === 'prod').flatMap(c => [...c.testDoubles]));
|
|
97
|
+
const result = {
|
|
98
|
+
schema: 1, tool: { name: 'propose.mjs' }, root: m.root, commit: m.commit, treeHash: m.treeHash, params,
|
|
99
|
+
totals: {
|
|
100
|
+
entries: m.entries.length, byGroup: Object.fromEntries(G4.map(k => [k, m.entries.filter(e => e.group === k).length])),
|
|
101
|
+
candidates: Object.fromEntries(G4.slice(0, 3).map(k => [k, cands.filter(c => c.group === k).length])),
|
|
102
|
+
topLevel: Object.fromEntries(G4.slice(0, 3).map(k => [k, cands.filter(c => c.group === k && !parentOf.has(c.entry.id)).length])),
|
|
103
|
+
continuations: parentOf.size, helpers: m.symbols.filter(s => W.isHelper(s.id)).length,
|
|
104
|
+
testDoublesRefused: allDoubles.size, tests: testLinks.length,
|
|
105
|
+
prodFunctions: prodFns.length, prodFunctionsDirect: prodFns.filter(s => direct.has(s.id)).length,
|
|
106
|
+
prodFunctionsReached: prodFns.filter(s => direct.has(s.id) || exercised.has(s.id)).length,
|
|
107
|
+
},
|
|
108
|
+
unwalked: unwalked.map(e => ({ id: e.id, group: e.group, file: e.file, line: e.line })),
|
|
109
|
+
candidates: Object.fromEntries(cands.map(c => [c.entry.id, { ...brief(c),
|
|
110
|
+
ownIds: [...c.own],
|
|
111
|
+
nodes: c.order.map(n => ({ id: n.id, depth: n.depth, via: n.via, ctx: n.ctx || undefined, parent: n.parent || undefined, folded: n.folded || undefined })),
|
|
112
|
+
crossings: c.crossings, exercises: c.exercises, dispatch: c.dispatch, branches: c.branches, constructs: [...c.constructs],
|
|
113
|
+
testDoubles: [...c.testDoubles], usesProd: [...c.usesProd], crossGroup: [...c.crossGroup],
|
|
114
|
+
segments: c.segments }])),
|
|
115
|
+
testLinks,
|
|
116
|
+
bySymbol: Object.fromEntries([...new Set([...direct.keys(), ...exercised.keys()])].sort()
|
|
117
|
+
.map(s => [s, { direct: [...(direct.get(s) || [])].sort(), exercised: exercised.get(s)?.size || 0 }])),
|
|
118
|
+
symbols,
|
|
119
|
+
};
|
|
120
|
+
writeFileSync(join(OUT, 'candidates.json'), JSON.stringify(result, null, 1) + '\n');
|
|
121
|
+
|
|
122
|
+
const t = result.totals;
|
|
123
|
+
console.log(`[propose] ${t.entries} entries ${JSON.stringify(t.byGroup)} walked in ${walkMs} ms · ${t.continuations} continuations · top-level ${JSON.stringify(t.topLevel)}`);
|
|
124
|
+
console.log(`[propose] helpers (same-group fan-in >= ${params.fanin}, subtree <= ${params.leafy}) ${t.helpers} · test doubles refused by production walks ${t.testDoublesRefused} · interface folding ${params.ifaceFold ? 'on' : 'off'}`);
|
|
125
|
+
console.log(`[propose] tests ${t.tests} · production functions ${t.prodFunctions}: direct test links ${t.prodFunctionsDirect}, reached from tests ${t.prodFunctionsReached}`);
|
|
126
|
+
console.log(`[propose] wrote ${relative(process.cwd(), join(OUT, 'candidates.json'))}`);
|
|
127
|
+
if (unwalked.length) { console.error(`[propose] ${unwalked.length} entries could not be walked (no such symbol in the code map): ${unwalked.map(e => e.id).join(', ')}`); process.exit(2); }
|