gitnexus 1.6.8-rc.2 → 1.6.8-rc.21

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 (193) hide show
  1. package/README.md +20 -0
  2. package/dist/_shared/scope-resolution/parsed-file.d.ts +21 -0
  3. package/dist/_shared/scope-resolution/parsed-file.d.ts.map +1 -1
  4. package/dist/_shared/scope-resolution/symbol-definition.d.ts +4 -0
  5. package/dist/_shared/scope-resolution/symbol-definition.d.ts.map +1 -1
  6. package/dist/cli/ai-context.js +1 -0
  7. package/dist/cli/analyze-config.js +40 -0
  8. package/dist/cli/analyze.d.ts +22 -0
  9. package/dist/cli/analyze.js +36 -6
  10. package/dist/cli/clean.d.ts +1 -0
  11. package/dist/cli/clean.js +43 -1
  12. package/dist/cli/eval-server.js +43 -0
  13. package/dist/cli/help-i18n.js +11 -0
  14. package/dist/cli/i18n/en.d.ts +13 -0
  15. package/dist/cli/i18n/en.js +13 -0
  16. package/dist/cli/i18n/resources.d.ts +26 -0
  17. package/dist/cli/i18n/zh-CN.d.ts +13 -0
  18. package/dist/cli/i18n/zh-CN.js +13 -0
  19. package/dist/cli/index.js +19 -0
  20. package/dist/cli/list.js +12 -0
  21. package/dist/cli/optional-grammars.d.ts +6 -8
  22. package/dist/cli/optional-grammars.js +8 -11
  23. package/dist/cli/skill-gen.js +1 -0
  24. package/dist/cli/status.js +26 -5
  25. package/dist/cli/tool.d.ts +12 -1
  26. package/dist/cli/tool.js +41 -1
  27. package/dist/core/embeddings/embedder.js +4 -0
  28. package/dist/core/embeddings/embedding-pipeline.js +27 -16
  29. package/dist/core/embeddings/onnxruntime-common-resolver.d.ts +6 -0
  30. package/dist/core/embeddings/onnxruntime-common-resolver.js +130 -0
  31. package/dist/core/graph/import-cycles.d.ts +10 -0
  32. package/dist/core/graph/import-cycles.js +103 -0
  33. package/dist/core/group/extractors/grpc-patterns/proto.js +10 -6
  34. package/dist/core/group/extractors/http-patterns/java.js +3 -50
  35. package/dist/core/group/extractors/http-patterns/kotlin.js +7 -7
  36. package/dist/core/group/extractors/include-extractor.js +7 -7
  37. package/dist/core/ingestion/cfg/cfg-builder.d.ts +64 -0
  38. package/dist/core/ingestion/cfg/cfg-builder.js +129 -0
  39. package/dist/core/ingestion/cfg/collect.d.ts +30 -0
  40. package/dist/core/ingestion/cfg/collect.js +34 -0
  41. package/dist/core/ingestion/cfg/control-flow-context.d.ts +97 -0
  42. package/dist/core/ingestion/cfg/control-flow-context.js +113 -0
  43. package/dist/core/ingestion/cfg/emit.d.ts +144 -0
  44. package/dist/core/ingestion/cfg/emit.js +315 -0
  45. package/dist/core/ingestion/cfg/reaching-defs.d.ts +90 -0
  46. package/dist/core/ingestion/cfg/reaching-defs.js +364 -0
  47. package/dist/core/ingestion/cfg/traversal-result.d.ts +20 -0
  48. package/dist/core/ingestion/cfg/traversal-result.js +2 -0
  49. package/dist/core/ingestion/cfg/types.d.ts +231 -0
  50. package/dist/core/ingestion/cfg/types.js +13 -0
  51. package/dist/core/ingestion/cfg/visitors/typescript-harvest.d.ts +144 -0
  52. package/dist/core/ingestion/cfg/visitors/typescript-harvest.js +1032 -0
  53. package/dist/core/ingestion/cfg/visitors/typescript.d.ts +66 -0
  54. package/dist/core/ingestion/cfg/visitors/typescript.js +584 -0
  55. package/dist/core/ingestion/language-provider.d.ts +36 -2
  56. package/dist/core/ingestion/languages/c-cpp.js +11 -4
  57. package/dist/core/ingestion/languages/cpp/arity-metadata.js +6 -2
  58. package/dist/core/ingestion/languages/cpp/captures.js +24 -1
  59. package/dist/core/ingestion/languages/cpp/query.js +23 -0
  60. package/dist/core/ingestion/languages/java.js +3 -0
  61. package/dist/core/ingestion/languages/kotlin/query.js +3 -2
  62. package/dist/core/ingestion/languages/kotlin.js +5 -1
  63. package/dist/core/ingestion/languages/typescript.js +5 -0
  64. package/dist/core/ingestion/method-extractors/configs/c-cpp.js +6 -13
  65. package/dist/core/ingestion/method-extractors/generic.js +1 -0
  66. package/dist/core/ingestion/method-types.d.ts +2 -0
  67. package/dist/core/ingestion/model/symbol-table.d.ts +1 -0
  68. package/dist/core/ingestion/model/symbol-table.js +1 -0
  69. package/dist/core/ingestion/parsing-processor.js +22 -0
  70. package/dist/core/ingestion/pipeline-phases/parse-impl.js +18 -1
  71. package/dist/core/ingestion/pipeline-phases/routes.js +64 -14
  72. package/dist/core/ingestion/pipeline.d.ts +57 -0
  73. package/dist/core/ingestion/route-extractors/spring-shared.d.ts +50 -0
  74. package/dist/core/ingestion/route-extractors/spring-shared.js +80 -0
  75. package/dist/core/ingestion/route-extractors/spring.d.ts +35 -0
  76. package/dist/core/ingestion/route-extractors/spring.js +136 -0
  77. package/dist/core/ingestion/scope-extractor.js +3 -0
  78. package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.js +12 -0
  79. package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.js +145 -24
  80. package/dist/core/ingestion/scope-resolution/pipeline/phase.js +6 -0
  81. package/dist/core/ingestion/scope-resolution/pipeline/reconcile-ownership.js +43 -3
  82. package/dist/core/ingestion/scope-resolution/pipeline/run.d.ts +21 -0
  83. package/dist/core/ingestion/scope-resolution/pipeline/run.js +207 -0
  84. package/dist/core/ingestion/scope-resolution/resolution-outcome.d.ts +1 -1
  85. package/dist/core/ingestion/taint/emit.d.ts +124 -0
  86. package/dist/core/ingestion/taint/emit.js +204 -0
  87. package/dist/core/ingestion/taint/match.d.ts +153 -0
  88. package/dist/core/ingestion/taint/match.js +278 -0
  89. package/dist/core/ingestion/taint/path-codec.d.ts +134 -0
  90. package/dist/core/ingestion/taint/path-codec.js +190 -0
  91. package/dist/core/ingestion/taint/propagate.d.ts +216 -0
  92. package/dist/core/ingestion/taint/propagate.js +664 -0
  93. package/dist/core/ingestion/taint/site-safety.d.ts +29 -0
  94. package/dist/core/ingestion/taint/site-safety.js +98 -0
  95. package/dist/core/ingestion/taint/source-sink-config.d.ts +94 -23
  96. package/dist/core/ingestion/taint/source-sink-config.js +11 -11
  97. package/dist/core/ingestion/taint/source-sink-registry.d.ts +6 -4
  98. package/dist/core/ingestion/taint/source-sink-registry.js +6 -4
  99. package/dist/core/ingestion/taint/typescript-model.d.ts +38 -0
  100. package/dist/core/ingestion/taint/typescript-model.js +102 -0
  101. package/dist/core/ingestion/utils/method-props.js +1 -0
  102. package/dist/core/ingestion/workers/clone-safety.d.ts +109 -0
  103. package/dist/core/ingestion/workers/clone-safety.js +465 -0
  104. package/dist/core/ingestion/workers/parse-worker.d.ts +11 -0
  105. package/dist/core/ingestion/workers/parse-worker.js +70 -53
  106. package/dist/core/ingestion/workers/post-result.d.ts +22 -0
  107. package/dist/core/ingestion/workers/post-result.js +87 -0
  108. package/dist/core/ingestion/workers/result-merge.d.ts +20 -0
  109. package/dist/core/ingestion/workers/result-merge.js +43 -0
  110. package/dist/core/ingestion/workers/worker-pool.d.ts +15 -0
  111. package/dist/core/ingestion/workers/worker-pool.js +33 -16
  112. package/dist/core/lbug/lbug-adapter.d.ts +19 -0
  113. package/dist/core/lbug/lbug-adapter.js +56 -1
  114. package/dist/core/run-analyze.d.ts +87 -0
  115. package/dist/core/run-analyze.js +280 -25
  116. package/dist/core/tree-sitter/parser-loader.js +5 -4
  117. package/dist/core/tree-sitter/vendored-grammars.d.ts +39 -0
  118. package/dist/core/tree-sitter/vendored-grammars.js +57 -0
  119. package/dist/mcp/core/embedder.js +4 -0
  120. package/dist/mcp/local/local-backend.d.ts +86 -2
  121. package/dist/mcp/local/local-backend.js +657 -27
  122. package/dist/mcp/resources.js +1 -0
  123. package/dist/mcp/tools.d.ts +9 -0
  124. package/dist/mcp/tools.js +109 -0
  125. package/dist/server/analyze-launch.d.ts +29 -0
  126. package/dist/server/analyze-launch.js +137 -0
  127. package/dist/server/analyze-upload.d.ts +33 -0
  128. package/dist/server/analyze-upload.js +123 -0
  129. package/dist/server/analyze-worker-ipc.d.ts +58 -0
  130. package/dist/server/analyze-worker-ipc.js +16 -0
  131. package/dist/server/analyze-worker.d.ts +17 -1
  132. package/dist/server/analyze-worker.js +7 -1
  133. package/dist/server/api.js +46 -140
  134. package/dist/server/git-clone.d.ts +1 -0
  135. package/dist/server/git-clone.js +1 -1
  136. package/dist/server/middleware.d.ts +11 -0
  137. package/dist/server/middleware.js +27 -0
  138. package/dist/server/upload-ingest.d.ts +56 -0
  139. package/dist/server/upload-ingest.js +276 -0
  140. package/dist/server/upload-paths.d.ts +31 -0
  141. package/dist/server/upload-paths.js +51 -0
  142. package/dist/server/upload-sweep.d.ts +20 -0
  143. package/dist/server/upload-sweep.js +57 -0
  144. package/dist/storage/branch-index.d.ts +52 -0
  145. package/dist/storage/branch-index.js +65 -0
  146. package/dist/storage/git.d.ts +11 -0
  147. package/dist/storage/git.js +28 -0
  148. package/dist/storage/parse-cache.d.ts +22 -1
  149. package/dist/storage/parse-cache.js +32 -10
  150. package/dist/storage/repo-manager.d.ts +122 -10
  151. package/dist/storage/repo-manager.js +162 -21
  152. package/hooks/antigravity/gitnexus-antigravity-hook.cjs +42 -8
  153. package/hooks/claude/gitnexus-hook.cjs +36 -8
  154. package/hooks/claude/hook-db-lock-probe.cjs +123 -2
  155. package/package.json +4 -2
  156. package/scripts/assert-publish-grammar-coverage.cjs +31 -0
  157. package/scripts/build-tree-sitter-grammars.cjs +16 -10
  158. package/skills/gitnexus-guide.md +11 -0
  159. package/vendor/tree-sitter-c/package.json +1 -1
  160. package/vendor/tree-sitter-dart/package.json +1 -1
  161. package/vendor/tree-sitter-kotlin/package.json +1 -1
  162. package/vendor/tree-sitter-proto/package.json +1 -1
  163. package/vendor/tree-sitter-swift/package.json +1 -1
  164. package/web/assets/{agent-CKPMqImC.js → agent-ay4LD70X.js} +1 -1
  165. package/web/assets/{architectureDiagram-UL44E2DR-D1EXI0zA.js → architectureDiagram-UL44E2DR-Dc-viYhd.js} +1 -1
  166. package/web/assets/{chunk-LCXTWHL2-Dfmux4m1.js → chunk-LCXTWHL2-4rpojOyj.js} +1 -1
  167. package/web/assets/{chunk-RG4AUYOV-CAkzcoRj.js → chunk-RG4AUYOV-BnOy944n.js} +1 -1
  168. package/web/assets/{classDiagram-KGZ6W3CR-Bv93af_b.js → classDiagram-KGZ6W3CR-Bsgpy98Q.js} +1 -1
  169. package/web/assets/{classDiagram-v2-72OJOZXJ-CLfEqHUa.js → classDiagram-v2-72OJOZXJ-D5atDGjc.js} +1 -1
  170. package/web/assets/{diagram-3NCE3AQN-BqAtKUpW.js → diagram-3NCE3AQN-Cz1OEMVi.js} +1 -1
  171. package/web/assets/{diagram-GF46GFSD-CaBG6n6o.js → diagram-GF46GFSD-CWYwzfP8.js} +1 -1
  172. package/web/assets/{diagram-QXG6HAR7-CZ-O3rcV.js → diagram-QXG6HAR7-DtBObr8L.js} +1 -1
  173. package/web/assets/{diagram-WEQXMOUZ-P4lSL4GH.js → diagram-WEQXMOUZ-BUV44Ov_.js} +1 -1
  174. package/web/assets/{erDiagram-L5TCEMPS-QN2eEP1e.js → erDiagram-L5TCEMPS-Ds9s-sRF.js} +1 -1
  175. package/web/assets/{flowDiagram-H6V6AXG4-CXbXImlN.js → flowDiagram-H6V6AXG4-CupStHQb.js} +1 -1
  176. package/web/assets/index-COMMmbxW.css +2 -0
  177. package/web/assets/{index-CG6q8eTs.js → index-Do8AE5yF.js} +85 -85
  178. package/web/assets/{infoDiagram-3YFTVSEB-DmLICZx1.js → infoDiagram-3YFTVSEB-DPTXSbgu.js} +1 -1
  179. package/web/assets/{ishikawaDiagram-BNXS4ZKH-6LKOvBfp.js → ishikawaDiagram-BNXS4ZKH-CeK-Qhet.js} +1 -1
  180. package/web/assets/{kanban-definition-75IXJCU3-DgDi9oJT.js → kanban-definition-75IXJCU3-D4tx6QQ-.js} +1 -1
  181. package/web/assets/{mindmap-definition-2TDM6QVE-CwR5sBB-.js → mindmap-definition-2TDM6QVE-Bcto_kGa.js} +1 -1
  182. package/web/assets/{pieDiagram-CU6KROY3-By8g6f6B.js → pieDiagram-CU6KROY3-DPIeBTG6.js} +1 -1
  183. package/web/assets/{requirementDiagram-JXO7QTGE-7oDcJ1_J.js → requirementDiagram-JXO7QTGE-DDGXjVKO.js} +1 -1
  184. package/web/assets/{sequenceDiagram-VS2MUI6T-FySeKCUy.js → sequenceDiagram-VS2MUI6T-BzDqpcVW.js} +1 -1
  185. package/web/assets/{stateDiagram-7D4R322I-CB2nABwH.js → stateDiagram-7D4R322I-Bx__als3.js} +1 -1
  186. package/web/assets/{stateDiagram-v2-36443NZ5-COBGd2RL.js → stateDiagram-v2-36443NZ5-QqOh2yO-.js} +1 -1
  187. package/web/assets/{timeline-definition-O6YCAMPW-Ds2CnVZK.js → timeline-definition-O6YCAMPW-C1eogTOG.js} +1 -1
  188. package/web/assets/{vennDiagram-MWXL3ELB-DUIEwXWp.js → vennDiagram-MWXL3ELB-D20F4rSW.js} +1 -1
  189. package/web/assets/{wardleyDiagram-CUQ6CDDI-DEiFPQih.js → wardleyDiagram-CUQ6CDDI-BeBHg7ST.js} +1 -1
  190. package/web/assets/{xychartDiagram-N2JHSOCM-BUbayhST.js → xychartDiagram-N2JHSOCM-CdOWeoNI.js} +1 -1
  191. package/web/index.html +2 -2
  192. package/scripts/materialize-vendor-grammars.cjs +0 -97
  193. package/web/assets/index-BKWA-m7o.css +0 -2
