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,162 @@
1
+ /* Map the Python collector's records onto a code map (design §3.17, Appendix A.7): which static edges fired, which
2
+ code ran under which test, and which calls ran with no static edge at all. It never removes or demotes an edge: the
3
+ result is an overlay beside the code map, which is not edited.
4
+
5
+ A record is (caller, callee, test, calls), each code unit as [file relative to the repo root, co_qualname]. Its id
6
+ is the code map's: `pkg.module:Class.method`, `…:outer.<locals>.inner`, `pkg.module:<module>` for module code. */
7
+
8
+ const KINDS = ['static', 'ctor', 'virtual', 'dispatch', 'framework', 'queue', 'http'];
9
+ const STATUS_RANK = { error: 4, failed: 3, xpassed: 2, passed: 1, xfailed: 0, skipped: 0 };
10
+
11
+ const modOf = rel => rel.replace(/\.py$/, '').replace(/\/__init__$/, '').split('/').join('.');
12
+ const idOf = ([rel, q]) => `${modOf(rel)}:${q}`;
13
+ const byStr = (a, b) => a < b ? -1 : a > b ? 1 : 0;
14
+ const pairKey = (a, b) => a + '|' + b;
15
+
16
+ // a test's outcome from its phases: pytest's counting (a failed setup is an error, a failed teardown adds one); a
17
+ // test with no call report was cut off mid-call (the time limit) and is an error
18
+ function statusOf(rec) {
19
+ if (rec.call === 'error' || rec.setup === 'failed') return 'error';
20
+ if (rec.setup === 'skipped' || rec.call === 'skipped') return rec.xfail ? 'xfailed' : 'skipped';
21
+ if (rec.call === 'failed') return 'failed';
22
+ if (rec.call === 'xfailed' || rec.call === 'xpassed') return rec.call;
23
+ if (rec.call === 'passed') return rec.xfail ? 'xpassed' : 'passed';
24
+ return 'error';
25
+ }
26
+
27
+ function category(sym, grp, a, b) {
28
+ const n = b.split(/[.:]/).pop(), s = sym.get(b);
29
+ if (/^__\w+__$/.test(n) && n !== '__init__') return 'dunder';
30
+ if ((s.decos || []).some(d => /property|setter|cached_property/.test(d))) return 'property';
31
+ if ((s.decos || []).some(d => /validator|model_validator|field_validator|computed_field/.test(d))) return 'validator';
32
+ if (grp(a) === 'test' || grp(b) === 'test') return 'test-side';
33
+ if (n === '__init__') return 'init';
34
+ return 'other';
35
+ }
36
+
37
+ export function observe(m, dumps) {
38
+ const sym = new Map(m.symbols.map(s => [s.id, s]));
39
+ const grp = id => sym.get(id)?.group || '?';
40
+ const callable = id => sym.has(id) && sym.get(id).kind !== 'class' && sym.get(id).kind !== 'main';
41
+ const tests = m.tests || [];
42
+ const dotted = new Map(tests.map(t => [t.id.replace(':', '.'), t.id]));
43
+ const known = new Set(tests.map(t => t.id));
44
+ // a collector key (`tests/x.py::Class::test[param]`, `unittest::pkg.mod.Class.test`) -> the code map's test id
45
+ const testIdOf = key => {
46
+ if (key.startsWith('unittest::')) return dotted.get(key.slice('unittest::'.length)) || null;
47
+ const [path, ...rest] = key.split('::');
48
+ const id = `${modOf(path)}:${rest.join('.').replace(/\[.*\]$/, '')}`;
49
+ return known.has(id) ? id : null;
50
+ };
51
+ const nameOf = key => testIdOf(key) || key;
52
+
53
+ /* ---- merge every process's records ---- */
54
+ const pairs = new Map(), ranBy = new Map(), outcomes = new Map(), network = new Map(), collectErrors = new Map(), fixtureErrors = new Map();
55
+ let collected = null, records = 0, unmappedRecords = 0, bodies = 0;
56
+ const unmappedSample = new Set(), python = new Set();
57
+ for (const d of dumps) {
58
+ if (d.python) python.add(d.python);
59
+ if (d.collected != null) collected = Math.max(collected ?? 0, d.collected);
60
+ for (const e of d.collectErrors || []) collectErrors.set(e.nodeid, e);
61
+ for (const e of d.fixtureErrors || []) fixtureErrors.set(e.nodeid, e);
62
+ for (const [k, rec] of Object.entries(d.outcomes || {})) outcomes.set(k, { ...outcomes.get(k), ...rec });
63
+ for (const [k, v] of Object.entries(d.network || {})) network.set(k, [...new Set([...(network.get(k) || []), ...v])]);
64
+ for (const p of d.pairs || []) {
65
+ records++;
66
+ const to = idOf(p.to);
67
+ if (!callable(to)) {
68
+ if (p.to[1] === '<module>' || sym.get(to)?.kind === 'class') bodies++;
69
+ else { unmappedRecords++; if (unmappedSample.size < 8) unmappedSample.add(to); }
70
+ continue;
71
+ }
72
+ if (p.test != null) { const t = nameOf(p.test); (ranBy.get(t) || ranBy.set(t, { keys: new Set(), ran: new Set() }).get(t)).ran.add(to); ranBy.get(t).keys.add(p.test); }
73
+ if (!p.from) continue;
74
+ const from = idOf(p.from), k = pairKey(from, to);
75
+ const x = pairs.get(k) || pairs.set(k, { from, to, n: 0, tests: new Set() }).get(k);
76
+ x.n += p.n;
77
+ if (p.test != null) x.tests.add(nameOf(p.test));
78
+ }
79
+ }
80
+
81
+ /* ---- outcome, failures, tests the sandbox denied ---- */
82
+ const outcome = { collected: Math.max(collected ?? 0, outcomes.size), passed: 0, failed: 0, errors: collectErrors.size + fixtureErrors.size, skipped: 0, xfailed: 0, xpassed: 0 };
83
+ const statusBy = new Map(), failures = [], denied = [];
84
+ for (const [key, rec] of [...outcomes].sort(([a], [b]) => byStr(a, b))) {
85
+ const status = statusOf(rec), name = nameOf(key);
86
+ outcome[{ passed: 'passed', failed: 'failed', error: 'errors', skipped: 'skipped', xfailed: 'xfailed', xpassed: 'xpassed' }[status]]++;
87
+ if (rec.teardown === 'failed') outcome.errors++;
88
+ const prev = statusBy.get(name);
89
+ if (!prev || STATUS_RANK[status] > STATUS_RANK[prev]) statusBy.set(name, status);
90
+ if (status !== 'failed' && status !== 'error' && rec.teardown !== 'failed') continue;
91
+ const phase = rec.setup === 'failed' ? 'setup' : (status === 'failed' || status === 'error') ? 'call' : 'teardown';
92
+ failures.push({ test: testIdOf(key), nodeid: rec.nodeid || key, status, phase, message: rec.message || '' });
93
+ const net = network.get(key) || [];
94
+ if (net.length || rec.permission) denied.push({ test: testIdOf(key), nodeid: rec.nodeid || key, status, network: net, permission: !!rec.permission, message: rec.message || '' });
95
+ }
96
+ for (const e of [...collectErrors.values()].sort((a, b) => byStr(a.nodeid, b.nodeid)))
97
+ failures.push({ test: null, nodeid: e.nodeid, status: 'error', phase: 'collect', message: e.message || '' });
98
+ for (const e of [...fixtureErrors.values()].sort((a, b) => byStr(a.nodeid, b.nodeid))) {
99
+ failures.push({ test: null, nodeid: e.nodeid, status: 'error', phase: e.phase, message: e.message || '' });
100
+ const net = network.get(e.nodeid) || [];
101
+ if (net.length || e.permission) denied.push({ test: null, nodeid: e.nodeid, status: 'error', network: net, permission: !!e.permission, message: e.message || '' });
102
+ }
103
+
104
+ /* ---- static edges that fired ---- */
105
+ const uniq = [...new Map(m.edges.map(e => [e.from + '|' + e.to + '|' + e.kind, e])).values()];
106
+ const countable = uniq.filter(e => KINDS.includes(e.kind) && sym.has(e.from) && callable(e.to));
107
+ const fired = e => pairs.get(pairKey(e.from, e.to));
108
+ const tally = edges => { const byKind = {}; let observed = 0;
109
+ for (const e of edges) { const k = e.kind + (e.rule ? '/' + e.rule : ''); const b = byKind[k] || (byKind[k] = [0, 0]); b[1]++; if (fired(e)) { b[0]++; observed++; } }
110
+ return { observed, total: edges.length, byKind: Object.fromEntries(Object.entries(byKind).sort(([a], [b]) => byStr(a, b))) }; };
111
+ const edges = countable.filter(fired).map(e => { const p = fired(e);
112
+ return { from: e.from, to: e.to, kind: e.kind, ...(e.rule ? { rule: e.rule } : {}), ...(e.passedBy ? { passedBy: e.passedBy } : {}), n: p.n, tests: [...p.tests].sort(byStr) }; })
113
+ .sort((a, b) => byStr(a.from, b.from) || byStr(a.to, b.to) || byStr(a.kind, b.kind));
114
+
115
+ /* ---- per test: direct links that ran under their own test ---- */
116
+ const byTest = {};
117
+ let dObs = 0, dAll = 0, pObs = 0, pAll = 0, linkedTests = 0;
118
+ const testById = new Map(tests.map(t => [t.id, t]));
119
+ for (const name of [...new Set([...ranBy.keys(), ...statusBy.keys()])].sort(byStr)) {
120
+ const r = ranBy.get(name), ran = r?.ran || new Set(), t = testById.get(name);
121
+ const direct = t ? [...new Set(t.direct.map(d => d.to).filter(to => callable(to) && grp(to) !== 'test'))] : [];
122
+ const missing = r ? direct.filter(to => !ran.has(to)).sort(byStr) : [];
123
+ if (r && direct.length) { linkedTests++; dAll += direct.length; dObs += direct.length - missing.length;
124
+ const prod = direct.filter(to => grp(to) === 'prod'); pAll += prod.length; pObs += prod.filter(to => ran.has(to)).length; }
125
+ byTest[name] = { nodeids: [...(r?.keys || [])].sort(byStr), status: statusBy.get(name) || null, ran: [...ran].sort(byStr),
126
+ direct: { total: direct.length, observed: direct.length - missing.length, missing } };
127
+ }
128
+ let fixO = 0, fixN = 0;
129
+ for (const b of m.fixtureBindings || []) { const r = ranBy.get(b.fn); if (!r) continue; fixN++; if (r.ran.has(b.fixture)) fixO++; }
130
+
131
+ /* ---- calls that ran with no static edge of any kind ---- */
132
+ const anyEdge = new Set(m.edges.map(e => pairKey(e.from, e.to)));
133
+ const noStaticEdge = [...pairs.values()].filter(p => sym.has(p.from) && p.from !== p.to && !anyEdge.has(pairKey(p.from, p.to)))
134
+ .map(p => ({ from: p.from, to: p.to, n: p.n, tests: p.tests.size, category: category(sym, grp, p.from, p.to) }))
135
+ .sort((a, b) => byStr(a.from, b.from) || byStr(a.to, b.to));
136
+ const byCategory = {};
137
+ for (const x of noStaticEdge) byCategory[x.category] = (byCategory[x.category] || 0) + 1;
138
+ // the callback links (a callable handed to the code that runs it) the run confirmed
139
+ const callbacks = edges.filter(e => e.rule === 'py.callback').map(e => ({ from: e.from, to: e.to, passedBy: e.passedBy || null, tests: e.tests.length }));
140
+ const prodNoEdge = noStaticEdge.filter(x => x.category === 'other' && grp(x.from) === 'prod' && grp(x.to) === 'prod').length;
141
+
142
+ const fns = m.symbols.filter(s => s.kind === 'function' || s.kind === 'method');
143
+ const ranAnywhere = new Set([...ranBy.values()].flatMap(r => [...r.ran]).concat([...pairs.values()].map(p => p.to)));
144
+ const functionsRan = {};
145
+ for (const g of ['prod', 'eval', 'test', 'dev']) { const f = fns.filter(s => s.group === g); functionsRan[g] = { ran: f.filter(s => ranAnywhere.has(s.id)).length, total: f.length }; }
146
+
147
+ const summary = {
148
+ records, distinctPairs: pairs.size, functionsRan,
149
+ edges: { all: tally(countable), prodToProd: tally(countable.filter(e => grp(e.from) === 'prod' && grp(e.to) === 'prod')),
150
+ testToNonTest: tally(countable.filter(e => grp(e.from) === 'test' && grp(e.to) !== 'test')) },
151
+ directLinksPerTest: { tests: linkedTests, observed: dObs, total: dAll, prod: { observed: pObs, total: pAll } },
152
+ fixtureLinks: { observed: fixO, total: fixN },
153
+ noStaticEdge: { pairs: noStaticEdge.length, prodToProd: prodNoEdge, byCategory: Object.fromEntries(Object.entries(byCategory).sort(([a], [b]) => byStr(a, b))) },
154
+ callbacks: callbacks.length,
155
+ unmapped: { records: unmappedRecords, sample: [...unmappedSample].sort(byStr) }, moduleAndClassBodies: bodies,
156
+ };
157
+ return {
158
+ python: [...python].sort(byStr), outcome, failures, deniedBySandbox: denied, summary,
159
+ pairs: [...pairs.values()].map(p => ({ from: p.from, to: p.to, n: p.n, tests: p.tests.size })).sort((a, b) => byStr(a.from, b.from) || byStr(a.to, b.to)),
160
+ edges, noStaticEdge, callbacks, byTest,
161
+ };
162
+ }
@@ -0,0 +1,28 @@
1
+ /* Redact credential-shaped strings from what a traced test run printed, before trace.mjs writes it to trace.log, shows
2
+ its tail or keeps a failure message in trace.json: private-key blocks, AWS access key ids, bearer and basic auth
3
+ headers, well-known token formats, JWTs, a URL's password, and the value of any key=value or "key": "value" whose
4
+ key names a secret (password, secret, token, api key, credential, ...). The secret is replaced, its label kept. */
5
+ export const REDACTED = '[REDACTED]';
6
+
7
+ // a key that names a secret (`db_password`, `API_KEY`, `x-auth-token`), but not `author` or `tokenizer.py`
8
+ const SECRET_KEY = String.raw`[A-Za-z0-9_-]*(?:passw(?:or)?d|pwd|secret|token|api[_-]?key|access[_-]?key|private[_-]?key|credentials?|authorization|auth(?![a-z]))[A-Za-z0-9_-]*`;
9
+
10
+ const RULES = [
11
+ // a private key block, or its first line when the output was cut short
12
+ [/-----BEGIN ([A-Z0-9 ]*)PRIVATE KEY-----[\s\S]*?(?:-----END \1PRIVATE KEY-----|$(?![\s\S]))/g, `-----BEGIN $1PRIVATE KEY----- ${REDACTED}`],
13
+ [/(\b(?:Bearer|Basic|Token)\s+)[A-Za-z0-9._~+/=-]{8,}/gi, `$1${REDACTED}`],
14
+ [/(:\/\/[^\s:/@]+:)[^\s@/]+@/g, `$1${REDACTED}@`],
15
+ // "key": "value" and key: 'value'
16
+ [new RegExp(String.raw`(["']?\b${SECRET_KEY}["']?\s*[:=](?![:=])\s*)(["'])(?:(?!\2)[^\\\n]|\\.)+\2`, 'gi'), `$1$2${REDACTED}$2`],
17
+ // key=value, key: value (unquoted, to the end of the word)
18
+ [new RegExp(String.raw`(\b${SECRET_KEY}\s*[:=](?![:=])\s*)(?!["'\s]|\[REDACTED\]|(?:Bearer|Basic|Token)\s)[^\s,;&'"}\])]+`, 'gi'), `$1${REDACTED}`],
19
+ [/\b(?:AKIA|ASIA|ABIA|ACCA|AGPA|AIDA|AROA|ANPA)[0-9A-Z]{16}\b/g, REDACTED],
20
+ [/\b(?:gh[pousr]_[A-Za-z0-9]{30,}|github_pat_[A-Za-z0-9_]{30,}|glpat-[A-Za-z0-9_-]{20,}|xox[abposr]-[A-Za-z0-9-]{10,}|sk-[A-Za-z0-9_-]{20,}|AIza[0-9A-Za-z_-]{35}|npm_[A-Za-z0-9]{36})\b/g, REDACTED],
21
+ [/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g, REDACTED],
22
+ ];
23
+
24
+ export function redactSecrets(text) {
25
+ let s = String(text ?? '');
26
+ for (const [re, by] of RULES) s = s.replace(re, by);
27
+ return s;
28
+ }
@@ -0,0 +1,97 @@
1
+ /* The OS sandboxes trace.mjs runs a repo's test command in (design §3.17, ADR-014). Each one denies all network (IP,
2
+ localhost and unix-socket connects), every write outside the scratch dir, and every read of the hidden paths (the
3
+ user's home folder and the original repo) except the read-only paths the run needs (the interpreter and its
4
+ virtualenv); the caller passes an empty environment.
5
+ - macos: sandbox-exec with trace/sandbox/macos-nonet.sb, plus the read rules built here for these paths;
6
+ - bwrap: bubblewrap through trace/sandbox/linux-bwrap.sh (namespaces, read-only /, the hidden paths replaced by
7
+ empty dirs, a seccomp filter on unix sockets).
8
+ There is no unsandboxed mode: where this platform's sandbox does not start, the caller refuses (exit 4). */
9
+ import { spawnSync } from 'node:child_process';
10
+ import { existsSync, readFileSync, realpathSync, mkdtempSync, rmSync, openSync, fstatSync, closeSync, readSync, constants } from 'node:fs';
11
+ import { join, dirname, sep } from 'node:path';
12
+ import { tmpdir, homedir } from 'node:os';
13
+ import { fileURLToPath } from 'node:url';
14
+
15
+ const DIR = join(dirname(fileURLToPath(import.meta.url)), '..', 'trace', 'sandbox');
16
+ export const MACOS_PROFILE = join(DIR, 'macos-nonet.sb');
17
+ export const BWRAP_SCRIPT = join(DIR, 'linux-bwrap.sh');
18
+ const SANDBOX_EXEC = '/usr/bin/sandbox-exec';
19
+ export const SANDBOX_LABEL = { macos: 'macos-sandbox-exec:macos-nonet.sb', bwrap: 'linux-bwrap:linux-bwrap.sh' };
20
+ const KIND_OF = { darwin: 'macos', linux: 'bwrap' };
21
+
22
+ const strip = p => p.replace(/\/+$/, '') || '/';
23
+ const inside = (p, dir) => p === dir || p.startsWith(dir === '/' ? '/' : dir + sep);
24
+ const sbString = p => `"${p.replace(/[\\"]/g, c => '\\' + c)}"`;
25
+ const ancestors = p => { const out = []; for (let d = dirname(p); d !== dirname(d); d = dirname(d)) out.push(d); return out; };
26
+
27
+ // the read-only paths the sandbox may expose: never scratch or one of its parents (a read-only mount there would make
28
+ // scratch read-only too), never a hidden path or one of its parents (that would expose all of it)
29
+ export function exposable(p, { scratch, hidden }) {
30
+ if (!p || !existsSync(p)) return false;
31
+ const d = strip(p);
32
+ return !inside(strip(scratch), d) && !hidden.some(h => inside(strip(h), d));
33
+ }
34
+
35
+ // a file the sandboxed run wrote (or could have replaced), read from outside the sandbox: opened without following a
36
+ // symlink and without waiting (a FIFO the run left there never blocks), and read only when the open file is a plain
37
+ // file; null otherwise
38
+ function withRunFile(p, read) {
39
+ let fd = null;
40
+ try { fd = openSync(p, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK); return fstatSync(fd).isFile() ? read(fd) : null; }
41
+ catch { return null; } finally { if (fd != null) closeSync(fd); }
42
+ }
43
+ export const readRunFile = p => withRunFile(p, fd => readFileSync(fd, 'utf8'));
44
+ // its first n bytes (fewer when it is shorter), as a Buffer
45
+ export const readRunHead = (p, n) => withRunFile(p, fd => { const b = Buffer.alloc(n); return b.subarray(0, readSync(fd, b, 0, n, 0)); });
46
+ // the same, parsed as JSON: [value], or [] when it cannot be read or parsed
47
+ export function readRunJson(p) {
48
+ const t = readRunFile(p); if (t == null) return [];
49
+ try { return [JSON.parse(t)]; } catch { return []; }
50
+ }
51
+
52
+ // the read rules the macOS profile gets for this run: nothing under a hidden path, except scratch and the read-only
53
+ // paths, and the metadata (not the contents) of the hidden dirs above those, so a path to them still resolves
54
+ function macosReadRules({ scratch, hidden, readOnly }) {
55
+ const allowed = [scratch, ...readOnly].map(strip);
56
+ const above = [...new Set(allowed.flatMap(ancestors).filter(a => hidden.some(h => inside(a, strip(h)))))].sort();
57
+ if (!hidden.length) return '';
58
+ return [
59
+ `(deny file-read* ${hidden.map(h => `(subpath ${sbString(strip(h))})`).join(' ')})`,
60
+ `(allow file-read* ${allowed.map(p => `(subpath ${sbString(p)})`).join(' ')})`,
61
+ ...(above.length ? [`(allow file-read-metadata ${above.map(a => `(literal ${sbString(a)})`).join(' ')})`] : []),
62
+ ].join('\n');
63
+ }
64
+
65
+ // the command line that runs argv inside the sandbox
66
+ export function sandboxArgv(kind, { scratch, hidden = [homedir()], readOnly = [], argv }) {
67
+ const ro = readOnly.filter(p => exposable(p, { scratch, hidden })).map(strip);
68
+ if (kind === 'macos') {
69
+ const profile = `${readFileSync(MACOS_PROFILE, 'utf8')}\n; reads, for this run\n${macosReadRules({ scratch, hidden, readOnly: [...new Set(ro)] })}\n`;
70
+ return [SANDBOX_EXEC, '-p', profile, '-D', `SCRATCH=${scratch}`, ...argv];
71
+ }
72
+ // the script applies each list last line first: longest first, so a dir is mounted before the paths inside it
73
+ const deepestFirst = ps => [...new Set(ps)].sort((a, b) => b.length - a.length).join('\n');
74
+ return ['/bin/sh', BWRAP_SCRIPT, scratch, deepestFirst(hidden.filter(h => existsSync(h)).map(strip)), deepestFirst(ro), ...argv];
75
+ }
76
+
77
+ // can this platform's sandbox run here? A probe runs `true` inside it: sandbox-exec refuses inside another sandbox,
78
+ // and bwrap fails where unprivileged user namespaces are off
79
+ export function sandboxWorks(kind) {
80
+ if (kind === 'macos' && !existsSync(SANDBOX_EXEC)) return { ok: false, why: `${SANDBOX_EXEC} is missing` };
81
+ const scratch = realpathSync(mkdtempSync(join(tmpdir(), 'fmtrace-probe-')));
82
+ const [cmd, ...args] = sandboxArgv(kind, { scratch, argv: [kind === 'macos' ? '/usr/bin/true' : '/bin/true'] });
83
+ const r = spawnSync(cmd, args, { env: { PATH: '/usr/local/bin:/usr/bin:/bin' }, encoding: 'utf8', timeout: 20000 });
84
+ rmSync(scratch, { recursive: true, force: true });
85
+ if (r.status === 0) return { ok: true };
86
+ const why = (r.error?.message || r.stderr || `exit ${r.status}`).trim().split('\n').pop();
87
+ return { ok: false, why: kind === 'bwrap' && /ENOENT|not found/.test(why) ? 'bwrap is not installed' : `the ${kind} sandbox does not start here: ${why}` };
88
+ }
89
+
90
+ // this platform's sandbox, or why there is none: a platform without one and a sandbox that cannot start here are
91
+ // refusals, never a fallback
92
+ export function pickSandbox() {
93
+ const kind = KIND_OF[process.platform];
94
+ if (!kind) return { why: `no OS sandbox is supported on ${process.platform}` };
95
+ const w = sandboxWorks(kind);
96
+ return w.ok ? { kind } : { why: w.why };
97
+ }
@@ -0,0 +1,28 @@
1
+ /* Direct test links, for every language whose test pack records tests, hooks bound as framework edges, and mock
2
+ targets (jest / vitest, go test, gtest / Catch2): every edge from a test's own side (its body, its hooks, test-side
3
+ helpers, reached through test-group edges) into non-test code is a direct link to its target, unless that test
4
+ mocks the target. A mock with a `scope: 'session'` applies to every test under its `covers` directory; one recorded
5
+ on `<file>:<module>` (hoisted) to every test in that file; any other to the tests whose side holds its `fn`. */
6
+ const under = (dir, file) => !dir || file.startsWith(dir + '/');
7
+
8
+ const FOLLOW = new Set(['static', 'ctor', 'framework', 'dispatch', 'queue', 'http', 'virtual']);
9
+ export function directLinks(tests, mocks, edges, symbolOf) {
10
+ const out = new Map(); for (const e of edges) (out.get(e.from) || out.set(e.from, []).get(e.from)).push(e);
11
+ const G = id => symbolOf(id)?.group || (id.startsWith('ext:') ? 'ext' : 'prod');
12
+ const mocksIn = new Map(); for (const m of mocks) if (m.target && !m.scope) (mocksIn.get(m.fn) || mocksIn.set(m.fn, new Set()).get(m.fn)).add(m.target);
13
+ const session = mocks.filter(m => m.target && m.scope === 'session');
14
+ for (const t of tests) {
15
+ const side = new Set([t.id]), q = [t.id];
16
+ while (q.length) { const x = q.pop(); for (const e of out.get(x) || []) if (FOLLOW.has(e.kind) && G(e.to) === 'test' && !side.has(e.to)) { side.add(e.to); q.push(e.to); } }
17
+ const mocked = new Set(session.filter(m => under(m.covers, t.file)).map(m => m.target));
18
+ for (const s of [...side, `${t.file}:<module>`]) for (const m of mocksIn.get(s) || []) mocked.add(m);
19
+ const hooks = new Set([...side].filter(s => symbolOf(s)?.hook));
20
+ const links = new Map();
21
+ for (const s of side) for (const e of out.get(s) || []) {
22
+ if (!FOLLOW.has(e.kind) || G(e.to) === 'test' || e.to.startsWith('ext:') || mocked.has(e.to) || symbolOf(e.to)?.kind === 'class' || links.has(e.to)) continue;
23
+ links.set(e.to, { to: e.to, via: s === t.id ? 'body' : hooks.has(s) ? 'fixture' : 'helper', viaId: s, kind: e.kind, ...(e.rule ? { rule: e.rule } : {}), line: e.line, ...(e.recvClass ? { recvClass: e.recvClass } : {}) });
24
+ }
25
+ t.mocked = [...mocked].sort();
26
+ t.direct = [...links.values()];
27
+ }
28
+ }
@@ -0,0 +1,129 @@
1
+ /* A TOML reader for dependency manifests (pyproject.toml, Pipfile): tables, arrays of tables, dotted and quoted keys,
2
+ the four string forms with their escapes, arrays and inline tables, comments anywhere a comment may stand. Numbers,
3
+ booleans and dates are kept as their source text, since a dependency name is only ever a string or a key. A `]`
4
+ inside a string (`"uvicorn[standard]"`) is string content, never the end of an array. Throws on a malformed
5
+ document, the way tomllib does. */
6
+ const BARE = /[A-Za-z0-9_-]+/y;
7
+ const SCALAR = /\d{4}-\d{2}-\d{2}[ T]\d{2}:[^\s,\]}#]*|[^\s,\]}#]+/y;
8
+ const ESC = { b: '\b', t: '\t', n: '\n', f: '\f', r: '\r', e: '\x1b', '"': '"', '\\': '\\' };
9
+ const isTable = v => v !== null && typeof v === 'object' && !Array.isArray(v);
10
+ const table = () => Object.create(null);
11
+
12
+ export function parseToml(src) {
13
+ let i = 0;
14
+ const err = msg => { throw new Error(`TOML: ${msg} at line ${src.slice(0, i).split('\n').length}`); };
15
+ const ws = () => { while (src[i] === ' ' || src[i] === '\t') i++; };
16
+ const comment = () => { if (src[i] === '#') while (i < src.length && src[i] !== '\n') i++; };
17
+ const gap = () => { for (;;) { ws(); comment(); if (src[i] === '\n' || src[i] === '\r') i++; else return; } }; // blank lines and comments
18
+ const eol = () => { ws(); comment(); if (src[i] === '\r') i++; if (i < src.length && src[i] !== '\n') err('expected the end of the line'); i++; };
19
+
20
+ function str() {
21
+ const q = src[i], multi = src.startsWith(q.repeat(3), i);
22
+ i += multi ? 3 : 1;
23
+ if (multi && src[i] === '\n') i++; else if (multi && src.startsWith('\r\n', i)) i += 2; // a newline right after """ is dropped
24
+ let out = '';
25
+ for (;;) {
26
+ if (i >= src.length) err('unterminated string');
27
+ const c = src[i];
28
+ if (!multi && c === q) { i++; return out; }
29
+ if (multi && src.startsWith(q.repeat(3), i)) { let n = 3; while (n < 5 && src[i + n] === q) n++; out += q.repeat(n - 3); i += n; return out; }
30
+ if (!multi && c === '\n') err('newline in a single-line string');
31
+ if (q === '"' && c === '\\') {
32
+ const e = src[i + 1];
33
+ if (multi && /[ \t\r\n]/.test(e)) { // a line-ending backslash swallows the newline and the whitespace after it
34
+ let j = i + 1; while (src[j] === ' ' || src[j] === '\t') j++;
35
+ if (src[j] === '\r') j++;
36
+ if (src[j] !== '\n') err('invalid escape');
37
+ while (/[ \t\r\n]/.test(src[j] ?? '')) j++;
38
+ i = j; continue;
39
+ }
40
+ const hex = { x: 2, u: 4, U: 8 }[e];
41
+ if (hex) { const h = src.slice(i + 2, i + 2 + hex); if (!new RegExp(`^[0-9a-fA-F]{${hex}}$`).test(h)) err('invalid escape'); out += String.fromCodePoint(parseInt(h, 16)); i += 2 + hex; continue; }
42
+ if (!(e in ESC)) err('invalid escape');
43
+ out += ESC[e]; i += 2; continue;
44
+ }
45
+ out += c; i++;
46
+ }
47
+ }
48
+
49
+ function key() {
50
+ const parts = [];
51
+ for (;;) {
52
+ ws();
53
+ if (src[i] === '"' || src[i] === "'") { if (src.startsWith(src[i].repeat(3), i)) err('a key cannot be a multi-line string'); parts.push(str()); }
54
+ else { BARE.lastIndex = i; const m = BARE.exec(src); if (!m) err('expected a key'); parts.push(m[0]); i += m[0].length; }
55
+ ws();
56
+ if (src[i] !== '.') return parts;
57
+ i++;
58
+ }
59
+ }
60
+
61
+ // the table a path names, created on the way; an array of tables is entered at its last element
62
+ const tableAt = (obj, path) => {
63
+ for (const k of path) {
64
+ if (!(k in obj)) obj[k] = table();
65
+ const v = Array.isArray(obj[k]) ? obj[k][obj[k].length - 1] : obj[k];
66
+ if (!isTable(v)) err(`\`${k}\` is not a table`);
67
+ obj = v;
68
+ }
69
+ return obj;
70
+ };
71
+ const assign = (obj, path, v) => {
72
+ const t = tableAt(obj, path.slice(0, -1)), k = path[path.length - 1];
73
+ if (k in t) err(`duplicate key \`${k}\``);
74
+ t[k] = v;
75
+ };
76
+
77
+ function value() {
78
+ const c = src[i];
79
+ if (c === '"' || c === "'") return str();
80
+ if (c === '[') {
81
+ i++; const out = [];
82
+ for (;;) {
83
+ gap();
84
+ if (src[i] === ']') { i++; return out; }
85
+ out.push(value()); gap();
86
+ if (src[i] === ',') i++; else if (src[i] === ']') { i++; return out; } else err('expected `,` or `]`');
87
+ }
88
+ }
89
+ if (c === '{') {
90
+ i++; const out = table();
91
+ for (;;) {
92
+ gap();
93
+ if (src[i] === '}') { i++; return out; }
94
+ const k = key();
95
+ if (src[i] !== '=') err('expected `=`');
96
+ i++; ws(); assign(out, k, value()); gap();
97
+ if (src[i] === ',') i++; else if (src[i] === '}') { i++; return out; } else err('expected `,` or `}`');
98
+ }
99
+ }
100
+ SCALAR.lastIndex = i; const m = SCALAR.exec(src);
101
+ if (!m) err('expected a value');
102
+ i += m[0].length; return m[0];
103
+ }
104
+
105
+ const root = table();
106
+ let cur = root;
107
+ for (;;) {
108
+ gap();
109
+ if (i >= src.length) return root;
110
+ if (src[i] === '[') {
111
+ const many = src[i + 1] === '['; i += many ? 2 : 1;
112
+ const path = key();
113
+ if (src[i] !== ']' || (many && src[i + 1] !== ']')) err('expected `]`');
114
+ i += many ? 2 : 1;
115
+ if (!many) cur = tableAt(root, path);
116
+ else {
117
+ const parent = tableAt(root, path.slice(0, -1)), k = path[path.length - 1];
118
+ if (!(k in parent)) parent[k] = [];
119
+ if (!Array.isArray(parent[k])) err(`\`${k}\` is not an array of tables`);
120
+ parent[k].push(cur = table());
121
+ }
122
+ } else {
123
+ const path = key();
124
+ if (src[i] !== '=') err('expected `=`');
125
+ i++; ws(); assign(cur, path, value());
126
+ }
127
+ eol();
128
+ }
129
+ }
@@ -0,0 +1,95 @@
1
+ /* trace.mjs's C / C++ runner (design §3.14 C and C++ rows, §3.17): the repo's own `make` test target, built with
2
+ coverage and read with gcov, per test binary.
3
+ - accepted: `make test`, `make check` and `make -C <dir> test|check` (the make on PATH); no variables, no other
4
+ target, no other flag: the trace sets CC, CXX and SHELL itself;
5
+ - the run builds and runs the target once with `CC="cc --coverage"`, `CXX="c++ --coverage"` (a command-line
6
+ variable wins over the Makefile's own, and the flag reaches both compiling and linking) and SHELL set to
7
+ trace/c/fmtrace-sh, which runs each recipe line under the shell the Makefile's own top-level `SHELL =` names
8
+ (else /bin/sh), with the line's .SHELLFLAGS, and GCOV_PREFIX pointing at a dir of its own: the counters each
9
+ line's test binary writes land apart, and its exit status and output are kept beside them;
10
+ - each recipe line that wrote counters is one unit (`granularity: binary`); after the run, inside the same sandbox,
11
+ each counter file and the notes file the compiler wrote beside its object are copied into a fresh dir and read
12
+ there with `gcov -t` for the line counts of each source it covers. A test whose own lines ran in a unit belongs to
13
+ that unit (the first, if several), and its direct links are judged against that unit's coverage; its status
14
+ comes from GoogleTest's `[ RUN ]` / `[ OK ]` / `[ FAILED ]` lines or Unity's `file:line:test:PASS` lines, else from
15
+ its recipe line's exit status, only when every command of the line (lineHeads: one simple command, or each part of
16
+ an all-`&&` chain) is a native executable the build produced in the copy (ELF or Mach-O, isNativeExecutable), a
17
+ relative head read from the dir that line ran in (a sub-make's own, as fmtrace-sh records it): one such command
18
+ is passed on 0 and failed otherwise, an `&&` chain of them is passed on 0 and `unknown` otherwise.
19
+ Every other line is `unknown` either way: a script or a shell or interpreter at its head (`sh tests/run.sh`,
20
+ `./run.sh` starting with `#!`, `env`, `python`), a command that runs others (`find -exec`, `xargs`, `cd`, `time`),
21
+ `;`, `||`, `|`, a loop or a subshell. */
22
+ import { posix } from 'node:path';
23
+
24
+ /* -> { runner: 'make', dir, target } or null */
25
+ export function recogniseMake(words) {
26
+ if (posix.basename(words[0]) !== 'make') return null;
27
+ let i = 1, dir = '';
28
+ if (words[i] === '-C' && words[i + 1] != null) { dir = words[i + 1]; i += 2; }
29
+ if (dir.startsWith('/') || dir.split('/').includes('..')) return null;
30
+ if (words.length !== i + 1 || !/^(test|check)$/.test(words[i])) return null;
31
+ return { runner: 'make', dir: dir.replace(/^\.\/?/, '').replace(/\/$/, ''), target: words[i] };
32
+ }
33
+
34
+ /* the shell a Makefile's own top-level `SHELL = …` names (its last such assignment), or null: one that needs make to
35
+ expand it (`$(shell which bash)`) is left to the default */
36
+ export function makefileShell(text) {
37
+ let sh = null;
38
+ for (const m of text.matchAll(/^(?:(?:export|override)\s+)*SHELL\s*:{0,3}=[ \t]*(.*?)\s*(?:#.*)?$/gm)) sh = m[1];
39
+ return sh && !sh.includes('$') ? sh : null;
40
+ }
41
+
42
+ /* the head word of each command a recipe line runs, when the line is one simple command or simple commands joined by
43
+ `&&` (so its exit status is 0 only if each one's was); null for any other line (`;`, `||`, `|`, `&`, a newline, a
44
+ subshell or group, a command substitution). Quotes and redirections are read as a shell would; a quoted head word is
45
+ kept unreadable, so it names no file */
46
+ export function lineHeads(cmd) {
47
+ const segs = ['']; let q = null;
48
+ for (let i = 0; i < cmd.length; i++) {
49
+ const c = cmd[i];
50
+ if (q === "'") { if (c === "'") q = null; segs[segs.length - 1] += '\0'; continue; }
51
+ if (c === '`' || (c === '$' && cmd[i + 1] === '(')) return null;
52
+ if (c === '\\') { i++; segs[segs.length - 1] += '\0'; continue; }
53
+ if (q === '"') { if (c === '"') q = null; segs[segs.length - 1] += '\0'; continue; }
54
+ if (c === "'" || c === '"') { q = c; segs[segs.length - 1] += '\0'; continue; }
55
+ if (c === '&' && cmd[i + 1] === '&') { segs.push(''); i++; continue; }
56
+ if (c === '>' || c === '<') { if (cmd[i + 1] === '&' || cmd[i + 1] === c) i++; segs[segs.length - 1] += ' '; continue; }
57
+ if (';|&(){}\n'.includes(c)) return null;
58
+ segs[segs.length - 1] += c;
59
+ }
60
+ if (q) return null;
61
+ const heads = segs.map(x => x.trim().split(/\s+/).find(w => !/^\w+=/.test(w)));
62
+ return heads.every(Boolean) ? heads : null;
63
+ }
64
+
65
+ // a native executable's first bytes: ELF, Mach-O (32 and 64 bit, either byte order) and a universal Mach-O
66
+ const NATIVE_MAGIC = ['7f454c46', 'feedface', 'cefaedfe', 'feedfacf', 'cffaedfe', 'cafebabe'];
67
+ export const isNativeExecutable = buf => !!buf && buf.length >= 4 && NATIVE_MAGIC.includes(buf.subarray(0, 4).toString('hex'));
68
+
69
+ export const makeArgv = ({ make, dir, target, shell }) => [make, ...(dir ? ['-C', dir] : []), target, 'CC=cc --coverage', 'CXX=c++ --coverage', `SHELL=${shell}`];
70
+
71
+ /* gcov -t text (one or more files) -> Map(source as compiled -> Map(line -> count)) */
72
+ export function parseGcov(text) {
73
+ const out = new Map(); let cur = null;
74
+ for (const line of text.split('\n')) {
75
+ const m = /^\s*([^:]+):\s*(\d+):(.*)$/.exec(line);
76
+ if (!m) continue;
77
+ if (m[2] === '0') { const src = /^Source:(.*)$/.exec(m[3]); if (src) { cur = out.get(src[1]) || out.set(src[1], new Map()).get(src[1]); } continue; }
78
+ const c = m[1].trim();
79
+ if (!cur || c === '-') continue;
80
+ const n = /^[#=]+$/.test(c) ? 0 : parseInt(c.replace(/\*$/, ''), 10);
81
+ if (Number.isFinite(n)) cur.set(+m[2], Math.max(n, cur.get(+m[2]) || 0));
82
+ }
83
+ return out;
84
+ }
85
+
86
+ /* test outcomes a make run printed: GoogleTest and Unity lines -> Map(name -> status) */
87
+ export function parseCTestOutput(text) {
88
+ const out = new Map();
89
+ for (const m of text.matchAll(/^\[\s+(OK|FAILED|SKIPPED)\s+\]\s+([\w/]+\.[\w/]+)(?: \(\d+ ms\))?\s*$/gm))
90
+ out.set(m[2], m[1] === 'OK' ? 'passed' : m[1] === 'FAILED' ? 'failed' : 'skipped');
91
+ for (const m of text.matchAll(/^[^\s:]+:\d+:(\w+):(PASS|FAIL|IGNORE)\b/gm)) out.set(m[1], m[2] === 'PASS' ? 'passed' : m[2] === 'FAIL' ? 'failed' : 'skipped');
92
+ return out;
93
+ }
94
+
95
+ export const C_NETWORK = /\b(connect|getaddrinfo|socket)\b[^\n]*\b(Operation not permitted|Permission denied|Connection refused|No route to host|failure)\b|\bconnect\(\)[^\n]*/g;