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,101 @@
1
+ /* trace.mjs's Go runner (design §3.14 Go row, §3.17): `go test`, recognised with the flags that only choose and run
2
+ tests, run once per test under `-coverprofile` so what ran is attributed to that test.
3
+ - accepted: `go test [flags] [packages]` and `go -C <dir> test …` (the go on PATH, or a path to one); packages are
4
+ `.`, `./dir` or `./dir/...` inside the module; flags: -run, -skip, -short, -v, -count, -tags, -timeout, -failfast,
5
+ -p, -parallel, -cpu (a value given as -flag=v or -flag v);
6
+ - refused: any flag the trace sets itself or that runs another program or writes elsewhere (-json, -cover*,
7
+ -coverprofile, -exec, -toolexec, -o, -c, -cpuprofile, -memprofile, -trace, -fuzz, -race, -vet, -modfile, -overlay);
8
+ - the module is the one `-C` names, else the repo root's go.mod, else the code map's one Go module;
9
+ - each code-map TestXxx / FuzzXxx in a selected package runs alone (`-run ^TestXxx$`, intersected with a -run the
10
+ command gave) with `-json -count=1 -coverprofile -coverpkg=./...`: the profile marks the module's non-test code
11
+ that ran under that test. go test does not instrument _test.go files, so test-side code (helpers, fakes,
12
+ TestMain) is never observed. */
13
+ import { posix } from 'node:path';
14
+
15
+ const VALUE = new Set(['-run', '-skip', '-count', '-tags', '-timeout', '-p', '-parallel', '-cpu']);
16
+ const BOOL = new Set(['-short', '-v', '-failfast']);
17
+
18
+ /* -> { runner: 'go-test', dir, flags, packages, run } or null */
19
+ export function recogniseGo(words) {
20
+ if (!/^go$/.test(posix.basename(words[0]))) return null;
21
+ let i = 1, dir = '';
22
+ if (words[i] === '-C' && words[i + 1] != null) { dir = words[i + 1]; i += 2; }
23
+ else if (/^-C=/.test(words[i] || '')) { dir = words[i].slice(3); i++; }
24
+ if (words[i] !== 'test') return null;
25
+ if (dir.startsWith('/') || dir.split('/').includes('..')) return null;
26
+ const flags = [], packages = []; let run = null;
27
+ for (let j = i + 1; j < words.length; j++) {
28
+ const w = words[j];
29
+ if (!w.startsWith('-')) { if (w.startsWith('/') || w.split('/').includes('..') || !/^\.(\/|$)/.test(w)) return null; packages.push(w); continue; }
30
+ const [f, v] = w.includes('=') ? [w.slice(0, w.indexOf('=')), w.slice(w.indexOf('=') + 1)] : [w, null];
31
+ const flag = f.replace(/^--/, '-');
32
+ if (BOOL.has(flag)) { if (v != null && !/^(true|false)$/.test(v)) return null; flags.push(w); continue; }
33
+ if (!VALUE.has(flag)) return null;
34
+ const val = v ?? words[++j];
35
+ if (val == null) return null;
36
+ if (flag === '-run') run = val; else flags.push(`${flag}=${val}`);
37
+ }
38
+ return { runner: 'go-test', dir: dir.replace(/^\.\/?/, '').replace(/\/$/, ''), flags, packages: packages.length ? packages : ['.'], run };
39
+ }
40
+
41
+ // does a package pattern (relative to the module dir) select a package dir (relative to the module dir, '' = root)?
42
+ const selects = (pat, dir) => { const p = pat.replace(/^\.\/?/, '').replace(/\/$/, '');
43
+ if (p === '...') return true;
44
+ if (p.endsWith('/...')) { const base = p.slice(0, -4); return dir === base || dir.startsWith(base + '/'); }
45
+ return dir === p; };
46
+
47
+ /* the module dir the run uses (repo-relative, '' = root), or { why } */
48
+ export function goModuleDir(m, cmd, hasRootGoMod) {
49
+ const mods = m.selfCheck?.go?.modules || [];
50
+ if (cmd.dir) return mods.some(x => (x.dir === '.' ? '' : x.dir) === cmd.dir) ? { dir: cmd.dir } : { why: `-C ${cmd.dir}: no go.mod there in the code map` };
51
+ if (hasRootGoMod) return { dir: '' };
52
+ if (mods.length === 1) return { dir: mods[0].dir === '.' ? '' : mods[0].dir };
53
+ return { why: mods.length ? `the repo holds ${mods.length} Go modules (${mods.map(x => x.dir).join(', ')}): name one with go -C <dir> test` : 'no go.mod in the code map' };
54
+ }
55
+
56
+ /* the code map's tests the command runs: [{ id, name, pkgDir (module-relative) }]; a test in a module nested inside
57
+ this one is that module's, which go test skips */
58
+ export function goTestsToRun(m, cmd, modDir) {
59
+ let re = null; if (cmd.run) { try { re = new RegExp(cmd.run); } catch { re = null; } }
60
+ const nested = (m.selfCheck?.go?.modules || []).map(x => x.dir === '.' ? '' : x.dir).filter(d => d !== modDir && (!modDir || d.startsWith(modDir + '/')));
61
+ return (m.tests || []).filter(t => t.file.endsWith('_test.go') && !nested.some(d => t.file.startsWith(d + '/'))).map(t => {
62
+ const rel = modDir ? (t.file.startsWith(modDir + '/') ? t.file.slice(modDir.length + 1) : null) : t.file;
63
+ return rel == null ? null : { id: t.id, name: t.id.split(':').pop(), file: t.file, pkgDir: posix.dirname(rel) === '.' ? '' : posix.dirname(rel) };
64
+ }).filter(t => t && cmd.packages.some(p => selects(p, t.pkgDir)) && (!re || re.test(t.name)));
65
+ }
66
+
67
+ export const goTestArgv = ({ go, flags, name, profile, pkgDir }) =>
68
+ [go, 'test', '-json', '-count=1', `-run=^${name}$`, `-coverprofile=${profile}`, '-coverpkg=./...', ...flags, pkgDir ? `./${pkgDir}` : '.'];
69
+
70
+ /* `go test -json` events -> { tests: [{ name, status, message }], buildErrors: [text] } (top-level tests only; a
71
+ subtest's failure fails its parent) */
72
+ export function parseGoTestJson(text) {
73
+ const status = new Map(), out = new Map(), pkgOut = [];
74
+ let pkgFailed = false;
75
+ for (const line of text.split('\n')) {
76
+ if (!line.startsWith('{')) { if (line.trim()) pkgOut.push(line + '\n'); continue; }
77
+ let e; try { e = JSON.parse(line); } catch { continue; }
78
+ const top = e.Test ? e.Test.split('/')[0] : null;
79
+ if (e.Action === 'output') { if (top) out.set(top, (out.get(top) || '') + e.Output); else pkgOut.push(e.Output); continue; }
80
+ if (e.Test && !e.Test.includes('/') && /^(pass|fail|skip)$/.test(e.Action)) status.set(e.Test, e.Action === 'pass' ? 'passed' : e.Action === 'fail' ? 'failed' : 'skipped');
81
+ if (!e.Test && e.Action === 'fail') pkgFailed = true;
82
+ }
83
+ const tests = [...status].map(([name, s]) => ({ name, status: s, message: (out.get(name) || '').trim() }));
84
+ return { tests, buildErrors: pkgFailed && !tests.length ? [pkgOut.join('').trim()] : [] };
85
+ }
86
+
87
+ /* a coverprofile -> Map(repo-relative file -> [{ start, end, count }]); `importDir` maps an import path to a repo dir */
88
+ export function parseCoverProfile(text, importDir) {
89
+ const out = new Map();
90
+ for (const line of text.split('\n')) {
91
+ const m = /^(.+\.go):(\d+)\.\d+,(\d+)\.\d+ \d+ (\d+)$/.exec(line.trim());
92
+ if (!m) continue;
93
+ const slash = m[1].lastIndexOf('/'), dir = importDir(m[1].slice(0, slash));
94
+ if (dir == null) continue;
95
+ const file = (dir ? `${dir}/` : '') + m[1].slice(slash + 1);
96
+ (out.get(file) || out.set(file, []).get(file)).push({ start: +m[2], end: +m[3], count: +m[4] });
97
+ }
98
+ return out;
99
+ }
100
+
101
+ export const GO_NETWORK = /\bdial (tcp|udp|unix)\S*[^\n]*|\bsocket: operation not permitted|\blookup \S+[^\n]*(no such host|operation not permitted|server misbehaving)/g;
@@ -0,0 +1,136 @@
1
+ /* trace.mjs's JavaScript / TypeScript runners (design §3.14, §3.17): which test commands are recognised, which test
2
+ files each one runs, and the command line that runs one file under V8 precise coverage with a machine-readable
3
+ report. Only the repo's own installed runner is ever run (its node_modules/.bin); nothing is installed. jest and vitest
4
+ run from the package that holds the runner: the repo root, else the one package below it (a package.json beside a
5
+ node_modules/.bin holding the runner, such as frontend/) that holds the code map's test files; test files and the
6
+ paths given are read relative to it, as the runner run there reads them.
7
+ - `node --test [--test-* flags] [paths]`: the TAP reporter on stdout;
8
+ - `npx jest [args]`, `jest [args]`, `node_modules/.bin/jest [args]`: --json --outputFile, --runInBand, no watchman;
9
+ - `npx vitest [run] [args]`, `vitest …`: --reporter=json --outputFile, run mode, the threads pool (a forked worker is
10
+ killed before it can write its coverage; worker threads write theirs).
11
+ A split value of a jest or vitest flag that takes one (`-t adds`, `--config vitest.config.ts`) is joined to it;
12
+ every other flag is boolean, so a word after it is a test path or filter. A positional selects test files as the
13
+ runner does: a path or directory for node, a case-insensitive regex over the path for jest (as is a
14
+ --testPathPattern value, which is not passed on: --runTestsByPath ignores it), a case-insensitive substring of it
15
+ for vitest. jest and vitest run with --passWithNoTests, so a file the repo's own config does not collect exits 0
16
+ instead of failing. */
17
+ import { existsSync, readFileSync, readdirSync, lstatSync, realpathSync } from 'node:fs';
18
+ import { join, basename, relative, sep } from 'node:path';
19
+
20
+ const NODE_FLAG = /^--(test-name-pattern|test-skip-pattern|test-timeout|test-concurrency)=.+$|^--test-only$|^--experimental-(strip|transform)-types$|^--experimental-test-module-mocks$|^--no-experimental-strip-types$/;
21
+ // flags the trace sets itself, or that would never end or report elsewhere
22
+ const OWN_FLAGS = /^--(watch|watchAll|ui|coverage|shard|outputFile|json|reporter|pool|threads|runInBand|watchman|ci|browser|api|inspect)(\b|=|$)/;
23
+ // each runner's flags that take a value
24
+ const VALUE_FLAGS = {
25
+ jest: new Set(['-t', '--testNamePattern', '-c', '--config', '--testPathPattern', '--testPathPatterns', '--testPathIgnorePatterns', '--rootDir', '--roots',
26
+ '--selectProjects', '--projects', '--testEnvironment', '--env', '-w', '--maxWorkers', '--testTimeout', '--testMatch', '--testRegex', '--seed', '--reporters']),
27
+ vitest: new Set(['-t', '--testNamePattern', '-c', '--config', '-r', '--root', '--dir', '--project', '--environment', '--testTimeout', '--maxWorkers',
28
+ '--minWorkers', '--mode', '--exclude', '--retry', '--bail']),
29
+ };
30
+
31
+ /* -> { runner, args } or null. `words` is the command split like a shell would */
32
+ export function recogniseJs(words) {
33
+ const head = basename(words[0]);
34
+ if (/^node(\d+)?$/.test(head) && words[1] === '--test') {
35
+ const args = words.slice(2);
36
+ return args.every(a => !a.startsWith('-') || NODE_FLAG.test(a)) ? { runner: 'node-test', args } : null;
37
+ }
38
+ let tool = null, args = [];
39
+ if (head === 'npx' && /^(jest|vitest)$/.test(words[1] || '')) { tool = words[1]; args = words.slice(2); }
40
+ else if (/^(jest|vitest)$/.test(head) && (!words[0].includes('/') || /(^|\/)node_modules\/\.bin\/(jest|vitest)$/.test(words[0]))) { tool = head; args = words.slice(1); }
41
+ if (!tool) return null;
42
+ if (tool === 'vitest') { if (/^(watch|dev|bench|init|list|serve)$/.test(args[0] || '')) return null; if (args[0] === 'run') args = args.slice(1); }
43
+ const joined = [];
44
+ for (let i = 0; i < args.length; i++) joined.push(VALUE_FLAGS[tool].has(args[i]) && args[i + 1] != null ? `${args[i]}=${args[++i]}` : args[i]);
45
+ if (joined.some(a => OWN_FLAGS.test(a))) return null;
46
+ return { runner: tool, args: joined };
47
+ }
48
+
49
+ // each runner's own default test-file pattern, applied to the code map's test files
50
+ const PATTERNS = {
51
+ 'node-test': f => /\.test\.[cm]?[jt]s$|[-_.]test\.[cm]?[jt]s$|(^|\/)test-[^/]*\.[cm]?[jt]s$|(^|\/)test\.[cm]?[jt]s$|(^|\/)test\/.*\.[cm]?[jt]s$/.test(f),
52
+ jest: f => /(^|\/)__tests__\/.*\.[jt]sx?$|\.(test|spec)\.[jt]sx?$/.test(f),
53
+ vitest: f => /\.(test|spec)\.[cm]?[jt]sx?$/.test(f),
54
+ };
55
+ // which files a positional selects, as each runner reads it
56
+ const SELECTS = {
57
+ 'node-test': p => f => f === p || f.startsWith(p + '/'),
58
+ jest: p => { let re; try { re = new RegExp(p, 'i'); } catch { return () => false; } return f => re.test(f); },
59
+ vitest: p => f => f.toLowerCase().includes(p.toLowerCase()),
60
+ };
61
+ // what an argument selects test files by: a positional, or jest's --testPathPattern(s) (which --runTestsByPath ignores)
62
+ const selectorOf = (runner, a) => !a.startsWith('-') ? a : runner === 'jest' && /^--testPathPatterns?=/.test(a) ? a.slice(a.indexOf('=') + 1) : null;
63
+ // -> the repo-relative test files the command runs, from the package `pkg` ('' for the repo root)
64
+ export function jsTestFiles(m, runner, args, pkg = '') {
65
+ const selects = args.map(a => selectorOf(runner, a)).filter(p => p != null).map(p => SELECTS[runner](p.replace(/^\.\//, '').replace(/\/$/, '')));
66
+ const files = [...new Set((m.tests || []).filter(t => /\.[cm]?[jt]sx?$/.test(t.file)).map(t => t.file))].sort();
67
+ const inPkg = f => pkg ? (f.startsWith(pkg + '/') ? f.slice(pkg.length + 1) : null) : f;
68
+ return files.filter(f => { const p = inPkg(f); return p != null && PATTERNS[runner](p) && (!selects.length || selects.some(sel => sel(p))); });
69
+ }
70
+ export const passThrough = (runner, args) => args.filter(a => selectorOf(runner, a) == null);
71
+
72
+ // the runner's entry in the repo's node_modules: a node script (run with node) or a shell shim (run as is)
73
+ export function runnerBin(root, runner) {
74
+ const bin = join(root, 'node_modules', '.bin', runner);
75
+ if (!existsSync(bin)) return null;
76
+ const real = realpathSync(bin);
77
+ const head = readFileSync(real, 'utf8').slice(0, 200);
78
+ return { rel: `node_modules/.bin/${runner}`, node: /\.[cm]?js$/.test(real) || /^#!.*\bnode\b/.test(head) };
79
+ }
80
+
81
+ // the package jest or vitest runs from: { dir, bin } (dir '' for the repo root), or { why } when none can be chosen
82
+ export function runnerPackage(root, m, runner) {
83
+ if (runnerBin(root, runner)) return { dir: '', bin: runnerBin(root, runner) };
84
+ const pkgs = nodeModulesDirs(root).map(d => d.slice(0, -'/node_modules'.length)).filter(d => d && existsSync(join(root, d, 'package.json')) && runnerBin(join(root, d), runner));
85
+ if (!pkgs.length) return { why: `${runner} is not installed in the repo (no node_modules/.bin/${runner} at its root or beside a package.json below it)` };
86
+ const owners = pkgs.filter(d => jsTestFiles(m, runner, [], d).length);
87
+ if (owners.length > 1) return { why: `${runner} is installed in ${owners.length} packages that hold test files (${owners.join(', ')}), and one command runs from one of them` };
88
+ const dir = owners[0] ?? (pkgs.length === 1 ? pkgs[0] : null);
89
+ return dir == null ? { why: `${runner} is installed in ${pkgs.join(', ')}, none of which holds a test file` } : { dir, bin: runnerBin(join(root, dir), runner) };
90
+ }
91
+
92
+ // node_modules dirs under the repo (not inside another one), to link into the copy and expose read-only
93
+ export function nodeModulesDirs(root, max = 4) {
94
+ const out = [];
95
+ (function walk(rel, depth) {
96
+ let names; try { names = readdirSync(join(root, rel)); } catch { return; }
97
+ for (const name of names) {
98
+ if (name === '.git') continue;
99
+ const r = rel ? `${rel}/${name}` : name;
100
+ let st; try { st = lstatSync(join(root, r)); } catch { continue; }
101
+ if (!st.isDirectory()) continue;
102
+ if (name === 'node_modules') out.push(r);
103
+ else if (depth < max && !name.startsWith('.')) walk(r, depth + 1);
104
+ }
105
+ })('', 0);
106
+ return out.sort();
107
+ }
108
+
109
+ // the links that put one of those node_modules dirs into the copy: each entry (a scope's entries one by one) to the
110
+ // repo's own, except a workspace package (a symlink into the repo, outside every node_modules dir) to its counterpart
111
+ // in the copy, since the sandbox hides the repo. vite's caches (which it writes into on every run) are left out, so the
112
+ // run makes its own in the copy. -> [{ rel, copy }], copy null for the repo's own entry
113
+ const VITE_CACHES = new Set(['.vite', '.vite-temp', '.vitest']);
114
+ export function nodeModulesLinks(root, dir, dirs) {
115
+ const out = [];
116
+ (function each(rel) {
117
+ for (const name of readdirSync(join(root, rel)).sort()) {
118
+ if (rel === dir && VITE_CACHES.has(name)) continue;
119
+ const r = `${rel}/${name}`;
120
+ let st; try { st = lstatSync(join(root, r)); } catch { continue; }
121
+ if (name.startsWith('@') && st.isDirectory()) { each(r); continue; }
122
+ let real = null; if (st.isSymbolicLink()) try { real = realpathSync(join(root, r)); } catch {}
123
+ const workspace = real && real.startsWith(root + sep) && !dirs.some(d => real.startsWith(join(root, d) + sep));
124
+ out.push({ rel: r, copy: workspace ? relative(root, real) : null });
125
+ }
126
+ })(dir);
127
+ return out;
128
+ }
129
+
130
+ // one file's command line, and where its report lands (null: stdout)
131
+ export function fileArgv({ runner, node, bin, flags, file, report }) {
132
+ const tool = bin ? (bin.node ? [node, bin.rel] : [bin.rel]) : null;
133
+ if (runner === 'node-test') return [node, ...flags, '--test', '--test-reporter=tap', file];
134
+ if (runner === 'jest') return [...tool, ...flags, '--ci', '--watchman=false', '--runInBand', '--passWithNoTests', '--json', `--outputFile=${report}`, '--runTestsByPath', file];
135
+ return [...tool, 'run', ...flags, '--passWithNoTests', '--reporter=json', `--outputFile=${report}`, '--pool=threads', file];
136
+ }
@@ -0,0 +1,19 @@
1
+ /* Load the vendored tree-sitter runtime and grammars. File names come from MANIFEST.json
2
+ (written by scripts/sync-tree-sitter.mjs), never from node_modules, so the tools run
3
+ from a git clone with no `npm install` and from any cwd. */
4
+ import { readFileSync } from 'node:fs';
5
+ import { join, dirname } from 'node:path';
6
+ import { fileURLToPath, pathToFileURL } from 'node:url';
7
+
8
+ const VENDOR = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'vendor', 'tree-sitter');
9
+
10
+ export async function loadGrammars(langs) {
11
+ const manifest = JSON.parse(readFileSync(join(VENDOR, 'MANIFEST.json'), 'utf8'));
12
+ const { Parser, Language } = await import(pathToFileURL(join(VENDOR, manifest.runtime.js)).href);
13
+ await Parser.init();
14
+ const grammars = {};
15
+ for (const lang of langs) grammars[lang] = await Language.load(join(VENDOR, manifest.grammars[lang].file));
16
+ const versions = { [manifest.runtime.package]: manifest.runtime.version,
17
+ ...Object.fromEntries(langs.map(l => [manifest.grammars[l].package, manifest.grammars[l].version])) };
18
+ return { Parser, grammars, versions };
19
+ }
@@ -0,0 +1,138 @@
1
+ /* The candidate walk over a code map: from one symbol, down and back through the call graph, with the
2
+ receiver class carried so self-calls resolve per receiver. Group rules (design §3.7, §3.15):
3
+ - a non-test walk never enters test-group code: test doubles are counted and skipped;
4
+ - a production walk expands any non-test code it reaches, labelling other groups' nodes (`crossGroup`);
5
+ - an eval or dev walk folds every production symbol into a `uses-prod` leaf;
6
+ - helper folding counts callers from the symbol's own group only, never through test code.
7
+ A `py.callback` link (the callee runs a callable a caller handed it) is followed only from the caller that
8
+ handed it over, so a walk that reached the callee some other way never picks up another caller's callable. */
9
+
10
+ export const FOLLOW = new Set(['static', 'ctor', 'framework', 'dispatch', 'queue', 'http', 'spawn', 'virtual']);
11
+ export const CROSSING = new Set(['queue', 'http', 'spawn']);
12
+ const DATA_BASES = /(^|\.)(BaseModel|BaseSettings|Exception|Error|Enum|StrEnum|IntEnum|TypedDict|NamedTuple|JSONResponse|Response)$/;
13
+ const isCallback = e => e.rule === 'py.callback';
14
+
15
+ export function indexCodemap(m, { fanin = 6, leafy = 5, ifaceFold = true } = {}) {
16
+ const sym = new Map(m.symbols.map(s => [s.id, s]));
17
+ const cls = new Map(m.classes.map(k => [k.id, k]));
18
+ const G = id => sym.get(id)?.group || (id.startsWith('ext:') ? 'ext' : 'prod');
19
+ const moduleOf = id => sym.get(id)?.module || (id.startsWith('ext:') ? 'ext' : id.split(':')[0]);
20
+ const declCls = id => sym.get(id)?.cls || null;
21
+ const isAbstract = id => (sym.get(id)?.decos || []).some(d => /abstractmethod/.test(d));
22
+ const subclasses = new Map();
23
+ for (const k of cls.values()) for (const b of k.bases) (subclasses.get(b) || subclasses.set(b, []).get(b)).push(k.id);
24
+ const mro = (id, seen = new Set()) => seen.has(id) ? [] : (seen.add(id), [id, ...(cls.get(id)?.bases || []).flatMap(b => mro(b, seen))]);
25
+ const lookup = (k, name) => { for (const c of mro(k)) { const x = cls.get(c)?.methods[name]; if (x) return x; } return null; };
26
+ const overridesOf = (k, name, acc = []) => { for (const s of subclasses.get(k) || []) { const x = cls.get(s).methods[name]; if (x) acc.push(x); overridesOf(s, name, acc); } return acc; };
27
+ const isData = (k, seen = new Set()) => { if (seen.has(k)) return false; seen.add(k); const c = cls.get(k); if (!c) return false;
28
+ if (c.decos?.some(d => /dataclass/.test(d)) || c.baseExprs.some(b => DATA_BASES.test(b))) return true; return c.bases.some(b => isData(b, seen)); };
29
+ const entryIds = new Set(m.entries.map(e => e.id));
30
+ const altFactories = new Set(Object.entries(m.factories || {}).filter(([f, v]) => new Set(v.map(x => x.cls || x.ext)).size > 1 && !entryIds.has(f)).map(([f]) => f));
31
+ const siteOf = new Map((m.config?.sites || []).map(s => [s.factory, s]));
32
+
33
+ const out = new Map(), handedBy = new Map();
34
+ for (const e of m.edges) {
35
+ if (isCallback(e)) (handedBy.get(e.passedBy) || handedBy.set(e.passedBy, []).get(e.passedBy)).push(e);
36
+ else (out.get(e.from) || out.set(e.from, []).get(e.from)).push(e);
37
+ }
38
+ const visible = (grp, id) => grp === 'test' || G(id) !== 'test';
39
+ const crossTargets = new Set(m.edges.filter(e => CROSSING.has(e.kind)).map(e => e.to));
40
+
41
+ // fan-in (same-group callers; a production caller also counts for a library target) and subtree size
42
+ const callers = new Map();
43
+ for (const e of m.edges) if (FOLLOW.has(e.kind) && e.kind !== 'virtual' && !isCallback(e) && (G(e.from) === G(e.to) || (G(e.to) === 'ext' && G(e.from) === 'prod')))
44
+ (callers.get(e.to) || callers.set(e.to, new Set()).get(e.to)).add(e.from);
45
+ const subtree = new Map();
46
+ const subtreeSize = start => {
47
+ if (subtree.has(start)) return subtree.get(start);
48
+ const grp = G(start), seen = new Set([start]), q = [start];
49
+ while (q.length) { const x = q.pop(); for (const e of out.get(x) || []) if (FOLLOW.has(e.kind) && visible(grp, e.to) && !seen.has(e.to)) { seen.add(e.to); q.push(e.to); } }
50
+ const n = [...seen].filter(x => !x.startsWith('ext:')).length - 1;
51
+ subtree.set(start, n);
52
+ return n;
53
+ };
54
+ const isHelper = id => !entryIds.has(id) && !crossTargets.has(id) && sym.get(id)?.kind !== 'class' &&
55
+ (callers.get(id)?.size || 0) >= fanin && subtreeSize(id) <= leafy;
56
+
57
+ /* walk from `start` as group `grp`. `stopAtCrossing` leaves queue/http/spawn targets unexpanded (segments,
58
+ test reach); `stop` are symbols left unexpanded as `mocked` (what a test replaces); `ctx0` the receiver class */
59
+ function expand(start, { grp = G(start.id), stopAtCrossing = false, stop = null, ctx0 } = {}) {
60
+ const nodes = new Map(), order = [], dispatch = [], constructs = new Set(), crossings = [], branches = [], folded = new Set(), seenSite = new Set();
61
+ const testDoubles = new Set(), usesProd = new Set(), crossGroup = new Set();
62
+ const leafOf = kind => ifaceFold ? kind : null;
63
+ const visit = (id, ctx, depth, via, parent, leaf) => {
64
+ if (!visible(grp, id)) { testDoubles.add(id); return; }
65
+ const key = id + '|' + (ctx || '');
66
+ if (nodes.has(key)) return;
67
+ const node = { id, ctx, depth, via, parent, folded: false };
68
+ nodes.set(key, node); order.push(node);
69
+ if (leaf) { node.folded = leaf; folded.add(id); return; }
70
+ if (stop && stop.has(id)) { node.folded = 'mocked'; folded.add(id); return; }
71
+ if (depth > 0 && grp !== 'prod' && G(id) === 'prod') { node.folded = 'uses-prod'; folded.add(id); usesProd.add(id); return; }
72
+ if (depth > 0 && isHelper(id)) { node.folded = 'helper'; folded.add(id); return; }
73
+ if (id.startsWith('ext:')) return;
74
+ if (G(id) !== grp) crossGroup.add(id);
75
+ const edges = out.get(id) || [];
76
+ const sites = new Map();
77
+ for (const e of edges) if (e.self || (e.site && (e.kind === 'dispatch' || e.kind === 'virtual'))) (sites.get(e.site) || sites.set(e.site, []).get(e.site)).push(e);
78
+ for (const e of edges) {
79
+ if (!FOLLOW.has(e.kind)) continue;
80
+ if (!visible(grp, e.to)) { testDoubles.add(e.to); continue; }
81
+ if (e.cfg) branches.push({ from: id, to: e.to, cfg: e.cfg, line: e.line, onFailure: e.onFailure });
82
+ if (e.kind === 'ctor') {
83
+ const k = cls.has(e.to) ? e.to : declCls(e.to);
84
+ if (cls.has(e.to)) { if (isData(e.to)) constructs.add(e.to); continue; }
85
+ if (isData(k)) continue;
86
+ visit(e.to, k, depth + 1, 'ctor', id, altFactories.has(id) ? leafOf('alternative') : null);
87
+ continue;
88
+ }
89
+ if (e.self) {
90
+ if (seenSite.has(e.site + key)) continue;
91
+ seenSite.add(e.site + key);
92
+ const all = sites.get(e.site).map(x => x.to).filter(t => visible(grp, t));
93
+ let targets, how;
94
+ if (ctx) { const t = lookup(ctx, e.self); targets = [...new Set([t, ...overridesOf(ctx, e.self)].filter(Boolean))].filter(t => visible(grp, t)); how = 'receiver'; }
95
+ else { targets = all; how = 'cha'; }
96
+ if (all.length > 1 || targets.length > 1) dispatch.push({ site: e.site, attr: e.self, how, before: all.length, after: targets.length, ctx, targets });
97
+ for (const t of targets) visit(t, ctx, depth + 1, targets.length > 1 ? 'virtual' : 'static', id);
98
+ continue;
99
+ }
100
+ if (e.kind === 'virtual' || e.kind === 'dispatch') {
101
+ if (e.site && seenSite.has(e.site + key + e.kind)) continue;
102
+ if (e.site) seenSite.add(e.site + key + e.kind);
103
+ const alts = (e.site ? (sites.get(e.site) || [e]).filter(x => x.kind === e.kind || x.kind === 'static') : [e]).filter(x => visible(grp, x.to));
104
+ if (e.kind === 'dispatch' && e.rule === 'haystack.pipeline') {
105
+ const comp = edges.filter(x => x.rule === 'haystack.pipeline' && x.component === e.component);
106
+ dispatch.push({ site: `${id}@${e.line}:${e.component}`, attr: e.component, how: 'factory', before: comp.length, after: comp.length, targets: comp.map(x => x.to),
107
+ selectors: comp.map(x => x.selectors), narrowedBy: e.narrowedBy, config: configOf(e.narrowedBy) });
108
+ for (const x of comp) visit(x.to, declCls(x.to), depth + 1, 'framework', id);
109
+ continue;
110
+ }
111
+ const concrete = alts.filter(x => !isAbstract(x.to));
112
+ dispatch.push({ site: e.site, attr: e.site?.split(':').pop(), how: e.kind === 'dispatch' ? 'factory' : 'cha', before: alts.length, after: concrete.length,
113
+ targets: alts.map(x => x.to), narrowedBy: e.narrowedBy, config: configOf(e.narrowedBy), collaborator: true });
114
+ for (const x of alts) visit(x.to, declCls(x.to), depth + 1, e.kind, id, isAbstract(x.to) ? leafOf('interface') : concrete.length > 1 ? leafOf('alternative') : null);
115
+ continue;
116
+ }
117
+ if (CROSSING.has(e.kind)) {
118
+ if (!crossings.some(x => x.from === id && x.to === e.to && x.kind === e.kind)) crossings.push({ from: id, to: e.to, kind: e.kind, evidence: e.evidence, intoGroup: G(e.to) });
119
+ if (!stopAtCrossing) visit(e.to, null, depth + 1, e.kind, id);
120
+ continue;
121
+ }
122
+ if (e.kind === 'framework') { visit(e.to, declCls(e.to), depth + 1, 'framework', id); continue; }
123
+ visit(e.to, e.recvClass || (e.passCtx ? ctx : null), depth + 1, 'static', id);
124
+ }
125
+ // callables this symbol handed to a callee: they run inside that callee, so they hang under it
126
+ for (const e of handedBy.get(id) || []) if (visible(grp, e.to)) visit(e.to, declCls(e.to) ? ctx : null, depth + 2, 'callback', e.from);
127
+ };
128
+ visit(start.id, ctx0 !== undefined ? ctx0 : start.kind === 'ui-route' ? null : declCls(start.id), 0, 'entry', null);
129
+ const ids = new Set(order.map(n => n.id));
130
+ const own = new Set([...ids].filter(i => !folded.has(i) && !i.startsWith('ext:')));
131
+ return { order, ids, own, folded, constructs, crossings, branches, dispatch, testDoubles, usesProd, crossGroup,
132
+ modules: new Set([...own].map(moduleOf)), maxDepth: Math.max(...order.map(n => n.depth)) };
133
+ }
134
+ const configOf = factory => { const s = factory && siteOf.get(factory);
135
+ return s ? { classification: s.classification, alternatives: s.alternatives.map(a => ({ cls: a.cls, group: a.group, guards: a.guards })) } : undefined; };
136
+
137
+ return { sym, G, moduleOf, declCls, out, entryIds, isHelper, expand };
138
+ }