@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.
- package/README.md +52 -12
- package/dist/api/types.d.ts +7 -0
- package/dist/approval/auditLog.d.ts +1 -1
- package/dist/approval/auditLog.js +11 -3
- package/dist/approval/auditLog.js.map +1 -1
- package/dist/approval/broker.d.ts +7 -2
- package/dist/approval/broker.js +239 -110
- package/dist/approval/broker.js.map +1 -1
- package/dist/approval/policy.d.ts +10 -0
- package/dist/approval/policy.js +104 -0
- package/dist/approval/policy.js.map +1 -0
- package/dist/approval/types.d.ts +11 -2
- package/dist/approval/types.js +10 -1
- package/dist/approval/types.js.map +1 -1
- package/dist/attack/agentScanExecutor.d.ts +35 -1
- package/dist/attack/agentScanExecutor.js +405 -76
- package/dist/attack/agentScanExecutor.js.map +1 -1
- package/dist/attack/agentScanLoop.d.ts +20 -0
- package/dist/attack/agentScanLoop.js +1264 -60
- package/dist/attack/agentScanLoop.js.map +1 -1
- package/dist/attack/agentScanProtocol.d.ts +382 -6
- package/dist/attack/agentScanProtocol.js +110 -4
- package/dist/attack/agentScanProtocol.js.map +1 -1
- package/dist/attack/agentTrace.d.ts +73 -0
- package/dist/attack/agentTrace.js +228 -0
- package/dist/attack/agentTrace.js.map +1 -0
- package/dist/attack/architectureScoutExecutor.d.ts +18 -0
- package/dist/attack/architectureScoutExecutor.js +211 -0
- package/dist/attack/architectureScoutExecutor.js.map +1 -0
- package/dist/attack/architectureScoutLoop.d.ts +36 -0
- package/dist/attack/architectureScoutLoop.js +403 -0
- package/dist/attack/architectureScoutLoop.js.map +1 -0
- package/dist/attack/architectureScoutProtocol.d.ts +209 -0
- package/dist/attack/architectureScoutProtocol.js +47 -0
- package/dist/attack/architectureScoutProtocol.js.map +1 -0
- package/dist/attack/candidateStore.d.ts +95 -0
- package/dist/attack/candidateStore.js +231 -0
- package/dist/attack/candidateStore.js.map +1 -0
- package/dist/attack/evidenceLedger.d.ts +67 -0
- package/dist/attack/evidenceLedger.js +192 -0
- package/dist/attack/evidenceLedger.js.map +1 -0
- package/dist/attack/finishGate.d.ts +62 -0
- package/dist/attack/finishGate.js +209 -0
- package/dist/attack/finishGate.js.map +1 -0
- package/dist/attack/fixCodeMerge.d.ts +27 -0
- package/dist/attack/fixCodeMerge.js +42 -0
- package/dist/attack/fixCodeMerge.js.map +1 -0
- package/dist/attack/fixVerifyLoop.d.ts +55 -0
- package/dist/attack/fixVerifyLoop.js +187 -0
- package/dist/attack/fixVerifyLoop.js.map +1 -0
- package/dist/attack/investigationProfiles.d.ts +36 -0
- package/dist/attack/investigationProfiles.js +144 -0
- package/dist/attack/investigationProfiles.js.map +1 -0
- package/dist/attack/investigationState.d.ts +227 -0
- package/dist/attack/investigationState.js +666 -0
- package/dist/attack/investigationState.js.map +1 -0
- package/dist/attack/mutationOperators.d.ts +22 -0
- package/dist/attack/mutationOperators.js +170 -0
- package/dist/attack/mutationOperators.js.map +1 -0
- package/dist/attack/mutationTest.d.ts +33 -0
- package/dist/attack/mutationTest.js +110 -0
- package/dist/attack/mutationTest.js.map +1 -0
- package/dist/attack/proofGate.d.ts +20 -0
- package/dist/attack/proofGate.js +104 -0
- package/dist/attack/proofGate.js.map +1 -0
- package/dist/attack/proofTypes.d.ts +57 -0
- package/dist/attack/proofTypes.js +32 -0
- package/dist/attack/proofTypes.js.map +1 -0
- package/dist/attack/protocolValidator.d.ts +50 -0
- package/dist/attack/protocolValidator.js +427 -0
- package/dist/attack/protocolValidator.js.map +1 -0
- package/dist/attack/qualityMetrics.d.ts +133 -0
- package/dist/attack/qualityMetrics.js +226 -0
- package/dist/attack/qualityMetrics.js.map +1 -0
- package/dist/attack/scanScheduler.d.ts +52 -0
- package/dist/attack/scanScheduler.js +265 -0
- package/dist/attack/scanScheduler.js.map +1 -0
- package/dist/attack/scanState.d.ts +58 -0
- package/dist/attack/scanState.js +102 -0
- package/dist/attack/scanState.js.map +1 -0
- package/dist/attack/searchIntent.d.ts +16 -0
- package/dist/attack/searchIntent.js +57 -0
- package/dist/attack/searchIntent.js.map +1 -0
- package/dist/attack/verifyLoop.d.ts +43 -0
- package/dist/attack/verifyLoop.js +314 -14
- package/dist/attack/verifyLoop.js.map +1 -1
- package/dist/attack/workItem.d.ts +57 -0
- package/dist/attack/workItem.js +225 -0
- package/dist/attack/workItem.js.map +1 -0
- package/dist/audit/findingReviewQueue.d.ts +109 -0
- package/dist/audit/findingReviewQueue.js +335 -0
- package/dist/audit/findingReviewQueue.js.map +1 -0
- package/dist/audit/scanAuditLog.d.ts +160 -0
- package/dist/audit/scanAuditLog.js +404 -0
- package/dist/audit/scanAuditLog.js.map +1 -0
- package/dist/dependency/dependencyChecker.js +35 -9
- package/dist/dependency/dependencyChecker.js.map +1 -1
- package/dist/dependency/exploitPriority.d.ts +33 -0
- package/dist/dependency/exploitPriority.js +78 -0
- package/dist/dependency/exploitPriority.js.map +1 -0
- package/dist/dependency/finding.d.ts +16 -0
- package/dist/dependency/osvClient.js +2 -0
- package/dist/dependency/osvClient.js.map +1 -1
- package/dist/dependency/types.d.ts +12 -1
- package/dist/mcp/server.js +13 -2
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tools.d.ts +1 -0
- package/dist/mcp/tools.js +127 -6
- package/dist/mcp/tools.js.map +1 -1
- package/dist/project-map/agentMemory.d.ts +83 -1
- package/dist/project-map/agentMemory.js +197 -7
- package/dist/project-map/agentMemory.js.map +1 -1
- package/dist/project-map/architectureContext.d.ts +202 -0
- package/dist/project-map/architectureContext.js +421 -0
- package/dist/project-map/architectureContext.js.map +1 -0
- package/dist/project-map/blastRadius.d.ts +54 -0
- package/dist/project-map/blastRadius.js +196 -0
- package/dist/project-map/blastRadius.js.map +1 -0
- package/dist/project-map/callGraphExtractor.d.ts +13 -0
- package/dist/project-map/callGraphExtractor.js +222 -0
- package/dist/project-map/callGraphExtractor.js.map +1 -0
- package/dist/project-map/capabilityRegistry.d.ts +46 -0
- package/dist/project-map/capabilityRegistry.js +91 -0
- package/dist/project-map/capabilityRegistry.js.map +1 -0
- package/dist/project-map/findTests.d.ts +29 -0
- package/dist/project-map/findTests.js +236 -0
- package/dist/project-map/findTests.js.map +1 -0
- package/dist/project-map/handlerInventory.d.ts +112 -0
- package/dist/project-map/handlerInventory.js +184 -0
- package/dist/project-map/handlerInventory.js.map +1 -0
- package/dist/project-map/implementationResolver.d.ts +43 -0
- package/dist/project-map/implementationResolver.js +217 -0
- package/dist/project-map/implementationResolver.js.map +1 -0
- package/dist/project-map/mapContext.d.ts +6 -1
- package/dist/project-map/mapContext.js +1 -0
- package/dist/project-map/mapContext.js.map +1 -1
- package/dist/project-map/scanCache.d.ts +57 -3
- package/dist/project-map/scanCache.js +61 -2
- package/dist/project-map/scanCache.js.map +1 -1
- package/dist/project-map/symbolIndex.d.ts +39 -0
- package/dist/project-map/symbolIndex.js +385 -0
- package/dist/project-map/symbolIndex.js.map +1 -0
- package/dist/tooling/agentEvalScoring.d.ts +77 -0
- package/dist/tooling/agentEvalScoring.js +140 -0
- package/dist/tooling/agentEvalScoring.js.map +1 -0
- package/dist/tooling/agentRegression.d.ts +53 -0
- package/dist/tooling/agentRegression.js +99 -0
- package/dist/tooling/agentRegression.js.map +1 -0
- package/dist/tools/agentScan.d.ts +69 -0
- package/dist/tools/agentScan.js +702 -43
- package/dist/tools/agentScan.js.map +1 -1
- package/dist/tools/findingReviewTools.d.ts +22 -0
- package/dist/tools/findingReviewTools.js +121 -0
- package/dist/tools/findingReviewTools.js.map +1 -0
- package/dist/tools/fix.js +1 -1
- package/dist/tools/fix.js.map +1 -1
- package/dist/tools/map.js +191 -1
- package/dist/tools/map.js.map +1 -1
- package/dist/tools/runTests.d.ts +2 -0
- package/dist/tools/runTests.js +29 -0
- package/dist/tools/runTests.js.map +1 -0
- package/dist/utils/effectMock.d.ts +1 -0
- package/dist/utils/effectMock.js +162 -0
- package/dist/utils/effectMock.js.map +1 -0
- package/dist/utils/gitContext.d.ts +26 -0
- package/dist/utils/gitContext.js +317 -0
- package/dist/utils/gitContext.js.map +1 -0
- package/dist/utils/localTestRunner.d.ts +57 -2
- package/dist/utils/localTestRunner.js +122 -115
- package/dist/utils/localTestRunner.js.map +1 -1
- package/dist/utils/securityConfig.d.ts +17 -0
- package/dist/utils/securityConfig.js +192 -0
- package/dist/utils/securityConfig.js.map +1 -0
- package/dist/utils/testCommandPolicy.d.ts +36 -0
- package/dist/utils/testCommandPolicy.js +252 -0
- package/dist/utils/testCommandPolicy.js.map +1 -0
- package/dist/utils/testRunner.d.ts +56 -0
- package/dist/utils/testRunner.js +246 -0
- package/dist/utils/testRunner.js.map +1 -0
- package/dist/utils/testSafety.d.ts +40 -2
- package/dist/utils/testSafety.js +205 -26
- package/dist/utils/testSafety.js.map +1 -1
- package/dist/utils/verificationSandbox.d.ts +113 -0
- package/dist/utils/verificationSandbox.js +656 -0
- package/dist/utils/verificationSandbox.js.map +1 -0
- 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
|