@harness-engineering/core 0.21.1 → 0.21.3
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/dist/architecture/matchers.js +383 -332
- package/dist/architecture/matchers.mjs +1 -1
- package/dist/{chunk-BQUWXBGR.mjs → chunk-4W4FRAA6.mjs} +383 -332
- package/dist/index.d.mts +362 -45
- package/dist/index.d.ts +362 -45
- package/dist/index.js +2052 -1363
- package/dist/index.mjs +1653 -1033
- package/package.json +3 -3
package/dist/index.mjs
CHANGED
|
@@ -41,7 +41,7 @@ import {
|
|
|
41
41
|
runAll,
|
|
42
42
|
validateDependencies,
|
|
43
43
|
violationId
|
|
44
|
-
} from "./chunk-
|
|
44
|
+
} from "./chunk-4W4FRAA6.mjs";
|
|
45
45
|
|
|
46
46
|
// src/index.ts
|
|
47
47
|
export * from "@harness-engineering/types";
|
|
@@ -84,15 +84,15 @@ function validateConfig(data, schema) {
|
|
|
84
84
|
let message = "Configuration validation failed";
|
|
85
85
|
const suggestions = [];
|
|
86
86
|
if (firstError) {
|
|
87
|
-
const
|
|
88
|
-
const pathDisplay =
|
|
87
|
+
const path34 = firstError.path.join(".");
|
|
88
|
+
const pathDisplay = path34 ? ` at "${path34}"` : "";
|
|
89
89
|
if (firstError.code === "invalid_type") {
|
|
90
90
|
const received = firstError.received;
|
|
91
91
|
const expected = firstError.expected;
|
|
92
92
|
if (received === "undefined") {
|
|
93
93
|
code = "MISSING_FIELD";
|
|
94
94
|
message = `Missing required field${pathDisplay}: ${firstError.message}`;
|
|
95
|
-
suggestions.push(`Field "${
|
|
95
|
+
suggestions.push(`Field "${path34}" is required and must be of type "${expected}"`);
|
|
96
96
|
} else {
|
|
97
97
|
code = "INVALID_TYPE";
|
|
98
98
|
message = `Invalid type${pathDisplay}: ${firstError.message}`;
|
|
@@ -149,21 +149,11 @@ function validateCommitMessage(message, format = "conventional") {
|
|
|
149
149
|
issues: []
|
|
150
150
|
});
|
|
151
151
|
}
|
|
152
|
-
function
|
|
153
|
-
const lines = message.split("\n");
|
|
154
|
-
const headerLine = lines[0];
|
|
155
|
-
if (!headerLine) {
|
|
156
|
-
const error = createError(
|
|
157
|
-
"VALIDATION_FAILED",
|
|
158
|
-
"Commit message header cannot be empty",
|
|
159
|
-
{ message },
|
|
160
|
-
["Provide a commit message with at least a header line"]
|
|
161
|
-
);
|
|
162
|
-
return Err(error);
|
|
163
|
-
}
|
|
152
|
+
function parseConventionalHeader(message, headerLine) {
|
|
164
153
|
const match = headerLine.match(CONVENTIONAL_PATTERN);
|
|
165
|
-
if (
|
|
166
|
-
|
|
154
|
+
if (match) return Ok(match);
|
|
155
|
+
return Err(
|
|
156
|
+
createError(
|
|
167
157
|
"VALIDATION_FAILED",
|
|
168
158
|
"Commit message does not follow conventional format",
|
|
169
159
|
{ message, header: headerLine },
|
|
@@ -172,13 +162,10 @@ function validateConventionalCommit(message) {
|
|
|
172
162
|
"Valid types: " + VALID_TYPES.join(", "),
|
|
173
163
|
"Example: feat(core): add new feature"
|
|
174
164
|
]
|
|
175
|
-
)
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const scope = match[3];
|
|
180
|
-
const breaking = match[4] === "!";
|
|
181
|
-
const description = match[5];
|
|
165
|
+
)
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
function collectCommitIssues(type, description) {
|
|
182
169
|
const issues = [];
|
|
183
170
|
if (!VALID_TYPES.includes(type)) {
|
|
184
171
|
issues.push(`Invalid commit type "${type}". Valid types: ${VALID_TYPES.join(", ")}`);
|
|
@@ -186,34 +173,50 @@ function validateConventionalCommit(message) {
|
|
|
186
173
|
if (!description || description.trim() === "") {
|
|
187
174
|
issues.push("Commit description cannot be empty");
|
|
188
175
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
176
|
+
return issues;
|
|
177
|
+
}
|
|
178
|
+
function hasBreakingChangeInBody(lines) {
|
|
179
|
+
if (lines.length <= 1) return false;
|
|
180
|
+
return lines.slice(1).join("\n").includes("BREAKING CHANGE:");
|
|
181
|
+
}
|
|
182
|
+
function validateConventionalCommit(message) {
|
|
183
|
+
const lines = message.split("\n");
|
|
184
|
+
const headerLine = lines[0];
|
|
185
|
+
if (!headerLine) {
|
|
186
|
+
return Err(
|
|
187
|
+
createError(
|
|
188
|
+
"VALIDATION_FAILED",
|
|
189
|
+
"Commit message header cannot be empty",
|
|
190
|
+
{ message },
|
|
191
|
+
["Provide a commit message with at least a header line"]
|
|
192
|
+
)
|
|
193
|
+
);
|
|
195
194
|
}
|
|
195
|
+
const matchResult = parseConventionalHeader(message, headerLine);
|
|
196
|
+
if (!matchResult.ok) return matchResult;
|
|
197
|
+
const match = matchResult.value;
|
|
198
|
+
const type = match[1];
|
|
199
|
+
const scope = match[3];
|
|
200
|
+
const breaking = match[4] === "!";
|
|
201
|
+
const description = match[5];
|
|
202
|
+
const issues = collectCommitIssues(type, description);
|
|
196
203
|
if (issues.length > 0) {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
{ message, issues, type, scope },
|
|
205
|
-
["Review and fix the validation issues above"]
|
|
204
|
+
return Err(
|
|
205
|
+
createError(
|
|
206
|
+
"VALIDATION_FAILED",
|
|
207
|
+
`Commit message validation failed: ${issues.join("; ")}`,
|
|
208
|
+
{ message, issues, type, scope },
|
|
209
|
+
["Review and fix the validation issues above"]
|
|
210
|
+
)
|
|
206
211
|
);
|
|
207
|
-
return Err(error);
|
|
208
212
|
}
|
|
209
|
-
|
|
213
|
+
return Ok({
|
|
210
214
|
valid: true,
|
|
211
215
|
type,
|
|
212
216
|
...scope && { scope },
|
|
213
|
-
breaking:
|
|
217
|
+
breaking: breaking || hasBreakingChangeInBody(lines),
|
|
214
218
|
issues: []
|
|
215
|
-
};
|
|
216
|
-
return Ok(result);
|
|
219
|
+
});
|
|
217
220
|
}
|
|
218
221
|
|
|
219
222
|
// src/context/types.ts
|
|
@@ -308,27 +311,27 @@ function extractSections(content) {
|
|
|
308
311
|
}
|
|
309
312
|
return sections.map((section) => buildAgentMapSection(section, lines));
|
|
310
313
|
}
|
|
311
|
-
function isExternalLink(
|
|
312
|
-
return
|
|
314
|
+
function isExternalLink(path34) {
|
|
315
|
+
return path34.startsWith("http://") || path34.startsWith("https://") || path34.startsWith("#") || path34.startsWith("mailto:");
|
|
313
316
|
}
|
|
314
317
|
function resolveLinkPath(linkPath, baseDir) {
|
|
315
318
|
return linkPath.startsWith(".") ? join(baseDir, linkPath) : linkPath;
|
|
316
319
|
}
|
|
317
|
-
async function validateAgentsMap(
|
|
318
|
-
const contentResult = await readFileContent(
|
|
320
|
+
async function validateAgentsMap(path34 = "./AGENTS.md") {
|
|
321
|
+
const contentResult = await readFileContent(path34);
|
|
319
322
|
if (!contentResult.ok) {
|
|
320
323
|
return Err(
|
|
321
324
|
createError(
|
|
322
325
|
"PARSE_ERROR",
|
|
323
326
|
`Failed to read AGENTS.md: ${contentResult.error.message}`,
|
|
324
|
-
{ path:
|
|
327
|
+
{ path: path34 },
|
|
325
328
|
["Ensure the file exists", "Check file permissions"]
|
|
326
329
|
)
|
|
327
330
|
);
|
|
328
331
|
}
|
|
329
332
|
const content = contentResult.value;
|
|
330
333
|
const sections = extractSections(content);
|
|
331
|
-
const baseDir = dirname(
|
|
334
|
+
const baseDir = dirname(path34);
|
|
332
335
|
const sectionTitles = sections.map((s) => s.title);
|
|
333
336
|
const missingSections = REQUIRED_SECTIONS.filter(
|
|
334
337
|
(required) => !sectionTitles.some((title) => title.toLowerCase().includes(required.toLowerCase()))
|
|
@@ -469,8 +472,8 @@ async function checkDocCoverage(domain, options = {}) {
|
|
|
469
472
|
|
|
470
473
|
// src/context/knowledge-map.ts
|
|
471
474
|
import { join as join2, basename as basename2 } from "path";
|
|
472
|
-
function suggestFix(
|
|
473
|
-
const targetName = basename2(
|
|
475
|
+
function suggestFix(path34, existingFiles) {
|
|
476
|
+
const targetName = basename2(path34).toLowerCase();
|
|
474
477
|
const similar = existingFiles.find((file) => {
|
|
475
478
|
const fileName = basename2(file).toLowerCase();
|
|
476
479
|
return fileName.includes(targetName) || targetName.includes(fileName);
|
|
@@ -478,7 +481,7 @@ function suggestFix(path31, existingFiles) {
|
|
|
478
481
|
if (similar) {
|
|
479
482
|
return `Did you mean "${similar}"?`;
|
|
480
483
|
}
|
|
481
|
-
return `Create the file "${
|
|
484
|
+
return `Create the file "${path34}" or remove the link`;
|
|
482
485
|
}
|
|
483
486
|
async function validateKnowledgeMap(rootDir = process.cwd()) {
|
|
484
487
|
const agentsPath = join2(rootDir, "AGENTS.md");
|
|
@@ -671,6 +674,47 @@ var NODE_TYPE_TO_CATEGORY = {
|
|
|
671
674
|
prompt: "systemPrompt",
|
|
672
675
|
system: "systemPrompt"
|
|
673
676
|
};
|
|
677
|
+
function makeZeroWeights() {
|
|
678
|
+
return {
|
|
679
|
+
systemPrompt: 0,
|
|
680
|
+
projectManifest: 0,
|
|
681
|
+
taskSpec: 0,
|
|
682
|
+
activeCode: 0,
|
|
683
|
+
interfaces: 0,
|
|
684
|
+
reserve: 0
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
function normalizeRatios(ratios) {
|
|
688
|
+
const sum = Object.values(ratios).reduce((s, r) => s + r, 0);
|
|
689
|
+
if (sum === 0) return;
|
|
690
|
+
for (const key of Object.keys(ratios)) {
|
|
691
|
+
ratios[key] = ratios[key] / sum;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
function enforceMinimumRatios(ratios, min) {
|
|
695
|
+
for (const key of Object.keys(ratios)) {
|
|
696
|
+
if (ratios[key] < min) ratios[key] = min;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
function applyGraphDensity(ratios, graphDensity) {
|
|
700
|
+
const weights = makeZeroWeights();
|
|
701
|
+
for (const [nodeType, count] of Object.entries(graphDensity)) {
|
|
702
|
+
const category = NODE_TYPE_TO_CATEGORY[nodeType];
|
|
703
|
+
if (category) weights[category] += count;
|
|
704
|
+
}
|
|
705
|
+
const totalWeight = Object.values(weights).reduce((s, w) => s + w, 0);
|
|
706
|
+
if (totalWeight === 0) return;
|
|
707
|
+
const MIN = 0.01;
|
|
708
|
+
for (const key of Object.keys(ratios)) {
|
|
709
|
+
ratios[key] = weights[key] > 0 ? weights[key] / totalWeight : MIN;
|
|
710
|
+
}
|
|
711
|
+
if (ratios.reserve < DEFAULT_RATIOS.reserve) ratios.reserve = DEFAULT_RATIOS.reserve;
|
|
712
|
+
if (ratios.systemPrompt < DEFAULT_RATIOS.systemPrompt)
|
|
713
|
+
ratios.systemPrompt = DEFAULT_RATIOS.systemPrompt;
|
|
714
|
+
normalizeRatios(ratios);
|
|
715
|
+
enforceMinimumRatios(ratios, MIN);
|
|
716
|
+
normalizeRatios(ratios);
|
|
717
|
+
}
|
|
674
718
|
function contextBudget(totalTokens, overrides, graphDensity) {
|
|
675
719
|
const ratios = {
|
|
676
720
|
systemPrompt: DEFAULT_RATIOS.systemPrompt,
|
|
@@ -681,50 +725,7 @@ function contextBudget(totalTokens, overrides, graphDensity) {
|
|
|
681
725
|
reserve: DEFAULT_RATIOS.reserve
|
|
682
726
|
};
|
|
683
727
|
if (graphDensity) {
|
|
684
|
-
|
|
685
|
-
systemPrompt: 0,
|
|
686
|
-
projectManifest: 0,
|
|
687
|
-
taskSpec: 0,
|
|
688
|
-
activeCode: 0,
|
|
689
|
-
interfaces: 0,
|
|
690
|
-
reserve: 0
|
|
691
|
-
};
|
|
692
|
-
for (const [nodeType, count] of Object.entries(graphDensity)) {
|
|
693
|
-
const category = NODE_TYPE_TO_CATEGORY[nodeType];
|
|
694
|
-
if (category) {
|
|
695
|
-
categoryWeights[category] += count;
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
const totalWeight = Object.values(categoryWeights).reduce((sum, w) => sum + w, 0);
|
|
699
|
-
if (totalWeight > 0) {
|
|
700
|
-
const MIN_ALLOCATION = 0.01;
|
|
701
|
-
for (const key of Object.keys(ratios)) {
|
|
702
|
-
if (categoryWeights[key] > 0) {
|
|
703
|
-
ratios[key] = categoryWeights[key] / totalWeight;
|
|
704
|
-
} else {
|
|
705
|
-
ratios[key] = MIN_ALLOCATION;
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
if (ratios.reserve < DEFAULT_RATIOS.reserve) {
|
|
709
|
-
ratios.reserve = DEFAULT_RATIOS.reserve;
|
|
710
|
-
}
|
|
711
|
-
if (ratios.systemPrompt < DEFAULT_RATIOS.systemPrompt) {
|
|
712
|
-
ratios.systemPrompt = DEFAULT_RATIOS.systemPrompt;
|
|
713
|
-
}
|
|
714
|
-
const ratioSum = Object.values(ratios).reduce((sum, r) => sum + r, 0);
|
|
715
|
-
for (const key of Object.keys(ratios)) {
|
|
716
|
-
ratios[key] = ratios[key] / ratioSum;
|
|
717
|
-
}
|
|
718
|
-
for (const key of Object.keys(ratios)) {
|
|
719
|
-
if (ratios[key] < MIN_ALLOCATION) {
|
|
720
|
-
ratios[key] = MIN_ALLOCATION;
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
const finalSum = Object.values(ratios).reduce((sum, r) => sum + r, 0);
|
|
724
|
-
for (const key of Object.keys(ratios)) {
|
|
725
|
-
ratios[key] = ratios[key] / finalSum;
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
+
applyGraphDensity(ratios, graphDensity);
|
|
728
729
|
}
|
|
729
730
|
if (overrides) {
|
|
730
731
|
let overrideSum = 0;
|
|
@@ -830,8 +831,8 @@ function createBoundaryValidator(schema, name) {
|
|
|
830
831
|
return Ok(result.data);
|
|
831
832
|
}
|
|
832
833
|
const suggestions = result.error.issues.map((issue) => {
|
|
833
|
-
const
|
|
834
|
-
return
|
|
834
|
+
const path34 = issue.path.join(".");
|
|
835
|
+
return path34 ? `${path34}: ${issue.message}` : issue.message;
|
|
835
836
|
});
|
|
836
837
|
return Err(
|
|
837
838
|
createError(
|
|
@@ -1031,21 +1032,23 @@ function extractBundle(manifest, config) {
|
|
|
1031
1032
|
}
|
|
1032
1033
|
|
|
1033
1034
|
// src/constraints/sharing/merge.ts
|
|
1035
|
+
function arraysEqual(a, b) {
|
|
1036
|
+
if (a.length !== b.length) return false;
|
|
1037
|
+
return a.every((val, i) => deepEqual(val, b[i]));
|
|
1038
|
+
}
|
|
1039
|
+
function objectsEqual(a, b) {
|
|
1040
|
+
const keysA = Object.keys(a);
|
|
1041
|
+
const keysB = Object.keys(b);
|
|
1042
|
+
if (keysA.length !== keysB.length) return false;
|
|
1043
|
+
return keysA.every((key) => deepEqual(a[key], b[key]));
|
|
1044
|
+
}
|
|
1034
1045
|
function deepEqual(a, b) {
|
|
1035
1046
|
if (a === b) return true;
|
|
1036
1047
|
if (typeof a !== typeof b) return false;
|
|
1037
1048
|
if (typeof a !== "object" || a === null || b === null) return false;
|
|
1038
|
-
if (Array.isArray(a) && Array.isArray(b))
|
|
1039
|
-
if (a.length !== b.length) return false;
|
|
1040
|
-
return a.every((val, i) => deepEqual(val, b[i]));
|
|
1041
|
-
}
|
|
1049
|
+
if (Array.isArray(a) && Array.isArray(b)) return arraysEqual(a, b);
|
|
1042
1050
|
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
1043
|
-
|
|
1044
|
-
const keysB = Object.keys(b);
|
|
1045
|
-
if (keysA.length !== keysB.length) return false;
|
|
1046
|
-
return keysA.every(
|
|
1047
|
-
(key) => deepEqual(a[key], b[key])
|
|
1048
|
-
);
|
|
1051
|
+
return objectsEqual(a, b);
|
|
1049
1052
|
}
|
|
1050
1053
|
function stringArraysEqual(a, b) {
|
|
1051
1054
|
if (a.length !== b.length) return false;
|
|
@@ -1463,11 +1466,11 @@ function processExportListSpecifiers(exportDecl, exports) {
|
|
|
1463
1466
|
var TypeScriptParser = class {
|
|
1464
1467
|
name = "typescript";
|
|
1465
1468
|
extensions = [".ts", ".tsx", ".mts", ".cts"];
|
|
1466
|
-
async parseFile(
|
|
1467
|
-
const contentResult = await readFileContent(
|
|
1469
|
+
async parseFile(path34) {
|
|
1470
|
+
const contentResult = await readFileContent(path34);
|
|
1468
1471
|
if (!contentResult.ok) {
|
|
1469
1472
|
return Err(
|
|
1470
|
-
createParseError("NOT_FOUND", `File not found: ${
|
|
1473
|
+
createParseError("NOT_FOUND", `File not found: ${path34}`, { path: path34 }, [
|
|
1471
1474
|
"Check that the file exists",
|
|
1472
1475
|
"Verify the path is correct"
|
|
1473
1476
|
])
|
|
@@ -1477,7 +1480,7 @@ var TypeScriptParser = class {
|
|
|
1477
1480
|
const ast = parse(contentResult.value, {
|
|
1478
1481
|
loc: true,
|
|
1479
1482
|
range: true,
|
|
1480
|
-
jsx:
|
|
1483
|
+
jsx: path34.endsWith(".tsx"),
|
|
1481
1484
|
errorOnUnknownASTType: false
|
|
1482
1485
|
});
|
|
1483
1486
|
return Ok({
|
|
@@ -1488,7 +1491,7 @@ var TypeScriptParser = class {
|
|
|
1488
1491
|
} catch (e) {
|
|
1489
1492
|
const error = e;
|
|
1490
1493
|
return Err(
|
|
1491
|
-
createParseError("SYNTAX_ERROR", `Failed to parse ${
|
|
1494
|
+
createParseError("SYNTAX_ERROR", `Failed to parse ${path34}: ${error.message}`, { path: path34 }, [
|
|
1492
1495
|
"Check for syntax errors in the file",
|
|
1493
1496
|
"Ensure valid TypeScript syntax"
|
|
1494
1497
|
])
|
|
@@ -1673,22 +1676,22 @@ function extractInlineRefs(content) {
|
|
|
1673
1676
|
}
|
|
1674
1677
|
return refs;
|
|
1675
1678
|
}
|
|
1676
|
-
async function parseDocumentationFile(
|
|
1677
|
-
const contentResult = await readFileContent(
|
|
1679
|
+
async function parseDocumentationFile(path34) {
|
|
1680
|
+
const contentResult = await readFileContent(path34);
|
|
1678
1681
|
if (!contentResult.ok) {
|
|
1679
1682
|
return Err(
|
|
1680
1683
|
createEntropyError(
|
|
1681
1684
|
"PARSE_ERROR",
|
|
1682
|
-
`Failed to read documentation file: ${
|
|
1683
|
-
{ file:
|
|
1685
|
+
`Failed to read documentation file: ${path34}`,
|
|
1686
|
+
{ file: path34 },
|
|
1684
1687
|
["Check that the file exists"]
|
|
1685
1688
|
)
|
|
1686
1689
|
);
|
|
1687
1690
|
}
|
|
1688
1691
|
const content = contentResult.value;
|
|
1689
|
-
const type =
|
|
1692
|
+
const type = path34.endsWith(".md") ? "markdown" : "text";
|
|
1690
1693
|
return Ok({
|
|
1691
|
-
path:
|
|
1694
|
+
path: path34,
|
|
1692
1695
|
type,
|
|
1693
1696
|
content,
|
|
1694
1697
|
codeBlocks: extractCodeBlocks(content),
|
|
@@ -1698,17 +1701,22 @@ async function parseDocumentationFile(path31) {
|
|
|
1698
1701
|
function makeInternalSymbol(name, type, line) {
|
|
1699
1702
|
return { name, type, line, references: 0, calledBy: [] };
|
|
1700
1703
|
}
|
|
1704
|
+
function extractFunctionSymbol(node, line) {
|
|
1705
|
+
if (node.id?.name) return [makeInternalSymbol(node.id.name, "function", line)];
|
|
1706
|
+
return [];
|
|
1707
|
+
}
|
|
1708
|
+
function extractVariableSymbols(node, line) {
|
|
1709
|
+
return (node.declarations || []).filter((decl) => decl.id?.name).map((decl) => makeInternalSymbol(decl.id.name, "variable", line));
|
|
1710
|
+
}
|
|
1711
|
+
function extractClassSymbol(node, line) {
|
|
1712
|
+
if (node.id?.name) return [makeInternalSymbol(node.id.name, "class", line)];
|
|
1713
|
+
return [];
|
|
1714
|
+
}
|
|
1701
1715
|
function extractSymbolsFromNode(node) {
|
|
1702
1716
|
const line = node.loc?.start?.line || 0;
|
|
1703
|
-
if (node.type === "FunctionDeclaration"
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
if (node.type === "VariableDeclaration") {
|
|
1707
|
-
return (node.declarations || []).filter((decl) => decl.id?.name).map((decl) => makeInternalSymbol(decl.id.name, "variable", line));
|
|
1708
|
-
}
|
|
1709
|
-
if (node.type === "ClassDeclaration" && node.id?.name) {
|
|
1710
|
-
return [makeInternalSymbol(node.id.name, "class", line)];
|
|
1711
|
-
}
|
|
1717
|
+
if (node.type === "FunctionDeclaration") return extractFunctionSymbol(node, line);
|
|
1718
|
+
if (node.type === "VariableDeclaration") return extractVariableSymbols(node, line);
|
|
1719
|
+
if (node.type === "ClassDeclaration") return extractClassSymbol(node, line);
|
|
1712
1720
|
return [];
|
|
1713
1721
|
}
|
|
1714
1722
|
function extractInternalSymbols(ast) {
|
|
@@ -1717,21 +1725,17 @@ function extractInternalSymbols(ast) {
|
|
|
1717
1725
|
const nodes = body.body;
|
|
1718
1726
|
return nodes.flatMap(extractSymbolsFromNode);
|
|
1719
1727
|
}
|
|
1728
|
+
function toJSDocComment(comment) {
|
|
1729
|
+
if (comment.type !== "Block" || !comment.value?.startsWith("*")) return null;
|
|
1730
|
+
return { content: comment.value, line: comment.loc?.start?.line || 0 };
|
|
1731
|
+
}
|
|
1720
1732
|
function extractJSDocComments(ast) {
|
|
1721
|
-
const comments = [];
|
|
1722
1733
|
const body = ast.body;
|
|
1723
|
-
if (body?.comments)
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
line: comment.loc?.start?.line || 0
|
|
1729
|
-
};
|
|
1730
|
-
comments.push(jsDocComment);
|
|
1731
|
-
}
|
|
1732
|
-
}
|
|
1733
|
-
}
|
|
1734
|
-
return comments;
|
|
1734
|
+
if (!body?.comments) return [];
|
|
1735
|
+
return body.comments.flatMap((c) => {
|
|
1736
|
+
const doc = toJSDocComment(c);
|
|
1737
|
+
return doc ? [doc] : [];
|
|
1738
|
+
});
|
|
1735
1739
|
}
|
|
1736
1740
|
function buildExportMap(files) {
|
|
1737
1741
|
const byFile = /* @__PURE__ */ new Map();
|
|
@@ -1746,41 +1750,42 @@ function buildExportMap(files) {
|
|
|
1746
1750
|
}
|
|
1747
1751
|
return { byFile, byName };
|
|
1748
1752
|
}
|
|
1749
|
-
|
|
1753
|
+
var CODE_BLOCK_LANGUAGES = /* @__PURE__ */ new Set(["typescript", "ts", "javascript", "js"]);
|
|
1754
|
+
function refsFromInlineRefs(doc) {
|
|
1755
|
+
return doc.inlineRefs.map((inlineRef) => ({
|
|
1756
|
+
docFile: doc.path,
|
|
1757
|
+
line: inlineRef.line,
|
|
1758
|
+
column: inlineRef.column,
|
|
1759
|
+
reference: inlineRef.reference,
|
|
1760
|
+
context: "inline"
|
|
1761
|
+
}));
|
|
1762
|
+
}
|
|
1763
|
+
function refsFromCodeBlock(docPath, block) {
|
|
1764
|
+
if (!CODE_BLOCK_LANGUAGES.has(block.language)) return [];
|
|
1750
1765
|
const refs = [];
|
|
1751
|
-
|
|
1752
|
-
|
|
1766
|
+
const importRegex = /import\s+\{([^}]+)\}\s+from/g;
|
|
1767
|
+
let match;
|
|
1768
|
+
while ((match = importRegex.exec(block.content)) !== null) {
|
|
1769
|
+
const group = match[1];
|
|
1770
|
+
if (group === void 0) continue;
|
|
1771
|
+
for (const name of group.split(",").map((n) => n.trim())) {
|
|
1753
1772
|
refs.push({
|
|
1754
|
-
docFile:
|
|
1755
|
-
line:
|
|
1756
|
-
column:
|
|
1757
|
-
reference:
|
|
1758
|
-
context: "
|
|
1773
|
+
docFile: docPath,
|
|
1774
|
+
line: block.line,
|
|
1775
|
+
column: 0,
|
|
1776
|
+
reference: name,
|
|
1777
|
+
context: "code-block"
|
|
1759
1778
|
});
|
|
1760
1779
|
}
|
|
1761
|
-
for (const block of doc.codeBlocks) {
|
|
1762
|
-
if (block.language === "typescript" || block.language === "ts" || block.language === "javascript" || block.language === "js") {
|
|
1763
|
-
const importRegex = /import\s+\{([^}]+)\}\s+from/g;
|
|
1764
|
-
let match;
|
|
1765
|
-
while ((match = importRegex.exec(block.content)) !== null) {
|
|
1766
|
-
const matchedGroup = match[1];
|
|
1767
|
-
if (matchedGroup === void 0) continue;
|
|
1768
|
-
const names = matchedGroup.split(",").map((n) => n.trim());
|
|
1769
|
-
for (const name of names) {
|
|
1770
|
-
refs.push({
|
|
1771
|
-
docFile: doc.path,
|
|
1772
|
-
line: block.line,
|
|
1773
|
-
column: 0,
|
|
1774
|
-
reference: name,
|
|
1775
|
-
context: "code-block"
|
|
1776
|
-
});
|
|
1777
|
-
}
|
|
1778
|
-
}
|
|
1779
|
-
}
|
|
1780
|
-
}
|
|
1781
1780
|
}
|
|
1782
1781
|
return refs;
|
|
1783
1782
|
}
|
|
1783
|
+
function refsFromCodeBlocks(doc) {
|
|
1784
|
+
return doc.codeBlocks.flatMap((block) => refsFromCodeBlock(doc.path, block));
|
|
1785
|
+
}
|
|
1786
|
+
function extractAllCodeReferences(docs) {
|
|
1787
|
+
return docs.flatMap((doc) => [...refsFromInlineRefs(doc), ...refsFromCodeBlocks(doc)]);
|
|
1788
|
+
}
|
|
1784
1789
|
async function buildSnapshot(config) {
|
|
1785
1790
|
const startTime = Date.now();
|
|
1786
1791
|
const parser = config.parser || new TypeScriptParser();
|
|
@@ -1986,44 +1991,52 @@ async function checkStructureDrift(snapshot, _config) {
|
|
|
1986
1991
|
}
|
|
1987
1992
|
return drifts;
|
|
1988
1993
|
}
|
|
1994
|
+
function computeDriftSeverity(driftCount) {
|
|
1995
|
+
if (driftCount === 0) return "none";
|
|
1996
|
+
if (driftCount <= 3) return "low";
|
|
1997
|
+
if (driftCount <= 10) return "medium";
|
|
1998
|
+
return "high";
|
|
1999
|
+
}
|
|
2000
|
+
function buildGraphDriftReport(graphDriftData) {
|
|
2001
|
+
const drifts = [];
|
|
2002
|
+
for (const target of graphDriftData.missingTargets) {
|
|
2003
|
+
drifts.push({
|
|
2004
|
+
type: "api-signature",
|
|
2005
|
+
docFile: target,
|
|
2006
|
+
line: 0,
|
|
2007
|
+
reference: target,
|
|
2008
|
+
context: "graph-missing-target",
|
|
2009
|
+
issue: "NOT_FOUND",
|
|
2010
|
+
details: `Graph node "${target}" has no matching code target`,
|
|
2011
|
+
confidence: "high"
|
|
2012
|
+
});
|
|
2013
|
+
}
|
|
2014
|
+
for (const edge of graphDriftData.staleEdges) {
|
|
2015
|
+
drifts.push({
|
|
2016
|
+
type: "api-signature",
|
|
2017
|
+
docFile: edge.docNodeId,
|
|
2018
|
+
line: 0,
|
|
2019
|
+
reference: edge.codeNodeId,
|
|
2020
|
+
context: `graph-stale-edge:${edge.edgeType}`,
|
|
2021
|
+
issue: "NOT_FOUND",
|
|
2022
|
+
details: `Stale edge from doc "${edge.docNodeId}" to code "${edge.codeNodeId}" (${edge.edgeType})`,
|
|
2023
|
+
confidence: "medium"
|
|
2024
|
+
});
|
|
2025
|
+
}
|
|
2026
|
+
return Ok({
|
|
2027
|
+
drifts,
|
|
2028
|
+
stats: {
|
|
2029
|
+
docsScanned: graphDriftData.staleEdges.length,
|
|
2030
|
+
referencesChecked: graphDriftData.staleEdges.length + graphDriftData.missingTargets.length,
|
|
2031
|
+
driftsFound: drifts.length,
|
|
2032
|
+
byType: { api: drifts.length, example: 0, structure: 0 }
|
|
2033
|
+
},
|
|
2034
|
+
severity: computeDriftSeverity(drifts.length)
|
|
2035
|
+
});
|
|
2036
|
+
}
|
|
1989
2037
|
async function detectDocDrift(snapshot, config, graphDriftData) {
|
|
1990
2038
|
if (graphDriftData) {
|
|
1991
|
-
|
|
1992
|
-
for (const target of graphDriftData.missingTargets) {
|
|
1993
|
-
drifts2.push({
|
|
1994
|
-
type: "api-signature",
|
|
1995
|
-
docFile: target,
|
|
1996
|
-
line: 0,
|
|
1997
|
-
reference: target,
|
|
1998
|
-
context: "graph-missing-target",
|
|
1999
|
-
issue: "NOT_FOUND",
|
|
2000
|
-
details: `Graph node "${target}" has no matching code target`,
|
|
2001
|
-
confidence: "high"
|
|
2002
|
-
});
|
|
2003
|
-
}
|
|
2004
|
-
for (const edge of graphDriftData.staleEdges) {
|
|
2005
|
-
drifts2.push({
|
|
2006
|
-
type: "api-signature",
|
|
2007
|
-
docFile: edge.docNodeId,
|
|
2008
|
-
line: 0,
|
|
2009
|
-
reference: edge.codeNodeId,
|
|
2010
|
-
context: `graph-stale-edge:${edge.edgeType}`,
|
|
2011
|
-
issue: "NOT_FOUND",
|
|
2012
|
-
details: `Stale edge from doc "${edge.docNodeId}" to code "${edge.codeNodeId}" (${edge.edgeType})`,
|
|
2013
|
-
confidence: "medium"
|
|
2014
|
-
});
|
|
2015
|
-
}
|
|
2016
|
-
const severity2 = drifts2.length === 0 ? "none" : drifts2.length <= 3 ? "low" : drifts2.length <= 10 ? "medium" : "high";
|
|
2017
|
-
return Ok({
|
|
2018
|
-
drifts: drifts2,
|
|
2019
|
-
stats: {
|
|
2020
|
-
docsScanned: graphDriftData.staleEdges.length,
|
|
2021
|
-
referencesChecked: graphDriftData.staleEdges.length + graphDriftData.missingTargets.length,
|
|
2022
|
-
driftsFound: drifts2.length,
|
|
2023
|
-
byType: { api: drifts2.length, example: 0, structure: 0 }
|
|
2024
|
-
},
|
|
2025
|
-
severity: severity2
|
|
2026
|
-
});
|
|
2039
|
+
return buildGraphDriftReport(graphDriftData);
|
|
2027
2040
|
}
|
|
2028
2041
|
const fullConfig = { ...DEFAULT_DRIFT_CONFIG, ...config };
|
|
2029
2042
|
const drifts = [];
|
|
@@ -2072,6 +2085,23 @@ function resolveImportToFile(importSource, fromFile, snapshot) {
|
|
|
2072
2085
|
}
|
|
2073
2086
|
return null;
|
|
2074
2087
|
}
|
|
2088
|
+
function enqueueResolved(sources, current, snapshot, visited, queue) {
|
|
2089
|
+
for (const item of sources) {
|
|
2090
|
+
if (!item.source) continue;
|
|
2091
|
+
const resolved = resolveImportToFile(item.source, current, snapshot);
|
|
2092
|
+
if (resolved && !visited.has(resolved)) {
|
|
2093
|
+
queue.push(resolved);
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
function processReachabilityNode(current, snapshot, reachability, visited, queue) {
|
|
2098
|
+
reachability.set(current, true);
|
|
2099
|
+
const sourceFile = snapshot.files.find((f) => f.path === current);
|
|
2100
|
+
if (!sourceFile) return;
|
|
2101
|
+
enqueueResolved(sourceFile.imports, current, snapshot, visited, queue);
|
|
2102
|
+
const reExports = sourceFile.exports.filter((e) => e.isReExport);
|
|
2103
|
+
enqueueResolved(reExports, current, snapshot, visited, queue);
|
|
2104
|
+
}
|
|
2075
2105
|
function buildReachabilityMap(snapshot) {
|
|
2076
2106
|
const reachability = /* @__PURE__ */ new Map();
|
|
2077
2107
|
for (const file of snapshot.files) {
|
|
@@ -2083,23 +2113,7 @@ function buildReachabilityMap(snapshot) {
|
|
|
2083
2113
|
const current = queue.shift();
|
|
2084
2114
|
if (visited.has(current)) continue;
|
|
2085
2115
|
visited.add(current);
|
|
2086
|
-
|
|
2087
|
-
const sourceFile = snapshot.files.find((f) => f.path === current);
|
|
2088
|
-
if (!sourceFile) continue;
|
|
2089
|
-
for (const imp of sourceFile.imports) {
|
|
2090
|
-
const resolved = resolveImportToFile(imp.source, current, snapshot);
|
|
2091
|
-
if (resolved && !visited.has(resolved)) {
|
|
2092
|
-
queue.push(resolved);
|
|
2093
|
-
}
|
|
2094
|
-
}
|
|
2095
|
-
for (const exp of sourceFile.exports) {
|
|
2096
|
-
if (exp.isReExport && exp.source) {
|
|
2097
|
-
const resolved = resolveImportToFile(exp.source, current, snapshot);
|
|
2098
|
-
if (resolved && !visited.has(resolved)) {
|
|
2099
|
-
queue.push(resolved);
|
|
2100
|
-
}
|
|
2101
|
-
}
|
|
2102
|
-
}
|
|
2116
|
+
processReachabilityNode(current, snapshot, reachability, visited, queue);
|
|
2103
2117
|
}
|
|
2104
2118
|
return reachability;
|
|
2105
2119
|
}
|
|
@@ -2169,21 +2183,27 @@ function findDeadExports(snapshot, usageMap, reachability) {
|
|
|
2169
2183
|
}
|
|
2170
2184
|
return deadExports;
|
|
2171
2185
|
}
|
|
2186
|
+
function maxLineOfValue(value) {
|
|
2187
|
+
if (Array.isArray(value)) {
|
|
2188
|
+
return value.reduce((m, item) => Math.max(m, findMaxLineInNode(item)), 0);
|
|
2189
|
+
}
|
|
2190
|
+
if (value && typeof value === "object") {
|
|
2191
|
+
return findMaxLineInNode(value);
|
|
2192
|
+
}
|
|
2193
|
+
return 0;
|
|
2194
|
+
}
|
|
2195
|
+
function maxLineOfNodeKeys(node) {
|
|
2196
|
+
let max = 0;
|
|
2197
|
+
for (const key of Object.keys(node)) {
|
|
2198
|
+
max = Math.max(max, maxLineOfValue(node[key]));
|
|
2199
|
+
}
|
|
2200
|
+
return max;
|
|
2201
|
+
}
|
|
2172
2202
|
function findMaxLineInNode(node) {
|
|
2173
2203
|
if (!node || typeof node !== "object") return 0;
|
|
2174
2204
|
const n = node;
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
const value = node[key];
|
|
2178
|
-
if (Array.isArray(value)) {
|
|
2179
|
-
for (const item of value) {
|
|
2180
|
-
maxLine = Math.max(maxLine, findMaxLineInNode(item));
|
|
2181
|
-
}
|
|
2182
|
-
} else if (value && typeof value === "object") {
|
|
2183
|
-
maxLine = Math.max(maxLine, findMaxLineInNode(value));
|
|
2184
|
-
}
|
|
2185
|
-
}
|
|
2186
|
-
return maxLine;
|
|
2205
|
+
const locLine = n.loc?.end?.line ?? 0;
|
|
2206
|
+
return Math.max(locLine, maxLineOfNodeKeys(node));
|
|
2187
2207
|
}
|
|
2188
2208
|
function countLinesFromAST(ast) {
|
|
2189
2209
|
if (!ast.body || !Array.isArray(ast.body)) return 1;
|
|
@@ -2257,54 +2277,59 @@ function findDeadInternals(snapshot, _reachability) {
|
|
|
2257
2277
|
}
|
|
2258
2278
|
return deadInternals;
|
|
2259
2279
|
}
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
type: exportType,
|
|
2281
|
-
isDefault: false,
|
|
2282
|
-
reason: "NO_IMPORTERS"
|
|
2283
|
-
});
|
|
2284
|
-
}
|
|
2285
|
-
}
|
|
2286
|
-
const reachableCount = graphDeadCodeData.reachableNodeIds instanceof Set ? graphDeadCodeData.reachableNodeIds.size : graphDeadCodeData.reachableNodeIds.length;
|
|
2287
|
-
const fileNodes = graphDeadCodeData.unreachableNodes.filter((n) => fileTypes.has(n.type));
|
|
2288
|
-
const exportNodes = graphDeadCodeData.unreachableNodes.filter((n) => exportTypes.has(n.type));
|
|
2289
|
-
const totalFiles = reachableCount + fileNodes.length;
|
|
2290
|
-
const totalExports2 = exportNodes.length + (reachableCount > 0 ? reachableCount : 0);
|
|
2291
|
-
const report2 = {
|
|
2292
|
-
deadExports: deadExports2,
|
|
2293
|
-
deadFiles: deadFiles2,
|
|
2294
|
-
deadInternals: [],
|
|
2295
|
-
unusedImports: [],
|
|
2296
|
-
stats: {
|
|
2297
|
-
filesAnalyzed: totalFiles,
|
|
2298
|
-
entryPointsUsed: [],
|
|
2299
|
-
totalExports: totalExports2,
|
|
2300
|
-
deadExportCount: deadExports2.length,
|
|
2301
|
-
totalFiles,
|
|
2302
|
-
deadFileCount: deadFiles2.length,
|
|
2303
|
-
estimatedDeadLines: 0
|
|
2304
|
-
}
|
|
2305
|
-
};
|
|
2306
|
-
return Ok(report2);
|
|
2280
|
+
var FILE_TYPES = /* @__PURE__ */ new Set(["file", "module"]);
|
|
2281
|
+
var EXPORT_TYPES = /* @__PURE__ */ new Set(["function", "class", "method", "interface", "variable"]);
|
|
2282
|
+
function classifyUnreachableNode(node, deadFiles, deadExports) {
|
|
2283
|
+
if (FILE_TYPES.has(node.type)) {
|
|
2284
|
+
deadFiles.push({
|
|
2285
|
+
path: node.path || node.id,
|
|
2286
|
+
reason: "NO_IMPORTERS",
|
|
2287
|
+
exportCount: 0,
|
|
2288
|
+
lineCount: 0
|
|
2289
|
+
});
|
|
2290
|
+
} else if (EXPORT_TYPES.has(node.type)) {
|
|
2291
|
+
const exportType = node.type === "method" ? "function" : node.type;
|
|
2292
|
+
deadExports.push({
|
|
2293
|
+
file: node.path || node.id,
|
|
2294
|
+
name: node.name,
|
|
2295
|
+
line: 0,
|
|
2296
|
+
type: exportType,
|
|
2297
|
+
isDefault: false,
|
|
2298
|
+
reason: "NO_IMPORTERS"
|
|
2299
|
+
});
|
|
2307
2300
|
}
|
|
2301
|
+
}
|
|
2302
|
+
function computeGraphReportStats(data, deadFiles, deadExports) {
|
|
2303
|
+
const reachableCount = data.reachableNodeIds instanceof Set ? data.reachableNodeIds.size : data.reachableNodeIds.length;
|
|
2304
|
+
const fileNodes = data.unreachableNodes.filter((n) => FILE_TYPES.has(n.type));
|
|
2305
|
+
const exportNodes = data.unreachableNodes.filter((n) => EXPORT_TYPES.has(n.type));
|
|
2306
|
+
const totalFiles = reachableCount + fileNodes.length;
|
|
2307
|
+
const totalExports = exportNodes.length + (reachableCount > 0 ? reachableCount : 0);
|
|
2308
|
+
return {
|
|
2309
|
+
filesAnalyzed: totalFiles,
|
|
2310
|
+
entryPointsUsed: [],
|
|
2311
|
+
totalExports,
|
|
2312
|
+
deadExportCount: deadExports.length,
|
|
2313
|
+
totalFiles,
|
|
2314
|
+
deadFileCount: deadFiles.length,
|
|
2315
|
+
estimatedDeadLines: 0
|
|
2316
|
+
};
|
|
2317
|
+
}
|
|
2318
|
+
function buildReportFromGraph(data) {
|
|
2319
|
+
const deadFiles = [];
|
|
2320
|
+
const deadExports = [];
|
|
2321
|
+
for (const node of data.unreachableNodes) {
|
|
2322
|
+
classifyUnreachableNode(node, deadFiles, deadExports);
|
|
2323
|
+
}
|
|
2324
|
+
return {
|
|
2325
|
+
deadExports,
|
|
2326
|
+
deadFiles,
|
|
2327
|
+
deadInternals: [],
|
|
2328
|
+
unusedImports: [],
|
|
2329
|
+
stats: computeGraphReportStats(data, deadFiles, deadExports)
|
|
2330
|
+
};
|
|
2331
|
+
}
|
|
2332
|
+
function buildReportFromSnapshot(snapshot) {
|
|
2308
2333
|
const reachability = buildReachabilityMap(snapshot);
|
|
2309
2334
|
const usageMap = buildExportUsageMap(snapshot);
|
|
2310
2335
|
const deadExports = findDeadExports(snapshot, usageMap, reachability);
|
|
@@ -2316,7 +2341,7 @@ async function detectDeadCode(snapshot, graphDeadCodeData) {
|
|
|
2316
2341
|
0
|
|
2317
2342
|
);
|
|
2318
2343
|
const estimatedDeadLines = deadFiles.reduce((acc, file) => acc + file.lineCount, 0);
|
|
2319
|
-
|
|
2344
|
+
return {
|
|
2320
2345
|
deadExports,
|
|
2321
2346
|
deadFiles,
|
|
2322
2347
|
deadInternals,
|
|
@@ -2331,6 +2356,9 @@ async function detectDeadCode(snapshot, graphDeadCodeData) {
|
|
|
2331
2356
|
estimatedDeadLines
|
|
2332
2357
|
}
|
|
2333
2358
|
};
|
|
2359
|
+
}
|
|
2360
|
+
async function detectDeadCode(snapshot, graphDeadCodeData) {
|
|
2361
|
+
const report = graphDeadCodeData ? buildReportFromGraph(graphDeadCodeData) : buildReportFromSnapshot(snapshot);
|
|
2334
2362
|
return Ok(report);
|
|
2335
2363
|
}
|
|
2336
2364
|
|
|
@@ -2614,48 +2642,52 @@ async function detectSizeBudgetViolations(rootDir, config) {
|
|
|
2614
2642
|
}
|
|
2615
2643
|
|
|
2616
2644
|
// src/entropy/fixers/suggestions.ts
|
|
2645
|
+
function deadFileSuggestion(file) {
|
|
2646
|
+
return {
|
|
2647
|
+
type: "delete",
|
|
2648
|
+
priority: "high",
|
|
2649
|
+
source: "dead-code",
|
|
2650
|
+
relatedIssues: [`dead-file:${file.path}`],
|
|
2651
|
+
title: `Remove dead file: ${file.path.split("/").pop()}`,
|
|
2652
|
+
description: `This file is not imported by any other file and can be safely removed.`,
|
|
2653
|
+
files: [file.path],
|
|
2654
|
+
steps: [`Delete ${file.path}`, "Run tests to verify no regressions"],
|
|
2655
|
+
whyManual: "File deletion requires verification that no dynamic imports exist"
|
|
2656
|
+
};
|
|
2657
|
+
}
|
|
2658
|
+
function deadExportSuggestion(exp) {
|
|
2659
|
+
return {
|
|
2660
|
+
type: "refactor",
|
|
2661
|
+
priority: "medium",
|
|
2662
|
+
source: "dead-code",
|
|
2663
|
+
relatedIssues: [`dead-export:${exp.file}:${exp.name}`],
|
|
2664
|
+
title: `Remove unused export: ${exp.name}`,
|
|
2665
|
+
description: `The export "${exp.name}" is not used anywhere. Consider removing it.`,
|
|
2666
|
+
files: [exp.file],
|
|
2667
|
+
steps: [`Remove export "${exp.name}" from ${exp.file}`, "Run tests to verify no regressions"],
|
|
2668
|
+
whyManual: "Export removal may affect external consumers not in scope"
|
|
2669
|
+
};
|
|
2670
|
+
}
|
|
2671
|
+
function unusedImportSuggestion(imp) {
|
|
2672
|
+
const plural = imp.specifiers.length > 1;
|
|
2673
|
+
return {
|
|
2674
|
+
type: "delete",
|
|
2675
|
+
priority: "medium",
|
|
2676
|
+
source: "dead-code",
|
|
2677
|
+
relatedIssues: [`unused-import:${imp.file}:${imp.specifiers.join(",")}`],
|
|
2678
|
+
title: `Remove unused import${plural ? "s" : ""}: ${imp.specifiers.join(", ")}`,
|
|
2679
|
+
description: `The import${plural ? "s" : ""} from "${imp.source}" ${plural ? "are" : "is"} not used.`,
|
|
2680
|
+
files: [imp.file],
|
|
2681
|
+
steps: imp.isFullyUnused ? [`Remove entire import line from ${imp.file}`] : [`Remove unused specifiers (${imp.specifiers.join(", ")}) from import statement`],
|
|
2682
|
+
whyManual: "Import removal can be auto-fixed"
|
|
2683
|
+
};
|
|
2684
|
+
}
|
|
2617
2685
|
function generateDeadCodeSuggestions(report) {
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
source: "dead-code",
|
|
2624
|
-
relatedIssues: [`dead-file:${file.path}`],
|
|
2625
|
-
title: `Remove dead file: ${file.path.split("/").pop()}`,
|
|
2626
|
-
description: `This file is not imported by any other file and can be safely removed.`,
|
|
2627
|
-
files: [file.path],
|
|
2628
|
-
steps: [`Delete ${file.path}`, "Run tests to verify no regressions"],
|
|
2629
|
-
whyManual: "File deletion requires verification that no dynamic imports exist"
|
|
2630
|
-
});
|
|
2631
|
-
}
|
|
2632
|
-
for (const exp of report.deadExports) {
|
|
2633
|
-
suggestions.push({
|
|
2634
|
-
type: "refactor",
|
|
2635
|
-
priority: "medium",
|
|
2636
|
-
source: "dead-code",
|
|
2637
|
-
relatedIssues: [`dead-export:${exp.file}:${exp.name}`],
|
|
2638
|
-
title: `Remove unused export: ${exp.name}`,
|
|
2639
|
-
description: `The export "${exp.name}" is not used anywhere. Consider removing it.`,
|
|
2640
|
-
files: [exp.file],
|
|
2641
|
-
steps: [`Remove export "${exp.name}" from ${exp.file}`, "Run tests to verify no regressions"],
|
|
2642
|
-
whyManual: "Export removal may affect external consumers not in scope"
|
|
2643
|
-
});
|
|
2644
|
-
}
|
|
2645
|
-
for (const imp of report.unusedImports) {
|
|
2646
|
-
suggestions.push({
|
|
2647
|
-
type: "delete",
|
|
2648
|
-
priority: "medium",
|
|
2649
|
-
source: "dead-code",
|
|
2650
|
-
relatedIssues: [`unused-import:${imp.file}:${imp.specifiers.join(",")}`],
|
|
2651
|
-
title: `Remove unused import${imp.specifiers.length > 1 ? "s" : ""}: ${imp.specifiers.join(", ")}`,
|
|
2652
|
-
description: `The import${imp.specifiers.length > 1 ? "s" : ""} from "${imp.source}" ${imp.specifiers.length > 1 ? "are" : "is"} not used.`,
|
|
2653
|
-
files: [imp.file],
|
|
2654
|
-
steps: imp.isFullyUnused ? [`Remove entire import line from ${imp.file}`] : [`Remove unused specifiers (${imp.specifiers.join(", ")}) from import statement`],
|
|
2655
|
-
whyManual: "Import removal can be auto-fixed"
|
|
2656
|
-
});
|
|
2657
|
-
}
|
|
2658
|
-
return suggestions;
|
|
2686
|
+
return [
|
|
2687
|
+
...report.deadFiles.map(deadFileSuggestion),
|
|
2688
|
+
...report.deadExports.map(deadExportSuggestion),
|
|
2689
|
+
...report.unusedImports.map(unusedImportSuggestion)
|
|
2690
|
+
];
|
|
2659
2691
|
}
|
|
2660
2692
|
function generateDriftSuggestions(report) {
|
|
2661
2693
|
const suggestions = [];
|
|
@@ -3098,43 +3130,55 @@ async function createBackup(filePath, backupDir) {
|
|
|
3098
3130
|
);
|
|
3099
3131
|
}
|
|
3100
3132
|
}
|
|
3133
|
+
async function applyDeleteFile(fix, config) {
|
|
3134
|
+
if (config.createBackup && config.backupDir) {
|
|
3135
|
+
const backupResult = await createBackup(fix.file, config.backupDir);
|
|
3136
|
+
if (!backupResult.ok) return Err({ fix, error: backupResult.error.message });
|
|
3137
|
+
}
|
|
3138
|
+
await unlink2(fix.file);
|
|
3139
|
+
return Ok(void 0);
|
|
3140
|
+
}
|
|
3141
|
+
async function applyDeleteLines(fix) {
|
|
3142
|
+
if (fix.line !== void 0) {
|
|
3143
|
+
const content = await readFile3(fix.file, "utf-8");
|
|
3144
|
+
const lines = content.split("\n");
|
|
3145
|
+
lines.splice(fix.line - 1, 1);
|
|
3146
|
+
await writeFile3(fix.file, lines.join("\n"));
|
|
3147
|
+
}
|
|
3148
|
+
}
|
|
3149
|
+
async function applyReplace(fix) {
|
|
3150
|
+
if (fix.oldContent && fix.newContent !== void 0) {
|
|
3151
|
+
const content = await readFile3(fix.file, "utf-8");
|
|
3152
|
+
await writeFile3(fix.file, content.replace(fix.oldContent, fix.newContent));
|
|
3153
|
+
}
|
|
3154
|
+
}
|
|
3155
|
+
async function applyInsert(fix) {
|
|
3156
|
+
if (fix.line !== void 0 && fix.newContent) {
|
|
3157
|
+
const content = await readFile3(fix.file, "utf-8");
|
|
3158
|
+
const lines = content.split("\n");
|
|
3159
|
+
lines.splice(fix.line - 1, 0, fix.newContent);
|
|
3160
|
+
await writeFile3(fix.file, lines.join("\n"));
|
|
3161
|
+
}
|
|
3162
|
+
}
|
|
3101
3163
|
async function applySingleFix(fix, config) {
|
|
3102
3164
|
if (config.dryRun) {
|
|
3103
3165
|
return Ok(fix);
|
|
3104
3166
|
}
|
|
3105
3167
|
try {
|
|
3106
3168
|
switch (fix.action) {
|
|
3107
|
-
case "delete-file":
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
if (!backupResult.ok) {
|
|
3111
|
-
return Err({ fix, error: backupResult.error.message });
|
|
3112
|
-
}
|
|
3113
|
-
}
|
|
3114
|
-
await unlink2(fix.file);
|
|
3169
|
+
case "delete-file": {
|
|
3170
|
+
const result = await applyDeleteFile(fix, config);
|
|
3171
|
+
if (!result.ok) return result;
|
|
3115
3172
|
break;
|
|
3173
|
+
}
|
|
3116
3174
|
case "delete-lines":
|
|
3117
|
-
|
|
3118
|
-
const content = await readFile3(fix.file, "utf-8");
|
|
3119
|
-
const lines = content.split("\n");
|
|
3120
|
-
lines.splice(fix.line - 1, 1);
|
|
3121
|
-
await writeFile3(fix.file, lines.join("\n"));
|
|
3122
|
-
}
|
|
3175
|
+
await applyDeleteLines(fix);
|
|
3123
3176
|
break;
|
|
3124
3177
|
case "replace":
|
|
3125
|
-
|
|
3126
|
-
const content = await readFile3(fix.file, "utf-8");
|
|
3127
|
-
const newContent = content.replace(fix.oldContent, fix.newContent);
|
|
3128
|
-
await writeFile3(fix.file, newContent);
|
|
3129
|
-
}
|
|
3178
|
+
await applyReplace(fix);
|
|
3130
3179
|
break;
|
|
3131
3180
|
case "insert":
|
|
3132
|
-
|
|
3133
|
-
const content = await readFile3(fix.file, "utf-8");
|
|
3134
|
-
const lines = content.split("\n");
|
|
3135
|
-
lines.splice(fix.line - 1, 0, fix.newContent);
|
|
3136
|
-
await writeFile3(fix.file, lines.join("\n"));
|
|
3137
|
-
}
|
|
3181
|
+
await applyInsert(fix);
|
|
3138
3182
|
break;
|
|
3139
3183
|
}
|
|
3140
3184
|
return Ok(fix);
|
|
@@ -3307,6 +3351,21 @@ function applyHotspotDowngrade(finding, hotspot) {
|
|
|
3307
3351
|
}
|
|
3308
3352
|
return finding;
|
|
3309
3353
|
}
|
|
3354
|
+
function mergeGroup(group) {
|
|
3355
|
+
if (group.length === 1) return [group[0]];
|
|
3356
|
+
const deadCode = group.find((f) => f.concern === "dead-code");
|
|
3357
|
+
const arch = group.find((f) => f.concern === "architecture");
|
|
3358
|
+
if (deadCode && arch) {
|
|
3359
|
+
return [
|
|
3360
|
+
{
|
|
3361
|
+
...deadCode,
|
|
3362
|
+
description: `${deadCode.description} (also violates architecture: ${arch.type})`,
|
|
3363
|
+
suggestion: deadCode.fixAction ? `${deadCode.fixAction} (resolves both dead code and architecture violation)` : deadCode.suggestion
|
|
3364
|
+
}
|
|
3365
|
+
];
|
|
3366
|
+
}
|
|
3367
|
+
return group;
|
|
3368
|
+
}
|
|
3310
3369
|
function deduplicateCleanupFindings(findings) {
|
|
3311
3370
|
const byFileAndLine = /* @__PURE__ */ new Map();
|
|
3312
3371
|
for (const f of findings) {
|
|
@@ -3317,21 +3376,7 @@ function deduplicateCleanupFindings(findings) {
|
|
|
3317
3376
|
}
|
|
3318
3377
|
const result = [];
|
|
3319
3378
|
for (const group of byFileAndLine.values()) {
|
|
3320
|
-
|
|
3321
|
-
result.push(group[0]);
|
|
3322
|
-
continue;
|
|
3323
|
-
}
|
|
3324
|
-
const deadCode = group.find((f) => f.concern === "dead-code");
|
|
3325
|
-
const arch = group.find((f) => f.concern === "architecture");
|
|
3326
|
-
if (deadCode && arch) {
|
|
3327
|
-
result.push({
|
|
3328
|
-
...deadCode,
|
|
3329
|
-
description: `${deadCode.description} (also violates architecture: ${arch.type})`,
|
|
3330
|
-
suggestion: deadCode.fixAction ? `${deadCode.fixAction} (resolves both dead code and architecture violation)` : deadCode.suggestion
|
|
3331
|
-
});
|
|
3332
|
-
} else {
|
|
3333
|
-
result.push(...group);
|
|
3334
|
-
}
|
|
3379
|
+
result.push(...mergeGroup(group));
|
|
3335
3380
|
}
|
|
3336
3381
|
return result;
|
|
3337
3382
|
}
|
|
@@ -3704,6 +3749,32 @@ var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", ".git"]);
|
|
|
3704
3749
|
var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx"]);
|
|
3705
3750
|
var FUNCTION_DECL_RE = /(?:export\s+)?(?:async\s+)?function\s+(\w+)/;
|
|
3706
3751
|
var CONST_DECL_RE = /(?:export\s+)?(?:const|let)\s+(\w+)\s*=/;
|
|
3752
|
+
function mergeGraphInferred(highFanInFunctions, seen) {
|
|
3753
|
+
let added = 0;
|
|
3754
|
+
for (const item of highFanInFunctions) {
|
|
3755
|
+
const key = `${item.file}::${item.function}`;
|
|
3756
|
+
if (!seen.has(key)) {
|
|
3757
|
+
seen.set(key, {
|
|
3758
|
+
file: item.file,
|
|
3759
|
+
function: item.function,
|
|
3760
|
+
source: "graph-inferred",
|
|
3761
|
+
fanIn: item.fanIn
|
|
3762
|
+
});
|
|
3763
|
+
added++;
|
|
3764
|
+
}
|
|
3765
|
+
}
|
|
3766
|
+
return added;
|
|
3767
|
+
}
|
|
3768
|
+
function isCommentOrBlank(line) {
|
|
3769
|
+
return line === "" || line === "*/" || line === "*" || line.startsWith("*") || line.startsWith("//");
|
|
3770
|
+
}
|
|
3771
|
+
function matchDeclarationName(line) {
|
|
3772
|
+
const funcMatch = line.match(FUNCTION_DECL_RE);
|
|
3773
|
+
if (funcMatch?.[1]) return funcMatch[1];
|
|
3774
|
+
const constMatch = line.match(CONST_DECL_RE);
|
|
3775
|
+
if (constMatch?.[1]) return constMatch[1];
|
|
3776
|
+
return null;
|
|
3777
|
+
}
|
|
3707
3778
|
var CriticalPathResolver = class {
|
|
3708
3779
|
projectRoot;
|
|
3709
3780
|
constructor(projectRoot) {
|
|
@@ -3716,27 +3787,12 @@ var CriticalPathResolver = class {
|
|
|
3716
3787
|
const key = `${entry.file}::${entry.function}`;
|
|
3717
3788
|
seen.set(key, entry);
|
|
3718
3789
|
}
|
|
3719
|
-
|
|
3720
|
-
if (graphData) {
|
|
3721
|
-
for (const item of graphData.highFanInFunctions) {
|
|
3722
|
-
const key = `${item.file}::${item.function}`;
|
|
3723
|
-
if (!seen.has(key)) {
|
|
3724
|
-
seen.set(key, {
|
|
3725
|
-
file: item.file,
|
|
3726
|
-
function: item.function,
|
|
3727
|
-
source: "graph-inferred",
|
|
3728
|
-
fanIn: item.fanIn
|
|
3729
|
-
});
|
|
3730
|
-
graphInferred++;
|
|
3731
|
-
}
|
|
3732
|
-
}
|
|
3733
|
-
}
|
|
3790
|
+
const graphInferred = graphData ? mergeGraphInferred(graphData.highFanInFunctions, seen) : 0;
|
|
3734
3791
|
const entries = Array.from(seen.values());
|
|
3735
|
-
const annotatedCount = annotated.length;
|
|
3736
3792
|
return {
|
|
3737
3793
|
entries,
|
|
3738
3794
|
stats: {
|
|
3739
|
-
annotated:
|
|
3795
|
+
annotated: annotated.length,
|
|
3740
3796
|
graphInferred,
|
|
3741
3797
|
total: entries.length
|
|
3742
3798
|
}
|
|
@@ -3763,6 +3819,14 @@ var CriticalPathResolver = class {
|
|
|
3763
3819
|
}
|
|
3764
3820
|
}
|
|
3765
3821
|
}
|
|
3822
|
+
resolveFunctionName(lines, fromIndex) {
|
|
3823
|
+
for (let j = fromIndex; j < lines.length; j++) {
|
|
3824
|
+
const nextLine = lines[j].trim();
|
|
3825
|
+
if (isCommentOrBlank(nextLine)) continue;
|
|
3826
|
+
return matchDeclarationName(nextLine);
|
|
3827
|
+
}
|
|
3828
|
+
return null;
|
|
3829
|
+
}
|
|
3766
3830
|
scanFile(filePath, entries) {
|
|
3767
3831
|
let content;
|
|
3768
3832
|
try {
|
|
@@ -3773,30 +3837,10 @@ var CriticalPathResolver = class {
|
|
|
3773
3837
|
const lines = content.split("\n");
|
|
3774
3838
|
const relativePath = path.relative(this.projectRoot, filePath).replace(/\\/g, "/");
|
|
3775
3839
|
for (let i = 0; i < lines.length; i++) {
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
if (nextLine === "" || nextLine === "*/" || nextLine === "*") continue;
|
|
3781
|
-
if (nextLine.startsWith("*") || nextLine.startsWith("//")) continue;
|
|
3782
|
-
const funcMatch = nextLine.match(FUNCTION_DECL_RE);
|
|
3783
|
-
if (funcMatch && funcMatch[1]) {
|
|
3784
|
-
entries.push({
|
|
3785
|
-
file: relativePath,
|
|
3786
|
-
function: funcMatch[1],
|
|
3787
|
-
source: "annotation"
|
|
3788
|
-
});
|
|
3789
|
-
} else {
|
|
3790
|
-
const constMatch = nextLine.match(CONST_DECL_RE);
|
|
3791
|
-
if (constMatch && constMatch[1]) {
|
|
3792
|
-
entries.push({
|
|
3793
|
-
file: relativePath,
|
|
3794
|
-
function: constMatch[1],
|
|
3795
|
-
source: "annotation"
|
|
3796
|
-
});
|
|
3797
|
-
}
|
|
3798
|
-
}
|
|
3799
|
-
break;
|
|
3840
|
+
if (!lines[i].includes("@perf-critical")) continue;
|
|
3841
|
+
const fnName = this.resolveFunctionName(lines, i + 1);
|
|
3842
|
+
if (fnName) {
|
|
3843
|
+
entries.push({ file: relativePath, function: fnName, source: "annotation" });
|
|
3800
3844
|
}
|
|
3801
3845
|
}
|
|
3802
3846
|
}
|
|
@@ -3951,14 +3995,19 @@ function detectFileStatus(part) {
|
|
|
3951
3995
|
if (part.includes("rename from")) return "renamed";
|
|
3952
3996
|
return "modified";
|
|
3953
3997
|
}
|
|
3954
|
-
function
|
|
3998
|
+
function parseDiffHeader(part) {
|
|
3955
3999
|
if (!part.trim()) return null;
|
|
3956
4000
|
const headerMatch = /diff --git a\/(.+?) b\/(.+?)(?:\n|$)/.exec(part);
|
|
3957
4001
|
if (!headerMatch || !headerMatch[2]) return null;
|
|
3958
|
-
|
|
4002
|
+
return headerMatch[2];
|
|
4003
|
+
}
|
|
4004
|
+
function parseDiffPart(part) {
|
|
4005
|
+
const path34 = parseDiffHeader(part);
|
|
4006
|
+
if (!path34) return null;
|
|
4007
|
+
const additionRegex = /^\+(?!\+\+)/gm;
|
|
3959
4008
|
const deletionRegex = /^-(?!--)/gm;
|
|
3960
4009
|
return {
|
|
3961
|
-
path:
|
|
4010
|
+
path: path34,
|
|
3962
4011
|
status: detectFileStatus(part),
|
|
3963
4012
|
additions: (part.match(additionRegex) || []).length,
|
|
3964
4013
|
deletions: (part.match(deletionRegex) || []).length
|
|
@@ -3980,100 +4029,136 @@ function parseDiff(diff2) {
|
|
|
3980
4029
|
});
|
|
3981
4030
|
}
|
|
3982
4031
|
}
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
4032
|
+
function checkForbiddenPatterns(diff2, forbiddenPatterns, nextId) {
|
|
4033
|
+
const items = [];
|
|
4034
|
+
if (!forbiddenPatterns) return items;
|
|
4035
|
+
for (const forbidden of forbiddenPatterns) {
|
|
4036
|
+
const pattern = typeof forbidden.pattern === "string" ? new RegExp(forbidden.pattern, "g") : forbidden.pattern;
|
|
4037
|
+
if (!pattern.test(diff2)) continue;
|
|
4038
|
+
items.push({
|
|
4039
|
+
id: nextId(),
|
|
4040
|
+
category: "diff",
|
|
4041
|
+
check: `Forbidden pattern: ${forbidden.pattern}`,
|
|
4042
|
+
passed: false,
|
|
4043
|
+
severity: forbidden.severity,
|
|
4044
|
+
details: forbidden.message,
|
|
4045
|
+
suggestion: `Remove occurrences of ${forbidden.pattern}`
|
|
4046
|
+
});
|
|
3986
4047
|
}
|
|
4048
|
+
return items;
|
|
4049
|
+
}
|
|
4050
|
+
function checkMaxChangedFiles(files, maxChangedFiles, nextId) {
|
|
4051
|
+
if (!maxChangedFiles || files.length <= maxChangedFiles) return [];
|
|
4052
|
+
return [
|
|
4053
|
+
{
|
|
4054
|
+
id: nextId(),
|
|
4055
|
+
category: "diff",
|
|
4056
|
+
check: `PR size: ${files.length} files changed`,
|
|
4057
|
+
passed: false,
|
|
4058
|
+
severity: "warning",
|
|
4059
|
+
details: `This PR changes ${files.length} files, which exceeds the recommended maximum of ${maxChangedFiles}`,
|
|
4060
|
+
suggestion: "Consider breaking this into smaller PRs"
|
|
4061
|
+
}
|
|
4062
|
+
];
|
|
4063
|
+
}
|
|
4064
|
+
function checkFileSizes(files, maxFileSize, nextId) {
|
|
3987
4065
|
const items = [];
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4066
|
+
if (!maxFileSize) return items;
|
|
4067
|
+
for (const file of files) {
|
|
4068
|
+
const totalLines = file.additions + file.deletions;
|
|
4069
|
+
if (totalLines <= maxFileSize) continue;
|
|
4070
|
+
items.push({
|
|
4071
|
+
id: nextId(),
|
|
4072
|
+
category: "diff",
|
|
4073
|
+
check: `File size: ${file.path}`,
|
|
4074
|
+
passed: false,
|
|
4075
|
+
severity: "warning",
|
|
4076
|
+
details: `File has ${totalLines} lines changed, exceeding limit of ${maxFileSize}`,
|
|
4077
|
+
file: file.path,
|
|
4078
|
+
suggestion: "Consider splitting this file into smaller modules"
|
|
4079
|
+
});
|
|
4080
|
+
}
|
|
4081
|
+
return items;
|
|
4082
|
+
}
|
|
4083
|
+
function checkTestCoverageGraph(files, graphImpactData) {
|
|
4084
|
+
const items = [];
|
|
4085
|
+
for (const file of files) {
|
|
4086
|
+
if (file.status !== "added" || !file.path.endsWith(".ts") || file.path.includes(".test.")) {
|
|
4087
|
+
continue;
|
|
4003
4088
|
}
|
|
4089
|
+
const hasGraphTest = graphImpactData.affectedTests.some((t) => t.coversFile === file.path);
|
|
4090
|
+
if (hasGraphTest) continue;
|
|
4091
|
+
items.push({
|
|
4092
|
+
id: `test-coverage-${file.path}`,
|
|
4093
|
+
category: "diff",
|
|
4094
|
+
check: "Test coverage (graph)",
|
|
4095
|
+
passed: false,
|
|
4096
|
+
severity: "warning",
|
|
4097
|
+
details: `New file ${file.path} has no test file linked in the graph`,
|
|
4098
|
+
file: file.path
|
|
4099
|
+
});
|
|
4004
4100
|
}
|
|
4005
|
-
|
|
4101
|
+
return items;
|
|
4102
|
+
}
|
|
4103
|
+
function checkTestCoverageFilename(files, nextId) {
|
|
4104
|
+
const items = [];
|
|
4105
|
+
const addedSourceFiles = files.filter(
|
|
4106
|
+
(f) => f.status === "added" && f.path.endsWith(".ts") && !f.path.includes(".test.")
|
|
4107
|
+
);
|
|
4108
|
+
const testFiles = files.filter((f) => f.path.includes(".test."));
|
|
4109
|
+
for (const sourceFile of addedSourceFiles) {
|
|
4110
|
+
const expectedTestPath = sourceFile.path.replace(".ts", ".test.ts");
|
|
4111
|
+
const hasTest = testFiles.some(
|
|
4112
|
+
(t) => t.path.includes(expectedTestPath) || t.path.includes(sourceFile.path.replace(".ts", ""))
|
|
4113
|
+
);
|
|
4114
|
+
if (hasTest) continue;
|
|
4006
4115
|
items.push({
|
|
4007
|
-
id:
|
|
4116
|
+
id: nextId(),
|
|
4008
4117
|
category: "diff",
|
|
4009
|
-
check: `
|
|
4118
|
+
check: `Test coverage: ${sourceFile.path}`,
|
|
4010
4119
|
passed: false,
|
|
4011
4120
|
severity: "warning",
|
|
4012
|
-
details:
|
|
4013
|
-
|
|
4121
|
+
details: "New source file added without corresponding test file",
|
|
4122
|
+
file: sourceFile.path,
|
|
4123
|
+
suggestion: `Add tests in ${expectedTestPath}`
|
|
4014
4124
|
});
|
|
4015
4125
|
}
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
check: `File size: ${file.path}`,
|
|
4024
|
-
passed: false,
|
|
4025
|
-
severity: "warning",
|
|
4026
|
-
details: `File has ${totalLines} lines changed, exceeding limit of ${options.maxFileSize}`,
|
|
4027
|
-
file: file.path,
|
|
4028
|
-
suggestion: "Consider splitting this file into smaller modules"
|
|
4029
|
-
});
|
|
4030
|
-
}
|
|
4126
|
+
return items;
|
|
4127
|
+
}
|
|
4128
|
+
function checkDocCoverage2(files, graphImpactData) {
|
|
4129
|
+
const items = [];
|
|
4130
|
+
for (const file of files) {
|
|
4131
|
+
if (file.status !== "modified" || !file.path.endsWith(".ts") || file.path.includes(".test.")) {
|
|
4132
|
+
continue;
|
|
4031
4133
|
}
|
|
4134
|
+
const hasDoc = graphImpactData.affectedDocs.some((d) => d.documentsFile === file.path);
|
|
4135
|
+
if (hasDoc) continue;
|
|
4136
|
+
items.push({
|
|
4137
|
+
id: `doc-coverage-${file.path}`,
|
|
4138
|
+
category: "diff",
|
|
4139
|
+
check: "Documentation coverage (graph)",
|
|
4140
|
+
passed: true,
|
|
4141
|
+
severity: "info",
|
|
4142
|
+
details: `Modified file ${file.path} has no documentation linked in the graph`,
|
|
4143
|
+
file: file.path
|
|
4144
|
+
});
|
|
4145
|
+
}
|
|
4146
|
+
return items;
|
|
4147
|
+
}
|
|
4148
|
+
async function analyzeDiff(changes, options, graphImpactData) {
|
|
4149
|
+
if (!options?.enabled) {
|
|
4150
|
+
return Ok([]);
|
|
4032
4151
|
}
|
|
4152
|
+
let itemId = 0;
|
|
4153
|
+
const nextId = () => `diff-${++itemId}`;
|
|
4154
|
+
const items = [
|
|
4155
|
+
...checkForbiddenPatterns(changes.diff, options.forbiddenPatterns, nextId),
|
|
4156
|
+
...checkMaxChangedFiles(changes.files, options.maxChangedFiles, nextId),
|
|
4157
|
+
...checkFileSizes(changes.files, options.maxFileSize, nextId)
|
|
4158
|
+
];
|
|
4033
4159
|
if (options.checkTestCoverage) {
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
if (file.status === "added" && file.path.endsWith(".ts") && !file.path.includes(".test.")) {
|
|
4037
|
-
const hasGraphTest = graphImpactData.affectedTests.some(
|
|
4038
|
-
(t) => t.coversFile === file.path
|
|
4039
|
-
);
|
|
4040
|
-
if (!hasGraphTest) {
|
|
4041
|
-
items.push({
|
|
4042
|
-
id: `test-coverage-${file.path}`,
|
|
4043
|
-
category: "diff",
|
|
4044
|
-
check: "Test coverage (graph)",
|
|
4045
|
-
passed: false,
|
|
4046
|
-
severity: "warning",
|
|
4047
|
-
details: `New file ${file.path} has no test file linked in the graph`,
|
|
4048
|
-
file: file.path
|
|
4049
|
-
});
|
|
4050
|
-
}
|
|
4051
|
-
}
|
|
4052
|
-
}
|
|
4053
|
-
} else {
|
|
4054
|
-
const addedSourceFiles = changes.files.filter(
|
|
4055
|
-
(f) => f.status === "added" && f.path.endsWith(".ts") && !f.path.includes(".test.")
|
|
4056
|
-
);
|
|
4057
|
-
const testFiles = changes.files.filter((f) => f.path.includes(".test."));
|
|
4058
|
-
for (const sourceFile of addedSourceFiles) {
|
|
4059
|
-
const expectedTestPath = sourceFile.path.replace(".ts", ".test.ts");
|
|
4060
|
-
const hasTest = testFiles.some(
|
|
4061
|
-
(t) => t.path.includes(expectedTestPath) || t.path.includes(sourceFile.path.replace(".ts", ""))
|
|
4062
|
-
);
|
|
4063
|
-
if (!hasTest) {
|
|
4064
|
-
items.push({
|
|
4065
|
-
id: `diff-${++itemId}`,
|
|
4066
|
-
category: "diff",
|
|
4067
|
-
check: `Test coverage: ${sourceFile.path}`,
|
|
4068
|
-
passed: false,
|
|
4069
|
-
severity: "warning",
|
|
4070
|
-
details: "New source file added without corresponding test file",
|
|
4071
|
-
file: sourceFile.path,
|
|
4072
|
-
suggestion: `Add tests in ${expectedTestPath}`
|
|
4073
|
-
});
|
|
4074
|
-
}
|
|
4075
|
-
}
|
|
4076
|
-
}
|
|
4160
|
+
const coverageItems = graphImpactData ? checkTestCoverageGraph(changes.files, graphImpactData) : checkTestCoverageFilename(changes.files, nextId);
|
|
4161
|
+
items.push(...coverageItems);
|
|
4077
4162
|
}
|
|
4078
4163
|
if (graphImpactData && graphImpactData.impactScope > 20) {
|
|
4079
4164
|
items.push({
|
|
@@ -4086,22 +4171,7 @@ async function analyzeDiff(changes, options, graphImpactData) {
|
|
|
4086
4171
|
});
|
|
4087
4172
|
}
|
|
4088
4173
|
if (graphImpactData) {
|
|
4089
|
-
|
|
4090
|
-
if (file.status === "modified" && file.path.endsWith(".ts") && !file.path.includes(".test.")) {
|
|
4091
|
-
const hasDoc = graphImpactData.affectedDocs.some((d) => d.documentsFile === file.path);
|
|
4092
|
-
if (!hasDoc) {
|
|
4093
|
-
items.push({
|
|
4094
|
-
id: `doc-coverage-${file.path}`,
|
|
4095
|
-
category: "diff",
|
|
4096
|
-
check: "Documentation coverage (graph)",
|
|
4097
|
-
passed: true,
|
|
4098
|
-
severity: "info",
|
|
4099
|
-
details: `Modified file ${file.path} has no documentation linked in the graph`,
|
|
4100
|
-
file: file.path
|
|
4101
|
-
});
|
|
4102
|
-
}
|
|
4103
|
-
}
|
|
4104
|
-
}
|
|
4174
|
+
items.push(...checkDocCoverage2(changes.files, graphImpactData));
|
|
4105
4175
|
}
|
|
4106
4176
|
return Ok(items);
|
|
4107
4177
|
}
|
|
@@ -4634,10 +4704,26 @@ function hasMatchingViolation(rule, violationsByCategory) {
|
|
|
4634
4704
|
}
|
|
4635
4705
|
|
|
4636
4706
|
// src/architecture/detect-stale.ts
|
|
4707
|
+
function evaluateStaleNode(node, now, cutoff) {
|
|
4708
|
+
const lastViolatedAt = node.lastViolatedAt ?? null;
|
|
4709
|
+
const createdAt = node.createdAt;
|
|
4710
|
+
const comparisonTimestamp = lastViolatedAt ?? createdAt;
|
|
4711
|
+
if (!comparisonTimestamp) return null;
|
|
4712
|
+
const timestampMs = new Date(comparisonTimestamp).getTime();
|
|
4713
|
+
if (timestampMs >= cutoff) return null;
|
|
4714
|
+
const daysSince = Math.floor((now - timestampMs) / (24 * 60 * 60 * 1e3));
|
|
4715
|
+
return {
|
|
4716
|
+
id: node.id,
|
|
4717
|
+
category: node.category,
|
|
4718
|
+
description: node.name ?? "",
|
|
4719
|
+
scope: node.scope ?? "project",
|
|
4720
|
+
lastViolatedAt,
|
|
4721
|
+
daysSinceLastViolation: daysSince
|
|
4722
|
+
};
|
|
4723
|
+
}
|
|
4637
4724
|
function detectStaleConstraints(store, windowDays = 30, category) {
|
|
4638
4725
|
const now = Date.now();
|
|
4639
|
-
const
|
|
4640
|
-
const cutoff = now - windowMs;
|
|
4726
|
+
const cutoff = now - windowDays * 24 * 60 * 60 * 1e3;
|
|
4641
4727
|
let constraints = store.findNodes({ type: "constraint" });
|
|
4642
4728
|
if (category) {
|
|
4643
4729
|
constraints = constraints.filter((n) => n.category === category);
|
|
@@ -4645,28 +4731,23 @@ function detectStaleConstraints(store, windowDays = 30, category) {
|
|
|
4645
4731
|
const totalConstraints = constraints.length;
|
|
4646
4732
|
const staleConstraints = [];
|
|
4647
4733
|
for (const node of constraints) {
|
|
4648
|
-
const
|
|
4649
|
-
|
|
4650
|
-
const comparisonTimestamp = lastViolatedAt ?? createdAt;
|
|
4651
|
-
if (!comparisonTimestamp) continue;
|
|
4652
|
-
const timestampMs = new Date(comparisonTimestamp).getTime();
|
|
4653
|
-
if (timestampMs < cutoff) {
|
|
4654
|
-
const daysSince = Math.floor((now - timestampMs) / (24 * 60 * 60 * 1e3));
|
|
4655
|
-
staleConstraints.push({
|
|
4656
|
-
id: node.id,
|
|
4657
|
-
category: node.category,
|
|
4658
|
-
description: node.name ?? "",
|
|
4659
|
-
scope: node.scope ?? "project",
|
|
4660
|
-
lastViolatedAt,
|
|
4661
|
-
daysSinceLastViolation: daysSince
|
|
4662
|
-
});
|
|
4663
|
-
}
|
|
4734
|
+
const entry = evaluateStaleNode(node, now, cutoff);
|
|
4735
|
+
if (entry) staleConstraints.push(entry);
|
|
4664
4736
|
}
|
|
4665
4737
|
staleConstraints.sort((a, b) => b.daysSinceLastViolation - a.daysSinceLastViolation);
|
|
4666
4738
|
return { staleConstraints, totalConstraints, windowDays };
|
|
4667
4739
|
}
|
|
4668
4740
|
|
|
4669
4741
|
// src/architecture/config.ts
|
|
4742
|
+
function mergeThresholdCategory(projectValue2, moduleValue) {
|
|
4743
|
+
if (projectValue2 !== void 0 && typeof projectValue2 === "object" && !Array.isArray(projectValue2) && typeof moduleValue === "object" && !Array.isArray(moduleValue)) {
|
|
4744
|
+
return {
|
|
4745
|
+
...projectValue2,
|
|
4746
|
+
...moduleValue
|
|
4747
|
+
};
|
|
4748
|
+
}
|
|
4749
|
+
return moduleValue;
|
|
4750
|
+
}
|
|
4670
4751
|
function resolveThresholds(scope, config) {
|
|
4671
4752
|
const projectThresholds = {};
|
|
4672
4753
|
for (const [key, val] of Object.entries(config.thresholds)) {
|
|
@@ -4682,14 +4763,7 @@ function resolveThresholds(scope, config) {
|
|
|
4682
4763
|
const merged = { ...projectThresholds };
|
|
4683
4764
|
for (const [category, moduleValue] of Object.entries(moduleOverrides)) {
|
|
4684
4765
|
const projectValue2 = projectThresholds[category];
|
|
4685
|
-
|
|
4686
|
-
merged[category] = {
|
|
4687
|
-
...projectValue2,
|
|
4688
|
-
...moduleValue
|
|
4689
|
-
};
|
|
4690
|
-
} else {
|
|
4691
|
-
merged[category] = moduleValue;
|
|
4692
|
-
}
|
|
4766
|
+
merged[category] = mergeThresholdCategory(projectValue2, moduleValue);
|
|
4693
4767
|
}
|
|
4694
4768
|
return merged;
|
|
4695
4769
|
}
|
|
@@ -5313,18 +5387,10 @@ var PredictionEngine = class {
|
|
|
5313
5387
|
*/
|
|
5314
5388
|
predict(options) {
|
|
5315
5389
|
const opts = this.resolveOptions(options);
|
|
5316
|
-
const
|
|
5317
|
-
const snapshots = timeline.snapshots;
|
|
5318
|
-
if (snapshots.length < 3) {
|
|
5319
|
-
throw new Error(
|
|
5320
|
-
`PredictionEngine requires at least 3 snapshots, got ${snapshots.length}. Run "harness snapshot" to capture more data points.`
|
|
5321
|
-
);
|
|
5322
|
-
}
|
|
5390
|
+
const snapshots = this.loadValidatedSnapshots();
|
|
5323
5391
|
const thresholds = this.resolveThresholds(opts);
|
|
5324
5392
|
const categoriesToProcess = opts.categories ?? [...ALL_CATEGORIES2];
|
|
5325
|
-
const firstDate =
|
|
5326
|
-
const lastSnapshot = snapshots[snapshots.length - 1];
|
|
5327
|
-
const currentT = (new Date(lastSnapshot.capturedAt).getTime() - firstDate) / (7 * 24 * 60 * 60 * 1e3);
|
|
5393
|
+
const { firstDate, lastSnapshot, currentT } = this.computeTimeOffsets(snapshots);
|
|
5328
5394
|
const baselines = this.computeBaselines(
|
|
5329
5395
|
categoriesToProcess,
|
|
5330
5396
|
thresholds,
|
|
@@ -5335,27 +5401,32 @@ var PredictionEngine = class {
|
|
|
5335
5401
|
);
|
|
5336
5402
|
const specImpacts = this.computeSpecImpacts(opts);
|
|
5337
5403
|
const categories = this.computeAdjustedForecasts(baselines, thresholds, specImpacts, currentT);
|
|
5338
|
-
const
|
|
5339
|
-
categories,
|
|
5340
|
-
opts.horizon
|
|
5341
|
-
);
|
|
5342
|
-
const stabilityForecast = this.computeStabilityForecast(
|
|
5343
|
-
categories,
|
|
5344
|
-
thresholds,
|
|
5345
|
-
snapshots
|
|
5346
|
-
);
|
|
5404
|
+
const adjustedCategories = categories;
|
|
5347
5405
|
return {
|
|
5348
5406
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5349
5407
|
snapshotsUsed: snapshots.length,
|
|
5350
|
-
timelineRange: {
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
stabilityForecast,
|
|
5355
|
-
categories,
|
|
5356
|
-
warnings
|
|
5408
|
+
timelineRange: { from: snapshots[0].capturedAt, to: lastSnapshot.capturedAt },
|
|
5409
|
+
stabilityForecast: this.computeStabilityForecast(adjustedCategories, thresholds, snapshots),
|
|
5410
|
+
categories: adjustedCategories,
|
|
5411
|
+
warnings: this.generateWarnings(adjustedCategories, opts.horizon)
|
|
5357
5412
|
};
|
|
5358
5413
|
}
|
|
5414
|
+
loadValidatedSnapshots() {
|
|
5415
|
+
const timeline = this.timelineManager.load();
|
|
5416
|
+
const snapshots = timeline.snapshots;
|
|
5417
|
+
if (snapshots.length < 3) {
|
|
5418
|
+
throw new Error(
|
|
5419
|
+
`PredictionEngine requires at least 3 snapshots, got ${snapshots.length}. Run "harness snapshot" to capture more data points.`
|
|
5420
|
+
);
|
|
5421
|
+
}
|
|
5422
|
+
return snapshots;
|
|
5423
|
+
}
|
|
5424
|
+
computeTimeOffsets(snapshots) {
|
|
5425
|
+
const firstDate = new Date(snapshots[0].capturedAt).getTime();
|
|
5426
|
+
const lastSnapshot = snapshots[snapshots.length - 1];
|
|
5427
|
+
const currentT = (new Date(lastSnapshot.capturedAt).getTime() - firstDate) / (7 * 24 * 60 * 60 * 1e3);
|
|
5428
|
+
return { firstDate, lastSnapshot, currentT };
|
|
5429
|
+
}
|
|
5359
5430
|
// --- Private helpers ---
|
|
5360
5431
|
resolveOptions(options) {
|
|
5361
5432
|
return {
|
|
@@ -5522,31 +5593,40 @@ var PredictionEngine = class {
|
|
|
5522
5593
|
for (const category of ALL_CATEGORIES2) {
|
|
5523
5594
|
const af = categories[category];
|
|
5524
5595
|
if (!af) continue;
|
|
5525
|
-
const
|
|
5526
|
-
|
|
5527
|
-
|
|
5528
|
-
|
|
5529
|
-
|
|
5530
|
-
|
|
5531
|
-
|
|
5532
|
-
|
|
5533
|
-
} else if (crossing <= horizon) {
|
|
5534
|
-
severity = "info";
|
|
5535
|
-
}
|
|
5536
|
-
if (severity) {
|
|
5537
|
-
const contributingNames = af.contributingFeatures.map((f) => f.name);
|
|
5538
|
-
warnings.push({
|
|
5539
|
-
severity,
|
|
5540
|
-
category,
|
|
5541
|
-
message: `${category} projected to exceed threshold (~${crossing}w, ${forecast.confidence} confidence)`,
|
|
5542
|
-
weeksUntil: crossing,
|
|
5543
|
-
confidence: forecast.confidence,
|
|
5544
|
-
contributingFeatures: contributingNames
|
|
5545
|
-
});
|
|
5546
|
-
}
|
|
5596
|
+
const warning = this.buildCategoryWarning(
|
|
5597
|
+
category,
|
|
5598
|
+
af,
|
|
5599
|
+
criticalWindow,
|
|
5600
|
+
warningWindow,
|
|
5601
|
+
horizon
|
|
5602
|
+
);
|
|
5603
|
+
if (warning) warnings.push(warning);
|
|
5547
5604
|
}
|
|
5548
5605
|
return warnings;
|
|
5549
5606
|
}
|
|
5607
|
+
buildCategoryWarning(category, af, criticalWindow, warningWindow, horizon) {
|
|
5608
|
+
const forecast = af.adjusted;
|
|
5609
|
+
const crossing = forecast.thresholdCrossingWeeks;
|
|
5610
|
+
if (crossing === null || crossing <= 0) return null;
|
|
5611
|
+
const isHighConfidence = forecast.confidence === "high" || forecast.confidence === "medium";
|
|
5612
|
+
let severity = null;
|
|
5613
|
+
if (crossing <= criticalWindow && isHighConfidence) {
|
|
5614
|
+
severity = "critical";
|
|
5615
|
+
} else if (crossing <= warningWindow && isHighConfidence) {
|
|
5616
|
+
severity = "warning";
|
|
5617
|
+
} else if (crossing <= horizon) {
|
|
5618
|
+
severity = "info";
|
|
5619
|
+
}
|
|
5620
|
+
if (!severity) return null;
|
|
5621
|
+
return {
|
|
5622
|
+
severity,
|
|
5623
|
+
category,
|
|
5624
|
+
message: `${category} projected to exceed threshold (~${crossing}w, ${forecast.confidence} confidence)`,
|
|
5625
|
+
weeksUntil: crossing,
|
|
5626
|
+
confidence: forecast.confidence,
|
|
5627
|
+
contributingFeatures: af.contributingFeatures.map((f) => f.name)
|
|
5628
|
+
};
|
|
5629
|
+
}
|
|
5550
5630
|
/**
|
|
5551
5631
|
* Compute composite stability forecast by projecting per-category values
|
|
5552
5632
|
* forward and computing stability scores at each horizon.
|
|
@@ -5615,14 +5695,9 @@ var PredictionEngine = class {
|
|
|
5615
5695
|
const raw = fs5.readFileSync(roadmapPath, "utf-8");
|
|
5616
5696
|
const parseResult = parseRoadmap(raw);
|
|
5617
5697
|
if (!parseResult.ok) return null;
|
|
5618
|
-
const features =
|
|
5619
|
-
|
|
5620
|
-
|
|
5621
|
-
if (feature.status === "planned" || feature.status === "in-progress") {
|
|
5622
|
-
features.push({ name: feature.name, spec: feature.spec });
|
|
5623
|
-
}
|
|
5624
|
-
}
|
|
5625
|
-
}
|
|
5698
|
+
const features = parseResult.value.milestones.flatMap(
|
|
5699
|
+
(m) => m.features.filter((f) => f.status === "planned" || f.status === "in-progress").map((f) => ({ name: f.name, spec: f.spec }))
|
|
5700
|
+
);
|
|
5626
5701
|
if (features.length === 0) return null;
|
|
5627
5702
|
return this.estimator.estimateAll(features);
|
|
5628
5703
|
} catch {
|
|
@@ -6273,7 +6348,36 @@ async function saveState(projectPath, state, stream, session) {
|
|
|
6273
6348
|
// src/state/learnings-content.ts
|
|
6274
6349
|
import * as fs11 from "fs";
|
|
6275
6350
|
import * as path8 from "path";
|
|
6276
|
-
import * as
|
|
6351
|
+
import * as crypto2 from "crypto";
|
|
6352
|
+
|
|
6353
|
+
// src/compaction/envelope.ts
|
|
6354
|
+
function estimateTokens(content) {
|
|
6355
|
+
return Math.ceil(content.length / 4);
|
|
6356
|
+
}
|
|
6357
|
+
function serializeEnvelope(envelope) {
|
|
6358
|
+
const { meta, sections } = envelope;
|
|
6359
|
+
const strategyLabel = meta.strategy.length > 0 ? meta.strategy.join("+") : "none";
|
|
6360
|
+
let cacheLabel = "";
|
|
6361
|
+
if (meta.cached) {
|
|
6362
|
+
if (meta.cacheReadTokens != null && meta.cacheInputTokens != null && meta.cacheInputTokens > 0) {
|
|
6363
|
+
const hitPct = Math.round(meta.cacheReadTokens / meta.cacheInputTokens * 100);
|
|
6364
|
+
const readFormatted = meta.cacheReadTokens >= 1e3 ? (meta.cacheReadTokens / 1e3).toFixed(1) + "K" : String(meta.cacheReadTokens);
|
|
6365
|
+
cacheLabel = ` [cached | cache: ${readFormatted} read, ${hitPct}% hit]`;
|
|
6366
|
+
} else {
|
|
6367
|
+
cacheLabel = " [cached]";
|
|
6368
|
+
}
|
|
6369
|
+
}
|
|
6370
|
+
const header = `<!-- packed: ${strategyLabel} | ${meta.originalTokenEstimate}\u2192${meta.compactedTokenEstimate} tokens (-${meta.reductionPct}%)${cacheLabel} -->`;
|
|
6371
|
+
if (sections.length === 0) {
|
|
6372
|
+
return header;
|
|
6373
|
+
}
|
|
6374
|
+
const body = sections.map((section) => `### [${section.source}]
|
|
6375
|
+
${section.content}`).join("\n\n");
|
|
6376
|
+
return `${header}
|
|
6377
|
+
${body}`;
|
|
6378
|
+
}
|
|
6379
|
+
|
|
6380
|
+
// src/state/learnings-content.ts
|
|
6277
6381
|
function parseFrontmatter2(line) {
|
|
6278
6382
|
const match = line.match(/^<!--\s+hash:([a-f0-9]+)(?:\s+tags:([^\s]+))?\s+-->/);
|
|
6279
6383
|
if (!match) return null;
|
|
@@ -6301,7 +6405,7 @@ function extractIndexEntry(entry) {
|
|
|
6301
6405
|
};
|
|
6302
6406
|
}
|
|
6303
6407
|
function computeEntryHash(text) {
|
|
6304
|
-
return
|
|
6408
|
+
return crypto2.createHash("sha256").update(text).digest("hex").slice(0, 8);
|
|
6305
6409
|
}
|
|
6306
6410
|
function normalizeLearningContent(text) {
|
|
6307
6411
|
let normalized = text;
|
|
@@ -6316,7 +6420,7 @@ function normalizeLearningContent(text) {
|
|
|
6316
6420
|
return normalized;
|
|
6317
6421
|
}
|
|
6318
6422
|
function computeContentHash(text) {
|
|
6319
|
-
return
|
|
6423
|
+
return crypto2.createHash("sha256").update(text).digest("hex").slice(0, 16);
|
|
6320
6424
|
}
|
|
6321
6425
|
function loadContentHashes(stateDir) {
|
|
6322
6426
|
const hashesPath = path8.join(stateDir, CONTENT_HASHES_FILE);
|
|
@@ -6377,9 +6481,6 @@ function analyzeLearningPatterns(entries) {
|
|
|
6377
6481
|
}
|
|
6378
6482
|
return patterns.sort((a, b) => b.count - a.count);
|
|
6379
6483
|
}
|
|
6380
|
-
function estimateTokens(text) {
|
|
6381
|
-
return Math.ceil(text.length / 4);
|
|
6382
|
-
}
|
|
6383
6484
|
function scoreRelevance(entry, intent) {
|
|
6384
6485
|
if (!intent || intent.trim() === "") return 0;
|
|
6385
6486
|
const intentWords = intent.toLowerCase().split(/\s+/).filter((w) => w.length > 2);
|
|
@@ -6456,7 +6557,7 @@ async function loadRelevantLearnings(projectPath, skillName, stream, session) {
|
|
|
6456
6557
|
// src/state/learnings.ts
|
|
6457
6558
|
import * as fs13 from "fs";
|
|
6458
6559
|
import * as path10 from "path";
|
|
6459
|
-
import * as
|
|
6560
|
+
import * as crypto3 from "crypto";
|
|
6460
6561
|
async function appendLearning(projectPath, learning, skillName, outcome, stream, session) {
|
|
6461
6562
|
try {
|
|
6462
6563
|
const dirResult = await getStateDir(projectPath, stream, session);
|
|
@@ -6493,7 +6594,7 @@ async function appendLearning(projectPath, learning, skillName, outcome, stream,
|
|
|
6493
6594
|
} else {
|
|
6494
6595
|
bulletLine = `- **${timestamp}:** ${learning}`;
|
|
6495
6596
|
}
|
|
6496
|
-
const hash =
|
|
6597
|
+
const hash = crypto3.createHash("sha256").update(bulletLine).digest("hex").slice(0, 8);
|
|
6497
6598
|
const tagsStr = fmTags.length > 0 ? ` tags:${fmTags.join(",")}` : "";
|
|
6498
6599
|
const frontmatter = `<!-- hash:${hash}${tagsStr} -->`;
|
|
6499
6600
|
const entry = `
|
|
@@ -7204,7 +7305,7 @@ async function updateSessionEntryStatus(projectPath, sessionSlug, section, entry
|
|
|
7204
7305
|
}
|
|
7205
7306
|
function generateEntryId() {
|
|
7206
7307
|
const timestamp = Date.now().toString(36);
|
|
7207
|
-
const random =
|
|
7308
|
+
const random = Buffer.from(crypto.getRandomValues(new Uint8Array(4))).toString("hex");
|
|
7208
7309
|
return `${timestamp}-${random}`;
|
|
7209
7310
|
}
|
|
7210
7311
|
|
|
@@ -7349,15 +7450,25 @@ async function loadEvents(projectPath, options) {
|
|
|
7349
7450
|
);
|
|
7350
7451
|
}
|
|
7351
7452
|
}
|
|
7453
|
+
function phaseTransitionFields(data) {
|
|
7454
|
+
return {
|
|
7455
|
+
from: data?.from ?? "?",
|
|
7456
|
+
to: data?.to ?? "?",
|
|
7457
|
+
suffix: data?.taskCount ? ` (${data.taskCount} tasks)` : ""
|
|
7458
|
+
};
|
|
7459
|
+
}
|
|
7352
7460
|
function formatPhaseTransition(event) {
|
|
7353
7461
|
const data = event.data;
|
|
7354
|
-
const
|
|
7355
|
-
return `phase: ${
|
|
7462
|
+
const { from, to, suffix } = phaseTransitionFields(data);
|
|
7463
|
+
return `phase: ${from} -> ${to}${suffix}`;
|
|
7464
|
+
}
|
|
7465
|
+
function formatGateChecks(checks) {
|
|
7466
|
+
return checks?.map((c) => `${c.name} ${c.passed ? "Y" : "N"}`).join(", ");
|
|
7356
7467
|
}
|
|
7357
7468
|
function formatGateResult(event) {
|
|
7358
7469
|
const data = event.data;
|
|
7359
7470
|
const status = data?.passed ? "passed" : "failed";
|
|
7360
|
-
const checks = data?.checks
|
|
7471
|
+
const checks = formatGateChecks(data?.checks);
|
|
7361
7472
|
return checks ? `gate: ${status} (${checks})` : `gate: ${status}`;
|
|
7362
7473
|
}
|
|
7363
7474
|
function formatHandoffDetail(event) {
|
|
@@ -7633,34 +7744,36 @@ function resolveRuleSeverity(ruleId, defaultSeverity, overrides, strict) {
|
|
|
7633
7744
|
// src/security/stack-detector.ts
|
|
7634
7745
|
import * as fs22 from "fs";
|
|
7635
7746
|
import * as path19 from "path";
|
|
7636
|
-
function
|
|
7637
|
-
const
|
|
7747
|
+
function nodeSubStacks(allDeps) {
|
|
7748
|
+
const found = [];
|
|
7749
|
+
if (allDeps.react || allDeps["react-dom"]) found.push("react");
|
|
7750
|
+
if (allDeps.express) found.push("express");
|
|
7751
|
+
if (allDeps.koa) found.push("koa");
|
|
7752
|
+
if (allDeps.fastify) found.push("fastify");
|
|
7753
|
+
if (allDeps.next) found.push("next");
|
|
7754
|
+
if (allDeps.vue) found.push("vue");
|
|
7755
|
+
if (allDeps.angular || allDeps["@angular/core"]) found.push("angular");
|
|
7756
|
+
return found;
|
|
7757
|
+
}
|
|
7758
|
+
function detectNodeStacks(projectRoot) {
|
|
7638
7759
|
const pkgJsonPath = path19.join(projectRoot, "package.json");
|
|
7639
|
-
if (fs22.existsSync(pkgJsonPath))
|
|
7640
|
-
|
|
7641
|
-
|
|
7642
|
-
|
|
7643
|
-
|
|
7644
|
-
|
|
7645
|
-
|
|
7646
|
-
};
|
|
7647
|
-
if (allDeps.react || allDeps["react-dom"]) stacks.push("react");
|
|
7648
|
-
if (allDeps.express) stacks.push("express");
|
|
7649
|
-
if (allDeps.koa) stacks.push("koa");
|
|
7650
|
-
if (allDeps.fastify) stacks.push("fastify");
|
|
7651
|
-
if (allDeps.next) stacks.push("next");
|
|
7652
|
-
if (allDeps.vue) stacks.push("vue");
|
|
7653
|
-
if (allDeps.angular || allDeps["@angular/core"]) stacks.push("angular");
|
|
7654
|
-
} catch {
|
|
7655
|
-
}
|
|
7760
|
+
if (!fs22.existsSync(pkgJsonPath)) return [];
|
|
7761
|
+
const stacks = ["node"];
|
|
7762
|
+
try {
|
|
7763
|
+
const pkgJson = JSON.parse(fs22.readFileSync(pkgJsonPath, "utf-8"));
|
|
7764
|
+
const allDeps = { ...pkgJson.dependencies, ...pkgJson.devDependencies };
|
|
7765
|
+
stacks.push(...nodeSubStacks(allDeps));
|
|
7766
|
+
} catch {
|
|
7656
7767
|
}
|
|
7657
|
-
|
|
7658
|
-
|
|
7768
|
+
return stacks;
|
|
7769
|
+
}
|
|
7770
|
+
function detectStack(projectRoot) {
|
|
7771
|
+
const stacks = [...detectNodeStacks(projectRoot)];
|
|
7772
|
+
if (fs22.existsSync(path19.join(projectRoot, "go.mod"))) {
|
|
7659
7773
|
stacks.push("go");
|
|
7660
7774
|
}
|
|
7661
|
-
const
|
|
7662
|
-
|
|
7663
|
-
if (fs22.existsSync(requirementsPath) || fs22.existsSync(pyprojectPath)) {
|
|
7775
|
+
const hasPython = fs22.existsSync(path19.join(projectRoot, "requirements.txt")) || fs22.existsSync(path19.join(projectRoot, "pyproject.toml"));
|
|
7776
|
+
if (hasPython) {
|
|
7664
7777
|
stacks.push("python");
|
|
7665
7778
|
}
|
|
7666
7779
|
return stacks;
|
|
@@ -8504,6 +8617,56 @@ var SecurityScanner = class {
|
|
|
8504
8617
|
});
|
|
8505
8618
|
return this.scanLinesWithRules(lines, applicableRules, filePath, startLine);
|
|
8506
8619
|
}
|
|
8620
|
+
/** Build a finding for a suppression comment that is missing its justification. */
|
|
8621
|
+
buildSuppressionFinding(rule, filePath, lineNumber, line) {
|
|
8622
|
+
return {
|
|
8623
|
+
ruleId: rule.id,
|
|
8624
|
+
ruleName: rule.name,
|
|
8625
|
+
category: rule.category,
|
|
8626
|
+
severity: this.config.strict ? "error" : "warning",
|
|
8627
|
+
confidence: "high",
|
|
8628
|
+
file: filePath,
|
|
8629
|
+
line: lineNumber,
|
|
8630
|
+
match: line.trim(),
|
|
8631
|
+
context: line,
|
|
8632
|
+
message: `Suppression of ${rule.id} requires justification: // harness-ignore ${rule.id}: <reason>`,
|
|
8633
|
+
remediation: `Add justification after colon: // harness-ignore ${rule.id}: false positive because ...`
|
|
8634
|
+
};
|
|
8635
|
+
}
|
|
8636
|
+
/** Check one line against a rule's patterns; return a finding or null. */
|
|
8637
|
+
matchRuleLine(rule, resolved, filePath, lineNumber, line) {
|
|
8638
|
+
for (const pattern of rule.patterns) {
|
|
8639
|
+
pattern.lastIndex = 0;
|
|
8640
|
+
if (!pattern.test(line)) continue;
|
|
8641
|
+
return {
|
|
8642
|
+
ruleId: rule.id,
|
|
8643
|
+
ruleName: rule.name,
|
|
8644
|
+
category: rule.category,
|
|
8645
|
+
severity: resolved,
|
|
8646
|
+
confidence: rule.confidence,
|
|
8647
|
+
file: filePath,
|
|
8648
|
+
line: lineNumber,
|
|
8649
|
+
match: line.trim(),
|
|
8650
|
+
context: line,
|
|
8651
|
+
message: rule.message,
|
|
8652
|
+
remediation: rule.remediation,
|
|
8653
|
+
...rule.references ? { references: rule.references } : {}
|
|
8654
|
+
};
|
|
8655
|
+
}
|
|
8656
|
+
return null;
|
|
8657
|
+
}
|
|
8658
|
+
/** Scan a single line against a resolved rule; push any findings into the array. */
|
|
8659
|
+
scanLineForRule(rule, resolved, line, lineNumber, filePath, findings) {
|
|
8660
|
+
const suppressionMatch = parseHarnessIgnore(line, rule.id);
|
|
8661
|
+
if (suppressionMatch) {
|
|
8662
|
+
if (!suppressionMatch.justification) {
|
|
8663
|
+
findings.push(this.buildSuppressionFinding(rule, filePath, lineNumber, line));
|
|
8664
|
+
}
|
|
8665
|
+
return;
|
|
8666
|
+
}
|
|
8667
|
+
const finding = this.matchRuleLine(rule, resolved, filePath, lineNumber, line);
|
|
8668
|
+
if (finding) findings.push(finding);
|
|
8669
|
+
}
|
|
8507
8670
|
/**
|
|
8508
8671
|
* Core scanning loop shared by scanContent and scanContentForFile.
|
|
8509
8672
|
* Evaluates each rule against each line, handling suppression (FP gate)
|
|
@@ -8520,46 +8683,7 @@ var SecurityScanner = class {
|
|
|
8520
8683
|
);
|
|
8521
8684
|
if (resolved === "off") continue;
|
|
8522
8685
|
for (let i = 0; i < lines.length; i++) {
|
|
8523
|
-
|
|
8524
|
-
const suppressionMatch = parseHarnessIgnore(line, rule.id);
|
|
8525
|
-
if (suppressionMatch) {
|
|
8526
|
-
if (!suppressionMatch.justification) {
|
|
8527
|
-
findings.push({
|
|
8528
|
-
ruleId: rule.id,
|
|
8529
|
-
ruleName: rule.name,
|
|
8530
|
-
category: rule.category,
|
|
8531
|
-
severity: this.config.strict ? "error" : "warning",
|
|
8532
|
-
confidence: "high",
|
|
8533
|
-
file: filePath,
|
|
8534
|
-
line: startLine + i,
|
|
8535
|
-
match: line.trim(),
|
|
8536
|
-
context: line,
|
|
8537
|
-
message: `Suppression of ${rule.id} requires justification: // harness-ignore ${rule.id}: <reason>`,
|
|
8538
|
-
remediation: `Add justification after colon: // harness-ignore ${rule.id}: false positive because ...`
|
|
8539
|
-
});
|
|
8540
|
-
}
|
|
8541
|
-
continue;
|
|
8542
|
-
}
|
|
8543
|
-
for (const pattern of rule.patterns) {
|
|
8544
|
-
pattern.lastIndex = 0;
|
|
8545
|
-
if (pattern.test(line)) {
|
|
8546
|
-
findings.push({
|
|
8547
|
-
ruleId: rule.id,
|
|
8548
|
-
ruleName: rule.name,
|
|
8549
|
-
category: rule.category,
|
|
8550
|
-
severity: resolved,
|
|
8551
|
-
confidence: rule.confidence,
|
|
8552
|
-
file: filePath,
|
|
8553
|
-
line: startLine + i,
|
|
8554
|
-
match: line.trim(),
|
|
8555
|
-
context: line,
|
|
8556
|
-
message: rule.message,
|
|
8557
|
-
remediation: rule.remediation,
|
|
8558
|
-
...rule.references ? { references: rule.references } : {}
|
|
8559
|
-
});
|
|
8560
|
-
break;
|
|
8561
|
-
}
|
|
8562
|
-
}
|
|
8686
|
+
this.scanLineForRule(rule, resolved, lines[i] ?? "", startLine + i, filePath, findings);
|
|
8563
8687
|
}
|
|
8564
8688
|
}
|
|
8565
8689
|
return findings;
|
|
@@ -9979,37 +10103,30 @@ function extractConventionRules(bundle) {
|
|
|
9979
10103
|
}
|
|
9980
10104
|
return rules;
|
|
9981
10105
|
}
|
|
9982
|
-
|
|
10106
|
+
var EXPORT_RE = /export\s+(?:async\s+)?(?:function|const|class|interface|type)\s+(\w+)/;
|
|
10107
|
+
function hasPrecedingJsDoc(lines, i) {
|
|
10108
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
10109
|
+
const prev = lines[j].trim();
|
|
10110
|
+
if (prev === "") continue;
|
|
10111
|
+
return prev.endsWith("*/");
|
|
10112
|
+
}
|
|
10113
|
+
return false;
|
|
10114
|
+
}
|
|
10115
|
+
function scanFileForMissingJsDoc(filePath, lines) {
|
|
9983
10116
|
const missing = [];
|
|
9984
|
-
for (
|
|
9985
|
-
const
|
|
9986
|
-
|
|
9987
|
-
|
|
9988
|
-
const exportMatch = line.match(
|
|
9989
|
-
/export\s+(?:async\s+)?(?:function|const|class|interface|type)\s+(\w+)/
|
|
9990
|
-
);
|
|
9991
|
-
if (exportMatch) {
|
|
9992
|
-
let hasJsDoc = false;
|
|
9993
|
-
for (let j = i - 1; j >= 0; j--) {
|
|
9994
|
-
const prev = lines[j].trim();
|
|
9995
|
-
if (prev === "") continue;
|
|
9996
|
-
if (prev.endsWith("*/")) {
|
|
9997
|
-
hasJsDoc = true;
|
|
9998
|
-
}
|
|
9999
|
-
break;
|
|
10000
|
-
}
|
|
10001
|
-
if (!hasJsDoc) {
|
|
10002
|
-
missing.push({
|
|
10003
|
-
file: cf.path,
|
|
10004
|
-
line: i + 1,
|
|
10005
|
-
exportName: exportMatch[1]
|
|
10006
|
-
});
|
|
10007
|
-
}
|
|
10008
|
-
}
|
|
10117
|
+
for (let i = 0; i < lines.length; i++) {
|
|
10118
|
+
const exportMatch = lines[i].match(EXPORT_RE);
|
|
10119
|
+
if (exportMatch && !hasPrecedingJsDoc(lines, i)) {
|
|
10120
|
+
missing.push({ file: filePath, line: i + 1, exportName: exportMatch[1] });
|
|
10009
10121
|
}
|
|
10010
10122
|
}
|
|
10011
10123
|
return missing;
|
|
10012
10124
|
}
|
|
10125
|
+
function findMissingJsDoc(bundle) {
|
|
10126
|
+
return bundle.changedFiles.flatMap(
|
|
10127
|
+
(cf) => scanFileForMissingJsDoc(cf.path, cf.content.split("\n"))
|
|
10128
|
+
);
|
|
10129
|
+
}
|
|
10013
10130
|
function checkMissingJsDoc(bundle, rules) {
|
|
10014
10131
|
const jsDocRule = rules.find((r) => r.text.toLowerCase().includes("jsdoc"));
|
|
10015
10132
|
if (!jsDocRule) return [];
|
|
@@ -10074,29 +10191,27 @@ function checkChangeTypeSpecific(bundle) {
|
|
|
10074
10191
|
return [];
|
|
10075
10192
|
}
|
|
10076
10193
|
}
|
|
10194
|
+
function checkFileResultTypeConvention(cf, bundle, rule) {
|
|
10195
|
+
const hasTryCatch = cf.content.includes("try {") || cf.content.includes("try{");
|
|
10196
|
+
const usesResult = cf.content.includes("Result<") || cf.content.includes("Result >") || cf.content.includes(": Result");
|
|
10197
|
+
if (!hasTryCatch || usesResult) return null;
|
|
10198
|
+
return {
|
|
10199
|
+
id: makeFindingId("compliance", cf.path, 1, "try-catch not Result"),
|
|
10200
|
+
file: cf.path,
|
|
10201
|
+
lineRange: [1, cf.lines],
|
|
10202
|
+
domain: "compliance",
|
|
10203
|
+
severity: "suggestion",
|
|
10204
|
+
title: "Fallible operation uses try/catch instead of Result type",
|
|
10205
|
+
rationale: `Convention requires using Result type for fallible operations (from ${rule.source}).`,
|
|
10206
|
+
suggestion: "Refactor error handling to use the Result type pattern.",
|
|
10207
|
+
evidence: [`changeType: ${bundle.changeType}`, `Convention rule: "${rule.text}"`],
|
|
10208
|
+
validatedBy: "heuristic"
|
|
10209
|
+
};
|
|
10210
|
+
}
|
|
10077
10211
|
function checkResultTypeConvention(bundle, rules) {
|
|
10078
10212
|
const resultTypeRule = rules.find((r) => r.text.toLowerCase().includes("result type"));
|
|
10079
10213
|
if (!resultTypeRule) return [];
|
|
10080
|
-
|
|
10081
|
-
for (const cf of bundle.changedFiles) {
|
|
10082
|
-
const hasTryCatch = cf.content.includes("try {") || cf.content.includes("try{");
|
|
10083
|
-
const usesResult = cf.content.includes("Result<") || cf.content.includes("Result >") || cf.content.includes(": Result");
|
|
10084
|
-
if (hasTryCatch && !usesResult) {
|
|
10085
|
-
findings.push({
|
|
10086
|
-
id: makeFindingId("compliance", cf.path, 1, "try-catch not Result"),
|
|
10087
|
-
file: cf.path,
|
|
10088
|
-
lineRange: [1, cf.lines],
|
|
10089
|
-
domain: "compliance",
|
|
10090
|
-
severity: "suggestion",
|
|
10091
|
-
title: "Fallible operation uses try/catch instead of Result type",
|
|
10092
|
-
rationale: `Convention requires using Result type for fallible operations (from ${resultTypeRule.source}).`,
|
|
10093
|
-
suggestion: "Refactor error handling to use the Result type pattern.",
|
|
10094
|
-
evidence: [`changeType: ${bundle.changeType}`, `Convention rule: "${resultTypeRule.text}"`],
|
|
10095
|
-
validatedBy: "heuristic"
|
|
10096
|
-
});
|
|
10097
|
-
}
|
|
10098
|
-
}
|
|
10099
|
-
return findings;
|
|
10214
|
+
return bundle.changedFiles.map((cf) => checkFileResultTypeConvention(cf, bundle, resultTypeRule)).filter((f) => f !== null);
|
|
10100
10215
|
}
|
|
10101
10216
|
function runComplianceAgent(bundle) {
|
|
10102
10217
|
const rules = extractConventionRules(bundle);
|
|
@@ -10122,53 +10237,58 @@ var BUG_DETECTION_DESCRIPTOR = {
|
|
|
10122
10237
|
"Test coverage \u2014 tests for happy path, error paths, and edge cases"
|
|
10123
10238
|
]
|
|
10124
10239
|
};
|
|
10240
|
+
function hasPrecedingZeroCheck(lines, i) {
|
|
10241
|
+
const preceding = lines.slice(Math.max(0, i - 3), i).join("\n");
|
|
10242
|
+
return preceding.includes("=== 0") || preceding.includes("!== 0") || preceding.includes("== 0") || preceding.includes("!= 0");
|
|
10243
|
+
}
|
|
10125
10244
|
function detectDivisionByZero(bundle) {
|
|
10126
10245
|
const findings = [];
|
|
10127
10246
|
for (const cf of bundle.changedFiles) {
|
|
10128
10247
|
const lines = cf.content.split("\n");
|
|
10129
10248
|
for (let i = 0; i < lines.length; i++) {
|
|
10130
10249
|
const line = lines[i];
|
|
10131
|
-
if (line.match(/[^=!<>]\s*\/\s*[a-zA-Z_]\w*/)
|
|
10132
|
-
|
|
10133
|
-
|
|
10134
|
-
|
|
10135
|
-
|
|
10136
|
-
|
|
10137
|
-
|
|
10138
|
-
|
|
10139
|
-
|
|
10140
|
-
|
|
10141
|
-
|
|
10142
|
-
|
|
10143
|
-
|
|
10144
|
-
|
|
10145
|
-
});
|
|
10146
|
-
}
|
|
10147
|
-
}
|
|
10250
|
+
if (!line.match(/[^=!<>]\s*\/\s*[a-zA-Z_]\w*/) || line.includes("//")) continue;
|
|
10251
|
+
if (hasPrecedingZeroCheck(lines, i)) continue;
|
|
10252
|
+
findings.push({
|
|
10253
|
+
id: makeFindingId("bug", cf.path, i + 1, "division by zero"),
|
|
10254
|
+
file: cf.path,
|
|
10255
|
+
lineRange: [i + 1, i + 1],
|
|
10256
|
+
domain: "bug",
|
|
10257
|
+
severity: "important",
|
|
10258
|
+
title: "Potential division by zero without guard",
|
|
10259
|
+
rationale: "Division operation found without a preceding zero check on the divisor. This can cause Infinity or NaN at runtime.",
|
|
10260
|
+
suggestion: "Add a check for zero before dividing, or use a safe division utility.",
|
|
10261
|
+
evidence: [`Line ${i + 1}: ${line.trim()}`],
|
|
10262
|
+
validatedBy: "heuristic"
|
|
10263
|
+
});
|
|
10148
10264
|
}
|
|
10149
10265
|
}
|
|
10150
10266
|
return findings;
|
|
10151
10267
|
}
|
|
10268
|
+
function isEmptyCatch(lines, i) {
|
|
10269
|
+
const line = lines[i];
|
|
10270
|
+
if (line.match(/catch\s*\([^)]*\)\s*\{\s*\}/)) return true;
|
|
10271
|
+
return line.match(/catch\s*\([^)]*\)\s*\{/) !== null && i + 1 < lines.length && lines[i + 1].trim() === "}";
|
|
10272
|
+
}
|
|
10152
10273
|
function detectEmptyCatch(bundle) {
|
|
10153
10274
|
const findings = [];
|
|
10154
10275
|
for (const cf of bundle.changedFiles) {
|
|
10155
10276
|
const lines = cf.content.split("\n");
|
|
10156
10277
|
for (let i = 0; i < lines.length; i++) {
|
|
10278
|
+
if (!isEmptyCatch(lines, i)) continue;
|
|
10157
10279
|
const line = lines[i];
|
|
10158
|
-
|
|
10159
|
-
|
|
10160
|
-
|
|
10161
|
-
|
|
10162
|
-
|
|
10163
|
-
|
|
10164
|
-
|
|
10165
|
-
|
|
10166
|
-
|
|
10167
|
-
|
|
10168
|
-
|
|
10169
|
-
|
|
10170
|
-
});
|
|
10171
|
-
}
|
|
10280
|
+
findings.push({
|
|
10281
|
+
id: makeFindingId("bug", cf.path, i + 1, "empty catch block"),
|
|
10282
|
+
file: cf.path,
|
|
10283
|
+
lineRange: [i + 1, i + 2],
|
|
10284
|
+
domain: "bug",
|
|
10285
|
+
severity: "important",
|
|
10286
|
+
title: "Empty catch block silently swallows error",
|
|
10287
|
+
rationale: "Catching an error without handling, logging, or re-throwing it hides failures and makes debugging difficult.",
|
|
10288
|
+
suggestion: "Log the error, re-throw it, or handle it explicitly. If intentionally ignoring, add a comment explaining why.",
|
|
10289
|
+
evidence: [`Line ${i + 1}: ${line.trim()}`],
|
|
10290
|
+
validatedBy: "heuristic"
|
|
10291
|
+
});
|
|
10172
10292
|
}
|
|
10173
10293
|
}
|
|
10174
10294
|
return findings;
|
|
@@ -10226,70 +10346,116 @@ var SECRET_PATTERNS = [
|
|
|
10226
10346
|
];
|
|
10227
10347
|
var SQL_CONCAT_PATTERN = /(?:SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER)\s+.*?\+\s*\w+|`[^`]*\$\{[^}]*\}[^`]*(?:SELECT|INSERT|UPDATE|DELETE|WHERE)/i;
|
|
10228
10348
|
var SHELL_EXEC_PATTERN = /(?:exec|execSync|spawn|spawnSync)\s*\(\s*`[^`]*\$\{/;
|
|
10229
|
-
function
|
|
10230
|
-
|
|
10231
|
-
|
|
10232
|
-
|
|
10233
|
-
|
|
10234
|
-
|
|
10235
|
-
|
|
10236
|
-
|
|
10237
|
-
|
|
10238
|
-
|
|
10239
|
-
|
|
10240
|
-
|
|
10241
|
-
|
|
10242
|
-
|
|
10243
|
-
|
|
10244
|
-
|
|
10245
|
-
|
|
10246
|
-
|
|
10247
|
-
|
|
10248
|
-
|
|
10249
|
-
|
|
10250
|
-
|
|
10251
|
-
|
|
10252
|
-
|
|
10253
|
-
|
|
10254
|
-
|
|
10255
|
-
|
|
10256
|
-
|
|
10257
|
-
|
|
10258
|
-
|
|
10259
|
-
|
|
10260
|
-
|
|
10261
|
-
|
|
10349
|
+
function makeEvalFinding(file, lineNum, line) {
|
|
10350
|
+
return {
|
|
10351
|
+
id: makeFindingId("security", file, lineNum, "eval usage CWE-94"),
|
|
10352
|
+
file,
|
|
10353
|
+
lineRange: [lineNum, lineNum],
|
|
10354
|
+
domain: "security",
|
|
10355
|
+
severity: "critical",
|
|
10356
|
+
title: `Dangerous ${"eval"}() or new ${"Function"}() usage`,
|
|
10357
|
+
rationale: `${"eval"}() and new ${"Function"}() execute arbitrary code. If user input reaches these calls, it enables Remote Code Execution (CWE-94).`,
|
|
10358
|
+
suggestion: "Replace eval/Function with a safe alternative (JSON.parse for data, a sandboxed evaluator for expressions).",
|
|
10359
|
+
evidence: [`Line ${lineNum}: ${line.trim()}`],
|
|
10360
|
+
validatedBy: "heuristic",
|
|
10361
|
+
cweId: "CWE-94",
|
|
10362
|
+
owaspCategory: "A03:2021 Injection",
|
|
10363
|
+
confidence: "high",
|
|
10364
|
+
remediation: "Replace eval/Function with a safe alternative (JSON.parse for data, a sandboxed evaluator for expressions).",
|
|
10365
|
+
references: [
|
|
10366
|
+
"https://cwe.mitre.org/data/definitions/94.html",
|
|
10367
|
+
"https://owasp.org/Top10/A03_2021-Injection/"
|
|
10368
|
+
]
|
|
10369
|
+
};
|
|
10370
|
+
}
|
|
10371
|
+
function makeSecretFinding(file, lineNum) {
|
|
10372
|
+
return {
|
|
10373
|
+
id: makeFindingId("security", file, lineNum, "hardcoded secret CWE-798"),
|
|
10374
|
+
file,
|
|
10375
|
+
lineRange: [lineNum, lineNum],
|
|
10376
|
+
domain: "security",
|
|
10377
|
+
severity: "critical",
|
|
10378
|
+
title: "Hardcoded secret or API key detected",
|
|
10379
|
+
rationale: "Hardcoded secrets in source code can be extracted from version history even after removal. Use environment variables or a secrets manager (CWE-798).",
|
|
10380
|
+
suggestion: "Move the secret to an environment variable and access it via process.env.",
|
|
10381
|
+
evidence: [`Line ${lineNum}: [secret detected \u2014 value redacted]`],
|
|
10382
|
+
validatedBy: "heuristic",
|
|
10383
|
+
cweId: "CWE-798",
|
|
10384
|
+
owaspCategory: "A07:2021 Identification and Authentication Failures",
|
|
10385
|
+
confidence: "high",
|
|
10386
|
+
remediation: "Move the secret to an environment variable and access it via process.env.",
|
|
10387
|
+
references: [
|
|
10388
|
+
"https://cwe.mitre.org/data/definitions/798.html",
|
|
10389
|
+
"https://owasp.org/Top10/A07_2021-Identification_and_Authentication_Failures/"
|
|
10390
|
+
]
|
|
10391
|
+
};
|
|
10392
|
+
}
|
|
10393
|
+
function makeSqlFinding(file, lineNum, line) {
|
|
10394
|
+
return {
|
|
10395
|
+
id: makeFindingId("security", file, lineNum, "SQL injection CWE-89"),
|
|
10396
|
+
file,
|
|
10397
|
+
lineRange: [lineNum, lineNum],
|
|
10398
|
+
domain: "security",
|
|
10399
|
+
severity: "critical",
|
|
10400
|
+
title: "Potential SQL injection via string concatenation",
|
|
10401
|
+
rationale: "Building SQL queries with string concatenation or template literals allows attackers to inject malicious SQL (CWE-89).",
|
|
10402
|
+
suggestion: "Use parameterized queries or a query builder (e.g., Knex, Prisma) instead of string concatenation.",
|
|
10403
|
+
evidence: [`Line ${lineNum}: ${line.trim()}`],
|
|
10404
|
+
validatedBy: "heuristic",
|
|
10405
|
+
cweId: "CWE-89",
|
|
10406
|
+
owaspCategory: "A03:2021 Injection",
|
|
10407
|
+
confidence: "high",
|
|
10408
|
+
remediation: "Use parameterized queries or a query builder (e.g., Knex, Prisma) instead of string concatenation.",
|
|
10409
|
+
references: [
|
|
10410
|
+
"https://cwe.mitre.org/data/definitions/89.html",
|
|
10411
|
+
"https://owasp.org/Top10/A03_2021-Injection/"
|
|
10412
|
+
]
|
|
10413
|
+
};
|
|
10414
|
+
}
|
|
10415
|
+
function makeCommandFinding(file, lineNum, line) {
|
|
10416
|
+
return {
|
|
10417
|
+
id: makeFindingId("security", file, lineNum, "command injection CWE-78"),
|
|
10418
|
+
file,
|
|
10419
|
+
lineRange: [lineNum, lineNum],
|
|
10420
|
+
domain: "security",
|
|
10421
|
+
severity: "critical",
|
|
10422
|
+
title: "Potential command injection via shell exec with interpolation",
|
|
10423
|
+
rationale: "Using exec/spawn with template literal interpolation allows attackers to inject shell commands (CWE-78).",
|
|
10424
|
+
suggestion: "Use execFile or spawn with an arguments array instead of shell string interpolation.",
|
|
10425
|
+
evidence: [`Line ${lineNum}: ${line.trim()}`],
|
|
10426
|
+
validatedBy: "heuristic",
|
|
10427
|
+
cweId: "CWE-78",
|
|
10428
|
+
owaspCategory: "A03:2021 Injection",
|
|
10429
|
+
confidence: "high",
|
|
10430
|
+
remediation: "Use execFile or spawn with an arguments array instead of shell string interpolation.",
|
|
10431
|
+
references: [
|
|
10432
|
+
"https://cwe.mitre.org/data/definitions/78.html",
|
|
10433
|
+
"https://owasp.org/Top10/A03_2021-Injection/"
|
|
10434
|
+
]
|
|
10435
|
+
};
|
|
10436
|
+
}
|
|
10437
|
+
function detectEvalUsage(bundle) {
|
|
10438
|
+
const findings = [];
|
|
10439
|
+
for (const cf of bundle.changedFiles) {
|
|
10440
|
+
const lines = cf.content.split("\n");
|
|
10441
|
+
for (let i = 0; i < lines.length; i++) {
|
|
10442
|
+
const line = lines[i];
|
|
10443
|
+
if (!EVAL_PATTERN.test(line)) continue;
|
|
10444
|
+
findings.push(makeEvalFinding(cf.path, i + 1, line));
|
|
10445
|
+
}
|
|
10446
|
+
}
|
|
10447
|
+
return findings;
|
|
10448
|
+
}
|
|
10449
|
+
function detectHardcodedSecrets(bundle) {
|
|
10262
10450
|
const findings = [];
|
|
10263
10451
|
for (const cf of bundle.changedFiles) {
|
|
10264
10452
|
const lines = cf.content.split("\n");
|
|
10265
10453
|
for (let i = 0; i < lines.length; i++) {
|
|
10266
10454
|
const line = lines[i];
|
|
10267
10455
|
const codePart = line.includes("//") ? line.slice(0, line.indexOf("//")) : line;
|
|
10268
|
-
|
|
10269
|
-
|
|
10270
|
-
|
|
10271
|
-
id: makeFindingId("security", cf.path, i + 1, "hardcoded secret CWE-798"),
|
|
10272
|
-
file: cf.path,
|
|
10273
|
-
lineRange: [i + 1, i + 1],
|
|
10274
|
-
domain: "security",
|
|
10275
|
-
severity: "critical",
|
|
10276
|
-
title: "Hardcoded secret or API key detected",
|
|
10277
|
-
rationale: "Hardcoded secrets in source code can be extracted from version history even after removal. Use environment variables or a secrets manager (CWE-798).",
|
|
10278
|
-
suggestion: "Move the secret to an environment variable and access it via process.env.",
|
|
10279
|
-
evidence: [`Line ${i + 1}: [secret detected \u2014 value redacted]`],
|
|
10280
|
-
validatedBy: "heuristic",
|
|
10281
|
-
cweId: "CWE-798",
|
|
10282
|
-
owaspCategory: "A07:2021 Identification and Authentication Failures",
|
|
10283
|
-
confidence: "high",
|
|
10284
|
-
remediation: "Move the secret to an environment variable and access it via process.env.",
|
|
10285
|
-
references: [
|
|
10286
|
-
"https://cwe.mitre.org/data/definitions/798.html",
|
|
10287
|
-
"https://owasp.org/Top10/A07_2021-Identification_and_Authentication_Failures/"
|
|
10288
|
-
]
|
|
10289
|
-
});
|
|
10290
|
-
break;
|
|
10291
|
-
}
|
|
10292
|
-
}
|
|
10456
|
+
const matched = SECRET_PATTERNS.some((p) => p.test(codePart));
|
|
10457
|
+
if (!matched) continue;
|
|
10458
|
+
findings.push(makeSecretFinding(cf.path, i + 1));
|
|
10293
10459
|
}
|
|
10294
10460
|
}
|
|
10295
10461
|
return findings;
|
|
@@ -10300,28 +10466,8 @@ function detectSqlInjection(bundle) {
|
|
|
10300
10466
|
const lines = cf.content.split("\n");
|
|
10301
10467
|
for (let i = 0; i < lines.length; i++) {
|
|
10302
10468
|
const line = lines[i];
|
|
10303
|
-
if (SQL_CONCAT_PATTERN.test(line))
|
|
10304
|
-
|
|
10305
|
-
id: makeFindingId("security", cf.path, i + 1, "SQL injection CWE-89"),
|
|
10306
|
-
file: cf.path,
|
|
10307
|
-
lineRange: [i + 1, i + 1],
|
|
10308
|
-
domain: "security",
|
|
10309
|
-
severity: "critical",
|
|
10310
|
-
title: "Potential SQL injection via string concatenation",
|
|
10311
|
-
rationale: "Building SQL queries with string concatenation or template literals allows attackers to inject malicious SQL (CWE-89).",
|
|
10312
|
-
suggestion: "Use parameterized queries or a query builder (e.g., Knex, Prisma) instead of string concatenation.",
|
|
10313
|
-
evidence: [`Line ${i + 1}: ${line.trim()}`],
|
|
10314
|
-
validatedBy: "heuristic",
|
|
10315
|
-
cweId: "CWE-89",
|
|
10316
|
-
owaspCategory: "A03:2021 Injection",
|
|
10317
|
-
confidence: "high",
|
|
10318
|
-
remediation: "Use parameterized queries or a query builder (e.g., Knex, Prisma) instead of string concatenation.",
|
|
10319
|
-
references: [
|
|
10320
|
-
"https://cwe.mitre.org/data/definitions/89.html",
|
|
10321
|
-
"https://owasp.org/Top10/A03_2021-Injection/"
|
|
10322
|
-
]
|
|
10323
|
-
});
|
|
10324
|
-
}
|
|
10469
|
+
if (!SQL_CONCAT_PATTERN.test(line)) continue;
|
|
10470
|
+
findings.push(makeSqlFinding(cf.path, i + 1, line));
|
|
10325
10471
|
}
|
|
10326
10472
|
}
|
|
10327
10473
|
return findings;
|
|
@@ -10332,28 +10478,8 @@ function detectCommandInjection(bundle) {
|
|
|
10332
10478
|
const lines = cf.content.split("\n");
|
|
10333
10479
|
for (let i = 0; i < lines.length; i++) {
|
|
10334
10480
|
const line = lines[i];
|
|
10335
|
-
if (SHELL_EXEC_PATTERN.test(line))
|
|
10336
|
-
|
|
10337
|
-
id: makeFindingId("security", cf.path, i + 1, "command injection CWE-78"),
|
|
10338
|
-
file: cf.path,
|
|
10339
|
-
lineRange: [i + 1, i + 1],
|
|
10340
|
-
domain: "security",
|
|
10341
|
-
severity: "critical",
|
|
10342
|
-
title: "Potential command injection via shell exec with interpolation",
|
|
10343
|
-
rationale: "Using exec/spawn with template literal interpolation allows attackers to inject shell commands (CWE-78).",
|
|
10344
|
-
suggestion: "Use execFile or spawn with an arguments array instead of shell string interpolation.",
|
|
10345
|
-
evidence: [`Line ${i + 1}: ${line.trim()}`],
|
|
10346
|
-
validatedBy: "heuristic",
|
|
10347
|
-
cweId: "CWE-78",
|
|
10348
|
-
owaspCategory: "A03:2021 Injection",
|
|
10349
|
-
confidence: "high",
|
|
10350
|
-
remediation: "Use execFile or spawn with an arguments array instead of shell string interpolation.",
|
|
10351
|
-
references: [
|
|
10352
|
-
"https://cwe.mitre.org/data/definitions/78.html",
|
|
10353
|
-
"https://owasp.org/Top10/A03_2021-Injection/"
|
|
10354
|
-
]
|
|
10355
|
-
});
|
|
10356
|
-
}
|
|
10481
|
+
if (!SHELL_EXEC_PATTERN.test(line)) continue;
|
|
10482
|
+
findings.push(makeCommandFinding(cf.path, i + 1, line));
|
|
10357
10483
|
}
|
|
10358
10484
|
}
|
|
10359
10485
|
return findings;
|
|
@@ -10386,10 +10512,15 @@ function isViolationLine(line) {
|
|
|
10386
10512
|
const lower = line.toLowerCase();
|
|
10387
10513
|
return lower.includes("violation") || lower.includes("layer");
|
|
10388
10514
|
}
|
|
10389
|
-
|
|
10390
|
-
|
|
10515
|
+
var VIOLATION_FILE_RE = /(?:in\s+)?(\S+\.(?:ts|tsx|js|jsx))(?::(\d+))?/;
|
|
10516
|
+
function extractViolationLocation(line, fallbackPath) {
|
|
10517
|
+
const fileMatch = line.match(VIOLATION_FILE_RE);
|
|
10391
10518
|
const file = fileMatch?.[1] ?? fallbackPath;
|
|
10392
10519
|
const lineNum = fileMatch?.[2] ? parseInt(fileMatch[2], 10) : 1;
|
|
10520
|
+
return { file, lineNum };
|
|
10521
|
+
}
|
|
10522
|
+
function createLayerViolationFinding(line, fallbackPath) {
|
|
10523
|
+
const { file, lineNum } = extractViolationLocation(line, fallbackPath);
|
|
10393
10524
|
return {
|
|
10394
10525
|
id: makeFindingId("arch", file, lineNum, "layer violation"),
|
|
10395
10526
|
file,
|
|
@@ -10562,6 +10693,26 @@ function normalizePath(filePath, projectRoot) {
|
|
|
10562
10693
|
}
|
|
10563
10694
|
return normalized;
|
|
10564
10695
|
}
|
|
10696
|
+
function resolveImportPath2(currentFile, importPath) {
|
|
10697
|
+
const dir = path23.dirname(currentFile);
|
|
10698
|
+
let resolved = path23.join(dir, importPath).replace(/\\/g, "/");
|
|
10699
|
+
if (!resolved.match(/\.(ts|tsx|js|jsx)$/)) {
|
|
10700
|
+
resolved += ".ts";
|
|
10701
|
+
}
|
|
10702
|
+
return path23.normalize(resolved).replace(/\\/g, "/");
|
|
10703
|
+
}
|
|
10704
|
+
function enqueueImports(content, current, visited, queue, maxDepth) {
|
|
10705
|
+
const importRegex = /import\s+.*?from\s+['"]([^'"]+)['"]/g;
|
|
10706
|
+
let match;
|
|
10707
|
+
while ((match = importRegex.exec(content)) !== null) {
|
|
10708
|
+
const importPath = match[1];
|
|
10709
|
+
if (!importPath.startsWith(".")) continue;
|
|
10710
|
+
const resolved = resolveImportPath2(current.file, importPath);
|
|
10711
|
+
if (!visited.has(resolved) && current.depth + 1 <= maxDepth) {
|
|
10712
|
+
queue.push({ file: resolved, depth: current.depth + 1 });
|
|
10713
|
+
}
|
|
10714
|
+
}
|
|
10715
|
+
}
|
|
10565
10716
|
function followImportChain(fromFile, fileContents, maxDepth = 2) {
|
|
10566
10717
|
const visited = /* @__PURE__ */ new Set();
|
|
10567
10718
|
const queue = [{ file: fromFile, depth: 0 }];
|
|
@@ -10571,82 +10722,63 @@ function followImportChain(fromFile, fileContents, maxDepth = 2) {
|
|
|
10571
10722
|
visited.add(current.file);
|
|
10572
10723
|
const content = fileContents.get(current.file);
|
|
10573
10724
|
if (!content) continue;
|
|
10574
|
-
|
|
10575
|
-
let match;
|
|
10576
|
-
while ((match = importRegex.exec(content)) !== null) {
|
|
10577
|
-
const importPath = match[1];
|
|
10578
|
-
if (!importPath.startsWith(".")) continue;
|
|
10579
|
-
const dir = path23.dirname(current.file);
|
|
10580
|
-
let resolved = path23.join(dir, importPath).replace(/\\/g, "/");
|
|
10581
|
-
if (!resolved.match(/\.(ts|tsx|js|jsx)$/)) {
|
|
10582
|
-
resolved += ".ts";
|
|
10583
|
-
}
|
|
10584
|
-
resolved = path23.normalize(resolved).replace(/\\/g, "/");
|
|
10585
|
-
if (!visited.has(resolved) && current.depth + 1 <= maxDepth) {
|
|
10586
|
-
queue.push({ file: resolved, depth: current.depth + 1 });
|
|
10587
|
-
}
|
|
10588
|
-
}
|
|
10725
|
+
enqueueImports(content, current, visited, queue, maxDepth);
|
|
10589
10726
|
}
|
|
10590
10727
|
visited.delete(fromFile);
|
|
10591
10728
|
return visited;
|
|
10592
10729
|
}
|
|
10730
|
+
function isMechanicallyExcluded(finding, exclusionSet, projectRoot) {
|
|
10731
|
+
const normalizedFile = normalizePath(finding.file, projectRoot);
|
|
10732
|
+
if (exclusionSet.isExcluded(normalizedFile, finding.lineRange)) return true;
|
|
10733
|
+
if (exclusionSet.isExcluded(finding.file, finding.lineRange)) return true;
|
|
10734
|
+
const absoluteFile = path23.isAbsolute(finding.file) ? finding.file : path23.join(projectRoot, finding.file).replace(/\\/g, "/");
|
|
10735
|
+
return exclusionSet.isExcluded(absoluteFile, finding.lineRange);
|
|
10736
|
+
}
|
|
10737
|
+
async function validateWithGraph(crossFileRefs, graph) {
|
|
10738
|
+
try {
|
|
10739
|
+
for (const ref of crossFileRefs) {
|
|
10740
|
+
const reachable = await graph.isReachable(ref.from, ref.to);
|
|
10741
|
+
if (!reachable) return { result: "discard" };
|
|
10742
|
+
}
|
|
10743
|
+
return { result: "keep" };
|
|
10744
|
+
} catch {
|
|
10745
|
+
return { result: "fallback" };
|
|
10746
|
+
}
|
|
10747
|
+
}
|
|
10748
|
+
function validateWithHeuristic(finding, crossFileRefs, fileContents, projectRoot) {
|
|
10749
|
+
if (fileContents) {
|
|
10750
|
+
for (const ref of crossFileRefs) {
|
|
10751
|
+
const normalizedFrom = normalizePath(ref.from, projectRoot);
|
|
10752
|
+
const reachable = followImportChain(normalizedFrom, fileContents, 2);
|
|
10753
|
+
const normalizedTo = normalizePath(ref.to, projectRoot);
|
|
10754
|
+
if (reachable.has(normalizedTo)) {
|
|
10755
|
+
return { ...finding, validatedBy: "heuristic" };
|
|
10756
|
+
}
|
|
10757
|
+
}
|
|
10758
|
+
}
|
|
10759
|
+
return {
|
|
10760
|
+
...finding,
|
|
10761
|
+
severity: DOWNGRADE_MAP[finding.severity],
|
|
10762
|
+
validatedBy: "heuristic"
|
|
10763
|
+
};
|
|
10764
|
+
}
|
|
10765
|
+
async function processFinding(finding, exclusionSet, graph, projectRoot, fileContents) {
|
|
10766
|
+
if (isMechanicallyExcluded(finding, exclusionSet, projectRoot)) return null;
|
|
10767
|
+
const crossFileRefs = extractCrossFileRefs(finding);
|
|
10768
|
+
if (crossFileRefs.length === 0) return { ...finding };
|
|
10769
|
+
if (graph) {
|
|
10770
|
+
const { result } = await validateWithGraph(crossFileRefs, graph);
|
|
10771
|
+
if (result === "keep") return { ...finding, validatedBy: "graph" };
|
|
10772
|
+
if (result === "discard") return null;
|
|
10773
|
+
}
|
|
10774
|
+
return validateWithHeuristic(finding, crossFileRefs, fileContents, projectRoot);
|
|
10775
|
+
}
|
|
10593
10776
|
async function validateFindings(options) {
|
|
10594
10777
|
const { findings, exclusionSet, graph, projectRoot, fileContents } = options;
|
|
10595
10778
|
const validated = [];
|
|
10596
10779
|
for (const finding of findings) {
|
|
10597
|
-
const
|
|
10598
|
-
if (
|
|
10599
|
-
continue;
|
|
10600
|
-
}
|
|
10601
|
-
const absoluteFile = path23.isAbsolute(finding.file) ? finding.file : path23.join(projectRoot, finding.file).replace(/\\/g, "/");
|
|
10602
|
-
if (exclusionSet.isExcluded(absoluteFile, finding.lineRange)) {
|
|
10603
|
-
continue;
|
|
10604
|
-
}
|
|
10605
|
-
const crossFileRefs = extractCrossFileRefs(finding);
|
|
10606
|
-
if (crossFileRefs.length === 0) {
|
|
10607
|
-
validated.push({ ...finding });
|
|
10608
|
-
continue;
|
|
10609
|
-
}
|
|
10610
|
-
if (graph) {
|
|
10611
|
-
try {
|
|
10612
|
-
let allReachable = true;
|
|
10613
|
-
for (const ref of crossFileRefs) {
|
|
10614
|
-
const reachable = await graph.isReachable(ref.from, ref.to);
|
|
10615
|
-
if (!reachable) {
|
|
10616
|
-
allReachable = false;
|
|
10617
|
-
break;
|
|
10618
|
-
}
|
|
10619
|
-
}
|
|
10620
|
-
if (allReachable) {
|
|
10621
|
-
validated.push({ ...finding, validatedBy: "graph" });
|
|
10622
|
-
}
|
|
10623
|
-
continue;
|
|
10624
|
-
} catch {
|
|
10625
|
-
}
|
|
10626
|
-
}
|
|
10627
|
-
{
|
|
10628
|
-
let chainValidated = false;
|
|
10629
|
-
if (fileContents) {
|
|
10630
|
-
for (const ref of crossFileRefs) {
|
|
10631
|
-
const normalizedFrom = normalizePath(ref.from, projectRoot);
|
|
10632
|
-
const reachable = followImportChain(normalizedFrom, fileContents, 2);
|
|
10633
|
-
const normalizedTo = normalizePath(ref.to, projectRoot);
|
|
10634
|
-
if (reachable.has(normalizedTo)) {
|
|
10635
|
-
chainValidated = true;
|
|
10636
|
-
break;
|
|
10637
|
-
}
|
|
10638
|
-
}
|
|
10639
|
-
}
|
|
10640
|
-
if (chainValidated) {
|
|
10641
|
-
validated.push({ ...finding, validatedBy: "heuristic" });
|
|
10642
|
-
} else {
|
|
10643
|
-
validated.push({
|
|
10644
|
-
...finding,
|
|
10645
|
-
severity: DOWNGRADE_MAP[finding.severity],
|
|
10646
|
-
validatedBy: "heuristic"
|
|
10647
|
-
});
|
|
10648
|
-
}
|
|
10649
|
-
}
|
|
10780
|
+
const result = await processFinding(finding, exclusionSet, graph, projectRoot, fileContents);
|
|
10781
|
+
if (result !== null) validated.push(result);
|
|
10650
10782
|
}
|
|
10651
10783
|
return validated;
|
|
10652
10784
|
}
|
|
@@ -11247,25 +11379,32 @@ function serializeRoadmap(roadmap) {
|
|
|
11247
11379
|
function serializeMilestoneHeading(milestone) {
|
|
11248
11380
|
return milestone.isBacklog ? "## Backlog" : `## ${milestone.name}`;
|
|
11249
11381
|
}
|
|
11382
|
+
function orDash(value) {
|
|
11383
|
+
return value ?? EM_DASH2;
|
|
11384
|
+
}
|
|
11385
|
+
function listOrDash(items) {
|
|
11386
|
+
return items.length > 0 ? items.join(", ") : EM_DASH2;
|
|
11387
|
+
}
|
|
11388
|
+
function serializeExtendedLines(feature) {
|
|
11389
|
+
const hasExtended = feature.assignee !== null || feature.priority !== null || feature.externalId !== null;
|
|
11390
|
+
if (!hasExtended) return [];
|
|
11391
|
+
return [
|
|
11392
|
+
`- **Assignee:** ${orDash(feature.assignee)}`,
|
|
11393
|
+
`- **Priority:** ${orDash(feature.priority)}`,
|
|
11394
|
+
`- **External-ID:** ${orDash(feature.externalId)}`
|
|
11395
|
+
];
|
|
11396
|
+
}
|
|
11250
11397
|
function serializeFeature(feature) {
|
|
11251
|
-
const spec = feature.spec ?? EM_DASH2;
|
|
11252
|
-
const plans = feature.plans.length > 0 ? feature.plans.join(", ") : EM_DASH2;
|
|
11253
|
-
const blockedBy = feature.blockedBy.length > 0 ? feature.blockedBy.join(", ") : EM_DASH2;
|
|
11254
11398
|
const lines = [
|
|
11255
11399
|
`### ${feature.name}`,
|
|
11256
11400
|
"",
|
|
11257
11401
|
`- **Status:** ${feature.status}`,
|
|
11258
|
-
`- **Spec:** ${spec}`,
|
|
11402
|
+
`- **Spec:** ${orDash(feature.spec)}`,
|
|
11259
11403
|
`- **Summary:** ${feature.summary}`,
|
|
11260
|
-
`- **Blockers:** ${blockedBy}`,
|
|
11261
|
-
`- **Plan:** ${plans}
|
|
11404
|
+
`- **Blockers:** ${listOrDash(feature.blockedBy)}`,
|
|
11405
|
+
`- **Plan:** ${listOrDash(feature.plans)}`,
|
|
11406
|
+
...serializeExtendedLines(feature)
|
|
11262
11407
|
];
|
|
11263
|
-
const hasExtended = feature.assignee !== null || feature.priority !== null || feature.externalId !== null;
|
|
11264
|
-
if (hasExtended) {
|
|
11265
|
-
lines.push(`- **Assignee:** ${feature.assignee ?? EM_DASH2}`);
|
|
11266
|
-
lines.push(`- **Priority:** ${feature.priority ?? EM_DASH2}`);
|
|
11267
|
-
lines.push(`- **External-ID:** ${feature.externalId ?? EM_DASH2}`);
|
|
11268
|
-
}
|
|
11269
11408
|
return lines;
|
|
11270
11409
|
}
|
|
11271
11410
|
function serializeAssignmentHistory(records) {
|
|
@@ -11299,6 +11438,26 @@ function isRegression(from, to) {
|
|
|
11299
11438
|
}
|
|
11300
11439
|
|
|
11301
11440
|
// src/roadmap/sync.ts
|
|
11441
|
+
function collectAutopilotStatuses(autopilotPath, featurePlans, allTaskStatuses) {
|
|
11442
|
+
try {
|
|
11443
|
+
const raw = fs24.readFileSync(autopilotPath, "utf-8");
|
|
11444
|
+
const autopilot = JSON.parse(raw);
|
|
11445
|
+
if (!autopilot.phases) return;
|
|
11446
|
+
const linkedPhases = autopilot.phases.filter(
|
|
11447
|
+
(phase) => phase.planPath ? featurePlans.some((p) => p === phase.planPath || phase.planPath.endsWith(p)) : false
|
|
11448
|
+
);
|
|
11449
|
+
for (const phase of linkedPhases) {
|
|
11450
|
+
if (phase.status === "complete") {
|
|
11451
|
+
allTaskStatuses.push("complete");
|
|
11452
|
+
} else if (phase.status === "pending") {
|
|
11453
|
+
allTaskStatuses.push("pending");
|
|
11454
|
+
} else {
|
|
11455
|
+
allTaskStatuses.push("in_progress");
|
|
11456
|
+
}
|
|
11457
|
+
}
|
|
11458
|
+
} catch {
|
|
11459
|
+
}
|
|
11460
|
+
}
|
|
11302
11461
|
function inferStatus(feature, projectPath, allFeatures) {
|
|
11303
11462
|
if (feature.blockedBy.length > 0) {
|
|
11304
11463
|
const blockerNotDone = feature.blockedBy.some((blockerName) => {
|
|
@@ -11334,26 +11493,7 @@ function inferStatus(feature, projectPath, allFeatures) {
|
|
|
11334
11493
|
if (!entry.isDirectory()) continue;
|
|
11335
11494
|
const autopilotPath = path24.join(sessionsDir, entry.name, "autopilot-state.json");
|
|
11336
11495
|
if (!fs24.existsSync(autopilotPath)) continue;
|
|
11337
|
-
|
|
11338
|
-
const raw = fs24.readFileSync(autopilotPath, "utf-8");
|
|
11339
|
-
const autopilot = JSON.parse(raw);
|
|
11340
|
-
if (!autopilot.phases) continue;
|
|
11341
|
-
const linkedPhases = autopilot.phases.filter(
|
|
11342
|
-
(phase) => phase.planPath ? feature.plans.some((p) => p === phase.planPath || phase.planPath.endsWith(p)) : false
|
|
11343
|
-
);
|
|
11344
|
-
if (linkedPhases.length > 0) {
|
|
11345
|
-
for (const phase of linkedPhases) {
|
|
11346
|
-
if (phase.status === "complete") {
|
|
11347
|
-
allTaskStatuses.push("complete");
|
|
11348
|
-
} else if (phase.status === "pending") {
|
|
11349
|
-
allTaskStatuses.push("pending");
|
|
11350
|
-
} else {
|
|
11351
|
-
allTaskStatuses.push("in_progress");
|
|
11352
|
-
}
|
|
11353
|
-
}
|
|
11354
|
-
}
|
|
11355
|
-
} catch {
|
|
11356
|
-
}
|
|
11496
|
+
collectAutopilotStatuses(autopilotPath, feature.plans, allTaskStatuses);
|
|
11357
11497
|
}
|
|
11358
11498
|
} catch {
|
|
11359
11499
|
}
|
|
@@ -11670,23 +11810,36 @@ var GitHubIssuesSyncAdapter = class {
|
|
|
11670
11810
|
return Err3(error instanceof Error ? error : new Error(String(error)));
|
|
11671
11811
|
}
|
|
11672
11812
|
}
|
|
11813
|
+
buildLabelsParam() {
|
|
11814
|
+
const filterLabels = this.config.labels ?? [];
|
|
11815
|
+
return filterLabels.length > 0 ? `&labels=${filterLabels.join(",")}` : "";
|
|
11816
|
+
}
|
|
11817
|
+
issueToTicketState(issue) {
|
|
11818
|
+
return {
|
|
11819
|
+
externalId: buildExternalId(this.owner, this.repo, issue.number),
|
|
11820
|
+
title: issue.title,
|
|
11821
|
+
status: issue.state,
|
|
11822
|
+
labels: issue.labels.map((l) => l.name),
|
|
11823
|
+
assignee: issue.assignee ? `@${issue.assignee.login}` : null
|
|
11824
|
+
};
|
|
11825
|
+
}
|
|
11826
|
+
async fetchIssuePage(page, labelsParam) {
|
|
11827
|
+
const perPage = 100;
|
|
11828
|
+
return fetchWithRetry(
|
|
11829
|
+
this.fetchFn,
|
|
11830
|
+
`${this.apiBase}/repos/${this.owner}/${this.repo}/issues?state=all&per_page=${perPage}&page=${page}${labelsParam}`,
|
|
11831
|
+
{ method: "GET", headers: this.headers() },
|
|
11832
|
+
this.retryOpts
|
|
11833
|
+
);
|
|
11834
|
+
}
|
|
11673
11835
|
async fetchAllTickets() {
|
|
11674
11836
|
try {
|
|
11675
|
-
const
|
|
11676
|
-
const labelsParam = filterLabels.length > 0 ? `&labels=${filterLabels.join(",")}` : "";
|
|
11837
|
+
const labelsParam = this.buildLabelsParam();
|
|
11677
11838
|
const tickets = [];
|
|
11678
11839
|
let page = 1;
|
|
11679
11840
|
const perPage = 100;
|
|
11680
11841
|
while (true) {
|
|
11681
|
-
const response = await
|
|
11682
|
-
this.fetchFn,
|
|
11683
|
-
`${this.apiBase}/repos/${this.owner}/${this.repo}/issues?state=all&per_page=${perPage}&page=${page}${labelsParam}`,
|
|
11684
|
-
{
|
|
11685
|
-
method: "GET",
|
|
11686
|
-
headers: this.headers()
|
|
11687
|
-
},
|
|
11688
|
-
this.retryOpts
|
|
11689
|
-
);
|
|
11842
|
+
const response = await this.fetchIssuePage(page, labelsParam);
|
|
11690
11843
|
if (!response.ok) {
|
|
11691
11844
|
const text = await response.text();
|
|
11692
11845
|
return Err3(new Error(`GitHub API error ${response.status}: ${text}`));
|
|
@@ -11694,13 +11847,7 @@ var GitHubIssuesSyncAdapter = class {
|
|
|
11694
11847
|
const data = await response.json();
|
|
11695
11848
|
const issues = data.filter((d) => !d.pull_request);
|
|
11696
11849
|
for (const issue of issues) {
|
|
11697
|
-
tickets.push(
|
|
11698
|
-
externalId: buildExternalId(this.owner, this.repo, issue.number),
|
|
11699
|
-
title: issue.title,
|
|
11700
|
-
status: issue.state,
|
|
11701
|
-
labels: issue.labels.map((l) => l.name),
|
|
11702
|
-
assignee: issue.assignee ? `@${issue.assignee.login}` : null
|
|
11703
|
-
});
|
|
11850
|
+
tickets.push(this.issueToTicketState(issue));
|
|
11704
11851
|
}
|
|
11705
11852
|
if (data.length < perPage) break;
|
|
11706
11853
|
page++;
|
|
@@ -11795,6 +11942,22 @@ async function syncToExternal(roadmap, adapter, config, prefetchedTickets) {
|
|
|
11795
11942
|
}
|
|
11796
11943
|
return result;
|
|
11797
11944
|
}
|
|
11945
|
+
function applyTicketToFeature(ticketState, feature, config, forceSync, result) {
|
|
11946
|
+
if (ticketState.assignee !== feature.assignee) {
|
|
11947
|
+
result.assignmentChanges.push({
|
|
11948
|
+
feature: feature.name,
|
|
11949
|
+
from: feature.assignee,
|
|
11950
|
+
to: ticketState.assignee
|
|
11951
|
+
});
|
|
11952
|
+
feature.assignee = ticketState.assignee;
|
|
11953
|
+
}
|
|
11954
|
+
const resolvedStatus = resolveReverseStatus(ticketState.status, ticketState.labels, config);
|
|
11955
|
+
if (!resolvedStatus || resolvedStatus === feature.status) return;
|
|
11956
|
+
const newStatus = resolvedStatus;
|
|
11957
|
+
if (!forceSync && isRegression(feature.status, newStatus)) return;
|
|
11958
|
+
if (!forceSync && feature.status === "blocked" && newStatus === "planned") return;
|
|
11959
|
+
feature.status = newStatus;
|
|
11960
|
+
}
|
|
11798
11961
|
async function syncFromExternal(roadmap, adapter, config, options, prefetchedTickets) {
|
|
11799
11962
|
const result = emptySyncResult();
|
|
11800
11963
|
const forceSync = options?.forceSync ?? false;
|
|
@@ -11821,25 +11984,7 @@ async function syncFromExternal(roadmap, adapter, config, options, prefetchedTic
|
|
|
11821
11984
|
for (const ticketState of tickets) {
|
|
11822
11985
|
const feature = featureByExternalId.get(ticketState.externalId);
|
|
11823
11986
|
if (!feature) continue;
|
|
11824
|
-
|
|
11825
|
-
result.assignmentChanges.push({
|
|
11826
|
-
feature: feature.name,
|
|
11827
|
-
from: feature.assignee,
|
|
11828
|
-
to: ticketState.assignee
|
|
11829
|
-
});
|
|
11830
|
-
feature.assignee = ticketState.assignee;
|
|
11831
|
-
}
|
|
11832
|
-
const resolvedStatus = resolveReverseStatus(ticketState.status, ticketState.labels, config);
|
|
11833
|
-
if (resolvedStatus && resolvedStatus !== feature.status) {
|
|
11834
|
-
const newStatus = resolvedStatus;
|
|
11835
|
-
if (!forceSync && isRegression(feature.status, newStatus)) {
|
|
11836
|
-
continue;
|
|
11837
|
-
}
|
|
11838
|
-
if (!forceSync && feature.status === "blocked" && newStatus === "planned") {
|
|
11839
|
-
continue;
|
|
11840
|
-
}
|
|
11841
|
-
feature.status = newStatus;
|
|
11842
|
-
}
|
|
11987
|
+
applyTicketToFeature(ticketState, feature, config, forceSync, result);
|
|
11843
11988
|
}
|
|
11844
11989
|
return result;
|
|
11845
11990
|
}
|
|
@@ -11887,6 +12032,24 @@ var PRIORITY_RANK = {
|
|
|
11887
12032
|
var POSITION_WEIGHT = 0.5;
|
|
11888
12033
|
var DEPENDENTS_WEIGHT = 0.3;
|
|
11889
12034
|
var AFFINITY_WEIGHT = 0.2;
|
|
12035
|
+
function isEligibleCandidate(feature, allFeatureNames, doneFeatures) {
|
|
12036
|
+
if (feature.status !== "planned" && feature.status !== "backlog") return false;
|
|
12037
|
+
const isBlocked = feature.blockedBy.some((blocker) => {
|
|
12038
|
+
const key = blocker.toLowerCase();
|
|
12039
|
+
return allFeatureNames.has(key) && !doneFeatures.has(key);
|
|
12040
|
+
});
|
|
12041
|
+
return !isBlocked;
|
|
12042
|
+
}
|
|
12043
|
+
function computeAffinityScore(feature, milestoneName, milestoneMap, userCompletedFeatures) {
|
|
12044
|
+
if (userCompletedFeatures.size === 0) return 0;
|
|
12045
|
+
const completedBlocker = feature.blockedBy.some(
|
|
12046
|
+
(b) => userCompletedFeatures.has(b.toLowerCase())
|
|
12047
|
+
);
|
|
12048
|
+
if (completedBlocker) return 1;
|
|
12049
|
+
const siblings = milestoneMap.get(milestoneName) ?? [];
|
|
12050
|
+
const completedSibling = siblings.some((s) => userCompletedFeatures.has(s));
|
|
12051
|
+
return completedSibling ? 0.5 : 0;
|
|
12052
|
+
}
|
|
11890
12053
|
function scoreRoadmapCandidates(roadmap, options) {
|
|
11891
12054
|
const allFeatures = roadmap.milestones.flatMap((m) => m.features);
|
|
11892
12055
|
const allFeatureNames = new Set(allFeatures.map((f) => f.name.toLowerCase()));
|
|
@@ -11925,33 +12088,18 @@ function scoreRoadmapCandidates(roadmap, options) {
|
|
|
11925
12088
|
const candidates = [];
|
|
11926
12089
|
let globalPosition = 0;
|
|
11927
12090
|
for (const ms of roadmap.milestones) {
|
|
11928
|
-
for (
|
|
11929
|
-
const feature = ms.features[featureIdx];
|
|
12091
|
+
for (const feature of ms.features) {
|
|
11930
12092
|
globalPosition++;
|
|
11931
|
-
if (feature
|
|
11932
|
-
const isBlocked = feature.blockedBy.some((blocker) => {
|
|
11933
|
-
const key = blocker.toLowerCase();
|
|
11934
|
-
return allFeatureNames.has(key) && !doneFeatures.has(key);
|
|
11935
|
-
});
|
|
11936
|
-
if (isBlocked) continue;
|
|
12093
|
+
if (!isEligibleCandidate(feature, allFeatureNames, doneFeatures)) continue;
|
|
11937
12094
|
const positionScore = 1 - (globalPosition - 1) / totalPositions;
|
|
11938
12095
|
const deps = dependentsCount.get(feature.name.toLowerCase()) ?? 0;
|
|
11939
12096
|
const dependentsScore = deps / maxDependents;
|
|
11940
|
-
|
|
11941
|
-
|
|
11942
|
-
|
|
11943
|
-
|
|
11944
|
-
|
|
11945
|
-
|
|
11946
|
-
affinityScore = 1;
|
|
11947
|
-
} else {
|
|
11948
|
-
const siblings = milestoneMap.get(ms.name) ?? [];
|
|
11949
|
-
const completedSiblings = siblings.filter((s) => userCompletedFeatures.has(s));
|
|
11950
|
-
if (completedSiblings.length > 0) {
|
|
11951
|
-
affinityScore = 0.5;
|
|
11952
|
-
}
|
|
11953
|
-
}
|
|
11954
|
-
}
|
|
12097
|
+
const affinityScore = computeAffinityScore(
|
|
12098
|
+
feature,
|
|
12099
|
+
ms.name,
|
|
12100
|
+
milestoneMap,
|
|
12101
|
+
userCompletedFeatures
|
|
12102
|
+
);
|
|
11955
12103
|
const weightedScore = POSITION_WEIGHT * positionScore + DEPENDENTS_WEIGHT * dependentsScore + AFFINITY_WEIGHT * affinityScore;
|
|
11956
12104
|
const priorityTier = feature.priority ? PRIORITY_RANK[feature.priority] : null;
|
|
11957
12105
|
candidates.push({
|
|
@@ -12292,9 +12440,9 @@ async function resolveWasmPath(grammarName) {
|
|
|
12292
12440
|
const { createRequire } = await import("module");
|
|
12293
12441
|
const require2 = createRequire(import.meta.url ?? __filename);
|
|
12294
12442
|
const pkgPath = require2.resolve("tree-sitter-wasms/package.json");
|
|
12295
|
-
const
|
|
12296
|
-
const pkgDir =
|
|
12297
|
-
return
|
|
12443
|
+
const path34 = await import("path");
|
|
12444
|
+
const pkgDir = path34.dirname(pkgPath);
|
|
12445
|
+
return path34.join(pkgDir, "out", `${grammarName}.wasm`);
|
|
12298
12446
|
}
|
|
12299
12447
|
async function loadLanguage(lang) {
|
|
12300
12448
|
const grammarName = GRAMMAR_MAP[lang];
|
|
@@ -13012,6 +13160,27 @@ function aggregateByDay(records) {
|
|
|
13012
13160
|
// src/usage/jsonl-reader.ts
|
|
13013
13161
|
import * as fs30 from "fs";
|
|
13014
13162
|
import * as path29 from "path";
|
|
13163
|
+
function extractTokenUsage(entry, lineNumber) {
|
|
13164
|
+
const tokenUsage = entry.token_usage;
|
|
13165
|
+
if (!tokenUsage || typeof tokenUsage !== "object") {
|
|
13166
|
+
console.warn(
|
|
13167
|
+
`[harness usage] Skipping malformed JSONL line ${lineNumber}: missing token_usage`
|
|
13168
|
+
);
|
|
13169
|
+
return null;
|
|
13170
|
+
}
|
|
13171
|
+
return tokenUsage;
|
|
13172
|
+
}
|
|
13173
|
+
function applyOptionalFields2(record, entry) {
|
|
13174
|
+
if (entry.cache_creation_tokens != null) {
|
|
13175
|
+
record.cacheCreationTokens = entry.cache_creation_tokens;
|
|
13176
|
+
}
|
|
13177
|
+
if (entry.cache_read_tokens != null) {
|
|
13178
|
+
record.cacheReadTokens = entry.cache_read_tokens;
|
|
13179
|
+
}
|
|
13180
|
+
if (entry.model != null) {
|
|
13181
|
+
record.model = entry.model;
|
|
13182
|
+
}
|
|
13183
|
+
}
|
|
13015
13184
|
function parseLine(line, lineNumber) {
|
|
13016
13185
|
let entry;
|
|
13017
13186
|
try {
|
|
@@ -13020,13 +13189,8 @@ function parseLine(line, lineNumber) {
|
|
|
13020
13189
|
console.warn(`[harness usage] Skipping malformed JSONL line ${lineNumber}`);
|
|
13021
13190
|
return null;
|
|
13022
13191
|
}
|
|
13023
|
-
const tokenUsage = entry
|
|
13024
|
-
if (!tokenUsage
|
|
13025
|
-
console.warn(
|
|
13026
|
-
`[harness usage] Skipping malformed JSONL line ${lineNumber}: missing token_usage`
|
|
13027
|
-
);
|
|
13028
|
-
return null;
|
|
13029
|
-
}
|
|
13192
|
+
const tokenUsage = extractTokenUsage(entry, lineNumber);
|
|
13193
|
+
if (!tokenUsage) return null;
|
|
13030
13194
|
const inputTokens = tokenUsage.input_tokens ?? 0;
|
|
13031
13195
|
const outputTokens = tokenUsage.output_tokens ?? 0;
|
|
13032
13196
|
const record = {
|
|
@@ -13038,15 +13202,7 @@ function parseLine(line, lineNumber) {
|
|
|
13038
13202
|
totalTokens: inputTokens + outputTokens
|
|
13039
13203
|
}
|
|
13040
13204
|
};
|
|
13041
|
-
|
|
13042
|
-
record.cacheCreationTokens = entry.cache_creation_tokens;
|
|
13043
|
-
}
|
|
13044
|
-
if (entry.cache_read_tokens != null) {
|
|
13045
|
-
record.cacheReadTokens = entry.cache_read_tokens;
|
|
13046
|
-
}
|
|
13047
|
-
if (entry.model != null) {
|
|
13048
|
-
record.model = entry.model;
|
|
13049
|
-
}
|
|
13205
|
+
applyOptionalFields2(record, entry);
|
|
13050
13206
|
return record;
|
|
13051
13207
|
}
|
|
13052
13208
|
function readCostRecords(projectRoot) {
|
|
@@ -13081,6 +13237,14 @@ function extractUsage(entry) {
|
|
|
13081
13237
|
const usage = message.usage;
|
|
13082
13238
|
return usage && typeof usage === "object" && !Array.isArray(usage) ? usage : null;
|
|
13083
13239
|
}
|
|
13240
|
+
function applyOptionalCCFields(record, message, usage) {
|
|
13241
|
+
const model = message.model;
|
|
13242
|
+
if (model) record.model = model;
|
|
13243
|
+
const cacheCreate = usage.cache_creation_input_tokens;
|
|
13244
|
+
const cacheRead = usage.cache_read_input_tokens;
|
|
13245
|
+
if (typeof cacheCreate === "number" && cacheCreate > 0) record.cacheCreationTokens = cacheCreate;
|
|
13246
|
+
if (typeof cacheRead === "number" && cacheRead > 0) record.cacheReadTokens = cacheRead;
|
|
13247
|
+
}
|
|
13084
13248
|
function buildRecord(entry, usage) {
|
|
13085
13249
|
const inputTokens = Number(usage.input_tokens) || 0;
|
|
13086
13250
|
const outputTokens = Number(usage.output_tokens) || 0;
|
|
@@ -13091,12 +13255,7 @@ function buildRecord(entry, usage) {
|
|
|
13091
13255
|
tokens: { inputTokens, outputTokens, totalTokens: inputTokens + outputTokens },
|
|
13092
13256
|
_source: "claude-code"
|
|
13093
13257
|
};
|
|
13094
|
-
|
|
13095
|
-
if (model) record.model = model;
|
|
13096
|
-
const cacheCreate = usage.cache_creation_input_tokens;
|
|
13097
|
-
const cacheRead = usage.cache_read_input_tokens;
|
|
13098
|
-
if (typeof cacheCreate === "number" && cacheCreate > 0) record.cacheCreationTokens = cacheCreate;
|
|
13099
|
-
if (typeof cacheRead === "number" && cacheRead > 0) record.cacheReadTokens = cacheRead;
|
|
13258
|
+
applyOptionalCCFields(record, message, usage);
|
|
13100
13259
|
return record;
|
|
13101
13260
|
}
|
|
13102
13261
|
function parseCCLine(line, filePath, lineNumber) {
|
|
@@ -13163,13 +13322,457 @@ function parseCCRecords() {
|
|
|
13163
13322
|
return records;
|
|
13164
13323
|
}
|
|
13165
13324
|
|
|
13166
|
-
// src/
|
|
13167
|
-
|
|
13325
|
+
// src/adoption/reader.ts
|
|
13326
|
+
import * as fs32 from "fs";
|
|
13327
|
+
import * as path31 from "path";
|
|
13328
|
+
function parseLine2(line, lineNumber) {
|
|
13329
|
+
try {
|
|
13330
|
+
const parsed = JSON.parse(line);
|
|
13331
|
+
if (typeof parsed.skill !== "string" || typeof parsed.startedAt !== "string" || typeof parsed.duration !== "number" || typeof parsed.outcome !== "string" || !Array.isArray(parsed.phasesReached)) {
|
|
13332
|
+
process.stderr.write(
|
|
13333
|
+
`[harness adoption] Skipping malformed JSONL line ${lineNumber}: missing required fields
|
|
13334
|
+
`
|
|
13335
|
+
);
|
|
13336
|
+
return null;
|
|
13337
|
+
}
|
|
13338
|
+
return parsed;
|
|
13339
|
+
} catch {
|
|
13340
|
+
process.stderr.write(`[harness adoption] Skipping malformed JSONL line ${lineNumber}
|
|
13341
|
+
`);
|
|
13342
|
+
return null;
|
|
13343
|
+
}
|
|
13344
|
+
}
|
|
13345
|
+
function readAdoptionRecords(projectRoot) {
|
|
13346
|
+
const adoptionFile = path31.join(projectRoot, ".harness", "metrics", "adoption.jsonl");
|
|
13347
|
+
let raw;
|
|
13348
|
+
try {
|
|
13349
|
+
raw = fs32.readFileSync(adoptionFile, "utf-8");
|
|
13350
|
+
} catch {
|
|
13351
|
+
return [];
|
|
13352
|
+
}
|
|
13353
|
+
const records = [];
|
|
13354
|
+
const lines = raw.split("\n");
|
|
13355
|
+
for (let i = 0; i < lines.length; i++) {
|
|
13356
|
+
const line = lines[i]?.trim();
|
|
13357
|
+
if (!line) continue;
|
|
13358
|
+
const record = parseLine2(line, i + 1);
|
|
13359
|
+
if (record) {
|
|
13360
|
+
records.push(record);
|
|
13361
|
+
}
|
|
13362
|
+
}
|
|
13363
|
+
return records;
|
|
13364
|
+
}
|
|
13365
|
+
|
|
13366
|
+
// src/adoption/aggregator.ts
|
|
13367
|
+
function aggregateBySkill(records) {
|
|
13368
|
+
if (records.length === 0) return [];
|
|
13369
|
+
const skillMap = /* @__PURE__ */ new Map();
|
|
13370
|
+
for (const record of records) {
|
|
13371
|
+
if (!skillMap.has(record.skill)) {
|
|
13372
|
+
const entry = { records: [] };
|
|
13373
|
+
if (record.tier != null) entry.tier = record.tier;
|
|
13374
|
+
skillMap.set(record.skill, entry);
|
|
13375
|
+
}
|
|
13376
|
+
skillMap.get(record.skill).records.push(record);
|
|
13377
|
+
}
|
|
13378
|
+
const results = [];
|
|
13379
|
+
for (const [skill, bucket] of skillMap) {
|
|
13380
|
+
const invocations = bucket.records.length;
|
|
13381
|
+
const completedCount = bucket.records.filter((r) => r.outcome === "completed").length;
|
|
13382
|
+
const totalDuration = bucket.records.reduce((sum, r) => sum + r.duration, 0);
|
|
13383
|
+
const timestamps = bucket.records.map((r) => r.startedAt).sort();
|
|
13384
|
+
const summary = {
|
|
13385
|
+
skill,
|
|
13386
|
+
invocations,
|
|
13387
|
+
successRate: completedCount / invocations,
|
|
13388
|
+
avgDuration: totalDuration / invocations,
|
|
13389
|
+
lastUsed: timestamps[timestamps.length - 1]
|
|
13390
|
+
};
|
|
13391
|
+
if (bucket.tier != null) summary.tier = bucket.tier;
|
|
13392
|
+
results.push(summary);
|
|
13393
|
+
}
|
|
13394
|
+
results.sort((a, b) => b.invocations - a.invocations);
|
|
13395
|
+
return results;
|
|
13396
|
+
}
|
|
13397
|
+
function aggregateByDay2(records) {
|
|
13398
|
+
if (records.length === 0) return [];
|
|
13399
|
+
const dayMap = /* @__PURE__ */ new Map();
|
|
13400
|
+
for (const record of records) {
|
|
13401
|
+
const date = record.startedAt.slice(0, 10);
|
|
13402
|
+
if (!dayMap.has(date)) {
|
|
13403
|
+
dayMap.set(date, { invocations: 0, skills: /* @__PURE__ */ new Set() });
|
|
13404
|
+
}
|
|
13405
|
+
const bucket = dayMap.get(date);
|
|
13406
|
+
bucket.invocations++;
|
|
13407
|
+
bucket.skills.add(record.skill);
|
|
13408
|
+
}
|
|
13409
|
+
const results = [];
|
|
13410
|
+
for (const [date, bucket] of dayMap) {
|
|
13411
|
+
results.push({
|
|
13412
|
+
date,
|
|
13413
|
+
invocations: bucket.invocations,
|
|
13414
|
+
uniqueSkills: bucket.skills.size
|
|
13415
|
+
});
|
|
13416
|
+
}
|
|
13417
|
+
results.sort((a, b) => b.date.localeCompare(a.date));
|
|
13418
|
+
return results;
|
|
13419
|
+
}
|
|
13420
|
+
function topSkills(records, n) {
|
|
13421
|
+
return aggregateBySkill(records).slice(0, n);
|
|
13422
|
+
}
|
|
13423
|
+
|
|
13424
|
+
// src/compaction/strategies/structural.ts
|
|
13425
|
+
function isEmptyObject(v) {
|
|
13426
|
+
return typeof v === "object" && v !== null && !Array.isArray(v) && Object.keys(v).length === 0;
|
|
13427
|
+
}
|
|
13428
|
+
function isRetainable(v) {
|
|
13429
|
+
return v !== void 0 && v !== "" && v !== null && !isEmptyObject(v);
|
|
13430
|
+
}
|
|
13431
|
+
function cleanArray(value) {
|
|
13432
|
+
const cleaned = value.map(cleanValue).filter(isRetainable);
|
|
13433
|
+
if (cleaned.length === 0) return void 0;
|
|
13434
|
+
if (cleaned.length === 1) return cleaned[0];
|
|
13435
|
+
return cleaned;
|
|
13436
|
+
}
|
|
13437
|
+
function cleanRecord(value) {
|
|
13438
|
+
const cleaned = {};
|
|
13439
|
+
for (const [k, v] of Object.entries(value)) {
|
|
13440
|
+
const result = cleanValue(v);
|
|
13441
|
+
if (isRetainable(result)) {
|
|
13442
|
+
cleaned[k] = result;
|
|
13443
|
+
}
|
|
13444
|
+
}
|
|
13445
|
+
if (Object.keys(cleaned).length === 0) return void 0;
|
|
13446
|
+
return cleaned;
|
|
13447
|
+
}
|
|
13448
|
+
function cleanValue(value) {
|
|
13449
|
+
if (value === null || value === void 0) return void 0;
|
|
13450
|
+
if (typeof value === "string") return value.replace(/\s+/g, " ").trim();
|
|
13451
|
+
if (Array.isArray(value)) return cleanArray(value);
|
|
13452
|
+
if (typeof value === "object") return cleanRecord(value);
|
|
13453
|
+
return value;
|
|
13454
|
+
}
|
|
13455
|
+
var StructuralStrategy = class {
|
|
13456
|
+
name = "structural";
|
|
13457
|
+
lossy = false;
|
|
13458
|
+
apply(content, _budget) {
|
|
13459
|
+
let parsed;
|
|
13460
|
+
try {
|
|
13461
|
+
parsed = JSON.parse(content);
|
|
13462
|
+
} catch {
|
|
13463
|
+
return content;
|
|
13464
|
+
}
|
|
13465
|
+
const cleaned = cleanValue(parsed);
|
|
13466
|
+
return JSON.stringify(cleaned) ?? "";
|
|
13467
|
+
}
|
|
13468
|
+
};
|
|
13469
|
+
|
|
13470
|
+
// src/compaction/strategies/truncation.ts
|
|
13471
|
+
var DEFAULT_TOKEN_BUDGET = 4e3;
|
|
13472
|
+
var CHARS_PER_TOKEN = 4;
|
|
13473
|
+
var TRUNCATION_MARKER = "\n[truncated \u2014 prioritized truncation applied]";
|
|
13474
|
+
function lineScore(line) {
|
|
13475
|
+
let score = 0;
|
|
13476
|
+
if (/\/[\w./-]/.test(line)) score += 40;
|
|
13477
|
+
if (/error|Error|ERROR|fail|FAIL|status/i.test(line)) score += 35;
|
|
13478
|
+
if (/\b[A-Z][a-z]+[A-Z]/.test(line) || /\b[a-z]+[A-Z]/.test(line)) score += 20;
|
|
13479
|
+
if (line.trim().length < 40) score += 10;
|
|
13480
|
+
return score;
|
|
13481
|
+
}
|
|
13482
|
+
function selectLines(lines, charBudget) {
|
|
13483
|
+
const scored = lines.map((line, idx) => ({ line, idx, score: lineScore(line) }));
|
|
13484
|
+
scored.sort((a, b) => b.score - a.score || a.idx - b.idx);
|
|
13485
|
+
const kept = [];
|
|
13486
|
+
let used = 0;
|
|
13487
|
+
for (const item of scored) {
|
|
13488
|
+
const lineLen = item.line.length + 1;
|
|
13489
|
+
if (used + lineLen > charBudget) continue;
|
|
13490
|
+
kept.push({ line: item.line, idx: item.idx });
|
|
13491
|
+
used += lineLen;
|
|
13492
|
+
}
|
|
13493
|
+
kept.sort((a, b) => a.idx - b.idx);
|
|
13494
|
+
return kept;
|
|
13495
|
+
}
|
|
13496
|
+
var TruncationStrategy = class {
|
|
13497
|
+
name = "truncate";
|
|
13498
|
+
lossy = false;
|
|
13499
|
+
// deliberate: spec Decision 2 — truncation is classified lossless at the pipeline level
|
|
13500
|
+
apply(content, budget = DEFAULT_TOKEN_BUDGET) {
|
|
13501
|
+
if (!content) return content;
|
|
13502
|
+
const charBudget = budget * CHARS_PER_TOKEN;
|
|
13503
|
+
if (content.length <= charBudget) return content;
|
|
13504
|
+
const lines = content.split("\n");
|
|
13505
|
+
const available = charBudget - TRUNCATION_MARKER.length;
|
|
13506
|
+
const kept = available > 0 ? selectLines(lines, available) : [{ line: (lines[0] ?? "").slice(0, charBudget), idx: 0 }];
|
|
13507
|
+
return kept.map((k) => k.line).join("\n") + TRUNCATION_MARKER;
|
|
13508
|
+
}
|
|
13509
|
+
};
|
|
13510
|
+
|
|
13511
|
+
// src/compaction/pipeline.ts
|
|
13512
|
+
var CompactionPipeline = class {
|
|
13513
|
+
strategies;
|
|
13514
|
+
constructor(strategies) {
|
|
13515
|
+
this.strategies = strategies;
|
|
13516
|
+
}
|
|
13517
|
+
/** The ordered list of strategy names in this pipeline. */
|
|
13518
|
+
get strategyNames() {
|
|
13519
|
+
return this.strategies.map((s) => s.name);
|
|
13520
|
+
}
|
|
13521
|
+
/**
|
|
13522
|
+
* Apply all strategies in order.
|
|
13523
|
+
* @param content — input string
|
|
13524
|
+
* @param budget — optional token budget forwarded to each strategy
|
|
13525
|
+
*/
|
|
13526
|
+
apply(content, budget) {
|
|
13527
|
+
return this.strategies.reduce((current, strategy) => {
|
|
13528
|
+
return strategy.apply(current, budget);
|
|
13529
|
+
}, content);
|
|
13530
|
+
}
|
|
13531
|
+
};
|
|
13532
|
+
|
|
13533
|
+
// src/caching/stability.ts
|
|
13534
|
+
import { NODE_STABILITY } from "@harness-engineering/graph";
|
|
13535
|
+
var STABILITY_LOOKUP = {};
|
|
13536
|
+
for (const [key, tier] of Object.entries(NODE_STABILITY)) {
|
|
13537
|
+
STABILITY_LOOKUP[key] = tier;
|
|
13538
|
+
STABILITY_LOOKUP[key.toLowerCase()] = tier;
|
|
13539
|
+
}
|
|
13540
|
+
STABILITY_LOOKUP["packed_summary"] = "session";
|
|
13541
|
+
STABILITY_LOOKUP["skill"] = "static";
|
|
13542
|
+
function resolveStability(contentType) {
|
|
13543
|
+
return STABILITY_LOOKUP[contentType] ?? "ephemeral";
|
|
13544
|
+
}
|
|
13545
|
+
|
|
13546
|
+
// src/caching/adapters/anthropic.ts
|
|
13547
|
+
var TIER_ORDER = {
|
|
13548
|
+
static: 0,
|
|
13549
|
+
session: 1,
|
|
13550
|
+
ephemeral: 2
|
|
13551
|
+
};
|
|
13552
|
+
var AnthropicCacheAdapter = class {
|
|
13553
|
+
provider = "claude";
|
|
13554
|
+
wrapSystemBlock(content, stability) {
|
|
13555
|
+
if (stability === "ephemeral") {
|
|
13556
|
+
return { type: "text", text: content };
|
|
13557
|
+
}
|
|
13558
|
+
const ttl = stability === "static" ? "1h" : void 0;
|
|
13559
|
+
return {
|
|
13560
|
+
type: "text",
|
|
13561
|
+
text: content,
|
|
13562
|
+
cache_control: {
|
|
13563
|
+
type: "ephemeral",
|
|
13564
|
+
...ttl !== void 0 && { ttl }
|
|
13565
|
+
}
|
|
13566
|
+
};
|
|
13567
|
+
}
|
|
13568
|
+
wrapTools(tools, stability) {
|
|
13569
|
+
if (tools.length === 0 || stability === "ephemeral") {
|
|
13570
|
+
return { tools: tools.map((t) => ({ ...t })) };
|
|
13571
|
+
}
|
|
13572
|
+
const wrapped = tools.map((t) => ({ ...t }));
|
|
13573
|
+
const last = wrapped[wrapped.length - 1];
|
|
13574
|
+
if (last) {
|
|
13575
|
+
last.cache_control = { type: "ephemeral" };
|
|
13576
|
+
}
|
|
13577
|
+
return { tools: wrapped };
|
|
13578
|
+
}
|
|
13579
|
+
orderContent(blocks) {
|
|
13580
|
+
return [...blocks].sort((a, b) => TIER_ORDER[a.stability] - TIER_ORDER[b.stability]);
|
|
13581
|
+
}
|
|
13582
|
+
parseCacheUsage(response) {
|
|
13583
|
+
const resp = response;
|
|
13584
|
+
const usage = resp?.usage;
|
|
13585
|
+
return {
|
|
13586
|
+
cacheCreationTokens: usage?.cache_creation_input_tokens ?? 0,
|
|
13587
|
+
cacheReadTokens: usage?.cache_read_input_tokens ?? 0
|
|
13588
|
+
};
|
|
13589
|
+
}
|
|
13590
|
+
};
|
|
13591
|
+
|
|
13592
|
+
// src/caching/adapters/openai.ts
|
|
13593
|
+
var TIER_ORDER2 = {
|
|
13594
|
+
static: 0,
|
|
13595
|
+
session: 1,
|
|
13596
|
+
ephemeral: 2
|
|
13597
|
+
};
|
|
13598
|
+
var OpenAICacheAdapter = class {
|
|
13599
|
+
provider = "openai";
|
|
13600
|
+
wrapSystemBlock(content, _stability) {
|
|
13601
|
+
return { type: "text", text: content };
|
|
13602
|
+
}
|
|
13603
|
+
wrapTools(tools, _stability) {
|
|
13604
|
+
return { tools: tools.map((t) => ({ ...t })) };
|
|
13605
|
+
}
|
|
13606
|
+
orderContent(blocks) {
|
|
13607
|
+
return [...blocks].sort((a, b) => TIER_ORDER2[a.stability] - TIER_ORDER2[b.stability]);
|
|
13608
|
+
}
|
|
13609
|
+
parseCacheUsage(response) {
|
|
13610
|
+
const resp = response;
|
|
13611
|
+
const usage = resp?.usage;
|
|
13612
|
+
const details = usage?.prompt_tokens_details;
|
|
13613
|
+
return {
|
|
13614
|
+
cacheCreationTokens: 0,
|
|
13615
|
+
cacheReadTokens: details?.cached_tokens ?? 0
|
|
13616
|
+
};
|
|
13617
|
+
}
|
|
13618
|
+
};
|
|
13619
|
+
|
|
13620
|
+
// src/caching/adapters/gemini.ts
|
|
13621
|
+
var TIER_ORDER3 = {
|
|
13622
|
+
static: 0,
|
|
13623
|
+
session: 1,
|
|
13624
|
+
ephemeral: 2
|
|
13625
|
+
};
|
|
13626
|
+
var CACHED_CONTENT_MARKER = "cachedContents:pending";
|
|
13627
|
+
var GeminiCacheAdapter = class {
|
|
13628
|
+
provider = "gemini";
|
|
13629
|
+
wrapSystemBlock(content, stability) {
|
|
13630
|
+
if (stability === "static") {
|
|
13631
|
+
return {
|
|
13632
|
+
type: "text",
|
|
13633
|
+
text: content,
|
|
13634
|
+
cachedContentRef: CACHED_CONTENT_MARKER
|
|
13635
|
+
};
|
|
13636
|
+
}
|
|
13637
|
+
return { type: "text", text: content };
|
|
13638
|
+
}
|
|
13639
|
+
wrapTools(tools, _stability) {
|
|
13640
|
+
return { tools: tools.map((t) => ({ ...t })) };
|
|
13641
|
+
}
|
|
13642
|
+
orderContent(blocks) {
|
|
13643
|
+
return [...blocks].sort((a, b) => TIER_ORDER3[a.stability] - TIER_ORDER3[b.stability]);
|
|
13644
|
+
}
|
|
13645
|
+
parseCacheUsage(response) {
|
|
13646
|
+
const resp = response;
|
|
13647
|
+
const metadata = resp?.usageMetadata;
|
|
13648
|
+
return {
|
|
13649
|
+
cacheCreationTokens: 0,
|
|
13650
|
+
cacheReadTokens: metadata?.cachedContentTokenCount ?? 0
|
|
13651
|
+
};
|
|
13652
|
+
}
|
|
13653
|
+
};
|
|
13654
|
+
|
|
13655
|
+
// src/telemetry/consent.ts
|
|
13656
|
+
import * as fs34 from "fs";
|
|
13657
|
+
import * as path33 from "path";
|
|
13658
|
+
|
|
13659
|
+
// src/telemetry/install-id.ts
|
|
13660
|
+
import * as fs33 from "fs";
|
|
13661
|
+
import * as path32 from "path";
|
|
13662
|
+
import * as crypto4 from "crypto";
|
|
13663
|
+
function getOrCreateInstallId(projectRoot) {
|
|
13664
|
+
const harnessDir = path32.join(projectRoot, ".harness");
|
|
13665
|
+
const installIdFile = path32.join(harnessDir, ".install-id");
|
|
13666
|
+
const UUID_V4_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
13667
|
+
try {
|
|
13668
|
+
const existing = fs33.readFileSync(installIdFile, "utf-8").trim();
|
|
13669
|
+
if (UUID_V4_RE.test(existing)) {
|
|
13670
|
+
return existing;
|
|
13671
|
+
}
|
|
13672
|
+
} catch {
|
|
13673
|
+
}
|
|
13674
|
+
const id = crypto4.randomUUID();
|
|
13675
|
+
fs33.mkdirSync(harnessDir, { recursive: true });
|
|
13676
|
+
fs33.writeFileSync(installIdFile, id, { encoding: "utf-8", mode: 384 });
|
|
13677
|
+
return id;
|
|
13678
|
+
}
|
|
13679
|
+
|
|
13680
|
+
// src/telemetry/consent.ts
|
|
13681
|
+
function readIdentity(projectRoot) {
|
|
13682
|
+
const filePath = path33.join(projectRoot, ".harness", "telemetry.json");
|
|
13683
|
+
try {
|
|
13684
|
+
const raw = fs34.readFileSync(filePath, "utf-8");
|
|
13685
|
+
const parsed = JSON.parse(raw);
|
|
13686
|
+
if (parsed && typeof parsed === "object" && parsed.identity) {
|
|
13687
|
+
const { project, team, alias } = parsed.identity;
|
|
13688
|
+
const identity = {};
|
|
13689
|
+
if (typeof project === "string") identity.project = project;
|
|
13690
|
+
if (typeof team === "string") identity.team = team;
|
|
13691
|
+
if (typeof alias === "string") identity.alias = alias;
|
|
13692
|
+
return identity;
|
|
13693
|
+
}
|
|
13694
|
+
return {};
|
|
13695
|
+
} catch {
|
|
13696
|
+
return {};
|
|
13697
|
+
}
|
|
13698
|
+
}
|
|
13699
|
+
function resolveConsent(projectRoot, config) {
|
|
13700
|
+
if (process.env.DO_NOT_TRACK === "1") return { allowed: false };
|
|
13701
|
+
if (process.env.HARNESS_TELEMETRY_OPTOUT === "1") return { allowed: false };
|
|
13702
|
+
const enabled = config?.enabled ?? true;
|
|
13703
|
+
if (!enabled) return { allowed: false };
|
|
13704
|
+
const installId = getOrCreateInstallId(projectRoot);
|
|
13705
|
+
const identity = readIdentity(projectRoot);
|
|
13706
|
+
return { allowed: true, installId, identity };
|
|
13707
|
+
}
|
|
13708
|
+
|
|
13709
|
+
// src/version.ts
|
|
13710
|
+
var VERSION = "0.21.3";
|
|
13711
|
+
|
|
13712
|
+
// src/telemetry/collector.ts
|
|
13713
|
+
function mapOutcome(outcome) {
|
|
13714
|
+
return outcome === "completed" ? "success" : "failure";
|
|
13715
|
+
}
|
|
13716
|
+
function collectEvents(projectRoot, consent) {
|
|
13717
|
+
const records = readAdoptionRecords(projectRoot);
|
|
13718
|
+
if (records.length === 0) return [];
|
|
13719
|
+
const { installId, identity } = consent;
|
|
13720
|
+
const distinctId = identity.alias ?? installId;
|
|
13721
|
+
return records.map(
|
|
13722
|
+
(record) => ({
|
|
13723
|
+
event: "skill_invocation",
|
|
13724
|
+
distinctId,
|
|
13725
|
+
timestamp: record.startedAt,
|
|
13726
|
+
properties: {
|
|
13727
|
+
installId,
|
|
13728
|
+
os: process.platform,
|
|
13729
|
+
nodeVersion: process.version,
|
|
13730
|
+
harnessVersion: VERSION,
|
|
13731
|
+
skillName: record.skill,
|
|
13732
|
+
duration: record.duration,
|
|
13733
|
+
outcome: mapOutcome(record.outcome),
|
|
13734
|
+
phasesReached: record.phasesReached,
|
|
13735
|
+
...identity.project ? { project: identity.project } : {},
|
|
13736
|
+
...identity.team ? { team: identity.team } : {}
|
|
13737
|
+
}
|
|
13738
|
+
})
|
|
13739
|
+
);
|
|
13740
|
+
}
|
|
13741
|
+
|
|
13742
|
+
// src/telemetry/transport.ts
|
|
13743
|
+
var POSTHOG_BATCH_URL = "https://app.posthog.com/batch";
|
|
13744
|
+
var MAX_ATTEMPTS = 3;
|
|
13745
|
+
var TIMEOUT_MS = 5e3;
|
|
13746
|
+
function sleep2(ms) {
|
|
13747
|
+
return new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
13748
|
+
}
|
|
13749
|
+
async function send(events, apiKey) {
|
|
13750
|
+
if (events.length === 0) return;
|
|
13751
|
+
const payload = { api_key: apiKey, batch: events };
|
|
13752
|
+
const body = JSON.stringify(payload);
|
|
13753
|
+
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
|
13754
|
+
try {
|
|
13755
|
+
const res = await fetch(POSTHOG_BATCH_URL, {
|
|
13756
|
+
method: "POST",
|
|
13757
|
+
headers: { "Content-Type": "application/json" },
|
|
13758
|
+
body,
|
|
13759
|
+
signal: AbortSignal.timeout(TIMEOUT_MS)
|
|
13760
|
+
});
|
|
13761
|
+
if (res.ok) return;
|
|
13762
|
+
if (res.status < 500) return;
|
|
13763
|
+
} catch {
|
|
13764
|
+
}
|
|
13765
|
+
if (attempt < MAX_ATTEMPTS - 1) {
|
|
13766
|
+
await sleep2(1e3 * (attempt + 1));
|
|
13767
|
+
}
|
|
13768
|
+
}
|
|
13769
|
+
}
|
|
13168
13770
|
export {
|
|
13169
13771
|
AGENT_DESCRIPTORS,
|
|
13170
13772
|
ARCHITECTURE_DESCRIPTOR,
|
|
13171
13773
|
AdjustedForecastSchema,
|
|
13172
13774
|
AgentActionEmitter,
|
|
13775
|
+
AnthropicCacheAdapter,
|
|
13173
13776
|
ArchBaselineManager,
|
|
13174
13777
|
ArchBaselineSchema,
|
|
13175
13778
|
ArchConfigSchema,
|
|
@@ -13189,12 +13792,12 @@ export {
|
|
|
13189
13792
|
CategorySnapshotSchema,
|
|
13190
13793
|
ChecklistBuilder,
|
|
13191
13794
|
CircularDepsCollector,
|
|
13795
|
+
CompactionPipeline,
|
|
13192
13796
|
ComplexityCollector,
|
|
13193
13797
|
ConfidenceTierSchema,
|
|
13194
13798
|
ConfirmationSchema,
|
|
13195
13799
|
ConsoleSink,
|
|
13196
13800
|
ConstraintRuleSchema,
|
|
13197
|
-
ContentPipeline,
|
|
13198
13801
|
ContributingFeatureSchema,
|
|
13199
13802
|
ContributionsSchema,
|
|
13200
13803
|
CouplingCollector,
|
|
@@ -13204,6 +13807,7 @@ export {
|
|
|
13204
13807
|
DEFAULT_STABILITY_THRESHOLDS,
|
|
13205
13808
|
DEFAULT_STATE,
|
|
13206
13809
|
DEFAULT_STREAM_INDEX,
|
|
13810
|
+
DEFAULT_TOKEN_BUDGET,
|
|
13207
13811
|
DESTRUCTIVE_BASH,
|
|
13208
13812
|
DepDepthCollector,
|
|
13209
13813
|
DirectionSchema,
|
|
@@ -13217,6 +13821,7 @@ export {
|
|
|
13217
13821
|
ForbiddenImportCollector,
|
|
13218
13822
|
GateConfigSchema,
|
|
13219
13823
|
GateResultSchema,
|
|
13824
|
+
GeminiCacheAdapter,
|
|
13220
13825
|
GitHubIssuesSyncAdapter,
|
|
13221
13826
|
HandoffSchema,
|
|
13222
13827
|
HarnessStateSchema,
|
|
@@ -13231,6 +13836,7 @@ export {
|
|
|
13231
13836
|
NoOpExecutor,
|
|
13232
13837
|
NoOpSink,
|
|
13233
13838
|
NoOpTelemetryAdapter,
|
|
13839
|
+
OpenAICacheAdapter,
|
|
13234
13840
|
PatternConfigSchema,
|
|
13235
13841
|
PredictionEngine,
|
|
13236
13842
|
PredictionOptionsSchema,
|
|
@@ -13258,6 +13864,7 @@ export {
|
|
|
13258
13864
|
StabilityForecastSchema,
|
|
13259
13865
|
StreamIndexSchema,
|
|
13260
13866
|
StreamInfoSchema,
|
|
13867
|
+
StructuralStrategy,
|
|
13261
13868
|
ThresholdConfigSchema,
|
|
13262
13869
|
TimelineFileSchema,
|
|
13263
13870
|
TimelineManager,
|
|
@@ -13265,13 +13872,16 @@ export {
|
|
|
13265
13872
|
TransitionSchema,
|
|
13266
13873
|
TrendLineSchema,
|
|
13267
13874
|
TrendResultSchema,
|
|
13875
|
+
TruncationStrategy,
|
|
13268
13876
|
TypeScriptParser,
|
|
13269
13877
|
VERSION,
|
|
13270
13878
|
ViolationSchema,
|
|
13271
13879
|
addProvenance,
|
|
13272
13880
|
agentConfigRules,
|
|
13881
|
+
aggregateByDay2 as aggregateAdoptionByDay,
|
|
13273
13882
|
aggregateByDay,
|
|
13274
13883
|
aggregateBySession,
|
|
13884
|
+
aggregateBySkill,
|
|
13275
13885
|
analyzeDiff,
|
|
13276
13886
|
analyzeLearningPatterns,
|
|
13277
13887
|
appendFailure,
|
|
@@ -13303,6 +13913,7 @@ export {
|
|
|
13303
13913
|
clearFailuresCache,
|
|
13304
13914
|
clearLearningsCache,
|
|
13305
13915
|
clearTaint,
|
|
13916
|
+
collectEvents,
|
|
13306
13917
|
computeContentHash,
|
|
13307
13918
|
computeOverallSeverity,
|
|
13308
13919
|
computeScanExitCode,
|
|
@@ -13342,6 +13953,7 @@ export {
|
|
|
13342
13953
|
determineAssessment,
|
|
13343
13954
|
diff,
|
|
13344
13955
|
emitEvent,
|
|
13956
|
+
estimateTokens,
|
|
13345
13957
|
executeWorkflow,
|
|
13346
13958
|
expressRules,
|
|
13347
13959
|
extractBundle,
|
|
@@ -13363,6 +13975,7 @@ export {
|
|
|
13363
13975
|
getFeedbackConfig,
|
|
13364
13976
|
getInjectionPatterns,
|
|
13365
13977
|
getModelPrice,
|
|
13978
|
+
getOrCreateInstallId,
|
|
13366
13979
|
getOutline,
|
|
13367
13980
|
getParser,
|
|
13368
13981
|
getPhaseCategories,
|
|
@@ -13415,8 +14028,10 @@ export {
|
|
|
13415
14028
|
promoteSessionLearnings,
|
|
13416
14029
|
pruneLearnings,
|
|
13417
14030
|
reactRules,
|
|
14031
|
+
readAdoptionRecords,
|
|
13418
14032
|
readCheckState,
|
|
13419
14033
|
readCostRecords,
|
|
14034
|
+
readIdentity,
|
|
13420
14035
|
readLockfile,
|
|
13421
14036
|
readSessionSection,
|
|
13422
14037
|
readSessionSections,
|
|
@@ -13427,11 +14042,13 @@ export {
|
|
|
13427
14042
|
requestPeerReview,
|
|
13428
14043
|
resetFeedbackConfig,
|
|
13429
14044
|
resetParserCache,
|
|
14045
|
+
resolveConsent,
|
|
13430
14046
|
resolveFileToLayer,
|
|
13431
14047
|
resolveModelTier,
|
|
13432
14048
|
resolveReverseStatus,
|
|
13433
14049
|
resolveRuleSeverity,
|
|
13434
14050
|
resolveSessionDir,
|
|
14051
|
+
resolveStability,
|
|
13435
14052
|
resolveStreamPath,
|
|
13436
14053
|
resolveThresholds,
|
|
13437
14054
|
runAll,
|
|
@@ -13453,6 +14070,8 @@ export {
|
|
|
13453
14070
|
scoreRoadmapCandidates,
|
|
13454
14071
|
searchSymbols,
|
|
13455
14072
|
secretRules,
|
|
14073
|
+
send,
|
|
14074
|
+
serializeEnvelope,
|
|
13456
14075
|
serializeRoadmap,
|
|
13457
14076
|
setActiveStream,
|
|
13458
14077
|
sharpEdgesRules,
|
|
@@ -13463,6 +14082,7 @@ export {
|
|
|
13463
14082
|
syncRoadmap,
|
|
13464
14083
|
syncToExternal,
|
|
13465
14084
|
tagUncitedFindings,
|
|
14085
|
+
topSkills,
|
|
13466
14086
|
touchStream,
|
|
13467
14087
|
trackAction,
|
|
13468
14088
|
unfoldRange,
|