@securecode-ai/mcp 0.5.5 → 0.5.7

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 (186) hide show
  1. package/README.md +52 -12
  2. package/dist/api/types.d.ts +7 -0
  3. package/dist/approval/auditLog.d.ts +1 -1
  4. package/dist/approval/auditLog.js +11 -3
  5. package/dist/approval/auditLog.js.map +1 -1
  6. package/dist/approval/broker.d.ts +7 -2
  7. package/dist/approval/broker.js +239 -110
  8. package/dist/approval/broker.js.map +1 -1
  9. package/dist/approval/policy.d.ts +10 -0
  10. package/dist/approval/policy.js +104 -0
  11. package/dist/approval/policy.js.map +1 -0
  12. package/dist/approval/types.d.ts +11 -2
  13. package/dist/approval/types.js +10 -1
  14. package/dist/approval/types.js.map +1 -1
  15. package/dist/attack/agentScanExecutor.d.ts +35 -1
  16. package/dist/attack/agentScanExecutor.js +405 -76
  17. package/dist/attack/agentScanExecutor.js.map +1 -1
  18. package/dist/attack/agentScanLoop.d.ts +20 -0
  19. package/dist/attack/agentScanLoop.js +1264 -60
  20. package/dist/attack/agentScanLoop.js.map +1 -1
  21. package/dist/attack/agentScanProtocol.d.ts +382 -6
  22. package/dist/attack/agentScanProtocol.js +110 -4
  23. package/dist/attack/agentScanProtocol.js.map +1 -1
  24. package/dist/attack/agentTrace.d.ts +73 -0
  25. package/dist/attack/agentTrace.js +228 -0
  26. package/dist/attack/agentTrace.js.map +1 -0
  27. package/dist/attack/architectureScoutExecutor.d.ts +18 -0
  28. package/dist/attack/architectureScoutExecutor.js +211 -0
  29. package/dist/attack/architectureScoutExecutor.js.map +1 -0
  30. package/dist/attack/architectureScoutLoop.d.ts +36 -0
  31. package/dist/attack/architectureScoutLoop.js +403 -0
  32. package/dist/attack/architectureScoutLoop.js.map +1 -0
  33. package/dist/attack/architectureScoutProtocol.d.ts +209 -0
  34. package/dist/attack/architectureScoutProtocol.js +47 -0
  35. package/dist/attack/architectureScoutProtocol.js.map +1 -0
  36. package/dist/attack/candidateStore.d.ts +95 -0
  37. package/dist/attack/candidateStore.js +231 -0
  38. package/dist/attack/candidateStore.js.map +1 -0
  39. package/dist/attack/evidenceLedger.d.ts +67 -0
  40. package/dist/attack/evidenceLedger.js +192 -0
  41. package/dist/attack/evidenceLedger.js.map +1 -0
  42. package/dist/attack/finishGate.d.ts +62 -0
  43. package/dist/attack/finishGate.js +209 -0
  44. package/dist/attack/finishGate.js.map +1 -0
  45. package/dist/attack/fixCodeMerge.d.ts +27 -0
  46. package/dist/attack/fixCodeMerge.js +42 -0
  47. package/dist/attack/fixCodeMerge.js.map +1 -0
  48. package/dist/attack/fixVerifyLoop.d.ts +55 -0
  49. package/dist/attack/fixVerifyLoop.js +187 -0
  50. package/dist/attack/fixVerifyLoop.js.map +1 -0
  51. package/dist/attack/investigationProfiles.d.ts +36 -0
  52. package/dist/attack/investigationProfiles.js +144 -0
  53. package/dist/attack/investigationProfiles.js.map +1 -0
  54. package/dist/attack/investigationState.d.ts +227 -0
  55. package/dist/attack/investigationState.js +666 -0
  56. package/dist/attack/investigationState.js.map +1 -0
  57. package/dist/attack/mutationOperators.d.ts +22 -0
  58. package/dist/attack/mutationOperators.js +170 -0
  59. package/dist/attack/mutationOperators.js.map +1 -0
  60. package/dist/attack/mutationTest.d.ts +33 -0
  61. package/dist/attack/mutationTest.js +110 -0
  62. package/dist/attack/mutationTest.js.map +1 -0
  63. package/dist/attack/proofGate.d.ts +20 -0
  64. package/dist/attack/proofGate.js +104 -0
  65. package/dist/attack/proofGate.js.map +1 -0
  66. package/dist/attack/proofTypes.d.ts +57 -0
  67. package/dist/attack/proofTypes.js +32 -0
  68. package/dist/attack/proofTypes.js.map +1 -0
  69. package/dist/attack/protocolValidator.d.ts +50 -0
  70. package/dist/attack/protocolValidator.js +427 -0
  71. package/dist/attack/protocolValidator.js.map +1 -0
  72. package/dist/attack/qualityMetrics.d.ts +133 -0
  73. package/dist/attack/qualityMetrics.js +226 -0
  74. package/dist/attack/qualityMetrics.js.map +1 -0
  75. package/dist/attack/scanScheduler.d.ts +52 -0
  76. package/dist/attack/scanScheduler.js +265 -0
  77. package/dist/attack/scanScheduler.js.map +1 -0
  78. package/dist/attack/scanState.d.ts +58 -0
  79. package/dist/attack/scanState.js +102 -0
  80. package/dist/attack/scanState.js.map +1 -0
  81. package/dist/attack/searchIntent.d.ts +16 -0
  82. package/dist/attack/searchIntent.js +57 -0
  83. package/dist/attack/searchIntent.js.map +1 -0
  84. package/dist/attack/verifyLoop.d.ts +43 -0
  85. package/dist/attack/verifyLoop.js +314 -14
  86. package/dist/attack/verifyLoop.js.map +1 -1
  87. package/dist/attack/workItem.d.ts +57 -0
  88. package/dist/attack/workItem.js +225 -0
  89. package/dist/attack/workItem.js.map +1 -0
  90. package/dist/audit/findingReviewQueue.d.ts +109 -0
  91. package/dist/audit/findingReviewQueue.js +335 -0
  92. package/dist/audit/findingReviewQueue.js.map +1 -0
  93. package/dist/audit/scanAuditLog.d.ts +160 -0
  94. package/dist/audit/scanAuditLog.js +404 -0
  95. package/dist/audit/scanAuditLog.js.map +1 -0
  96. package/dist/dependency/dependencyChecker.js +35 -9
  97. package/dist/dependency/dependencyChecker.js.map +1 -1
  98. package/dist/dependency/exploitPriority.d.ts +33 -0
  99. package/dist/dependency/exploitPriority.js +78 -0
  100. package/dist/dependency/exploitPriority.js.map +1 -0
  101. package/dist/dependency/finding.d.ts +16 -0
  102. package/dist/dependency/osvClient.js +2 -0
  103. package/dist/dependency/osvClient.js.map +1 -1
  104. package/dist/dependency/types.d.ts +12 -1
  105. package/dist/mcp/server.js +13 -2
  106. package/dist/mcp/server.js.map +1 -1
  107. package/dist/mcp/tools.d.ts +1 -0
  108. package/dist/mcp/tools.js +127 -6
  109. package/dist/mcp/tools.js.map +1 -1
  110. package/dist/project-map/agentMemory.d.ts +83 -1
  111. package/dist/project-map/agentMemory.js +197 -7
  112. package/dist/project-map/agentMemory.js.map +1 -1
  113. package/dist/project-map/architectureContext.d.ts +202 -0
  114. package/dist/project-map/architectureContext.js +421 -0
  115. package/dist/project-map/architectureContext.js.map +1 -0
  116. package/dist/project-map/blastRadius.d.ts +54 -0
  117. package/dist/project-map/blastRadius.js +196 -0
  118. package/dist/project-map/blastRadius.js.map +1 -0
  119. package/dist/project-map/callGraphExtractor.d.ts +13 -0
  120. package/dist/project-map/callGraphExtractor.js +222 -0
  121. package/dist/project-map/callGraphExtractor.js.map +1 -0
  122. package/dist/project-map/capabilityRegistry.d.ts +46 -0
  123. package/dist/project-map/capabilityRegistry.js +91 -0
  124. package/dist/project-map/capabilityRegistry.js.map +1 -0
  125. package/dist/project-map/findTests.d.ts +29 -0
  126. package/dist/project-map/findTests.js +236 -0
  127. package/dist/project-map/findTests.js.map +1 -0
  128. package/dist/project-map/handlerInventory.d.ts +112 -0
  129. package/dist/project-map/handlerInventory.js +184 -0
  130. package/dist/project-map/handlerInventory.js.map +1 -0
  131. package/dist/project-map/implementationResolver.d.ts +43 -0
  132. package/dist/project-map/implementationResolver.js +217 -0
  133. package/dist/project-map/implementationResolver.js.map +1 -0
  134. package/dist/project-map/mapContext.d.ts +6 -1
  135. package/dist/project-map/mapContext.js +1 -0
  136. package/dist/project-map/mapContext.js.map +1 -1
  137. package/dist/project-map/scanCache.d.ts +57 -3
  138. package/dist/project-map/scanCache.js +61 -2
  139. package/dist/project-map/scanCache.js.map +1 -1
  140. package/dist/project-map/symbolIndex.d.ts +39 -0
  141. package/dist/project-map/symbolIndex.js +385 -0
  142. package/dist/project-map/symbolIndex.js.map +1 -0
  143. package/dist/tooling/agentEvalScoring.d.ts +77 -0
  144. package/dist/tooling/agentEvalScoring.js +140 -0
  145. package/dist/tooling/agentEvalScoring.js.map +1 -0
  146. package/dist/tooling/agentRegression.d.ts +53 -0
  147. package/dist/tooling/agentRegression.js +99 -0
  148. package/dist/tooling/agentRegression.js.map +1 -0
  149. package/dist/tools/agentScan.d.ts +69 -0
  150. package/dist/tools/agentScan.js +702 -43
  151. package/dist/tools/agentScan.js.map +1 -1
  152. package/dist/tools/findingReviewTools.d.ts +22 -0
  153. package/dist/tools/findingReviewTools.js +121 -0
  154. package/dist/tools/findingReviewTools.js.map +1 -0
  155. package/dist/tools/fix.js +1 -1
  156. package/dist/tools/fix.js.map +1 -1
  157. package/dist/tools/map.js +191 -1
  158. package/dist/tools/map.js.map +1 -1
  159. package/dist/tools/runTests.d.ts +2 -0
  160. package/dist/tools/runTests.js +29 -0
  161. package/dist/tools/runTests.js.map +1 -0
  162. package/dist/utils/effectMock.d.ts +1 -0
  163. package/dist/utils/effectMock.js +162 -0
  164. package/dist/utils/effectMock.js.map +1 -0
  165. package/dist/utils/gitContext.d.ts +26 -0
  166. package/dist/utils/gitContext.js +317 -0
  167. package/dist/utils/gitContext.js.map +1 -0
  168. package/dist/utils/localTestRunner.d.ts +57 -2
  169. package/dist/utils/localTestRunner.js +122 -115
  170. package/dist/utils/localTestRunner.js.map +1 -1
  171. package/dist/utils/securityConfig.d.ts +17 -0
  172. package/dist/utils/securityConfig.js +192 -0
  173. package/dist/utils/securityConfig.js.map +1 -0
  174. package/dist/utils/testCommandPolicy.d.ts +36 -0
  175. package/dist/utils/testCommandPolicy.js +252 -0
  176. package/dist/utils/testCommandPolicy.js.map +1 -0
  177. package/dist/utils/testRunner.d.ts +56 -0
  178. package/dist/utils/testRunner.js +246 -0
  179. package/dist/utils/testRunner.js.map +1 -0
  180. package/dist/utils/testSafety.d.ts +40 -2
  181. package/dist/utils/testSafety.js +205 -26
  182. package/dist/utils/testSafety.js.map +1 -1
  183. package/dist/utils/verificationSandbox.d.ts +113 -0
  184. package/dist/utils/verificationSandbox.js +656 -0
  185. package/dist/utils/verificationSandbox.js.map +1 -0
  186. package/package.json +1 -1
