@harness-engineering/core 0.13.1 → 0.14.0
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 +240 -188
- package/dist/architecture/matchers.mjs +1 -1
- package/dist/{chunk-D6VFA6AS.mjs → chunk-BQUWXBGR.mjs} +240 -188
- package/dist/index.d.mts +93 -25
- package/dist/index.d.ts +93 -25
- package/dist/index.js +1609 -1182
- package/dist/index.mjs +1344 -976
- 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-BQUWXBGR.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 path22 = firstError.path.join(".");
|
|
88
|
+
const pathDisplay = path22 ? ` at "${path22}"` : "";
|
|
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 "${path22}" is required and must be of type "${expected}"`);
|
|
96
96
|
} else {
|
|
97
97
|
code = "INVALID_TYPE";
|
|
98
98
|
message = `Invalid type${pathDisplay}: ${firstError.message}`;
|
|
@@ -246,6 +246,43 @@ function extractMarkdownLinks(content) {
|
|
|
246
246
|
}
|
|
247
247
|
return links;
|
|
248
248
|
}
|
|
249
|
+
function isDescriptionTerminator(trimmed) {
|
|
250
|
+
return trimmed.startsWith("#") || trimmed.startsWith("-") || trimmed.startsWith("*") || trimmed.startsWith("```");
|
|
251
|
+
}
|
|
252
|
+
function extractDescription(sectionLines) {
|
|
253
|
+
const descriptionLines = [];
|
|
254
|
+
for (const line of sectionLines) {
|
|
255
|
+
const trimmed = line.trim();
|
|
256
|
+
if (trimmed === "") {
|
|
257
|
+
if (descriptionLines.length > 0) break;
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (isDescriptionTerminator(trimmed)) break;
|
|
261
|
+
descriptionLines.push(trimmed);
|
|
262
|
+
}
|
|
263
|
+
return descriptionLines.length > 0 ? descriptionLines.join(" ") : void 0;
|
|
264
|
+
}
|
|
265
|
+
function buildAgentMapSection(section, lines) {
|
|
266
|
+
const endIndex = section.endIndex ?? lines.length;
|
|
267
|
+
const sectionLines = lines.slice(section.startIndex + 1, endIndex);
|
|
268
|
+
const sectionContent = sectionLines.join("\n");
|
|
269
|
+
const links = extractMarkdownLinks(sectionContent).map((link) => ({
|
|
270
|
+
...link,
|
|
271
|
+
line: link.line + section.startIndex + 1,
|
|
272
|
+
exists: false
|
|
273
|
+
}));
|
|
274
|
+
const result = {
|
|
275
|
+
title: section.title,
|
|
276
|
+
level: section.level,
|
|
277
|
+
line: section.line,
|
|
278
|
+
links
|
|
279
|
+
};
|
|
280
|
+
const description = extractDescription(sectionLines);
|
|
281
|
+
if (description) {
|
|
282
|
+
result.description = description;
|
|
283
|
+
}
|
|
284
|
+
return result;
|
|
285
|
+
}
|
|
249
286
|
function extractSections(content) {
|
|
250
287
|
const lines = content.split("\n");
|
|
251
288
|
const sections = [];
|
|
@@ -258,7 +295,6 @@ function extractSections(content) {
|
|
|
258
295
|
title: match[2].trim(),
|
|
259
296
|
level: match[1].length,
|
|
260
297
|
line: i + 1,
|
|
261
|
-
// 1-indexed
|
|
262
298
|
startIndex: i
|
|
263
299
|
});
|
|
264
300
|
}
|
|
@@ -270,62 +306,29 @@ function extractSections(content) {
|
|
|
270
306
|
currentSection.endIndex = nextSection ? nextSection.startIndex : lines.length;
|
|
271
307
|
}
|
|
272
308
|
}
|
|
273
|
-
return sections.map((section) =>
|
|
274
|
-
const endIndex = section.endIndex ?? lines.length;
|
|
275
|
-
const sectionLines = lines.slice(section.startIndex + 1, endIndex);
|
|
276
|
-
const sectionContent = sectionLines.join("\n");
|
|
277
|
-
const links = extractMarkdownLinks(sectionContent).map((link) => ({
|
|
278
|
-
...link,
|
|
279
|
-
line: link.line + section.startIndex + 1,
|
|
280
|
-
// Adjust line number
|
|
281
|
-
exists: false
|
|
282
|
-
// Will be set later by validateAgentsMap
|
|
283
|
-
}));
|
|
284
|
-
const descriptionLines = [];
|
|
285
|
-
for (const line of sectionLines) {
|
|
286
|
-
const trimmed = line.trim();
|
|
287
|
-
if (trimmed === "") {
|
|
288
|
-
if (descriptionLines.length > 0) break;
|
|
289
|
-
continue;
|
|
290
|
-
}
|
|
291
|
-
if (trimmed.startsWith("#")) break;
|
|
292
|
-
if (trimmed.startsWith("-") || trimmed.startsWith("*")) break;
|
|
293
|
-
if (trimmed.startsWith("```")) break;
|
|
294
|
-
descriptionLines.push(trimmed);
|
|
295
|
-
}
|
|
296
|
-
const result = {
|
|
297
|
-
title: section.title,
|
|
298
|
-
level: section.level,
|
|
299
|
-
line: section.line,
|
|
300
|
-
links
|
|
301
|
-
};
|
|
302
|
-
if (descriptionLines.length > 0) {
|
|
303
|
-
result.description = descriptionLines.join(" ");
|
|
304
|
-
}
|
|
305
|
-
return result;
|
|
306
|
-
});
|
|
309
|
+
return sections.map((section) => buildAgentMapSection(section, lines));
|
|
307
310
|
}
|
|
308
|
-
function isExternalLink(
|
|
309
|
-
return
|
|
311
|
+
function isExternalLink(path22) {
|
|
312
|
+
return path22.startsWith("http://") || path22.startsWith("https://") || path22.startsWith("#") || path22.startsWith("mailto:");
|
|
310
313
|
}
|
|
311
314
|
function resolveLinkPath(linkPath, baseDir) {
|
|
312
315
|
return linkPath.startsWith(".") ? join(baseDir, linkPath) : linkPath;
|
|
313
316
|
}
|
|
314
|
-
async function validateAgentsMap(
|
|
315
|
-
const contentResult = await readFileContent(
|
|
317
|
+
async function validateAgentsMap(path22 = "./AGENTS.md") {
|
|
318
|
+
const contentResult = await readFileContent(path22);
|
|
316
319
|
if (!contentResult.ok) {
|
|
317
320
|
return Err(
|
|
318
321
|
createError(
|
|
319
322
|
"PARSE_ERROR",
|
|
320
323
|
`Failed to read AGENTS.md: ${contentResult.error.message}`,
|
|
321
|
-
{ path:
|
|
324
|
+
{ path: path22 },
|
|
322
325
|
["Ensure the file exists", "Check file permissions"]
|
|
323
326
|
)
|
|
324
327
|
);
|
|
325
328
|
}
|
|
326
329
|
const content = contentResult.value;
|
|
327
330
|
const sections = extractSections(content);
|
|
328
|
-
const baseDir = dirname(
|
|
331
|
+
const baseDir = dirname(path22);
|
|
329
332
|
const sectionTitles = sections.map((s) => s.title);
|
|
330
333
|
const missingSections = REQUIRED_SECTIONS.filter(
|
|
331
334
|
(required) => !sectionTitles.some((title) => title.toLowerCase().includes(required.toLowerCase()))
|
|
@@ -466,8 +469,8 @@ async function checkDocCoverage(domain, options = {}) {
|
|
|
466
469
|
|
|
467
470
|
// src/context/knowledge-map.ts
|
|
468
471
|
import { join as join2, basename as basename2 } from "path";
|
|
469
|
-
function suggestFix(
|
|
470
|
-
const targetName = basename2(
|
|
472
|
+
function suggestFix(path22, existingFiles) {
|
|
473
|
+
const targetName = basename2(path22).toLowerCase();
|
|
471
474
|
const similar = existingFiles.find((file) => {
|
|
472
475
|
const fileName = basename2(file).toLowerCase();
|
|
473
476
|
return fileName.includes(targetName) || targetName.includes(fileName);
|
|
@@ -475,7 +478,7 @@ function suggestFix(path20, existingFiles) {
|
|
|
475
478
|
if (similar) {
|
|
476
479
|
return `Did you mean "${similar}"?`;
|
|
477
480
|
}
|
|
478
|
-
return `Create the file "${
|
|
481
|
+
return `Create the file "${path22}" or remove the link`;
|
|
479
482
|
}
|
|
480
483
|
async function validateKnowledgeMap(rootDir = process.cwd()) {
|
|
481
484
|
const agentsPath = join2(rootDir, "AGENTS.md");
|
|
@@ -827,8 +830,8 @@ function createBoundaryValidator(schema, name) {
|
|
|
827
830
|
return Ok(result.data);
|
|
828
831
|
}
|
|
829
832
|
const suggestions = result.error.issues.map((issue) => {
|
|
830
|
-
const
|
|
831
|
-
return
|
|
833
|
+
const path22 = issue.path.join(".");
|
|
834
|
+
return path22 ? `${path22}: ${issue.message}` : issue.message;
|
|
832
835
|
});
|
|
833
836
|
return Err(
|
|
834
837
|
createError(
|
|
@@ -1050,175 +1053,183 @@ function stringArraysEqual(a, b) {
|
|
|
1050
1053
|
const sortedB = [...b].sort();
|
|
1051
1054
|
return sortedA.every((val, i) => val === sortedB[i]);
|
|
1052
1055
|
}
|
|
1053
|
-
function
|
|
1054
|
-
const
|
|
1055
|
-
const
|
|
1056
|
-
const
|
|
1057
|
-
|
|
1058
|
-
const
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1056
|
+
function mergeLayers(localConfig, bundleLayers, config, contributions, conflicts) {
|
|
1057
|
+
const localLayers = Array.isArray(localConfig.layers) ? localConfig.layers : [];
|
|
1058
|
+
const mergedLayers = [...localLayers];
|
|
1059
|
+
const contributedLayerNames = [];
|
|
1060
|
+
for (const bundleLayer of bundleLayers) {
|
|
1061
|
+
const existing = localLayers.find((l) => l.name === bundleLayer.name);
|
|
1062
|
+
if (!existing) {
|
|
1063
|
+
mergedLayers.push(bundleLayer);
|
|
1064
|
+
contributedLayerNames.push(bundleLayer.name);
|
|
1065
|
+
} else {
|
|
1066
|
+
const same = existing.pattern === bundleLayer.pattern && stringArraysEqual(existing.allowedDependencies, bundleLayer.allowedDependencies);
|
|
1067
|
+
if (!same) {
|
|
1068
|
+
conflicts.push({
|
|
1069
|
+
section: "layers",
|
|
1070
|
+
key: bundleLayer.name,
|
|
1071
|
+
localValue: existing,
|
|
1072
|
+
packageValue: bundleLayer,
|
|
1073
|
+
description: `Layer '${bundleLayer.name}' already exists locally with different configuration`
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
config.layers = mergedLayers;
|
|
1079
|
+
if (contributedLayerNames.length > 0) contributions.layers = contributedLayerNames;
|
|
1080
|
+
}
|
|
1081
|
+
function mergeForbiddenImports(localConfig, bundleRules, config, contributions, conflicts) {
|
|
1082
|
+
const localFI = Array.isArray(localConfig.forbiddenImports) ? localConfig.forbiddenImports : [];
|
|
1083
|
+
const mergedFI = [...localFI];
|
|
1084
|
+
const contributedFromKeys = [];
|
|
1085
|
+
for (const bundleRule of bundleRules) {
|
|
1086
|
+
const existing = localFI.find((r) => r.from === bundleRule.from);
|
|
1087
|
+
if (!existing) {
|
|
1088
|
+
const entry = { from: bundleRule.from, disallow: bundleRule.disallow };
|
|
1089
|
+
if (bundleRule.message !== void 0) entry.message = bundleRule.message;
|
|
1090
|
+
mergedFI.push(entry);
|
|
1091
|
+
contributedFromKeys.push(bundleRule.from);
|
|
1092
|
+
} else {
|
|
1093
|
+
if (!stringArraysEqual(existing.disallow, bundleRule.disallow)) {
|
|
1094
|
+
conflicts.push({
|
|
1095
|
+
section: "forbiddenImports",
|
|
1096
|
+
key: bundleRule.from,
|
|
1097
|
+
localValue: existing,
|
|
1098
|
+
packageValue: bundleRule,
|
|
1099
|
+
description: `Forbidden import rule for '${bundleRule.from}' already exists locally with different disallow list`
|
|
1100
|
+
});
|
|
1077
1101
|
}
|
|
1078
1102
|
}
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1103
|
+
}
|
|
1104
|
+
config.forbiddenImports = mergedFI;
|
|
1105
|
+
if (contributedFromKeys.length > 0) contributions.forbiddenImports = contributedFromKeys;
|
|
1106
|
+
}
|
|
1107
|
+
function mergeBoundaries(localConfig, bundleBoundaries, config, contributions) {
|
|
1108
|
+
const localBoundaries = localConfig.boundaries ?? { requireSchema: [] };
|
|
1109
|
+
const localSchemas = new Set(localBoundaries.requireSchema ?? []);
|
|
1110
|
+
const newSchemas = [];
|
|
1111
|
+
for (const schema of bundleBoundaries.requireSchema ?? []) {
|
|
1112
|
+
if (!localSchemas.has(schema)) {
|
|
1113
|
+
newSchemas.push(schema);
|
|
1114
|
+
localSchemas.add(schema);
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
config.boundaries = { requireSchema: [...localBoundaries.requireSchema ?? [], ...newSchemas] };
|
|
1118
|
+
if (newSchemas.length > 0) contributions.boundaries = newSchemas;
|
|
1119
|
+
}
|
|
1120
|
+
function mergeArchitecture(localConfig, bundleArch, config, contributions, conflicts) {
|
|
1121
|
+
const localArch = localConfig.architecture ?? { thresholds: {}, modules: {} };
|
|
1122
|
+
const mergedThresholds = { ...localArch.thresholds };
|
|
1123
|
+
const contributedThresholdKeys = [];
|
|
1124
|
+
for (const [category, value] of Object.entries(bundleArch.thresholds ?? {})) {
|
|
1125
|
+
if (!(category in mergedThresholds)) {
|
|
1126
|
+
mergedThresholds[category] = value;
|
|
1127
|
+
contributedThresholdKeys.push(category);
|
|
1128
|
+
} else if (!deepEqual(mergedThresholds[category], value)) {
|
|
1129
|
+
conflicts.push({
|
|
1130
|
+
section: "architecture.thresholds",
|
|
1131
|
+
key: category,
|
|
1132
|
+
localValue: mergedThresholds[category],
|
|
1133
|
+
packageValue: value,
|
|
1134
|
+
description: `Architecture threshold '${category}' already exists locally with a different value`
|
|
1135
|
+
});
|
|
1082
1136
|
}
|
|
1083
1137
|
}
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
const
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
}
|
|
1098
|
-
mergedFI.push(entry);
|
|
1099
|
-
contributedFromKeys.push(bundleRule.from);
|
|
1100
|
-
} else {
|
|
1101
|
-
const same = stringArraysEqual(existing.disallow, bundleRule.disallow);
|
|
1102
|
-
if (!same) {
|
|
1138
|
+
const mergedModules = { ...localArch.modules };
|
|
1139
|
+
const contributedModuleKeys = [];
|
|
1140
|
+
for (const [modulePath, bundleCategoryMap] of Object.entries(bundleArch.modules ?? {})) {
|
|
1141
|
+
if (!(modulePath in mergedModules)) {
|
|
1142
|
+
mergedModules[modulePath] = bundleCategoryMap;
|
|
1143
|
+
for (const cat of Object.keys(bundleCategoryMap))
|
|
1144
|
+
contributedModuleKeys.push(`${modulePath}:${cat}`);
|
|
1145
|
+
} else {
|
|
1146
|
+
const mergedCategoryMap = { ...mergedModules[modulePath] };
|
|
1147
|
+
for (const [category, value] of Object.entries(bundleCategoryMap)) {
|
|
1148
|
+
if (!(category in mergedCategoryMap)) {
|
|
1149
|
+
mergedCategoryMap[category] = value;
|
|
1150
|
+
contributedModuleKeys.push(`${modulePath}:${category}`);
|
|
1151
|
+
} else if (!deepEqual(mergedCategoryMap[category], value)) {
|
|
1103
1152
|
conflicts.push({
|
|
1104
|
-
section: "
|
|
1105
|
-
key:
|
|
1106
|
-
localValue:
|
|
1107
|
-
packageValue:
|
|
1108
|
-
description: `
|
|
1153
|
+
section: "architecture.modules",
|
|
1154
|
+
key: `${modulePath}:${category}`,
|
|
1155
|
+
localValue: mergedCategoryMap[category],
|
|
1156
|
+
packageValue: value,
|
|
1157
|
+
description: `Architecture module override '${modulePath}' category '${category}' already exists locally with a different value`
|
|
1109
1158
|
});
|
|
1110
1159
|
}
|
|
1111
1160
|
}
|
|
1161
|
+
mergedModules[modulePath] = mergedCategoryMap;
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
config.architecture = { ...localArch, thresholds: mergedThresholds, modules: mergedModules };
|
|
1165
|
+
if (contributedThresholdKeys.length > 0)
|
|
1166
|
+
contributions["architecture.thresholds"] = contributedThresholdKeys;
|
|
1167
|
+
if (contributedModuleKeys.length > 0)
|
|
1168
|
+
contributions["architecture.modules"] = contributedModuleKeys;
|
|
1169
|
+
}
|
|
1170
|
+
function mergeSecurityRules(localConfig, bundleRules, config, contributions, conflicts) {
|
|
1171
|
+
const localSecurity = localConfig.security ?? { rules: {} };
|
|
1172
|
+
const localRules = localSecurity.rules ?? {};
|
|
1173
|
+
const mergedRules = { ...localRules };
|
|
1174
|
+
const contributedRuleIds = [];
|
|
1175
|
+
for (const [ruleId, severity] of Object.entries(bundleRules)) {
|
|
1176
|
+
if (!(ruleId in mergedRules)) {
|
|
1177
|
+
mergedRules[ruleId] = severity;
|
|
1178
|
+
contributedRuleIds.push(ruleId);
|
|
1179
|
+
} else if (mergedRules[ruleId] !== severity) {
|
|
1180
|
+
conflicts.push({
|
|
1181
|
+
section: "security.rules",
|
|
1182
|
+
key: ruleId,
|
|
1183
|
+
localValue: mergedRules[ruleId],
|
|
1184
|
+
packageValue: severity,
|
|
1185
|
+
description: `Security rule '${ruleId}' already exists locally with severity '${mergedRules[ruleId]}', bundle has '${severity}'`
|
|
1186
|
+
});
|
|
1112
1187
|
}
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1188
|
+
}
|
|
1189
|
+
config.security = { ...localSecurity, rules: mergedRules };
|
|
1190
|
+
if (contributedRuleIds.length > 0) contributions["security.rules"] = contributedRuleIds;
|
|
1191
|
+
}
|
|
1192
|
+
function deepMergeConstraints(localConfig, bundleConstraints, _existingContributions) {
|
|
1193
|
+
const config = { ...localConfig };
|
|
1194
|
+
const contributions = {};
|
|
1195
|
+
const conflicts = [];
|
|
1196
|
+
if (bundleConstraints.layers && bundleConstraints.layers.length > 0) {
|
|
1197
|
+
mergeLayers(localConfig, bundleConstraints.layers, config, contributions, conflicts);
|
|
1198
|
+
}
|
|
1199
|
+
if (bundleConstraints.forbiddenImports && bundleConstraints.forbiddenImports.length > 0) {
|
|
1200
|
+
mergeForbiddenImports(
|
|
1201
|
+
localConfig,
|
|
1202
|
+
bundleConstraints.forbiddenImports,
|
|
1203
|
+
config,
|
|
1204
|
+
contributions,
|
|
1205
|
+
conflicts
|
|
1206
|
+
);
|
|
1117
1207
|
}
|
|
1118
1208
|
if (bundleConstraints.boundaries) {
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
newSchemas.push(schema);
|
|
1126
|
-
localSchemas.add(schema);
|
|
1127
|
-
}
|
|
1128
|
-
}
|
|
1129
|
-
config.boundaries = {
|
|
1130
|
-
requireSchema: [...localBoundaries.requireSchema ?? [], ...newSchemas]
|
|
1131
|
-
};
|
|
1132
|
-
if (newSchemas.length > 0) {
|
|
1133
|
-
contributions.boundaries = newSchemas;
|
|
1134
|
-
}
|
|
1209
|
+
mergeBoundaries(
|
|
1210
|
+
localConfig,
|
|
1211
|
+
bundleConstraints.boundaries,
|
|
1212
|
+
config,
|
|
1213
|
+
contributions
|
|
1214
|
+
);
|
|
1135
1215
|
}
|
|
1136
1216
|
if (bundleConstraints.architecture) {
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
for (const [category, value] of Object.entries(bundleThresholds)) {
|
|
1145
|
-
if (!(category in mergedThresholds)) {
|
|
1146
|
-
mergedThresholds[category] = value;
|
|
1147
|
-
contributedThresholdKeys.push(category);
|
|
1148
|
-
} else if (!deepEqual(mergedThresholds[category], value)) {
|
|
1149
|
-
conflicts.push({
|
|
1150
|
-
section: "architecture.thresholds",
|
|
1151
|
-
key: category,
|
|
1152
|
-
localValue: mergedThresholds[category],
|
|
1153
|
-
packageValue: value,
|
|
1154
|
-
description: `Architecture threshold '${category}' already exists locally with a different value`
|
|
1155
|
-
});
|
|
1156
|
-
}
|
|
1157
|
-
}
|
|
1158
|
-
const mergedModules = { ...localArch.modules };
|
|
1159
|
-
const contributedModuleKeys = [];
|
|
1160
|
-
const bundleModules = bundleConstraints.architecture.modules ?? {};
|
|
1161
|
-
for (const [modulePath, bundleCategoryMap] of Object.entries(bundleModules)) {
|
|
1162
|
-
if (!(modulePath in mergedModules)) {
|
|
1163
|
-
mergedModules[modulePath] = bundleCategoryMap;
|
|
1164
|
-
for (const cat of Object.keys(bundleCategoryMap)) {
|
|
1165
|
-
contributedModuleKeys.push(`${modulePath}:${cat}`);
|
|
1166
|
-
}
|
|
1167
|
-
} else {
|
|
1168
|
-
const localCategoryMap = mergedModules[modulePath];
|
|
1169
|
-
const mergedCategoryMap = { ...localCategoryMap };
|
|
1170
|
-
for (const [category, value] of Object.entries(bundleCategoryMap)) {
|
|
1171
|
-
if (!(category in mergedCategoryMap)) {
|
|
1172
|
-
mergedCategoryMap[category] = value;
|
|
1173
|
-
contributedModuleKeys.push(`${modulePath}:${category}`);
|
|
1174
|
-
} else if (!deepEqual(mergedCategoryMap[category], value)) {
|
|
1175
|
-
conflicts.push({
|
|
1176
|
-
section: "architecture.modules",
|
|
1177
|
-
key: `${modulePath}:${category}`,
|
|
1178
|
-
localValue: mergedCategoryMap[category],
|
|
1179
|
-
packageValue: value,
|
|
1180
|
-
description: `Architecture module override '${modulePath}' category '${category}' already exists locally with a different value`
|
|
1181
|
-
});
|
|
1182
|
-
}
|
|
1183
|
-
}
|
|
1184
|
-
mergedModules[modulePath] = mergedCategoryMap;
|
|
1185
|
-
}
|
|
1186
|
-
}
|
|
1187
|
-
config.architecture = {
|
|
1188
|
-
...localArch,
|
|
1189
|
-
thresholds: mergedThresholds,
|
|
1190
|
-
modules: mergedModules
|
|
1191
|
-
};
|
|
1192
|
-
if (contributedThresholdKeys.length > 0) {
|
|
1193
|
-
contributions["architecture.thresholds"] = contributedThresholdKeys;
|
|
1194
|
-
}
|
|
1195
|
-
if (contributedModuleKeys.length > 0) {
|
|
1196
|
-
contributions["architecture.modules"] = contributedModuleKeys;
|
|
1197
|
-
}
|
|
1217
|
+
mergeArchitecture(
|
|
1218
|
+
localConfig,
|
|
1219
|
+
bundleConstraints.architecture,
|
|
1220
|
+
config,
|
|
1221
|
+
contributions,
|
|
1222
|
+
conflicts
|
|
1223
|
+
);
|
|
1198
1224
|
}
|
|
1199
1225
|
if (bundleConstraints.security?.rules) {
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
contributedRuleIds.push(ruleId);
|
|
1208
|
-
} else if (mergedRules[ruleId] !== severity) {
|
|
1209
|
-
conflicts.push({
|
|
1210
|
-
section: "security.rules",
|
|
1211
|
-
key: ruleId,
|
|
1212
|
-
localValue: mergedRules[ruleId],
|
|
1213
|
-
packageValue: severity,
|
|
1214
|
-
description: `Security rule '${ruleId}' already exists locally with severity '${mergedRules[ruleId]}', bundle has '${severity}'`
|
|
1215
|
-
});
|
|
1216
|
-
}
|
|
1217
|
-
}
|
|
1218
|
-
config.security = { ...localSecurity, rules: mergedRules };
|
|
1219
|
-
if (contributedRuleIds.length > 0) {
|
|
1220
|
-
contributions["security.rules"] = contributedRuleIds;
|
|
1221
|
-
}
|
|
1226
|
+
mergeSecurityRules(
|
|
1227
|
+
localConfig,
|
|
1228
|
+
bundleConstraints.security.rules,
|
|
1229
|
+
config,
|
|
1230
|
+
contributions,
|
|
1231
|
+
conflicts
|
|
1232
|
+
);
|
|
1222
1233
|
}
|
|
1223
1234
|
return { config, contributions, conflicts };
|
|
1224
1235
|
}
|
|
@@ -1379,14 +1390,84 @@ function walk(node, visitor) {
|
|
|
1379
1390
|
}
|
|
1380
1391
|
}
|
|
1381
1392
|
}
|
|
1393
|
+
function makeLocation(node) {
|
|
1394
|
+
return {
|
|
1395
|
+
file: "",
|
|
1396
|
+
line: node.loc?.start.line ?? 0,
|
|
1397
|
+
column: node.loc?.start.column ?? 0
|
|
1398
|
+
};
|
|
1399
|
+
}
|
|
1400
|
+
function processImportSpecifiers(importDecl, imp) {
|
|
1401
|
+
for (const spec of importDecl.specifiers) {
|
|
1402
|
+
if (spec.type === "ImportDefaultSpecifier") {
|
|
1403
|
+
imp.default = spec.local.name;
|
|
1404
|
+
} else if (spec.type === "ImportNamespaceSpecifier") {
|
|
1405
|
+
imp.namespace = spec.local.name;
|
|
1406
|
+
} else if (spec.type === "ImportSpecifier") {
|
|
1407
|
+
imp.specifiers.push(spec.local.name);
|
|
1408
|
+
if (spec.importKind === "type") {
|
|
1409
|
+
imp.kind = "type";
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
function getExportedName(exported) {
|
|
1415
|
+
return exported.type === "Identifier" ? exported.name : String(exported.value);
|
|
1416
|
+
}
|
|
1417
|
+
function processReExportSpecifiers(exportDecl, exports) {
|
|
1418
|
+
for (const spec of exportDecl.specifiers) {
|
|
1419
|
+
if (spec.type !== "ExportSpecifier") continue;
|
|
1420
|
+
exports.push({
|
|
1421
|
+
name: getExportedName(spec.exported),
|
|
1422
|
+
type: "named",
|
|
1423
|
+
location: makeLocation(exportDecl),
|
|
1424
|
+
isReExport: true,
|
|
1425
|
+
source: exportDecl.source.value
|
|
1426
|
+
});
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
function processExportDeclaration(exportDecl, exports) {
|
|
1430
|
+
const decl = exportDecl.declaration;
|
|
1431
|
+
if (!decl) return;
|
|
1432
|
+
if (decl.type === "VariableDeclaration") {
|
|
1433
|
+
for (const declarator of decl.declarations) {
|
|
1434
|
+
if (declarator.id.type === "Identifier") {
|
|
1435
|
+
exports.push({
|
|
1436
|
+
name: declarator.id.name,
|
|
1437
|
+
type: "named",
|
|
1438
|
+
location: makeLocation(decl),
|
|
1439
|
+
isReExport: false
|
|
1440
|
+
});
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
} else if ((decl.type === "FunctionDeclaration" || decl.type === "ClassDeclaration") && decl.id) {
|
|
1444
|
+
exports.push({
|
|
1445
|
+
name: decl.id.name,
|
|
1446
|
+
type: "named",
|
|
1447
|
+
location: makeLocation(decl),
|
|
1448
|
+
isReExport: false
|
|
1449
|
+
});
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
function processExportListSpecifiers(exportDecl, exports) {
|
|
1453
|
+
for (const spec of exportDecl.specifiers) {
|
|
1454
|
+
if (spec.type !== "ExportSpecifier") continue;
|
|
1455
|
+
exports.push({
|
|
1456
|
+
name: getExportedName(spec.exported),
|
|
1457
|
+
type: "named",
|
|
1458
|
+
location: makeLocation(exportDecl),
|
|
1459
|
+
isReExport: false
|
|
1460
|
+
});
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1382
1463
|
var TypeScriptParser = class {
|
|
1383
1464
|
name = "typescript";
|
|
1384
1465
|
extensions = [".ts", ".tsx", ".mts", ".cts"];
|
|
1385
|
-
async parseFile(
|
|
1386
|
-
const contentResult = await readFileContent(
|
|
1466
|
+
async parseFile(path22) {
|
|
1467
|
+
const contentResult = await readFileContent(path22);
|
|
1387
1468
|
if (!contentResult.ok) {
|
|
1388
1469
|
return Err(
|
|
1389
|
-
createParseError("NOT_FOUND", `File not found: ${
|
|
1470
|
+
createParseError("NOT_FOUND", `File not found: ${path22}`, { path: path22 }, [
|
|
1390
1471
|
"Check that the file exists",
|
|
1391
1472
|
"Verify the path is correct"
|
|
1392
1473
|
])
|
|
@@ -1396,7 +1477,7 @@ var TypeScriptParser = class {
|
|
|
1396
1477
|
const ast = parse(contentResult.value, {
|
|
1397
1478
|
loc: true,
|
|
1398
1479
|
range: true,
|
|
1399
|
-
jsx:
|
|
1480
|
+
jsx: path22.endsWith(".tsx"),
|
|
1400
1481
|
errorOnUnknownASTType: false
|
|
1401
1482
|
});
|
|
1402
1483
|
return Ok({
|
|
@@ -1407,7 +1488,7 @@ var TypeScriptParser = class {
|
|
|
1407
1488
|
} catch (e) {
|
|
1408
1489
|
const error = e;
|
|
1409
1490
|
return Err(
|
|
1410
|
-
createParseError("SYNTAX_ERROR", `Failed to parse ${
|
|
1491
|
+
createParseError("SYNTAX_ERROR", `Failed to parse ${path22}: ${error.message}`, { path: path22 }, [
|
|
1411
1492
|
"Check for syntax errors in the file",
|
|
1412
1493
|
"Ensure valid TypeScript syntax"
|
|
1413
1494
|
])
|
|
@@ -1423,26 +1504,12 @@ var TypeScriptParser = class {
|
|
|
1423
1504
|
const imp = {
|
|
1424
1505
|
source: importDecl.source.value,
|
|
1425
1506
|
specifiers: [],
|
|
1426
|
-
location:
|
|
1427
|
-
file: "",
|
|
1428
|
-
line: importDecl.loc?.start.line ?? 0,
|
|
1429
|
-
column: importDecl.loc?.start.column ?? 0
|
|
1430
|
-
},
|
|
1507
|
+
location: makeLocation(importDecl),
|
|
1431
1508
|
kind: importDecl.importKind === "type" ? "type" : "value"
|
|
1432
1509
|
};
|
|
1433
|
-
|
|
1434
|
-
if (spec.type === "ImportDefaultSpecifier") {
|
|
1435
|
-
imp.default = spec.local.name;
|
|
1436
|
-
} else if (spec.type === "ImportNamespaceSpecifier") {
|
|
1437
|
-
imp.namespace = spec.local.name;
|
|
1438
|
-
} else if (spec.type === "ImportSpecifier") {
|
|
1439
|
-
imp.specifiers.push(spec.local.name);
|
|
1440
|
-
if (spec.importKind === "type") {
|
|
1441
|
-
imp.kind = "type";
|
|
1442
|
-
}
|
|
1443
|
-
}
|
|
1444
|
-
}
|
|
1510
|
+
processImportSpecifiers(importDecl, imp);
|
|
1445
1511
|
imports.push(imp);
|
|
1512
|
+
return;
|
|
1446
1513
|
}
|
|
1447
1514
|
if (node.type === "ImportExpression") {
|
|
1448
1515
|
const importExpr = node;
|
|
@@ -1450,11 +1517,7 @@ var TypeScriptParser = class {
|
|
|
1450
1517
|
imports.push({
|
|
1451
1518
|
source: importExpr.source.value,
|
|
1452
1519
|
specifiers: [],
|
|
1453
|
-
location:
|
|
1454
|
-
file: "",
|
|
1455
|
-
line: importExpr.loc?.start.line ?? 0,
|
|
1456
|
-
column: importExpr.loc?.start.column ?? 0
|
|
1457
|
-
},
|
|
1520
|
+
location: makeLocation(importExpr),
|
|
1458
1521
|
kind: "value"
|
|
1459
1522
|
});
|
|
1460
1523
|
}
|
|
@@ -1469,97 +1532,29 @@ var TypeScriptParser = class {
|
|
|
1469
1532
|
if (node.type === "ExportNamedDeclaration") {
|
|
1470
1533
|
const exportDecl = node;
|
|
1471
1534
|
if (exportDecl.source) {
|
|
1472
|
-
|
|
1473
|
-
if (spec.type === "ExportSpecifier") {
|
|
1474
|
-
const exported = spec.exported;
|
|
1475
|
-
const name = exported.type === "Identifier" ? exported.name : String(exported.value);
|
|
1476
|
-
exports.push({
|
|
1477
|
-
name,
|
|
1478
|
-
type: "named",
|
|
1479
|
-
location: {
|
|
1480
|
-
file: "",
|
|
1481
|
-
line: exportDecl.loc?.start.line ?? 0,
|
|
1482
|
-
column: exportDecl.loc?.start.column ?? 0
|
|
1483
|
-
},
|
|
1484
|
-
isReExport: true,
|
|
1485
|
-
source: exportDecl.source.value
|
|
1486
|
-
});
|
|
1487
|
-
}
|
|
1488
|
-
}
|
|
1535
|
+
processReExportSpecifiers(exportDecl, exports);
|
|
1489
1536
|
return;
|
|
1490
1537
|
}
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
for (const declarator of decl.declarations) {
|
|
1495
|
-
if (declarator.id.type === "Identifier") {
|
|
1496
|
-
exports.push({
|
|
1497
|
-
name: declarator.id.name,
|
|
1498
|
-
type: "named",
|
|
1499
|
-
location: {
|
|
1500
|
-
file: "",
|
|
1501
|
-
line: decl.loc?.start.line ?? 0,
|
|
1502
|
-
column: decl.loc?.start.column ?? 0
|
|
1503
|
-
},
|
|
1504
|
-
isReExport: false
|
|
1505
|
-
});
|
|
1506
|
-
}
|
|
1507
|
-
}
|
|
1508
|
-
} else if (decl.type === "FunctionDeclaration" || decl.type === "ClassDeclaration") {
|
|
1509
|
-
if (decl.id) {
|
|
1510
|
-
exports.push({
|
|
1511
|
-
name: decl.id.name,
|
|
1512
|
-
type: "named",
|
|
1513
|
-
location: {
|
|
1514
|
-
file: "",
|
|
1515
|
-
line: decl.loc?.start.line ?? 0,
|
|
1516
|
-
column: decl.loc?.start.column ?? 0
|
|
1517
|
-
},
|
|
1518
|
-
isReExport: false
|
|
1519
|
-
});
|
|
1520
|
-
}
|
|
1521
|
-
}
|
|
1522
|
-
}
|
|
1523
|
-
for (const spec of exportDecl.specifiers) {
|
|
1524
|
-
if (spec.type === "ExportSpecifier") {
|
|
1525
|
-
const exported = spec.exported;
|
|
1526
|
-
const name = exported.type === "Identifier" ? exported.name : String(exported.value);
|
|
1527
|
-
exports.push({
|
|
1528
|
-
name,
|
|
1529
|
-
type: "named",
|
|
1530
|
-
location: {
|
|
1531
|
-
file: "",
|
|
1532
|
-
line: exportDecl.loc?.start.line ?? 0,
|
|
1533
|
-
column: exportDecl.loc?.start.column ?? 0
|
|
1534
|
-
},
|
|
1535
|
-
isReExport: false
|
|
1536
|
-
});
|
|
1537
|
-
}
|
|
1538
|
-
}
|
|
1538
|
+
processExportDeclaration(exportDecl, exports);
|
|
1539
|
+
processExportListSpecifiers(exportDecl, exports);
|
|
1540
|
+
return;
|
|
1539
1541
|
}
|
|
1540
1542
|
if (node.type === "ExportDefaultDeclaration") {
|
|
1541
1543
|
const exportDecl = node;
|
|
1542
1544
|
exports.push({
|
|
1543
1545
|
name: "default",
|
|
1544
1546
|
type: "default",
|
|
1545
|
-
location:
|
|
1546
|
-
file: "",
|
|
1547
|
-
line: exportDecl.loc?.start.line ?? 0,
|
|
1548
|
-
column: exportDecl.loc?.start.column ?? 0
|
|
1549
|
-
},
|
|
1547
|
+
location: makeLocation(exportDecl),
|
|
1550
1548
|
isReExport: false
|
|
1551
1549
|
});
|
|
1550
|
+
return;
|
|
1552
1551
|
}
|
|
1553
1552
|
if (node.type === "ExportAllDeclaration") {
|
|
1554
1553
|
const exportDecl = node;
|
|
1555
1554
|
exports.push({
|
|
1556
1555
|
name: exportDecl.exported?.name ?? "*",
|
|
1557
1556
|
type: "namespace",
|
|
1558
|
-
location:
|
|
1559
|
-
file: "",
|
|
1560
|
-
line: exportDecl.loc?.start.line ?? 0,
|
|
1561
|
-
column: exportDecl.loc?.start.column ?? 0
|
|
1562
|
-
},
|
|
1557
|
+
location: makeLocation(exportDecl),
|
|
1563
1558
|
isReExport: true,
|
|
1564
1559
|
source: exportDecl.source.value
|
|
1565
1560
|
});
|
|
@@ -1575,10 +1570,27 @@ var TypeScriptParser = class {
|
|
|
1575
1570
|
// src/entropy/snapshot.ts
|
|
1576
1571
|
import { join as join3, resolve } from "path";
|
|
1577
1572
|
import { minimatch as minimatch2 } from "minimatch";
|
|
1573
|
+
function collectFieldEntries(rootDir, field) {
|
|
1574
|
+
if (typeof field === "string") return [resolve(rootDir, field)];
|
|
1575
|
+
if (typeof field === "object" && field !== null) {
|
|
1576
|
+
return Object.values(field).filter((v) => typeof v === "string").map((v) => resolve(rootDir, v));
|
|
1577
|
+
}
|
|
1578
|
+
return [];
|
|
1579
|
+
}
|
|
1580
|
+
function extractPackageEntries(rootDir, pkg) {
|
|
1581
|
+
const entries = [];
|
|
1582
|
+
entries.push(...collectFieldEntries(rootDir, pkg["exports"]));
|
|
1583
|
+
if (entries.length === 0 && typeof pkg["main"] === "string") {
|
|
1584
|
+
entries.push(resolve(rootDir, pkg["main"]));
|
|
1585
|
+
}
|
|
1586
|
+
if (pkg["bin"]) {
|
|
1587
|
+
entries.push(...collectFieldEntries(rootDir, pkg["bin"]));
|
|
1588
|
+
}
|
|
1589
|
+
return entries;
|
|
1590
|
+
}
|
|
1578
1591
|
async function resolveEntryPoints(rootDir, explicitEntries) {
|
|
1579
1592
|
if (explicitEntries && explicitEntries.length > 0) {
|
|
1580
|
-
|
|
1581
|
-
return Ok(resolved);
|
|
1593
|
+
return Ok(explicitEntries.map((e) => resolve(rootDir, e)));
|
|
1582
1594
|
}
|
|
1583
1595
|
const pkgPath = join3(rootDir, "package.json");
|
|
1584
1596
|
if (await fileExists(pkgPath)) {
|
|
@@ -1586,38 +1598,8 @@ async function resolveEntryPoints(rootDir, explicitEntries) {
|
|
|
1586
1598
|
if (pkgContent.ok) {
|
|
1587
1599
|
try {
|
|
1588
1600
|
const pkg = JSON.parse(pkgContent.value);
|
|
1589
|
-
const entries =
|
|
1590
|
-
if (
|
|
1591
|
-
const exports = pkg["exports"];
|
|
1592
|
-
if (typeof exports === "string") {
|
|
1593
|
-
entries.push(resolve(rootDir, exports));
|
|
1594
|
-
} else if (typeof exports === "object" && exports !== null) {
|
|
1595
|
-
for (const value of Object.values(exports)) {
|
|
1596
|
-
if (typeof value === "string") {
|
|
1597
|
-
entries.push(resolve(rootDir, value));
|
|
1598
|
-
}
|
|
1599
|
-
}
|
|
1600
|
-
}
|
|
1601
|
-
}
|
|
1602
|
-
const main = pkg["main"];
|
|
1603
|
-
if (typeof main === "string" && entries.length === 0) {
|
|
1604
|
-
entries.push(resolve(rootDir, main));
|
|
1605
|
-
}
|
|
1606
|
-
const bin = pkg["bin"];
|
|
1607
|
-
if (bin) {
|
|
1608
|
-
if (typeof bin === "string") {
|
|
1609
|
-
entries.push(resolve(rootDir, bin));
|
|
1610
|
-
} else if (typeof bin === "object") {
|
|
1611
|
-
for (const value of Object.values(bin)) {
|
|
1612
|
-
if (typeof value === "string") {
|
|
1613
|
-
entries.push(resolve(rootDir, value));
|
|
1614
|
-
}
|
|
1615
|
-
}
|
|
1616
|
-
}
|
|
1617
|
-
}
|
|
1618
|
-
if (entries.length > 0) {
|
|
1619
|
-
return Ok(entries);
|
|
1620
|
-
}
|
|
1601
|
+
const entries = extractPackageEntries(rootDir, pkg);
|
|
1602
|
+
if (entries.length > 0) return Ok(entries);
|
|
1621
1603
|
} catch {
|
|
1622
1604
|
}
|
|
1623
1605
|
}
|
|
@@ -1691,66 +1673,49 @@ function extractInlineRefs(content) {
|
|
|
1691
1673
|
}
|
|
1692
1674
|
return refs;
|
|
1693
1675
|
}
|
|
1694
|
-
async function parseDocumentationFile(
|
|
1695
|
-
const contentResult = await readFileContent(
|
|
1676
|
+
async function parseDocumentationFile(path22) {
|
|
1677
|
+
const contentResult = await readFileContent(path22);
|
|
1696
1678
|
if (!contentResult.ok) {
|
|
1697
1679
|
return Err(
|
|
1698
1680
|
createEntropyError(
|
|
1699
1681
|
"PARSE_ERROR",
|
|
1700
|
-
`Failed to read documentation file: ${
|
|
1701
|
-
{ file:
|
|
1682
|
+
`Failed to read documentation file: ${path22}`,
|
|
1683
|
+
{ file: path22 },
|
|
1702
1684
|
["Check that the file exists"]
|
|
1703
1685
|
)
|
|
1704
1686
|
);
|
|
1705
1687
|
}
|
|
1706
1688
|
const content = contentResult.value;
|
|
1707
|
-
const type =
|
|
1689
|
+
const type = path22.endsWith(".md") ? "markdown" : "text";
|
|
1708
1690
|
return Ok({
|
|
1709
|
-
path:
|
|
1691
|
+
path: path22,
|
|
1710
1692
|
type,
|
|
1711
1693
|
content,
|
|
1712
1694
|
codeBlocks: extractCodeBlocks(content),
|
|
1713
1695
|
inlineRefs: extractInlineRefs(content)
|
|
1714
1696
|
});
|
|
1715
1697
|
}
|
|
1698
|
+
function makeInternalSymbol(name, type, line) {
|
|
1699
|
+
return { name, type, line, references: 0, calledBy: [] };
|
|
1700
|
+
}
|
|
1701
|
+
function extractSymbolsFromNode(node) {
|
|
1702
|
+
const line = node.loc?.start?.line || 0;
|
|
1703
|
+
if (node.type === "FunctionDeclaration" && node.id?.name) {
|
|
1704
|
+
return [makeInternalSymbol(node.id.name, "function", line)];
|
|
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
|
+
}
|
|
1712
|
+
return [];
|
|
1713
|
+
}
|
|
1716
1714
|
function extractInternalSymbols(ast) {
|
|
1717
|
-
const symbols = [];
|
|
1718
1715
|
const body = ast.body;
|
|
1719
|
-
if (!body?.body) return
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
symbols.push({
|
|
1723
|
-
name: node.id.name,
|
|
1724
|
-
type: "function",
|
|
1725
|
-
line: node.loc?.start?.line || 0,
|
|
1726
|
-
references: 0,
|
|
1727
|
-
calledBy: []
|
|
1728
|
-
});
|
|
1729
|
-
}
|
|
1730
|
-
if (node.type === "VariableDeclaration") {
|
|
1731
|
-
for (const decl of node.declarations || []) {
|
|
1732
|
-
if (decl.id?.name) {
|
|
1733
|
-
symbols.push({
|
|
1734
|
-
name: decl.id.name,
|
|
1735
|
-
type: "variable",
|
|
1736
|
-
line: node.loc?.start?.line || 0,
|
|
1737
|
-
references: 0,
|
|
1738
|
-
calledBy: []
|
|
1739
|
-
});
|
|
1740
|
-
}
|
|
1741
|
-
}
|
|
1742
|
-
}
|
|
1743
|
-
if (node.type === "ClassDeclaration" && node.id?.name) {
|
|
1744
|
-
symbols.push({
|
|
1745
|
-
name: node.id.name,
|
|
1746
|
-
type: "class",
|
|
1747
|
-
line: node.loc?.start?.line || 0,
|
|
1748
|
-
references: 0,
|
|
1749
|
-
calledBy: []
|
|
1750
|
-
});
|
|
1751
|
-
}
|
|
1752
|
-
}
|
|
1753
|
-
return symbols;
|
|
1716
|
+
if (!body?.body) return [];
|
|
1717
|
+
const nodes = body.body;
|
|
1718
|
+
return nodes.flatMap(extractSymbolsFromNode);
|
|
1754
1719
|
}
|
|
1755
1720
|
function extractJSDocComments(ast) {
|
|
1756
1721
|
const comments = [];
|
|
@@ -1891,27 +1856,34 @@ async function buildSnapshot(config) {
|
|
|
1891
1856
|
|
|
1892
1857
|
// src/entropy/detectors/drift.ts
|
|
1893
1858
|
import { dirname as dirname3, resolve as resolve2 } from "path";
|
|
1894
|
-
function
|
|
1859
|
+
function initLevenshteinMatrix(aLen, bLen) {
|
|
1895
1860
|
const matrix = [];
|
|
1896
|
-
for (let i = 0; i <=
|
|
1861
|
+
for (let i = 0; i <= bLen; i++) {
|
|
1897
1862
|
matrix[i] = [i];
|
|
1898
1863
|
}
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1864
|
+
const firstRow = matrix[0];
|
|
1865
|
+
if (firstRow) {
|
|
1866
|
+
for (let j = 0; j <= aLen; j++) {
|
|
1867
|
+
firstRow[j] = j;
|
|
1903
1868
|
}
|
|
1904
1869
|
}
|
|
1870
|
+
return matrix;
|
|
1871
|
+
}
|
|
1872
|
+
function computeLevenshteinCell(row, prevRow, j, charsMatch) {
|
|
1873
|
+
if (charsMatch) {
|
|
1874
|
+
row[j] = prevRow[j - 1] ?? 0;
|
|
1875
|
+
} else {
|
|
1876
|
+
row[j] = Math.min((prevRow[j - 1] ?? 0) + 1, (row[j - 1] ?? 0) + 1, (prevRow[j] ?? 0) + 1);
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
function levenshteinDistance(a, b) {
|
|
1880
|
+
const matrix = initLevenshteinMatrix(a.length, b.length);
|
|
1905
1881
|
for (let i = 1; i <= b.length; i++) {
|
|
1906
1882
|
for (let j = 1; j <= a.length; j++) {
|
|
1907
1883
|
const row = matrix[i];
|
|
1908
1884
|
const prevRow = matrix[i - 1];
|
|
1909
1885
|
if (!row || !prevRow) continue;
|
|
1910
|
-
|
|
1911
|
-
row[j] = prevRow[j - 1] ?? 0;
|
|
1912
|
-
} else {
|
|
1913
|
-
row[j] = Math.min((prevRow[j - 1] ?? 0) + 1, (row[j - 1] ?? 0) + 1, (prevRow[j] ?? 0) + 1);
|
|
1914
|
-
}
|
|
1886
|
+
computeLevenshteinCell(row, prevRow, j, b.charAt(i - 1) === a.charAt(j - 1));
|
|
1915
1887
|
}
|
|
1916
1888
|
}
|
|
1917
1889
|
const lastRow = matrix[b.length];
|
|
@@ -2197,32 +2169,27 @@ function findDeadExports(snapshot, usageMap, reachability) {
|
|
|
2197
2169
|
}
|
|
2198
2170
|
return deadExports;
|
|
2199
2171
|
}
|
|
2200
|
-
function
|
|
2201
|
-
if (
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
for (const key of Object.keys(node)) {
|
|
2210
|
-
const value = node[key];
|
|
2211
|
-
if (Array.isArray(value)) {
|
|
2212
|
-
for (const item of value) {
|
|
2213
|
-
traverse(item);
|
|
2214
|
-
}
|
|
2215
|
-
} else if (value && typeof value === "object") {
|
|
2216
|
-
traverse(value);
|
|
2217
|
-
}
|
|
2218
|
-
}
|
|
2172
|
+
function findMaxLineInNode(node) {
|
|
2173
|
+
if (!node || typeof node !== "object") return 0;
|
|
2174
|
+
const n = node;
|
|
2175
|
+
let maxLine = n.loc?.end?.line ?? 0;
|
|
2176
|
+
for (const key of Object.keys(node)) {
|
|
2177
|
+
const value = node[key];
|
|
2178
|
+
if (Array.isArray(value)) {
|
|
2179
|
+
for (const item of value) {
|
|
2180
|
+
maxLine = Math.max(maxLine, findMaxLineInNode(item));
|
|
2219
2181
|
}
|
|
2220
|
-
}
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
return Math.max(ast.body.length * 3, 1);
|
|
2182
|
+
} else if (value && typeof value === "object") {
|
|
2183
|
+
maxLine = Math.max(maxLine, findMaxLineInNode(value));
|
|
2184
|
+
}
|
|
2224
2185
|
}
|
|
2225
|
-
return
|
|
2186
|
+
return maxLine;
|
|
2187
|
+
}
|
|
2188
|
+
function countLinesFromAST(ast) {
|
|
2189
|
+
if (!ast.body || !Array.isArray(ast.body)) return 1;
|
|
2190
|
+
const maxLine = findMaxLineInNode(ast);
|
|
2191
|
+
if (maxLine > 0) return maxLine;
|
|
2192
|
+
return Math.max(ast.body.length * 3, 1);
|
|
2226
2193
|
}
|
|
2227
2194
|
function findDeadFiles(snapshot, reachability) {
|
|
2228
2195
|
const deadFiles = [];
|
|
@@ -2373,130 +2340,146 @@ function fileMatchesPattern(filePath, pattern, rootDir) {
|
|
|
2373
2340
|
const relativePath = relativePosix(rootDir, filePath);
|
|
2374
2341
|
return minimatch3(relativePath, pattern);
|
|
2375
2342
|
}
|
|
2376
|
-
|
|
2343
|
+
var CONVENTION_DESCRIPTIONS = {
|
|
2344
|
+
camelCase: "camelCase (e.g., myFunction)",
|
|
2345
|
+
PascalCase: "PascalCase (e.g., MyClass)",
|
|
2346
|
+
UPPER_SNAKE: "UPPER_SNAKE_CASE (e.g., MY_CONSTANT)",
|
|
2347
|
+
"kebab-case": "kebab-case (e.g., my-component)"
|
|
2348
|
+
};
|
|
2349
|
+
function checkMustExport(rule, file, message) {
|
|
2350
|
+
if (rule.type !== "must-export") return [];
|
|
2377
2351
|
const matches = [];
|
|
2378
|
-
const
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
for (const name of rule.names) {
|
|
2386
|
-
const hasExport = file.exports.some((e) => e.name === name);
|
|
2387
|
-
if (!hasExport) {
|
|
2388
|
-
matches.push({
|
|
2389
|
-
line: 1,
|
|
2390
|
-
message: pattern.message || `Missing required export: "${name}"`,
|
|
2391
|
-
suggestion: `Add export for "${name}"`
|
|
2392
|
-
});
|
|
2393
|
-
}
|
|
2394
|
-
}
|
|
2395
|
-
break;
|
|
2396
|
-
}
|
|
2397
|
-
case "must-export-default": {
|
|
2398
|
-
const hasDefault = file.exports.some((e) => e.type === "default");
|
|
2399
|
-
if (!hasDefault) {
|
|
2400
|
-
matches.push({
|
|
2401
|
-
line: 1,
|
|
2402
|
-
message: pattern.message || "File must have a default export",
|
|
2403
|
-
suggestion: "Add a default export"
|
|
2404
|
-
});
|
|
2405
|
-
}
|
|
2406
|
-
break;
|
|
2407
|
-
}
|
|
2408
|
-
case "no-export": {
|
|
2409
|
-
for (const name of rule.names) {
|
|
2410
|
-
const exp = file.exports.find((e) => e.name === name);
|
|
2411
|
-
if (exp) {
|
|
2412
|
-
matches.push({
|
|
2413
|
-
line: exp.location.line,
|
|
2414
|
-
message: pattern.message || `Forbidden export: "${name}"`,
|
|
2415
|
-
suggestion: `Remove export "${name}"`
|
|
2416
|
-
});
|
|
2417
|
-
}
|
|
2418
|
-
}
|
|
2419
|
-
break;
|
|
2352
|
+
for (const name of rule.names) {
|
|
2353
|
+
if (!file.exports.some((e) => e.name === name)) {
|
|
2354
|
+
matches.push({
|
|
2355
|
+
line: 1,
|
|
2356
|
+
message: message || `Missing required export: "${name}"`,
|
|
2357
|
+
suggestion: `Add export for "${name}"`
|
|
2358
|
+
});
|
|
2420
2359
|
}
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2360
|
+
}
|
|
2361
|
+
return matches;
|
|
2362
|
+
}
|
|
2363
|
+
function checkMustExportDefault(_rule, file, message) {
|
|
2364
|
+
if (!file.exports.some((e) => e.type === "default")) {
|
|
2365
|
+
return [
|
|
2366
|
+
{
|
|
2367
|
+
line: 1,
|
|
2368
|
+
message: message || "File must have a default export",
|
|
2369
|
+
suggestion: "Add a default export"
|
|
2431
2370
|
}
|
|
2432
|
-
|
|
2371
|
+
];
|
|
2372
|
+
}
|
|
2373
|
+
return [];
|
|
2374
|
+
}
|
|
2375
|
+
function checkNoExport(rule, file, message) {
|
|
2376
|
+
if (rule.type !== "no-export") return [];
|
|
2377
|
+
const matches = [];
|
|
2378
|
+
for (const name of rule.names) {
|
|
2379
|
+
const exp = file.exports.find((e) => e.name === name);
|
|
2380
|
+
if (exp) {
|
|
2381
|
+
matches.push({
|
|
2382
|
+
line: exp.location.line,
|
|
2383
|
+
message: message || `Forbidden export: "${name}"`,
|
|
2384
|
+
suggestion: `Remove export "${name}"`
|
|
2385
|
+
});
|
|
2433
2386
|
}
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2387
|
+
}
|
|
2388
|
+
return matches;
|
|
2389
|
+
}
|
|
2390
|
+
function checkMustImport(rule, file, message) {
|
|
2391
|
+
if (rule.type !== "must-import") return [];
|
|
2392
|
+
const hasImport = file.imports.some(
|
|
2393
|
+
(i) => i.source === rule.from || i.source.endsWith(rule.from)
|
|
2394
|
+
);
|
|
2395
|
+
if (!hasImport) {
|
|
2396
|
+
return [
|
|
2397
|
+
{
|
|
2398
|
+
line: 1,
|
|
2399
|
+
message: message || `Missing required import from "${rule.from}"`,
|
|
2400
|
+
suggestion: `Add import from "${rule.from}"`
|
|
2444
2401
|
}
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
expected = "UPPER_SNAKE_CASE (e.g., MY_CONSTANT)";
|
|
2461
|
-
break;
|
|
2462
|
-
case "kebab-case":
|
|
2463
|
-
expected = "kebab-case (e.g., my-component)";
|
|
2464
|
-
break;
|
|
2465
|
-
}
|
|
2466
|
-
matches.push({
|
|
2467
|
-
line: exp.location.line,
|
|
2468
|
-
message: pattern.message || `"${exp.name}" does not follow ${rule.convention} convention`,
|
|
2469
|
-
suggestion: `Rename to follow ${expected}`
|
|
2470
|
-
});
|
|
2471
|
-
}
|
|
2402
|
+
];
|
|
2403
|
+
}
|
|
2404
|
+
return [];
|
|
2405
|
+
}
|
|
2406
|
+
function checkNoImport(rule, file, message) {
|
|
2407
|
+
if (rule.type !== "no-import") return [];
|
|
2408
|
+
const forbiddenImport = file.imports.find(
|
|
2409
|
+
(i) => i.source === rule.from || i.source.endsWith(rule.from)
|
|
2410
|
+
);
|
|
2411
|
+
if (forbiddenImport) {
|
|
2412
|
+
return [
|
|
2413
|
+
{
|
|
2414
|
+
line: forbiddenImport.location.line,
|
|
2415
|
+
message: message || `Forbidden import from "${rule.from}"`,
|
|
2416
|
+
suggestion: `Remove import from "${rule.from}"`
|
|
2472
2417
|
}
|
|
2473
|
-
|
|
2418
|
+
];
|
|
2419
|
+
}
|
|
2420
|
+
return [];
|
|
2421
|
+
}
|
|
2422
|
+
function checkNaming(rule, file, message) {
|
|
2423
|
+
if (rule.type !== "naming") return [];
|
|
2424
|
+
const regex = new RegExp(rule.match);
|
|
2425
|
+
const matches = [];
|
|
2426
|
+
for (const exp of file.exports) {
|
|
2427
|
+
if (!regex.test(exp.name)) {
|
|
2428
|
+
const expected = CONVENTION_DESCRIPTIONS[rule.convention] ?? rule.convention;
|
|
2429
|
+
matches.push({
|
|
2430
|
+
line: exp.location.line,
|
|
2431
|
+
message: message || `"${exp.name}" does not follow ${rule.convention} convention`,
|
|
2432
|
+
suggestion: `Rename to follow ${expected}`
|
|
2433
|
+
});
|
|
2474
2434
|
}
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2435
|
+
}
|
|
2436
|
+
return matches;
|
|
2437
|
+
}
|
|
2438
|
+
function checkMaxExports(rule, file, message) {
|
|
2439
|
+
if (rule.type !== "max-exports") return [];
|
|
2440
|
+
if (file.exports.length > rule.count) {
|
|
2441
|
+
return [
|
|
2442
|
+
{
|
|
2443
|
+
line: 1,
|
|
2444
|
+
message: message || `File has ${file.exports.length} exports, max is ${rule.count}`,
|
|
2445
|
+
suggestion: `Split into multiple files or reduce exports to ${rule.count}`
|
|
2482
2446
|
}
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2447
|
+
];
|
|
2448
|
+
}
|
|
2449
|
+
return [];
|
|
2450
|
+
}
|
|
2451
|
+
function checkMaxLines(_rule, _file, _message) {
|
|
2452
|
+
return [];
|
|
2453
|
+
}
|
|
2454
|
+
function checkRequireJsdoc(_rule, file, message) {
|
|
2455
|
+
if (file.jsDocComments.length === 0 && file.exports.length > 0) {
|
|
2456
|
+
return [
|
|
2457
|
+
{
|
|
2458
|
+
line: 1,
|
|
2459
|
+
message: message || "Exported symbols require JSDoc documentation",
|
|
2460
|
+
suggestion: "Add JSDoc comments to exports"
|
|
2495
2461
|
}
|
|
2496
|
-
|
|
2497
|
-
}
|
|
2462
|
+
];
|
|
2498
2463
|
}
|
|
2499
|
-
return
|
|
2464
|
+
return [];
|
|
2465
|
+
}
|
|
2466
|
+
var RULE_CHECKERS = {
|
|
2467
|
+
"must-export": checkMustExport,
|
|
2468
|
+
"must-export-default": checkMustExportDefault,
|
|
2469
|
+
"no-export": checkNoExport,
|
|
2470
|
+
"must-import": checkMustImport,
|
|
2471
|
+
"no-import": checkNoImport,
|
|
2472
|
+
naming: checkNaming,
|
|
2473
|
+
"max-exports": checkMaxExports,
|
|
2474
|
+
"max-lines": checkMaxLines,
|
|
2475
|
+
"require-jsdoc": checkRequireJsdoc
|
|
2476
|
+
};
|
|
2477
|
+
function checkConfigPattern(pattern, file, rootDir) {
|
|
2478
|
+
const fileMatches = pattern.files.some((glob) => fileMatchesPattern(file.path, glob, rootDir));
|
|
2479
|
+
if (!fileMatches) return [];
|
|
2480
|
+
const checker = RULE_CHECKERS[pattern.rule.type];
|
|
2481
|
+
if (!checker) return [];
|
|
2482
|
+
return checker(pattern.rule, file, pattern.message);
|
|
2500
2483
|
}
|
|
2501
2484
|
async function detectPatternViolations(snapshot, config) {
|
|
2502
2485
|
const violations = [];
|
|
@@ -3015,17 +2998,35 @@ function createUnusedImportFixes(deadCodeReport) {
|
|
|
3015
2998
|
reversible: true
|
|
3016
2999
|
}));
|
|
3017
3000
|
}
|
|
3001
|
+
var EXPORT_TYPE_KEYWORD = {
|
|
3002
|
+
class: "class",
|
|
3003
|
+
function: "function",
|
|
3004
|
+
variable: "const",
|
|
3005
|
+
type: "type",
|
|
3006
|
+
interface: "interface",
|
|
3007
|
+
enum: "enum"
|
|
3008
|
+
};
|
|
3009
|
+
function getExportKeyword(exportType) {
|
|
3010
|
+
return EXPORT_TYPE_KEYWORD[exportType] ?? "enum";
|
|
3011
|
+
}
|
|
3012
|
+
function getDefaultExportKeyword(exportType) {
|
|
3013
|
+
if (exportType === "class" || exportType === "function") return exportType;
|
|
3014
|
+
return "";
|
|
3015
|
+
}
|
|
3018
3016
|
function createDeadExportFixes(deadCodeReport) {
|
|
3019
|
-
return deadCodeReport.deadExports.filter((exp) => exp.reason === "NO_IMPORTERS").map((exp) =>
|
|
3020
|
-
type:
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3017
|
+
return deadCodeReport.deadExports.filter((exp) => exp.reason === "NO_IMPORTERS").map((exp) => {
|
|
3018
|
+
const keyword = exp.isDefault ? getDefaultExportKeyword(exp.type) : getExportKeyword(exp.type);
|
|
3019
|
+
return {
|
|
3020
|
+
type: "dead-exports",
|
|
3021
|
+
file: exp.file,
|
|
3022
|
+
description: `Remove export keyword from ${exp.name} (${exp.reason})`,
|
|
3023
|
+
action: "replace",
|
|
3024
|
+
oldContent: exp.isDefault ? `export default ${keyword} ${exp.name}` : `export ${keyword} ${exp.name}`,
|
|
3025
|
+
newContent: `${keyword} ${exp.name}`,
|
|
3026
|
+
safe: true,
|
|
3027
|
+
reversible: true
|
|
3028
|
+
};
|
|
3029
|
+
});
|
|
3029
3030
|
}
|
|
3030
3031
|
function createCommentedCodeFixes(blocks) {
|
|
3031
3032
|
return blocks.map((block) => ({
|
|
@@ -3204,53 +3205,80 @@ var ALWAYS_UNSAFE_TYPES = /* @__PURE__ */ new Set([
|
|
|
3204
3205
|
"dead-internal"
|
|
3205
3206
|
]);
|
|
3206
3207
|
var idCounter = 0;
|
|
3208
|
+
var DEAD_CODE_FIX_ACTIONS = {
|
|
3209
|
+
"dead-export": "Remove export keyword",
|
|
3210
|
+
"dead-file": "Delete file",
|
|
3211
|
+
"commented-code": "Delete commented block",
|
|
3212
|
+
"unused-import": "Remove import"
|
|
3213
|
+
};
|
|
3214
|
+
function classifyDeadCode(input) {
|
|
3215
|
+
if (input.isPublicApi) {
|
|
3216
|
+
return {
|
|
3217
|
+
safety: "unsafe",
|
|
3218
|
+
safetyReason: "Public API export may have external consumers",
|
|
3219
|
+
suggestion: "Deprecate before removing"
|
|
3220
|
+
};
|
|
3221
|
+
}
|
|
3222
|
+
const fixAction = DEAD_CODE_FIX_ACTIONS[input.type];
|
|
3223
|
+
if (fixAction) {
|
|
3224
|
+
return {
|
|
3225
|
+
safety: "safe",
|
|
3226
|
+
safetyReason: "zero importers, non-public",
|
|
3227
|
+
fixAction,
|
|
3228
|
+
suggestion: fixAction
|
|
3229
|
+
};
|
|
3230
|
+
}
|
|
3231
|
+
if (input.type === "orphaned-dep") {
|
|
3232
|
+
return {
|
|
3233
|
+
safety: "probably-safe",
|
|
3234
|
+
safetyReason: "No imports found, but needs install+test verification",
|
|
3235
|
+
fixAction: "Remove from package.json",
|
|
3236
|
+
suggestion: "Remove from package.json"
|
|
3237
|
+
};
|
|
3238
|
+
}
|
|
3239
|
+
return {
|
|
3240
|
+
safety: "unsafe",
|
|
3241
|
+
safetyReason: "Unknown dead code type",
|
|
3242
|
+
suggestion: "Manual review required"
|
|
3243
|
+
};
|
|
3244
|
+
}
|
|
3245
|
+
function classifyArchitecture(input) {
|
|
3246
|
+
if (input.type === "import-ordering") {
|
|
3247
|
+
return {
|
|
3248
|
+
safety: "safe",
|
|
3249
|
+
safetyReason: "Mechanical reorder, no semantic change",
|
|
3250
|
+
fixAction: "Reorder imports",
|
|
3251
|
+
suggestion: "Reorder imports"
|
|
3252
|
+
};
|
|
3253
|
+
}
|
|
3254
|
+
if (input.type === "forbidden-import" && input.hasAlternative) {
|
|
3255
|
+
return {
|
|
3256
|
+
safety: "probably-safe",
|
|
3257
|
+
safetyReason: "Alternative configured, needs typecheck+test",
|
|
3258
|
+
fixAction: "Replace with configured alternative",
|
|
3259
|
+
suggestion: "Replace with configured alternative"
|
|
3260
|
+
};
|
|
3261
|
+
}
|
|
3262
|
+
return {
|
|
3263
|
+
safety: "unsafe",
|
|
3264
|
+
safetyReason: `${input.type} requires structural changes`,
|
|
3265
|
+
suggestion: "Restructure code to fix violation"
|
|
3266
|
+
};
|
|
3267
|
+
}
|
|
3207
3268
|
function classifyFinding(input) {
|
|
3208
3269
|
idCounter++;
|
|
3209
3270
|
const id = `${input.concern === "dead-code" ? "dc" : "arch"}-${idCounter}`;
|
|
3210
|
-
let
|
|
3211
|
-
let safetyReason;
|
|
3212
|
-
let fixAction;
|
|
3213
|
-
let suggestion;
|
|
3271
|
+
let classification;
|
|
3214
3272
|
if (ALWAYS_UNSAFE_TYPES.has(input.type)) {
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3273
|
+
classification = {
|
|
3274
|
+
safety: "unsafe",
|
|
3275
|
+
safetyReason: `${input.type} requires human judgment`,
|
|
3276
|
+
suggestion: "Review and refactor manually"
|
|
3277
|
+
};
|
|
3218
3278
|
} else if (input.concern === "dead-code") {
|
|
3219
|
-
|
|
3220
|
-
safety = "unsafe";
|
|
3221
|
-
safetyReason = "Public API export may have external consumers";
|
|
3222
|
-
suggestion = "Deprecate before removing";
|
|
3223
|
-
} else if (input.type === "dead-export" || input.type === "unused-import" || input.type === "commented-code" || input.type === "dead-file") {
|
|
3224
|
-
safety = "safe";
|
|
3225
|
-
safetyReason = "zero importers, non-public";
|
|
3226
|
-
fixAction = input.type === "dead-export" ? "Remove export keyword" : input.type === "dead-file" ? "Delete file" : input.type === "commented-code" ? "Delete commented block" : "Remove import";
|
|
3227
|
-
suggestion = fixAction;
|
|
3228
|
-
} else if (input.type === "orphaned-dep") {
|
|
3229
|
-
safety = "probably-safe";
|
|
3230
|
-
safetyReason = "No imports found, but needs install+test verification";
|
|
3231
|
-
fixAction = "Remove from package.json";
|
|
3232
|
-
suggestion = fixAction;
|
|
3233
|
-
} else {
|
|
3234
|
-
safety = "unsafe";
|
|
3235
|
-
safetyReason = "Unknown dead code type";
|
|
3236
|
-
suggestion = "Manual review required";
|
|
3237
|
-
}
|
|
3279
|
+
classification = classifyDeadCode(input);
|
|
3238
3280
|
} else {
|
|
3239
|
-
|
|
3240
|
-
safety = "safe";
|
|
3241
|
-
safetyReason = "Mechanical reorder, no semantic change";
|
|
3242
|
-
fixAction = "Reorder imports";
|
|
3243
|
-
suggestion = fixAction;
|
|
3244
|
-
} else if (input.type === "forbidden-import" && input.hasAlternative) {
|
|
3245
|
-
safety = "probably-safe";
|
|
3246
|
-
safetyReason = "Alternative configured, needs typecheck+test";
|
|
3247
|
-
fixAction = "Replace with configured alternative";
|
|
3248
|
-
suggestion = fixAction;
|
|
3249
|
-
} else {
|
|
3250
|
-
safety = "unsafe";
|
|
3251
|
-
safetyReason = `${input.type} requires structural changes`;
|
|
3252
|
-
suggestion = "Restructure code to fix violation";
|
|
3253
|
-
}
|
|
3281
|
+
classification = classifyArchitecture(input);
|
|
3254
3282
|
}
|
|
3255
3283
|
return {
|
|
3256
3284
|
id,
|
|
@@ -3259,11 +3287,11 @@ function classifyFinding(input) {
|
|
|
3259
3287
|
...input.line !== void 0 ? { line: input.line } : {},
|
|
3260
3288
|
type: input.type,
|
|
3261
3289
|
description: input.description,
|
|
3262
|
-
safety,
|
|
3263
|
-
safetyReason,
|
|
3290
|
+
safety: classification.safety,
|
|
3291
|
+
safetyReason: classification.safetyReason,
|
|
3264
3292
|
hotspotDowngraded: false,
|
|
3265
|
-
...fixAction !== void 0 ? { fixAction } : {},
|
|
3266
|
-
suggestion
|
|
3293
|
+
...classification.fixAction !== void 0 ? { fixAction: classification.fixAction } : {},
|
|
3294
|
+
suggestion: classification.suggestion
|
|
3267
3295
|
};
|
|
3268
3296
|
}
|
|
3269
3297
|
function applyHotspotDowngrade(finding, hotspot) {
|
|
@@ -3557,43 +3585,57 @@ var BenchmarkRunner = class {
|
|
|
3557
3585
|
};
|
|
3558
3586
|
}
|
|
3559
3587
|
}
|
|
3588
|
+
/**
|
|
3589
|
+
* Extract a BenchmarkResult from a single assertion with benchmark data.
|
|
3590
|
+
*/
|
|
3591
|
+
parseBenchAssertion(assertion, file) {
|
|
3592
|
+
if (!assertion.benchmark) return null;
|
|
3593
|
+
const bench = assertion.benchmark;
|
|
3594
|
+
return {
|
|
3595
|
+
name: assertion.fullName || assertion.title || "unknown",
|
|
3596
|
+
file: file.replace(process.cwd() + "/", ""),
|
|
3597
|
+
opsPerSec: Math.round(bench.hz || 0),
|
|
3598
|
+
meanMs: bench.mean ? bench.mean * 1e3 : 0,
|
|
3599
|
+
p99Ms: bench.p99 ? bench.p99 * 1e3 : bench.mean ? bench.mean * 1e3 * 1.5 : 0,
|
|
3600
|
+
marginOfError: bench.rme ? bench.rme / 100 : 0.05
|
|
3601
|
+
};
|
|
3602
|
+
}
|
|
3603
|
+
/**
|
|
3604
|
+
* Extract JSON from output that may contain non-JSON preamble.
|
|
3605
|
+
*/
|
|
3606
|
+
extractJson(output) {
|
|
3607
|
+
const jsonStart = output.indexOf("{");
|
|
3608
|
+
const jsonEnd = output.lastIndexOf("}");
|
|
3609
|
+
if (jsonStart === -1 || jsonEnd === -1) return null;
|
|
3610
|
+
return JSON.parse(output.slice(jsonStart, jsonEnd + 1));
|
|
3611
|
+
}
|
|
3560
3612
|
/**
|
|
3561
3613
|
* Parse vitest bench JSON reporter output into BenchmarkResult[].
|
|
3562
3614
|
* Vitest bench JSON output contains testResults with benchmark data.
|
|
3563
3615
|
*/
|
|
3564
|
-
|
|
3616
|
+
collectAssertionResults(testResults) {
|
|
3565
3617
|
const results = [];
|
|
3566
|
-
|
|
3567
|
-
const
|
|
3568
|
-
const
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
if (parsed.testResults) {
|
|
3573
|
-
for (const testResult of parsed.testResults) {
|
|
3574
|
-
const file = testResult.name || testResult.filepath || "";
|
|
3575
|
-
if (testResult.assertionResults) {
|
|
3576
|
-
for (const assertion of testResult.assertionResults) {
|
|
3577
|
-
if (assertion.benchmark) {
|
|
3578
|
-
const bench = assertion.benchmark;
|
|
3579
|
-
results.push({
|
|
3580
|
-
name: assertion.fullName || assertion.title || "unknown",
|
|
3581
|
-
file: file.replace(process.cwd() + "/", ""),
|
|
3582
|
-
opsPerSec: Math.round(bench.hz || 0),
|
|
3583
|
-
meanMs: bench.mean ? bench.mean * 1e3 : 0,
|
|
3584
|
-
// p99: use actual p99 if available, otherwise estimate as 1.5× mean
|
|
3585
|
-
p99Ms: bench.p99 ? bench.p99 * 1e3 : bench.mean ? bench.mean * 1e3 * 1.5 : 0,
|
|
3586
|
-
marginOfError: bench.rme ? bench.rme / 100 : 0.05
|
|
3587
|
-
});
|
|
3588
|
-
}
|
|
3589
|
-
}
|
|
3590
|
-
}
|
|
3591
|
-
}
|
|
3618
|
+
for (const testResult of testResults) {
|
|
3619
|
+
const file = testResult.name || testResult.filepath || "";
|
|
3620
|
+
const assertions = testResult.assertionResults ?? [];
|
|
3621
|
+
for (const assertion of assertions) {
|
|
3622
|
+
const result = this.parseBenchAssertion(assertion, file);
|
|
3623
|
+
if (result) results.push(result);
|
|
3592
3624
|
}
|
|
3593
|
-
} catch {
|
|
3594
3625
|
}
|
|
3595
3626
|
return results;
|
|
3596
3627
|
}
|
|
3628
|
+
parseVitestBenchOutput(output) {
|
|
3629
|
+
try {
|
|
3630
|
+
const parsed = this.extractJson(output);
|
|
3631
|
+
if (!parsed) return [];
|
|
3632
|
+
const testResults = parsed.testResults;
|
|
3633
|
+
if (!testResults) return [];
|
|
3634
|
+
return this.collectAssertionResults(testResults);
|
|
3635
|
+
} catch {
|
|
3636
|
+
return [];
|
|
3637
|
+
}
|
|
3638
|
+
}
|
|
3597
3639
|
};
|
|
3598
3640
|
|
|
3599
3641
|
// src/performance/regression-detector.ts
|
|
@@ -3903,39 +3945,31 @@ function resetFeedbackConfig() {
|
|
|
3903
3945
|
}
|
|
3904
3946
|
|
|
3905
3947
|
// src/feedback/review/diff-analyzer.ts
|
|
3948
|
+
function detectFileStatus(part) {
|
|
3949
|
+
if (/new file mode/.test(part)) return "added";
|
|
3950
|
+
if (/deleted file mode/.test(part)) return "deleted";
|
|
3951
|
+
if (part.includes("rename from")) return "renamed";
|
|
3952
|
+
return "modified";
|
|
3953
|
+
}
|
|
3954
|
+
function parseDiffPart(part) {
|
|
3955
|
+
if (!part.trim()) return null;
|
|
3956
|
+
const headerMatch = /diff --git a\/(.+?) b\/(.+?)(?:\n|$)/.exec(part);
|
|
3957
|
+
if (!headerMatch || !headerMatch[2]) return null;
|
|
3958
|
+
const additionRegex = /^\+(?!\+\+)/gm;
|
|
3959
|
+
const deletionRegex = /^-(?!--)/gm;
|
|
3960
|
+
return {
|
|
3961
|
+
path: headerMatch[2],
|
|
3962
|
+
status: detectFileStatus(part),
|
|
3963
|
+
additions: (part.match(additionRegex) || []).length,
|
|
3964
|
+
deletions: (part.match(deletionRegex) || []).length
|
|
3965
|
+
};
|
|
3966
|
+
}
|
|
3906
3967
|
function parseDiff(diff2) {
|
|
3907
3968
|
try {
|
|
3908
3969
|
if (!diff2.trim()) {
|
|
3909
3970
|
return Ok({ diff: diff2, files: [] });
|
|
3910
3971
|
}
|
|
3911
|
-
const files =
|
|
3912
|
-
const newFileRegex = /new file mode/;
|
|
3913
|
-
const deletedFileRegex = /deleted file mode/;
|
|
3914
|
-
const additionRegex = /^\+(?!\+\+)/gm;
|
|
3915
|
-
const deletionRegex = /^-(?!--)/gm;
|
|
3916
|
-
const diffParts = diff2.split(/(?=diff --git)/);
|
|
3917
|
-
for (const part of diffParts) {
|
|
3918
|
-
if (!part.trim()) continue;
|
|
3919
|
-
const headerMatch = /diff --git a\/(.+?) b\/(.+?)(?:\n|$)/.exec(part);
|
|
3920
|
-
if (!headerMatch || !headerMatch[2]) continue;
|
|
3921
|
-
const filePath = headerMatch[2];
|
|
3922
|
-
let status = "modified";
|
|
3923
|
-
if (newFileRegex.test(part)) {
|
|
3924
|
-
status = "added";
|
|
3925
|
-
} else if (deletedFileRegex.test(part)) {
|
|
3926
|
-
status = "deleted";
|
|
3927
|
-
} else if (part.includes("rename from")) {
|
|
3928
|
-
status = "renamed";
|
|
3929
|
-
}
|
|
3930
|
-
const additions = (part.match(additionRegex) || []).length;
|
|
3931
|
-
const deletions = (part.match(deletionRegex) || []).length;
|
|
3932
|
-
files.push({
|
|
3933
|
-
path: filePath,
|
|
3934
|
-
status,
|
|
3935
|
-
additions,
|
|
3936
|
-
deletions
|
|
3937
|
-
});
|
|
3938
|
-
}
|
|
3972
|
+
const files = diff2.split(/(?=diff --git)/).map(parseDiffPart).filter((f) => f !== null);
|
|
3939
3973
|
return Ok({ diff: diff2, files });
|
|
3940
3974
|
} catch (error) {
|
|
3941
3975
|
return Err({
|
|
@@ -4101,107 +4135,123 @@ var ChecklistBuilder = class {
|
|
|
4101
4135
|
this.graphImpactData = graphImpactData;
|
|
4102
4136
|
return this;
|
|
4103
4137
|
}
|
|
4104
|
-
|
|
4105
|
-
|
|
4138
|
+
/**
|
|
4139
|
+
* Build a single harness check item with or without graph data.
|
|
4140
|
+
*/
|
|
4141
|
+
buildHarnessCheckItem(id, check, fallbackDetails, graphItemBuilder) {
|
|
4142
|
+
if (this.graphHarnessData && graphItemBuilder) {
|
|
4143
|
+
return graphItemBuilder();
|
|
4144
|
+
}
|
|
4145
|
+
return {
|
|
4146
|
+
id,
|
|
4147
|
+
category: "harness",
|
|
4148
|
+
check,
|
|
4149
|
+
passed: true,
|
|
4150
|
+
severity: "info",
|
|
4151
|
+
details: fallbackDetails
|
|
4152
|
+
};
|
|
4153
|
+
}
|
|
4154
|
+
/**
|
|
4155
|
+
* Build all harness check items based on harnessOptions and graph data.
|
|
4156
|
+
*/
|
|
4157
|
+
buildHarnessItems() {
|
|
4158
|
+
if (!this.harnessOptions) return [];
|
|
4106
4159
|
const items = [];
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
severity: "info",
|
|
4116
|
-
details: this.graphHarnessData.graphExists ? `Graph loaded: ${this.graphHarnessData.nodeCount} nodes, ${this.graphHarnessData.edgeCount} edges` : "No graph available \u2014 run harness scan to build the knowledge graph"
|
|
4117
|
-
});
|
|
4118
|
-
} else {
|
|
4119
|
-
items.push({
|
|
4160
|
+
const graphData = this.graphHarnessData;
|
|
4161
|
+
if (this.harnessOptions.context !== false) {
|
|
4162
|
+
items.push(
|
|
4163
|
+
this.buildHarnessCheckItem(
|
|
4164
|
+
"harness-context",
|
|
4165
|
+
"Context validation",
|
|
4166
|
+
"Harness context validation not yet integrated (run with graph for real checks)",
|
|
4167
|
+
graphData ? () => ({
|
|
4120
4168
|
id: "harness-context",
|
|
4121
4169
|
category: "harness",
|
|
4122
4170
|
check: "Context validation",
|
|
4123
|
-
passed:
|
|
4124
|
-
severity: "info",
|
|
4125
|
-
details: "Harness context validation not yet integrated (run with graph for real checks)"
|
|
4126
|
-
});
|
|
4127
|
-
}
|
|
4128
|
-
}
|
|
4129
|
-
if (this.harnessOptions.constraints !== false) {
|
|
4130
|
-
if (this.graphHarnessData) {
|
|
4131
|
-
const violations = this.graphHarnessData.constraintViolations;
|
|
4132
|
-
items.push({
|
|
4133
|
-
id: "harness-constraints",
|
|
4134
|
-
category: "harness",
|
|
4135
|
-
check: "Constraint validation",
|
|
4136
|
-
passed: violations === 0,
|
|
4137
|
-
severity: violations > 0 ? "error" : "info",
|
|
4138
|
-
details: violations === 0 ? "No constraint violations detected" : `${violations} constraint violation(s) detected`
|
|
4139
|
-
});
|
|
4140
|
-
} else {
|
|
4141
|
-
items.push({
|
|
4142
|
-
id: "harness-constraints",
|
|
4143
|
-
category: "harness",
|
|
4144
|
-
check: "Constraint validation",
|
|
4145
|
-
passed: true,
|
|
4171
|
+
passed: graphData.graphExists && graphData.nodeCount > 0,
|
|
4146
4172
|
severity: "info",
|
|
4147
|
-
details:
|
|
4148
|
-
})
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
+
details: graphData.graphExists ? `Graph loaded: ${graphData.nodeCount} nodes, ${graphData.edgeCount} edges` : "No graph available \u2014 run harness scan to build the knowledge graph"
|
|
4174
|
+
}) : void 0
|
|
4175
|
+
)
|
|
4176
|
+
);
|
|
4177
|
+
}
|
|
4178
|
+
if (this.harnessOptions.constraints !== false) {
|
|
4179
|
+
items.push(
|
|
4180
|
+
this.buildHarnessCheckItem(
|
|
4181
|
+
"harness-constraints",
|
|
4182
|
+
"Constraint validation",
|
|
4183
|
+
"Harness constraint validation not yet integrated (run with graph for real checks)",
|
|
4184
|
+
graphData ? () => {
|
|
4185
|
+
const violations = graphData.constraintViolations;
|
|
4186
|
+
return {
|
|
4187
|
+
id: "harness-constraints",
|
|
4188
|
+
category: "harness",
|
|
4189
|
+
check: "Constraint validation",
|
|
4190
|
+
passed: violations === 0,
|
|
4191
|
+
severity: violations > 0 ? "error" : "info",
|
|
4192
|
+
details: violations === 0 ? "No constraint violations detected" : `${violations} constraint violation(s) detected`
|
|
4193
|
+
};
|
|
4194
|
+
} : void 0
|
|
4195
|
+
)
|
|
4196
|
+
);
|
|
4197
|
+
}
|
|
4198
|
+
if (this.harnessOptions.entropy !== false) {
|
|
4199
|
+
items.push(
|
|
4200
|
+
this.buildHarnessCheckItem(
|
|
4201
|
+
"harness-entropy",
|
|
4202
|
+
"Entropy detection",
|
|
4203
|
+
"Harness entropy detection not yet integrated (run with graph for real checks)",
|
|
4204
|
+
graphData ? () => {
|
|
4205
|
+
const issues = graphData.unreachableNodes + graphData.undocumentedFiles;
|
|
4206
|
+
return {
|
|
4207
|
+
id: "harness-entropy",
|
|
4208
|
+
category: "harness",
|
|
4209
|
+
check: "Entropy detection",
|
|
4210
|
+
passed: issues === 0,
|
|
4211
|
+
severity: issues > 0 ? "warning" : "info",
|
|
4212
|
+
details: issues === 0 ? "No entropy issues detected" : `${graphData.unreachableNodes} unreachable node(s), ${graphData.undocumentedFiles} undocumented file(s)`
|
|
4213
|
+
};
|
|
4214
|
+
} : void 0
|
|
4215
|
+
)
|
|
4216
|
+
);
|
|
4217
|
+
}
|
|
4218
|
+
return items;
|
|
4219
|
+
}
|
|
4220
|
+
/**
|
|
4221
|
+
* Execute a single custom rule and return a ReviewItem.
|
|
4222
|
+
*/
|
|
4223
|
+
async executeCustomRule(rule, changes) {
|
|
4224
|
+
try {
|
|
4225
|
+
const result = await rule.check(changes, this.rootDir);
|
|
4226
|
+
const item = {
|
|
4227
|
+
id: rule.id,
|
|
4228
|
+
category: "custom",
|
|
4229
|
+
check: rule.name,
|
|
4230
|
+
passed: result.passed,
|
|
4231
|
+
severity: rule.severity,
|
|
4232
|
+
details: result.details
|
|
4233
|
+
};
|
|
4234
|
+
if (result.suggestion !== void 0) item.suggestion = result.suggestion;
|
|
4235
|
+
if (result.file !== void 0) item.file = result.file;
|
|
4236
|
+
if (result.line !== void 0) item.line = result.line;
|
|
4237
|
+
return item;
|
|
4238
|
+
} catch (error) {
|
|
4239
|
+
return {
|
|
4240
|
+
id: rule.id,
|
|
4241
|
+
category: "custom",
|
|
4242
|
+
check: rule.name,
|
|
4243
|
+
passed: false,
|
|
4244
|
+
severity: "error",
|
|
4245
|
+
details: `Rule execution failed: ${String(error)}`
|
|
4246
|
+
};
|
|
4173
4247
|
}
|
|
4248
|
+
}
|
|
4249
|
+
async run(changes) {
|
|
4250
|
+
const startTime = Date.now();
|
|
4251
|
+
const items = [];
|
|
4252
|
+
items.push(...this.buildHarnessItems());
|
|
4174
4253
|
for (const rule of this.customRules) {
|
|
4175
|
-
|
|
4176
|
-
const result = await rule.check(changes, this.rootDir);
|
|
4177
|
-
const item = {
|
|
4178
|
-
id: rule.id,
|
|
4179
|
-
category: "custom",
|
|
4180
|
-
check: rule.name,
|
|
4181
|
-
passed: result.passed,
|
|
4182
|
-
severity: rule.severity,
|
|
4183
|
-
details: result.details
|
|
4184
|
-
};
|
|
4185
|
-
if (result.suggestion !== void 0) {
|
|
4186
|
-
item.suggestion = result.suggestion;
|
|
4187
|
-
}
|
|
4188
|
-
if (result.file !== void 0) {
|
|
4189
|
-
item.file = result.file;
|
|
4190
|
-
}
|
|
4191
|
-
if (result.line !== void 0) {
|
|
4192
|
-
item.line = result.line;
|
|
4193
|
-
}
|
|
4194
|
-
items.push(item);
|
|
4195
|
-
} catch (error) {
|
|
4196
|
-
items.push({
|
|
4197
|
-
id: rule.id,
|
|
4198
|
-
category: "custom",
|
|
4199
|
-
check: rule.name,
|
|
4200
|
-
passed: false,
|
|
4201
|
-
severity: "error",
|
|
4202
|
-
details: `Rule execution failed: ${String(error)}`
|
|
4203
|
-
});
|
|
4204
|
-
}
|
|
4254
|
+
items.push(await this.executeCustomRule(rule, changes));
|
|
4205
4255
|
}
|
|
4206
4256
|
if (this.diffOptions) {
|
|
4207
4257
|
const diffResult = await analyzeDiff(changes, this.diffOptions, this.graphImpactData);
|
|
@@ -4216,7 +4266,6 @@ var ChecklistBuilder = class {
|
|
|
4216
4266
|
const checklist = {
|
|
4217
4267
|
items,
|
|
4218
4268
|
passed: failed === 0,
|
|
4219
|
-
// Pass if no failed items
|
|
4220
4269
|
summary: {
|
|
4221
4270
|
total: items.length,
|
|
4222
4271
|
passed,
|
|
@@ -4769,6 +4818,8 @@ var INDEX_FILE = "index.json";
|
|
|
4769
4818
|
var SESSIONS_DIR = "sessions";
|
|
4770
4819
|
var SESSION_INDEX_FILE = "index.md";
|
|
4771
4820
|
var SUMMARY_FILE = "summary.md";
|
|
4821
|
+
var SESSION_STATE_FILE = "session-state.json";
|
|
4822
|
+
var ARCHIVE_DIR = "archive";
|
|
4772
4823
|
|
|
4773
4824
|
// src/state/stream-resolver.ts
|
|
4774
4825
|
var STREAMS_DIR = "streams";
|
|
@@ -5677,6 +5728,143 @@ function listActiveSessions(projectPath) {
|
|
|
5677
5728
|
}
|
|
5678
5729
|
}
|
|
5679
5730
|
|
|
5731
|
+
// src/state/session-sections.ts
|
|
5732
|
+
import * as fs14 from "fs";
|
|
5733
|
+
import * as path11 from "path";
|
|
5734
|
+
import { SESSION_SECTION_NAMES } from "@harness-engineering/types";
|
|
5735
|
+
function emptySections() {
|
|
5736
|
+
const sections = {};
|
|
5737
|
+
for (const name of SESSION_SECTION_NAMES) {
|
|
5738
|
+
sections[name] = [];
|
|
5739
|
+
}
|
|
5740
|
+
return sections;
|
|
5741
|
+
}
|
|
5742
|
+
async function loadSessionState(projectPath, sessionSlug) {
|
|
5743
|
+
const dirResult = resolveSessionDir(projectPath, sessionSlug);
|
|
5744
|
+
if (!dirResult.ok) return dirResult;
|
|
5745
|
+
const sessionDir = dirResult.value;
|
|
5746
|
+
const filePath = path11.join(sessionDir, SESSION_STATE_FILE);
|
|
5747
|
+
if (!fs14.existsSync(filePath)) {
|
|
5748
|
+
return Ok(emptySections());
|
|
5749
|
+
}
|
|
5750
|
+
try {
|
|
5751
|
+
const raw = fs14.readFileSync(filePath, "utf-8");
|
|
5752
|
+
const parsed = JSON.parse(raw);
|
|
5753
|
+
const sections = emptySections();
|
|
5754
|
+
for (const name of SESSION_SECTION_NAMES) {
|
|
5755
|
+
if (Array.isArray(parsed[name])) {
|
|
5756
|
+
sections[name] = parsed[name];
|
|
5757
|
+
}
|
|
5758
|
+
}
|
|
5759
|
+
return Ok(sections);
|
|
5760
|
+
} catch (error) {
|
|
5761
|
+
return Err(
|
|
5762
|
+
new Error(
|
|
5763
|
+
`Failed to load session state: ${error instanceof Error ? error.message : String(error)}`
|
|
5764
|
+
)
|
|
5765
|
+
);
|
|
5766
|
+
}
|
|
5767
|
+
}
|
|
5768
|
+
async function saveSessionState(projectPath, sessionSlug, sections) {
|
|
5769
|
+
const dirResult = resolveSessionDir(projectPath, sessionSlug, { create: true });
|
|
5770
|
+
if (!dirResult.ok) return dirResult;
|
|
5771
|
+
const sessionDir = dirResult.value;
|
|
5772
|
+
const filePath = path11.join(sessionDir, SESSION_STATE_FILE);
|
|
5773
|
+
try {
|
|
5774
|
+
fs14.writeFileSync(filePath, JSON.stringify(sections, null, 2));
|
|
5775
|
+
return Ok(void 0);
|
|
5776
|
+
} catch (error) {
|
|
5777
|
+
return Err(
|
|
5778
|
+
new Error(
|
|
5779
|
+
`Failed to save session state: ${error instanceof Error ? error.message : String(error)}`
|
|
5780
|
+
)
|
|
5781
|
+
);
|
|
5782
|
+
}
|
|
5783
|
+
}
|
|
5784
|
+
async function readSessionSections(projectPath, sessionSlug) {
|
|
5785
|
+
return loadSessionState(projectPath, sessionSlug);
|
|
5786
|
+
}
|
|
5787
|
+
async function readSessionSection(projectPath, sessionSlug, section) {
|
|
5788
|
+
const result = await loadSessionState(projectPath, sessionSlug);
|
|
5789
|
+
if (!result.ok) return result;
|
|
5790
|
+
return Ok(result.value[section]);
|
|
5791
|
+
}
|
|
5792
|
+
async function appendSessionEntry(projectPath, sessionSlug, section, authorSkill, content) {
|
|
5793
|
+
const loadResult = await loadSessionState(projectPath, sessionSlug);
|
|
5794
|
+
if (!loadResult.ok) return loadResult;
|
|
5795
|
+
const sections = loadResult.value;
|
|
5796
|
+
const entry = {
|
|
5797
|
+
id: generateEntryId(),
|
|
5798
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5799
|
+
authorSkill,
|
|
5800
|
+
content,
|
|
5801
|
+
status: "active"
|
|
5802
|
+
};
|
|
5803
|
+
sections[section].push(entry);
|
|
5804
|
+
const saveResult = await saveSessionState(projectPath, sessionSlug, sections);
|
|
5805
|
+
if (!saveResult.ok) return saveResult;
|
|
5806
|
+
return Ok(entry);
|
|
5807
|
+
}
|
|
5808
|
+
async function updateSessionEntryStatus(projectPath, sessionSlug, section, entryId, newStatus) {
|
|
5809
|
+
const loadResult = await loadSessionState(projectPath, sessionSlug);
|
|
5810
|
+
if (!loadResult.ok) return loadResult;
|
|
5811
|
+
const sections = loadResult.value;
|
|
5812
|
+
const entry = sections[section].find((e) => e.id === entryId);
|
|
5813
|
+
if (!entry) {
|
|
5814
|
+
return Err(new Error(`Entry '${entryId}' not found in section '${section}'`));
|
|
5815
|
+
}
|
|
5816
|
+
entry.status = newStatus;
|
|
5817
|
+
const saveResult = await saveSessionState(projectPath, sessionSlug, sections);
|
|
5818
|
+
if (!saveResult.ok) return saveResult;
|
|
5819
|
+
return Ok(entry);
|
|
5820
|
+
}
|
|
5821
|
+
function generateEntryId() {
|
|
5822
|
+
const timestamp = Date.now().toString(36);
|
|
5823
|
+
const random = Math.random().toString(36).substring(2, 8);
|
|
5824
|
+
return `${timestamp}-${random}`;
|
|
5825
|
+
}
|
|
5826
|
+
|
|
5827
|
+
// src/state/session-archive.ts
|
|
5828
|
+
import * as fs15 from "fs";
|
|
5829
|
+
import * as path12 from "path";
|
|
5830
|
+
async function archiveSession(projectPath, sessionSlug) {
|
|
5831
|
+
const dirResult = resolveSessionDir(projectPath, sessionSlug);
|
|
5832
|
+
if (!dirResult.ok) return dirResult;
|
|
5833
|
+
const sessionDir = dirResult.value;
|
|
5834
|
+
if (!fs15.existsSync(sessionDir)) {
|
|
5835
|
+
return Err(new Error(`Session '${sessionSlug}' not found at ${sessionDir}`));
|
|
5836
|
+
}
|
|
5837
|
+
const archiveBase = path12.join(projectPath, HARNESS_DIR, ARCHIVE_DIR, "sessions");
|
|
5838
|
+
try {
|
|
5839
|
+
fs15.mkdirSync(archiveBase, { recursive: true });
|
|
5840
|
+
const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
5841
|
+
let archiveName = `${sessionSlug}-${date}`;
|
|
5842
|
+
let counter = 1;
|
|
5843
|
+
while (fs15.existsSync(path12.join(archiveBase, archiveName))) {
|
|
5844
|
+
archiveName = `${sessionSlug}-${date}-${counter}`;
|
|
5845
|
+
counter++;
|
|
5846
|
+
}
|
|
5847
|
+
const dest = path12.join(archiveBase, archiveName);
|
|
5848
|
+
try {
|
|
5849
|
+
fs15.renameSync(sessionDir, dest);
|
|
5850
|
+
} catch (renameErr) {
|
|
5851
|
+
if (renameErr instanceof Error && "code" in renameErr && renameErr.code === "EXDEV") {
|
|
5852
|
+
fs15.cpSync(sessionDir, dest, { recursive: true });
|
|
5853
|
+
fs15.rmSync(sessionDir, { recursive: true });
|
|
5854
|
+
} else {
|
|
5855
|
+
throw renameErr;
|
|
5856
|
+
}
|
|
5857
|
+
}
|
|
5858
|
+
return Ok(void 0);
|
|
5859
|
+
} catch (error) {
|
|
5860
|
+
return Err(
|
|
5861
|
+
new Error(
|
|
5862
|
+
`Failed to archive session: ${error instanceof Error ? error.message : String(error)}`
|
|
5863
|
+
)
|
|
5864
|
+
);
|
|
5865
|
+
}
|
|
5866
|
+
}
|
|
5867
|
+
|
|
5680
5868
|
// src/workflow/runner.ts
|
|
5681
5869
|
async function executeWorkflow(workflow, executor) {
|
|
5682
5870
|
const stepResults = [];
|
|
@@ -5826,7 +6014,7 @@ async function runMultiTurnPipeline(initialContext, turnExecutor, options) {
|
|
|
5826
6014
|
}
|
|
5827
6015
|
|
|
5828
6016
|
// src/security/scanner.ts
|
|
5829
|
-
import * as
|
|
6017
|
+
import * as fs17 from "fs/promises";
|
|
5830
6018
|
|
|
5831
6019
|
// src/security/rules/registry.ts
|
|
5832
6020
|
var RuleRegistry = class {
|
|
@@ -5913,15 +6101,15 @@ function resolveRuleSeverity(ruleId, defaultSeverity, overrides, strict) {
|
|
|
5913
6101
|
}
|
|
5914
6102
|
|
|
5915
6103
|
// src/security/stack-detector.ts
|
|
5916
|
-
import * as
|
|
5917
|
-
import * as
|
|
6104
|
+
import * as fs16 from "fs";
|
|
6105
|
+
import * as path13 from "path";
|
|
5918
6106
|
function detectStack(projectRoot) {
|
|
5919
6107
|
const stacks = [];
|
|
5920
|
-
const pkgJsonPath =
|
|
5921
|
-
if (
|
|
6108
|
+
const pkgJsonPath = path13.join(projectRoot, "package.json");
|
|
6109
|
+
if (fs16.existsSync(pkgJsonPath)) {
|
|
5922
6110
|
stacks.push("node");
|
|
5923
6111
|
try {
|
|
5924
|
-
const pkgJson = JSON.parse(
|
|
6112
|
+
const pkgJson = JSON.parse(fs16.readFileSync(pkgJsonPath, "utf-8"));
|
|
5925
6113
|
const allDeps = {
|
|
5926
6114
|
...pkgJson.dependencies,
|
|
5927
6115
|
...pkgJson.devDependencies
|
|
@@ -5936,13 +6124,13 @@ function detectStack(projectRoot) {
|
|
|
5936
6124
|
} catch {
|
|
5937
6125
|
}
|
|
5938
6126
|
}
|
|
5939
|
-
const goModPath =
|
|
5940
|
-
if (
|
|
6127
|
+
const goModPath = path13.join(projectRoot, "go.mod");
|
|
6128
|
+
if (fs16.existsSync(goModPath)) {
|
|
5941
6129
|
stacks.push("go");
|
|
5942
6130
|
}
|
|
5943
|
-
const requirementsPath =
|
|
5944
|
-
const pyprojectPath =
|
|
5945
|
-
if (
|
|
6131
|
+
const requirementsPath = path13.join(projectRoot, "requirements.txt");
|
|
6132
|
+
const pyprojectPath = path13.join(projectRoot, "pyproject.toml");
|
|
6133
|
+
if (fs16.existsSync(requirementsPath) || fs16.existsSync(pyprojectPath)) {
|
|
5946
6134
|
stacks.push("python");
|
|
5947
6135
|
}
|
|
5948
6136
|
return stacks;
|
|
@@ -6369,7 +6557,7 @@ var SecurityScanner = class {
|
|
|
6369
6557
|
}
|
|
6370
6558
|
async scanFile(filePath) {
|
|
6371
6559
|
if (!this.config.enabled) return [];
|
|
6372
|
-
const content = await
|
|
6560
|
+
const content = await fs17.readFile(filePath, "utf-8");
|
|
6373
6561
|
return this.scanContent(content, filePath, 1);
|
|
6374
6562
|
}
|
|
6375
6563
|
async scanFiles(filePaths) {
|
|
@@ -6394,7 +6582,7 @@ var SecurityScanner = class {
|
|
|
6394
6582
|
};
|
|
6395
6583
|
|
|
6396
6584
|
// src/ci/check-orchestrator.ts
|
|
6397
|
-
import * as
|
|
6585
|
+
import * as path14 from "path";
|
|
6398
6586
|
var ALL_CHECKS = [
|
|
6399
6587
|
"validate",
|
|
6400
6588
|
"deps",
|
|
@@ -6407,7 +6595,7 @@ var ALL_CHECKS = [
|
|
|
6407
6595
|
];
|
|
6408
6596
|
async function runValidateCheck(projectRoot, config) {
|
|
6409
6597
|
const issues = [];
|
|
6410
|
-
const agentsPath =
|
|
6598
|
+
const agentsPath = path14.join(projectRoot, config.agentsMapPath ?? "AGENTS.md");
|
|
6411
6599
|
const result = await validateAgentsMap(agentsPath);
|
|
6412
6600
|
if (!result.ok) {
|
|
6413
6601
|
issues.push({ severity: "error", message: result.error.message });
|
|
@@ -6464,7 +6652,7 @@ async function runDepsCheck(projectRoot, config) {
|
|
|
6464
6652
|
}
|
|
6465
6653
|
async function runDocsCheck(projectRoot, config) {
|
|
6466
6654
|
const issues = [];
|
|
6467
|
-
const docsDir =
|
|
6655
|
+
const docsDir = path14.join(projectRoot, config.docsDir ?? "docs");
|
|
6468
6656
|
const entropyConfig = config.entropy || {};
|
|
6469
6657
|
const result = await checkDocCoverage("project", {
|
|
6470
6658
|
docsDir,
|
|
@@ -6489,10 +6677,14 @@ async function runDocsCheck(projectRoot, config) {
|
|
|
6489
6677
|
}
|
|
6490
6678
|
return issues;
|
|
6491
6679
|
}
|
|
6492
|
-
async function runEntropyCheck(projectRoot,
|
|
6680
|
+
async function runEntropyCheck(projectRoot, config) {
|
|
6493
6681
|
const issues = [];
|
|
6682
|
+
const entropyConfig = config.entropy || {};
|
|
6683
|
+
const perfConfig = config.performance || {};
|
|
6684
|
+
const entryPoints = entropyConfig.entryPoints ?? perfConfig.entryPoints;
|
|
6494
6685
|
const analyzer = new EntropyAnalyzer({
|
|
6495
6686
|
rootDir: projectRoot,
|
|
6687
|
+
...entryPoints ? { entryPoints } : {},
|
|
6496
6688
|
analyze: { drift: true, deadCode: true, patterns: false }
|
|
6497
6689
|
});
|
|
6498
6690
|
const result = await analyzer.analyze();
|
|
@@ -6554,8 +6746,10 @@ async function runSecurityCheck(projectRoot, config) {
|
|
|
6554
6746
|
async function runPerfCheck(projectRoot, config) {
|
|
6555
6747
|
const issues = [];
|
|
6556
6748
|
const perfConfig = config.performance || {};
|
|
6749
|
+
const entryPoints = perfConfig.entryPoints;
|
|
6557
6750
|
const perfAnalyzer = new EntropyAnalyzer({
|
|
6558
6751
|
rootDir: projectRoot,
|
|
6752
|
+
...entryPoints ? { entryPoints } : {},
|
|
6559
6753
|
analyze: {
|
|
6560
6754
|
complexity: perfConfig.complexity || true,
|
|
6561
6755
|
coupling: perfConfig.coupling || true,
|
|
@@ -6736,7 +6930,7 @@ async function runCIChecks(input) {
|
|
|
6736
6930
|
}
|
|
6737
6931
|
|
|
6738
6932
|
// src/review/mechanical-checks.ts
|
|
6739
|
-
import * as
|
|
6933
|
+
import * as path15 from "path";
|
|
6740
6934
|
async function runMechanicalChecks(options) {
|
|
6741
6935
|
const { projectRoot, config, skip = [], changedFiles } = options;
|
|
6742
6936
|
const findings = [];
|
|
@@ -6748,7 +6942,7 @@ async function runMechanicalChecks(options) {
|
|
|
6748
6942
|
};
|
|
6749
6943
|
if (!skip.includes("validate")) {
|
|
6750
6944
|
try {
|
|
6751
|
-
const agentsPath =
|
|
6945
|
+
const agentsPath = path15.join(projectRoot, config.agentsMapPath ?? "AGENTS.md");
|
|
6752
6946
|
const result = await validateAgentsMap(agentsPath);
|
|
6753
6947
|
if (!result.ok) {
|
|
6754
6948
|
statuses.validate = "fail";
|
|
@@ -6785,7 +6979,7 @@ async function runMechanicalChecks(options) {
|
|
|
6785
6979
|
statuses.validate = "fail";
|
|
6786
6980
|
findings.push({
|
|
6787
6981
|
tool: "validate",
|
|
6788
|
-
file:
|
|
6982
|
+
file: path15.join(projectRoot, "AGENTS.md"),
|
|
6789
6983
|
message: err instanceof Error ? err.message : String(err),
|
|
6790
6984
|
severity: "error"
|
|
6791
6985
|
});
|
|
@@ -6849,7 +7043,7 @@ async function runMechanicalChecks(options) {
|
|
|
6849
7043
|
(async () => {
|
|
6850
7044
|
const localFindings = [];
|
|
6851
7045
|
try {
|
|
6852
|
-
const docsDir =
|
|
7046
|
+
const docsDir = path15.join(projectRoot, config.docsDir ?? "docs");
|
|
6853
7047
|
const result = await checkDocCoverage("project", { docsDir });
|
|
6854
7048
|
if (!result.ok) {
|
|
6855
7049
|
statuses["check-docs"] = "warn";
|
|
@@ -6876,7 +7070,7 @@ async function runMechanicalChecks(options) {
|
|
|
6876
7070
|
statuses["check-docs"] = "warn";
|
|
6877
7071
|
localFindings.push({
|
|
6878
7072
|
tool: "check-docs",
|
|
6879
|
-
file:
|
|
7073
|
+
file: path15.join(projectRoot, "docs"),
|
|
6880
7074
|
message: err instanceof Error ? err.message : String(err),
|
|
6881
7075
|
severity: "warning"
|
|
6882
7076
|
});
|
|
@@ -7024,7 +7218,7 @@ function detectChangeType(commitMessage, diff2) {
|
|
|
7024
7218
|
}
|
|
7025
7219
|
|
|
7026
7220
|
// src/review/context-scoper.ts
|
|
7027
|
-
import * as
|
|
7221
|
+
import * as path16 from "path";
|
|
7028
7222
|
var ALL_DOMAINS = ["compliance", "bug", "security", "architecture"];
|
|
7029
7223
|
var SECURITY_PATTERNS = /auth|crypto|password|secret|token|session|cookie|hash|encrypt|decrypt|sql|shell|exec|eval/i;
|
|
7030
7224
|
function computeContextBudget(diffLines) {
|
|
@@ -7032,18 +7226,18 @@ function computeContextBudget(diffLines) {
|
|
|
7032
7226
|
return diffLines;
|
|
7033
7227
|
}
|
|
7034
7228
|
function isWithinProject(absPath, projectRoot) {
|
|
7035
|
-
const resolvedRoot =
|
|
7036
|
-
const resolvedPath =
|
|
7037
|
-
return resolvedPath.startsWith(resolvedRoot) || resolvedPath ===
|
|
7229
|
+
const resolvedRoot = path16.resolve(projectRoot) + path16.sep;
|
|
7230
|
+
const resolvedPath = path16.resolve(absPath);
|
|
7231
|
+
return resolvedPath.startsWith(resolvedRoot) || resolvedPath === path16.resolve(projectRoot);
|
|
7038
7232
|
}
|
|
7039
7233
|
async function readContextFile(projectRoot, filePath, reason) {
|
|
7040
|
-
const absPath =
|
|
7234
|
+
const absPath = path16.isAbsolute(filePath) ? filePath : path16.join(projectRoot, filePath);
|
|
7041
7235
|
if (!isWithinProject(absPath, projectRoot)) return null;
|
|
7042
7236
|
const result = await readFileContent(absPath);
|
|
7043
7237
|
if (!result.ok) return null;
|
|
7044
7238
|
const content = result.value;
|
|
7045
7239
|
const lines = content.split("\n").length;
|
|
7046
|
-
const relPath =
|
|
7240
|
+
const relPath = path16.isAbsolute(filePath) ? relativePosix(projectRoot, filePath) : filePath;
|
|
7047
7241
|
return { path: relPath, content, reason, lines };
|
|
7048
7242
|
}
|
|
7049
7243
|
function extractImportSources(content) {
|
|
@@ -7058,18 +7252,18 @@ function extractImportSources(content) {
|
|
|
7058
7252
|
}
|
|
7059
7253
|
async function resolveImportPath(projectRoot, fromFile, importSource) {
|
|
7060
7254
|
if (!importSource.startsWith(".")) return null;
|
|
7061
|
-
const fromDir =
|
|
7062
|
-
const basePath =
|
|
7255
|
+
const fromDir = path16.dirname(path16.join(projectRoot, fromFile));
|
|
7256
|
+
const basePath = path16.resolve(fromDir, importSource);
|
|
7063
7257
|
if (!isWithinProject(basePath, projectRoot)) return null;
|
|
7064
7258
|
const relBase = relativePosix(projectRoot, basePath);
|
|
7065
7259
|
const candidates = [
|
|
7066
7260
|
relBase + ".ts",
|
|
7067
7261
|
relBase + ".tsx",
|
|
7068
7262
|
relBase + ".mts",
|
|
7069
|
-
|
|
7263
|
+
path16.join(relBase, "index.ts")
|
|
7070
7264
|
];
|
|
7071
7265
|
for (const candidate of candidates) {
|
|
7072
|
-
const absCandidate =
|
|
7266
|
+
const absCandidate = path16.join(projectRoot, candidate);
|
|
7073
7267
|
if (await fileExists(absCandidate)) {
|
|
7074
7268
|
return candidate;
|
|
7075
7269
|
}
|
|
@@ -7077,7 +7271,7 @@ async function resolveImportPath(projectRoot, fromFile, importSource) {
|
|
|
7077
7271
|
return null;
|
|
7078
7272
|
}
|
|
7079
7273
|
async function findTestFiles(projectRoot, sourceFile) {
|
|
7080
|
-
const baseName =
|
|
7274
|
+
const baseName = path16.basename(sourceFile, path16.extname(sourceFile));
|
|
7081
7275
|
const pattern = `**/${baseName}.{test,spec}.{ts,tsx,mts}`;
|
|
7082
7276
|
const results = await findFiles(pattern, projectRoot);
|
|
7083
7277
|
return results.map((f) => relativePosix(projectRoot, f));
|
|
@@ -7366,101 +7560,102 @@ function findMissingJsDoc(bundle) {
|
|
|
7366
7560
|
}
|
|
7367
7561
|
return missing;
|
|
7368
7562
|
}
|
|
7369
|
-
function
|
|
7563
|
+
function checkMissingJsDoc(bundle, rules) {
|
|
7564
|
+
const jsDocRule = rules.find((r) => r.text.toLowerCase().includes("jsdoc"));
|
|
7565
|
+
if (!jsDocRule) return [];
|
|
7566
|
+
const missingDocs = findMissingJsDoc(bundle);
|
|
7567
|
+
return missingDocs.map((m) => ({
|
|
7568
|
+
id: makeFindingId("compliance", m.file, m.line, `Missing JSDoc ${m.exportName}`),
|
|
7569
|
+
file: m.file,
|
|
7570
|
+
lineRange: [m.line, m.line],
|
|
7571
|
+
domain: "compliance",
|
|
7572
|
+
severity: "important",
|
|
7573
|
+
title: `Missing JSDoc on exported \`${m.exportName}\``,
|
|
7574
|
+
rationale: `Convention requires all exports to have JSDoc comments (from ${jsDocRule.source}).`,
|
|
7575
|
+
suggestion: `Add a JSDoc comment above the export of \`${m.exportName}\`.`,
|
|
7576
|
+
evidence: [`changeType: ${bundle.changeType}`, `Convention rule: "${jsDocRule.text}"`],
|
|
7577
|
+
validatedBy: "heuristic"
|
|
7578
|
+
}));
|
|
7579
|
+
}
|
|
7580
|
+
function checkFeatureSpec(bundle) {
|
|
7581
|
+
const hasSpecContext = bundle.contextFiles.some(
|
|
7582
|
+
(f) => f.reason === "spec" || f.reason === "convention"
|
|
7583
|
+
);
|
|
7584
|
+
if (hasSpecContext || bundle.changedFiles.length === 0) return [];
|
|
7585
|
+
const firstFile = bundle.changedFiles[0];
|
|
7586
|
+
return [
|
|
7587
|
+
{
|
|
7588
|
+
id: makeFindingId("compliance", firstFile.path, 1, "No spec for feature"),
|
|
7589
|
+
file: firstFile.path,
|
|
7590
|
+
lineRange: [1, 1],
|
|
7591
|
+
domain: "compliance",
|
|
7592
|
+
severity: "suggestion",
|
|
7593
|
+
title: "No spec/design doc found for feature change",
|
|
7594
|
+
rationale: "Feature changes should reference a spec or design doc to verify alignment. No spec context was included in the review bundle.",
|
|
7595
|
+
evidence: [`changeType: feature`, `contextFiles count: ${bundle.contextFiles.length}`],
|
|
7596
|
+
validatedBy: "heuristic"
|
|
7597
|
+
}
|
|
7598
|
+
];
|
|
7599
|
+
}
|
|
7600
|
+
function checkBugfixHistory(bundle) {
|
|
7601
|
+
if (bundle.commitHistory.length > 0 || bundle.changedFiles.length === 0) return [];
|
|
7602
|
+
const firstFile = bundle.changedFiles[0];
|
|
7603
|
+
return [
|
|
7604
|
+
{
|
|
7605
|
+
id: makeFindingId("compliance", firstFile.path, 1, "Bugfix no history"),
|
|
7606
|
+
file: firstFile.path,
|
|
7607
|
+
lineRange: [1, 1],
|
|
7608
|
+
domain: "compliance",
|
|
7609
|
+
severity: "suggestion",
|
|
7610
|
+
title: "Bugfix without commit history context",
|
|
7611
|
+
rationale: "Bugfix changes benefit from commit history to verify the root cause is addressed, not just the symptom. No commit history was provided.",
|
|
7612
|
+
evidence: [`changeType: bugfix`, `commitHistory entries: ${bundle.commitHistory.length}`],
|
|
7613
|
+
validatedBy: "heuristic"
|
|
7614
|
+
}
|
|
7615
|
+
];
|
|
7616
|
+
}
|
|
7617
|
+
function checkChangeTypeSpecific(bundle) {
|
|
7618
|
+
switch (bundle.changeType) {
|
|
7619
|
+
case "feature":
|
|
7620
|
+
return checkFeatureSpec(bundle);
|
|
7621
|
+
case "bugfix":
|
|
7622
|
+
return checkBugfixHistory(bundle);
|
|
7623
|
+
default:
|
|
7624
|
+
return [];
|
|
7625
|
+
}
|
|
7626
|
+
}
|
|
7627
|
+
function checkResultTypeConvention(bundle, rules) {
|
|
7628
|
+
const resultTypeRule = rules.find((r) => r.text.toLowerCase().includes("result type"));
|
|
7629
|
+
if (!resultTypeRule) return [];
|
|
7370
7630
|
const findings = [];
|
|
7371
|
-
const
|
|
7372
|
-
|
|
7373
|
-
|
|
7374
|
-
|
|
7375
|
-
for (const m of missingDocs) {
|
|
7631
|
+
for (const cf of bundle.changedFiles) {
|
|
7632
|
+
const hasTryCatch = cf.content.includes("try {") || cf.content.includes("try{");
|
|
7633
|
+
const usesResult = cf.content.includes("Result<") || cf.content.includes("Result >") || cf.content.includes(": Result");
|
|
7634
|
+
if (hasTryCatch && !usesResult) {
|
|
7376
7635
|
findings.push({
|
|
7377
|
-
id: makeFindingId("compliance",
|
|
7378
|
-
file:
|
|
7379
|
-
lineRange: [
|
|
7636
|
+
id: makeFindingId("compliance", cf.path, 1, "try-catch not Result"),
|
|
7637
|
+
file: cf.path,
|
|
7638
|
+
lineRange: [1, cf.lines],
|
|
7380
7639
|
domain: "compliance",
|
|
7381
|
-
severity: "
|
|
7382
|
-
title:
|
|
7383
|
-
rationale: `Convention requires
|
|
7384
|
-
suggestion:
|
|
7385
|
-
evidence: [
|
|
7386
|
-
`changeType: ${bundle.changeType}`,
|
|
7387
|
-
`Convention rule: "${rules.find((r) => r.text.toLowerCase().includes("jsdoc"))?.text ?? ""}"`
|
|
7388
|
-
],
|
|
7640
|
+
severity: "suggestion",
|
|
7641
|
+
title: "Fallible operation uses try/catch instead of Result type",
|
|
7642
|
+
rationale: `Convention requires using Result type for fallible operations (from ${resultTypeRule.source}).`,
|
|
7643
|
+
suggestion: "Refactor error handling to use the Result type pattern.",
|
|
7644
|
+
evidence: [`changeType: ${bundle.changeType}`, `Convention rule: "${resultTypeRule.text}"`],
|
|
7389
7645
|
validatedBy: "heuristic"
|
|
7390
7646
|
});
|
|
7391
7647
|
}
|
|
7392
7648
|
}
|
|
7393
|
-
switch (bundle.changeType) {
|
|
7394
|
-
case "feature": {
|
|
7395
|
-
const hasSpecContext = bundle.contextFiles.some(
|
|
7396
|
-
(f) => f.reason === "spec" || f.reason === "convention"
|
|
7397
|
-
);
|
|
7398
|
-
if (!hasSpecContext && bundle.changedFiles.length > 0) {
|
|
7399
|
-
const firstFile = bundle.changedFiles[0];
|
|
7400
|
-
findings.push({
|
|
7401
|
-
id: makeFindingId("compliance", firstFile.path, 1, "No spec for feature"),
|
|
7402
|
-
file: firstFile.path,
|
|
7403
|
-
lineRange: [1, 1],
|
|
7404
|
-
domain: "compliance",
|
|
7405
|
-
severity: "suggestion",
|
|
7406
|
-
title: "No spec/design doc found for feature change",
|
|
7407
|
-
rationale: "Feature changes should reference a spec or design doc to verify alignment. No spec context was included in the review bundle.",
|
|
7408
|
-
evidence: [`changeType: feature`, `contextFiles count: ${bundle.contextFiles.length}`],
|
|
7409
|
-
validatedBy: "heuristic"
|
|
7410
|
-
});
|
|
7411
|
-
}
|
|
7412
|
-
break;
|
|
7413
|
-
}
|
|
7414
|
-
case "bugfix": {
|
|
7415
|
-
if (bundle.commitHistory.length === 0 && bundle.changedFiles.length > 0) {
|
|
7416
|
-
const firstFile = bundle.changedFiles[0];
|
|
7417
|
-
findings.push({
|
|
7418
|
-
id: makeFindingId("compliance", firstFile.path, 1, "Bugfix no history"),
|
|
7419
|
-
file: firstFile.path,
|
|
7420
|
-
lineRange: [1, 1],
|
|
7421
|
-
domain: "compliance",
|
|
7422
|
-
severity: "suggestion",
|
|
7423
|
-
title: "Bugfix without commit history context",
|
|
7424
|
-
rationale: "Bugfix changes benefit from commit history to verify the root cause is addressed, not just the symptom. No commit history was provided.",
|
|
7425
|
-
evidence: [`changeType: bugfix`, `commitHistory entries: ${bundle.commitHistory.length}`],
|
|
7426
|
-
validatedBy: "heuristic"
|
|
7427
|
-
});
|
|
7428
|
-
}
|
|
7429
|
-
break;
|
|
7430
|
-
}
|
|
7431
|
-
case "refactor": {
|
|
7432
|
-
break;
|
|
7433
|
-
}
|
|
7434
|
-
case "docs": {
|
|
7435
|
-
break;
|
|
7436
|
-
}
|
|
7437
|
-
}
|
|
7438
|
-
const resultTypeRule = rules.find((r) => r.text.toLowerCase().includes("result type"));
|
|
7439
|
-
if (resultTypeRule) {
|
|
7440
|
-
for (const cf of bundle.changedFiles) {
|
|
7441
|
-
const hasTryCatch = cf.content.includes("try {") || cf.content.includes("try{");
|
|
7442
|
-
const usesResult = cf.content.includes("Result<") || cf.content.includes("Result >") || cf.content.includes(": Result");
|
|
7443
|
-
if (hasTryCatch && !usesResult) {
|
|
7444
|
-
findings.push({
|
|
7445
|
-
id: makeFindingId("compliance", cf.path, 1, "try-catch not Result"),
|
|
7446
|
-
file: cf.path,
|
|
7447
|
-
lineRange: [1, cf.lines],
|
|
7448
|
-
domain: "compliance",
|
|
7449
|
-
severity: "suggestion",
|
|
7450
|
-
title: "Fallible operation uses try/catch instead of Result type",
|
|
7451
|
-
rationale: `Convention requires using Result type for fallible operations (from ${resultTypeRule.source}).`,
|
|
7452
|
-
suggestion: "Refactor error handling to use the Result type pattern.",
|
|
7453
|
-
evidence: [
|
|
7454
|
-
`changeType: ${bundle.changeType}`,
|
|
7455
|
-
`Convention rule: "${resultTypeRule.text}"`
|
|
7456
|
-
],
|
|
7457
|
-
validatedBy: "heuristic"
|
|
7458
|
-
});
|
|
7459
|
-
}
|
|
7460
|
-
}
|
|
7461
|
-
}
|
|
7462
7649
|
return findings;
|
|
7463
7650
|
}
|
|
7651
|
+
function runComplianceAgent(bundle) {
|
|
7652
|
+
const rules = extractConventionRules(bundle);
|
|
7653
|
+
return [
|
|
7654
|
+
...checkMissingJsDoc(bundle, rules),
|
|
7655
|
+
...checkChangeTypeSpecific(bundle),
|
|
7656
|
+
...checkResultTypeConvention(bundle, rules)
|
|
7657
|
+
];
|
|
7658
|
+
}
|
|
7464
7659
|
|
|
7465
7660
|
// src/review/agents/bug-agent.ts
|
|
7466
7661
|
var BUG_DETECTION_DESCRIPTOR = {
|
|
@@ -7737,31 +7932,32 @@ var ARCHITECTURE_DESCRIPTOR = {
|
|
|
7737
7932
|
]
|
|
7738
7933
|
};
|
|
7739
7934
|
var LARGE_FILE_THRESHOLD = 300;
|
|
7935
|
+
function isViolationLine(line) {
|
|
7936
|
+
const lower = line.toLowerCase();
|
|
7937
|
+
return lower.includes("violation") || lower.includes("layer");
|
|
7938
|
+
}
|
|
7939
|
+
function createLayerViolationFinding(line, fallbackPath) {
|
|
7940
|
+
const fileMatch = line.match(/(?:in\s+)?(\S+\.(?:ts|tsx|js|jsx))(?::(\d+))?/);
|
|
7941
|
+
const file = fileMatch?.[1] ?? fallbackPath;
|
|
7942
|
+
const lineNum = fileMatch?.[2] ? parseInt(fileMatch[2], 10) : 1;
|
|
7943
|
+
return {
|
|
7944
|
+
id: makeFindingId("arch", file, lineNum, "layer violation"),
|
|
7945
|
+
file,
|
|
7946
|
+
lineRange: [lineNum, lineNum],
|
|
7947
|
+
domain: "architecture",
|
|
7948
|
+
severity: "critical",
|
|
7949
|
+
title: "Layer boundary violation detected by check-deps",
|
|
7950
|
+
rationale: `Architectural layer violation: ${line.trim()}. Imports must flow in the correct direction per the project's layer definitions.`,
|
|
7951
|
+
suggestion: "Route the dependency through the correct intermediate layer (e.g., routes -> services -> db, not routes -> db).",
|
|
7952
|
+
evidence: [line.trim()],
|
|
7953
|
+
validatedBy: "heuristic"
|
|
7954
|
+
};
|
|
7955
|
+
}
|
|
7740
7956
|
function detectLayerViolations(bundle) {
|
|
7741
|
-
const findings = [];
|
|
7742
7957
|
const checkDepsFile = bundle.contextFiles.find((f) => f.path === "harness-check-deps-output");
|
|
7743
|
-
if (!checkDepsFile) return
|
|
7744
|
-
const
|
|
7745
|
-
|
|
7746
|
-
if (line.toLowerCase().includes("violation") || line.toLowerCase().includes("layer")) {
|
|
7747
|
-
const fileMatch = line.match(/(?:in\s+)?(\S+\.(?:ts|tsx|js|jsx))(?::(\d+))?/);
|
|
7748
|
-
const file = fileMatch?.[1] ?? bundle.changedFiles[0]?.path ?? "unknown";
|
|
7749
|
-
const lineNum = fileMatch?.[2] ? parseInt(fileMatch[2], 10) : 1;
|
|
7750
|
-
findings.push({
|
|
7751
|
-
id: makeFindingId("arch", file, lineNum, "layer violation"),
|
|
7752
|
-
file,
|
|
7753
|
-
lineRange: [lineNum, lineNum],
|
|
7754
|
-
domain: "architecture",
|
|
7755
|
-
severity: "critical",
|
|
7756
|
-
title: "Layer boundary violation detected by check-deps",
|
|
7757
|
-
rationale: `Architectural layer violation: ${line.trim()}. Imports must flow in the correct direction per the project's layer definitions.`,
|
|
7758
|
-
suggestion: "Route the dependency through the correct intermediate layer (e.g., routes -> services -> db, not routes -> db).",
|
|
7759
|
-
evidence: [line.trim()],
|
|
7760
|
-
validatedBy: "heuristic"
|
|
7761
|
-
});
|
|
7762
|
-
}
|
|
7763
|
-
}
|
|
7764
|
-
return findings;
|
|
7958
|
+
if (!checkDepsFile) return [];
|
|
7959
|
+
const fallbackPath = bundle.changedFiles[0]?.path ?? "unknown";
|
|
7960
|
+
return checkDepsFile.content.split("\n").filter(isViolationLine).map((line) => createLayerViolationFinding(line, fallbackPath));
|
|
7765
7961
|
}
|
|
7766
7962
|
function detectLargeFiles(bundle) {
|
|
7767
7963
|
const findings = [];
|
|
@@ -7783,45 +7979,61 @@ function detectLargeFiles(bundle) {
|
|
|
7783
7979
|
}
|
|
7784
7980
|
return findings;
|
|
7785
7981
|
}
|
|
7982
|
+
function extractRelativeImports(content) {
|
|
7983
|
+
const importRegex = /import\s+.*?from\s+['"]([^'"]+)['"]/g;
|
|
7984
|
+
let match;
|
|
7985
|
+
const imports = /* @__PURE__ */ new Set();
|
|
7986
|
+
while ((match = importRegex.exec(content)) !== null) {
|
|
7987
|
+
const source = match[1];
|
|
7988
|
+
if (source.startsWith(".")) {
|
|
7989
|
+
imports.add(source.replace(/^\.\//, "").replace(/^\.\.\//, ""));
|
|
7990
|
+
}
|
|
7991
|
+
}
|
|
7992
|
+
return imports;
|
|
7993
|
+
}
|
|
7994
|
+
function fileBaseName(filePath) {
|
|
7995
|
+
return filePath.replace(/.*\//, "").replace(/\.(ts|tsx|js|jsx)$/, "");
|
|
7996
|
+
}
|
|
7997
|
+
function findCircularImportInCtxFile(ctxFile, changedFilePath, changedPaths, fileImports) {
|
|
7998
|
+
const ctxImportRegex = /import\s+.*?from\s+['"]([^'"]+)['"]/g;
|
|
7999
|
+
let ctxMatch;
|
|
8000
|
+
while ((ctxMatch = ctxImportRegex.exec(ctxFile.content)) !== null) {
|
|
8001
|
+
const ctxSource = ctxMatch[1];
|
|
8002
|
+
if (!ctxSource.startsWith(".")) continue;
|
|
8003
|
+
for (const changedPath of changedPaths) {
|
|
8004
|
+
const baseName = fileBaseName(changedPath);
|
|
8005
|
+
const ctxBaseName = fileBaseName(ctxFile.path);
|
|
8006
|
+
if (ctxSource.includes(baseName) && fileImports.has(ctxBaseName)) {
|
|
8007
|
+
return {
|
|
8008
|
+
id: makeFindingId("arch", changedFilePath, 1, `circular ${ctxFile.path}`),
|
|
8009
|
+
file: changedFilePath,
|
|
8010
|
+
lineRange: [1, 1],
|
|
8011
|
+
domain: "architecture",
|
|
8012
|
+
severity: "important",
|
|
8013
|
+
title: `Potential circular import between ${changedFilePath} and ${ctxFile.path}`,
|
|
8014
|
+
rationale: "Circular imports can cause runtime issues (undefined values at import time) and indicate tightly coupled modules that should be refactored.",
|
|
8015
|
+
suggestion: "Extract shared types/interfaces into a separate module that both files can import from.",
|
|
8016
|
+
evidence: [
|
|
8017
|
+
`${changedFilePath} imports from a module that also imports from ${changedFilePath}`
|
|
8018
|
+
],
|
|
8019
|
+
validatedBy: "heuristic"
|
|
8020
|
+
};
|
|
8021
|
+
}
|
|
8022
|
+
}
|
|
8023
|
+
}
|
|
8024
|
+
return null;
|
|
8025
|
+
}
|
|
7786
8026
|
function detectCircularImports(bundle) {
|
|
7787
8027
|
const findings = [];
|
|
7788
8028
|
const changedPaths = new Set(bundle.changedFiles.map((f) => f.path));
|
|
8029
|
+
const relevantCtxFiles = bundle.contextFiles.filter(
|
|
8030
|
+
(f) => f.reason === "import" || f.reason === "graph-dependency"
|
|
8031
|
+
);
|
|
7789
8032
|
for (const cf of bundle.changedFiles) {
|
|
7790
|
-
const
|
|
7791
|
-
|
|
7792
|
-
|
|
7793
|
-
|
|
7794
|
-
const source = match[1];
|
|
7795
|
-
if (source.startsWith(".")) {
|
|
7796
|
-
imports.add(source.replace(/^\.\//, "").replace(/^\.\.\//, ""));
|
|
7797
|
-
}
|
|
7798
|
-
}
|
|
7799
|
-
for (const ctxFile of bundle.contextFiles) {
|
|
7800
|
-
if (ctxFile.reason !== "import" && ctxFile.reason !== "graph-dependency") continue;
|
|
7801
|
-
const ctxImportRegex = /import\s+.*?from\s+['"]([^'"]+)['"]/g;
|
|
7802
|
-
let ctxMatch;
|
|
7803
|
-
while ((ctxMatch = ctxImportRegex.exec(ctxFile.content)) !== null) {
|
|
7804
|
-
const ctxSource = ctxMatch[1];
|
|
7805
|
-
if (ctxSource.startsWith(".")) {
|
|
7806
|
-
for (const changedPath of changedPaths) {
|
|
7807
|
-
const baseName = changedPath.replace(/.*\//, "").replace(/\.(ts|tsx|js|jsx)$/, "");
|
|
7808
|
-
if (ctxSource.includes(baseName) && imports.has(ctxFile.path.replace(/.*\//, "").replace(/\.(ts|tsx|js|jsx)$/, ""))) {
|
|
7809
|
-
findings.push({
|
|
7810
|
-
id: makeFindingId("arch", cf.path, 1, `circular ${ctxFile.path}`),
|
|
7811
|
-
file: cf.path,
|
|
7812
|
-
lineRange: [1, 1],
|
|
7813
|
-
domain: "architecture",
|
|
7814
|
-
severity: "important",
|
|
7815
|
-
title: `Potential circular import between ${cf.path} and ${ctxFile.path}`,
|
|
7816
|
-
rationale: "Circular imports can cause runtime issues (undefined values at import time) and indicate tightly coupled modules that should be refactored.",
|
|
7817
|
-
suggestion: "Extract shared types/interfaces into a separate module that both files can import from.",
|
|
7818
|
-
evidence: [`${cf.path} imports from a module that also imports from ${cf.path}`],
|
|
7819
|
-
validatedBy: "heuristic"
|
|
7820
|
-
});
|
|
7821
|
-
}
|
|
7822
|
-
}
|
|
7823
|
-
}
|
|
7824
|
-
}
|
|
8033
|
+
const imports = extractRelativeImports(cf.content);
|
|
8034
|
+
for (const ctxFile of relevantCtxFiles) {
|
|
8035
|
+
const finding = findCircularImportInCtxFile(ctxFile, cf.path, changedPaths, imports);
|
|
8036
|
+
if (finding) findings.push(finding);
|
|
7825
8037
|
}
|
|
7826
8038
|
}
|
|
7827
8039
|
return findings;
|
|
@@ -7868,7 +8080,7 @@ async function fanOutReview(options) {
|
|
|
7868
8080
|
}
|
|
7869
8081
|
|
|
7870
8082
|
// src/review/validate-findings.ts
|
|
7871
|
-
import * as
|
|
8083
|
+
import * as path17 from "path";
|
|
7872
8084
|
var DOWNGRADE_MAP = {
|
|
7873
8085
|
critical: "important",
|
|
7874
8086
|
important: "suggestion",
|
|
@@ -7889,7 +8101,7 @@ function normalizePath(filePath, projectRoot) {
|
|
|
7889
8101
|
let normalized = filePath;
|
|
7890
8102
|
normalized = normalized.replace(/\\/g, "/");
|
|
7891
8103
|
const normalizedRoot = projectRoot.replace(/\\/g, "/");
|
|
7892
|
-
if (
|
|
8104
|
+
if (path17.isAbsolute(normalized)) {
|
|
7893
8105
|
const root = normalizedRoot.endsWith("/") ? normalizedRoot : normalizedRoot + "/";
|
|
7894
8106
|
if (normalized.startsWith(root)) {
|
|
7895
8107
|
normalized = normalized.slice(root.length);
|
|
@@ -7914,12 +8126,12 @@ function followImportChain(fromFile, fileContents, maxDepth = 2) {
|
|
|
7914
8126
|
while ((match = importRegex.exec(content)) !== null) {
|
|
7915
8127
|
const importPath = match[1];
|
|
7916
8128
|
if (!importPath.startsWith(".")) continue;
|
|
7917
|
-
const dir =
|
|
7918
|
-
let resolved =
|
|
8129
|
+
const dir = path17.dirname(current.file);
|
|
8130
|
+
let resolved = path17.join(dir, importPath).replace(/\\/g, "/");
|
|
7919
8131
|
if (!resolved.match(/\.(ts|tsx|js|jsx)$/)) {
|
|
7920
8132
|
resolved += ".ts";
|
|
7921
8133
|
}
|
|
7922
|
-
resolved =
|
|
8134
|
+
resolved = path17.normalize(resolved).replace(/\\/g, "/");
|
|
7923
8135
|
if (!visited.has(resolved) && current.depth + 1 <= maxDepth) {
|
|
7924
8136
|
queue.push({ file: resolved, depth: current.depth + 1 });
|
|
7925
8137
|
}
|
|
@@ -7936,7 +8148,7 @@ async function validateFindings(options) {
|
|
|
7936
8148
|
if (exclusionSet.isExcluded(normalizedFile, finding.lineRange) || exclusionSet.isExcluded(finding.file, finding.lineRange)) {
|
|
7937
8149
|
continue;
|
|
7938
8150
|
}
|
|
7939
|
-
const absoluteFile =
|
|
8151
|
+
const absoluteFile = path17.isAbsolute(finding.file) ? finding.file : path17.join(projectRoot, finding.file).replace(/\\/g, "/");
|
|
7940
8152
|
if (exclusionSet.isExcluded(absoluteFile, finding.lineRange)) {
|
|
7941
8153
|
continue;
|
|
7942
8154
|
}
|
|
@@ -7993,6 +8205,28 @@ async function validateFindings(options) {
|
|
|
7993
8205
|
function rangesOverlap(a, b, gap) {
|
|
7994
8206
|
return a[0] <= b[1] + gap && b[0] <= a[1] + gap;
|
|
7995
8207
|
}
|
|
8208
|
+
function pickLongest(a, b) {
|
|
8209
|
+
if (a && b) return a.length >= b.length ? a : b;
|
|
8210
|
+
return a ?? b;
|
|
8211
|
+
}
|
|
8212
|
+
function buildMergedTitle(a, b, domains) {
|
|
8213
|
+
const primaryFinding = SEVERITY_RANK[a.severity] >= SEVERITY_RANK[b.severity] ? a : b;
|
|
8214
|
+
const domainList = [...domains].sort().join(", ");
|
|
8215
|
+
const cleanTitle = primaryFinding.title.replace(/^\[.*?\]\s*/, "");
|
|
8216
|
+
return { title: `[${domainList}] ${cleanTitle}`, primaryFinding };
|
|
8217
|
+
}
|
|
8218
|
+
function mergeSecurityFields(merged, primary, a, b) {
|
|
8219
|
+
const cweId = primary.cweId ?? a.cweId ?? b.cweId;
|
|
8220
|
+
const owaspCategory = primary.owaspCategory ?? a.owaspCategory ?? b.owaspCategory;
|
|
8221
|
+
const confidence = primary.confidence ?? a.confidence ?? b.confidence;
|
|
8222
|
+
const remediation = pickLongest(a.remediation, b.remediation);
|
|
8223
|
+
const mergedRefs = [.../* @__PURE__ */ new Set([...a.references ?? [], ...b.references ?? []])];
|
|
8224
|
+
if (cweId !== void 0) merged.cweId = cweId;
|
|
8225
|
+
if (owaspCategory !== void 0) merged.owaspCategory = owaspCategory;
|
|
8226
|
+
if (confidence !== void 0) merged.confidence = confidence;
|
|
8227
|
+
if (remediation !== void 0) merged.remediation = remediation;
|
|
8228
|
+
if (mergedRefs.length > 0) merged.references = mergedRefs;
|
|
8229
|
+
}
|
|
7996
8230
|
function mergeFindings(a, b) {
|
|
7997
8231
|
const highestSeverity = SEVERITY_RANK[a.severity] >= SEVERITY_RANK[b.severity] ? a.severity : b.severity;
|
|
7998
8232
|
const highestValidatedBy = (VALIDATED_BY_RANK[a.validatedBy] ?? 0) >= (VALIDATED_BY_RANK[b.validatedBy] ?? 0) ? a.validatedBy : b.validatedBy;
|
|
@@ -8002,18 +8236,12 @@ function mergeFindings(a, b) {
|
|
|
8002
8236
|
Math.min(a.lineRange[0], b.lineRange[0]),
|
|
8003
8237
|
Math.max(a.lineRange[1], b.lineRange[1])
|
|
8004
8238
|
];
|
|
8005
|
-
const domains = /* @__PURE__ */ new Set();
|
|
8006
|
-
|
|
8007
|
-
|
|
8008
|
-
const suggestion = a.suggestion && b.suggestion ? a.suggestion.length >= b.suggestion.length ? a.suggestion : b.suggestion : a.suggestion ?? b.suggestion;
|
|
8009
|
-
const primaryFinding = SEVERITY_RANK[a.severity] >= SEVERITY_RANK[b.severity] ? a : b;
|
|
8010
|
-
const domainList = [...domains].sort().join(", ");
|
|
8011
|
-
const cleanTitle = primaryFinding.title.replace(/^\[.*?\]\s*/, "");
|
|
8012
|
-
const title = `[${domainList}] ${cleanTitle}`;
|
|
8239
|
+
const domains = /* @__PURE__ */ new Set([a.domain, b.domain]);
|
|
8240
|
+
const suggestion = pickLongest(a.suggestion, b.suggestion);
|
|
8241
|
+
const { title, primaryFinding } = buildMergedTitle(a, b, domains);
|
|
8013
8242
|
const merged = {
|
|
8014
8243
|
id: primaryFinding.id,
|
|
8015
8244
|
file: a.file,
|
|
8016
|
-
// same file for all merged findings
|
|
8017
8245
|
lineRange,
|
|
8018
8246
|
domain: primaryFinding.domain,
|
|
8019
8247
|
severity: highestSeverity,
|
|
@@ -8025,16 +8253,7 @@ function mergeFindings(a, b) {
|
|
|
8025
8253
|
if (suggestion !== void 0) {
|
|
8026
8254
|
merged.suggestion = suggestion;
|
|
8027
8255
|
}
|
|
8028
|
-
|
|
8029
|
-
const owaspCategory = primaryFinding.owaspCategory ?? a.owaspCategory ?? b.owaspCategory;
|
|
8030
|
-
const confidence = primaryFinding.confidence ?? a.confidence ?? b.confidence;
|
|
8031
|
-
const remediation = a.remediation && b.remediation ? a.remediation.length >= b.remediation.length ? a.remediation : b.remediation : a.remediation ?? b.remediation;
|
|
8032
|
-
const mergedRefs = [.../* @__PURE__ */ new Set([...a.references ?? [], ...b.references ?? []])];
|
|
8033
|
-
if (cweId !== void 0) merged.cweId = cweId;
|
|
8034
|
-
if (owaspCategory !== void 0) merged.owaspCategory = owaspCategory;
|
|
8035
|
-
if (confidence !== void 0) merged.confidence = confidence;
|
|
8036
|
-
if (remediation !== void 0) merged.remediation = remediation;
|
|
8037
|
-
if (mergedRefs.length > 0) merged.references = mergedRefs;
|
|
8256
|
+
mergeSecurityFields(merged, primaryFinding, a, b);
|
|
8038
8257
|
return merged;
|
|
8039
8258
|
}
|
|
8040
8259
|
function deduplicateFindings(options) {
|
|
@@ -8206,6 +8425,17 @@ function formatTerminalOutput(options) {
|
|
|
8206
8425
|
if (suggestionCount > 0) parts.push(`${suggestionCount} suggestion(s)`);
|
|
8207
8426
|
sections.push(` Found ${issueCount} issue(s): ${parts.join(", ")}.`);
|
|
8208
8427
|
}
|
|
8428
|
+
if (options.evidenceCoverage) {
|
|
8429
|
+
const ec = options.evidenceCoverage;
|
|
8430
|
+
sections.push("");
|
|
8431
|
+
sections.push("## Evidence Coverage\n");
|
|
8432
|
+
sections.push(` Evidence entries: ${ec.totalEntries}`);
|
|
8433
|
+
sections.push(
|
|
8434
|
+
` Findings with evidence: ${ec.findingsWithEvidence}/${ec.findingsWithEvidence + ec.uncitedCount}`
|
|
8435
|
+
);
|
|
8436
|
+
sections.push(` Uncited findings: ${ec.uncitedCount} (flagged as [UNVERIFIED])`);
|
|
8437
|
+
sections.push(` Coverage: ${ec.coveragePercentage}%`);
|
|
8438
|
+
}
|
|
8209
8439
|
return sections.join("\n");
|
|
8210
8440
|
}
|
|
8211
8441
|
|
|
@@ -8282,9 +8512,108 @@ function formatGitHubSummary(options) {
|
|
|
8282
8512
|
const assessment = determineAssessment(findings);
|
|
8283
8513
|
const assessmentLabel = assessment === "approve" ? "Approve" : assessment === "comment" ? "Comment" : "Request Changes";
|
|
8284
8514
|
sections.push(`## Assessment: ${assessmentLabel}`);
|
|
8515
|
+
if (options.evidenceCoverage) {
|
|
8516
|
+
const ec = options.evidenceCoverage;
|
|
8517
|
+
sections.push("");
|
|
8518
|
+
sections.push("## Evidence Coverage\n");
|
|
8519
|
+
sections.push(`- Evidence entries: ${ec.totalEntries}`);
|
|
8520
|
+
sections.push(
|
|
8521
|
+
`- Findings with evidence: ${ec.findingsWithEvidence}/${ec.findingsWithEvidence + ec.uncitedCount}`
|
|
8522
|
+
);
|
|
8523
|
+
sections.push(`- Uncited findings: ${ec.uncitedCount} (flagged as \\[UNVERIFIED\\])`);
|
|
8524
|
+
sections.push(`- Coverage: ${ec.coveragePercentage}%`);
|
|
8525
|
+
}
|
|
8285
8526
|
return sections.join("\n");
|
|
8286
8527
|
}
|
|
8287
8528
|
|
|
8529
|
+
// src/review/evidence-gate.ts
|
|
8530
|
+
var FILE_LINE_RANGE_PATTERN = /^([\w./@-]+\.\w+):(\d+)-(\d+)/;
|
|
8531
|
+
var FILE_LINE_PATTERN = /^([\w./@-]+\.\w+):(\d+)/;
|
|
8532
|
+
var FILE_ONLY_PATTERN = /^([\w./@-]+\.\w+)\s/;
|
|
8533
|
+
function parseEvidenceRef(content) {
|
|
8534
|
+
const trimmed = content.trim();
|
|
8535
|
+
const rangeMatch = trimmed.match(FILE_LINE_RANGE_PATTERN);
|
|
8536
|
+
if (rangeMatch) {
|
|
8537
|
+
return {
|
|
8538
|
+
file: rangeMatch[1],
|
|
8539
|
+
lineStart: parseInt(rangeMatch[2], 10),
|
|
8540
|
+
lineEnd: parseInt(rangeMatch[3], 10)
|
|
8541
|
+
};
|
|
8542
|
+
}
|
|
8543
|
+
const lineMatch = trimmed.match(FILE_LINE_PATTERN);
|
|
8544
|
+
if (lineMatch) {
|
|
8545
|
+
return {
|
|
8546
|
+
file: lineMatch[1],
|
|
8547
|
+
lineStart: parseInt(lineMatch[2], 10)
|
|
8548
|
+
};
|
|
8549
|
+
}
|
|
8550
|
+
const fileMatch = trimmed.match(FILE_ONLY_PATTERN);
|
|
8551
|
+
if (fileMatch) {
|
|
8552
|
+
return { file: fileMatch[1] };
|
|
8553
|
+
}
|
|
8554
|
+
return null;
|
|
8555
|
+
}
|
|
8556
|
+
function evidenceMatchesFinding(ref, finding) {
|
|
8557
|
+
if (ref.file !== finding.file) return false;
|
|
8558
|
+
if (ref.lineStart === void 0) return true;
|
|
8559
|
+
const [findStart, findEnd] = finding.lineRange;
|
|
8560
|
+
if (ref.lineEnd !== void 0) {
|
|
8561
|
+
return ref.lineStart <= findEnd && ref.lineEnd >= findStart;
|
|
8562
|
+
}
|
|
8563
|
+
return ref.lineStart >= findStart && ref.lineStart <= findEnd;
|
|
8564
|
+
}
|
|
8565
|
+
function checkEvidenceCoverage(findings, evidenceEntries) {
|
|
8566
|
+
if (findings.length === 0) {
|
|
8567
|
+
return {
|
|
8568
|
+
totalEntries: evidenceEntries.filter((e) => e.status === "active").length,
|
|
8569
|
+
findingsWithEvidence: 0,
|
|
8570
|
+
uncitedCount: 0,
|
|
8571
|
+
uncitedFindings: [],
|
|
8572
|
+
coveragePercentage: 100
|
|
8573
|
+
};
|
|
8574
|
+
}
|
|
8575
|
+
const activeEvidence = evidenceEntries.filter((e) => e.status === "active");
|
|
8576
|
+
const evidenceRefs = [];
|
|
8577
|
+
for (const entry of activeEvidence) {
|
|
8578
|
+
const ref = parseEvidenceRef(entry.content);
|
|
8579
|
+
if (ref) evidenceRefs.push(ref);
|
|
8580
|
+
}
|
|
8581
|
+
let findingsWithEvidence = 0;
|
|
8582
|
+
const uncitedFindings = [];
|
|
8583
|
+
for (const finding of findings) {
|
|
8584
|
+
const hasEvidence = evidenceRefs.some((ref) => evidenceMatchesFinding(ref, finding));
|
|
8585
|
+
if (hasEvidence) {
|
|
8586
|
+
findingsWithEvidence++;
|
|
8587
|
+
} else {
|
|
8588
|
+
uncitedFindings.push(finding.title);
|
|
8589
|
+
}
|
|
8590
|
+
}
|
|
8591
|
+
const uncitedCount = findings.length - findingsWithEvidence;
|
|
8592
|
+
const coveragePercentage = Math.round(findingsWithEvidence / findings.length * 100);
|
|
8593
|
+
return {
|
|
8594
|
+
totalEntries: activeEvidence.length,
|
|
8595
|
+
findingsWithEvidence,
|
|
8596
|
+
uncitedCount,
|
|
8597
|
+
uncitedFindings,
|
|
8598
|
+
coveragePercentage
|
|
8599
|
+
};
|
|
8600
|
+
}
|
|
8601
|
+
function tagUncitedFindings(findings, evidenceEntries) {
|
|
8602
|
+
const activeEvidence = evidenceEntries.filter((e) => e.status === "active");
|
|
8603
|
+
const evidenceRefs = [];
|
|
8604
|
+
for (const entry of activeEvidence) {
|
|
8605
|
+
const ref = parseEvidenceRef(entry.content);
|
|
8606
|
+
if (ref) evidenceRefs.push(ref);
|
|
8607
|
+
}
|
|
8608
|
+
for (const finding of findings) {
|
|
8609
|
+
const hasEvidence = evidenceRefs.some((ref) => evidenceMatchesFinding(ref, finding));
|
|
8610
|
+
if (!hasEvidence && !finding.title.startsWith("[UNVERIFIED]")) {
|
|
8611
|
+
finding.title = `[UNVERIFIED] ${finding.title}`;
|
|
8612
|
+
}
|
|
8613
|
+
}
|
|
8614
|
+
return findings;
|
|
8615
|
+
}
|
|
8616
|
+
|
|
8288
8617
|
// src/review/pipeline-orchestrator.ts
|
|
8289
8618
|
async function runReviewPipeline(options) {
|
|
8290
8619
|
const {
|
|
@@ -8297,7 +8626,8 @@ async function runReviewPipeline(options) {
|
|
|
8297
8626
|
conventionFiles,
|
|
8298
8627
|
checkDepsOutput,
|
|
8299
8628
|
config = {},
|
|
8300
|
-
commitHistory
|
|
8629
|
+
commitHistory,
|
|
8630
|
+
sessionSlug
|
|
8301
8631
|
} = options;
|
|
8302
8632
|
if (flags.ci && prMetadata) {
|
|
8303
8633
|
const eligibility = checkEligibility(prMetadata, true);
|
|
@@ -8393,13 +8723,25 @@ async function runReviewPipeline(options) {
|
|
|
8393
8723
|
projectRoot,
|
|
8394
8724
|
fileContents
|
|
8395
8725
|
});
|
|
8726
|
+
let evidenceCoverage;
|
|
8727
|
+
if (sessionSlug) {
|
|
8728
|
+
try {
|
|
8729
|
+
const evidenceResult = await readSessionSection(projectRoot, sessionSlug, "evidence");
|
|
8730
|
+
if (evidenceResult.ok) {
|
|
8731
|
+
evidenceCoverage = checkEvidenceCoverage(validatedFindings, evidenceResult.value);
|
|
8732
|
+
tagUncitedFindings(validatedFindings, evidenceResult.value);
|
|
8733
|
+
}
|
|
8734
|
+
} catch {
|
|
8735
|
+
}
|
|
8736
|
+
}
|
|
8396
8737
|
const dedupedFindings = deduplicateFindings({ findings: validatedFindings });
|
|
8397
8738
|
const strengths = [];
|
|
8398
8739
|
const assessment = determineAssessment(dedupedFindings);
|
|
8399
8740
|
const exitCode = getExitCode(assessment);
|
|
8400
8741
|
const terminalOutput = formatTerminalOutput({
|
|
8401
8742
|
findings: dedupedFindings,
|
|
8402
|
-
strengths
|
|
8743
|
+
strengths,
|
|
8744
|
+
...evidenceCoverage != null ? { evidenceCoverage } : {}
|
|
8403
8745
|
});
|
|
8404
8746
|
let githubComments = [];
|
|
8405
8747
|
if (flags.comment) {
|
|
@@ -8414,7 +8756,8 @@ async function runReviewPipeline(options) {
|
|
|
8414
8756
|
terminalOutput,
|
|
8415
8757
|
githubComments,
|
|
8416
8758
|
exitCode,
|
|
8417
|
-
...mechanicalResult
|
|
8759
|
+
...mechanicalResult != null ? { mechanicalResult } : {},
|
|
8760
|
+
...evidenceCoverage != null ? { evidenceCoverage } : {}
|
|
8418
8761
|
};
|
|
8419
8762
|
}
|
|
8420
8763
|
|
|
@@ -8517,13 +8860,29 @@ function parseFeatures(sectionBody) {
|
|
|
8517
8860
|
}
|
|
8518
8861
|
return Ok2(features);
|
|
8519
8862
|
}
|
|
8520
|
-
function
|
|
8863
|
+
function extractFieldMap(body) {
|
|
8521
8864
|
const fieldMap = /* @__PURE__ */ new Map();
|
|
8522
8865
|
const fieldPattern = /^- \*\*(.+?):\*\* (.+)$/gm;
|
|
8523
8866
|
let match;
|
|
8524
8867
|
while ((match = fieldPattern.exec(body)) !== null) {
|
|
8525
8868
|
fieldMap.set(match[1], match[2]);
|
|
8526
8869
|
}
|
|
8870
|
+
return fieldMap;
|
|
8871
|
+
}
|
|
8872
|
+
function parseListField(fieldMap, ...keys) {
|
|
8873
|
+
let raw = EM_DASH;
|
|
8874
|
+
for (const key of keys) {
|
|
8875
|
+
const val = fieldMap.get(key);
|
|
8876
|
+
if (val !== void 0) {
|
|
8877
|
+
raw = val;
|
|
8878
|
+
break;
|
|
8879
|
+
}
|
|
8880
|
+
}
|
|
8881
|
+
if (raw === EM_DASH || raw === "none") return [];
|
|
8882
|
+
return raw.split(",").map((s) => s.trim());
|
|
8883
|
+
}
|
|
8884
|
+
function parseFeatureFields(name, body) {
|
|
8885
|
+
const fieldMap = extractFieldMap(body);
|
|
8527
8886
|
const statusRaw = fieldMap.get("Status");
|
|
8528
8887
|
if (!statusRaw || !VALID_STATUSES.has(statusRaw)) {
|
|
8529
8888
|
return Err2(
|
|
@@ -8532,15 +8891,17 @@ function parseFeatureFields(name, body) {
|
|
|
8532
8891
|
)
|
|
8533
8892
|
);
|
|
8534
8893
|
}
|
|
8535
|
-
const status = statusRaw;
|
|
8536
8894
|
const specRaw = fieldMap.get("Spec") ?? EM_DASH;
|
|
8537
|
-
const
|
|
8538
|
-
const
|
|
8539
|
-
|
|
8540
|
-
|
|
8541
|
-
|
|
8542
|
-
|
|
8543
|
-
|
|
8895
|
+
const plans = parseListField(fieldMap, "Plans", "Plan");
|
|
8896
|
+
const blockedBy = parseListField(fieldMap, "Blocked by", "Blockers");
|
|
8897
|
+
return Ok2({
|
|
8898
|
+
name,
|
|
8899
|
+
status: statusRaw,
|
|
8900
|
+
spec: specRaw === EM_DASH ? null : specRaw,
|
|
8901
|
+
plans,
|
|
8902
|
+
blockedBy,
|
|
8903
|
+
summary: fieldMap.get("Summary") ?? ""
|
|
8904
|
+
});
|
|
8544
8905
|
}
|
|
8545
8906
|
|
|
8546
8907
|
// src/roadmap/serialize.ts
|
|
@@ -8591,8 +8952,8 @@ function serializeFeature(feature) {
|
|
|
8591
8952
|
}
|
|
8592
8953
|
|
|
8593
8954
|
// src/roadmap/sync.ts
|
|
8594
|
-
import * as
|
|
8595
|
-
import * as
|
|
8955
|
+
import * as fs18 from "fs";
|
|
8956
|
+
import * as path18 from "path";
|
|
8596
8957
|
import { Ok as Ok3 } from "@harness-engineering/types";
|
|
8597
8958
|
function inferStatus(feature, projectPath, allFeatures) {
|
|
8598
8959
|
if (feature.blockedBy.length > 0) {
|
|
@@ -8607,10 +8968,10 @@ function inferStatus(feature, projectPath, allFeatures) {
|
|
|
8607
8968
|
const featuresWithPlans = allFeatures.filter((f) => f.plans.length > 0);
|
|
8608
8969
|
const useRootState = featuresWithPlans.length <= 1;
|
|
8609
8970
|
if (useRootState) {
|
|
8610
|
-
const rootStatePath =
|
|
8611
|
-
if (
|
|
8971
|
+
const rootStatePath = path18.join(projectPath, ".harness", "state.json");
|
|
8972
|
+
if (fs18.existsSync(rootStatePath)) {
|
|
8612
8973
|
try {
|
|
8613
|
-
const raw =
|
|
8974
|
+
const raw = fs18.readFileSync(rootStatePath, "utf-8");
|
|
8614
8975
|
const state = JSON.parse(raw);
|
|
8615
8976
|
if (state.progress) {
|
|
8616
8977
|
for (const status of Object.values(state.progress)) {
|
|
@@ -8621,16 +8982,16 @@ function inferStatus(feature, projectPath, allFeatures) {
|
|
|
8621
8982
|
}
|
|
8622
8983
|
}
|
|
8623
8984
|
}
|
|
8624
|
-
const sessionsDir =
|
|
8625
|
-
if (
|
|
8985
|
+
const sessionsDir = path18.join(projectPath, ".harness", "sessions");
|
|
8986
|
+
if (fs18.existsSync(sessionsDir)) {
|
|
8626
8987
|
try {
|
|
8627
|
-
const sessionDirs =
|
|
8988
|
+
const sessionDirs = fs18.readdirSync(sessionsDir, { withFileTypes: true });
|
|
8628
8989
|
for (const entry of sessionDirs) {
|
|
8629
8990
|
if (!entry.isDirectory()) continue;
|
|
8630
|
-
const autopilotPath =
|
|
8631
|
-
if (!
|
|
8991
|
+
const autopilotPath = path18.join(sessionsDir, entry.name, "autopilot-state.json");
|
|
8992
|
+
if (!fs18.existsSync(autopilotPath)) continue;
|
|
8632
8993
|
try {
|
|
8633
|
-
const raw =
|
|
8994
|
+
const raw = fs18.readFileSync(autopilotPath, "utf-8");
|
|
8634
8995
|
const autopilot = JSON.parse(raw);
|
|
8635
8996
|
if (!autopilot.phases) continue;
|
|
8636
8997
|
const linkedPhases = autopilot.phases.filter(
|
|
@@ -8710,17 +9071,17 @@ var EmitInteractionInputSchema = z6.object({
|
|
|
8710
9071
|
});
|
|
8711
9072
|
|
|
8712
9073
|
// src/blueprint/scanner.ts
|
|
8713
|
-
import * as
|
|
8714
|
-
import * as
|
|
9074
|
+
import * as fs19 from "fs/promises";
|
|
9075
|
+
import * as path19 from "path";
|
|
8715
9076
|
var ProjectScanner = class {
|
|
8716
9077
|
constructor(rootDir) {
|
|
8717
9078
|
this.rootDir = rootDir;
|
|
8718
9079
|
}
|
|
8719
9080
|
async scan() {
|
|
8720
|
-
let projectName =
|
|
9081
|
+
let projectName = path19.basename(this.rootDir);
|
|
8721
9082
|
try {
|
|
8722
|
-
const pkgPath =
|
|
8723
|
-
const pkgRaw = await
|
|
9083
|
+
const pkgPath = path19.join(this.rootDir, "package.json");
|
|
9084
|
+
const pkgRaw = await fs19.readFile(pkgPath, "utf-8");
|
|
8724
9085
|
const pkg = JSON.parse(pkgRaw);
|
|
8725
9086
|
if (pkg.name) projectName = pkg.name;
|
|
8726
9087
|
} catch {
|
|
@@ -8761,8 +9122,8 @@ var ProjectScanner = class {
|
|
|
8761
9122
|
};
|
|
8762
9123
|
|
|
8763
9124
|
// src/blueprint/generator.ts
|
|
8764
|
-
import * as
|
|
8765
|
-
import * as
|
|
9125
|
+
import * as fs20 from "fs/promises";
|
|
9126
|
+
import * as path20 from "path";
|
|
8766
9127
|
import * as ejs from "ejs";
|
|
8767
9128
|
|
|
8768
9129
|
// src/blueprint/templates.ts
|
|
@@ -8846,19 +9207,19 @@ var BlueprintGenerator = class {
|
|
|
8846
9207
|
styles: STYLES,
|
|
8847
9208
|
scripts: SCRIPTS
|
|
8848
9209
|
});
|
|
8849
|
-
await
|
|
8850
|
-
await
|
|
9210
|
+
await fs20.mkdir(options.outputDir, { recursive: true });
|
|
9211
|
+
await fs20.writeFile(path20.join(options.outputDir, "index.html"), html);
|
|
8851
9212
|
}
|
|
8852
9213
|
};
|
|
8853
9214
|
|
|
8854
9215
|
// src/update-checker.ts
|
|
8855
|
-
import * as
|
|
8856
|
-
import * as
|
|
9216
|
+
import * as fs21 from "fs";
|
|
9217
|
+
import * as path21 from "path";
|
|
8857
9218
|
import * as os from "os";
|
|
8858
9219
|
import { spawn } from "child_process";
|
|
8859
9220
|
function getStatePath() {
|
|
8860
9221
|
const home = process.env["HOME"] || os.homedir();
|
|
8861
|
-
return
|
|
9222
|
+
return path21.join(home, ".harness", "update-check.json");
|
|
8862
9223
|
}
|
|
8863
9224
|
function isUpdateCheckEnabled(configInterval) {
|
|
8864
9225
|
if (process.env["HARNESS_NO_UPDATE_CHECK"] === "1") return false;
|
|
@@ -8871,7 +9232,7 @@ function shouldRunCheck(state, intervalMs) {
|
|
|
8871
9232
|
}
|
|
8872
9233
|
function readCheckState() {
|
|
8873
9234
|
try {
|
|
8874
|
-
const raw =
|
|
9235
|
+
const raw = fs21.readFileSync(getStatePath(), "utf-8");
|
|
8875
9236
|
const parsed = JSON.parse(raw);
|
|
8876
9237
|
if (typeof parsed === "object" && parsed !== null && "lastCheckTime" in parsed && typeof parsed.lastCheckTime === "number" && "currentVersion" in parsed && typeof parsed.currentVersion === "string") {
|
|
8877
9238
|
const state = parsed;
|
|
@@ -8888,7 +9249,7 @@ function readCheckState() {
|
|
|
8888
9249
|
}
|
|
8889
9250
|
function spawnBackgroundCheck(currentVersion) {
|
|
8890
9251
|
const statePath = getStatePath();
|
|
8891
|
-
const stateDir =
|
|
9252
|
+
const stateDir = path21.dirname(statePath);
|
|
8892
9253
|
const script = `
|
|
8893
9254
|
const { execSync } = require('child_process');
|
|
8894
9255
|
const fs = require('fs');
|
|
@@ -8942,7 +9303,7 @@ Run "harness update" to upgrade.`;
|
|
|
8942
9303
|
}
|
|
8943
9304
|
|
|
8944
9305
|
// src/index.ts
|
|
8945
|
-
var VERSION = "0.
|
|
9306
|
+
var VERSION = "0.14.0";
|
|
8946
9307
|
export {
|
|
8947
9308
|
AGENT_DESCRIPTORS,
|
|
8948
9309
|
ARCHITECTURE_DESCRIPTOR,
|
|
@@ -9022,6 +9383,7 @@ export {
|
|
|
9022
9383
|
analyzeLearningPatterns,
|
|
9023
9384
|
appendFailure,
|
|
9024
9385
|
appendLearning,
|
|
9386
|
+
appendSessionEntry,
|
|
9025
9387
|
applyFixes,
|
|
9026
9388
|
applyHotspotDowngrade,
|
|
9027
9389
|
archMatchers,
|
|
@@ -9029,12 +9391,14 @@ export {
|
|
|
9029
9391
|
architecture,
|
|
9030
9392
|
archiveFailures,
|
|
9031
9393
|
archiveLearnings,
|
|
9394
|
+
archiveSession,
|
|
9032
9395
|
archiveStream,
|
|
9033
9396
|
buildDependencyGraph,
|
|
9034
9397
|
buildExclusionSet,
|
|
9035
9398
|
buildSnapshot,
|
|
9036
9399
|
checkDocCoverage,
|
|
9037
9400
|
checkEligibility,
|
|
9401
|
+
checkEvidenceCoverage,
|
|
9038
9402
|
classifyFinding,
|
|
9039
9403
|
clearFailuresCache,
|
|
9040
9404
|
clearLearningsCache,
|
|
@@ -9118,6 +9482,8 @@ export {
|
|
|
9118
9482
|
reactRules,
|
|
9119
9483
|
readCheckState,
|
|
9120
9484
|
readLockfile,
|
|
9485
|
+
readSessionSection,
|
|
9486
|
+
readSessionSections,
|
|
9121
9487
|
removeContributions,
|
|
9122
9488
|
removeProvenance,
|
|
9123
9489
|
requestMultiplePeerReviews,
|
|
@@ -9151,8 +9517,10 @@ export {
|
|
|
9151
9517
|
spawnBackgroundCheck,
|
|
9152
9518
|
syncConstraintNodes,
|
|
9153
9519
|
syncRoadmap,
|
|
9520
|
+
tagUncitedFindings,
|
|
9154
9521
|
touchStream,
|
|
9155
9522
|
trackAction,
|
|
9523
|
+
updateSessionEntryStatus,
|
|
9156
9524
|
updateSessionIndex,
|
|
9157
9525
|
validateAgentsMap,
|
|
9158
9526
|
validateBoundaries,
|