@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/index.mjs CHANGED
@@ -41,7 +41,7 @@ import {
41
41
  runAll,
42
42
  validateDependencies,
43
43
  violationId
44
- } from "./chunk-D6VFA6AS.mjs";
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 path20 = firstError.path.join(".");
88
- const pathDisplay = path20 ? ` at "${path20}"` : "";
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 "${path20}" is required and must be of type "${expected}"`);
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(path20) {
309
- return path20.startsWith("http://") || path20.startsWith("https://") || path20.startsWith("#") || path20.startsWith("mailto:");
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(path20 = "./AGENTS.md") {
315
- const contentResult = await readFileContent(path20);
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: path20 },
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(path20);
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(path20, existingFiles) {
470
- const targetName = basename2(path20).toLowerCase();
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 "${path20}" or remove the link`;
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 path20 = issue.path.join(".");
831
- return path20 ? `${path20}: ${issue.message}` : issue.message;
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 deepMergeConstraints(localConfig, bundleConstraints, _existingContributions) {
1054
- const config = { ...localConfig };
1055
- const contributions = {};
1056
- const conflicts = [];
1057
- if (bundleConstraints.layers && bundleConstraints.layers.length > 0) {
1058
- const localLayers = Array.isArray(localConfig.layers) ? localConfig.layers : [];
1059
- const mergedLayers = [...localLayers];
1060
- const contributedLayerNames = [];
1061
- for (const bundleLayer of bundleConstraints.layers) {
1062
- const existing = localLayers.find((l) => l.name === bundleLayer.name);
1063
- if (!existing) {
1064
- mergedLayers.push(bundleLayer);
1065
- contributedLayerNames.push(bundleLayer.name);
1066
- } else {
1067
- const same = existing.pattern === bundleLayer.pattern && stringArraysEqual(existing.allowedDependencies, bundleLayer.allowedDependencies);
1068
- if (!same) {
1069
- conflicts.push({
1070
- section: "layers",
1071
- key: bundleLayer.name,
1072
- localValue: existing,
1073
- packageValue: bundleLayer,
1074
- description: `Layer '${bundleLayer.name}' already exists locally with different configuration`
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
- config.layers = mergedLayers;
1080
- if (contributedLayerNames.length > 0) {
1081
- contributions.layers = contributedLayerNames;
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
- if (bundleConstraints.forbiddenImports && bundleConstraints.forbiddenImports.length > 0) {
1085
- const localFI = Array.isArray(localConfig.forbiddenImports) ? localConfig.forbiddenImports : [];
1086
- const mergedFI = [...localFI];
1087
- const contributedFromKeys = [];
1088
- for (const bundleRule of bundleConstraints.forbiddenImports) {
1089
- const existing = localFI.find((r) => r.from === bundleRule.from);
1090
- if (!existing) {
1091
- const entry = {
1092
- from: bundleRule.from,
1093
- disallow: bundleRule.disallow
1094
- };
1095
- if (bundleRule.message !== void 0) {
1096
- entry.message = bundleRule.message;
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: "forbiddenImports",
1105
- key: bundleRule.from,
1106
- localValue: existing,
1107
- packageValue: bundleRule,
1108
- description: `Forbidden import rule for '${bundleRule.from}' already exists locally with different disallow list`
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
- config.forbiddenImports = mergedFI;
1114
- if (contributedFromKeys.length > 0) {
1115
- contributions.forbiddenImports = contributedFromKeys;
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
- const localBoundaries = localConfig.boundaries ?? { requireSchema: [] };
1120
- const localSchemas = new Set(localBoundaries.requireSchema ?? []);
1121
- const bundleSchemas = bundleConstraints.boundaries.requireSchema ?? [];
1122
- const newSchemas = [];
1123
- for (const schema of bundleSchemas) {
1124
- if (!localSchemas.has(schema)) {
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
- const localArch = localConfig.architecture ?? {
1138
- thresholds: {},
1139
- modules: {}
1140
- };
1141
- const mergedThresholds = { ...localArch.thresholds };
1142
- const contributedThresholdKeys = [];
1143
- const bundleThresholds = bundleConstraints.architecture.thresholds ?? {};
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
- const localSecurity = localConfig.security ?? { rules: {} };
1201
- const localRules = localSecurity.rules ?? {};
1202
- const mergedRules = { ...localRules };
1203
- const contributedRuleIds = [];
1204
- for (const [ruleId, severity] of Object.entries(bundleConstraints.security.rules)) {
1205
- if (!(ruleId in mergedRules)) {
1206
- mergedRules[ruleId] = severity;
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(path20) {
1386
- const contentResult = await readFileContent(path20);
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: ${path20}`, { path: path20 }, [
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: path20.endsWith(".tsx"),
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 ${path20}: ${error.message}`, { path: path20 }, [
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
- for (const spec of importDecl.specifiers) {
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
- for (const spec of exportDecl.specifiers) {
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
- if (exportDecl.declaration) {
1492
- const decl = exportDecl.declaration;
1493
- if (decl.type === "VariableDeclaration") {
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
- const resolved = explicitEntries.map((e) => resolve(rootDir, e));
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 (pkg["exports"]) {
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(path20) {
1695
- const contentResult = await readFileContent(path20);
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: ${path20}`,
1701
- { file: path20 },
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 = path20.endsWith(".md") ? "markdown" : "text";
1689
+ const type = path22.endsWith(".md") ? "markdown" : "text";
1708
1690
  return Ok({
1709
- path: path20,
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 symbols;
1720
- for (const node of body.body) {
1721
- if (node.type === "FunctionDeclaration" && node.id?.name) {
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 levenshteinDistance(a, b) {
1859
+ function initLevenshteinMatrix(aLen, bLen) {
1895
1860
  const matrix = [];
1896
- for (let i = 0; i <= b.length; i++) {
1861
+ for (let i = 0; i <= bLen; i++) {
1897
1862
  matrix[i] = [i];
1898
1863
  }
1899
- for (let j = 0; j <= a.length; j++) {
1900
- const row = matrix[0];
1901
- if (row) {
1902
- row[j] = j;
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
- if (b.charAt(i - 1) === a.charAt(j - 1)) {
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 countLinesFromAST(ast) {
2201
- if (ast.body && Array.isArray(ast.body)) {
2202
- let maxLine = 0;
2203
- const traverse = (node) => {
2204
- if (node && typeof node === "object") {
2205
- const n = node;
2206
- if (n.loc?.end?.line && n.loc.end.line > maxLine) {
2207
- maxLine = n.loc.end.line;
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
- traverse(ast);
2222
- if (maxLine > 0) return maxLine;
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 1;
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
- function checkConfigPattern(pattern, file, rootDir) {
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 fileMatches = pattern.files.some((glob) => fileMatchesPattern(file.path, glob, rootDir));
2379
- if (!fileMatches) {
2380
- return matches;
2381
- }
2382
- const rule = pattern.rule;
2383
- switch (rule.type) {
2384
- case "must-export": {
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
- case "must-import": {
2422
- const hasImport = file.imports.some(
2423
- (i) => i.source === rule.from || i.source.endsWith(rule.from)
2424
- );
2425
- if (!hasImport) {
2426
- matches.push({
2427
- line: 1,
2428
- message: pattern.message || `Missing required import from "${rule.from}"`,
2429
- suggestion: `Add import from "${rule.from}"`
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
- break;
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
- case "no-import": {
2435
- const forbiddenImport = file.imports.find(
2436
- (i) => i.source === rule.from || i.source.endsWith(rule.from)
2437
- );
2438
- if (forbiddenImport) {
2439
- matches.push({
2440
- line: forbiddenImport.location.line,
2441
- message: pattern.message || `Forbidden import from "${rule.from}"`,
2442
- suggestion: `Remove import from "${rule.from}"`
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
- break;
2446
- }
2447
- case "naming": {
2448
- const regex = new RegExp(rule.match);
2449
- for (const exp of file.exports) {
2450
- if (!regex.test(exp.name)) {
2451
- let expected = "";
2452
- switch (rule.convention) {
2453
- case "camelCase":
2454
- expected = "camelCase (e.g., myFunction)";
2455
- break;
2456
- case "PascalCase":
2457
- expected = "PascalCase (e.g., MyClass)";
2458
- break;
2459
- case "UPPER_SNAKE":
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
- break;
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
- case "max-exports": {
2476
- if (file.exports.length > rule.count) {
2477
- matches.push({
2478
- line: 1,
2479
- message: pattern.message || `File has ${file.exports.length} exports, max is ${rule.count}`,
2480
- suggestion: `Split into multiple files or reduce exports to ${rule.count}`
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
- break;
2484
- }
2485
- case "max-lines": {
2486
- break;
2487
- }
2488
- case "require-jsdoc": {
2489
- if (file.jsDocComments.length === 0 && file.exports.length > 0) {
2490
- matches.push({
2491
- line: 1,
2492
- message: pattern.message || "Exported symbols require JSDoc documentation",
2493
- suggestion: "Add JSDoc comments to exports"
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
- break;
2497
- }
2462
+ ];
2498
2463
  }
2499
- return matches;
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: "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 ${exp.type === "class" ? "class" : exp.type === "function" ? "function" : ""} ${exp.name}` : `export ${exp.type === "class" ? "class" : exp.type === "function" ? "function" : exp.type === "variable" ? "const" : exp.type === "type" ? "type" : exp.type === "interface" ? "interface" : "enum"} ${exp.name}`,
3025
- newContent: exp.isDefault ? `${exp.type === "class" ? "class" : exp.type === "function" ? "function" : ""} ${exp.name}` : `${exp.type === "class" ? "class" : exp.type === "function" ? "function" : exp.type === "variable" ? "const" : exp.type === "type" ? "type" : exp.type === "interface" ? "interface" : "enum"} ${exp.name}`,
3026
- safe: true,
3027
- reversible: true
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 safety;
3211
- let safetyReason;
3212
- let fixAction;
3213
- let suggestion;
3271
+ let classification;
3214
3272
  if (ALWAYS_UNSAFE_TYPES.has(input.type)) {
3215
- safety = "unsafe";
3216
- safetyReason = `${input.type} requires human judgment`;
3217
- suggestion = "Review and refactor manually";
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
- if (input.isPublicApi) {
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
- if (input.type === "import-ordering") {
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
- parseVitestBenchOutput(output) {
3616
+ collectAssertionResults(testResults) {
3565
3617
  const results = [];
3566
- try {
3567
- const jsonStart = output.indexOf("{");
3568
- const jsonEnd = output.lastIndexOf("}");
3569
- if (jsonStart === -1 || jsonEnd === -1) return results;
3570
- const jsonStr = output.slice(jsonStart, jsonEnd + 1);
3571
- const parsed = JSON.parse(jsonStr);
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
- async run(changes) {
4105
- const startTime = Date.now();
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
- if (this.harnessOptions) {
4108
- if (this.harnessOptions.context !== false) {
4109
- if (this.graphHarnessData) {
4110
- items.push({
4111
- id: "harness-context",
4112
- category: "harness",
4113
- check: "Context validation",
4114
- passed: this.graphHarnessData.graphExists && this.graphHarnessData.nodeCount > 0,
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: true,
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: "Harness constraint validation not yet integrated (run with graph for real checks)"
4148
- });
4149
- }
4150
- }
4151
- if (this.harnessOptions.entropy !== false) {
4152
- if (this.graphHarnessData) {
4153
- const issues = this.graphHarnessData.unreachableNodes + this.graphHarnessData.undocumentedFiles;
4154
- items.push({
4155
- id: "harness-entropy",
4156
- category: "harness",
4157
- check: "Entropy detection",
4158
- passed: issues === 0,
4159
- severity: issues > 0 ? "warning" : "info",
4160
- details: issues === 0 ? "No entropy issues detected" : `${this.graphHarnessData.unreachableNodes} unreachable node(s), ${this.graphHarnessData.undocumentedFiles} undocumented file(s)`
4161
- });
4162
- } else {
4163
- items.push({
4164
- id: "harness-entropy",
4165
- category: "harness",
4166
- check: "Entropy detection",
4167
- passed: true,
4168
- severity: "info",
4169
- details: "Harness entropy detection not yet integrated (run with graph for real checks)"
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
- try {
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 fs15 from "fs/promises";
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 fs14 from "fs";
5917
- import * as path11 from "path";
6104
+ import * as fs16 from "fs";
6105
+ import * as path13 from "path";
5918
6106
  function detectStack(projectRoot) {
5919
6107
  const stacks = [];
5920
- const pkgJsonPath = path11.join(projectRoot, "package.json");
5921
- if (fs14.existsSync(pkgJsonPath)) {
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(fs14.readFileSync(pkgJsonPath, "utf-8"));
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 = path11.join(projectRoot, "go.mod");
5940
- if (fs14.existsSync(goModPath)) {
6127
+ const goModPath = path13.join(projectRoot, "go.mod");
6128
+ if (fs16.existsSync(goModPath)) {
5941
6129
  stacks.push("go");
5942
6130
  }
5943
- const requirementsPath = path11.join(projectRoot, "requirements.txt");
5944
- const pyprojectPath = path11.join(projectRoot, "pyproject.toml");
5945
- if (fs14.existsSync(requirementsPath) || fs14.existsSync(pyprojectPath)) {
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 fs15.readFile(filePath, "utf-8");
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 path12 from "path";
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 = path12.join(projectRoot, config.agentsMapPath ?? "AGENTS.md");
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 = path12.join(projectRoot, config.docsDir ?? "docs");
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, _config) {
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 path13 from "path";
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 = path13.join(projectRoot, config.agentsMapPath ?? "AGENTS.md");
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: path13.join(projectRoot, "AGENTS.md"),
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 = path13.join(projectRoot, config.docsDir ?? "docs");
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: path13.join(projectRoot, "docs"),
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 path14 from "path";
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 = path14.resolve(projectRoot) + path14.sep;
7036
- const resolvedPath = path14.resolve(absPath);
7037
- return resolvedPath.startsWith(resolvedRoot) || resolvedPath === path14.resolve(projectRoot);
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 = path14.isAbsolute(filePath) ? filePath : path14.join(projectRoot, filePath);
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 = path14.isAbsolute(filePath) ? relativePosix(projectRoot, filePath) : filePath;
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 = path14.dirname(path14.join(projectRoot, fromFile));
7062
- const basePath = path14.resolve(fromDir, importSource);
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
- path14.join(relBase, "index.ts")
7263
+ path16.join(relBase, "index.ts")
7070
7264
  ];
7071
7265
  for (const candidate of candidates) {
7072
- const absCandidate = path14.join(projectRoot, candidate);
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 = path14.basename(sourceFile, path14.extname(sourceFile));
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 runComplianceAgent(bundle) {
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 rules = extractConventionRules(bundle);
7372
- const jsDocRuleExists = rules.some((r) => r.text.toLowerCase().includes("jsdoc"));
7373
- if (jsDocRuleExists) {
7374
- const missingDocs = findMissingJsDoc(bundle);
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", m.file, m.line, `Missing JSDoc ${m.exportName}`),
7378
- file: m.file,
7379
- lineRange: [m.line, m.line],
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: "important",
7382
- title: `Missing JSDoc on exported \`${m.exportName}\``,
7383
- rationale: `Convention requires all exports to have JSDoc comments (from ${rules.find((r) => r.text.toLowerCase().includes("jsdoc"))?.source ?? "conventions"}).`,
7384
- suggestion: `Add a JSDoc comment above the export of \`${m.exportName}\`.`,
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 findings;
7744
- const lines = checkDepsFile.content.split("\n");
7745
- for (const line of lines) {
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 importRegex = /import\s+.*?from\s+['"]([^'"]+)['"]/g;
7791
- let match;
7792
- const imports = /* @__PURE__ */ new Set();
7793
- while ((match = importRegex.exec(cf.content)) !== null) {
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 path15 from "path";
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 (path15.isAbsolute(normalized)) {
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 = path15.dirname(current.file);
7918
- let resolved = path15.join(dir, importPath).replace(/\\/g, "/");
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 = path15.normalize(resolved).replace(/\\/g, "/");
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 = path15.isAbsolute(finding.file) ? finding.file : path15.join(projectRoot, finding.file).replace(/\\/g, "/");
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
- domains.add(a.domain);
8007
- domains.add(b.domain);
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
- const cweId = primaryFinding.cweId ?? a.cweId ?? b.cweId;
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 !== void 0 ? { 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 parseFeatureFields(name, body) {
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 spec = specRaw === EM_DASH ? null : specRaw;
8538
- const plansRaw = fieldMap.get("Plans") ?? fieldMap.get("Plan") ?? EM_DASH;
8539
- const plans = plansRaw === EM_DASH || plansRaw === "none" ? [] : plansRaw.split(",").map((p) => p.trim());
8540
- const blockedByRaw = fieldMap.get("Blocked by") ?? fieldMap.get("Blockers") ?? EM_DASH;
8541
- const blockedBy = blockedByRaw === EM_DASH || blockedByRaw === "none" ? [] : blockedByRaw.split(",").map((b) => b.trim());
8542
- const summary = fieldMap.get("Summary") ?? "";
8543
- return Ok2({ name, status, spec, plans, blockedBy, summary });
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 fs16 from "fs";
8595
- import * as path16 from "path";
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 = path16.join(projectPath, ".harness", "state.json");
8611
- if (fs16.existsSync(rootStatePath)) {
8971
+ const rootStatePath = path18.join(projectPath, ".harness", "state.json");
8972
+ if (fs18.existsSync(rootStatePath)) {
8612
8973
  try {
8613
- const raw = fs16.readFileSync(rootStatePath, "utf-8");
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 = path16.join(projectPath, ".harness", "sessions");
8625
- if (fs16.existsSync(sessionsDir)) {
8985
+ const sessionsDir = path18.join(projectPath, ".harness", "sessions");
8986
+ if (fs18.existsSync(sessionsDir)) {
8626
8987
  try {
8627
- const sessionDirs = fs16.readdirSync(sessionsDir, { withFileTypes: true });
8988
+ const sessionDirs = fs18.readdirSync(sessionsDir, { withFileTypes: true });
8628
8989
  for (const entry of sessionDirs) {
8629
8990
  if (!entry.isDirectory()) continue;
8630
- const autopilotPath = path16.join(sessionsDir, entry.name, "autopilot-state.json");
8631
- if (!fs16.existsSync(autopilotPath)) continue;
8991
+ const autopilotPath = path18.join(sessionsDir, entry.name, "autopilot-state.json");
8992
+ if (!fs18.existsSync(autopilotPath)) continue;
8632
8993
  try {
8633
- const raw = fs16.readFileSync(autopilotPath, "utf-8");
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 fs17 from "fs/promises";
8714
- import * as path17 from "path";
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 = path17.basename(this.rootDir);
9081
+ let projectName = path19.basename(this.rootDir);
8721
9082
  try {
8722
- const pkgPath = path17.join(this.rootDir, "package.json");
8723
- const pkgRaw = await fs17.readFile(pkgPath, "utf-8");
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 fs18 from "fs/promises";
8765
- import * as path18 from "path";
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 fs18.mkdir(options.outputDir, { recursive: true });
8850
- await fs18.writeFile(path18.join(options.outputDir, "index.html"), html);
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 fs19 from "fs";
8856
- import * as path19 from "path";
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 path19.join(home, ".harness", "update-check.json");
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 = fs19.readFileSync(getStatePath(), "utf-8");
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 = path19.dirname(statePath);
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.13.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,