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.
Files changed (150) hide show
  1. package/.claude-plugin/plugin.json +12 -0
  2. package/LICENSE +21 -0
  3. package/README.md +152 -2
  4. package/cordis.patch.yml +7 -0
  5. package/lib/index.js +35 -0
  6. package/package.json +56 -4
  7. package/plugin.json +13 -0
  8. package/skills/coursework-author/.synced-from +5 -0
  9. package/skills/coursework-author/SKILL.md +209 -0
  10. package/skills/craft-codeflow-flows/SKILL.md +116 -0
  11. package/skills/craft-codeflow-mine/SKILL.md +207 -0
  12. package/skills/craft-codeflow-plan/SKILL.md +65 -0
  13. package/skills/craft-codeflow-scene/SKILL.md +168 -0
  14. package/skills/craft-video-align/SKILL.md +109 -0
  15. package/skills/craft-video-assemble/SKILL.md +69 -0
  16. package/skills/craft-video-audio/SKILL.md +59 -0
  17. package/skills/craft-video-enrich/SKILL.md +46 -0
  18. package/skills/craft-video-plan/SKILL.md +57 -0
  19. package/skills/craft-video-publish/SKILL.md +79 -0
  20. package/skills/craft-video-render/SKILL.md +68 -0
  21. package/skills/craft-video-scene/SKILL.md +112 -0
  22. package/skills/craft-video-script/SKILL.md +80 -0
  23. package/skills/explainer-video-from-codebase/FLOWS.md +577 -0
  24. package/skills/explainer-video-from-codebase/SKILL.md +659 -0
  25. package/skills/explainer-video-from-codebase/tools/apply-flows.mjs +531 -0
  26. package/skills/explainer-video-from-codebase/tools/approve-flows.mjs +56 -0
  27. package/skills/explainer-video-from-codebase/tools/codemap-md.mjs +116 -0
  28. package/skills/explainer-video-from-codebase/tools/config/dotenv.mjs +13 -0
  29. package/skills/explainer-video-from-codebase/tools/config/json.mjs +14 -0
  30. package/skills/explainer-video-from-codebase/tools/config/redact.mjs +16 -0
  31. package/skills/explainer-video-from-codebase/tools/config/toml-lite.mjs +13 -0
  32. package/skills/explainer-video-from-codebase/tools/config/yaml-lite.mjs +18 -0
  33. package/skills/explainer-video-from-codebase/tools/excerpt-check.mjs +186 -0
  34. package/skills/explainer-video-from-codebase/tools/excerpt.mjs +116 -0
  35. package/skills/explainer-video-from-codebase/tools/flow-diagram.mjs +168 -0
  36. package/skills/explainer-video-from-codebase/tools/flow-plan.mjs +129 -0
  37. package/skills/explainer-video-from-codebase/tools/flows-md.mjs +87 -0
  38. package/skills/explainer-video-from-codebase/tools/lang/c.mjs +684 -0
  39. package/skills/explainer-video-from-codebase/tools/lang/go.mjs +673 -0
  40. package/skills/explainer-video-from-codebase/tools/lang/python-config.mjs +143 -0
  41. package/skills/explainer-video-from-codebase/tools/lang/python-deps.mjs +62 -0
  42. package/skills/explainer-video-from-codebase/tools/lang/python-stdlib.mjs +24 -0
  43. package/skills/explainer-video-from-codebase/tools/lang/python.mjs +609 -0
  44. package/skills/explainer-video-from-codebase/tools/lang/typescript.mjs +963 -0
  45. package/skills/explainer-video-from-codebase/tools/lib/decompose.mjs +102 -0
  46. package/skills/explainer-video-from-codebase/tools/lib/groups.mjs +77 -0
  47. package/skills/explainer-video-from-codebase/tools/lib/observe-coverage.mjs +68 -0
  48. package/skills/explainer-video-from-codebase/tools/lib/observe-v8.mjs +232 -0
  49. package/skills/explainer-video-from-codebase/tools/lib/observe.mjs +162 -0
  50. package/skills/explainer-video-from-codebase/tools/lib/redact.mjs +28 -0
  51. package/skills/explainer-video-from-codebase/tools/lib/sandbox.mjs +97 -0
  52. package/skills/explainer-video-from-codebase/tools/lib/test-links.mjs +28 -0
  53. package/skills/explainer-video-from-codebase/tools/lib/toml.mjs +129 -0
  54. package/skills/explainer-video-from-codebase/tools/lib/trace-c.mjs +95 -0
  55. package/skills/explainer-video-from-codebase/tools/lib/trace-go.mjs +101 -0
  56. package/skills/explainer-video-from-codebase/tools/lib/trace-js.mjs +136 -0
  57. package/skills/explainer-video-from-codebase/tools/lib/tree-sitter.mjs +19 -0
  58. package/skills/explainer-video-from-codebase/tools/lib/walk.mjs +138 -0
  59. package/skills/explainer-video-from-codebase/tools/mine.mjs +409 -0
  60. package/skills/explainer-video-from-codebase/tools/propose.mjs +127 -0
  61. package/skills/explainer-video-from-codebase/tools/rank.mjs +119 -0
  62. package/skills/explainer-video-from-codebase/tools/ranking-md.mjs +76 -0
  63. package/skills/explainer-video-from-codebase/tools/rules/c-handlers.mjs +53 -0
  64. package/skills/explainer-video-from-codebase/tools/rules/celery-taskiq.mjs +48 -0
  65. package/skills/explainer-video-from-codebase/tools/rules/click-typer.mjs +6 -0
  66. package/skills/explainer-video-from-codebase/tools/rules/express.mjs +72 -0
  67. package/skills/explainer-video-from-codebase/tools/rules/fastapi.mjs +99 -0
  68. package/skills/explainer-video-from-codebase/tools/rules/go-http.mjs +173 -0
  69. package/skills/explainer-video-from-codebase/tools/rules/go-test.mjs +108 -0
  70. package/skills/explainer-video-from-codebase/tools/rules/grpc.mjs +57 -0
  71. package/skills/explainer-video-from-codebase/tools/rules/gtest.mjs +97 -0
  72. package/skills/explainer-video-from-codebase/tools/rules/haystack.mjs +55 -0
  73. package/skills/explainer-video-from-codebase/tools/rules/jest-vitest.mjs +100 -0
  74. package/skills/explainer-video-from-codebase/tools/rules/js-http-client.mjs +111 -0
  75. package/skills/explainer-video-from-codebase/tools/rules/nextjs.mjs +53 -0
  76. package/skills/explainer-video-from-codebase/tools/rules/process-spawn.mjs +49 -0
  77. package/skills/explainer-video-from-codebase/tools/rules/py-http-client.mjs +26 -0
  78. package/skills/explainer-video-from-codebase/tools/rules/pytest.mjs +107 -0
  79. package/skills/explainer-video-from-codebase/tools/rules/react-router.mjs +17 -0
  80. package/skills/explainer-video-from-codebase/tools/rules/registry.mjs +31 -0
  81. package/skills/explainer-video-from-codebase/tools/trace/c/fmtrace-sh +14 -0
  82. package/skills/explainer-video-from-codebase/tools/trace/js/fmtrace-net.cjs +33 -0
  83. package/skills/explainer-video-from-codebase/tools/trace/js/fmtrace-srcmap.cjs +41 -0
  84. package/skills/explainer-video-from-codebase/tools/trace/python/fmtrace_plugin.py +87 -0
  85. package/skills/explainer-video-from-codebase/tools/trace/python/sitecustomize.py +330 -0
  86. package/skills/explainer-video-from-codebase/tools/trace/sandbox/linux-bwrap.sh +73 -0
  87. package/skills/explainer-video-from-codebase/tools/trace/sandbox/macos-nonet.sb +41 -0
  88. package/skills/explainer-video-from-codebase/tools/trace.mjs +577 -0
  89. package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-c.txt +21 -0
  90. package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-cpp.txt +21 -0
  91. package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-go.txt +21 -0
  92. package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-javascript.txt +21 -0
  93. package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-python.txt +21 -0
  94. package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-typescript.txt +21 -0
  95. package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/web-tree-sitter.txt +21 -0
  96. package/skills/explainer-video-from-codebase/vendor/tree-sitter/MANIFEST.json +172 -0
  97. package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-c.wasm +0 -0
  98. package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-cpp.wasm +0 -0
  99. package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-go.wasm +0 -0
  100. package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-javascript.wasm +0 -0
  101. package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-python.wasm +0 -0
  102. package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-tsx.wasm +0 -0
  103. package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-typescript.wasm +0 -0
  104. package/skills/explainer-video-from-codebase/vendor/tree-sitter/web-tree-sitter.js +4081 -0
  105. package/skills/explainer-video-from-codebase/vendor/tree-sitter/web-tree-sitter.wasm +0 -0
  106. package/skills/explainer-video-from-coursework/ARCHITECTURE.md +59 -0
  107. package/skills/explainer-video-from-coursework/PIPELINE.md +343 -0
  108. package/skills/explainer-video-from-coursework/ROADMAP.md +725 -0
  109. package/skills/explainer-video-from-coursework/SERIES.md +232 -0
  110. package/skills/explainer-video-from-coursework/SETUP.md +105 -0
  111. package/skills/explainer-video-from-coursework/SKILL.md +858 -0
  112. package/skills/explainer-video-from-coursework/scenes/_pt-chart.js +12 -0
  113. package/skills/explainer-video-from-coursework/scenes/_pt-equation.js +10 -0
  114. package/skills/explainer-video-from-coursework/scenes/_pt-hubspoke.js +7 -0
  115. package/skills/explainer-video-from-coursework/scenes/_pt-loopflow.js +7 -0
  116. package/skills/explainer-video-from-coursework/scenes/_pt-sequence.js +12 -0
  117. package/skills/explainer-video-from-coursework/scenes/_pt-timeline.js +11 -0
  118. package/skills/explainer-video-from-coursework/src/scene-code.html +127 -0
  119. package/skills/explainer-video-from-coursework/src/scene-code.js +379 -0
  120. package/skills/explainer-video-from-coursework/src/scene-core.js +1250 -0
  121. package/skills/explainer-video-from-coursework/src/scene-diagram.html +50 -0
  122. package/skills/explainer-video-from-coursework/src/scene-svg.html +50 -0
  123. package/skills/explainer-video-from-coursework/src/timeline.js +297 -0
  124. package/skills/explainer-video-from-coursework/src/timing-chart.js +160 -0
  125. package/skills/explainer-video-from-coursework/tools/align.mjs +419 -0
  126. package/skills/explainer-video-from-coursework/tools/apply-enrichment.mjs +206 -0
  127. package/skills/explainer-video-from-coursework/tools/assemble.mjs +223 -0
  128. package/skills/explainer-video-from-coursework/tools/build-audio.mjs +234 -0
  129. package/skills/explainer-video-from-coursework/tools/chromium.mjs +73 -0
  130. package/skills/explainer-video-from-coursework/tools/debug-page.mjs +75 -0
  131. package/skills/explainer-video-from-coursework/tools/env.mjs +30 -0
  132. package/skills/explainer-video-from-coursework/tools/frame-dedup.mjs +131 -0
  133. package/skills/explainer-video-from-coursework/tools/key-source.mjs +151 -0
  134. package/skills/explainer-video-from-coursework/tools/model.mjs +21 -0
  135. package/skills/explainer-video-from-coursework/tools/naming.mjs +38 -0
  136. package/skills/explainer-video-from-coursework/tools/onset-audit.mjs +51 -0
  137. package/skills/explainer-video-from-coursework/tools/pacing-check.mjs +350 -0
  138. package/skills/explainer-video-from-coursework/tools/planner.mjs +616 -0
  139. package/skills/explainer-video-from-coursework/tools/publish.mjs +230 -0
  140. package/skills/explainer-video-from-coursework/tools/render.mjs +205 -0
  141. package/skills/explainer-video-from-coursework/tools/resolve-beats.mjs +239 -0
  142. package/skills/explainer-video-from-coursework/tools/script-check.mjs +174 -0
  143. package/skills/explainer-video-from-coursework/tools/stamp.mjs +71 -0
  144. package/skills/explainer-video-from-coursework/tools/timing-report.mjs +299 -0
  145. package/skills/explainer-video-from-coursework/tools/transcribe-gemini.mjs +96 -0
  146. package/skills/explainer-video-from-coursework/tools/tts-gemini.mjs +394 -0
  147. package/skills/explainer-video-from-coursework/tools/verify-audio.mjs +88 -0
  148. package/skills/explainer-video-from-coursework/tools/workdir.mjs +124 -0
  149. package/skills/explainer-video-from-coursework/vendor/plotly-basic.min.js +8 -0
  150. 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); }