@@ -0,0 +1,364 @@
1
+ /**
2
+ * Canonical `block:stmt` string key for a program point. Colon-separated to
3
+ * match the codebase's `blockIndex:stmtIndex` id conventions. Shared by the
4
+ * taint propagation engine (dedup/state keys) and the taint emit path
5
+ * (persisted edge-id material) so the two never drift.
6
+ */
7
+ export function pointKey(p) {
8
+ return `${p.blockIndex}:${p.stmtIndex}`;
9
+ }
10
+ /**
11
+ * def-site key: packs (blockIndex, stmtIndex) into one number. The stride is
12
+ * a per-BLOCK statement bound, and `maxFunctionLines` caps LINES, not
13
+ * statements — a minified one-line function coalesces arbitrarily many
14
+ * statements into one block, so an overflow would silently alias
15
+ * (block b, stmt STRIDE+k) with (block b+1, stmt k) and fabricate wrong-block
16
+ * facts. computeReachingDefs therefore range-checks up front and bails to a
17
+ * sound empty `truncated` result instead of ever letting a key alias.
18
+ * 2^21 statements per block × blocks ≤ 2^32 stays inside Number's 2^53.
19
+ */
20
+ const STMT_STRIDE = 1 << 21;
21
+ const defKey = (blockIndex, stmtIndex) => blockIndex * STMT_STRIDE + stmtIndex;
22
+ const EMPTY_LATTICE = new Map();
23
+ /**
24
+ * Compute reaching definitions for one function. See the module doc for the
25
+ * purity/determinism/sharing contract.
26
+ */
27
+ export function computeReachingDefs(cfg, limits) {
28
+ if (!cfg.bindings) {
29
+ return { status: 'no-facts', bindings: [], facts: [], defCount: 0, useCount: 0 };
30
+ }
31
+ const blocks = cfg.blocks;
32
+ const n = blocks.length;
33
+ // Key-aliasing guard (see STMT_STRIDE): a block with ≥ STRIDE statements
34
+ // cannot be keyed without aliasing into the next block's def sites, which
35
+ // would fabricate wrong-block facts — strictly worse than producing none.
36
+ // Bail to a sound empty `overflow` result (the emit path warns distinctly).
37
+ for (const b of blocks) {
38
+ if ((b.statements?.length ?? 0) >= STMT_STRIDE) {
39
+ return { status: 'overflow', bindings: cfg.bindings, facts: [], defCount: 0, useCount: 0 };
40
+ }
41
+ }
42
+ // ── adjacency (sorted for deterministic merges) ─────────────────────────
43
+ // A `throw` edge contributes IN(from) ∪ allDefs(from) to its handler, not
44
+ // OUT: an exception can fire BEFORE the block's defs complete (the seed def
45
+ // in `let x = seed(); try { x = risky(); } catch { sink(x) }` must reach the
46
+ // sink) AND between any two defs of a multi-def coalesced block (the parse
47
+ // def in `x = parse(a); x = normalize(x);` is live exactly when normalize
48
+ // throws — OUT's last-def-wins misses it). Sound over-approximation;
49
+ // monotone, so the fixpoint absorbs it. See mergePreds.
50
+ const preds = Array.from({ length: n }, () => []);
51
+ const succs = Array.from({ length: n }, () => []);
52
+ // Handlers whose IN depends on this block's IN (throw edges) — requeued on
53
+ // IN change, since a genned binding can absorb IN growth without changing
54
+ // OUT, which would otherwise leave the handler stale.
55
+ const throwSuccs = Array.from({ length: n }, () => []);
56
+ for (const e of cfg.edges) {
57
+ // Optional-chained pushes drop out-of-range endpoints defensively — the
58
+ // emit path validates via isEmitSafeCfg, but this pure function also runs
59
+ // on hand-built CFGs.
60
+ succs[e.from]?.push(e.to);
61
+ preds[e.to]?.push({ from: e.from, viaThrow: e.kind === 'throw' });
62
+ if (e.kind === 'throw')
63
+ throwSuccs[e.from]?.push(e.to);
64
+ }
65
+ for (const list of preds) {
66
+ list.sort((a, b) => a.from - b.from || Number(a.viaThrow) - Number(b.viaThrow));
67
+ // duplicate (from, throw+non-throw) pairs both survive — the throw leg
68
+ // adds IN(from); the merge dedups set-wise.
69
+ }
70
+ for (const list of succs)
71
+ list.sort((a, b) => a - b);
72
+ const gen = new Array(n).fill(null);
73
+ // allDefsGen[b]: bindingIdx → EVERY def-site key in the block (must + may).
74
+ // This is what a throw edge delivers to its handler: an exception can fire
75
+ // between any two statements, so every intermediate def may be the live one
76
+ // at the handler — IN∪OUT alone misses defs overwritten later in the same
77
+ // coalesced block (`try { x = parse(a); x = normalize(x); } catch { sink(x) }`
78
+ // — parse's value is exactly what sink sees when normalize throws).
79
+ const allDefsGen = new Array(n).fill(null);
80
+ const defLine = new Map(); // defKey → source line
81
+ let defCount = 0;
82
+ let useCount = 0;
83
+ for (const b of blocks) {
84
+ const stmts = b.statements;
85
+ if (!stmts || stmts.length === 0)
86
+ continue;
87
+ let g = null;
88
+ let all = null;
89
+ for (let i = 0; i < stmts.length; i++) {
90
+ const s = stmts[i];
91
+ useCount += s.uses.length;
92
+ const key = defKey(b.index, i);
93
+ const record = (d, kills) => {
94
+ defCount += 1;
95
+ defLine.set(key, s.line);
96
+ if (!g)
97
+ g = new Map();
98
+ const entry = g.get(d);
99
+ if (kills || !entry) {
100
+ g.set(d, { set: new Set([key]), kills: kills || (entry?.kills ?? false) });
101
+ }
102
+ else {
103
+ entry.set.add(key); // may-def accumulates; never clears
104
+ }
105
+ if (!all)
106
+ all = new Map();
107
+ const allSet = all.get(d);
108
+ if (allSet)
109
+ allSet.add(key);
110
+ else
111
+ all.set(d, new Set([key]));
112
+ };
113
+ if (s.mayDefs)
114
+ for (const d of s.mayDefs)
115
+ record(d, false);
116
+ for (const d of s.defs)
117
+ record(d, true);
118
+ }
119
+ gen[b.index] = g;
120
+ allDefsGen[b.index] = all;
121
+ }
122
+ // ── iteration order: RPO over reachable blocks, then the rest by index ──
123
+ const order = reversePostOrder(cfg.entryIndex, succs, n);
124
+ // ── fixpoint ────────────────────────────────────────────────────────────
125
+ const inSets = new Array(n).fill(EMPTY_LATTICE);
126
+ const outSets = new Array(n).fill(EMPTY_LATTICE);
127
+ const inWorklist = new Array(n).fill(true);
128
+ let pending = n;
129
+ while (pending > 0) {
130
+ for (const b of order) {
131
+ if (!inWorklist[b])
132
+ continue;
133
+ inWorklist[b] = false;
134
+ pending -= 1;
135
+ const p = preds[b];
136
+ const inB = p.length === 0
137
+ ? EMPTY_LATTICE
138
+ : p.length === 1 && !p[0].viaThrow
139
+ ? outSets[p[0].from] // alias — zero allocation on straight-line chains
140
+ : mergePreds(p, inSets, outSets, allDefsGen);
141
+ const inChanged = !latticeEquals(inSets[b], inB);
142
+ inSets[b] = inB;
143
+ const g = gen[b];
144
+ // OUT = overlay(IN): a KILLING gen entry replaces the binding's set; a
145
+ // may-def-only entry unions with the incoming set (never kills). When
146
+ // nothing is genned, OUT aliases IN outright.
147
+ let outB;
148
+ if (!g) {
149
+ outB = inB;
150
+ }
151
+ else {
152
+ outB = new Map(inB); // copies REFERENCES, never set contents
153
+ for (const [bindingIdx, entry] of g) {
154
+ if (entry.kills) {
155
+ outB.set(bindingIdx, entry.set);
156
+ }
157
+ else {
158
+ const incoming = inB.get(bindingIdx);
159
+ outB.set(bindingIdx, incoming ? unionSets(incoming, entry.set) : entry.set);
160
+ }
161
+ }
162
+ }
163
+ const requeue = (s) => {
164
+ if (!inWorklist[s]) {
165
+ inWorklist[s] = true;
166
+ pending += 1;
167
+ }
168
+ };
169
+ if (!latticeEquals(outSets[b], outB)) {
170
+ outSets[b] = outB;
171
+ for (const s of succs[b])
172
+ requeue(s);
173
+ }
174
+ if (inChanged)
175
+ for (const s of throwSuccs[b])
176
+ requeue(s);
177
+ }
178
+ }
179
+ // ── statement sweep: recover statement-granular def→use facts ───────────
180
+ const maxFacts = limits?.maxFacts && limits.maxFacts > 0 ? limits.maxFacts : Infinity;
181
+ const facts = [];
182
+ let truncated = false;
183
+ outer: for (const b of blocks) {
184
+ const stmts = b.statements;
185
+ if (!stmts || stmts.length === 0)
186
+ continue;
187
+ // Lazy overlay of IN — entries are replaced (never mutated) on def, so the
188
+ // shared sets stay intact.
189
+ let reach = null;
190
+ for (let i = 0; i < stmts.length; i++) {
191
+ const s = stmts[i];
192
+ // A use's binding that the SAME statement also defines could be a
193
+ // read-then-write (`x += 1` — sees prior defs) OR a write-then-read
194
+ // (`if ((m = re.exec(s)) && m[1])` — sees the same-statement def).
195
+ // StatementFacts carries no intra-statement order, so emit BOTH: prior
196
+ // defs ∪ the same-statement def. Sound over-approximation — the extra
197
+ // self-fact on compound assignments is harmless; missing the
198
+ // assign-and-test def→use (the most common JS idiom) would be a taint
199
+ // false negative. May-defs join the self-key set the same way.
200
+ const sameStmtDefs = s.defs.length > 0 || s.mayDefs?.length ? new Set([...s.defs, ...(s.mayDefs ?? [])]) : null;
201
+ for (const u of s.uses) {
202
+ const reaching = (reach ?? inSets[b.index]).get(u);
203
+ const selfKey = sameStmtDefs?.has(u) ? defKey(b.index, i) : undefined;
204
+ if (!reaching && selfKey === undefined)
205
+ continue;
206
+ const keys = selfKey !== undefined && !reaching?.has(selfKey)
207
+ ? [...(reaching ?? []), selfKey]
208
+ : [...(reaching ?? [])];
209
+ for (const key of keys) {
210
+ if (facts.length >= maxFacts) {
211
+ truncated = true;
212
+ break outer;
213
+ }
214
+ const defBlock = Math.floor(key / STMT_STRIDE);
215
+ const defStmt = key % STMT_STRIDE;
216
+ facts.push({
217
+ bindingIdx: u,
218
+ def: { blockIndex: defBlock, stmtIndex: defStmt, line: defLine.get(key) ?? s.line },
219
+ use: { blockIndex: b.index, stmtIndex: i, line: s.line },
220
+ });
221
+ }
222
+ }
223
+ if (s.mayDefs?.length) {
224
+ // Gen WITHOUT kill: the conditional def joins the binding's set.
225
+ if (!reach)
226
+ reach = new Map(inSets[b.index]);
227
+ const key = defKey(b.index, i);
228
+ for (const d of s.mayDefs) {
229
+ const prior = reach.get(d);
230
+ reach.set(d, prior ? unionSets(prior, new Set([key])) : new Set([key]));
231
+ }
232
+ }
233
+ if (s.defs.length > 0) {
234
+ if (!reach)
235
+ reach = new Map(inSets[b.index]);
236
+ for (const d of s.defs)
237
+ reach.set(d, new Set([defKey(b.index, i)])); // kill + gen
238
+ }
239
+ }
240
+ }
241
+ facts.sort((a, b) => a.def.blockIndex - b.def.blockIndex ||
242
+ a.def.stmtIndex - b.def.stmtIndex ||
243
+ a.use.blockIndex - b.use.blockIndex ||
244
+ a.use.stmtIndex - b.use.stmtIndex ||
245
+ a.bindingIdx - b.bindingIdx);
246
+ return {
247
+ status: truncated ? 'truncated' : 'computed',
248
+ bindings: cfg.bindings,
249
+ facts,
250
+ defCount,
251
+ useCount,
252
+ };
253
+ }
254
+ /** RPO over blocks reachable from `entry`; unreachable blocks appended by index. */
255
+ function reversePostOrder(entry, succs, n) {
256
+ const visited = new Array(n).fill(false);
257
+ const post = [];
258
+ // Iterative DFS with an explicit phase stack (children pushed in reverse so
259
+ // they pop in sorted order — determinism).
260
+ const stack = [{ node: entry, childIdx: 0 }];
261
+ visited[entry] = true;
262
+ while (stack.length) {
263
+ const top = stack[stack.length - 1];
264
+ const children = succs[top.node];
265
+ if (top.childIdx < children.length) {
266
+ const next = children[top.childIdx];
267
+ top.childIdx += 1;
268
+ if (!visited[next]) {
269
+ visited[next] = true;
270
+ stack.push({ node: next, childIdx: 0 });
271
+ }
272
+ }
273
+ else {
274
+ post.push(top.node);
275
+ stack.pop();
276
+ }
277
+ }
278
+ const order = post.reverse();
279
+ for (let b = 0; b < n; b++)
280
+ if (!visited[b])
281
+ order.push(b);
282
+ return order;
283
+ }
284
+ /**
285
+ * Union predecessor lattices, sharing sets where possible. A normal edge
286
+ * contributes OUT(from). A THROW edge contributes IN(from) ∪ allDefs(from):
287
+ * an exception may fire before, between, or after any of the block's defs, so
288
+ * the handler can observe the incoming state OR any intermediate def — OUT
289
+ * alone (last-def-wins) misses defs overwritten later in the same block.
290
+ * IN ∪ allDefs ⊇ OUT, so the throw contribution subsumes it.
291
+ */
292
+ function mergePreds(preds, inSets, outSets, allDefsGen) {
293
+ const merged = new Map();
294
+ const mergeOne = (source) => {
295
+ for (const [bindingIdx, set] of source) {
296
+ const existing = merged.get(bindingIdx);
297
+ if (!existing) {
298
+ merged.set(bindingIdx, set); // share the first contributor's set
299
+ }
300
+ else if (existing !== set) {
301
+ // Union only when the references differ. Copy-on-extend: `existing`
302
+ // may be a shared set from another block — never mutate it.
303
+ let target = existing;
304
+ let copied = false;
305
+ for (const key of set) {
306
+ if (!target.has(key)) {
307
+ if (!copied) {
308
+ target = new Set(existing);
309
+ copied = true;
310
+ }
311
+ target.add(key);
312
+ }
313
+ }
314
+ if (copied)
315
+ merged.set(bindingIdx, target);
316
+ }
317
+ }
318
+ };
319
+ for (const p of preds) {
320
+ if (p.viaThrow) {
321
+ mergeOne(inSets[p.from]); // exception may fire pre-defs…
322
+ const all = allDefsGen[p.from];
323
+ if (all)
324
+ mergeOne(all); // …or after ANY of the block's defs
325
+ }
326
+ else {
327
+ mergeOne(outSets[p.from]);
328
+ }
329
+ }
330
+ return merged;
331
+ }
332
+ /** Order-stable union of two def-sets (shares `a` when `b` adds nothing). */
333
+ function unionSets(a, b) {
334
+ let target = a;
335
+ let copied = false;
336
+ for (const key of b) {
337
+ if (!target.has(key)) {
338
+ if (!copied) {
339
+ target = new Set(a);
340
+ copied = true;
341
+ }
342
+ target.add(key);
343
+ }
344
+ }
345
+ return target;
346
+ }
347
+ /** Per-binding equality with a reference fast path (sets only ever grow). */
348
+ function latticeEquals(a, b) {
349
+ if (a === b)
350
+ return true;
351
+ if (a.size !== b.size)
352
+ return false;
353
+ for (const [k, bSet] of b) {
354
+ const aSet = a.get(k);
355
+ if (aSet === bSet)
356
+ continue;
357
+ if (!aSet || aSet.size !== bSet.size)
358
+ return false;
359
+ for (const v of bSet)
360
+ if (!aSet.has(v))
361
+ return false;
362
+ }
363
+ return true;
364
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * TraversalResult (issue #2081, M1).
3
+ *
4
+ * Visiting a statement (or a statement sequence) returns the block its control
5
+ * flow ENTERS through, plus the set of blocks whose **normal** control flows
6
+ * out the bottom (the "dangling exits") — to be wired to the entry of whatever
7
+ * comes next. Abnormal exits (return/break/continue/throw) are wired directly
8
+ * to their targets during the walk and are NOT part of `exits`.
9
+ *
10
+ * A statement that cannot fall through (e.g. ends in `return`/`throw`, or both
11
+ * branches of an `if` return) yields an empty `exits` array.
12
+ */
13
+ export interface TraversalResult {
14
+ /** Block index control enters this statement/sequence through. */
15
+ readonly entry: number;
16
+ /** Block indices whose normal control falls out the bottom (may be empty). */
17
+ readonly exits: readonly number[];
18
+ }
19
+ /** A sequence of statements that produced no blocks (e.g. an empty body). */
20
+ export declare const emptyTraversal: (entry: number) => TraversalResult;
@@ -0,0 +1,2 @@
1
+ /** A sequence of statements that produced no blocks (e.g. an empty body). */
2
+ export const emptyTraversal = (entry) => ({ entry, exits: [entry] });
@@ -0,0 +1,231 @@
1
+ /**
2
+ * CFG data model — plain, JSON-serializable types (issue #2081, M1).
3
+ *
4
+ * These cross the worker→main boundary and the disk-backed/durable ParsedFile
5
+ * store, so they must contain NO tree-sitter AST references, class instances,
6
+ * or anything that does not survive `JSON.stringify` → `JSON.parse`. Block and
7
+ * edge endpoints are referenced by integer index within a function's CFG.
8
+ *
9
+ * The per-language `CfgVisitor` (built in the parse worker, where the AST
10
+ * lives — see the M1 plan KTD1/KTD7) produces a `FunctionCfg` per function; the
11
+ * array of them is what rides on `ParsedFile.cfgSideChannel`.
12
+ */
13
+ /**
14
+ * One distinct declared variable (binding) within a function (#2082 M2 U1).
15
+ *
16
+ * Statement facts reference bindings by integer index into
17
+ * {@link FunctionCfg.bindings} — names appear once per binding instead of once
18
+ * per occurrence (measured ~4× smaller serialized payload than named records).
19
+ * Distinct bindings of the same name (shadowing) get distinct entries, which is
20
+ * what keeps an inner `let x` from falsely killing the outer `x`'s definitions
21
+ * in the reaching-defs solver. NOTE: no field here may be named `nodeId` — the
22
+ * durable parsedfile-store reviver dedups objects keyed on that field name.
23
+ */
24
+ export interface BindingEntry {
25
+ /** Source-level variable name (what the persisted edge's `reason` carries). */
26
+ readonly name: string;
27
+ /**
28
+ * 1-based line/0-based column of the canonical declaration site — `var`
29
+ * multi-declarations canonicalize to the FIRST declaration in source order.
30
+ * Both 0 for synthetic bindings.
31
+ */
32
+ readonly declLine: number;
33
+ readonly declColumn: number;
34
+ /** How the binding was introduced (param/catch matter to the M3 taint pass). */
35
+ readonly kind: 'var' | 'let' | 'const' | 'param' | 'catch' | 'function' | 'class' | 'module';
36
+ /**
37
+ * True when the name has no in-function declaration site (implicit global,
38
+ * import, or a variable captured from an enclosing function) — keyed
39
+ * `name@module` in edge ids instead of `name:line:col`.
40
+ */
41
+ readonly synthetic?: boolean;
42
+ }
43
+ /**
44
+ * One occurrence of a binding inside a call/new site's argument position
45
+ * (#2083 M3 U1). A bare `number` is a DIRECT occurrence (binding index into
46
+ * {@link FunctionCfg.bindings}); a `[bindingIdx, viaSiteIdx]` tuple marks an
47
+ * occurrence that reaches this argument THROUGH the nested site at
48
+ * `viaSiteIdx` (an index into the SAME statement's {@link StatementFacts.sites}
49
+ * array). The tag is load-bearing for sanitizer interposition (plan KTD4a):
50
+ * a flat per-arg binding set cannot distinguish `exec(escape(x))` (kill) from
51
+ * `exec(x)` (finding) — the single most common safe pattern would
52
+ * false-positive without it.
53
+ */
54
+ export type SiteArgOccurrence = number | readonly [number, number];
55
+ /**
56
+ * One call site, constructor call, or value-position member read harvested
57
+ * from a statement (#2083 M3 U1, plan KTD2). Worker-side substrate for the M3
58
+ * taint pass: the M2 facts carry no expression structure, and the main thread
59
+ * cannot re-parse (the #1983 OOM shape). Spec-AGNOSTIC — records structure
60
+ * only, never source/sink/sanitizer-ness (matching is a main-thread concern).
61
+ *
62
+ * Integer indices: binding fields (`receiver`/`object`/`resultDefs`/arg
63
+ * occurrences) index {@link FunctionCfg.bindings}; site references (`parent`,
64
+ * via-tags) index the OWNING statement's `sites` array. JSON-plain; NO field
65
+ * here may be named `nodeId` (durable parsedfile-store reviver hazard — see
66
+ * {@link BindingEntry}).
67
+ */
68
+ export interface SiteRecord {
69
+ readonly kind: 'call' | 'new' | 'member-read';
70
+ /**
71
+ * Dotted callee path for call/new sites whose callee chain is rooted at an
72
+ * identifier/`this`/`super` (`child_process.exec`, `req.body.toString`).
73
+ * Optional chaining is normalized (`a?.b()` ⇒ `a.b`); string-literal
74
+ * subscripts fold into the path (`cp["exec"]` ⇒ `cp.exec`). Absent when the
75
+ * chain is not statically resolvable (dynamic key, call-rooted chain).
76
+ */
77
+ readonly callee?: string;
78
+ /**
79
+ * Binding index of the callee chain's ROOT identifier when the callee is a
80
+ * member chain (`userInput.trim()` ⇒ `userInput`). Method calls launder
81
+ * taint without it (plan KTD5 receiver-position TITO). Absent for bare
82
+ * calls (`exec(x)`) and non-identifier roots.
83
+ */
84
+ readonly receiver?: number;
85
+ /**
86
+ * Per-argument-position occurrence entries (trailing empty positions are
87
+ * trimmed; absent when no argument carries a binding occurrence). For
88
+ * `template: true` sites every substitution occurrence aggregates at
89
+ * position 0 (tagged templates have no positional argument list).
90
+ */
91
+ readonly args?: ReadonlyArray<readonly SiteArgOccurrence[]>;
92
+ /**
93
+ * Bindings defined by a declarator/assignment whose ENTIRE value (after
94
+ * unwrapping parens/`await`/`as`/`!`) is this call — `const b = escape(t)`
95
+ * ⇒ `[b]`. Per-declarator: `const a = t, b = escape(t)` attaches `[b]`
96
+ * only. Kill placement (KTD4b) keys on this: a sanitizer kills exactly the
97
+ * defs that receive its result directly.
98
+ */
99
+ readonly resultDefs?: readonly number[];
100
+ /**
101
+ * `[siteIdx, argIdx]` of the innermost enclosing call/new site argument
102
+ * position this site occurs in (`exec(escape(x))` ⇒ escape's parent is
103
+ * `[execSiteIdx, 0]`). Absent for top-level sites.
104
+ */
105
+ readonly parent?: readonly [number, number];
106
+ /**
107
+ * Index of the FIRST spread argument (`exec(...args)` ⇒ 0). Presence means
108
+ * position matching must degrade soundly (any sink position ≥ this index —
109
+ * plan KTD2/U2). A number (not boolean) because the matcher needs the index.
110
+ */
111
+ readonly spread?: number;
112
+ /** Tagged-template call (`sql\`…${id}\``) — argument positions are not positional. */
113
+ readonly template?: boolean;
114
+ /**
115
+ * String-literal first argument when the callee is bare `require` —
116
+ * CommonJS aliases resolve like ESM imports on the main thread (KTD7).
117
+ */
118
+ readonly requireArg?: string;
119
+ /** Member read: binding index of the object root (`req.body` ⇒ `req`). */
120
+ readonly object?: number;
121
+ /**
122
+ * Member read: property name (`req.body` ⇒ `'body'`; `req["body"]`
123
+ * included; dynamic `req[key]` is never recorded — documented KTD10 FN).
124
+ */
125
+ readonly property?: string;
126
+ }
127
+ /**
128
+ * Def/use facts for one harvested statement (or construct header), in
129
+ * execution order within its block (#2082 M2 U1). `defs`/`uses` are indices
130
+ * into {@link FunctionCfg.bindings}. A compound assignment / update expression
131
+ * lists its binding in BOTH. Self-describing — `line` is carried here, never
132
+ * inferred from the block's text fragments (facts-only records exist, e.g.
133
+ * params on ENTRY and catch params).
134
+ *
135
+ * `mayDefs` (tri-review P1): defs harvested inside CONDITIONALLY-EVALUATED
136
+ * subexpressions — short-circuit right operands (`a && (x = v)`,
137
+ * `c ?? (c = load())`), ternary arms, logical-assignment operators, and
138
+ * switch case-test expressions. The solver treats them as GEN WITHOUT KILL:
139
+ * treating them as must-defs would falsely kill the prior def on the
140
+ * not-taken path (a taint false negative on core JS idioms). Optional —
141
+ * absent means none.
142
+ *
143
+ * `sites` (#2083 M3 U1): call/member-read structure for the taint pass —
144
+ * see {@link SiteRecord}. Optional and omit-when-empty; absent on pre-M3
145
+ * channels and on statements with no calls or member reads. Sites inside
146
+ * nested functions are NOT recorded (consistent with def/use invisibility —
147
+ * the enclosing `arr.forEach(...)` call IS, with receiver `arr`).
148
+ */
149
+ export interface StatementFacts {
150
+ readonly line: number;
151
+ readonly defs: readonly number[];
152
+ readonly uses: readonly number[];
153
+ readonly mayDefs?: readonly number[];
154
+ readonly sites?: readonly SiteRecord[];
155
+ }
156
+ /** A basic block: a maximal straight-line run of statements between leaders. */
157
+ export interface BasicBlockData {
158
+ /** Block index within its function. The synthetic ENTRY is always 0. */
159
+ readonly index: number;
160
+ readonly startLine: number;
161
+ readonly endLine: number;
162
+ /** Source snippet for the block (empty for synthetic ENTRY/EXIT). */
163
+ readonly text: string;
164
+ readonly kind: 'entry' | 'exit' | 'normal';
165
+ /**
166
+ * Per-statement def/use facts in execution order (#2082 M2 U1). Present only
167
+ * when the producing visitor harvests (TS/JS under `--pdg`); absent on
168
+ * hand-built or pre-M2 CFGs — the reaching-defs solver reports `no-facts`.
169
+ */
170
+ readonly statements?: readonly StatementFacts[];
171
+ }
172
+ /**
173
+ * Why one block flows to another — drives the `reason` on the emitted CFG edge.
174
+ *
175
+ * Kind invariant (M2): a bare jump kind (`return`/`break`/`continue`) means the
176
+ * SOURCE block's terminator is that jump statement. A `finally-*` kind marks a
177
+ * COMPLETION edge out of a `finally` body's exit — the leg that resumes a jump
178
+ * which was re-routed through the finally (issue #2082 U2). Reusing the bare
179
+ * kinds on completion edges would silently break consumers that infer the
180
+ * source block's terminator from the kind, and a single generic kind would lose
181
+ * WHICH jump each completion edge completes when a shared finally has several
182
+ * pending targets.
183
+ */
184
+ export type CfgEdgeKind = 'seq' | 'cond-true' | 'cond-false' | 'loop-back' | 'break' | 'continue' | 'return' | 'throw' | 'switch-case' | 'fallthrough' | 'finally-return' | 'finally-break' | 'finally-continue';
185
+ export interface CfgEdgeData {
186
+ readonly from: number;
187
+ readonly to: number;
188
+ readonly kind: CfgEdgeKind;
189
+ }
190
+ /** One function's control-flow graph. `cfgSideChannel` is `readonly FunctionCfg[]`. */
191
+ export interface FunctionCfg {
192
+ readonly filePath: string;
193
+ /** Source span of the owning function — anchors the BasicBlock node ids. */
194
+ readonly functionStartLine: number;
195
+ readonly functionEndLine: number;
196
+ /**
197
+ * Start COLUMN of the owning function. Combined with `functionStartLine` it
198
+ * disambiguates the BasicBlock node ids when two functions share a start line
199
+ * — e.g. `{ a: () => x(), b: () => y() }`, where both arrows begin on the same
200
+ * line and each restarts its block indices at 0. Without the column the ids
201
+ * collide and the graph's first-writer-wins `addNode` silently drops the
202
+ * second function's blocks and cross-wires its edges.
203
+ */
204
+ readonly functionStartColumn: number;
205
+ readonly entryIndex: number;
206
+ readonly exitIndex: number;
207
+ readonly blocks: readonly BasicBlockData[];
208
+ readonly edges: readonly CfgEdgeData[];
209
+ /**
210
+ * The function's binding table (#2082 M2 U1) — referenced by index from
211
+ * {@link BasicBlockData.statements}. Present iff statement facts are.
212
+ */
213
+ readonly bindings?: readonly BindingEntry[];
214
+ }
215
+ /**
216
+ * Per-language CFG strategy. Invoked **in the parse worker** for each function
217
+ * node. `TNode` is the language's AST node type (tree-sitter `SyntaxNode` for
218
+ * TS/JS) — kept generic so this module stays AST-library-agnostic. Returns
219
+ * `undefined` when the node is not a CFG-bearing function (the caller skips it).
220
+ */
221
+ export interface CfgVisitor<TNode = unknown> {
222
+ buildFunctionCfg(fnNode: TNode, filePath: string): FunctionCfg | undefined;
223
+ /**
224
+ * Whether `node` is a CFG-bearing function this visitor handles. Lets the
225
+ * worker enumerate functions (and apply the per-function line budget) by a
226
+ * cheap node-type test, instead of attempting to build a CFG for every AST
227
+ * node. `buildFunctionCfg` still re-checks, so this is purely an optimization
228
+ * + the seam the line-budget hooks into.
229
+ */
230
+ isFunction(node: TNode): boolean;
231
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * CFG data model — plain, JSON-serializable types (issue #2081, M1).
3
+ *
4
+ * These cross the worker→main boundary and the disk-backed/durable ParsedFile
5
+ * store, so they must contain NO tree-sitter AST references, class instances,
6
+ * or anything that does not survive `JSON.stringify` → `JSON.parse`. Block and
7
+ * edge endpoints are referenced by integer index within a function's CFG.
8
+ *
9
+ * The per-language `CfgVisitor` (built in the parse worker, where the AST
10
+ * lives — see the M1 plan KTD1/KTD7) produces a `FunctionCfg` per function; the
11
+ * array of them is what rides on `ParsedFile.cfgSideChannel`.
12
+ */
13
+ export {};