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,134 @@
1
+ /**
2
+ * Taint-path reason codec (#2083 M3 U4/U6, plan KTD6).
3
+ *
4
+ * THE one shared encoder/decoder for the hop-encoded `reason` carried on
5
+ * persisted `TAINTED` edges: the U4 emit path writes it, the U6 MCP `explain`
6
+ * tool reads it. Two hand-rolled copies of a wire format drift — both sides
7
+ * MUST import from here.
8
+ *
9
+ * ## Wire format (version `1`)
10
+ *
11
+ * ```
12
+ * 1[;<kind>]|<name>:<line>[:<flags>]|<name>:<line>[:<flags>]|…[|~]
13
+ * ```
14
+ *
15
+ * - One-character version prefix (`TAINT_PATH_CODEC_VERSION`), then an
16
+ * OPTIONAL `;<kind>` header segment, then ordered source→sink hops, each
17
+ * `variable:line[:flags]`.
18
+ * - `kind` is the finding's sink category (`SinkKind`, e.g.
19
+ * `command-injection`). It rides the reason because it is the ONLY
20
+ * persisted channel: the CodeRelation columns are
21
+ * `type/confidence/reason/step` — `step` is INT32 and the emit-time edge id
22
+ * (which embeds the kind) is not a stored column. The U6 `explain` tool
23
+ * reads it for finding classification. Charset `[a-z0-9-]` (printable
24
+ * ASCII, disjoint from every structural delimiter); `;` itself is printable
25
+ * ASCII and never appears in hop names (identifier charset) or flags.
26
+ * U6 deviation note: this header was added by U6 WITHIN version `1` —
27
+ * U4 and U6 ship in the same release, so no reason string without the
28
+ * header was ever persisted by a released build; the decoder still accepts
29
+ * header-less strings (`kind` simply decodes as `undefined`).
30
+ * - `flags` is a lowercase-letter set; only `c` (= the hop passed through an
31
+ * unmodeled call, KTD5 `viaCall`) is defined today — the rest of the
32
+ * alphabet is RESERVED, and the decoder accepts unknown flag letters so a
33
+ * future writer's output stays decodable.
34
+ * - A trailing `|~` segment is the TRUNCATION MARKER: the encoded path is a
35
+ * source-side PREFIX of the real one (hop cap, byte cap, or an unencodable
36
+ * hop name). Decoders MUST report it as "path incomplete" — never an error.
37
+ *
38
+ * ## Delimiter / round-trip discipline (KTD6)
39
+ *
40
+ * Every structural character (`|`, `:`, `~`, digits, flag letters) is
41
+ * printable ASCII: `sanitizeUTF8` (csv-generator.ts) strips control
42
+ * characters, lone surrogates, and U+FFFE/FFFF — printable ASCII passes
43
+ * through byte-exact, so the encoding survives `escapeCSVField ∘
44
+ * sanitizeUTF8` and the DB load unchanged (pinned by the round-trip test).
45
+ * None of the delimiters can appear in a JS identifier.
46
+ *
47
+ * Hop names are identifier-charset by U1 construction (the harvest records
48
+ * binding names), but the encoder DEFENDS anyway: a hop whose name falls
49
+ * outside the safe charset (or whose line is not a non-negative integer) is
50
+ * never emitted — encoding stops at the offending hop and sets the truncation
51
+ * marker, preserving the prefix-of-the-true-path invariant rather than
52
+ * corrupting the format. (`#` is in the charset: JS private names are
53
+ * `#field`, and the propagation engine's fallback hop names are `#<idx>`.)
54
+ *
55
+ * The byte cap (`TAINT_REASON_MAX_BYTES`, KTD6's "absolute reason-byte cap")
56
+ * bounds the persisted reason column regardless of hop caps: overflow drops
57
+ * TRAILING hops (keeps the source side) and sets the marker. All structural
58
+ * chars and valid names are single-byte ASCII, so `string.length` IS the
59
+ * byte length.
60
+ */
61
+ /** One-character format version prefix. Bump on any wire-format change. */
62
+ export declare const TAINT_PATH_CODEC_VERSION = "1";
63
+ /**
64
+ * Absolute cap on the encoded reason's byte length (KTD6). 4096 comfortably
65
+ * holds ~100 hops of realistic identifiers — far beyond the default hop cap
66
+ * (32) — while bounding the persisted column even at `maxHops: 0` (unlimited).
67
+ */
68
+ export declare const TAINT_REASON_MAX_BYTES = 4096;
69
+ /** The truncation-marker segment content (rides as a trailing `|~`). */
70
+ export declare const TAINT_PATH_TRUNCATION_MARKER = "~";
71
+ /** Encoder input hop — shape-compatible with `TaintHop` (propagate.ts). */
72
+ export interface TaintPathHopInput {
73
+ readonly name: string;
74
+ readonly line: number;
75
+ readonly viaCall?: boolean;
76
+ }
77
+ export interface EncodeTaintPathOptions {
78
+ /**
79
+ * The hop list is already a truncated prefix (e.g. the propagation engine's
80
+ * `hopsTruncated` from its hop cap) — emit the marker even when every hop
81
+ * fits.
82
+ */
83
+ readonly truncated?: boolean;
84
+ /** Byte cap override (tests). Default {@link TAINT_REASON_MAX_BYTES}. */
85
+ readonly maxBytes?: number;
86
+ /**
87
+ * Finding sink category (`SinkKind`) carried in the `;<kind>` header — the
88
+ * only persisted channel for it (see the module doc). A value outside the
89
+ * `[a-z0-9-]` charset is DROPPED (header omitted), never corrupted into the
90
+ * wire string; `SinkKind` is a closed lowercase-hyphen union so this is
91
+ * purely defensive.
92
+ */
93
+ readonly kind?: string;
94
+ }
95
+ export interface EncodedTaintPath {
96
+ /** The wire string for the TAINTED edge's `reason` column. */
97
+ readonly reason: string;
98
+ /** True when the marker was emitted (caller-flagged, byte cap, or bad hop). */
99
+ readonly truncated: boolean;
100
+ }
101
+ export interface DecodedTaintHop {
102
+ readonly variable: string;
103
+ readonly line: number;
104
+ /** The hop passed through an unmodeled call (flag `c`, KTD5). */
105
+ readonly viaCall: boolean;
106
+ }
107
+ export interface DecodedTaintPath {
108
+ readonly ok: true;
109
+ readonly version: string;
110
+ /** Finding sink category from the `;<kind>` header; absent when not encoded. */
111
+ readonly kind?: string;
112
+ /** Ordered source→sink hops (a PREFIX when `truncated`). */
113
+ readonly hops: readonly DecodedTaintHop[];
114
+ /** Path incomplete (trailing `|~`) — informational, NOT an error. */
115
+ readonly truncated: boolean;
116
+ }
117
+ /** Typed parse failure — the decoder never throws. */
118
+ export interface TaintPathDecodeFailure {
119
+ readonly ok: false;
120
+ readonly error: string;
121
+ }
122
+ export type TaintPathDecodeResult = DecodedTaintPath | TaintPathDecodeFailure;
123
+ /**
124
+ * Encode an ordered hop list into the versioned `reason` wire string.
125
+ * Deterministic; never throws. See the module doc for the format and the
126
+ * three truncation triggers (caller flag, unencodable hop, byte cap).
127
+ */
128
+ export declare function encodeTaintPath(hops: readonly TaintPathHopInput[], options?: EncodeTaintPathOptions): EncodedTaintPath;
129
+ /**
130
+ * Decode a `reason` wire string. Returns a typed failure for anything that is
131
+ * not a well-formed version-`1` path — never throws. A truncated path decodes
132
+ * `ok: true` with `truncated: true` ("path incomplete", per KTD6).
133
+ */
134
+ export declare function decodeTaintPath(reason: unknown): TaintPathDecodeResult;
@@ -0,0 +1,190 @@
1
+ /**
2
+ * Taint-path reason codec (#2083 M3 U4/U6, plan KTD6).
3
+ *
4
+ * THE one shared encoder/decoder for the hop-encoded `reason` carried on
5
+ * persisted `TAINTED` edges: the U4 emit path writes it, the U6 MCP `explain`
6
+ * tool reads it. Two hand-rolled copies of a wire format drift — both sides
7
+ * MUST import from here.
8
+ *
9
+ * ## Wire format (version `1`)
10
+ *
11
+ * ```
12
+ * 1[;<kind>]|<name>:<line>[:<flags>]|<name>:<line>[:<flags>]|…[|~]
13
+ * ```
14
+ *
15
+ * - One-character version prefix (`TAINT_PATH_CODEC_VERSION`), then an
16
+ * OPTIONAL `;<kind>` header segment, then ordered source→sink hops, each
17
+ * `variable:line[:flags]`.
18
+ * - `kind` is the finding's sink category (`SinkKind`, e.g.
19
+ * `command-injection`). It rides the reason because it is the ONLY
20
+ * persisted channel: the CodeRelation columns are
21
+ * `type/confidence/reason/step` — `step` is INT32 and the emit-time edge id
22
+ * (which embeds the kind) is not a stored column. The U6 `explain` tool
23
+ * reads it for finding classification. Charset `[a-z0-9-]` (printable
24
+ * ASCII, disjoint from every structural delimiter); `;` itself is printable
25
+ * ASCII and never appears in hop names (identifier charset) or flags.
26
+ * U6 deviation note: this header was added by U6 WITHIN version `1` —
27
+ * U4 and U6 ship in the same release, so no reason string without the
28
+ * header was ever persisted by a released build; the decoder still accepts
29
+ * header-less strings (`kind` simply decodes as `undefined`).
30
+ * - `flags` is a lowercase-letter set; only `c` (= the hop passed through an
31
+ * unmodeled call, KTD5 `viaCall`) is defined today — the rest of the
32
+ * alphabet is RESERVED, and the decoder accepts unknown flag letters so a
33
+ * future writer's output stays decodable.
34
+ * - A trailing `|~` segment is the TRUNCATION MARKER: the encoded path is a
35
+ * source-side PREFIX of the real one (hop cap, byte cap, or an unencodable
36
+ * hop name). Decoders MUST report it as "path incomplete" — never an error.
37
+ *
38
+ * ## Delimiter / round-trip discipline (KTD6)
39
+ *
40
+ * Every structural character (`|`, `:`, `~`, digits, flag letters) is
41
+ * printable ASCII: `sanitizeUTF8` (csv-generator.ts) strips control
42
+ * characters, lone surrogates, and U+FFFE/FFFF — printable ASCII passes
43
+ * through byte-exact, so the encoding survives `escapeCSVField ∘
44
+ * sanitizeUTF8` and the DB load unchanged (pinned by the round-trip test).
45
+ * None of the delimiters can appear in a JS identifier.
46
+ *
47
+ * Hop names are identifier-charset by U1 construction (the harvest records
48
+ * binding names), but the encoder DEFENDS anyway: a hop whose name falls
49
+ * outside the safe charset (or whose line is not a non-negative integer) is
50
+ * never emitted — encoding stops at the offending hop and sets the truncation
51
+ * marker, preserving the prefix-of-the-true-path invariant rather than
52
+ * corrupting the format. (`#` is in the charset: JS private names are
53
+ * `#field`, and the propagation engine's fallback hop names are `#<idx>`.)
54
+ *
55
+ * The byte cap (`TAINT_REASON_MAX_BYTES`, KTD6's "absolute reason-byte cap")
56
+ * bounds the persisted reason column regardless of hop caps: overflow drops
57
+ * TRAILING hops (keeps the source side) and sets the marker. All structural
58
+ * chars and valid names are single-byte ASCII, so `string.length` IS the
59
+ * byte length.
60
+ */
61
+ /** One-character format version prefix. Bump on any wire-format change. */
62
+ export const TAINT_PATH_CODEC_VERSION = '1';
63
+ /**
64
+ * Absolute cap on the encoded reason's byte length (KTD6). 4096 comfortably
65
+ * holds ~100 hops of realistic identifiers — far beyond the default hop cap
66
+ * (32) — while bounding the persisted column even at `maxHops: 0` (unlimited).
67
+ */
68
+ export const TAINT_REASON_MAX_BYTES = 4096;
69
+ /** The truncation-marker segment content (rides as a trailing `|~`). */
70
+ export const TAINT_PATH_TRUNCATION_MARKER = '~';
71
+ /**
72
+ * Safe hop-name charset: ASCII identifier characters plus `#` (JS private
73
+ * names / the propagation engine's `#<idx>` fallback). Deliberately ASCII-only
74
+ * — a Unicode identifier is VALID JS but is skipped (truncation marker) rather
75
+ * than risking a `sanitizeUTF8` byte change breaking decode (defensive
76
+ * simplification; documented FN on path completeness, never on the finding).
77
+ */
78
+ const SAFE_NAME = /^[A-Za-z0-9_$#]+$/;
79
+ /** Decoder-side flags charset — `c` defined, the rest reserved (see module doc). */
80
+ const FLAGS = /^[a-z]*$/;
81
+ /**
82
+ * Kind-header charset: lowercase + digits + hyphen — covers every `SinkKind`
83
+ * label and stays disjoint from the structural delimiters (`;|:~`).
84
+ */
85
+ const SAFE_KIND = /^[a-z0-9-]+$/;
86
+ /**
87
+ * Encode an ordered hop list into the versioned `reason` wire string.
88
+ * Deterministic; never throws. See the module doc for the format and the
89
+ * three truncation triggers (caller flag, unencodable hop, byte cap).
90
+ */
91
+ export function encodeTaintPath(hops, options) {
92
+ // Kind header (defensively validated — see EncodeTaintPathOptions.kind).
93
+ const kindHeader = typeof options?.kind === 'string' && SAFE_KIND.test(options.kind) ? `;${options.kind}` : '';
94
+ // Floor: version char + kind header + room for the marker — a smaller cap
95
+ // could not hold even the empty truncated path. The header is identity
96
+ // material (finding classification), so it is never sacrificed to the byte
97
+ // cap; trailing hops are.
98
+ const maxBytes = Math.max(options?.maxBytes ?? TAINT_REASON_MAX_BYTES, TAINT_PATH_CODEC_VERSION.length + kindHeader.length + 2);
99
+ let truncated = options?.truncated === true;
100
+ const segments = [];
101
+ let total = TAINT_PATH_CODEC_VERSION.length + kindHeader.length;
102
+ for (const hop of hops) {
103
+ if (typeof hop.name !== 'string' ||
104
+ !SAFE_NAME.test(hop.name) ||
105
+ !Number.isInteger(hop.line) ||
106
+ hop.line < 0) {
107
+ // Unencodable hop: drop it AND everything after it so the emitted hops
108
+ // stay a faithful source-side prefix (a silent mid-path gap would lie).
109
+ truncated = true;
110
+ break;
111
+ }
112
+ const segment = `|${hop.name}:${hop.line}${hop.viaCall === true ? ':c' : ''}`;
113
+ if (total + segment.length > maxBytes) {
114
+ truncated = true;
115
+ break;
116
+ }
117
+ segments.push(segment);
118
+ total += segment.length;
119
+ }
120
+ if (truncated) {
121
+ // Make room for the trailing `|~` marker (drop trailing hops as needed).
122
+ while (segments.length > 0 && total + 2 > maxBytes) {
123
+ total -= segments.pop().length;
124
+ }
125
+ }
126
+ const reason = TAINT_PATH_CODEC_VERSION +
127
+ kindHeader +
128
+ segments.join('') +
129
+ (truncated ? `|${TAINT_PATH_TRUNCATION_MARKER}` : '');
130
+ return { reason, truncated };
131
+ }
132
+ /**
133
+ * Decode a `reason` wire string. Returns a typed failure for anything that is
134
+ * not a well-formed version-`1` path — never throws. A truncated path decodes
135
+ * `ok: true` with `truncated: true` ("path incomplete", per KTD6).
136
+ */
137
+ export function decodeTaintPath(reason) {
138
+ if (typeof reason !== 'string' || reason.length === 0) {
139
+ return { ok: false, error: 'empty or non-string reason' };
140
+ }
141
+ const version = reason[0];
142
+ if (version !== TAINT_PATH_CODEC_VERSION) {
143
+ return { ok: false, error: `unsupported taint-path version '${version}'` };
144
+ }
145
+ let body = reason.slice(1);
146
+ // Optional `;<kind>` header segment (finding sink category — see module doc).
147
+ let kind;
148
+ if (body.startsWith(';')) {
149
+ const headerEnd = body.indexOf('|');
150
+ kind = headerEnd === -1 ? body.slice(1) : body.slice(1, headerEnd);
151
+ if (!SAFE_KIND.test(kind)) {
152
+ return { ok: false, error: `invalid kind header '${kind}'` };
153
+ }
154
+ body = headerEnd === -1 ? '' : body.slice(headerEnd);
155
+ }
156
+ const hops = [];
157
+ if (body.length === 0)
158
+ return { ok: true, version, ...(kind ? { kind } : {}), hops, truncated: false };
159
+ if (!body.startsWith('|')) {
160
+ return { ok: false, error: 'malformed body: expected a hop separator after the version' };
161
+ }
162
+ const parts = body.slice(1).split('|');
163
+ let truncated = false;
164
+ for (let i = 0; i < parts.length; i++) {
165
+ const part = parts[i];
166
+ if (part === TAINT_PATH_TRUNCATION_MARKER) {
167
+ if (i !== parts.length - 1) {
168
+ return { ok: false, error: 'truncation marker not in trailing position' };
169
+ }
170
+ truncated = true;
171
+ break;
172
+ }
173
+ const fields = part.split(':');
174
+ if (fields.length < 2 || fields.length > 3) {
175
+ return { ok: false, error: `malformed hop segment '${part}'` };
176
+ }
177
+ const [name, lineStr, flags = ''] = fields;
178
+ if (!SAFE_NAME.test(name)) {
179
+ return { ok: false, error: `invalid hop variable '${name}'` };
180
+ }
181
+ if (!/^\d+$/.test(lineStr)) {
182
+ return { ok: false, error: `invalid hop line '${lineStr}'` };
183
+ }
184
+ if (!FLAGS.test(flags)) {
185
+ return { ok: false, error: `invalid hop flags '${flags}'` };
186
+ }
187
+ hops.push({ variable: name, line: Number(lineStr), viaCall: flags.includes('c') });
188
+ }
189
+ return { ok: true, version, ...(kind ? { kind } : {}), hops, truncated };
190
+ }
@@ -0,0 +1,216 @@
1
+ /**
2
+ * Pure intra-procedural taint propagation engine (#2083 M3 U3).
3
+ *
4
+ * Forward taint reachability over one function's reaching-definition facts
5
+ * (M2 `computeReachingDefs`) and matched taint sites (U2 `matchFunctionSites`)
6
+ * — sources in, findings + sanitizer kills + coverage status out. PURE AND
7
+ * DETERMINISTIC, mirroring the reaching-defs contract: no graph, no I/O, no
8
+ * logger; insertion-ordered worklist; explicitly sorted outputs; snapshot
9
+ * tests and content-derived edge ids (U4) rely on it.
10
+ *
11
+ * PRECONDITIONS: the caller gates the CFG through `hasTaintSafeSites`
12
+ * (taint/site-safety.ts) and the emit-safety checks before calling — this
13
+ * module dereferences binding/site/statement indices without re-validating.
14
+ *
15
+ * ## The two-rule model (plan HTD)
16
+ *
17
+ * - **Rule (b), statement-local:** a matched SOURCE occurrence (member read)
18
+ * whose intra-statement occurrence path — the member-read's `parent` chain —
19
+ * reaches a matched SINK argument position produces an immediate single-hop
20
+ * finding (`exec(req.body)`). The same statement SEEDS taint: every binding
21
+ * the statement defines becomes tainted (see the precision floor below).
22
+ * - **Rule (a), worklist:** for each tainted `(binding, defPoint)`, every
23
+ * def→use fact delivers the taint to a use statement, where occurrences of
24
+ * the binding in matched sink argument positions produce findings and the
25
+ * statement's own defs are tainted onward. The fact graph contains genuine
26
+ * cycles (loop back-edges, same-statement self-facts) — the visited-set
27
+ * discipline below is load-bearing, not defensive.
28
+ *
29
+ * ## Sanitizer semantics — the KIND-SET exclusion model (KTD4, sharpened)
30
+ *
31
+ * The plan sketches a binary kill; this module implements the strictly more
32
+ * precise SOUND refinement: a taint carries a set of *excluded* (neutralized)
33
+ * `SinkKind`s accumulated through sanitizer hops, and a sink fires unless its
34
+ * kind is in the taint's exclusion set. A binary kill is the special case
35
+ * where the sanitizer neutralizes the sink's kind; the kind-set model
36
+ * additionally keeps `const b = escape(req.body); db.query(b)` a FINDING
37
+ * (an HTML escaper does not neutralize SQL — un-tainting `b` outright would
38
+ * be a suppressed live injection, the forbidden false-negative direction)
39
+ * while still suppressing `res.send(b)` (xss IS neutralized).
40
+ *
41
+ * - **Occurrence interposition (KTD4a):** evaluated over the U1 site
42
+ * structure. An occurrence reaching a sink arg / def-feeding position
43
+ * through a matched sanitizer site accumulates that sanitizer's
44
+ * `neutralizes` kinds on that PATH; a direct occurrence contributes the
45
+ * empty set. Per-position narrowing (`entry.args`) is respected; receiver
46
+ * flow through a sanitizer is NOT neutralized (the receiver is not the
47
+ * sanitized payload), and spread/template positions are never neutralized
48
+ * (position unprovable) — both sound-direction choices (under-kill).
49
+ * - **Intersection over paths:** a def fed by several occurrence paths
50
+ * excludes a kind only when EVERY path neutralizes it
51
+ * (`const c = cond ? escape(b) : b` taints `c` with NO exclusions — the
52
+ * direct arm's ∅ intersects everything away). Equally, a taint re-derived
53
+ * along a second route keeps the INTERSECTION of the exclusion sets and is
54
+ * re-processed whenever the set SHRINKS — a less-neutralized taint is
55
+ * strictly more dangerous. Exclusion sets only shrink over a finite
56
+ * lattice, so the worklist terminates.
57
+ * - **Kill locality (KTD4b):** a kill applies to the def the sanitizer
58
+ * produces (`SiteRecord.resultDefs`) only; the flowing binding's own taint
59
+ * is untouched (`const c = escape(b); exec(b)` still finds `b`'s flow).
60
+ * `x = escape(x)` works because taint keys on the DEF POINT: the
61
+ * sanitizer statement's def enters the set with the sanitizer's kinds
62
+ * excluded, while the seed def keeps flowing wherever the CFG still
63
+ * carries it (zero-iteration loops, conditional sanitizers — may-path
64
+ * mechanics need no special handling here, kills are absent from facts).
65
+ *
66
+ * ## Statement-coalescing precision floor (documented FP)
67
+ *
68
+ * Statement facts conflate multi-declarator statements: a statement that
69
+ * uses tainted `b` and defines `c` taints `c` with NO exclusions even when
70
+ * the two are textually unrelated (`const a = clean(z), b = g(t)` floor-
71
+ * taints `a` from `t` — pinned by a test). The per-declarator `resultDefs`
72
+ * precision narrows the EXCLUSION computation (and powers kills) only — a
73
+ * def in a call's `resultDefs` is fed exactly through that call, so its
74
+ * exclusions come from the paths into it; when the tainted input provably
75
+ * never flows into that call, the floor still taints the def (sound) but
76
+ * records no kill (a kill requires evidence of flow through the sanitizer).
77
+ *
78
+ * ## Propagate-through (KTD5)
79
+ *
80
+ * Taint in any argument or in the receiver of an UNMODELED call flows to
81
+ * the call's result defs, marked `viaCall` on the hop so `explain` can
82
+ * express lower confidence. An occurrence that reaches the unmodeled call
83
+ * only through a sanitizer carries the neutralization through
84
+ * (`const y = unknownFn(escape(b))` excludes the sanitizer's kinds — the
85
+ * plan's deliberate precision choice over flat-conservative).
86
+ *
87
+ * ## Kills output
88
+ *
89
+ * `kills` records every sanitizer that ACTUALLY neutralized kinds on a
90
+ * flowing taint — U4 emits `SANITIZES` edges from them. Two shapes share the
91
+ * record: result-def kills (`killedDef` = the def the sanitizer produces;
92
+ * `bindingIdx` = that def's binding) and value-position interposition kills
93
+ * (`exec(escape(x))` — no def exists; `killedDef` = the sink statement's own
94
+ * point, `bindingIdx` = the interposed binding). Interposition kills are
95
+ * recorded only when the (input, sink, position) produced no finding — a
96
+ * bypassed sanitizer (`exec(x + escape(x))`) killed nothing.
97
+ */
98
+ import type { FunctionCfg } from '../cfg/types.js';
99
+ import type { FunctionDefUse, ProgramPoint } from '../cfg/reaching-defs.js';
100
+ import type { FunctionSiteMatches } from './match.js';
101
+ import type { SinkKind, SourceKind } from './source-sink-config.js';
102
+ /**
103
+ * Default per-function findings cap (U5 config resolution; cfg/emit.ts
104
+ * DEFAULT_* pattern). Resolved into the RepoMeta `pdg` stamp by
105
+ * `resolvePdgConfig` so a cap change trips full writeback; `0` = unlimited
106
+ * is preserved like the other pdg caps. 200 is generous — a real function
107
+ * with more deduped source→sink findings is a fixture or a disaster, and
108
+ * the truncation is deterministic + counted (`droppedFindings`).
109
+ */
110
+ export declare const DEFAULT_PDG_MAX_TAINT_FINDINGS_PER_FUNCTION = 200;
111
+ /**
112
+ * Default per-finding hop cap (U5; joins the RepoMeta `pdg` stamp like the
113
+ * findings cap). Bounds the persisted `reason` hop encoding (KTD6 pins the
114
+ * hop cap in config); 32 intra-procedural def→use hops is far beyond any
115
+ * legible path — overflow keeps the source-side prefix and sets
116
+ * `hopsTruncated`, parsed downstream as "path incomplete", never an error.
117
+ */
118
+ export declare const DEFAULT_PDG_MAX_TAINT_HOPS = 32;
119
+ export interface TaintLimits {
120
+ /**
121
+ * Maximum findings per function AFTER dedup; the sorted finding list is
122
+ * truncated deterministically and the overflow counted in
123
+ * `droppedFindings`. `undefined`/0 ⇒ unlimited.
124
+ */
125
+ readonly maxFindingsPerFunction?: number;
126
+ /**
127
+ * Maximum hops retained per finding (source-side prefix kept); overflow
128
+ * sets `hopsTruncated`. `undefined`/0 ⇒ unlimited.
129
+ */
130
+ readonly maxHops?: number;
131
+ }
132
+ /** One hop of a finding's path — enough for U4's reason codec (name, line, flag). */
133
+ export interface TaintHop {
134
+ /** Index into the function's binding table. */
135
+ readonly bindingIdx: number;
136
+ /** Resolved binding name (carried so U4 never re-joins the table). */
137
+ readonly name: string;
138
+ readonly point: ProgramPoint;
139
+ /** The value passed through an unmodeled call to get here (KTD5). */
140
+ readonly viaCall?: boolean;
141
+ }
142
+ /**
143
+ * The KTD6 rule-(b) source identity material: the matched member-read
144
+ * occurrence itself — statement point + site index + object/property. For
145
+ * worklist findings this is the ROOT source the taint chain was seeded from.
146
+ */
147
+ export interface TaintSourceOccurrence {
148
+ readonly point: ProgramPoint;
149
+ /** Index into the source statement's `sites` array. */
150
+ readonly siteIndex: number;
151
+ readonly objectBindingIdx: number;
152
+ readonly property: string;
153
+ readonly kind: SourceKind;
154
+ }
155
+ /** The sink side of a finding's identity: point + site + argument + binding. */
156
+ export interface TaintSinkOccurrence {
157
+ readonly point: ProgramPoint;
158
+ /** Index into the sink statement's `sites` array. */
159
+ readonly siteIndex: number;
160
+ /** Matched sink argument position the tainted occurrence landed in. */
161
+ readonly argIndex: number;
162
+ /**
163
+ * The binding whose occurrence reached the sink position (for rule-(b)
164
+ * findings: the source member-read's object binding).
165
+ */
166
+ readonly bindingIdx: number;
167
+ /** The matched sink entry's `name` (e.g. `exec`) — finding classification. */
168
+ readonly entryName: string;
169
+ }
170
+ export interface TaintFinding {
171
+ readonly sinkKind: SinkKind;
172
+ readonly source: TaintSourceOccurrence;
173
+ readonly sink: TaintSinkOccurrence;
174
+ /**
175
+ * Ordered source→sink path, one path per finding (the CodeQL
176
+ * `--max-paths=1` convention): the taint chain's def hops followed by the
177
+ * sink-use hop. Rule-(b) findings carry the single sink-statement hop.
178
+ */
179
+ readonly hops: readonly TaintHop[];
180
+ readonly hopsTruncated?: boolean;
181
+ }
182
+ /** A sanitizer that neutralized kinds on a flowing taint — U4's SANITIZES rows. */
183
+ export interface SanitizerKill {
184
+ /** Statement point of the sanitizer call site. */
185
+ readonly sanitizer: ProgramPoint;
186
+ /**
187
+ * The killed def's point (result-def kills — always the sanitizer's own
188
+ * statement in the intra-statement model) or the suppressed sink
189
+ * statement's point (value-position interposition kills).
190
+ */
191
+ readonly killedDef: ProgramPoint;
192
+ /** The killed def's binding, or the interposed binding for value-position kills. */
193
+ readonly bindingIdx: number;
194
+ /** Sorted, deduped kinds the sanitizer neutralized at that position. */
195
+ readonly neutralized: readonly SinkKind[];
196
+ }
197
+ export interface FunctionTaintResult {
198
+ /**
199
+ * `computed` — full propagation ran.
200
+ * `coverage-gap` — the solver result was not `computed`; the function is
201
+ * skipped for findings entirely (R4: never partially
202
+ * analyzed), `gapReason` carries the solver status.
203
+ */
204
+ readonly status: 'computed' | 'coverage-gap';
205
+ readonly gapReason?: 'truncated' | 'overflow' | 'no-facts';
206
+ /** Deduped (KTD6 identity), deterministically sorted, capped. */
207
+ readonly findings: readonly TaintFinding[];
208
+ readonly kills: readonly SanitizerKill[];
209
+ /** Findings dropped by `maxFindingsPerFunction` (post-dedup). */
210
+ readonly droppedFindings: number;
211
+ }
212
+ /**
213
+ * Compute taint flows for one function. See the module doc for the two-rule
214
+ * model, the kind-set exclusion semantics, and the precision floor.
215
+ */
216
+ export declare function computeTaintFlows(cfg: FunctionCfg, defUse: FunctionDefUse, matches: FunctionSiteMatches, limits?: TaintLimits): FunctionTaintResult;