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,173 @@
1
+ /* Rule pack: Go HTTP routers — net/http, gin, echo, chi. A router is a value made by `http.NewServeMux()` (or the
2
+ `http` package's default mux), `gin.Default()` / `gin.New()`, `echo.New()`, `chi.NewRouter()` / `chi.NewMux()`; a
3
+ variable, a struct field or a parameter typed as one of their router types (`*http.ServeMux`, `*gin.Engine`,
4
+ `*gin.RouterGroup`, `*echo.Echo`, `*echo.Group`, `chi.Router`, `*chi.Mux`) is one too. A route registration makes
5
+ an `http` entry at its handler:
6
+ - net/http: `Handle` / `HandleFunc(pattern, h)`, the pattern's method and path (Go 1.22: "GET /items/{id}"),
7
+ else any method; `http.HandlerFunc(f)` is f, a value whose type has `ServeHTTP` is that method, and a call of an
8
+ in-repo function that wraps a handler (`logged(h)`) is h, the wrapper linked as middleware;
9
+ - gin: `GET/POST/…/Any/Handle(…, h...)`, the last handler is the route's, the ones before it its middleware;
10
+ - echo: `GET/POST/…/Any/Add(…, h, mw...)`, the first is the route's, the rest its middleware;
11
+ - chi: `Get/Post/…/Method/Handle/HandleFunc(…, h)`.
12
+ Prefixes compose: gin and echo `Group(prefix)`, chi `Route(prefix, func(r chi.Router) {…})`, `Group(func(r) {…})`
13
+ and `Mount(prefix, sub)`, net/http `Handle(prefix, http.StripPrefix(p, sub))` or `Handle(p, sub)`, and a router
14
+ handed to a function (`routes.Register(api)`) carries its prefix into that function's parameter. `:id` and `*rest`
15
+ (gin, echo), `{id:[0-9]+}` (chi) and `{rest...}` (net/http) are written `{id}`. A function handed to a
16
+ registration is the router's to run, never a call from the registering code. */
17
+ import { addGoEdge, foldGo } from '../lang/go.mjs';
18
+
19
+ const KIND = [
20
+ [/^net\/http\.(ServeMux|NewServeMux)$/, 'net-http'],
21
+ [/^github\.com\/gin-gonic\/gin\.(Engine|RouterGroup|IRouter|IRoutes|Default|New)$/, 'gin'],
22
+ [/^github\.com\/labstack\/echo(\/v\d+)?\.(Echo|Group|New)$/, 'echo'],
23
+ [/^github\.com\/go-chi\/chi(\/v\d+)?\.(Mux|Router|NewRouter|NewMux)$/, 'chi'],
24
+ ];
25
+ const kindOfExt = ext => KIND.find(([re]) => re.test(ext || ''))?.[1] || null;
26
+ const METHODS = {
27
+ 'net-http': /^(Handle|HandleFunc)$/,
28
+ gin: /^(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS|Any|Handle)$/,
29
+ echo: /^(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS|CONNECT|TRACE|Any|Add)$/,
30
+ chi: /^(Get|Post|Put|Patch|Delete|Head|Options|Connect|Trace|Method|MethodFunc|Handle|HandleFunc)$/,
31
+ };
32
+ const VERB = { Get: 'GET', Post: 'POST', Put: 'PUT', Patch: 'PATCH', Delete: 'DELETE', Head: 'HEAD', Options: 'OPTIONS', Connect: 'CONNECT', Trace: 'TRACE' };
33
+
34
+ export function goRoutes(go, R) {
35
+ const fold = (a, c) => a ? foldGo(R, a.ir, c) : null;
36
+ const extCallee = (ir, sc) => ir?.k === 'call' ? R.calleeOf(ir.fn, sc)?.ext || null : null;
37
+ // a router expression -> { key, kind } (the key names one router value); mounts records where a child hangs
38
+ const mounts = new Map(), kinds = new Map();
39
+ const root = (key, kind) => { kinds.set(key, kind); return { key, kind }; };
40
+ const mount = (child, parent, prefix) => { if (child && parent && child.key !== parent.key && !mounts.has(child.key)) mounts.set(child.key, { parent: parent.key, prefix }); };
41
+ function ownerOf(ir, c, depth = 0) {
42
+ if (!ir || depth > 6) return null;
43
+ if (ir.k === 'id') {
44
+ const r = R.resolveName(ir.name, c);
45
+ if (r?.ext === 'net/http') return root('net/http#DefaultServeMux', 'net-http');
46
+ const spec = r?.local || r?.var;
47
+ if (!spec) return null;
48
+ const key = r.local ? `${c.from}#${ir.name}` : `${spec.module}#${ir.name}`;
49
+ if (spec.t === 'annot') {
50
+ const k = kindOfExt(R.classOf(spec.type, spec.module || c.module)?.ext);
51
+ if (!k) return null;
52
+ if (spec.closureOf != null) { // chi `Route(prefix, func(r chi.Router) {…})` / `Group(func(r) {…})`: a child of the call's router
53
+ const call = go.rawCalls[spec.closureOf], parent = call?.obj && ownerOf(call.obj, call, depth + 1);
54
+ const child = root(`${c.from}@${call?.line}#${ir.name}`, k);
55
+ if (parent) mount(child, parent, call.attr === 'Route' ? fold(call.args[0], call) : '');
56
+ return child;
57
+ }
58
+ const p = go.symbols.get(c.from)?.params?.indexOf(ir.name);
59
+ return root(p >= 0 ? `${c.from}#p${p}` : key, k);
60
+ }
61
+ if (spec.t !== 'expr') return null;
62
+ const sc = r.local ? c : { module: spec.module, from: `${spec.module}:<module>`, types: null, consts: null };
63
+ return named(key, valueOwner(spec.ir, sc, depth + 1));
64
+ }
65
+ if (ir.k === 'sel') { // a struct field: `s.router`
66
+ const t = R.typeOf(ir, c), k = t && kindOfExt(R.classOf(t.t, t.module)?.ext);
67
+ if (!k) return null;
68
+ const o = R.typeOf(ir.obj, c), cls = o && R.classOf(o.t, o.module)?.cls;
69
+ return root(`${cls || c.from}.${ir.field}`, k);
70
+ }
71
+ if (ir.k === 'call') return named(`${c.from}@${c.line}`, valueOwner(ir, c, depth + 1)); // a chain: `r.Group("/v1").GET(…)`
72
+ return null;
73
+ }
74
+ // the router a key names, given what its value makes
75
+ const named = (key, v) => !v ? null : v.how === 'same' ? v.owner : v.how === 'new' ? root(key, v.kind) : (mount(root(key, v.parent.kind), v.parent, v.prefix), { key, kind: v.parent.kind });
76
+ // what a value expression makes: a new router, a group of an existing one, or an existing one
77
+ function valueOwner(ir, sc, depth) {
78
+ if (depth > 6) return null;
79
+ if (ir.k !== 'call') { const o = ownerOf(ir, sc, depth + 1); return o ? { how: 'same', owner: o } : null; }
80
+ const ext = extCallee(ir, sc), k = kindOfExt(ext);
81
+ if (k && /\.(NewServeMux|Default|New|NewRouter|NewMux)$/.test(ext)) return { how: 'new', kind: k };
82
+ if (ir.fn.k === 'sel' && ir.fn.field === 'With') { const o = ownerOf(ir.fn.obj, sc, depth + 1); return o ? { how: 'same', owner: o } : null; } // chi `r.With(mw)`
83
+ if (ir.fn.k === 'sel' && /^(Group|Route)$/.test(ir.fn.field) && ir.args[0] && ir.args[0].k !== 'fn') {
84
+ const parent = ownerOf(ir.fn.obj, sc, depth + 1);
85
+ if (parent) return { how: 'group', parent, prefix: foldGo(R, ir.args[0], sc) };
86
+ }
87
+ // a function that builds and returns a router: `apiRouter()` is the router it returns
88
+ const fn = R.calleeOf(ir.fn, sc)?.id;
89
+ for (const ret of fn ? go.returns.filter(x => x.fn === fn && x.ir.k === 'id') : []) { const o = ownerOf(ret.ir, { ...ret, from: fn }, depth + 1); if (o) return { how: 'same', owner: o }; }
90
+ return null;
91
+ }
92
+ const fnOf = id => { const s = id && go.symbols.get(id); return s && (s.kind === 'function' || s.kind === 'method') && !(s.decos || []).includes('abstractmethod') ? id : null; };
93
+ // a handler argument -> { id, middleware: [ids] }
94
+ function handlerOf(a, c, depth = 0) {
95
+ if (!a || depth > 4) return null;
96
+ if (a.fnId) return { id: a.fnId, middleware: [] };
97
+ const ir = a.ir || a;
98
+ if (ir.k === 'id' || ir.k === 'sel') {
99
+ const t = R.calleeOf(ir, c); if (fnOf(t?.id)) return { id: t.id, middleware: [] };
100
+ const v = R.typeOf(ir, c), cls = v && R.classOf(v.t, v.module)?.cls, sh = cls && R.lookup(cls, 'ServeHTTP');
101
+ return sh ? { id: sh, middleware: [] } : null;
102
+ }
103
+ if (ir.k === 'lit') { const cls = R.classOf(ir.type, c.module)?.cls, sh = cls && R.lookup(cls, 'ServeHTTP'); return sh ? { id: sh, middleware: [] } : null; }
104
+ if (ir.k === 'call') {
105
+ const ext = extCallee(ir, c);
106
+ if (/^net\/http\.HandlerFunc$/.test(ext || '')) return handlerOf(ir.args[0], c, depth + 1);
107
+ const t = R.calleeOf(ir.fn, c), fn = fnOf(t?.id);
108
+ if (!fn) return null;
109
+ const inner = ir.args.map(x => handlerOf(x, c, depth + 1)).find(Boolean);
110
+ return inner ? { id: inner.id, middleware: [...inner.middleware, fn] } : { id: fn, middleware: [] }; // `s.handleX()` returns the handler it builds
111
+ }
112
+ return null;
113
+ }
114
+ const regs = [], claimed = new Set(), byTarget = new Map();
115
+ for (const c of go.rawCalls) if (c.target) (byTarget.get(c.target) || byTarget.set(c.target, []).get(c.target)).push(c);
116
+ // a router handed to a function carries its prefix into the parameter
117
+ for (const [fn, calls] of byTarget) {
118
+ const s = go.symbols.get(fn); if (!s?.paramTypes) continue;
119
+ s.paramTypes.forEach((t, p) => { if (!kindOfExt(R.classOf(t, s.file)?.ext)) return;
120
+ for (const c of calls) { const o = c.args[p] && ownerOf(c.args[p].ir, c); if (o) { mount({ key: `${fn}#p${p}` }, o, ''); break; } } });
121
+ }
122
+ go.rawCalls.forEach((c, i) => {
123
+ if (!c.attr || !c.obj) return;
124
+ const owner = ownerOf(c.obj, c);
125
+ if (!owner) return;
126
+ const k = owner.kind;
127
+ if (c.attr === 'Mount' && k === 'chi') { const sub = ownerOf(c.args[1]?.ir, c); if (sub) mount(sub, owner, fold(c.args[0], c)); claimed.add(i); return; }
128
+ if (k === 'net-http' && c.attr === 'Handle') { // a sub-mux under a pattern, with or without StripPrefix
129
+ const a1 = c.args[1]?.ir;
130
+ const strip = a1?.k === 'call' && /^net\/http\.StripPrefix$/.test(extCallee(a1, c) || '') ? a1 : null;
131
+ const sub = ownerOf(strip ? strip.args[1] : a1, c);
132
+ if (sub) { mount(sub, owner, strip ? foldGo(R, strip.args[0], c) : ''); claimed.add(i); return; }
133
+ }
134
+ if (!METHODS[k]?.test(c.attr)) return;
135
+ let method, pathArg, hs;
136
+ if (k === 'net-http') {
137
+ const pat = fold(c.args[0], c);
138
+ const m = pat && /^(?:([A-Z]+)\s+)?(?:[\w.-]+)?(\/\S*)$/.exec(pat);
139
+ if (!m) return;
140
+ method = m[1] || '*'; pathArg = m[2]; hs = [c.args[1]];
141
+ } else if ((k === 'gin' && c.attr === 'Handle') || (k === 'echo' && c.attr === 'Add') || (k === 'chi' && /^Method(Func)?$/.test(c.attr))) {
142
+ method = fold(c.args[0], c); pathArg = fold(c.args[1], c); hs = c.args.slice(2);
143
+ } else {
144
+ method = c.attr === 'Any' || c.attr === 'Handle' || c.attr === 'HandleFunc' ? '*' : VERB[c.attr] || c.attr; pathArg = fold(c.args[0], c); hs = c.args.slice(1);
145
+ }
146
+ if (pathArg == null || (!pathArg.startsWith('/') && !(pathArg === '' && mounts.has(owner.key)))) return;
147
+ claimed.add(i);
148
+ // chi `r.With(mw...).Get(…)`: the With call's arguments are this route's middleware, handed to the router too
149
+ const withMw = [];
150
+ for (let o = c.obj; o?.k === 'call' && o.fn.k === 'sel' && o.fn.field === 'With'; o = o.fn.obj) {
151
+ withMw.push(...o.args.map(a => handlerOf(a, c)?.id).filter(Boolean));
152
+ const w = go.rawCalls.findIndex(x => x.attr === 'With' && x.line === c.line && x.from === c.from); if (w >= 0) claimed.add(w);
153
+ }
154
+ const found = hs.map(a => handlerOf(a, c)).filter(Boolean);
155
+ if (!found.length) return;
156
+ const main = k === 'echo' ? found[0] : found.at(-1);
157
+ const middleware = [...withMw, ...(k === 'echo' ? found.slice(1) : found.slice(0, -1)).map(h => h.id), ...main.middleware];
158
+ regs.push({ owner: owner.key, kind: k, method: method.toUpperCase(), path: pathArg, handler: main.id, middleware, file: c.file, line: c.line });
159
+ });
160
+ const fullPath = (owner, path, seen = new Set()) => {
161
+ let p = path;
162
+ for (let o = owner; mounts.has(o) && !seen.has(o); o = mounts.get(o).parent) { seen.add(o); const pre = mounts.get(o).prefix || ''; p = `${pre.replace(/\/$/, '')}${p.startsWith('/') ? '' : '/'}${p}`; }
163
+ p = p.replace(/\/{2,}/g, '/').replace(/\{\$\}/g, '').replace(/[:*](\w+)/g, '{$1}').replace(/\{(\w+)(?::[^}]*|\.\.\.)\}/g, '{$1}');
164
+ return p.length > 1 ? p.replace(/\/$/, '') : p;
165
+ };
166
+ const routes = [];
167
+ for (const g of regs) {
168
+ const path = fullPath(g.owner, g.path);
169
+ routes.push({ kind: 'http', method: g.method, path, id: g.handler, file: g.file, line: g.line, rule: `go.${g.kind}` });
170
+ for (const mw of g.middleware) addGoEdge(go, g.handler, mw, 'framework', g.line, { rule: 'go.middleware', evidence: `${g.file}:${g.line} ${g.method} ${path} runs ${go.symbols.get(mw)?.name} around it` });
171
+ }
172
+ return { routes, claimed, mounts: [...mounts].map(([child, m]) => ({ child, parent: m.parent, prefix: m.prefix })) };
173
+ }
@@ -0,0 +1,108 @@
1
+ /* Rule pack: go test, in test-group (`_test.go`) files.
2
+ - tests: `func TestXxx(t *testing.T)` and `func FuzzXxx(f *testing.F)` (Xxx not starting with a lower-case letter),
3
+ the functions `go test` runs; assertion lines are `t.Error*` / `t.Fatal*` / `t.Fail*` calls, testify
4
+ `assert.*` / `require.*` calls (or an `assert.New(t)` value's) and a testify mock's `AssertExpectations` /
5
+ `AssertCalled`; a function literal's body (a subtest) belongs to its test;
6
+ - subtests: `t.Run("name", func(t *testing.T) {…})` names a subtest; in a table test,
7
+ `for _, tc := range cases { t.Run(tc.name, …) }`, the table's case count is recorded when `cases` is a composite
8
+ literal (`table: { cases, line }`);
9
+ - hooks: `TestMain(m *testing.M)` runs around every test of its directory's test binary (the pytest pack's autouse
10
+ fixture);
11
+ - mock targets: a test-group type the test side constructs (`&fakeStore{}`, `new(fakeStore)`, a testify
12
+ `mock.Mock` embedder, a gomock `NewMockX(ctrl)`) that implements a production interface replaces, in that test,
13
+ every production implementation of that interface's methods: the code it stands in for is not what the test
14
+ exercises;
15
+ - direct test links: every edge from a test's own side (its body, TestMain, test-side helpers) into non-test code,
16
+ minus whatever that test mocks. */
17
+ import { posix } from 'node:path';
18
+ import { addGoEdge } from '../lang/go.mjs';
19
+
20
+ const TEST = /^(Test|Fuzz)([^a-z]|$)/;
21
+ const ASSERT_ATTR = /^(Error|Errorf|Fatal|Fatalf|Fail|FailNow)$/;
22
+ const TESTIFY = /^github\.com\/stretchr\/testify\/(assert|require)\.(?!New$)\w+$/;
23
+ const MOCK_ASSERT = /^Assert(Expectations|Called|NotCalled|NumberOfCalls)$/; // a testify mock.Mock's own checks
24
+ const dirOf = f => posix.dirname(f) === '.' ? '' : posix.dirname(f);
25
+
26
+ export function findGoTests(go, R) {
27
+ const isTestingT = (ir, c) => { const t = R.typeOf(ir, c), x = t && R.classOf(t.t, t.module)?.ext; return /^testing\.(T|B|F|TB)$/.test(x || ''); };
28
+ const isAssertions = (ir, c) => { const t = R.typeOf(ir, c), x = t && R.classOf(t.t, t.module)?.ext;
29
+ if (/^github\.com\/stretchr\/testify\/(assert|require)\.Assertions$/.test(x || '')) return true;
30
+ const spec = ir?.k === 'id' ? R.resolveName(ir.name, c)?.local : null; // a := assert.New(t)
31
+ return spec?.t === 'expr' && spec.ir.k === 'call' && /\/(assert|require)\.New$/.test(R.calleeOf(spec.ir.fn, c)?.ext || ''); };
32
+ const asserts = new Map(), runs = new Map();
33
+ for (const c of go.rawCalls) {
34
+ const s = go.symbols.get(c.from);
35
+ if (s?.group !== 'test') continue;
36
+ const isAssert = (c.attr && ASSERT_ATTR.test(c.attr) && isTestingT(c.obj, c)) || TESTIFY.test(R.calleeOf(c.attr ? { k: 'sel', obj: c.obj, field: c.attr } : c.fn, c)?.ext || '')
37
+ || (c.attr && (isAssertions(c.obj, c) || MOCK_ASSERT.test(c.attr)));
38
+ if (isAssert) (asserts.get(c.from) || asserts.set(c.from, new Set()).get(c.from)).add(c.line);
39
+ if (c.attr === 'Run' && isTestingT(c.obj, c)) {
40
+ const title = c.args[0]?.ir.k === 'str' ? c.args[0].ir.v : null;
41
+ (runs.get(c.from) || runs.set(c.from, []).get(c.from)).push({ title, line: c.line, ...(title == null ? { name: c.args[0]?.text } : {}),
42
+ ...(c.range ? { table: { over: c.range.over, line: c.range.line, cases: c.range.cases } } : {}) });
43
+ }
44
+ }
45
+ const tests = [];
46
+ for (const s of go.symbols.values()) {
47
+ if (s.group !== 'test' || s.kind !== 'function' || !/_test\.go$/.test(s.file) || s.id.includes('<locals>') || !TEST.test(s.name) || s.name === 'TestMain') continue;
48
+ if (!/^testing\.(T|F)$/.test(R.classOf(s.paramTypes?.[0], s.file)?.ext || '')) continue;
49
+ const lines = [...(asserts.get(s.id) || [])].sort((a, b) => a - b);
50
+ s.asserts = lines.length; s.assertLines = lines;
51
+ const sub = runs.get(s.id) || [];
52
+ const table = sub.find(r => r.table)?.table;
53
+ tests.push({ id: s.id, file: s.file, line: s.start, end: s.end, asserts: lines.length, assertLines: lines,
54
+ ...(sub.length ? { subtests: sub.map(r => r.title ?? `<${r.name}>`) } : {}), ...(table ? { table } : {}) });
55
+ }
56
+ return tests;
57
+ }
58
+
59
+ export function findGoHooks(go, R) {
60
+ return [...go.symbols.values()].filter(s => s.group === 'test' && s.kind === 'function' && s.name === 'TestMain' && /_test\.go$/.test(s.file)
61
+ && /^testing\.M$/.test(R.classOf(s.paramTypes?.[0], s.file)?.ext || ''))
62
+ .map(s => { s.hook = 'TestMain'; return { id: s.id, name: 'TestMain', file: s.file, line: s.start, cls: null, autouse: true, scope: [], covers: dirOf(s.file) }; });
63
+ }
64
+
65
+ // TestMain runs around every test in its directory: both the package's and its external `_test` package's
66
+ export function bindGoHooks(tests, hooks) {
67
+ const out = [];
68
+ for (const t of tests) for (const h of hooks) if (dirOf(t.file) === h.covers) out.push({ fn: t.id, param: null, fixture: h.id, autouse: true, rule: 'go.TestMain' });
69
+ return out;
70
+ }
71
+
72
+ export function goMockTargets(go, R, readText) {
73
+ const mocks = [];
74
+ const how = k => {
75
+ if (k.embeds.some(t => /testify\/mock\.Mock$/.test(R.classOf(t, k.module)?.ext || ''))) return 'testify.mock';
76
+ if (/Code generated by MockGen/.test(readText(k.file).slice(0, 400))) return 'gomock';
77
+ return 'go.fake';
78
+ };
79
+ const doubleOf = cls => { const k = go.classes.get(cls); return k && !k.iface && go.symbols.get(cls)?.group === 'test' ? k : null; };
80
+ const constructs = [];
81
+ for (const l of go.literals) if (l.cls && doubleOf(l.cls)) constructs.push({ from: l.from, cls: l.cls, line: l.line });
82
+ for (const c of go.rawCalls) { // NewMockStore(ctrl), newFake(): a function whose declared result is a test double
83
+ const s = c.target && go.symbols.get(c.target), r = s?.results?.[0] && R.classOf(s.results[0], s.file)?.cls;
84
+ if (r && doubleOf(r)) constructs.push({ from: c.from, cls: r, line: c.line });
85
+ }
86
+ const seen = new Set();
87
+ for (const x of constructs) {
88
+ const k = doubleOf(x.cls);
89
+ for (const iface of k.implementsIds || []) {
90
+ if (go.symbols.get(iface)?.group === 'test') continue;
91
+ for (const n of go.classes.get(iface).methods.keys())
92
+ for (const impl of R.implementers.get(iface) || []) {
93
+ if (go.symbols.get(impl)?.group === 'test') continue;
94
+ const target = R.lookup(impl, n), key = `${x.from}\0${target}`;
95
+ if (!target || seen.has(key)) continue;
96
+ seen.add(key);
97
+ mocks.push({ fn: x.from, raw: `${k.name} (for ${go.symbols.get(iface).name}.${n})`, target, line: x.line, how: how(k) });
98
+ }
99
+ }
100
+ }
101
+ return mocks;
102
+ }
103
+
104
+ export function linkGoHooks(go, bindings) {
105
+ const s = id => go.symbols.get(id);
106
+ for (const b of bindings) addGoEdge(go, b.fn, b.fixture, 'framework', s(b.fn)?.start, { rule: 'go.test-main',
107
+ evidence: `${s(b.fn)?.file}: TestMain at ${s(b.fixture)?.file}:${s(b.fixture)?.start} runs around this test` });
108
+ }
@@ -0,0 +1,57 @@
1
+ /* Rule pack: gRPC (Go). A server registration `pb.Register<Svc>Server(s, impl)` makes an `rpc` entry at each method
2
+ of impl's type that serves the service: the methods of the `<Svc>Server` interface when the generated code is in
3
+ the repo (its `mustEmbed…` marker left out), else impl's exported methods that take a `context.Context` first. A
4
+ method impl only inherits from an embedded `Unimplemented<Svc>Server` is not served by it. The entry's path is the
5
+ gRPC method's HTTP/2 path, `/<package>.<Svc>/<Method>`, the package read from the generated `grpc.ServiceDesc`
6
+ (`ServiceName: "pkg.Svc"`), else `/<Svc>/<Method>`.
7
+ A client call is an `http` crossing to that method's handler: in generated client code, `cc.Invoke(ctx, method, …)`
8
+ / `cc.NewStream(ctx, desc, method)` whose method string (or the const naming it) is the path; else a call on a
9
+ value made by `pb.New<Svc>Client(conn)` from an external package, matched by service and method name. */
10
+ import { addGoEdge, foldGo } from '../lang/go.mjs';
11
+
12
+ export function grpcEntries(go, R) {
13
+ const entries = [];
14
+ const fullName = svc => go.serviceNames.find(s => s.iface === `${svc}Server`)?.name || go.serviceNames.find(s => s.name.split('.').pop() === svc)?.name || svc;
15
+ const ifaceOf = svc => [...go.classes.values()].find(k => k.iface && k.name === `${svc}Server`);
16
+ for (const c of go.rawCalls) {
17
+ const name = c.attr || (c.fn?.k === 'id' ? c.fn.name : null);
18
+ const m = name && /^Register(\w+)Server$/.exec(name);
19
+ if (!m || c.args.length < 2) continue;
20
+ const svc = m[1], t = R.typeOf(c.args[1].ir, c), impl = t && R.classOf(t.t, t.module)?.cls;
21
+ if (!impl) continue;
22
+ const iface = ifaceOf(svc);
23
+ const names = iface ? [...iface.methods.keys()].filter(n => !/^mustEmbed/.test(n))
24
+ : [...new Set(R.embedChain(impl).flatMap(k => [...go.classes.get(k).methods.keys()]))].filter(n => /^[A-Z]/.test(n) && /context\.Context/.test(go.symbols.get(R.lookup(impl, n))?.text.split('\n')[0] || ''));
25
+ const full = fullName(svc);
26
+ for (const n of names) {
27
+ const id = R.lookup(impl, n), s = id && go.symbols.get(id);
28
+ if (!s || /^Unimplemented/.test(go.symbols.get(s.cls)?.name || '') || (s.decos || []).includes('abstractmethod')) continue;
29
+ entries.push({ kind: 'rpc', path: `/${full}/${n}`, key: `${full}/${n}`, id, file: c.file, line: c.line, rule: 'grpc', service: full });
30
+ }
31
+ }
32
+ return entries;
33
+ }
34
+
35
+ export function linkGrpcClients(go, R, rpcEntries) {
36
+ const byPath = new Map(rpcEntries.map(e => [e.path, e])), links = [];
37
+ const link = (c, e, how) => { addGoEdge(go, c.from, e.id, 'http', c.line, { rule: 'grpc.client', evidence: `${c.file}:${c.line} ${how} ${e.path}` });
38
+ links.push({ from: c.from, file: c.file, line: c.line, path: e.path, handler: e.id }); };
39
+ for (const c of go.rawCalls) {
40
+ if (!c.attr) continue;
41
+ if (c.attr === 'Invoke' || c.attr === 'NewStream') {
42
+ const p = foldGo(R, c.args[c.attr === 'Invoke' ? 1 : 2]?.ir, c);
43
+ const e = byPath.get(p);
44
+ if (e) link(c, e, `invokes`);
45
+ continue;
46
+ }
47
+ // a client made by an external package's New<Svc>Client: matched by service and method name
48
+ let h = c.obj; if (h?.k !== 'id') continue;
49
+ const spec = R.resolveName(h.name, c)?.local;
50
+ const made = spec?.t === 'expr' && spec.ir.k === 'call' ? R.calleeOf(spec.ir.fn, c)?.ext : null;
51
+ const m = made && /\.New(\w+)Client$/.exec(made);
52
+ if (!m) continue;
53
+ const e = rpcEntries.find(x => x.key.split('/')[0].split('.').pop() === m[1] && x.key.endsWith(`/${c.attr}`));
54
+ if (e) link(c, e, `calls`);
55
+ }
56
+ return links;
57
+ }
@@ -0,0 +1,97 @@
1
+ /* Rule pack: C and C++ tests — GoogleTest, Catch2, and plain C test functions (Unity, cmocka, a hand-rolled runner) —
2
+ in test-group files.
3
+ - tests: gtest `TEST(Suite, Name)`, `TEST_F(Fixture, Name)`, `TEST_P`, `TYPED_TEST` (named `Suite.Name`); Catch2
4
+ `TEST_CASE("title", "[tags]")`, `SCENARIO`, `TEST_CASE_METHOD(Fixture, "title")`, its `SECTION("…")`s recorded
5
+ as subtests; a C function named `test_*`; assertion lines are `EXPECT_*` / `ASSERT_*` (gmock's `EXPECT_CALL`
6
+ included), `REQUIRE*` / `CHECK*`, Unity `TEST_ASSERT*`, cmocka `assert_*`, check's `ck_assert*` and `assert`;
7
+ - hooks: a TEST_F / TEST_CASE_METHOD fixture class's constructor, `SetUp` and `TearDown` run around each of its
8
+ tests (the pytest pack's autouse fixtures), as do a Unity file's `setUp` / `tearDown`; the test body is a member of
9
+ its fixture, so `cart_.add()` resolves through the fixture's fields;
10
+ - mock targets: a test-group class that derives from a production class — a gmock `MOCK_METHOD` mock or a
11
+ hand-written fake — and that the test side constructs replaces, in that test, every production implementation of
12
+ the methods it overrides in that hierarchy;
13
+ - direct test links (lib/test-links.mjs): every edge from a test's side into non-test code, minus its mocks. */
14
+ import { addCEdge } from '../lang/c.mjs';
15
+
16
+ const ASSERT = /^((EXPECT|ASSERT)_[A-Z0-9_]+|REQUIRE(_\w+)?|CHECK(_\w+)?|TEST_ASSERT\w*|assert|assert_\w+|ck_assert\w*|ASSERT|CU_ASSERT\w*)$/;
17
+
18
+ export function findCTests(cm) {
19
+ const asserts = new Map(), sections = new Map();
20
+ for (const c of cm.rawCalls) {
21
+ const s = cm.symbols.get(c.from);
22
+ if (s?.group !== 'test' || c.attr || c.isNew) continue;
23
+ const name = c.fn?.k === 'id' ? c.fn.name.split('::').pop() : c.expr;
24
+ if (ASSERT.test(name)) (asserts.get(c.from) || asserts.set(c.from, new Set()).get(c.from)).add(c.line);
25
+ if (name === 'SECTION' || name === 'GIVEN' || name === 'WHEN' || name === 'THEN') (sections.get(c.from) || sections.set(c.from, []).get(c.from)).push(c.args[0]?.str ?? c.args[0]?.text);
26
+ }
27
+ const tests = [];
28
+ for (const s of cm.symbols.values()) {
29
+ if (s.group !== 'test' || s.kind !== 'function' || s.id.includes('<locals>')) continue;
30
+ if (!s.test && !/^test_\w+$/.test(s.name)) continue;
31
+ const lines = [...(asserts.get(s.id) || [])].sort((a, b) => a - b);
32
+ s.asserts = lines.length; s.assertLines = lines;
33
+ const sub = sections.get(s.id) || [];
34
+ tests.push({ id: s.id, file: s.file, line: s.start, end: s.end, asserts: lines.length, assertLines: lines, framework: s.macro ? (/TEST_CASE|SCENARIO/.test(s.macro) ? 'catch2' : 'gtest') : 'c',
35
+ ...(s.fixture ? { fixture: s.fixture } : {}), ...(sub.length ? { subtests: sub } : {}) });
36
+ }
37
+ return tests;
38
+ }
39
+
40
+ export function findCHooks(cm, R, tests) {
41
+ const hooks = new Map();
42
+ const add = (id, name, autouse = true) => { const s = cm.symbols.get(id); if (s && !hooks.has(id)) hooks.set(id, { id, name, file: s.file, line: s.start, cls: s.cls || null, autouse }); };
43
+ for (const t of tests) {
44
+ const s = cm.symbols.get(t.id);
45
+ if (t.fixture) { const k = R.classNamed(t.fixture, { module: s.module, ns: s.ns }); if (!k) continue;
46
+ for (const m of [R.ctorOf(k), R.lookup(k, 'SetUp'), R.lookup(k, 'TearDown')]) if (m) add(m, cm.symbols.get(m).name); }
47
+ }
48
+ for (const s of cm.symbols.values()) if (s.group === 'test' && s.kind === 'function' && /^(setUp|tearDown)$/.test(s.name)) add(s.id, s.name);
49
+ for (const h of hooks.values()) cm.symbols.get(h.id).hook = h.name.split('.').pop(); // a direct link through it is `via: fixture`
50
+ return [...hooks.values()];
51
+ }
52
+
53
+ export function bindCHooks(cm, R, tests, hooks) {
54
+ const out = [];
55
+ for (const t of tests) {
56
+ const s = cm.symbols.get(t.id);
57
+ const k = t.fixture ? R.classNamed(t.fixture, { module: s.module, ns: s.ns }) : null;
58
+ const chain = k ? new Set(R.mro(k)) : new Set();
59
+ for (const h of hooks) {
60
+ if (h.cls && chain.has(h.cls)) out.push({ fn: t.id, param: null, fixture: h.id, autouse: true, rule: 'gtest.fixture' });
61
+ else if (!h.cls && h.file === t.file && !t.fixture && t.framework === 'c') out.push({ fn: t.id, param: null, fixture: h.id, autouse: true, rule: 'unity.setUp' });
62
+ }
63
+ }
64
+ return out;
65
+ }
66
+
67
+ export function linkCHooks(cm, bindings) {
68
+ const s = id => cm.symbols.get(id);
69
+ for (const b of bindings) addCEdge(cm, b.fn, b.fixture, 'framework', s(b.fn)?.start, { rule: b.rule,
70
+ evidence: `${s(b.fn)?.file}: ${s(b.fixture)?.name} at ${s(b.fixture)?.file}:${s(b.fixture)?.start} runs around this test` });
71
+ }
72
+
73
+ export function cMockTargets(cm, R, hooks) {
74
+ const mocks = [], seen = new Set();
75
+ const isTest = id => cm.symbols.get(id)?.group === 'test';
76
+ // what the test side constructs: `MockPrices m;`, `new FakeDb`, make_unique<…>, and a fixture's fields (made with it)
77
+ const built = cm.rawCalls.filter(c => c.isNew && c.target).map(c => ({ from: c.from, cls: c.target, line: c.line }));
78
+ for (const h of hooks) if (h.cls) for (const k of R.mro(h.cls)) for (const [f, t] of cm.classes.get(k)?.fields || []) {
79
+ const d = t.k === 'name' ? R.classNamed(t.name, { module: cm.classes.get(k).module, ns: cm.classes.get(k).ns }) : null;
80
+ if (d) built.push({ from: h.id, cls: d, line: cm.classes.get(k).line, field: f });
81
+ }
82
+ for (const c of built) {
83
+ if (!isTest(c.cls) || !cm.classes.has(c.cls)) continue;
84
+ const k = cm.classes.get(c.cls);
85
+ const roots = R.mro(k.id).slice(1).filter(b => !isTest(b));
86
+ if (!roots.length) continue;
87
+ for (const m of k.methods.keys()) for (const r of roots) {
88
+ const impls = [R.lookup(r, m), ...R.overrides(r, m)].filter(x => x && !isTest(x) && !(cm.symbols.get(x).decos || []).includes('abstractmethod'));
89
+ for (const target of impls) {
90
+ const key = `${c.from}\0${target}`;
91
+ if (seen.has(key)) continue; seen.add(key);
92
+ mocks.push({ fn: c.from, raw: `${k.name} (for ${cm.symbols.get(r).name}.${m})`, target, line: c.line, how: k.mocked ? 'gmock' : 'cpp.fake' });
93
+ }
94
+ }
95
+ }
96
+ return mocks;
97
+ }
@@ -0,0 +1,55 @@
1
+ /* Rule pack: Haystack pipelines. A builder function (Pipeline() + add_component + connect) wires components;
2
+ `p.run()` on a builder's result calls each component's `run` in connect order. A component built by a
3
+ factory becomes `dispatch` links carrying the factory's branch guards; a library component is a named
4
+ external leaf. */
5
+ import { addEdge } from '../lang/python.mjs';
6
+
7
+ const literal = a => a && a.type === 'string' ? a.s : null;
8
+
9
+ export function linkPipelines(py, R) {
10
+ const builders = new Map();
11
+ const sym = id => py.symbols.get(id);
12
+ for (const c of py.rawCalls) {
13
+ if (c.attr === 'add_component' && c.args.length >= 2) {
14
+ const b = builders.get(c.from) || builders.set(c.from, { fn: c.from, comps: new Map(), order: [] }).get(c.from);
15
+ const v = c.args[1];
16
+ const asTargets = (expr, sc) => { const r = R.resolveExpr(expr, sc);
17
+ if (r?.id && py.classes.has(r.id)) return [{ cls: r.id }];
18
+ if (r?.ext) return [{ ext: r.ext }];
19
+ if (r?.id && sym(r.id)?.kind === 'function') return R.factoryReturns(r.id).map(x => ({ ...x, factory: r.id }));
20
+ return []; };
21
+ const type = v.type === 'identifier' ? c.types?.get(v.text) : null;
22
+ const targets = v.type === 'call' ? asTargets(v.fn, c) : type ? asTargets(type.expr, c) : [];
23
+ b.comps.set(literal(c.args[0]), { targets, line: c.line });
24
+ }
25
+ }
26
+ for (const c of py.rawCalls) if (c.attr === 'connect' && c.args.length >= 2 && literal(c.args[0]) != null && literal(c.args[1]) != null) {
27
+ const b = builders.get(c.from); if (b) b.order.push([literal(c.args[0]).split('.')[0], literal(c.args[1]).split('.')[0], c.line]);
28
+ }
29
+ for (const b of builders.values()) { // topological order from the connect pairs
30
+ const names = [...b.comps.keys()], indeg = new Map(names.map(n => [n, 0]));
31
+ for (const [, y] of b.order) indeg.set(y, (indeg.get(y) || 0) + 1);
32
+ const seq = [], q = names.filter(n => !indeg.get(n));
33
+ while (q.length) { const n = q.shift(); seq.push(n); for (const [x, y] of b.order) if (x === n) { indeg.set(y, indeg.get(y) - 1); if (!indeg.get(y)) q.push(y); } }
34
+ b.seq = seq.length === names.length ? seq : names;
35
+ }
36
+ const runs = [];
37
+ for (const c of py.rawCalls) if (c.attr === 'run' && c.types?.has(c.obj)) {
38
+ const spec = c.types.get(c.obj);
39
+ const r = spec.expr ? R.resolveExpr(spec.expr, c) : null;
40
+ const b = r?.id ? builders.get(r.id) : (spec.expr === 'Pipeline' ? builders.get(c.from) : null);
41
+ if (!b) continue;
42
+ b.seq.forEach((name, i) => {
43
+ const comp = b.comps.get(name);
44
+ const ev = `${sym(b.fn).file}:${comp.line} add_component("${name}") · connect order ${i + 1}/${b.seq.length}`;
45
+ const kind = comp.targets.length > 1 ? 'dispatch' : 'framework';
46
+ for (const t of comp.targets) {
47
+ const extra = { rule: 'haystack.pipeline', order: i, component: name, evidence: ev, selectors: t.guards?.map(g => (g.neg ? 'not ' : '') + g.text), narrowedBy: t.factory };
48
+ if (t.cls) { const run = R.lookup(t.cls, 'run'); if (run) addEdge(py, c.from, run, kind, c.line, extra); }
49
+ else if (t.ext) addEdge(py, c.from, `ext:${t.ext}.run`, kind, c.line, { ...extra, external: true });
50
+ }
51
+ });
52
+ runs.push({ caller: c.from, builder: b.fn, seq: b.seq, comps: Object.fromEntries([...b.comps].map(([n, v]) => [n, v.targets.map(t => t.cls || 'ext:' + t.ext)])) });
53
+ }
54
+ return runs;
55
+ }
@@ -0,0 +1,100 @@
1
+ /* Rule pack: jest / vitest (and node:test and mocha, which share the API) in test-group files.
2
+ - tests: `it(title, fn)` / `test(title, fn)` (and `.only`, `.skip`, `.each(...)`), named by their `describe` titles
3
+ and their own (`A > B > adds`); assertion lines are their `expect(…)` and `assert…(…)` calls;
4
+ - hooks: `beforeEach` / `beforeAll` / `afterEach` / `afterAll` bind to every test under their `describe` (the
5
+ pytest pack's autouse fixtures); a hook in a setup file (setupTests.*, jest.setup.*, vitest.setup.*, or a file a
6
+ jest / vitest config lists in setupFiles) binds to every test below that setup file's directory (a listed one:
7
+ below its config's directory);
8
+ - mock targets: `jest.mock('mod')` / `vi.mock('mod')` (and `doMock`) replace every export of the module the
9
+ string resolves to (from the test file, the way the module system would), or only the factory's keys when the
10
+ factory spreads the real module; they are hoisted, so they apply to the whole test file, and in a setup file to
11
+ every test in its scope (session-wide). `jest.spyOn(obj, 'm')` / `vi.spyOn` replaces `m` only when a
12
+ `.mockImplementation` / `.mockReturnValue` / `.mockResolvedValue` / `.mockRejectedValue` follows (a bare spy
13
+ calls through);
14
+ - direct test links (lib/test-links.mjs): every edge from a test's own side (its body, its hooks, test-side
15
+ helpers) into non-test code, minus whatever that test mocks. */
16
+ import { posix } from 'node:path';
17
+ import { addTsEdge } from '../lang/typescript.mjs';
18
+
19
+ export const SETUP_FILE = /(^|\/)(setupTests|setup-tests|jest\.setup|vitest\.setup|test-setup)\.[cm]?[jt]sx?$/;
20
+ const ASSERT = /^(expect|assert)$|^assert\.\w+$|^expect\.\w+$|^t\.assert\w*$/;
21
+
22
+ // files a jest or vitest config names in setupFiles / setupFilesAfterEnv (`<rootDir>/x`, './x' or 'x', from the config's
23
+ // dir) -> the directory whose tests they set up (the config's)
24
+ export function configuredSetupFiles(configs) {
25
+ const out = new Map();
26
+ for (const { rel, text } of configs) {
27
+ const dir = posix.dirname(rel) === '.' ? '' : posix.dirname(rel);
28
+ for (const m of text.matchAll(/setupFiles(?:AfterEnv)?["']?\s*:\s*\[([^\]]*)\]/g))
29
+ for (const s of m[1].matchAll(/["'`]([^"'`]+)["'`]/g)) out.set(posix.normalize(posix.join(dir, s[1].replace(/^<rootDir>\/?/, ''))), dir);
30
+ }
31
+ return out;
32
+ }
33
+ // the directory a setup file's hooks and mocks cover: its config's, else its own
34
+ const setupScope = (file, setupFiles) => setupFiles.has(file) ? setupFiles.get(file) : posix.dirname(file) === '.' ? '' : posix.dirname(file);
35
+ const under = (dir, file) => !dir || file.startsWith(dir + '/');
36
+
37
+ export function findTests(ts) {
38
+ const assertsOf = new Map();
39
+ for (const c of ts.rawCalls) if (ASSERT.test(c.expr) && ts.symbols.get(c.from)?.test) (assertsOf.get(c.from) || assertsOf.set(c.from, new Set()).get(c.from)).add(c.line);
40
+ return [...ts.symbols.values()].filter(s => s.test && s.group === 'test').map(s => {
41
+ const lines = [...(assertsOf.get(s.id) || [])].sort((a, b) => a - b);
42
+ s.asserts = lines.length; s.assertLines = lines;
43
+ return { id: s.id, file: s.file, line: s.start, end: s.end, asserts: lines.length, assertLines: lines, title: s.title, describe: s.describe };
44
+ });
45
+ }
46
+
47
+ export function findHooks(ts, setupFiles) {
48
+ return ts.testCalls.filter(t => t.kind === 'hook').map(h => {
49
+ const setup = SETUP_FILE.test(h.file) || setupFiles.has(h.file);
50
+ return { id: h.id, name: h.hook, file: h.file, line: h.line, cls: null, autouse: true, scope: h.describe, ...(setup ? { setup: true, covers: setupScope(h.file, setupFiles) } : {}) };
51
+ });
52
+ }
53
+
54
+ export function bindHooks(tests, hooks) {
55
+ const binds = (t, h) => h.setup ? under(h.covers, t.file) : h.file === t.file && h.scope.every((x, i) => t.describe[i] === x);
56
+ const out = [];
57
+ for (const t of tests) for (const h of hooks) if (binds(t, h)) out.push({ fn: t.id, param: null, fixture: h.id, autouse: true, rule: `${h.setup ? 'setup-file' : 'test'}.${h.name}` });
58
+ return out;
59
+ }
60
+
61
+ export function linkHooks(ts, bindings) {
62
+ const s = id => ts.symbols.get(id);
63
+ for (const b of bindings) addTsEdge(ts, b.fn, b.fixture, 'framework', s(b.fn)?.start, { rule: 'js.test-hook',
64
+ evidence: `${s(b.fn)?.file}: ${s(b.fixture)?.name} at ${s(b.fixture)?.file}:${s(b.fixture)?.start} runs around this test` });
65
+ }
66
+
67
+ export function mockTargets(ts, R, setupFiles) {
68
+ const exportsOf = (rel, seen = new Set()) => {
69
+ if (seen.has(rel)) return []; seen.add(rel);
70
+ const M = ts.modules.get(rel); if (!M) return [];
71
+ const names = [...M.exports.keys(), ...(M.defaultExport ? ['default'] : [])];
72
+ const ids = names.map(n => R.exportOf(rel, n)?.id).filter(Boolean);
73
+ for (const s of M.starExports) { const t = R.resolveModule(s, rel); if (t) ids.push(...exportsOf(t, seen)); }
74
+ // a mocked class has every method replaced
75
+ return [...new Set(ids.flatMap(id => ts.classes.has(id) ? [id, ...ts.classes.get(id).methods.values()] : [id]))];
76
+ };
77
+ const mocks = [];
78
+ for (const c of ts.mockCalls) {
79
+ const setup = SETUP_FILE.test(c.file) || setupFiles.has(c.file);
80
+ const where = setup ? { scope: 'session', conftest: c.file, covers: setupScope(c.file, setupFiles) } : {};
81
+ if (/(^|\.)(mock|doMock)$/.test(c.how) && c.spec != null) {
82
+ const mod = R.resolveModule(c.spec, c.file);
83
+ const fn = `${c.file}:<module>`; // hoisted: the whole test file
84
+ if (!mod) { mocks.push({ fn, raw: c.spec, target: null, line: c.line, how: c.how, ...where }); continue; }
85
+ const targets = c.keys ? c.keys.map(k => R.exportOf(mod, k)?.id).filter(Boolean) : exportsOf(mod);
86
+ if (!targets.length) mocks.push({ fn, raw: c.spec, target: null, module: mod, line: c.line, how: c.how, ...where });
87
+ for (const t of targets) mocks.push({ fn, raw: c.spec, target: t, module: mod, line: c.line, how: c.how, ...(c.keys ? { partial: true } : {}), ...where });
88
+ } else if (/spyOn$/.test(c.how) && c.obj && c.prop && c.replaced) {
89
+ let target = null;
90
+ const obj = c.obj.replace(/\.prototype$/, '');
91
+ const r = /^[\w$.]+$/.test(obj) ? R.resolveExpr(obj, c) : null;
92
+ if (r?.module) target = R.exportOf(r.module, c.prop)?.id || null;
93
+ else if (r?.id && ts.classes.has(r.id)) target = R.lookup(r.id, c.prop);
94
+ else if (r?.obj) { const m = r.obj.get(c.prop); target = typeof m === 'string' ? m : null; }
95
+ else { const spec = c.types?.get(obj) || r?.var; const rc = spec ? R.receiverOf(spec, spec.module ? spec : c) : null; if (rc) target = R.lookup(rc.cls[0], c.prop); }
96
+ mocks.push({ fn: c.fn, raw: `${c.obj}.${c.prop}`, target, line: c.line, how: c.how, ...where });
97
+ }
98
+ }
99
+ return mocks;
100
+ }