explainer-video 0.0.0-stage → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +12 -0
- package/LICENSE +21 -0
- package/README.md +152 -2
- package/cordis.patch.yml +7 -0
- package/lib/index.js +35 -0
- package/package.json +56 -4
- package/plugin.json +13 -0
- package/skills/coursework-author/.synced-from +5 -0
- package/skills/coursework-author/SKILL.md +209 -0
- package/skills/craft-codeflow-flows/SKILL.md +116 -0
- package/skills/craft-codeflow-mine/SKILL.md +207 -0
- package/skills/craft-codeflow-plan/SKILL.md +65 -0
- package/skills/craft-codeflow-scene/SKILL.md +168 -0
- package/skills/craft-video-align/SKILL.md +109 -0
- package/skills/craft-video-assemble/SKILL.md +69 -0
- package/skills/craft-video-audio/SKILL.md +59 -0
- package/skills/craft-video-enrich/SKILL.md +46 -0
- package/skills/craft-video-plan/SKILL.md +57 -0
- package/skills/craft-video-publish/SKILL.md +79 -0
- package/skills/craft-video-render/SKILL.md +68 -0
- package/skills/craft-video-scene/SKILL.md +112 -0
- package/skills/craft-video-script/SKILL.md +80 -0
- package/skills/explainer-video-from-codebase/FLOWS.md +577 -0
- package/skills/explainer-video-from-codebase/SKILL.md +659 -0
- package/skills/explainer-video-from-codebase/tools/apply-flows.mjs +531 -0
- package/skills/explainer-video-from-codebase/tools/approve-flows.mjs +56 -0
- package/skills/explainer-video-from-codebase/tools/codemap-md.mjs +116 -0
- package/skills/explainer-video-from-codebase/tools/config/dotenv.mjs +13 -0
- package/skills/explainer-video-from-codebase/tools/config/json.mjs +14 -0
- package/skills/explainer-video-from-codebase/tools/config/redact.mjs +16 -0
- package/skills/explainer-video-from-codebase/tools/config/toml-lite.mjs +13 -0
- package/skills/explainer-video-from-codebase/tools/config/yaml-lite.mjs +18 -0
- package/skills/explainer-video-from-codebase/tools/excerpt-check.mjs +186 -0
- package/skills/explainer-video-from-codebase/tools/excerpt.mjs +116 -0
- package/skills/explainer-video-from-codebase/tools/flow-diagram.mjs +168 -0
- package/skills/explainer-video-from-codebase/tools/flow-plan.mjs +129 -0
- package/skills/explainer-video-from-codebase/tools/flows-md.mjs +87 -0
- package/skills/explainer-video-from-codebase/tools/lang/c.mjs +684 -0
- package/skills/explainer-video-from-codebase/tools/lang/go.mjs +673 -0
- package/skills/explainer-video-from-codebase/tools/lang/python-config.mjs +143 -0
- package/skills/explainer-video-from-codebase/tools/lang/python-deps.mjs +62 -0
- package/skills/explainer-video-from-codebase/tools/lang/python-stdlib.mjs +24 -0
- package/skills/explainer-video-from-codebase/tools/lang/python.mjs +609 -0
- package/skills/explainer-video-from-codebase/tools/lang/typescript.mjs +963 -0
- package/skills/explainer-video-from-codebase/tools/lib/decompose.mjs +102 -0
- package/skills/explainer-video-from-codebase/tools/lib/groups.mjs +77 -0
- package/skills/explainer-video-from-codebase/tools/lib/observe-coverage.mjs +68 -0
- package/skills/explainer-video-from-codebase/tools/lib/observe-v8.mjs +232 -0
- package/skills/explainer-video-from-codebase/tools/lib/observe.mjs +162 -0
- package/skills/explainer-video-from-codebase/tools/lib/redact.mjs +28 -0
- package/skills/explainer-video-from-codebase/tools/lib/sandbox.mjs +97 -0
- package/skills/explainer-video-from-codebase/tools/lib/test-links.mjs +28 -0
- package/skills/explainer-video-from-codebase/tools/lib/toml.mjs +129 -0
- package/skills/explainer-video-from-codebase/tools/lib/trace-c.mjs +95 -0
- package/skills/explainer-video-from-codebase/tools/lib/trace-go.mjs +101 -0
- package/skills/explainer-video-from-codebase/tools/lib/trace-js.mjs +136 -0
- package/skills/explainer-video-from-codebase/tools/lib/tree-sitter.mjs +19 -0
- package/skills/explainer-video-from-codebase/tools/lib/walk.mjs +138 -0
- package/skills/explainer-video-from-codebase/tools/mine.mjs +409 -0
- package/skills/explainer-video-from-codebase/tools/propose.mjs +127 -0
- package/skills/explainer-video-from-codebase/tools/rank.mjs +119 -0
- package/skills/explainer-video-from-codebase/tools/ranking-md.mjs +76 -0
- package/skills/explainer-video-from-codebase/tools/rules/c-handlers.mjs +53 -0
- package/skills/explainer-video-from-codebase/tools/rules/celery-taskiq.mjs +48 -0
- package/skills/explainer-video-from-codebase/tools/rules/click-typer.mjs +6 -0
- package/skills/explainer-video-from-codebase/tools/rules/express.mjs +72 -0
- package/skills/explainer-video-from-codebase/tools/rules/fastapi.mjs +99 -0
- package/skills/explainer-video-from-codebase/tools/rules/go-http.mjs +173 -0
- package/skills/explainer-video-from-codebase/tools/rules/go-test.mjs +108 -0
- package/skills/explainer-video-from-codebase/tools/rules/grpc.mjs +57 -0
- package/skills/explainer-video-from-codebase/tools/rules/gtest.mjs +97 -0
- package/skills/explainer-video-from-codebase/tools/rules/haystack.mjs +55 -0
- package/skills/explainer-video-from-codebase/tools/rules/jest-vitest.mjs +100 -0
- package/skills/explainer-video-from-codebase/tools/rules/js-http-client.mjs +111 -0
- package/skills/explainer-video-from-codebase/tools/rules/nextjs.mjs +53 -0
- package/skills/explainer-video-from-codebase/tools/rules/process-spawn.mjs +49 -0
- package/skills/explainer-video-from-codebase/tools/rules/py-http-client.mjs +26 -0
- package/skills/explainer-video-from-codebase/tools/rules/pytest.mjs +107 -0
- package/skills/explainer-video-from-codebase/tools/rules/react-router.mjs +17 -0
- package/skills/explainer-video-from-codebase/tools/rules/registry.mjs +31 -0
- package/skills/explainer-video-from-codebase/tools/trace/c/fmtrace-sh +14 -0
- package/skills/explainer-video-from-codebase/tools/trace/js/fmtrace-net.cjs +33 -0
- package/skills/explainer-video-from-codebase/tools/trace/js/fmtrace-srcmap.cjs +41 -0
- package/skills/explainer-video-from-codebase/tools/trace/python/fmtrace_plugin.py +87 -0
- package/skills/explainer-video-from-codebase/tools/trace/python/sitecustomize.py +330 -0
- package/skills/explainer-video-from-codebase/tools/trace/sandbox/linux-bwrap.sh +73 -0
- package/skills/explainer-video-from-codebase/tools/trace/sandbox/macos-nonet.sb +41 -0
- package/skills/explainer-video-from-codebase/tools/trace.mjs +577 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-c.txt +21 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-cpp.txt +21 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-go.txt +21 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-javascript.txt +21 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-python.txt +21 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/tree-sitter-typescript.txt +21 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/LICENSES/web-tree-sitter.txt +21 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/MANIFEST.json +172 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-c.wasm +0 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-cpp.wasm +0 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-go.wasm +0 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-javascript.wasm +0 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-python.wasm +0 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-tsx.wasm +0 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/tree-sitter-typescript.wasm +0 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/web-tree-sitter.js +4081 -0
- package/skills/explainer-video-from-codebase/vendor/tree-sitter/web-tree-sitter.wasm +0 -0
- package/skills/explainer-video-from-coursework/ARCHITECTURE.md +59 -0
- package/skills/explainer-video-from-coursework/PIPELINE.md +343 -0
- package/skills/explainer-video-from-coursework/ROADMAP.md +725 -0
- package/skills/explainer-video-from-coursework/SERIES.md +232 -0
- package/skills/explainer-video-from-coursework/SETUP.md +105 -0
- package/skills/explainer-video-from-coursework/SKILL.md +858 -0
- package/skills/explainer-video-from-coursework/scenes/_pt-chart.js +12 -0
- package/skills/explainer-video-from-coursework/scenes/_pt-equation.js +10 -0
- package/skills/explainer-video-from-coursework/scenes/_pt-hubspoke.js +7 -0
- package/skills/explainer-video-from-coursework/scenes/_pt-loopflow.js +7 -0
- package/skills/explainer-video-from-coursework/scenes/_pt-sequence.js +12 -0
- package/skills/explainer-video-from-coursework/scenes/_pt-timeline.js +11 -0
- package/skills/explainer-video-from-coursework/src/scene-code.html +127 -0
- package/skills/explainer-video-from-coursework/src/scene-code.js +379 -0
- package/skills/explainer-video-from-coursework/src/scene-core.js +1250 -0
- package/skills/explainer-video-from-coursework/src/scene-diagram.html +50 -0
- package/skills/explainer-video-from-coursework/src/scene-svg.html +50 -0
- package/skills/explainer-video-from-coursework/src/timeline.js +297 -0
- package/skills/explainer-video-from-coursework/src/timing-chart.js +160 -0
- package/skills/explainer-video-from-coursework/tools/align.mjs +419 -0
- package/skills/explainer-video-from-coursework/tools/apply-enrichment.mjs +206 -0
- package/skills/explainer-video-from-coursework/tools/assemble.mjs +223 -0
- package/skills/explainer-video-from-coursework/tools/build-audio.mjs +234 -0
- package/skills/explainer-video-from-coursework/tools/chromium.mjs +73 -0
- package/skills/explainer-video-from-coursework/tools/debug-page.mjs +75 -0
- package/skills/explainer-video-from-coursework/tools/env.mjs +30 -0
- package/skills/explainer-video-from-coursework/tools/frame-dedup.mjs +131 -0
- package/skills/explainer-video-from-coursework/tools/key-source.mjs +151 -0
- package/skills/explainer-video-from-coursework/tools/model.mjs +21 -0
- package/skills/explainer-video-from-coursework/tools/naming.mjs +38 -0
- package/skills/explainer-video-from-coursework/tools/onset-audit.mjs +51 -0
- package/skills/explainer-video-from-coursework/tools/pacing-check.mjs +350 -0
- package/skills/explainer-video-from-coursework/tools/planner.mjs +616 -0
- package/skills/explainer-video-from-coursework/tools/publish.mjs +230 -0
- package/skills/explainer-video-from-coursework/tools/render.mjs +205 -0
- package/skills/explainer-video-from-coursework/tools/resolve-beats.mjs +239 -0
- package/skills/explainer-video-from-coursework/tools/script-check.mjs +174 -0
- package/skills/explainer-video-from-coursework/tools/stamp.mjs +71 -0
- package/skills/explainer-video-from-coursework/tools/timing-report.mjs +299 -0
- package/skills/explainer-video-from-coursework/tools/transcribe-gemini.mjs +96 -0
- package/skills/explainer-video-from-coursework/tools/tts-gemini.mjs +394 -0
- package/skills/explainer-video-from-coursework/tools/verify-audio.mjs +88 -0
- package/skills/explainer-video-from-coursework/tools/workdir.mjs +124 -0
- package/skills/explainer-video-from-coursework/vendor/plotly-basic.min.js +8 -0
- package/skills/explainer-video-from-coursework/vendor/plotly-basic.min.js.LICENSE.txt +16 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* =============================================================================
|
|
3
|
+
rank.mjs — order each group's top-level candidates by the new code they cover,
|
|
4
|
+
and print every row with the evidence the agent reads to propose a series.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
node tools/rank.mjs --candidates out/<name>/candidates.json [--explain-top 10] [--explain-top-eval 5]
|
|
8
|
+
|
|
9
|
+
Writes ranking.json and ranking.md beside candidates.json. Per group
|
|
10
|
+
(prod, eval, dev) the order is greedy marginal coverage of that group's
|
|
11
|
+
own symbols, ties broken by more crossings. There is no cut line and no series:
|
|
12
|
+
each row carries its nearest higher-ranked row with their Jaccard overlap, how
|
|
13
|
+
many of its functions tests pin, which evals exercise it, and its config-
|
|
14
|
+
dependent dispatch points. A second table lists production modules that many
|
|
15
|
+
candidates share and many tests pin, since cross-cutting code has no entry point
|
|
16
|
+
of its own. The merged prod+eval order is evidence only. Dev rows are printed
|
|
17
|
+
and filtered by default.
|
|
18
|
+
|
|
19
|
+
Exit: 0 ok · 1 bad flags, unreadable candidates, or candidates.json inside the repo
|
|
20
|
+
========================================================================== */
|
|
21
|
+
import { readFileSync, writeFileSync, existsSync, realpathSync } from 'node:fs';
|
|
22
|
+
import { join, resolve, dirname, sep, relative } from 'node:path';
|
|
23
|
+
import { renderRanking, configDispatchPoints } from './ranking-md.mjs';
|
|
24
|
+
|
|
25
|
+
const fail = (code, msg) => { console.error(`[rank] ${msg}`); process.exit(code); };
|
|
26
|
+
|
|
27
|
+
/* ---------------- args ---------------- */
|
|
28
|
+
const argv = process.argv.slice(2);
|
|
29
|
+
const VALUED = new Set(['--candidates', '--explain-top', '--explain-top-eval']);
|
|
30
|
+
const flags = new Map();
|
|
31
|
+
for (let i = 0; i < argv.length; i++) {
|
|
32
|
+
if (VALUED.has(argv[i])) { if (argv[i + 1] == null) fail(1, `${argv[i]} needs a value`); flags.set(argv[i], argv[++i]); }
|
|
33
|
+
else fail(1, `unknown argument ${argv[i]}`);
|
|
34
|
+
}
|
|
35
|
+
if (!flags.has('--candidates')) fail(1, 'usage: rank.mjs --candidates out/<name>/candidates.json [--explain-top 10] [--explain-top-eval 5]');
|
|
36
|
+
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; };
|
|
37
|
+
const params = { explainTop: count('--explain-top', 10), explainTopEval: count('--explain-top-eval', 5) };
|
|
38
|
+
const CANDIDATES = resolve(flags.get('--candidates'));
|
|
39
|
+
let r;
|
|
40
|
+
try { r = JSON.parse(readFileSync(CANDIDATES, 'utf8')); } catch (e) { fail(1, `cannot read ${CANDIDATES}: ${e.message}`); }
|
|
41
|
+
if (r.schema !== 1 || !r.candidates || !r.bySymbol) fail(1, `${CANDIDATES} is not a propose.mjs candidates file (schema 1)`);
|
|
42
|
+
const OUT = dirname(CANDIDATES);
|
|
43
|
+
if (r.root && existsSync(r.root)) { const root = realpathSync(r.root), o = realpathSync(OUT); if (o === root || o.startsWith(root + sep)) fail(1, `refusing to write inside the repo: ${OUT}`); }
|
|
44
|
+
|
|
45
|
+
/* ---------------- rank ---------------- */
|
|
46
|
+
const cands = Object.entries(r.candidates).map(([id, c]) => ({ id, ...c, own: new Set(c.ownIds) }));
|
|
47
|
+
const topLevel = grp => cands.filter(c => c.group === grp && !c.continues);
|
|
48
|
+
const pinnedBy = id => r.bySymbol[id]?.direct || [];
|
|
49
|
+
const jaccard = (a, b) => { let i = 0; for (const x of a) if (b.has(x)) i++; return i / (a.size + b.size - i || 1); };
|
|
50
|
+
const evalPool = topLevel('eval');
|
|
51
|
+
const exercisesOf = c => c.exercises.map(x => x.to);
|
|
52
|
+
|
|
53
|
+
// greedy marginal coverage: repeatedly take the candidate that adds the most not-yet-covered own symbols
|
|
54
|
+
function rankPool(pool) {
|
|
55
|
+
const universe = new Set(pool.flatMap(c => [...c.own])), covered = new Set(), ranked = [], left = [...pool];
|
|
56
|
+
while (left.length) {
|
|
57
|
+
let best = null, bestGain = -1;
|
|
58
|
+
for (const c of left) { let gain = 0; for (const s of c.own) if (!covered.has(s)) gain++;
|
|
59
|
+
if (gain > bestGain || (gain === bestGain && c.crossings.length > best.crossings.length)) { best = c; bestGain = gain; } }
|
|
60
|
+
left.splice(left.indexOf(best), 1);
|
|
61
|
+
const newSyms = [...best.own].filter(s => !covered.has(s));
|
|
62
|
+
for (const s of best.own) covered.add(s);
|
|
63
|
+
let near = null, nj = -1;
|
|
64
|
+
for (const x of ranked) { const j = jaccard(best.own, x.c.own); if (j > nj) { nj = j; near = x; } }
|
|
65
|
+
const tests = new Set([...best.own].flatMap(pinnedBy)), testsNew = new Set(newSyms.flatMap(pinnedBy));
|
|
66
|
+
ranked.push({ c: best, gain: bestGain, cumulative: covered.size, nearest: near ? near.c.id : null, jaccard: near ? Number(nj.toFixed(2)) : null,
|
|
67
|
+
pinned: [...best.own].filter(s => pinnedBy(s).length).length, tests: tests.size, testsNew: testsNew.size,
|
|
68
|
+
cfg: configDispatchPoints(best.dispatch).length,
|
|
69
|
+
evals: evalPool.filter(e => e.usesProd.some(s => best.own.has(s)) || exercisesOf(e).some(s => best.own.has(s))).map(e => e.id) });
|
|
70
|
+
}
|
|
71
|
+
return { universe, ranked };
|
|
72
|
+
}
|
|
73
|
+
const pools = { prod: rankPool(topLevel('prod')), eval: rankPool(evalPool), dev: rankPool(topLevel('dev')) };
|
|
74
|
+
const merged = rankPool([...topLevel('prod'), ...evalPool]); // evidence only: where eval flows would land on one list
|
|
75
|
+
|
|
76
|
+
// the first-ranked production row whose own code holds a symbol: which flow an eval exercises
|
|
77
|
+
const prodRanked = pools.prod.ranked;
|
|
78
|
+
const flowRank = id => { const i = prodRanked.findIndex(x => x.c.own.has(id)); return i === -1 ? null : i + 1; };
|
|
79
|
+
const row = (x, i, universe) => ({
|
|
80
|
+
rank: i + 1, entry: x.c.id, group: x.c.group, kind: x.c.kind, method: x.c.method, path: x.c.path, key: x.c.key, file: x.c.file, line: x.c.line,
|
|
81
|
+
gain: x.gain, cumulative: x.cumulative, coverage: Number((x.cumulative / (universe.size || 1)).toFixed(3)),
|
|
82
|
+
own: x.c.own.size, folded: x.c.folded, modules: x.c.modules, crossings: x.c.crossings.length, nearest: x.nearest, jaccard: x.jaccard,
|
|
83
|
+
pinned: x.pinned, tests: x.tests, testsNew: x.testsNew, cfg: x.cfg, evals: x.evals,
|
|
84
|
+
...(x.c.group !== 'prod' ? {
|
|
85
|
+
exercises: [...new Map(x.c.exercises.map(e => [e.to, { to: e.to, kind: e.kind, flowRank: flowRank(e.to) }])).values()],
|
|
86
|
+
usesProd: x.c.usesProd.map(id => ({ id, flowRank: flowRank(id) })) } : {}),
|
|
87
|
+
});
|
|
88
|
+
const ranked = Object.fromEntries(Object.entries(pools).map(([g, p]) => [g, p.ranked.map((x, i) => row(x, i, p.universe))]));
|
|
89
|
+
|
|
90
|
+
// shared, heavily tested production code: a module whose functions appear in >= 3 top-level production candidates
|
|
91
|
+
// and that >= 5 tests pin directly, listed with the first row that owns it
|
|
92
|
+
const sym = r.symbols;
|
|
93
|
+
const mods = new Map();
|
|
94
|
+
prodRanked.forEach((x, i) => { for (const n of x.c.nodes) { const s = sym[n.id];
|
|
95
|
+
if (!s || s.group !== 'prod' || (s.kind !== 'function' && s.kind !== 'method')) continue;
|
|
96
|
+
const mod = mods.get(s.file) || mods.set(s.file, { cands: new Set(), fns: new Set(), tests: new Set() }).get(s.file);
|
|
97
|
+
mod.cands.add(i + 1); mod.fns.add(n.id); for (const t of pinnedBy(n.id)) mod.tests.add(t); } });
|
|
98
|
+
const shared = [...mods].map(([file, x]) => ({ module: file, candidates: x.cands.size, firstOwner: Math.min(...x.cands),
|
|
99
|
+
firstOwnerEntry: prodRanked[Math.min(...x.cands) - 1].c.id, functions: x.fns.size, directTests: x.tests.size }))
|
|
100
|
+
.filter(x => x.candidates >= 3 && x.directTests >= 5)
|
|
101
|
+
.sort((a, b) => b.directTests - a.directTests || b.candidates - a.candidates || (a.module < b.module ? -1 : 1));
|
|
102
|
+
|
|
103
|
+
const continuations = cands.filter(c => c.continues).map(c => ({ entry: c.id, group: c.group, kind: c.kind, method: c.method, path: c.path, key: c.key, parents: c.continues, own: c.own.size }));
|
|
104
|
+
const ranking = {
|
|
105
|
+
schema: 1, tool: { name: 'rank.mjs' }, root: r.root, commit: r.commit, treeHash: r.treeHash, params,
|
|
106
|
+
totals: { topLevel: Object.fromEntries(Object.entries(pools).map(([g, p]) => [g, p.ranked.length])),
|
|
107
|
+
universe: Object.fromEntries(Object.entries(pools).map(([g, p]) => [g, p.universe.size])), continuations: continuations.length },
|
|
108
|
+
ranked,
|
|
109
|
+
merged: merged.ranked.map((x, i) => ({ rank: i + 1, entry: x.c.id, group: x.c.group, gain: x.gain })),
|
|
110
|
+
shared, continuations,
|
|
111
|
+
};
|
|
112
|
+
writeFileSync(join(OUT, 'ranking.json'), JSON.stringify(ranking, null, 1) + '\n');
|
|
113
|
+
writeFileSync(join(OUT, 'ranking.md'), renderRanking(ranking, r));
|
|
114
|
+
|
|
115
|
+
const t = ranking.totals;
|
|
116
|
+
console.log(`[rank] top-level ${JSON.stringify(t.topLevel)} · universes ${JSON.stringify(t.universe)} · ${t.continuations} continuations · no cut line`);
|
|
117
|
+
console.log(`[rank] merged prod+eval (evidence only): ${ranking.merged.slice(0, 5).map(x => `${x.rank}:${x.entry.split(':').pop()}[${x.group}]`).join(' ')}`);
|
|
118
|
+
console.log(`[rank] shared heavily-tested production modules ${shared.length}${shared.length ? `, led by ${shared[0].module} (${shared[0].directTests} direct tests)` : ''}`);
|
|
119
|
+
console.log(`[rank] wrote ${relative(process.cwd(), OUT) || '.'}/ranking.{json,md}`);
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/* ranking.md: every row of every group with its evidence columns, the shared heavily-tested table, every
|
|
2
|
+
continuation with its parent, the merged prod+eval order, and for the top rows of prod and eval their segments,
|
|
3
|
+
config-dependent dispatch points and config-guarded branches. No row is marked chosen: the agent proposes. */
|
|
4
|
+
const short = id => id.endsWith(':<main>') ? id.split(':')[0].split('.').pop() + ':<main>' : id.split(':').pop();
|
|
5
|
+
const table = (head, rows) => rows.length ? [`| ${head.join(' | ')} |`, `|${head.map(() => '---').join('|')}|`, ...rows.map(r => `| ${r.map(c => String(c ?? '').replace(/\|/g, '\\|')).join(' | ')} |`)].join('\n') : '_none_';
|
|
6
|
+
const what = x => x.path ? [x.method, x.path].filter(Boolean).join(' ') : x.key || '';
|
|
7
|
+
const pct = x => `${Math.round(100 * x)}%`;
|
|
8
|
+
|
|
9
|
+
// a dispatch point whose targets depend on config: narrowed by a config read, picked by selectors, or a collaborator
|
|
10
|
+
// with more than one concrete implementation; one per site and target set, however many times the walk passed it
|
|
11
|
+
export const configDispatchPoints = dispatch => { const seen = new Set();
|
|
12
|
+
return dispatch.filter(d => d.config || d.selectors || (d.collaborator && d.after > 1)).filter(d => { const key = d.site + d.targets.join(); return !seen.has(key) && seen.add(key); }); };
|
|
13
|
+
|
|
14
|
+
export function renderRanking(k, r) {
|
|
15
|
+
const out = [];
|
|
16
|
+
const t = k.totals;
|
|
17
|
+
out.push(`# Ranking: ${(k.root || '').split('/').pop()}`, '',
|
|
18
|
+
`- ${k.commit ? `commit \`${k.commit.slice(0, 7)}\` · ` : ''}tree ${(k.treeHash || '').slice(0, 19)}…`,
|
|
19
|
+
`- top-level candidates: prod ${t.topLevel.prod}, eval ${t.topLevel.eval}, dev ${t.topLevel.dev} · ${t.continuations} continuations · universes prod ${t.universe.prod}, eval ${t.universe.eval}, dev ${t.universe.dev} own symbols`,
|
|
20
|
+
`- walk: same-group fan-in >= ${r.params.fanin} with a subtree <= ${r.params.leafy} folds a helper · interface folding ${r.params.ifaceFold ? 'on' : 'off'}`, '',
|
|
21
|
+
'Each group is ordered by the new own code a row adds (`gain`), ties by more crossings. **There is no cut line and no series**: ' +
|
|
22
|
+
'new code is not important code. Read `nearest (J)` to judge "same flow" against "different flow", and the test, eval and config ' +
|
|
23
|
+
'columns for importance the order cannot see. Every high-ranked row needs a decision with a reason in `flows.json`.', '',
|
|
24
|
+
'Columns: `own` expanded symbols · `cross` crossings (queue, HTTP, spawn) · `pinned` own functions with a direct test link · ' +
|
|
25
|
+
'`tests` distinct tests pinning own code · `new` tests pinning only this row\'s new code · `cfg` config-dependent dispatch points · `evals` eval candidates exercising this row.', '');
|
|
26
|
+
|
|
27
|
+
const rows = (grp, xs) => table(['rank', 'gain', 'cum', 'kind', 'entry', 'route / key', 'own', 'cross', 'nearest (J)', 'pinned', 'tests', 'new', 'cfg', 'evals'],
|
|
28
|
+
xs.map(x => [x.rank, x.gain, pct(x.coverage), x.kind, `\`${short(x.entry)}\``, what(x), x.own, x.crossings,
|
|
29
|
+
x.nearest ? `#${xs.find(y => y.entry === x.nearest).rank} \`${short(x.nearest)}\` (${x.jaccard})` : '—', x.pinned, x.tests, x.testsNew, x.cfg,
|
|
30
|
+
grp === 'prod' ? x.evals.map(e => `\`${short(e)}\``).join(', ') : x.evals.length]));
|
|
31
|
+
const prodRank = new Map(k.ranked.prod.map(x => [x.entry, x.rank]));
|
|
32
|
+
const flow = f => f.flowRank ? ` (in #${f.flowRank})` : '';
|
|
33
|
+
|
|
34
|
+
out.push(`## prod: ${t.topLevel.prod} rows`, '', rows('prod', k.ranked.prod), '');
|
|
35
|
+
out.push(`## eval: ${t.topLevel.eval} rows`, '', 'Eval flows are ranked in their own group and never filtered: account for each high-ranked row (include, variant, or skip with a reason). ' +
|
|
36
|
+
'An eval walk folds the production code it calls into `uses-prod` leaves; its crossings into production are `exercises` links, not continuations.', '', rows('eval', k.ranked.eval), '');
|
|
37
|
+
const KIND_ORDER = ['http', 'queue', 'spawn'];
|
|
38
|
+
const byKind = xs => [...xs].sort((a, b) => KIND_ORDER.indexOf(a.kind) - KIND_ORDER.indexOf(b.kind));
|
|
39
|
+
out.push('### What each eval row exercises in production', '', 'A spawn link is a process the harness starts (a worker, a scheduler) and every consumer it serves.', '',
|
|
40
|
+
table(['rank', 'entry', 'exercises (crossings)', 'other uses-prod leaves'], k.ranked.eval.map(x => { const crossed = new Set(x.exercises.map(e => e.to));
|
|
41
|
+
return [x.rank, `\`${short(x.entry)}\``, byKind(x.exercises).map(e => `${e.kind} \`${short(e.to)}\`${flow(e)}`).join(', ') || '—',
|
|
42
|
+
x.usesProd.filter(u => !crossed.has(u.id)).map(u => `\`${short(u.id)}\`${flow(u)}`).join(', ') || '—']; })), '');
|
|
43
|
+
out.push(`## dev: ${t.topLevel.dev} rows (filtered by default)`, '', 'Migrations, dev routes, debug, verification and ops scripts: printed, not proposed. Promote a row in `flows.json` with a reason.', '',
|
|
44
|
+
rows('dev', k.ranked.dev), '');
|
|
45
|
+
|
|
46
|
+
out.push('## Shared, heavily tested production code', '',
|
|
47
|
+
'Production modules whose functions appear in at least 3 production rows and that at least 5 tests pin directly. Cross-cutting code (auth, retry, tokens) ' +
|
|
48
|
+
'has no entry point, so the order above counts it only inside the first row that reaches it.', '',
|
|
49
|
+
table(['module', 'in rows', 'first owner', 'functions', 'direct tests'], k.shared.map(x => [`\`${x.module}\``, x.candidates, `#${x.firstOwner} \`${short(x.firstOwnerEntry)}\``, x.functions, x.directTests])), '');
|
|
50
|
+
|
|
51
|
+
out.push('## Continuations', '', 'Entries reached through another candidate\'s same-group crossing. They are shown inside their parent, not ranked on their own.', '',
|
|
52
|
+
table(['entry', 'group', 'route / key', 'own', 'reached from'], k.continuations.map(c => [`\`${short(c.entry)}\``, c.group, what(c), c.own,
|
|
53
|
+
c.parents.map(p => `${prodRank.has(p) ? `#${prodRank.get(p)} ` : ''}\`${short(p)}\``).join(', ')])), '');
|
|
54
|
+
|
|
55
|
+
out.push('## Merged prod + eval order (evidence only)', '', 'Where eval flows would land on one list. Groups stay separate; this order decides nothing.', '',
|
|
56
|
+
table(['rank', 'entry', 'group', 'gain'], k.merged.map(x => [x.rank, `\`${short(x.entry)}\``, x.group, x.gain])), '');
|
|
57
|
+
|
|
58
|
+
const explain = (grp, n) => {
|
|
59
|
+
for (const x of k.ranked[grp].slice(0, n)) {
|
|
60
|
+
const c = r.candidates[x.entry];
|
|
61
|
+
out.push(`### ${grp} #${x.rank} \`${short(x.entry)}\`${what(x) ? ` — ${what(x)}` : ''}`, '',
|
|
62
|
+
`${c.file}${c.line ? `:${c.line}` : ''} · ${x.own} own, ${c.folded} folded, ${x.modules} modules, depth ${c.maxDepth} · ${c.testDoubles.length} test doubles refused`, '',
|
|
63
|
+
table(['segment root', 'reached from', 'via', 'own', 'folded'], c.segments.map(s => [`\`${short(s.root)}\``, s.from ? `\`${short(s.from)}\`` : '(entry)', s.kind || '', s.own, s.folded])), '');
|
|
64
|
+
const points = configDispatchPoints(c.dispatch);
|
|
65
|
+
if (points.length) out.push('Dispatch points:', '', table(['site', 'how', 'targets', 'config'], points.map(d => [`\`${d.site}\``, `${d.how} ${d.before}→${d.after}${d.narrowedBy ? ` by \`${short(d.narrowedBy)}\`` : ''}`,
|
|
66
|
+
d.targets.map(short).join(', '), d.config ? `${d.config.classification}: ${d.config.alternatives.map(a => `\`${short(a.cls)}\` when ${a.guards.join(' & ') || '(fallthrough)'}`).join('; ')}` : ''])), '');
|
|
67
|
+
const bseen = new Set();
|
|
68
|
+
const branches = c.branches.filter(b => { const key = `${b.from}|${b.to}|${b.line}`; return !bseen.has(key) && bseen.add(key); });
|
|
69
|
+
if (branches.length) out.push('Config-guarded branches:', '', table(['from', 'to', 'guard', 'line'], branches.map(b => [`\`${short(b.from)}\``, `\`${short(b.to)}\``, b.cfg.join(' & ') + (b.onFailure ? ` (on failure: ${b.onFailure})` : ''), b.line])), '');
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
out.push('## Top rows explained', '');
|
|
73
|
+
explain('prod', k.params.explainTop);
|
|
74
|
+
explain('eval', k.params.explainTopEval);
|
|
75
|
+
return out.join('\n');
|
|
76
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/* Rule pack: C and C++ string-to-handler tables. A table is
|
|
2
|
+
- a C array of rows, each a string and a function (`{ "add", cmd_add }`, `{ .name = "list", .run = cmd_list }`);
|
|
3
|
+
- a C++ map initialised with such pairs (`std::map<std::string, Handler> t = {{"add", cmd_add}, …}`), a lambda
|
|
4
|
+
value included (named `t["add"]`), or filled by assignments (`t["add"] = cmd_add;`, `this->t_["x"] = …`).
|
|
5
|
+
Each row is a `command` entry at its function, keyed by its string. A function that names the table and calls
|
|
6
|
+
through it — the row's function field of a C struct (`c->run(…)`, `commands[i].run(…)`), or a C++ map's value
|
|
7
|
+
(`t[name](…)`, `t.at(name)(…)`, `it->second(…)`) — is the dispatcher: a `dispatch` edge to every row's function,
|
|
8
|
+
one site, so a walk from the dispatcher folds them as alternatives and each command is its own flow. */
|
|
9
|
+
import { addCEdge } from '../lang/c.mjs';
|
|
10
|
+
|
|
11
|
+
export function handlerTables(cm, R) {
|
|
12
|
+
const entries = [], claimed = new Set(), tables = [];
|
|
13
|
+
// assignments to one map (`t["a"] = f; t["b"] = g;`) are one table
|
|
14
|
+
const byName = new Map();
|
|
15
|
+
for (const t of cm.tables) {
|
|
16
|
+
const name = t.name.replace(/^this->/, '');
|
|
17
|
+
const key = t.assigned ? `${t.module}#${name}` : t.var;
|
|
18
|
+
if (!byName.has(key)) { byName.set(key, { ...t, name, rows: [] }); tables.push(byName.get(key)); }
|
|
19
|
+
byName.get(key).rows.push(...t.rows);
|
|
20
|
+
}
|
|
21
|
+
for (const t of tables) {
|
|
22
|
+
const sc = { module: t.module, ns: t.ns || [] };
|
|
23
|
+
const k = !t.map && t.elemType ? R.classNamed(t.elemType, sc) : null;
|
|
24
|
+
const fieldAt = i => k ? cm.classes.get(k).fieldOrder[i] : null;
|
|
25
|
+
t.handlers = [];
|
|
26
|
+
for (const r of t.rows) {
|
|
27
|
+
const key = r.cells.find(x => x.str != null)?.str;
|
|
28
|
+
const fnCell = r.cells.find(x => x.fnId || (x.ref && R.funcNamed(x.ref, sc)));
|
|
29
|
+
if (key == null || !fnCell) continue;
|
|
30
|
+
const id = fnCell.fnId || R.funcNamed(fnCell.ref, sc);
|
|
31
|
+
t.field ||= fnCell.field || fieldAt(fnCell.i);
|
|
32
|
+
t.handlers.push({ key, id, line: r.line });
|
|
33
|
+
entries.push({ kind: 'command', key, id, file: t.file, line: r.line, rule: 'c.handler-table', table: t.name });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// the dispatcher: a function that names the table and calls through a row (C) or a map value (C++)
|
|
37
|
+
const links = [];
|
|
38
|
+
cm.rawCalls.forEach((c, i) => {
|
|
39
|
+
if (c.isNew || c.top) return;
|
|
40
|
+
const refs = cm.refs.get(c.from);
|
|
41
|
+
for (const t of tables) {
|
|
42
|
+
if (!t.handlers.length || !refs?.has(t.name)) continue;
|
|
43
|
+
const through = t.map ? (c.fnType === 'subscript_expression' || c.fnType === 'call_expression' || c.attr === 'second') : (c.attr && c.attr === t.field);
|
|
44
|
+
if (!through) continue;
|
|
45
|
+
claimed.add(i);
|
|
46
|
+
const site = `${c.from}@${c.line}:${t.name}`;
|
|
47
|
+
for (const h of t.handlers) addCEdge(cm, c.from, h.id, 'dispatch', c.line, { site, rule: 'c.handler-table', key: h.key,
|
|
48
|
+
evidence: `${c.file}:${c.line} calls the ${t.name} row for "${h.key}" (${t.file}:${h.line})` });
|
|
49
|
+
links.push({ from: c.from, table: t.name, file: c.file, line: c.line, handlers: t.handlers.length });
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
return { entries, claimed, tables: tables.filter(t => t.handlers.length).map(t => ({ name: t.name, file: t.file, line: t.line, rows: t.handlers.length, field: t.field || null })), dispatchers: links };
|
|
53
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/* Rule pack: Celery / Taskiq.
|
|
2
|
+
- consumers: `@<broker>.task(task_name=K)` / `@shared_task` -> a `queue-consumer` entry keyed K, or `scheduled`
|
|
3
|
+
when the decorator carries `schedule=`;
|
|
4
|
+
- producers: `.kiq()/.delay()/.apply_async()` on a consumer, `send_task("K")`, `AsyncKicker(task_name="K")` ->
|
|
5
|
+
a `queue` crossing to the consumer. A kicker whose name is not a literal makes its function a dispatcher, which
|
|
6
|
+
the registry pack uses. */
|
|
7
|
+
import { addEdge } from '../lang/python.mjs';
|
|
8
|
+
|
|
9
|
+
export function consumerEntry(py, d) {
|
|
10
|
+
if (!/(\.|^)(task|shared_task)$/.test(d.expr)) return null;
|
|
11
|
+
const s = py.symbols.get(d.id);
|
|
12
|
+
return { kind: d.kwNames.includes('schedule') ? 'scheduled' : 'queue-consumer', id: d.id,
|
|
13
|
+
key: d.kwArgs.task_name || d.kwArgs.name || d.strArgs[0] || s.name, line: d.line, file: s.file, rule: 'celery-taskiq.consumer' };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const literal = a => a && a.type === 'string' ? a.s : null;
|
|
17
|
+
|
|
18
|
+
export function linkProducers(py, R, entries) {
|
|
19
|
+
const consumerByKey = new Map(entries.filter(e => e.kind === 'queue-consumer' || e.kind === 'scheduled').map(e => [e.key, e.id]));
|
|
20
|
+
const consumers = new Set(consumerByKey.values());
|
|
21
|
+
const kickers = [];
|
|
22
|
+
for (const c of py.rawCalls) {
|
|
23
|
+
if (c.attr === 'send_task' && literal(c.args[0]) != null) kickers.push({ fn: c.from, key: literal(c.args[0]), line: c.line });
|
|
24
|
+
if (/(^|\.)AsyncKicker$|(^|\.)kicker$/.test(c.expr)) {
|
|
25
|
+
const kw = c.args.find(a => a.kw === 'task_name');
|
|
26
|
+
const key = kw ? kw.s : literal(c.args[0]);
|
|
27
|
+
kickers.push({ fn: c.from, key, dynamic: key == null, line: c.line });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const links = [], dispatchers = new Set();
|
|
31
|
+
const sym = id => py.symbols.get(id);
|
|
32
|
+
const sameNameInBases = id => { const s = sym(id); return s?.cls ? R.mro(s.cls).slice(1).map(b => py.classes.get(b)?.methods.get(s.name)).filter(Boolean) : []; };
|
|
33
|
+
for (const k of kickers) {
|
|
34
|
+
if (k.key != null) { const consumer = consumerByKey.get(k.key);
|
|
35
|
+
if (consumer) { addEdge(py, k.fn, consumer, 'queue', k.line, { rule: 'taskiq.literal-key', evidence: `${sym(k.fn)?.file}:${k.line} kicks "${k.key}"` }); links.push({ fn: k.fn, key: k.key }); } }
|
|
36
|
+
else { dispatchers.add(k.fn); for (const m of sameNameInBases(k.fn)) dispatchers.add(m); }
|
|
37
|
+
}
|
|
38
|
+
// methods that call a dispatcher in the same class count as dispatchers too (enqueue_batch -> enqueue)
|
|
39
|
+
const rev = new Map();
|
|
40
|
+
for (const e of py.edges) if (!e.rule && /^(static|virtual|dispatch|ctor)$/.test(e.kind)) (rev.get(e.to) || rev.set(e.to, new Set()).get(e.to)).add(e.from);
|
|
41
|
+
for (const d of [...dispatchers]) for (const caller of rev.get(d) || []) if (sym(caller)?.cls && sym(caller)?.cls === sym(d)?.cls) dispatchers.add(caller);
|
|
42
|
+
for (const d of [...dispatchers]) for (const m of sameNameInBases(d)) dispatchers.add(m);
|
|
43
|
+
for (const c of py.rawCalls) if (c.attr && /^(kiq|delay|apply_async)$/.test(c.attr) && !c.obj.includes('(')) {
|
|
44
|
+
const r = R.resolveName(c.obj.split('.')[0], c);
|
|
45
|
+
if (r?.id && consumers.has(r.id)) { addEdge(py, c.from, r.id, 'queue', c.line, { rule: `taskiq.${c.attr}` }); links.push({ fn: c.from, direct: r.id }); }
|
|
46
|
+
}
|
|
47
|
+
return { links, dispatchers, consumerByKey };
|
|
48
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/* Rule pack: click / typer. Only a command decorator makes a CLI entry; `@click.option` and friends decorate
|
|
2
|
+
the same function and must not make one entry each. */
|
|
3
|
+
export function commandEntry(py, d, entries) {
|
|
4
|
+
if (!/(^|\.)command$|^app\.callback$/.test(d.expr) || entries.some(e => e.kind === 'cli' && e.id === d.id)) return null;
|
|
5
|
+
return { kind: 'cli', id: d.id, line: d.line, file: py.symbols.get(d.id).file, rule: 'click-typer.command' };
|
|
6
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/* Rule pack: Express. An app is `express()` (the default import of 'express' or its `require`); a router is
|
|
2
|
+
`express.Router()` or `Router()` from 'express' (a module-level variable, imported or not, a function's local, or a
|
|
3
|
+
parameter annotated `Router` / `Express` / `Application`). `<app|router>.<method>(path, ...handlers)` and
|
|
4
|
+
`<router>.route(path).<method>(...handlers)` make an `http` entry at the last handler; each handler before it is
|
|
5
|
+
middleware, linked from that handler (`framework`, express.middleware). `<app|router>.use([prefix,] router)` mounts
|
|
6
|
+
a router (a variable, a `require`, or a call of a function that returns one by name); the prefix is folded like a
|
|
7
|
+
route path (a const works). A route's path is every mount prefix up the chain, then its own; `:id` is written
|
|
8
|
+
`{id}`. A function handed to a registration is the router's to run, so it is never a call from the registering code. */
|
|
9
|
+
import { addTsEdge, foldString } from '../lang/typescript.mjs';
|
|
10
|
+
|
|
11
|
+
const METHOD = /^(get|post|put|patch|delete|del|all|options|head)$/;
|
|
12
|
+
const EXPRESS = /^express(\.default)?(\.Router)?$|^express\.(Express|Application|Router)$/;
|
|
13
|
+
|
|
14
|
+
export function expressRoutes(ts, R) {
|
|
15
|
+
const isExpress = (spec, sc) => { if (!spec) return false; const r = R.resolveExpr(spec.expr, sc); return !!(r?.ext && EXPRESS.test(r.ext)); };
|
|
16
|
+
// the router an expression names, as `<owner>#<name>`, or null
|
|
17
|
+
const routerOf = (name, c) => {
|
|
18
|
+
if (!/^[\w$]+$/.test(name)) return null;
|
|
19
|
+
if (c.types?.has(name)) return isExpress(c.types.get(name), c) ? `${c.from}#${name}` : null;
|
|
20
|
+
const r = R.resolveName(name, c);
|
|
21
|
+
return r?.var && isExpress(r.var, r.var) ? `${r.in}#${r.name}` : null;
|
|
22
|
+
};
|
|
23
|
+
const argRouter = (a, c) => {
|
|
24
|
+
if (a.name) return routerOf(a.name, c);
|
|
25
|
+
const req = /^require\(\s*["'`]([^"'`]+)["'`]\s*\)$/.exec(a.text);
|
|
26
|
+
if (req) { const mod = R.resolveModule(req[1], c.module); const r = mod && R.exportOf(mod, '*'); return r?.var && isExpress(r.var, r.var) ? `${r.in}#${r.name}` : null; }
|
|
27
|
+
const callee = a.type === 'call_expression' && /^([\w$]+(?:\.[\w$]+)*)\s*\(/.exec(a.text);
|
|
28
|
+
const fn = callee && R.resolveExpr(callee[1], c)?.id;
|
|
29
|
+
for (const ret of fn ? ts.returns.filter(x => x.fn === fn && x.kind === 'name') : []) { const r = routerOf(ret.name, { ...ret, from: fn }); if (r) return r; }
|
|
30
|
+
return null;
|
|
31
|
+
};
|
|
32
|
+
const handlerOf = (a, c) => {
|
|
33
|
+
if (a.fnId) return a.fnId;
|
|
34
|
+
if (a.name || a.member) { const r = R.resolveExpr(a.name || a.member, c); const s = r?.id && ts.symbols.get(r.id); return s && (s.kind === 'function' || s.kind === 'method') ? r.id : null; }
|
|
35
|
+
return null;
|
|
36
|
+
};
|
|
37
|
+
const pathOf = (a, c) => a?.ir ? foldString(ts, R, a.ir, c).s : null;
|
|
38
|
+
const mounts = new Map(), regs = [], claimed = new Set();
|
|
39
|
+
ts.rawCalls.forEach((c, i) => {
|
|
40
|
+
if (!c.attr) return;
|
|
41
|
+
const chained = c.routeChain || null;
|
|
42
|
+
const owner = chained ? routerOf(chained.owner, c) : c.obj ? routerOf(c.obj, c) : null;
|
|
43
|
+
if (!owner) return;
|
|
44
|
+
if (c.attr === 'use') {
|
|
45
|
+
const first = c.args[0] && !argRouter(c.args[0], c) ? pathOf(c.args[0], c) : null;
|
|
46
|
+
const prefix = first?.startsWith('/') ? first : '';
|
|
47
|
+
for (const a of c.args) { const child = argRouter(a, c); if (child && child !== owner && !mounts.has(child)) mounts.set(child, { parent: owner, prefix }); }
|
|
48
|
+
claimed.add(i);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (!METHOD.test(c.attr)) return;
|
|
52
|
+
const path = chained ? (chained.path ? foldString(ts, R, chained.path, c).s : null) : pathOf(c.args[0], c);
|
|
53
|
+
if (path == null || !path.startsWith('/') && path !== '*') return;
|
|
54
|
+
const handlers = (chained ? c.args : c.args.slice(1)).map(a => handlerOf(a, c)).filter(Boolean);
|
|
55
|
+
claimed.add(i);
|
|
56
|
+
if (!handlers.length) return;
|
|
57
|
+
regs.push({ owner, method: c.attr === 'all' ? '*' : c.attr === 'del' ? 'DELETE' : c.attr.toUpperCase(), path, handler: handlers.at(-1), middleware: handlers.slice(0, -1), file: c.file, line: c.line });
|
|
58
|
+
});
|
|
59
|
+
const fullPath = (owner, path, seen = new Set()) => {
|
|
60
|
+
let p = path;
|
|
61
|
+
for (let o = owner; mounts.has(o) && !seen.has(o); o = mounts.get(o).parent) { seen.add(o); p = `${mounts.get(o).prefix.replace(/\/$/, '')}${p.startsWith('/') ? '' : '/'}${p}`; }
|
|
62
|
+
p = p.replace(/\/{2,}/g, '/').replace(/:(\w+)\??/g, '{$1}');
|
|
63
|
+
return p.length > 1 ? p.replace(/\/$/, '') : p;
|
|
64
|
+
};
|
|
65
|
+
const routes = [];
|
|
66
|
+
for (const g of regs) {
|
|
67
|
+
const path = fullPath(g.owner, g.path);
|
|
68
|
+
routes.push({ kind: 'http', method: g.method, path, id: g.handler, file: g.file, line: g.line, rule: 'express' });
|
|
69
|
+
for (const mw of g.middleware) addTsEdge(ts, g.handler, mw, 'framework', g.line, { rule: 'express.middleware', evidence: `${g.file}:${g.line} ${g.method} ${path} runs ${ts.symbols.get(mw)?.name} first` });
|
|
70
|
+
}
|
|
71
|
+
return { routes, claimed, mounts: [...mounts].map(([child, m]) => ({ child, parent: m.parent, prefix: m.prefix })) };
|
|
72
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/* Rule pack: FastAPI / Starlette.
|
|
2
|
+
- routes: `@<router>.get/post/…(path)` where <router> is an APIRouter or FastAPI app (a module-level variable,
|
|
3
|
+
imported or not, or a function's local or parameter, built by `APIRouter()` / `FastAPI()`, an in-repo subclass
|
|
4
|
+
of either, or a factory returning one) -> an `http` entry. Its path is every `include_router(r, prefix=)` up the include chain (at module level
|
|
5
|
+
or inside an app factory) + `APIRouter(prefix=)` + the decorator path;
|
|
6
|
+
- DI: `p: T = Depends(provider)` and `Alias = Annotated[T, Depends(provider)]` -> handler links to provider;
|
|
7
|
+
a parameter typed by such an alias is narrowed to whatever the provider returns. */
|
|
8
|
+
import { addEdge } from '../lang/python.mjs';
|
|
9
|
+
|
|
10
|
+
const simpleType = t => {
|
|
11
|
+
const s = t.replace(/^["']|["']$/g, '').replace(/\s*\|\s*None\b|\bNone\s*\|\s*/g, '').replace(/^Optional\[(.*)\]$/, '$1').trim();
|
|
12
|
+
return /^[A-Za-z_][\w.]*$/.test(s) ? s : null;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// before call resolution: aliases must be known for receivers typed by them
|
|
16
|
+
export function registerAliases(py) {
|
|
17
|
+
for (const a of py.moduleAssigns) {
|
|
18
|
+
if (!a.sub || !/(^|\.)Annotated$/.test(a.sub.value)) continue;
|
|
19
|
+
const dep = a.sub.items.find(x => x.type === 'call' && /(^|\.)Depends$/.test(x.fn));
|
|
20
|
+
if (!dep || !a.sub.items[0]) continue;
|
|
21
|
+
const aid = `${a.module}:${a.name}`;
|
|
22
|
+
py.modules.get(a.module).defs.set(a.name, aid);
|
|
23
|
+
const provider = /^[\w.]+\(\s*([^,)]+)/.exec(dep.text)?.[1].trim() ?? null;
|
|
24
|
+
py.typeAliases.set(aid, { type: simpleType(a.sub.items[0].text), provider, line: a.line, module: a.module, imports: a.imports, locals: new Map() });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const ROUTER = /(^|\.)(APIRouter|FastAPI)$/;
|
|
29
|
+
|
|
30
|
+
// a router's prefix as mounted: each include_router prefix up the chain, then its own APIRouter(prefix=)
|
|
31
|
+
export function routePrefixes(py, R) {
|
|
32
|
+
const routerClass = id => R.mro(id).some(c => py.classes.get(c).baseExprs.some(b => ROUTER.test(b) || ROUTER.test(R.resolveExpr(b, py.classes.get(c))?.ext || '')));
|
|
33
|
+
const makesRouter = (expr, sc) => { if (!expr) return false; if (ROUTER.test(expr)) return true; const r = R.resolveExpr(expr, sc);
|
|
34
|
+
return !!(r?.ext && ROUTER.test(r.ext)) || !!(r?.id && (py.classes.has(r.id) ? routerClass(r.id) : R.factoryReturns(r.id).some(x => x.ext && ROUTER.test(x.ext)))); };
|
|
35
|
+
const routers = new Map(); // `module:name` (or `fn#name` for a local) -> own prefix
|
|
36
|
+
for (const a of py.moduleAssigns) if (a.fn && makesRouter(a.fn, a)) {
|
|
37
|
+
const kw = a.args.find(x => x.kw === 'prefix');
|
|
38
|
+
routers.set(`${a.module}:${a.name}`, kw?.s || '');
|
|
39
|
+
}
|
|
40
|
+
const moduleRouter = (name, sc, depth = 0) => { // a module-level router named in scope, followed through imports
|
|
41
|
+
if (sc.locals?.has(name)) return null;
|
|
42
|
+
if (routers.has(`${sc.module}:${name}`)) return `${sc.module}:${name}`;
|
|
43
|
+
const t = sc.imports.get(name);
|
|
44
|
+
if (!t?.from || depth > 4) return null;
|
|
45
|
+
const mod = R.findModule(t.from, t.in), M = py.modules.get(mod);
|
|
46
|
+
return M ? moduleRouter(t.name, { imports: M.imports, module: mod }, depth + 1) : null;
|
|
47
|
+
};
|
|
48
|
+
const routerOf = (expr, sc, fn) => { // `router`, `items.router`, an imported router, or a local / parameter
|
|
49
|
+
const parts = expr.split('.');
|
|
50
|
+
if (parts.length === 1) {
|
|
51
|
+
const spec = sc.types?.get(expr);
|
|
52
|
+
if (fn && spec?.expr && makesRouter(spec.expr, sc)) return `${fn}#${expr}`;
|
|
53
|
+
return moduleRouter(expr, sc);
|
|
54
|
+
}
|
|
55
|
+
const r = R.resolveName(parts[0], sc);
|
|
56
|
+
if (!r?.module) return null;
|
|
57
|
+
const id = `${[r.module, ...parts.slice(1, -1)].join('.')}:${parts.at(-1)}`;
|
|
58
|
+
return routers.has(id) ? id : null;
|
|
59
|
+
};
|
|
60
|
+
const includes = new Map(); // router -> { parent, prefix }, the first include_router of it
|
|
61
|
+
for (const c of py.rawCalls) if (c.attr === 'include_router' && c.args[0] && c.args[0].type !== 'keyword_argument') {
|
|
62
|
+
const parent = routerOf(c.obj, c, c.top ? null : c.from), child = routerOf(c.args[0].text, c, c.top ? null : c.from);
|
|
63
|
+
if (parent && child && !includes.has(child)) includes.set(child, { parent, prefix: c.args.find(x => x.kw === 'prefix')?.s || '' });
|
|
64
|
+
}
|
|
65
|
+
const mounted = (id, seen = new Set()) => { if (seen.has(id)) return ''; seen.add(id);
|
|
66
|
+
const inc = includes.get(id); return (inc ? mounted(inc.parent, seen) + inc.prefix : '') + (routers.get(id) || ''); };
|
|
67
|
+
// a decorator's route prefix, or null when its receiver is not a router (`@mock.patch(…)` is no route)
|
|
68
|
+
return d => { const id = routerOf(d.expr.slice(0, d.expr.lastIndexOf('.')), d, d.fn); return id ? mounted(id) : null; };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const HTTP = /\.(get|post|put|patch|delete|api_route|websocket)$/;
|
|
72
|
+
export function routeEntry(py, d, prefixOf) {
|
|
73
|
+
if (!HTTP.test(d.expr)) return null;
|
|
74
|
+
const prefix = prefixOf(d);
|
|
75
|
+
if (prefix == null) return null;
|
|
76
|
+
const s = py.symbols.get(d.id);
|
|
77
|
+
return { kind: 'http', id: d.id, method: d.expr.split('.').pop().toUpperCase(), path: prefix + (d.strArgs[0] || ''),
|
|
78
|
+
line: d.line, file: s.file, rule: 'fastapi.route' };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function linkDepends(py, R) {
|
|
82
|
+
const links = [];
|
|
83
|
+
const file = id => py.symbols.get(id)?.file;
|
|
84
|
+
for (const p of py.annotParams) {
|
|
85
|
+
const r = R.resolveExpr(p.annot, p); if (!r?.id || !py.typeAliases.has(r.id)) continue;
|
|
86
|
+
const a = py.typeAliases.get(r.id); const pr = a.provider ? R.resolveExpr(a.provider, a) : null;
|
|
87
|
+
if (!pr?.id) continue;
|
|
88
|
+
addEdge(py, p.fn, pr.id, 'framework', p.line, { rule: 'fastapi.Depends', evidence: `${file(p.fn)}:${p.line} ${p.param}: ${p.annot} = Annotated[${a.type}, Depends(${a.provider})] (${py.modules.get(a.module)?.file}:${a.line})` });
|
|
89
|
+
links.push({ fn: p.fn, provider: pr.id, param: p.param, via: p.annot });
|
|
90
|
+
}
|
|
91
|
+
for (const d of py.paramDefaults) {
|
|
92
|
+
if (!/(^|\.)Depends$/.test(d.call) || !d.a0) continue;
|
|
93
|
+
const r = R.resolveExpr(d.a0, d);
|
|
94
|
+
if (!r?.id) continue;
|
|
95
|
+
addEdge(py, d.fn, r.id, 'framework', d.line, { rule: 'fastapi.Depends', evidence: `${file(d.fn)}:${d.line} ${d.param}=Depends(${d.a0})` });
|
|
96
|
+
links.push({ fn: d.fn, provider: r.id, param: d.param });
|
|
97
|
+
}
|
|
98
|
+
return links;
|
|
99
|
+
}
|