@@ -0,0 +1,666 @@
1
+ "use strict";
2
+ /**
3
+ * Structured investigation state — tracks what the agent has already
4
+ * investigated so it doesn't waste steps re-reading the same code and
5
+ * can see which required checks remain incomplete.
6
+ *
7
+ * The Synara scan exposed two problems this module addresses:
8
+ *
9
+ * 1. The agent read `http.ts` 12 times with different line ranges, many
10
+ * overlapping. The exact-range dedup blocked identical ranges but
11
+ * not overlapping ones. This module tracks merged coverage so a read
12
+ * that substantially overlaps an already-read range is caught.
13
+ *
14
+ * 2. The agent called `finish` without completing the required
15
+ * investigation protocol (configuration inspection, threat-model
16
+ * establishment, cross-file ownership tracing). The checklist makes
17
+ * the missing steps visible to the agent AND to the controller.
18
+ */
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.InvestigationState = void 0;
21
+ const SECURITY_KEYWORD_WEIGHTS = [
22
+ ['authenticat', 30], ['authorize', 30], ['authorization', 30],
23
+ ['permission', 25], ['ownership', 25], ['owner', 20],
24
+ ['token', 25], ['session', 25], ['cookie', 20], ['credential', 25],
25
+ ['password', 30], ['secret', 25], ['apikey', 25], ['api_key', 25],
26
+ ['execfile', 35], ['exec(', 30], ['spawn(', 30], ['child_process', 35],
27
+ ['writefile', 30], ['write(', 25], ['unlink', 25], ['mkdir', 20],
28
+ ['path.', 20], ['resolve(', 15], ['join(', 15], ['traversal', 30],
29
+ ['sql', 25], ['query(', 20], ['rawquery', 30], ['queryraw', 30],
30
+ ['inject', 30], ['eval(', 30], ['function(', 10],
31
+ ['router.', 20], ['route(', 20], ['handler', 15], ['middleware', 15],
32
+ ['guard', 20], ['check(', 15], ['validate', 15], ['verify', 15],
33
+ ['login', 25], ['logout', 20], ['register', 20], ['signup', 20],
34
+ ['bootstrap', 25], ['pairing', 25], ['enroll', 20],
35
+ ['rate', 15], ['limit', 15], ['throttle', 15],
36
+ ['origin', 20], ['cors', 20], ['csrf', 25],
37
+ ['csp', 20], ['header', 10], ['helmet', 15],
38
+ ['websocket', 20], ['upgrade', 15], ['rpc', 20],
39
+ ['file', 10], ['read(', 10], ['open(', 10],
40
+ ['import', 5], ['export', 5], ['require(', 5],
41
+ ['config', 10], ['env', 10], ['process.env', 15],
42
+ ['crypto', 15], ['hash', 15], ['encrypt', 20], ['decrypt', 20],
43
+ ['sanitiz', 15], ['escape', 15], ['encode', 10], ['decode', 10],
44
+ ['error', 5], ['catch', 5], ['throw', 5], ['reject', 5],
45
+ ];
46
+ class InvestigationState {
47
+ fileCoverages = new Map();
48
+ checklist;
49
+ toolsUsed = new Set();
50
+ filesRead = new Set();
51
+ symbolsSearched = new Set();
52
+ rootCauses = new Map();
53
+ duplicateReadKeys = new Set();
54
+ tasks = new Map();
55
+ flowVerifications = [];
56
+ constructor() {
57
+ this.checklist = {
58
+ completed: new Set(),
59
+ required: new Set([
60
+ 'initial-read',
61
+ 'route-discovery',
62
+ 'policy-check',
63
+ 'auth-symbol-search',
64
+ 'cross-file-flow',
65
+ 'config-inspection',
66
+ 'all-handlers-reviewed',
67
+ 'candidates-verified',
68
+ ]),
69
+ requiredForFindingType: new Map(),
70
+ };
71
+ }
72
+ static normalizePath(p) {
73
+ return p.replace(/\\/g, '/').toLowerCase();
74
+ }
75
+ static parseRange(startLine, endLine, totalLines) {
76
+ const start = startLine ?? 1;
77
+ const end = endLine ?? totalLines ?? start;
78
+ return { start: Math.min(start, end), end: Math.max(start, end) };
79
+ }
80
+ static rangesOverlap(a, b) {
81
+ return a.start <= b.end && b.start <= a.end;
82
+ }
83
+ static mergeRanges(ranges) {
84
+ if (ranges.length <= 1)
85
+ return [...ranges];
86
+ const sorted = [...ranges].sort((a, b) => a.start - b.start);
87
+ const merged = [sorted[0]];
88
+ for (let i = 1; i < sorted.length; i++) {
89
+ const last = merged[merged.length - 1];
90
+ if (sorted[i].start <= last.end + 1) {
91
+ last.end = Math.max(last.end, sorted[i].end);
92
+ }
93
+ else {
94
+ merged.push(sorted[i]);
95
+ }
96
+ }
97
+ return merged;
98
+ }
99
+ static overlapFraction(a, b) {
100
+ if (!this.rangesOverlap(a, b))
101
+ return 0;
102
+ const overlapStart = Math.max(a.start, b.start);
103
+ const overlapEnd = Math.min(a.end, b.end);
104
+ const overlapLines = overlapEnd - overlapStart + 1;
105
+ const newLines = (b.end - b.start + 1) - overlapLines;
106
+ if (newLines <= 0)
107
+ return 1.0;
108
+ return overlapLines / (b.end - b.start + 1);
109
+ }
110
+ /**
111
+ * Check if a read would be blocked (duplicate or overlapping) WITHOUT
112
+ * recording it. This lets the loop decide before committing coverage.
113
+ */
114
+ checkRead(filePath, startLine, endLine, totalLines) {
115
+ const normalized = InvestigationState.normalizePath(filePath);
116
+ const range = InvestigationState.parseRange(startLine, endLine, totalLines);
117
+ const rangeKey = `${normalized}:${startLine || 0}:${endLine || 0}`;
118
+ const coverage = this.fileCoverages.get(normalized);
119
+ const isExactDuplicate = this.duplicateReadKeys.has(rangeKey);
120
+ let maxOverlap = 0;
121
+ if (coverage) {
122
+ for (const existing of coverage.ranges) {
123
+ const frac = InvestigationState.overlapFraction(existing, range);
124
+ if (frac > maxOverlap)
125
+ maxOverlap = frac;
126
+ }
127
+ }
128
+ return {
129
+ isExactDuplicate,
130
+ overlapping: maxOverlap > 0.5,
131
+ overlapFraction: maxOverlap,
132
+ coverageAfter: coverage ? [...coverage.ranges] : [],
133
+ };
134
+ }
135
+ classifyRead(filePath, startLine, endLine, totalLines) {
136
+ const normalized = InvestigationState.normalizePath(filePath);
137
+ const range = InvestigationState.parseRange(startLine, endLine, totalLines);
138
+ const rangeKey = `${normalized}:${startLine || 0}:${endLine || 0}`;
139
+ const coverage = this.fileCoverages.get(normalized);
140
+ if (startLine !== undefined && endLine !== undefined && startLine > endLine) {
141
+ return {
142
+ classification: 'invalid',
143
+ overlapFraction: 0,
144
+ newLines: 0,
145
+ coverageAfter: coverage ? [...coverage.ranges] : [],
146
+ nextUnreadRange: null,
147
+ };
148
+ }
149
+ if (this.duplicateReadKeys.has(rangeKey)) {
150
+ return {
151
+ classification: 'duplicate',
152
+ overlapFraction: 1,
153
+ newLines: 0,
154
+ coverageAfter: coverage ? [...coverage.ranges] : [],
155
+ nextUnreadRange: this.getNextUnreadRange(filePath),
156
+ };
157
+ }
158
+ let maxOverlap = 0;
159
+ if (coverage) {
160
+ for (const existing of coverage.ranges) {
161
+ const frac = InvestigationState.overlapFraction(existing, range);
162
+ if (frac > maxOverlap)
163
+ maxOverlap = frac;
164
+ }
165
+ }
166
+ const requestedLines = range.end - range.start + 1;
167
+ const overlapLines = Math.round(maxOverlap * requestedLines);
168
+ const newLines = requestedLines - overlapLines;
169
+ let classification;
170
+ if (maxOverlap === 0) {
171
+ classification = 'new-coverage';
172
+ }
173
+ else if (maxOverlap > 0.5) {
174
+ classification = 'high-overlap';
175
+ }
176
+ else {
177
+ classification = 'partial-new-coverage';
178
+ }
179
+ return {
180
+ classification,
181
+ overlapFraction: maxOverlap,
182
+ newLines,
183
+ coverageAfter: coverage ? [...coverage.ranges] : [],
184
+ nextUnreadRange: this.getNextUnreadRange(filePath),
185
+ };
186
+ }
187
+ /**
188
+ * Record a blocked read attempt — increments the blocked counter
189
+ * without merging ranges or counting it as a successful read.
190
+ */
191
+ recordBlockedRead(filePath) {
192
+ const normalized = InvestigationState.normalizePath(filePath);
193
+ let coverage = this.fileCoverages.get(normalized);
194
+ if (!coverage) {
195
+ coverage = {
196
+ filePath: normalized,
197
+ ranges: [],
198
+ totalLines: undefined,
199
+ readCount: 0,
200
+ blockedReadCount: 0,
201
+ };
202
+ this.fileCoverages.set(normalized, coverage);
203
+ }
204
+ coverage.blockedReadCount++;
205
+ }
206
+ recordRead(filePath, startLine, endLine, totalLines) {
207
+ const normalized = InvestigationState.normalizePath(filePath);
208
+ const range = InvestigationState.parseRange(startLine, endLine, totalLines);
209
+ let coverage = this.fileCoverages.get(normalized);
210
+ if (!coverage) {
211
+ coverage = {
212
+ filePath: normalized,
213
+ ranges: [],
214
+ totalLines,
215
+ readCount: 0,
216
+ blockedReadCount: 0,
217
+ };
218
+ this.fileCoverages.set(normalized, coverage);
219
+ }
220
+ coverage.readCount++;
221
+ if (totalLines)
222
+ coverage.totalLines = totalLines;
223
+ const overlap = InvestigationState.overlapFraction(range, { start: 1, end: totalLines ?? range.end });
224
+ let maxOverlap = 0;
225
+ for (const existing of coverage.ranges) {
226
+ const frac = InvestigationState.overlapFraction(existing, range);
227
+ if (frac > maxOverlap)
228
+ maxOverlap = frac;
229
+ }
230
+ coverage.ranges = InvestigationState.mergeRanges([...coverage.ranges, range]);
231
+ this.filesRead.add(normalized);
232
+ const rangeKey = `${normalized}:${startLine || 0}:${endLine || 0}`;
233
+ this.duplicateReadKeys.add(rangeKey);
234
+ this.markStepComplete('initial-read');
235
+ return {
236
+ overlapping: maxOverlap > 0.5,
237
+ overlapFraction: maxOverlap,
238
+ coverageAfter: [...coverage.ranges],
239
+ };
240
+ }
241
+ /**
242
+ * Record a read using the ACTUAL delivered range (not the requested range).
243
+ *
244
+ * Use this when the executor returns structured metadata telling you
245
+ * exactly which lines were delivered. A large-file read that returns a
246
+ * function map (truncated=true, actualStart=0, actualEnd=0) does NOT
247
+ * record any content coverage — the agent must still read specific
248
+ * line ranges.
249
+ */
250
+ recordActualRead(filePath, actualStart, actualEnd, totalLines, truncated) {
251
+ const normalized = InvestigationState.normalizePath(filePath);
252
+ let coverage = this.fileCoverages.get(normalized);
253
+ if (!coverage) {
254
+ coverage = {
255
+ filePath: normalized,
256
+ ranges: [],
257
+ totalLines,
258
+ readCount: 0,
259
+ blockedReadCount: 0,
260
+ };
261
+ this.fileCoverages.set(normalized, coverage);
262
+ }
263
+ coverage.readCount++;
264
+ if (totalLines)
265
+ coverage.totalLines = totalLines;
266
+ // Truncated reads (function map) deliver no content lines — don't
267
+ // record any range coverage and don't complete initial-read. The
268
+ // agent must still read specific sections with actual content.
269
+ if (truncated || actualStart === 0 || actualEnd === 0) {
270
+ this.filesRead.add(normalized);
271
+ return {
272
+ overlapping: false,
273
+ overlapFraction: 0,
274
+ coverageAfter: [...coverage.ranges],
275
+ };
276
+ }
277
+ // Reject inverted ranges (actualStart > actualEnd) — these can
278
+ // happen when startLine > totalLines. Don't record coverage.
279
+ if (actualStart > actualEnd) {
280
+ this.filesRead.add(normalized);
281
+ return {
282
+ overlapping: false,
283
+ overlapFraction: 0,
284
+ coverageAfter: [...coverage.ranges],
285
+ };
286
+ }
287
+ const range = { start: actualStart, end: actualEnd };
288
+ let maxOverlap = 0;
289
+ for (const existing of coverage.ranges) {
290
+ const frac = InvestigationState.overlapFraction(existing, range);
291
+ if (frac > maxOverlap)
292
+ maxOverlap = frac;
293
+ }
294
+ coverage.ranges = InvestigationState.mergeRanges([...coverage.ranges, range]);
295
+ this.filesRead.add(normalized);
296
+ const rangeKey = `${normalized}:${actualStart || 0}:${actualEnd || 0}`;
297
+ this.duplicateReadKeys.add(rangeKey);
298
+ this.markStepComplete('initial-read');
299
+ return {
300
+ overlapping: maxOverlap > 0.5,
301
+ overlapFraction: maxOverlap,
302
+ coverageAfter: [...coverage.ranges],
303
+ };
304
+ }
305
+ getCoverage(filePath) {
306
+ return this.fileCoverages.get(InvestigationState.normalizePath(filePath));
307
+ }
308
+ isCovered(filePath, startLine, endLine) {
309
+ const coverage = this.getCoverage(filePath);
310
+ if (!coverage)
311
+ return false;
312
+ if (startLine === undefined && endLine === undefined)
313
+ return coverage.ranges.length > 0;
314
+ const range = InvestigationState.parseRange(startLine, endLine, coverage.totalLines);
315
+ return coverage.ranges.some(r => r.start <= range.start && r.end >= range.end);
316
+ }
317
+ /**
318
+ * Return the gaps in coverage for a file — the line ranges that have NOT
319
+ * been read yet. Returns [] if the file is fully covered or not tracked.
320
+ */
321
+ getUncoveredRanges(filePath) {
322
+ const coverage = this.getCoverage(filePath);
323
+ if (!coverage || !coverage.totalLines)
324
+ return [];
325
+ if (coverage.ranges.length === 0) {
326
+ return [{ start: 1, end: coverage.totalLines }];
327
+ }
328
+ const gaps = [];
329
+ const sorted = [...coverage.ranges].sort((a, b) => a.start - b.start);
330
+ // Gap before the first range
331
+ if (sorted[0].start > 1) {
332
+ gaps.push({ start: 1, end: sorted[0].start - 1 });
333
+ }
334
+ // Gaps between ranges
335
+ for (let i = 0; i < sorted.length - 1; i++) {
336
+ const gapStart = sorted[i].end + 1;
337
+ const gapEnd = sorted[i + 1].start - 1;
338
+ if (gapStart <= gapEnd) {
339
+ gaps.push({ start: gapStart, end: gapEnd });
340
+ }
341
+ }
342
+ // Gap after the last range
343
+ const last = sorted[sorted.length - 1];
344
+ if (last.end < coverage.totalLines) {
345
+ gaps.push({ start: last.end + 1, end: coverage.totalLines });
346
+ }
347
+ return gaps;
348
+ }
349
+ /**
350
+ * Return the next unread chunk for a file, using a chunk-size policy
351
+ * based on file size:
352
+ * < 300 lines full-file read (chunk = totalLines)
353
+ * 300-1,000 lines 250-line chunks
354
+ * 1,000-5,000 300-line chunks
355
+ * > 5,000 lines 400-line chunks
356
+ *
357
+ * Returns null if the file is fully covered or not tracked.
358
+ */
359
+ getNextUnreadRange(filePath, chunkSize) {
360
+ const coverage = this.getCoverage(filePath);
361
+ if (!coverage || !coverage.totalLines)
362
+ return null;
363
+ const chunk = chunkSize ?? InvestigationState.chunkSizeForLines(coverage.totalLines);
364
+ const gaps = this.getUncoveredRanges(filePath);
365
+ if (gaps.length === 0)
366
+ return null;
367
+ const firstGap = gaps[0];
368
+ const end = Math.min(firstGap.start + chunk - 1, firstGap.end);
369
+ return { start: firstGap.start, end };
370
+ }
371
+ getPrioritizedUnreadRange(filePath, fileContent, chunkSize, candidateLocations, functionBoundaries) {
372
+ const coverage = this.getCoverage(filePath);
373
+ if (!coverage || !coverage.totalLines)
374
+ return null;
375
+ const chunk = chunkSize ?? InvestigationState.chunkSizeForLines(coverage.totalLines);
376
+ const gaps = this.getUncoveredRanges(filePath);
377
+ if (gaps.length === 0)
378
+ return null;
379
+ if (gaps.length === 1) {
380
+ const end = Math.min(gaps[0].start + chunk - 1, gaps[0].end);
381
+ return this.snapToFunctionBoundary(gaps[0].start, end, functionBoundaries);
382
+ }
383
+ const lines = fileContent.split('\n');
384
+ let bestGap = gaps[0];
385
+ let bestScore = -1;
386
+ for (const gap of gaps) {
387
+ let score = 0;
388
+ const gapLines = lines.slice(Math.max(0, gap.start - 1), Math.min(lines.length, gap.end));
389
+ const gapText = gapLines.join('\n').toLowerCase();
390
+ for (const [pattern, weight] of SECURITY_KEYWORD_WEIGHTS) {
391
+ if (gapText.includes(pattern)) {
392
+ score += weight;
393
+ }
394
+ }
395
+ if (candidateLocations) {
396
+ for (const loc of candidateLocations) {
397
+ if (loc.start >= gap.start && loc.start <= gap.end) {
398
+ score += 100;
399
+ }
400
+ if (loc.end >= gap.start && loc.end <= gap.end) {
401
+ score += 50;
402
+ }
403
+ }
404
+ }
405
+ if (functionBoundaries) {
406
+ for (const fn of functionBoundaries) {
407
+ if (fn.startLine >= gap.start && fn.startLine <= gap.end) {
408
+ score += 15;
409
+ }
410
+ if (fn.endLine >= gap.start && fn.endLine <= gap.end) {
411
+ score += 10;
412
+ }
413
+ }
414
+ }
415
+ score += Math.min(gap.end - gap.start, chunk) * 0.01;
416
+ if (score > bestScore) {
417
+ bestScore = score;
418
+ bestGap = gap;
419
+ }
420
+ }
421
+ const end = Math.min(bestGap.start + chunk - 1, bestGap.end);
422
+ return this.snapToFunctionBoundary(bestGap.start, end, functionBoundaries);
423
+ }
424
+ snapToFunctionBoundary(start, end, boundaries) {
425
+ if (!boundaries || boundaries.length === 0)
426
+ return { start, end };
427
+ let snappedStart = start;
428
+ let snappedEnd = end;
429
+ for (const fn of boundaries) {
430
+ if (fn.startLine >= start && fn.startLine <= end && fn.endLine > end) {
431
+ snappedEnd = Math.min(fn.endLine, end + 100);
432
+ }
433
+ if (fn.endLine >= start && fn.endLine <= end && fn.startLine < start) {
434
+ snappedStart = Math.max(fn.startLine, start - 20);
435
+ }
436
+ }
437
+ return { start: snappedStart, end: snappedEnd };
438
+ }
439
+ /**
440
+ * Return coverage percentage for a file (0-100). Returns 0 if the file
441
+ * is not tracked or has no totalLines.
442
+ */
443
+ getCoveragePercent(filePath) {
444
+ const coverage = this.getCoverage(filePath);
445
+ if (!coverage || !coverage.totalLines)
446
+ return 0;
447
+ const coveredLines = coverage.ranges.reduce((sum, r) => sum + (r.end - r.start + 1), 0);
448
+ return Math.round((coveredLines / coverage.totalLines) * 100);
449
+ }
450
+ static chunkSizeForLines(totalLines) {
451
+ if (totalLines < 300)
452
+ return totalLines;
453
+ if (totalLines < 1000)
454
+ return 250;
455
+ if (totalLines < 5000)
456
+ return 300;
457
+ return 400;
458
+ }
459
+ recordToolUse(toolType) {
460
+ this.toolsUsed.add(toolType);
461
+ switch (toolType) {
462
+ case 'check_policy':
463
+ this.markStepComplete('policy-check');
464
+ break;
465
+ case 'search_code':
466
+ if (this.checklist.completed.has('initial-read')) {
467
+ this.markStepComplete('auth-symbol-search');
468
+ }
469
+ break;
470
+ case 'trace_flow':
471
+ case 'trace_flow_cross_file':
472
+ this.markStepComplete('ownership-analysis');
473
+ break;
474
+ case 'read_config':
475
+ this.markStepComplete('config-inspection');
476
+ break;
477
+ case 'get_endpoints':
478
+ this.markStepComplete('route-discovery');
479
+ break;
480
+ case 'find_tests':
481
+ this.markStepComplete('tests-found');
482
+ break;
483
+ }
484
+ }
485
+ recordFlowVerification(filePath, tool, status, flowCount, reason, structured) {
486
+ this.flowVerifications.push({
487
+ filePath,
488
+ tool,
489
+ status,
490
+ flowCount,
491
+ reason,
492
+ timestamp: Date.now(),
493
+ riskId: structured?.riskId,
494
+ candidateId: structured?.candidateId,
495
+ source: structured?.source,
496
+ sink: structured?.sink,
497
+ hops: structured?.hops,
498
+ truncated: structured?.truncated,
499
+ error: structured?.error,
500
+ });
501
+ if (status === 'confirmed' || status === 'refuted') {
502
+ this.markStepComplete('cross-file-flow');
503
+ }
504
+ }
505
+ getFlowVerifications() {
506
+ return [...this.flowVerifications];
507
+ }
508
+ hasClassifiedFlow() {
509
+ return this.flowVerifications.some(v => v.status !== 'blocked');
510
+ }
511
+ hasResolvedFlow(riskId) {
512
+ if (riskId) {
513
+ return this.flowVerifications.some(v => v.riskId === riskId && (v.status === 'confirmed' || v.status === 'refuted'));
514
+ }
515
+ return this.flowVerifications.some(v => v.status === 'confirmed' || v.status === 'refuted');
516
+ }
517
+ recordSymbolSearch(symbol) {
518
+ this.symbolsSearched.add(symbol);
519
+ }
520
+ markStepComplete(step) {
521
+ this.checklist.completed.add(step);
522
+ }
523
+ markAllHandlersReviewed() {
524
+ this.markStepComplete('all-handlers-reviewed');
525
+ }
526
+ markCandidatesVerified() {
527
+ this.markStepComplete('candidates-verified');
528
+ }
529
+ markArchitectureRisksAddressed() {
530
+ this.markStepComplete('architecture-risks-addressed');
531
+ }
532
+ /**
533
+ * Replace the required checklist with a specific set of steps.
534
+ * Used by target-specific investigation profiles.
535
+ */
536
+ setRequiredSteps(steps) {
537
+ this.checklist.required = new Set(steps);
538
+ }
539
+ // ── Investigation task tracking ──────────────────────────────────────────
540
+ addInvestigationTasks(tasks) {
541
+ for (const task of tasks) {
542
+ if (!this.tasks.has(task.id)) {
543
+ this.tasks.set(task.id, { ...task });
544
+ }
545
+ }
546
+ }
547
+ updateTaskStatus(taskId, status) {
548
+ const task = this.tasks.get(taskId);
549
+ if (task) {
550
+ task.status = status;
551
+ if (this.tasks.size > 0 && [...this.tasks.values()].every(t => t.status === 'verified' || t.status === 'refuted')) {
552
+ this.markStepComplete('architecture-risks-addressed');
553
+ }
554
+ }
555
+ }
556
+ getPendingTasks() {
557
+ return [...this.tasks.values()].filter(t => t.status === 'pending');
558
+ }
559
+ getUnresolvedTasks() {
560
+ return [...this.tasks.values()].filter(t => t.status === 'pending' || t.status === 'blocked' || t.status === 'unproven');
561
+ }
562
+ getAllTasks() {
563
+ return [...this.tasks.values()];
564
+ }
565
+ getIncompleteSteps() {
566
+ const incomplete = [];
567
+ for (const required of this.checklist.required) {
568
+ if (!this.checklist.completed.has(required)) {
569
+ incomplete.push(required);
570
+ }
571
+ }
572
+ return incomplete;
573
+ }
574
+ getCompletedSteps() {
575
+ return [...this.checklist.completed];
576
+ }
577
+ getToolsUsed() {
578
+ return [...this.toolsUsed];
579
+ }
580
+ getFilesRead() {
581
+ return [...this.filesRead];
582
+ }
583
+ getReadCount(filePath) {
584
+ return this.getCoverage(filePath)?.readCount ?? 0;
585
+ }
586
+ getCoverageSummary(filePath) {
587
+ const coverage = this.getCoverage(filePath);
588
+ const totalLines = coverage?.totalLines ?? 0;
589
+ const coveredLines = (coverage?.ranges ?? []).reduce((sum, r) => sum + (r.end - r.start + 1), 0);
590
+ const uncovered = this.getUncoveredRanges(filePath);
591
+ const largestUncovered = uncovered.length > 0
592
+ ? uncovered.reduce((max, r) => (r.end - r.start) > (max.end - max.start) ? r : max)
593
+ : null;
594
+ return {
595
+ totalLines,
596
+ coveredLines,
597
+ coveragePercentage: totalLines > 0 ? Math.round(100 * coveredLines / totalLines) : 0,
598
+ uncoveredRangeCount: uncovered.length,
599
+ largestUncoveredRange: largestUncovered,
600
+ readCount: coverage?.readCount ?? 0,
601
+ blockedReadCount: coverage?.blockedReadCount ?? 0,
602
+ };
603
+ }
604
+ registerRootCause(rootCauseId, description) {
605
+ if (!this.rootCauses.has(rootCauseId)) {
606
+ this.rootCauses.set(rootCauseId, description);
607
+ }
608
+ }
609
+ getRootCauses() {
610
+ return new Map(this.rootCauses);
611
+ }
612
+ getRecommendedRecoveryAction() {
613
+ const incomplete = this.getIncompleteSteps();
614
+ if (incomplete.length === 0)
615
+ return null;
616
+ const step = incomplete[0];
617
+ switch (step) {
618
+ case 'route-discovery': return 'get_endpoints';
619
+ case 'policy-check': return 'check_policy';
620
+ case 'auth-symbol-search': return 'search_code';
621
+ case 'cross-file-flow': return 'trace_flow_cross_file';
622
+ case 'config-inspection': return 'read_config';
623
+ case 'ownership-analysis': return 'trace_flow_cross_file';
624
+ case 'tests-found': return 'find_tests';
625
+ default: return null;
626
+ }
627
+ }
628
+ formatChecklistForPrompt() {
629
+ const incomplete = this.getIncompleteSteps();
630
+ const completed = this.getCompletedSteps();
631
+ if (incomplete.length === 0) {
632
+ return `[Investigation checklist: ALL COMPLETE]`;
633
+ }
634
+ const lines = ['[Investigation checklist — incomplete steps block finish with 0 findings:]', ''];
635
+ for (const step of incomplete) {
636
+ const desc = this.stepDescription(step);
637
+ lines.push(` ✗ ${step}: ${desc}`);
638
+ }
639
+ if (completed.length > 0) {
640
+ lines.push('');
641
+ lines.push(' Completed:');
642
+ for (const step of completed) {
643
+ lines.push(` ✓ ${step}`);
644
+ }
645
+ }
646
+ return lines.join('\n');
647
+ }
648
+ stepDescription(step) {
649
+ switch (step) {
650
+ case 'initial-read': return 'Read the target file to understand the code';
651
+ case 'route-discovery': return 'Call get_endpoints to discover sibling routes';
652
+ case 'policy-check': return 'Call check_policy on route handler files';
653
+ case 'auth-symbol-search': return 'Search for auth functions used in the target';
654
+ case 'cross-file-flow': return 'Trace data flow with trace_flow or trace_flow_cross_file';
655
+ case 'config-inspection': return 'Inspect security configuration with read_config';
656
+ case 'ownership-analysis': return 'Trace ownership through repository/service layer';
657
+ case 'tests-found': return 'Find tests for the target code';
658
+ case 'architecture-risks-addressed': return 'Address architecture-scout risks for the target file';
659
+ case 'all-handlers-reviewed': return 'Review ALL handlers in the target file';
660
+ case 'candidates-verified': return 'Verify each candidate finding with tools';
661
+ default: return step;
662
+ }
663
+ }
664
+ }
665
+ exports.InvestigationState = InvestigationState;
666
+ //# sourceMappingURL=investigationState.js.map